diff --git a/DiscordBotPlugin/DiscordBotPlugin.csproj b/DiscordBotPlugin/DiscordBotPlugin.csproj index 4da8e41..6051f49 100644 --- a/DiscordBotPlugin/DiscordBotPlugin.csproj +++ b/DiscordBotPlugin/DiscordBotPlugin.csproj @@ -84,7 +84,7 @@ ..\packages\Microsoft.Extensions.DependencyInjection.Abstractions.6.0.0\lib\net461\Microsoft.Extensions.DependencyInjection.Abstractions.dll - H:\AMPDatastore\Instances\ConanExiles01\ModuleShared.dll + H:\AMPDatastore\Instances\Minecraft01\ModuleShared.dll False diff --git a/DiscordBotPlugin/PluginMain.cs b/DiscordBotPlugin/PluginMain.cs index 13b17de..6ebed08 100644 --- a/DiscordBotPlugin/PluginMain.cs +++ b/DiscordBotPlugin/PluginMain.cs @@ -27,87 +27,126 @@ public class PluginMain : AMPPlugin private List playerPlayTimes = new List(); public PluginMain(ILogger log, IConfigSerializer config, IPlatformInfo platform, - IRunningTasksManager taskManager, IApplicationWrapper Application, IAMPInstanceInfo AMPInstanceInfo) + IRunningTasksManager taskManager, IApplicationWrapper application, IAMPInstanceInfo AMPInstanceInfo) { - config.SaveMethod = PluginSaveMethod.KVP; - config.KVPSeparator = "="; + _config = config; this.log = log; this.platform = platform; - _settings = config.Load(AutoSave: true); //Automatically saves settings when they're changed. + _settings = config.Load(AutoSave: true); _tasks = taskManager; - application = Application; + this.application = application; aMPInstanceInfo = AMPInstanceInfo; - _config = config; + + config.SaveMethod = PluginSaveMethod.KVP; + config.KVPSeparator = "="; + _settings.SettingModified += Settings_SettingModified; log.MessageLogged += Log_MessageLogged; - IHasSimpleUserList hasSimpleUserList = application as IHasSimpleUserList; - //register join and leave events - hasSimpleUserList.UserJoins += UserJoins; - hasSimpleUserList.UserLeaves += UserLeaves; + if (application is IHasSimpleUserList hasSimpleUserList) + { + hasSimpleUserList.UserJoins += UserJoins; + hasSimpleUserList.UserLeaves += UserLeaves; + } } + /// + /// Runs on MessageLogged event + /// + /// Sender + /// Event Args private void Log_MessageLogged(object sender, LogEventArgs e) { - if (e.Level == LogLevels.Chat.ToString() && _settings.MainSettings.SendChatToDiscord && _settings.MainSettings.ChatToDiscordChannel != "") + if (e.Level == LogLevels.Chat.ToString() && + _settings.MainSettings.SendChatToDiscord && + !string.IsNullOrEmpty(_settings.MainSettings.ChatToDiscordChannel)) { - //chat message, clean and send to discord + // Clean the message to avoid code blocks and send it to Discord string clean = e.Message.Replace("`", "'"); _ = ChatMessageSend(clean); } + + if ((e.Level == LogLevels.Console.ToString() || e.Level == LogLevels.Chat.ToString()) && _settings.MainSettings.SendConsoleToDiscord && + !string.IsNullOrEmpty(_settings.MainSettings.ConsoleToDiscordChannel)) + { + // Clean the message to avoid code blocks and send it to Discord + string clean = e.Message.Replace("`", "'"); + _ = ConsoleOutputSend(clean); + } } + /// + /// Initializes the bot and assigns an instance of WebMethods to APIMethods. + /// + /// An output parameter to hold the instance of WebMethods. public override void Init(out WebMethodsBase APIMethods) { + // Create a new instance of WebMethods and assign it to APIMethods APIMethods = new WebMethods(_tasks); } + /// + /// Runs on SettingsModified event + /// + /// Sender + /// Event Args void Settings_SettingModified(object sender, SettingModifiedEventArgs e) { - //if bot setting activated, try starting it if (_settings.MainSettings.BotActive) { try { + // Start the Discord bot if it's not already running if (_client == null || _client.ConnectionState == ConnectionState.Disconnected) + { _ = ConnectDiscordAsync(_settings.MainSettings.BotToken); + } } catch (Exception exception) { - log.Error("Error with the Discord Bot : " + exception.Message); + // Log any errors that occur during bot connection + log.Error("Error with the Discord Bot: " + exception.Message); } } - - //bot deactivated, disconnect from Discord (if connected) - if (!_settings.MainSettings.BotActive) + else { - if (_client != null) + if (_client != null && _client.ConnectionState == ConnectionState.Connected) { - if (_client.ConnectionState == ConnectionState.Connected) + // Unsubscribe from events and logout from Discord if the bot is deactivated + _client.ButtonExecuted -= OnButtonPress; + _client.Log -= Log; + _client.Ready -= ClientReady; + _client.SlashCommandExecuted -= SlashCommandHandler; + _client.MessageReceived -= MessageHandler; + + try { - _client.ButtonExecuted -= OnButtonPress; - _client.Log -= Log; - _client.Ready -= ClientReady; - _client.SlashCommandExecuted -= SlashCommandHandler; - _client.MessageReceived -= MessageHandler; + // Logout from Discord _client.LogoutAsync(); } + catch (Exception exception) + { + // Log any errors that occur during logout + log.Error("Error logging out from Discord: " + exception.Message); + } } } } public override bool HasFrontendContent => false; + /// + /// Performs post-initialization actions for the bot. + /// public override void PostInit() { - - //check if the bot is turned on + // Check if the bot is turned on if (_settings.MainSettings.BotActive) { log.Info("Discord Bot Activated"); - //check if we have a bot token and attempt to connect - if (_settings.MainSettings.BotToken != null && _settings.MainSettings.BotToken != "") + // Check if we have a bot token and attempt to connect + if (!string.IsNullOrEmpty(_settings.MainSettings.BotToken)) { try { @@ -115,7 +154,8 @@ public override void PostInit() } catch (Exception exception) { - log.Error("Error with the Discord Bot : " + exception.Message); + // Log any errors that occur during bot connection + log.Error("Error with the Discord Bot: " + exception.Message); } } } @@ -126,14 +166,16 @@ public override void PostInit() /// /// Async task to handle the Discord connection and call the status check /// - /// - /// + /// Discord Bot Token + /// Task public async Task ConnectDiscordAsync(string BotToken) { DiscordSocketConfig config; + // Determine the GatewayIntents based on the chat settings if (_settings.MainSettings.SendChatToDiscord || _settings.MainSettings.SendDiscordChatToServer) { + // Include MessageContent intent if chat is sent between Discord and the server config = new DiscordSocketConfig { GatewayIntents = GatewayIntents.DirectMessages | GatewayIntents.GuildMessages | GatewayIntents.Guilds | GatewayIntents.MessageContent }; } else @@ -141,43 +183,56 @@ public async Task ConnectDiscordAsync(string BotToken) config = new DiscordSocketConfig { GatewayIntents = GatewayIntents.DirectMessages | GatewayIntents.GuildMessages | GatewayIntents.Guilds }; } - - - //init Discord client & command service + // Initialize Discord client with the specified configuration _client = new DiscordSocketClient(config); - //attach logs and events + // Attach event handlers for logs and events _client.Log += Log; _client.ButtonExecuted += OnButtonPress; _client.Ready += ClientReady; _client.SlashCommandExecuted += SlashCommandHandler; - if(_settings.MainSettings.SendChatToDiscord || _settings.MainSettings.SendDiscordChatToServer) + if (_settings.MainSettings.SendChatToDiscord || _settings.MainSettings.SendDiscordChatToServer) _client.MessageReceived += MessageHandler; + // Login and start the Discord client await _client.LoginAsync(TokenType.Bot, BotToken); await _client.StartAsync(); + // Set the bot's status await SetStatus(); // Block this task until the program is closed or bot is stopped. await Task.Delay(-1); } + /// + /// Logs a message with an information level. + /// + /// The message to be logged. + /// A task representing the asynchronous operation. private Task Log(LogMessage msg) { + // Log the message as an information level message log.Info(msg.ToString()); + + // Return a completed task to fulfill the method signature return Task.CompletedTask; } - - + /// + /// Send a command to the AMP instance + /// + /// Command to send to the server + /// Task private Task SendConsoleCommand(SocketSlashCommand msg) { try { + // Initialize the command string string command = ""; - //get the command to be sent - if(_settings.MainSettings.RemoveBotName) + + // Get the command to be sent based on the bot name removal setting + if (_settings.MainSettings.RemoveBotName) { command = msg.Data.Options.First().Value.ToString(); } @@ -186,42 +241,68 @@ private Task SendConsoleCommand(SocketSlashCommand msg) command = msg.Data.Options.First().Options.First().Value.ToString(); } - //send the command to the instance + // Send the command to the AMP instance IHasWriteableConsole writeableConsole = application as IHasWriteableConsole; writeableConsole.WriteLine(command); + + // Return a completed task to fulfill the method signature return Task.CompletedTask; } catch (Exception exception) { + // Log any errors that occur during command sending log.Error("Cannot send command: " + exception.Message); + + // Return a completed task to fulfill the method signature return Task.CompletedTask; } } + /// + /// Send a chat message to the AMP instance, only for Minecraft for now + /// + /// Discord name of the sender + /// Message to send + /// Task private Task SendChatCommand(string author, string msg) { try { - //send the command to the instance + // Construct the command to send + string command = "say [" + author + "] " + msg; + + // Send the chat command to the AMP instance IHasWriteableConsole writeableConsole = application as IHasWriteableConsole; - writeableConsole.WriteLine("say [" + author + "] " + msg); + writeableConsole.WriteLine(command); + + // Return a completed task to fulfill the method signature return Task.CompletedTask; } catch (Exception exception) { + // Log any errors that occur during chat message sending log.Error("Cannot send chat message: " + exception.Message); + + // Return a completed task to fulfill the method signature return Task.CompletedTask; } } + /// + /// Task to get current server info and create or update an embeded message + /// + /// Embed already exists? + /// Command from Discord + /// Should the embed be buttonless? + /// private async Task GetServerInfo(bool updateExisting, SocketSlashCommand msg, bool Buttonless) { + //if bot isn't connected, stop any further action if (_client.ConnectionState != ConnectionState.Connected) return; - //cast to get player count / info + //get count of players online and maximum slots IHasSimpleUserList hasSimpleUserList = application as IHasSimpleUserList; - var onlinePlayers = hasSimpleUserList.Users.Count; var maximumPlayers = hasSimpleUserList.MaxUsers; @@ -232,6 +313,7 @@ private async Task GetServerInfo(bool updateExisting, SocketSlashCommand msg, bo ThumbnailUrl = _settings.MainSettings.GameImageURL }; + //if a custom colour is set use it, otherwise use default if (!_settings.ColourSettings.InfoPanelColour.Equals("")) { embed.Color = GetColour("Info", _settings.ColourSettings.InfoPanelColour); @@ -241,12 +323,12 @@ private async Task GetServerInfo(bool updateExisting, SocketSlashCommand msg, bo embed.Color = Color.DarkGrey; } - //server online + //if server is online if (application.State == ApplicationState.Ready) { embed.AddField("Server Status", ":white_check_mark: " + GetApplicationStateString(), false); } - //server off or errored + //if server is off or errored else if (application.State == ApplicationState.Failed || application.State == ApplicationState.Stopped) { embed.AddField("Server Status", ":no_entry: " + GetApplicationStateString(), false); @@ -257,15 +339,25 @@ private async Task GetServerInfo(bool updateExisting, SocketSlashCommand msg, bo embed.AddField("Server Status", ":hourglass: " + GetApplicationStateString(), false); } + //set server name field embed.AddField("Server Name", "`" + _settings.MainSettings.ServerDisplayName + "`", true); + + //set server IP field embed.AddField("Server IP", "`" + _settings.MainSettings.ServerConnectionURL + "`", true); + + //set password field if populated in setttings if (_settings.MainSettings.ServerPassword != "") { embed.AddField("Server Password", "`" + _settings.MainSettings.ServerPassword + "`", true); } + + //set CPU usage field embed.AddField("CPU Usage", application.GetCPUUsage() + "%", true); + + //set mem usage field embed.AddField("Memory Usage", application.GetRAMUsage() + "MB", true); + //if server is online, get the uptime info and set the field accordingly if (application.State == ApplicationState.Ready) { TimeSpan uptime = DateTime.Now.Subtract(application.StartTime); @@ -299,6 +391,7 @@ private async Task GetServerInfo(bool updateExisting, SocketSlashCommand msg, bo } } + //set modpack url field if populated in settings if (_settings.MainSettings.ModpackURL != "") { embed.AddField("Server Mod Pack", _settings.MainSettings.ModpackURL, false); @@ -311,18 +404,25 @@ private async Task GetServerInfo(bool updateExisting, SocketSlashCommand msg, bo embed.AddField("Top 5 Players by Play Time", leaderboard, false); } + //if user has added an additonal embed field, add it if(_settings.MainSettings.AdditionalEmbedFieldTitle.Length > 0) { embed.AddField(_settings.MainSettings.AdditionalEmbedFieldTitle, _settings.MainSettings.AdditionalEmbedFieldText); } + //add the footer embed.WithFooter(_settings.MainSettings.BotTagline); + + //set the update time embed.WithCurrentTimestamp(); + //set the thumbnail embed.WithThumbnailUrl(_settings.MainSettings.GameImageURL); - //add buttons + //create new component builder for buttons var builder = new ComponentBuilder(); + + //if start button required, add it and set the state depending on the server status (disabled if ready or starting or installing/updating) if (_settings.MainSettings.ShowStartButton) if (application.State == ApplicationState.Ready || application.State == ApplicationState.Starting || application.State == ApplicationState.Installing) { @@ -333,6 +433,7 @@ private async Task GetServerInfo(bool updateExisting, SocketSlashCommand msg, bo builder.WithButton("Start", "start-server-" + aMPInstanceInfo.InstanceId, ButtonStyle.Success, disabled: false); } + //if stop button required, add it and set the state depending on the server status (disabled if stopped or failed) if (_settings.MainSettings.ShowStopButton) if (application.State == ApplicationState.Stopped || application.State == ApplicationState.Failed) { @@ -343,6 +444,7 @@ private async Task GetServerInfo(bool updateExisting, SocketSlashCommand msg, bo builder.WithButton("Stop", "stop-server-" + aMPInstanceInfo.InstanceId, ButtonStyle.Danger, disabled: false); } + //if restart button required, add it and set the state depending on the server status (disabled if stopper or failed) if (_settings.MainSettings.ShowRestartButton) if (application.State == ApplicationState.Stopped || application.State == ApplicationState.Failed) { @@ -353,6 +455,7 @@ private async Task GetServerInfo(bool updateExisting, SocketSlashCommand msg, bo builder.WithButton("Restart", "restart-server-" + aMPInstanceInfo.InstanceId, ButtonStyle.Danger, disabled: false); } + //if kill button required, add it and set the state depending on the server status (disabled if stopped or failed) if (_settings.MainSettings.ShowKillButton) if (application.State == ApplicationState.Stopped || application.State == ApplicationState.Failed) { @@ -363,6 +466,7 @@ private async Task GetServerInfo(bool updateExisting, SocketSlashCommand msg, bo builder.WithButton("Kill", "kill-server-" + aMPInstanceInfo.InstanceId, ButtonStyle.Danger, disabled: false); } + //if update button required, add it and set the state depending on the server status (disabled if installing/updating) if (_settings.MainSettings.ShowUpdateButton) if (application.State == ApplicationState.Installing) { @@ -373,23 +477,27 @@ private async Task GetServerInfo(bool updateExisting, SocketSlashCommand msg, bo builder.WithButton("Update", "update-server-" + aMPInstanceInfo.InstanceId, ButtonStyle.Primary, disabled: false); } + //if manage button required, add it if (_settings.MainSettings.ShowManageButton) builder.WithButton("Manage", "manage-server-" + aMPInstanceInfo.InstanceId, ButtonStyle.Primary); - //if updating an existing message + //if the message already exists, try to update it if (updateExisting) { + //cycle through each stored embed message ID (could be multiple across different channels/servers) foreach (string details in _settings.MainSettings.InfoMessageDetails) { try { string[] split = details.Split('-'); + //get the IUserMessage var existingMsg = await _client .GetGuild(Convert.ToUInt64(split[0])) .GetTextChannel(Convert.ToUInt64(split[1])) .GetMessageAsync(Convert.ToUInt64(split[2])) as IUserMessage; + //if it's not null then continue if (existingMsg != null) { await existingMsg.ModifyAsync(x => @@ -397,6 +505,7 @@ await existingMsg.ModifyAsync(x => x.Embed = embed.Build(); if (split.Length > 3) { + //check if it's configured as a buttonless panel if (split[3].ToString().Equals("True")) { //buttonless panel - do not build buttons @@ -426,13 +535,14 @@ await existingMsg.ModifyAsync(x => } } } + //create a new embedded message and store the ID for updating later on else { var chnl = msg.Channel as SocketGuildChannel; var guild = chnl.Guild.Id; var channelID = msg.Channel.Id; - //post bot reply + //create the embed according to the request if (Buttonless) { var message = await _client.GetGuild(guild).GetTextChannel(channelID).SendMessageAsync(embed: embed.Build()); @@ -446,85 +556,94 @@ await existingMsg.ModifyAsync(x => _settings.MainSettings.InfoMessageDetails.Add(guild.ToString() + "-" + channelID.ToString() + "-" + message.Id.ToString() + "-" + Buttonless); } + //save the newly added info message details _config.Save(_settings); } } + /// + /// Show play time on the server + /// + /// Command from Discord + /// private async Task ShowPlayerPlayTime(SocketSlashCommand msg) { - //build bot response + // Build the bot response var embed = new EmbedBuilder { Title = "Play Time Leaderboard", - ThumbnailUrl = _settings.MainSettings.GameImageURL + ThumbnailUrl = _settings.MainSettings.GameImageURL, + Description = GetPlayTimeLeaderBoard(15, false, null, false), + Color = !string.IsNullOrEmpty(_settings.ColourSettings.PlaytimeLeaderboardColour) + ? GetColour("Leaderboard", _settings.ColourSettings.PlaytimeLeaderboardColour) + : Color.DarkGrey }; - string leaderboard = GetPlayTimeLeaderBoard(15, false, null, false); - - embed.Description = leaderboard; - - embed.WithFooter(_settings.MainSettings.BotTagline); - embed.WithCurrentTimestamp(); - - if (!_settings.ColourSettings.PlaytimeLeaderboardColour.Equals("")) - { - embed.Color = GetColour("Leaderboard", _settings.ColourSettings.PlaytimeLeaderboardColour); - } - else - { - embed.Color = Color.DarkGrey; - } + // Set the footer and the current timestamp + embed.WithFooter(_settings.MainSettings.BotTagline) + .WithCurrentTimestamp(); - //get guild - var chnl = msg.Channel as SocketGuildChannel; - var guild = chnl.Guild.Id; - var channelID = msg.Channel.Id; + // Get the guild and channel IDs + var guildId = (msg.Channel as SocketGuildChannel)?.Guild.Id; + var channelId = msg.Channel.Id; - //post leaderboard - await _client.GetGuild(guild).GetTextChannel(channelID).SendMessageAsync(embed: embed.Build()); + // Post the leaderboard in the specified channel + await _client.GetGuild(guildId.Value)?.GetTextChannel(channelId)?.SendMessageAsync(embed: embed.Build()); } - //Looping task to update bot status/presence + /// + /// Looping task to update bot status/presence + /// + /// public async Task SetStatus() { + // While the bot is active, update its status while (_settings.MainSettings.BotActive) { try { UserStatus status; + // If the server is stopped or in a failed state, set the presence to DoNotDisturb if (application.State == ApplicationState.Stopped || application.State == ApplicationState.Failed) { status = UserStatus.DoNotDisturb; - //if there are still players listed in the timer, remove them + // If there are still players listed in the timer, remove them if (playerPlayTimes.Count != 0) ClearAllPlayTimes(); } + // If the server is running, set presence to Online else if (application.State == ApplicationState.Ready) { status = UserStatus.Online; } + // For everything else, set to Idle else { status = UserStatus.Idle; - //if there are still players listed in the timer, remove them + // If there are still players listed in the timer, remove them if (playerPlayTimes.Count != 0) ClearAllPlayTimes(); } + // Get the current user and max user count IHasSimpleUserList hasSimpleUserList = application as IHasSimpleUserList; - var onlinePlayers = hasSimpleUserList.Users.Count; var maximumPlayers = hasSimpleUserList.MaxUsers; + + // Get the CPU usage and memory usage var cpuUsage = application.GetCPUUsage(); + var cpuUsageString = cpuUsage + "%"; var memUsage = application.GetRAMUsage(); + + // Get the name of the instance var instanceName = platform.PlatformName; - var cpuUsageString = cpuUsage + "%"; log.Debug("Server Status: " + application.State + " || Players: " + onlinePlayers + "/" + maximumPlayers + " || CPU: " + application.GetCPUUsage() + "% || Memory: " + application.GetPhysicalRAMUsage() + "MB, Bot Connection Status: " + _client.ConnectionState); + // Set the presence/activity based on the server state if (application.State == ApplicationState.Ready) { await _client.SetGameAsync(OnlineBotPresenceString(onlinePlayers, maximumPlayers), null, ActivityType.Playing); @@ -534,14 +653,13 @@ public async Task SetStatus() await _client.SetGameAsync(application.State.ToString(), null, ActivityType.Playing); } - await _client.SetStatusAsync(status); - //update the embed if it exists - if (_settings.MainSettings.InfoMessageDetails != null) - if (_settings.MainSettings.InfoMessageDetails.Count > 0) - _ = GetServerInfo(true, null, false); - + // Update the embed if it exists + if (_settings.MainSettings.InfoMessageDetails != null && _settings.MainSettings.InfoMessageDetails.Count > 0) + { + _ = GetServerInfo(true, null, false); + } } catch (System.Net.WebException exception) { @@ -550,82 +668,133 @@ public async Task SetStatus() log.Info("Exception: " + exception.Message); } + // Loop the task according to the bot refresh interval setting await Task.Delay(_settings.MainSettings.BotRefreshInterval * 1000); } } + /// + /// Task that runs when a button is pressed on the info panel + /// + /// Component info of the button that was pressed + /// private async Task OnButtonPress(SocketMessageComponent arg) { log.Debug("Button pressed: " + arg.Data.CustomId.ToString()); - //temp bool for permission check + // Check if the user that pressed the button has permission bool hasServerPermission = false; - //check if the user that pressed the button has permission - _client.PurgeUserCache(); //try to clear cache so we can get the latest roles + // Check if the user has the appropriate role if (arg.User is SocketGuildUser user) - //The user has the permission if either RestrictFunctions is turned off, or if they are part of the appropriate role. + { + _client.PurgeUserCache(); // Try to clear cache so we can get the latest roles hasServerPermission = !_settings.MainSettings.RestrictFunctions || user.Roles.Any(r => r.Name == _settings.MainSettings.DiscordRole); + } if (!hasServerPermission) { - //no permission, mark as responded and get out of here + // No permission, mark as responded and exit the method await arg.DeferAsync(); return; } - if (arg.Data.CustomId.Equals("start-server-" + aMPInstanceInfo.InstanceId)) - { - application.Start(); - await ButtonResonse("Start", arg); - } - if (arg.Data.CustomId.Equals("stop-server-" + aMPInstanceInfo.InstanceId)) - { - application.Stop(); - await ButtonResonse("Stop", arg); - } - if (arg.Data.CustomId.Equals("restart-server-" + aMPInstanceInfo.InstanceId)) - { - application.Restart(); - await ButtonResonse("Restart", arg); - } - if (arg.Data.CustomId.Equals("kill-server-" + aMPInstanceInfo.InstanceId)) - { - application.Kill(); - await ButtonResonse("Kill", arg); - } - if (arg.Data.CustomId.Equals("update-server-" + aMPInstanceInfo.InstanceId)) - { - application.Update(); - await ButtonResonse("Update", arg); + // Get the button ID without the instance ID suffix + var buttonId = arg.Data.CustomId.Replace("-" + aMPInstanceInfo.InstanceId, ""); + + // Perform the appropriate action based on the button ID + switch (buttonId) + { + case "start-server": + application.Start(); + break; + case "stop-server": + application.Stop(); + break; + case "restart-server": + application.Restart(); + break; + case "kill-server": + application.Kill(); + break; + case "update-server": + application.Update(); + break; + case "manage-server": + await ManageServer(arg); + break; + default: + // Invalid button ID, exit the method + return; } - if (arg.Data.CustomId.Equals("manage-server-" + aMPInstanceInfo.InstanceId)) + + // Capitalize the first letter of the button response + var capitalizedButtonResponse = char.ToUpper(buttonId[0]) + buttonId.Substring(1).Replace("-server", ""); + + // Send button response + await ButtonResponse(capitalizedButtonResponse, arg); + } + + /// + /// Sends a chat message to the specified text channel in each guild the bot is a member of. + /// + /// The message to send. + /// A task representing the asynchronous operation. + private async Task ChatMessageSend(string Message) + { + // Get all guilds the bot is a member of + var guilds = _client.Guilds; + + // Iterate over each guild + foreach (SocketGuild guild in guilds) { - await ManageServer(arg); - await ButtonResonse("Manage", arg); + // Find the text channel with the specified name + var channel = guild.TextChannels.FirstOrDefault(x => x.Name == _settings.MainSettings.ChatToDiscordChannel); + + if (channel != null) + { + // Send the message to the channel + await _client.GetGuild(guild.Id).GetTextChannel(channel.Id).SendMessageAsync("`" + Message + "`"); + } } } - private async Task ChatMessageSend(string Message) + /// + /// Sends console output to the specified text channel in each guild the bot is a member of. + /// + /// The message to send. + /// A task representing the asynchronous operation. + private async Task ConsoleOutputSend(string Message) { - var _guild = _client.Guilds; - foreach(SocketGuild guild in _guild) + // Get all guilds the bot is a member of + var guilds = _client.Guilds; + + // Iterate over each guild + foreach (SocketGuild guild in guilds) { - var _channel = guild.TextChannels.FirstOrDefault(x => x.Name == _settings.MainSettings.ChatToDiscordChannel); - if(_channel != null) + // Find the text channel with the specified name + var channel = guild.TextChannels.FirstOrDefault(x => x.Name == _settings.MainSettings.ConsoleToDiscordChannel); + + if (channel != null) { - await _client.GetGuild(guild.Id).GetTextChannel(_channel.Id).SendMessageAsync("`" + Message + "`"); + // Send the message to the channel + await _client.GetGuild(guild.Id).GetTextChannel(channel.Id).SendMessageAsync("`" + Message + "`"); } } } - private async Task ButtonResonse(string Command, SocketMessageComponent arg) + /// + /// Handles button response and logs the command if enabled in settings. + /// + /// Command received from the button. + /// SocketMessageComponent object containing information about the button click. + private async Task ButtonResponse(string Command, SocketMessageComponent arg) { - //only log if option is enabled + // Only log if option is enabled if (_settings.MainSettings.LogButtonsAndCommands) { - //build bot response var embed = new EmbedBuilder(); + if (Command == "Manage") { embed.Title = "Manage Request"; @@ -634,94 +803,57 @@ private async Task ButtonResonse(string Command, SocketMessageComponent arg) else { embed.Title = "Server Command Sent"; - embed.Description = Command + " command has been sent to the " + application.ApplicationName + " server."; + embed.Description = $"{Command} command has been sent to the {application.ApplicationName} server."; } - //start command + // Start command if (Command.Equals("Start")) { - if (!_settings.ColourSettings.ServerStartColour.Equals("")) - { - embed.Color = GetColour("Start", _settings.ColourSettings.ServerStartColour); - } - else - { - embed.Color = Color.Green; - } + embed.Color = !string.IsNullOrEmpty(_settings.ColourSettings.ServerStartColour) + ? GetColour("Start", _settings.ColourSettings.ServerStartColour) + : Color.Green; } - - //stop command - if (Command.Equals("Stop")) + // Stop command + else if (Command.Equals("Stop")) { - if (!_settings.ColourSettings.ServerStopColour.Equals("")) - { - embed.Color = GetColour("Stop", _settings.ColourSettings.ServerStopColour); - } - else - { - embed.Color = Color.Red; - } + embed.Color = !string.IsNullOrEmpty(_settings.ColourSettings.ServerStopColour) + ? GetColour("Stop", _settings.ColourSettings.ServerStopColour) + : Color.Red; } - - //restart command - if (Command.Equals("Restart")) + // Restart command + else if (Command.Equals("Restart")) { - if (!_settings.ColourSettings.ServerRestartColour.Equals("")) - { - embed.Color = GetColour("Restart", _settings.ColourSettings.ServerRestartColour); - } - else - { - embed.Color = Color.Orange; - } + embed.Color = !string.IsNullOrEmpty(_settings.ColourSettings.ServerRestartColour) + ? GetColour("Restart", _settings.ColourSettings.ServerRestartColour) + : Color.Orange; } - - //kill command - if (Command.Equals("Kill")) + // Kill command + else if (Command.Equals("Kill")) { - if (!_settings.ColourSettings.ServerKillColour.Equals("")) - { - embed.Color = GetColour("Kill", _settings.ColourSettings.ServerKillColour); - } - else - { - embed.Color = Color.Red; - } + embed.Color = !string.IsNullOrEmpty(_settings.ColourSettings.ServerKillColour) + ? GetColour("Kill", _settings.ColourSettings.ServerKillColour) + : Color.Red; } - - //update command - if (Command.Equals("Update")) + // Update command + else if (Command.Equals("Update")) { - if (!_settings.ColourSettings.ServerUpdateColour.Equals("")) - { - embed.Color = GetColour("Update", _settings.ColourSettings.ServerUpdateColour); - } - else - { - embed.Color = Color.Blue; - } + embed.Color = !string.IsNullOrEmpty(_settings.ColourSettings.ServerUpdateColour) + ? GetColour("Update", _settings.ColourSettings.ServerUpdateColour) + : Color.Blue; } - - //manage command - if (Command.Equals("Manage")) + // Manage command + else if (Command.Equals("Manage")) { - if (!_settings.ColourSettings.ManageLinkColour.Equals("")) - { - embed.Color = GetColour("Manage", _settings.ColourSettings.ManageLinkColour); - } - else - { - embed.Color = Color.Blue; - } + embed.Color = !string.IsNullOrEmpty(_settings.ColourSettings.ManageLinkColour) + ? GetColour("Manage", _settings.ColourSettings.ManageLinkColour) + : Color.Blue; } - //embed.Color = Color.LightGrey; embed.ThumbnailUrl = _settings.MainSettings.GameImageURL; embed.AddField("Requested by", arg.User.Mention, true); embed.WithFooter(_settings.MainSettings.BotTagline); embed.WithCurrentTimestamp(); - //get guild var chnl = arg.Message.Channel as SocketGuildChannel; var guild = chnl.Guild.Id; var logChannel = GetEventChannel(guild, _settings.MainSettings.ButtonResponseChannel); @@ -730,78 +862,80 @@ private async Task ButtonResonse(string Command, SocketMessageComponent arg) if (logChannel != null) channelID = logChannel.Id; - log.Debug("Guild: " + guild + " || Channel: " + channelID); + log.Debug($"Guild: {guild} || Channel: {channelID}"); await _client.GetGuild(guild).GetTextChannel(channelID).SendMessageAsync(embed: embed.Build()); } + await arg.DeferAsync(); } + /// + /// Retrieves the color based on the given command and hexadecimal color code. + /// + /// Command associated with the color. + /// Hexadecimal color code. + /// The Color object corresponding to the command and color code. private Color GetColour(string command, string hexColour) { try { - //remove # if it's in the string + // Remove '#' if it's present in the string string tmp = hexColour.Replace("#", ""); - //convert to uint + // Convert to uint uint colourCode = uint.Parse(tmp, System.Globalization.NumberStyles.HexNumber); return new Color(colourCode); } catch { - //couldn't convert to uint, log it and revert to default colour - log.Info("Colour code for " + command + " is invalid, reverting to default"); - - if (command.Equals("Info")) - { - return Color.DarkGrey; - } + // Log an error message when the color code is invalid and revert to default color - if (command.Equals("Start") || command.Equals("PlayerJoin")) - { - return Color.Green; - } + log.Info($"Colour code for {command} is invalid, reverting to default"); - if (command.Equals("Stop") || command.Equals("Kill") || command.Equals("PlayerLeave")) + // Return default colors based on the command + switch (command) { - return Color.Red; - } - - if (command.Equals("Restart")) - { - return Color.Orange; - } - - if (command.Equals("Update") || command.Equals("Manage")) - { - return Color.Blue; - } - - if (command.Equals("Console")) - { - return Color.DarkGreen; - } - - if (command.Equals("Leaderboard")) - { - return Color.DarkGrey; + case "Info": + return Color.DarkGrey; + case "Start": + case "PlayerJoin": + return Color.Green; + case "Stop": + case "Kill": + case "PlayerLeave": + return Color.Red; + case "Restart": + return Color.Orange; + case "Update": + case "Manage": + return Color.Blue; + case "Console": + return Color.DarkGreen; + case "Leaderboard": + return Color.DarkGrey; } } return Color.DarkerGrey; } - private async Task CommandResponse(string Command, SocketSlashCommand arg) + /// + /// Sends a command response with an embed message. + /// + /// The command received. + /// The received command arguments. + private async Task CommandResponse(string command, SocketSlashCommand arg) { - //only log if option is enabled + // Only log if option is enabled if (!_settings.MainSettings.LogButtonsAndCommands) return; - //build bot response var embed = new EmbedBuilder(); - if (Command == "Manage") + + // Set the title and description of the embed based on the command + if (command == "Manage") { embed.Title = "Manage Request"; embed.Description = "Manage URL Request Received"; @@ -809,107 +943,58 @@ private async Task CommandResponse(string Command, SocketSlashCommand arg) else { embed.Title = "Server Command Sent"; - embed.Description = Command + " command has been sent to the " + application.ApplicationName + " server."; + embed.Description = $"{command} command has been sent to the {application.ApplicationName} server."; } - //start command - if (Command.Equals("Start Server")) + // Set the embed color based on the command + if (command.Equals("Start Server")) { - if (!_settings.ColourSettings.ServerStartColour.Equals("")) - { - embed.Color = GetColour("Start", _settings.ColourSettings.ServerStartColour); - } - else - { + embed.Color = GetColour("Start", _settings.ColourSettings.ServerStartColour); + if (embed.Color == null) embed.Color = Color.Green; - } } - - //stop command - if (Command.Equals("Stop Server")) + else if (command.Equals("Stop Server")) { - if (!_settings.ColourSettings.ServerStopColour.Equals("")) - { - embed.Color = GetColour("Stop", _settings.ColourSettings.ServerStopColour); - } - else - { + embed.Color = GetColour("Stop", _settings.ColourSettings.ServerStopColour); + if (embed.Color == null) embed.Color = Color.Red; - } } - - //restart command - if (Command.Equals("Restart Server")) + else if (command.Equals("Restart Server")) { - if (!_settings.ColourSettings.ServerRestartColour.Equals("")) - { - embed.Color = GetColour("Restart", _settings.ColourSettings.ServerRestartColour); - } - else - { + embed.Color = GetColour("Restart", _settings.ColourSettings.ServerRestartColour); + if (embed.Color == null) embed.Color = Color.Orange; - } } - - //kill command - if (Command.Equals("Kill Server")) + else if (command.Equals("Kill Server")) { - if (!_settings.ColourSettings.ServerKillColour.Equals("")) - { - embed.Color = GetColour("Kill", _settings.ColourSettings.ServerKillColour); - } - else - { + embed.Color = GetColour("Kill", _settings.ColourSettings.ServerKillColour); + if (embed.Color == null) embed.Color = Color.Red; - } } - - //update command - if (Command.Equals("Update Server")) + else if (command.Equals("Update Server")) { - if (!_settings.ColourSettings.ServerUpdateColour.Equals("")) - { - embed.Color = GetColour("Update", _settings.ColourSettings.ServerUpdateColour); - } - else - { + embed.Color = GetColour("Update", _settings.ColourSettings.ServerUpdateColour); + if (embed.Color == null) embed.Color = Color.Blue; - } } - - //manage command - if (Command.Equals("Manage Server")) + else if (command.Equals("Manage Server")) { - if (!_settings.ColourSettings.ManageLinkColour.Equals("")) - { - embed.Color = GetColour("Manage", _settings.ColourSettings.ManageLinkColour); - } - else - { + embed.Color = GetColour("Manage", _settings.ColourSettings.ManageLinkColour); + if (embed.Color == null) embed.Color = Color.Blue; - } } - - //console command - if (Command.Contains("console")) + else if (command.Contains("console")) { - if (!_settings.ColourSettings.ConsoleCommandColour.Equals("")) - { - embed.Color = GetColour("Console", _settings.ColourSettings.ConsoleCommandColour); - } - else - { + embed.Color = GetColour("Console", _settings.ColourSettings.ConsoleCommandColour); + if (embed.Color == null) embed.Color = Color.DarkGreen; - } } - embed.ThumbnailUrl = _settings.MainSettings.GameImageURL; embed.AddField("Requested by", arg.User.Mention, true); embed.WithFooter(_settings.MainSettings.BotTagline); embed.WithCurrentTimestamp(); - //get guild var chnl = arg.Channel as SocketGuildChannel; var guild = chnl.Guild.Id; var logChannel = GetEventChannel(guild, _settings.MainSettings.ButtonResponseChannel); @@ -918,19 +1003,25 @@ private async Task CommandResponse(string Command, SocketSlashCommand arg) if (logChannel != null) channelID = logChannel.Id; - log.Debug("Guild: " + guild + " || Channel: " + channelID); + log.Debug($"Guild: {guild} || Channel: {channelID}"); await _client.GetGuild(guild).GetTextChannel(channelID).SendMessageAsync(embed: embed.Build()); } + /// + /// Event handler for when a user joins the server. + /// + /// The event sender. + /// The event arguments containing user information. private async void UserJoins(object sender, UserEventArgs args) { - //check if player is already in the list, if so remove it - shouldn't be there at this point + // Remove the player from the list if they are already present playerPlayTimes.RemoveAll(p => p.PlayerName == args.User.Name); - //log jointime for player + // Log the join time for the player playerPlayTimes.Add(new PlayerPlayTime() { PlayerName = args.User.Name, JoinTime = DateTime.Now }); + // Initialize play time and last seen information for the user if it doesn't exist if (!_settings.MainSettings.PlayTime.ContainsKey(args.User.Name)) { _settings.MainSettings.PlayTime.Add(args.User.Name, TimeSpan.Zero); @@ -938,6 +1029,7 @@ private async void UserJoins(object sender, UserEventArgs args) _config.Save(_settings); } + // Check if posting player events is disabled if (!_settings.MainSettings.PostPlayerEvents) return; @@ -947,27 +1039,28 @@ private async void UserJoins(object sender, UserEventArgs args) var eventChannel = GetEventChannel(guildID, _settings.MainSettings.PostPlayerEventsChannel); if (eventChannel == null) - break; //doesn't exist so stop here + break; // Event channel doesn't exist, stop processing string userName = args.User.Name; - //build bot message + // Build the embed message for the event var embed = new EmbedBuilder { Title = "Server Event", ThumbnailUrl = _settings.MainSettings.GameImageURL }; - if (userName != "") + if (string.IsNullOrEmpty(userName)) { - embed.Description = userName + " joined the " + application.ApplicationName + " server."; + embed.Description = "A player joined the " + application.ApplicationName + " server."; } else { - embed.Description = "A player joined the " + application.ApplicationName + " server."; + embed.Description = userName + " joined the " + application.ApplicationName + " server."; } - if (!_settings.ColourSettings.ServerPlayerJoinEventColour.Equals("")) + // Set the embed color based on the configuration + if (!string.IsNullOrEmpty(_settings.ColourSettings.ServerPlayerJoinEventColour)) { embed.Color = GetColour("PlayerJoin", _settings.ColourSettings.ServerPlayerJoinEventColour); } @@ -982,34 +1075,39 @@ private async void UserJoins(object sender, UserEventArgs args) } } + /// + /// Event handler for when a user leaves the server. + /// + /// The event sender. + /// The event arguments containing user information. private async void UserLeaves(object sender, UserEventArgs args) { try { - //add leavetime for player + // Add leave time for the player playerPlayTimes.Find(p => p.PlayerName == args.User.Name).LeaveTime = DateTime.Now; - //check entry for player, if not there add new entry + // Check if the player's entry exists in the playtime dictionary, if not, add a new entry if (!_settings.MainSettings.PlayTime.ContainsKey(args.User.Name)) - _settings.MainSettings.PlayTime.Add(args.User.Name, new TimeSpan(0)); + _settings.MainSettings.PlayTime.Add(args.User.Name, TimeSpan.Zero); + // Calculate the session playtime for the player TimeSpan sessionPlayTime = playerPlayTimes.Find(p => p.PlayerName == args.User.Name).LeaveTime - playerPlayTimes.Find(p => p.PlayerName == args.User.Name).JoinTime; - //update main playtime list + // Update the main playtime list _settings.MainSettings.PlayTime[args.User.Name] += sessionPlayTime; _settings.MainSettings.LastSeen[args.User.Name] = DateTime.Now; _config.Save(_settings); - //remove from 'live' list + // Remove the player from the 'live' list playerPlayTimes.RemoveAll(p => p.PlayerName == args.User.Name); - } catch (Exception exception) { log.Error(exception.Message); } - + // Check if posting player events is disabled if (!_settings.MainSettings.PostPlayerEvents) return; @@ -1018,27 +1116,28 @@ private async void UserLeaves(object sender, UserEventArgs args) var guildID = socketGuild.Id; var eventChannel = GetEventChannel(guildID, _settings.MainSettings.PostPlayerEventsChannel); if (eventChannel == null) - return; //doesn't exist so stop here + return; // Event channel doesn't exist, stop processing string userName = args.User.Name; - //build bot message + // Build the embed message for the event var embed = new EmbedBuilder { Title = "Server Event", ThumbnailUrl = _settings.MainSettings.GameImageURL }; - if (userName != "") + if (string.IsNullOrEmpty(userName)) { - embed.Description = userName + " left the " + application.ApplicationName + " server."; + embed.Description = "A player left the " + application.ApplicationName + " server."; } else { - embed.Description = "A player left the " + application.ApplicationName + " server."; + embed.Description = userName + " left the " + application.ApplicationName + " server."; } - if (!_settings.ColourSettings.ServerPlayerLeaveEventColour.Equals("")) + // Set the embed color based on the configuration + if (!string.IsNullOrEmpty(_settings.ColourSettings.ServerPlayerLeaveEventColour)) { embed.Color = GetColour("PlayerLeave", _settings.ColourSettings.ServerPlayerLeaveEventColour); } @@ -1053,64 +1152,102 @@ private async void UserLeaves(object sender, UserEventArgs args) } } + /// + /// Manages the server by sending a private message to the user with a link to the management panel. + /// + /// The SocketMessageComponent argument. private async Task ManageServer(SocketMessageComponent arg) { var builder = new ComponentBuilder(); string managementProtocol = "http://"; + + // Check if SSL is enabled in the main settings and update the managementProtocol accordingly if (_settings.MainSettings.ManagementURLSSL) + { managementProtocol = "https://"; - builder.WithButton("Manage Server", style: ButtonStyle.Link, url: managementProtocol + _settings.MainSettings.ManagementURL + "/?instance=" + aMPInstanceInfo.InstanceId); + } + + // Build the button with the management panel link using the appropriate protocol and instance ID + string managementPanelLink = $"{managementProtocol}{_settings.MainSettings.ManagementURL}/?instance={aMPInstanceInfo.InstanceId}"; + builder.WithButton("Manage Server", style: ButtonStyle.Link, url: managementPanelLink); + + // Send a private message to the user with the link to the management panel await arg.User.SendMessageAsync("Link to management panel:", components: builder.Build()); } + /// + /// Gets the string representation of the application state, with the option to use a replacement value if available. + /// + /// The string representation of the application state. private string GetApplicationStateString() { - //if replacement value exists, return it + // Check if a replacement value exists for the current application state if (_settings.MainSettings.ChangeStatus.ContainsKey(application.State.ToString())) + { + // Return the replacement value return _settings.MainSettings.ChangeStatus[application.State.ToString()]; + } - //no replacement exists so return the default value + // No replacement value exists, so return the default value (the application state as string) return application.State.ToString(); } + /// + /// Generates the string representation of the bot's online presence based on the number of online players and maximum players. + /// + /// The number of online players. + /// The maximum number of players. + /// The string representation of the bot's online presence. private string OnlineBotPresenceString(int onlinePlayers, int maximumPlayers) { - //if valid player count and no custom value + // Check if there is a valid player count and no custom value if (_settings.MainSettings.OnlineBotPresence == "" && _settings.MainSettings.ValidPlayerCount) - return onlinePlayers + "/" + maximumPlayers + " players"; + { + // Return the default representation of online players and maximum players + return $"{onlinePlayers}/{maximumPlayers} players"; + } - //if no valid player count and no custom value + // Check if there is no valid player count and no custom value if (_settings.MainSettings.OnlineBotPresence == "") + { + // Return the default "Online" presence return "Online"; + } - //get custom value + // Get the custom value for the online bot presence string presence = _settings.MainSettings.OnlineBotPresence; - //replace variables + // Replace variables in the custom value with the actual values presence = presence.Replace("{OnlinePlayers}", onlinePlayers.ToString()); presence = presence.Replace("{MaximumPlayers}", maximumPlayers.ToString()); return presence; } + /// + /// Generates a leaderboard of players based on their playtime. + /// + /// The number of leaderboard positions to show. + /// Flag indicating if the leaderboard is player-specific. + /// The name of the player (used when playerSpecific is true). + /// Flag indicating if the full leaderboard list should be shown. + /// The string representation of the playtime leaderboard. private string GetPlayTimeLeaderBoard(int placesToShow, bool playerSpecific, string playerName, bool fullList) { - //create new dictionary to hold logged time plus any current session time + // Create a new dictionary to hold the logged playtime plus any current session time Dictionary playtime = new Dictionary(_settings.MainSettings.PlayTime); + // Calculate current session time for each player and add it to the logged playtime foreach (PlayerPlayTime player in playerPlayTimes) { TimeSpan currentSession = DateTime.Now - player.JoinTime; - - log.Debug("Player: " + player.PlayerName + " || Current Session: " + currentSession + " || Logged: " + _settings.MainSettings.PlayTime[player.PlayerName]); - - //add any current sessions to the logged playtime playtime[player.PlayerName] = playtime[player.PlayerName].Add(currentSession); } + // Sort the playtime dictionary in descending order of playtime var sortedList = playtime.OrderByDescending(v => v.Value).ToList(); - //if nothing is logged yet return no data + // If no playtime is logged yet, return a message indicating so if (sortedList.Count == 0) { return "```No play time logged yet```"; @@ -1118,17 +1255,19 @@ private string GetPlayTimeLeaderBoard(int placesToShow, bool playerSpecific, str if (playerSpecific) { + // Check if the specified player is found in the leaderboard if (sortedList.FindAll(p => p.Key == playerName).Count > 0) { TimeSpan time = sortedList.Find(p => p.Key == playerName).Value; - return "`" + time.Days + "d " + time.Hours + "h " + time.Minutes + "m " + time.Seconds + "s, position " + (sortedList.FindIndex(p => p.Key == playerName) + 1) + ", last seen " + GetLastSeen(playerName) + "`"; + // Return the playtime, position, and last seen information for the specific player + return $"`{time.Days}d {time.Hours}h {time.Minutes}m {time.Seconds}s, position {(sortedList.FindIndex(p => p.Key == playerName) + 1)}, last seen {GetLastSeen(playerName)}`"; } else { + // If the specified player is not found in the leaderboard, return a message indicating so return "```No play time logged yet```"; } - } else { @@ -1137,23 +1276,23 @@ private string GetPlayTimeLeaderBoard(int placesToShow, bool playerSpecific, str if (fullList) { - leaderboard += string.Format("{0,-4}{1,-20}{2,-15}{3,-30}", "Pos", "Player Name", "Play Time", "Last Seen") + Environment.NewLine; + leaderboard += $"{string.Format("{0,-4}{1,-20}{2,-15}{3,-30}", "Pos", "Player Name", "Play Time", "Last Seen")}{Environment.NewLine}"; } - + // Generate the leaderboard string with the specified number of positions to show foreach (KeyValuePair player in sortedList) { - //if outside places to show, stop processing + // If outside the specified places to show, stop processing if (position > placesToShow) break; if (fullList) { - leaderboard += string.Format("{0,-4}{1,-20}{2,-15}{3,-30}", position + ".", player.Key, string.Format("{0}d {1}h {2}m {3}s", player.Value.Days, player.Value.Hours, player.Value.Minutes, player.Value.Seconds), GetLastSeen(player.Key)) + Environment.NewLine; + leaderboard += $"{string.Format("{0,-4}{1,-20}{2,-15}{3,-30}", position + ".", player.Key, string.Format("{0}d {1}h {2}m {3}s", player.Value.Days, player.Value.Hours, player.Value.Minutes, player.Value.Seconds), GetLastSeen(player.Key))}{Environment.NewLine}"; } else { - leaderboard += string.Format("{0,-4}{1,-20}{2,-15}", position + ".", player.Key, string.Format("{0}d {1}h {2}m {3}s", player.Value.Days, player.Value.Hours, player.Value.Minutes, player.Value.Seconds)) + Environment.NewLine; + leaderboard += $"{string.Format("{0,-4}{1,-20}{2,-15}", position + ".", player.Key, string.Format("{0}d {1}h {2}m {3}s", player.Value.Days, player.Value.Hours, player.Value.Minutes, player.Value.Seconds))}{Environment.NewLine}"; } position++; } @@ -1162,13 +1301,19 @@ private string GetPlayTimeLeaderBoard(int placesToShow, bool playerSpecific, str return leaderboard; } - } + /// + /// Gets the last seen timestamp for a player. + /// + /// The name of the player. + /// The last seen timestamp. private string GetLastSeen(string playerName) { IHasSimpleUserList hasSimpleUserList = application as IHasSimpleUserList; bool playerOnline = false; + + // Check if the player is online by iterating through the user list foreach (SimpleUser user in hasSimpleUserList.Users) { if (user.Name == playerName) @@ -1181,48 +1326,61 @@ private string GetLastSeen(string playerName) if (playerOnline) { + // If the player is online, set the last seen timestamp to the current time lastSeen = DateTime.Now.ToString("dddd, dd MMMM yyyy HH:mm:ss"); } else { try { + // Get the last seen timestamp from the settings if available lastSeen = _settings.MainSettings.LastSeen[playerName].ToString("dddd, dd MMMM yyyy HH:mm:ss"); } catch (KeyNotFoundException) { - //player not found in list + // If the player is not found in the last seen list, set the last seen timestamp to "N/A" lastSeen = "N/A"; } - - } return lastSeen; } + /// + /// Clears all playtimes and updates the main playtime list and last seen timestamps. + /// private void ClearAllPlayTimes() { try { + // Iterate through the playerPlayTimes list foreach (PlayerPlayTime playerPlayTime in playerPlayTimes) { - log.Debug("Saving playtime for " + playerPlayTime.PlayerName); - //set the leave time as now + + // Set the leave time as now playerPlayTime.LeaveTime = DateTime.Now; - //check entry for player, if not there add new entry + // Check if the player is already in the playtime list, if not, add a new entry if (!_settings.MainSettings.PlayTime.ContainsKey(playerPlayTime.PlayerName)) + { _settings.MainSettings.PlayTime.Add(playerPlayTime.PlayerName, new TimeSpan(0)); + } + // Calculate the session playtime TimeSpan sessionPlayTime = playerPlayTime.LeaveTime - playerPlayTime.JoinTime; - //update main playtime list + // Update the main playtime list by adding the session playtime _settings.MainSettings.PlayTime[playerPlayTime.PlayerName] += sessionPlayTime; + + // Update the last seen timestamp for the player _settings.MainSettings.LastSeen[playerPlayTime.PlayerName] = DateTime.Now; + + // Save the updated settings to the configuration file _config.Save(_settings); } + + // Clear the playerPlayTimes list playerPlayTimes.Clear(); } catch (Exception exception) @@ -1231,13 +1389,18 @@ private void ClearAllPlayTimes() } } + /// + /// Sets up and registers application commands for the client. + /// public async Task ClientReady() { + // Create lists to store command properties and command builders List applicationCommandProperties = new List(); List commandList = new List(); if (_settings.MainSettings.RemoveBotName) { + // Add individual commands to the command list commandList.Add(new SlashCommandBuilder() .WithName("info") .WithDescription("Create the Server Info Panel") @@ -1280,72 +1443,74 @@ public async Task ClientReady() } else { - //bot name for command + // Get bot name for command string botName = _client.CurrentUser.Username.ToLower(); - //replace any spaces with - + // Replace any spaces with '-' botName = Regex.Replace(botName, "[^a-zA-Z0-9]", String.Empty); log.Info("Base command for bot: " + botName); - // global command - commandList.Add(new SlashCommandBuilder() + // Create the base bot command with subcommands + SlashCommandBuilder baseCommand = new SlashCommandBuilder() .WithName(botName) - .WithDescription("Base bot command") - .AddOption(new SlashCommandOptionBuilder() - .WithName("info") - .WithDescription("Create the Server Info Panel") - .WithType(ApplicationCommandOptionType.SubCommand) - .AddOption("nobuttons", ApplicationCommandOptionType.Boolean, "Hide buttons for this panel?", isRequired: false) - ).AddOption(new SlashCommandOptionBuilder() - .WithName("start-server") - .WithDescription("Start the Server") - .WithType(ApplicationCommandOptionType.SubCommand) - ).AddOption(new SlashCommandOptionBuilder() - .WithName("stop-server") - .WithDescription("Stop the Server") - .WithType(ApplicationCommandOptionType.SubCommand) - ).AddOption(new SlashCommandOptionBuilder() - .WithName("restart-server") - .WithDescription("Restart the Server") - .WithType(ApplicationCommandOptionType.SubCommand) - ).AddOption(new SlashCommandOptionBuilder() - .WithName("kill-server") - .WithDescription("Kill the Server") - .WithType(ApplicationCommandOptionType.SubCommand) - ).AddOption(new SlashCommandOptionBuilder() - .WithName("update-server") - .WithDescription("Update the Server") - .WithType(ApplicationCommandOptionType.SubCommand) - ).AddOption(new SlashCommandOptionBuilder() - .WithName("show-playtime") - .WithDescription("Show the Playtime Leaderboard") - .WithType(ApplicationCommandOptionType.SubCommand) - .AddOption("playername", ApplicationCommandOptionType.String, "Get playtime for a specific player", isRequired: false) - ).AddOption(new SlashCommandOptionBuilder() - .WithName("console") - .WithDescription("Send a Console Command to the Application") - .WithType(ApplicationCommandOptionType.SubCommand) - .AddOption("value", ApplicationCommandOptionType.String, "Command text", isRequired: true) - ).AddOption(new SlashCommandOptionBuilder() - .WithName("full-playtime-list") - .WithDescription("Full Playtime List") - .WithType(ApplicationCommandOptionType.SubCommand) - .AddOption("playername", ApplicationCommandOptionType.String, "Get info for a specific player", isRequired: false) - ) - ); - } + .WithDescription("Base bot command"); + + // Add subcommands to the base command + baseCommand.AddOption(new SlashCommandOptionBuilder() + .WithName("info") + .WithDescription("Create the Server Info Panel") + .WithType(ApplicationCommandOptionType.SubCommand) + .AddOption("nobuttons", ApplicationCommandOptionType.Boolean, "Hide buttons for this panel?", isRequired: false)) + .AddOption(new SlashCommandOptionBuilder() + .WithName("start-server") + .WithDescription("Start the Server") + .WithType(ApplicationCommandOptionType.SubCommand)) + .AddOption(new SlashCommandOptionBuilder() + .WithName("stop-server") + .WithDescription("Stop the Server") + .WithType(ApplicationCommandOptionType.SubCommand)) + .AddOption(new SlashCommandOptionBuilder() + .WithName("restart-server") + .WithDescription("Restart the Server") + .WithType(ApplicationCommandOptionType.SubCommand)) + .AddOption(new SlashCommandOptionBuilder() + .WithName("kill-server") + .WithDescription("Kill the Server") + .WithType(ApplicationCommandOptionType.SubCommand)) + .AddOption(new SlashCommandOptionBuilder() + .WithName("update-server") + .WithDescription("Update the Server") + .WithType(ApplicationCommandOptionType.SubCommand)) + .AddOption(new SlashCommandOptionBuilder() + .WithName("show-playtime") + .WithDescription("Show the Playtime Leaderboard") + .WithType(ApplicationCommandOptionType.SubCommand) + .AddOption("playername", ApplicationCommandOptionType.String, "Get playtime for a specific player", isRequired: false)) + .AddOption(new SlashCommandOptionBuilder() + .WithName("console") + .WithDescription("Send a Console Command to the Application") + .WithType(ApplicationCommandOptionType.SubCommand) + .AddOption("value", ApplicationCommandOptionType.String, "Command text", isRequired: true)) + .AddOption(new SlashCommandOptionBuilder() + .WithName("full-playtime-list") + .WithDescription("Full Playtime List") + .WithType(ApplicationCommandOptionType.SubCommand) + .AddOption("playername", ApplicationCommandOptionType.String, "Get info for a specific player", isRequired: false)); + // Add the base command to the command list + commandList.Add(baseCommand); + } try { + // Build the application command properties from the command builders foreach (SlashCommandBuilder command in commandList) { applicationCommandProperties.Add(command.Build()); } - //applicationCommandProperties.Add(globalCommand.Build()); - + // Bulk overwrite the global application commands with the built command properties await _client.BulkOverwriteGlobalApplicationCommandsAsync(applicationCommandProperties.ToArray()); } catch (HttpException exception) @@ -1354,24 +1519,34 @@ public async Task ClientReady() } } + /// + /// Handles incoming socket messages. + /// + /// The incoming socket message. private async Task MessageHandler(SocketMessage message) { - //wrong channel or bot then return and don't do anything further - if (!_settings.MainSettings.SendDiscordChatToServer || message.Author.IsBot == true) + // If sending Discord chat to server is disabled or the message is from a bot, return and do nothing further + if (!_settings.MainSettings.SendDiscordChatToServer || message.Author.IsBot) return; - if(message.Channel.Name.Equals(_settings.MainSettings.ChatToDiscordChannel)) + // Check if the message is in the specified chat-to-Discord channel + if (message.Channel.Name.Equals(_settings.MainSettings.ChatToDiscordChannel)) { - await SendChatCommand(message.Author.Username.ToString(), message.CleanContent.ToString()); + // Send the chat command to the server + await SendChatCommand(message.Author.Username, message.CleanContent); } } + /// + /// Handles incoming socket slash commands. + /// + /// The incoming socket slash command. private async Task SlashCommandHandler(SocketSlashCommand command) { - //using bot name base command + // Using bot name as the base command if (!_settings.MainSettings.RemoveBotName) { - //leaderboard permissionless + // Leaderboard permissionless if (command.Data.Options.First().Name.Equals("show-playtime")) { if (command.Data.Options.First().Options.Count > 0) @@ -1387,12 +1562,15 @@ private async Task SlashCommandHandler(SocketSlashCommand command) } return; } - //init bool for permission check + + // Initialize bool for permission check bool hasServerPermission = false; if (command.User is SocketGuildUser user) - //The user has the permission if either RestrictFunctions is turned off, or if they are part of the appropriate role. + { + // The user has the permission if either RestrictFunctions is turned off, or if they are part of the appropriate role. hasServerPermission = !_settings.MainSettings.RestrictFunctions || user.Roles.Any(r => r.Name == _settings.MainSettings.DiscordRole); + } if (!hasServerPermission) { @@ -1403,15 +1581,8 @@ private async Task SlashCommandHandler(SocketSlashCommand command) switch (command.Data.Options.First().Name) { case "info": - if (command.Data.Options.First().Options.Count > 0) - { - bool buttonless = Convert.ToBoolean(command.Data.Options.First().Options.First().Value.ToString()); - await GetServerInfo(false, command, buttonless); - } - else - { - await GetServerInfo(false, command, false); - } + bool buttonless = command.Data.Options.First().Options.Count > 0 && Convert.ToBoolean(command.Data.Options.First().Options.First().Value.ToString()); + await GetServerInfo(false, command, buttonless); await command.RespondAsync("Info panel created", ephemeral: true); break; case "start-server": @@ -1441,8 +1612,9 @@ private async Task SlashCommandHandler(SocketSlashCommand command) break; case "console": await SendConsoleCommand(command); - await CommandResponse("`" + command.Data.Options.First().Options.First().Value.ToString() + "` console ", command); - await command.RespondAsync("Command sent to the server: `" + command.Data.Options.First().Options.First().Value.ToString() + "`", ephemeral: true); + string consoleCommand = command.Data.Options.First().Options.First().Value.ToString(); + await CommandResponse("`" + consoleCommand + "` console ", command); + await command.RespondAsync("Command sent to the server: `" + consoleCommand + "`", ephemeral: true); break; case "full-playtime-list": if (command.Data.Options.First().Options.Count > 0) @@ -1487,16 +1659,15 @@ private async Task SlashCommandHandler(SocketSlashCommand command) await command.RespondAsync(playTime, ephemeral: true); } - //await command.User.SendMessageAsync(playTime); + // await command.User.SendMessageAsync(playTime); } break; } - } else { - //no bot prefix - //leaderboard permissionless + // No bot prefix + // Leaderboard permissionless if (command.Data.Name.Equals("show-playtime")) { if (command.Data.Options.Count > 0) @@ -1512,12 +1683,15 @@ private async Task SlashCommandHandler(SocketSlashCommand command) } return; } - //init bool for permission check + + // Initialize bool for permission check bool hasServerPermission = false; if (command.User is SocketGuildUser user) - //The user has the permission if either RestrictFunctions is turned off, or if they are part of the appropriate role. + { + // The user has the permission if either RestrictFunctions is turned off, or if they are part of the appropriate role. hasServerPermission = !_settings.MainSettings.RestrictFunctions || user.Roles.Any(r => r.Name == _settings.MainSettings.DiscordRole); + } if (!hasServerPermission) { @@ -1528,15 +1702,8 @@ private async Task SlashCommandHandler(SocketSlashCommand command) switch (command.Data.Name) { case "info": - if (command.Data.Options.Count > 0) - { - bool buttonless = Convert.ToBoolean(command.Data.Options.First().Value.ToString()); - await GetServerInfo(false, command, buttonless); - } - else - { - await GetServerInfo(false, command, false); - } + bool buttonless = command.Data.Options.Count > 0 && Convert.ToBoolean(command.Data.Options.First().Value.ToString()); + await GetServerInfo(false, command, buttonless); await command.RespondAsync("Info panel created", ephemeral: true); break; case "start-server": @@ -1612,13 +1779,16 @@ private async Task SlashCommandHandler(SocketSlashCommand command) await command.RespondAsync(playTime, ephemeral: true); } - //await command.User.SendMessageAsync(playTime); + // await command.User.SendMessageAsync(playTime); } break; } } } + /// + /// Represents player playtime information. + /// public class PlayerPlayTime { public string PlayerName { get; set; } @@ -1626,17 +1796,24 @@ public class PlayerPlayTime public DateTime LeaveTime { get; set; } } + /// + /// Retrieves the event channel from the specified guild by ID or name. + /// + /// The ID of the guild. + /// The ID or name of the channel. + /// The event channel if found; otherwise, null. private SocketGuildChannel GetEventChannel(ulong guildID, string channel) { SocketGuildChannel eventChannel; - //try by ID first + // Try by ID first try { eventChannel = _client.GetGuild(guildID).Channels.FirstOrDefault(x => x.Id == Convert.ToUInt64(channel)); } catch { + // If the ID retrieval fails, try by name eventChannel = _client.GetGuild(guildID).Channels.FirstOrDefault(x => x.Name == channel); } diff --git a/DiscordBotPlugin/Settings.cs b/DiscordBotPlugin/Settings.cs index ee98a0c..27949b6 100644 --- a/DiscordBotPlugin/Settings.cs +++ b/DiscordBotPlugin/Settings.cs @@ -115,6 +115,12 @@ public class DiscordBotSettings : SettingSectionStore [WebSetting("Send Chat from Discord to Server", "Attempt to send chat messages from Discord chat channel to the server (currently only supported for Minecraft)", false)] public bool SendDiscordChatToServer = false; + [WebSetting("Send Console to Discord", "Send console output to a Discord channel",false)] + public bool SendConsoleToDiscord; + + [WebSetting("Console Discord Channel", "Discord channel name to send console output to (if enabled)", false)] + public string ConsoleToDiscordChannel = ""; + public Dictionary LastSeen = new Dictionary(); } diff --git a/README.md b/README.md index 87da521..4a79885 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,12 @@ # AMP Discord Bot Plugin -A Discord bot plugin for [AMP by Cubecoders](https://cubecoders.com/AMP) that can be used to display the server status in a dynamically updating info panel along with the ability to manage the server directly from Discord (start / stop / restart / kill / update) via buttons and commands. +A full featured Discord bot plugin for [AMP by Cubecoders](https://cubecoders.com/AMP) that can be used to display the server status in a dynamically updating info panel along with the ability to manage the server directly from Discord. +* Use buttons or commands to start/stop/restart/kill/update/manage. +* Send console commands directly from Discord to your server. +* Send server output to a Discord channel. +* Track playtime of players. +* Assign permissions to a role so only select members can manage the server. **Submit any bug reports or feature requests [here](https://github.com/winglessraven/AMP-Discord-Bot/issues)** @@ -95,11 +100,13 @@ Before the plugin can be used you need to configure AMP in a specific way. **NO |Online Server Bot Presence Text|Change the presence text when the application is running. Use `{OnlinePlayers}` and `{MaximumPlayers}` as variables| |Display Playtime Leaderboard|Toggle the playtime leaderboard on the info panel| |Remove Bot Name|Removes the bot name from the base command to allow granular permissions from within the Discord server settings| -|Additional Embed Field Title|Title for an additional field on the info panel embed| -|Additional Embed Field Text|Content for an additional field on the info panel embed| -|Send Chat to Discord|Send chat messages from the server to a Discord channel| -|Chat Discord Channel|Channel to send messages to if previous option is enabled| -|Send Chat from Discord to Server|Attempt to send chat messages from Discord channel (specified in previous option) to server chat - currently only Minecraft supported!| +|Additional Embed Field Title|Add an additional field in the info panel embed, put your title here| +|Additional Embed Field Text|Add an additional field in the info panel embed, put your content here| +|Send Chat to Discord|Send chat messages to a Discord channel| +|Chat Discord Channel|Discord channel name to send chat messages to (if enabled)| +|Send Chat from Discord to Server|Attempt to send chat messages from Discord chat channel to the server (currently only supported for Minecraft)| +|Send Console to Discord|Send console output to a Discord channel| +|Console Discord Channel|Discord channel name to send console output to (if enabled)| ## AMP Discord Bot Colours The `Discord Bot Colours` section give you the ability to change the colour of your embedded messages in Discord. For each option you want to change insert the hex colour code required.