From 9e3a72dcbc1e427c7689b39dd29b95df0ab1dfba Mon Sep 17 00:00:00 2001 From: TheUbMunster Date: Wed, 21 Dec 2022 17:42:43 -0700 Subject: [PATCH] new discord bot --- README.md | 2 +- Server/DiscordBot.cs | 480 +++++++++++++++++++++++++++++++------------ Server/Program.cs | 4 +- Server/Server.csproj | 3 +- Server/Settings.cs | 1 + 5 files changed, 359 insertions(+), 131 deletions(-) diff --git a/README.md b/README.md index e982eca..2e2cc47 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ Banlist: banned people are unable to join the server, default: false PersistShines/Moons: Allows the server to remember moon progress across crashes/restarts ### Discord -Note: Token and LogChannel needs to a string puts "" around it +Note: Token and LogChannel needs to have quotes "" around it Token: the token of the bot you want to load into, default: null Prefix: the bot prefix to be used, default: $ LogChannel: logs the server console to that channel, default: null \ No newline at end of file diff --git a/Server/DiscordBot.cs b/Server/DiscordBot.cs index cf1457a..c5294c5 100644 --- a/Server/DiscordBot.cs +++ b/Server/DiscordBot.cs @@ -1,160 +1,386 @@ -using DSharpPlus; -using DSharpPlus.Entities; -using Microsoft.Extensions.Logging; +using Discord; +using Discord.Commands; +using Discord.Net; +using Discord.WebSocket; +using Newtonsoft.Json.Linq; using Shared; namespace Server; -public class DiscordBot { - private DiscordClient? DiscordClient; - private string? Token; - private Settings.DiscordTable Config => Settings.Instance.Discord; - private string Prefix => Config.Prefix; - private readonly Logger Logger = new Logger("Discord"); - private DiscordChannel? CommandChannel; - private DiscordChannel? LogChannel; - private bool Reconnecting; +public class DiscordBot +{ + private readonly Logger logger = new Logger("Discord"); + private Settings.DiscordTable localSettings = Settings.Instance.Discord; + private DiscordSocketClient? client = null; + //private SocketTextChannel? commandChannel = null; + private SocketTextChannel? logChannel = null; + private bool firstInitTriggered = false; - public DiscordBot() { - Token = Config.Token; - Logger.AddLogHandler(Log); - CommandHandler.RegisterCommand("dscrestart", _ => { - // this should be async'ed but i'm lazy - Reconnecting = true; - Task.Run(Reconnect); - return "Restarting Discord bot"; + //check how this works with neither, one or the other, or both channels set. + + public DiscordBot() + { + CommandHandler.RegisterCommand("dscrestart", _ => + { + Stop(); +#pragma warning disable CS4014 + Init(); +#pragma warning restore CS4014 + return "Restarting Discord bot..."; }); - if (Config.Token == null) return; - if (Config.CommandChannel == null) - Logger.Warn("You probably should set your CommandChannel in settings.json"); - if (Config.LogChannel == null) - Logger.Warn("You probably should set your LogChannel in settings.json"); - Settings.LoadHandler += SettingsLoadHandler; + logger.Info("Starting discord bot (ctor)"); + Settings.LoadHandler += OnLoadSettings; + Logger.AddLogHandler(LogToDiscordLogChannel); } - private async Task Reconnect() { - if (DiscordClient != null) // usually null prop works, not here though...` - await DiscordClient.DisconnectAsync(); - await Run(); - } - - private async void SettingsLoadHandler() { - if (DiscordClient == null || Token != Config.Token) { - await Run(); + private object firstInitLockObj = new object(); + public async Task FirstInit() + { + lock (firstInitLockObj) + { + if (firstInitTriggered) + return; + firstInitTriggered = true; } + await Init(); + } - if (DiscordClient == null) { - Logger.Error(new NullReferenceException("Discord client not setup yet!")); + private async Task Init() + { + if (client != null) + { + return; //this is bad if the client ever crashes and isn't reassigned to null, but we don't want multiple instances of the bot running at the same time. + } + if (localSettings.Token == null || (localSettings.LogChannel == null && localSettings.CommandChannel == null)) + { + //no point trying to run anything if there's no discord token and/or no channel for a user to interact with the bot through. + logger.Error("Tried to run the discord bot, but the Token and/or communication channels are not specified in the settings."); return; } + client = new DiscordSocketClient( + new DiscordSocketConfig() + { + LogLevel = LogSeverity.Warning + }); - if (Config.CommandChannel != null) { - try { - CommandChannel = await DiscordClient.GetChannelAsync(ulong.Parse(Config.CommandChannel)); - } catch (Exception e) { - Logger.Error($"Failed to get command channel \"{Config.CommandChannel}\""); - Logger.Error(e); + client.Log += async (a) => await Task.Run(() => LogToDiscordLogChannel($"Discord: {a.Source}", a.Severity.ToString(), a.Exception?.ToString() ?? "null", (int)a.Severity <= 2 ? ConsoleColor.Yellow : ConsoleColor.Black)); + try + { + await client.LoginAsync(Discord.TokenType.Bot, localSettings.Token); + await client.StartAsync(); + SemaphoreSlim wait = new SemaphoreSlim(0); +#pragma warning disable CS1998 + client.Ready += async () => +#pragma warning restore CS1998 + { + wait.Release(); + }; + await wait.WaitAsync(); + //we need to wait for the ready event before we can do any of this nonsense. + logChannel = (ulong.TryParse(localSettings.LogChannel, out ulong lcid) ? (client != null ? await client.GetChannelAsync(lcid) : null) : null) as SocketTextChannel; + //commandChannel = (ulong.TryParse(localSettings.CommandChannel, out ulong ccid) ? (client != null ? await client.GetChannelAsync(ccid) : null) : null) as SocketTextChannel; + client!.MessageReceived += (m) => HandleCommandAsync(m); + logger.Info("Discord bot has been initialized."); + } + catch (Exception e) + { + logger.Error(e); + } + } + + private void Stop() + { + try + { + if (client != null) + client.StopAsync().Wait(); + client?.Dispose(); + } + catch { /*lol (lmao)*/ } + client = null; + logChannel = null; + //commandChannel = null; + localSettings = Settings.Instance.Discord; + } + + private async void LogToDiscordLogChannel(string source, string level, string text, ConsoleColor color) + { + logChannel = (ulong.TryParse(localSettings.LogChannel, out ulong lcid) ? (client != null ? await client.GetChannelAsync(lcid) : null) : null) as SocketTextChannel; + if (logChannel != null) + { + try + { + switch (color) + { + //I looked into other hacky methods of doing more colors, the rest seemed unreliable. + default: + foreach (string mesg in SplitMessage(Logger.PrefixNewLines(text, $"{level} [{source}]"), 1994)) //room for 6 '`' + await logChannel.SendMessageAsync($"```{mesg}```"); + break; + case ConsoleColor.Yellow: + foreach (string mesg in SplitMessage(Logger.PrefixNewLines(text, $"{level} [{source}]"), 1990)) //room for 6 '`', "fix" and "\n" + await logChannel.SendMessageAsync($"```fix\n{mesg}```"); + break; + case ConsoleColor.Red: + foreach (string mesg in SplitMessage(Logger.PrefixNewLines(text, $"-{level} [{source}]"), 1989)) //room for 6 '`', "diff" and "\n" + await logChannel.SendMessageAsync($"```diff\n{mesg}```"); + break; + case ConsoleColor.Green: + foreach (string mesg in SplitMessage(Logger.PrefixNewLines(text, $"+{level} [{source}]"), 1989)) //room for 6 '`', "diff" and "\n" + await logChannel.SendMessageAsync($"```diff\n{mesg}```"); + break; + } + } + catch (Exception e) + { + // don't log again, it'll just stack overflow the server! + await Console.Error.WriteLineAsync("Exception in discord logger"); + await Console.Error.WriteLineAsync(e.ToString()); } } + } - if (Config.LogChannel != null) { - try { - LogChannel = await DiscordClient.GetChannelAsync(ulong.Parse(Config.LogChannel)); - } catch (Exception e) { - Logger.Error($"Failed to get log channel \"{Config.LogChannel}\""); - Logger.Error(e); + private async Task HandleCommandAsync(SocketMessage arg) + { + if (arg is not SocketUserMessage) + return; //idk what to do in this circumstance. + if ((arg.Channel.Id.ToString() == localSettings.CommandChannel || arg.Channel.Id.ToString() == localSettings.LogChannel) && !arg.Author.IsBot) + { + string message = (await arg.Channel.GetMessageAsync(arg.Id)).Content; + if (localSettings.LogCommands) + { + logger.Info($"\"{arg.Author.Username}\" ran the command: \"{message}\" via discord"); + } + //run command + try + { + string? resp = null; + if (string.IsNullOrEmpty(localSettings.Prefix)) + { + await arg.Channel.TriggerTypingAsync(); + resp = string.Join('\n', CommandHandler.GetResult(message).ReturnStrings); + } + else if (message.StartsWith(localSettings.Prefix)) + { + await arg.Channel.TriggerTypingAsync(); + resp = string.Join('\n', CommandHandler.GetResult(message[localSettings.Prefix.Length..]).ReturnStrings); + } + else if (message.StartsWith($"<@{client!.CurrentUser.Id}>")) + { + await arg.Channel.TriggerTypingAsync(); + resp = string.Join('\n', CommandHandler.GetResult(message[client!.CurrentUser.Mention.Length..].TrimStart()).ReturnStrings); + } + if (resp != null) + { + foreach (string mesg in SplitMessage(resp)) + await (arg as SocketUserMessage).ReplyAsync(mesg); + } + } + catch (Exception e) + { + logger.Error(e); } } + else + { + //don't respond to commands not in these channels, and no bots + //probably don't print out error message because DDOS + } + } + + private void OnLoadSettings() + { + Settings.DiscordTable oldSettings = localSettings; + localSettings = Settings.Instance.Discord; + + if (localSettings.CommandChannel == null) + logger.Warn("You probably should set your CommandChannel in settings.json"); + if (localSettings.LogChannel == null) + logger.Warn("You probably should set your LogChannel in settings.json"); + + if (oldSettings.Token != localSettings.Token || oldSettings.LogChannel != localSettings.LogChannel || oldSettings.CommandChannel != localSettings.CommandChannel) + { + //start over fresh (there might be a more intelligent way to do this without restarting the bot if only the log/command channel changed, but I'm lazy. + Stop(); +#pragma warning disable CS4014 + Init(); +#pragma warning restore CS4014 + } + } + + private async Task WeGotRateLimitedLBozo(IRateLimitInfo info) + { + //this is spamming because apparently this is called for more than just rate limiting. + //await Console.Error.WriteLineAsync("We got rate limited!"); } private static List SplitMessage(string message, int maxSizePerElem = 2000) { List result = new List(); - for (int i = 0; i < message.Length; i += maxSizePerElem) + for (int i = 0; i < message.Length; i += maxSizePerElem) { result.Add(message.Substring(i, message.Length - i < maxSizePerElem ? message.Length - i : maxSizePerElem)); } return result; } - private async void Log(string source, string level, string text, ConsoleColor _) { - try { - if (DiscordClient != null && LogChannel != null) { - foreach (string mesg in SplitMessage(Logger.PrefixNewLines(text, $"{level} [{source}]"), 1994)) //room for 6 '`' - await DiscordClient.SendMessageAsync(LogChannel, $"```{mesg}```"); - } - } catch (Exception e) { - // don't log again, it'll just stack overflow the server! - if (Reconnecting) return; // skip if reconnecting - await Console.Error.WriteLineAsync("Exception in discord logger"); - await Console.Error.WriteLineAsync(e.ToString()); - } + ~DiscordBot() + { + Stop(); } - public async Task Run() { - Token = Config.Token; - DiscordClient?.Dispose(); - if (Config.Token == null) { - DiscordClient = null; - return; - } + #region Old + //private DiscordClient? DiscordClient; + //private string? Token; + //private Settings.DiscordTable Config => Settings.Instance.Discord; + //private string Prefix => Config.Prefix; + //private readonly Logger Logger = new Logger("Discord"); + //private DiscordChannel? CommandChannel; + //private DiscordChannel? LogChannel; + //private bool Reconnecting; - try { - DiscordClient = new DiscordClient(new DiscordConfiguration { - Token = Config.Token, - MinimumLogLevel = LogLevel.None - }); - await DiscordClient.ConnectAsync(new DiscordActivity("Hide and Seek", ActivityType.Competing)); - SettingsLoadHandler(); - Logger.Info( - $"Discord bot logged in as {DiscordClient.CurrentUser.Username}#{DiscordClient.CurrentUser.Discriminator}"); - Reconnecting = false; - string mentionPrefix = $"{DiscordClient.CurrentUser.Mention}"; - DiscordClient.MessageCreated += async (_, args) => { - if (args.Author.IsCurrent) return; //dont respond to commands from ourselves (prevent "sql-injection" esq attacks) - //prevent commands via dm and non-public channels - if (CommandChannel == null) { - if (args.Channel is DiscordDmChannel) - return; //no dm'ing the bot allowed! - } - else if (args.Channel.Id != CommandChannel.Id && (LogChannel != null && args.Channel.Id != LogChannel.Id)) - return; - //run command - try { - DiscordMessage msg = args.Message; - string? resp = null; - if (string.IsNullOrEmpty(Prefix)) { - await msg.Channel.TriggerTypingAsync(); - resp = string.Join('\n', CommandHandler.GetResult(msg.Content).ReturnStrings); - } else if (msg.Content.StartsWith(Prefix)) { - await msg.Channel.TriggerTypingAsync(); - resp = string.Join('\n', CommandHandler.GetResult(msg.Content[Prefix.Length..]).ReturnStrings); - } else if (msg.Content.StartsWith(mentionPrefix)) { - await msg.Channel.TriggerTypingAsync(); - resp = string.Join('\n', CommandHandler.GetResult(msg.Content[mentionPrefix.Length..].TrimStart()).ReturnStrings); - } - if (resp != null) - { - foreach (string mesg in SplitMessage(resp)) - await msg.RespondAsync(mesg); - } - } catch (Exception e) { - Logger.Error(e); - } - }; - DiscordClient.ClientErrored += (_, args) => { - Logger.Error("Discord client caught an error in handler!"); - Logger.Error(args.Exception); - return Task.CompletedTask; - }; - DiscordClient.SocketErrored += (_, args) => { - Logger.Error("Discord client caught an error on socket!"); - Logger.Error(args.Exception); - return Task.CompletedTask; - }; - } catch (Exception e) { - Logger.Error("Exception occurred in discord runner!"); - Logger.Error(e); - } - } + //public DiscordBot() { + // Token = Config.Token; + // Logger.AddLogHandler(Log); + // CommandHandler.RegisterCommand("dscrestart", _ => { + // // this should be async'ed but i'm lazy + // Reconnecting = true; + // Task.Run(Reconnect); + // return "Restarting Discord bot"; + // }); + // if (Config.Token == null) return; + // if (Config.CommandChannel == null) + // Logger.Warn("You probably should set your CommandChannel in settings.json"); + // if (Config.LogChannel == null) + // Logger.Warn("You probably should set your LogChannel in settings.json"); + // Settings.LoadHandler += SettingsLoadHandler; + //} + + //private async Task Reconnect() { + // if (DiscordClient != null) // usually null prop works, not here though...` + // await DiscordClient.DisconnectAsync(); + // await Run(); + //} + + //private async void SettingsLoadHandler() { + // if (DiscordClient == null || Token != Config.Token) { + // await Run(); + // } + + // if (DiscordClient == null) { + // Logger.Error(new NullReferenceException("Discord client not setup yet!")); + // return; + // } + + // if (Config.CommandChannel != null) { + // try { + // CommandChannel = await DiscordClient.GetChannelAsync(ulong.Parse(Config.CommandChannel)); + // } catch (Exception e) { + // Logger.Error($"Failed to get command channel \"{Config.CommandChannel}\""); + // Logger.Error(e); + // } + // } + + // if (Config.LogChannel != null) { + // try { + // LogChannel = await DiscordClient.GetChannelAsync(ulong.Parse(Config.LogChannel)); + // } catch (Exception e) { + // Logger.Error($"Failed to get log channel \"{Config.LogChannel}\""); + // Logger.Error(e); + // } + // } + //} + + //private static List SplitMessage(string message, int maxSizePerElem = 2000) + //{ + // List result = new List(); + // for (int i = 0; i < message.Length; i += maxSizePerElem) + // { + // result.Add(message.Substring(i, message.Length - i < maxSizePerElem ? message.Length - i : maxSizePerElem)); + // } + // return result; + //} + + //private async void Log(string source, string level, string text, ConsoleColor _) { + // try { + // if (DiscordClient != null && LogChannel != null) { + // foreach (string mesg in SplitMessage(Logger.PrefixNewLines(text, $"{level} [{source}]"), 1994)) //room for 6 '`' + // await DiscordClient.SendMessageAsync(LogChannel, $"```{mesg}```"); + // } + // } catch (Exception e) { + // // don't log again, it'll just stack overflow the server! + // if (Reconnecting) return; // skip if reconnecting + // await Console.Error.WriteLineAsync("Exception in discord logger"); + // await Console.Error.WriteLineAsync(e.ToString()); + // } + //} + + //public async Task Run() { + // Token = Config.Token; + // DiscordClient?.Dispose(); + // if (Config.Token == null) { + // DiscordClient = null; + // return; + // } + + // try { + // DiscordClient = new DiscordClient(new DiscordConfiguration { + // Token = Config.Token, + // MinimumLogLevel = LogLevel.None + // }); + // await DiscordClient.ConnectAsync(new DiscordActivity("Hide and Seek", ActivityType.Competing)); + // SettingsLoadHandler(); + // Logger.Info( + // $"Discord bot logged in as {DiscordClient.CurrentUser.Username}#{DiscordClient.CurrentUser.Discriminator}"); + // Reconnecting = false; + // string mentionPrefix = $"{DiscordClient.CurrentUser.Mention}"; + // DiscordClient.MessageCreated += async (_, args) => { + // if (args.Author.IsCurrent) return; //dont respond to commands from ourselves (prevent "sql-injection" esq attacks) + // //prevent commands via dm and non-public channels + // if (CommandChannel == null) { + // if (args.Channel is DiscordDmChannel) + // return; //no dm'ing the bot allowed! + // } + // else if (args.Channel.Id != CommandChannel.Id && (LogChannel != null && args.Channel.Id != LogChannel.Id)) + // return; + // //run command + // try { + // DiscordMessage msg = args.Message; + // string? resp = null; + // if (string.IsNullOrEmpty(Prefix)) { + // await msg.Channel.TriggerTypingAsync(); + // resp = string.Join('\n', CommandHandler.GetResult(msg.Content).ReturnStrings); + // } else if (msg.Content.StartsWith(Prefix)) { + // await msg.Channel.TriggerTypingAsync(); + // resp = string.Join('\n', CommandHandler.GetResult(msg.Content[Prefix.Length..]).ReturnStrings); + // } else if (msg.Content.StartsWith(mentionPrefix)) { + // await msg.Channel.TriggerTypingAsync(); + // resp = string.Join('\n', CommandHandler.GetResult(msg.Content[mentionPrefix.Length..].TrimStart()).ReturnStrings); + // } + // if (resp != null) + // { + // foreach (string mesg in SplitMessage(resp)) + // await msg.RespondAsync(mesg); + // } + // } catch (Exception e) { + // Logger.Error(e); + // } + // }; + // DiscordClient.ClientErrored += (_, args) => { + // Logger.Error("Discord client caught an error in handler!"); + // Logger.Error(args.Exception); + // return Task.CompletedTask; + // }; + // DiscordClient.SocketErrored += (_, args) => { + // Logger.Error("This is probably that stupid bug again!"); + // Logger.Error("Discord client caught an error on socket!"); + // Logger.Error(args.Exception); + // return Task.CompletedTask; + // }; + // } catch (Exception e) { + // Logger.Error("Exception occurred in discord runner!"); + // Logger.Error(e); + // } + //} + #endregion } diff --git a/Server/Program.cs b/Server/Program.cs index 71c0f3c..b98b6b9 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -14,7 +14,7 @@ CancellationTokenSource cts = new CancellationTokenSource(); bool restartRequested = false; Logger consoleLogger = new Logger("Console"); DiscordBot bot = new DiscordBot(); -await bot.Run(); +await bot.FirstInit(); async Task PersistShines() { @@ -394,7 +394,7 @@ CommandHandler.RegisterCommand("sendall", args => { } if (!stage.Contains("Stage") && !stage.Contains("Zone")) { - return "Invalid Stage Name! ```cap -> Cap Kingdom\ncascade -> Cascade Kingdom\nsand -> Sand Kingdom\nlake -> Lake Kingdom\nwooded -> Wooded Kingdom\ncloud -> Cloud Kingdom\nlost -> Lost Kingdom\nmetro -> Metro Kingdom\nsea -> Sea Kingdom\nsnow -> Snow Kingdom\nlunch -> Luncheon Kingdom\nruined -> Ruined Kingdom\nbowser -> Bowser's Kingdom\nmoon -> Moon Kingdom\nmush -> Mushroom Kingdom\ndark -> Dark Side\ndarker -> Darker Side```"; + return "Invalid Stage Name!\ncap -> Cap Kingdom\ncascade -> Cascade Kingdom\nsand -> Sand Kingdom\nlake -> Lake Kingdom\nwooded -> Wooded Kingdom\ncloud -> Cloud Kingdom\nlost -> Lost Kingdom\nmetro -> Metro Kingdom\nsea -> Sea Kingdom\nsnow -> Snow Kingdom\nlunch -> Luncheon Kingdom\nruined -> Ruined Kingdom\nbowser -> Bowser's Kingdom\nmoon -> Moon Kingdom\nmush -> Mushroom Kingdom\ndark -> Dark Side\ndarker -> Darker Side"; } Client[] players = server.Clients.Where(c => c.Connected).ToArray(); diff --git a/Server/Server.csproj b/Server/Server.csproj index 5710034..6608e67 100644 --- a/Server/Server.csproj +++ b/Server/Server.csproj @@ -12,7 +12,8 @@ - + + diff --git a/Server/Settings.cs b/Server/Settings.cs index fe075e5..4d7d663 100644 --- a/Server/Settings.cs +++ b/Server/Settings.cs @@ -75,6 +75,7 @@ public class Settings { public string Prefix { get; set; } = "$"; public string? CommandChannel { get; set; } public string? LogChannel { get; set; } + public bool LogCommands { get; set; } = false; } public class ShineTable {