diff --git a/DiscordBot/Bot/Actions/AddPlugin.cs b/DiscordBot/Bot/Actions/AddPlugin.cs index f7b1100..cac82f5 100644 --- a/DiscordBot/Bot/Actions/AddPlugin.cs +++ b/DiscordBot/Bot/Actions/AddPlugin.cs @@ -23,7 +23,7 @@ namespace DiscordBot.Bot.Actions new InternalActionOption("fileName", "The file name") ]; - public InternalActionRunType RunType => InternalActionRunType.ON_CALL; + public InternalActionRunType RunType => InternalActionRunType.OnCall; public async Task Execute(string[] args) { diff --git a/DiscordBot/Bot/Actions/Clear.cs b/DiscordBot/Bot/Actions/Clear.cs index 1569452..6830caf 100644 --- a/DiscordBot/Bot/Actions/Clear.cs +++ b/DiscordBot/Bot/Actions/Clear.cs @@ -15,7 +15,7 @@ public class Clear: ICommandAction public string Usage => "clear"; public IEnumerable ListOfOptions => []; - public InternalActionRunType RunType => InternalActionRunType.ON_CALL; + public InternalActionRunType RunType => InternalActionRunType.OnCall; public Task Execute(string[] args) { diff --git a/DiscordBot/Bot/Actions/Exit.cs b/DiscordBot/Bot/Actions/Exit.cs index 92ffcb8..0cffb07 100644 --- a/DiscordBot/Bot/Actions/Exit.cs +++ b/DiscordBot/Bot/Actions/Exit.cs @@ -18,13 +18,13 @@ public class Exit: ICommandAction new InternalActionOption("help", "Displays this message"), new InternalActionOption("force | -f", "Exits the bot without saving the config") }; - public InternalActionRunType RunType => InternalActionRunType.ON_CALL; + public InternalActionRunType RunType => InternalActionRunType.OnCall; public async Task Execute(string[] args) { if (args is null || args.Length == 0) { - Application.CurrentApplication.Logger.Log("Exiting...", this, LogType.WARNING); + Application.CurrentApplication.Logger.Log("Exiting...", this, LogType.Warning); await Application.CurrentApplication.ApplicationEnvironmentVariables.SaveToFile(); Environment.Exit(0); } @@ -40,7 +40,7 @@ public class Exit: ICommandAction case "-f": case "force": - Application.CurrentApplication.Logger.Log("Exiting (FORCE)...", this, LogType.WARNING); + Application.CurrentApplication.Logger.Log("Exiting (FORCE)...", this, LogType.Warning); Environment.Exit(0); break; diff --git a/DiscordBot/Bot/Actions/Extra/PluginMethods.cs b/DiscordBot/Bot/Actions/Extra/PluginMethods.cs index 1f3623a..5458f93 100644 --- a/DiscordBot/Bot/Actions/Extra/PluginMethods.cs +++ b/DiscordBot/Bot/Actions/Extra/PluginMethods.cs @@ -227,13 +227,13 @@ internal static class PluginMethods { if (data.IsSuccess) { - Application.CurrentApplication.Logger.Log("Successfully loaded command : " + data.PluginName, LogType.INFO, "\t\t > {Message}"); + Application.CurrentApplication.Logger.Log("Successfully loaded command : " + data.PluginName, LogType.Info, "\t\t > {Message}"); } else { Application.CurrentApplication.Logger.Log("Failed to load command : " + data.PluginName + " because " + data.ErrorMessage, - typeof(PluginMethods), LogType.ERROR + typeof(PluginMethods), LogType.Error ); } @@ -243,12 +243,12 @@ internal static class PluginMethods { if (data.IsSuccess) { - Application.CurrentApplication.Logger.Log("Successfully loaded event : " + data.PluginName, LogType.INFO, "\t\t > {Message}"); + Application.CurrentApplication.Logger.Log("Successfully loaded event : " + data.PluginName, LogType.Info, "\t\t > {Message}"); } else { Application.CurrentApplication.Logger.Log("Failed to load event : " + data.PluginName + " because " + data.ErrorMessage, - typeof(PluginMethods), LogType.ERROR + typeof(PluginMethods), LogType.Error ); } @@ -259,12 +259,12 @@ internal static class PluginMethods { if (data.IsSuccess) { - Application.CurrentApplication.Logger.Log("Successfully loaded slash command : " + data.PluginName, LogType.INFO, "\t\t > {Message}"); + Application.CurrentApplication.Logger.Log("Successfully loaded slash command : " + data.PluginName, LogType.Info, "\t\t > {Message}"); } else { Application.CurrentApplication.Logger.Log("Failed to load slash command : " + data.PluginName + " because " + data.ErrorMessage, - typeof(PluginMethods), LogType.ERROR + typeof(PluginMethods), LogType.Error ); } @@ -275,12 +275,12 @@ internal static class PluginMethods { if (data.IsSuccess) { - Application.CurrentApplication.Logger.Log("Successfully loaded action : " + data.PluginName, LogType.INFO, "\t\t > {Message}"); + Application.CurrentApplication.Logger.Log("Successfully loaded action : " + data.PluginName, LogType.Info, "\t\t > {Message}"); } else { Application.CurrentApplication.Logger.Log("Failed to load action : " + data.PluginName + " because " + data.ErrorMessage, - typeof(PluginMethods), LogType.ERROR + typeof(PluginMethods), LogType.Error ); } diff --git a/DiscordBot/Bot/Actions/Help.cs b/DiscordBot/Bot/Actions/Help.cs index fcca846..40a7c3e 100644 --- a/DiscordBot/Bot/Actions/Help.cs +++ b/DiscordBot/Bot/Actions/Help.cs @@ -25,7 +25,7 @@ public class Help: ICommandAction new InternalActionOption("command", "The command to get help for") ]; - public InternalActionRunType RunType => InternalActionRunType.ON_CALL; + public InternalActionRunType RunType => InternalActionRunType.OnCall; public async Task Execute(string[] args) { diff --git a/DiscordBot/Bot/Actions/Module.cs b/DiscordBot/Bot/Actions/Module.cs index 3bffb48..abae8cd 100644 --- a/DiscordBot/Bot/Actions/Module.cs +++ b/DiscordBot/Bot/Actions/Module.cs @@ -18,7 +18,7 @@ namespace DiscordBot.Bot.Actions public IEnumerable ListOfOptions => []; - public InternalActionRunType RunType => InternalActionRunType.ON_CALL; + public InternalActionRunType RunType => InternalActionRunType.OnCall; public Task Execute(string[] args) { diff --git a/DiscordBot/Bot/Actions/Plugin.cs b/DiscordBot/Bot/Actions/Plugin.cs index 8ddaf90..68e768b 100644 --- a/DiscordBot/Bot/Actions/Plugin.cs +++ b/DiscordBot/Bot/Actions/Plugin.cs @@ -38,7 +38,7 @@ public class Plugin: ICommandAction ]) }; - public InternalActionRunType RunType => InternalActionRunType.ON_CALL; + public InternalActionRunType RunType => InternalActionRunType.OnCall; public async Task Execute(string[] args) { @@ -126,13 +126,13 @@ public class Plugin: ICommandAction case "load": if (pluginsLoaded) { - Application.CurrentApplication.Logger.Log("Plugins already loaded", this, LogType.WARNING); + Application.CurrentApplication.Logger.Log("Plugins already loaded", this, LogType.Warning); break; } if (Application.CurrentApplication.DiscordBotClient is null) { - Application.CurrentApplication.Logger.Log("DiscordBot is null", this, LogType.WARNING); + Application.CurrentApplication.Logger.Log("DiscordBot is null", this, LogType.Warning); break; } diff --git a/DiscordBot/Bot/Actions/SettingsConfig.cs b/DiscordBot/Bot/Actions/SettingsConfig.cs index d45ea73..f2a4feb 100644 --- a/DiscordBot/Bot/Actions/SettingsConfig.cs +++ b/DiscordBot/Bot/Actions/SettingsConfig.cs @@ -21,7 +21,7 @@ public class SettingsConfig: ICommandAction new InternalActionOption("remove", "Remove a setting"), new InternalActionOption("add", "Add a setting") }; - public InternalActionRunType RunType => InternalActionRunType.ON_CALL; + public InternalActionRunType RunType => InternalActionRunType.OnCall; public Task Execute(string[] args) { if (args is null) diff --git a/DiscordBot/Bot/Commands/NormalCommands/Help.cs b/DiscordBot/Bot/Commands/NormalCommands/Help.cs index 02750d0..cf940ba 100644 --- a/DiscordBot/Bot/Commands/NormalCommands/Help.cs +++ b/DiscordBot/Bot/Commands/NormalCommands/Help.cs @@ -40,13 +40,13 @@ internal class Help: DBCommand /// The command context public void ExecuteServer(DbCommandExecutingArguments args) { - if (args.arguments is not null) + if (args.Arguments is not null) { - var e = GenerateHelpCommand(args.arguments[0]); + var e = GenerateHelpCommand(args.Arguments[0]); if (e is null) - args.context.Channel.SendMessageAsync("Unknown Command " + args.arguments[0]); + args.Context.Channel.SendMessageAsync("Unknown Command " + args.Arguments[0]); else - args.context.Channel.SendMessageAsync(embed: e.Build()); + args.Context.Channel.SendMessageAsync(embed: e.Build()); return; @@ -68,7 +68,7 @@ internal class Help: DBCommand embedBuilder.AddField("Admin Commands", adminCommands); if (normalCommands.Length > 0) embedBuilder.AddField("Normal Commands", normalCommands); - args.context.Channel.SendMessageAsync(embed: embedBuilder.Build()); + args.Context.Channel.SendMessageAsync(embed: embedBuilder.Build()); } private EmbedBuilder GenerateHelpCommand(string command) diff --git a/DiscordBot/Program.cs b/DiscordBot/Program.cs index 822484c..40cd712 100644 --- a/DiscordBot/Program.cs +++ b/DiscordBot/Program.cs @@ -71,7 +71,7 @@ public class Program } catch (Exception ex) { - Application.CurrentApplication.Logger.Log(ex.ToString(), typeof(Program), LogType.CRITICAL); + Application.CurrentApplication.Logger.Log(ex.ToString(), typeof(Program), LogType.Critical); } } diff --git a/DiscordBotCore/Bot/App.cs b/DiscordBotCore/Bot/App.cs index 5d25198..5751162 100644 --- a/DiscordBotCore/Bot/App.cs +++ b/DiscordBotCore/Bot/App.cs @@ -107,7 +107,7 @@ public class App if (arg.Message.Contains("401")) { Application.CurrentApplication.ApplicationEnvironmentVariables.Remove("token"); - Application.CurrentApplication.Logger.Log("The token is invalid.", this, LogType.CRITICAL); + Application.CurrentApplication.Logger.Log("The token is invalid.", this, LogType.Critical); await Application.CurrentApplication.ApplicationEnvironmentVariables.SaveToFile(); await Task.Delay(3000); @@ -134,12 +134,12 @@ public class App { case LogSeverity.Error: case LogSeverity.Critical: - Application.CurrentApplication.Logger.Log(message.Message, this, LogType.ERROR); + Application.CurrentApplication.Logger.Log(message.Message, this, LogType.Error); break; case LogSeverity.Info: case LogSeverity.Debug: - Application.CurrentApplication.Logger.Log(message.Message, this, LogType.INFO); + Application.CurrentApplication.Logger.Log(message.Message, this, LogType.Info); break; diff --git a/DiscordBotCore/Bot/CommandHandler.cs b/DiscordBotCore/Bot/CommandHandler.cs index b67e657..dec06e8 100644 --- a/DiscordBotCore/Bot/CommandHandler.cs +++ b/DiscordBotCore/Bot/CommandHandler.cs @@ -141,9 +141,9 @@ internal class CommandHandler DbCommandExecutingArguments cmd = new(context, cleanMessage, split[0], argsClean); Application.CurrentApplication.Logger.Log( - $"User ({context.User.Username}) from Guild \"{context.Guild.Name}\" executed command \"{cmd.cleanContent}\"", + $"User ({context.User.Username}) from Guild \"{context.Guild.Name}\" executed command \"{cmd.CleanContent}\"", this, - LogType.INFO + LogType.Info ); if (context.Channel is SocketDMChannel) diff --git a/DiscordBotCore/Loaders/PluginLoader.cs b/DiscordBotCore/Loaders/PluginLoader.cs index bac59a5..969e73a 100644 --- a/DiscordBotCore/Loaders/PluginLoader.cs +++ b/DiscordBotCore/Loaders/PluginLoader.cs @@ -41,7 +41,7 @@ public class PluginLoader if (_Client == null) { - Application.CurrentApplication.Logger.Log("Discord client is null", this, LogType.ERROR); + Application.CurrentApplication.Logger.Log("Discord client is null", this, LogType.Error); return; } @@ -62,7 +62,7 @@ public class PluginLoader private void FileLoadedException(FileLoaderResult result) { - Application.CurrentApplication.Logger.Log(result.ErrorMessage, this, LogType.ERROR); + Application.CurrentApplication.Logger.Log(result.ErrorMessage, this, LogType.Error); } private async void OnPluginLoaded(PluginLoadResultData result) @@ -71,10 +71,10 @@ public class PluginLoader { case PluginType.ACTION: ICommandAction action = (ICommandAction)result.Plugin; - if (action.RunType == InternalActionRunType.ON_STARTUP || action.RunType == InternalActionRunType.BOTH) + if (action.RunType == InternalActionRunType.OnStartup || action.RunType == InternalActionRunType.OnStartupAndCall) action.Execute(null); - if(action.RunType == InternalActionRunType.ON_CALL || action.RunType == InternalActionRunType.BOTH) + if(action.RunType == InternalActionRunType.OnCall || action.RunType == InternalActionRunType.OnStartupAndCall) Actions.Add(action); OnActionLoaded?.Invoke(result); @@ -101,11 +101,11 @@ public class PluginLoader OnSlashCommandLoaded?.Invoke(result); } else - Application.CurrentApplication.Logger.Log($"Failed to start slash command {result.PluginName}", this, LogType.ERROR); + Application.CurrentApplication.Logger.Log($"Failed to start slash command {result.PluginName}", this, LogType.Error); break; case PluginType.UNKNOWN: default: - Application.CurrentApplication.Logger.Log("Unknown plugin type", this, LogType.ERROR); + Application.CurrentApplication.Logger.Log("Unknown plugin type", this, LogType.Error); break; } } diff --git a/DiscordBotCore/Loaders/PluginLoaderExtensions.cs b/DiscordBotCore/Loaders/PluginLoaderExtensions.cs index 643670a..cbb3e2e 100644 --- a/DiscordBotCore/Loaders/PluginLoaderExtensions.cs +++ b/DiscordBotCore/Loaders/PluginLoaderExtensions.cs @@ -26,7 +26,7 @@ internal static class PluginLoaderExtensions } catch (Exception e) { - Application.CurrentApplication.Logger.Log($"Error starting event {dbEvent.Name}: {e.Message}", typeof(PluginLoader), LogType.ERROR); + Application.CurrentApplication.Logger.Log($"Error starting event {dbEvent.Name}: {e.Message}", typeof(PluginLoader), LogType.Error); Application.CurrentApplication.Logger.LogException(e, typeof(PluginLoader)); return false; } @@ -39,14 +39,14 @@ internal static class PluginLoaderExtensions if(pluginLoader._Client.Guilds.Count == 0) return; if (!ulong.TryParse(Application.CurrentApplication.ServerID, out _)) { - Application.CurrentApplication.Logger.Log("Invalid ServerID in config file. Can not reset specific guild commands", typeof(PluginLoader), LogType.ERROR); + Application.CurrentApplication.Logger.Log("Invalid ServerID in config file. Can not reset specific guild commands", typeof(PluginLoader), LogType.Error); return; } SocketGuild? guild = pluginLoader._Client.GetGuild(ulong.Parse(Application.CurrentApplication.ServerID)); if(guild is null) { - Application.CurrentApplication.Logger.Log("Failed to get guild with ID " + Application.CurrentApplication.ServerID, typeof(PluginLoader), LogType.ERROR); + Application.CurrentApplication.Logger.Log("Failed to get guild with ID " + Application.CurrentApplication.ServerID, typeof(PluginLoader), LogType.Error); return; } @@ -79,7 +79,7 @@ internal static class PluginLoaderExtensions SocketGuild? guild = pluginLoader._Client.GetGuild(result); if (guild is null) { - Application.CurrentApplication.Logger.Log("Failed to get guild with ID " + Application.CurrentApplication.ServerID, typeof(PluginLoader), LogType.ERROR); + Application.CurrentApplication.Logger.Log("Failed to get guild with ID " + Application.CurrentApplication.ServerID, typeof(PluginLoader), LogType.Error); return false; } @@ -90,7 +90,7 @@ internal static class PluginLoaderExtensions } catch (Exception e) { - Application.CurrentApplication.Logger.Log($"Error starting slash command {dbSlashCommand.Name}: {e.Message}", typeof(PluginLoader), LogType.ERROR); + Application.CurrentApplication.Logger.Log($"Error starting slash command {dbSlashCommand.Name}: {e.Message}", typeof(PluginLoader), LogType.Error); return false; } } diff --git a/DiscordBotCore/Online/PluginManager.cs b/DiscordBotCore/Online/PluginManager.cs index 3fbe918..b066b10 100644 --- a/DiscordBotCore/Online/PluginManager.cs +++ b/DiscordBotCore/Online/PluginManager.cs @@ -119,7 +119,7 @@ public class PluginManager : IPluginManager { if (await pluginUpdater.HasUpdate(plugin.PluginName)) { - Application.CurrentApplication.Logger.Log("Updating plugin: " + plugin.PluginName, this, LogType.INFO); + Application.CurrentApplication.Logger.Log("Updating plugin: " + plugin.PluginName, this, LogType.Info); await pluginUpdater.UpdatePlugin(plugin.PluginName); } } diff --git a/DiscordBotCore/Others/Actions/InternalActionsManager.cs b/DiscordBotCore/Others/Actions/InternalActionsManager.cs index cf88cd8..e8590c0 100644 --- a/DiscordBotCore/Others/Actions/InternalActionsManager.cs +++ b/DiscordBotCore/Others/Actions/InternalActionsManager.cs @@ -16,12 +16,12 @@ public class InternalActionManager PluginLoader.Actions.ForEach(action => { - if (action.RunType == InternalActionRunType.ON_CALL || action.RunType == InternalActionRunType.BOTH) + if (action.RunType == InternalActionRunType.OnCall || action.RunType == InternalActionRunType.OnStartupAndCall) { if (this.Actions.ContainsKey(action.ActionName)) { // This should never happen. If it does, log it and return - Application.CurrentApplication.Logger.Log($"Action {action.ActionName} already exists", this, LogType.ERROR); + Application.CurrentApplication.Logger.Log($"Action {action.ActionName} already exists", this, LogType.Error); return; } @@ -49,15 +49,15 @@ public class InternalActionManager { if (!Actions.ContainsKey(actionName)) { - Application.CurrentApplication.Logger.Log($"Action {actionName} not found", this, LogType.ERROR); + Application.CurrentApplication.Logger.Log($"Action {actionName} not found", this, LogType.Error); return false; } try { - if (Actions[actionName].RunType == InternalActionRunType.ON_STARTUP) + if (Actions[actionName].RunType == InternalActionRunType.OnStartup) { - Application.CurrentApplication.Logger.Log($"Action {actionName} is not executable", this, LogType.ERROR); + Application.CurrentApplication.Logger.Log($"Action {actionName} is not executable", this, LogType.Error); return false; } @@ -66,7 +66,7 @@ public class InternalActionManager } catch (Exception e) { - Application.CurrentApplication.Logger.Log(e.Message, type: LogType.ERROR, Sender: this); + Application.CurrentApplication.Logger.Log(e.Message, type: LogType.Error, Sender: this); return false; } } diff --git a/DiscordBotCore/Others/ArchiveManager.cs b/DiscordBotCore/Others/ArchiveManager.cs index dbde54c..3b72a6b 100644 --- a/DiscordBotCore/Others/ArchiveManager.cs +++ b/DiscordBotCore/Others/ArchiveManager.cs @@ -87,7 +87,7 @@ public static class ArchiveManager } catch (Exception ex) { - Application.CurrentApplication.Logger.Log(ex.Message, typeof(ArchiveManager), LogType.ERROR); // Write the error to a file + Application.CurrentApplication.Logger.Log(ex.Message, typeof(ArchiveManager), LogType.Error); // Write the error to a file await Task.Delay(100); return await ReadFromPakAsync(fileName, archFile); } @@ -123,7 +123,7 @@ public static class ArchiveManager } catch (Exception ex) { - Application.CurrentApplication.Logger.Log(ex.Message, typeof(ArchiveManager), LogType.ERROR); + Application.CurrentApplication.Logger.Log(ex.Message, typeof(ArchiveManager), LogType.Error); } currentZipFile++; @@ -158,7 +158,7 @@ public static class ArchiveManager } catch (Exception ex) { - Application.CurrentApplication.Logger.Log(ex.Message, typeof(ArchiveManager), LogType.ERROR); + Application.CurrentApplication.Logger.Log(ex.Message, typeof(ArchiveManager), LogType.Error); } await Task.Delay(10); diff --git a/DiscordBotCore/Others/DBCommandExecutingArguments.cs b/DiscordBotCore/Others/DBCommandExecutingArguments.cs index 4faf8e9..d2c9647 100644 --- a/DiscordBotCore/Others/DBCommandExecutingArguments.cs +++ b/DiscordBotCore/Others/DBCommandExecutingArguments.cs @@ -7,42 +7,42 @@ namespace DiscordBotCore.Others; public class DbCommandExecutingArguments { - public SocketCommandContext context { get; init; } - public string cleanContent { get; init; } - public string commandUsed { get; init; } - public string[]? arguments { get; init; } - public ISocketMessageChannel Channel => context.Channel; + public SocketCommandContext Context { get; init; } + public string CleanContent { get; init; } + public string CommandUsed { get; init; } + public string[]? Arguments { get; init; } + public ISocketMessageChannel Channel => Context.Channel; public DbCommandExecutingArguments( SocketCommandContext context, string cleanContent, string commandUsed, string[]? arguments) { - this.context = context; - this.cleanContent = cleanContent; - this.commandUsed = commandUsed; - this.arguments = arguments; + this.Context = context; + this.CleanContent = cleanContent; + this.CommandUsed = commandUsed; + this.Arguments = arguments; } public DbCommandExecutingArguments(SocketUserMessage? message, DiscordSocketClient client) { - context = new SocketCommandContext(client, message); + Context = new SocketCommandContext(client, message); var pos = 0; if (message.HasMentionPrefix(client.CurrentUser, ref pos)) { var mentionPrefix = "<@" + client.CurrentUser.Id + ">"; - cleanContent = message.Content.Substring(mentionPrefix.Length + 1); + CleanContent = message.Content.Substring(mentionPrefix.Length + 1); } else { - cleanContent = message.Content.Substring(Application.CurrentApplication.DiscordBotClient.BotPrefix.Length); + CleanContent = message.Content.Substring(Application.CurrentApplication.DiscordBotClient.BotPrefix.Length); } - var split = cleanContent.Split(' '); + var split = CleanContent.Split(' '); string[]? argsClean = null; if (split.Length > 1) argsClean = string.Join(' ', split, 1, split.Length - 1).Split(' '); - commandUsed = split[0]; - arguments = argsClean; + CommandUsed = split[0]; + Arguments = argsClean; } } diff --git a/DiscordBotCore/Others/Enums.cs b/DiscordBotCore/Others/Enums.cs index f48de1b..f7b911d 100644 --- a/DiscordBotCore/Others/Enums.cs +++ b/DiscordBotCore/Others/Enums.cs @@ -7,10 +7,10 @@ namespace DiscordBotCore.Others; /// public enum LogType { - INFO, - WARNING, - ERROR, - CRITICAL + Info, + Warning, + Error, + Critical } public enum UnzipProgressType @@ -21,9 +21,9 @@ public enum UnzipProgressType public enum InternalActionRunType { - ON_STARTUP, - ON_CALL, - BOTH + OnStartup, + OnCall, + OnStartupAndCall } [Flags] diff --git a/Modules/LoggerModule/Entry.cs b/Modules/LoggerModule/Entry.cs new file mode 100644 index 0000000..10c3d94 --- /dev/null +++ b/Modules/LoggerModule/Entry.cs @@ -0,0 +1,22 @@ +using DiscordBotCore; +using DiscordBotCore.Interfaces.Logger; +using DiscordBotCore.Interfaces.Modules; + +namespace LoggerModule +{ + public class Entry : IModule + { + public string Name => "Logger Module"; + const string _LogFolder = "./Data/Logs/"; + const string _LogFormat = "{ThrowTime} {SenderName} {Message}"; + + public ILogger Module { get; private set; } + + public Task Initialize() + { + ILogger logger = new Logger(_LogFolder, _LogFormat); + Module = logger; + return Task.CompletedTask; + } + } +} diff --git a/Modules/LoggerModule/LogMessage.cs b/Modules/LoggerModule/LogMessage.cs new file mode 100644 index 0000000..d408544 --- /dev/null +++ b/Modules/LoggerModule/LogMessage.cs @@ -0,0 +1,79 @@ +using DiscordBotCore.Interfaces.Logger; +using DiscordBotCore.Others; + +namespace LoggerModule +{ + internal sealed class LogMessage : ILogMessage + { + private static readonly string _DefaultLogMessageSender = "\b"; + public string Message { get; set; } + public DateTime ThrowTime { get; set; } + public string SenderName { get; set; } + public LogType LogMessageType { get; set; } + + public LogMessage(string message, LogType logMessageType) + { + Message = message; + LogMessageType = logMessageType; + ThrowTime = DateTime.Now; + SenderName = string.Empty; + } + + public LogMessage(string message, object sender) + { + Message = message; + SenderName = sender is string && sender as string == string.Empty ? _DefaultLogMessageSender : sender.GetType().FullName ?? sender.GetType().Name; + ThrowTime = DateTime.Now; + LogMessageType = LogType.Info; + } + + public LogMessage(string message, object sender, DateTime throwTime) + { + Message = message; + SenderName = sender is string && sender as string == string.Empty ? _DefaultLogMessageSender : sender.GetType().FullName ?? sender.GetType().Name; + ThrowTime = throwTime; + LogMessageType = LogType.Info; + } + + public LogMessage(string message, object sender, LogType logMessageType) + { + Message = message; + SenderName = sender is string && sender as string == string.Empty ? _DefaultLogMessageSender : sender.GetType().FullName ?? sender.GetType().Name; + ThrowTime = DateTime.Now; + LogMessageType = logMessageType; + + } + + public LogMessage(string message, DateTime throwTime, object sender, LogType logMessageType) + { + Message = message; + ThrowTime = throwTime; + SenderName = sender is string && sender as string == string.Empty ? _DefaultLogMessageSender : sender.GetType().FullName ?? sender.GetType().Name; + LogMessageType = logMessageType; + } + + public LogMessage WithMessage(string message) + { + this.Message = message; + return this; + } + + public LogMessage WithCurrentThrowTime() + { + this.ThrowTime = DateTime.Now; + return this; + } + + public LogMessage WithMessageType(LogType logType) + { + this.LogMessageType = logType; + return this; + } + + public static LogMessage CreateFromException(Exception exception, object Sender, bool logFullStack) + { + LogMessage message = new LogMessage(logFullStack? exception.ToString() : exception.Message, Sender, LogType.Error); + return message; + } + } +} diff --git a/Modules/LoggerModule/Logger.cs b/Modules/LoggerModule/Logger.cs new file mode 100644 index 0000000..ea50420 --- /dev/null +++ b/Modules/LoggerModule/Logger.cs @@ -0,0 +1,122 @@ +using DiscordBotCore.Interfaces.Logger; +using DiscordBotCore.Others; + +namespace LoggerModule; + +public sealed class Logger : ILogger +{ + private readonly FileStream _LogFileStream; + + public List LogMessageProperties = typeof(ILogMessage).GetProperties().Select(p => p.Name).ToList(); + private Action? _OutFunction; + public string LogMessageFormat { get ; set; } + + public Logger(string logFolder, string logMessageFormat, Action? outFunction = null) + { + this.LogMessageFormat = logMessageFormat; + this._OutFunction = outFunction; + var logFile = logFolder + DateTime.Now.ToString("yyyy-MM-dd") + ".log"; + _LogFileStream = File.Open(logFile, FileMode.Append, FileAccess.Write, FileShare.Read); + } + + /// + /// Generate a formatted string based on the default parameters of the ILogMessage and a string defined as model + /// + /// The message + /// A formatted string with the message values + private string GenerateLogMessage(ILogMessage message) + { + string messageAsString = new string(LogMessageFormat); + foreach (var prop in LogMessageProperties) + { + Type messageType = typeof(ILogMessage); + messageAsString = messageAsString.Replace("{" + prop + "}", messageType?.GetProperty(prop)?.GetValue(message)?.ToString()); + } + + switch (message.LogMessageType) + { + case LogType.Info: + messageAsString = $"[green]{messageAsString} [/]"; + break; + case LogType.Warning: + messageAsString = $"[yellow]{messageAsString} [/]"; + break; + case LogType.Error: + messageAsString = $"[red]{messageAsString} [/]"; + break; + case LogType.Critical: + messageAsString = $"[red] [bold]{messageAsString} [/][/]"; + break; + + } + + return messageAsString; + } + + private async void LogToFile(string message) + { + byte[] messageAsBytes = System.Text.Encoding.ASCII.GetBytes(message); + await _LogFileStream.WriteAsync(messageAsBytes, 0, messageAsBytes.Length); + + byte[] newLine = System.Text.Encoding.ASCII.GetBytes(Environment.NewLine); + await _LogFileStream.WriteAsync(newLine, 0, newLine.Length); + + await _LogFileStream.FlushAsync(); + } + + private string GenerateLogMessage(ILogMessage message, string customFormat) + { + string messageAsString = customFormat; + foreach (var prop in LogMessageProperties) + { + Type messageType = typeof(ILogMessage); + messageAsString = messageAsString.Replace("{" + prop + "}", messageType?.GetProperty(prop)?.GetValue(message)?.ToString()); + } + + switch (message.LogMessageType) + { + case LogType.Info: + messageAsString = $"[green]{messageAsString} [/]"; + break; + case LogType.Warning: + messageAsString = $"[yellow]{messageAsString} [/]"; + break; + case LogType.Error: + messageAsString = $"[red]{messageAsString} [/]"; + break; + case LogType.Critical: + messageAsString = $"[red][bold]{messageAsString} [/][/]"; + break; + + } + + return messageAsString; + } + + public void Log(ILogMessage message, string format) + { + string messageAsString = GenerateLogMessage(message, format); + _OutFunction?.Invoke(messageAsString); + LogToFile(messageAsString); + } + + public void Log(ILogMessage message) + { + string messageAsString = GenerateLogMessage(message); + _OutFunction?.Invoke(messageAsString); + LogToFile(messageAsString); + + } + + public void Log(string message) => Log(new LogMessage(message, string.Empty, LogType.Info)); + public void Log(string message, LogType logType, string format) => Log(new LogMessage(message, logType), format); + public void Log(string message, LogType logType) => Log(new LogMessage(message, logType)); + public void Log(string message, object Sender) => Log(new LogMessage(message, Sender)); + public void Log(string message, object Sender, LogType type) => Log(new LogMessage(message, Sender, type)); + public void LogException(Exception exception, object Sender, bool logFullStack = false) => Log(LogMessage.CreateFromException(exception, Sender, logFullStack)); + + public void SetOutFunction(Action outFunction) + { + this._OutFunction = outFunction; + } +} diff --git a/Modules/LoggerModule/LoggerModule.csproj b/Modules/LoggerModule/LoggerModule.csproj new file mode 100644 index 0000000..72beaa5 --- /dev/null +++ b/Modules/LoggerModule/LoggerModule.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/Plugins/CppWrapper/CppWrapper.csproj b/Plugins/CppWrapper/CppWrapper.csproj new file mode 100644 index 0000000..72beaa5 --- /dev/null +++ b/Plugins/CppWrapper/CppWrapper.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/Plugins/CppWrapper/LibraryManagement/ExternLibrary.cs b/Plugins/CppWrapper/LibraryManagement/ExternLibrary.cs new file mode 100644 index 0000000..58845b6 --- /dev/null +++ b/Plugins/CppWrapper/LibraryManagement/ExternLibrary.cs @@ -0,0 +1,112 @@ +using System.Runtime.InteropServices; +using DiscordBotCore; + +namespace CppWrapper.LibraryManagement +{ + public sealed class ExternLibrary + { + public string LibraryPath { get; init; } + public IntPtr LibraryHandle { get; private set; } + + public ExternLibrary(string libraryPath) + { + LibraryPath = libraryPath; + LibraryHandle = IntPtr.Zero; + } + + public void InitializeLibrary() + { + if(LibraryHandle != IntPtr.Zero) + { + return; + } + + Application.CurrentApplication.Logger.Log($"Loading library {LibraryPath}"); + + + if(!NativeLibrary.TryLoad(LibraryPath, out IntPtr hModule)) + { + throw new DllNotFoundException($"Unable to load library {LibraryPath}"); + } + + Application.CurrentApplication.Logger.Log($"Library {LibraryPath} loaded successfully [{hModule}]"); + + LibraryHandle = hModule; + } + + public void FreeLibrary() + { + if(LibraryHandle == IntPtr.Zero) + { + return; + } + + NativeLibrary.Free(LibraryHandle); + LibraryHandle = IntPtr.Zero; + + Application.CurrentApplication.Logger.Log($"Library {LibraryPath} freed successfully"); + } + + private IntPtr GetFunctionPointer(string functionName) + { + if(LibraryHandle == IntPtr.Zero) + { + throw new InvalidOperationException("Library is not loaded"); + } + + if(!NativeLibrary.TryGetExport(LibraryHandle, functionName, out IntPtr functionPointer)) + { + throw new EntryPointNotFoundException($"Unable to find function {functionName}"); + } + + return functionPointer; + } + + public T GetDelegateForFunctionPointer(string methodName) where T : Delegate + { + IntPtr functionPointer = GetFunctionPointer(methodName); + + Application.CurrentApplication.Logger.Log($"Function pointer for {methodName} obtained successfully [address: {functionPointer}]"); + + T result = (T)Marshal.GetDelegateForFunctionPointer(functionPointer, typeof(T)); + + Application.CurrentApplication.Logger.Log($"Delegate for {methodName} created successfully"); + + return result; + } + + private IntPtr GetFunctionPointerForDelegate(T functionDelegate) where T : Delegate + { + IntPtr functionPointer = Marshal.GetFunctionPointerForDelegate(functionDelegate); + + Application.CurrentApplication.Logger.Log($"Function pointer for delegate {functionDelegate.Method.Name} obtained successfully [address: {functionPointer}]"); + + return functionPointer; + } + + /// + /// Tells the extern setter function to point its function to this C# function instead. + /// This function takes the name of the extern setter function and the C# function to be executed. + /// How it works: + /// Find the external setter method by its name. It should take one parameter, which is the pointer to the function to be executed. + /// Take the delegate function that should be executed and get its function pointer. + /// Call the external setter with the new function memory address. This should replace the old C++ function with the new C# function. + /// + /// The setter function name + /// The function that the C++ setter will make its internal function to point to + /// A delegate that reflects the executable function structure + /// The Setter delegate + /// A response if it exists as an object + public object? SetExternFunctionSetterPointerToCustomDelegate(string setterExternFunctionName, ExecuteDelegate executableFunction) where ExecuteDelegate : Delegate where SetDelegate : Delegate + { + SetDelegate setterDelegate = GetDelegateForFunctionPointer(setterExternFunctionName); + IntPtr executableFunctionPtr = GetFunctionPointerForDelegate(executableFunction); + + var result = setterDelegate.DynamicInvoke(executableFunctionPtr); + + Application.CurrentApplication.Logger.Log($"Function {setterExternFunctionName} bound to local action successfully"); + + return result; + } + } +} diff --git a/Plugins/CppWrapper/Objects/ApplicationStruct.cs b/Plugins/CppWrapper/Objects/ApplicationStruct.cs new file mode 100644 index 0000000..5255860 --- /dev/null +++ b/Plugins/CppWrapper/Objects/ApplicationStruct.cs @@ -0,0 +1,34 @@ +using System.Runtime.InteropServices; + +namespace CppWrapper.Objects +{ + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] + public struct ApplicationStruct + { + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 19)] + public string ServerId; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 2)] + public string Prefix; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 71)] + public string Token; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] + public struct ComplexObject + { + public int Integer; + public double DoubleValue; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] + public string strValue; + + public ComplexObject(int integer, double doubleValue, string strValue) + { + Integer = integer; + DoubleValue = doubleValue; + this.strValue = strValue; + } + } +} diff --git a/Plugins/CppWrapper/Objects/ObjectConvertor.cs b/Plugins/CppWrapper/Objects/ObjectConvertor.cs new file mode 100644 index 0000000..61421fd --- /dev/null +++ b/Plugins/CppWrapper/Objects/ObjectConvertor.cs @@ -0,0 +1,17 @@ +using DiscordBotCore; + +namespace CppWrapper.Objects +{ + public static class ObjectConvertor + { + public static ApplicationStruct ToApplicationStruct(this Application application) + { + return new ApplicationStruct + { + Token = application.ApplicationEnvironmentVariables["token"], + Prefix = application.ApplicationEnvironmentVariables["prefix"], + ServerId = application.ServerID + }; + } + } +} diff --git a/Plugins/DiscordBotUI/Delegates.cs b/Plugins/DiscordBotUI/Delegates.cs new file mode 100644 index 0000000..98c1300 --- /dev/null +++ b/Plugins/DiscordBotUI/Delegates.cs @@ -0,0 +1,22 @@ +using System.Runtime.InteropServices; + +using CppWrapper.Objects; + +namespace DiscordBotUI +{ + public abstract class Delegates + { + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void ProcessApplicationData(ref ApplicationStruct appData); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void ProcessComplexObject(ref ComplexObject complexObject); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void CsharpFunctionDelegate(); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void SetCsharpFunctionPointerDelegate(IntPtr funcPtr); + } +} diff --git a/Plugins/DiscordBotUI/DiscordBotUI.csproj b/Plugins/DiscordBotUI/DiscordBotUI.csproj new file mode 100644 index 0000000..198e59f --- /dev/null +++ b/Plugins/DiscordBotUI/DiscordBotUI.csproj @@ -0,0 +1,13 @@ + + + net8.0 + enable + enable + + + + + + + + diff --git a/Plugins/DiscordBotUI/Entry.cs b/Plugins/DiscordBotUI/Entry.cs new file mode 100644 index 0000000..ccc9434 --- /dev/null +++ b/Plugins/DiscordBotUI/Entry.cs @@ -0,0 +1,52 @@ +using DiscordBotCore.Interfaces; +using DiscordBotCore.Others; +using DiscordBotCore.Others.Actions; + +using CppWrapper.Objects; +using CppWrapper.LibraryManagement; +using DiscordBotCore; +using CppWrapper; + +namespace DiscordBotUI; + +public class Entry : ICommandAction +{ + public string ActionName => "cppui"; + + public string? Description => "A C++ linker to the C++ UI for the bot"; + + public string? Usage => "cppui"; + + public IEnumerable ListOfOptions => []; + + public InternalActionRunType RunType => InternalActionRunType.OnStartupAndCall; + + public async Task Execute(string[]? args) + { + try{ + + string appUiComponent = "./Data/Test/libtestlib.dll"; + + ExternLibrary externalLibrary = new ExternLibrary(appUiComponent); + externalLibrary.InitializeLibrary(); + + externalLibrary.SetExternFunctionSetterPointerToCustomDelegate("setCSharpFunctionPointer", () => + { + Console.WriteLine("Hello from C#. This code is called from the C# function"); + }); + + Delegates.ProcessComplexObject processObj = externalLibrary.GetDelegateForFunctionPointer("ProcessComplexObject"); + + ComplexObject complexObject = new ComplexObject(10, 10.5, "Hello from C#"); + processObj(ref complexObject); + + Console.WriteLine($"Integer: {complexObject.Integer}"); + Console.WriteLine($"Double: {complexObject.DoubleValue}"); + Console.WriteLine($"String: {complexObject.strValue}"); + + externalLibrary.FreeLibrary(); + } catch (Exception dllException) { + Application.CurrentApplication.Logger.LogException(dllException, this); + } + } +} diff --git a/Plugins/LevelingSystem/LevelCommand.cs b/Plugins/LevelingSystem/LevelCommand.cs new file mode 100644 index 0000000..e04feff --- /dev/null +++ b/Plugins/LevelingSystem/LevelCommand.cs @@ -0,0 +1,52 @@ +using Discord; +using DiscordBotCore; +using DiscordBotCore.Interfaces; +using DiscordBotCore.Others; + +namespace LevelingSystem; + +internal class LevelCommand: DBCommand +{ + public string Command => "level"; + + public List Aliases => ["lvl", "rank"]; + + public string Description => "Display tour current level"; + + public string Usage => "level"; + + public bool requireAdmin => false; + + public async void ExecuteServer(DbCommandExecutingArguments args) + { + if(Variables.Database is null) + { + Application.CurrentApplication.Logger.Log("Database is not initialized", this, LogType.Warning); + return; + } + + + object[]? user = await Variables.Database.ReadDataArrayAsync($"SELECT * FROM Levels WHERE UserID=@userId", + new KeyValuePair("userId", args.Context.Message.Author.Id)); + + + if (user is null) + { + await args.Context.Channel.SendMessageAsync("You are now unranked !"); + return; + } + + var level = (int)user[1]; + var exp = (int)user[2]; + + var builder = new EmbedBuilder(); + var r = new Random(); + builder.WithColor(r.Next(256), r.Next(256), r.Next(256)); + builder.AddField("Current Level", level, true) + .AddField("Current EXP", exp, true) + .AddField("Required Exp", (level * 8 + 24).ToString(), true); + builder.WithTimestamp(DateTimeOffset.Now); + builder.WithAuthor(args.Context.Message.Author); + await args.Context.Channel.SendMessageAsync(embed: builder.Build()); + } +} diff --git a/Plugins/LevelingSystem/LevelEvent.cs b/Plugins/LevelingSystem/LevelEvent.cs new file mode 100644 index 0000000..87b9b52 --- /dev/null +++ b/Plugins/LevelingSystem/LevelEvent.cs @@ -0,0 +1,83 @@ +using Discord.WebSocket; + +using DiscordBotCore; +using DiscordBotCore.Database; +using DiscordBotCore.Interfaces; +using static LevelingSystem.Variables; + +namespace LevelingSystem; + +internal class LevelEvent : DBEvent +{ + public string Name => "Leveling System Event Handler"; + public string Description => "The Leveling System Event Handler"; + + public bool RequireOtherThread => false; + + public async void Start(DiscordSocketClient client) + { + + Directory.CreateDirectory(DataFolder); + await Task.Delay(200); + Database = new SqlDatabase(DataFolder + "Users.db"); + await Database.Open(); + + + if (!File.Exists(DataFolder + "Settings.txt")) + { + GlobalSettings = new Settings + { + SecondsToWaitBetweenMessages = 5, + MaxExp = 7, + MinExp = 1 + }; + await DiscordBotCore.Others.JsonManager.SaveToJsonFile(DataFolder + "Settings.txt", GlobalSettings); + } + else + GlobalSettings = await DiscordBotCore.Others.JsonManager.ConvertFromJson(DataFolder + "Settings.txt"); + + if (!await Database.TableExistsAsync("Levels")) + await Database.CreateTableAsync("Levels", "UserID VARCHAR(128)", "Level INT", "EXP INT"); + + if (!await Database.TableExistsAsync("Users")) + await Database.CreateTableAsync("Users", "UserID VARCHAR(128)", "UserMention VARCHAR(128)", "Username VARCHAR(128)", "Discriminator VARCHAR(128)"); + + + + client.MessageReceived += ClientOnMessageReceived; + } + + private async Task ClientOnMessageReceived(SocketMessage arg) + { + if (arg.Author.IsBot || arg.IsTTS || arg.Content.StartsWith(Application.CurrentApplication.ApplicationEnvironmentVariables["prefix"])) + return; + + if (WaitingList.ContainsKey(arg.Author.Id) && WaitingList[arg.Author.Id] > DateTime.Now.AddSeconds(-GlobalSettings.SecondsToWaitBetweenMessages)) + return; + + var userID = arg.Author.Id.ToString(); + + object[] userData = await Database.ReadDataArrayAsync($"SELECT * FROM Levels WHERE userID='{userID}'"); + if (userData is null) + { + await Database.ExecuteAsync($"INSERT INTO Levels (UserID, Level, EXP) VALUES ('{userID}', 1, 0)"); + await Database.ExecuteAsync($"INSERT INTO Users (UserID, UserMention) VALUES ('{userID}', '{arg.Author.Mention}')"); + return; + } + + var level = (int)userData[1]; + var exp = (int)userData[2]; + + + var random = new Random().Next(GlobalSettings.MinExp, GlobalSettings.MaxExp); + if (exp + random >= level * 8 + 24) + { + await Database.ExecuteAsync($"UPDATE Levels SET Level={level + 1}, EXP={random - (level * 8 + 24 - exp)} WHERE UserID='{userID}'"); + await arg.Channel.SendMessageAsync($"{arg.Author.Mention} has leveled up to level {level + 1}!"); + } + else await Database.ExecuteAsync($"UPDATE Levels SET EXP={exp + random} WHERE UserID='{userID}'"); + + WaitingList.Add(arg.Author.Id, DateTime.Now); + } + +} diff --git a/Plugins/LevelingSystem/LevelingSystem.csproj b/Plugins/LevelingSystem/LevelingSystem.csproj new file mode 100644 index 0000000..16c9e56 --- /dev/null +++ b/Plugins/LevelingSystem/LevelingSystem.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/Plugins/LevelingSystem/ReloadAction.cs b/Plugins/LevelingSystem/ReloadAction.cs new file mode 100644 index 0000000..87209e8 --- /dev/null +++ b/Plugins/LevelingSystem/ReloadAction.cs @@ -0,0 +1,20 @@ +using DiscordBotCore.Interfaces; +using DiscordBotCore.Others; +using DiscordBotCore.Others.Actions; + +namespace LevelingSystem; + +public class ReloadAction: ICommandAction +{ + public string ActionName => "LevelingSystemReload"; + public string? Description => "Reloads the Leveling System config file"; + public string? Usage => "LevelingSystemReload"; + public InternalActionRunType RunType => InternalActionRunType.OnCall; + + public IEnumerable ListOfOptions => []; + + public async Task Execute(string[]? args) + { + Variables.GlobalSettings = await JsonManager.ConvertFromJson(Variables.DataFolder + "Settings.txt"); + } +} diff --git a/Plugins/LevelingSystem/Settings.cs b/Plugins/LevelingSystem/Settings.cs new file mode 100644 index 0000000..612997c --- /dev/null +++ b/Plugins/LevelingSystem/Settings.cs @@ -0,0 +1,19 @@ +using DiscordBotCore; +using DiscordBotCore.Database; + +namespace LevelingSystem; + +public class Settings +{ + public int SecondsToWaitBetweenMessages { get; set; } + public int MinExp { get; set; } + public int MaxExp { get; set; } +} + +internal static class Variables +{ + internal static readonly string DataFolder = Application.GetResourceFullPath("LevelingSystem/"); + internal static SqlDatabase? Database; + internal static readonly Dictionary WaitingList = new(); + internal static Settings GlobalSettings = new(); +} diff --git a/Plugins/MusicPlayer/Commands/AddMelody.cs b/Plugins/MusicPlayer/Commands/AddMelody.cs new file mode 100644 index 0000000..802752f --- /dev/null +++ b/Plugins/MusicPlayer/Commands/AddMelody.cs @@ -0,0 +1,96 @@ +using Discord; + +using DiscordBotCore; +using DiscordBotCore.Interfaces; +using DiscordBotCore.Online; +using DiscordBotCore.Others; + +namespace MusicPlayer.Commands; + +public class AddMelody: DBCommand +{ + public string Command => "add_melody"; + + public List? Aliases => new() + { + "madd" + }; + + public string Description => "Add a custom melody to the database"; + public string Usage => "add_melody [title],[description?],[aliases],[byteSize]"; + public bool requireAdmin => false; + + public async void ExecuteServer(DbCommandExecutingArguments args) + { + var arguments = string.Join(" ", args.Arguments); + string[] split = arguments.Split(','); + + if (split.Length < 4) + { + var message = ""; + message += "Invalid arguments given. Please use the following format:\n"; + message += "add_melody [title],[description?],[aliases],[byteSize]\n"; + message += "title: The title of the melody\n"; + message += "description: The description of the melody\n"; + message += "aliases: The aliases of the melody. Use | to separate them\n"; + message += "byteSize: The byte size of the melody. Default is 1024. ( & will use default)\n"; + + await args.Context.Channel.SendMessageAsync(message); + + return; + } + + if (args.Context.Message.Attachments.Count == 0) + { + await args.Context.Channel.SendMessageAsync("You must upload a valid .mp3 audio or .mp4 video file !!"); + return; + } + + var file = args.Context.Message.Attachments.FirstOrDefault(); + if (!(file.Filename.EndsWith(".mp3") || file.Filename.EndsWith(".mp4"))) + { + await args.Context.Channel.SendMessageAsync("Invalid file format !!"); + return; + } + + + var title = split[0]; + var description = split[1]; + string[]? aliases = split[2]?.Split('|') ?? null; + var byteSize = split[3]; + int bsize; + if (!int.TryParse(byteSize, out bsize)) + bsize = 1024; + + + var msg = await args.Context.Channel.SendMessageAsync("Saving melody ..."); + Console.WriteLine("Saving melody"); + + IProgress downloadProgress = new Progress(); + + + var location = Application.GetResourceFullPath($"Music/Melodies/{title}.mp3"); + Directory.CreateDirectory(Application.GetResourceFullPath("Music/Melodies")); + await ServerCom.DownloadFileAsync(file.Url, location, downloadProgress); + + Console.WriteLine($"Done. Saved at {location}"); + + await msg.ModifyAsync(a => a.Content = "Done"); + + + var info = + MusicInfoExtensions.CreateMusicInfo(title, location, description ?? "Unknown", aliases.ToList(), bsize); + + Variables._MusicDatabase?.Add(title, info); + + var builder = new EmbedBuilder(); + builder.Title = "A new music was successfully added !"; + builder.AddField("Title", info.Title); + builder.AddField("Description", info.Description); + builder.AddField("Aliases", string.Join(" | ", aliases)); + await args.Context.Channel.SendMessageAsync(embed: builder.Build()); + + await Variables._MusicDatabase.SaveToFile(); + + } +} diff --git a/Plugins/MusicPlayer/Commands/AddMelodyYoutube.cs b/Plugins/MusicPlayer/Commands/AddMelodyYoutube.cs new file mode 100644 index 0000000..a5e20a8 --- /dev/null +++ b/Plugins/MusicPlayer/Commands/AddMelodyYoutube.cs @@ -0,0 +1,77 @@ +using System.Diagnostics; + +using DiscordBotCore; +using DiscordBotCore.Interfaces; +using DiscordBotCore.Others; + +namespace MusicPlayer.Commands; + +public class AddMelodyYoutube: DBCommand +{ + public string Command => "add_melody_youtube"; + + public List? Aliases => new() + { + "madd-yt" + }; + + public string Description => "Add melody to the database from a youtube link"; + public string Usage => "add_melody_youtube [URL] "; + public bool requireAdmin => true; + + public async void ExecuteServer(DbCommandExecutingArguments args) + { + if (args.Arguments is null) + { + await args.Context.Channel.SendMessageAsync("Invalid arguments given. Please use the following format:\nadd_melody_youtube [URL]"); + return; + } + + + var URL = args.Arguments[0]; + + if (!URL.StartsWith("https://www.youtube.com/watch?v=") && !URL.StartsWith("https://youtu.be/")) + { + await args.Context.Channel.SendMessageAsync("Invalid URL given. Please use the following format:\nadd_melody_youtube [URL]"); + return; + } + + if (args.Arguments.Length <= 1) + { + await args.Channel.SendMessageAsync("Please specify at least one alias for the melody !"); + return; + } + + var msg = await args.Context.Channel.SendMessageAsync("Saving melody ..."); + + var title = await YoutubeDLP.DownloadMelody(URL); + + if (title == null) + { + await msg.ModifyAsync(x => x.Content = "Failed to download melody !"); + return; + } + + var joinedAliases = string.Join(" ", args.Arguments.Skip(1)); + List aliases = joinedAliases.Split('|').ToList(); + + + if (Variables._MusicDatabase.ContainsMelodyWithNameOrAlias(title)) + Variables._MusicDatabase.Remove(title); + + Variables._MusicDatabase.Add(title, new MusicInfo() + { + Aliases = aliases, + ByteSize = 1024, + Description = "Melody added from youtube link", + Location = Application.GetResourceFullPath($"Music/Melodies/{title}.mp3"), + Title = title + } + ); + + + + await Variables._MusicDatabase.SaveToFile(); + await msg.ModifyAsync(x => x.Content = "Melody saved !"); + } +} diff --git a/Plugins/MusicPlayer/Commands/SearchMelody.cs b/Plugins/MusicPlayer/Commands/SearchMelody.cs new file mode 100644 index 0000000..81c9811 --- /dev/null +++ b/Plugins/MusicPlayer/Commands/SearchMelody.cs @@ -0,0 +1,38 @@ +using Discord; +using DiscordBotCore.Interfaces; +using DiscordBotCore.Others; + +namespace MusicPlayer.Commands; + +public class SearchMelody: DBCommand +{ + + public string Command => "search_melody"; + public List? Aliases => null; + public string Description => "Search for a melody in the database"; + public string Usage => "search_melody [melody name OR one of its aliases]"; + public bool requireAdmin => false; + + public void ExecuteServer(DbCommandExecutingArguments args) + { + var title = string.Join(" ", args.Arguments); + + if (string.IsNullOrWhiteSpace(title)) + { + args.Context.Channel.SendMessageAsync("You need to specify a melody name"); + return; + } + + List? info = Variables._MusicDatabase.GetMusicInfoList(title); + if (info == null || info.Count == 0) + { + args.Context.Channel.SendMessageAsync("No melody with that name or alias was found"); + return; + } + + if (info.Count > 1) + args.Context.Channel.SendMessageAsync(embed: info.ToEmbed(Color.DarkOrange)); + else + args.Context.Channel.SendMessageAsync(embed: info[0].ToEmbed(Color.DarkOrange)); + } +} diff --git a/Plugins/MusicPlayer/Events/OnLoad.cs b/Plugins/MusicPlayer/Events/OnLoad.cs new file mode 100644 index 0000000..ea44646 --- /dev/null +++ b/Plugins/MusicPlayer/Events/OnLoad.cs @@ -0,0 +1,27 @@ +using Discord.WebSocket; + +using DiscordBotCore; +using DiscordBotCore.Interfaces; + +namespace MusicPlayer.Events; + +public class OnLoad: DBEvent +{ + private static readonly string _DefaultMusicPath = "Music/"; + private static readonly string _DefaultSaveLocation = "Music/Melodies/"; + private static readonly string _DefaultMusicDB = "Music/music_db.json"; + public string Name => "Music Commands"; + public string Description => "The default music commands event loader"; + public bool RequireOtherThread => false; + + public async void Start(DiscordSocketClient client) + { + var path1 = Application.GetResourceFullPath(_DefaultMusicPath); + var path2 = Application.GetResourceFullPath(_DefaultSaveLocation); + var path3 = Application.GetResourceFullPath(_DefaultMusicDB); + Directory.CreateDirectory(path1); + Directory.CreateDirectory(path2); + Variables._MusicDatabase = new MusicDatabase(path3); + await Variables._MusicDatabase.LoadFromFile(); + } +} diff --git a/Plugins/MusicPlayer/Events/OnVoiceRemoved.cs b/Plugins/MusicPlayer/Events/OnVoiceRemoved.cs new file mode 100644 index 0000000..c833e99 --- /dev/null +++ b/Plugins/MusicPlayer/Events/OnVoiceRemoved.cs @@ -0,0 +1,35 @@ +using Discord.WebSocket; +using DiscordBotCore; +using DiscordBotCore.Interfaces; +using DiscordBotCore.Others; + +namespace MusicPlayer.Events; + +public class OnVoiceRemoved: DBEvent +{ + + public string Name => "Event: OnVoiceRemoved"; + public string Description => "Called when bot leaves a voice channel"; + + public bool RequireOtherThread => false; + + public void Start(DiscordSocketClient client) + { + client.UserVoiceStateUpdated += async (user, oldState, newState) => + { + if (user.Id == client.CurrentUser.Id && newState.VoiceChannel == null) + { + Variables._MusicPlayer?.MusicQueue.Clear(); + Variables._MusicPlayer?.Skip(); + Variables._MusicPlayer?.Stop(); + await Variables.audioClient!.StopAsync(); + Variables.audioClient = null; + Variables._MusicPlayer = null; + + Application.CurrentApplication.Logger.Log("Bot left voice channel.", this, LogType.Info); + } + + }; + } + +} diff --git a/Plugins/MusicPlayer/MusicDatabase.cs b/Plugins/MusicPlayer/MusicDatabase.cs new file mode 100644 index 0000000..748d912 --- /dev/null +++ b/Plugins/MusicPlayer/MusicDatabase.cs @@ -0,0 +1,64 @@ +using DiscordBotCore.Others; + +namespace MusicPlayer; + +public class MusicDatabase: SettingsDictionary +{ + public MusicDatabase(string file): base(file) + { + } + + /// + /// Checks if the database contains a melody with the specified name or alias + /// + /// The name (alias) of the melody + /// + public bool ContainsMelodyWithNameOrAlias(string melodyName) + { + return ContainsKey(melodyName) || Values.Any(elem => elem.Aliases.Contains(melodyName, StringComparer.CurrentCultureIgnoreCase)); + } + + /// + /// Tries to get the music info of a melody with the specified name or alias. Returns the first match or null if no match was found. + /// + /// The music name or one of its aliases. + /// + public MusicInfo? GetMusicInfo(string searchQuery) + { + return FirstOrDefault(kvp => kvp.Key.Contains(searchQuery, StringComparison.InvariantCultureIgnoreCase) || + kvp.Value.Aliases.Any(alias => alias.Contains(searchQuery, StringComparison.InvariantCultureIgnoreCase)) + ).Value; + } + + /// + /// Get a list of music info that match the search query. Returns null if an error occurred, or empty list if no match was found. + /// + /// The search query + /// null if an error occured, otherwise a list with songs that match the search query. If no song match the list is empty + public List? GetMusicInfoList(string searchQuery) + { + try + { + return this.Where(kvp => + kvp.Key.Contains(searchQuery, StringComparison.InvariantCultureIgnoreCase) || + kvp.Value.Aliases.Any(alias => alias.Contains(searchQuery, StringComparison.InvariantCultureIgnoreCase)) + ) + .Select(item => item.Value).ToList(); + } + + catch (Exception ex) + { + Console.WriteLine(ex.ToString()); + return null; + } + } + + /// + /// Adds a new entry to the database based on the music info. It uses the title as the key. + /// + /// The music to add to + public void AddNewEntryBasedOnMusicInfo(MusicInfo musicInfo) + { + Add(musicInfo.Title, musicInfo); + } +} diff --git a/Plugins/MusicPlayer/MusicInfo.cs b/Plugins/MusicPlayer/MusicInfo.cs new file mode 100644 index 0000000..867fc58 --- /dev/null +++ b/Plugins/MusicPlayer/MusicInfo.cs @@ -0,0 +1,10 @@ +namespace MusicPlayer; + +public class MusicInfo +{ + public string Title { get; init; } + public string? Description { get; init; } + public string Location { get; init; } + public List? Aliases { get; init; } + public int? ByteSize { get; init; } = 1024; +} diff --git a/Plugins/MusicPlayer/MusicInfoExtensions.cs b/Plugins/MusicPlayer/MusicInfoExtensions.cs new file mode 100644 index 0000000..36d6d24 --- /dev/null +++ b/Plugins/MusicPlayer/MusicInfoExtensions.cs @@ -0,0 +1,53 @@ +using Discord; + +namespace MusicPlayer; + +public static class MusicInfoExtensions +{ + public static void AddAlias(this MusicInfo musicInfo, string alias) + { + musicInfo.Aliases.Add(alias); + } + + public static void RemoveAlias(this MusicInfo musicInfo, string alias) + { + musicInfo.Aliases.Remove(alias); + } + + public static MusicInfo CreateMusicInfo(string title, string fileLocation, string? Description = "Unknown", List? aliases = null, int? byteSize = 1024) + { + return new MusicInfo() + { + Title = title, + Aliases = aliases, + Description = Description, + Location = fileLocation, + ByteSize = byteSize + }; + } + + public static Embed ToEmbed(this MusicInfo musicInfo, Color? embedColor = null) + { + var builder = new EmbedBuilder(); + builder.Color = embedColor ?? Color.Default; + builder.WithTitle(musicInfo.Title); + builder.WithDescription(musicInfo.Description); + if (musicInfo.Aliases != null) + builder.AddField("Aliases", string.Join(", ", musicInfo.Aliases)); + else + builder.AddField("Aliases", "None"); + builder.AddField("Location", musicInfo.Location); + builder.AddField("ByteSize", musicInfo.ByteSize); + return builder.Build(); + } + + public static Embed ToEmbed(this List musicInfo, Color? embedColor = null) + { + var builder = new EmbedBuilder(); + builder.Color = embedColor ?? Color.Default; + builder.WithTitle("Search results"); + builder.WithDescription("Found " + musicInfo.Count + " results"); + builder.AddField("Results", string.Join("\n", musicInfo.Select(item => item.Title))); + return builder.Build(); + } +} diff --git a/Plugins/MusicPlayer/MusicPlayer.cs b/Plugins/MusicPlayer/MusicPlayer.cs new file mode 100644 index 0000000..1ec9b84 --- /dev/null +++ b/Plugins/MusicPlayer/MusicPlayer.cs @@ -0,0 +1,175 @@ +using System.Diagnostics; +using Discord.Audio; + +using DiscordBotCore; +using DiscordBotCore.Others; + +namespace MusicPlayer; + +public class MusicPlayer +{ + private static int defaultByteSize = 1024; + + public Queue MusicQueue { get; private set; } + + public bool isPaused { get; private set; } + public bool isPlaying { get; private set; } + + private bool isQueueRunning; + public int ByteSize { get; private set; } + + public MusicInfo? CurrentlyPlaying { get; private set; } + + public MusicPlayer() + { + MusicQueue = new Queue(); + } + + public async Task PlayQueue() + { + if (isQueueRunning) + { + Application.CurrentApplication.Logger.Log("Another queue is running !", typeof(MusicPlayer), LogType.Warning); + return; + } + + if (Variables.audioClient is null) + { + Application.CurrentApplication.Logger.Log("Audio Client is null", typeof(MusicPlayer), LogType.Warning); + return; + } + + isQueueRunning = true; + + string? ffmpegPath = await Application.CurrentApplication.PluginManager.GetDependencyLocation("FFMPEG"); + if(ffmpegPath is null) + { + Application.CurrentApplication.Logger.Log("FFMPEG is missing. Please install it and try again.", typeof(MusicPlayer), LogType.Error); + isQueueRunning = false; + return; + } + + ffmpegPath = ffmpegPath.Replace("\\", "/"); + ffmpegPath = Path.GetFullPath(ffmpegPath); + + Console.WriteLine("FFMPEG Path: " + ffmpegPath); + + while (MusicQueue.TryDequeue(out var dequeuedMusic)) + { + CurrentlyPlaying = dequeuedMusic; + await using var dsAudioStream = Variables.audioClient.CreatePCMStream(AudioApplication.Mixed); + using var ffmpeg = CreateStream(ffmpegPath, CurrentlyPlaying.Location); + if (ffmpeg is null) + { + Application.CurrentApplication.Logger.Log($"Failed to start ffmpeg process. FFMPEG is missing or the {CurrentlyPlaying.Location} has an invalid format.", typeof(MusicPlayer), LogType.Error); + continue; + } + await using var ffmpegOut = ffmpeg.StandardOutput.BaseStream; + await PlayCurrentTrack(dsAudioStream, ffmpegOut, CurrentlyPlaying.ByteSize ?? defaultByteSize); + } + isQueueRunning = false; + CurrentlyPlaying = null; + } + + public void Loop(int numberOfTimes) + { + if (CurrentlyPlaying is null) return; + + Queue tempQueue = new(); + for (var i = 0; i < numberOfTimes; i++) + { + tempQueue.Enqueue(CurrentlyPlaying); + } + + foreach (var musicInfo in MusicQueue) + { + tempQueue.Enqueue(musicInfo); + } + + MusicQueue = tempQueue; + } + + private async Task PlayCurrentTrack(Stream discordVoiceChannelStream, Stream fileStreamFfmpeg, int byteSize) + { + if (isPlaying) return; + ByteSize = byteSize; + + isPlaying = true; + isPaused = false; + + while (isPlaying) + { + if (isPaused) continue; + + var bits = new byte[byteSize]; + var read = await fileStreamFfmpeg.ReadAsync(bits, 0, ByteSize); + if (read == 0) break; + + try + { + await discordVoiceChannelStream.WriteAsync(bits, 0, read); + } + catch (Exception ex) + { + Application.CurrentApplication.Logger.LogException(ex, this); + break; + } + } + + + + await discordVoiceChannelStream.FlushAsync(); + await fileStreamFfmpeg.FlushAsync(); + + isPlaying = false; + isPaused = false; + } + + public void Pause() + { + isPaused = true; + } + + public void Unpause() + { + isPaused = false; + } + + public bool Enqueue(string musicName) + { + var minfo = Variables._MusicDatabase.GetMusicInfo(musicName); + if (minfo is null) return false; + + MusicQueue.Enqueue(minfo); + return true; + } + + public void Skip() + { + isPlaying = false; + } + + public void SetVolume(float volume) + { + // set volume + } + + private static Process? CreateStream(string? fileName, string path) + { + return Process.Start(new ProcessStartInfo + { + FileName = fileName, + Arguments = $"-hide_banner -loglevel panic -i \"{path}\" -ac 2 -f s16le -ar 48000 pipe:1", + UseShellExecute = false, + RedirectStandardOutput = true + } + ); + } + + public void Stop() + { + MusicQueue.Clear(); + isPlaying = false; + } + +} diff --git a/Plugins/MusicPlayer/MusicPlayer.csproj b/Plugins/MusicPlayer/MusicPlayer.csproj new file mode 100644 index 0000000..b6abc4c --- /dev/null +++ b/Plugins/MusicPlayer/MusicPlayer.csproj @@ -0,0 +1,25 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + + + + + diff --git a/Plugins/MusicPlayer/SlashCommands/Loop.cs b/Plugins/MusicPlayer/SlashCommands/Loop.cs new file mode 100644 index 0000000..d16bfe6 --- /dev/null +++ b/Plugins/MusicPlayer/SlashCommands/Loop.cs @@ -0,0 +1,54 @@ +using Discord; +using Discord.WebSocket; +using DiscordBotCore.Interfaces; + +namespace MusicPlayer.SlashCommands; + +public class Loop: DBSlashCommand +{ + + public string Name => "loop"; + public string Description => "Loop the current song for a certain amount of times. If no times are specified, it will loop once"; + public bool canUseDM => false; + public bool HasInteraction => false; + + public List Options => new() + { + new() + { + Type = ApplicationCommandOptionType.Integer, + Name = "times", + Description = "How many times to loop the song", + IsRequired = false + } + }; + + + public void ExecuteServer(SocketSlashCommand context) + { + if (Variables._MusicPlayer.CurrentlyPlaying == null) + { + context.RespondAsync("There is nothing playing right now"); + return; + } + + var times = context.Data.Options.FirstOrDefault()?.Value.ToString() ?? "1"; + + if (!int.TryParse(times, out var timesToLoop)) + { + context.RespondAsync("Invalid number"); + return; + } + + if (timesToLoop < 1) + { + context.RespondAsync("You need to specify a number greater than 0"); + return; + } + + Variables._MusicPlayer.Loop(timesToLoop); + + context.RespondAsync($"Looping {Variables._MusicPlayer.CurrentlyPlaying.Title} {timesToLoop} times. Check the queue to see the progress"); + + } +} diff --git a/Plugins/MusicPlayer/SlashCommands/Play.cs b/Plugins/MusicPlayer/SlashCommands/Play.cs new file mode 100644 index 0000000..ea6e8af --- /dev/null +++ b/Plugins/MusicPlayer/SlashCommands/Play.cs @@ -0,0 +1,78 @@ +using Discord; +using Discord.WebSocket; + +using DiscordBotCore; +using DiscordBotCore.Interfaces; +using DiscordBotCore.Others; + +namespace MusicPlayer.SlashCommands; + +public class Play: DBSlashCommand +{ + public string Name => "play"; + public string Description => "Play music command"; + public bool canUseDM => false; + public bool HasInteraction => false; + + public List Options => new() + { + new() + { + IsRequired = true, + Description = "The music name to be played", + Name = "music-name", + Type = ApplicationCommandOptionType.String + } + }; + + + + public async void ExecuteServer(SocketSlashCommand context) + { + var melodyName = context.Data.Options.First().Value as string; + + if (melodyName is null) + { + await context.RespondAsync("Failed to retrieve melody with name " + melodyName); + return; + } + + var melody = Variables._MusicDatabase.GetMusicInfo(melodyName); + if (melody is null) + { + await context.RespondAsync("The searched melody does not exists in the database. Sorry :("); + return; + } + + var user = context.User as IGuildUser; + if (user is null) + { + await context.RespondAsync("Failed to get user data from channel ! Check error log at " + DateTime.Now.ToLongTimeString()); + Application.CurrentApplication.Logger.Log("User is null while trying to convert from context.User to IGuildUser.", typeof(Play), LogType.Error); + return; + } + + var voiceChannel = user.VoiceChannel; + if (voiceChannel is null) + { + await context.RespondAsync("Unknown voice channel. Maybe I do not have permission to join it ?"); + return; + } + + if (Variables.audioClient is null) + { + Variables.audioClient = await voiceChannel.ConnectAsync(true); // self deaf + } + + Variables._MusicPlayer ??= new MusicPlayer(); + + if (!Variables._MusicPlayer.Enqueue(melodyName)) + { + await context.RespondAsync("Failed to enqueue your request. Something went wrong !"); + return; + } + await context.RespondAsync("Enqueued your request"); + + await Variables._MusicPlayer.PlayQueue(); //start queue + } +} diff --git a/Plugins/MusicPlayer/SlashCommands/Queue.cs b/Plugins/MusicPlayer/SlashCommands/Queue.cs new file mode 100644 index 0000000..601e235 --- /dev/null +++ b/Plugins/MusicPlayer/SlashCommands/Queue.cs @@ -0,0 +1,49 @@ +using Discord; +using Discord.WebSocket; +using DiscordBotCore.Interfaces; + +namespace MusicPlayer.SlashCommands; + +public class Queue: DBSlashCommand +{ + public string Name => "queue"; + public string Description => "Queue a melody to play"; + public bool canUseDM => false; + public bool HasInteraction => false; + public List Options => null; + + + public async void ExecuteServer(SocketSlashCommand context) + { + if (Variables._MusicPlayer is null) + { + await context.RespondAsync("No music is currently playing."); + return; + } + + if (Variables._MusicPlayer.MusicQueue.Count == 0 && Variables._MusicPlayer.CurrentlyPlaying == null) + { + await context.RespondAsync("No music is currently playing"); + return; + } + + var builder = new EmbedBuilder() + { + Title = "Music Queue", + Description = "Here is the current music queue", + Color = Color.Blue + }; + + if (Variables._MusicPlayer.CurrentlyPlaying != null) + builder.AddField("Current music", Variables._MusicPlayer.CurrentlyPlaying.Title); + + var i = 1; + foreach (var melody in Variables._MusicPlayer.MusicQueue) + { + builder.AddField($"#{i}", melody.Title); + i++; + } + + await context.RespondAsync(embed: builder.Build()); + } +} diff --git a/Plugins/MusicPlayer/SlashCommands/Skip.cs b/Plugins/MusicPlayer/SlashCommands/Skip.cs new file mode 100644 index 0000000..2cbea24 --- /dev/null +++ b/Plugins/MusicPlayer/SlashCommands/Skip.cs @@ -0,0 +1,35 @@ +using Discord; +using Discord.WebSocket; +using DiscordBotCore.Interfaces; + +namespace MusicPlayer.SlashCommands; + +public class Skip: DBSlashCommand +{ + public string Name => "skip"; + public string Description => "Skip the current melody"; + public bool canUseDM => false; + public bool HasInteraction => false; + public List Options => null; + + public async void ExecuteServer(SocketSlashCommand context) + { + if (Variables._MusicPlayer is null) + { + await context.RespondAsync("No music is currently playing."); + return; + } + + if (Variables._MusicPlayer.MusicQueue.Count == 0 && Variables._MusicPlayer.CurrentlyPlaying == null) + { + await context.RespondAsync("No music is currently playing"); + return; + } + + var melodyTitle = Variables._MusicPlayer.CurrentlyPlaying.Title; + + await context.RespondAsync($"Skipping {melodyTitle} ..."); + Variables._MusicPlayer.Skip(); + await context.ModifyOriginalResponseAsync(x => x.Content = $"Skipped {melodyTitle}"); + } +} diff --git a/Plugins/MusicPlayer/Variables.cs b/Plugins/MusicPlayer/Variables.cs new file mode 100644 index 0000000..a42a1e3 --- /dev/null +++ b/Plugins/MusicPlayer/Variables.cs @@ -0,0 +1,11 @@ +using Discord.Audio; + +namespace MusicPlayer; + +public class Variables +{ + public static MusicDatabase? _MusicDatabase; + public static MusicPlayer? _MusicPlayer; + + public static IAudioClient? audioClient; +} diff --git a/Plugins/MusicPlayer/YoutubeDLP.cs b/Plugins/MusicPlayer/YoutubeDLP.cs new file mode 100644 index 0000000..66ccb47 --- /dev/null +++ b/Plugins/MusicPlayer/YoutubeDLP.cs @@ -0,0 +1,42 @@ +using System.Diagnostics; + +namespace MusicPlayer; + +public class YoutubeDLP +{ + public static async Task DownloadMelody(string url) + { + Console.WriteLine("Downloading melody: " + url); + var process = new Process(); + process.StartInfo.FileName = await DiscordBotCore.Application.CurrentApplication.PluginManager.GetDependencyLocation("yt-dlp"); + process.StartInfo.Arguments = $"-x --force-overwrites -o \"{DiscordBotCore.Application.GetResourceFullPath("/Music/Melodies")}/%(title)s.%(ext)s\" --audio-format mp3 {url}"; + + process.StartInfo.RedirectStandardOutput = true; + var title = ""; + process.OutputDataReceived += (sender, args) => + { + if (args.Data != null) + { + if (args.Data.StartsWith("[ExtractAudio] Destination: ")) + { + title = args.Data.Replace("[ExtractAudio] Destination: ", "").Replace(".mp3", ""); + title = title.Replace("\\", "/"); + title = title.Split('/').Last().Replace(".mp3", "").TrimEnd(); + + Console.WriteLine("Output title: " + title); + + return; + } + + Console.WriteLine(args.Data); + } + }; + + process.Start(); + Console.WriteLine("Waiting for process to exit ..."); + process.BeginOutputReadLine(); + await process.WaitForExitAsync(); + + return title ?? null; + } +} diff --git a/Plugins/MusicPlayer/libs/linux/ffmpeg b/Plugins/MusicPlayer/libs/linux/ffmpeg new file mode 100644 index 0000000..a010cab Binary files /dev/null and b/Plugins/MusicPlayer/libs/linux/ffmpeg differ diff --git a/Plugins/MusicPlayer/libs/linux/libopus.so b/Plugins/MusicPlayer/libs/linux/libopus.so new file mode 100644 index 0000000..ea84275 Binary files /dev/null and b/Plugins/MusicPlayer/libs/linux/libopus.so differ diff --git a/Plugins/MusicPlayer/libs/linux/libsodium.so b/Plugins/MusicPlayer/libs/linux/libsodium.so new file mode 100644 index 0000000..b3fb9f6 Binary files /dev/null and b/Plugins/MusicPlayer/libs/linux/libsodium.so differ diff --git a/Plugins/MusicPlayer/libs/windows/ffmpeg.exe b/Plugins/MusicPlayer/libs/windows/ffmpeg.exe new file mode 100644 index 0000000..8213764 Binary files /dev/null and b/Plugins/MusicPlayer/libs/windows/ffmpeg.exe differ diff --git a/Plugins/MusicPlayer/libs/windows/libopus.dll b/Plugins/MusicPlayer/libs/windows/libopus.dll new file mode 100644 index 0000000..74a8e35 Binary files /dev/null and b/Plugins/MusicPlayer/libs/windows/libopus.dll differ diff --git a/Plugins/MusicPlayer/libs/windows/libsodium.dll b/Plugins/MusicPlayer/libs/windows/libsodium.dll new file mode 100644 index 0000000..cd122b7 Binary files /dev/null and b/Plugins/MusicPlayer/libs/windows/libsodium.dll differ diff --git a/Plugins/MusicPlayer/libs/windows/opus.dll b/Plugins/MusicPlayer/libs/windows/opus.dll new file mode 100644 index 0000000..74a8e35 Binary files /dev/null and b/Plugins/MusicPlayer/libs/windows/opus.dll differ diff --git a/SethDiscordBot.sln b/SethDiscordBot.sln index 5631c4d..f6374f1 100644 --- a/SethDiscordBot.sln +++ b/SethDiscordBot.sln @@ -6,6 +6,20 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscordBot", "DiscordBot\Di EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscordBotCore", "DiscordBotCore\DiscordBotCore.csproj", "{5A99BFC3-EB39-4AEF-8D61-3CE22D013B02}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{5CF9AD7B-6BF0-4035-835F-722F989C01E1}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Modules", "Modules", "{EA4FA308-7B2C-458E-8485-8747D745DD59}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CppWrapper", "Plugins\CppWrapper\CppWrapper.csproj", "{B21FC858-C397-4B0B-BFED-218EFA28E3E1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscordBotUI", "Plugins\DiscordBotUI\DiscordBotUI.csproj", "{E1A93D4E-A541-44B1-9DD1-FEF9B503CFD6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LevelingSystem", "Plugins\LevelingSystem\LevelingSystem.csproj", "{FCE9743F-7EB4-4639-A080-FCDDFCC7D689}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MusicPlayer", "Plugins\MusicPlayer\MusicPlayer.csproj", "{F3C61A47-F758-4145-B496-E3ECCF979D89}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LoggerModule", "Modules\LoggerModule\LoggerModule.csproj", "{367F3197-8B9E-4BDC-A6DE-226E721F9ED1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -20,6 +34,26 @@ Global {5A99BFC3-EB39-4AEF-8D61-3CE22D013B02}.Debug|Any CPU.Build.0 = Debug|Any CPU {5A99BFC3-EB39-4AEF-8D61-3CE22D013B02}.Release|Any CPU.ActiveCfg = Release|Any CPU {5A99BFC3-EB39-4AEF-8D61-3CE22D013B02}.Release|Any CPU.Build.0 = Release|Any CPU + {B21FC858-C397-4B0B-BFED-218EFA28E3E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B21FC858-C397-4B0B-BFED-218EFA28E3E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B21FC858-C397-4B0B-BFED-218EFA28E3E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B21FC858-C397-4B0B-BFED-218EFA28E3E1}.Release|Any CPU.Build.0 = Release|Any CPU + {E1A93D4E-A541-44B1-9DD1-FEF9B503CFD6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E1A93D4E-A541-44B1-9DD1-FEF9B503CFD6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E1A93D4E-A541-44B1-9DD1-FEF9B503CFD6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E1A93D4E-A541-44B1-9DD1-FEF9B503CFD6}.Release|Any CPU.Build.0 = Release|Any CPU + {FCE9743F-7EB4-4639-A080-FCDDFCC7D689}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FCE9743F-7EB4-4639-A080-FCDDFCC7D689}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FCE9743F-7EB4-4639-A080-FCDDFCC7D689}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FCE9743F-7EB4-4639-A080-FCDDFCC7D689}.Release|Any CPU.Build.0 = Release|Any CPU + {F3C61A47-F758-4145-B496-E3ECCF979D89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F3C61A47-F758-4145-B496-E3ECCF979D89}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F3C61A47-F758-4145-B496-E3ECCF979D89}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F3C61A47-F758-4145-B496-E3ECCF979D89}.Release|Any CPU.Build.0 = Release|Any CPU + {367F3197-8B9E-4BDC-A6DE-226E721F9ED1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {367F3197-8B9E-4BDC-A6DE-226E721F9ED1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {367F3197-8B9E-4BDC-A6DE-226E721F9ED1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {367F3197-8B9E-4BDC-A6DE-226E721F9ED1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -27,4 +61,11 @@ Global GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3FB3C5DE-ED21-4D2E-ABDD-3A00EE4A2FFF} EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {B21FC858-C397-4B0B-BFED-218EFA28E3E1} = {5CF9AD7B-6BF0-4035-835F-722F989C01E1} + {E1A93D4E-A541-44B1-9DD1-FEF9B503CFD6} = {5CF9AD7B-6BF0-4035-835F-722F989C01E1} + {FCE9743F-7EB4-4639-A080-FCDDFCC7D689} = {5CF9AD7B-6BF0-4035-835F-722F989C01E1} + {F3C61A47-F758-4145-B496-E3ECCF979D89} = {5CF9AD7B-6BF0-4035-835F-722F989C01E1} + {367F3197-8B9E-4BDC-A6DE-226E721F9ED1} = {EA4FA308-7B2C-458E-8485-8747D745DD59} + EndGlobalSection EndGlobal