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