Merge branch 'preview'
This commit is contained in:
25
.dockerignore
Normal file
25
.dockerignore
Normal file
@@ -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
|
||||
110
.gitignore
vendored
110
.gitignore
vendored
@@ -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/
|
||||
@@ -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<InternalActionOption> 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;
|
||||
}
|
||||
}
|
||||
@@ -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 <option?>";
|
||||
public IEnumerable<InternalActionOption> ListOfOptions => new List<InternalActionOption>
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<float> progress = new Progress<float>(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<Tuple<ProgressTask, IProgress<float>, 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<float> progress = new Progress<float>(p =>
|
||||
{
|
||||
task.Value = p;
|
||||
}
|
||||
);
|
||||
|
||||
task.IsIndeterminate = true;
|
||||
downloadTasks.Add(new Tuple<ProgressTask, IProgress<float>, 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<bool> 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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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 <command?>";
|
||||
|
||||
public IEnumerable<InternalActionOption> 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();
|
||||
}
|
||||
}
|
||||
@@ -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 <option!>";
|
||||
|
||||
public IEnumerable<InternalActionOption> ListOfOptions => new List<InternalActionOption>
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <options!>";
|
||||
public IEnumerable<InternalActionOption> ListOfOptions => new List<InternalActionOption>
|
||||
{
|
||||
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 <settingName> <newValue>: Set a setting");
|
||||
Console.WriteLine("-r <settingName>: Remove a setting");
|
||||
Console.WriteLine("-a <settingName> <newValue>: Add a setting");
|
||||
Console.WriteLine("-h: Show this help message");
|
||||
break;
|
||||
|
||||
default:
|
||||
Console.WriteLine("Invalid option");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// The help command
|
||||
/// </summary>
|
||||
internal class Help: DBCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Command name
|
||||
/// </summary>
|
||||
public string Command => "help";
|
||||
|
||||
public List<string> Aliases => null;
|
||||
|
||||
/// <summary>
|
||||
/// Command Description
|
||||
/// </summary>
|
||||
public string Description => "This command allows you to check all loaded commands";
|
||||
|
||||
/// <summary>
|
||||
/// Command usage
|
||||
/// </summary>
|
||||
public string Usage => "help <command>";
|
||||
|
||||
/// <summary>
|
||||
/// Check if the command require administrator to be executed
|
||||
/// </summary>
|
||||
public bool requireAdmin => false;
|
||||
|
||||
/// <summary>
|
||||
/// The main body of the command
|
||||
/// </summary>
|
||||
/// <param name="context">The command context</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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<SlashCommandOptionBuilder> 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());
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>disable</Nullable>
|
||||
<ApplicationIcon />
|
||||
<StartupObject />
|
||||
<SignAssembly>False</SignAssembly>
|
||||
<IsPublishable>True</IsPublishable>
|
||||
<AssemblyVersion>1.0.4.0</AssemblyVersion>
|
||||
<PublishAot>False</PublishAot>
|
||||
<FileVersion>1.0.4.0</FileVersion>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<DebugType>none</DebugType>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<DebugType>none</DebugType>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="Data\**" />
|
||||
<Compile Remove="obj\**" />
|
||||
<Compile Remove="Output\**" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Remove="Data\**" />
|
||||
<EmbeddedResource Remove="obj\**" />
|
||||
<EmbeddedResource Remove="Output\**" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Remove="Data\**" />
|
||||
<None Remove="obj\**" />
|
||||
<None Remove="Output\**" />
|
||||
<None Remove="builder.bat" />
|
||||
<None Remove="builder.sh" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Spectre.Console" Version="0.49.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\PluginManager\PluginManager.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<string>($"[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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// The main entry point for the application.
|
||||
/// </summary>
|
||||
public static void Startup(string[] args)
|
||||
{
|
||||
PreLoadComponents(args).Wait();
|
||||
|
||||
if (!AppSettings.ContainsKey("ServerID") || !AppSettings.ContainsKey("token") || !AppSettings.ContainsKey("prefix"))
|
||||
Installer.GenerateStartupConfig().Wait();
|
||||
|
||||
HandleInput().Wait();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The main loop for the discord bot
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start the bot without user interface
|
||||
/// </summary>
|
||||
/// <returns>Returns the bootloader for the Discord Bot</returns>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle user input arguments from the startup of the application
|
||||
/// </summary>
|
||||
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} [/]");
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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<T> ExecuteWithProgressBar<T>(Task<T> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<string> Columns;
|
||||
public List<OneOf<string, IRenderable>[]> Rows;
|
||||
|
||||
public TableData()
|
||||
{
|
||||
Columns = new List<string>();
|
||||
Rows = new List<OneOf<string, IRenderable>[]>();
|
||||
}
|
||||
|
||||
public TableData(List<string> columns)
|
||||
{
|
||||
Columns = columns;
|
||||
Rows = new List<OneOf<string, IRenderable>[]>();
|
||||
}
|
||||
|
||||
public bool IsEmpty => Rows.Count == 0;
|
||||
public bool HasRoundBorders { get; set; } = true;
|
||||
public bool DisplayLinesBetweenRows { get; set; } = false;
|
||||
|
||||
public void AddRow(OneOf<string, IRenderable>[] 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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!"
|
||||
@@ -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!"
|
||||
113
DiscordBotCore.Configuration/Configuration.cs
Normal file
113
DiscordBotCore.Configuration/Configuration.cs
Normal file
@@ -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<T>(string key, T defaultValue)
|
||||
{
|
||||
T value = base.Get(key, defaultValue);
|
||||
|
||||
if (_EnableAutoAddOnGetWithDefault && value.Equals(defaultValue))
|
||||
{
|
||||
Add(key, defaultValue);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public override List<T> GetList<T>(string key, List<T> defaultValue)
|
||||
{
|
||||
List<T> 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<JObject>(jsonContent);
|
||||
|
||||
if (jObject is null)
|
||||
{
|
||||
SaveToFile().Wait();
|
||||
return;
|
||||
}
|
||||
|
||||
_InternalDictionary.Clear();
|
||||
|
||||
foreach (var kvp in jObject)
|
||||
{
|
||||
AddPairToDictionary(kvp, _InternalDictionary);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddPairToDictionary(KeyValuePair<string, JToken> kvp, IDictionary<string, object> dict)
|
||||
{
|
||||
if (kvp.Value is JObject nestedJObject)
|
||||
{
|
||||
dict[kvp.Key] = nestedJObject.ToObject<Dictionary<string, object>>();
|
||||
|
||||
foreach (var nestedKvp in nestedJObject)
|
||||
{
|
||||
AddPairToDictionary(nestedKvp, dict[kvp.Key] as Dictionary<string, object>);
|
||||
}
|
||||
}
|
||||
else if (kvp.Value is JArray nestedJArray)
|
||||
{
|
||||
dict[kvp.Key] = nestedJArray.ToObject<List<object>>();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (kvp.Value.Type == JTokenType.Integer)
|
||||
dict[kvp.Key] = kvp.Value.Value<int>();
|
||||
else if (kvp.Value.Type == JTokenType.Float)
|
||||
dict[kvp.Key] = kvp.Value.Value<float>();
|
||||
else if (kvp.Value.Type == JTokenType.Boolean)
|
||||
dict[kvp.Key] = kvp.Value.Value<bool>();
|
||||
else if (kvp.Value.Type == JTokenType.String)
|
||||
dict[kvp.Key] = kvp.Value.Value<string>();
|
||||
else if (kvp.Value.Type == JTokenType.Date)
|
||||
dict[kvp.Key] = kvp.Value.Value<DateTime>();
|
||||
else
|
||||
dict[kvp.Key] = kvp.Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new Settings Dictionary from a file
|
||||
/// </summary>
|
||||
/// <param name="baseFile">The file location</param>
|
||||
/// <param name="enableAutoAddOnGetWithDefault">Set this to true if you want to update the dictionary with default values on get</param>
|
||||
public static Configuration CreateFromFile(ILogger logger, string baseFile, bool enableAutoAddOnGetWithDefault)
|
||||
{
|
||||
var settings = new Configuration(logger, baseFile, enableAutoAddOnGetWithDefault);
|
||||
settings.LoadFromFile();
|
||||
return settings;
|
||||
}
|
||||
}
|
||||
167
DiscordBotCore.Configuration/ConfigurationBase.cs
Normal file
167
DiscordBotCore.Configuration/ConfigurationBase.cs
Normal file
@@ -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<string, object> _InternalDictionary = new Dictionary<string, object>();
|
||||
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<T>(string key, T defaulobject)
|
||||
{
|
||||
if (_InternalDictionary.TryGetValue(key, out var value))
|
||||
{
|
||||
return (T)Convert.ChangeType(value, typeof(T));
|
||||
}
|
||||
|
||||
return defaulobject;
|
||||
}
|
||||
|
||||
public virtual T? Get<T>(string key)
|
||||
{
|
||||
if (_InternalDictionary.TryGetValue(key, out var value))
|
||||
{
|
||||
return (T)Convert.ChangeType(value, typeof(T));
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public virtual IDictionary<TSubKey, TSubValue> GetDictionary<TSubKey, TSubValue>(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<TSubKey, TSubValue>();
|
||||
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<TSubKey, TSubValue>();
|
||||
}
|
||||
|
||||
public virtual List<T> GetList<T>(string key, List<T> 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<T>();
|
||||
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<KeyValuePair<string, object>> GetEnumerator()
|
||||
{
|
||||
return _InternalDictionary.GetEnumerator();
|
||||
}
|
||||
|
||||
public virtual void Clear()
|
||||
{
|
||||
_InternalDictionary.Clear();
|
||||
}
|
||||
|
||||
public virtual bool ContainsKey(string key)
|
||||
{
|
||||
return _InternalDictionary.ContainsKey(key);
|
||||
}
|
||||
|
||||
public virtual IEnumerable<KeyValuePair<string, object>> Where(Func<KeyValuePair<string, object>, bool> predicate)
|
||||
{
|
||||
return _InternalDictionary.Where(predicate);
|
||||
}
|
||||
|
||||
public virtual IEnumerable<KeyValuePair<string, object>> Where(Func<KeyValuePair<string, object>, int, bool> predicate)
|
||||
{
|
||||
return _InternalDictionary.Where(predicate);
|
||||
}
|
||||
|
||||
public virtual IEnumerable<TResult> Where<TResult>(Func<KeyValuePair<string, object>, TResult> selector)
|
||||
{
|
||||
return _InternalDictionary.Select(selector);
|
||||
}
|
||||
|
||||
public virtual IEnumerable<TResult> Where<TResult>(Func<KeyValuePair<string, object>, int, TResult> selector)
|
||||
{
|
||||
return _InternalDictionary.Select(selector);
|
||||
}
|
||||
|
||||
public virtual KeyValuePair<string, object> FirstOrDefault(Func<KeyValuePair<string, object>, bool> predicate)
|
||||
{
|
||||
return _InternalDictionary.FirstOrDefault(predicate);
|
||||
}
|
||||
|
||||
public virtual KeyValuePair<string, object> 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();
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>AnyCPU;x64;ARM64</Platforms>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DiscordBotCore.Logging\DiscordBotCore.Logging.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
132
DiscordBotCore.Configuration/IConfiguration.cs
Normal file
132
DiscordBotCore.Configuration/IConfiguration.cs
Normal file
@@ -0,0 +1,132 @@
|
||||
namespace DiscordBotCore.Configuration;
|
||||
|
||||
public interface IConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds an element to the custom settings dictionary
|
||||
/// </summary>
|
||||
/// <param name="key">The key</param>
|
||||
/// <param name="value">The value</param>
|
||||
void Add(string key, object value);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the value of a key in the custom settings dictionary
|
||||
/// </summary>
|
||||
/// <param name="key">The key</param>
|
||||
/// <param name="value">The value</param>
|
||||
void Set(string key, object value);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="key">The key</param>
|
||||
/// <param name="defaultObject">The default value to be returned if the searched value is not found</param>
|
||||
/// <typeparam name="T">The type of the returned value</typeparam>
|
||||
/// <returns></returns>
|
||||
T Get<T>(string key, T defaultObject);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="key">The key</param>
|
||||
/// <typeparam name="T">The type of the returned value</typeparam>
|
||||
/// <returns></returns>
|
||||
T? Get<T>(string key);
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of values from the custom settings dictionary
|
||||
/// </summary>
|
||||
/// <param name="key">The key</param>
|
||||
/// <param name="defaultObject">The default list to be returned if nothing is found</param>
|
||||
/// <typeparam name="T">The type of the returned value</typeparam>
|
||||
/// <returns></returns>
|
||||
List<T> GetList<T>(string key, List<T> defaultObject);
|
||||
|
||||
/// <summary>
|
||||
/// Remove a key from the custom settings dictionary
|
||||
/// </summary>
|
||||
/// <param name="key">The key</param>
|
||||
void Remove(string key);
|
||||
|
||||
/// <summary>
|
||||
/// Get the enumerator of the custom settings dictionary
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IEnumerator<KeyValuePair<string, object>> GetEnumerator();
|
||||
|
||||
/// <summary>
|
||||
/// Clear the custom settings dictionary
|
||||
/// </summary>
|
||||
void Clear();
|
||||
|
||||
/// <summary>
|
||||
/// Check if the custom settings dictionary contains a key
|
||||
/// </summary>
|
||||
/// <param name="key">The key</param>
|
||||
/// <returns></returns>
|
||||
bool ContainsKey(string key);
|
||||
|
||||
/// <summary>
|
||||
/// Filter the custom settings dictionary based on a predicate
|
||||
/// </summary>
|
||||
/// <param name="predicate">The predicate</param>
|
||||
/// <returns></returns>
|
||||
IEnumerable<KeyValuePair<string, object>> Where(Func<KeyValuePair<string, object>, bool> predicate);
|
||||
|
||||
/// <summary>
|
||||
/// Filter the custom settings dictionary based on a predicate
|
||||
/// </summary>
|
||||
/// <param name="predicate">The predicate</param>
|
||||
IEnumerable<KeyValuePair<string, object>> Where(Func<KeyValuePair<string, object>, int, bool> predicate);
|
||||
|
||||
/// <summary>
|
||||
/// Filter the custom settings dictionary based on a predicate
|
||||
/// </summary>
|
||||
/// <param name="selector">The predicate</param>
|
||||
IEnumerable<TResult> Where<TResult>(Func<KeyValuePair<string, object>, TResult> selector);
|
||||
|
||||
/// <summary>
|
||||
/// Filter the custom settings dictionary based on a predicate
|
||||
/// </summary>
|
||||
/// <param name="selector">The predicate</param>
|
||||
IEnumerable<TResult> Where<TResult>(Func<KeyValuePair<string, object>, int, TResult> selector);
|
||||
|
||||
/// <summary>
|
||||
/// Get the first element of the custom settings dictionary based on a predicate
|
||||
/// </summary>
|
||||
/// <param name="predicate">The predicate</param>
|
||||
KeyValuePair<string, object> FirstOrDefault(Func<KeyValuePair<string, object>, bool> predicate);
|
||||
|
||||
/// <summary>
|
||||
/// Get the first element of the custom settings dictionary
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
KeyValuePair<string, object> FirstOrDefault();
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the custom settings dictionary contains all the keys
|
||||
/// </summary>
|
||||
/// <param name="keys">A list of keys</param>
|
||||
/// <returns></returns>
|
||||
bool ContainsAllKeys(params string[] keys);
|
||||
|
||||
/// <summary>
|
||||
/// Try to get the value of a key in the custom settings dictionary
|
||||
/// </summary>
|
||||
/// <param name="key">The key</param>
|
||||
/// <param name="value">The value</param>
|
||||
/// <returns></returns>
|
||||
bool TryGetValue(string key, out object? value);
|
||||
|
||||
/// <summary>
|
||||
/// Save the custom settings dictionary to a file
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task SaveToFile();
|
||||
|
||||
/// <summary>
|
||||
/// Load the custom settings dictionary from a file
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
void LoadFromFile();
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>AnyCPU;x64;ARM64</Platforms>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="9.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a SQL connection by specifing its private path
|
||||
/// Initialize a SQL connection by specifying its private path
|
||||
/// </summary>
|
||||
/// <param name="fileName">The path to the database (it is starting from ./Data/Resources/)</param>
|
||||
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
|
||||
/// <returns></returns>
|
||||
public async Task Open()
|
||||
{
|
||||
await _connection.OpenAsync();
|
||||
await _Connection.OpenAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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
|
||||
/// <returns></returns>
|
||||
public async void Stop()
|
||||
{
|
||||
await _connection.CloseAsync();
|
||||
await _Connection.CloseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -237,7 +229,7 @@ public class SqlDatabase
|
||||
/// <returns></returns>
|
||||
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<string>();
|
||||
@@ -261,7 +253,7 @@ public class SqlDatabase
|
||||
/// <returns></returns>
|
||||
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<string>();
|
||||
@@ -283,7 +275,7 @@ public class SqlDatabase
|
||||
/// <returns>True if the table exists, false if not</returns>
|
||||
public async Task<bool> 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
|
||||
/// <returns>True if the table exists, false if not</returns>
|
||||
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
|
||||
/// <returns></returns>
|
||||
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
|
||||
/// <returns></returns>
|
||||
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
|
||||
/// <returns>The number of rows that the query modified</returns>
|
||||
public async Task<int> 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
|
||||
/// <returns>The number of rows that the query modified</returns>
|
||||
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
|
||||
/// <returns>The result is a string that has all values separated by space character</returns>
|
||||
public async Task<string?> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read data from the result table and return the first row
|
||||
/// </summary>
|
||||
/// <param name="query">The query</param>
|
||||
/// <param name="parameters">The parameters of the query</param>
|
||||
/// <returns>The result is a string that has all values separated by space character</returns>
|
||||
public async Task<string?> ReadDataAsync(string query, params KeyValuePair<string, object>[] 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<object>(" ", values);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read data from the result table and return the first row
|
||||
/// </summary>
|
||||
@@ -392,9 +415,9 @@ public class SqlDatabase
|
||||
/// <returns>The result is a string that has all values separated by space character</returns>
|
||||
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
|
||||
/// <returns>The first row as separated items</returns>
|
||||
public async Task<object[]?> 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<object[]?> ReadDataArrayAsync(string query, params KeyValuePair<string, object>[] 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;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read data from the result table and return the first row
|
||||
/// </summary>
|
||||
@@ -437,9 +485,9 @@ public class SqlDatabase
|
||||
/// <returns>The first row as separated items</returns>
|
||||
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
|
||||
/// <returns>A list of string arrays representing the values that the query returns</returns>
|
||||
public async Task<List<string[]>?> 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
|
||||
/// <param name="name">The name of the parameter</param>
|
||||
/// <param name="value">The value of the parameter</param>
|
||||
/// <returns>The SQLiteParameter that has the name, value and DBType set according to your inputs</returns>
|
||||
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
|
||||
/// </summary>
|
||||
/// <param name="parameterValues">The parameter raw inputs. The Key is name and the Value is the value of the parameter</param>
|
||||
/// <returns>The SQLiteParameter that has the name, value and DBType set according to your inputs</returns>
|
||||
private SQLiteParameter? CreateParameter(KeyValuePair<string, object> parameterValues)
|
||||
private static SqliteParameter? CreateParameter(KeyValuePair<string, object> parameterValues)
|
||||
{
|
||||
return CreateParameter(parameterValues.Key, parameterValues.Value);
|
||||
}
|
||||
@@ -557,10 +606,10 @@ public class SqlDatabase
|
||||
/// <returns>The number of rows that the query modified in the database</returns>
|
||||
public async Task<int> ExecuteNonQueryAsync(string query, params KeyValuePair<string, object>[] 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
|
||||
/// <returns>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</returns>
|
||||
public async Task<T?> ReadObjectOfTypeAsync<T>(string query, Func<object[], T> convertor, params KeyValuePair<string, object>[] 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<List<T>> ReadListOfTypeAsync<T>(string query, Func<object[], T> convertor,
|
||||
params KeyValuePair<string, object>[] 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);
|
||||
10
DiscordBotCore.Logging/DiscordBotCore.Logging.csproj
Normal file
10
DiscordBotCore.Logging/DiscordBotCore.Logging.csproj
Normal file
@@ -0,0 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>AnyCPU;x64;ARM64</Platforms>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
10
DiscordBotCore.Logging/ILogMessage.cs
Normal file
10
DiscordBotCore.Logging/ILogMessage.cs
Normal file
@@ -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; }
|
||||
|
||||
}
|
||||
13
DiscordBotCore.Logging/ILogger.cs
Normal file
13
DiscordBotCore.Logging/ILogger.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace DiscordBotCore.Logging;
|
||||
|
||||
public interface ILogger
|
||||
{
|
||||
List<ILogMessage> LogMessages { get; protected set; }
|
||||
event Action<ILogMessage>? 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);
|
||||
}
|
||||
75
DiscordBotCore.Logging/LogMessage.cs
Normal file
75
DiscordBotCore.Logging/LogMessage.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
9
DiscordBotCore.Logging/LogType.cs
Normal file
9
DiscordBotCore.Logging/LogType.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace DiscordBotCore.Logging;
|
||||
|
||||
public enum LogType
|
||||
{
|
||||
Info,
|
||||
Warning,
|
||||
Error,
|
||||
Critical
|
||||
}
|
||||
62
DiscordBotCore.Logging/Logger.cs
Normal file
62
DiscordBotCore.Logging/Logger.cs
Normal file
@@ -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<string> _logMessageProperties = typeof(ILogMessage)
|
||||
.GetProperties()
|
||||
.Select(p => p.Name)
|
||||
.ToList();
|
||||
|
||||
|
||||
public List<ILogMessage> LogMessages { get; set; }
|
||||
public event Action<ILogMessage>? 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<ILogMessage>();
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
10
DiscordBotCore.Networking/DiscordBotCore.Networking.csproj
Normal file
10
DiscordBotCore.Networking/DiscordBotCore.Networking.csproj
Normal file
@@ -0,0 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>AnyCPU;x64;ARM64</Platforms>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
25
DiscordBotCore.Networking/FileDownloader.cs
Normal file
25
DiscordBotCore.Networking/FileDownloader.cs
Normal file
@@ -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<float> progressCallback)
|
||||
{
|
||||
await using var fileStream = new FileStream(_DownloadLocation, FileMode.Create, FileAccess.Write, FileShare.None);
|
||||
await _HttpClient.DownloadFileAsync(_DownloadUrl, fileStream, new Progress<float>(progressCallback));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Copy one Stream to another <see langword="async" />
|
||||
/// </summary>
|
||||
/// <param name="stream">The base stream</param>
|
||||
/// <param name="destination">The destination stream</param>
|
||||
/// <param name="bufferSize">The buffer to read</param>
|
||||
/// <param name="progress">The progress</param>
|
||||
/// <param name="cancellationToken">The cancellation token</param>
|
||||
/// <exception cref="ArgumentNullException">Triggered if any <see cref="Stream" /> is empty</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Triggered if <paramref name="bufferSize" /> is less then or equal to 0</exception>
|
||||
/// <exception cref="InvalidOperationException">Triggered if <paramref name="stream" /> is not readable</exception>
|
||||
/// <exception cref="ArgumentException">Triggered in <paramref name="destination" /> is not writable</exception>
|
||||
private static async Task CopyToOtherStreamAsync(
|
||||
this Stream stream, Stream destination, int bufferSize,
|
||||
IProgress<long>? 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Downloads a <see cref="Stream" /> and saves it to another <see cref="Stream" />.
|
||||
/// </summary>
|
||||
@@ -57,16 +88,4 @@ internal static class OnlineFunctions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read contents of a file as string from specified URL
|
||||
/// </summary>
|
||||
/// <param name="url">The URL to read from</param>
|
||||
/// <param name="cancellation">The cancellation token</param>
|
||||
/// <returns></returns>
|
||||
internal static async Task<string> DownloadStringAsync(string url, CancellationToken cancellation = default)
|
||||
{
|
||||
using var client = new HttpClient();
|
||||
return await client.GetStringAsync(url, cancellation);
|
||||
}
|
||||
}
|
||||
89
DiscordBotCore.Networking/ParallelDownloadExecutor.cs
Normal file
89
DiscordBotCore.Networking/ParallelDownloadExecutor.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
using DiscordBotCore.Networking.Helpers;
|
||||
|
||||
namespace DiscordBotCore.Networking;
|
||||
|
||||
public class ParallelDownloadExecutor
|
||||
{
|
||||
private readonly List<Task> _listOfTasks;
|
||||
private readonly HttpClient _httpClient;
|
||||
private Action? OnFinishAction { get; set; }
|
||||
|
||||
public ParallelDownloadExecutor(List<Task> listOfTasks)
|
||||
{
|
||||
_httpClient = new HttpClient();
|
||||
_listOfTasks = listOfTasks;
|
||||
}
|
||||
|
||||
public ParallelDownloadExecutor()
|
||||
{
|
||||
_httpClient = new HttpClient();
|
||||
_listOfTasks = new List<Task>();
|
||||
}
|
||||
|
||||
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<float> 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<float>(progressCallback));
|
||||
_listOfTasks.Add(task);
|
||||
}
|
||||
|
||||
private Task CreateDownloadTask(string downloadLink, string downloadLocation, IProgress<float> progress)
|
||||
{
|
||||
var fileStream = new FileStream(downloadLocation, FileMode.Create, FileAccess.Write, FileShare.None);
|
||||
return _httpClient.DownloadFileAsync(downloadLink, fileStream, progress);
|
||||
}
|
||||
|
||||
}
|
||||
22
DiscordBotCore.PluginCore/DiscordBotCore.PluginCore.csproj
Normal file
22
DiscordBotCore.PluginCore/DiscordBotCore.PluginCore.csproj
Normal file
@@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>AnyCPU;x64;ARM64</Platforms>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Discord.Net" Version="3.17.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DiscordBotCore.Logging\DiscordBotCore.Logging.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Helpers\Execution\DbSlashCommand\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Command to be executed
|
||||
@@ -14,7 +13,7 @@ public interface DBCommand
|
||||
/// <summary>
|
||||
/// Command aliases. Users may use this to execute the command
|
||||
/// </summary>
|
||||
List<string>? Aliases { get; }
|
||||
List<string> Aliases { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Command description
|
||||
@@ -30,21 +29,17 @@ public interface DBCommand
|
||||
/// <summary>
|
||||
/// true if the command requre admin, otherwise false
|
||||
/// </summary>
|
||||
bool requireAdmin { get; }
|
||||
bool RequireAdmin { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The main body of the command. This is what is executed when user calls the command in Server
|
||||
/// </summary>
|
||||
/// <param name="args">The disocrd Context</param>
|
||||
void ExecuteServer(DbCommandExecutingArguments args)
|
||||
{
|
||||
}
|
||||
/// <param name="args">The Discord Context</param>
|
||||
Task ExecuteServer(IDbCommandExecutingArgument args) => Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// The main body of the command. This is what is executed when user calls the command in DM
|
||||
/// </summary>
|
||||
/// <param name="args">The disocrd Context</param>
|
||||
void ExecuteDM(DbCommandExecutingArguments args)
|
||||
{
|
||||
}
|
||||
/// <param name="args">The Discord Context</param>
|
||||
Task ExecuteDm(IDbCommandExecutingArgument args) => Task.CompletedTask;
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the event
|
||||
@@ -17,6 +17,6 @@ public interface DBEvent
|
||||
/// <summary>
|
||||
/// The method that is invoked when the event is loaded into memory
|
||||
/// </summary>
|
||||
/// <param name="client">The discord bot client</param>
|
||||
void Start(DiscordSocketClient client);
|
||||
/// <param name="args">The arguments for the start method</param>
|
||||
void Start(IDbEventExecutingArgument args);
|
||||
}
|
||||
22
DiscordBotCore.PluginCore/Interfaces/IDbSlashCommand.cs
Normal file
22
DiscordBotCore.PluginCore/Interfaces/IDbSlashCommand.cs
Normal file
@@ -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<SlashCommandOptionBuilder> Options { get; }
|
||||
|
||||
void ExecuteServer(ILogger logger, SocketSlashCommand context)
|
||||
{ }
|
||||
|
||||
void ExecuteDm(ILogger logger, SocketSlashCommand context) { }
|
||||
|
||||
Task ExecuteInteraction(ILogger logger, SocketInteraction interaction) => Task.CompletedTask;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>AnyCPU;x64;ARM64</Platforms>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DiscordBotCore.Configuration\DiscordBotCore.Configuration.csproj" />
|
||||
<ProjectReference Include="..\DiscordBotCore.Logging\DiscordBotCore.Logging.csproj" />
|
||||
<ProjectReference Include="..\DiscordBotCore.PluginCore\DiscordBotCore.PluginCore.csproj" />
|
||||
<ProjectReference Include="..\DiscordBotCore.PluginManagement\DiscordBotCore.PluginManagement.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -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}") { }
|
||||
}
|
||||
27
DiscordBotCore.PluginManagement.Loading/IPluginLoader.cs
Normal file
27
DiscordBotCore.PluginManagement.Loading/IPluginLoader.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using Discord.WebSocket;
|
||||
using DiscordBotCore.PluginCore.Interfaces;
|
||||
|
||||
namespace DiscordBotCore.PluginManagement.Loading;
|
||||
|
||||
public interface IPluginLoader
|
||||
{
|
||||
public IReadOnlyList<IDbCommand> Commands { get; }
|
||||
public IReadOnlyList<IDbEvent> Events { get; }
|
||||
public IReadOnlyList<IDbSlashCommand> SlashCommands { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the Discord client for the plugin loader. This is used to initialize the slash commands and events.
|
||||
/// </summary>
|
||||
/// <param name="discordSocketClient">The socket client that represents the running Discord Bot</param>
|
||||
public void SetDiscordClient(DiscordSocketClient discordSocketClient);
|
||||
|
||||
/// <summary>
|
||||
/// Loads all the plugins that are installed.
|
||||
/// </summary>
|
||||
public Task LoadPlugins();
|
||||
|
||||
/// <summary>
|
||||
/// Unload all plugins from the plugin manager.
|
||||
/// </summary>
|
||||
public Task UnloadAllPlugins();
|
||||
}
|
||||
371
DiscordBotCore.PluginManagement.Loading/PluginLoader.cs
Normal file
371
DiscordBotCore.PluginManagement.Loading/PluginLoader.cs
Normal file
@@ -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<IDbCommand> _Commands = new List<IDbCommand>();
|
||||
private readonly List<IDbEvent> _Events = new List<IDbEvent>();
|
||||
private readonly List<IDbSlashCommand> _SlashCommands = new List<IDbSlashCommand>();
|
||||
private readonly List<SocketApplicationCommand> _ApplicationCommands = new List<SocketApplicationCommand>();
|
||||
|
||||
public PluginLoader(IPluginManager pluginManager, ILogger logger, IConfiguration configuration)
|
||||
{
|
||||
_PluginManager = pluginManager;
|
||||
_Logger = logger;
|
||||
_Configuration = configuration;
|
||||
}
|
||||
|
||||
public IReadOnlyList<IDbCommand> Commands => _Commands;
|
||||
public IReadOnlyList<IDbEvent> Events => _Events;
|
||||
public IReadOnlyList<IDbSlashCommand> 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<IDbEvent>();
|
||||
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<IDbCommand>(helpCommandType);
|
||||
}
|
||||
}
|
||||
|
||||
LoadEverythingOfType<IDbCommand>();
|
||||
LoadEverythingOfType<IDbSlashCommand>();
|
||||
|
||||
|
||||
_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<T>()
|
||||
{
|
||||
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<T>(type);
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeType<T>(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<string>("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<string>("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<bool> 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<ulong> serverIds = _Configuration.GetList("ServerIds", new List<ulong>());
|
||||
|
||||
if (serverIds.Any())
|
||||
{
|
||||
foreach(ulong guildId in serverIds)
|
||||
{
|
||||
IResponse<SocketApplicationCommand> 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<IResponse<SocketApplicationCommand>> 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<SocketApplicationCommand>.Failure("Failed to get guild with ID " + guildId);
|
||||
}
|
||||
|
||||
var command = await guild.CreateApplicationCommandAsync(builder.Build());
|
||||
return Response<SocketApplicationCommand>.Success(command);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>AnyCPU;x64;ARM64</Platforms>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="2.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DiscordBotCore.Configuration\DiscordBotCore.Configuration.csproj" />
|
||||
<ProjectReference Include="..\DiscordBotCore.Logging\DiscordBotCore.Logging.csproj" />
|
||||
<ProjectReference Include="..\DiscordBotCore.Networking\DiscordBotCore.Networking.csproj" />
|
||||
<ProjectReference Include="..\DiscordBotCore.Utilities\DiscordBotCore.Utilities.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
14
DiscordBotCore.PluginManagement/Helpers/IPluginRepository.cs
Normal file
14
DiscordBotCore.PluginManagement/Helpers/IPluginRepository.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using DiscordBotCore.PluginManagement.Models;
|
||||
|
||||
namespace DiscordBotCore.PluginManagement.Helpers;
|
||||
|
||||
public interface IPluginRepository
|
||||
{
|
||||
public Task<List<OnlinePlugin>> GetAllPlugins(int operatingSystem, bool includeNotApproved);
|
||||
|
||||
public Task<OnlinePlugin?> GetPluginById(int pluginId);
|
||||
public Task<OnlinePlugin?> GetPluginByName(string pluginName, int operatingSystem, bool includeNotApproved);
|
||||
|
||||
public Task<List<OnlineDependencyInfo>> GetDependenciesForPlugin(int pluginId);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace DiscordBotCore.PluginManagement.Helpers;
|
||||
|
||||
public interface IPluginRepositoryConfiguration
|
||||
{
|
||||
public string BaseUrl { get; }
|
||||
|
||||
public string PluginRepositoryLocation { get; }
|
||||
public string DependenciesRepositoryLocation { get; }
|
||||
}
|
||||
161
DiscordBotCore.PluginManagement/Helpers/PluginRepository.cs
Normal file
161
DiscordBotCore.PluginManagement/Helpers/PluginRepository.cs
Normal file
@@ -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<List<OnlinePlugin>> GetAllPlugins(int operatingSystem, bool includeNotApproved)
|
||||
{
|
||||
string url = CreateUrlWithQueryParams(_pluginRepositoryConfiguration.PluginRepositoryLocation,
|
||||
"get-all-plugins", new Dictionary<string, string>
|
||||
{
|
||||
{ "operatingSystem", operatingSystem.ToString() },
|
||||
{ "includeNotApproved", includeNotApproved.ToString() }
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
HttpResponseMessage response = await _httpClient.GetAsync(url);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
string content = await response.Content.ReadAsStringAsync();
|
||||
List<OnlinePlugin> plugins = await JsonManager.ConvertFromJson<List<OnlinePlugin>>(content);
|
||||
|
||||
return plugins;
|
||||
}
|
||||
catch (HttpRequestException exception)
|
||||
{
|
||||
_logger.LogException(exception,this);
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public async Task<OnlinePlugin?> GetPluginById(int pluginId)
|
||||
{
|
||||
string url = CreateUrlWithQueryParams(_pluginRepositoryConfiguration.PluginRepositoryLocation,
|
||||
"get-by-id", new Dictionary<string, string>
|
||||
{
|
||||
{ "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<OnlinePlugin>(content);
|
||||
|
||||
return plugin;
|
||||
}
|
||||
catch (HttpRequestException exception)
|
||||
{
|
||||
_logger.LogException(exception, this);
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public async Task<OnlinePlugin?> GetPluginByName(string pluginName, int operatingSystem, bool includeNotApproved)
|
||||
{
|
||||
string url = CreateUrlWithQueryParams(_pluginRepositoryConfiguration.PluginRepositoryLocation,
|
||||
"get-by-name", new Dictionary<string, string>
|
||||
{
|
||||
{ "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<OnlinePlugin>(content);
|
||||
|
||||
return plugin;
|
||||
}
|
||||
catch (HttpRequestException exception)
|
||||
{
|
||||
_logger.LogException(exception, this);
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public async Task<List<OnlineDependencyInfo>> GetDependenciesForPlugin(int pluginId)
|
||||
{
|
||||
string url = CreateUrlWithQueryParams(_pluginRepositoryConfiguration.DependenciesRepositoryLocation,
|
||||
"get-by-plugin-id", new Dictionary<string, string>
|
||||
{
|
||||
{ "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<OnlineDependencyInfo> dependencies = await JsonManager.ConvertFromJson<List<OnlineDependencyInfo>>(content);
|
||||
|
||||
return dependencies;
|
||||
}
|
||||
catch(HttpRequestException exception)
|
||||
{
|
||||
_logger.LogException(exception, this);
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private string CreateUrlWithQueryParams(string baseUrl, string endpoint, Dictionary<string, string> 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
20
DiscordBotCore.PluginManagement/IPluginManager.cs
Normal file
20
DiscordBotCore.PluginManagement/IPluginManager.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using DiscordBotCore.PluginManagement.Models;
|
||||
using DiscordBotCore.Utilities.Responses;
|
||||
|
||||
namespace DiscordBotCore.PluginManagement;
|
||||
|
||||
public interface IPluginManager
|
||||
{
|
||||
Task<List<OnlinePlugin>> GetPluginsList();
|
||||
Task<IResponse<OnlinePlugin>> GetPluginDataByName(string pluginName);
|
||||
Task<IResponse<OnlinePlugin>> GetPluginDataById(int pluginId);
|
||||
Task<IResponse<bool>> AppendPluginToDatabase(LocalPlugin pluginData);
|
||||
Task<List<LocalPlugin>> GetInstalledPlugins();
|
||||
Task<IResponse<string>> GetDependencyLocation(string dependencyName);
|
||||
Task<IResponse<string>> GetDependencyLocation(string dependencyName, string pluginName);
|
||||
string GenerateDependencyRelativePath(string pluginName, string dependencyPath);
|
||||
Task<IResponse<bool>> InstallPlugin(OnlinePlugin plugin, IProgress<float> progress);
|
||||
Task SetEnabledStatus(string pluginName, bool status);
|
||||
Task<IResponse<bool>> UninstallPluginByName(string pluginName);
|
||||
Task<IResponse<LocalPlugin>> GetLocalPluginByName(string pluginName);
|
||||
}
|
||||
46
DiscordBotCore.PluginManagement/Models/LocalPlugin.cs
Normal file
46
DiscordBotCore.PluginManagement/Models/LocalPlugin.cs
Normal file
@@ -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<string, string> 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<string, string> 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<string, string> listOfExecutableDependencies)
|
||||
{
|
||||
PluginName = pluginName;
|
||||
PluginVersion = pluginVersion;
|
||||
ListOfExecutableDependencies = listOfExecutableDependencies;
|
||||
FilePath = filePath;
|
||||
IsOfflineAdded = false;
|
||||
IsEnabled = true;
|
||||
}
|
||||
|
||||
public static LocalPlugin FromOnlineInfo(OnlinePlugin plugin, List<OnlineDependencyInfo> 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
29
DiscordBotCore.PluginManagement/Models/OnlinePlugin.cs
Normal file
29
DiscordBotCore.PluginManagement/Models/OnlinePlugin.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
316
DiscordBotCore.PluginManagement/PluginManager.cs
Normal file
316
DiscordBotCore.PluginManagement/PluginManager.cs
Normal file
@@ -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<List<OnlinePlugin>> 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<IResponse<OnlinePlugin>> GetPluginDataByName(string pluginName)
|
||||
{
|
||||
int os = OperatingSystem.GetOperatingSystemInt();
|
||||
var plugin = await _PluginRepository.GetPluginByName(pluginName, os, false);
|
||||
|
||||
if (plugin is null)
|
||||
{
|
||||
return Response<OnlinePlugin>.Failure($"Plugin {pluginName} not found in the repository for operating system {OperatingSystem.GetOperatingSystemString((OperatingSystem.OperatingSystemEnum)os)}.");
|
||||
}
|
||||
|
||||
return Response<OnlinePlugin>.Success(plugin);
|
||||
}
|
||||
|
||||
public async Task<IResponse<OnlinePlugin>> GetPluginDataById(int pluginId)
|
||||
{
|
||||
var plugin = await _PluginRepository.GetPluginById(pluginId);
|
||||
if (plugin is null)
|
||||
{
|
||||
return Response<OnlinePlugin>.Failure($"Plugin {pluginId} not found in the repository.");
|
||||
}
|
||||
|
||||
return Response<OnlinePlugin>.Success(plugin);
|
||||
}
|
||||
|
||||
private async Task<IResponse<bool>> RemovePluginFromDatabase(string pluginName)
|
||||
{
|
||||
string? pluginDatabaseFile = _Configuration.Get<string>("PluginDatabase");
|
||||
|
||||
if (pluginDatabaseFile is null)
|
||||
{
|
||||
return Response.Failure("PluginDatabase file path is not present in the config file");
|
||||
}
|
||||
|
||||
List<LocalPlugin> installedPlugins = await JsonManager.ConvertFromJson<List<LocalPlugin>>(await File.ReadAllTextAsync(pluginDatabaseFile));
|
||||
|
||||
installedPlugins.RemoveAll(p => p.PluginName == pluginName);
|
||||
await JsonManager.SaveToJsonFile(pluginDatabaseFile, installedPlugins);
|
||||
|
||||
return Response.Success();
|
||||
}
|
||||
|
||||
public async Task<IResponse<bool>> AppendPluginToDatabase(LocalPlugin pluginData)
|
||||
{
|
||||
string? pluginDatabaseFile = _Configuration.Get<string>("PluginDatabase");
|
||||
if (pluginDatabaseFile is null)
|
||||
{
|
||||
return Response.Failure("PluginDatabase file path is not present in the config file");
|
||||
}
|
||||
|
||||
List<LocalPlugin> 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<List<LocalPlugin>> GetInstalledPlugins()
|
||||
{
|
||||
string? pluginDatabaseFile = _Configuration.Get<string>("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<List<LocalPlugin>>(await File.ReadAllTextAsync(pluginDatabaseFile));
|
||||
}
|
||||
|
||||
public async Task<IResponse<string>> GetDependencyLocation(string dependencyName)
|
||||
{
|
||||
List<LocalPlugin> installedPlugins = await GetInstalledPlugins();
|
||||
|
||||
foreach (var plugin in installedPlugins)
|
||||
{
|
||||
if (plugin.ListOfExecutableDependencies.TryGetValue(dependencyName, out var dependencyPath))
|
||||
{
|
||||
string relativePath = GenerateDependencyRelativePath(plugin.PluginName, dependencyPath);
|
||||
return Response<string>.Success(relativePath);
|
||||
}
|
||||
}
|
||||
|
||||
return Response<string>.Failure($"Dependency {dependencyName} not found in the installed plugins.");
|
||||
}
|
||||
|
||||
public async Task<IResponse<string>> GetDependencyLocation(string dependencyName, string pluginName)
|
||||
{
|
||||
List<LocalPlugin> 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<string>.Success(relativePath);
|
||||
}
|
||||
}
|
||||
|
||||
return Response<string>.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<IResponse<bool>> InstallPlugin(OnlinePlugin plugin, IProgress<float> progress)
|
||||
{
|
||||
string? pluginsFolder = _Configuration.Get<string>("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<OnlineDependencyInfo> dependencies = await _PluginRepository.GetDependenciesForPlugin(plugin.Id);
|
||||
|
||||
string downloadLocation = $"{pluginsFolder}/{plugin.Name}.dll";
|
||||
|
||||
IProgress<float> downloadProgress = new Progress<float>(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<IResponse<bool>> 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<IResponse<LocalPlugin>> GetLocalPluginByName(string pluginName)
|
||||
{
|
||||
List<LocalPlugin> installedPlugins = await GetInstalledPlugins();
|
||||
var plugin = installedPlugins.Find(p => p.PluginName == pluginName);
|
||||
|
||||
if (plugin is null)
|
||||
{
|
||||
return Response<LocalPlugin>.Failure($"Plugin {pluginName} not found in the database");
|
||||
}
|
||||
|
||||
return Response<LocalPlugin>.Success(plugin);
|
||||
}
|
||||
|
||||
private async Task<IResponse<bool>> 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<bool> CreateEmptyPluginDatabase()
|
||||
{
|
||||
string ? pluginDatabaseFile = _Configuration.Get<string>("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<LocalPlugin> installedPlugins = new List<LocalPlugin>();
|
||||
await JsonManager.SaveToJsonFile(pluginDatabaseFile, installedPlugins);
|
||||
_Logger.Log("Plugin database file created", this, LogType.Info);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -30,10 +34,15 @@ public static class ArchiveManager
|
||||
/// <param name="fileName">The file name in the archive</param>
|
||||
/// <param name="archName">The archive location on the disk</param>
|
||||
/// <returns>An array of bytes that represents the Stream value from the file that was read inside the archive</returns>
|
||||
public static async Task<byte[]?> ReadStreamFromPakAsync(string fileName, string archName)
|
||||
public async Task<byte[]?> ReadAllBytes(string fileName, string archName)
|
||||
{
|
||||
string? archiveFolderBasePath = _Configuration.Get<string>("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
|
||||
/// <param name="fileName">The file name that is inside the archive or its full path</param>
|
||||
/// <param name="archFile">The archive location from the PAKs folder</param>
|
||||
/// <returns>A string that represents the content of the file or null if the file does not exists or it has no content</returns>
|
||||
public static async Task<string?> ReadFromPakAsync(string fileName, string archFile)
|
||||
public async Task<string?> ReadFromPakAsync(string fileName, string archFile)
|
||||
{
|
||||
archFile = Config.AppSettings["ArchiveFolder"] + archFile;
|
||||
string? archiveFolderBasePath = _Configuration.Get<string>("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
|
||||
/// <param name="progress">The progress that is updated as a file is processed</param>
|
||||
/// <param name="type">The type of progress</param>
|
||||
/// <returns></returns>
|
||||
public static async Task ExtractArchive(
|
||||
public async Task ExtractArchive(
|
||||
string zip, string folder, IProgress<float> 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);
|
||||
15
DiscordBotCore.Utilities/DiscordBotCore.Utilities.csproj
Normal file
15
DiscordBotCore.Utilities/DiscordBotCore.Utilities.csproj
Normal file
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>AnyCPU;x64;ARM64</Platforms>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DiscordBotCore.Configuration\DiscordBotCore.Configuration.csproj" />
|
||||
<ProjectReference Include="..\DiscordBotCore.Logging\DiscordBotCore.Logging.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
102
DiscordBotCore.Utilities/JsonManager.cs
Normal file
102
DiscordBotCore.Utilities/JsonManager.cs
Normal file
@@ -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<string> ConvertToJson<T>(List<T> 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<string> ConvertToJsonString<T>(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save to JSON file
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The class type</typeparam>
|
||||
/// <param name="file">The file path</param>
|
||||
/// <param name="Data">The values</param>
|
||||
/// <returns></returns>
|
||||
public static async Task SaveToJsonFile<T>(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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert json text or file to some kind of data
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The data type</typeparam>
|
||||
/// <param name="input">The file or json text</param>
|
||||
/// <returns></returns>
|
||||
public static async Task<T> ConvertFromJson<T>(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<T>(text, options);
|
||||
await text.FlushAsync();
|
||||
text.Close();
|
||||
|
||||
return (obj ?? default)!;
|
||||
}
|
||||
}
|
||||
@@ -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<T0, T1>
|
||||
public class OneOf<T0, T1>
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
46
DiscordBotCore.Utilities/OperatingSystem.cs
Normal file
46
DiscordBotCore.Utilities/OperatingSystem.cs
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
258
DiscordBotCore.Utilities/Option.cs
Normal file
258
DiscordBotCore.Utilities/Option.cs
Normal file
@@ -0,0 +1,258 @@
|
||||
namespace DiscordBotCore.Utilities;
|
||||
public class Option2<T0, T1, TError> 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, T1, TError>(T0 item0) => new Option2<T0, T1, TError>(item0);
|
||||
public static implicit operator Option2<T0, T1, TError>(T1 item1) => new Option2<T0, T1, TError>(item1);
|
||||
public static implicit operator Option2<T0, T1, TError>(TError error) => new Option2<T0, T1, TError>(error);
|
||||
|
||||
public void Match(Action<T0> item0, Action<T1> item1, Action<TError> 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<TResult>(Func<T0, TResult> item0, Func<T1, TResult> item1, Func<TError, TResult> 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<T0, T1, T2, TError> 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, T1, T2, TError>(T0 item0) => new Option3<T0, T1, T2, TError>(item0);
|
||||
public static implicit operator Option3<T0, T1, T2, TError>(T1 item1) => new Option3<T0, T1, T2, TError>(item1);
|
||||
public static implicit operator Option3<T0, T1, T2, TError>(T2 item2) => new Option3<T0, T1, T2, TError>(item2);
|
||||
public static implicit operator Option3<T0, T1, T2, TError>(TError error) => new Option3<T0, T1, T2, TError>(error);
|
||||
|
||||
public void Match(Action<T0> item0, Action<T1> item1, Action<T2> item2, Action<TError> 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<TResult>(Func<T0, TResult> item0, Func<T1, TResult> item1, Func<T2, TResult> item2, Func<TError, TResult> 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<T0, T1, T2, T3, TError> 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, T1, T2, T3, TError>(T0 item0) => new Option4<T0, T1, T2, T3, TError>(item0);
|
||||
public static implicit operator Option4<T0, T1, T2, T3, TError>(T1 item1) => new Option4<T0, T1, T2, T3, TError>(item1);
|
||||
public static implicit operator Option4<T0, T1, T2, T3, TError>(T2 item2) => new Option4<T0, T1, T2, T3, TError>(item2);
|
||||
public static implicit operator Option4<T0, T1, T2, T3, TError>(T3 item3) => new Option4<T0, T1, T2, T3, TError>(item3);
|
||||
public static implicit operator Option4<T0, T1, T2, T3, TError>(TError error) => new Option4<T0, T1, T2, T3, TError>(error);
|
||||
|
||||
|
||||
public void Match(Action<T0> item0, Action<T1> item1, Action<T2> item2, Action<T3> item3, Action<TError> 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<TResult>(Func<T0, TResult> item0, Func<T1, TResult> item1, Func<T2, TResult> item2, Func<T3, TResult> item3, Func<TError, TResult> 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"
|
||||
};
|
||||
}
|
||||
}
|
||||
8
DiscordBotCore.Utilities/Responses/IResponse.cs
Normal file
8
DiscordBotCore.Utilities/Responses/IResponse.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace DiscordBotCore.Utilities.Responses;
|
||||
|
||||
public interface IResponse<out T>
|
||||
{
|
||||
public bool IsSuccess { get; }
|
||||
public string Message { get; }
|
||||
public T? Data { get; }
|
||||
}
|
||||
45
DiscordBotCore.Utilities/Responses/Response.cs
Normal file
45
DiscordBotCore.Utilities/Responses/Response.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
namespace DiscordBotCore.Utilities.Responses;
|
||||
|
||||
public class Response : IResponse<bool>
|
||||
{
|
||||
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<T> : IResponse<T> 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<T> Success(T data) => new Response<T>(data);
|
||||
public static Response<T> Failure(string message) => new Response<T>(message);
|
||||
}
|
||||
80
DiscordBotCore.Utilities/Result.cs
Normal file
80
DiscordBotCore.Utilities/Result.cs
Normal file
@@ -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<Exception> 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<Exception> exceptionAction)
|
||||
{
|
||||
if (_Result.HasValue && _Result.Value)
|
||||
{
|
||||
successAction();
|
||||
}
|
||||
else
|
||||
{
|
||||
exceptionAction(Exception!);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public TResult Match<TResult>(Func<TResult> successAction, Func<Exception,TResult> errorAction)
|
||||
{
|
||||
return IsSuccess ? successAction() : errorAction(Exception!);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class Result<T>
|
||||
{
|
||||
private readonly OneOf<T, Exception> _Result;
|
||||
|
||||
private Result(OneOf<T, Exception> result)
|
||||
{
|
||||
_Result = result;
|
||||
}
|
||||
|
||||
public static Result<T> From (T value) => new Result<T>(new OneOf<T, Exception>(value));
|
||||
public static implicit operator Result<T>(Exception exception) => new Result<T>(new OneOf<T, Exception>(exception));
|
||||
|
||||
|
||||
|
||||
public void Match(Action<T> valueAction, Action<Exception> exceptionAction)
|
||||
{
|
||||
_Result.Match(valueAction, exceptionAction);
|
||||
}
|
||||
|
||||
public TResult Match<TResult>(Func<T, TResult> valueFunc, Func<Exception, TResult> exceptionFunc)
|
||||
{
|
||||
return _Result.Match(valueFunc, exceptionFunc);
|
||||
}
|
||||
}
|
||||
7
DiscordBotCore.Utilities/UnzipProgressType.cs
Normal file
7
DiscordBotCore.Utilities/UnzipProgressType.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace DiscordBotCore.Utilities;
|
||||
|
||||
public enum UnzipProgressType
|
||||
{
|
||||
PercentageFromNumberOfFiles,
|
||||
PercentageFromTotalSize
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Command handler constructor
|
||||
/// </summary>
|
||||
/// <param name="client">The discord bot client</param>
|
||||
/// <param name="pluginLoader">The plugin loader</param>
|
||||
/// <param name="commandService">The discord bot command service</param>
|
||||
/// <param name="botPrefix">The prefix to watch for</param>
|
||||
public CommandHandler(DiscordSocketClient client, CommandService commandService, string botPrefix)
|
||||
/// <param name="logger">The logger</param>
|
||||
public CommandHandler(ILogger logger, IPluginLoader pluginLoader, IConfiguration configuration, CommandService commandService)
|
||||
{
|
||||
_client = client;
|
||||
_commandService = commandService;
|
||||
_botPrefix = botPrefix;
|
||||
_logger = logger;
|
||||
_pluginLoader = pluginLoader;
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The method to initialize all commands
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
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
|
||||
/// </summary>
|
||||
/// <param name="Message">The message got from the user in discord chat</param>
|
||||
/// <returns></returns>
|
||||
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<string>("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<string>("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);
|
||||
}
|
||||
}
|
||||
}
|
||||
144
DiscordBotCore/Bot/DiscordBotApplication.cs
Normal file
144
DiscordBotCore/Bot/DiscordBotApplication.cs
Normal file
@@ -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; }
|
||||
|
||||
/// <summary>
|
||||
/// The main Boot constructor
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The start method for the bot. This method is used to load the bot
|
||||
/// </summary>
|
||||
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<string>("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;
|
||||
}
|
||||
}
|
||||
13
DiscordBotCore/Bot/ICommandHandler.cs
Normal file
13
DiscordBotCore/Bot/ICommandHandler.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System.Threading.Tasks;
|
||||
using Discord.WebSocket;
|
||||
|
||||
namespace DiscordBotCore.Bot;
|
||||
|
||||
internal interface ICommandHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// The method to initialize all commands
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task InstallCommandsAsync(DiscordSocketClient client);
|
||||
}
|
||||
20
DiscordBotCore/Bot/IDiscordBotApplication.cs
Normal file
20
DiscordBotCore/Bot/IDiscordBotApplication.cs
Normal file
@@ -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; }
|
||||
|
||||
/// <summary>
|
||||
/// The start method for the bot. This method is used to load the bot
|
||||
/// </summary>
|
||||
Task StartAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Stops the bot and cleans up resources.
|
||||
/// </summary>
|
||||
Task StopAsync();
|
||||
}
|
||||
77
DiscordBotCore/Commands/HelpCommand.cs
Normal file
77
DiscordBotCore/Commands/HelpCommand.cs
Normal file
@@ -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<string> Aliases => [];
|
||||
public string Description => "Help command for the bot.";
|
||||
public string Usage => "help <command>";
|
||||
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;
|
||||
}
|
||||
}
|
||||
18
DiscordBotCore/DiscordBotCore.csproj
Normal file
18
DiscordBotCore/DiscordBotCore.csproj
Normal file
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<OutputType>Library</OutputType>
|
||||
<Platforms>AnyCPU;x64;ARM64</Platforms>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Discord.Net" Version="3.17.2" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.3" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DiscordBotCore.Configuration\DiscordBotCore.Configuration.csproj" />
|
||||
<ProjectReference Include="..\DiscordBotCore.PluginCore\DiscordBotCore.PluginCore.csproj" />
|
||||
<ProjectReference Include="..\DiscordBotCore.PluginManagement.Loading\DiscordBotCore.PluginManagement.Loading.csproj" />
|
||||
<ProjectReference Include="..\DiscordBotCore.PluginManagement\DiscordBotCore.PluginManagement.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,12 +1,9 @@
|
||||
using System.Linq;
|
||||
using System.Linq;
|
||||
using Discord;
|
||||
using Discord.WebSocket;
|
||||
|
||||
namespace PluginManager.Others.Permissions;
|
||||
namespace DiscordBotCore.Others;
|
||||
|
||||
/// <summary>
|
||||
/// A class whith all discord permissions
|
||||
/// </summary>
|
||||
public static class DiscordPermissions
|
||||
{
|
||||
/// <summary>
|
||||
@@ -15,7 +12,7 @@ public static class DiscordPermissions
|
||||
/// <param name="role">The role</param>
|
||||
/// <param name="permission">The permission</param>
|
||||
/// <returns></returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if user has the specified permission
|
||||
/// </summary>
|
||||
@@ -39,7 +35,7 @@ public static class DiscordPermissions
|
||||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -61,4 +57,4 @@ public static class DiscordPermissions
|
||||
{
|
||||
return IsAdmin((SocketGuildUser)user);
|
||||
}
|
||||
}
|
||||
}
|
||||
12
DiscordBotCore/Properties/launchSettings.json
Normal file
12
DiscordBotCore/Properties/launchSettings.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"profiles": {
|
||||
"DiscordBotCore": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"applicationUrl": "https://localhost:49707;http://localhost:49708"
|
||||
}
|
||||
}
|
||||
}
|
||||
454
DiscordBotUI/.gitignore
vendored
454
DiscordBotUI/.gitignore
vendored
@@ -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
|
||||
@@ -1,24 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<!--If you are willing to use Windows/MacOS native APIs you will need to create 3 projects.
|
||||
One for Windows with net8.0-windows TFM, one for MacOS with net8.0-macos and one with net8.0 TFM for Linux.-->
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.0.10" />
|
||||
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.10" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DiscordBotUI\DiscordBotUI.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -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<App>()
|
||||
.UsePlatformDetect()
|
||||
.WithInterFont()
|
||||
.LogToTrace()
|
||||
.UseReactiveUI();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"profiles": {
|
||||
"DiscordBotUI.Desktop": {
|
||||
"commandName": "Project"
|
||||
},
|
||||
"WSL": {
|
||||
"commandName": "WSL2",
|
||||
"distributionName": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<!-- This manifest is used on Windows only.
|
||||
Don't remove it as it might cause problems with window transparency and embedded controls.
|
||||
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
|
||||
<assemblyIdentity version="1.0.0.0" name="DiscordBotUI.Desktop"/>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- A list of the Windows versions that this application has been tested on
|
||||
and is designed to work with. Uncomment the appropriate elements
|
||||
and Windows will automatically select the most compatible environment. -->
|
||||
|
||||
<!-- Windows 10 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
</assembly>
|
||||
@@ -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!"
|
||||
@@ -1,17 +0,0 @@
|
||||
<Application xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:DiscordBotUI"
|
||||
x:Class="DiscordBotUI.App"
|
||||
RequestedThemeVariant="Default">
|
||||
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
|
||||
|
||||
<Application.DataTemplates>
|
||||
<local:ViewLocator/>
|
||||
</Application.DataTemplates>
|
||||
|
||||
|
||||
<Application.Styles>
|
||||
<FluentTheme />
|
||||
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/>
|
||||
</Application.Styles>
|
||||
</Application>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 172 KiB |
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// The help command
|
||||
/// </summary>
|
||||
internal class Help: DBCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Command name
|
||||
/// </summary>
|
||||
public string Command => "help";
|
||||
|
||||
public List<string> Aliases => null;
|
||||
|
||||
/// <summary>
|
||||
/// Command Description
|
||||
/// </summary>
|
||||
public string Description => "This command allows you to check all loaded commands";
|
||||
|
||||
/// <summary>
|
||||
/// Command usage
|
||||
/// </summary>
|
||||
public string Usage => "help <command>";
|
||||
|
||||
/// <summary>
|
||||
/// Check if the command require administrator to be executed
|
||||
/// </summary>
|
||||
public bool requireAdmin => false;
|
||||
|
||||
/// <summary>
|
||||
/// The main body of the command
|
||||
/// </summary>
|
||||
/// <param name="context">The command context</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AvaloniaResource Include="Assets\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="11.0.10" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.10" />
|
||||
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.10" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.10" />
|
||||
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.10" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\PluginManager\PluginManager.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
using ReactiveUI;
|
||||
|
||||
namespace DiscordBotUI.ViewModels
|
||||
{
|
||||
public class ViewModelBase : ReactiveObject
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="DiscordBotUI.Views.HomePage"
|
||||
Title="HomePage" MinWidth="900" MinHeight="500">
|
||||
|
||||
<DockPanel LastChildFill="True">
|
||||
<Menu DockPanel.Dock="Top">
|
||||
<MenuItem Header="Settings" Click="SettingsMenuClick"></MenuItem>
|
||||
<MenuItem Header="Installed Plugins" Click="PluginsMenuClick"></MenuItem>
|
||||
<MenuItem Header="New Plugins" Click="NewPluginsMenuClick"></MenuItem>
|
||||
</Menu>
|
||||
|
||||
<Border Width="500" BorderBrush="Black" BorderThickness="1" DockPanel.Dock="Right">
|
||||
<RelativePanel Margin="10">
|
||||
<Label Content="Bot Token: " Name="labelToken" RelativePanel.AlignTopWithPanel="True"/>
|
||||
<TextBox Name="textBoxToken" Text="" IsReadOnly="True" RelativePanel.AlignRightWithPanel="True" Width="350" />
|
||||
|
||||
<Label Content="Bot Prefix: " Name="labelPrefix" RelativePanel.Below="labelToken" Margin="0,20,0,0"/>
|
||||
<TextBox Name="textBoxPrefix" Text="" RelativePanel.AlignRightWithPanel="True" RelativePanel.Below="textBoxToken"
|
||||
IsReadOnly="True" Margin="0,10,0,0" Width="350" />
|
||||
|
||||
<Label Content="Server Id: " Name="labelServerId" RelativePanel.Below="labelPrefix" Margin="0,20,0,0"/>
|
||||
<TextBox Name="textBoxServerId" Text="" RelativePanel.AlignRightWithPanel="True"
|
||||
IsReadOnly="True" RelativePanel.Below="textBoxPrefix" Margin="0,10,0,0" Width="350" />
|
||||
|
||||
<Button Click="ButtonStartBotClick" Name="buttonStartBot" Content="Start" RelativePanel.AlignBottomWithPanel="True" Margin="0,-100,0,0"
|
||||
Width="120" Height="40" Background="#FF008CFF" Foreground="White" BorderThickness="0" CornerRadius="5" FontWeight="Bold"
|
||||
RelativePanel.AlignHorizontalCenterWithPanel="True" />
|
||||
</RelativePanel>
|
||||
|
||||
</Border>
|
||||
|
||||
<Border Background="White" BorderBrush="Black" BorderThickness="1">
|
||||
<TextBlock Name="logTextBlock" Foreground="Black" Text="" />
|
||||
</Border>
|
||||
</DockPanel>
|
||||
|
||||
</Window>
|
||||
@@ -1,80 +0,0 @@
|
||||
using System;
|
||||
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Threading;
|
||||
|
||||
using DiscordBotUI.Bot;
|
||||
|
||||
using PluginManager;
|
||||
using PluginManager.Others.Logger;
|
||||
|
||||
namespace DiscordBotUI.Views;
|
||||
|
||||
public partial class HomePage : Window
|
||||
{
|
||||
private readonly DiscordBot _DiscordBot;
|
||||
|
||||
public HomePage()
|
||||
{
|
||||
InitializeComponent();
|
||||
_DiscordBot = new DiscordBot(null!);
|
||||
|
||||
Loaded += HomePage_Loaded;
|
||||
}
|
||||
|
||||
private async void HomePage_Loaded(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
await Config.Initialize();
|
||||
|
||||
if(!Config.AppSettings.ContainsAllKeys("token", "prefix"))
|
||||
{
|
||||
await new SettingsPage().ShowDialog(this);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Config.AppSettings["token"]) || string.IsNullOrWhiteSpace(Config.AppSettings["prefix"]))
|
||||
Environment.Exit(-1);
|
||||
}
|
||||
|
||||
|
||||
textBoxToken.Text = Config.AppSettings["token"];
|
||||
textBoxPrefix.Text = Config.AppSettings["prefix"];
|
||||
textBoxServerId.Text = Config.AppSettings["ServerID"];
|
||||
}
|
||||
|
||||
private void SetTextToTB(Log logMessage)
|
||||
{
|
||||
logTextBlock.Text += $"[{logMessage.Type}] [{logMessage.ThrowTime.ToShortTimeString()}] {logMessage.Message}\n";
|
||||
}
|
||||
|
||||
private async void ButtonStartBotClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
|
||||
Config.Logger.OnLog += async (sender, logMessage) =>
|
||||
{
|
||||
await Dispatcher.UIThread.InvokeAsync(() => SetTextToTB(logMessage), DispatcherPriority.Background);
|
||||
};
|
||||
|
||||
await _DiscordBot.InitializeBot();
|
||||
|
||||
await _DiscordBot.LoadPlugins();
|
||||
|
||||
}
|
||||
|
||||
private async void SettingsMenuClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
//await new SettingsPage().ShowDialog(this);
|
||||
new SettingsPage().Show();
|
||||
}
|
||||
|
||||
private async void PluginsMenuClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
//await new PluginsPage().ShowDialog(this);
|
||||
new PluginsPage().Show();
|
||||
}
|
||||
|
||||
private void NewPluginsMenuClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
new PluginInstaller().Show();
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:views="clr-namespace:DiscordBotUI.Views;assembly=DiscordBotUI"
|
||||
xmlns:viewmodels="using:DiscordBotUI.ViewModels"
|
||||
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="300"
|
||||
x:Class="DiscordBotUI.Views.PluginInstaller"
|
||||
x:DataType="views:PluginInstaller"
|
||||
Title="PluginInstaller"
|
||||
Name="PluginInstallerWindow"
|
||||
>
|
||||
|
||||
<DataGrid Name="dataGridInstallablePlugins" ItemsSource="{Binding Plugins}">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="Plugin Name" Foreground="Aquamarine" Binding="{Binding Name}"/>
|
||||
<DataGridTextColumn Header="Plugin Version" Binding="{Binding Version}"/>
|
||||
<DataGridTextColumn Header="Plugin Description" Binding="{Binding Description}" Width="*"/>
|
||||
|
||||
<DataGridTemplateColumn Header="Download">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate DataType="viewmodels:OnlinePlugin">
|
||||
<Button Content="Download"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
Command="{Binding InstallPlugin, RelativeSource={RelativeSource AncestorType=views:PluginInstaller}}"
|
||||
CommandParameter="{Binding Name}"/>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</Window>
|
||||
@@ -1,62 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Windows.Input;
|
||||
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Markup.Xaml.Templates;
|
||||
using Avalonia.Media;
|
||||
|
||||
using DiscordBotUI.ViewModels;
|
||||
|
||||
using PluginManager;
|
||||
using PluginManager.Plugin;
|
||||
|
||||
namespace DiscordBotUI.Views;
|
||||
|
||||
public partial class PluginInstaller : Window
|
||||
{
|
||||
|
||||
public ObservableCollection<OnlinePlugin> Plugins { get; private set; }
|
||||
|
||||
public PluginInstaller()
|
||||
{
|
||||
InitializeComponent();
|
||||
Loaded += OnPageLoaded;
|
||||
|
||||
}
|
||||
|
||||
private async void OnPageLoaded(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (Config.PluginsManager is null) return;
|
||||
|
||||
List<PluginOnlineInfo>? onlineInfos = await Config.PluginsManager.GetPluginsList();
|
||||
|
||||
if(onlineInfos is null) return;
|
||||
|
||||
List<OnlinePlugin> plugins = new List<OnlinePlugin>();
|
||||
|
||||
foreach(PluginOnlineInfo onlinePlugin in onlineInfos)
|
||||
{
|
||||
plugins.Add(new OnlinePlugin(onlinePlugin.Name, onlinePlugin.Description, onlinePlugin.Version.ToShortString()));
|
||||
}
|
||||
|
||||
Plugins = new ObservableCollection<OnlinePlugin>(plugins);
|
||||
|
||||
dataGridInstallablePlugins.ItemsSource = Plugins;
|
||||
|
||||
}
|
||||
|
||||
public async void InstallPlugin(string name)
|
||||
{
|
||||
|
||||
PluginOnlineInfo? info = await Config.PluginsManager.GetPluginDataByName(name);
|
||||
if(info is null) return;
|
||||
|
||||
|
||||
|
||||
await Config.PluginsManager.InstallPlugin(info, null);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
xmlns:model ="clr-namespace:DiscordBotUI.Views;assembly=DiscordBotUI"
|
||||
x:Class="DiscordBotUI.Views.PluginsPage"
|
||||
Title="Plugins Page"
|
||||
x:DataType="model:PluginsPage">
|
||||
|
||||
|
||||
<DataGrid Name="dataGridPlugins" Margin="20" ItemsSource="{Binding Plugins}"
|
||||
IsReadOnly="False"
|
||||
CanUserSortColumns="False"
|
||||
GridLinesVisibility="All"
|
||||
AutoGenerateColumns="False"
|
||||
BorderThickness="1" BorderBrush="Gray">
|
||||
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="Plugin Name" Foreground="Aquamarine" Binding="{Binding Name}"/>
|
||||
<DataGridTextColumn Header="Plugin Version" Binding="{Binding Version}"/>
|
||||
<DataGridCheckBoxColumn Header="Is Marked for Uninstall" Binding="{Binding IsMarkedToUninstall}"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</Window>
|
||||
@@ -1,43 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
|
||||
using DiscordBotUI.ViewModels;
|
||||
|
||||
using PluginManager;
|
||||
|
||||
namespace DiscordBotUI.Views;
|
||||
|
||||
|
||||
public partial class PluginsPage: Window
|
||||
{
|
||||
|
||||
public ObservableCollection<Plugin> Plugins { get; private set; }
|
||||
|
||||
public PluginsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
Loaded += OnPageLoaded;
|
||||
}
|
||||
|
||||
private async void OnPageLoaded(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (Config.PluginsManager is null) return;
|
||||
|
||||
var plugins = await Config.PluginsManager.GetInstalledPlugins();
|
||||
var localList = new List<Plugin>();
|
||||
foreach (var plugin in plugins)
|
||||
{
|
||||
localList.Add(new Plugin(plugin.PluginName, plugin.PluginVersion.ToShortString(), plugin.IsMarkedToUninstall));
|
||||
}
|
||||
|
||||
Plugins = new ObservableCollection<Plugin>(localList);
|
||||
|
||||
|
||||
dataGridPlugins.ItemsSource = Plugins;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="200"
|
||||
x:Class="DiscordBotUI.Views.SettingsPage"
|
||||
Title="SettingsPage" Width="500" Height="200" MinWidth="500" MaxWidth="500" MinHeight="200" MaxHeight="200">
|
||||
<RelativePanel Margin="10,10,10,0">
|
||||
<Label Content="Bot Token: " Name="labelToken" RelativePanel.AlignTopWithPanel="True"/>
|
||||
<TextBox Name="textBoxToken" Text="" IsReadOnly="False" RelativePanel.AlignRightWithPanel="True" Width="350" />
|
||||
|
||||
<Label Content="Bot Prefix: " Name="labelPrefix" RelativePanel.Below="labelToken" Margin="0,20,0,0"/>
|
||||
<TextBox Name="textBoxPrefix" Text="" RelativePanel.AlignRightWithPanel="True" RelativePanel.Below="textBoxToken"
|
||||
IsReadOnly="False" Margin="0,10,0,0" Width="350" />
|
||||
|
||||
<Label Content="Server Id: " Name="labelServerId" RelativePanel.Below="labelPrefix" Margin="0,20,0,0"/>
|
||||
<TextBox Name="textBoxServerId" Text="" RelativePanel.AlignRightWithPanel="True"
|
||||
IsReadOnly="False" RelativePanel.Below="textBoxPrefix" Margin="0,10,0,0" Width="350" />
|
||||
|
||||
<Label Name="labelErrorMessage" Foreground="Red" RelativePanel.Below="textBoxServerId" />
|
||||
|
||||
<Button Name="buttonSaveSettings" Click="ButtonSaveSettingsClick" Content="Save" RelativePanel.AlignBottomWithPanel="True" Margin="0,-50,0,0"
|
||||
Width="120" Height="40" Background="#FF008CFF" Foreground="White" BorderThickness="0" CornerRadius="5" FontWeight="Bold"
|
||||
RelativePanel.AlignHorizontalCenterWithPanel="True" />
|
||||
</RelativePanel>
|
||||
</Window>
|
||||
@@ -1,44 +0,0 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
using PluginManager;
|
||||
|
||||
namespace DiscordBotUI.Views;
|
||||
|
||||
public partial class SettingsPage : Window
|
||||
{
|
||||
public SettingsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private async void ButtonSaveSettingsClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
string token = textBoxToken.Text;
|
||||
string botPrefix = textBoxPrefix.Text;
|
||||
string serverId = textBoxServerId.Text;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(serverId)) serverId = string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
{
|
||||
labelErrorMessage.Content = "The token is invalid";
|
||||
return;
|
||||
}
|
||||
|
||||
if(string.IsNullOrWhiteSpace(botPrefix) || botPrefix.Length > 1 || botPrefix.Length < 1)
|
||||
{
|
||||
labelErrorMessage.Content = "The prefix is invalid";
|
||||
return;
|
||||
}
|
||||
|
||||
Config.AppSettings.Add("token", token);
|
||||
Config.AppSettings.Add("prefix", botPrefix);
|
||||
Config.AppSettings.Add("ServerID", serverId);
|
||||
|
||||
await Config.AppSettings.SaveToFile();
|
||||
|
||||
Config.Logger.Log("Config Saved");
|
||||
|
||||
Close();
|
||||
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user