new discord bot

This commit is contained in:
TheUbMunster 2022-12-21 17:42:43 -07:00
parent 47fc1527bf
commit 9e3a72dcbc
5 changed files with 359 additions and 131 deletions

View File

@ -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 PersistShines/Moons: Allows the server to remember moon progress across crashes/restarts
### Discord ### 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 Token: the token of the bot you want to load into, default: null
Prefix: the bot prefix to be used, default: $ Prefix: the bot prefix to be used, default: $
LogChannel: logs the server console to that channel, default: null LogChannel: logs the server console to that channel, default: null

View File

@ -1,160 +1,386 @@
using DSharpPlus; using Discord;
using DSharpPlus.Entities; using Discord.Commands;
using Microsoft.Extensions.Logging; using Discord.Net;
using Discord.WebSocket;
using Newtonsoft.Json.Linq;
using Shared; using Shared;
namespace Server; namespace Server;
public class DiscordBot { public class DiscordBot
private DiscordClient? DiscordClient; {
private string? Token; private readonly Logger logger = new Logger("Discord");
private Settings.DiscordTable Config => Settings.Instance.Discord; private Settings.DiscordTable localSettings = Settings.Instance.Discord;
private string Prefix => Config.Prefix; private DiscordSocketClient? client = null;
private readonly Logger Logger = new Logger("Discord"); //private SocketTextChannel? commandChannel = null;
private DiscordChannel? CommandChannel; private SocketTextChannel? logChannel = null;
private DiscordChannel? LogChannel; private bool firstInitTriggered = false;
private bool Reconnecting;
public DiscordBot() { //check how this works with neither, one or the other, or both channels set.
Token = Config.Token;
Logger.AddLogHandler(Log); public DiscordBot()
CommandHandler.RegisterCommand("dscrestart", _ => { {
// this should be async'ed but i'm lazy CommandHandler.RegisterCommand("dscrestart", _ =>
Reconnecting = true; {
Task.Run(Reconnect); Stop();
return "Restarting Discord bot"; #pragma warning disable CS4014
Init();
#pragma warning restore CS4014
return "Restarting Discord bot...";
}); });
if (Config.Token == null) return; logger.Info("Starting discord bot (ctor)");
if (Config.CommandChannel == null) Settings.LoadHandler += OnLoadSettings;
Logger.Warn("You probably should set your CommandChannel in settings.json"); Logger.AddLogHandler(LogToDiscordLogChannel);
if (Config.LogChannel == null)
Logger.Warn("You probably should set your LogChannel in settings.json");
Settings.LoadHandler += SettingsLoadHandler;
} }
private async Task Reconnect() { private object firstInitLockObj = new object();
if (DiscordClient != null) // usually null prop works, not here though...` public async Task FirstInit()
await DiscordClient.DisconnectAsync(); {
await Run(); lock (firstInitLockObj)
} {
if (firstInitTriggered)
private async void SettingsLoadHandler() { return;
if (DiscordClient == null || Token != Config.Token) { firstInitTriggered = true;
await Run();
} }
await Init();
}
if (DiscordClient == null) { private async Task Init()
Logger.Error(new NullReferenceException("Discord client not setup yet!")); {
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; return;
} }
client = new DiscordSocketClient(
new DiscordSocketConfig()
{
LogLevel = LogSeverity.Warning
});
if (Config.CommandChannel != null) { 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 { try
CommandChannel = await DiscordClient.GetChannelAsync(ulong.Parse(Config.CommandChannel)); {
} catch (Exception e) { await client.LoginAsync(Discord.TokenType.Bot, localSettings.Token);
Logger.Error($"Failed to get command channel \"{Config.CommandChannel}\""); await client.StartAsync();
Logger.Error(e); 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) { private async Task HandleCommandAsync(SocketMessage arg)
try { {
LogChannel = await DiscordClient.GetChannelAsync(ulong.Parse(Config.LogChannel)); if (arg is not SocketUserMessage)
} catch (Exception e) { return; //idk what to do in this circumstance.
Logger.Error($"Failed to get log channel \"{Config.LogChannel}\""); if ((arg.Channel.Id.ToString() == localSettings.CommandChannel || arg.Channel.Id.ToString() == localSettings.LogChannel) && !arg.Author.IsBot)
Logger.Error(e); {
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<string> SplitMessage(string message, int maxSizePerElem = 2000) private static List<string> SplitMessage(string message, int maxSizePerElem = 2000)
{ {
List<string> result = new List<string>(); List<string> result = new List<string>();
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)); result.Add(message.Substring(i, message.Length - i < maxSizePerElem ? message.Length - i : maxSizePerElem));
} }
return result; return result;
} }
private async void Log(string source, string level, string text, ConsoleColor _) { ~DiscordBot()
try { {
if (DiscordClient != null && LogChannel != null) { Stop();
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() { #region Old
Token = Config.Token; //private DiscordClient? DiscordClient;
DiscordClient?.Dispose(); //private string? Token;
if (Config.Token == null) { //private Settings.DiscordTable Config => Settings.Instance.Discord;
DiscordClient = null; //private string Prefix => Config.Prefix;
return; //private readonly Logger Logger = new Logger("Discord");
} //private DiscordChannel? CommandChannel;
//private DiscordChannel? LogChannel;
//private bool Reconnecting;
try { //public DiscordBot() {
DiscordClient = new DiscordClient(new DiscordConfiguration { // Token = Config.Token;
Token = Config.Token, // Logger.AddLogHandler(Log);
MinimumLogLevel = LogLevel.None // CommandHandler.RegisterCommand("dscrestart", _ => {
}); // // this should be async'ed but i'm lazy
await DiscordClient.ConnectAsync(new DiscordActivity("Hide and Seek", ActivityType.Competing)); // Reconnecting = true;
SettingsLoadHandler(); // Task.Run(Reconnect);
Logger.Info( // return "Restarting Discord bot";
$"Discord bot logged in as {DiscordClient.CurrentUser.Username}#{DiscordClient.CurrentUser.Discriminator}"); // });
Reconnecting = false; // if (Config.Token == null) return;
string mentionPrefix = $"{DiscordClient.CurrentUser.Mention}"; // if (Config.CommandChannel == null)
DiscordClient.MessageCreated += async (_, args) => { // Logger.Warn("You probably should set your CommandChannel in settings.json");
if (args.Author.IsCurrent) return; //dont respond to commands from ourselves (prevent "sql-injection" esq attacks) // if (Config.LogChannel == null)
//prevent commands via dm and non-public channels // Logger.Warn("You probably should set your LogChannel in settings.json");
if (CommandChannel == null) { // Settings.LoadHandler += SettingsLoadHandler;
if (args.Channel is DiscordDmChannel) //}
return; //no dm'ing the bot allowed!
} //private async Task Reconnect() {
else if (args.Channel.Id != CommandChannel.Id && (LogChannel != null && args.Channel.Id != LogChannel.Id)) // if (DiscordClient != null) // usually null prop works, not here though...`
return; // await DiscordClient.DisconnectAsync();
//run command // await Run();
try { //}
DiscordMessage msg = args.Message;
string? resp = null; //private async void SettingsLoadHandler() {
if (string.IsNullOrEmpty(Prefix)) { // if (DiscordClient == null || Token != Config.Token) {
await msg.Channel.TriggerTypingAsync(); // await Run();
resp = string.Join('\n', CommandHandler.GetResult(msg.Content).ReturnStrings); // }
} else if (msg.Content.StartsWith(Prefix)) {
await msg.Channel.TriggerTypingAsync(); // if (DiscordClient == null) {
resp = string.Join('\n', CommandHandler.GetResult(msg.Content[Prefix.Length..]).ReturnStrings); // Logger.Error(new NullReferenceException("Discord client not setup yet!"));
} else if (msg.Content.StartsWith(mentionPrefix)) { // return;
await msg.Channel.TriggerTypingAsync(); // }
resp = string.Join('\n', CommandHandler.GetResult(msg.Content[mentionPrefix.Length..].TrimStart()).ReturnStrings);
} // if (Config.CommandChannel != null) {
if (resp != null) // try {
{ // CommandChannel = await DiscordClient.GetChannelAsync(ulong.Parse(Config.CommandChannel));
foreach (string mesg in SplitMessage(resp)) // } catch (Exception e) {
await msg.RespondAsync(mesg); // Logger.Error($"Failed to get command channel \"{Config.CommandChannel}\"");
} // Logger.Error(e);
} catch (Exception e) { // }
Logger.Error(e); // }
}
}; // if (Config.LogChannel != null) {
DiscordClient.ClientErrored += (_, args) => { // try {
Logger.Error("Discord client caught an error in handler!"); // LogChannel = await DiscordClient.GetChannelAsync(ulong.Parse(Config.LogChannel));
Logger.Error(args.Exception); // } catch (Exception e) {
return Task.CompletedTask; // Logger.Error($"Failed to get log channel \"{Config.LogChannel}\"");
}; // Logger.Error(e);
DiscordClient.SocketErrored += (_, args) => { // }
Logger.Error("Discord client caught an error on socket!"); // }
Logger.Error(args.Exception); //}
return Task.CompletedTask;
}; //private static List<string> SplitMessage(string message, int maxSizePerElem = 2000)
} catch (Exception e) { //{
Logger.Error("Exception occurred in discord runner!"); // List<string> result = new List<string>();
Logger.Error(e); // 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
} }

View File

@ -14,7 +14,7 @@ CancellationTokenSource cts = new CancellationTokenSource();
bool restartRequested = false; bool restartRequested = false;
Logger consoleLogger = new Logger("Console"); Logger consoleLogger = new Logger("Console");
DiscordBot bot = new DiscordBot(); DiscordBot bot = new DiscordBot();
await bot.Run(); await bot.FirstInit();
async Task PersistShines() async Task PersistShines()
{ {
@ -394,7 +394,7 @@ CommandHandler.RegisterCommand("sendall", args => {
} }
if (!stage.Contains("Stage") && !stage.Contains("Zone")) { 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(); Client[] players = server.Clients.Where(c => c.Connected).ToArray();

View File

@ -12,7 +12,8 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="DSharpPlus" Version="4.3.0-nightly-01142" /> <PackageReference Include="Discord.Net" Version="3.8.1" />
<!--<PackageReference Include="DSharpPlus" Version="4.3.0-nightly-01142" />-->
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup> </ItemGroup>

View File

@ -75,6 +75,7 @@ public class Settings {
public string Prefix { get; set; } = "$"; public string Prefix { get; set; } = "$";
public string? CommandChannel { get; set; } public string? CommandChannel { get; set; }
public string? LogChannel { get; set; } public string? LogChannel { get; set; }
public bool LogCommands { get; set; } = false;
} }
public class ShineTable { public class ShineTable {