//https://github.com/RMSoftware-Development Commit soon. using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.IO; using Discord; using Discord.WebSocket; using System.Reflection; using Discord.Commands; using System.Threading; using Microsoft.Extensions.DependencyInjection; using RMSoftware.IO; using System.Diagnostics; using Newtonsoft.Json; using System.Runtime.ExceptionServices; namespace RMSoftware.ModularBot { class Program { #region STATIC fields & properties public static DateTime StartTime; public static bool BCMDStarted = false; public static bool WizardDebug = false; public static bool discon = false; public static bool CriticalError = false; public static bool RestartRequested = false; public static Exception crashException = null; static string auth = ""; static bool CRASH = false; public static string[] ARGS = null; public static CustomCommandManager ccmg { get; private set; } public static CmdRoleManager rolemgt { get; private set; } public static char CommandPrefix { get; private set; } public static DiscordSocketClient _client; static List messageQueue = new List(); public static bool LOG_ONLY_MODE = false;//Set by args on start. if true, console header and resets are ignored. Should only be done if hosting the process in a UI. public static bool MessagesDisabled = false; public static ConsoleLogWriter writer = new ConsoleLogWriter(); /// /// Application's main configuration file /// public static INIFile MainCFG { get; private set;} #endregion #region Instance fields & properties public IServiceCollection serviceCollection; public IServiceProvider services; public CommandService cmdsvr = new Discord.Commands.CommandService(); #endregion #region STATIC methods public static int Main(string[] args) { ARGS = args; try { foreach (string item in args) { if (item.ToLower() == "-log_only") { LOG_ONLY_MODE = true; } if (item.ToLower() == "-crashed") { CRASH = true; } if (item.StartsWith("-auth")) { auth = item.Split(' ')[1]; } } BCMDStarted = false; ConsoleGUIReset(ConsoleColor.Green, ConsoleColor.Black, "Welcome", 79, 45); Console.Title = "RMSoftware.ModularBOT"; Console.WriteLine("Oh, Hello! Greetings! Salutations! Stuff is about to happen... Please wait..."); System.Threading.Thread.Sleep(800); ConsoleWriteImage(Prog.res1.Resource1.RMSoftwareICO); System.Threading.Thread.Sleep(3000); ConsoleGUIReset(ConsoleColor.Cyan, ConsoleColor.Black, "Program Starting"); ccmg = new CustomCommandManager(); rolemgt = new CmdRoleManager(); AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; //AppDomain.CurrentDomain.FirstChanceException += CurrentDomain_FirstChanceException; SetupWizard(); if (auth.Length == 0) { ConsoleGUIReset(ConsoleColor.White, ConsoleColor.DarkBlue, "Initializing"); Console.WriteLine("You didn't specify an authorization token as parameter...."); Console.WriteLine("usage: RMSoftwareModularBot.exe "); Console.WriteLine("No big deal though, Will use configuration instead"); auth = Program.MainCFG.GetCategoryByName("Application").GetEntryByName("botToken").GetAsString(); } //set command prefix CommandPrefix = Convert.ToChar(MainCFG.GetCategoryByName("Application").GetEntryByName("cmdPrefix").GetAsInteger()); Program.LogToConsole(new LogMessage(LogSeverity.Info, "Initialize", $"Using the command prefix: {(char)CommandPrefix}")); var prg = new Program(); prg.MainAsync(auth).GetAwaiter().GetResult(); Task.Run(new Action(ReadConsole)); Task.Run(new Action(writer.ProcessStack().GetAwaiter().GetResult)); SpinWait.SpinUntil(BotShutdown);//Wait until shutdown; BCMDStarted = false; ConsoleGUIReset(ConsoleColor.White, ConsoleColor.DarkRed, "Disconnected"); if (CriticalError) { Program.LogToConsole(new LogMessage(LogSeverity.Critical, "Session", "Failed to resume previous session.",crashException)); using (FileStream fs = File.Create("CRASH.LOG")) { using (StreamWriter sw = new StreamWriter(fs)) { LogMessage m = new LogMessage(LogSeverity.Critical, "Session", crashException.Message, crashException); sw.WriteLine(m.ToString()); List restart_args = new List(); restart_args.AddRange(ARGS); if (!restart_args.Contains("-crashed")) { restart_args.Add("-crashed"); } ARGS = restart_args.ToArray(); sw.Flush(); } } if (!LOG_ONLY_MODE) { Program.LogToConsole(new LogMessage(LogSeverity.Warning, "Application", "Restarting in 4 seconds...")); Thread.Sleep(1000); } RestartRequested = true; _client = null; } } catch (Exception ex) { crashException = ex; ConsoleGUIReset(ConsoleColor.White,ConsoleColor.DarkRed,"Critical Application Error."); Program.LogToConsole(new LogMessage(LogSeverity.Critical, "Exception", ex.Message, ex)); CriticalError = true; RestartRequested = true; using (FileStream fs = File.Create("CRASH.LOG")) { using (StreamWriter sw = new StreamWriter(fs)) { LogMessage m = new LogMessage(LogSeverity.Critical, "Session", ex.Message, ex); sw.WriteLine(m.ToString()); List restart_args = new List(); restart_args.AddRange(ARGS); if (!restart_args.Contains("-crashed")) { restart_args.Add("-crashed"); } ARGS = restart_args.ToArray(); sw.Flush(); } } using (FileStream fs = new FileStream("ERRORS.LOG", FileMode.Append)) { using (StreamWriter sw = new StreamWriter(fs)) { sw.WriteLine(DateTime.Today.ToString("MM/dd/yyyy") + " " + ex.ToString()); sw.Flush(); } } } if (RestartRequested) { Process p = new Process(); string flattened = ""; foreach (string item in ARGS) { flattened += item + " "; } p.StartInfo = new ProcessStartInfo(Process.GetCurrentProcess().MainModule.FileName, flattened); ConsoleGUIReset(ConsoleColor.White, ConsoleColor.DarkRed, "Disconnected"); writer.WriteEntry(new LogMessage(LogSeverity.Warning, "Program", "Restarting in 3...")).GetAwaiter().GetResult();//using direct writer to try and prevent blanks. Thread.Sleep(1000); ConsoleGUIReset(ConsoleColor.White, ConsoleColor.DarkRed, "Disconnected"); writer.WriteEntry(new LogMessage(LogSeverity.Warning, "Program", "Restarting in 2...")).GetAwaiter().GetResult(); Thread.Sleep(1000); ConsoleGUIReset(ConsoleColor.White, ConsoleColor.DarkRed, "Disconnected"); writer.WriteEntry(new LogMessage(LogSeverity.Warning, "Program", "Restarting in 1...")).GetAwaiter().GetResult(); Thread.Sleep(1000); p.Start(); return 0x00000c4; } writer.WriteEntry(new LogMessage(LogSeverity.Warning, "Program", "Termination.")).GetAwaiter().GetResult(); if (!CriticalError) { return 4007;//NOT OKAY } else { return 200;//OK status } } private static void CurrentDomain_FirstChanceException(object sender, FirstChanceExceptionEventArgs e) { writer.push(new LogMessage(LogSeverity.Critical, "FirstERR", e.Exception.ToString())); } static void ReadConsole() { ulong chID = 0; while (true) { if (discon) { break; } string input = Console.ReadLine(); if (discon) { if(string.IsNullOrWhiteSpace(input)) { return; } } if (input.ToLower() == "bot.stopbot") { _client.SetGameAsync(""); System.Threading.Thread.Sleep(2000); _client.SetStatusAsync(UserStatus.Invisible); System.Threading.Thread.Sleep(2000); Program.BCMDStarted = false; _client.StopAsync(); System.Threading.Thread.Sleep(3000);//Allow the bot to shut down fully before telling Main() to scream at user to finger the keyboard to close the console. Program.discon = true; break; } if (input.ToLower() == "cn_term") { Console.WriteLine("Termination"); break; } if (input.ToLower() == "bot.disablecmd") { Program.LogToConsole(new LogMessage(LogSeverity.Warning, "Console", "Command processing disabled!")); _client.SetStatusAsync(UserStatus.DoNotDisturb); _client.SetGameAsync(""); MessagesDisabled = true; } if (input.ToLower() == "bot.enablecmd") { Program.LogToConsole(new LogMessage(LogSeverity.Info, "Console", "Command processing enabled.")); _client.SetStatusAsync(UserStatus.Online); _client.SetGameAsync("READY!"); MessagesDisabled = false; } if (input.ToLower().StartsWith("bot.status")) { string status = input.Remove(0, 10).Trim(); Program.LogToConsole(new LogMessage(LogSeverity.Warning, "Client", "client status changed.")); _client.SetGameAsync(status); } if (input.StartsWith("setgch")) { input = input.Remove(0, 6).Trim(); if (!ulong.TryParse(input, out chID)) { Program.LogToConsole(new LogMessage(LogSeverity.Error, "Console", "Invalid ULONG.")); continue; } } if (input.StartsWith("conmsg")) { input = input.Remove(0, 6).Trim(); SocketTextChannel Channel = _client.GetChannel(chID) as SocketTextChannel; if (Channel == null) { Program.LogToConsole(new LogMessage(LogSeverity.Warning, "Console", "Invalid channel.")); continue; } Channel.SendMessageAsync(input); } } } private static void SetupWizard() { if (InitializeConfig())//If true, do setup. { #region Page 1 ConsoleGUIReset(ConsoleColor.Cyan, ConsoleColor.Black, "Welcome"); Console.WriteLine("Welcome to the initial setup wizard for your new modular discord bot...\r\n Some things to note before we get started:"); Console.WriteLine("\t- This bot requires a token. If you don't know what that is," + " please visit this site first!"); Console.WriteLine("\t- https://discordapp.com/developers/docs/intro"); Console.WriteLine("\t- Privacy notice: This application will output any message that mentions the bot, or messages that start with ! to the console.\r\n\t" + " THEY DO NOT GET SAVED OR POSTED ANYWHERE ELSE... That would be creepy and probably illegal..."); Console.WriteLine("\r\nPress ENTER to continue..."); Console.ReadLine(); #endregion #region Page 2 ConsoleGUIReset(ConsoleColor.Green, ConsoleColor.Black, "Step 1: Token Configuration"); Console.WriteLine("As mentioned before, this bot requires a token. It can be added in one of two ways."); Console.WriteLine("\t- METHOD 1: Program arguments. Simply launch the program from a batch file,\r\n\t- USAGE: RMSoftwareModularBot.exe "); Console.WriteLine("\t- METHOD 2: Configure it. Actually, let's do that now."); Console.WriteLine("Go ahead and find your token and copy/paste* it here, then press enter."); Console.WriteLine("*Only if your CMD console will let you... If not, that sucks... You can painfully type it in also..."); Console.Write("> "); string conf_Token = Console.ReadLine(); if (!WizardDebug) { MainCFG.CreateCategory("Application"); MainCFG.CreateEntry("Application", "botToken", conf_Token); } Console.WriteLine("Okay! Please remember, You can also start the program with a token as an argument."); Console.WriteLine("Be advised, the token in the configuration is ignored when using the program parameter."); Console.WriteLine("\r\nPress ENTER to continue..."); Console.ReadLine(); #endregion #region Page 3 ConsoleGUIReset(ConsoleColor.Green, ConsoleColor.Black, "Step 2: Control Channel & Command prefix"); Console.WriteLine("Ah, yes... The Control channel..."); Console.WriteLine("\t- Whenever you start your bot, it will execute AutoStart.bcmd, a script full of commands to prepare for that bot life..."); Console.WriteLine("\t- However, in order to do this, the bot needs to know where to send these messages and commands..."); Console.WriteLine("\t- Not doing this now would result in a crashing bot that doesn't know what to do with its life."); Console.WriteLine("\t- All I need is the ID of that channel to put into the configuration."); Console.WriteLine("\t- If you need a refresher on how to find a channel ID: https://goo.gl/nqAbhw \r\n\t" + "(It goes to the discord's official docs)"); Console.WriteLine("copy/paste or painfully type in your Initialization Channel's id and press enter"); Console.Write("> "); string conf_BotChannel = Console.ReadLine(); if (!WizardDebug) { Program.MainCFG.CreateEntry("Application", "botChannel", conf_BotChannel); } Console.WriteLine("Great! Now that channel will be the bot's main log channel..."); Console.WriteLine(); Console.WriteLine("Now... The Command Prefix: Please enter a single character (Recommended: A symbol of some kind), to use as the bot's command prefix"); Console.Write("> "); int conf_cmdPrefix = Console.Read(); if (!WizardDebug) { Program.MainCFG.CreateEntry("Application", "cmdPrefix", conf_cmdPrefix); Program.MainCFG.SaveConfiguration();//save } Console.WriteLine("Great! Your bot will use '" + Convert.ToChar(conf_cmdPrefix) + "' [without quotes] as a prefix for it's commands!"); Console.WriteLine("\r\nPress ENTER to continue..."); Console.ReadLine(); #endregion #region Page 4 ConsoleGUIReset(ConsoleColor.Cyan, ConsoleColor.Black, "Step 3: Final Notes & ProTips"); Console.WriteLine("That is all the configuration for right now! Here are a few more things to know:"); Console.WriteLine("\t- If you want to re-run this configuration wizard, delete the 'rmsftModBot.ini' file in the program directory."); Console.WriteLine("\t- The source code for this bot is available on http://rmsoftware.org"); Console.WriteLine("\r\nCORE Command usage (in discord):"); Console.WriteLine("\t- use addmgrole [@roles] to add roles to the command user database."); Console.WriteLine("\t- usage: addcmd "); Console.WriteLine("\t- Actions: Any text/emotes with optional formatting."); Console.WriteLine("\t- addcmd sample1SplitParam false false splitparam 3|" + " This is a sample of splitparam. Var1: {0} var2: {1} and var3: {2} all walked into a bar"); Console.WriteLine("\t- addcmd hug false false You hug {params} for a long time"); Console.WriteLine("\t- More Action parameters: EXEC and CLI_EXEC "); Console.WriteLine("\t- addcmd exectest falase false EXEC modname.dll ModNameSpace.ModClass StaticMethod {params}"); Console.WriteLine("\t- addcmd exectest falase false CLI_EXEC modname.dll ModNameSpace.ModClass StaticMethod {params}"); Console.WriteLine("\t - NOTE: splitparam is not supported for EXEC or CLI_EXEC"); Console.WriteLine("\t - NOTE: EXEC: Allows you to execute a class method for a more advanced command"); Console.WriteLine("\t - NOTE: CLI_EXEC is the same thing, but it gives the class access to the bot directly..."); Console.WriteLine("Extra Configuration Options:"); Console.WriteLine("\t - Adding the line DisableCore=True to the [Application] section of the config file, will disable all core commands."); Console.WriteLine("\t - Remember this will disable the ability to manage commands. putting the bot in a sort of 'read-only' state."); Console.WriteLine("Override core: "); Console.WriteLine("\t - You can create custom commands that use the same name as core commands. This is useful for overriding core commands for even more customization..."); Console.WriteLine("\r\nPlease visit http://rmsoftware.org/rmsoftwareModularBot for more information and documentation."); Console.WriteLine("\r\nPress ENTER to launch the bot!"); Console.ReadLine(); #endregion } } private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { //Exception ex = e.ExceptionObject as Exception; //LogToConsole("CritERR", ex.Message); //ConsoleColor Last = Console.ForegroundColor; //Console.ForegroundColor = ConsoleColor.Gray; //LogToConsole("ExStack\r\n\r\n", ex.StackTrace); //Console.ForegroundColor = Last; CriticalError = true; RestartRequested = true; crashException = (Exception)e.ExceptionObject; using (FileStream fs = File.Create("CRASH.LOG")) { using (StreamWriter sw = new StreamWriter(fs)) { LogMessage m = new LogMessage(LogSeverity.Critical, "Session", crashException.Message, crashException); sw.WriteLine(m.ToString()); List restart_args = new List(); restart_args.AddRange(ARGS); if (!restart_args.Contains("-crashed")) { restart_args.Add("-crashed"); } ARGS = restart_args.ToArray(); sw.Flush(); } } using (FileStream fs = new FileStream("ERRORS.LOG", FileMode.Append)) { using (StreamWriter sw = new StreamWriter(fs)) { sw.WriteLine(DateTime.Today.ToString("MM/dd/yyyy") + " " + ((Exception)e.ExceptionObject).ToString()); sw.Flush(); } } } static Func BotShutdown = delegate () { return discon; }; /// /// Reset the console layout using specified values /// /// Text color /// Background color /// Console's header title (not window title) public static void ConsoleGUIReset(ConsoleColor fore, ConsoleColor back, string title) { if(LOG_ONLY_MODE) { return; } Console.Clear(); Console.SetWindowSize(144, 32); Console.SetBufferSize(144, 256); Console.BackgroundColor = back; Console.ForegroundColor = fore; Console.Clear(); Console.BackgroundColor = ConsoleColor.Black; Console.ForegroundColor = ConsoleColor.White; DecorateTop(); string WTitle = (""+DateTime.Now.ToString("HH:mm:ss") + " " + title + " - RMSoftwareModularBot v" + Assembly.GetExecutingAssembly().GetName().Version.ToString(4)); string pTitle = WTitle.PadLeft(71+WTitle.Length/2); pTitle += "".PadRight(71-WTitle.Length/2); Console.Write("\u2551{0}\u2551", pTitle); DecorateBottom(); Console.BackgroundColor = back; Console.ForegroundColor = fore; } /// /// Reset the console layout using specified values /// /// Text color /// Background color /// Console's header title (not window title) /// Console window & buffer width /// Console window & buffer height public static void ConsoleGUIReset(ConsoleColor fore, ConsoleColor back, string title,short w, short h) { if (LOG_ONLY_MODE) { return; } Console.Clear(); Console.SetWindowSize(w, h); Console.SetBufferSize(w, h); Console.BackgroundColor = back; Console.ForegroundColor = fore; Console.Clear(); Console.BackgroundColor = ConsoleColor.Black; Console.ForegroundColor = ConsoleColor.White; DecorateTop(); string WTitle = ("" + DateTime.Now.ToString("HH:mm:ss") + " " + title + " - RMSoftwareModularBot v" + Assembly.GetExecutingAssembly().GetName().Version.ToString(4)); string pTitle = WTitle.PadLeft(((w/2)+2) + WTitle.Length / 2); pTitle += "".PadRight(((w / 2)-3) - WTitle.Length / 2); Console.Write("\u2551{0}\u2551", pTitle); DecorateBottom(); Console.BackgroundColor = back; Console.ForegroundColor = fore; } private static void DecorateTop() { for (int i = 0; i < Console.WindowWidth; i++) { if(i==0) { Console.Write("\u2554"); continue; } if (i == Console.WindowWidth-1) { Console.Write("\u2557"); break; } Console.Write("\u2550"); } } private static void DecorateBottom() { for (int i = 0; i < Console.WindowWidth; i++) { if (i == 0) { Console.Write("\u255A"); continue; } if (i == Console.WindowWidth - 1) { Console.Write("\u255D"); break; } Console.Write("\u2550"); } } public static void LogToConsole(LogMessage msg) { if(!LOG_ONLY_MODE) { //Console.WriteLine(l.ToString()); writer.push(msg); } else { Console.WriteLine(JsonConvert.SerializeObject(msg)); } if (msg.Exception != null) { using (FileStream fs = new FileStream("ERRORS.LOG", FileMode.Append)) { using (StreamWriter sw = new StreamWriter(fs)) { sw.WriteLine(DateTime.Today.ToString("MM/dd/yyyy") + " " + msg.ToString()); sw.Flush(); } } } } /// /// Initialize configuration. /// /// True if it generated a new file, or has zero categories in said file. private static bool InitializeConfig() { ConsoleGUIReset(ConsoleColor.Cyan, ConsoleColor.Black, "Initializing Configuration"); Program.MainCFG = new INIFile("rmsftModBot.ini"); if (Program.MainCFG.Categories.Count == 0) { return true; } else { if (Program.MainCFG.CheckForCategory("Application")) { if (Program.MainCFG.GetCategoryByName("Application").CheckForEntry("Dev-ShowWizard")) { WizardDebug = true; return Program.MainCFG.GetCategoryByName("Application").GetEntryByName("Dev-ShowWizard").GetAsBool(); } //REQUIRED entries. if (!Program.MainCFG.GetCategoryByName("Application").CheckForEntry("botToken") || !Program.MainCFG.GetCategoryByName("Application").CheckForEntry("botChannel") || !Program.MainCFG.GetCategoryByName("Application").CheckForEntry("cmdPrefix")) { Program.LogToConsole(new LogMessage(LogSeverity.Warning, "Warning", "Invalid or missing config. Reset all settings.")); File.Delete("rmsftModBot.ini"); Program.MainCFG = new INIFile("rmsftModBot.ini");//Re-write from scratch. return true; } } } return false;//basically the file is found and nothing broke. } #region Console pixel art - Courtesy of Stackoverflow.com static int[] cColors = { 0x000000, 0x000080, 0x008000, 0x008080, 0x800000, 0x800080, 0x808000, 0xC0C0C0, 0x808080, 0x0000FF, 0x00FF00, 0x00FFFF, 0xFF0000, 0xFF00FF, 0xFFFF00, 0xFFFFFF }; private static bool term; public static void ConsoleWritePixel(System.Drawing.Color cValue) { System.Drawing.Color[] cTable = cColors.Select(x => System.Drawing.Color.FromArgb(x)).ToArray(); char[] rList = new char[] { (char)9617, (char)9618, (char)9619, (char)9608 }; // 1/4, 2/4, 3/4, 4/4 int[] bestHit = new int[] { 0, 0, 4, int.MaxValue }; //ForeColor, BackColor, Symbol, Score for (int rChar = rList.Length; rChar > 0; rChar--) { for (int cFore = 0; cFore < cTable.Length; cFore++) { for (int cBack = 0; cBack < cTable.Length; cBack++) { int R = (cTable[cFore].R * rChar + cTable[cBack].R * (rList.Length - rChar)) / rList.Length; int G = (cTable[cFore].G * rChar + cTable[cBack].G * (rList.Length - rChar)) / rList.Length; int B = (cTable[cFore].B * rChar + cTable[cBack].B * (rList.Length - rChar)) / rList.Length; int iScore = (cValue.R - R) * (cValue.R - R) + (cValue.G - G) * (cValue.G - G) + (cValue.B - B) * (cValue.B - B); if (!(rChar > 1 && rChar < 4 && iScore > 50000)) // rule out too weird combinations { if (iScore < bestHit[3]) { bestHit[3] = iScore; //Score bestHit[0] = cFore; //ForeColor bestHit[1] = cBack; //BackColor bestHit[2] = rChar; //Symbol } } } } } Console.ForegroundColor = (ConsoleColor)bestHit[0]; Console.BackgroundColor = (ConsoleColor)bestHit[1]; Console.Write(rList[bestHit[2] - 1]); } public static void ConsoleWriteImage(System.Drawing.Bitmap source) { if (LOG_ONLY_MODE) { return; } int sMax = 39; decimal percent = Math.Min(decimal.Divide(sMax, source.Width), decimal.Divide(sMax, source.Height)); System.Drawing.Size dSize = new System.Drawing.Size((int)(source.Width * percent), (int)(source.Height * percent)); System.Drawing.Bitmap bmpMax = new System.Drawing.Bitmap(source, dSize.Width * 2, dSize.Height); for (int i = 0; i < dSize.Height; i++) { for (int j = 0; j < dSize.Width; j++) { ConsoleWritePixel(bmpMax.GetPixel(j * 2, i)); ConsoleWritePixel(bmpMax.GetPixel(j * 2 + 1, i)); } System.Console.WriteLine(); } Console.ResetColor(); } #endregion #endregion #region Async tasks public async Task LoadModules() { cmdsvr = new CommandService(); serviceCollection = serviceCollection.AddSingleton(_client); serviceCollection = serviceCollection.AddSingleton(cmdsvr); foreach (string item in Directory.EnumerateFiles("CMDModules","*.dll",SearchOption.TopDirectoryOnly)) { LogToConsole(new LogMessage(LogSeverity.Info,"Modules", $"Adding commands from module library: {item}")); try { Assembly asmb = Assembly.LoadFile(Path.GetFullPath(item)); string serviceFilename = $"CMDModules\\services.{Path.GetFileNameWithoutExtension(item)}.ini"; string servicefilePath = Path.GetFullPath(serviceFilename); if(File.Exists(servicefilePath)) { INIFile serviceFile = new INIFile(servicefilePath); foreach (INIEntry entryitem in serviceFile.GetCategoryByName("Services").Entries) { string typename = entryitem.GetAsString(); LogToConsole(new LogMessage(LogSeverity.Info,"Modules", $"Injecting service: {typename} from {asmb.GetName().Name}")); serviceCollection = serviceCollection.AddSingleton(asmb.GetType(typename)); } } await cmdsvr.AddModulesAsync(asmb);//ADD EXTERNAL. } catch (Exception ex) { Program.LogToConsole(new LogMessage(LogSeverity.Error, "CritERR",ex.Message,ex)); using (FileStream fs = new FileStream("ERRORS.LOG", FileMode.Append)) { using (StreamWriter sw = new StreamWriter(fs)) { sw.WriteLine(DateTime.Today.ToString("MM/dd/yyyy") + " " + ex.ToString()); sw.Flush(); } } } } services = serviceCollection.BuildServiceProvider(); //check cfg for disabled core. if(MainCFG.GetCategoryByName("Application").CheckForEntry("disableCore")) { if(MainCFG.GetCategoryByName("Application").GetEntryByName("disableCore").GetAsBool()) { LogToConsole(new LogMessage(LogSeverity.Warning,"Program", "RMSoftware.ModularBOT CORE was disabled in the configuration.")); LogToConsole(new LogMessage(LogSeverity.Warning,"Program", "You will not be able to manage existing commands, nor add new ones, via discord.")); return;//DO NOT LOAD CORE IF THIS IS TRUE. } } await cmdsvr.AddModulesAsync(Assembly.GetEntryAssembly());//ADD CORE. } public async Task MainAsync(string token) { try { ConsoleGUIReset(ConsoleColor.White, ConsoleColor.DarkBlue, "Application Running"); _client = new DiscordSocketClient(); _client.Log += Log; serviceCollection = new ServiceCollection(); _client.Ready += _ClientReady; _client.Connected += _client_Connected; _client.MessageReceived += _client_MessageReceived; _client.Disconnected += _client_Disconnected; await LoadModules();//ADD CORE AND EXTERNAL MODULES await _client.LoginAsync(TokenType.Bot, token); await _client.StartAsync(); } catch (Exception ex) { CriticalError = true; RestartRequested = true; crashException = ex; using (FileStream fs = File.Create("CRASH.LOG")) { using (StreamWriter sw = new StreamWriter(fs)) { LogMessage m = new LogMessage(LogSeverity.Critical, "Exception", crashException.Message, crashException); sw.WriteLine(m.ToString()); List restart_args = new List(); restart_args.AddRange(ARGS); if (!restart_args.Contains("-crashed")) { restart_args.Add("-crashed"); } ARGS = restart_args.ToArray(); sw.Flush(); } } using (FileStream fs = new FileStream("ERRORS.LOG", FileMode.Append)) { using (StreamWriter sw = new StreamWriter(fs)) { sw.WriteLine(DateTime.Today.ToString("MM/dd/yyyy") + " " + ex.ToString()); sw.Flush(); } } } } private Task _client_Connected() { StartTime = DateTime.Now; LogToConsole(new LogMessage(LogSeverity.Info,"Uptime", "Reset uptime to ALL zero.")); Console.Title = "RMSoftware.ModularBOT -> " + _client.CurrentUser.Username + " | Connected to " + _client.Guilds.Count + " guilds."; return Task.Delay(1); } private Task _client_Disconnected(Exception arg) { LogToConsole(new LogMessage(LogSeverity.Warning,"Session", "Disconnected: "+ arg.Message,arg)); Console.Title = "RMSoftware.ModularBot - Disconnected"; LogToConsole(new LogMessage(LogSeverity.Info,"Uptime", "The client is disconnected.")); return Task.Delay(3); } private async Task _ClientReady() { await Log(new LogMessage(LogSeverity.Info, "TaskMgr", "Running Onstart.bcmd - task")); await Task.Run(new Action(OffloadReady)); Console.Title = "RMSoftware.ModularBOT -> " + _client.CurrentUser.Username + " | Connected to " + _client.Guilds.Count + " guilds."; } private async void OffloadReady() { try { if (!BCMDStarted) { await _client.SetStatusAsync(UserStatus.DoNotDisturb); //PROCESS THE AutoEXEC file ulong id = MainCFG.GetCategoryByName("Application").GetEntryByName("botChannel").GetAsUlong(); using (StreamReader fs = File.OpenText("OnStart.bcmd")) { while (!fs.EndOfStream) { string line = await fs.ReadLineAsync(); if (!string.IsNullOrWhiteSpace(line)) { if (!line.StartsWith(@"\\")) { IGuildChannel ch = _client.GetChannel(id) as IGuildChannel; //line = line.Replace("%p_", CommandPrefix.ToString()); if (CRASH) { ConsoleColor last = Console.BackgroundColor; Console.BackgroundColor = ConsoleColor.DarkRed; LogToConsole(new LogMessage(LogSeverity.Warning,"Program", "The program was auto-restarted due to a crash. Please see CRASH.LOG for details on what happened.")); Console.BackgroundColor = last; await ((IMessageChannel)ch).SendMessageAsync("**WARNING:** `The bot was auto-restarted due to a crash. Please see CRASH.LOG for details on what happened.`"); CRASH = false;//don't make it happen more than once pls. } if (!line.StartsWith("%p_")) { await ((IMessageChannel)ch).SendMessageAsync(line); } else { line = line.Replace("%p_", CommandPrefix.ToString()); if (await ccmg.Process(new PsuedoMessage(line, _client.CurrentUser, ch, MessageSource.User))) { LogToConsole(new LogMessage(LogSeverity.Info,"OnStart", line)); LogToConsole(new LogMessage(LogSeverity.Info, "OnStart", "CustomCMD Success...")); continue; } //Damn, I can't be sassy here... If it was a command, but not a ccmg command, then try the context for modules. If THAT didn't work //Then it will output the result of the context. var context = new CommandContext(_client, new PsuedoMessage(line,_client.CurrentUser,ch,MessageSource.User)); // Execute the command. (result does not indicate a return value, // rather an object stating if the command executed successfully) var result = await cmdsvr.ExecuteAsync(context, 1, services); LogToConsole(new LogMessage(LogSeverity.Info,"OnStart", line)); LogToConsole(new LogMessage(LogSeverity.Info,"OnStart", result.ToString())); } await Task.Delay(250); } } } } BCMDStarted = true; await _client.SetGameAsync("READY!"); LogToConsole(new LogMessage(LogSeverity.Info,"TaskMgr", "Task is complete.")); await _client.SetStatusAsync(UserStatus.Online); LogToConsole(new LogMessage(LogSeverity.Info,"TaskMgr", "Running Message Queue - task")); foreach (var item in messageQueue) { await _client_MessageReceived(item); await Task.Delay(500); } LogToConsole(new LogMessage(LogSeverity.Info,"TaskMgr", "Task is complete")); } } catch (Exception ex) { BCMDStarted = false; LogToConsole(new LogMessage(LogSeverity.Error,"CritERR", ex.Message,ex)); } } private async Task _client_MessageReceived(SocketMessage arg) { if(MessagesDisabled) { return; } if(rolemgt.UserBlacklisted(arg.Author)) { return; } #region Console Output debug //DEBUG: Output the bot-mentioned chat message to the console foreach (SocketUser item in arg.MentionedUsers) { if (item.Mention == _client.CurrentUser.Mention) { LogToConsole(new LogMessage(LogSeverity.Info,"Mention", "<[" + arg.Channel.Name + "] " + arg.Author.Username + " >: " + arg.Content)); } } //DEBUG: output ! prefixed messages to console. if (arg.Content.StartsWith(CommandPrefix.ToString())) { LogToConsole(new LogMessage(LogSeverity.Info,"Command", "<[" + arg.Channel.Name + "] " + arg.Author.Username + " >: " + arg.Content)); } #endregion // Don't process the command if it was a System Message var message = arg as SocketUserMessage; if (message == null) return; // Create a number to track where the prefix ends and the command begins int argPos = 0; // Determine if the message is a command, based on if it starts with 'commandprefix' or a mention prefix. if not, ignore it. if (!(message.HasCharPrefix(CommandPrefix, ref argPos) || message.HasMentionPrefix(_client.CurrentUser, ref argPos))) return; if (!arg.Author.IsBot && !BCMDStarted) { messageQueue.Add(arg); //queue it up. The bcmdStarted check should //provide PLENTY (if not an excessive amount) of time for modules,and extra commands //to be fully loaded by the time it is set to true. Preemptively solving the "hey, you just ignored my commands completely" //when the bot starts and doesn't respond to a command at first return; } // Create a Command Context for command modules //Process CoreCustom commands if (await ccmg.Process(arg)) { return;//If the message contained a custom command from config, No need to continue. }; var context = new CommandContext(_client, message); // Execute the command. (result does not indicate a return value, // rather an object stating if the command executed successfully) var result = await cmdsvr.ExecuteAsync(context, argPos, services); //If the result is unsuccessful AND not unknown command, send the error details. if (result.Error.HasValue) { if (!result.Error.Value.HasFlag(CommandError.UnknownCommand) && !result.IsSuccess) { await arg.Channel.SendMessageAsync(result.ErrorReason); } if (result.Error.Value.HasFlag(CommandError.BadArgCount)) { await arg.Channel.SendMessageAsync(result.ErrorReason); } } } private Task Log(LogMessage msg) { if(!LOG_ONLY_MODE) { LogToConsole(msg); } else { Console.WriteLine(JsonConvert.SerializeObject(msg)); } if (msg.Message == "Failed to resume previous session") { _client.StopAsync(); _client = null; crashException = new Exception("Failed to resume previous session. See ERRORS.LOG for any additional information."); CriticalError = true; discon = true; Thread.Sleep(3000); } if(msg.Exception != null) { using (FileStream fs = new FileStream("ERRORS.LOG",FileMode.Append)) { using (StreamWriter sw = new StreamWriter(fs)) { sw.WriteLine(DateTime.Today.ToString("MM/dd/yyyy")+" "+msg.ToString()); sw.Flush(); } } } return Task.Delay(0); } #endregion } #region Retry clause - API error workaround [possibly obsolete] [Stackoverflow.com] public static class Retry { public static void Do( Action action, TimeSpan retryInterval, int maxAttemptCount = 3) { Do(() => { action(); return null; }, retryInterval, maxAttemptCount); } public static T Do( Func action, TimeSpan retryInterval, int maxAttemptCount = 3) { var exceptions = new List(); for (int attempted = 0; attempted < maxAttemptCount; attempted++) { try { if (attempted > 0) { Thread.Sleep(retryInterval); } return action(); } catch (Exception ex) { exceptions.Add(ex); Program.LogToConsole(new LogMessage(LogSeverity.Warning,"API_RETRY", ex.Message,ex)); Program.LogToConsole(new LogMessage(LogSeverity.Warning,"API_RETRY", "Retrying " + (maxAttemptCount-attempted) + " more time(s)")); } } throw new AggregateException(exceptions); } } #endregion #region Advanced Command Processing public class CmdRoleManager { INIFile mgmt; INIFile userBlackList; public CmdRoleManager() { mgmt = new INIFile("cmdMgr.ini"); userBlackList = new INIFile("blacklist.ini"); if(!userBlackList.CheckForCategory("Blacklist")) { userBlackList.CreateCategory("Blacklist"); userBlackList.SaveConfiguration(); } } public bool AddUserToBlacklist(SocketUser user) { string guildCat = "Blacklist"; bool check = userBlackList.GetCategoryByName(guildCat).CheckForEntry(user.Id.ToString()); if(!check) { userBlackList.CreateEntry(guildCat, user.Id.ToString(), user.Username.Replace("=", "_")); } userBlackList.SaveConfiguration(); return !check; } public bool DeleteUserFromBlacklist(SocketUser user) { string guildCat = "Blacklist"; bool check = userBlackList.GetCategoryByName(guildCat).CheckForEntry(user.Id.ToString()); if (check) { userBlackList.DeleteEntry(guildCat, user.Id.ToString()); } userBlackList.SaveConfiguration(); return check; } public void AddCommandManagerRole(SocketRole role) { string guildCat = role.Guild.Id.ToString(); bool check = mgmt.CheckForCategory(guildCat); int indx = -1; if(check) { indx = mgmt.GetCategoryByName(guildCat).Entries.Count-1; mgmt.CreateEntry(guildCat, "role" + (indx + 1), role.Id); } else { mgmt.CreateCategory(guildCat); indx = mgmt.GetCategoryByName(guildCat).Entries.Count - 1; mgmt.CreateEntry(guildCat, "role" + (indx + 1), role.Id); } mgmt.SaveConfiguration(); } public bool UserBlacklisted(SocketUser user) { return userBlackList.GetCategoryByName("Blacklist").CheckForEntry(user.Id.ToString()); } public string DeleteCommandManager(SocketRole role) { string guildCat = role.Guild.Id.ToString(); bool check = mgmt.CheckForCategory(guildCat); int indx = -1; if (check) { indx = mgmt.GetCategoryByName(guildCat).Entries.Count - 1; try { mgmt.DeleteEntry(guildCat, mgmt.GetCategoryByName(guildCat).Entries.Find(x => x.GetAsUlong() == role.Id).Name); //Then get all entries and rename them. for (int i = 0; i < mgmt.GetCategoryByName(guildCat).Entries.Count; i++) { mgmt.GetCategoryByName(guildCat).Entries[i].Name = $"role{i}"; } } catch (Exception ex) { return ex.Message; } } else { return "No results. The guild ID didn't exist in the database."; } mgmt.SaveConfiguration(); return "Command Manager database updated."; } public bool CheckUserRole(SocketGuildUser user) { string guildcat = user.Guild.Id.ToString();//if the category does not exist, return false... can't have that; if(!mgmt.CheckForCategory(guildcat)) { return false; } foreach (var role in user.Roles) { ulong id = role.Id; if (mgmt.GetCategoryByName(guildcat).Entries.Exists(x => x.GetAsUlong() == id)) { return true;//keep doing it until it returns true. } } return false;//default; } public SocketRole[] GetRolesForGuild(SocketGuild guild) { List items = new List(); string guildcat = guild.Id.ToString(); if (!mgmt.CheckForCategory(guildcat)) { return null; } foreach (var item in mgmt.GetCategoryByName(guildcat).Entries) { items.Add(guild.GetRole(item.GetAsUlong())); } return items.ToArray(); } public SocketRole[] GetAllRoles() { List items = new List(); foreach (var itemc in mgmt.Categories) { foreach (var item in itemc.Entries) { items.Add(Program._client.GetGuild(Convert.ToUInt64(itemc.Name)).GetRole(item.GetAsUlong())); } } return items.ToArray(); } } public class PsuedoMessage : IMessage, IUserMessage { string _content = ""; SocketUser _author; IGuildChannel _c; MessageSource _source; MessageType _type; public PsuedoMessage(string content,SocketUser author,IGuildChannel ch ,MessageSource source) { _content = content; _author = author; _c = ch; _source = source; _type = MessageType.Default; } IReadOnlyCollection IMessage.Attachments { get; } IUser IMessage.Author { get { return _author; } } IMessageChannel IMessage.Channel { get { return _c as IMessageChannel; } } string IMessage.Content { get { return _content; } } DateTimeOffset ISnowflakeEntity.CreatedAt { get; } DateTimeOffset? IMessage.EditedTimestamp { get; } IReadOnlyCollection IMessage.Embeds { get; } ulong IEntity.Id { get { return (ulong)new Random().Next(0,int.MaxValue); } } bool IMessage.IsPinned { get { return false; } } bool IMessage.IsTTS { get { return false; } } IReadOnlyCollection IMessage.MentionedChannelIds { get; } IReadOnlyCollection IMessage.MentionedRoleIds { get; } IReadOnlyCollection IMessage.MentionedUserIds { get; } MessageSource IMessage.Source { get { return _source; } } IReadOnlyCollection IMessage.Tags { get; } DateTimeOffset IMessage.Timestamp { get; } MessageType IMessage.Type { get { return _type; } } public IReadOnlyDictionary Reactions { get { throw new NotImplementedException(); } } Task IDeletable.DeleteAsync(RequestOptions options) { return Task.Delay(0); } public Task ModifyAsync(Action func, RequestOptions options = null) { throw new NotImplementedException(); } public Task PinAsync(RequestOptions options = null) { throw new NotImplementedException(); } public Task UnpinAsync(RequestOptions options = null) { throw new NotImplementedException(); } public Task AddReactionAsync(IEmote emote, RequestOptions options = null) { throw new NotImplementedException(); } public Task RemoveReactionAsync(IEmote emote, IUser user, RequestOptions options = null) { throw new NotImplementedException(); } public Task RemoveAllReactionsAsync(RequestOptions options = null) { throw new NotImplementedException(); } public Task> GetReactionUsersAsync(string emoji, int limit = 100, ulong? afterUserId = default(ulong?), RequestOptions options = null) { throw new NotImplementedException(); } public string Resolve(TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name, TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name) { throw new NotImplementedException(); } } #endregion }