From c4bf4906ea9b5676c57f0d3c5a0b18874f372be9 Mon Sep 17 00:00:00 2001 From: Brett Hewitson Date: Sat, 4 Dec 2021 11:39:01 +1000 Subject: [PATCH 1/9] Discord Bot Scaffolding --- src/ARKServerManager/App.config | 9 + src/ARKServerManager/App.xaml.cs | 57 ++++- src/ARKServerManager/Config.Designer.cs | 36 +++ src/ARKServerManager/Config.settings | 9 + .../Globalization/en-US/en-US.xaml | 6 + src/ConanServerManager/App.config | 9 + src/ConanServerManager/App.xaml.cs | 56 ++++- src/ConanServerManager/Config.Designer.cs | 36 +++ src/ConanServerManager/Config.settings | 9 + .../Globalization/en-US/en-US.xaml | 6 + .../Delegates/HandleCommandDelegate.cs | 7 + src/ServerManager.Discord/DiscordBot.cs | 15 ++ .../Enums/CommandType.cs | 14 ++ .../Interfaces/IServerManagerBot.cs | 11 + .../Modules/HelpModule.cs | 151 +++++++++++++ .../Modules/ServerCommandModule.cs | 207 ++++++++++++++++++ .../Modules/ServerQueryModule.cs | 125 +++++++++++ .../ServerManager.Discord.csproj | 17 +- src/ServerManager.Discord/ServerManagerBot.cs | 137 ++++++++++++ .../ServerManagerBotFactory.cs | 19 ++ .../Services/CommandHandlerService.cs | 65 ++++++ .../Services/LoggingService.cs | 57 +++++ .../Services/ShutdownService.cs | 21 ++ .../Services/StartupService.cs | 45 ++++ 24 files changed, 1119 insertions(+), 5 deletions(-) create mode 100644 src/ServerManager.Discord/Delegates/HandleCommandDelegate.cs create mode 100644 src/ServerManager.Discord/DiscordBot.cs create mode 100644 src/ServerManager.Discord/Enums/CommandType.cs create mode 100644 src/ServerManager.Discord/Interfaces/IServerManagerBot.cs create mode 100644 src/ServerManager.Discord/Modules/HelpModule.cs create mode 100644 src/ServerManager.Discord/Modules/ServerCommandModule.cs create mode 100644 src/ServerManager.Discord/Modules/ServerQueryModule.cs create mode 100644 src/ServerManager.Discord/ServerManagerBot.cs create mode 100644 src/ServerManager.Discord/ServerManagerBotFactory.cs create mode 100644 src/ServerManager.Discord/Services/CommandHandlerService.cs create mode 100644 src/ServerManager.Discord/Services/LoggingService.cs create mode 100644 src/ServerManager.Discord/Services/ShutdownService.cs create mode 100644 src/ServerManager.Discord/Services/StartupService.cs diff --git a/src/ARKServerManager/App.config b/src/ARKServerManager/App.config index 605e2a65..f18196df 100644 --- a/src/ARKServerManager/App.config +++ b/src/ARKServerManager/App.config @@ -804,6 +804,15 @@ 50 + + False + + + asm + + + + diff --git a/src/ARKServerManager/App.xaml.cs b/src/ARKServerManager/App.xaml.cs index 52716ef4..db92093f 100644 --- a/src/ARKServerManager/App.xaml.cs +++ b/src/ARKServerManager/App.xaml.cs @@ -1,5 +1,5 @@ using ArkData; -using Microsoft.WindowsAPICodePack.Dialogs; +using ServerManagerTool.Discord.Interfaces; using NLog; using NLog.Config; using NLog.Targets; @@ -23,6 +23,8 @@ using System.Threading.Tasks; using System.Windows; using System.Xml; using WPFSharp.Globalizer; +using ServerManagerTool.Discord; +using ServerManagerTool.Discord.Enums; namespace ServerManagerTool { @@ -39,6 +41,7 @@ namespace ServerManagerTool public event PropertyChangedEventHandler PropertyChanged; + private CancellationTokenSource _tokenSource; private GlobalizedApplication _globalizer; private bool _applicationStarted; private string _args; @@ -144,6 +147,12 @@ namespace ServerManagerTool } } + public IServerManagerBot ServerManagerBot + { + get; + set; + } + public static void DiscoverMachinePublicIP(bool forceOverride) { if (forceOverride || string.IsNullOrWhiteSpace(Config.Default.MachinePublicIP)) @@ -179,7 +188,11 @@ namespace ServerManagerTool private IList FetchProfiles() { - return ServerManager.Instance.Servers.Select(s => new ServerManagerTool.Plugin.Common.Lib.Profile() { ProfileName = s?.Profile?.ProfileName ?? string.Empty, InstallationFolder = s?.Profile?.InstallDirectory ?? string.Empty }).ToList(); + return ServerManager.Instance.Servers.Select(s => new ServerManagerTool.Plugin.Common.Lib.Profile() + { + ProfileName = s?.Profile?.ProfileName ?? string.Empty, + InstallationFolder = s?.Profile?.InstallDirectory ?? string.Empty + }).ToList(); } public static string GetLogFolder() => IOUtils.NormalizePath(Path.Combine(Config.Default.DataDir, Config.Default.LogsDir)); @@ -218,6 +231,11 @@ namespace ServerManagerTool return LogManager.GetLogger(loggerName); } + private static IList HandleDiscordCommand(CommandType commandType, string channelId, string profileId) + { + return new List() { $"{commandType}; {channelId}; {profileId ?? "no profile"}" }; + } + private static void MigrateSettings() { var installFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); @@ -412,6 +430,7 @@ namespace ServerManagerTool ApplicationStarted = true; + var restartRequired = false; if (string.IsNullOrWhiteSpace(Config.Default.DataDir)) { var dataDirectoryWindow = new DataDirectoryWindow(); @@ -422,6 +441,8 @@ namespace ServerManagerTool { Environment.Exit(0); } + + restartRequired = true; } Config.Default.ConfigDirectory = Path.Combine(Config.Default.DataDir, Config.Default.ProfilesDir); @@ -429,6 +450,11 @@ namespace ServerManagerTool Config.Default.Save(); CommonConfig.Default.Save(); + if (restartRequired) + { + Environment.Exit(0); + } + DataFileDetails.PlayerFileExtension = Config.Default.PlayerFileExtension; DataFileDetails.TribeFileExtension = Config.Default.TribeFileExtension; @@ -446,6 +472,27 @@ namespace ServerManagerTool StartupUri = new Uri("Windows/AutoUpdateWindow.xaml", UriKind.RelativeOrAbsolute); } + + if (Config.Default.DiscordBotEnabled) + { + _tokenSource = new CancellationTokenSource(); + + ServerManagerBot = ServerManagerBotFactory.GetServerManagerBot(); + + Task discordTask = Task.Run(async () => + { + await ServerManagerBot.StartAsync(Config.Default.DiscordBotPrefix, Config.Default.DiscordBotToken, Config.Default.DataDir, HandleDiscordCommand, _tokenSource.Token); + }, _tokenSource.Token) + .ContinueWith(t => { + var message = t.Exception.InnerException is null ? t.Exception.Message : t.Exception.InnerException.Message; + if (message.StartsWith("#")) + { + message = _globalizer.GetResourceString(message.Substring(1)) ?? message.Substring(1); + } + + MessageBox.Show(message, _globalizer.GetResourceString("DiscordBot_ErrorTitle"), MessageBoxButton.OK, MessageBoxImage.Error); + }, TaskContinuationOptions.OnlyOnFaulted); + } } protected override void OnExit(ExitEventArgs e) @@ -486,6 +533,12 @@ namespace ServerManagerTool private void ShutDownApplication() { + if (!(_tokenSource is null)) + { + _tokenSource.Cancel(); + _tokenSource.Dispose(); + } + if (ApplicationStarted) { foreach (var server in ServerManager.Instance.Servers) diff --git a/src/ARKServerManager/Config.Designer.cs b/src/ARKServerManager/Config.Designer.cs index dbac3d1f..2ef36e03 100644 --- a/src/ARKServerManager/Config.Designer.cs +++ b/src/ARKServerManager/Config.Designer.cs @@ -2812,5 +2812,41 @@ namespace ServerManagerTool { return ((string)(this["DefaultDataDirectoryName"])); } } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool DiscordBotEnabled { + get { + return ((bool)(this["DiscordBotEnabled"])); + } + set { + this["DiscordBotEnabled"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("asm")] + public string DiscordBotPrefix { + get { + return ((string)(this["DiscordBotPrefix"])); + } + set { + this["DiscordBotPrefix"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("")] + public string DiscordBotToken { + get { + return ((string)(this["DiscordBotToken"])); + } + set { + this["DiscordBotToken"] = value; + } + } } } diff --git a/src/ARKServerManager/Config.settings b/src/ARKServerManager/Config.settings index 72345566..e4710c8e 100644 --- a/src/ARKServerManager/Config.settings +++ b/src/ARKServerManager/Config.settings @@ -779,5 +779,14 @@ asmdata + + False + + + asm + + + + \ No newline at end of file diff --git a/src/ARKServerManager/Globalization/en-US/en-US.xaml b/src/ARKServerManager/Globalization/en-US/en-US.xaml index 49ab73d1..fe8119e6 100644 --- a/src/ARKServerManager/Globalization/en-US/en-US.xaml +++ b/src/ARKServerManager/Globalization/en-US/en-US.xaml @@ -5523,4 +5523,10 @@ There was a problem while performing the server update. This may leave your server in a incomplete state.\r\n\r\nDo you want to continue with the server start, this could cause problems? + + Discord Bot Error + The discord bot requires a valid token so it can log into the discord server\r\nThis can be set in the global settings. + The discord bot prefix contains invalid characters. Only letters and numbers are allowed. + + \ No newline at end of file diff --git a/src/ConanServerManager/App.config b/src/ConanServerManager/App.config index 5b188135..3239cf16 100644 --- a/src/ConanServerManager/App.config +++ b/src/ConanServerManager/App.config @@ -570,6 +570,15 @@ 250 + + False + + + csm + + + + \ No newline at end of file diff --git a/src/ConanServerManager/App.xaml.cs b/src/ConanServerManager/App.xaml.cs index 00eb25a8..9e6c6391 100644 --- a/src/ConanServerManager/App.xaml.cs +++ b/src/ConanServerManager/App.xaml.cs @@ -4,6 +4,9 @@ using NLog.Config; using NLog.Targets; using ServerManagerTool.Common; using ServerManagerTool.Common.Utils; +using ServerManagerTool.Discord; +using ServerManagerTool.Discord.Enums; +using ServerManagerTool.Discord.Interfaces; using ServerManagerTool.Enums; using ServerManagerTool.Lib; using ServerManagerTool.Plugin.Common; @@ -37,6 +40,7 @@ namespace ServerManagerTool public event PropertyChangedEventHandler PropertyChanged; + private CancellationTokenSource _tokenSource; private GlobalizedApplication _globalizer; private bool _applicationStarted; private string _args; @@ -142,6 +146,12 @@ namespace ServerManagerTool } } + public IServerManagerBot ServerManagerBot + { + get; + set; + } + public static void DiscoverMachinePublicIP(bool forceOverride) { if (forceOverride || string.IsNullOrWhiteSpace(Config.Default.MachinePublicIP)) @@ -177,7 +187,11 @@ namespace ServerManagerTool private IList FetchProfiles() { - return ServerManager.Instance.Servers.Select(s => new ServerManagerTool.Plugin.Common.Lib.Profile() { ProfileName = s?.Profile?.ProfileName ?? string.Empty, InstallationFolder = s?.Profile?.InstallDirectory ?? string.Empty }).ToList(); + return ServerManager.Instance.Servers.Select(s => new ServerManagerTool.Plugin.Common.Lib.Profile() + { + ProfileName = s?.Profile?.ProfileName ?? string.Empty, + InstallationFolder = s?.Profile?.InstallDirectory ?? string.Empty + }).ToList(); } public static string GetLogFolder() => IOUtils.NormalizePath(Path.Combine(Config.Default.DataPath, Config.Default.LogsRelativePath)); @@ -216,6 +230,11 @@ namespace ServerManagerTool return LogManager.GetLogger(loggerName); } + private static IList HandleDiscordCommand(CommandType commandType, string channelId, string profileId) + { + return null; + } + private static void MigrateSettings() { var installFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); @@ -395,6 +414,7 @@ namespace ServerManagerTool this.ApplicationStarted = true; + var restartRequired = false; if (string.IsNullOrWhiteSpace(Config.Default.DataPath)) { var dataDirectoryWindow = new DataDirectoryWindow(); @@ -405,6 +425,8 @@ namespace ServerManagerTool { Environment.Exit(0); } + + restartRequired = true; } Config.Default.ConfigPath = Path.Combine(Config.Default.DataPath, Config.Default.ProfilesRelativePath); @@ -412,6 +434,11 @@ namespace ServerManagerTool Config.Default.Save(); CommonConfig.Default.Save(); + if (restartRequired) + { + Environment.Exit(0); + } + if (e.Args.Any(a => a.StartsWith(Constants.ARG_SERVERMONITOR, StringComparison.OrdinalIgnoreCase))) { ServerRuntime.EnableUpdateModStatus = false; @@ -426,6 +453,27 @@ namespace ServerManagerTool StartupUri = new Uri("Windows/AutoUpdateWindow.xaml", UriKind.RelativeOrAbsolute); } + + if (Config.Default.DiscordBotEnabled) + { + _tokenSource = new CancellationTokenSource(); + + ServerManagerBot = ServerManagerBotFactory.GetServerManagerBot(); + + Task discordTask = Task.Run(async () => + { + await ServerManagerBot.StartAsync(Config.Default.DiscordBotPrefix, Config.Default.DiscordBotToken, Config.Default.DataPath, HandleDiscordCommand, _tokenSource.Token); + }, _tokenSource.Token) + .ContinueWith(t => { + var message = t.Exception.InnerException is null ? t.Exception.Message : t.Exception.InnerException.Message; + if (message.StartsWith("#")) + { + message = _globalizer.GetResourceString(message.Substring(1)) ?? message.Substring(1); + } + + MessageBox.Show(message, _globalizer.GetResourceString("DiscordBot_ErrorTitle"), MessageBoxButton.OK, MessageBoxImage.Error); + }, TaskContinuationOptions.OnlyOnFaulted); + } } protected override void OnExit(ExitEventArgs e) @@ -466,6 +514,12 @@ namespace ServerManagerTool private void ShutDownApplication() { + if (!(_tokenSource is null)) + { + _tokenSource.Cancel(); + _tokenSource.Dispose(); + } + if (this.ApplicationStarted) { foreach (var server in ServerManager.Instance.Servers) diff --git a/src/ConanServerManager/Config.Designer.cs b/src/ConanServerManager/Config.Designer.cs index 7239440f..924bbc88 100644 --- a/src/ConanServerManager/Config.Designer.cs +++ b/src/ConanServerManager/Config.Designer.cs @@ -1965,5 +1965,41 @@ namespace ServerManagerTool { return ((string)(this["DefaultDataDirectoryName"])); } } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool DiscordBotEnabled { + get { + return ((bool)(this["DiscordBotEnabled"])); + } + set { + this["DiscordBotEnabled"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("csm")] + public string DiscordBotPrefix { + get { + return ((string)(this["DiscordBotPrefix"])); + } + set { + this["DiscordBotPrefix"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("")] + public string DiscordBotToken { + get { + return ((string)(this["DiscordBotToken"])); + } + set { + this["DiscordBotToken"] = value; + } + } } } diff --git a/src/ConanServerManager/Config.settings b/src/ConanServerManager/Config.settings index edc93f78..ed28a58a 100644 --- a/src/ConanServerManager/Config.settings +++ b/src/ConanServerManager/Config.settings @@ -545,5 +545,14 @@ csmdata + + False + + + csm + + + + \ No newline at end of file diff --git a/src/ConanServerManager/Globalization/en-US/en-US.xaml b/src/ConanServerManager/Globalization/en-US/en-US.xaml index c1e939f7..60916cec 100644 --- a/src/ConanServerManager/Globalization/en-US/en-US.xaml +++ b/src/ConanServerManager/Globalization/en-US/en-US.xaml @@ -1193,4 +1193,10 @@ There was a problem while performing the server update. This may leave your server in a incomplete state.\r\n\r\nDo you want to continue with the server start, this could cause problems? + + Discord Bot Error + The discord bot requires a valid token so it can log into the discord server\r\nThis can be set in the global settings. + The discord bot prefix contains invalid characters. Only letters and numbers are allowed. + + \ No newline at end of file diff --git a/src/ServerManager.Discord/Delegates/HandleCommandDelegate.cs b/src/ServerManager.Discord/Delegates/HandleCommandDelegate.cs new file mode 100644 index 00000000..fcbbc5d2 --- /dev/null +++ b/src/ServerManager.Discord/Delegates/HandleCommandDelegate.cs @@ -0,0 +1,7 @@ +using ServerManagerTool.Discord.Enums; +using System.Collections.Generic; + +namespace ServerManagerTool.Discord.Delegates +{ + public delegate IList HandleCommandDelegate(CommandType commandType, string channelId, string profileId); +} diff --git a/src/ServerManager.Discord/DiscordBot.cs b/src/ServerManager.Discord/DiscordBot.cs new file mode 100644 index 00000000..55834e42 --- /dev/null +++ b/src/ServerManager.Discord/DiscordBot.cs @@ -0,0 +1,15 @@ +using ServerManagerTool.Discord.Delegates; + +namespace ServerManagerTool.Discord +{ + public static class DiscordBot + { + public const string PREFIX_DELIMITER = "!"; + + internal static HandleCommandDelegate HandleCommandCallback + { + get; + set; + } + } +} diff --git a/src/ServerManager.Discord/Enums/CommandType.cs b/src/ServerManager.Discord/Enums/CommandType.cs new file mode 100644 index 00000000..1da59236 --- /dev/null +++ b/src/ServerManager.Discord/Enums/CommandType.cs @@ -0,0 +1,14 @@ +namespace ServerManagerTool.Discord.Enums +{ + public enum CommandType + { + BackupServer, + ServerInfo, + ServerList, + ServerStatus, + ShutdownServer, + StartServer, + StopServer, + UpdateServer, + } +} diff --git a/src/ServerManager.Discord/Interfaces/IServerManagerBot.cs b/src/ServerManager.Discord/Interfaces/IServerManagerBot.cs new file mode 100644 index 00000000..f1dd6be4 --- /dev/null +++ b/src/ServerManager.Discord/Interfaces/IServerManagerBot.cs @@ -0,0 +1,11 @@ +using ServerManagerTool.Discord.Delegates; +using System.Threading; +using System.Threading.Tasks; + +namespace ServerManagerTool.Discord.Interfaces +{ + public interface IServerManagerBot + { + Task StartAsync(string commandPrefix, string discordToken, string dataDirectory, HandleCommandDelegate handleCommandCallback, CancellationToken token); + } +} diff --git a/src/ServerManager.Discord/Modules/HelpModule.cs b/src/ServerManager.Discord/Modules/HelpModule.cs new file mode 100644 index 00000000..d4b70531 --- /dev/null +++ b/src/ServerManager.Discord/Modules/HelpModule.cs @@ -0,0 +1,151 @@ +using Discord; +using Discord.Commands; +using Microsoft.Extensions.Configuration; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace ServerManagerTool.Discord.Modules +{ + [Name("Help")] + public sealed class HelpModule : ModuleBase + { + private const int MAX_VALUE_LENGTH = 1024; + + private readonly CommandService _service; + private readonly IConfigurationRoot _config; + private readonly IServiceProvider _services; + + public HelpModule(CommandService service, IConfigurationRoot config, IServiceProvider services) + { + _service = service; + _config = config; + _services = services; + } + + [Command("help")] + [Summary("Provides a list of available commands")] + public async Task HelpAsync() + { + var prefix = _config["DiscordSettings:Prefix"]; + + var builder = new EmbedBuilder() + { + Color = new Color(114, 137, 218), + Description = "These are the commands you can use" + }; + + foreach (var module in _service.Modules) + { + var moduleName = module.Name; + + // create the list of accessible commands + var commands = new List(module.Commands.Count); + + foreach (var cmd in module.Commands) + { + var result = await cmd.CheckPreconditionsAsync(Context, _services); + if (!result.IsSuccess) + { + continue; + } + + commands.Add($"{prefix}{cmd.Aliases.First()}"); + } + + // remove all duplicate commands + commands = commands.Distinct().ToList(); + + var commandString = string.Empty; + + foreach (var command in commands) + { + if (string.IsNullOrWhiteSpace(command)) + { + continue; + } + + var commandToAdd = $"{command}\n"; + + if (commandString.Length + commandToAdd.Length > MAX_VALUE_LENGTH) + { + // force the output, string would be too long + builder.AddField(x => + { + x.Name = moduleName; + x.Value = $"{commandString}\n"; + x.IsInline = false; + }); + + // reset the module name and command string + moduleName = $"{module.Name} cont."; + commandString = string.Empty; + } + + commandString += commandToAdd; + } + + if (!string.IsNullOrWhiteSpace(commandString)) + { + builder.AddField(x => + { + x.Name = moduleName; + x.Value = $"{commandString}\n"; + x.IsInline = false; + }); + } + } + + await ReplyAsync(string.Empty, false, builder.Build()); + } + + [Command("help")] + [Summary("Searches a list of available commands")] + public async Task HelpAsync(string command) + { + var searchResults = _service.Search(Context, command); + + if (!searchResults.IsSuccess) + { + await ReplyAsync($"Sorry, couldn't find a command like **{command}**."); + return; + } + + var prefix = _config["DiscordSettings:Prefix"]; + + var builder = new EmbedBuilder() + { + Color = new Color(114, 137, 218), + Description = $"Here are some commands like **{command}**" + }; + + foreach (var match in searchResults.Commands) + { + var cmd = match.Command; + + var result = await cmd.CheckPreconditionsAsync(Context, _services); + if (!result.IsSuccess) + { + continue; + } + + var usage = $"{prefix}{cmd.Aliases.First()}"; + if (cmd.Parameters.Count > 0) + { + usage += $" {string.Join(" ", cmd.Parameters.Select(p => p.Name))}"; + } + usage += $"\n"; + + builder.AddField(x => + { + x.Name = string.Join(", ", cmd.Aliases); + x.Value = $"Summary: {cmd.Summary}\nUsage: {usage}"; + x.IsInline = false; + }); + } + + await ReplyAsync(string.Empty, false, builder.Build()); + } + } +} \ No newline at end of file diff --git a/src/ServerManager.Discord/Modules/ServerCommandModule.cs b/src/ServerManager.Discord/Modules/ServerCommandModule.cs new file mode 100644 index 00000000..63ac7593 --- /dev/null +++ b/src/ServerManager.Discord/Modules/ServerCommandModule.cs @@ -0,0 +1,207 @@ +using Discord.Addons.Interactive; +using Discord.Commands; +using Microsoft.Extensions.Configuration; +using ServerManagerTool.Discord.Enums; +using System; +using System.Threading.Tasks; + +namespace ServerManagerTool.Discord.Modules +{ + [Name("Server Commands")] + public sealed class ServerCommandModule : InteractiveBase + { + private readonly CommandService _service; + private readonly IConfigurationRoot _config; + + public ServerCommandModule(CommandService service, IConfigurationRoot config) + { + _service = service; + _config = config; + } + + [Command("backup", RunMode = RunMode.Async)] + [Summary("Perform a backup of the server")] + [Remarks("backup")] + public async Task BackupServerAsync() + { + await BackupServerAsync(null); + } + + [Command("backup", RunMode = RunMode.Async)] + [Summary("Perform a backup of the server")] + [Remarks("backup profileId")] + public async Task BackupServerAsync(string profileId) + { + try + { + var channelId = Context?.Channel?.Id.ToString() ?? string.Empty; + + var response = DiscordBot.HandleCommandCallback?.Invoke(CommandType.BackupServer, channelId, profileId); + if (response is null || response.Count == 0) + { + await ReplyAsync("No servers associated with this channel."); + } + else + { + foreach (var output in response) + { + await ReplyAsync(output); + await Task.Delay(1000); + } + } + } + catch (Exception ex) + { + await ReplyAsync($"'{Context.Message}' command sent and failed with exception ({ex.Message})"); + } + } + + [Command("shutdown", RunMode = RunMode.Async)] + [Summary("Shuts down the server properly")] + [Remarks("shutdown")] + public async Task ShutdownServerAsync() + { + await ShutdownServerAsync(null); + } + + [Command("shutdown", RunMode = RunMode.Async)] + [Summary("Shuts down the server properly")] + [Remarks("shutdown profileId")] + public async Task ShutdownServerAsync(string profileId) + { + try + { + var channelId = Context?.Channel?.Id.ToString() ?? string.Empty; + + var response = DiscordBot.HandleCommandCallback?.Invoke(CommandType.ShutdownServer, channelId, profileId); + if (response is null || response.Count == 0) + { + await ReplyAsync("No servers associated with this channel."); + } + else + { + foreach (var output in response) + { + await ReplyAsync(output); + await Task.Delay(1000); + } + } + } + catch (Exception ex) + { + await ReplyAsync($"'{Context.Message}' command sent and failed with exception ({ex.Message})"); + } + } + + [Command("start", RunMode = RunMode.Async)] + [Summary("Starts the server")] + [Remarks("start")] + public async Task StartServerAsync() + { + await StartServerAsync(null); + } + + [Command("start", RunMode = RunMode.Async)] + [Summary("Starts the server")] + [Remarks("start profileId")] + public async Task StartServerAsync(string profileId) + { + try + { + var channelId = Context?.Channel?.Id.ToString() ?? string.Empty; + + var response = DiscordBot.HandleCommandCallback?.Invoke(CommandType.StartServer, channelId, profileId); + if (response is null || response.Count == 0) + { + await ReplyAsync("No servers associated with this channel."); + } + else + { + foreach (var output in response) + { + await ReplyAsync(output); + await Task.Delay(1000); + } + } + } + catch (Exception ex) + { + await ReplyAsync($"'{Context.Message}' command sent and failed with exception ({ex.Message})"); + } + } + + [Command("stop", RunMode = RunMode.Async)] + [Summary("Forcibly stops the server")] + [Remarks("stop")] + public async Task StopServerAsync() + { + await StopServerAsync(null); + } + + [Command("stop", RunMode = RunMode.Async)] + [Summary("Forcibly stops the server")] + [Remarks("stop profileId")] + public async Task StopServerAsync(string profileId) + { + try + { + var channelId = Context?.Channel?.Id.ToString() ?? string.Empty; + + var response = DiscordBot.HandleCommandCallback?.Invoke(CommandType.StopServer, channelId, profileId); + if (response is null || response.Count == 0) + { + await ReplyAsync("No servers associated with this channel."); + } + else + { + foreach (var output in response) + { + await ReplyAsync(output); + await Task.Delay(1000); + } + } + } + catch (Exception ex) + { + await ReplyAsync($"'{Context.Message}' command sent and failed with exception ({ex.Message})"); + } + } + + [Command("update", RunMode = RunMode.Async)] + [Summary("Updates the server")] + [Remarks("update")] + public async Task UpdateServerAsync() + { + await UpdateServerAsync(null); + } + + [Command("update", RunMode = RunMode.Async)] + [Summary("Updates the server")] + [Remarks("update profileId")] + public async Task UpdateServerAsync(string profileId) + { + try + { + var channelId = Context?.Channel?.Id.ToString() ?? string.Empty; + + var response = DiscordBot.HandleCommandCallback?.Invoke(CommandType.UpdateServer, channelId, profileId); + if (response is null || response.Count == 0) + { + await ReplyAsync("No servers associated with this channel."); + } + else + { + foreach (var output in response) + { + await ReplyAsync(output); + await Task.Delay(1000); + } + } + } + catch (Exception ex) + { + await ReplyAsync($"'{Context.Message}' command sent and failed with exception ({ex.Message})"); + } + } + } +} diff --git a/src/ServerManager.Discord/Modules/ServerQueryModule.cs b/src/ServerManager.Discord/Modules/ServerQueryModule.cs new file mode 100644 index 00000000..9b5b7590 --- /dev/null +++ b/src/ServerManager.Discord/Modules/ServerQueryModule.cs @@ -0,0 +1,125 @@ +using Discord.Addons.Interactive; +using Discord.Commands; +using Microsoft.Extensions.Configuration; +using ServerManagerTool.Discord.Enums; +using System; +using System.Threading.Tasks; + +namespace ServerManagerTool.Discord.Modules +{ + [Name("Server Query")] + public sealed class ServerQueryModule : InteractiveBase + { + private readonly CommandService _service; + private readonly IConfigurationRoot _config; + + public ServerQueryModule(CommandService service, IConfigurationRoot config) + { + _service = service; + _config = config; + } + + [Command("info", RunMode = RunMode.Async)] + [Summary("Poll server for information")] + [Remarks("info")] + public async Task ServerInfoAsync() + { + await ServerInfoAsync(null); + } + + [Command("info", RunMode = RunMode.Async)] + [Summary("Poll server for information")] + [Remarks("info profileId")] + public async Task ServerInfoAsync(string profileId) + { + try + { + var channelId = Context?.Channel?.Id.ToString() ?? string.Empty; + + var response = DiscordBot.HandleCommandCallback?.Invoke(CommandType.ServerInfo, channelId, profileId); + if (response is null || response.Count == 0) + { + await ReplyAsync("No servers associated with this channel."); + } + else + { + foreach (var output in response) + { + await ReplyAsync(output); + await Task.Delay(1000); + } + } + } + catch (Exception ex) + { + await ReplyAsync($"'{Context.Message}' command sent and failed with exception ({ex.Message})"); + } + } + + [Command("list", RunMode = RunMode.Async)] + [Summary("List of all servers associated with this channel")] + [Remarks("list")] + public async Task ServerListAsync() + { + try + { + var channelId = Context?.Channel?.Id.ToString() ?? string.Empty; + + var response = DiscordBot.HandleCommandCallback?.Invoke(CommandType.ServerList, channelId, null); + if (response is null || response.Count == 0) + { + await ReplyAsync("No servers associated with this channel."); + } + else + { + foreach (var output in response) + { + await ReplyAsync(output); + await Task.Delay(1000); + } + } + } + catch (Exception ex) + { + await ReplyAsync($"'{Context.Message}' command sent and failed with exception ({ex.Message})"); + } + } + + [Command("status", RunMode = RunMode.Async)] + [Summary("Poll server for status")] + [Remarks("status")] + public async Task ServerStatusAsync() + { + await ServerStatusAsync(null); + } + + [Command("status", RunMode = RunMode.Async)] + [Summary("Poll server for status")] + [Remarks("status profileId")] + public async Task ServerStatusAsync(string profileId) + { + try + { + var channelId = Context?.Channel?.Id.ToString() ?? string.Empty; + + var response = DiscordBot.HandleCommandCallback?.Invoke(CommandType.ServerStatus, channelId, profileId); + if (response is null || response.Count == 0) + { + await ReplyAsync("No servers associated with this channel."); + } + else + { + foreach (var output in response) + { + await ReplyAsync(output); + await Task.Delay(1000); + } + } + } + catch (Exception ex) + { + await ReplyAsync($"'{Context.Message}' command sent and failed with exception ({ex.Message})"); + } + } + } +} diff --git a/src/ServerManager.Discord/ServerManager.Discord.csproj b/src/ServerManager.Discord/ServerManager.Discord.csproj index 3b657bf3..51a6255d 100644 --- a/src/ServerManager.Discord/ServerManager.Discord.csproj +++ b/src/ServerManager.Discord/ServerManager.Discord.csproj @@ -1,4 +1,4 @@ - + Debug;Release;Debug - Beta @@ -11,5 +11,18 @@ none false - + + $(DefineConstants);DEBUG + + + + + + + + + + + + diff --git a/src/ServerManager.Discord/ServerManagerBot.cs b/src/ServerManager.Discord/ServerManagerBot.cs new file mode 100644 index 00000000..f9530c34 --- /dev/null +++ b/src/ServerManager.Discord/ServerManagerBot.cs @@ -0,0 +1,137 @@ +using Discord; +using Discord.Addons.Interactive; +using Discord.Commands; +using Discord.Net.Providers.WS4Net; +using Discord.WebSocket; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using ServerManagerTool.Discord.Delegates; +using ServerManagerTool.Discord.Interfaces; +using ServerManagerTool.Discord.Services; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace ServerManagerTool.Discord +{ + public sealed class ServerManagerBot : IServerManagerBot + { + internal ServerManagerBot() + { + } + + private bool Started + { + get; + set; + } + + public async Task StartAsync(string commandPrefix, string discordToken, string dataDirectory, HandleCommandDelegate handleCommandCallback, CancellationToken token) + { + if (Started) + { + return; + } + Started = true; + + if (string.IsNullOrWhiteSpace(commandPrefix) || string.IsNullOrWhiteSpace(discordToken)) + { + return; + } + + if (commandPrefix.Any(c => !char.IsLetterOrDigit(c))) + { + throw new Exception("#DiscordBot_InvalidPrefixError"); + } + + if (!commandPrefix.EndsWith(DiscordBot.PREFIX_DELIMITER)) + { + commandPrefix += DiscordBot.PREFIX_DELIMITER; + } + + var settings = new Dictionary + { + { "DiscordSettings:Prefix", commandPrefix }, + { "DiscordSettings:Token", discordToken }, + { "ServerManager:DataDirectory", dataDirectory } + }; + + // Begin building the configuration file + var config = new ConfigurationBuilder() + .AddInMemoryCollection(settings) + .Build(); + + var socketConfig = new DiscordSocketConfig + { +#if DEBUG + LogLevel = LogSeverity.Verbose, +#else + LogLevel = LogSeverity.Info, +#endif + // Tell Discord.Net to cache 1000 messages per channel + MessageCacheSize = 1000, + }; + if (Environment.OSVersion.Version < new Version(6, 2)) + { + // windows 7 or early + socketConfig.WebSocketProvider = WS4NetProvider.Instance; + } + + var commandConfig = new CommandServiceConfig + { + // Force all commands to run async + DefaultRunMode = RunMode.Async, +#if DEBUG + LogLevel = LogSeverity.Verbose, +#else + LogLevel = LogSeverity.Info, +#endif + }; + + // Build the service provider + var services = new ServiceCollection() + // Add the discord client to the service provider + .AddSingleton(new DiscordSocketClient(socketConfig)) + // Add the command service to the service provider + .AddSingleton(new CommandService(commandConfig)) + // Add remaining services to the provider + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(config); + + // Create the service provider + using (var provider = services.BuildServiceProvider()) + { + // Initialize the logging service, startup service, and command handler + provider?.GetRequiredService(); + await provider?.GetRequiredService().StartAsync(); + provider?.GetRequiredService(); + + DiscordBot.HandleCommandCallback = handleCommandCallback; + + try + { + // Prevent the application from closing + await Task.Delay(Timeout.Infinite, token); + } + catch (TaskCanceledException) + { + Debug.WriteLine("Task Canceled"); + } + catch (OperationCanceledException) + { + Debug.WriteLine("Operation Canceled"); + } + + await provider?.GetRequiredService().StopAsync(); + } + } + } +} diff --git a/src/ServerManager.Discord/ServerManagerBotFactory.cs b/src/ServerManager.Discord/ServerManagerBotFactory.cs new file mode 100644 index 00000000..f4143d5a --- /dev/null +++ b/src/ServerManager.Discord/ServerManagerBotFactory.cs @@ -0,0 +1,19 @@ +using ServerManagerTool.Discord.Interfaces; + +namespace ServerManagerTool.Discord +{ + public static class ServerManagerBotFactory + { + private static IServerManagerBot _serverManagerBot; + + public static IServerManagerBot GetServerManagerBot() + { + if (_serverManagerBot is null) + { + _serverManagerBot = new ServerManagerBot(); + } + + return _serverManagerBot; + } + } +} diff --git a/src/ServerManager.Discord/Services/CommandHandlerService.cs b/src/ServerManager.Discord/Services/CommandHandlerService.cs new file mode 100644 index 00000000..a4578631 --- /dev/null +++ b/src/ServerManager.Discord/Services/CommandHandlerService.cs @@ -0,0 +1,65 @@ +using Discord.Commands; +using Discord.WebSocket; +using Microsoft.Extensions.Configuration; +using System; +using System.Threading.Tasks; + +namespace ServerManagerTool.Discord.Services +{ + public class CommandHandlerService + { + private readonly DiscordSocketClient _discord; + private readonly CommandService _commands; + private readonly IConfigurationRoot _config; + private readonly IServiceProvider _provider; + + public CommandHandlerService(DiscordSocketClient discord, CommandService commands, IConfigurationRoot config, IServiceProvider provider) + { + _discord = discord; + _commands = commands; + _config = config; + _provider = provider; + + _discord.MessageReceived += OnMessageReceivedAsync; + } + + private async Task OnMessageReceivedAsync(SocketMessage s) + { + // Ensure the message is from a user/bot + var msg = s as SocketUserMessage; + if (msg is null) + { + return; + } + + // Ignore self when checking commands + if (msg.Author == _discord.CurrentUser) + { + return; + } + + //Tell bot to ignore itself. + if (msg.Author.IsBot) + { + return; + } + + // Create the command context + var context = new SocketCommandContext(_discord, msg); + + // Check if the message has a valid command prefix + var argPos = 0; + if (msg.HasStringPrefix(_config["DiscordSettings:Prefix"], ref argPos) || msg.HasMentionPrefix(_discord.CurrentUser, ref argPos)) + { + // Execute the command + var result = await _commands.ExecuteAsync(context, argPos, _provider); + + if (!result.IsSuccess) + { + // If not successful, reply with the error. + await context.Channel.SendMessageAsync(result.ToString()); + } + } + } + } +} \ No newline at end of file diff --git a/src/ServerManager.Discord/Services/LoggingService.cs b/src/ServerManager.Discord/Services/LoggingService.cs new file mode 100644 index 00000000..de540d9d --- /dev/null +++ b/src/ServerManager.Discord/Services/LoggingService.cs @@ -0,0 +1,57 @@ +using Discord; +using Discord.Commands; +using Discord.WebSocket; +using Microsoft.Extensions.Configuration; +using System; +using System.IO; +using System.Threading.Tasks; + +namespace ServerManagerTool.Discord.Services +{ + public class LoggingService + { + private readonly DiscordSocketClient _discord; + private readonly CommandService _commands; + private readonly IConfigurationRoot _config; + + private string LogDirectory { get; } + private string LogFile => Path.Combine(LogDirectory, $"ServerManager_DiscordBot.{DateTime.Now:yyyyMMdd}.log"); + + public LoggingService(DiscordSocketClient discord, CommandService commands, IConfigurationRoot config) + { + _discord = discord; + _commands = commands; + _config = config; + + // Get the data directory from the config file + var rootDirectory = _config["ServerManager:DataDirectory"] ?? AppContext.BaseDirectory; + LogDirectory = Path.Combine(rootDirectory, "logs"); + + _discord.Log += OnLogAsync; + _commands.Log += OnLogAsync; + } + + private async Task OnLogAsync(LogMessage message) + { + // Create the log directory if it doesn't exist + if (!Directory.Exists(LogDirectory)) + { + Directory.CreateDirectory(LogDirectory); + } + + // Create today's log file if it doesn't exist + if (!File.Exists(LogFile)) + { + File.Create(LogFile).Dispose(); + } + + var logText = $"{DateTime.Now:HH:mm:ss:ffff} [{message.Severity}] {message.Source}: {message.Exception?.ToString() ?? message.Message}"; + + // Write the log text to a file + File.AppendAllText(LogFile, logText + "\n"); + + // Write the log text to the console + await Console.Out.WriteLineAsync(logText); + } + } +} \ No newline at end of file diff --git a/src/ServerManager.Discord/Services/ShutdownService.cs b/src/ServerManager.Discord/Services/ShutdownService.cs new file mode 100644 index 00000000..65d816f5 --- /dev/null +++ b/src/ServerManager.Discord/Services/ShutdownService.cs @@ -0,0 +1,21 @@ +using Discord.WebSocket; +using System.Threading.Tasks; + +namespace ServerManagerTool.Discord.Services +{ + public class ShutdownService + { + private readonly DiscordSocketClient _discord; + + public ShutdownService(DiscordSocketClient discord) + { + _discord = discord; + } + + public async Task StopAsync() + { + await _discord.StopAsync(); + await _discord.LogoutAsync(); + } + } +} diff --git a/src/ServerManager.Discord/Services/StartupService.cs b/src/ServerManager.Discord/Services/StartupService.cs new file mode 100644 index 00000000..98f63328 --- /dev/null +++ b/src/ServerManager.Discord/Services/StartupService.cs @@ -0,0 +1,45 @@ +using Discord; +using Discord.Commands; +using Discord.WebSocket; +using Microsoft.Extensions.Configuration; +using System; +using System.Reflection; +using System.Threading.Tasks; + +namespace ServerManagerTool.Discord.Services +{ + public class StartupService + { + private readonly DiscordSocketClient _discord; + private readonly CommandService _commands; + private readonly IConfigurationRoot _config; + private readonly IServiceProvider _provider; + + public StartupService(DiscordSocketClient discord, CommandService commands, IConfigurationRoot config, IServiceProvider provider) + { + _discord = discord; + _commands = commands; + _config = config; + _provider = provider; + } + + public async Task StartAsync() + { + // Get the discord token from the config file + var discordToken = _config["DiscordSettings:Token"]; + + if (string.IsNullOrWhiteSpace(discordToken)) + { + throw new Exception("#DiscordBot_MissingTokenError"); + } + + // Login to discord + await _discord.LoginAsync(TokenType.Bot, discordToken); + // Connect to the websocket + await _discord.StartAsync(); + + // Load commands and modules into the command service + await _commands.AddModulesAsync(Assembly.GetExecutingAssembly(), _provider); + } + } +} \ No newline at end of file From c5775181b300523d46ebc161443753da6bfa88d8 Mon Sep 17 00:00:00 2001 From: Brett Hewitson Date: Sat, 4 Dec 2021 14:35:07 +1000 Subject: [PATCH 2/9] Discord Bot Changes 1. Added ServerId to Delegate 2. Change Delegate storage to use DI. --- .../Delegates/HandleCommandDelegate.cs | 2 +- src/ServerManager.Discord/DiscordBot.cs | 6 ------ .../Interfaces/IServerManagerBot.cs | 2 +- .../Modules/ServerCommandModule.cs | 20 +++++++++++++------ .../Modules/ServerQueryModule.cs | 14 +++++++++---- src/ServerManager.Discord/ServerManagerBot.cs | 10 +++++----- 6 files changed, 31 insertions(+), 23 deletions(-) diff --git a/src/ServerManager.Discord/Delegates/HandleCommandDelegate.cs b/src/ServerManager.Discord/Delegates/HandleCommandDelegate.cs index fcbbc5d2..5d7f9a1d 100644 --- a/src/ServerManager.Discord/Delegates/HandleCommandDelegate.cs +++ b/src/ServerManager.Discord/Delegates/HandleCommandDelegate.cs @@ -3,5 +3,5 @@ using System.Collections.Generic; namespace ServerManagerTool.Discord.Delegates { - public delegate IList HandleCommandDelegate(CommandType commandType, string channelId, string profileId); + public delegate IList HandleCommandDelegate(CommandType commandType, string serverId, string channelId, string profileId); } diff --git a/src/ServerManager.Discord/DiscordBot.cs b/src/ServerManager.Discord/DiscordBot.cs index 55834e42..976cc984 100644 --- a/src/ServerManager.Discord/DiscordBot.cs +++ b/src/ServerManager.Discord/DiscordBot.cs @@ -5,11 +5,5 @@ namespace ServerManagerTool.Discord public static class DiscordBot { public const string PREFIX_DELIMITER = "!"; - - internal static HandleCommandDelegate HandleCommandCallback - { - get; - set; - } } } diff --git a/src/ServerManager.Discord/Interfaces/IServerManagerBot.cs b/src/ServerManager.Discord/Interfaces/IServerManagerBot.cs index f1dd6be4..4b6714bc 100644 --- a/src/ServerManager.Discord/Interfaces/IServerManagerBot.cs +++ b/src/ServerManager.Discord/Interfaces/IServerManagerBot.cs @@ -6,6 +6,6 @@ namespace ServerManagerTool.Discord.Interfaces { public interface IServerManagerBot { - Task StartAsync(string commandPrefix, string discordToken, string dataDirectory, HandleCommandDelegate handleCommandCallback, CancellationToken token); + Task StartAsync(string discordToken, string commandPrefix, string dataDirectory, HandleCommandDelegate handleCommandCallback, CancellationToken token); } } diff --git a/src/ServerManager.Discord/Modules/ServerCommandModule.cs b/src/ServerManager.Discord/Modules/ServerCommandModule.cs index 63ac7593..49921a6a 100644 --- a/src/ServerManager.Discord/Modules/ServerCommandModule.cs +++ b/src/ServerManager.Discord/Modules/ServerCommandModule.cs @@ -1,6 +1,7 @@ using Discord.Addons.Interactive; using Discord.Commands; using Microsoft.Extensions.Configuration; +using ServerManagerTool.Discord.Delegates; using ServerManagerTool.Discord.Enums; using System; using System.Threading.Tasks; @@ -11,11 +12,13 @@ namespace ServerManagerTool.Discord.Modules public sealed class ServerCommandModule : InteractiveBase { private readonly CommandService _service; + private readonly HandleCommandDelegate _handleCommandCallback; private readonly IConfigurationRoot _config; - public ServerCommandModule(CommandService service, IConfigurationRoot config) + public ServerCommandModule(CommandService service, HandleCommandDelegate handleCommandCallback, IConfigurationRoot config) { _service = service; + _handleCommandCallback = handleCommandCallback; _config = config; } @@ -34,9 +37,10 @@ namespace ServerManagerTool.Discord.Modules { try { + var serverId = Context?.Guild?.Id.ToString() ?? string.Empty; var channelId = Context?.Channel?.Id.ToString() ?? string.Empty; - var response = DiscordBot.HandleCommandCallback?.Invoke(CommandType.BackupServer, channelId, profileId); + var response = _handleCommandCallback?.Invoke(CommandType.BackupServer, serverId, channelId, profileId); if (response is null || response.Count == 0) { await ReplyAsync("No servers associated with this channel."); @@ -71,9 +75,10 @@ namespace ServerManagerTool.Discord.Modules { try { + var serverId = Context?.Guild?.Id.ToString() ?? string.Empty; var channelId = Context?.Channel?.Id.ToString() ?? string.Empty; - var response = DiscordBot.HandleCommandCallback?.Invoke(CommandType.ShutdownServer, channelId, profileId); + var response = _handleCommandCallback?.Invoke(CommandType.ShutdownServer, serverId, channelId, profileId); if (response is null || response.Count == 0) { await ReplyAsync("No servers associated with this channel."); @@ -108,9 +113,10 @@ namespace ServerManagerTool.Discord.Modules { try { + var serverId = Context?.Guild?.Id.ToString() ?? string.Empty; var channelId = Context?.Channel?.Id.ToString() ?? string.Empty; - var response = DiscordBot.HandleCommandCallback?.Invoke(CommandType.StartServer, channelId, profileId); + var response = _handleCommandCallback?.Invoke(CommandType.StartServer, serverId, channelId, profileId); if (response is null || response.Count == 0) { await ReplyAsync("No servers associated with this channel."); @@ -145,9 +151,10 @@ namespace ServerManagerTool.Discord.Modules { try { + var serverId = Context?.Guild?.Id.ToString() ?? string.Empty; var channelId = Context?.Channel?.Id.ToString() ?? string.Empty; - var response = DiscordBot.HandleCommandCallback?.Invoke(CommandType.StopServer, channelId, profileId); + var response = _handleCommandCallback?.Invoke(CommandType.StopServer, serverId, channelId, profileId); if (response is null || response.Count == 0) { await ReplyAsync("No servers associated with this channel."); @@ -182,9 +189,10 @@ namespace ServerManagerTool.Discord.Modules { try { + var serverId = Context?.Guild?.Id.ToString() ?? string.Empty; var channelId = Context?.Channel?.Id.ToString() ?? string.Empty; - var response = DiscordBot.HandleCommandCallback?.Invoke(CommandType.UpdateServer, channelId, profileId); + var response = _handleCommandCallback?.Invoke(CommandType.UpdateServer, serverId, channelId, profileId); if (response is null || response.Count == 0) { await ReplyAsync("No servers associated with this channel."); diff --git a/src/ServerManager.Discord/Modules/ServerQueryModule.cs b/src/ServerManager.Discord/Modules/ServerQueryModule.cs index 9b5b7590..767a3413 100644 --- a/src/ServerManager.Discord/Modules/ServerQueryModule.cs +++ b/src/ServerManager.Discord/Modules/ServerQueryModule.cs @@ -1,6 +1,7 @@ using Discord.Addons.Interactive; using Discord.Commands; using Microsoft.Extensions.Configuration; +using ServerManagerTool.Discord.Delegates; using ServerManagerTool.Discord.Enums; using System; using System.Threading.Tasks; @@ -11,11 +12,13 @@ namespace ServerManagerTool.Discord.Modules public sealed class ServerQueryModule : InteractiveBase { private readonly CommandService _service; + private readonly HandleCommandDelegate _handleCommandCallback; private readonly IConfigurationRoot _config; - public ServerQueryModule(CommandService service, IConfigurationRoot config) + public ServerQueryModule(CommandService service, HandleCommandDelegate handleCommandCallback, IConfigurationRoot config) { _service = service; + _handleCommandCallback = handleCommandCallback; _config = config; } @@ -34,9 +37,10 @@ namespace ServerManagerTool.Discord.Modules { try { + var serverId = Context?.Guild?.Id.ToString() ?? string.Empty; var channelId = Context?.Channel?.Id.ToString() ?? string.Empty; - var response = DiscordBot.HandleCommandCallback?.Invoke(CommandType.ServerInfo, channelId, profileId); + var response = _handleCommandCallback?.Invoke(CommandType.ServerInfo, serverId, channelId, profileId); if (response is null || response.Count == 0) { await ReplyAsync("No servers associated with this channel."); @@ -63,9 +67,10 @@ namespace ServerManagerTool.Discord.Modules { try { + var serverId = Context?.Guild?.Id.ToString() ?? string.Empty; var channelId = Context?.Channel?.Id.ToString() ?? string.Empty; - var response = DiscordBot.HandleCommandCallback?.Invoke(CommandType.ServerList, channelId, null); + var response = _handleCommandCallback?.Invoke(CommandType.ServerList, serverId, channelId, null); if (response is null || response.Count == 0) { await ReplyAsync("No servers associated with this channel."); @@ -100,9 +105,10 @@ namespace ServerManagerTool.Discord.Modules { try { + var serverId = Context?.Guild?.Id.ToString() ?? string.Empty; var channelId = Context?.Channel?.Id.ToString() ?? string.Empty; - var response = DiscordBot.HandleCommandCallback?.Invoke(CommandType.ServerStatus, channelId, profileId); + var response = _handleCommandCallback?.Invoke(CommandType.ServerStatus, serverId, channelId, profileId); if (response is null || response.Count == 0) { await ReplyAsync("No servers associated with this channel."); diff --git a/src/ServerManager.Discord/ServerManagerBot.cs b/src/ServerManager.Discord/ServerManagerBot.cs index f9530c34..72e44731 100644 --- a/src/ServerManager.Discord/ServerManagerBot.cs +++ b/src/ServerManager.Discord/ServerManagerBot.cs @@ -21,6 +21,7 @@ namespace ServerManagerTool.Discord { internal ServerManagerBot() { + Started = false; } private bool Started @@ -29,7 +30,7 @@ namespace ServerManagerTool.Discord set; } - public async Task StartAsync(string commandPrefix, string discordToken, string dataDirectory, HandleCommandDelegate handleCommandCallback, CancellationToken token) + public async Task StartAsync(string discordToken, string commandPrefix, string dataDirectory, HandleCommandDelegate handleCommandCallback, CancellationToken token) { if (Started) { @@ -37,7 +38,7 @@ namespace ServerManagerTool.Discord } Started = true; - if (string.IsNullOrWhiteSpace(commandPrefix) || string.IsNullOrWhiteSpace(discordToken)) + if (string.IsNullOrWhiteSpace(commandPrefix) || string.IsNullOrWhiteSpace(discordToken) || handleCommandCallback is null) { return; } @@ -104,7 +105,8 @@ namespace ServerManagerTool.Discord .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(config); + .AddSingleton(config) + .AddSingleton(handleCommandCallback); // Create the service provider using (var provider = services.BuildServiceProvider()) @@ -114,8 +116,6 @@ namespace ServerManagerTool.Discord await provider?.GetRequiredService().StartAsync(); provider?.GetRequiredService(); - DiscordBot.HandleCommandCallback = handleCommandCallback; - try { // Prevent the application from closing From a792b10253f1af87407121cad7a1ecc394640f18 Mon Sep 17 00:00:00 2001 From: Brett Hewitson Date: Sat, 4 Dec 2021 14:36:34 +1000 Subject: [PATCH 3/9] Server Manager Changes 1. Added Discord Bot section to Global Settings 2. Added Discord Bot section to Server Settings. --- src/ARKServerManager/App.config | 12 ++++++ src/ARKServerManager/App.xaml.cs | 6 +-- src/ARKServerManager/Config.Designer.cs | 42 +++++++++++++++++++ src/ARKServerManager/Config.settings | 12 ++++++ .../Globalization/en-US/en-US.xaml | 19 ++++++++- src/ARKServerManager/Lib/ServerProfile.cs | 10 +++++ src/ARKServerManager/Styles/Default.xaml | 10 +++++ .../Windows/GlobalSettingsControl.xaml | 41 +++++++++++++++++- .../Windows/GlobalSettingsControl.xaml.cs | 14 +++++++ .../Windows/ServerSettingsControl.xaml | 39 +++++++++++++++++ src/ConanServerManager/App.config | 12 ++++++ src/ConanServerManager/App.xaml.cs | 6 +-- src/ConanServerManager/Config.Designer.cs | 42 +++++++++++++++++++ src/ConanServerManager/Config.settings | 12 ++++++ .../Globalization/en-US/en-US.xaml | 17 ++++++++ src/ConanServerManager/Lib/ServerProfile.cs | 10 +++++ src/ConanServerManager/Styles/Default.xaml | 10 +++++ .../Windows/GlobalSettingsControl.xaml | 41 +++++++++++++++++- .../Windows/GlobalSettingsControl.xaml.cs | 14 +++++++ .../Windows/ServerSettingsControl.xaml | 31 ++++++++++++++ 20 files changed, 389 insertions(+), 11 deletions(-) diff --git a/src/ARKServerManager/App.config b/src/ARKServerManager/App.config index f18196df..f75a4ea4 100644 --- a/src/ARKServerManager/App.config +++ b/src/ARKServerManager/App.config @@ -347,6 +347,12 @@ asmdata + + https://discord.com/developers/applications + + + https://arkservermanager.freeforums.net/thread/8764/get-own-discord-bot + @@ -813,6 +819,12 @@ + + + + + False + diff --git a/src/ARKServerManager/App.xaml.cs b/src/ARKServerManager/App.xaml.cs index db92093f..38c4ce70 100644 --- a/src/ARKServerManager/App.xaml.cs +++ b/src/ARKServerManager/App.xaml.cs @@ -231,9 +231,9 @@ namespace ServerManagerTool return LogManager.GetLogger(loggerName); } - private static IList HandleDiscordCommand(CommandType commandType, string channelId, string profileId) + private static IList HandleDiscordCommand(CommandType commandType, string serverId, string channelId, string profileId) { - return new List() { $"{commandType}; {channelId}; {profileId ?? "no profile"}" }; + return new List() { $"{commandType}; {serverId}; {channelId}; {profileId ?? "no profile"}" }; } private static void MigrateSettings() @@ -481,7 +481,7 @@ namespace ServerManagerTool Task discordTask = Task.Run(async () => { - await ServerManagerBot.StartAsync(Config.Default.DiscordBotPrefix, Config.Default.DiscordBotToken, Config.Default.DataDir, HandleDiscordCommand, _tokenSource.Token); + await ServerManagerBot.StartAsync(Config.Default.DiscordBotToken, Config.Default.DiscordBotPrefix, Config.Default.DataDir, HandleDiscordCommand, _tokenSource.Token); }, _tokenSource.Token) .ContinueWith(t => { var message = t.Exception.InnerException is null ? t.Exception.Message : t.Exception.InnerException.Message; diff --git a/src/ARKServerManager/Config.Designer.cs b/src/ARKServerManager/Config.Designer.cs index 2ef36e03..f3913c05 100644 --- a/src/ARKServerManager/Config.Designer.cs +++ b/src/ARKServerManager/Config.Designer.cs @@ -2848,5 +2848,47 @@ namespace ServerManagerTool { this["DiscordBotToken"] = value; } } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("")] + public string DiscordBotServerId { + get { + return ((string)(this["DiscordBotServerId"])); + } + set { + this["DiscordBotServerId"] = value; + } + } + + [global::System.Configuration.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("https://discord.com/developers/applications")] + public string DiscordBotApplyUrl { + get { + return ((string)(this["DiscordBotApplyUrl"])); + } + } + + [global::System.Configuration.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("https://arkservermanager.freeforums.net/thread/8764/get-own-discord-bot")] + public string DiscordBotHelpUrl { + get { + return ((string)(this["DiscordBotHelpUrl"])); + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool SectionDiscordBotIsExpanded { + get { + return ((bool)(this["SectionDiscordBotIsExpanded"])); + } + set { + this["SectionDiscordBotIsExpanded"] = value; + } + } } } diff --git a/src/ARKServerManager/Config.settings b/src/ARKServerManager/Config.settings index e4710c8e..8aa116c6 100644 --- a/src/ARKServerManager/Config.settings +++ b/src/ARKServerManager/Config.settings @@ -788,5 +788,17 @@ + + + + + https://discord.com/developers/applications + + + https://arkservermanager.freeforums.net/thread/8764/get-own-discord-bot + + + False + \ No newline at end of file diff --git a/src/ARKServerManager/Globalization/en-US/en-US.xaml b/src/ARKServerManager/Globalization/en-US/en-US.xaml index fe8119e6..97c957cf 100644 --- a/src/ARKServerManager/Globalization/en-US/en-US.xaml +++ b/src/ARKServerManager/Globalization/en-US/en-US.xaml @@ -610,7 +610,18 @@ This message will be displayed when the server shutdown has been cancelled. Show shutdown reason with ALL shutdown messages If enabled, the shutdown reason will be shown with all shutdown message; otherwise it will only be shown at the start of the server shutdown. - + + Enable Discord Bot + You will need to restart the server manager if you change any settings for the Discord Bot. + Token: + The token associated with the discord bot. + Server Id: + The id of the discord server the bot will listen to. + Prefix: + The prefix that must be used when sending a command via discord. + Get Token... + Help... + SMTP Email Settings Host: The name or IP address of the host used for SMTP transmissions. @@ -1179,6 +1190,12 @@ If enabled, the server will be restarted even if shutdown for Auto-Restarts and Auto-Updates. + + Discord Bot Details + Channel Id: + The id of the discord server channel the bot will listen to. + + Rules Enable Hardcore Mode diff --git a/src/ARKServerManager/Lib/ServerProfile.cs b/src/ARKServerManager/Lib/ServerProfile.cs index dfac1b1a..d517acb4 100644 --- a/src/ARKServerManager/Lib/ServerProfile.cs +++ b/src/ARKServerManager/Lib/ServerProfile.cs @@ -957,6 +957,16 @@ namespace ServerManagerTool.Lib } #endregion + #region Discord Bot + public static readonly DependencyProperty DiscordChannelIdProperty = DependencyProperty.Register(nameof(DiscordChannelId), typeof(string), typeof(ServerProfile), new PropertyMetadata(String.Empty)); + [DataMember] + public string DiscordChannelId + { + get { return (string)GetValue(DiscordChannelIdProperty); } + set { SetValue(DiscordChannelIdProperty, value); } + } + #endregion + #region Rules public static readonly DependencyProperty EnableHardcoreProperty = DependencyProperty.Register(nameof(EnableHardcore), typeof(bool), typeof(ServerProfile), new PropertyMetadata(false)); [IniFileEntry(IniFiles.GameUserSettings, IniSections.GUS_ServerSettings, ServerProfileCategory.Rules, "ServerHardcore")] diff --git a/src/ARKServerManager/Styles/Default.xaml b/src/ARKServerManager/Styles/Default.xaml index e2b09de0..9fecf6e6 100644 --- a/src/ARKServerManager/Styles/Default.xaml +++ b/src/ARKServerManager/Styles/Default.xaml @@ -713,5 +713,15 @@