diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..cd967fc --- /dev/null +++ b/.dockerignore @@ -0,0 +1,25 @@ +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.idea +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1deecb5..ef93686 100644 --- a/.gitignore +++ b/.gitignore @@ -29,10 +29,8 @@ x86/ bld/ [Bb]in/ [Oo]bj/ -[Oo]ut/ [Ll]og/ [Ll]ogs/ -[Dd]ata/ # Visual Studio 2015/2017 cache/options directory .vs/ @@ -64,6 +62,9 @@ project.lock.json project.fragment.lock.json artifacts/ +# Tye +.tye/ + # ASP.NET Scaffolding ScaffoldingReadMe.txt @@ -98,7 +99,6 @@ StyleCopReport.xml *.pidb *.svclog *.scc -*.code-workspace # Chutzpah Test files _Chutzpah* @@ -364,15 +364,99 @@ MigrationBackup/ # Fody - auto-generated XML schema FodyWeavers.xsd -*.txt +## +## Visual studio for Mac +## -#folders -/Plugins/ -/DiscordBot.rar -/DiscordBot/Data/ -/DiscordBot/Updater/ + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# JetBrains Rider .idea/ -DiscordBot/Launcher.exe -DiscordBotUI/bin -DiscordBotUI/obj -/.vscode +*.sln.iml + +## +## Visual Studio Code +## +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +/DiscordBotWebUI/Data +/DiscordBot/Data +/WebUI/Data +/WebUI_Old/Data +/WebUI/bin +/WebUI_Old/bin +Data/ \ No newline at end of file diff --git a/DiscordBot/Bot/Actions/Clear.cs b/DiscordBot/Bot/Actions/Clear.cs deleted file mode 100644 index 3be31da..0000000 --- a/DiscordBot/Bot/Actions/Clear.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using PluginManager.Interfaces; -using PluginManager.Others; -using PluginManager.Others.Actions; - -namespace DiscordBot.Bot.Actions; - -public class Clear: ICommandAction -{ - public string ActionName => "clear"; - public string Description => "Clears the console"; - public string Usage => "clear"; - public IEnumerable ListOfOptions => []; - - public InternalActionRunType RunType => InternalActionRunType.ON_CALL; - - public Task Execute(string[] args) - { - Console.Clear(); - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine("===== Seth Discord Bot ====="); - Console.ResetColor(); - return Task.CompletedTask; - } -} diff --git a/DiscordBot/Bot/Actions/Exit.cs b/DiscordBot/Bot/Actions/Exit.cs deleted file mode 100644 index 612ec7f..0000000 --- a/DiscordBot/Bot/Actions/Exit.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using PluginManager; -using PluginManager.Interfaces; -using PluginManager.Others; -using PluginManager.Others.Actions; - -namespace DiscordBot.Bot.Actions; - -public class Exit: ICommandAction -{ - public string ActionName => "exit"; - public string Description => "Exits the bot and saves the config. Use exit help for more info."; - public string Usage => "exit "; - public IEnumerable ListOfOptions => new List - { - new InternalActionOption("help", "Displays this message"), - new InternalActionOption("force | -f", "Exits the bot without saving the config") - }; - public InternalActionRunType RunType => InternalActionRunType.ON_CALL; - - public async Task Execute(string[] args) - { - if (args is null || args.Length == 0) - { - Config.Logger.Log("Exiting...", typeof(ICommandAction), LogType.WARNING); - await Config.AppSettings.SaveToFile(); - Environment.Exit(0); - } - else - { - switch (args[0]) - { - case "help": - Console.WriteLine("Usage : exit [help|force]"); - Console.WriteLine("help : Displays this message"); - Console.WriteLine("force | -f : Exits the bot without saving the config"); - break; - - case "-f": - case "force": - Config.Logger.Log("Exiting (FORCE)...", typeof(ICommandAction), LogType.WARNING); - Environment.Exit(0); - break; - - default: - Console.WriteLine("Invalid argument !"); - break; - } - } - } -} diff --git a/DiscordBot/Bot/Actions/Extra/PluginMethods.cs b/DiscordBot/Bot/Actions/Extra/PluginMethods.cs deleted file mode 100644 index 1e71bf2..0000000 --- a/DiscordBot/Bot/Actions/Extra/PluginMethods.cs +++ /dev/null @@ -1,206 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -using DiscordBot.Utilities; - -using PluginManager; -using PluginManager.Interfaces; -using PluginManager.Loaders; -using PluginManager.Online; -using PluginManager.Others; - -using Spectre.Console; - -namespace DiscordBot.Bot.Actions.Extra; - -internal static class PluginMethods -{ - internal static async Task List(PluginsManager manager) - { - var data = await ConsoleUtilities.ExecuteWithProgressBar(manager.GetPluginsList(), "Reading remote database"); - - TableData tableData = new(["Name", "Description", "Version", "Is Installed"]); - - var installedPlugins = await ConsoleUtilities.ExecuteWithProgressBar(manager.GetInstalledPlugins(), "Reading local database "); - - foreach (var plugin in data) - { - bool isInstalled = installedPlugins.Any(p => p.PluginName == plugin.Name); - tableData.AddRow([plugin.Name, plugin.Description, plugin.Version.ToString(), isInstalled ? "Yes" : "No"]); - } - - tableData.HasRoundBorders = false; - tableData.PrintTable(); - } - - internal static async Task RefreshPlugins(bool quiet) - { - await Program.internalActionManager.Execute("plugin", "load", quiet ? "-q" : string.Empty); - await Program.internalActionManager.Refresh(); - } - - internal static async Task DownloadPlugin(PluginsManager manager, string pluginName) - { - var pluginData = await manager.GetPluginDataByName(pluginName); - if (pluginData is null) - { - Console.WriteLine($"Plugin {pluginName} not found. Please check the spelling and try again."); - return; - } - - var pluginLink = pluginData.DownLoadLink; - - - await AnsiConsole.Progress() - .Columns(new ProgressColumn[] - { - new TaskDescriptionColumn(), new ProgressBarColumn(), new PercentageColumn() - } - ) - .StartAsync(async ctx => - { - var downloadTask = ctx.AddTask("Downloading plugin..."); - - IProgress progress = new Progress(p => { downloadTask.Value = p; }); - - await ServerCom.DownloadFileAsync(pluginLink, $"{Config.AppSettings["PluginFolder"]}/{pluginName}.dll", progress); - - downloadTask.Increment(100); - - ctx.Refresh(); - } - ); - - if (!pluginData.HasDependencies) - { - await manager.AppendPluginToDatabase(new PluginManager.Plugin.PluginInfo(pluginName, pluginData.Version, [])); - Console.WriteLine("Finished installing " + pluginName + " successfully"); - await RefreshPlugins(false); - return; - } - - List, string, string>> downloadTasks = new(); - await AnsiConsole.Progress() - .Columns(new ProgressColumn[] - { - new TaskDescriptionColumn(), new ProgressBarColumn(), new PercentageColumn() - } - ) - .StartAsync(async ctx => - { - - - foreach (var dependency in pluginData.Dependencies) - { - var task = ctx.AddTask($"Downloading {dependency.DownloadLocation}: "); - IProgress progress = new Progress(p => - { - task.Value = p; - } - ); - - task.IsIndeterminate = true; - downloadTasks.Add(new Tuple, string, string>(task, progress, dependency.DownloadLink, dependency.DownloadLocation)); - } - - int maxParallelDownloads = 5; - - if (Config.AppSettings.ContainsKey("MaxParallelDownloads")) - maxParallelDownloads = int.Parse(Config.AppSettings["MaxParallelDownloads"]); - - var options = new ParallelOptions() - { - MaxDegreeOfParallelism = maxParallelDownloads, - TaskScheduler = TaskScheduler.Default - }; - - await Parallel.ForEachAsync(downloadTasks, options, async (tuple, token) => - { - tuple.Item1.IsIndeterminate = false; - await ServerCom.DownloadFileAsync(tuple.Item3, $"./{tuple.Item4}", tuple.Item2); - } - ); - - - - } - ); - - await manager.AppendPluginToDatabase(new PluginManager.Plugin.PluginInfo(pluginName, pluginData.Version, pluginData.Dependencies.Select(sep => sep.DownloadLocation).ToList())); - await RefreshPlugins(false); - } - - internal static async Task LoadPlugins(string[] args) - { - var loader = new PluginLoader(Config.DiscordBot.Client); - if (args.Length == 2 && args[1] == "-q") - { - await loader.LoadPlugins(); - return true; - } - - var cc = Console.ForegroundColor; - loader.OnCommandLoaded += (data) => - { - if (data.IsSuccess) - { - Config.Logger.Log("Successfully loaded command : " + data.PluginName, typeof(ICommandAction), - LogType.INFO - ); - } - - else - { - Config.Logger.Log("Failed to load command : " + data.PluginName + " because " + data.ErrorMessage, - typeof(ICommandAction), LogType.ERROR - ); - } - - Console.ForegroundColor = cc; - }; - loader.OnEventLoaded += (data) => - { - if (data.IsSuccess) - { - Config.Logger.Log("Successfully loaded event : " + data.PluginName, typeof(ICommandAction), - LogType.INFO - ); - } - else - { - Config.Logger.Log("Failed to load event : " + data.PluginName + " because " + data.ErrorMessage, - typeof(ICommandAction), LogType.ERROR - ); - } - - Console.ForegroundColor = cc; - }; - - loader.OnSlashCommandLoaded += (data) => - { - if (data.IsSuccess) - { - Config.Logger.Log("Successfully loaded slash command : " + data.PluginName, typeof(ICommandAction), - LogType.INFO - ); - } - else - { - Config.Logger.Log("Failed to load slash command : " + data.PluginName + " because " + data.ErrorMessage, - typeof(ICommandAction), LogType.ERROR - ); - } - - Console.ForegroundColor = cc; - }; - - await loader.LoadPlugins(); - Console.ForegroundColor = cc; - return true; - } - - -} diff --git a/DiscordBot/Bot/Actions/Extra/SettingsConfigExtra.cs b/DiscordBot/Bot/Actions/Extra/SettingsConfigExtra.cs deleted file mode 100644 index 9d78f7b..0000000 --- a/DiscordBot/Bot/Actions/Extra/SettingsConfigExtra.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Linq; -using PluginManager; -using PluginManager.Loaders; - -namespace DiscordBot.Bot.Actions.Extra; - -internal static class SettingsConfigExtra -{ - - internal static void SetSettings(string key, params string[] value) - { - if (key is null) return; - - if (value is null) return; - - if (!Config.AppSettings.ContainsKey(key)) - return; - - Config.AppSettings[key] = string.Join(' ', value); - // Config.AppSettings.SaveToFile().Wait(); - } - - internal static void RemoveSettings(string key) - { - if (key is null) return; - - if (!Config.AppSettings.ContainsKey(key)) - return; - - Config.AppSettings.Remove(key); - } - - internal static void AddSettings(string key, params string[] value) - { - if (key is null) return; - - if (value is null) return; - - if (Config.AppSettings.ContainsKey(key)) - return; - - Config.AppSettings.Add(key, string.Join(' ', value)); - // Config.AppSettings.SaveToFile().Wait(); - } -} diff --git a/DiscordBot/Bot/Actions/Help.cs b/DiscordBot/Bot/Actions/Help.cs deleted file mode 100644 index 1378b62..0000000 --- a/DiscordBot/Bot/Actions/Help.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using DiscordBot.Utilities; -using PluginManager.Interfaces; -using PluginManager.Others; -using PluginManager.Others.Actions; -using Spectre.Console; - -namespace DiscordBot.Bot.Actions; - -public class Help: ICommandAction -{ - public string ActionName => "help"; - - public string Description => "Shows the list of commands and their usage"; - - public string Usage => "help "; - - public IEnumerable ListOfOptions => []; - - public InternalActionRunType RunType => InternalActionRunType.ON_CALL; - - public async Task Execute(string[] args) - { - TableData tableData = new TableData(); - if (args == null || args.Length == 0) - { - - tableData.Columns = ["Command", "Usage", "Description", "Options"]; - - foreach (var a in Program.internalActionManager.Actions) - { - Markup actionName = new Markup($"[bold]{a.Key}[/]"); - Markup usage = new Markup($"[italic]{a.Value.Usage}[/]"); - Markup description = new Markup($"[dim]{a.Value.Description}[/]"); - - if (a.Value.ListOfOptions.Any()) - { - - var optionsTable = new Table(); - optionsTable.AddColumn("Option"); - optionsTable.AddColumn("Description"); - - foreach (var option in a.Value.ListOfOptions) - { - - optionsTable.AddRow(option.OptionName, option.OptionDescription); - } - - tableData.AddRow([actionName, usage, description, optionsTable]); - } - else - { - tableData.AddRow([actionName, usage, description]); - } - - - } - - // render the table - tableData.HasRoundBorders = true; - tableData.DisplayLinesBetweenRows = true; - tableData.PrintTable(); - - - return; - } - - if (!Program.internalActionManager.Actions.ContainsKey(args[0])) - { - Console.WriteLine("Command not found"); - return; - } - - var action = Program.internalActionManager.Actions[args[0]]; - tableData.Columns = ["Command", "Usage", "Description"]; - tableData.AddRow([action.ActionName, action.Usage, action.Description]); - - - tableData.PrintTable(); - } -} diff --git a/DiscordBot/Bot/Actions/Plugin.cs b/DiscordBot/Bot/Actions/Plugin.cs deleted file mode 100644 index be753cf..0000000 --- a/DiscordBot/Bot/Actions/Plugin.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using DiscordBot.Bot.Actions.Extra; -using PluginManager; -using PluginManager.Interfaces; -using PluginManager.Others; -using PluginManager.Others.Actions; - -namespace DiscordBot.Bot.Actions; - -public class Plugin: ICommandAction -{ - private bool pluginsLoaded; - public string ActionName => "plugin"; - public string Description => "Manages plugins. Use plugin help for more info."; - public string Usage => "plugin "; - - public IEnumerable ListOfOptions => new List - { - new InternalActionOption("help", "Displays this message"), - new InternalActionOption("list", "Lists all plugins"), - new InternalActionOption("load", "Loads all plugins"), - new InternalActionOption("install", "Installs a plugin"), - new InternalActionOption("refresh", "Refreshes the plugin list"), - new InternalActionOption("uninstall", "Uninstalls a plugin") - }; - - public InternalActionRunType RunType => InternalActionRunType.ON_CALL; - - public async Task Execute(string[] args) - { - if (args is null || args.Length == 0 || args[0] == "help") - { - Console.WriteLine("Usage : plugin [help|list|load|install]"); - Console.WriteLine("help : Displays this message"); - Console.WriteLine("list : Lists all plugins"); - Console.WriteLine("load : Loads all plugins"); - Console.WriteLine("install : Installs a plugin"); - Console.WriteLine("refresh : Refreshes the plugin list"); - - return; - } - - switch (args[0]) - { - case "refresh": - await PluginMethods.RefreshPlugins(true); - break; - - case "uninstall": - string plugName = string.Join(' ', args, 1, args.Length-1); - bool result = await Config.PluginsManager.MarkPluginToUninstall(plugName); - if(result) - Console.WriteLine($"Marked to uninstall plugin {plugName}. Please restart the bot"); - break; - - case "list": - await PluginMethods.List(Config.PluginsManager); - break; - case "load": - if (pluginsLoaded) - { - Config.Logger.Log("Plugins already loaded", typeof(ICommandAction), LogType.WARNING); - break; - } - - if (Config.DiscordBot is null) - { - Config.Logger.Log("DiscordBot is null", typeof(ICommandAction), LogType.WARNING); - break; - } - - pluginsLoaded = await PluginMethods.LoadPlugins(args); - break; - - case "install": - var pluginName = string.Join(' ', args, 1, args.Length - 1); - if (string.IsNullOrEmpty(pluginName) || pluginName.Length < 2) - { - Console.WriteLine("Please specify a plugin name"); - Console.Write("Plugin name : "); - pluginName = Console.ReadLine(); - if (string.IsNullOrEmpty(pluginName) || pluginName.Length < 2) - { - Console.WriteLine("Invalid plugin name"); - break; - } - } - - await PluginMethods.DownloadPlugin(Config.PluginsManager, pluginName); - break; - } - } -} diff --git a/DiscordBot/Bot/Actions/SettingsConfig.cs b/DiscordBot/Bot/Actions/SettingsConfig.cs deleted file mode 100644 index 54389de..0000000 --- a/DiscordBot/Bot/Actions/SettingsConfig.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using DiscordBot.Bot.Actions.Extra; -using PluginManager; -using PluginManager.Interfaces; -using PluginManager.Others; -using PluginManager.Others.Actions; - -namespace DiscordBot.Bot.Actions; - -public class SettingsConfig: ICommandAction -{ - public string ActionName => "config"; - public string Description => "Change the settings of the bot"; - public string Usage => "config "; - public IEnumerable ListOfOptions => new List - { - new InternalActionOption("help", "Displays this message"), - new InternalActionOption("set", "Set a setting"), - new InternalActionOption("remove", "Remove a setting"), - new InternalActionOption("add", "Add a setting") - }; - public InternalActionRunType RunType => InternalActionRunType.ON_CALL; - public Task Execute(string[] args) - { - if (args is null) - { - foreach (var settings in Config.AppSettings) - Console.WriteLine(settings.Key + ": " + settings.Value); - - return Task.CompletedTask; - } - - switch (args[0]) - { - case "-s": - case "set": - if (args.Length < 3) - return Task.CompletedTask; - SettingsConfigExtra.SetSettings(args[1], args[2..]); - break; - - case "-r": - case "remove": - if (args.Length < 2) - return Task.CompletedTask; - SettingsConfigExtra.RemoveSettings(args[1]); - break; - - case "-a": - case "add": - if (args.Length < 3) - return Task.CompletedTask; - SettingsConfigExtra.AddSettings(args[1], args[2..]); - break; - - case "-h": - case "-help": - Console.WriteLine("Options:"); - Console.WriteLine("-s : Set a setting"); - Console.WriteLine("-r : Remove a setting"); - Console.WriteLine("-a : Add a setting"); - Console.WriteLine("-h: Show this help message"); - break; - - default: - Console.WriteLine("Invalid option"); - return Task.CompletedTask; - } - - - - return Task.CompletedTask; - } -} diff --git a/DiscordBot/Bot/Commands/NormalCommands/Help.cs b/DiscordBot/Bot/Commands/NormalCommands/Help.cs deleted file mode 100644 index c45d2a0..0000000 --- a/DiscordBot/Bot/Commands/NormalCommands/Help.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System.Collections.Generic; -using Discord; -using PluginManager; -using PluginManager.Interfaces; -using PluginManager.Loaders; -using PluginManager.Others; - -namespace DiscordBot.Bot.Commands; - -/// -/// The help command -/// -internal class Help: DBCommand -{ - /// - /// Command name - /// - public string Command => "help"; - - public List Aliases => null; - - /// - /// Command Description - /// - public string Description => "This command allows you to check all loaded commands"; - - /// - /// Command usage - /// - public string Usage => "help "; - - /// - /// Check if the command require administrator to be executed - /// - public bool requireAdmin => false; - - /// - /// The main body of the command - /// - /// The command context - public void ExecuteServer(DbCommandExecutingArguments args) - { - if (args.arguments is not null) - { - var e = GenerateHelpCommand(args.arguments[0]); - if (e is null) - args.context.Channel.SendMessageAsync("Unknown Command " + args.arguments[0]); - else - args.context.Channel.SendMessageAsync(embed: e.Build()); - - - return; - } - - var embedBuilder = new EmbedBuilder(); - - var adminCommands = ""; - var normalCommands = ""; - - foreach (var cmd in PluginLoader.Commands) - if (cmd.requireAdmin) - adminCommands += cmd.Command + " "; - else - normalCommands += cmd.Command + " "; - - - if (adminCommands.Length > 0) - embedBuilder.AddField("Admin Commands", adminCommands); - if (normalCommands.Length > 0) - embedBuilder.AddField("Normal Commands", normalCommands); - args.context.Channel.SendMessageAsync(embed: embedBuilder.Build()); - } - - private EmbedBuilder GenerateHelpCommand(string command) - { - var embedBuilder = new EmbedBuilder(); - var cmd = PluginLoader.Commands.Find(p => p.Command == command || - p.Aliases is not null && p.Aliases.Contains(command) - ); - if (cmd == null) return null; - - embedBuilder.AddField("Usage", Config.AppSettings["prefix"] + cmd.Usage); - embedBuilder.AddField("Description", cmd.Description); - if (cmd.Aliases is null) - return embedBuilder; - embedBuilder.AddField("Alias", cmd.Aliases.Count == 0 ? "-" : string.Join(", ", cmd.Aliases)); - - return embedBuilder; - } -} diff --git a/DiscordBot/Bot/Commands/SlashCommands/Help.cs b/DiscordBot/Bot/Commands/SlashCommands/Help.cs deleted file mode 100644 index b36a443..0000000 --- a/DiscordBot/Bot/Commands/SlashCommands/Help.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Discord; -using Discord.WebSocket; -using PluginManager.Interfaces; -using PluginManager.Loaders; -using PluginManager.Others; - -namespace DiscordBot.Bot.Commands.SlashCommands; - -public class Help: DBSlashCommand -{ - public string Name => "help"; - public string Description => "This command allows you to check all loaded commands"; - public bool canUseDM => true; - - public bool HasInteraction => false; - - public List Options => - new() - { - new SlashCommandOptionBuilder() - .WithName("command") - .WithDescription("The command you want to get help for") - .WithRequired(false) - .WithType(ApplicationCommandOptionType.String) - }; - - public async void ExecuteServer(SocketSlashCommand context) - { - EmbedBuilder embedBuilder = new(); - - embedBuilder.WithTitle("Help Command"); - embedBuilder.WithColor(Functions.RandomColor); - var slashCommands = PluginLoader.SlashCommands; - var options = context.Data.Options; - - //Console.WriteLine("Options: " + options.Count); - if (options is null || options.Count == 0) - foreach (var slashCommand in slashCommands) - embedBuilder.AddField(slashCommand.Name, slashCommand.Description); - - if (options.Count > 0) - { - var commandName = options.First().Value; - var slashCommand = slashCommands.FirstOrDefault(x => x.Name.TrimEnd() == commandName.ToString()); - if (slashCommand is null) - { - await context.RespondAsync("Unknown Command " + commandName); - return; - } - - embedBuilder.AddField("DM Usable:", slashCommand.canUseDM, true) - .WithDescription(slashCommand.Description); - } - - await context.RespondAsync(embed: embedBuilder.Build()); - } -} diff --git a/DiscordBot/DiscordBot.csproj b/DiscordBot/DiscordBot.csproj deleted file mode 100644 index 1372f59..0000000 --- a/DiscordBot/DiscordBot.csproj +++ /dev/null @@ -1,43 +0,0 @@ - - - Exe - net8.0 - disable - - - False - True - 1.0.4.0 - False - 1.0.4.0 - - - none - - - none - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/DiscordBot/Entry.cs b/DiscordBot/Entry.cs deleted file mode 100644 index 2451247..0000000 --- a/DiscordBot/Entry.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.IO; -using System.Reflection; - - -namespace DiscordBot; - -public static class Entry -{ - private static readonly string logo = @" - _____ _ _ _____ _ _ ____ _ - / ____| | | | | | __ \(_) | | | _ \ | | - | (___ ___| |_| |__ | | | |_ ___ ___ ___ _ __ __| | | |_) | ___ | |_ - \___ \ / _ \ __| '_ \ | | | | / __|/ __/ _ \| '__/ _` | | _ < / _ \| __| - ____) | __/ |_| | | | | |__| | \__ \ (_| (_) | | | (_| | | |_) | (_) | |_ - |_____/ \___|\__|_| |_| |_____/|_|___/\___\___/|_| \__,_| |____/ \___/ \__| - - -"; - public static void Main(string[] args) - { - #if DEBUG - if (args.Length == 1 && args[0] == "/purge_plugins" ) - { - foreach (var plugin in Directory.GetFiles("./Data/Plugins", "*.dll", SearchOption.AllDirectories)) - { - File.Delete(plugin); - } - } - - #endif - - Console.ForegroundColor = ConsoleColor.DarkYellow; - Console.WriteLine(logo); - Console.ResetColor(); - - - var currentDomain = AppDomain.CurrentDomain; - currentDomain.AssemblyResolve += LoadFromSameFolder; - - static Assembly LoadFromSameFolder(object sender, ResolveEventArgs args) - { - var folderPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "./Libraries"); - var assemblyPath = Path.Combine(folderPath, new AssemblyName(args.Name).Name + ".dll"); - if (!File.Exists(assemblyPath)) return null; - var assembly = Assembly.LoadFrom(assemblyPath); - - return assembly; - } - - Program.Startup(args); - } -} diff --git a/DiscordBot/Installer.cs b/DiscordBot/Installer.cs deleted file mode 100644 index d38381e..0000000 --- a/DiscordBot/Installer.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using PluginManager; -using System.Threading.Tasks; -using Spectre.Console; - -namespace DiscordBot; - -public static class Installer -{ - private static async Task AskForConfig(string key, string message) - { - var value = AnsiConsole.Ask($"[green]{message}[/]"); - - if (string.IsNullOrWhiteSpace(value)) - { - AnsiConsole.MarkupLine($"Invalid {key} !"); - - Environment.Exit(-20); - } - - Config.AppSettings.Add(key, value); - } - public static async Task GenerateStartupConfig() - { - - if(!Config.AppSettings.ContainsKey("token")) - await AskForConfig("token", "Token:"); - - if(!Config.AppSettings.ContainsKey("prefix")) - await AskForConfig("prefix", "Prefix:"); - - if(!Config.AppSettings.ContainsKey("ServerID")) - await AskForConfig("ServerID", "Server ID:"); - - await Config.AppSettings.SaveToFile(); - - Config.Logger.Log("Config Saved", typeof(Installer)); - } -} diff --git a/DiscordBot/Program.cs b/DiscordBot/Program.cs deleted file mode 100644 index fb3eddd..0000000 --- a/DiscordBot/Program.cs +++ /dev/null @@ -1,144 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Threading.Tasks; -using DiscordBot.Utilities; -using PluginManager.Bot; -using PluginManager.Others; -using PluginManager.Others.Actions; -using Spectre.Console; -using static PluginManager.Config; - -namespace DiscordBot; - -public class Program -{ - public static InternalActionManager internalActionManager; - - /// - /// The main entry point for the application. - /// - public static void Startup(string[] args) - { - PreLoadComponents(args).Wait(); - - if (!AppSettings.ContainsKey("ServerID") || !AppSettings.ContainsKey("token") || !AppSettings.ContainsKey("prefix")) - Installer.GenerateStartupConfig().Wait(); - - HandleInput().Wait(); - } - - /// - /// The main loop for the discord bot - /// - private static void NoGUI() - { - internalActionManager.Initialize().Wait(); - internalActionManager.Execute("plugin", "load").Wait(); - internalActionManager.Refresh().Wait(); - - while (true) - { - var cmd = Console.ReadLine(); - var args = cmd.Split(' '); - var command = args[0]; - args = args.Skip(1).ToArray(); - if (args.Length == 0) - args = null; - - internalActionManager.Execute(command, args).Wait(); // Execute the command - } - } - - /// - /// Start the bot without user interface - /// - /// Returns the bootloader for the Discord Bot - private static async Task StartNoGui() - { - - AnsiConsole.MarkupLine($"[yellow]Running on version: {AppSettings["Version"]}[/]"); - AnsiConsole.MarkupLine("[yellow]Git SethBot: https://github.com/andreitdr/SethDiscordBot [/]"); - AnsiConsole.MarkupLine("[yellow]Git Plugins: https://github.com/andreitdr/SethPlugins [/]"); - - AnsiConsole.MarkupLine("[yellow]Remember to close the bot using the shutdown command ([/][red]exit[/][yellow]) or some settings won't be saved[/]"); - AnsiConsole.MarkupLine($"[yellow]Running on [/][magenta]{(OperatingSystem.IsWindows() ? "Windows" : "Linux")}[/]"); - - AnsiConsole.MarkupLine("[yellow]===== Seth Discord Bot =====[/]"); - - try - { - var token = AppSettings["token"]; - var prefix = AppSettings["prefix"]; - var discordbooter = new Boot(token, prefix); - await discordbooter.Awake(); - } - catch (Exception ex) - { - Logger.Log(ex.ToString(), typeof(Program), LogType.CRITICAL); - } - } - - /// - /// Handle user input arguments from the startup of the application - /// - private static async Task HandleInput() - { - await StartNoGui(); - try - { - internalActionManager = new InternalActionManager(AppSettings["PluginFolder"], "*.dll"); - NoGUI(); - } - catch (IOException ex) - { - if (ex.Message == "No process is on the other end of the pipe." || (uint)ex.HResult == 0x800700E9) - { - Logger.Log("An error occured while closing the bot last time. Please consider closing the bot using the &rexit&c method !\n" + - "There is a risk of losing all data or corruption of the save file, which in some cases requires to reinstall the bot !", - typeof(Program), LogType.ERROR - ); - } - } - } - - private static async Task PreLoadComponents(string[] args) - { - await Initialize(); - - AppSettings["Version"] = Assembly.GetExecutingAssembly().GetName().Version.ToString(); - - PluginManager.Updater.Application.AppUpdater updater = new(); - var update = await updater.CheckForUpdates(); - - if (update != PluginManager.Updater.Application.Update.None) - { - Console.WriteLine($"New update available: {update.UpdateVersion}"); - Console.WriteLine($"Download link: {update.UpdateUrl}"); - Console.WriteLine($"Update notes: {update.UpdateNotes}\n\n"); - - Environment.Exit(0); - } - - Logger.OnLog += (sender, logMessage) => - { - var messageColor = logMessage.Type switch - { - LogType.INFO => "[green]", - LogType.WARNING => "[yellow]", - LogType.ERROR => "[red]", - LogType.CRITICAL => "[red]", - _ => "[white]" - }; - - if (logMessage.Message.Contains('[')) - { - Console.WriteLine(logMessage.Message); - return; - } - - AnsiConsole.MarkupLine($"{messageColor}{logMessage.ThrowTime} {logMessage.Message} [/]"); - }; - } -} diff --git a/DiscordBot/Utilities/Console Utilities.cs b/DiscordBot/Utilities/Console Utilities.cs deleted file mode 100644 index 45b0eed..0000000 --- a/DiscordBot/Utilities/Console Utilities.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Spectre.Console; - -namespace DiscordBot.Utilities; - -public static class ConsoleUtilities -{ - public static async Task ExecuteWithProgressBar(Task function, string message) - { - T result = default; - await AnsiConsole.Progress() - .AutoClear(true) - .Columns(new TaskDescriptionColumn(), new ProgressBarColumn(), new PercentageColumn()) - .StartAsync( - async ctx => - { - var task = ctx.AddTask(message); - task.IsIndeterminate = true; - result = await function; - task.Increment(100); - } - ); - - - return result; - } -} \ No newline at end of file diff --git a/DiscordBot/Utilities/TableData.cs b/DiscordBot/Utilities/TableData.cs deleted file mode 100644 index 27b1e34..0000000 --- a/DiscordBot/Utilities/TableData.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices.ComTypes; -using System.Text; -using System.Threading.Tasks; -using PluginManager.Others; -using Spectre.Console; -using Spectre.Console.Rendering; - -namespace DiscordBot.Utilities -{ - public class TableData - { - public List Columns; - public List[]> Rows; - - public TableData() - { - Columns = new List(); - Rows = new List[]>(); - } - - public TableData(List columns) - { - Columns = columns; - Rows = new List[]>(); - } - - public bool IsEmpty => Rows.Count == 0; - public bool HasRoundBorders { get; set; } = true; - public bool DisplayLinesBetweenRows { get; set; } = false; - - public void AddRow(OneOf[] row) - { - Rows.Add(row); - } - - public void PrintTable() - { - var table = new Table(); - table.Border(this.HasRoundBorders ? TableBorder.Rounded : TableBorder.Square); - table.AddColumns(this.Columns.ToArray()); - table.ShowRowSeparators = DisplayLinesBetweenRows; - foreach (var row in this.Rows) - { - table.AddRow(row.Select(element => element.Match( - (data) => new Markup(data), - (data) => data - ))); - } - - AnsiConsole.Write(table); - } - } -} diff --git a/DiscordBot/builder.bat b/DiscordBot/builder.bat deleted file mode 100644 index f73d6b4..0000000 --- a/DiscordBot/builder.bat +++ /dev/null @@ -1,35 +0,0 @@ -@echo off -echo "Building..." - -echo "Building linux-x64 not self-contained" -dotnet publish -r linux-x64 -p:PublishSingleFile=false --self-contained true -c Release -o ../publish/linux-x64 - -echo "Building win-x64 not self-contained" -dotnet publish -r win-x64 -p:PublishSingleFile=false --self-contained true -c Release -o ../publish/win-x64 - -echo "Building osx-x64 not self-contained" -dotnet publish -r osx-x64 -p:PublishSingleFile=false --self-contained true -c Release -o ../publish/osx-x64 - - -echo "Building linux-x64 self-contained" -dotnet publish -r linux-x64 -p:PublishSingleFile=true --self-contained true -c Release -o ../publish/linux-x64-selfcontained - -echo "Building win-x64 self-contained" -dotnet publish -r win-x64 -p:PublishSingleFile=true --self-contained true -c Release -o ../publish/win-x64-selfcontained - -echo "Building osx-x64 self-contained" -dotnet publish -r osx-x64 -p:PublishSingleFile=true --self-contained true -c Release -o ../publish/osx-x64-selfcontained - -echo "Zipping..." -mkdir ../publish/zip - - -zip -r ../publish/zip/linux-x64.zip ../publish/linux-x64 -zip -r ../publish/zip/win-x64.zip ../publish/win-x64 -zip -r ../publish/zip/osx-x64.zip ../publish/osx-x64 - -zip -r ../publish/zip/linux-x64-selfcontained.zip ../publish/linux-x64-selfcontained -zip -r ../publish/zip/win-x64-selfcontained.zip ../publish/win-x64-selfcontained -zip -r ../publish/zip/osx-x64-selfcontained.zip ../publish/osx-x64-selfcontained - -echo "Done!" \ No newline at end of file diff --git a/DiscordBot/builder.sh b/DiscordBot/builder.sh deleted file mode 100644 index 0e53c37..0000000 --- a/DiscordBot/builder.sh +++ /dev/null @@ -1,36 +0,0 @@ -# All files in this directory will be copied to the root of the container - -echo "Building..." - -echo "Building linux-x64 not self-contained" -dotnet publish -r linux-x64 -p:PublishSingleFile=false --self-contained true -c Release -o ./publish/linux-x64 - -echo "Building win-x64 not self-contained" -dotnet publish -r win-x64 -p:PublishSingleFile=false --self-contained true -c Release -o ./publish/win-x64 - -echo "Building osx-x64 not self-contained" -dotnet publish -r osx-x64 -p:PublishSingleFile=false --self-contained true -c Release -o ./publish/osx-x64 - -#One file per platform -echo "Building linux-x64 self-contained" -dotnet publish -r linux-x64 -p:PublishSingleFile=true --self-contained true -c Release -o ./publish/linux-x64-selfcontained - -echo "Building win-x64 self-contained" -dotnet publish -r win-x64 -p:PublishSingleFile=true --self-contained true -c Release -o ./publish/win-x64-selfcontained - -echo "Building osx-x64 self-contained" -dotnet publish -r osx-x64 -p:PublishSingleFile=true --self-contained true -c Release -o ./publish/osx-x64-selfcontained - -echo "Zipping..." -mkdir ./publish/zip - - -zip -r ./publish/zip/linux-x64.zip ./publish/linux-x64 -zip -r ./publish/zip/win-x64.zip ./publish/win-x64 -zip -r ./publish/zip/osx-x64.zip ./publish/osx-x64 - -zip -r ./publish/zip/linux-x64-selfcontained.zip ./publish/linux-x64-selfcontained -zip -r ./publish/zip/win-x64-selfcontained.zip ./publish/win-x64-selfcontained -zip -r ./publish/zip/osx-x64-selfcontained.zip ./publish/osx-x64-selfcontained - -echo "Done!" \ No newline at end of file diff --git a/DiscordBotCore.Configuration/Configuration.cs b/DiscordBotCore.Configuration/Configuration.cs new file mode 100644 index 0000000..4af9078 --- /dev/null +++ b/DiscordBotCore.Configuration/Configuration.cs @@ -0,0 +1,113 @@ +using DiscordBotCore.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace DiscordBotCore.Configuration; + +public class Configuration : ConfigurationBase +{ + private readonly bool _EnableAutoAddOnGetWithDefault; + private Configuration(ILogger logger, string diskLocation, bool enableAutoAddOnGetWithDefault): base(logger, diskLocation) + { + _EnableAutoAddOnGetWithDefault = enableAutoAddOnGetWithDefault; + } + + public override async Task SaveToFile() + { + var json = JsonConvert.SerializeObject(_InternalDictionary, Formatting.Indented); + await File.WriteAllTextAsync(_DiskLocation, json); + } + + public override T Get(string key, T defaultValue) + { + T value = base.Get(key, defaultValue); + + if (_EnableAutoAddOnGetWithDefault && value.Equals(defaultValue)) + { + Add(key, defaultValue); + } + + return value; + } + + public override List GetList(string key, List defaultValue) + { + List value = base.GetList(key, defaultValue); + + if (_EnableAutoAddOnGetWithDefault && value.All(defaultValue.Contains)) + { + Add(key, defaultValue); + } + + return value; + } + + public override void LoadFromFile() + { + if (!File.Exists(_DiskLocation)) + { + SaveToFile().Wait(); + return; + } + + string jsonContent = File.ReadAllText(_DiskLocation); + var jObject = JsonConvert.DeserializeObject(jsonContent); + + if (jObject is null) + { + SaveToFile().Wait(); + return; + } + + _InternalDictionary.Clear(); + + foreach (var kvp in jObject) + { + AddPairToDictionary(kvp, _InternalDictionary); + } + } + + private void AddPairToDictionary(KeyValuePair kvp, IDictionary dict) + { + if (kvp.Value is JObject nestedJObject) + { + dict[kvp.Key] = nestedJObject.ToObject>(); + + foreach (var nestedKvp in nestedJObject) + { + AddPairToDictionary(nestedKvp, dict[kvp.Key] as Dictionary); + } + } + else if (kvp.Value is JArray nestedJArray) + { + dict[kvp.Key] = nestedJArray.ToObject>(); + } + else + { + if (kvp.Value.Type == JTokenType.Integer) + dict[kvp.Key] = kvp.Value.Value(); + else if (kvp.Value.Type == JTokenType.Float) + dict[kvp.Key] = kvp.Value.Value(); + else if (kvp.Value.Type == JTokenType.Boolean) + dict[kvp.Key] = kvp.Value.Value(); + else if (kvp.Value.Type == JTokenType.String) + dict[kvp.Key] = kvp.Value.Value(); + else if (kvp.Value.Type == JTokenType.Date) + dict[kvp.Key] = kvp.Value.Value(); + else + dict[kvp.Key] = kvp.Value; + } + } + + /// + /// Create a new Settings Dictionary from a file + /// + /// The file location + /// Set this to true if you want to update the dictionary with default values on get + public static Configuration CreateFromFile(ILogger logger, string baseFile, bool enableAutoAddOnGetWithDefault) + { + var settings = new Configuration(logger, baseFile, enableAutoAddOnGetWithDefault); + settings.LoadFromFile(); + return settings; + } +} \ No newline at end of file diff --git a/DiscordBotCore.Configuration/ConfigurationBase.cs b/DiscordBotCore.Configuration/ConfigurationBase.cs new file mode 100644 index 0000000..e2d3a71 --- /dev/null +++ b/DiscordBotCore.Configuration/ConfigurationBase.cs @@ -0,0 +1,167 @@ +using System.Collections; +using System.Net.Mime; +using DiscordBotCore.Logging; + +namespace DiscordBotCore.Configuration; + +public abstract class ConfigurationBase : IConfiguration +{ + protected readonly IDictionary _InternalDictionary = new Dictionary(); + protected readonly string _DiskLocation; + protected readonly ILogger _Logger; + + protected ConfigurationBase(ILogger logger, string diskLocation) + { + this._DiskLocation = diskLocation; + this._Logger = logger; + } + + public virtual void Add(string key, object? value) + { + if (_InternalDictionary.ContainsKey(key)) + return; + + if (value is null) + return; + + _InternalDictionary.Add(key, value); + } + + public virtual void Set(string key, object value) + { + _InternalDictionary[key] = value; + } + + public virtual object Get(string key) + { + return _InternalDictionary[key]; + } + + public virtual T Get(string key, T defaulobject) + { + if (_InternalDictionary.TryGetValue(key, out var value)) + { + return (T)Convert.ChangeType(value, typeof(T)); + } + + return defaulobject; + } + + public virtual T? Get(string key) + { + if (_InternalDictionary.TryGetValue(key, out var value)) + { + return (T)Convert.ChangeType(value, typeof(T)); + } + + return default; + } + + public virtual IDictionary GetDictionary(string key) + { + if (_InternalDictionary.TryGetValue(key, out var value)) + { + if (value is not IDictionary) + { + throw new Exception("The value is not a dictionary"); + } + + var dictionary = new Dictionary(); + foreach (DictionaryEntry item in (IDictionary)value) + { + dictionary.Add((TSubKey)Convert.ChangeType(item.Key, typeof(TSubKey)), (TSubValue)Convert.ChangeType(item.Value, typeof(TSubValue))); + } + + return dictionary; + } + + return new Dictionary(); + } + + public virtual List GetList(string key, List defaulobject) + { + if(_InternalDictionary.TryGetValue(key, out var value)) + { + if (value is not IList) + { + throw new Exception("The value is not a list"); + } + + var list = new List(); + foreach (object? item in (IList)value) + { + list.Add((T)Convert.ChangeType(item, typeof(T))); + } + + return list; + } + + _Logger.Log($"Key '{key}' not found in settings dictionary. Adding default value.", LogType.Warning); + + return defaulobject; + } + + public virtual void Remove(string key) + { + _InternalDictionary.Remove(key); + } + + public virtual IEnumerator> GetEnumerator() + { + return _InternalDictionary.GetEnumerator(); + } + + public virtual void Clear() + { + _InternalDictionary.Clear(); + } + + public virtual bool ContainsKey(string key) + { + return _InternalDictionary.ContainsKey(key); + } + + public virtual IEnumerable> Where(Func, bool> predicate) + { + return _InternalDictionary.Where(predicate); + } + + public virtual IEnumerable> Where(Func, int, bool> predicate) + { + return _InternalDictionary.Where(predicate); + } + + public virtual IEnumerable Where(Func, TResult> selector) + { + return _InternalDictionary.Select(selector); + } + + public virtual IEnumerable Where(Func, int, TResult> selector) + { + return _InternalDictionary.Select(selector); + } + + public virtual KeyValuePair FirstOrDefault(Func, bool> predicate) + { + return _InternalDictionary.FirstOrDefault(predicate); + } + + public virtual KeyValuePair FirstOrDefault() + { + return _InternalDictionary.FirstOrDefault(); + } + + public virtual bool ContainsAllKeys(params string[] keys) + { + return keys.All(ContainsKey); + } + + public virtual bool TryGetValue(string key, out object? value) + { + return _InternalDictionary.TryGetValue(key, out value); + } + + public abstract Task SaveToFile(); + + public abstract void LoadFromFile(); +} \ No newline at end of file diff --git a/DiscordBotCore.Configuration/DiscordBotCore.Configuration.csproj b/DiscordBotCore.Configuration/DiscordBotCore.Configuration.csproj new file mode 100644 index 0000000..51bcd3d --- /dev/null +++ b/DiscordBotCore.Configuration/DiscordBotCore.Configuration.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + AnyCPU;x64;ARM64 + + + + + + + + + + + diff --git a/DiscordBotCore.Configuration/IConfiguration.cs b/DiscordBotCore.Configuration/IConfiguration.cs new file mode 100644 index 0000000..44a2b52 --- /dev/null +++ b/DiscordBotCore.Configuration/IConfiguration.cs @@ -0,0 +1,132 @@ +namespace DiscordBotCore.Configuration; + +public interface IConfiguration +{ + /// + /// Adds an element to the custom settings dictionary + /// + /// The key + /// The value + void Add(string key, object value); + + /// + /// Sets the value of a key in the custom settings dictionary + /// + /// The key + /// The value + void Set(string key, object value); + + /// + /// Gets the value of a key in the custom settings dictionary. If the T type is different then the object type, it will try to convert it. + /// + /// The key + /// The default value to be returned if the searched value is not found + /// The type of the returned value + /// + T Get(string key, T defaultObject); + + /// + /// Gets the value of a key in the custom settings dictionary. If the T type is different then the object type, it will try to convert it. + /// + /// The key + /// The type of the returned value + /// + T? Get(string key); + + /// + /// Get a list of values from the custom settings dictionary + /// + /// The key + /// The default list to be returned if nothing is found + /// The type of the returned value + /// + List GetList(string key, List defaultObject); + + /// + /// Remove a key from the custom settings dictionary + /// + /// The key + void Remove(string key); + + /// + /// Get the enumerator of the custom settings dictionary + /// + /// + IEnumerator> GetEnumerator(); + + /// + /// Clear the custom settings dictionary + /// + void Clear(); + + /// + /// Check if the custom settings dictionary contains a key + /// + /// The key + /// + bool ContainsKey(string key); + + /// + /// Filter the custom settings dictionary based on a predicate + /// + /// The predicate + /// + IEnumerable> Where(Func, bool> predicate); + + /// + /// Filter the custom settings dictionary based on a predicate + /// + /// The predicate + IEnumerable> Where(Func, int, bool> predicate); + + /// + /// Filter the custom settings dictionary based on a predicate + /// + /// The predicate + IEnumerable Where(Func, TResult> selector); + + /// + /// Filter the custom settings dictionary based on a predicate + /// + /// The predicate + IEnumerable Where(Func, int, TResult> selector); + + /// + /// Get the first element of the custom settings dictionary based on a predicate + /// + /// The predicate + KeyValuePair FirstOrDefault(Func, bool> predicate); + + /// + /// Get the first element of the custom settings dictionary + /// + /// + KeyValuePair FirstOrDefault(); + + /// + /// Checks if the custom settings dictionary contains all the keys + /// + /// A list of keys + /// + bool ContainsAllKeys(params string[] keys); + + /// + /// Try to get the value of a key in the custom settings dictionary + /// + /// The key + /// The value + /// + bool TryGetValue(string key, out object? value); + + /// + /// Save the custom settings dictionary to a file + /// + /// + Task SaveToFile(); + + /// + /// Load the custom settings dictionary from a file + /// + /// + void LoadFromFile(); +} \ No newline at end of file diff --git a/DiscordBotCore.Database.Sqlite/DiscordBotCore.Database.Sqlite.csproj b/DiscordBotCore.Database.Sqlite/DiscordBotCore.Database.Sqlite.csproj new file mode 100644 index 0000000..3790f79 --- /dev/null +++ b/DiscordBotCore.Database.Sqlite/DiscordBotCore.Database.Sqlite.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + AnyCPU;x64;ARM64 + + + + + + + diff --git a/PluginManager/Database/SqlDatabase.cs b/DiscordBotCore.Database.Sqlite/SqlDatabase.cs similarity index 81% rename from PluginManager/Database/SqlDatabase.cs rename to DiscordBotCore.Database.Sqlite/SqlDatabase.cs index 943e04f..bdf1e6f 100644 --- a/PluginManager/Database/SqlDatabase.cs +++ b/DiscordBotCore.Database.Sqlite/SqlDatabase.cs @@ -1,28 +1,20 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Data.SQLite; -using System.IO; -using System.Threading.Tasks; +using System.Data; +using Microsoft.Data.Sqlite; -namespace PluginManager.Database; +namespace DiscordBotCore.Database.Sqlite; public class SqlDatabase { - private readonly SQLiteConnection _connection; + private readonly SqliteConnection _Connection; /// - /// Initialize a SQL connection by specifing its private path + /// Initialize a SQL connection by specifying its private path /// /// The path to the database (it is starting from ./Data/Resources/) public SqlDatabase(string fileName) { - if (!fileName.StartsWith("./Data/Resources/")) - fileName = Path.Combine("./Data/Resources", fileName); - if (!File.Exists(fileName)) - SQLiteConnection.CreateFile(fileName); - var connectionString = $"URI=file:{fileName}"; - _connection = new SQLiteConnection(connectionString); + var connectionString = $"Data Source={fileName}"; + _Connection = new SqliteConnection(connectionString); } @@ -32,7 +24,7 @@ public class SqlDatabase /// public async Task Open() { - await _connection.OpenAsync(); + await _Connection.OpenAsync(); } /// @@ -55,7 +47,7 @@ public class SqlDatabase query += ")"; - var command = new SQLiteCommand(query, _connection); + var command = new SqliteCommand(query, _Connection); await command.ExecuteNonQueryAsync(); } @@ -79,7 +71,7 @@ public class SqlDatabase query += ")"; - var command = new SQLiteCommand(query, _connection); + var command = new SqliteCommand(query, _Connection); command.ExecuteNonQuery(); } @@ -94,7 +86,7 @@ public class SqlDatabase { var query = $"DELETE FROM {tableName} WHERE {KeyName} = '{KeyValue}'"; - var command = new SQLiteCommand(query, _connection); + var command = new SqliteCommand(query, _Connection); await command.ExecuteNonQueryAsync(); } @@ -109,7 +101,7 @@ public class SqlDatabase { var query = $"DELETE FROM {tableName} WHERE {KeyName} = '{KeyValue}'"; - var command = new SQLiteCommand(query, _connection); + var command = new SqliteCommand(query, _Connection); command.ExecuteNonQuery(); } @@ -225,7 +217,7 @@ public class SqlDatabase /// public async void Stop() { - await _connection.CloseAsync(); + await _Connection.CloseAsync(); } /// @@ -237,7 +229,7 @@ public class SqlDatabase /// public async Task AddColumnsToTableAsync(string tableName, string[] columns, string TYPE = "TEXT") { - var command = _connection.CreateCommand(); + var command = _Connection.CreateCommand(); command.CommandText = $"SELECT * FROM {tableName}"; var reader = await command.ExecuteReaderAsync(); var tableColumns = new List(); @@ -261,7 +253,7 @@ public class SqlDatabase /// public void AddColumnsToTable(string tableName, string[] columns, string TYPE = "TEXT") { - var command = _connection.CreateCommand(); + var command = _Connection.CreateCommand(); command.CommandText = $"SELECT * FROM {tableName}"; var reader = command.ExecuteReader(); var tableColumns = new List(); @@ -283,7 +275,7 @@ public class SqlDatabase /// True if the table exists, false if not public async Task TableExistsAsync(string tableName) { - var cmd = _connection.CreateCommand(); + var cmd = _Connection.CreateCommand(); cmd.CommandText = $"SELECT name FROM sqlite_master WHERE type='table' AND name='{tableName}'"; var result = await cmd.ExecuteScalarAsync(); @@ -299,7 +291,7 @@ public class SqlDatabase /// True if the table exists, false if not public bool TableExists(string tableName) { - var cmd = _connection.CreateCommand(); + var cmd = _Connection.CreateCommand(); cmd.CommandText = $"SELECT name FROM sqlite_master WHERE type='table' AND name='{tableName}'"; var result = cmd.ExecuteScalar(); @@ -316,7 +308,7 @@ public class SqlDatabase /// public async Task CreateTableAsync(string tableName, params string[] columns) { - var cmd = _connection.CreateCommand(); + var cmd = _Connection.CreateCommand(); cmd.CommandText = $"CREATE TABLE IF NOT EXISTS {tableName} ({string.Join(", ", columns)})"; await cmd.ExecuteNonQueryAsync(); } @@ -329,7 +321,7 @@ public class SqlDatabase /// public void CreateTable(string tableName, params string[] columns) { - var cmd = _connection.CreateCommand(); + var cmd = _Connection.CreateCommand(); cmd.CommandText = $"CREATE TABLE IF NOT EXISTS {tableName} ({string.Join(", ", columns)})"; cmd.ExecuteNonQuery(); } @@ -341,9 +333,9 @@ public class SqlDatabase /// The number of rows that the query modified public async Task ExecuteAsync(string query) { - if (!_connection.State.HasFlag(ConnectionState.Open)) - await _connection.OpenAsync(); - var command = new SQLiteCommand(query, _connection); + if (!_Connection.State.HasFlag(ConnectionState.Open)) + await _Connection.OpenAsync(); + var command = new SqliteCommand(query, _Connection); var answer = await command.ExecuteNonQueryAsync(); return answer; } @@ -355,9 +347,9 @@ public class SqlDatabase /// The number of rows that the query modified public int Execute(string query) { - if (!_connection.State.HasFlag(ConnectionState.Open)) - _connection.Open(); - var command = new SQLiteCommand(query, _connection); + if (!_Connection.State.HasFlag(ConnectionState.Open)) + _Connection.Open(); + var command = new SqliteCommand(query, _Connection); var r = command.ExecuteNonQuery(); return r; @@ -370,9 +362,9 @@ public class SqlDatabase /// The result is a string that has all values separated by space character public async Task ReadDataAsync(string query) { - if (!_connection.State.HasFlag(ConnectionState.Open)) - await _connection.OpenAsync(); - var command = new SQLiteCommand(query, _connection); + if (!_Connection.State.HasFlag(ConnectionState.Open)) + await _Connection.OpenAsync(); + var command = new SqliteCommand(query, _Connection); var reader = await command.ExecuteReaderAsync(); var values = new object[reader.FieldCount]; @@ -385,6 +377,37 @@ public class SqlDatabase return null; } + /// + /// Read data from the result table and return the first row + /// + /// The query + /// The parameters of the query + /// The result is a string that has all values separated by space character + public async Task ReadDataAsync(string query, params KeyValuePair[] parameters) + { + if (!_Connection.State.HasFlag(ConnectionState.Open)) + await _Connection.OpenAsync(); + + var command = new SqliteCommand(query, _Connection); + foreach (var parameter in parameters) + { + var p = CreateParameter(parameter); + if (p is not null) + command.Parameters.Add(p); + } + + var reader = await command.ExecuteReaderAsync(); + + var values = new object[reader.FieldCount]; + if (reader.Read()) + { + reader.GetValues(values); + return string.Join(" ", values); + } + + return null; + } + /// /// Read data from the result table and return the first row /// @@ -392,9 +415,9 @@ public class SqlDatabase /// The result is a string that has all values separated by space character public string? ReadData(string query) { - if (!_connection.State.HasFlag(ConnectionState.Open)) - _connection.Open(); - var command = new SQLiteCommand(query, _connection); + if (!_Connection.State.HasFlag(ConnectionState.Open)) + _Connection.Open(); + var command = new SqliteCommand(query, _Connection); var reader = command.ExecuteReader(); var values = new object[reader.FieldCount]; @@ -414,9 +437,9 @@ public class SqlDatabase /// The first row as separated items public async Task ReadDataArrayAsync(string query) { - if (!_connection.State.HasFlag(ConnectionState.Open)) - await _connection.OpenAsync(); - var command = new SQLiteCommand(query, _connection); + if (!_Connection.State.HasFlag(ConnectionState.Open)) + await _Connection.OpenAsync(); + var command = new SqliteCommand(query, _Connection); var reader = await command.ExecuteReaderAsync(); var values = new object[reader.FieldCount]; @@ -429,7 +452,32 @@ public class SqlDatabase return null; } + public async Task ReadDataArrayAsync(string query, params KeyValuePair[] parameters) + { + if (!_Connection.State.HasFlag(ConnectionState.Open)) + await _Connection.OpenAsync(); + var command = new SqliteCommand(query, _Connection); + foreach (var parameter in parameters) + { + var p = CreateParameter(parameter); + if (p is not null) + command.Parameters.Add(p); + } + + var reader = await command.ExecuteReaderAsync(); + + var values = new object[reader.FieldCount]; + if (reader.Read()) + { + reader.GetValues(values); + return values; + } + + return null; + + } + /// /// Read data from the result table and return the first row /// @@ -437,9 +485,9 @@ public class SqlDatabase /// The first row as separated items public object[]? ReadDataArray(string query) { - if (!_connection.State.HasFlag(ConnectionState.Open)) - _connection.Open(); - var command = new SQLiteCommand(query, _connection); + if (!_Connection.State.HasFlag(ConnectionState.Open)) + _Connection.Open(); + var command = new SqliteCommand(query, _Connection); var reader = command.ExecuteReader(); var values = new object[reader.FieldCount]; @@ -460,9 +508,9 @@ public class SqlDatabase /// A list of string arrays representing the values that the query returns public async Task?> ReadAllRowsAsync(string query) { - if (!_connection.State.HasFlag(ConnectionState.Open)) - await _connection.OpenAsync(); - var command = new SQLiteCommand(query, _connection); + if (!_Connection.State.HasFlag(ConnectionState.Open)) + await _Connection.OpenAsync(); + var command = new SqliteCommand(query, _Connection); var reader = await command.ExecuteReaderAsync(); if (!reader.HasRows) @@ -487,9 +535,10 @@ public class SqlDatabase /// The name of the parameter /// The value of the parameter /// The SQLiteParameter that has the name, value and DBType set according to your inputs - private SQLiteParameter? CreateParameter(string name, object value) + private static SqliteParameter? CreateParameter(string name, object value) { - var parameter = new SQLiteParameter(name); + var parameter = new SqliteParameter(); + parameter.ParameterName = name; parameter.Value = value; if (value is string) @@ -544,7 +593,7 @@ public class SqlDatabase /// /// The parameter raw inputs. The Key is name and the Value is the value of the parameter /// The SQLiteParameter that has the name, value and DBType set according to your inputs - private SQLiteParameter? CreateParameter(KeyValuePair parameterValues) + private static SqliteParameter? CreateParameter(KeyValuePair parameterValues) { return CreateParameter(parameterValues.Key, parameterValues.Value); } @@ -557,10 +606,10 @@ public class SqlDatabase /// The number of rows that the query modified in the database public async Task ExecuteNonQueryAsync(string query, params KeyValuePair[] parameters) { - if (!_connection.State.HasFlag(ConnectionState.Open)) - await _connection.OpenAsync(); + if (!_Connection.State.HasFlag(ConnectionState.Open)) + await _Connection.OpenAsync(); - var command = new SQLiteCommand(query, _connection); + var command = new SqliteCommand(query, _Connection); foreach (var parameter in parameters) { var p = CreateParameter(parameter); @@ -581,10 +630,10 @@ public class SqlDatabase /// An object of type T that represents the output of the convertor function based on the array of objects that the first row of the result has public async Task ReadObjectOfTypeAsync(string query, Func convertor, params KeyValuePair[] parameters) { - if (!_connection.State.HasFlag(ConnectionState.Open)) - await _connection.OpenAsync(); + if (!_Connection.State.HasFlag(ConnectionState.Open)) + await _Connection.OpenAsync(); - var command = new SQLiteCommand(query, _connection); + var command = new SqliteCommand(query, _Connection); foreach (var parameter in parameters) { var p = CreateParameter(parameter); @@ -615,10 +664,10 @@ public class SqlDatabase public async Task> ReadListOfTypeAsync(string query, Func convertor, params KeyValuePair[] parameters) { - if (!_connection.State.HasFlag(ConnectionState.Open)) - await _connection.OpenAsync(); + if (!_Connection.State.HasFlag(ConnectionState.Open)) + await _Connection.OpenAsync(); - var command = new SQLiteCommand(query, _connection); + var command = new SqliteCommand(query, _Connection); foreach (var parameter in parameters) { var p = CreateParameter(parameter); diff --git a/DiscordBotCore.Logging/DiscordBotCore.Logging.csproj b/DiscordBotCore.Logging/DiscordBotCore.Logging.csproj new file mode 100644 index 0000000..e25dbbd --- /dev/null +++ b/DiscordBotCore.Logging/DiscordBotCore.Logging.csproj @@ -0,0 +1,10 @@ + + + + net8.0 + enable + enable + AnyCPU;x64;ARM64 + + + diff --git a/DiscordBotCore.Logging/ILogMessage.cs b/DiscordBotCore.Logging/ILogMessage.cs new file mode 100644 index 0000000..b8558bf --- /dev/null +++ b/DiscordBotCore.Logging/ILogMessage.cs @@ -0,0 +1,10 @@ +namespace DiscordBotCore.Logging; + +public interface ILogMessage +{ + public string Message { get; protected set; } + public DateTime ThrowTime { get; protected set; } + public string SenderName { get; protected set; } + public LogType LogMessageType { get; protected set; } + +} \ No newline at end of file diff --git a/DiscordBotCore.Logging/ILogger.cs b/DiscordBotCore.Logging/ILogger.cs new file mode 100644 index 0000000..de0c04a --- /dev/null +++ b/DiscordBotCore.Logging/ILogger.cs @@ -0,0 +1,13 @@ +namespace DiscordBotCore.Logging; + +public interface ILogger +{ + List LogMessages { get; protected set; } + event Action? OnLogReceived; + + void Log(string message); + void Log(string message, LogType logType); + void Log(string message, object sender); + void Log(string message, object sender, LogType type); + void LogException(Exception exception, object sender, bool logFullStack = false); +} \ No newline at end of file diff --git a/DiscordBotCore.Logging/LogMessage.cs b/DiscordBotCore.Logging/LogMessage.cs new file mode 100644 index 0000000..ab3c05a --- /dev/null +++ b/DiscordBotCore.Logging/LogMessage.cs @@ -0,0 +1,75 @@ +namespace DiscordBotCore.Logging; + +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; + } +} \ No newline at end of file diff --git a/DiscordBotCore.Logging/LogType.cs b/DiscordBotCore.Logging/LogType.cs new file mode 100644 index 0000000..ab16c6e --- /dev/null +++ b/DiscordBotCore.Logging/LogType.cs @@ -0,0 +1,9 @@ +namespace DiscordBotCore.Logging; + +public enum LogType +{ + Info, + Warning, + Error, + Critical +} \ No newline at end of file diff --git a/DiscordBotCore.Logging/Logger.cs b/DiscordBotCore.Logging/Logger.cs new file mode 100644 index 0000000..ab681f8 --- /dev/null +++ b/DiscordBotCore.Logging/Logger.cs @@ -0,0 +1,62 @@ +namespace DiscordBotCore.Logging; + +public sealed class Logger : ILogger +{ + private readonly string _LogFile; + private readonly string _LogMessageFormat; + private readonly int _MaxHistorySize; + + private readonly List _logMessageProperties = typeof(ILogMessage) + .GetProperties() + .Select(p => p.Name) + .ToList(); + + + public List LogMessages { get; set; } + public event Action? OnLogReceived; + + + public Logger(string logFolder, string logMessageFormat, int maxHistorySize) + { + this._LogMessageFormat = logMessageFormat; + this._LogFile = Path.Combine(logFolder, $"{DateTime.Now:yyyy-MM-dd}.log"); + this._MaxHistorySize = maxHistorySize; + LogMessages = new List(); + } + + 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()); + } + + return messageAsString; + } + + private async Task LogToFile(string message) + { + await using var streamWriter = new StreamWriter(_LogFile, true); + await streamWriter.WriteLineAsync(message); + } + + private async void Log(ILogMessage message) + { + var messageAsString = GenerateLogMessage(message); + OnLogReceived?.Invoke(message); + LogMessages.Add(message); + if (LogMessages.Count > _MaxHistorySize) + { + LogMessages.RemoveAt(0); + } + await LogToFile(messageAsString); + } + + public void Log(string message) => Log(new LogMessage(message, string.Empty, LogType.Info)); + 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)); +} diff --git a/DiscordBotCore.Networking/DiscordBotCore.Networking.csproj b/DiscordBotCore.Networking/DiscordBotCore.Networking.csproj new file mode 100644 index 0000000..e25dbbd --- /dev/null +++ b/DiscordBotCore.Networking/DiscordBotCore.Networking.csproj @@ -0,0 +1,10 @@ + + + + net8.0 + enable + enable + AnyCPU;x64;ARM64 + + + diff --git a/DiscordBotCore.Networking/FileDownloader.cs b/DiscordBotCore.Networking/FileDownloader.cs new file mode 100644 index 0000000..78a5784 --- /dev/null +++ b/DiscordBotCore.Networking/FileDownloader.cs @@ -0,0 +1,25 @@ +using DiscordBotCore.Networking.Helpers; + +namespace DiscordBotCore.Networking; + +public class FileDownloader +{ + private readonly string _DownloadUrl; + private readonly string _DownloadLocation; + + private readonly HttpClient _HttpClient; + + public FileDownloader(string downloadUrl, string downloadLocation) + { + _DownloadUrl = downloadUrl; + _DownloadLocation = downloadLocation; + + _HttpClient = new HttpClient(); + } + + public async Task DownloadFile(Action progressCallback) + { + await using var fileStream = new FileStream(_DownloadLocation, FileMode.Create, FileAccess.Write, FileShare.None); + await _HttpClient.DownloadFileAsync(_DownloadUrl, fileStream, new Progress(progressCallback)); + } +} \ No newline at end of file diff --git a/PluginManager/Online/Helpers/OnlineFunctions.cs b/DiscordBotCore.Networking/Helpers/OnlineFunctions.cs similarity index 55% rename from PluginManager/Online/Helpers/OnlineFunctions.cs rename to DiscordBotCore.Networking/Helpers/OnlineFunctions.cs index 0762d14..9f4d281 100644 --- a/PluginManager/Online/Helpers/OnlineFunctions.cs +++ b/DiscordBotCore.Networking/Helpers/OnlineFunctions.cs @@ -1,14 +1,45 @@ -using System; -using System.IO; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using PluginManager.Others; - -namespace PluginManager.Online.Helpers; +namespace DiscordBotCore.Networking.Helpers; internal static class OnlineFunctions { + + /// + /// Copy one Stream to another + /// + /// The base stream + /// The destination stream + /// The buffer to read + /// The progress + /// The cancellation token + /// Triggered if any is empty + /// Triggered if is less then or equal to 0 + /// Triggered if is not readable + /// Triggered in is not writable + private static async Task CopyToOtherStreamAsync( + this Stream stream, Stream destination, int bufferSize, + IProgress? progress = null, + CancellationToken cancellationToken = default) + { + if (stream == null) throw new ArgumentNullException(nameof(stream)); + if (destination == null) throw new ArgumentNullException(nameof(destination)); + if (bufferSize <= 0) throw new ArgumentOutOfRangeException(nameof(bufferSize)); + if (!stream.CanRead) throw new InvalidOperationException("The stream is not readable."); + if (!destination.CanWrite) + throw new ArgumentException("Destination stream is not writable", nameof(destination)); + + var buffer = new byte[bufferSize]; + long totalBytesRead = 0; + int bytesRead; + while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken) + .ConfigureAwait(false)) != 0) + { + await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false); + totalBytesRead += bytesRead; + progress?.Report(totalBytesRead); + } + } + + /// /// Downloads a and saves it to another . /// @@ -57,16 +88,4 @@ internal static class OnlineFunctions } } } - - /// - /// Read contents of a file as string from specified URL - /// - /// The URL to read from - /// The cancellation token - /// - internal static async Task DownloadStringAsync(string url, CancellationToken cancellation = default) - { - using var client = new HttpClient(); - return await client.GetStringAsync(url, cancellation); - } } diff --git a/DiscordBotCore.Networking/ParallelDownloadExecutor.cs b/DiscordBotCore.Networking/ParallelDownloadExecutor.cs new file mode 100644 index 0000000..e6e130b --- /dev/null +++ b/DiscordBotCore.Networking/ParallelDownloadExecutor.cs @@ -0,0 +1,89 @@ +using DiscordBotCore.Networking.Helpers; + +namespace DiscordBotCore.Networking; + +public class ParallelDownloadExecutor +{ + private readonly List _listOfTasks; + private readonly HttpClient _httpClient; + private Action? OnFinishAction { get; set; } + + public ParallelDownloadExecutor(List listOfTasks) + { + _httpClient = new HttpClient(); + _listOfTasks = listOfTasks; + } + + public ParallelDownloadExecutor() + { + _httpClient = new HttpClient(); + _listOfTasks = new List(); + } + + public async Task StartTasks() + { + await Task.WhenAll(_listOfTasks); + OnFinishAction?.Invoke(); + } + + public async Task ExecuteAllTasks(int maxDegreeOfParallelism = 4) + { + using var semaphore = new SemaphoreSlim(maxDegreeOfParallelism); + + var tasks = _listOfTasks.Select(async task => + { + await semaphore.WaitAsync(); + try + { + await task; + } + finally + { + semaphore.Release(); + } + }); + + await Task.WhenAll(tasks); + OnFinishAction?.Invoke(); + } + + public void SetFinishAction(Action action) + { + OnFinishAction = action; + } + + public void AddTask(string downloadLink, string downloadLocation) + { + if (string.IsNullOrEmpty(downloadLink) || string.IsNullOrEmpty(downloadLocation)) + throw new ArgumentException("Download link or location cannot be null or empty."); + + if (Directory.Exists(Path.GetDirectoryName(downloadLocation)) == false) + { + Directory.CreateDirectory(Path.GetDirectoryName(downloadLocation)); + } + + var task = CreateDownloadTask(downloadLink, downloadLocation, null); + _listOfTasks.Add(task); + } + + public void AddTask(string downloadLink, string downloadLocation, Action progressCallback) + { + if (string.IsNullOrEmpty(downloadLink) || string.IsNullOrEmpty(downloadLocation)) + throw new ArgumentException("Download link or location cannot be null or empty."); + + if (Directory.Exists(Path.GetDirectoryName(downloadLocation)) == false) + { + Directory.CreateDirectory(Path.GetDirectoryName(downloadLocation)); + } + + var task = CreateDownloadTask(downloadLink, downloadLocation, new Progress(progressCallback)); + _listOfTasks.Add(task); + } + + private Task CreateDownloadTask(string downloadLink, string downloadLocation, IProgress progress) + { + var fileStream = new FileStream(downloadLocation, FileMode.Create, FileAccess.Write, FileShare.None); + return _httpClient.DownloadFileAsync(downloadLink, fileStream, progress); + } + +} \ No newline at end of file diff --git a/DiscordBotCore.PluginCore/DiscordBotCore.PluginCore.csproj b/DiscordBotCore.PluginCore/DiscordBotCore.PluginCore.csproj new file mode 100644 index 0000000..c4ad51a --- /dev/null +++ b/DiscordBotCore.PluginCore/DiscordBotCore.PluginCore.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + enable + AnyCPU;x64;ARM64 + + + + + + + + + + + + + + + diff --git a/DiscordBotCore.PluginCore/Helpers/Execution/DbCommand/DbCommandExecutingArgument.cs b/DiscordBotCore.PluginCore/Helpers/Execution/DbCommand/DbCommandExecutingArgument.cs new file mode 100644 index 0000000..68575bb --- /dev/null +++ b/DiscordBotCore.PluginCore/Helpers/Execution/DbCommand/DbCommandExecutingArgument.cs @@ -0,0 +1,26 @@ +using Discord.Commands; +using Discord.WebSocket; +using DiscordBotCore.Logging; + +namespace DiscordBotCore.PluginCore.Helpers.Execution.DbCommand; + +public class DbCommandExecutingArgument : IDbCommandExecutingArgument +{ + public SocketCommandContext Context { get; init; } + public string CleanContent { get; init; } + public string CommandUsed { get; init; } + public string[]? Arguments { get; init; } + public ILogger Logger { get; init; } + public DirectoryInfo PluginBaseDirectory { get; init; } + + public DbCommandExecutingArgument(ILogger logger, SocketCommandContext context, string cleanContent, string commandUsed, string[]? arguments, DirectoryInfo pluginBaseDirectory) + { + this.Logger = logger; + this.Context = context; + this.CleanContent = cleanContent; + this.CommandUsed = commandUsed; + this.Arguments = arguments; + this.PluginBaseDirectory = pluginBaseDirectory; + } + +} \ No newline at end of file diff --git a/DiscordBotCore.PluginCore/Helpers/Execution/DbCommand/IDbCommandExecutingArgument.cs b/DiscordBotCore.PluginCore/Helpers/Execution/DbCommand/IDbCommandExecutingArgument.cs new file mode 100644 index 0000000..acbdcca --- /dev/null +++ b/DiscordBotCore.PluginCore/Helpers/Execution/DbCommand/IDbCommandExecutingArgument.cs @@ -0,0 +1,16 @@ +using Discord.Commands; +using Discord.WebSocket; +using DiscordBotCore.Logging; + +namespace DiscordBotCore.PluginCore.Helpers.Execution.DbCommand; + +public interface IDbCommandExecutingArgument +{ + ILogger Logger { get; init; } + string CleanContent { get; init; } + string CommandUsed { get; init; } + string[]? Arguments { get; init; } + + SocketCommandContext Context { get; init; } + public DirectoryInfo PluginBaseDirectory { get; init; } +} \ No newline at end of file diff --git a/DiscordBotCore.PluginCore/Helpers/Execution/DbEvent/DbEventExecutingArgument.cs b/DiscordBotCore.PluginCore/Helpers/Execution/DbEvent/DbEventExecutingArgument.cs new file mode 100644 index 0000000..d5f5770 --- /dev/null +++ b/DiscordBotCore.PluginCore/Helpers/Execution/DbEvent/DbEventExecutingArgument.cs @@ -0,0 +1,20 @@ +using Discord.WebSocket; +using DiscordBotCore.Logging; + +namespace DiscordBotCore.PluginCore.Helpers.Execution.DbEvent; + +public class DbEventExecutingArgument : IDbEventExecutingArgument +{ + public ILogger Logger { get; } + public DiscordSocketClient Client { get; } + public string BotPrefix { get; } + public DirectoryInfo PluginBaseDirectory { get; } + + public DbEventExecutingArgument(ILogger logger, DiscordSocketClient client, string botPrefix, DirectoryInfo pluginBaseDirectory) + { + Logger = logger; + Client = client; + BotPrefix = botPrefix; + PluginBaseDirectory = pluginBaseDirectory; + } +} \ No newline at end of file diff --git a/DiscordBotCore.PluginCore/Helpers/Execution/DbEvent/IDbEventExecutingArgument.cs b/DiscordBotCore.PluginCore/Helpers/Execution/DbEvent/IDbEventExecutingArgument.cs new file mode 100644 index 0000000..ffef441 --- /dev/null +++ b/DiscordBotCore.PluginCore/Helpers/Execution/DbEvent/IDbEventExecutingArgument.cs @@ -0,0 +1,12 @@ +using Discord.WebSocket; +using DiscordBotCore.Logging; + +namespace DiscordBotCore.PluginCore.Helpers.Execution.DbEvent; + +public interface IDbEventExecutingArgument +{ + public ILogger Logger { get; } + public DiscordSocketClient Client { get; } + public string BotPrefix { get; } + public DirectoryInfo PluginBaseDirectory { get; } +} \ No newline at end of file diff --git a/PluginManager/Interfaces/DBCommand.cs b/DiscordBotCore.PluginCore/Interfaces/IDbCommand.cs similarity index 65% rename from PluginManager/Interfaces/DBCommand.cs rename to DiscordBotCore.PluginCore/Interfaces/IDbCommand.cs index e25fe15..f430f45 100644 --- a/PluginManager/Interfaces/DBCommand.cs +++ b/DiscordBotCore.PluginCore/Interfaces/IDbCommand.cs @@ -1,9 +1,8 @@ -using System.Collections.Generic; -using PluginManager.Others; +using DiscordBotCore.PluginCore.Helpers.Execution.DbCommand; -namespace PluginManager.Interfaces; +namespace DiscordBotCore.PluginCore.Interfaces; -public interface DBCommand +public interface IDbCommand { /// /// Command to be executed @@ -14,7 +13,7 @@ public interface DBCommand /// /// Command aliases. Users may use this to execute the command /// - List? Aliases { get; } + List Aliases { get; } /// /// Command description @@ -30,21 +29,17 @@ public interface DBCommand /// /// true if the command requre admin, otherwise false /// - bool requireAdmin { get; } + bool RequireAdmin { get; } /// /// The main body of the command. This is what is executed when user calls the command in Server /// - /// The disocrd Context - void ExecuteServer(DbCommandExecutingArguments args) - { - } + /// The Discord Context + Task ExecuteServer(IDbCommandExecutingArgument args) => Task.CompletedTask; /// /// The main body of the command. This is what is executed when user calls the command in DM /// - /// The disocrd Context - void ExecuteDM(DbCommandExecutingArguments args) - { - } + /// The Discord Context + Task ExecuteDm(IDbCommandExecutingArgument args) => Task.CompletedTask; } diff --git a/PluginManager/Interfaces/DBEvent.cs b/DiscordBotCore.PluginCore/Interfaces/IDbEvent.cs similarity index 56% rename from PluginManager/Interfaces/DBEvent.cs rename to DiscordBotCore.PluginCore/Interfaces/IDbEvent.cs index 4c7e06a..bbe03ee 100644 --- a/PluginManager/Interfaces/DBEvent.cs +++ b/DiscordBotCore.PluginCore/Interfaces/IDbEvent.cs @@ -1,8 +1,8 @@ -using Discord.WebSocket; +using DiscordBotCore.PluginCore.Helpers.Execution.DbEvent; -namespace PluginManager.Interfaces; +namespace DiscordBotCore.PluginCore.Interfaces; -public interface DBEvent +public interface IDbEvent { /// /// The name of the event @@ -17,6 +17,6 @@ public interface DBEvent /// /// The method that is invoked when the event is loaded into memory /// - /// The discord bot client - void Start(DiscordSocketClient client); + /// The arguments for the start method + void Start(IDbEventExecutingArgument args); } diff --git a/DiscordBotCore.PluginCore/Interfaces/IDbSlashCommand.cs b/DiscordBotCore.PluginCore/Interfaces/IDbSlashCommand.cs new file mode 100644 index 0000000..9906752 --- /dev/null +++ b/DiscordBotCore.PluginCore/Interfaces/IDbSlashCommand.cs @@ -0,0 +1,22 @@ +using Discord; +using Discord.WebSocket; +using DiscordBotCore.Logging; + +namespace DiscordBotCore.PluginCore.Interfaces; + +public interface IDbSlashCommand +{ + string Name { get; } + string Description { get; } + bool CanUseDm { get; } + bool HasInteraction { get; } + + List Options { get; } + + void ExecuteServer(ILogger logger, SocketSlashCommand context) + { } + + void ExecuteDm(ILogger logger, SocketSlashCommand context) { } + + Task ExecuteInteraction(ILogger logger, SocketInteraction interaction) => Task.CompletedTask; +} diff --git a/DiscordBotCore.PluginManagement.Loading/DiscordBotCore.PluginManagement.Loading.csproj b/DiscordBotCore.PluginManagement.Loading/DiscordBotCore.PluginManagement.Loading.csproj new file mode 100644 index 0000000..24fad4b --- /dev/null +++ b/DiscordBotCore.PluginManagement.Loading/DiscordBotCore.PluginManagement.Loading.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + AnyCPU;x64;ARM64 + + + + + + + + + + diff --git a/DiscordBotCore.PluginManagement.Loading/Exceptions/PluginNotFoundException.cs b/DiscordBotCore.PluginManagement.Loading/Exceptions/PluginNotFoundException.cs new file mode 100644 index 0000000..af7516e --- /dev/null +++ b/DiscordBotCore.PluginManagement.Loading/Exceptions/PluginNotFoundException.cs @@ -0,0 +1,9 @@ +namespace DiscordBotCore.PluginManagement.Loading.Exceptions; + +public class PluginNotFoundException : Exception +{ + public PluginNotFoundException(string pluginName) : base($"Plugin {pluginName} was not found") { } + + public PluginNotFoundException(string pluginName, string url, string branch) : + base ($"Plugin {pluginName} was not found on {url} (branch: {branch}") { } +} \ No newline at end of file diff --git a/DiscordBotCore.PluginManagement.Loading/IPluginLoader.cs b/DiscordBotCore.PluginManagement.Loading/IPluginLoader.cs new file mode 100644 index 0000000..87583f0 --- /dev/null +++ b/DiscordBotCore.PluginManagement.Loading/IPluginLoader.cs @@ -0,0 +1,27 @@ +using Discord.WebSocket; +using DiscordBotCore.PluginCore.Interfaces; + +namespace DiscordBotCore.PluginManagement.Loading; + +public interface IPluginLoader +{ + public IReadOnlyList Commands { get; } + public IReadOnlyList Events { get; } + public IReadOnlyList SlashCommands { get; } + + /// + /// Sets the Discord client for the plugin loader. This is used to initialize the slash commands and events. + /// + /// The socket client that represents the running Discord Bot + public void SetDiscordClient(DiscordSocketClient discordSocketClient); + + /// + /// Loads all the plugins that are installed. + /// + public Task LoadPlugins(); + + /// + /// Unload all plugins from the plugin manager. + /// + public Task UnloadAllPlugins(); +} \ No newline at end of file diff --git a/DiscordBotCore.PluginManagement.Loading/PluginLoader.cs b/DiscordBotCore.PluginManagement.Loading/PluginLoader.cs new file mode 100644 index 0000000..dea8f4c --- /dev/null +++ b/DiscordBotCore.PluginManagement.Loading/PluginLoader.cs @@ -0,0 +1,371 @@ +using System.Collections.ObjectModel; +using System.Reflection; +using Discord; +using Discord.WebSocket; +using DiscordBotCore.Configuration; +using DiscordBotCore.Logging; +using DiscordBotCore.PluginCore.Helpers.Execution.DbEvent; +using DiscordBotCore.PluginCore.Interfaces; +using DiscordBotCore.PluginManagement.Loading.Exceptions; +using DiscordBotCore.Utilities.Responses; + +namespace DiscordBotCore.PluginManagement.Loading; + +public class PluginLoader : IPluginLoader +{ + private static readonly string _HelpCommandNamespaceFullName = "DiscordBotCore.Commands.HelpCommand"; + + private readonly IPluginManager _PluginManager; + private readonly ILogger _Logger; + private readonly IConfiguration _Configuration; + + private DiscordSocketClient? _DiscordClient; + private PluginLoaderContext? PluginLoaderContext; + + private readonly List _Commands = new List(); + private readonly List _Events = new List(); + private readonly List _SlashCommands = new List(); + private readonly List _ApplicationCommands = new List(); + + public PluginLoader(IPluginManager pluginManager, ILogger logger, IConfiguration configuration) + { + _PluginManager = pluginManager; + _Logger = logger; + _Configuration = configuration; + } + + public IReadOnlyList Commands => _Commands; + public IReadOnlyList Events => _Events; + public IReadOnlyList SlashCommands => _SlashCommands; + + public void SetDiscordClient(DiscordSocketClient discordSocketClient) + { + if (_DiscordClient is not null && discordSocketClient == _DiscordClient) + { + _Logger.Log("A client is already set. Please set the client only once.", this, LogType.Warning); + return; + } + + if (discordSocketClient.LoginState != LoginState.LoggedIn) + { + _Logger.Log("The client must be logged in before setting it.", this, LogType.Error); + return; + } + + _DiscordClient = discordSocketClient; + } + + public async Task LoadPlugins() + { + if (PluginLoaderContext is not null) + { + _Logger.Log("The plugins are already loaded", this, LogType.Error); + return; + } + + _Events.Clear(); + _Commands.Clear(); + _SlashCommands.Clear(); + _ApplicationCommands.Clear(); + + await LoadPluginFiles(); + + LoadEverythingOfType(); + var helpCommand = AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(assembly => assembly.DefinedTypes.Any(type => type.FullName == _HelpCommandNamespaceFullName) + && assembly.FullName != null + && assembly.FullName.StartsWith("DiscordBotCore")); + + if (helpCommand is not null) + { + var helpCommandType = helpCommand.DefinedTypes.FirstOrDefault(type => type.FullName == _HelpCommandNamespaceFullName && + typeof(IDbCommand).IsAssignableFrom(type)); + if (helpCommandType is not null) + { + InitializeType(helpCommandType); + } + } + + LoadEverythingOfType(); + LoadEverythingOfType(); + + + _Logger.Log("Loaded plugins", this); + } + + public async Task UnloadAllPlugins() + { + if (PluginLoaderContext is null) + { + _Logger.Log("The plugins are not loaded. Please load the plugins before unloading them.", this, LogType.Error); + return; + } + + await UnloadSlashCommands(); + + PluginLoaderContext.Unload(); + PluginLoaderContext = null; + + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + + + } + + private async Task UnloadSlashCommands() + { + if (_DiscordClient is null) + { + _Logger.Log("The client is not set. Please set the client before unloading slash commands.", this, LogType.Error); + return; + } + + foreach (SocketApplicationCommand command in _ApplicationCommands) + { + await command.DeleteAsync(); + } + + _ApplicationCommands.Clear(); + _Logger.Log("Unloaded all slash commands", this); + } + + private async Task LoadPluginFiles() + { + var installedPlugins = await _PluginManager.GetInstalledPlugins(); + + if (installedPlugins.Count == 0) + { + _Logger.Log("No plugin files found. Please check the plugin files.", this, LogType.Error); + return; + } + + var files = installedPlugins.Where(plugin => plugin.IsEnabled).Select(plugin => plugin.FilePath); + + PluginLoaderContext = new PluginLoaderContext(_Logger, "PluginLoader"); + + foreach (var file in files) + { + string fullFilePath = Path.GetFullPath(file); + if (string.IsNullOrEmpty(fullFilePath)) + { + _Logger.Log("The file path is empty. Please check the plugin file path.", PluginLoaderContext, LogType.Error); + continue; + } + + if (!File.Exists(fullFilePath)) + { + _Logger.Log("The file does not exist. Please check the plugin file path.", PluginLoaderContext, LogType.Error); + continue; + } + + try + { + PluginLoaderContext.LoadFromAssemblyPath(fullFilePath); + } + catch (Exception ex) + { + _Logger.LogException(ex, this); + } + } + + _Logger.Log($"Loaded {PluginLoaderContext.Assemblies.Count()} assemblies", this); + } + + private void LoadEverythingOfType() + { + if (PluginLoaderContext is null) + { + _Logger.Log("The plugins are not loaded. Please load the plugins before loading them.", this, LogType.Error); + return; + } + + var types = PluginLoaderContext.Assemblies + .SelectMany(s => s.GetTypes()) + .Where(p => typeof(T).IsAssignableFrom(p) && !p.IsInterface); + + foreach (var type in types) + { + InitializeType(type); + } + } + + private void InitializeType(Type type) + { + T? plugin = (T?)Activator.CreateInstance(type); + if (plugin is null) + { + _Logger.Log($"Failed to create instance of plugin with type {type.FullName} [{type.Assembly}]", this, LogType.Error); + } + + switch (plugin) + { + case IDbEvent dbEvent: + InitializeEvent(dbEvent); + break; + case IDbCommand dbCommand: + InitializeDbCommand(dbCommand); + break; + case IDbSlashCommand dbSlashCommand: + InitializeSlashCommand(dbSlashCommand); + break; + default: + throw new PluginNotFoundException($"Unknown plugin type {plugin.GetType().FullName}"); + } + } + + private void InitializeDbCommand(IDbCommand command) + { + _Commands.Add(command); + _Logger.Log("Command loaded: " + command.Command, this); + } + + private void InitializeEvent(IDbEvent eEvent) + { + if (!TryStartEvent(eEvent)) + { + return; + } + + _Events.Add(eEvent); + _Logger.Log("Event loaded: " + eEvent, this); + } + + private async void InitializeSlashCommand(IDbSlashCommand slashCommand) + { + bool result = await TryStartSlashCommand(slashCommand); + + if (!result) + { + return; + } + + if (_DiscordClient is null) + { + return; + } + + if (slashCommand.HasInteraction) + { + _DiscordClient.InteractionCreated += interaction => slashCommand.ExecuteInteraction(_Logger, interaction); + } + + _SlashCommands.Add(slashCommand); + _Logger.Log("Slash command loaded: " + slashCommand.Name, this); + } + + private bool TryStartEvent(IDbEvent dbEvent) + { + string? botPrefix = _Configuration.Get("prefix"); + if (string.IsNullOrEmpty(botPrefix)) + { + _Logger.Log("Bot prefix is not set. Please set the bot prefix in the configuration.", this, LogType.Error); + return false; + } + + if (_DiscordClient is null) + { + _Logger.Log("Discord client is not set. Please set the discord client before starting events.", this, LogType.Error); + return false; + } + + string? resourcesFolder = _Configuration.Get("ResourcesFolder"); + if (string.IsNullOrEmpty(resourcesFolder)) + { + _Logger.Log("Resources folder is not set. Please set the resources folder in the configuration.", this, LogType.Error); + return false; + } + + if (!Directory.Exists(resourcesFolder)) + { + _Logger.Log("Resources folder does not exist. Please create the resources folder.", this, LogType.Error); + return false; + } + + string? eventConfigDirectory = Path.Combine(resourcesFolder, dbEvent.GetType().Assembly.GetName().Name); + + Directory.CreateDirectory(eventConfigDirectory); + + IDbEventExecutingArgument args = new DbEventExecutingArgument( + _Logger, + _DiscordClient, + botPrefix, + new DirectoryInfo(eventConfigDirectory)); + + dbEvent.Start(args); + return true; + } + + private async Task TryStartSlashCommand(IDbSlashCommand? dbSlashCommand) + { + if (dbSlashCommand is null) + { + _Logger.Log("The loaded slash command was null. Please check the plugin.", this, LogType.Error); + return false; + } + + if (_DiscordClient is null) + { + _Logger.Log("The client is not set. Please set the client before starting slash commands.", this, LogType.Error); + return false; + } + + if (_DiscordClient.Guilds.Count == 0) + { + _Logger.Log("The client is not connected to any guilds. Please check the client.", this, LogType.Error); + return false; + } + + var builder = new SlashCommandBuilder(); + builder.WithName(dbSlashCommand.Name); + builder.WithDescription(dbSlashCommand.Description); + builder.Options = dbSlashCommand.Options; + + if (dbSlashCommand.CanUseDm) + builder.WithContextTypes(InteractionContextType.BotDm, InteractionContextType.Guild); + else + builder.WithContextTypes(InteractionContextType.Guild); + + List serverIds = _Configuration.GetList("ServerIds", new List()); + + if (serverIds.Any()) + { + foreach(ulong guildId in serverIds) + { + IResponse result = await EnableSlashCommandPerGuild(guildId, builder); + + if (!result.IsSuccess) + { + _Logger.Log($"Failed to enable slash command {dbSlashCommand.Name} for guild {guildId}", this, LogType.Error); + continue; + } + + if (result.Data is null) + { + continue; + } + + _ApplicationCommands.Add(result.Data); + } + + return true; + } + + var command = await _DiscordClient.CreateGlobalApplicationCommandAsync(builder.Build()); + _ApplicationCommands.Add(command); + return true; + } + + private async Task> EnableSlashCommandPerGuild(ulong guildId, SlashCommandBuilder builder) + { + SocketGuild? guild = _DiscordClient?.GetGuild(guildId); + if (guild is null) + { + _Logger.Log("Failed to get guild with ID " + guildId, this, LogType.Error); + return Response.Failure("Failed to get guild with ID " + guildId); + } + + var command = await guild.CreateApplicationCommandAsync(builder.Build()); + return Response.Success(command); + } +} \ No newline at end of file diff --git a/DiscordBotCore.PluginManagement.Loading/PluginLoaderContext.cs b/DiscordBotCore.PluginManagement.Loading/PluginLoaderContext.cs new file mode 100644 index 0000000..3f82d12 --- /dev/null +++ b/DiscordBotCore.PluginManagement.Loading/PluginLoaderContext.cs @@ -0,0 +1,21 @@ +using System.Reflection; +using System.Runtime.Loader; +using DiscordBotCore.Logging; + +namespace DiscordBotCore.PluginManagement.Loading; + +public class PluginLoaderContext : AssemblyLoadContext +{ + private readonly ILogger _logger; + + public PluginLoaderContext(ILogger logger, string name) : base(name: name, isCollectible: true) + { + _logger = logger; + } + + protected override Assembly? Load(AssemblyName assemblyName) + { + //_logger.Log("Assembly load requested: " + assemblyName.Name, this); + return base.Load(assemblyName); + } +} \ No newline at end of file diff --git a/DiscordBotCore.PluginManagement/DiscordBotCore.PluginManagement.csproj b/DiscordBotCore.PluginManagement/DiscordBotCore.PluginManagement.csproj new file mode 100644 index 0000000..dc86673 --- /dev/null +++ b/DiscordBotCore.PluginManagement/DiscordBotCore.PluginManagement.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + enable + enable + AnyCPU;x64;ARM64 + + + + + + + + + + + + + + diff --git a/DiscordBotCore.PluginManagement/Helpers/IPluginRepository.cs b/DiscordBotCore.PluginManagement/Helpers/IPluginRepository.cs new file mode 100644 index 0000000..7b91c50 --- /dev/null +++ b/DiscordBotCore.PluginManagement/Helpers/IPluginRepository.cs @@ -0,0 +1,14 @@ +using DiscordBotCore.PluginManagement.Models; + +namespace DiscordBotCore.PluginManagement.Helpers; + +public interface IPluginRepository +{ + public Task> GetAllPlugins(int operatingSystem, bool includeNotApproved); + + public Task GetPluginById(int pluginId); + public Task GetPluginByName(string pluginName, int operatingSystem, bool includeNotApproved); + + public Task> GetDependenciesForPlugin(int pluginId); + +} \ No newline at end of file diff --git a/DiscordBotCore.PluginManagement/Helpers/IPluginRepositoryConfiguration.cs b/DiscordBotCore.PluginManagement/Helpers/IPluginRepositoryConfiguration.cs new file mode 100644 index 0000000..69718d1 --- /dev/null +++ b/DiscordBotCore.PluginManagement/Helpers/IPluginRepositoryConfiguration.cs @@ -0,0 +1,9 @@ +namespace DiscordBotCore.PluginManagement.Helpers; + +public interface IPluginRepositoryConfiguration +{ + public string BaseUrl { get; } + + public string PluginRepositoryLocation { get; } + public string DependenciesRepositoryLocation { get; } +} \ No newline at end of file diff --git a/DiscordBotCore.PluginManagement/Helpers/PluginRepository.cs b/DiscordBotCore.PluginManagement/Helpers/PluginRepository.cs new file mode 100644 index 0000000..08c2142 --- /dev/null +++ b/DiscordBotCore.PluginManagement/Helpers/PluginRepository.cs @@ -0,0 +1,161 @@ +using System.Net.Mime; +using DiscordBotCore.Logging; +using DiscordBotCore.PluginManagement.Models; +using DiscordBotCore.Utilities; +using Microsoft.AspNetCore.Http.Extensions; + +namespace DiscordBotCore.PluginManagement.Helpers; + +public class PluginRepository : IPluginRepository +{ + private readonly IPluginRepositoryConfiguration _pluginRepositoryConfiguration; + private readonly HttpClient _httpClient; + private readonly ILogger _logger; + + public PluginRepository(IPluginRepositoryConfiguration pluginRepositoryConfiguration, ILogger logger) + { + _pluginRepositoryConfiguration = pluginRepositoryConfiguration; + _httpClient = new HttpClient(); + _httpClient.BaseAddress = new Uri(_pluginRepositoryConfiguration.BaseUrl); + _logger = logger; + } + + public async Task> GetAllPlugins(int operatingSystem, bool includeNotApproved) + { + string url = CreateUrlWithQueryParams(_pluginRepositoryConfiguration.PluginRepositoryLocation, + "get-all-plugins", new Dictionary + { + { "operatingSystem", operatingSystem.ToString() }, + { "includeNotApproved", includeNotApproved.ToString() } + }); + + try + { + HttpResponseMessage response = await _httpClient.GetAsync(url); + + if (!response.IsSuccessStatusCode) + { + return []; + } + + string content = await response.Content.ReadAsStringAsync(); + List plugins = await JsonManager.ConvertFromJson>(content); + + return plugins; + } + catch (HttpRequestException exception) + { + _logger.LogException(exception,this); + return []; + } + + } + + public async Task GetPluginById(int pluginId) + { + string url = CreateUrlWithQueryParams(_pluginRepositoryConfiguration.PluginRepositoryLocation, + "get-by-id", new Dictionary + { + { "pluginId", pluginId.ToString() }, + { "includeNotApproved", "false" } + }); + + try + { + HttpResponseMessage response = await _httpClient.GetAsync(url); + + if (!response.IsSuccessStatusCode) + { + return null; + } + + string content = await response.Content.ReadAsStringAsync(); + OnlinePlugin plugin = await JsonManager.ConvertFromJson(content); + + return plugin; + } + catch (HttpRequestException exception) + { + _logger.LogException(exception, this); + return null; + } + + } + + public async Task GetPluginByName(string pluginName, int operatingSystem, bool includeNotApproved) + { + string url = CreateUrlWithQueryParams(_pluginRepositoryConfiguration.PluginRepositoryLocation, + "get-by-name", new Dictionary + { + { "pluginName", pluginName }, + { "operatingSystem", operatingSystem.ToString() }, + { "includeNotApproved", includeNotApproved.ToString() } + }); + + try + { + HttpResponseMessage response = await _httpClient.GetAsync(url); + + if (!response.IsSuccessStatusCode) + { + _logger.Log($"Plugin {pluginName} not found"); + return null; + } + + string content = await response.Content.ReadAsStringAsync(); + OnlinePlugin plugin = await JsonManager.ConvertFromJson(content); + + return plugin; + } + catch (HttpRequestException exception) + { + _logger.LogException(exception, this); + return null; + } + + } + + public async Task> GetDependenciesForPlugin(int pluginId) + { + string url = CreateUrlWithQueryParams(_pluginRepositoryConfiguration.DependenciesRepositoryLocation, + "get-by-plugin-id", new Dictionary + { + { "pluginId", pluginId.ToString() } + }); + + try + { + HttpResponseMessage response = await _httpClient.GetAsync(url); + if(!response.IsSuccessStatusCode) + { + _logger.Log($"Failed to get dependencies for plugin with ID {pluginId}"); + return []; + } + + string content = await response.Content.ReadAsStringAsync(); + List dependencies = await JsonManager.ConvertFromJson>(content); + + return dependencies; + } + catch(HttpRequestException exception) + { + _logger.LogException(exception, this); + return []; + } + + } + + private string CreateUrlWithQueryParams(string baseUrl, string endpoint, Dictionary queryParams) + { + QueryBuilder queryBuilder = new QueryBuilder(); + foreach (var(key,value) in queryParams) + { + queryBuilder.Add(key, value); + } + + string queryString = queryBuilder.ToQueryString().ToString(); + string url = baseUrl + endpoint + queryString; + + return url; + } +} \ No newline at end of file diff --git a/DiscordBotCore.PluginManagement/Helpers/PluginRepositoryConfiguration.cs b/DiscordBotCore.PluginManagement/Helpers/PluginRepositoryConfiguration.cs new file mode 100644 index 0000000..0d479cd --- /dev/null +++ b/DiscordBotCore.PluginManagement/Helpers/PluginRepositoryConfiguration.cs @@ -0,0 +1,22 @@ +using System.Text.Json.Serialization; + +namespace DiscordBotCore.PluginManagement.Helpers; + +public class PluginRepositoryConfiguration : IPluginRepositoryConfiguration +{ + public static PluginRepositoryConfiguration Default => new ("http://localhost:8080/api/v1/", + "plugin/", + "dependency/"); + + public string BaseUrl { get; } + public string PluginRepositoryLocation { get; } + public string DependenciesRepositoryLocation { get; } + + [JsonConstructor] + public PluginRepositoryConfiguration(string baseUrl, string pluginRepositoryLocation, string dependenciesRepositoryLocation) + { + BaseUrl = baseUrl; + PluginRepositoryLocation = pluginRepositoryLocation; + DependenciesRepositoryLocation = dependenciesRepositoryLocation; + } +} \ No newline at end of file diff --git a/DiscordBotCore.PluginManagement/IPluginManager.cs b/DiscordBotCore.PluginManagement/IPluginManager.cs new file mode 100644 index 0000000..35d8495 --- /dev/null +++ b/DiscordBotCore.PluginManagement/IPluginManager.cs @@ -0,0 +1,20 @@ +using DiscordBotCore.PluginManagement.Models; +using DiscordBotCore.Utilities.Responses; + +namespace DiscordBotCore.PluginManagement; + +public interface IPluginManager +{ + Task> GetPluginsList(); + Task> GetPluginDataByName(string pluginName); + Task> GetPluginDataById(int pluginId); + Task> AppendPluginToDatabase(LocalPlugin pluginData); + Task> GetInstalledPlugins(); + Task> GetDependencyLocation(string dependencyName); + Task> GetDependencyLocation(string dependencyName, string pluginName); + string GenerateDependencyRelativePath(string pluginName, string dependencyPath); + Task> InstallPlugin(OnlinePlugin plugin, IProgress progress); + Task SetEnabledStatus(string pluginName, bool status); + Task> UninstallPluginByName(string pluginName); + Task> GetLocalPluginByName(string pluginName); +} \ No newline at end of file diff --git a/DiscordBotCore.PluginManagement/Models/LocalPlugin.cs b/DiscordBotCore.PluginManagement/Models/LocalPlugin.cs new file mode 100644 index 0000000..7ec78d2 --- /dev/null +++ b/DiscordBotCore.PluginManagement/Models/LocalPlugin.cs @@ -0,0 +1,46 @@ +using System.Text.Json.Serialization; + +namespace DiscordBotCore.PluginManagement.Models; + +public class LocalPlugin +{ + public string PluginName { get; private set; } + public string PluginVersion { get; private set; } + public string FilePath { get; private set; } + public Dictionary ListOfExecutableDependencies {get; private set;} + public bool IsOfflineAdded { get; internal set; } + public bool IsEnabled { get; internal set; } + + [JsonConstructor] + public LocalPlugin(string pluginName, string pluginVersion, string filePath, Dictionary listOfExecutableDependencies, bool isOfflineAdded, bool isEnabled) + { + PluginName = pluginName; + PluginVersion = pluginVersion; + ListOfExecutableDependencies = listOfExecutableDependencies; + FilePath = filePath; + IsOfflineAdded = isOfflineAdded; + IsEnabled = isEnabled; + } + + private LocalPlugin(string pluginName, string pluginVersion, string filePath, + Dictionary listOfExecutableDependencies) + { + PluginName = pluginName; + PluginVersion = pluginVersion; + ListOfExecutableDependencies = listOfExecutableDependencies; + FilePath = filePath; + IsOfflineAdded = false; + IsEnabled = true; + } + + public static LocalPlugin FromOnlineInfo(OnlinePlugin plugin, List dependencies, string downloadLocation) + { + LocalPlugin localPlugin = new LocalPlugin( + plugin.Name, plugin.Version, downloadLocation, + dependencies.Where(dependency => dependency.IsExecutable) + .ToDictionary(dependency => dependency.DependencyName, dependency => dependency.DownloadLocation) + ); + + return localPlugin; + } +} diff --git a/DiscordBotCore.PluginManagement/Models/OnlineDependencyInfo.cs b/DiscordBotCore.PluginManagement/Models/OnlineDependencyInfo.cs new file mode 100644 index 0000000..e528621 --- /dev/null +++ b/DiscordBotCore.PluginManagement/Models/OnlineDependencyInfo.cs @@ -0,0 +1,23 @@ + +using System.Text.Json.Serialization; + +namespace DiscordBotCore.PluginManagement.Models; + +public class OnlineDependencyInfo +{ + public string DependencyName { get; private set; } + [JsonPropertyName("dependencyLink")] + public string DownloadLink { get; private set; } + [JsonPropertyName("dependencyLocation")] + public string DownloadLocation { get; private set; } + public bool IsExecutable { get; private set; } + + [JsonConstructor] + public OnlineDependencyInfo(string dependencyName, string downloadLink, string downloadLocation, bool isExecutable) + { + DependencyName = dependencyName; + DownloadLink = downloadLink; + DownloadLocation = downloadLocation; + IsExecutable = isExecutable; + } +} diff --git a/DiscordBotCore.PluginManagement/Models/OnlinePlugin.cs b/DiscordBotCore.PluginManagement/Models/OnlinePlugin.cs new file mode 100644 index 0000000..b6815e7 --- /dev/null +++ b/DiscordBotCore.PluginManagement/Models/OnlinePlugin.cs @@ -0,0 +1,29 @@ +using System.Text.Json.Serialization; + +namespace DiscordBotCore.PluginManagement.Models; + +public class OnlinePlugin +{ + public int Id { get; private set; } + public string Name { get; private set; } + public string Description { get; private set; } + public string Version { get; private set; } + public string Author { get; private set; } + public string DownloadLink { get; private set; } + public int OperatingSystem { get; private set; } + public bool IsApproved { get; private set; } + + [JsonConstructor] + public OnlinePlugin(int id, string name, string description, string version, + string author, string downloadLink, int operatingSystem, bool isApproved) + { + Id = id; + Name = name; + Description = description; + Version = version; + Author = author; + DownloadLink = downloadLink; + OperatingSystem = operatingSystem; + IsApproved = isApproved; + } +} \ No newline at end of file diff --git a/DiscordBotCore.PluginManagement/PluginManager.cs b/DiscordBotCore.PluginManagement/PluginManager.cs new file mode 100644 index 0000000..f7bf0b0 --- /dev/null +++ b/DiscordBotCore.PluginManagement/PluginManager.cs @@ -0,0 +1,316 @@ +using System.Diagnostics; +using DiscordBotCore.Logging; +using DiscordBotCore.Networking; +using DiscordBotCore.PluginManagement.Helpers; +using DiscordBotCore.PluginManagement.Models; +using DiscordBotCore.Utilities; +using DiscordBotCore.Configuration; +using DiscordBotCore.Utilities.Responses; +using OperatingSystem = DiscordBotCore.Utilities.OperatingSystem; + +namespace DiscordBotCore.PluginManagement; + +public sealed class PluginManager : IPluginManager +{ + private static readonly string _LibrariesBaseFolder = "Libraries"; + private readonly IPluginRepository _PluginRepository; + private readonly ILogger _Logger; + private readonly IConfiguration _Configuration; + + public PluginManager(IPluginRepository pluginRepository, ILogger logger, IConfiguration configuration) + { + _PluginRepository = pluginRepository; + _Logger = logger; + _Configuration = configuration; + } + + public async Task> GetPluginsList() + { + int os = OperatingSystem.GetOperatingSystemInt(); + var onlinePlugins = await _PluginRepository.GetAllPlugins(os, false); + + if (!onlinePlugins.Any()) + { + _Logger.Log($"No plugins found for operatingSystem: {OperatingSystem.GetOperatingSystemString((OperatingSystem.OperatingSystemEnum)os)}", LogType.Warning); + return []; + } + + return onlinePlugins; + } + + public async Task> GetPluginDataByName(string pluginName) + { + int os = OperatingSystem.GetOperatingSystemInt(); + var plugin = await _PluginRepository.GetPluginByName(pluginName, os, false); + + if (plugin is null) + { + return Response.Failure($"Plugin {pluginName} not found in the repository for operating system {OperatingSystem.GetOperatingSystemString((OperatingSystem.OperatingSystemEnum)os)}."); + } + + return Response.Success(plugin); + } + + public async Task> GetPluginDataById(int pluginId) + { + var plugin = await _PluginRepository.GetPluginById(pluginId); + if (plugin is null) + { + return Response.Failure($"Plugin {pluginId} not found in the repository."); + } + + return Response.Success(plugin); + } + + private async Task> RemovePluginFromDatabase(string pluginName) + { + string? pluginDatabaseFile = _Configuration.Get("PluginDatabase"); + + if (pluginDatabaseFile is null) + { + return Response.Failure("PluginDatabase file path is not present in the config file"); + } + + List installedPlugins = await JsonManager.ConvertFromJson>(await File.ReadAllTextAsync(pluginDatabaseFile)); + + installedPlugins.RemoveAll(p => p.PluginName == pluginName); + await JsonManager.SaveToJsonFile(pluginDatabaseFile, installedPlugins); + + return Response.Success(); + } + + public async Task> AppendPluginToDatabase(LocalPlugin pluginData) + { + string? pluginDatabaseFile = _Configuration.Get("PluginDatabase"); + if (pluginDatabaseFile is null) + { + return Response.Failure("PluginDatabase file path is not present in the config file"); + } + + List installedPlugins = await GetInstalledPlugins(); + + foreach (var dependency in pluginData.ListOfExecutableDependencies) + { + pluginData.ListOfExecutableDependencies[dependency.Key] = dependency.Value; + } + + if (installedPlugins.Any(plugin => plugin.PluginName == pluginData.PluginName)) + { + _Logger.Log($"Plugin {pluginData.PluginName} already exists in the database. Updating...", this, LogType.Info); + installedPlugins.RemoveAll(p => p.PluginName == pluginData.PluginName); + } + + installedPlugins.Add(pluginData); + await JsonManager.SaveToJsonFile(pluginDatabaseFile, installedPlugins); + + return Response.Success(); + } + + public async Task> GetInstalledPlugins() + { + string? pluginDatabaseFile = _Configuration.Get("PluginDatabase"); + if (pluginDatabaseFile is null) + { + _Logger.Log("Plugin database file path is not present in the config file", this, LogType.Warning); + return []; + } + + if (!File.Exists(pluginDatabaseFile)) + { + _Logger.Log("Plugin database file not found", this, LogType.Warning); + await CreateEmptyPluginDatabase(); + return []; + } + + return await JsonManager.ConvertFromJson>(await File.ReadAllTextAsync(pluginDatabaseFile)); + } + + public async Task> GetDependencyLocation(string dependencyName) + { + List installedPlugins = await GetInstalledPlugins(); + + foreach (var plugin in installedPlugins) + { + if (plugin.ListOfExecutableDependencies.TryGetValue(dependencyName, out var dependencyPath)) + { + string relativePath = GenerateDependencyRelativePath(plugin.PluginName, dependencyPath); + return Response.Success(relativePath); + } + } + + return Response.Failure($"Dependency {dependencyName} not found in the installed plugins."); + } + + public async Task> GetDependencyLocation(string dependencyName, string pluginName) + { + List installedPlugins = await GetInstalledPlugins(); + + foreach (var plugin in installedPlugins) + { + if (plugin.PluginName == pluginName && plugin.ListOfExecutableDependencies.ContainsKey(dependencyName)) + { + string dependencyPath = plugin.ListOfExecutableDependencies[dependencyName]; + string relativePath = GenerateDependencyRelativePath(pluginName, dependencyPath); + return Response.Success(relativePath); + } + } + + return Response.Failure($"Dependency {dependencyName} not found in the installed plugins."); + } + + public string GenerateDependencyRelativePath(string pluginName, string dependencyPath) + { + string relative = $"./{_LibrariesBaseFolder}/{pluginName}/{dependencyPath}"; + return relative; + } + + public async Task> InstallPlugin(OnlinePlugin plugin, IProgress progress) + { + string? pluginsFolder = _Configuration.Get("PluginFolder"); + if (pluginsFolder is null) + { + return Response.Failure("Plugin folder path is not present in the config file"); + } + + var localPluginResponse = await GetLocalPluginByName(plugin.Name); + if (localPluginResponse is { IsSuccess: true, Data: not null }) + { + var response = await IsNewVersion(localPluginResponse.Data.PluginVersion, plugin.Version); + if (!response.IsSuccess) + { + return response; + } + } + + List dependencies = await _PluginRepository.GetDependenciesForPlugin(plugin.Id); + + string downloadLocation = $"{pluginsFolder}/{plugin.Name}.dll"; + + IProgress downloadProgress = new Progress(progress.Report); + + FileDownloader fileDownloader = new FileDownloader(plugin.DownloadLink, downloadLocation); + await fileDownloader.DownloadFile(downloadProgress.Report); + + ParallelDownloadExecutor executor = new ParallelDownloadExecutor(); + + foreach (var dependency in dependencies) + { + string dependencyLocation = GenerateDependencyRelativePath(plugin.Name, dependency.DownloadLocation); + + executor.AddTask(dependency.DownloadLink, dependencyLocation, progress.Report); + } + + await executor.ExecuteAllTasks(); + + LocalPlugin localPlugin = LocalPlugin.FromOnlineInfo(plugin, dependencies, downloadLocation); + var result = await AppendPluginToDatabase(localPlugin); + + return result; + } + + public async Task SetEnabledStatus(string pluginName, bool status) + { + var plugins = await GetInstalledPlugins(); + var plugin = plugins.Find(p => p.PluginName == pluginName); + + if (plugin == null) + return; + + plugin.IsEnabled = status; + + await RemovePluginFromDatabase(pluginName); + await AppendPluginToDatabase(plugin); + + } + + public async Task> UninstallPluginByName(string pluginName) + { + var localPluginResponse = await GetLocalPluginByName(pluginName); + if (!localPluginResponse.IsSuccess) + { + return Response.Failure(localPluginResponse.Message); + } + + var localPlugin = localPluginResponse.Data; + + if (localPlugin is null) + { + return Response.Failure($"Plugin {pluginName} not found in the database"); + } + + File.Delete(localPlugin.FilePath); + + if (Directory.Exists($"./{_LibrariesBaseFolder}/{pluginName}")) + { + foreach (var file in Directory.EnumerateFiles($"./{_LibrariesBaseFolder}/{pluginName}")) + { + File.Delete(file); + } + } + + var response = await RemovePluginFromDatabase(pluginName); + return response; + } + + public async Task> GetLocalPluginByName(string pluginName) + { + List installedPlugins = await GetInstalledPlugins(); + var plugin = installedPlugins.Find(p => p.PluginName == pluginName); + + if (plugin is null) + { + return Response.Failure($"Plugin {pluginName} not found in the database"); + } + + return Response.Success(plugin); + } + + private async Task> IsNewVersion(string currentVersion, string newVersion) + { + // currentVersion = "1.0.0" + // newVersion = "1.0.1" + + var currentVersionParts = currentVersion.Split('.').Select(int.Parse).ToArray(); + var newVersionParts = newVersion.Split('.').Select(int.Parse).ToArray(); + + if (currentVersionParts.Length != 3 || newVersionParts.Length != 3) + { + return Response.Failure("Invalid version format"); + } + + for (int i = 0; i < 3; i++) + { + if (newVersionParts[i] > currentVersionParts[i]) + { + return Response.Success(); + } + else if (newVersionParts[i] < currentVersionParts[i]) + { + return Response.Failure("Current version is newer"); + } + } + + return Response.Failure("Versions are the same"); + } + + private async Task CreateEmptyPluginDatabase() + { + string ? pluginDatabaseFile = _Configuration.Get("PluginDatabase"); + if (pluginDatabaseFile is null) + { + _Logger.Log("Plugin database file path is not present in the config file", this, LogType.Warning); + return false; + } + + if (File.Exists(pluginDatabaseFile)) + { + _Logger.Log("Plugin database file already exists", this, LogType.Warning); + return false; + } + + List installedPlugins = new List(); + await JsonManager.SaveToJsonFile(pluginDatabaseFile, installedPlugins); + _Logger.Log("Plugin database file created", this, LogType.Info); + return true; + } +} diff --git a/PluginManager/Others/ArchiveManager.cs b/DiscordBotCore.Utilities/ArchiveManager.cs similarity index 69% rename from PluginManager/Others/ArchiveManager.cs rename to DiscordBotCore.Utilities/ArchiveManager.cs index a74411c..d44ddb8 100644 --- a/PluginManager/Others/ArchiveManager.cs +++ b/DiscordBotCore.Utilities/ArchiveManager.cs @@ -1,15 +1,21 @@ -using System; -using System.IO; -using System.IO.Compression; -using System.Linq; -using System.Threading.Tasks; +using System.IO.Compression; +using DiscordBotCore.Logging; +using DiscordBotCore.Configuration; -namespace PluginManager.Others; +namespace DiscordBotCore.Utilities; -public static class ArchiveManager +public class ArchiveManager { + private readonly ILogger _Logger; + private readonly IConfiguration _Configuration; + + public ArchiveManager(ILogger logger, IConfiguration configuration) + { + _Logger = logger; + _Configuration = configuration; + } - public static void CreateFromFile(string file, string folder) + public void CreateFromFile(string file, string folder) { if (!Directory.Exists(folder)) Directory.CreateDirectory(folder); @@ -18,10 +24,8 @@ public static class ArchiveManager if (File.Exists(archiveName)) File.Delete(archiveName); - using(ZipArchive archive = ZipFile.Open(archiveName, ZipArchiveMode.Create)) - { - archive.CreateEntryFromFile(file, Path.GetFileName(file)); - } + using ZipArchive archive = ZipFile.Open(archiveName, ZipArchiveMode.Create); + archive.CreateEntryFromFile(file, Path.GetFileName(file)); } /// @@ -30,10 +34,15 @@ public static class ArchiveManager /// The file name in the archive /// The archive location on the disk /// An array of bytes that represents the Stream value from the file that was read inside the archive - public static async Task ReadStreamFromPakAsync(string fileName, string archName) + public async Task ReadAllBytes(string fileName, string archName) { + string? archiveFolderBasePath = _Configuration.Get("ArchiveFolder"); + if(archiveFolderBasePath is null) + throw new Exception("Archive folder not found"); - archName = Config.AppSettings["ArchiveFolder"] + archName; + Directory.CreateDirectory(archiveFolderBasePath); + + archName = Path.Combine(archiveFolderBasePath, archName); if (!File.Exists(archName)) throw new Exception("Failed to load file !"); @@ -49,6 +58,9 @@ public static class ArchiveManager stream.Close(); memoryStream.Close(); + + Console.WriteLine("Read file from archive: " + fileName); + Console.WriteLine("Size: " + data.Length); return data; } @@ -59,9 +71,16 @@ public static class ArchiveManager /// The file name that is inside the archive or its full path /// The archive location from the PAKs folder /// A string that represents the content of the file or null if the file does not exists or it has no content - public static async Task ReadFromPakAsync(string fileName, string archFile) + public async Task ReadFromPakAsync(string fileName, string archFile) { - archFile = Config.AppSettings["ArchiveFolder"] + archFile; + string? archiveFolderBasePath = _Configuration.Get("ArchiveFolder"); + if(archiveFolderBasePath is null) + throw new Exception("Archive folder not found"); + + Directory.CreateDirectory(archiveFolderBasePath); + + archFile = Path.Combine(archiveFolderBasePath, archFile); + if (!File.Exists(archFile)) throw new Exception("Failed to load file !"); @@ -87,7 +106,7 @@ public static class ArchiveManager } catch (Exception ex) { - Config.Logger.Log(ex.Message, typeof(ArchiveManager), LogType.ERROR); // Write the error to a file + _Logger.LogException(ex, this); await Task.Delay(100); return await ReadFromPakAsync(fileName, archFile); } @@ -101,14 +120,14 @@ public static class ArchiveManager /// The progress that is updated as a file is processed /// The type of progress /// - public static async Task ExtractArchive( + public async Task ExtractArchive( string zip, string folder, IProgress progress, UnzipProgressType type) { Directory.CreateDirectory(folder); using var archive = ZipFile.OpenRead(zip); var totalZipFiles = archive.Entries.Count(); - if (type == UnzipProgressType.PERCENTAGE_FROM_NUMBER_OF_FILES) + if (type == UnzipProgressType.PercentageFromNumberOfFiles) { var currentZipFile = 0; foreach (var entry in archive.Entries) @@ -123,7 +142,7 @@ public static class ArchiveManager } catch (Exception ex) { - Config.Logger.Log(ex.Message, typeof(ArchiveManager), LogType.ERROR); + _Logger.LogException(ex, this); } currentZipFile++; @@ -132,7 +151,7 @@ public static class ArchiveManager progress.Report((float)currentZipFile / totalZipFiles * 100); } } - else if (type == UnzipProgressType.PERCENTAGE_FROM_TOTAL_SIZE) + else if (type == UnzipProgressType.PercentageFromTotalSize) { ulong zipSize = 0; @@ -150,12 +169,15 @@ public static class ArchiveManager try { - entry.ExtractToFile(Path.Combine(folder, entry.FullName), true); + string path = Path.Combine(folder, entry.FullName); + Directory.CreateDirectory(Path.GetDirectoryName(path)); + + entry.ExtractToFile(path, true); currentSize += (ulong)entry.CompressedLength; } catch (Exception ex) { - Config.Logger.Log(ex.Message, typeof(ArchiveManager), LogType.ERROR); + _Logger.LogException(ex, this); } await Task.Delay(10); diff --git a/DiscordBotCore.Utilities/DiscordBotCore.Utilities.csproj b/DiscordBotCore.Utilities/DiscordBotCore.Utilities.csproj new file mode 100644 index 0000000..4cdc3ff --- /dev/null +++ b/DiscordBotCore.Utilities/DiscordBotCore.Utilities.csproj @@ -0,0 +1,15 @@ + + + + net8.0 + enable + enable + AnyCPU;x64;ARM64 + + + + + + + + diff --git a/DiscordBotCore.Utilities/JsonManager.cs b/DiscordBotCore.Utilities/JsonManager.cs new file mode 100644 index 0000000..653dc4e --- /dev/null +++ b/DiscordBotCore.Utilities/JsonManager.cs @@ -0,0 +1,102 @@ +using System.Reflection; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace DiscordBotCore.Utilities; + +public static class JsonManager +{ + + public static async Task ConvertToJson(List data, string[] propertyNamesToUse) + { + if (data == null) throw new ArgumentNullException(nameof(data)); + if (propertyNamesToUse == null) throw new ArgumentNullException(nameof(propertyNamesToUse)); + + // Use reflection to filter properties dynamically + var filteredData = data.Select(item => + { + if (item == null) return null; + + var type = typeof(T); + var propertyInfos = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); + + // Create a dictionary with specified properties and their values + var selectedProperties = propertyInfos + .Where(p => propertyNamesToUse.Contains(p.Name)) + .ToDictionary(p => p.Name, p => p.GetValue(item)); + + return selectedProperties; + }).ToList(); + + // Serialize the filtered data to JSON + var options = new JsonSerializerOptions + { + WriteIndented = true, // For pretty-print JSON; remove if not needed + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + + return await Task.FromResult(JsonSerializer.Serialize(filteredData, options)); + } + + public static async Task ConvertToJsonString(T Data) + { + var str = new MemoryStream(); + await JsonSerializer.SerializeAsync(str, Data, typeof(T), new JsonSerializerOptions + { + WriteIndented = false, + }); + var result = Encoding.ASCII.GetString(str.ToArray()); + await str.FlushAsync(); + str.Close(); + return result; + } + + /// + /// Save to JSON file + /// + /// The class type + /// The file path + /// The values + /// + public static async Task SaveToJsonFile(string file, T Data) + { + var str = new MemoryStream(); + await JsonSerializer.SerializeAsync(str, Data, typeof(T), new JsonSerializerOptions + { + WriteIndented = true, + } + ); + await File.WriteAllBytesAsync(file, str.ToArray()); + await str.FlushAsync(); + str.Close(); + } + + /// + /// Convert json text or file to some kind of data + /// + /// The data type + /// The file or json text + /// + public static async Task ConvertFromJson(string input) + { + Stream text; + if (File.Exists(input)) + text = new MemoryStream(await File.ReadAllBytesAsync(input)); + else + text = new MemoryStream(Encoding.ASCII.GetBytes(input)); + + text.Position = 0; + + JsonSerializerOptions options = new JsonSerializerOptions() + { + PropertyNameCaseInsensitive = true + }; + + var obj = await JsonSerializer.DeserializeAsync(text, options); + await text.FlushAsync(); + text.Close(); + + return (obj ?? default)!; + } +} diff --git a/PluginManager/Others/OneOf.cs b/DiscordBotCore.Utilities/OneOf.cs similarity index 95% rename from PluginManager/Others/OneOf.cs rename to DiscordBotCore.Utilities/OneOf.cs index 44647c7..93548ce 100644 --- a/PluginManager/Others/OneOf.cs +++ b/DiscordBotCore.Utilities/OneOf.cs @@ -1,12 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace DiscordBotCore.Utilities; -namespace PluginManager.Others -{ - public class OneOf + public class OneOf { public T0 Item0 { get; } public T1 Item1 { get; } @@ -135,5 +129,4 @@ namespace PluginManager.Others return Item0 != null ? item0(Item0) : Item1 != null ? item1(Item1) : Item2 != null ? item2(Item2) : item3(Item3); } - } -} + } \ No newline at end of file diff --git a/DiscordBotCore.Utilities/OperatingSystem.cs b/DiscordBotCore.Utilities/OperatingSystem.cs new file mode 100644 index 0000000..92ce69c --- /dev/null +++ b/DiscordBotCore.Utilities/OperatingSystem.cs @@ -0,0 +1,46 @@ +namespace DiscordBotCore.Utilities; + +public class OperatingSystem +{ + public enum OperatingSystemEnum : int + { + Windows = 0, + Linux = 1, + MacOs = 2 + } + + public static OperatingSystemEnum GetOperatingSystem() + { + if(System.OperatingSystem.IsLinux()) return OperatingSystemEnum.Linux; + if(System.OperatingSystem.IsWindows()) return OperatingSystemEnum.Windows; + if(System.OperatingSystem.IsMacOS()) return OperatingSystemEnum.MacOs; + throw new PlatformNotSupportedException(); + } + + public static string GetOperatingSystemString(OperatingSystemEnum os) + { + return os switch + { + OperatingSystemEnum.Windows => "Windows", + OperatingSystemEnum.Linux => "Linux", + OperatingSystemEnum.MacOs => "MacOS", + _ => throw new ArgumentOutOfRangeException() + }; + } + + public static OperatingSystemEnum GetOperatingSystemFromString(string os) + { + return os.ToLower() switch + { + "windows" => OperatingSystemEnum.Windows, + "linux" => OperatingSystemEnum.Linux, + "macos" => OperatingSystemEnum.MacOs, + _ => throw new ArgumentOutOfRangeException() + }; + } + + public static int GetOperatingSystemInt() + { + return (int) GetOperatingSystem(); + } +} \ No newline at end of file diff --git a/DiscordBotCore.Utilities/Option.cs b/DiscordBotCore.Utilities/Option.cs new file mode 100644 index 0000000..46240f7 --- /dev/null +++ b/DiscordBotCore.Utilities/Option.cs @@ -0,0 +1,258 @@ +namespace DiscordBotCore.Utilities; +public class Option2 where TError : Exception +{ + private readonly int _Index; + + private T0 Item0 { get; } = default!; + private T1 Item1 { get; } = default!; + + private TError Error { get; } = default!; + + public Option2(T0 item0) + { + Item0 = item0; + _Index = 0; + } + + public Option2(T1 item1) + { + Item1 = item1; + _Index = 1; + } + + public Option2(TError error) + { + Error = error; + _Index = 2; + } + + public static implicit operator Option2(T0 item0) => new Option2(item0); + public static implicit operator Option2(T1 item1) => new Option2(item1); + public static implicit operator Option2(TError error) => new Option2(error); + + public void Match(Action item0, Action item1, Action error) + { + switch (_Index) + { + case 0: + item0(Item0); + break; + case 1: + item1(Item1); + break; + case 2: + error(Error); + break; + default: + throw new InvalidOperationException(); + } + } + + public TResult Match(Func item0, Func item1, Func error) + { + return _Index switch + { + 0 => item0(Item0), + 1 => item1(Item1), + 2 => error(Error), + _ => throw new InvalidOperationException(), + }; + } + + public override string ToString() + { + return _Index switch + { + 0 => $"Option2<{typeof(T0).Name}>: {Item0}", + 1 => $"Option2<{typeof(T1).Name}>: {Item1}", + 2 => $"Option2<{typeof(TError).Name}>: {Error}", + _ => "Invalid Option2" + }; + } + +} + +public class Option3 where TError : Exception +{ + private readonly int _Index; + + private T0 Item0 { get; } = default!; + private T1 Item1 { get; } = default!; + private T2 Item2 { get; } = default!; + private TError Error { get; } = default!; + + public Option3(T0 item0) + { + Item0 = item0; + _Index = 0; + } + + public Option3(T1 item1) + { + Item1 = item1; + _Index = 1; + } + + public Option3(T2 item2) + { + Item2 = item2; + _Index = 2; + } + + public Option3(TError error) + { + Error = error; + _Index = 3; + } + + public static implicit operator Option3(T0 item0) => new Option3(item0); + public static implicit operator Option3(T1 item1) => new Option3(item1); + public static implicit operator Option3(T2 item2) => new Option3(item2); + public static implicit operator Option3(TError error) => new Option3(error); + + public void Match(Action item0, Action item1, Action item2, Action error) + { + switch (_Index) + { + case 0: + item0(Item0); + break; + case 1: + item1(Item1); + break; + case 2: + item2(Item2); + break; + case 3: + error(Error); + break; + default: + throw new InvalidOperationException(); + } + } + + public TResult Match(Func item0, Func item1, Func item2, Func error) + { + return _Index switch + { + 0 => item0(Item0), + 1 => item1(Item1), + 2 => item2(Item2), + 3 => error(Error), + _ => throw new InvalidOperationException(), + }; + } + + public override string ToString() + { + return _Index switch + { + 0 => $"Option3<{typeof(T0).Name}>: {Item0}", + 1 => $"Option3<{typeof(T1).Name}>: {Item1}", + 2 => $"Option3<{typeof(T2).Name}>: {Item2}", + 3 => $"Option3<{typeof(TError).Name}>: {Error}", + _ => "Invalid Option3" + }; + } +} + +public class Option4 where TError : Exception +{ + private readonly int _Index; + + private T0 Item0 { get; } = default!; + private T1 Item1 { get; } = default!; + private T2 Item2 { get; } = default!; + private T3 Item3 { get; } = default!; + + private TError Error { get; } = default!; + + public Option4(T0 item0) + { + Item0 = item0; + _Index = 0; + } + + public Option4(T1 item1) + { + Item1 = item1; + _Index = 1; + } + + public Option4(T2 item2) + { + Item2 = item2; + _Index = 2; + } + + public Option4(T3 item3) + { + Item3 = item3; + _Index = 3; + } + + public Option4(TError error) + { + Error = error; + _Index = 4; + } + + + public static implicit operator Option4(T0 item0) => new Option4(item0); + public static implicit operator Option4(T1 item1) => new Option4(item1); + public static implicit operator Option4(T2 item2) => new Option4(item2); + public static implicit operator Option4(T3 item3) => new Option4(item3); + public static implicit operator Option4(TError error) => new Option4(error); + + + public void Match(Action item0, Action item1, Action item2, Action item3, Action error) + { + switch (_Index) + { + case 0: + item0(Item0); + break; + case 1: + item1(Item1); + break; + case 2: + item2(Item2); + break; + case 3: + item3(Item3); + break; + case 4: + error(Error); + break; + default: + throw new InvalidOperationException(); + } + } + + + public TResult Match(Func item0, Func item1, Func item2, Func item3, Func error) + { + return _Index switch + { + 0 => item0(Item0), + 1 => item1(Item1), + 2 => item2(Item2), + 3 => item3(Item3), + 4 => error(Error), + _ => throw new InvalidOperationException(), + }; + } + + public override string ToString() + { + return _Index switch + { + 0 => $"Option4<{typeof(T0).Name}>: {Item0}", + 1 => $"Option4<{typeof(T1).Name}>: {Item1}", + 2 => $"Option4<{typeof(T2).Name}>: {Item2}", + 3 => $"Option4<{typeof(T3).Name}>: {Item3}", + 4 => $"Option4<{typeof(TError).Name}>: {Error}", + _ => "Invalid Option4" + }; + } +} \ No newline at end of file diff --git a/DiscordBotCore.Utilities/Responses/IResponse.cs b/DiscordBotCore.Utilities/Responses/IResponse.cs new file mode 100644 index 0000000..dfe01c5 --- /dev/null +++ b/DiscordBotCore.Utilities/Responses/IResponse.cs @@ -0,0 +1,8 @@ +namespace DiscordBotCore.Utilities.Responses; + +public interface IResponse +{ + public bool IsSuccess { get; } + public string Message { get; } + public T? Data { get; } +} \ No newline at end of file diff --git a/DiscordBotCore.Utilities/Responses/Response.cs b/DiscordBotCore.Utilities/Responses/Response.cs new file mode 100644 index 0000000..144396f --- /dev/null +++ b/DiscordBotCore.Utilities/Responses/Response.cs @@ -0,0 +1,45 @@ +namespace DiscordBotCore.Utilities.Responses; + +public class Response : IResponse +{ + public bool IsSuccess => Data; + public string Message { get; } + public bool Data { get; } + + private Response(bool result) + { + Data = result; + Message = string.Empty; + } + + private Response(string message) + { + Data = false; + Message = message; + } + + public static Response Success() => new Response(true); + public static Response Failure(string message) => new Response(message); +} + +public class Response : IResponse where T : class +{ + public bool IsSuccess => Data is not null; + public string Message { get; } + public T? Data { get; } + + private Response(T data) + { + Data = data; + Message = string.Empty; + } + + private Response(string message) + { + Data = null; + Message = message; + } + + public static Response Success(T data) => new Response(data); + public static Response Failure(string message) => new Response(message); +} \ No newline at end of file diff --git a/DiscordBotCore.Utilities/Result.cs b/DiscordBotCore.Utilities/Result.cs new file mode 100644 index 0000000..e35ccaa --- /dev/null +++ b/DiscordBotCore.Utilities/Result.cs @@ -0,0 +1,80 @@ +namespace DiscordBotCore.Utilities; + +public class Result +{ + private bool? _Result; + private Exception? Exception { get; } + + + private Result(Exception exception) + { + _Result = null; + Exception = exception; + } + + private Result(bool result) + { + _Result = result; + Exception = null; + } + + public bool IsSuccess => _Result.HasValue && _Result.Value; + + public void HandleException(Action action) + { + if(IsSuccess) + { + return; + } + + action(Exception!); + } + + public static Result Success() => new Result(true); + public static Result Failure(Exception ex) => new Result(ex); + public static Result Failure(string message) => new Result(new Exception(message)); + + public void Match(Action successAction, Action exceptionAction) + { + if (_Result.HasValue && _Result.Value) + { + successAction(); + } + else + { + exceptionAction(Exception!); + } + } + + + public TResult Match(Func successAction, Func errorAction) + { + return IsSuccess ? successAction() : errorAction(Exception!); + } + +} + +public class Result +{ + private readonly OneOf _Result; + + private Result(OneOf result) + { + _Result = result; + } + + public static Result From (T value) => new Result(new OneOf(value)); + public static implicit operator Result(Exception exception) => new Result(new OneOf(exception)); + + + + public void Match(Action valueAction, Action exceptionAction) + { + _Result.Match(valueAction, exceptionAction); + } + + public TResult Match(Func valueFunc, Func exceptionFunc) + { + return _Result.Match(valueFunc, exceptionFunc); + } +} \ No newline at end of file diff --git a/DiscordBotCore.Utilities/UnzipProgressType.cs b/DiscordBotCore.Utilities/UnzipProgressType.cs new file mode 100644 index 0000000..3225ec5 --- /dev/null +++ b/DiscordBotCore.Utilities/UnzipProgressType.cs @@ -0,0 +1,7 @@ +namespace DiscordBotCore.Utilities; + +public enum UnzipProgressType +{ + PercentageFromNumberOfFiles, + PercentageFromTotalSize +} \ No newline at end of file diff --git a/PluginManager/Bot/CommandHandler.cs b/DiscordBotCore/Bot/CommandHandler.cs similarity index 51% rename from PluginManager/Bot/CommandHandler.cs rename to DiscordBotCore/Bot/CommandHandler.cs index 1789a80..bde2ca4 100644 --- a/PluginManager/Bot/CommandHandler.cs +++ b/DiscordBotCore/Bot/CommandHandler.cs @@ -1,44 +1,53 @@ using System; +using System.IO; using System.Linq; using System.Reflection; using System.Threading.Tasks; + using Discord.Commands; using Discord.WebSocket; -using PluginManager.Interfaces; -using PluginManager.Loaders; -using PluginManager.Online; -using PluginManager.Others; -using PluginManager.Others.Permissions; +using DiscordBotCore.Configuration; +using DiscordBotCore.Logging; +using DiscordBotCore.Others; +using DiscordBotCore.PluginCore.Helpers; +using DiscordBotCore.PluginCore.Helpers.Execution.DbCommand; +using DiscordBotCore.PluginCore.Interfaces; +using DiscordBotCore.PluginManagement.Loading; -namespace PluginManager.Bot; +namespace DiscordBotCore.Bot; -internal class CommandHandler +internal class CommandHandler : ICommandHandler { - private readonly string _botPrefix; - private readonly DiscordSocketClient _client; + private static readonly string _DefaultPrefix = ";"; + private readonly CommandService _commandService; + private readonly ILogger _logger; + private readonly IPluginLoader _pluginLoader; + private readonly IConfiguration _configuration; /// /// Command handler constructor /// - /// The discord bot client + /// The plugin loader /// The discord bot command service /// The prefix to watch for - public CommandHandler(DiscordSocketClient client, CommandService commandService, string botPrefix) + /// The logger + public CommandHandler(ILogger logger, IPluginLoader pluginLoader, IConfiguration configuration, CommandService commandService) { - _client = client; _commandService = commandService; - _botPrefix = botPrefix; + _logger = logger; + _pluginLoader = pluginLoader; + _configuration = configuration; } /// /// The method to initialize all commands /// /// - public async Task InstallCommandsAsync() + public async Task InstallCommandsAsync(DiscordSocketClient client) { - _client.MessageReceived += MessageHandler; - _client.SlashCommandExecuted += Client_SlashCommandExecuted; + client.MessageReceived += (message) => MessageHandler(client, message); + client.SlashCommandExecuted += Client_SlashCommandExecuted; await _commandService.AddModulesAsync(Assembly.GetEntryAssembly(), null); } @@ -46,18 +55,18 @@ internal class CommandHandler { try { - var plugin = PluginLoader.SlashCommands.FirstOrDefault(p => p.Name == arg.Data.Name); + var plugin = _pluginLoader.SlashCommands.FirstOrDefault(p => p.Name == arg.Data.Name); if (plugin is null) throw new Exception("Failed to run command !"); if (arg.Channel is SocketDMChannel) - plugin.ExecuteDM(arg); - else plugin.ExecuteServer(arg); + plugin.ExecuteDm(_logger, arg); + else plugin.ExecuteServer(_logger, arg); } catch (Exception ex) { - Config.Logger.Log(ex.Message, type: LogType.ERROR, source: typeof(CommandHandler)); + _logger.LogException(ex, this); } return Task.CompletedTask; @@ -68,42 +77,43 @@ internal class CommandHandler /// /// The message got from the user in discord chat /// - private async Task MessageHandler(SocketMessage Message) + private async Task MessageHandler(DiscordSocketClient socketClient, SocketMessage socketMessage) { try { - if (Message.Author.IsBot) + if (socketMessage.Author.IsBot) return; - if (Message as SocketUserMessage == null) + if (socketMessage as SocketUserMessage == null) return; - var message = Message as SocketUserMessage; + var message = socketMessage as SocketUserMessage; if (message is null) return; var argPos = 0; - if (!message.Content.StartsWith(_botPrefix) && !message.HasMentionPrefix(_client.CurrentUser, ref argPos)) + string botPrefix = this._configuration.Get("prefix", _DefaultPrefix); + + if (!message.Content.StartsWith(botPrefix) && !message.HasMentionPrefix(socketClient.CurrentUser, ref argPos)) return; - var context = new SocketCommandContext(_client, message); + var context = new SocketCommandContext(socketClient, message); await _commandService.ExecuteAsync(context, argPos, null); - DBCommand? plugin; + IDbCommand? plugin; var cleanMessage = ""; - if (message.HasMentionPrefix(_client.CurrentUser, ref argPos)) + if (message.HasMentionPrefix(socketClient.CurrentUser, ref argPos)) { - var mentionPrefix = "<@" + _client.CurrentUser.Id + ">"; + var mentionPrefix = "<@" + socketClient.CurrentUser.Id + ">"; - plugin = PluginLoader.Commands! + plugin = _pluginLoader.Commands! .FirstOrDefault(plug => plug.Command == message.Content.Substring(mentionPrefix.Length + 1) .Split(' ')[0] || - plug.Aliases is not null && plug.Aliases.Contains(message.CleanContent .Substring(mentionPrefix.Length + 1) .Split(' ')[0] @@ -112,48 +122,62 @@ internal class CommandHandler cleanMessage = message.Content.Substring(mentionPrefix.Length + 1); } - else { - plugin = PluginLoader.Commands! + plugin = _pluginLoader.Commands! .FirstOrDefault(p => p.Command == - message.Content.Split(' ')[0].Substring(_botPrefix.Length) || - p.Aliases is not null && + message.Content.Split(' ')[0].Substring(botPrefix.Length) || p.Aliases.Contains( message.Content.Split(' ')[0] - .Substring(_botPrefix.Length) + .Substring(botPrefix.Length) ) ); - cleanMessage = message.Content.Substring(_botPrefix.Length); + cleanMessage = message.Content.Substring(botPrefix.Length); } if (plugin is null) + { return; + } - if (plugin.requireAdmin && !context.Message.Author.IsAdmin()) + if (plugin.RequireAdmin && !context.Message.Author.IsAdmin()) + { return; + } var split = cleanMessage.Split(' '); string[]? argsClean = null; if (split.Length > 1) + { argsClean = string.Join(' ', split, 1, split.Length - 1).Split(' '); + } - DbCommandExecutingArguments cmd = new(context, cleanMessage, split[0], argsClean); + DbCommandExecutingArgument cmd = new(_logger, + context, + cleanMessage, + split[0], + argsClean, + new DirectoryInfo(Path.Combine(_configuration.Get("ResourcesFolder"), plugin.Command))); - Config.Logger.Log( - $"User ({context.User.Username}) from Guild \"{context.Guild.Name}\" executed command \"{cmd.cleanContent}\"", - typeof(CommandHandler), - LogType.INFO + _logger.Log( + $"User ({context.User.Username}) from Guild \"{context.Guild.Name}\" executed command \"{cmd.CleanContent}\"", + this, + LogType.Info ); if (context.Channel is SocketDMChannel) - plugin.ExecuteDM(cmd); - else plugin.ExecuteServer(cmd); + { + await plugin.ExecuteDm(cmd); + } + else + { + await plugin.ExecuteServer(cmd); + } } catch (Exception ex) { - Config.Logger.Log(ex.Message, type: LogType.ERROR, source: typeof(CommandHandler)); + _logger.LogException(ex, this); } } } diff --git a/DiscordBotCore/Bot/DiscordBotApplication.cs b/DiscordBotCore/Bot/DiscordBotApplication.cs new file mode 100644 index 0000000..683bee9 --- /dev/null +++ b/DiscordBotCore/Bot/DiscordBotApplication.cs @@ -0,0 +1,144 @@ +using System; +using System.Threading.Tasks; +using Discord; +using Discord.Commands; +using Discord.WebSocket; +using DiscordBotCore.Configuration; +using DiscordBotCore.Logging; +using DiscordBotCore.PluginManagement.Loading; + + +namespace DiscordBotCore.Bot; + +public class DiscordBotApplication : IDiscordBotApplication +{ + internal static IPluginLoader _InternalPluginLoader; + + private CommandHandler _CommandServiceHandler; + private CommandService _Service; + private readonly ILogger _Logger; + private readonly IConfiguration _Configuration; + private readonly IPluginLoader _PluginLoader; + + public bool IsReady { get; private set; } + + public DiscordSocketClient Client { get; private set; } + + /// + /// The main Boot constructor + /// + public DiscordBotApplication(ILogger logger, IConfiguration configuration, IPluginLoader pluginLoader) + { + this._Logger = logger; + this._Configuration = configuration; + this._PluginLoader = pluginLoader; + + _InternalPluginLoader = pluginLoader; + } + + public async Task StopAsync() + { + if (!IsReady) + { + _Logger.Log("Can not stop the bot. It is not yet initialized.", this, LogType.Error); + return; + } + + await _PluginLoader.UnloadAllPlugins(); + + await Client.LogoutAsync(); + await Client.StopAsync(); + + Client.Log -= Log; + Client.LoggedIn -= LoggedIn; + Client.Ready -= Ready; + Client.Disconnected -= Client_Disconnected; + + await Client.DisposeAsync(); + + + IsReady = false; + + } + + /// + /// The start method for the bot. This method is used to load the bot + /// + public async Task StartAsync() + { + var config = new DiscordSocketConfig + { + AlwaysDownloadUsers = true, + + //Disable system clock checkup (for responses at slash commands) + UseInteractionSnowflakeDate = false, + GatewayIntents = GatewayIntents.All + }; + + DiscordSocketClient client = new DiscordSocketClient(config); + + + _Service = new CommandService(); + + client.Log += Log; + client.LoggedIn += LoggedIn; + client.Ready += Ready; + client.Disconnected += Client_Disconnected; + + Client = client; + await client.LoginAsync(TokenType.Bot, _Configuration.Get("token")); + await client.StartAsync(); + + _CommandServiceHandler = new CommandHandler(_Logger, _PluginLoader, _Configuration, _Service); + + await _CommandServiceHandler.InstallCommandsAsync(client); + + while (!IsReady) + { + await Task.Delay(100); + } + } + + private async Task Client_Disconnected(Exception arg) + { + if (arg.Message.Contains("401")) + { + _Configuration.Set("token", string.Empty); + _Logger.Log("The token is invalid.", this, LogType.Critical); + await _Configuration.SaveToFile(); + } + } + + private Task Ready() + { + IsReady = true; + return Task.CompletedTask; + } + + private Task LoggedIn() + { + _Logger.Log("Successfully Logged In", this); + _PluginLoader.SetDiscordClient(Client); + return Task.CompletedTask; + } + + private Task Log(LogMessage message) + { + switch (message.Severity) + { + case LogSeverity.Error: + case LogSeverity.Critical: + _Logger.Log(message.Message, this, LogType.Error); + break; + + case LogSeverity.Info: + case LogSeverity.Debug: + _Logger.Log(message.Message, this, LogType.Info); + + + break; + } + + return Task.CompletedTask; + } +} diff --git a/DiscordBotCore/Bot/ICommandHandler.cs b/DiscordBotCore/Bot/ICommandHandler.cs new file mode 100644 index 0000000..9720ea2 --- /dev/null +++ b/DiscordBotCore/Bot/ICommandHandler.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; +using Discord.WebSocket; + +namespace DiscordBotCore.Bot; + +internal interface ICommandHandler +{ + /// + /// The method to initialize all commands + /// + /// + Task InstallCommandsAsync(DiscordSocketClient client); +} \ No newline at end of file diff --git a/DiscordBotCore/Bot/IDiscordBotApplication.cs b/DiscordBotCore/Bot/IDiscordBotApplication.cs new file mode 100644 index 0000000..3478fdd --- /dev/null +++ b/DiscordBotCore/Bot/IDiscordBotApplication.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; +using Discord.WebSocket; + +namespace DiscordBotCore.Bot; + +public interface IDiscordBotApplication +{ + public bool IsReady { get; } + public DiscordSocketClient Client { get; } + + /// + /// The start method for the bot. This method is used to load the bot + /// + Task StartAsync(); + + /// + /// Stops the bot and cleans up resources. + /// + Task StopAsync(); +} \ No newline at end of file diff --git a/DiscordBotCore/Commands/HelpCommand.cs b/DiscordBotCore/Commands/HelpCommand.cs new file mode 100644 index 0000000..9cae45c --- /dev/null +++ b/DiscordBotCore/Commands/HelpCommand.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using Discord; +using DiscordBotCore.Bot; +using DiscordBotCore.PluginCore.Helpers.Execution.DbCommand; +using DiscordBotCore.PluginCore.Interfaces; + +namespace DiscordBotCore.Commands; + +public class HelpCommand : IDbCommand +{ + public string Command => "help"; + public List Aliases => []; + public string Description => "Help command for the bot."; + public string Usage => "help "; + public bool RequireAdmin => false; + + public async Task ExecuteServer(IDbCommandExecutingArgument args) + { + if (args.Arguments is not null) + { + string searchedCommand = args.Arguments[0]; + IDbCommand? command = DiscordBotApplication._InternalPluginLoader.Commands.FirstOrDefault(c => c.Command.Equals(searchedCommand, StringComparison.OrdinalIgnoreCase)); + + if (command is null) + { + await args.Context.Channel.SendMessageAsync($"Command `{searchedCommand}` not found."); + return; + } + + EmbedBuilder helpEmbed = GenerateHelpCommand(command); + await args.Context.Channel.SendMessageAsync(embed: helpEmbed.Build()); + return; + } + + if (DiscordBotApplication._InternalPluginLoader.Commands.Count == 0) + { + await args.Context.Channel.SendMessageAsync("No commands found."); + return; + } + + var embedBuilder = new EmbedBuilder(); + + var adminCommands = ""; + var normalCommands = ""; + + foreach (var cmd in DiscordBotApplication._InternalPluginLoader.Commands) + if (cmd.RequireAdmin) + adminCommands += cmd.Command + " "; + else + normalCommands += cmd.Command + " "; + + + if (adminCommands.Length > 0) + embedBuilder.AddField("Admin Commands", adminCommands); + if (normalCommands.Length > 0) + embedBuilder.AddField("Normal Commands", normalCommands); + await args.Context.Channel.SendMessageAsync(embed: embedBuilder.Build()); + } + + private EmbedBuilder GenerateHelpCommand(IDbCommand command) + { + EmbedBuilder builder = new(); + builder.WithTitle($"Command: {command.Command}"); + builder.WithDescription(command.Description); + builder.WithColor(Color.Blue); + builder.AddField("Usage", command.Usage); + string aliases = ""; + foreach (var alias in command.Aliases) + aliases += alias + " "; + builder.AddField("Aliases", aliases.Length > 0 ? aliases : "None"); + return builder; + } +} \ No newline at end of file diff --git a/DiscordBotCore/DiscordBotCore.csproj b/DiscordBotCore/DiscordBotCore.csproj new file mode 100644 index 0000000..391d934 --- /dev/null +++ b/DiscordBotCore/DiscordBotCore.csproj @@ -0,0 +1,18 @@ + + + net8.0 + enable + Library + AnyCPU;x64;ARM64 + + + + + + + + + + + + \ No newline at end of file diff --git a/PluginManager/Others/Permissions/DiscordPermissions.cs b/DiscordBotCore/Others/DiscordPermissions.cs similarity index 83% rename from PluginManager/Others/Permissions/DiscordPermissions.cs rename to DiscordBotCore/Others/DiscordPermissions.cs index 24b600c..2c88b97 100644 --- a/PluginManager/Others/Permissions/DiscordPermissions.cs +++ b/DiscordBotCore/Others/DiscordPermissions.cs @@ -1,12 +1,9 @@ -using System.Linq; +using System.Linq; using Discord; using Discord.WebSocket; -namespace PluginManager.Others.Permissions; +namespace DiscordBotCore.Others; -/// -/// A class whith all discord permissions -/// public static class DiscordPermissions { /// @@ -15,7 +12,7 @@ public static class DiscordPermissions /// The role /// The permission /// - public static bool hasPermission(this IRole role, GuildPermission permission) + public static bool HasPermission(this IRole role, GuildPermission permission) { return role.Permissions.Has(permission); } @@ -30,7 +27,6 @@ public static class DiscordPermissions { return user.Roles.Contains(role); } - /// /// Check if user has the specified permission /// @@ -39,7 +35,7 @@ public static class DiscordPermissions /// public static bool HasPermission(this SocketGuildUser user, GuildPermission permission) { - return user.Roles.Where(role => role.hasPermission(permission)).Any() || user.Guild.Owner == user; + return user.Roles.Any(role => role.HasPermission(permission)) || user.Guild.Owner == user; } /// @@ -61,4 +57,4 @@ public static class DiscordPermissions { return IsAdmin((SocketGuildUser)user); } -} +} \ No newline at end of file diff --git a/DiscordBotCore/Properties/launchSettings.json b/DiscordBotCore/Properties/launchSettings.json new file mode 100644 index 0000000..ba70e2c --- /dev/null +++ b/DiscordBotCore/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "DiscordBotCore": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:49707;http://localhost:49708" + } + } +} \ No newline at end of file diff --git a/DiscordBotUI/.gitignore b/DiscordBotUI/.gitignore deleted file mode 100644 index 8afdcb6..0000000 --- a/DiscordBotUI/.gitignore +++ /dev/null @@ -1,454 +0,0 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore - -# User-specific files -*.rsuser -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Mono auto generated files -mono_crash.* - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -[Ww][Ii][Nn]32/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ -[Ll]ogs/ - -# Visual Studio 2015/2017 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUnit -*.VisualState.xml -TestResult.xml -nunit-*.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ - -# Tye -.tye/ - -# ASP.NET Scaffolding -ScaffoldingReadMe.txt - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio -*_i.c -*_p.c -*_h.h -*.ilk -*.meta -*.obj -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*_wpftmp.csproj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Coverlet is a free, cross platform Code Coverage Tool -coverage*.json -coverage*.xml -coverage*.info - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# NuGet Symbol Packages -*.snupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx -*.appxbundle -*.appxupload - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!?*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser -*- [Bb]ackup.rdl -*- [Bb]ackup ([0-9]).rdl -*- [Bb]ackup ([0-9][0-9]).rdl - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# CodeRush personal settings -.cr/personal - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -.mfractor/ - -# Local History for Visual Studio -.localhistory/ - -# BeatPulse healthcheck temp database -healthchecksdb - -# Backup folder for Package Reference Convert tool in Visual Studio 2017 -MigrationBackup/ - -# Ionide (cross platform F# VS Code tools) working folder -.ionide/ - -# Fody - auto-generated XML schema -FodyWeavers.xsd - -## -## Visual studio for Mac -## - - -# globs -Makefile.in -*.userprefs -*.usertasks -config.make -config.status -aclocal.m4 -install-sh -autom4te.cache/ -*.tar.gz -tarballs/ -test-results/ - -# Mac bundle stuff -*.dmg -*.app - -# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore -# General -.DS_Store -.AppleDouble -.LSOverride - -# Icon must end with two \r -Icon - - -# Thumbnails -._* - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns -.com.apple.timemachine.donotpresent - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - -# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore -# Windows thumbnail cache files -Thumbs.db -ehthumbs.db -ehthumbs_vista.db - -# Dump file -*.stackdump - -# Folder config file -[Dd]esktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Windows Installer files -*.cab -*.msi -*.msix -*.msm -*.msp - -# Windows shortcuts -*.lnk - -# JetBrains Rider -.idea/ -*.sln.iml - -## -## Visual Studio Code -## -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json diff --git a/DiscordBotUI/DiscordBotUI.Desktop/DiscordBotUI.Desktop.csproj b/DiscordBotUI/DiscordBotUI.Desktop/DiscordBotUI.Desktop.csproj deleted file mode 100644 index 6befdf1..0000000 --- a/DiscordBotUI/DiscordBotUI.Desktop/DiscordBotUI.Desktop.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - WinExe - - net8.0 - enable - true - - - - app.manifest - - - - - - - - - - - - diff --git a/DiscordBotUI/DiscordBotUI.Desktop/Program.cs b/DiscordBotUI/DiscordBotUI.Desktop/Program.cs deleted file mode 100644 index 0d660fb..0000000 --- a/DiscordBotUI/DiscordBotUI.Desktop/Program.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; - -using Avalonia; -using Avalonia.ReactiveUI; - -namespace DiscordBotUI.Desktop -{ - internal sealed class Program - { - // Initialization code. Don't use any Avalonia, third-party APIs or any - // SynchronizationContext-reliant code before AppMain is called: things aren't initialized - // yet and stuff might break. - [STAThread] - public static void Main(string[] args) => BuildAvaloniaApp() - .StartWithClassicDesktopLifetime(args); - - // Avalonia configuration, don't remove; also used by visual designer. - public static AppBuilder BuildAvaloniaApp() - => AppBuilder.Configure() - .UsePlatformDetect() - .WithInterFont() - .LogToTrace() - .UseReactiveUI(); - } -} diff --git a/DiscordBotUI/DiscordBotUI.Desktop/Properties/launchSettings.json b/DiscordBotUI/DiscordBotUI.Desktop/Properties/launchSettings.json deleted file mode 100644 index 69f26c5..0000000 --- a/DiscordBotUI/DiscordBotUI.Desktop/Properties/launchSettings.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "profiles": { - "DiscordBotUI.Desktop": { - "commandName": "Project" - }, - "WSL": { - "commandName": "WSL2", - "distributionName": "" - } - } -} \ No newline at end of file diff --git a/DiscordBotUI/DiscordBotUI.Desktop/app.manifest b/DiscordBotUI/DiscordBotUI.Desktop/app.manifest deleted file mode 100644 index 1a06515..0000000 --- a/DiscordBotUI/DiscordBotUI.Desktop/app.manifest +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - diff --git a/DiscordBotUI/DiscordBotUI.Desktop/builder.bat b/DiscordBotUI/DiscordBotUI.Desktop/builder.bat deleted file mode 100644 index f73d6b4..0000000 --- a/DiscordBotUI/DiscordBotUI.Desktop/builder.bat +++ /dev/null @@ -1,35 +0,0 @@ -@echo off -echo "Building..." - -echo "Building linux-x64 not self-contained" -dotnet publish -r linux-x64 -p:PublishSingleFile=false --self-contained true -c Release -o ../publish/linux-x64 - -echo "Building win-x64 not self-contained" -dotnet publish -r win-x64 -p:PublishSingleFile=false --self-contained true -c Release -o ../publish/win-x64 - -echo "Building osx-x64 not self-contained" -dotnet publish -r osx-x64 -p:PublishSingleFile=false --self-contained true -c Release -o ../publish/osx-x64 - - -echo "Building linux-x64 self-contained" -dotnet publish -r linux-x64 -p:PublishSingleFile=true --self-contained true -c Release -o ../publish/linux-x64-selfcontained - -echo "Building win-x64 self-contained" -dotnet publish -r win-x64 -p:PublishSingleFile=true --self-contained true -c Release -o ../publish/win-x64-selfcontained - -echo "Building osx-x64 self-contained" -dotnet publish -r osx-x64 -p:PublishSingleFile=true --self-contained true -c Release -o ../publish/osx-x64-selfcontained - -echo "Zipping..." -mkdir ../publish/zip - - -zip -r ../publish/zip/linux-x64.zip ../publish/linux-x64 -zip -r ../publish/zip/win-x64.zip ../publish/win-x64 -zip -r ../publish/zip/osx-x64.zip ../publish/osx-x64 - -zip -r ../publish/zip/linux-x64-selfcontained.zip ../publish/linux-x64-selfcontained -zip -r ../publish/zip/win-x64-selfcontained.zip ../publish/win-x64-selfcontained -zip -r ../publish/zip/osx-x64-selfcontained.zip ../publish/osx-x64-selfcontained - -echo "Done!" \ No newline at end of file diff --git a/DiscordBotUI/DiscordBotUI/App.axaml b/DiscordBotUI/DiscordBotUI/App.axaml deleted file mode 100644 index c9d9a69..0000000 --- a/DiscordBotUI/DiscordBotUI/App.axaml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/DiscordBotUI/DiscordBotUI/App.axaml.cs b/DiscordBotUI/DiscordBotUI/App.axaml.cs deleted file mode 100644 index 5194606..0000000 --- a/DiscordBotUI/DiscordBotUI/App.axaml.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Avalonia; -using Avalonia.Controls.ApplicationLifetimes; -using Avalonia.Markup.Xaml; - -using DiscordBotUI.ViewModels; -using DiscordBotUI.Views; - -namespace DiscordBotUI -{ - public partial class App : Application - { - public override void Initialize() - { - AvaloniaXamlLoader.Load(this); - } - - public override void OnFrameworkInitializationCompleted() - { - if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) - { - desktop.MainWindow = new HomePage(); - } - else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform) - { - singleViewPlatform.MainView = new HomePage(); - } - - base.OnFrameworkInitializationCompleted(); - } - } -} \ No newline at end of file diff --git a/DiscordBotUI/DiscordBotUI/Assets/avalonia-logo.ico b/DiscordBotUI/DiscordBotUI/Assets/avalonia-logo.ico deleted file mode 100644 index da8d49f..0000000 Binary files a/DiscordBotUI/DiscordBotUI/Assets/avalonia-logo.ico and /dev/null differ diff --git a/DiscordBotUI/DiscordBotUI/Bot/Commands/Help.cs b/DiscordBotUI/DiscordBotUI/Bot/Commands/Help.cs deleted file mode 100644 index 8f9ec34..0000000 --- a/DiscordBotUI/DiscordBotUI/Bot/Commands/Help.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System.Collections.Generic; -using Discord; -using PluginManager; -using PluginManager.Interfaces; -using PluginManager.Loaders; -using PluginManager.Others; - -namespace DiscordBotUI.Bot.Commands; - -/// -/// The help command -/// -internal class Help: DBCommand -{ - /// - /// Command name - /// - public string Command => "help"; - - public List Aliases => null; - - /// - /// Command Description - /// - public string Description => "This command allows you to check all loaded commands"; - - /// - /// Command usage - /// - public string Usage => "help "; - - /// - /// Check if the command require administrator to be executed - /// - public bool requireAdmin => false; - - /// - /// The main body of the command - /// - /// The command context - public void ExecuteServer(DbCommandExecutingArguments args) - { - if (args.arguments is not null) - { - var e = GenerateHelpCommand(args.arguments[0]); - if (e is null) - args.context.Channel.SendMessageAsync("Unknown Command " + args.arguments[0]); - else - args.context.Channel.SendMessageAsync(embed: e.Build()); - - - return; - } - - var embedBuilder = new EmbedBuilder(); - - var adminCommands = ""; - var normalCommands = ""; - - foreach (var cmd in PluginLoader.Commands) - if (cmd.requireAdmin) - adminCommands += cmd.Command + " "; - else - normalCommands += cmd.Command + " "; - - - if (adminCommands.Length > 0) - embedBuilder.AddField("Admin Commands", adminCommands); - if (normalCommands.Length > 0) - embedBuilder.AddField("Normal Commands", normalCommands); - args.context.Channel.SendMessageAsync(embed: embedBuilder.Build()); - } - - private EmbedBuilder GenerateHelpCommand(string command) - { - var embedBuilder = new EmbedBuilder(); - var cmd = PluginLoader.Commands.Find(p => p.Command == command || - p.Aliases is not null && p.Aliases.Contains(command) - ); - if (cmd == null) return null; - - embedBuilder.AddField("Usage", Config.AppSettings["prefix"] + cmd.Usage); - embedBuilder.AddField("Description", cmd.Description); - if (cmd.Aliases is null) - return embedBuilder; - embedBuilder.AddField("Alias", cmd.Aliases.Count == 0 ? "-" : string.Join(", ", cmd.Aliases)); - - return embedBuilder; - } -} diff --git a/DiscordBotUI/DiscordBotUI/Bot/DiscordBot.cs b/DiscordBotUI/DiscordBotUI/Bot/DiscordBot.cs deleted file mode 100644 index 1e7ed32..0000000 --- a/DiscordBotUI/DiscordBotUI/Bot/DiscordBot.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System.Threading.Tasks; - -using PluginManager; -using PluginManager.Interfaces; -using PluginManager.Loaders; -using PluginManager.Others; - -namespace DiscordBotUI.Bot -{ - internal class DiscordBot - { - private readonly string[] _StartArguments; - - public DiscordBot(string[] args) - { - this._StartArguments = args; - } - - public async Task InitializeBot() - { - string token = Config.AppSettings["token"]; - string prefix = Config.AppSettings["prefix"]; - PluginManager.Bot.Boot discordBooter = new PluginManager.Bot.Boot(token, prefix); - await discordBooter.Awake(); - } - - public async Task LoadPlugins() - { - var loader = new PluginLoader(Config.DiscordBot.Client); - - loader.OnCommandLoaded += (data) => - { - if (data.IsSuccess) - { - Config.Logger.Log("Successfully loaded command : " + data.PluginName, typeof(ICommandAction), - LogType.INFO - ); - } - - else - { - Config.Logger.Log("Failed to load command : " + data.PluginName + " because " + data.ErrorMessage, - typeof(ICommandAction), LogType.ERROR - ); - } - }; - loader.OnEventLoaded += (data) => - { - if (data.IsSuccess) - { - Config.Logger.Log("Successfully loaded event : " + data.PluginName, typeof(ICommandAction), - LogType.INFO - ); - } - else - { - Config.Logger.Log("Failed to load event : " + data.PluginName + " because " + data.ErrorMessage, - typeof(ICommandAction), LogType.ERROR - ); - } - }; - - loader.OnSlashCommandLoaded += (data) => - { - if (data.IsSuccess) - { - Config.Logger.Log("Successfully loaded slash command : " + data.PluginName, typeof(ICommandAction), - LogType.INFO - ); - } - else - { - Config.Logger.Log("Failed to load slash command : " + data.PluginName + " because " + data.ErrorMessage, - typeof(ICommandAction), LogType.ERROR - ); - } - }; - - await loader.LoadPlugins(); - } - - } -} diff --git a/DiscordBotUI/DiscordBotUI/DiscordBotUI.csproj b/DiscordBotUI/DiscordBotUI/DiscordBotUI.csproj deleted file mode 100644 index 9ceec94..0000000 --- a/DiscordBotUI/DiscordBotUI/DiscordBotUI.csproj +++ /dev/null @@ -1,25 +0,0 @@ - - - net8.0 - enable - latest - true - - - - - - - - - - - - - - - - - - - diff --git a/DiscordBotUI/DiscordBotUI/ViewLocator.cs b/DiscordBotUI/DiscordBotUI/ViewLocator.cs deleted file mode 100644 index c72ec8c..0000000 --- a/DiscordBotUI/DiscordBotUI/ViewLocator.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; - -using Avalonia.Controls; -using Avalonia.Controls.Templates; - -using DiscordBotUI.ViewModels; - -namespace DiscordBotUI -{ - public class ViewLocator : IDataTemplate - { - public Control? Build(object? data) - { - if (data is null) - return null; - - var name = data.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal); - var type = Type.GetType(name); - - if (type != null) - { - return (Control)Activator.CreateInstance(type)!; - } - - return new TextBlock { Text = "Not Found: " + name }; - } - - public bool Match(object? data) - { - return data is ViewModelBase; - } - } -} \ No newline at end of file diff --git a/DiscordBotUI/DiscordBotUI/ViewModels/OnlinePlugin.cs b/DiscordBotUI/DiscordBotUI/ViewModels/OnlinePlugin.cs deleted file mode 100644 index 0545e38..0000000 --- a/DiscordBotUI/DiscordBotUI/ViewModels/OnlinePlugin.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace DiscordBotUI.ViewModels -{ - public class OnlinePlugin - { - public string Name { get; set; } - public string Description { get; set; } - public string Version { get; set; } - - public OnlinePlugin(string name, string description, string version) { - Name = name; - Description = description; - Version = version; - - } - } -} diff --git a/DiscordBotUI/DiscordBotUI/ViewModels/Plugin.cs b/DiscordBotUI/DiscordBotUI/ViewModels/Plugin.cs deleted file mode 100644 index 55bb941..0000000 --- a/DiscordBotUI/DiscordBotUI/ViewModels/Plugin.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace DiscordBotUI.ViewModels -{ - public class Plugin - { - public string Name { get; set; } - public string Version { get; set; } - public bool IsMarkedToUninstall { get; set; } - - public Plugin(string Name, string Version, bool isMarkedToUninstall) - { - this.Name = Name; - this.Version = Version; - IsMarkedToUninstall = isMarkedToUninstall; - } - } -} diff --git a/DiscordBotUI/DiscordBotUI/ViewModels/ViewModelBase.cs b/DiscordBotUI/DiscordBotUI/ViewModels/ViewModelBase.cs deleted file mode 100644 index 6d48d97..0000000 --- a/DiscordBotUI/DiscordBotUI/ViewModels/ViewModelBase.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.ComponentModel; -using System.Runtime.CompilerServices; - -using ReactiveUI; - -namespace DiscordBotUI.ViewModels -{ - public class ViewModelBase : ReactiveObject - { - } -} diff --git a/DiscordBotUI/DiscordBotUI/Views/HomePage.axaml b/DiscordBotUI/DiscordBotUI/Views/HomePage.axaml deleted file mode 100644 index f783e49..0000000 --- a/DiscordBotUI/DiscordBotUI/Views/HomePage.axaml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - -