157 Commits

Author SHA1 Message Date
d5f78c831e Update version to 1.0.4.0 2024-05-10 15:00:47 +03:00
7847c6cc8d Merge with preview. Version 1.0.4 2024-05-10 14:57:00 +03:00
82716a4f4f update builder 2024-05-10 14:55:19 +03:00
9476f9ec31 Added InternalActionOption in ICommandAction interface.
Updated ConsoleUtilities and removed obsolete functions.
2024-05-10 14:39:39 +03:00
dc787ac130 fixed maxParallel downloads default value not being automatically selected at first boot 2024-05-09 22:17:53 +03:00
9525394a6e Updated discord.net library to version 3.14.1. Added method in SettingsDictionary to bulk check if keys are in dictionary. 2024-05-08 14:58:15 +03:00
fc93255503 switched plugin update message to logger instead of Console 2024-04-19 01:01:28 +03:00
cadf500400 Fixed some issues with SettingsDictionary 2024-04-19 00:57:28 +03:00
780614e1e7 gitignore updated 2024-04-19 00:57:09 +03:00
f32920c564 The slash commands can now use interactions 2024-04-17 17:45:35 +03:00
123e8e90a1 Fixed some null errors 2024-04-09 20:40:18 +03:00
0323c888b3 Updated readme 2024-04-01 01:36:45 +03:00
1bb6d3b731 Small refactor 2024-04-01 01:35:20 +03:00
4dc5819c4e The bot now checks for update 2024-04-01 01:33:45 +03:00
5d4fa6fba7 plugin list command now shows if the plugin is already installed. 2024-03-26 23:54:44 +02:00
b6675af9cb Updated readme 2024-03-23 22:53:52 +02:00
23a914d2c9 Fixed loading problem on Discord Bot UI 2024-03-23 22:34:55 +02:00
e8822deeac New UI page 2024-03-07 14:15:50 +02:00
29ecdb6883 Updated UI 2024-03-05 23:16:24 +02:00
90aa5875b5 Added Basic UI Functionality 2024-03-03 18:00:43 +02:00
0fccf706a1 Fixed some errors on SettingsDictionary 2024-03-03 15:07:06 +02:00
fd9cd49844 Deleting plugins is now available 2024-02-28 13:57:12 +02:00
3c3c6a1301 Updated 2024-02-27 22:20:25 +02:00
a2179787b9 Plugin Updater 2024-02-27 19:42:59 +02:00
8c06df9110 Removed help app 2024-02-27 11:09:28 +02:00
ef7a2c0896 Formatted code and rebuilt PluginLoader 2024-02-27 11:07:27 +02:00
14f280baef Moved to Json Database for online plugins 2024-02-26 23:36:19 +02:00
196fb6d3d1 Code formatting and renamed DBCommandExecutingArguments to DbCommandExecutingArguments 2024-02-24 23:22:02 +02:00
cc355d7d4f Fixed some names 2023-12-27 18:24:32 +02:00
af90ae5fba Added UI support for LINUX KDE Plasma 2023-12-27 18:03:26 +02:00
c8480b3c83 Added Channel to DBCommandExecutingArguments.cs 2023-12-17 17:14:48 +02:00
fe32ebc4d7 Fixed bug in linux version for downloading the first plugin only whatever the plugin name was 2023-12-17 16:44:52 +02:00
2280957ea9 Fixed some warnings 2023-11-21 22:07:28 +02:00
944d59d9a3 Reformatting code 2023-11-21 22:04:38 +02:00
79ecff971b Playing with tests 2023-11-20 13:43:43 +02:00
1f0e6516fd New SqlDatabase functions 2023-11-07 10:13:22 +02:00
692f3d8f8c Merge remote-tracking branch 'origin/preview' into preview 2023-10-31 17:36:13 +02:00
6d41d51694 Added config to set the max concurrent downloads 2023-10-31 17:35:58 +02:00
5f23bdadcf Fixed error on ubuntu downloading the wrong plugins 2023-10-30 11:07:59 +02:00
d3555b6fca fixed typo 2023-10-25 10:08:13 +03:00
b5cdc0afeb Merged errors with logs 2023-10-22 13:38:40 +03:00
3858156393 Made Entry Class static 2023-10-22 12:52:52 +03:00
6279c5c3a9 Updated Logger and Created Command to change settings variables 2023-10-01 14:11:34 +03:00
f58a57c6cd Improved logging. 2023-09-26 21:46:54 +03:00
d00ebfd7ed Fixed new name in README 2023-09-25 22:14:53 +03:00
ab279bd284 updated README.md 2023-09-25 22:12:44 +03:00
89c4932cd7 Fixed plugin refresh command and added new method for executing tasks without return type in ConsoleUtilities 2023-09-25 22:06:42 +03:00
c577f625c2 New method to execute using a progress bar feedback on process 2023-09-18 23:54:04 +03:00
58624f4037 Improved download speed and started using Spectre.Console package 2023-09-18 23:13:44 +03:00
c9249dc71b Plugin install does not display all logs when reloading plugin list 2023-09-07 14:55:09 +03:00
5e4f1ca35f The plugin install command now automatically refreshes the installed plugins. 2023-09-07 14:43:10 +03:00
0d8fdb5904 Updated log manager 2023-09-07 13:28:49 +03:00
92a18e3495 Removed URLs file from bot config 2023-09-07 12:50:36 +03:00
e929646e8e removed error when invalid plugin. It was called even when a typo was made 2023-08-15 16:42:13 +03:00
6315d13d18 Fixed Internal Actions to refresh after external actions are loaded 2023-08-15 16:17:32 +03:00
ee527bb36f a 2023-08-08 22:21:36 +03:00
86514d1770 Fixed version 2023-08-08 22:20:16 +03:00
9e8ed1e911 Fixed version 2023-08-08 22:19:29 +03:00
f3c3c7939c Fixed some outputs in plugin manager 2023-08-06 17:37:02 +03:00
361ed37362 Reimplemented error handling for SettingsFile 2023-08-06 17:14:57 +03:00
ed3128b940 Created a batch file to build on windows 2023-08-06 14:25:18 +03:00
6bbd68a135 Fixed release Config not found bug 2023-08-05 21:54:05 +03:00
06d322b5b3 Removed temp file creation 2023-08-05 21:53:08 +03:00
5497ee9119 - 2023-08-05 21:33:16 +03:00
0aa78e3560 Changed README.md 2023-08-05 21:32:49 +03:00
41ad37b3bb Fixed a bug when loading empty json file 2023-08-05 21:25:47 +03:00
0104d09509 Delete .github/workflows directory 2023-08-01 21:33:45 +03:00
b4f5e40f12 Moved items and reimplemented SettingsDictionary.cs 2023-08-01 21:28:44 +03:00
7107b17f19 Update README.md 2023-07-31 22:15:24 +03:00
42e1fd917e Removed some logs from console about json files that are opened 2023-07-31 22:10:26 +03:00
9e6fcdbe6f removed submodules 2023-07-31 21:47:43 +03:00
a24d8aa222 Removed dependencies 2023-07-31 21:11:17 +03:00
0e5c9ff14b Fixed help message on /help command 2023-07-31 20:55:38 +03:00
f8977d8840 Update submodules 2023-07-30 23:09:34 +03:00
bb9768f3a1 patch 2023-07-30 22:30:53 +03:00
b3d6930142 Updated functions in Discord Bot 2023-07-30 22:26:43 +03:00
5254be44be Added plugins folder to solution 2023-07-30 18:36:41 +03:00
207e0d6abd Added plugins submodule 2023-07-30 18:32:47 +03:00
968d23380f message 2023-07-30 18:31:06 +03:00
fff9e2e897 - 2023-07-30 18:27:05 +03:00
3e4e777d8d updated project file 2023-07-30 18:22:20 +03:00
7f906d400f - 2023-07-20 20:38:52 +03:00
c1161a3bca Bot no longer exits when a plugin fails to load 2023-07-20 11:32:15 +03:00
ac512e3a27 Updated display when installing a new plugin 2023-07-16 00:18:27 +03:00
730b628fe3 New method in archive manager and shortcut on exit command 2023-07-15 23:23:06 +03:00
701edc5c6a Update README.md 2023-07-10 21:29:36 +03:00
e7688762b8 Moved to API v11 due to the discriminator removal from discord 2023-07-05 21:01:51 +03:00
a7a71bf49a The logger now supports colors 2023-07-05 19:30:38 +03:00
ac7212ca00 Merge remote-tracking branch 'origin/preview' into preview 2023-07-03 14:40:08 +03:00
298e557260 Fixed some text and added some missing texts to commands. Added new command to clear screen and formated code. 2023-07-03 14:39:50 +03:00
7ba791f906 Create dotnet.yml 2023-07-01 17:37:59 +03:00
4a6a12baae Moved to API v3.10.0 2023-06-26 14:55:54 +03:00
f1dda5da3c Added new functions into Functions.cs and added help command for slash Commands. 2023-06-26 14:51:15 +03:00
3ab96e2d0d Fixed some warnings 2023-06-25 21:35:53 +03:00
970c519a32 Fixed loading plugins at startup 2023-06-15 22:12:15 +03:00
188920ec7f Cleaned up after removal of old commands 2023-06-15 21:59:48 +03:00
dcfc4ea32f Removed ConsoleCommandsHandler_OLD.cs 2023-06-15 21:57:22 +03:00
a8c02176d3 Fixed bug in PluginManager at getting info about plugin & new console commands 2023-06-15 21:55:42 +03:00
1665d47a25 Fixed a bug with invalid token bot startup 2023-06-15 14:46:28 +03:00
0b2f1e6ab6 Reimplemented Command Actions. 2023-06-07 20:36:11 +03:00
bcd9245502 update to display the changelogs for application when an update was found 2023-05-28 19:17:06 +03:00
59da9b295b 2023-05-28 19:14:39 +03:00
99d7d5e7e7 self update removed 2023-05-28 19:08:23 +03:00
e4c60f1606 builder added 2023-05-28 17:54:14 +03:00
77f1bef862 Changed from TextType to LogLevel 2023-05-28 17:37:19 +03:00
f16c139362 The bot is running on the main Thread now 2023-05-28 17:18:55 +03:00
c94cdca6eb New plugin downloader based on threads 2023-05-27 16:28:01 +03:00
dcdf80112d Merge branch 'preview' of https://github.com/Wizzy69/SethDiscordBot into preview 2023-05-23 17:00:14 +03:00
eb836c5b74 2023-05-23 17:00:10 +03:00
de680c6771 removed the economy plugin from the sln 2023-05-21 10:28:08 +03:00
Wizzy69
bcef58a46b Removed plugins from the project and reworked the Plugin Manager 2023-04-29 19:35:19 +03:00
Wizzy69
0dc8cdbce5 2023-04-29 18:57:33 +03:00
Wizzy69
dbdbaa9802 2023-04-29 18:57:26 +03:00
Tudor Andrei
5edcf93371 cleaning up PluginManager (phase 1) 2023-04-25 14:27:55 +03:00
Andrei Tudor
b0be76c62b Created new logger 2023-04-20 19:52:55 +03:00
75a77389a8 2023-04-13 19:58:16 +03:00
0bbced3d58 patch 2023-04-09 20:47:21 +03:00
244209093e patch on discord bot UI 2023-04-08 15:54:39 +03:00
54a68d635d 2023-04-08 13:48:53 +03:00
d7a5cb5a64 updated .vscode 2023-04-08 13:45:41 +03:00
6124f89cb0 updated to start the webpage at startup 2023-04-08 13:33:16 +03:00
810a527cc1 Discord Bot web UI first preview 2023-04-08 13:18:25 +03:00
0a2dff0c6d 2023-04-07 10:02:40 +03:00
382c376c03 2023-04-07 10:01:10 +03:00
84b7d663bc fixed README 2023-04-07 09:59:41 +03:00
623232b67e added documentation to the sql database class 2023-04-07 09:42:36 +03:00
d5df6cfb9d update 2023-04-05 20:10:43 +03:00
10b9548c29 Delete test directory 2023-04-01 16:16:02 +03:00
fa1a136ef1 2023-04-01 16:15:49 +03:00
d20cb62139 Updated initial setup 2023-04-01 16:14:04 +03:00
f2418d0395 fixed start error when no config file exists or is null 2023-03-25 11:51:48 +02:00
460a85944a changed to .json file instead of database for settings 2023-03-24 21:52:03 +02:00
7e2fa02d07 2023-03-11 00:07:11 +02:00
873855937f 2023-02-24 16:46:59 +02:00
1cdd2644df 2023-02-24 11:37:21 +02:00
532540b74f 2023-02-24 11:36:43 +02:00
9ba4ca43e2 Update 2023-02-24 11:12:23 +02:00
8bcaf3f254 2023-02-14 11:33:03 +02:00
0d5c90323a 2023-02-12 12:25:53 +02:00
5b01b15216 Merge branch 'preview' of https://github.com/Wizzy69/DiscordBotWithAPI into preview 2023-01-31 16:08:02 +02:00
4f18f505f4 Updated to allow mention as command prefix. Updated DBCommand 2023-01-31 16:07:53 +02:00
2d3566a01a 2023-01-12 15:21:45 +02:00
22f2cd4e59 linux updater 2023-01-10 19:42:10 +02:00
1683234376 updater for linux is now working 2023-01-10 19:41:03 +02:00
69d99b4189 Updated Logger and message handler. Updated to latest Discord.Net version 2023-01-01 21:55:29 +02:00
4a5e0ef2f3 2022-12-17 12:38:21 +02:00
79731a9704 2022-12-17 12:35:26 +02:00
bd53d099d1 2022-12-09 14:49:05 +02:00
de61f5de88 2022-12-09 14:49:03 +02:00
0527d43dd2 patch 2022-12-09 14:46:10 +02:00
e3511cd96b 2022-11-18 10:45:43 +02:00
d355d3c9b7 2022-11-18 10:38:47 +02:00
5bb13aa4a6 The library can now be used for Windows exclusive bots (Made with WinForm or Wpf) 2022-11-13 16:28:44 +02:00
655f5e2ce0 2022-11-12 12:20:02 +02:00
9014d78a7d 2022-11-04 14:08:35 +02:00
1c026e7f49 patch 2022-11-02 19:42:58 +02:00
d32b3902c9 2022-11-02 18:59:33 +02:00
99 changed files with 5307 additions and 3417 deletions

6
.gitignore vendored
View File

@@ -98,6 +98,7 @@ StyleCopReport.xml
*.pidb *.pidb
*.svclog *.svclog
*.scc *.scc
*.code-workspace
# Chutzpah Test files # Chutzpah Test files
_Chutzpah* _Chutzpah*
@@ -370,3 +371,8 @@ FodyWeavers.xsd
/DiscordBot.rar /DiscordBot.rar
/DiscordBot/Data/ /DiscordBot/Data/
/DiscordBot/Updater/ /DiscordBot/Updater/
.idea/
DiscordBot/Launcher.exe
DiscordBotUI/bin
DiscordBotUI/obj
/.vscode

View File

@@ -0,0 +1,28 @@
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;
}
}

View File

@@ -0,0 +1,53 @@
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;
}
}
}
}

View File

@@ -0,0 +1,206 @@
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;
}
}

View File

@@ -0,0 +1,45 @@
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();
}
}

View File

@@ -0,0 +1,84 @@
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();
}
}

View File

@@ -0,0 +1,95 @@
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;
}
}
}

View File

@@ -0,0 +1,76 @@
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;
}
}

View File

@@ -1,19 +1,16 @@
using System.Collections.Generic; using System.Collections.Generic;
using Discord; using Discord;
using Discord.Commands;
using PluginManager; using PluginManager;
using PluginManager.Interfaces; using PluginManager.Interfaces;
using PluginManager.Loaders; using PluginManager.Loaders;
using PluginManager.Others; using PluginManager.Others;
namespace DiscordBot.Discord.Commands; namespace DiscordBot.Bot.Commands;
/// <summary> /// <summary>
/// The help command /// The help command
/// </summary> /// </summary>
internal class Help : DBCommand internal class Help: DBCommand
{ {
/// <summary> /// <summary>
/// Command name /// Command name
@@ -41,47 +38,48 @@ internal class Help : DBCommand
/// The main body of the command /// The main body of the command
/// </summary> /// </summary>
/// <param name="context">The command context</param> /// <param name="context">The command context</param>
public void ExecuteServer(SocketCommandContext context) public void ExecuteServer(DbCommandExecutingArguments args)
{ {
var args = Functions.GetArguments(context.Message); if (args.arguments is not null)
if (args.Count != 0)
{ {
foreach (var item in args) var e = GenerateHelpCommand(args.arguments[0]);
{ if (e is null)
var e = GenerateHelpCommand(item); args.context.Channel.SendMessageAsync("Unknown Command " + args.arguments[0]);
if (e is null) else
context.Channel.SendMessageAsync("Unknown Command " + item); args.context.Channel.SendMessageAsync(embed: e.Build());
else
context.Channel.SendMessageAsync(embed: e.Build());
}
return; return;
} }
var embedBuilder = new EmbedBuilder(); var embedBuilder = new EmbedBuilder();
var adminCommands = ""; var adminCommands = "";
var normalCommands = ""; var normalCommands = "";
foreach (var cmd in PluginLoader.Commands!) foreach (var cmd in PluginLoader.Commands)
if (cmd.requireAdmin) if (cmd.requireAdmin)
adminCommands += cmd.Command + " "; adminCommands += cmd.Command + " ";
else else
normalCommands += cmd.Command + " "; normalCommands += cmd.Command + " ";
embedBuilder.AddField("Admin Commands", adminCommands);
embedBuilder.AddField("Normal Commands", normalCommands); if (adminCommands.Length > 0)
context.Channel.SendMessageAsync(embed: embedBuilder.Build()); 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) private EmbedBuilder GenerateHelpCommand(string command)
{ {
var embedBuilder = new EmbedBuilder(); var embedBuilder = new EmbedBuilder();
var cmd = PluginLoader.Commands!.Find(p => p.Command == command || var cmd = PluginLoader.Commands.Find(p => p.Command == command ||
(p.Aliases is not null && p.Aliases.Contains(command))); p.Aliases is not null && p.Aliases.Contains(command)
);
if (cmd == null) return null; if (cmd == null) return null;
embedBuilder.AddField("Usage", Config.Variables.GetValue("prefix") + cmd.Usage); embedBuilder.AddField("Usage", Config.AppSettings["prefix"] + cmd.Usage);
embedBuilder.AddField("Description", cmd.Description); embedBuilder.AddField("Description", cmd.Description);
if (cmd.Aliases is null) if (cmd.Aliases is null)
return embedBuilder; return embedBuilder;

View File

@@ -0,0 +1,59 @@
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());
}
}

View File

@@ -1,100 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using PluginManager.Interfaces;
using PluginManager.Others;
using DiscordLibCommands = Discord.Commands;
using DiscordLib = Discord;
using OperatingSystem = PluginManager.Others.OperatingSystem;
namespace DiscordBot.Discord.Commands;
internal class Restart : DBCommand
{
/// <summary>
/// Command name
/// </summary>
public string Command => "restart";
public List<string> Aliases => null;
/// <summary>
/// Command Description
/// </summary>
public string Description => "Restart the bot";
/// <summary>
/// Command usage
/// </summary>
public string Usage => "restart [-p | -c | -args | -cmd] <args>";
/// <summary>
/// Check if the command require administrator to be executed
/// </summary>
public bool requireAdmin => true;
/// <summary>
/// The main body of the command
/// </summary>
/// <param name="context">The command context</param>
public async void ExecuteServer(DiscordLibCommands.SocketCommandContext context)
{
var args = Functions.GetArguments(context.Message);
var OS = Functions.GetOperatingSystem();
if (args.Count == 0)
{
switch (OS)
{
case OperatingSystem.WINDOWS:
Process.Start("./DiscordBot.exe");
break;
case OperatingSystem.LINUX:
case OperatingSystem.MAC_OS:
Process.Start("./DiscordBot");
break;
default:
return;
}
return;
}
switch (args[0])
{
case "-p":
case "-poweroff":
case "-c":
case "-close":
Environment.Exit(0);
break;
case "-cmd":
case "-args":
var cmd = "--args";
if (args.Count > 1)
for (var i = 1; i < args.Count; i++)
cmd += $" {args[i]}";
switch (OS)
{
case OperatingSystem.WINDOWS:
Functions.WriteLogFile("Restarting the bot with the following arguments: \"" + cmd + "\"");
Process.Start("./DiscordBot.exe", cmd);
break;
case OperatingSystem.LINUX:
//case PluginManager.Others.OperatingSystem.MAC_OS: ?? - not tested
Process.Start("./DiscordBot", cmd);
break;
default:
return;
}
Environment.Exit(0);
break;
default:
await context.Channel.SendMessageAsync("Invalid argument. Use `help restart` to see the usage.");
break;
}
}
}

View File

@@ -1,154 +0,0 @@
using System;
using System.Threading.Tasks;
using Discord;
using Discord.Commands;
using Discord.WebSocket;
using static PluginManager.Others.Functions;
namespace DiscordBot.Discord.Core;
internal class Boot
{
/// <summary>
/// The bot prefix
/// </summary>
public readonly string botPrefix;
/// <summary>
/// The bot token
/// </summary>
public readonly string botToken;
/// <summary>
/// The bot client
/// </summary>
public DiscordSocketClient client;
/// <summary>
/// The bot command handler
/// </summary>
private CommandHandler commandServiceHandler;
/// <summary>
/// The command service
/// </summary>
private CommandService service;
/// <summary>
/// The main Boot constructor
/// </summary>
/// <param name="botToken">The bot token</param>
/// <param name="botPrefix">The bot prefix</param>
public Boot(string botToken, string botPrefix)
{
this.botPrefix = botPrefix;
this.botToken = botToken;
}
/// <summary>
/// Checks if the bot is ready
/// </summary>
/// <value> true if the bot is ready, othwerwise false </value>
public bool isReady { get; private set; }
/// <summary>
/// The start method for the bot. This method is used to load the bot
/// </summary>
/// <returns>Task</returns>
public async Task Awake()
{
var config = new DiscordSocketConfig
{
AlwaysDownloadUsers = true,
//Disable system clock checkup (for responses at slash commands)
UseInteractionSnowflakeDate = false
};
client = new DiscordSocketClient(config);
service = new CommandService();
CommonTasks();
await client.LoginAsync(TokenType.Bot, botToken);
await client.StartAsync();
commandServiceHandler = new CommandHandler(client, service, botPrefix);
await commandServiceHandler.InstallCommandsAsync();
await Task.Delay(2000);
while (!isReady) ;
}
private void CommonTasks()
{
if (client == null) return;
client.LoggedOut += Client_LoggedOut;
client.Log += Log;
client.LoggedIn += LoggedIn;
client.Ready += Ready;
}
private async Task Client_LoggedOut()
{
WriteLogFile("Successfully Logged Out");
await Log(new LogMessage(LogSeverity.Info, "Boot", "Successfully logged out from discord !"));
/* var cmds = await client.GetGlobalApplicationCommandsAsync();
foreach (var cmd in cmds)
await cmd.DeleteAsync();*/
}
private async Task Ready()
{
Console.Title = "ONLINE";
isReady = true;
}
private Task LoggedIn()
{
Console.Title = "CONNECTED";
WriteLogFile("The bot has been logged in at " + DateTime.Now.ToShortDateString() + " (" +
DateTime.Now.ToShortTimeString() + ")"
);
return Task.CompletedTask;
}
private Task Log(LogMessage message)
{
switch (message.Severity)
{
case LogSeverity.Error:
case LogSeverity.Critical:
WriteErrFile(message.Message);
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("[ERROR] " + message.Message);
Console.ForegroundColor = ConsoleColor.White;
break;
case LogSeverity.Info:
case LogSeverity.Debug:
WriteLogFile(message.Message);
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine("[INFO] " + message.Message);
Console.ForegroundColor = ConsoleColor.White;
break;
}
return Task.CompletedTask;
}
}

View File

@@ -1,126 +0,0 @@
using System;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Discord.Commands;
using Discord.WebSocket;
using PluginManager.Loaders;
using PluginManager.Others;
using PluginManager.Others.Permissions;
namespace DiscordBot.Discord.Core;
internal class CommandHandler
{
private readonly string botPrefix;
private readonly DiscordSocketClient client;
private readonly CommandService commandService;
/// <summary>
/// Command handler constructor
/// </summary>
/// <param name="client">The discord bot client</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)
{
this.client = client;
this.commandService = commandService;
this.botPrefix = botPrefix;
}
/// <summary>
/// The method to initialize all commands
/// </summary>
/// <returns></returns>
public async Task InstallCommandsAsync()
{
client.MessageReceived += MessageHandler;
client.SlashCommandExecuted += Client_SlashCommandExecuted;
await commandService.AddModulesAsync(Assembly.GetEntryAssembly(), null);
}
private async Task Client_SlashCommandExecuted(SocketSlashCommand arg)
{
try
{
var plugin = PluginLoader.SlashCommands!
.Where(p => p.Name == arg.Data.Name)
.FirstOrDefault();
if (plugin is null) throw new Exception("Failed to run command. !");
if (arg.Channel is SocketDMChannel)
plugin.ExecuteDM(arg);
else plugin.ExecuteServer(arg);
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
ex.WriteErrFile();
}
}
/// <summary>
/// The message handler for the bot
/// </summary>
/// <param name="Message">The message got from the user in discord chat</param>
/// <returns></returns>
private async Task MessageHandler(SocketMessage Message)
{
try
{
if (Message as SocketUserMessage == null)
return;
var message = Message as SocketUserMessage;
if (message == null)
return;
if (!message.Content.StartsWith(botPrefix))
return;
var argPos = 0;
if (message.HasMentionPrefix(client.CurrentUser, ref argPos))
{
await message.Channel.SendMessageAsync("Can not exec mentioned commands !");
return;
}
if (message.Author.IsBot)
return;
var context = new SocketCommandContext(client, message);
await commandService.ExecuteAsync(context, argPos, null);
var plugin = PluginLoader.Commands!
.Where(
p => p.Command == message.Content.Split(' ')[0].Substring(botPrefix.Length) ||
(p.Aliases is not null &&
p.Aliases.Contains(
message.Content.Split(' ')[0].Substring(botPrefix.Length))))
.FirstOrDefault();
if (plugin is null) throw new Exception("Failed to run command. !");
if (plugin.requireAdmin && !context.Message.Author.isAdmin())
return;
if (context.Channel is SocketDMChannel)
plugin.ExecuteDM(context);
else plugin.ExecuteServer(context);
}
catch (Exception ex)
{
ex.WriteErrFile();
}
}
}

View File

@@ -1,52 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<Nullable>disable</Nullable>
<ApplicationIcon />
<StartupObject />
<SignAssembly>False</SignAssembly>
<IsPublishable>True</IsPublishable>
<AssemblyVersion>1.0.1.0</AssemblyVersion>
</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\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Discord.Net" Version="3.7.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PluginManager\PluginManager.csproj" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="xcopy /B /Y &quot;$(TargetDir)*.dll&quot; &quot;$(TargetDir)Libraries&quot;" />
</Target>
</Project>

View File

@@ -1,48 +1,43 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<PropertyGroup> <OutputType>Exe</OutputType>
<OutputType>Exe</OutputType> <TargetFramework>net8.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework> <Nullable>disable</Nullable>
<Nullable>disable</Nullable> <ApplicationIcon />
<ApplicationIcon /> <StartupObject />
<StartupObject /> <SignAssembly>False</SignAssembly>
<SignAssembly>False</SignAssembly> <IsPublishable>True</IsPublishable>
<IsPublishable>True</IsPublishable> <AssemblyVersion>1.0.4.0</AssemblyVersion>
<AssemblyVersion>1.0.1.0</AssemblyVersion> <PublishAot>False</PublishAot>
</PropertyGroup> <FileVersion>1.0.4.0</FileVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DebugType>none</DebugType> <DebugType>none</DebugType>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> <DebugType>none</DebugType>
<DebugType>none</DebugType> </PropertyGroup>
</PropertyGroup> <ItemGroup>
<Compile Remove="Data\**" />
<ItemGroup> <Compile Remove="obj\**" />
<Compile Remove="Data\**" /> <Compile Remove="Output\**" />
<Compile Remove="obj\**" /> </ItemGroup>
<Compile Remove="Output\**" /> <ItemGroup>
</ItemGroup> <EmbeddedResource Remove="Data\**" />
<EmbeddedResource Remove="obj\**" />
<ItemGroup> <EmbeddedResource Remove="Output\**" />
<EmbeddedResource Remove="Data\**" /> </ItemGroup>
<EmbeddedResource Remove="obj\**" /> <ItemGroup>
<EmbeddedResource Remove="Output\**" /> <None Remove="Data\**" />
</ItemGroup> <None Remove="obj\**" />
<None Remove="Output\**" />
<ItemGroup> <None Remove="builder.bat" />
<None Remove="Data\**" /> <None Remove="builder.sh" />
<None Remove="obj\**" /> </ItemGroup>
<None Remove="Output\**" /> <ItemGroup>
</ItemGroup> <PackageReference Include="Spectre.Console" Version="0.49.1" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Discord.Net" Version="3.7.2" /> <ProjectReference Include="..\PluginManager\PluginManager.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PluginManager\PluginManager.csproj" />
</ItemGroup>
</Project> </Project>

View File

@@ -2,28 +2,52 @@
using System.IO; using System.IO;
using System.Reflection; using System.Reflection;
namespace DiscordBot
namespace DiscordBot;
public static class Entry
{ {
private static readonly string logo = @"
_____ _ _ _____ _ _ ____ _
/ ____| | | | | | __ \(_) | | | _ \ | |
| (___ ___| |_| |__ | | | |_ ___ ___ ___ _ __ __| | | |_) | ___ | |_
\___ \ / _ \ __| '_ \ | | | | / __|/ __/ _ \| '__/ _` | | _ < / _ \| __|
____) | __/ |_| | | | | |__| | \__ \ (_| (_) | | | (_| | | |_) | (_) | |_
|_____/ \___|\__|_| |_| |_____/|_|___/\___\___/|_| \__,_| |____/ \___/ \__|
public class Entry
";
public static void Main(string[] args)
{ {
[STAThread] #if DEBUG
public static void Main(string[] args) if (args.Length == 1 && args[0] == "/purge_plugins" )
{ {
AppDomain currentDomain = AppDomain.CurrentDomain; foreach (var plugin in Directory.GetFiles("./Data/Plugins", "*.dll", SearchOption.AllDirectories))
currentDomain.AssemblyResolve += new ResolveEventHandler(LoadFromSameFolder);
static Assembly LoadFromSameFolder(object sender, ResolveEventArgs args)
{ {
string folderPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "./Libraries"); File.Delete(plugin);
string assemblyPath = Path.Combine(folderPath, new AssemblyName(args.Name).Name + ".dll");
if (!File.Exists(assemblyPath)) return null;
Assembly assembly = Assembly.LoadFrom(assemblyPath);
return assembly;
} }
Program.Startup(args);
} }
#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);
} }
} }

39
DiscordBot/Installer.cs Normal file
View File

@@ -0,0 +1,39 @@
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));
}
}

View File

@@ -1,191 +1,32 @@
using System; using System;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq;
using System.Reflection; using System.Reflection;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using DiscordBot.Utilities;
using DiscordBot.Discord.Core; using PluginManager.Bot;
using PluginManager;
using PluginManager.Database;
using PluginManager.Items;
using PluginManager.Online;
using PluginManager.Online.Helpers;
using PluginManager.Others; using PluginManager.Others;
using PluginManager.Others.Actions;
using Terminal.Gui; using Spectre.Console;
using static PluginManager.Config;
using OperatingSystem = PluginManager.Others.OperatingSystem;
namespace DiscordBot; namespace DiscordBot;
public class Program public class Program
{ {
private static bool loadPluginsOnStartup; public static InternalActionManager internalActionManager;
private static bool listPluginsAtStartup;
private static ConsoleCommandsHandler consoleCommandsHandler;
/// <summary> /// <summary>
/// The main entry point for the application. /// The main entry point for the application.
/// </summary> /// </summary>
[STAThread]
public static void Startup(string[] args) public static void Startup(string[] args)
{ {
PreLoadComponents(args).Wait();
PreLoadComponents().Wait(); if (!AppSettings.ContainsKey("ServerID") || !AppSettings.ContainsKey("token") || !AppSettings.ContainsKey("prefix"))
Installer.GenerateStartupConfig().Wait();
if (!Config.Variables.Exists("ServerID") || !Config.Variables.Exists("token") || HandleInput().Wait();
Config.Variables.GetValue("token") == null ||
(Config.Variables.GetValue("token")?.Length != 70 && Config.Variables.GetValue("token")?.Length != 59) ||
!Config.Variables.Exists("prefix") || Config.Variables.GetValue("prefix") == null ||
Config.Variables.GetValue("prefix")?.Length != 1 ||
(args.Length == 1 && args[0] == "/reset"))
{
Application.Init();
var top = Application.Top;
var win = new Window("Discord Bot Config - " + Assembly.GetExecutingAssembly().GetName().Version)
{
X = 0,
Y = 1,
Width = Dim.Fill(),
Height = Dim.Fill()
};
top.Add(win);
var labelInfo = new Label(
"Configuration file not found or invalid. " +
"Please fill the following fields to create a new configuration file."
)
{
X = Pos.Center(),
Y = 2
};
var labelToken = new Label("Please insert your token here: ")
{
X = 5,
Y = 5
};
var textFiledToken = new TextField("")
{
X = Pos.Left(labelToken) + labelToken.Text.Length + 2,
Y = labelToken.Y,
Width = 70
};
var labelPrefix = new Label("Please insert your prefix here: ")
{
X = 5,
Y = 8
};
var textFiledPrefix = new TextField("")
{
X = Pos.Left(labelPrefix) + labelPrefix.Text.Length + 2,
Y = labelPrefix.Y,
Width = 1
};
var labelServerid = new Label("Please insert your server id here (optional): ")
{
X = 5,
Y = 11
};
var textFiledServerID = new TextField("")
{
X = Pos.Left(labelServerid) + labelServerid.Text.Length + 2,
Y = labelServerid.Y,
Width = 18
};
var button = new Button("Submit")
{
X = Pos.Center() - 10,
Y = 16
};
var button2 = new Button("License")
{
X = Pos.Center() + 10,
Y = 16
};
var button3 = new Button("ⓘ")
{
X = Pos.Left(textFiledServerID) + 20,
Y = textFiledServerID.Y
};
Console.CancelKeyPress += (sender, e) => { top.Running = false; };
button.Clicked += () =>
{
var passMessage = "";
if (textFiledToken.Text.Length != 70 && textFiledToken.Text.Length != 59)
passMessage += "Invalid token, ";
if (textFiledPrefix.Text.ContainsAny("0123456789/\\ ") || textFiledPrefix.Text.Length != 1)
passMessage += "Invalid prefix, ";
if (textFiledServerID.Text.Length != 18 && textFiledServerID.Text.Length > 0)
passMessage += "Invalid serverID";
if (passMessage != "")
{
MessageBox.ErrorQuery("Discord Bot Settings",
"Failed to pass check. Invalid information given:\n" + passMessage, "Retry");
return;
}
Config.Variables.Add("ServerID", (string)textFiledServerID.Text, true);
Config.Variables.Add("token", (string)textFiledToken.Text, true);
Config.Variables.Add("prefix", (string)textFiledPrefix.Text, true);
MessageBox.Query("Discord Bot Settings", "Successfully saved config !\nJust start the bot :D",
"Start :D");
top.Running = false;
};
button2.Clicked += async () =>
{
var license =
await ServerCom.ReadTextFromURL(
"https://raw.githubusercontent.com/Wizzy69/installer/discord-bot-files/LICENSE.txt");
var ProductLicense =
"Seth Discord Bot\n\nDeveloped by Wizzy#9181\nThis application can be used and modified by anyone. Plugin development for this application is also free and supported";
var r = MessageBox.Query("Discord Bot Settings", ProductLicense, "Close", "Read about libraries used");
if (r == 1)
{
var i = 0;
while (i < license.Count)
{
var print_message = license[i++] + "\n";
for (; i < license.Count && !license[i].StartsWith("-----------"); i++)
print_message += license[i] + "\n";
if (print_message.Contains("https://"))
print_message += "\n\nCTRL + Click on a link to open it";
if (MessageBox.Query("Licenses", print_message, "Next", "Quit") == 1) break;
}
}
};
button3.Clicked += () =>
{
MessageBox.Query("Discord Bot Settings",
"Server ID can be found in Server settings => Widget => Server ID",
"Close");
};
win.Add(labelInfo, labelPrefix, labelServerid, labelToken);
win.Add(textFiledToken, textFiledPrefix, textFiledServerID, button3);
win.Add(button, button2);
Application.Run();
Application.Shutdown();
}
HandleInput(args).Wait();
} }
/// <summary> /// <summary>
@@ -193,249 +34,111 @@ public class Program
/// </summary> /// </summary>
private static void NoGUI() private static void NoGUI()
{ {
#if DEBUG internalActionManager.Initialize().Wait();
Settings.Variables.outputStream.WriteLine(); internalActionManager.Execute("plugin", "load").Wait();
ConsoleCommandsHandler.ExecuteCommad("lp").Wait(); internalActionManager.Refresh().Wait();
#else
if (loadPluginsOnStartup) consoleCommandsHandler.HandleCommand("lp");
if (listPluginsAtStartup) consoleCommandsHandler.HandleCommand("listplugs");
#endif
while (true) while (true)
{ {
var cmd = Console.ReadLine(); var cmd = Console.ReadLine();
if (!consoleCommandsHandler.HandleCommand(cmd! var args = cmd.Split(' ');
#if DEBUG var command = args[0];
, false args = args.Skip(1).ToArray();
#endif if (args.Length == 0)
args = null;
) && cmd.Length > 0) internalActionManager.Execute(command, args).Wait(); // Execute the command
Settings.Variables.outputStream.WriteLine("Failed to run command " + cmd);
} }
} }
/// <summary> /// <summary>
/// Start the bot without user interface /// Start the bot without user interface
/// </summary> /// </summary>
/// <returns>Returns the boot loader for the Discord Bot</returns> /// <returns>Returns the bootloader for the Discord Bot</returns>
private static async Task<Boot> StartNoGui() private static async Task StartNoGui()
{ {
Console.Clear();
Console.ForegroundColor = ConsoleColor.DarkYellow;
var startupMessageList = AnsiConsole.MarkupLine($"[yellow]Running on version: {AppSettings["Version"]}[/]");
await ServerCom.ReadTextFromURL( AnsiConsole.MarkupLine("[yellow]Git SethBot: https://github.com/andreitdr/SethDiscordBot [/]");
"https://raw.githubusercontent.com/Wizzy69/installer/discord-bot-files/StartupMessage"); AnsiConsole.MarkupLine("[yellow]Git Plugins: https://github.com/andreitdr/SethPlugins [/]");
foreach (var message in startupMessageList) AnsiConsole.MarkupLine("[yellow]Remember to close the bot using the shutdown command ([/][red]exit[/][yellow]) or some settings won't be saved[/]");
Settings.Variables.outputStream.WriteLine(message); AnsiConsole.MarkupLine($"[yellow]Running on [/][magenta]{(OperatingSystem.IsWindows() ? "Windows" : "Linux")}[/]");
Settings.Variables.outputStream.WriteLine( AnsiConsole.MarkupLine("[yellow]===== Seth Discord Bot =====[/]");
$"Running on version: {Assembly.GetExecutingAssembly().GetName().Version}");
Settings.Variables.outputStream.WriteLine($"Git URL: {Settings.Variables.WebsiteURL}");
Utilities.WriteColorText(
"&rRemember to close the bot using the ShutDown command (&ysd&r) or some settings won't be saved\n");
Console.ForegroundColor = ConsoleColor.White;
if (Config.Variables.Exists("LaunchMessage"))
Utilities.WriteColorText(Config.Variables.GetValue("LaunchMessage"));
Utilities.WriteColorText(
"Please note that the bot saves a backup save file every time you are using the shudown command (&ysd&c)");
Settings.Variables.outputStream.WriteLine("============================ LOG ============================");
try try
{ {
string token = ""; var token = AppSettings["token"];
#if DEBUG var prefix = AppSettings["prefix"];
if (await Settings.sqlDatabase.TableExistsAsync("BetaTest"))
{
Settings.Variables.outputStream.WriteLine("Starting in DEBUG MODE");
token = await Settings.sqlDatabase.GetValueAsync("BetaTest", "VariableName", "Token", "Value");
}
#else
token = Config.Variables.GetValue("token");
#endif
var prefix = Config.Variables.GetValue("prefix");
var discordbooter = new Boot(token, prefix); var discordbooter = new Boot(token, prefix);
await discordbooter.Awake(); await discordbooter.Awake();
return discordbooter;
} }
catch (Exception ex) catch (Exception ex)
{ {
Settings.Variables.outputStream.WriteLine(ex); Logger.Log(ex.ToString(), typeof(Program), LogType.CRITICAL);
return null;
} }
} }
/// <summary> /// <summary>
/// Handle user input arguments from the startup of the application /// Handle user input arguments from the startup of the application
/// </summary> /// </summary>
/// <param name="args">The arguments</param> private static async Task HandleInput()
private static async Task HandleInput(string[] args)
{ {
var len = args.Length; await StartNoGui();
try
var b = await StartNoGui();
consoleCommandsHandler = new ConsoleCommandsHandler(b.client);
if (len > 0 && args[0] == "/remplug")
{ {
var plugName = string.Join(' ', args, 1, args.Length - 1); internalActionManager = new InternalActionManager(AppSettings["PluginFolder"], "*.dll");
Settings.Variables.outputStream.WriteLine("Starting to remove " + plugName); NoGUI();
await ConsoleCommandsHandler.ExecuteCommad("remplug " + plugName);
loadPluginsOnStartup = true;
} }
catch (IOException ex)
if (len > 0 && args[0] == "/lp")
loadPluginsOnStartup = true;
var mainThread = new Thread(() =>
{ {
try if (ex.Message == "No process is on the other end of the pipe." || (uint)ex.HResult == 0x800700E9)
{ {
NoGUI(); 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
);
} }
catch (IOException ex) }
{
if (ex.Message == "No process is on the other end of the pipe." || (uint)ex.HResult == 0x800700E9)
{
if (Config.Variables.Exists("LaunchMessage"))
Config.Variables.Add("LaunchMessage",
"An error occured while closing the bot last time. Please consider closing the bot using the &rsd&c method !\nThere is a risk of losing all data or corruption of the save file, which in some cases requires to reinstall the bot !",
false);
Functions.WriteErrFile(ex.ToString());
}
}
});
mainThread.Start();
} }
private static async Task PreLoadComponents() private static async Task PreLoadComponents(string[] args)
{ {
Settings.Variables.outputStream = Console.Out; await Initialize();
Settings.Variables.outputStream.WriteLine("Loading resources ...");
var main = new Utilities.ProgressBar(ProgressBarType.NO_END);
main.Start();
Directory.CreateDirectory("./Data/Resources");
Directory.CreateDirectory("./Data/Plugins");
Directory.CreateDirectory("./Data/PAKS");
Settings.sqlDatabase = new SqlDatabase(Functions.dataFolder + "SetDB.dat"); AppSettings["Version"] = Assembly.GetExecutingAssembly().GetName().Version.ToString();
await Settings.sqlDatabase.Open(); PluginManager.Updater.Application.AppUpdater updater = new();
await Config.Initialize(); var update = await updater.CheckForUpdates();
if (update != PluginManager.Updater.Application.Update.None)
if (await Config.Variables.ExistsAsync("DeleteLogsAtStartup"))
if (await Config.Variables.GetValueAsync("DeleteLogsAtStartup") == "true")
foreach (var file in Directory.GetFiles("./Output/Logs/"))
File.Delete(file);
var OnlineDefaultKeys =
await ServerCom.ReadTextFromURL(
"https://raw.githubusercontent.com/Wizzy69/installer/discord-bot-files/SetupKeys");
if (!await Config.Variables.ExistsAsync("Version"))
await Config.Variables.AddAsync("Version", Assembly.GetExecutingAssembly().GetName().Version.ToString(), false);
else
await Config.Variables.SetValueAsync("Version", Assembly.GetExecutingAssembly().GetName().Version.ToString());
foreach (var key in OnlineDefaultKeys)
{ {
if (key.Length <= 3 || !key.Contains(' ')) continue; Console.WriteLine($"New update available: {update.UpdateVersion}");
var s = key.Split(' '); Console.WriteLine($"Download link: {update.UpdateUrl}");
try Console.WriteLine($"Update notes: {update.UpdateNotes}\n\n");
{
if (await Config.Variables.ExistsAsync(s[0])) await Config.Variables.SetValueAsync(s[0], s[1]); Environment.Exit(0);
else
await Config.Variables.AddAsync(s[0], s[1], s[2].ToLower() == "true");
}
catch (Exception ex)
{
Functions.WriteErrFile(ex.Message);
}
} }
Logger.OnLog += (sender, logMessage) =>
var onlineSettingsList =
await ServerCom.ReadTextFromURL(
"https://raw.githubusercontent.com/Wizzy69/installer/discord-bot-files/OnlineData");
main.Stop("Loaded online settings. Loading updates ...");
foreach (var key in onlineSettingsList)
{ {
if (key.Length <= 3 || !key.Contains(' ')) continue; var messageColor = logMessage.Type switch
var s = key.Split(' ');
switch (s[0])
{ {
case "CurrentVersion": LogType.INFO => "[green]",
var newVersion = s[1]; LogType.WARNING => "[yellow]",
if (!newVersion.Equals(await Config.Variables.GetValueAsync("Version"))) LogType.ERROR => "[red]",
{ LogType.CRITICAL => "[red]",
var nVer = new VersionString(newVersion.Substring(2)); _ => "[white]"
var cVer = new VersionString((await Config.Variables.GetValueAsync("Version")).Substring(2)); };
if (cVer > nVer)
{
await Config.Variables.SetValueAsync("Version", "1." + cVer.ToShortString() + " (Beta)");
break;
}
if (Functions.GetOperatingSystem() == OperatingSystem.WINDOWS) if (logMessage.Message.Contains('['))
{ {
var url = Console.WriteLine(logMessage.Message);
$"https://github.com/Wizzy69/SethDiscordBot/releases/download/v{newVersion}/net6.0.zip"; return;
Process.Start(".\\Updater\\Updater.exe",
$"{newVersion} {url} {Process.GetCurrentProcess().ProcessName}");
}
else
{
var url =
$"https://github.com/Wizzy69/SethDiscordBot/releases/download/v{newVersion}/net6.0_linux.zip";
Settings.Variables.outputStream.WriteLine("Downloading update ...");
await ServerCom.DownloadFileNoProgressAsync(url, "./update.zip");
await File.WriteAllTextAsync("Install.sh",
"#!/bin/bash\nunzip -qq update.zip -d ./\nrm update.zip\nchmod +x SethDiscordBot\n./DiscordBot");
Process.Start("Install.sh").WaitForExit();
Environment.Exit(0);
}
}
break;
case "UpdaterVersion":
var updaternewversion = s[1];
if (Functions.GetOperatingSystem() == OperatingSystem.LINUX)
break;
if (!await Config.Variables.ExistsAsync("UpdaterVersion"))
await Config.Variables.AddAsync("UpdaterVersion", "0.0.0.0", false);
if (await Config.Variables.GetValueAsync("UpdaterVersion") != updaternewversion ||
!Directory.Exists("./Updater") ||
!File.Exists("./Updater/Updater.exe"))
{
Console.Clear();
Settings.Variables.outputStream.WriteLine("Installing updater ...\nDo NOT close the bot during update !");
var bar = new Utilities.ProgressBar(ProgressBarType.NO_END);
bar.Start();
await ServerCom.DownloadFileNoProgressAsync(
"https://github.com/Wizzy69/installer/releases/download/release-1-discordbot/Updater.zip",
"./Updater.zip");
await Functions.ExtractArchive("./Updater.zip", "./", null,
UnzipProgressType.PercentageFromTotalSize);
await Config.Variables.SetValueAsync("UpdaterVersion", updaternewversion);
File.Delete("Updater.zip");
bar.Stop("Updater has been updated !");
Console.Clear();
}
break;
} }
}
Console.Clear(); AnsiConsole.MarkupLine($"{messageColor}{logMessage.ThrowTime} {logMessage.Message} [/]");
};
} }
} }

View File

@@ -0,0 +1,31 @@
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;
}
}

View File

@@ -0,0 +1,56 @@
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);
}
}
}

35
DiscordBot/builder.bat Normal file
View File

@@ -0,0 +1,35 @@
@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!"

36
DiscordBot/builder.sh Normal file
View File

@@ -0,0 +1,36 @@
# 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!"

454
DiscordBotUI/.gitignore vendored Normal file
View File

@@ -0,0 +1,454 @@
## 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

View File

@@ -0,0 +1,24 @@
<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>

View File

@@ -0,0 +1,25 @@
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();
}
}

View File

@@ -0,0 +1,11 @@
{
"profiles": {
"DiscordBotUI.Desktop": {
"commandName": "Project"
},
"WSL": {
"commandName": "WSL2",
"distributionName": ""
}
}
}

View File

@@ -0,0 +1,18 @@
<?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>

View File

@@ -0,0 +1,35 @@
@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!"

View File

@@ -0,0 +1,17 @@
<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>

View File

@@ -0,0 +1,31 @@
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.

After

Width:  |  Height:  |  Size: 172 KiB

View File

@@ -0,0 +1,90 @@
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;
}
}

View File

@@ -0,0 +1,83 @@
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();
}
}
}

View File

@@ -0,0 +1,25 @@
<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>

View File

@@ -0,0 +1,33 @@
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;
}
}
}

View File

@@ -0,0 +1,22 @@
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;
}
}
}

View File

@@ -0,0 +1,22 @@
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;
}
}
}

View File

@@ -0,0 +1,11 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using ReactiveUI;
namespace DiscordBotUI.ViewModels
{
public class ViewModelBase : ReactiveObject
{
}
}

View File

@@ -0,0 +1,41 @@
<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>

View File

@@ -0,0 +1,80 @@
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();
}
}

View File

@@ -0,0 +1,33 @@
<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>

View File

@@ -0,0 +1,62 @@
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);
}
}

View File

@@ -0,0 +1,25 @@
<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>

View File

@@ -0,0 +1,43 @@
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;
}
}

View File

@@ -0,0 +1,26 @@
<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>

View File

@@ -0,0 +1,44 @@
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();
}
}

153
PluginManager/Bot/Boot.cs Normal file
View File

@@ -0,0 +1,153 @@
using System;
using System.Threading.Tasks;
using Discord;
using Discord.Commands;
using Discord.WebSocket;
using PluginManager.Others;
namespace PluginManager.Bot;
public class Boot
{
/// <summary>
/// The bot prefix
/// </summary>
public readonly string BotPrefix;
/// <summary>
/// The bot token
/// </summary>
public readonly string BotToken;
/// <summary>
/// The bot client
/// </summary>
public DiscordSocketClient Client;
/// <summary>
/// The bot command handler
/// </summary>
private CommandHandler _commandServiceHandler;
/// <summary>
/// The command service
/// </summary>
private CommandService _service;
/// <summary>
/// The main Boot constructor
/// </summary>
/// <param name="botToken">The bot token</param>
/// <param name="botPrefix">The bot prefix</param>
public Boot(string botToken, string botPrefix)
{
this.BotPrefix = botPrefix;
this.BotToken = botToken;
}
/// <summary>
/// Checks if the bot is ready
/// </summary>
/// <value> true if the bot is ready, otherwise false </value>
public bool IsReady { get; private set; }
/// <summary>
/// The start method for the bot. This method is used to load the bot
/// </summary>
/// <param name="config">
/// The discord socket config. If null then the default one will be applied (AlwaysDownloadUsers=true,
/// UseInteractionSnowflakeDate=false, GatewayIntents=GatewayIntents.All)
/// </param>
/// <returns>Task</returns>
public async Task Awake(DiscordSocketConfig? config = null)
{
if (config is null)
config = new DiscordSocketConfig
{
AlwaysDownloadUsers = true,
//Disable system clock checkup (for responses at slash commands)
UseInteractionSnowflakeDate = false,
GatewayIntents = GatewayIntents.All
};
Client = new DiscordSocketClient(config);
_service = new CommandService();
CommonTasks();
await Client.LoginAsync(TokenType.Bot, BotToken);
await Client.StartAsync();
_commandServiceHandler = new CommandHandler(Client, _service, BotPrefix);
await _commandServiceHandler.InstallCommandsAsync();
Config.DiscordBotClient = this;
while (!IsReady) ;
}
private void CommonTasks()
{
if (Client == null) return;
Client.LoggedOut += Client_LoggedOut;
Client.Log += Log;
Client.LoggedIn += LoggedIn;
Client.Ready += Ready;
Client.Disconnected += Client_Disconnected;
}
private async Task Client_Disconnected(Exception arg)
{
if (arg.Message.Contains("401"))
{
Config.AppSettings.Remove("token");
Config.Logger.Log("The token is invalid. Please restart the bot and follow the instructions", typeof(Boot), LogType.CRITICAL);
await Config.AppSettings.SaveToFile();
Environment.Exit(0);
}
}
private async Task Client_LoggedOut()
{
Config.Logger.Log("Successfully Logged Out", typeof(Boot));
await Log(new LogMessage(LogSeverity.Info, "Boot", "Successfully logged out from discord !"));
}
private Task Ready()
{
IsReady = true;
return Task.CompletedTask;
}
private Task LoggedIn()
{
Config.Logger.Log("Successfully Logged In", typeof(Boot));
return Task.CompletedTask;
}
private Task Log(LogMessage message)
{
switch (message.Severity)
{
case LogSeverity.Error:
case LogSeverity.Critical:
Config.Logger.Log(message.Message, typeof(Boot), LogType.ERROR);
break;
case LogSeverity.Info:
case LogSeverity.Debug:
Config.Logger.Log(message.Message, typeof(Boot), LogType.INFO);
break;
}
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,159 @@
using System;
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;
namespace PluginManager.Bot;
internal class CommandHandler
{
private readonly string _botPrefix;
private readonly DiscordSocketClient _client;
private readonly CommandService _commandService;
/// <summary>
/// Command handler constructor
/// </summary>
/// <param name="client">The discord bot client</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)
{
_client = client;
_commandService = commandService;
_botPrefix = botPrefix;
}
/// <summary>
/// The method to initialize all commands
/// </summary>
/// <returns></returns>
public async Task InstallCommandsAsync()
{
_client.MessageReceived += MessageHandler;
_client.SlashCommandExecuted += Client_SlashCommandExecuted;
await _commandService.AddModulesAsync(Assembly.GetEntryAssembly(), null);
}
private Task Client_SlashCommandExecuted(SocketSlashCommand arg)
{
try
{
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);
}
catch (Exception ex)
{
Config.Logger.Log(ex.Message, type: LogType.ERROR, source: typeof(CommandHandler));
}
return Task.CompletedTask;
}
/// <summary>
/// The message handler for the bot
/// </summary>
/// <param name="Message">The message got from the user in discord chat</param>
/// <returns></returns>
private async Task MessageHandler(SocketMessage Message)
{
try
{
if (Message.Author.IsBot)
return;
if (Message as SocketUserMessage == null)
return;
var message = Message as SocketUserMessage;
if (message is null)
return;
var argPos = 0;
if (!message.Content.StartsWith(_botPrefix) && !message.HasMentionPrefix(_client.CurrentUser, ref argPos))
return;
var context = new SocketCommandContext(_client, message);
await _commandService.ExecuteAsync(context, argPos, null);
DBCommand? plugin;
var cleanMessage = "";
if (message.HasMentionPrefix(_client.CurrentUser, ref argPos))
{
var mentionPrefix = "<@" + _client.CurrentUser.Id + ">";
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]
)
);
cleanMessage = message.Content.Substring(mentionPrefix.Length + 1);
}
else
{
plugin = PluginLoader.Commands!
.FirstOrDefault(p => p.Command ==
message.Content.Split(' ')[0].Substring(_botPrefix.Length) ||
p.Aliases is not null &&
p.Aliases.Contains(
message.Content.Split(' ')[0]
.Substring(_botPrefix.Length)
)
);
cleanMessage = message.Content.Substring(_botPrefix.Length);
}
if (plugin is null)
return;
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);
Config.Logger.Log(
$"User ({context.User.Username}) from Guild \"{context.Guild.Name}\" executed command \"{cmd.cleanContent}\"",
typeof(CommandHandler),
LogType.INFO
);
if (context.Channel is SocketDMChannel)
plugin.ExecuteDM(cmd);
else plugin.ExecuteServer(cmd);
}
catch (Exception ex)
{
Config.Logger.Log(ex.Message, type: LogType.ERROR, source: typeof(CommandHandler));
}
}
}

View File

@@ -1,199 +1,84 @@
using System; using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using PluginManager.Bot;
using PluginManager.Online.Helpers; using PluginManager.Interfaces.Updater;
using PluginManager.Online;
using PluginManager.Others;
using PluginManager.Others.Actions;
using PluginManager.Others.Logger;
using PluginManager.Plugin;
using PluginManager.Updater.Application;
namespace PluginManager; namespace PluginManager;
public static class Config public class Config
{ {
private static bool IsLoaded = false;
private static readonly string _DefaultBranchForPlugins = "releases";
private static readonly string _ConfigFile = "./Data/Resources/config.json";
private static readonly string _PluginsDatabaseFile = "./Data/Resources/plugins.json";
private static readonly string _ResourcesFolder = "./Data/Resources";
private static readonly string _PluginsFolder = "./Data/Plugins";
private static readonly string _ArchivesFolder = "./Data/Archives";
private static readonly string _LogsFolder = "./Data/Logs";
private static bool _isLoaded;
public static Logger Logger;
public static SettingsDictionary<string, string> AppSettings;
internal static string PluginDatabase => AppSettings["PluginDatabase"];
internal static string ServerID => AppSettings["ServerID"];
public static InternalActionManager InternalActionManager;
public static PluginsManager PluginsManager;
internal static Boot? DiscordBotClient;
public static Boot? DiscordBot => DiscordBotClient;
public static async Task Initialize() public static async Task Initialize()
{ {
if (IsLoaded) if (_isLoaded) return;
return;
if (!await Settings.sqlDatabase.TableExistsAsync("Plugins")) Directory.CreateDirectory(_ResourcesFolder);
await Settings.sqlDatabase.CreateTableAsync("Plugins", "PluginName", "Version"); Directory.CreateDirectory(_PluginsFolder);
if (!await Settings.sqlDatabase.TableExistsAsync("Variables")) Directory.CreateDirectory(_ArchivesFolder);
await Settings.sqlDatabase.CreateTableAsync("Variables", "VarName", "Value", "ReadOnly"); Directory.CreateDirectory(_LogsFolder);
AppSettings = new SettingsDictionary<string, string>(_ConfigFile);
bool response = await AppSettings.LoadFromFile();
if (!response)
throw new Exception("Invalid config file");
AppSettings["LogFolder"] = _LogsFolder;
AppSettings["PluginFolder"] = _PluginsFolder;
AppSettings["ArchiveFolder"] = _ArchivesFolder;
AppSettings["PluginDatabase"] = _PluginsDatabaseFile;
if (!File.Exists(_PluginsDatabaseFile))
{
List<PluginInfo> plugins = new();
await JsonManager.SaveToJsonFile(_PluginsDatabaseFile, plugins);
}
Logger = new Logger(false, true, _LogsFolder + $"/{DateTime.Today.ToShortDateString().Replace("/", "")}.log");
PluginsManager = new PluginsManager(_DefaultBranchForPlugins);
await PluginsManager.UninstallMarkedPlugins();
await PluginsManager.CheckForUpdates();
_isLoaded = true;
Logger.Log("Config initialized", typeof(Config));
IsLoaded = true;
} }
public static class Variables
{
public static async Task<string> GetValueAsync(string VarName)
{
if (!IsLoaded)
throw new Exception("Config is not loaded");
return await Settings.sqlDatabase.GetValueAsync("Variables", "VarName", VarName, "Value");
}
public static string GetValue(string VarName)
{
if (!IsLoaded)
throw new Exception("Config is not loaded");
return Settings.sqlDatabase.GetValue("Variables", "VarName", VarName, "Value");
}
public static async Task SetValueAsync(string VarName, string Value)
{
if (!IsLoaded)
throw new Exception("Config is not loaded");
if (await IsReadOnlyAsync(VarName))
throw new Exception($"Variable ({VarName}) is read only and can not be changed to {Value}");
await Settings.sqlDatabase.SetValueAsync("Variables", "VarName", VarName, "Value", Value);
}
public static void SetValue(string VarName, string Value)
{
if (!IsLoaded)
throw new Exception("Config is not loaded");
if (IsReadOnly(VarName))
throw new Exception($"Variable ({VarName}) is read only and can not be changed to {Value}");
Settings.sqlDatabase.SetValue("Variables", "VarName", VarName, "Value", Value);
}
public static async Task<bool> IsReadOnlyAsync(string VarName)
{
if (!IsLoaded)
throw new Exception("Config is not loaded");
return (await Settings.sqlDatabase.GetValueAsync("Variables", "VarName", VarName, "ReadOnly")).Equals("true", StringComparison.CurrentCultureIgnoreCase);
}
public static bool IsReadOnly(string VarName)
{
if (!IsLoaded)
throw new Exception("Config is not loaded");
return (Settings.sqlDatabase.GetValue("Variables", "VarName", VarName, "ReadOnly")).Equals("true", StringComparison.CurrentCultureIgnoreCase);
}
public static async Task SetReadOnlyAsync(string VarName, bool ReadOnly)
{
if (!IsLoaded)
throw new Exception("Config is not loaded");
await Settings.sqlDatabase.SetValueAsync("Variables", "VarName", VarName, "ReadOnly", ReadOnly ? "true" : "false");
}
public static void SetReadOnly(string VarName, bool ReadOnly)
{
if (!IsLoaded)
throw new Exception("Config is not loaded");
Settings.sqlDatabase.SetValue("Variables", "VarName", VarName, "ReadOnly", ReadOnly ? "true" : "false");
}
public static async Task<bool> ExistsAsync(string VarName)
{
if (!IsLoaded)
throw new Exception("Config is not loaded");
return await Settings.sqlDatabase.KeyExistsAsync("Variables", "VarName", VarName);
}
public static bool Exists(string VarName)
{
if (!IsLoaded)
throw new Exception("Config is not loaded");
return Settings.sqlDatabase.KeyExists("Variables", "VarName", VarName);
}
public static async Task AddAsync(string VarName, string Value, bool ReadOnly = false)
{
if (!IsLoaded)
throw new Exception("Config is not loaded");
if (await ExistsAsync(VarName))
{
await SetValueAsync(VarName, Value);
await SetReadOnlyAsync(VarName, ReadOnly);
return;
}
await Settings.sqlDatabase.InsertAsync("Variables", VarName, Value, ReadOnly ? "true" : "false");
}
public static void Add(string VarName, string Value, bool ReadOnly = false)
{
if (!IsLoaded)
throw new Exception("Config is not loaded");
if (Exists(VarName))
{
SetValue(VarName, Value);
SetReadOnly(VarName, ReadOnly);
return;
}
Settings.sqlDatabase.Insert("Variables", VarName, Value, ReadOnly ? "true" : "false");
}
public static async Task RemoveKeyAsync(string VarName)
{
if (!IsLoaded)
throw new Exception("Config is not loaded");
await Settings.sqlDatabase.RemoveKeyAsync("Variables", "VarName", VarName);
}
public static void RemoveKey(string VarName)
{
if (!IsLoaded)
throw new Exception("Config is not loaded");
Settings.sqlDatabase.RemoveKey("Variables", "VarName", VarName);
}
}
public static class Plugins
{
public static async Task<string> GetVersionAsync(string pluginName)
{
if (!IsLoaded)
throw new Exception("Config is not loaded yet");
string result = await Settings.sqlDatabase.GetValueAsync("Plugins", "PluginName", pluginName, "Version");
if (result is null)
return "0.0.0";
return result;
}
public static string GetVersion(string pluginName)
{
if (!IsLoaded)
throw new Exception("Config is not loaded yet");
string result = Settings.sqlDatabase.GetValue("Plugins", "PluginName", pluginName, "Version");
if (result is null)
return "0.0.0";
return result;
}
public static async Task SetVersionAsync(string pluginName, VersionString version)
{
if (!IsLoaded)
throw new Exception("Config is not loaded yet");
if (!await Settings.sqlDatabase.KeyExistsAsync("Plugins", "PluginName", pluginName))
{
await Settings.sqlDatabase.InsertAsync("Plugins", pluginName, version.ToShortString());
return;
}
await Settings.sqlDatabase.SetValueAsync("Plugins", "PluginName", pluginName, "Version", version.ToShortString());
}
public static void SetVersion(string pluginName, VersionString version)
{
if (!IsLoaded)
throw new Exception("Config is not loaded yet");
if (!Settings.sqlDatabase.KeyExists("Plugins", "PluginName", pluginName))
{
Settings.sqlDatabase.Insert("Plugins", pluginName, version.ToShortString());
return;
}
Settings.sqlDatabase.SetValue("Plugins", "PluginName", pluginName, "Version", version.ToShortString());
}
}
} }

View File

@@ -1,310 +1,644 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SQLite; using System.Data.SQLite;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace PluginManager.Database
namespace PluginManager.Database;
public class SqlDatabase
{ {
public class SqlDatabase private readonly SQLiteConnection _connection;
/// <summary>
/// Initialize a SQL connection by specifing its private path
/// </summary>
/// <param name="fileName">The path to the database (it is starting from ./Data/Resources/)</param>
public SqlDatabase(string fileName)
{ {
private string ConnectionString; if (!fileName.StartsWith("./Data/Resources/"))
private SQLiteConnection Connection; fileName = Path.Combine("./Data/Resources", fileName);
if (!File.Exists(fileName))
SQLiteConnection.CreateFile(fileName);
var connectionString = $"URI=file:{fileName}";
_connection = new SQLiteConnection(connectionString);
}
public SqlDatabase(string fileName)
/// <summary>
/// Open the SQL Connection. To close use the Stop() method
/// </summary>
/// <returns></returns>
public async Task Open()
{
await _connection.OpenAsync();
}
/// <summary>
/// <para>
/// Insert into a specified table some values
/// </para>
/// </summary>
/// <param name="tableName">The table name</param>
/// <param name="values">The values to be inserted (in the correct order and number)</param>
/// <returns></returns>
public async Task InsertAsync(string tableName, params string[] values)
{
var query = $"INSERT INTO {tableName} VALUES (";
for (var i = 0; i < values.Length; i++)
{ {
if (!File.Exists(fileName)) query += $"'{values[i]}'";
SQLiteConnection.CreateFile(fileName); if (i != values.Length - 1)
ConnectionString = $"URI=file:{fileName}"; query += ", ";
Connection = new SQLiteConnection(ConnectionString);
} }
public async Task Open() query += ")";
{
await Connection.OpenAsync();
//Console.WriteLine("Opened database successfully"); var command = new SQLiteCommand(query, _connection);
await command.ExecuteNonQueryAsync();
}
/// <summary>
/// <para>
/// Insert into a specified table some values
/// </para>
/// </summary>
/// <param name="tableName">The table name</param>
/// <param name="values">The values to be inserted (in the correct order and number)</param>
/// <returns></returns>
public void Insert(string tableName, params string[] values)
{
var query = $"INSERT INTO {tableName} VALUES (";
for (var i = 0; i < values.Length; i++)
{
query += $"'{values[i]}'";
if (i != values.Length - 1)
query += ", ";
} }
public async Task InsertAsync(string tableName, params string[] values) query += ")";
{
string query = $"INSERT INTO {tableName} VALUES ("; var command = new SQLiteCommand(query, _connection);
for (int i = 0; i < values.Length; i++) command.ExecuteNonQuery();
{ }
query += $"'{values[i]}'";
if (i != values.Length - 1)
query += ", ";
}
query += ")";
SQLiteCommand command = new SQLiteCommand(query, Connection); /// <summary>
await command.ExecuteNonQueryAsync(); /// Remove every row in a table that has a certain propery
} /// </summary>
/// <param name="tableName">The table name</param>
/// <param name="KeyName">The column name that the search is made by</param>
/// <param name="KeyValue">The value that is searched in the specified column</param>
/// <returns></returns>
public async Task RemoveKeyAsync(string tableName, string KeyName, string KeyValue)
{
var query = $"DELETE FROM {tableName} WHERE {KeyName} = '{KeyValue}'";
public void Insert(string tableName, params string[] values) var command = new SQLiteCommand(query, _connection);
{ await command.ExecuteNonQueryAsync();
}
string query = $"INSERT INTO {tableName} VALUES ("; /// <summary>
for (int i = 0; i < values.Length; i++) /// Remove every row in a table that has a certain propery
{ /// </summary>
query += $"'{values[i]}'"; /// <param name="tableName">The table name</param>
if (i != values.Length - 1) /// <param name="KeyName">The column name that the search is made by</param>
query += ", "; /// <param name="KeyValue">The value that is searched in the specified column</param>
} /// <returns></returns>
query += ")"; public void RemoveKey(string tableName, string KeyName, string KeyValue)
{
var query = $"DELETE FROM {tableName} WHERE {KeyName} = '{KeyValue}'";
SQLiteCommand command = new SQLiteCommand(query, Connection); var command = new SQLiteCommand(query, _connection);
command.ExecuteNonQuery(); command.ExecuteNonQuery();
}
} /// <summary>
/// Check if the key exists in the table
/// </summary>
/// <param name="tableName">The table name</param>
/// <param name="keyName">The column that the search is made by</param>
/// <param name="KeyValue">The value that is searched in the specified column</param>
/// <returns></returns>
public async Task<bool> KeyExistsAsync(string tableName, string keyName, string KeyValue)
{
var query = $"SELECT * FROM {tableName} where {keyName} = '{KeyValue}'";
public async Task RemoveKeyAsync(string tableName, string KeyName, string KeyValue) if (await ReadDataAsync(query) is not null)
{
string query = $"DELETE FROM {tableName} WHERE {KeyName} = '{KeyValue}'";
SQLiteCommand command = new SQLiteCommand(query, Connection);
await command.ExecuteNonQueryAsync();
}
public void RemoveKey(string tableName, string KeyName, string KeyValue)
{
string query = $"DELETE FROM {tableName} WHERE {KeyName} = '{KeyValue}'";
SQLiteCommand command = new SQLiteCommand(query, Connection);
command.ExecuteNonQuery();
}
public async Task<bool> KeyExistsAsync(string tableName, string keyName, string KeyValue)
{
string query = $"SELECT * FROM {tableName} where {keyName} = '{KeyValue}'";
if (await ReadDataAsync(query) is not null)
return true;
return false;
}
public bool KeyExists(string tableName, string keyName, string KeyValue)
{
string query = $"SELECT * FROM {tableName} where {keyName} = '{KeyValue}'";
if (ReadData(query) is not null)
return true;
return false;
}
public async Task SetValueAsync(string tableName, string keyName, string KeyValue, string ResultColumnName, string ResultColumnValue)
{
if (!await TableExistsAsync(tableName))
throw new System.Exception($"Table {tableName} does not exist");
await ExecuteAsync($"UPDATE {tableName} SET {ResultColumnName}='{ResultColumnValue}' WHERE {keyName}='{KeyValue}'");
}
public void SetValue(string tableName, string keyName, string KeyValue, string ResultColumnName, string ResultColumnValue)
{
if (!TableExists(tableName))
throw new System.Exception($"Table {tableName} does not exist");
Execute($"UPDATE {tableName} SET {ResultColumnName}='{ResultColumnValue}' WHERE {keyName}='{KeyValue}'");
}
public async Task<string> GetValueAsync(string tableName, string keyName, string KeyValue, string ResultColumnName)
{
if (!await TableExistsAsync(tableName))
throw new System.Exception($"Table {tableName} does not exist");
return await ReadDataAsync($"SELECT {ResultColumnName} FROM {tableName} WHERE {keyName}='{KeyValue}'");
}
public string GetValue(string tableName, string keyName, string KeyValue, string ResultColumnName)
{
if (!TableExists(tableName))
throw new System.Exception($"Table {tableName} does not exist");
return ReadData($"SELECT {ResultColumnName} FROM {tableName} WHERE {keyName}='{KeyValue}'");
}
public async void Stop()
{
await Connection.CloseAsync();
}
public async Task AddColumnsToTableAsync(string tableName, string[] columns)
{
var command = Connection.CreateCommand();
command.CommandText = $"SELECT * FROM {tableName}";
var reader = await command.ExecuteReaderAsync();
var tableColumns = new List<string>();
for (int i = 0; i < reader.FieldCount; i++)
tableColumns.Add(reader.GetName(i));
foreach (var column in columns)
{
if (!tableColumns.Contains(column))
{
command.CommandText = $"ALTER TABLE {tableName} ADD COLUMN {column} TEXT";
await command.ExecuteNonQueryAsync();
}
}
}
public void AddColumnsToTable(string tableName, string[] columns)
{
var command = Connection.CreateCommand();
command.CommandText = $"SELECT * FROM {tableName}";
var reader = command.ExecuteReader();
var tableColumns = new List<string>();
for (int i = 0; i < reader.FieldCount; i++)
tableColumns.Add(reader.GetName(i));
foreach (var column in columns)
{
if (!tableColumns.Contains(column))
{
command.CommandText = $"ALTER TABLE {tableName} ADD COLUMN {column} TEXT";
command.ExecuteNonQuery();
}
}
}
public async Task<bool> TableExistsAsync(string tableName)
{
var cmd = Connection.CreateCommand();
cmd.CommandText = $"SELECT name FROM sqlite_master WHERE type='table' AND name='{tableName}'";
var result = await cmd.ExecuteScalarAsync();
if (result == null)
return false;
return true; return true;
}
public bool TableExists(string tableName) return false;
{ }
var cmd = Connection.CreateCommand(); /// <summary>
cmd.CommandText = $"SELECT name FROM sqlite_master WHERE type='table' AND name='{tableName}'"; /// Check if the key exists in the table
var result = cmd.ExecuteScalar(); /// </summary>
/// <param name="tableName">The table name</param>
/// <param name="keyName">The column that the search is made by</param>
/// <param name="KeyValue">The value that is searched in the specified column</param>
/// <returns></returns>
public bool KeyExists(string tableName, string keyName, string KeyValue)
{
var query = $"SELECT * FROM {tableName} where {keyName} = '{KeyValue}'";
if (result == null) if (ReadData(query) is not null)
return false;
return true; return true;
}
public async Task CreateTableAsync(string tableName, params string[] columns) return false;
{ }
var cmd = Connection.CreateCommand(); /// <summary>
cmd.CommandText = $"CREATE TABLE IF NOT EXISTS {tableName} ({string.Join(", ", columns)})"; /// Set value of a column in a table
await cmd.ExecuteNonQueryAsync(); /// </summary>
/// <param name="tableName">The table name</param>
/// <param name="keyName">The column that the search is made by</param>
/// <param name="KeyValue">The value that is searched in the column specified</param>
/// <param name="ResultColumnName">The column that has to be modified</param>
/// <param name="ResultColumnValue">The new value that will replace the old value from the column specified</param>
public async Task SetValueAsync(
string tableName, string keyName, string KeyValue, string ResultColumnName,
string ResultColumnValue)
{
if (!await TableExistsAsync(tableName))
throw new Exception($"Table {tableName} does not exist");
} await ExecuteAsync(
$"UPDATE {tableName} SET {ResultColumnName}='{ResultColumnValue}' WHERE {keyName}='{KeyValue}'"
);
}
public void CreateTable(string tableName, params string[] columns) /// <summary>
{ /// Set value of a column in a table
/// </summary>
/// <param name="tableName">The table name</param>
/// <param name="keyName">The column that the search is made by</param>
/// <param name="KeyValue">The value that is searched in the column specified</param>
/// <param name="ResultColumnName">The column that has to be modified</param>
/// <param name="ResultColumnValue">The new value that will replace the old value from the column specified</param>
public void SetValue(
string tableName, string keyName, string KeyValue, string ResultColumnName,
string ResultColumnValue)
{
if (!TableExists(tableName))
throw new Exception($"Table {tableName} does not exist");
var cmd = Connection.CreateCommand(); Execute($"UPDATE {tableName} SET {ResultColumnName}='{ResultColumnValue}' WHERE {keyName}='{KeyValue}'");
cmd.CommandText = $"CREATE TABLE IF NOT EXISTS {tableName} ({string.Join(", ", columns)})"; }
cmd.ExecuteNonQuery();
} /// <summary>
/// Get value from a column in a table
/// </summary>
/// <param name="tableName">The table name</param>
/// <param name="keyName">The column that the search is made by</param>
/// <param name="KeyValue">The value that is searched in the specified column</param>
/// <param name="ResultColumnName">The column that has the result</param>
/// <returns>A string that has the requested value (can be null if nothing found)</returns>
public async Task<string?> GetValueAsync(
string tableName, string keyName, string KeyValue,
string ResultColumnName)
{
if (!await TableExistsAsync(tableName))
throw new Exception($"Table {tableName} does not exist");
public async Task<int> ExecuteAsync(string query) return await ReadDataAsync($"SELECT {ResultColumnName} FROM {tableName} WHERE {keyName}='{KeyValue}'");
{ }
if (!Connection.State.HasFlag(System.Data.ConnectionState.Open))
await Connection.OpenAsync();
var command = new SQLiteCommand(query, Connection);
int answer = await command.ExecuteNonQueryAsync();
return answer;
}
public int Execute(string query) /// <summary>
{ /// Get value from a column in a table
if (!Connection.State.HasFlag(System.Data.ConnectionState.Open)) /// </summary>
Connection.Open(); /// <param name="tableName">The table name</param>
var command = new SQLiteCommand(query, Connection); /// <param name="keyName">The column that the search is made by</param>
int r = command.ExecuteNonQuery(); /// <param name="KeyValue">The value that is searched in the specified column</param>
/// <param name="ResultColumnName">The column that has the result</param>
/// <returns>A string that has the requested value (can be null if nothing found)</returns>
public string? GetValue(string tableName, string keyName, string KeyValue, string ResultColumnName)
{
if (!TableExists(tableName))
throw new Exception($"Table {tableName} does not exist");
return r; return ReadData($"SELECT {ResultColumnName} FROM {tableName} WHERE {keyName}='{KeyValue}'");
} }
public async Task<string> ReadDataAsync(string query) /// <summary>
{ /// Stop the connection to the SQL Database
if (!Connection.State.HasFlag(System.Data.ConnectionState.Open)) /// </summary>
await Connection.OpenAsync(); /// <returns></returns>
var command = new SQLiteCommand(query, Connection); public async void Stop()
var reader = await command.ExecuteReaderAsync(); {
await _connection.CloseAsync();
}
object[] values = new object[reader.FieldCount]; /// <summary>
if (reader.Read()) /// Change the structure of a table by adding new columns
/// </summary>
/// <param name="tableName">The table name</param>
/// <param name="columns">The columns to be added</param>
/// <param name="TYPE">The type of the columns (TEXT, INTEGER, FLOAT, etc)</param>
/// <returns></returns>
public async Task AddColumnsToTableAsync(string tableName, string[] columns, string TYPE = "TEXT")
{
var command = _connection.CreateCommand();
command.CommandText = $"SELECT * FROM {tableName}";
var reader = await command.ExecuteReaderAsync();
var tableColumns = new List<string>();
for (var i = 0; i < reader.FieldCount; i++)
tableColumns.Add(reader.GetName(i));
foreach (var column in columns)
if (!tableColumns.Contains(column))
{ {
reader.GetValues(values); command.CommandText = $"ALTER TABLE {tableName} ADD COLUMN {column} {TYPE}";
return string.Join<object>(" ", values); await command.ExecuteNonQueryAsync();
} }
return null; }
}
public string ReadData(string query) /// <summary>
{ /// Change the structure of a table by adding new columns
if (!Connection.State.HasFlag(System.Data.ConnectionState.Open)) /// </summary>
Connection.Open(); /// <param name="tableName">The table name</param>
var command = new SQLiteCommand(query, Connection); /// <param name="columns">The columns to be added</param>
var reader = command.ExecuteReader(); /// <param name="TYPE">The type of the columns (TEXT, INTEGER, FLOAT, etc)</param>
/// <returns></returns>
public void AddColumnsToTable(string tableName, string[] columns, string TYPE = "TEXT")
{
var command = _connection.CreateCommand();
command.CommandText = $"SELECT * FROM {tableName}";
var reader = command.ExecuteReader();
var tableColumns = new List<string>();
for (var i = 0; i < reader.FieldCount; i++)
tableColumns.Add(reader.GetName(i));
object[] values = new object[reader.FieldCount]; foreach (var column in columns)
if (reader.Read()) if (!tableColumns.Contains(column))
{ {
reader.GetValues(values); command.CommandText = $"ALTER TABLE {tableName} ADD COLUMN {column} {TYPE}";
return string.Join<object>(" ", values); command.ExecuteNonQuery();
} }
return null; }
}
public async Task<object[]> ReadDataArrayAsync(string query) /// <summary>
/// Check if a table exists
/// </summary>
/// <param name="tableName">The table name</param>
/// <returns>True if the table exists, false if not</returns>
public async Task<bool> TableExistsAsync(string tableName)
{
var cmd = _connection.CreateCommand();
cmd.CommandText = $"SELECT name FROM sqlite_master WHERE type='table' AND name='{tableName}'";
var result = await cmd.ExecuteScalarAsync();
if (result == null)
return false;
return true;
}
/// <summary>
/// Check if a table exists
/// </summary>
/// <param name="tableName">The table name</param>
/// <returns>True if the table exists, false if not</returns>
public bool TableExists(string tableName)
{
var cmd = _connection.CreateCommand();
cmd.CommandText = $"SELECT name FROM sqlite_master WHERE type='table' AND name='{tableName}'";
var result = cmd.ExecuteScalar();
if (result == null)
return false;
return true;
}
/// <summary>
/// Create a table
/// </summary>
/// <param name="tableName">The table name</param>
/// <param name="columns">The columns of the table</param>
/// <returns></returns>
public async Task CreateTableAsync(string tableName, params string[] columns)
{
var cmd = _connection.CreateCommand();
cmd.CommandText = $"CREATE TABLE IF NOT EXISTS {tableName} ({string.Join(", ", columns)})";
await cmd.ExecuteNonQueryAsync();
}
/// <summary>
/// Create a table
/// </summary>
/// <param name="tableName">The table name</param>
/// <param name="columns">The columns of the table</param>
/// <returns></returns>
public void CreateTable(string tableName, params string[] columns)
{
var cmd = _connection.CreateCommand();
cmd.CommandText = $"CREATE TABLE IF NOT EXISTS {tableName} ({string.Join(", ", columns)})";
cmd.ExecuteNonQuery();
}
/// <summary>
/// Execute a custom query
/// </summary>
/// <param name="query">The query</param>
/// <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);
var answer = await command.ExecuteNonQueryAsync();
return answer;
}
/// <summary>
/// Execute a custom query
/// </summary>
/// <param name="query">The query</param>
/// <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);
var r = command.ExecuteNonQuery();
return r;
}
/// <summary>
/// Read data from the result table and return the first row
/// </summary>
/// <param name="query">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)
{
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];
if (reader.Read())
{ {
if (!Connection.State.HasFlag(System.Data.ConnectionState.Open)) reader.GetValues(values);
await Connection.OpenAsync(); return string.Join<object>(" ", values);
var command = new SQLiteCommand(query, Connection);
var reader = await command.ExecuteReaderAsync();
object[] values = new object[reader.FieldCount];
if (reader.Read())
{
reader.GetValues(values);
return values;
}
return null;
} }
public object[] ReadDataArray(string query) return null;
}
/// <summary>
/// Read data from the result table and return the first row
/// </summary>
/// <param name="query">The query</param>
/// <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);
var reader = command.ExecuteReader();
var values = new object[reader.FieldCount];
if (reader.Read())
{ {
if (!Connection.State.HasFlag(System.Data.ConnectionState.Open)) reader.GetValues(values);
Connection.Open(); return string.Join<object>(" ", values);
var command = new SQLiteCommand(query, Connection);
var reader = command.ExecuteReader();
object[] values = new object[reader.FieldCount];
if (reader.Read())
{
reader.GetValues(values);
return values;
}
return null;
} }
return null;
}
/// <summary>
/// Read data from the result table and return the first row
/// </summary>
/// <param name="query">The query</param>
/// <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);
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>
/// <param name="query">The query</param>
/// <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);
var reader = command.ExecuteReader();
var values = new object[reader.FieldCount];
if (reader.Read())
{
reader.GetValues(values);
return values;
}
return null;
}
/// <summary>
/// Read all rows from the result table and return them as a list of string arrays. The string arrays contain the
/// values of each row
/// </summary>
/// <param name="query">The query</param>
/// <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);
var reader = await command.ExecuteReaderAsync();
if (!reader.HasRows)
return null;
List<string[]> rows = new();
while (await reader.ReadAsync())
{
var values = new string[reader.FieldCount];
reader.GetValues(values);
rows.Add(values);
}
if (rows.Count == 0) return null;
return rows;
}
/// <summary>
/// Create a parameter for a query
/// </summary>
/// <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)
{
var parameter = new SQLiteParameter(name);
parameter.Value = value;
if (value is string)
parameter.DbType = DbType.String;
else if (value is int)
parameter.DbType = DbType.Int32;
else if (value is long)
parameter.DbType = DbType.Int64;
else if (value is float)
parameter.DbType = DbType.Single;
else if (value is double)
parameter.DbType = DbType.Double;
else if (value is bool)
parameter.DbType = DbType.Boolean;
else if (value is DateTime)
parameter.DbType = DbType.DateTime;
else if (value is byte[])
parameter.DbType = DbType.Binary;
else if (value is Guid)
parameter.DbType = DbType.Guid;
else if (value is decimal)
parameter.DbType = DbType.Decimal;
else if (value is TimeSpan)
parameter.DbType = DbType.Time;
else if (value is DateTimeOffset)
parameter.DbType = DbType.DateTimeOffset;
else if (value is ushort)
parameter.DbType = DbType.UInt16;
else if (value is uint)
parameter.DbType = DbType.UInt32;
else if (value is ulong)
parameter.DbType = DbType.UInt64;
else if (value is sbyte)
parameter.DbType = DbType.SByte;
else if (value is short)
parameter.DbType = DbType.Int16;
else if (value is byte)
parameter.DbType = DbType.Byte;
else if (value is char)
parameter.DbType = DbType.StringFixedLength;
else if (value is char[])
parameter.DbType = DbType.StringFixedLength;
else
return null;
return parameter;
}
/// <summary>
/// Create a parameter for a query. The function automatically detects the type of the value.
/// </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)
{
return CreateParameter(parameterValues.Key, parameterValues.Value);
}
/// <summary>
/// Execute a query with parameters
/// </summary>
/// <param name="query">The query to execute</param>
/// <param name="parameters">The parameters of the query</param>
/// <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();
var command = new SQLiteCommand(query, _connection);
foreach (var parameter in parameters)
{
var p = CreateParameter(parameter);
if (p is not null)
command.Parameters.Add(p);
}
return await command.ExecuteNonQueryAsync();
}
/// <summary>
/// Execute a query with parameters that returns a specific type of object. The function will return the first row of the result transformed into the specified type.
/// </summary>
/// <param name="query">The query to execute</param>
/// <param name="convertor">The convertor function that will convert each row of the response into an object of <typeparamref name="T"/></param>
/// <param name="parameters">The parameters of the query</param>
/// <typeparam name="T">The return object type</typeparam>
/// <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();
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 convertor(values);
}
return default;
}
/// <summary>
/// Execute a query with parameters that returns a specific type of object. The function will return a list of objects of the specified type.
/// </summary>
/// <param name="query">The query to execute</param>
/// <param name="convertor">The convertor from object[] to T</param>
/// <param name="parameters">The parameters of the query</param>
/// <typeparam name="T">The expected object type</typeparam>
/// <returns>A list of objects of type T that represents each line of the output of the specified query, converted to T</returns>
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();
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();
//
if (!reader.HasRows)
return null;
List<T> rows = new();
while (await reader.ReadAsync())
{
var values = new object[reader.FieldCount];
reader.GetValues(values);
rows.Add(convertor(values));
}
return rows;
} }
} }

View File

@@ -1,6 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using PluginManager.Others;
using Discord.Commands;
namespace PluginManager.Interfaces; namespace PluginManager.Interfaces;
@@ -36,16 +35,16 @@ public interface DBCommand
/// <summary> /// <summary>
/// The main body of the command. This is what is executed when user calls the command in Server /// The main body of the command. This is what is executed when user calls the command in Server
/// </summary> /// </summary>
/// <param name="context">The disocrd Context</param> /// <param name="args">The disocrd Context</param>
void ExecuteServer(SocketCommandContext context) void ExecuteServer(DbCommandExecutingArguments args)
{ {
} }
/// <summary> /// <summary>
/// The main body of the command. This is what is executed when user calls the command in DM /// The main body of the command. This is what is executed when user calls the command in DM
/// </summary> /// </summary>
/// <param name="context">The disocrd Context</param> /// <param name="args">The disocrd Context</param>
void ExecuteDM(SocketCommandContext context) void ExecuteDM(DbCommandExecutingArguments args)
{ {
} }
} }

View File

@@ -1,25 +1,24 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks;
using Discord; using Discord;
using Discord.WebSocket; using Discord.WebSocket;
namespace PluginManager.Interfaces namespace PluginManager.Interfaces;
public interface DBSlashCommand
{ {
public interface DBSlashCommand string Name { get; }
{ string Description { get; }
string Name { get; } bool canUseDM { get; }
string Description { get; } bool HasInteraction { get; }
bool canUseDM { get; } List<SlashCommandOptionBuilder> Options { get; }
List<SlashCommandOptionBuilder> Options { get; } void ExecuteServer(SocketSlashCommand context)
{ }
void ExecuteServer(SocketSlashCommand context) void ExecuteDM(SocketSlashCommand context) { }
{
} Task ExecuteInteraction(SocketInteraction interaction) => Task.CompletedTask;
void ExecuteDM(SocketSlashCommand context) { }
}
} }

View File

@@ -0,0 +1,22 @@
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using PluginManager.Others;
using PluginManager.Others.Actions;
namespace PluginManager.Interfaces;
public interface ICommandAction
{
public string ActionName { get; }
public string? Description { get; }
public string? Usage { get; }
public IEnumerable<InternalActionOption> ListOfOptions { get; }
public InternalActionRunType RunType { get; }
public Task Execute(string[]? args);
}

View File

@@ -0,0 +1,14 @@
using System;
using PluginManager.Others;
namespace PluginManager.Interfaces.Logger;
internal interface ILog
{
string Message { get; set; }
Type? Source { get; set; }
LogType Type { get; set; }
DateTime ThrowTime { get; set; }
}

View File

@@ -0,0 +1,19 @@
using System;
using System.Threading.Tasks;
using PluginManager.Others;
using PluginManager.Others.Logger;
namespace PluginManager.Interfaces.Logger;
internal interface ILogger
{
bool IsEnabled { get; init; }
bool OutputToFile { get; init; }
string OutputFile { get; init; }
event EventHandler<Log> OnLog;
void Log(
string message = "", Type? source = default, LogType type = LogType.INFO,
DateTime throwTime = default);
}

View File

@@ -0,0 +1,87 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PluginManager.Interfaces.Updater
{
public class AppVersion : IVersion
{
public int Major { get; set; }
public int Minor { get; set; }
public int Patch { get; set; }
public int PatchVersion { get; set; }
public static readonly AppVersion CurrentAppVersion = new AppVersion(Config.AppSettings["Version"]);
private readonly char _Separator = '.';
public AppVersion(string versionAsString)
{
string[] versionParts = versionAsString.Split(_Separator);
if (versionParts.Length != 4)
{
throw new ArgumentException("Invalid version string");
}
Major = int.Parse(versionParts[0]);
Minor = int.Parse(versionParts[1]);
Patch = int.Parse(versionParts[2]);
PatchVersion = int.Parse(versionParts[3]);
}
public bool IsNewerThan(IVersion version)
{
if (Major > version.Major)
return true;
if (Major == version.Major && Minor > version.Minor)
return true;
if (Major == version.Major && Minor == version.Minor && Patch > version.Patch)
return true;
if (Major == version.Major && Minor == version.Minor && Patch == version.Patch && PatchVersion > version.PatchVersion)
return true;
return false;
}
public bool IsOlderThan(IVersion version)
{
if (Major < version.Major)
return true;
if (Major == version.Major && Minor < version.Minor)
return true;
if (Major == version.Major && Minor == version.Minor && Patch < version.Patch)
return true;
if (Major == version.Major && Minor == version.Minor && Patch == version.Patch && PatchVersion < version.PatchVersion)
return true;
return false;
}
public bool IsEqualTo(IVersion version)
{
return Major == version.Major && Minor == version.Minor && Patch == version.Patch && PatchVersion == version.PatchVersion;
}
public string ToShortString()
{
return $"{Major}.{Minor}.{Patch}.{PatchVersion}";
}
public override string ToString()
{
return ToShortString();
}
}
}

View File

@@ -0,0 +1,17 @@
namespace PluginManager.Interfaces.Updater;
public interface IVersion
{
public int Major { get; }
public int Minor { get; }
public int Patch { get; }
public int PatchVersion => 0;
public bool IsNewerThan(IVersion version);
public bool IsOlderThan(IVersion version);
public bool IsEqualTo(IVersion version);
public string ToShortString();
}

View File

@@ -0,0 +1,76 @@
using System;
namespace PluginManager.Interfaces.Updater;
public abstract class Version: IVersion
{
public int Major { get; }
public int Minor { get; }
public int Patch { get; }
protected readonly char _Separator = '.';
protected Version(int major, int minor, int patch)
{
Major = major;
Minor = minor;
Patch = patch;
}
protected Version(string versionAsString)
{
string[] versionParts = versionAsString.Split(_Separator);
if (versionParts.Length != 3)
{
throw new ArgumentException("Invalid version string");
}
Major = int.Parse(versionParts[0]);
Minor = int.Parse(versionParts[1]);
Patch = int.Parse(versionParts[2]);
}
public bool IsNewerThan(IVersion version)
{
if (Major > version.Major)
return true;
if (Major == version.Major && Minor > version.Minor)
return true;
if (Major == version.Major && Minor == version.Minor && Patch > version.Patch)
return true;
return false;
}
public bool IsOlderThan(IVersion version)
{
if (Major < version.Major)
return true;
if (Major == version.Major && Minor < version.Minor)
return true;
if (Major == version.Major && Minor == version.Minor && Patch < version.Patch)
return true;
return false;
}
public bool IsEqualTo(IVersion version)
{
return Major == version.Major && Minor == version.Minor && Patch == version.Patch;
}
public string ToShortString()
{
return $"{Major}.{Minor}.{Patch}";
}
public override string ToString()
{
return ToShortString();
}
}

View File

@@ -1,50 +0,0 @@
using System;
using System.Collections.Generic;
using Discord.WebSocket;
namespace PluginManager.Items;
public class Command
{
/// <summary>
/// The author of the command
/// </summary>
public SocketUser? Author;
/// <summary>
/// The Command class contructor
/// </summary>
/// <param name="message">The message that was sent</param>
public Command(SocketMessage message)
{
Author = message.Author;
var data = message.Content.Split(' ');
Arguments = data.Length > 1 ? new List<string>(string.Join(' ', data, 1, data.Length - 1).Split(' ')) : new List<string>();
CommandName = data[0].Substring(1);
PrefixUsed = data[0][0];
}
/// <summary>
/// The list of arguments
/// </summary>
public List<string> Arguments { get; }
/// <summary>
/// The command that is executed
/// </summary>
public string CommandName { get; }
/// <summary>
/// The prefix that is used for the command
/// </summary>
public char PrefixUsed { get; }
}
public class ConsoleCommand
{
public string CommandName { get; init; }
public string Description { get; init; }
public string Usage { get; init; }
public Action<string[]> Action { get; init; }
}

View File

@@ -1,506 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Threading.Tasks;
using Discord.WebSocket;
using PluginManager.Interfaces;
using PluginManager.Loaders;
using PluginManager.Online;
using PluginManager.Others;
using OperatingSystem = PluginManager.Others.OperatingSystem;
namespace PluginManager.Items;
public class ConsoleCommandsHandler
{
private static readonly PluginsManager manager =
new("https://raw.githubusercontent.com/Wizzy69/installer/discord-bot-files/Plugins.txt");
private static readonly List<ConsoleCommand> commandList = new();
private static bool isDownloading;
private static bool pluginsLoaded;
private readonly DiscordSocketClient? client;
public ConsoleCommandsHandler(DiscordSocketClient client)
{
this.client = client;
InitializeBasicCommands();
//Settings.Variables.outputStream.WriteLine("Initialized console command handler !");
}
private void InitializeBasicCommands()
{
commandList.Clear();
AddCommand("help", "Show help", "help <command>", args =>
{
if (args.Length <= 1)
{
Settings.Variables.outputStream.WriteLine("Available commands:");
var items = new List<string[]>();
items.Add(new[] { "-", "-", "-" });
items.Add(new[] { "Command", "Description", "Usage" });
items.Add(new[] { " ", " ", "Argument type: <optional> [required]" });
items.Add(new[] { "-", "-", "-" });
foreach (var command in commandList)
{
var pa = from p in command.Action.Method.GetParameters()
where p.Name != null
select p.ParameterType.FullName;
items.Add(new[] { command.CommandName, command.Description, command.Usage });
}
items.Add(new[] { "-", "-", "-" });
Utilities.FormatAndAlignTable(items, TableFormat.DEFAULT);
}
else
{
foreach (var command in commandList)
if (command.CommandName == args[1])
{
Settings.Variables.outputStream.WriteLine("Command description: " + command.Description);
Settings.Variables.outputStream.WriteLine("Command execution format:" + command.Usage);
return;
}
Settings.Variables.outputStream.WriteLine("Command not found");
}
}
);
AddCommand("lp", "Load plugins", () =>
{
if (pluginsLoaded)
return;
var loader = new PluginLoader(client!);
var cc = Console.ForegroundColor;
loader.onCMDLoad += (name, typeName, success, exception) =>
{
if (name == null || name.Length < 2)
name = typeName;
if (success)
{
Console.ForegroundColor = ConsoleColor.Green;
Settings.Variables.outputStream.WriteLine("[CMD] Successfully loaded command : " + name);
}
else
{
Console.ForegroundColor = ConsoleColor.Red;
if (exception is null)
Settings.Variables.outputStream.WriteLine("An error occured while loading: " + name);
else
Settings.Variables.outputStream.WriteLine("[CMD] Failed to load command : " + name + " because " + exception!.Message);
}
Console.ForegroundColor = cc;
};
loader.onEVELoad += (name, typeName, success, exception) =>
{
if (name == null || name.Length < 2)
name = typeName;
if (success)
{
Console.ForegroundColor = ConsoleColor.Green;
Settings.Variables.outputStream.WriteLine("[EVENT] Successfully loaded event : " + name);
}
else
{
Console.ForegroundColor = ConsoleColor.Red;
Settings.Variables.outputStream.WriteLine("[EVENT] Failed to load event : " + name + " because " + exception!.Message);
}
Console.ForegroundColor = cc;
};
loader.onSLSHLoad += (name, typeName, success, exception) =>
{
if (name == null || name.Length < 2)
name = typeName;
if (success)
{
Console.ForegroundColor = ConsoleColor.Green;
Settings.Variables.outputStream.WriteLine("[SLASH] Successfully loaded command : " + name);
}
else
{
Console.ForegroundColor = ConsoleColor.Red;
Settings.Variables.outputStream.WriteLine("[SLASH] Failed to load command : " + name + " because " + exception!.Message);
}
Console.ForegroundColor = cc;
};
loader.LoadPlugins();
Console.ForegroundColor = cc;
pluginsLoaded = true;
}
);
AddCommand("listplugs", "list available plugins", () => { manager.ListAvailablePlugins().Wait(); });
AddCommand("dwplug", "download plugin", "dwplug [name]", async args =>
{
isDownloading = true;
if (args.Length == 1)
{
isDownloading = false;
Settings.Variables.outputStream.WriteLine("Please specify plugin name");
return;
}
var name = string.Join(' ', args, 1, args.Length - 1);
// info[0] = plugin type
// info[1] = plugin link
// info[2] = if others are required, or string.Empty if none
var info = await manager.GetPluginLinkByName(name);
if (info[1] == null) // link is null
{
if (name == "")
{
isDownloading = false;
Utilities.WriteColorText("Name is invalid");
return;
}
isDownloading = false;
Utilities.WriteColorText($"Failed to find plugin &b{name} &c!" +
" Use &glistplugs &ccommand to display all available plugins !");
return;
}
string path;
if (info[0] == "Plugin")
path = "./Data/Plugins/" + name + ".dll";
else
path = $"./{info[1].Split('/')[info[1].Split('/').Length - 1]}";
if (OperatingSystem.WINDOWS == Functions.GetOperatingSystem())
{
await ServerCom.DownloadFileAsync(info[1], path);
}
else if (OperatingSystem.LINUX == Functions.GetOperatingSystem())
{
var bar = new Utilities.ProgressBar(ProgressBarType.NO_END);
bar.Start();
await ServerCom.DownloadFileNoProgressAsync(info[1], path);
bar.Stop("Plugin Downloaded !");
}
Settings.Variables.outputStream.WriteLine("\n");
// check requirements if any
if (info.Length == 3 && info[2] != string.Empty && info[2] != null)
{
Settings.Variables.outputStream.WriteLine($"Downloading requirements for plugin : {name}");
var lines = await ServerCom.ReadTextFromURL(info[2]);
foreach (var line in lines)
{
if (!(line.Length > 0 && line.Contains(",")))
continue;
var split = line.Split(',');
Settings.Variables.outputStream.WriteLine($"\nDownloading item: {split[1]}");
if (File.Exists("./" + split[1])) File.Delete("./" + split[1]);
if (OperatingSystem.WINDOWS == Functions.GetOperatingSystem())
{
await ServerCom.DownloadFileAsync(split[0], "./" + split[1]);
}
else if (OperatingSystem.LINUX == Functions.GetOperatingSystem())
{
var bar = new Utilities.ProgressBar(ProgressBarType.NO_END);
bar.Start();
await ServerCom.DownloadFileNoProgressAsync(split[0], "./" + split[1]);
bar.Stop("Item downloaded !");
}
Settings.Variables.outputStream.WriteLine();
if (split[0].EndsWith(".pak"))
{
File.Move("./" + split[1], "./Data/PAKS/" + split[1], true);
}
else if (split[0].EndsWith(".zip") || split[0].EndsWith(".pkg"))
{
Settings.Variables.outputStream.WriteLine($"Extracting {split[1]} ...");
var bar = new Utilities.ProgressBar(
ProgressBarType.NO_END);
bar.Start();
await Functions.ExtractArchive("./" + split[1], "./", null,
UnzipProgressType.PercentageFromTotalSize);
bar.Stop("Extracted");
Settings.Variables.outputStream.WriteLine("\n");
File.Delete("./" + split[1]);
}
}
Settings.Variables.outputStream.WriteLine();
}
var ver = await ServerCom.GetVersionOfPackageFromWeb(name);
if (ver is null) throw new Exception("Incorrect version");
await Config.Plugins.SetVersionAsync(name, ver);
isDownloading = false;
}
);
AddCommand("value", "read value from VariableStack", "value [key]", args =>
{
if (args.Length != 2)
return;
if (!Config.Variables.Exists(args[1]))
return;
var data = Config.Variables.GetValue(args[1]);
Settings.Variables.outputStream.WriteLine($"{args[1]} => {data}");
}
);
AddCommand("add", "add variable to the system variables", "add [key] [value] [isReadOnly=true/false]", args =>
{
if (args.Length < 4)
return;
var key = args[1];
var value = args[2];
var isReadOnly = args[3].Equals("true", StringComparison.CurrentCultureIgnoreCase);
try
{
Config.Variables.Add(key, value, isReadOnly);
Settings.Variables.outputStream.WriteLine($"Updated config file with the following command: {args[1]} => {value}");
}
catch (Exception ex)
{
Settings.Variables.outputStream.WriteLine(ex.ToString());
}
}
);
AddCommand("remv", "remove variable from system variables", "remv [key]", args =>
{
if (args.Length < 2)
return;
Config.Variables.RemoveKey(args[1]);
}
);
AddCommand("sd", "Shuts down the discord bot", async () =>
{
if (client is null)
return;
var bar = new Utilities.ProgressBar(ProgressBarType.NO_END);
bar.Start();
bar.Stop("Saved config !");
Settings.Variables.outputStream.WriteLine();
Settings.sqlDatabase.Stop();
await client.StopAsync();
await client.DisposeAsync();
await Task.Delay(1000);
Environment.Exit(0);
}
);
AddCommand("import", "Load an external command", "import [pluginName]", async args =>
{
if (args.Length <= 1) return;
try
{
var pName = string.Join(' ', args, 1, args.Length - 1);
var client = new HttpClient();
var url = (await manager.GetPluginLinkByName(pName))[1];
if (url is null) throw new Exception($"Invalid plugin name {pName}.");
var s = await client.GetStreamAsync(url);
var str = new MemoryStream();
await s.CopyToAsync(str);
var asmb = Assembly.Load(str.ToArray());
var types = asmb.GetTypes();
foreach (var type in types)
if (type.IsClass && typeof(DBEvent).IsAssignableFrom(type))
{
var instance = (DBEvent)Activator.CreateInstance(type);
instance.Start(this.client);
Settings.Variables.outputStream.WriteLine($"[EVENT] Loaded external {type.FullName}!");
}
else if (type.IsClass && typeof(DBCommand).IsAssignableFrom(type))
{
var instance = (DBCommand)Activator.CreateInstance(type);
Settings.Variables.outputStream.WriteLine($"[CMD] Instance: {type.FullName} loaded !");
}
}
catch (Exception ex)
{
Settings.Variables.outputStream.WriteLine(ex.Message);
}
});
AddCommand("remplug", "Remove a plugin", "remplug [plugName]", async args =>
{
if (args.Length <= 1) return;
isDownloading = true;
var plugName = string.Join(' ', args, 1, args.Length - 1);
if (pluginsLoaded)
{
if (Functions.GetOperatingSystem() == OperatingSystem.WINDOWS)
{
Process.Start("DiscordBot.exe", $"/remplug {plugName}");
await Task.Delay(100);
Environment.Exit(0);
}
else
{
Process.Start("./DiscordBot", $"/remplug {plugName}");
await Task.Delay(100);
Environment.Exit(0);
}
isDownloading = false;
return;
}
var location = $"./Data/Plugins/{plugName}.dll";
if (!File.Exists(location))
{
Settings.Variables.outputStream.WriteLine("The plugin does not exist");
return;
}
File.Delete(location);
Settings.Variables.outputStream.WriteLine("Removed the plugin DLL. Checking for other files ...");
var info = await manager.GetPluginLinkByName(plugName);
if (info[2] != string.Empty)
{
var lines = await ServerCom.ReadTextFromURL(info[2]);
foreach (var line in lines)
{
if (!(line.Length > 0 && line.Contains(",")))
continue;
var split = line.Split(',');
if (File.Exists("./" + split[1]))
File.Delete("./" + split[1]);
Settings.Variables.outputStream.WriteLine("Removed: " + split[1]);
}
if (Directory.Exists($"./Data/Plugins/{plugName}"))
Directory.Delete($"./Data/Plugins/{plugName}", true);
if (Directory.Exists(plugName))
Directory.Delete(plugName, true);
}
isDownloading = false;
Settings.Variables.outputStream.WriteLine(plugName + " has been successfully deleted !");
});
AddCommand("reload", "Reload the bot with all plugins", () =>
{
if (Functions.GetOperatingSystem() == OperatingSystem.WINDOWS)
{
Process.Start("DiscordBot.exe", "lp");
HandleCommand("sd");
}
else
{
Process.Start("./DiscordBot", "lp");
HandleCommand("sd");
}
});
//AddCommand("");
//Sort the commands by name
commandList.Sort((x, y) => x.CommandName.CompareTo(y.CommandName));
}
public static void AddCommand(string command, string description, string usage, Action<string[]> action)
{
commandList.Add(new ConsoleCommand
{ CommandName = command, Description = description, Action = action, Usage = usage });
Console.ForegroundColor = ConsoleColor.White;
Utilities.WriteColorText($"Command &r{command} &cadded to the list of commands");
}
public static void AddCommand(string command, string description, Action action)
{
AddCommand(command, description, command, args => action());
}
public static void RemoveCommand(string command)
{
commandList.RemoveAll(x => x.CommandName == command);
}
public static bool CommandExists(string command)
{
return GetCommand(command) is not null;
}
public static ConsoleCommand? GetCommand(string command)
{
return commandList.FirstOrDefault(t => t.CommandName == command);
}
public static async Task ExecuteCommad(string command)
{
var args = command.Split(' ');
foreach (var item in commandList.ToList())
if (item.CommandName == args[0])
{
item.Action.Invoke(args);
while (isDownloading) await Task.Delay(1000);
}
}
public bool HandleCommand(string command, bool removeCommandExecution = true)
{
Console.ForegroundColor = ConsoleColor.White;
var args = command.Split(' ');
foreach (var item in commandList.ToList())
if (item.CommandName == args[0])
{
if (removeCommandExecution)
{
Console.SetCursorPosition(0, Console.CursorTop - 1);
for (var i = 0; i < command.Length + 30; i++)
Settings.Variables.outputStream.Write(" ");
Console.SetCursorPosition(0, Console.CursorTop);
}
Settings.Variables.outputStream.WriteLine();
item.Action(args);
return true;
}
return false;
//Settings.Variables.outputStream.WriteLine($"Executing: {args[0]} with the following parameters: {args.MergeStrings(1)}");
}
}

View File

@@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using PluginManager.Interfaces;
using PluginManager.Others;
namespace PluginManager.Loaders;
public class ActionsLoader
{
public delegate void ActionLoaded(string name, string typeName, bool success, Exception? e = null);
private readonly string _actionExtension = "dll";
private readonly string _actionFolder = @"./Data/Plugins/";
public ActionsLoader(string path, string extension)
{
_actionFolder = path;
_actionExtension = extension;
}
public event ActionLoaded? ActionLoadedEvent;
public async Task<List<ICommandAction>?> Load()
{
Directory.CreateDirectory(_actionFolder);
var files = Directory.GetFiles(_actionFolder, $"*.{_actionExtension}", SearchOption.AllDirectories);
var actions = new List<ICommandAction>();
foreach (var file in files)
try
{
Assembly.LoadFrom(file);
}
catch (Exception e)
{
ActionLoadedEvent?.Invoke(file, "", false, e);
}
var types = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(s => s.GetTypes())
.Where(p => typeof(ICommandAction).IsAssignableFrom(p) && !p.IsInterface);
foreach (var type in types)
try
{
var action = (ICommandAction)Activator.CreateInstance(type);
if (action.ActionName == null)
{
ActionLoadedEvent?.Invoke(action.ActionName, type.Name, false);
continue;
}
if (action.RunType == InternalActionRunType.ON_STARTUP)
await action.Execute(null);
ActionLoadedEvent?.Invoke(action.ActionName, type.Name, true);
actions.Add(action);
}
catch (Exception e)
{
ActionLoadedEvent?.Invoke(type.Name, type.Name, false, e);
}
return actions;
}
}

View File

@@ -0,0 +1,21 @@
namespace PluginManager.Loaders;
public class FileLoaderResult
{
public string PluginName { get; private set; }
public string ErrorMessage { get; private set; }
public FileLoaderResult(string pluginName, string errorMessage)
{
PluginName = pluginName;
ErrorMessage = errorMessage;
}
public FileLoaderResult(string pluginName)
{
PluginName = pluginName;
ErrorMessage = string.Empty;
}
}

View File

@@ -1,116 +1,89 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks;
using PluginManager.Interfaces;
using PluginManager.Others; using PluginManager.Others;
namespace PluginManager.Loaders; namespace PluginManager.Loaders;
internal class LoaderArgs : EventArgs internal class Loader
{ {
internal string? PluginName { get; init; } private readonly string _SearchPath;
internal string? TypeName { get; init; } private readonly string _FileExtension;
internal bool IsLoaded { get; init; }
internal Exception? Exception { get; init; }
internal object? Plugin { get; init; }
}
internal class Loader<T> internal delegate void FileLoadedHandler(FileLoaderResult result);
{
internal Loader(string path, string extension) internal delegate void PluginLoadedHandler(PluginLoadResultData result);
internal event FileLoadedHandler? OnFileLoadedException;
internal event PluginLoadedHandler? OnPluginLoaded;
internal Loader(string searchPath, string fileExtension)
{ {
this.path = path; _SearchPath = searchPath;
this.extension = extension; _FileExtension = fileExtension;
} }
internal async Task Load()
private string path { get; }
private string extension { get; }
internal event FileLoadedEventHandler? FileLoaded;
internal event PluginLoadedEventHandler? PluginLoaded;
internal List<T>? Load()
{ {
var list = new List<T>(); if (!Directory.Exists(_SearchPath))
if (!Directory.Exists(path))
{ {
Directory.CreateDirectory(path); Directory.CreateDirectory(_SearchPath);
return null; return;
} }
var files = Directory.GetFiles(path, $"*.{extension}", SearchOption.AllDirectories); var files = Directory.GetFiles(_SearchPath, $"*.{_FileExtension}", SearchOption.TopDirectoryOnly);
foreach (var file in files) foreach (var file in files)
{ {
Assembly.LoadFrom(file); try
if (FileLoaded != null)
{ {
var args = new LoaderArgs Assembly.LoadFrom(file);
{ }
Exception = null, catch
TypeName = nameof(T), {
IsLoaded = false, OnFileLoadedException?.Invoke(new FileLoaderResult(file, $"Failed to load file {file}"));
PluginName = new FileInfo(file).Name.Split('.')[0],
Plugin = null
};
FileLoaded.Invoke(args);
} }
} }
try await LoadEverythingOfType<DBEvent>();
{ await LoadEverythingOfType<DBCommand>();
var interfaceType = typeof(T); await LoadEverythingOfType<DBSlashCommand>();
var types = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(a => a.GetTypes())
.Where(p => interfaceType.IsAssignableFrom(p) && p.IsClass)
.ToArray();
list.Clear();
foreach (var type in types)
try
{
var plugin = (T)Activator.CreateInstance(type)!;
list.Add(plugin);
if (PluginLoaded != null)
PluginLoaded.Invoke(new LoaderArgs
{
Exception = null,
IsLoaded = true,
PluginName = type.FullName,
TypeName = nameof(T),
Plugin = plugin
}
);
}
catch (Exception ex)
{
if (PluginLoaded != null)
PluginLoaded.Invoke(new LoaderArgs
{
Exception = ex,
IsLoaded = false,
PluginName = type.FullName,
TypeName = nameof(T)
});
}
}
catch (Exception ex)
{
Functions.WriteErrFile(ex.ToString());
}
return list;
} }
private async Task LoadEverythingOfType<T>()
{
var types = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(s => s.GetTypes())
.Where(p => typeof(T).IsAssignableFrom(p) && !p.IsInterface);
internal delegate void FileLoadedEventHandler(LoaderArgs args); foreach (var type in types)
{
try
{
var plugin = (T?)Activator.CreateInstance(type);
if (plugin is null)
{
throw new Exception($"Failed to create instance of plugin with type {type.FullName} [{type.Assembly}]");
}
var pluginType = plugin switch
{
DBEvent => PluginType.EVENT,
DBCommand => PluginType.COMMAND,
DBSlashCommand => PluginType.SLASH_COMMAND,
_ => PluginType.UNKNOWN
};
OnPluginLoaded?.Invoke(new PluginLoadResultData(type.FullName, pluginType, true, plugin: plugin));
}
catch (Exception ex)
{
OnPluginLoaded?.Invoke(new PluginLoadResultData(type.FullName, PluginType.UNKNOWN, false, ex.Message));
}
}
}
internal delegate void PluginLoadedEventHandler(LoaderArgs args);
} }

View File

@@ -1,127 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using PluginManager.Interfaces;
using PluginManager.Others;
namespace PluginManager.Loaders
{
internal class LoaderV2
{
internal LoaderV2(string path, string extension)
{
this.path = path;
this.extension = extension;
}
private string path { get; }
private string extension { get; }
internal event FileLoadedEventHandler? FileLoaded;
internal event PluginLoadedEventHandler? PluginLoaded;
internal delegate void FileLoadedEventHandler(LoaderArgs args);
internal delegate void PluginLoadedEventHandler(LoaderArgs args);
internal (List<DBEvent>?, List<DBCommand>?, List<DBSlashCommand>?) Load()
{
List<DBEvent> events = new();
List<DBSlashCommand> slashCommands = new();
List<DBCommand> commands = new();
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
return (null, null, null);
}
var files = Directory.GetFiles(path, $"*.{extension}", SearchOption.AllDirectories);
foreach (var file in files)
{
Assembly.LoadFrom(file);
if (FileLoaded != null)
{
var args = new LoaderArgs
{
Exception = null,
TypeName = null,
IsLoaded = false,
PluginName = new FileInfo(file).Name.Split('.')[0],
Plugin = null
};
FileLoaded.Invoke(args);
}
}
return (LoadItems<DBEvent>(), LoadItems<DBCommand>(), LoadItems<DBSlashCommand>());
}
internal List<T> LoadItems<T>()
{
List<T> list = new();
try
{
var interfaceType = typeof(T);
var types = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(a => a.GetTypes())
.Where(p => interfaceType.IsAssignableFrom(p) && p.IsClass)
.ToArray();
list.Clear();
foreach (var type in types)
try
{
var plugin = (T)Activator.CreateInstance(type)!;
list.Add(plugin);
if (PluginLoaded != null)
PluginLoaded.Invoke(new LoaderArgs
{
Exception = null,
IsLoaded = true,
PluginName = type.FullName,
TypeName = typeof(T) == typeof(DBCommand) ? "DBCommand" : typeof(T) == typeof(DBEvent) ? "DBEvent" : "DBSlashCommand",
Plugin = plugin
}
);
}
catch (Exception ex)
{
if (PluginLoaded != null)
PluginLoaded.Invoke(new LoaderArgs
{
Exception = ex,
IsLoaded = false,
PluginName = type.FullName,
TypeName = nameof(T)
});
}
return list;
}
catch (Exception ex)
{
Functions.WriteErrFile(ex.ToString());
return null;
}
return null;
}
}
}

View File

@@ -0,0 +1,23 @@
using PluginManager.Others;
namespace PluginManager.Loaders;
public class PluginLoadResultData
{
public string PluginName { get; init; }
public PluginType PluginType { get; init; }
public string? ErrorMessage { get; init; }
public bool IsSuccess { get; init; }
public object Plugin { get; init; }
public PluginLoadResultData(string pluginName, PluginType pluginType, bool isSuccess, string? errorMessage = null,
object? plugin = null)
{
PluginName = pluginName;
PluginType = pluginType;
IsSuccess = isSuccess;
ErrorMessage = errorMessage;
Plugin = plugin is null ? new() : plugin;
}
}

View File

@@ -1,150 +1,84 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Discord;
using Discord.WebSocket; using Discord.WebSocket;
using PluginManager.Interfaces; using PluginManager.Interfaces;
using PluginManager.Online;
using PluginManager.Online.Updates;
using PluginManager.Others; using PluginManager.Others;
namespace PluginManager.Loaders; namespace PluginManager.Loaders;
public class PluginLoader public class PluginLoader
{ {
public delegate void CMDLoaded(string name, string typeName, bool success, Exception? e = null); internal readonly DiscordSocketClient _Client;
public delegate void EVELoaded(string name, string typeName, bool success, Exception? e = null); public delegate void CommandLoaded(PluginLoadResultData resultData);
public delegate void SLSHLoaded(string name, string tyypename, bool success, Exception? e = null); public delegate void EventLoaded(PluginLoadResultData resultData);
private const string pluginFolder = @"./Data/Plugins/"; public delegate void SlashCommandLoaded(PluginLoadResultData resultData);
internal const string pluginExtension = "dll"; public CommandLoaded? OnCommandLoaded;
private readonly DiscordSocketClient _client; public EventLoaded? OnEventLoaded;
public SlashCommandLoaded? OnSlashCommandLoaded;
/// <summary> public static List<DBCommand> Commands { get; private set; } = new List<DBCommand>();
/// Event that is fired when a <see cref="DBCommand" /> is successfully loaded into commands list public static List<DBEvent> Events { get; private set; } = new List<DBEvent>();
/// </summary> public static List<DBSlashCommand> SlashCommands { get; private set; } = new List<DBSlashCommand>();
public CMDLoaded? onCMDLoad;
/// <summary>
/// Event that is fired when a <see cref="DBEvent" /> is successfully loaded into events list
/// </summary>
public EVELoaded? onEVELoad;
/// <summary>
/// Event that is fired when a <see cref="DBEvent" /> is successfully loaded into events list
/// </summary>
public SLSHLoaded? onSLSHLoad;
/// <summary>
/// The Plugin Loader constructor
/// </summary>
/// <param name="discordSocketClient">The discord bot client where the plugins will pe attached to</param>
public PluginLoader(DiscordSocketClient discordSocketClient) public PluginLoader(DiscordSocketClient discordSocketClient)
{ {
_client = discordSocketClient; _Client = discordSocketClient;
} }
public async Task LoadPlugins()
/// <summary>
/// A list of <see cref="DBCommand" /> commands
/// </summary>
public static List<DBCommand>? Commands { get; set; }
/// <summary>
/// A list of <see cref="DBEvent" /> commands
/// </summary>
public static List<DBEvent>? Events { get; set; }
/// <summary>
/// A list of <see cref="DBSlashCommand"/> commands
/// </summary>
public static List<DBSlashCommand>? SlashCommands { get; set; }
/// <summary>
/// The main mathod that is called to load all events
/// </summary>
public async void LoadPlugins()
{ {
//Check for updates in commands Config.Logger.Log("Loading plugins...", typeof(PluginLoader));
foreach (var file in Directory.GetFiles("./Data/Plugins/", $"*.{pluginExtension}",
SearchOption.AllDirectories))
await Task.Run(async () =>
{
var name = new FileInfo(file).Name.Split('.')[0];
var version = await ServerCom.GetVersionOfPackageFromWeb(name);
if (version is null)
return;
if (Config.Plugins.GetVersion(name) is not null)
Config.Plugins.SetVersion(name, version);
if (await PluginUpdater.CheckForUpdates(name)) var loader = new Loader(Config.AppSettings["PluginFolder"], "dll");
await PluginUpdater.Download(name);
});
//await this.ResetSlashCommands();
//Load all plugins loader.OnFileLoadedException += FileLoadedException;
loader.OnPluginLoaded += OnPluginLoaded;
Commands = new List<DBCommand>(); await loader.Load();
Events = new List<DBEvent>();
SlashCommands = new List<DBSlashCommand>();
Functions.WriteLogFile("Starting plugin loader ... Client: " + _client.CurrentUser.Username);
Settings.Variables.outputStream.WriteLine("Loading plugins");
var loader = new LoaderV2("./Data/Plugins", "dll");
loader.FileLoaded += (args) => Functions.WriteLogFile($"{args.PluginName} file Loaded");
loader.PluginLoaded += Loader_PluginLoaded;
var res = loader.Load();
Events = res.Item1;
Commands = res.Item2;
SlashCommands = res.Item3;
} }
private async void Loader_PluginLoaded(LoaderArgs args) private void FileLoadedException(FileLoaderResult result)
{ {
// Settings.Variables.outputStream.WriteLine(args.TypeName); Config.Logger.Log(result.ErrorMessage, typeof(PluginLoader), LogType.ERROR);
switch (args.TypeName) }
private async void OnPluginLoaded(PluginLoadResultData result)
{
switch (result.PluginType)
{ {
case "DBCommand": case PluginType.COMMAND:
onCMDLoad?.Invoke(((DBCommand)args.Plugin!).Command, args.TypeName!, args.IsLoaded, args.Exception); Commands.Add((DBCommand)result.Plugin);
OnCommandLoaded?.Invoke(result);
break; break;
case "DBEvent": case PluginType.EVENT:
try if (this.TryStartEvent((DBEvent)result.Plugin))
{ {
if (args.IsLoaded) Events.Add((DBEvent)result.Plugin);
((DBEvent)args.Plugin!).Start(_client); OnEventLoaded?.Invoke(result);
}
onEVELoad?.Invoke(((DBEvent)args.Plugin!).Name, args.TypeName!, args.IsLoaded, args.Exception);
}
catch (Exception ex)
{
Settings.Variables.outputStream.WriteLine(ex.ToString());
Settings.Variables.outputStream.WriteLine("Plugin: " + args.PluginName);
Settings.Variables.outputStream.WriteLine("Type: " + args.TypeName);
Settings.Variables.outputStream.WriteLine("IsLoaded: " + args.IsLoaded);
}
break; break;
case "DBSlashCommand": case PluginType.SLASH_COMMAND:
if (args.IsLoaded) if (await this.TryStartSlashCommand((DBSlashCommand)result.Plugin))
{ {
var slash = (DBSlashCommand)args.Plugin; if(((DBSlashCommand)result.Plugin).HasInteraction)
SlashCommandBuilder builder = new SlashCommandBuilder(); _Client.InteractionCreated += ((DBSlashCommand)result.Plugin).ExecuteInteraction;
builder.WithName(slash.Name); SlashCommands.Add((DBSlashCommand)result.Plugin);
builder.WithDescription(slash.Description); OnSlashCommandLoaded?.Invoke(result);
builder.WithDMPermission(slash.canUseDM);
builder.Options = slash.Options;
//Settings.Variables.outputStream.WriteLine("Loaded " + slash.Name);
onSLSHLoad?.Invoke(((DBSlashCommand)args.Plugin!).Name, args.TypeName, args.IsLoaded, args.Exception);
await _client.CreateGlobalApplicationCommandAsync(builder.Build());
} }
else
Config.Logger.Log($"Failed to start slash command {result.PluginName}", typeof(PluginLoader), LogType.ERROR);
break;
case PluginType.UNKNOWN:
default:
Config.Logger.Log("Unknown plugin type", typeof(PluginLoader), LogType.ERROR);
break; break;
} }
} }

View File

@@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Discord;
using Discord.WebSocket;
using PluginManager.Interfaces;
using PluginManager.Others;
namespace PluginManager.Loaders;
internal static class PluginLoaderExtensions
{
internal static bool TryStartEvent(this PluginLoader pluginLoader, DBEvent? dbEvent)
{
try
{
if (dbEvent is null)
{
throw new ArgumentNullException(nameof(dbEvent));
}
dbEvent.Start(pluginLoader._Client);
return true;
}
catch (Exception e)
{
Config.Logger.Log($"Error starting event {dbEvent.Name}: {e.Message}", typeof(PluginLoader), LogType.ERROR);
return false;
}
}
internal static async Task ResetSlashCommands(this PluginLoader pluginLoader)
{
await pluginLoader._Client.Rest.DeleteAllGlobalCommandsAsync();
if(pluginLoader._Client.Guilds.Count == 0) return;
if (!ulong.TryParse(Config.ServerID, out _))
{
Config.Logger.Log("Invalid ServerID in config file. Can not reset specific guild commands", typeof(PluginLoader), LogType.ERROR);
return;
}
SocketGuild? guild = pluginLoader._Client.GetGuild(ulong.Parse(Config.ServerID));
if(guild is null)
{
Config.Logger.Log("Failed to get guild with ID " + Config.ServerID, typeof(PluginLoader), LogType.ERROR);
return;
}
await guild.DeleteApplicationCommandsAsync();
Config.Logger.Log($"Cleared all slash commands from guild {guild.Id}", typeof(PluginLoader));
}
internal static async Task<bool> TryStartSlashCommand(this PluginLoader pluginLoader, DBSlashCommand? dbSlashCommand)
{
try
{
if (dbSlashCommand is null)
{
//throw new ArgumentNullException(nameof(dbSlashCommand));
return false;
}
if (pluginLoader._Client.Guilds.Count == 0) return false;
var builder = new SlashCommandBuilder();
builder.WithName(dbSlashCommand.Name);
builder.WithDescription(dbSlashCommand.Description);
builder.WithDMPermission(dbSlashCommand.canUseDM);
builder.Options = dbSlashCommand.Options;
if (uint.TryParse(Config.ServerID, out uint result))
{
SocketGuild? guild = pluginLoader._Client.GetGuild(result);
if (guild is null)
{
Config.Logger.Log("Failed to get guild with ID " + Config.ServerID, typeof(PluginLoader), LogType.ERROR);
return false;
}
await guild.CreateApplicationCommandAsync(builder.Build());
}else await pluginLoader._Client.CreateGlobalApplicationCommandAsync(builder.Build());
return true;
}
catch (Exception e)
{
Config.Logger.Log($"Error starting slash command {dbSlashCommand.Name}: {e.Message}", typeof(PluginLoader), LogType.ERROR);
return false;
}
}
}

View File

@@ -18,10 +18,11 @@ internal static class OnlineFunctions
/// <param name="progress">The <see cref="IProgress{T}" /> that is used to track the download progress</param> /// <param name="progress">The <see cref="IProgress{T}" /> that is used to track the download progress</param>
/// <param name="cancellation">The cancellation token</param> /// <param name="cancellation">The cancellation token</param>
/// <returns></returns> /// <returns></returns>
internal static async Task DownloadFileAsync(this HttpClient client, string url, Stream destination, internal static async Task DownloadFileAsync(
IProgress<float>? progress = null, this HttpClient client, string url, Stream destination,
IProgress<long>? downloadedBytes = null, int bufferSize = 81920, IProgress<float>? progress = null,
CancellationToken cancellation = default) IProgress<long>? downloadedBytes = null, int bufferSize = 81920,
CancellationToken cancellation = default)
{ {
using (var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellation)) using (var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellation))
{ {
@@ -34,20 +35,25 @@ internal static class OnlineFunctions
if (progress == null || !contentLength.HasValue) if (progress == null || !contentLength.HasValue)
{ {
await download.CopyToAsync(destination, cancellation); await download.CopyToAsync(destination, cancellation);
if (!contentLength.HasValue)
progress?.Report(100f);
return; return;
} }
// Convert absolute progress (bytes downloaded) into relative progress (0% - 100%) // Convert absolute progress (bytes downloaded) into relative progress (0% - 100%)
var relativeProgress = new Progress<long>(totalBytes => // total ... 100%
// downloaded ... x%
// x = downloaded * 100 / total => x = downloaded / total * 100
var relativeProgress = new Progress<long>(totalBytesDownloaded =>
{ {
progress.Report((float)totalBytes / contentLength.Value * 100); progress?.Report(totalBytesDownloaded / (float)contentLength.Value * 100);
downloadedBytes?.Report(totalBytes); downloadedBytes?.Report(totalBytesDownloaded);
} }
); );
// Use extension method to report progress while downloading // Use extension method to report progress while downloading
await download.CopyToOtherStreamAsync(destination, bufferSize, relativeProgress, cancellation); await download.CopyToOtherStreamAsync(destination, bufferSize, relativeProgress, cancellation);
progress.Report(1); progress.Report(100f);
} }
} }
} }

View File

@@ -0,0 +1,20 @@
using System.Text.Json.Serialization;
using PluginManager.Interfaces.Updater;
namespace PluginManager.Online.Helpers;
public class PluginVersion: Version
{
[JsonConstructor]
public PluginVersion(int major, int minor, int patch): base(major, minor, patch)
{
}
public PluginVersion(string versionAsString): base(versionAsString)
{
}
public override string ToString()
{
return ToShortString();
}
}

View File

@@ -1,84 +0,0 @@
using System;
namespace PluginManager.Online.Helpers;
public class VersionString
{
public int PackageCheckVersion;
public int PackageMainVersion;
public int PackageVersionID;
public VersionString(string version)
{
var data = version.Split('.');
try
{
PackageVersionID = int.Parse(data[0]);
PackageMainVersion = int.Parse(data[1]);
PackageCheckVersion = int.Parse(data[2]);
}
catch (Exception ex)
{
Console.WriteLine(version);
throw new Exception("Failed to write Version", ex);
}
}
public override string ToString()
{
return "{PackageID: " + PackageVersionID + ", PackageVersion: " + PackageMainVersion +
", PackageCheckVersion: " + PackageCheckVersion + "}";
}
public string ToShortString()
{
if (PackageVersionID == 0 && PackageCheckVersion == 0 && PackageMainVersion == 0)
return "Unknown";
return $"{PackageVersionID}.{PackageMainVersion}.{PackageCheckVersion}";
}
#region operators
public static bool operator >(VersionString s1, VersionString s2)
{
if (s1.PackageVersionID > s2.PackageVersionID) return true;
if (s1.PackageVersionID == s2.PackageVersionID)
{
if (s1.PackageMainVersion > s2.PackageMainVersion) return true;
if (s1.PackageMainVersion == s2.PackageMainVersion &&
s1.PackageCheckVersion > s2.PackageCheckVersion) return true;
}
return false;
}
public static bool operator <(VersionString s1, VersionString s2)
{
return !(s1 > s2) && s1 != s2;
}
public static bool operator ==(VersionString s1, VersionString s2)
{
if (s1.PackageVersionID == s2.PackageVersionID && s1.PackageMainVersion == s2.PackageMainVersion &&
s1.PackageCheckVersion == s2.PackageCheckVersion) return true;
return false;
}
public static bool operator !=(VersionString s1, VersionString s2)
{
return !(s1 == s2);
}
public static bool operator <=(VersionString s1, VersionString s2)
{
return s1 < s2 || s1 == s2;
}
public static bool operator >=(VersionString s1, VersionString s2)
{
return s1 > s2 || s1 == s2;
}
#endregion
}

View File

@@ -1,125 +1,172 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using PluginManager.Online.Helpers;
using PluginManager.Others; using PluginManager.Others;
using PluginManager.Plugin;
using OperatingSystem = PluginManager.Others.OperatingSystem; using PluginManager.Updater.Plugins;
namespace PluginManager.Online; namespace PluginManager.Online;
public class PluginsManager public class PluginsManager
{ {
/// <summary> private static readonly string _DefaultBranch = "releases";
/// The Plugin Manager constructor private static readonly string _DefaultBaseUrl = "https://raw.githubusercontent.com/andreitdr/SethPlugins";
/// </summary>
/// <param name="link">The link to the file where all plugins are stored</param> private static readonly string _DefaultPluginsLink = "PluginsList.json";
public PluginsManager(string link)
public string Branch { get; init; }
public string BaseUrl { get; init; }
private string PluginsLink => $"{BaseUrl}/{Branch}/{_DefaultPluginsLink}";
public PluginsManager(Uri baseUrl, string branch)
{ {
PluginsLink = link; BaseUrl = baseUrl.ToString();
Branch = branch;
} }
/// <summary> public PluginsManager(string branch)
/// The URL of the server
/// </summary>
public string PluginsLink { get; }
/// <summary>
/// The method to load all plugins
/// </summary>
/// <returns></returns>
public async Task ListAvailablePlugins()
{ {
try BaseUrl = _DefaultBaseUrl;
{ Branch = branch;
var list = await ServerCom.ReadTextFromURL(PluginsLink);
var lines = list.ToArray();
var data = new List<string[]>();
var op = Functions.GetOperatingSystem();
var len = lines.Length;
string[] titles = { "Name", "Description", "Type", "Version" };
data.Add(new[] { "-", "-", "-", "-" });
data.Add(titles);
data.Add(new[] { "-", "-", "-", "-" });
for (var i = 0; i < len; i++)
{
if (lines[i].Length <= 2)
continue;
var content = lines[i].Split(',');
var display = new string[titles.Length];
if (op == OperatingSystem.WINDOWS)
{
if (content[4].Contains("Windows"))
{
display[0] = content[0];
display[1] = content[1];
display[2] = content[2];
display[3] =
(await ServerCom.GetVersionOfPackageFromWeb(content[0]) ?? new VersionString("0.0.0"))
.ToShortString();
data.Add(display);
}
}
else if (op == OperatingSystem.LINUX)
{
if (content[4].Contains("Linux"))
{
display[0] = content[0];
display[1] = content[1];
display[2] = content[2];
display[3] =
(await ServerCom.GetVersionOfPackageFromWeb(content[0]) ?? new VersionString("0.0.0"))
.ToShortString();
data.Add(display);
}
}
}
data.Add(new[] { "-", "-", "-", "-" });
Utilities.FormatAndAlignTable(data, TableFormat.CENTER_EACH_COLUMN_BASED);
}
catch (Exception exception)
{
Settings.Variables.outputStream.WriteLine("Failed to execute command: listplugs\nReason: " + exception.Message);
Functions.WriteErrFile(exception.ToString());
}
} }
/// <summary> public PluginsManager()
/// The method to get plugin information by its name
/// </summary>
/// <param name="name">The plugin name</param>
/// <returns></returns>
public async Task<string[]> GetPluginLinkByName(string name)
{ {
try BaseUrl = _DefaultBaseUrl;
Branch = _DefaultBranch;
}
public async Task<List<PluginOnlineInfo>?> GetPluginsList()
{
var jsonText = await ServerCom.GetAllTextFromUrl(PluginsLink);
List<PluginOnlineInfo> result = await JsonManager.ConvertFromJson<List<PluginOnlineInfo>>(jsonText);
var currentOS = OperatingSystem.IsWindows() ? OSType.WINDOWS :
OperatingSystem.IsLinux() ? OSType.LINUX :
OperatingSystem.IsMacOS() ? OSType.MACOSX : OSType.NONE;
return result.FindAll(pl => (pl.SupportedOS & currentOS) != 0);
}
public async Task<PluginOnlineInfo?> GetPluginDataByName(string pluginName)
{
List<PluginOnlineInfo>? plugins = await GetPluginsList();
var result = plugins?.Find(p => p.Name == pluginName);
return result;
}
public async Task RemovePluginFromDatabase(string pluginName)
{
List<PluginInfo> installedPlugins = await JsonManager.ConvertFromJson<List<PluginInfo>>(await File.ReadAllTextAsync(Config.PluginDatabase));
installedPlugins.RemoveAll(p => p.PluginName == pluginName);
await JsonManager.SaveToJsonFile( Config.PluginDatabase,installedPlugins);
}
public async Task AppendPluginToDatabase(PluginInfo pluginData)
{
List<PluginInfo> installedPlugins = await JsonManager.ConvertFromJson<List<PluginInfo>>(await File.ReadAllTextAsync(Config.PluginDatabase));
installedPlugins.Add(pluginData);
await JsonManager.SaveToJsonFile( Config.PluginDatabase, installedPlugins);
}
public async Task<List<PluginInfo>> GetInstalledPlugins()
{
return await JsonManager.ConvertFromJson<List<PluginInfo>>(await File.ReadAllTextAsync(Config.PluginDatabase));
}
public async Task<bool> IsPluginInstalled(string pluginName)
{
List<PluginInfo> installedPlugins = await JsonManager.ConvertFromJson<List<PluginInfo>>(await File.ReadAllTextAsync(Config.PluginDatabase));
return installedPlugins.Any(plugin => plugin.PluginName == pluginName);
}
public async Task CheckForUpdates()
{
var pluginUpdater = new PluginUpdater(this);
List<PluginInfo> installedPlugins = await GetInstalledPlugins();
foreach (var plugin in installedPlugins)
{ {
var list = await ServerCom.ReadTextFromURL(PluginsLink); if (await pluginUpdater.HasUpdate(plugin.PluginName))
var lines = list.ToArray();
var len = lines.Length;
for (var i = 0; i < len; i++)
{ {
var contents = lines[i].Split(','); Config.Logger.Log("Updating plugin: " + plugin.PluginName, typeof(PluginsManager), LogType.INFO);
if (contents[0] == name) await pluginUpdater.UpdatePlugin(plugin.PluginName);
{
if (contents.Length == 6)
return new[] { contents[2], contents[3], contents[5] };
if (contents.Length == 5)
return new[] { contents[2], contents[3], string.Empty };
throw new Exception("Failed to download plugin. Invalid Argument Length");
}
} }
} }
catch (Exception exception)
{
Settings.Variables.outputStream.WriteLine("Failed to execute command: listplugs\nReason: " + exception.Message);
Functions.WriteErrFile(exception.ToString());
}
return new string[] { null!, null!, null! };
} }
public async Task<bool> MarkPluginToUninstall(string pluginName)
{
List<PluginInfo> installedPlugins = await GetInstalledPlugins();
PluginInfo? info = installedPlugins.Find(info => info.PluginName == pluginName);
if(info == null)
return false;
await RemovePluginFromDatabase(pluginName);
info.IsMarkedToUninstall = true;
await AppendPluginToDatabase(info);
return true;
}
public async Task UninstallMarkedPlugins()
{
List<PluginInfo> installedPlugins = await GetInstalledPlugins();
foreach(PluginInfo plugin in installedPlugins)
{
if(!plugin.IsMarkedToUninstall) continue;
await UninstallPlugin(plugin);
}
}
private async Task UninstallPlugin(PluginInfo pluginInfo)
{
File.Delete(pluginInfo.FilePath);
foreach(string dependency in pluginInfo.ListOfDependancies)
File.Delete(dependency);
await RemovePluginFromDatabase(pluginInfo.PluginName);
}
public async Task InstallPlugin(PluginOnlineInfo pluginData, IProgress<float>? installProgress)
{
installProgress?.Report(0f);
int totalSteps = pluginData.HasDependencies ? pluginData.Dependencies.Count + 1 : 1;
float stepProgress = 1f / totalSteps;
float currentProgress = 0f;
IProgress<float> progress = new Progress<float>((p) => {
installProgress?.Report(currentProgress + stepProgress * p);
});
await ServerCom.DownloadFileAsync(pluginData.DownLoadLink, $"{Config.AppSettings["PluginFolder"]}/{pluginData.Name}.dll", progress);
foreach (var dependency in pluginData.Dependencies)
{
await ServerCom.DownloadFileAsync(dependency.DownloadLink, dependency.DownloadLocation, progress);
currentProgress += stepProgress;
}
}
} }

View File

@@ -3,11 +3,8 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using PluginManager.Online.Helpers; using PluginManager.Online.Helpers;
using PluginManager.Others;
namespace PluginManager.Online; namespace PluginManager.Online;
@@ -21,10 +18,21 @@ public static class ServerCom
public static async Task<List<string>> ReadTextFromURL(string link) public static async Task<List<string>> ReadTextFromURL(string link)
{ {
var response = await OnlineFunctions.DownloadStringAsync(link); var response = await OnlineFunctions.DownloadStringAsync(link);
var lines = response.Split('\n'); var lines = response.Split('\n');
return lines.ToList(); return lines.ToList();
} }
/// <summary>
/// Get all text from a file async
/// </summary>
/// <param name="link">The link of the file</param>
/// <returns></returns>
public static async Task<string> GetAllTextFromUrl(string link)
{
var response = await OnlineFunctions.DownloadStringAsync(link);
return response;
}
/// <summary> /// <summary>
/// Download file from url /// Download file from url
/// </summary> /// </summary>
@@ -32,8 +40,9 @@ public static class ServerCom
/// <param name="location">The location where to store the downloaded data</param> /// <param name="location">The location where to store the downloaded data</param>
/// <param name="progress">The <see cref="IProgress{T}" /> to track the download</param> /// <param name="progress">The <see cref="IProgress{T}" /> to track the download</param>
/// <returns></returns> /// <returns></returns>
public static async Task DownloadFileAsync(string URL, string location, IProgress<float> progress, public static async Task DownloadFileAsync(
IProgress<long>? downloadedBytes = null) string URL, string location, IProgress<float>? progress,
IProgress<long>? downloadedBytes)
{ {
using (var client = new HttpClient()) using (var client = new HttpClient())
{ {
@@ -46,69 +55,24 @@ public static class ServerCom
} }
} }
/// <summary> public static async Task DownloadFileAsync(string URl, string location, IProgress<float> progress)
/// Download file from url
/// </summary>
/// <param name="URL">The url to the file</param>
/// <param name="location">The location where to store the downloaded data</param>
/// <returns></returns>
public static async Task DownloadFileAsync(string URL, string location)
{ {
var isDownloading = true; await DownloadFileAsync(URl, location, progress, null);
float c_progress = 0;
var pbar = new Utilities.ProgressBar(ProgressBarType.NORMAL) { Max = 100f, NoColor = true };
IProgress<float> progress = new Progress<float>(percent => { c_progress = percent; });
var updateProgressBarTask = new Task(() =>
{
while (isDownloading)
{
pbar.Update(c_progress);
if (c_progress == 100f)
break;
Thread.Sleep(500);
}
}
);
new Thread(updateProgressBarTask.Start).Start();
await DownloadFileAsync(URL, location, progress);
c_progress = pbar.Max;
pbar.Update(100f);
isDownloading = false;
} }
public static async Task DownloadFileNoProgressAsync(string URL, string location) public static async Task DownloadFileAsync(string url, string location)
{ {
IProgress<float> progress = new Progress<float>(); await DownloadFileAsync(url, location, null, null);
await DownloadFileAsync(URL, location, progress);
} }
public static VersionString? GetVersionOfPackage(string pakName) public static Task CreateDownloadTask(string URl, string location)
{ {
if (Config.Plugins.GetVersion(pakName) is null) return DownloadFileAsync(URl, location, null, null);
return null;
return new VersionString(Config.Plugins.GetVersion(pakName));
} }
public static async Task<VersionString?> GetVersionOfPackageFromWeb(string pakName) public static Task CreateDownloadTask(string URl, string location, IProgress<float> progress)
{ {
var url = "https://raw.githubusercontent.com/Wizzy69/installer/discord-bot-files/Versions"; return DownloadFileAsync(URl, location, progress, null);
var data = await ReadTextFromURL(url);
foreach (var item in data)
{
if (item.StartsWith("#"))
continue;
string[] split = item.Split(',');
if (split[0] == pakName)
return new VersionString(split[1]);
}
return null;
} }
} }

View File

@@ -1,51 +0,0 @@
using System;
using System.Threading.Tasks;
using PluginManager.Items;
using PluginManager.Others;
namespace PluginManager.Online.Updates;
public class PluginUpdater
{
public static async Task<bool> CheckForUpdates(string pakName)
{
try
{
var webV = await ServerCom.GetVersionOfPackageFromWeb(pakName);
var local = ServerCom.GetVersionOfPackage(pakName);
if (local is null) return true;
if (webV is null) return false;
if (webV == local) return false;
if (webV > local) return true;
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
return false;
}
public static async Task<Update> DownloadUpdateInfo(string pakName)
{
var url = "https://raw.githubusercontent.com/Wizzy69/installer/discord-bot-files/Versions";
var info = await ServerCom.ReadTextFromURL(url);
var version = await ServerCom.GetVersionOfPackageFromWeb(pakName);
if (version is null) return Update.Empty;
var update = new Update(pakName, string.Join('\n', info), version);
return update;
}
public static async Task Download(string pakName)
{
Utilities.WriteColorText("An update was found for &g" + pakName + "&c. Version: &r" +
(await ServerCom.GetVersionOfPackageFromWeb(pakName))?.ToShortString() +
"&c. Current Version: &y" +
ServerCom.GetVersionOfPackage(pakName)?.ToShortString());
await ConsoleCommandsHandler.ExecuteCommad("dwplug " + pakName);
}
}

View File

@@ -1,34 +0,0 @@
using System;
using PluginManager.Online.Helpers;
namespace PluginManager.Online.Updates;
public class Update
{
public static Update Empty = new(null, null, null);
private readonly bool isEmpty;
public VersionString newVersion;
public string pakName;
public string UpdateMessage;
public Update(string pakName, string updateMessage, VersionString newVersion)
{
this.pakName = pakName;
UpdateMessage = updateMessage;
this.newVersion = newVersion;
if (pakName is null && updateMessage is null && newVersion is null)
isEmpty = true;
}
public override string ToString()
{
if (isEmpty)
throw new Exception("The update is EMPTY. Can not print information about an empty update !");
return $"Package Name: {pakName}\n" +
$"Update Message: {UpdateMessage}\n" +
$"Version: {newVersion}";
}
}

View File

@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PluginManager.Others.Actions
{
public class InternalActionOption
{
public string OptionName { get; set; }
public string OptionDescription { get; set; }
public InternalActionOption(string optionName, string optionDescription)
{
OptionName = optionName;
OptionDescription = optionDescription;
}
}
}

View File

@@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using PluginManager.Interfaces;
using PluginManager.Loaders;
namespace PluginManager.Others.Actions;
public class InternalActionManager
{
public Dictionary<string, ICommandAction> Actions = new();
private readonly ActionsLoader _loader;
public InternalActionManager(string path, string extension)
{
_loader = new ActionsLoader(path, extension);
}
public async Task Initialize()
{
var loadedActions = await _loader.Load();
if (loadedActions == null)
return;
foreach (var action in loadedActions)
Actions.TryAdd(action.ActionName, action);
}
public async Task Refresh()
{
Actions.Clear();
await Initialize();
}
public async Task<bool> Execute(string actionName, params string[]? args)
{
if (!Actions.ContainsKey(actionName))
{
Config.Logger.Log($"Action {actionName} not found", type: LogType.ERROR, source: typeof(InternalActionManager));
return false;
}
try
{
await Actions[actionName].Execute(args);
return true;
}
catch (Exception e)
{
Config.Logger.Log(e.Message, type: LogType.ERROR, source: typeof(InternalActionManager));
return false;
}
}
}

View File

@@ -0,0 +1,167 @@
using System;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Threading.Tasks;
namespace PluginManager.Others;
public static class ArchiveManager
{
public static void CreateFromFile(string file, string folder)
{
if (!Directory.Exists(folder))
Directory.CreateDirectory(folder);
var archiveName = folder + Path.GetFileNameWithoutExtension(file) + ".zip";
if (File.Exists(archiveName))
File.Delete(archiveName);
using(ZipArchive archive = ZipFile.Open(archiveName, ZipArchiveMode.Create))
{
archive.CreateEntryFromFile(file, Path.GetFileName(file));
}
}
/// <summary>
/// Read a file from a zip archive. The output is a byte array
/// </summary>
/// <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)
{
archName = Config.AppSettings["ArchiveFolder"] + archName;
if (!File.Exists(archName))
throw new Exception("Failed to load file !");
using var zip = ZipFile.OpenRead(archName);
var entry = zip.Entries.FirstOrDefault(entry => entry.FullName == fileName || entry.Name == fileName);
if (entry is null) throw new Exception("File not found in archive");
await using var memoryStream = new MemoryStream();
var stream = entry.Open();
await stream.CopyToAsync(memoryStream);
var data = memoryStream.ToArray();
stream.Close();
memoryStream.Close();
return data;
}
/// <summary>
/// Read data from a file that is inside an archive (ZIP format)
/// </summary>
/// <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)
{
archFile = Config.AppSettings["ArchiveFolder"] + archFile;
if (!File.Exists(archFile))
throw new Exception("Failed to load file !");
try
{
string? textValue = null;
using (var fs = new FileStream(archFile, FileMode.Open))
using (var zip = new ZipArchive(fs, ZipArchiveMode.Read))
{
foreach (var entry in zip.Entries)
if (entry.Name == fileName || entry.FullName == fileName)
using (var s = entry.Open())
using (var reader = new StreamReader(s))
{
textValue = await reader.ReadToEndAsync();
reader.Close();
s.Close();
fs.Close();
}
}
return textValue;
}
catch (Exception ex)
{
Config.Logger.Log(ex.Message, typeof(ArchiveManager), LogType.ERROR); // Write the error to a file
await Task.Delay(100);
return await ReadFromPakAsync(fileName, archFile);
}
}
/// <summary>
/// Extract zip to location
/// </summary>
/// <param name="zip">The zip location</param>
/// <param name="folder">The target location</param>
/// <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(
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)
{
var currentZipFile = 0;
foreach (var entry in archive.Entries)
{
if (entry.FullName.EndsWith("/")) // it is a folder
Directory.CreateDirectory(Path.Combine(folder, entry.FullName));
else
try
{
entry.ExtractToFile(Path.Combine(folder, entry.FullName), true);
}
catch (Exception ex)
{
Config.Logger.Log(ex.Message, typeof(ArchiveManager), LogType.ERROR);
}
currentZipFile++;
await Task.Delay(10);
if (progress != null)
progress.Report((float)currentZipFile / totalZipFiles * 100);
}
}
else if (type == UnzipProgressType.PERCENTAGE_FROM_TOTAL_SIZE)
{
ulong zipSize = 0;
foreach (var entry in archive.Entries)
zipSize += (ulong)entry.CompressedLength;
ulong currentSize = 0;
foreach (var entry in archive.Entries)
{
if (entry.FullName.EndsWith("/"))
{
Directory.CreateDirectory(Path.Combine(folder, entry.FullName));
continue;
}
try
{
entry.ExtractToFile(Path.Combine(folder, entry.FullName), true);
currentSize += (ulong)entry.CompressedLength;
}
catch (Exception ex)
{
Config.Logger.Log(ex.Message, typeof(ArchiveManager), LogType.ERROR);
}
await Task.Delay(10);
if (progress != null)
progress.Report((float)currentSize / zipSize * 100);
}
}
}
}

View File

@@ -1,60 +0,0 @@
using System.Threading.Tasks;
using Discord;
namespace PluginManager.Others;
/// <summary>
/// A class that handles the sending of messages to the user.
/// </summary>
public static class ChannelManagement
{
/// <summary>
/// Get the text channel by name from server
/// </summary>
/// <param name="server">The server</param>
/// <param name="name">The channel name</param>
/// <returns>
/// <see cref="IGuildChannel" />
/// </returns>
public static IGuildChannel GetTextChannel(this IGuild server, string name)
{
return server.GetTextChannel(name);
}
/// <summary>
/// Get the voice channel by name from server
/// </summary>
/// <param name="server">The server</param>
/// <param name="name">The channel name</param>
/// <returns>
/// <see cref="IGuildChannel" />
/// </returns>
public static IGuildChannel GetVoiceChannel(this IGuild server, string name)
{
return server.GetVoiceChannel(name);
}
/// <summary>
/// Get the DM channel between <see cref="Discord.WebSocket.DiscordSocketClient" /> and <see cref="IGuildUser" />
/// </summary>
/// <param name="user"></param>
/// <returns>
/// <see cref="IDMChannel" />
/// </returns>
public static async Task<IDMChannel> GetDMChannel(IGuildUser user)
{
return await user.CreateDMChannelAsync();
}
/// <summary>
/// Get the channel where the message was sent
/// </summary>
/// <param name="message">The message</param>
/// <returns>
/// <see cref="IChannel" />
/// </returns>
public static IChannel GetChannel(IMessage message)
{
return message.Channel;
}
}

View File

@@ -1,365 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace PluginManager.Others;
public static class Utilities
{
private static Dictionary<char, ConsoleColor> Colors = new()
{
{ 'g', ConsoleColor.Green },
{ 'b', ConsoleColor.Blue },
{ 'r', ConsoleColor.Red },
{ 'm', ConsoleColor.Magenta },
{ 'y', ConsoleColor.Yellow }
};
private static char ColorPrefix = '&';
private static bool CanAproximateTo(this float f, float y)
{
return MathF.Abs(f - y) < 0.000001;
}
/// <summary>
/// A way to create a table based on input data
/// </summary>
/// <param name="data">The List of arrays of strings that represent the rows.</param>
public static void FormatAndAlignTable(List<string[]> data, TableFormat format)
{
if (format == TableFormat.CENTER_EACH_COLUMN_BASED)
{
var tableLine = '-';
var tableCross = '+';
var tableWall = '|';
var len = new int[data[0].Length];
foreach (var line in data)
for (var i = 0; i < line.Length; i++)
if (line[i].Length > len[i])
len[i] = line[i].Length;
foreach (var row in data)
{
if (row[0][0] == tableLine)
Settings.Variables.outputStream.Write(tableCross);
else
Settings.Variables.outputStream.Write(tableWall);
for (var l = 0; l < row.Length; l++)
{
if (row[l][0] == tableLine)
{
for (var i = 0; i < len[l] + 4; ++i)
Settings.Variables.outputStream.Write(tableLine);
}
else if (row[l].Length == len[l])
{
Settings.Variables.outputStream.Write(" ");
Settings.Variables.outputStream.Write(row[l]);
Settings.Variables.outputStream.Write(" ");
}
else
{
var lenHalf = row[l].Length / 2;
for (var i = 0; i < (len[l] + 4) / 2 - lenHalf; ++i)
Settings.Variables.outputStream.Write(" ");
Settings.Variables.outputStream.Write(row[l]);
for (var i = (len[l] + 4) / 2 + lenHalf + 1; i < len[l] + 4; ++i)
Settings.Variables.outputStream.Write(" ");
if (row[l].Length % 2 == 0)
Settings.Variables.outputStream.Write(" ");
}
Settings.Variables.outputStream.Write(row[l][0] == tableLine ? tableCross : tableWall);
}
Settings.Variables.outputStream.WriteLine(); //end line
}
return;
}
if (format == TableFormat.CENTER_OVERALL_LENGTH)
{
var maxLen = 0;
foreach (var row in data)
foreach (var s in row)
if (s.Length > maxLen)
maxLen = s.Length;
var div = (maxLen + 4) / 2;
foreach (var row in data)
{
Settings.Variables.outputStream.Write("\t");
if (row[0] == "-")
Settings.Variables.outputStream.Write("+");
else
Settings.Variables.outputStream.Write("|");
foreach (var s in row)
{
if (s == "-")
{
for (var i = 0; i < maxLen + 4; ++i)
Settings.Variables.outputStream.Write("-");
}
else if (s.Length == maxLen)
{
Settings.Variables.outputStream.Write(" ");
Settings.Variables.outputStream.Write(s);
Settings.Variables.outputStream.Write(" ");
}
else
{
var lenHalf = s.Length / 2;
for (var i = 0; i < div - lenHalf; ++i)
Settings.Variables.outputStream.Write(" ");
Settings.Variables.outputStream.Write(s);
for (var i = div + lenHalf + 1; i < maxLen + 4; ++i)
Settings.Variables.outputStream.Write(" ");
if (s.Length % 2 == 0)
Settings.Variables.outputStream.Write(" ");
}
if (s == "-")
Settings.Variables.outputStream.Write("+");
else
Settings.Variables.outputStream.Write("|");
}
Settings.Variables.outputStream.WriteLine(); //end line
}
return;
}
if (format == TableFormat.DEFAULT)
{
var widths = new int[data[0].Length];
var space_between_columns = 3;
for (var i = 0; i < data.Count; i++)
for (var j = 0; j < data[i].Length; j++)
if (data[i][j].Length > widths[j])
widths[j] = data[i][j].Length;
for (var i = 0; i < data.Count; i++)
{
for (var j = 0; j < data[i].Length; j++)
{
if (data[i][j] == "-")
data[i][j] = " ";
Settings.Variables.outputStream.Write(data[i][j]);
for (var k = 0; k < widths[j] - data[i][j].Length + 1 + space_between_columns; k++)
Settings.Variables.outputStream.Write(" ");
}
Settings.Variables.outputStream.WriteLine();
}
return;
}
throw new Exception("Unknown type of table");
}
public static void WriteColorText(string text, bool appendNewLineAtEnd = true)
{
if (Console.Out != Settings.Variables.outputStream)
{
Settings.Variables.outputStream.Write(text);
if (appendNewLineAtEnd)
Settings.Variables.outputStream.WriteLine();
return;
}
var initialForeGround = Console.ForegroundColor;
var input = text.ToCharArray();
for (var i = 0; i < input.Length; i++)
if (input[i] == ColorPrefix)
{
if (i + 1 < input.Length)
{
if (Colors.ContainsKey(input[i + 1]))
{
Console.ForegroundColor = Colors[input[i + 1]];
i++;
}
else if (input[i + 1] == 'c')
{
Console.ForegroundColor = initialForeGround;
i++;
}
}
}
else
{
Settings.Variables.outputStream.Write(input[i]);
}
Console.ForegroundColor = initialForeGround;
if (appendNewLineAtEnd)
Settings.Variables.outputStream.WriteLine();
}
/// <summary>
/// Progress bar object
/// </summary>
public class ProgressBar
{
private readonly int BarLength = 32;
private bool isRunning;
private int position = 1;
private bool positive = true;
public ProgressBar(ProgressBarType type)
{
if (Settings.Variables.outputStream != Console.Out)
throw new Exception("This class (or function) can be used with console only. For UI please use another approach.");
this.type = type;
}
public float Max { get; init; }
public ConsoleColor Color { get; init; }
public bool NoColor { get; init; }
public ProgressBarType type { get; set; }
public int TotalLength { get; private set; }
public async void Start()
{
if (type != ProgressBarType.NO_END)
throw new Exception("Only NO_END progress bar can use this method");
if (isRunning)
throw new Exception("This progress bar is already running");
isRunning = true;
while (isRunning)
{
UpdateNoEnd();
await Task.Delay(100);
}
}
public async void Start(string message)
{
if (type != ProgressBarType.NO_END)
throw new Exception("Only NO_END progress bar can use this method");
if (isRunning)
throw new Exception("This progress bar is already running");
isRunning = true;
TotalLength = message.Length + BarLength + 5;
while (isRunning)
{
UpdateNoEnd(message);
await Task.Delay(100);
}
}
public void Stop()
{
if (type != ProgressBarType.NO_END)
throw new Exception("Only NO_END progress bar can use this method");
if (!isRunning)
throw new Exception("Can not stop a progressbar that did not start");
isRunning = false;
}
public void Stop(string message)
{
Stop();
if (message is not null)
{
Console.CursorLeft = 0;
for (var i = 0; i < BarLength + message.Length + 1; i++)
Settings.Variables.outputStream.Write(" ");
Console.CursorLeft = 0;
Settings.Variables.outputStream.WriteLine(message);
}
}
public void Update(float progress)
{
if (type == ProgressBarType.NO_END)
throw new Exception("This function is for progress bars with end");
UpdateNormal(progress);
}
private void UpdateNoEnd(string message)
{
Console.CursorLeft = 0;
Settings.Variables.outputStream.Write("[");
for (var i = 1; i <= position; i++)
Settings.Variables.outputStream.Write(" ");
Settings.Variables.outputStream.Write("<==()==>");
position += positive ? 1 : -1;
for (var i = position; i <= BarLength - 1 - (positive ? 0 : 2); i++)
Settings.Variables.outputStream.Write(" ");
Settings.Variables.outputStream.Write("] " + message);
if (position == BarLength - 1 || position == 1)
positive = !positive;
}
private void UpdateNoEnd()
{
Console.CursorLeft = 0;
Settings.Variables.outputStream.Write("[");
for (var i = 1; i <= position; i++)
Settings.Variables.outputStream.Write(" ");
Settings.Variables.outputStream.Write("<==()==>");
position += positive ? 1 : -1;
for (var i = position; i <= BarLength - 1 - (positive ? 0 : 2); i++)
Settings.Variables.outputStream.Write(" ");
Settings.Variables.outputStream.Write("]");
if (position == BarLength - 1 || position == 1)
positive = !positive;
}
private void UpdateNormal(float progress)
{
Console.CursorLeft = 0;
Settings.Variables.outputStream.Write("[");
Console.CursorLeft = BarLength;
Settings.Variables.outputStream.Write("]");
Console.CursorLeft = 1;
var onechunk = 30.0f / Max;
var position = 1;
for (var i = 0; i < onechunk * progress; i++)
{
Console.BackgroundColor = NoColor ? ConsoleColor.Black : Color;
Console.CursorLeft = position++;
Settings.Variables.outputStream.Write("#");
}
for (var i = position; i < BarLength; i++)
{
Console.BackgroundColor = NoColor ? ConsoleColor.Black : ConsoleColor.DarkGray;
Console.CursorLeft = position++;
Settings.Variables.outputStream.Write(" ");
}
Console.CursorLeft = BarLength + 4;
Console.BackgroundColor = ConsoleColor.Black;
if (progress.CanAproximateTo(Max))
Settings.Variables.outputStream.Write(progress + " % ✓");
else
Settings.Variables.outputStream.Write(MathF.Round(progress, 2) + " % ");
}
}
}

View File

@@ -0,0 +1,48 @@
using Discord.Commands;
using Discord.WebSocket;
namespace PluginManager.Others;
public class DbCommandExecutingArguments
{
public SocketCommandContext context { get; init; }
public string cleanContent { get; init; }
public string commandUsed { get; init; }
public string[]? arguments { get; init; }
public ISocketMessageChannel Channel => context.Channel;
public DbCommandExecutingArguments(
SocketCommandContext context, string cleanContent, string commandUsed, string[]? arguments)
{
this.context = context;
this.cleanContent = cleanContent;
this.commandUsed = commandUsed;
this.arguments = arguments;
}
public DbCommandExecutingArguments(SocketUserMessage? message, DiscordSocketClient client)
{
context = new SocketCommandContext(client, message);
var pos = 0;
if (message.HasMentionPrefix(client.CurrentUser, ref pos))
{
var mentionPrefix = "<@" + client.CurrentUser.Id + ">";
cleanContent = message.Content.Substring(mentionPrefix.Length + 1);
}
else
{
cleanContent = message.Content.Substring(Config.DiscordBot.BotPrefix.Length);
}
var split = cleanContent.Split(' ');
string[]? argsClean = null;
if (split.Length > 1)
argsClean = string.Join(' ', split, 1, split.Length - 1).Split(' ');
commandUsed = split[0];
arguments = argsClean;
}
}

View File

@@ -1,35 +1,12 @@
namespace PluginManager.Others; using System;
/// <summary> namespace PluginManager.Others;
/// A list of operating systems
/// </summary>
public enum OperatingSystem
{
WINDOWS,
LINUX,
MAC_OS,
UNKNOWN
}
/// <summary>
/// A list with all errors
/// </summary>
public enum Error
{
UNKNOWN_ERROR,
GUILD_NOT_FOUND,
STREAM_NOT_FOUND,
INVALID_USER,
INVALID_CHANNEL,
INVALID_PERMISSIONS
}
/// <summary> /// <summary>
/// The output log type /// The output log type
/// </summary> /// </summary>
public enum OutputLogLevel public enum LogType
{ {
NONE,
INFO, INFO,
WARNING, WARNING,
ERROR, ERROR,
@@ -38,25 +15,29 @@ public enum OutputLogLevel
public enum UnzipProgressType public enum UnzipProgressType
{ {
PercentageFromNumberOfFiles, PERCENTAGE_FROM_NUMBER_OF_FILES,
PercentageFromTotalSize PERCENTAGE_FROM_TOTAL_SIZE
} }
public enum TableFormat public enum InternalActionRunType
{ {
CENTER_EACH_COLUMN_BASED, ON_STARTUP,
CENTER_OVERALL_LENGTH, ON_CALL
DEFAULT
} }
public enum SaveType [Flags]
public enum OSType: byte
{ {
NORMAL, NONE = 0,
BACKUP WINDOWS = 1 << 0,
LINUX = 2 << 1,
MACOSX = 3 << 2
} }
public enum ProgressBarType public enum PluginType
{ {
NORMAL, UNKNOWN,
NO_END COMMAND,
EVENT,
SLASH_COMMAND
} }

View File

@@ -1,18 +1,11 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Discord;
using Discord.WebSocket;
using PluginManager.Items;
namespace PluginManager.Others; namespace PluginManager.Others;
@@ -23,114 +16,17 @@ public static class Functions
{ {
/// <summary> /// <summary>
/// The location for the Resources folder /// The location for the Resources folder
/// String: ./Data/Resources/
/// </summary> /// </summary>
public static readonly string dataFolder = @"./Data/Resources/"; public static readonly string dataFolder = @"./Data/Resources/";
/// <summary> public static Color RandomColor
/// The location for all logs
/// </summary>
public static readonly string logFolder = @"./Data/Output/Logs/";
/// <summary>
/// The location for all errors
/// </summary>
public static readonly string errFolder = @"./Data/Output/Errors/";
/// <summary>
/// Archives folder
/// </summary>
public static readonly string pakFolder = @"./Data/PAKS/";
/// <summary>
/// Beta testing folder
/// </summary>
public static readonly string betaFolder = @"./Data/BetaTest/";
/// <summary>
/// Read data from a file that is inside an archive (ZIP format)
/// </summary>
/// <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)
{ {
archFile = pakFolder + archFile; get
if (!File.Exists(archFile))
throw new Exception("Failed to load file !");
try
{ {
string textValue = null; var random = new Random();
using (var fs = new FileStream(archFile, FileMode.Open)) return new Color(random.Next(0, 255), random.Next(0, 255), random.Next(0, 255));
using (var zip = new ZipArchive(fs, ZipArchiveMode.Read))
{
foreach (var entry in zip.Entries)
if (entry.Name == FileName || entry.FullName == FileName)
using (var s = entry.Open())
using (var reader = new StreamReader(s))
{
textValue = await reader.ReadToEndAsync();
reader.Close();
s.Close();
fs.Close();
}
}
return textValue;
} }
catch
{
await Task.Delay(100);
return await ReadFromPakAsync(FileName, archFile);
}
}
/// <summary>
/// Write logs to file
/// </summary>
/// <param name="LogMessage">The message to be wrote</param>
public static void WriteLogFile(string LogMessage)
{
var logsPath = logFolder + $"{DateTime.Today.ToShortDateString().Replace("/", "-").Replace("\\", "-")} Log.txt";
Directory.CreateDirectory(logFolder);
File.AppendAllText(logsPath, LogMessage + " \n");
}
/// <summary>
/// Write error to file
/// </summary>
/// <param name="ErrMessage">The message to be wrote</param>
public static void WriteErrFile(string ErrMessage)
{
var errPath = errFolder +
$"{DateTime.Today.ToShortDateString().Replace("/", "-").Replace("\\", "-")} Error.txt";
Directory.CreateDirectory(errFolder);
File.AppendAllText(errPath, ErrMessage + " \n");
}
public static void WriteErrFile(this Exception ex)
{
WriteErrFile(ex.ToString());
}
/// <summary>
/// Get the Operating system you are runnin on
/// </summary>
/// <returns>An Operating system</returns>
public static OperatingSystem GetOperatingSystem()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return OperatingSystem.WINDOWS;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) return OperatingSystem.LINUX;
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) return OperatingSystem.MAC_OS;
return OperatingSystem.UNKNOWN;
}
public static List<string> GetArguments(SocketMessage message)
{
var command = new Command(message);
return command.Arguments;
} }
/// <summary> /// <summary>
@@ -145,9 +41,10 @@ public static class Functions
/// <exception cref="ArgumentOutOfRangeException">Triggered if <paramref name="bufferSize" /> is less then or equal to 0</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="InvalidOperationException">Triggered if <paramref name="stream" /> is not readable</exception>
/// <exception cref="ArgumentException">Triggered in <paramref name="destination" /> is not writable</exception> /// <exception cref="ArgumentException">Triggered in <paramref name="destination" /> is not writable</exception>
public static async Task CopyToOtherStreamAsync(this Stream stream, Stream destination, int bufferSize, public static async Task CopyToOtherStreamAsync(
IProgress<long>? progress = null, this Stream stream, Stream destination, int bufferSize,
CancellationToken cancellationToken = default) IProgress<long>? progress = null,
CancellationToken cancellationToken = default)
{ {
if (stream == null) throw new ArgumentNullException(nameof(stream)); if (stream == null) throw new ArgumentNullException(nameof(stream));
if (destination == null) throw new ArgumentNullException(nameof(destination)); if (destination == null) throw new ArgumentNullException(nameof(destination));
@@ -156,10 +53,11 @@ public static class Functions
if (!destination.CanWrite) if (!destination.CanWrite)
throw new ArgumentException("Destination stream is not writable", nameof(destination)); throw new ArgumentException("Destination stream is not writable", nameof(destination));
var buffer = new byte[bufferSize]; var buffer = new byte[bufferSize];
long totalBytesRead = 0; long totalBytesRead = 0;
int bytesRead; int bytesRead;
while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken)
.ConfigureAwait(false)) != 0)
{ {
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false); await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
totalBytesRead += bytesRead; totalBytesRead += bytesRead;
@@ -167,134 +65,22 @@ public static class Functions
} }
} }
/// <summary>
/// Extract zip to location public static T SelectRandomValueOf<T>()
/// </summary>
/// <param name="zip">The zip location</param>
/// <param name="folder">The target location</param>
/// <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(string zip, string folder, IProgress<float> progress,
UnzipProgressType type)
{ {
Directory.CreateDirectory(folder); var enums = Enum.GetValues(typeof(T));
using (var archive = ZipFile.OpenRead(zip)) var random = new Random();
{ return (T)enums.GetValue(random.Next(enums.Length));
if (type == UnzipProgressType.PercentageFromNumberOfFiles)
{
var totalZIPFiles = archive.Entries.Count();
var currentZIPFile = 0;
foreach (var entry in archive.Entries)
{
if (entry.FullName.EndsWith("/")) // it is a folder
Directory.CreateDirectory(Path.Combine(folder, entry.FullName));
else
try
{
entry.ExtractToFile(Path.Combine(folder, entry.FullName), true);
}
catch (Exception ex)
{
Settings.Variables.outputStream.WriteLine($"Failed to extract {entry.Name}. Exception: {ex.Message}");
}
currentZIPFile++;
await Task.Delay(10);
if (progress != null)
progress.Report((float)currentZIPFile / totalZIPFiles * 100);
}
}
else if (type == UnzipProgressType.PercentageFromTotalSize)
{
ulong zipSize = 0;
foreach (var entry in archive.Entries)
zipSize += (ulong)entry.CompressedLength;
ulong currentSize = 0;
foreach (var entry in archive.Entries)
{
if (entry.FullName.EndsWith("/"))
{
Directory.CreateDirectory(Path.Combine(folder, entry.FullName));
continue;
}
try
{
entry.ExtractToFile(Path.Combine(folder, entry.FullName), true);
currentSize += (ulong)entry.CompressedLength;
}
catch (Exception ex)
{
Settings.Variables.outputStream.WriteLine($"Failed to extract {entry.Name}. Exception: {ex.Message}");
}
await Task.Delay(10);
if (progress != null)
progress.Report((float)currentSize / zipSize * 100);
}
}
}
} }
/// <summary> public static T RandomValue<T>(this T[] values)
/// 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(); Random random = new();
await JsonSerializer.SerializeAsync(str, Data, typeof(T), new JsonSerializerOptions { WriteIndented = true }); return values[random.Next(values.Length)];
await File.WriteAllBytesAsync(file, str.ToArray());
} }
/// <summary> public static string ToResourcesPath(this string path)
/// 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; return Path.Combine(dataFolder, path);
if (File.Exists(input))
text = new MemoryStream(await File.ReadAllBytesAsync(input));
else
text = new MemoryStream(Encoding.ASCII.GetBytes(input));
text.Position = 0;
var obj = await JsonSerializer.DeserializeAsync<T>(text);
text.Close();
return (obj ?? default)!;
}
public static bool TryReadValueFromJson(string input, string codeName, out JsonElement element)
{
Stream text;
if (File.Exists(input))
text = File.OpenRead(input);
else
text = new MemoryStream(Encoding.ASCII.GetBytes(input));
var jsonObject = JsonDocument.Parse(text);
var data = jsonObject.RootElement.TryGetProperty(codeName, out element);
return data;
}
public static string CreateMD5(string input)
{
using (var md5 = MD5.Create())
{
var inputBytes = Encoding.ASCII.GetBytes(input);
var hashBytes = md5.ComputeHash(inputBytes);
return Convert.ToHexString(hashBytes);
}
} }
} }

View File

@@ -0,0 +1,53 @@
using System;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace PluginManager;
public class JsonManager
{
/// <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;
var obj = await JsonSerializer.DeserializeAsync<T>(text);
await text.FlushAsync();
text.Close();
return (obj ?? default)!;
}
}

View File

@@ -0,0 +1,70 @@
using System;
using System.Linq;
using PluginManager.Interfaces.Logger;
namespace PluginManager.Others.Logger;
public class Log: ILog
{
public string Message { get; set; }
public Type? Source { get; set; }
public LogType Type { get; set; }
public DateTime ThrowTime { get; set; }
public Log(string message, Type? source, LogType type, DateTime throwTime)
{
Message = message;
Source = source;
Type = type;
ThrowTime = throwTime;
}
public Log(string message, Type? source, LogType type)
{
Message = message;
Source = source;
Type = type;
ThrowTime = DateTime.Now;
}
public Log(string message, Type? source)
{
Message = message;
Source = source;
Type = LogType.INFO;
ThrowTime = DateTime.Now;
}
public Log(string message)
{
Message = message;
Source = typeof(Log);
Type = LogType.INFO;
ThrowTime = DateTime.Now;
}
public static implicit operator Log(string message)
{
return new Log(message);
}
public static implicit operator string(Log log)
{
return $"[{log.ThrowTime}] {log.Message}";
}
public string AsLongString()
{
return $"[{ThrowTime}] [{Source}] [{Type}] {Message}";
}
public string AsShortString()
{
return this;
}
public string FormatedLongString()
{
return $"[{ThrowTime}]\t[{Source}]\t\t\t[{Type}]\t{Message}";
}
}

View File

@@ -0,0 +1,79 @@
using System;
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using PluginManager.Interfaces.Logger;
namespace PluginManager.Others.Logger;
public sealed class Logger: ILogger
{
public bool IsEnabled { get; init; }
public bool OutputToFile { get; init; }
public string? OutputFile { get; init; }
private LogType LowestLogLevel { get; }
private bool UseShortVersion { get; }
public Logger(bool useShortVersion, bool outputToFile, string outputFile, LogType lowestLogLevel = LogType.INFO)
{
UseShortVersion = useShortVersion;
OutputToFile = outputToFile;
IsEnabled = true;
LowestLogLevel = lowestLogLevel;
OutputFile = outputFile;
}
public Logger(bool useShortVersion, LogType lowestLogLevel = LogType.INFO)
{
UseShortVersion = useShortVersion;
OutputToFile = false;
IsEnabled = true;
LowestLogLevel = lowestLogLevel;
OutputFile = null;
}
public event EventHandler<Log>? OnLog;
private async Task Log(Log logMessage)
{
if (!IsEnabled) return;
OnLog?.Invoke(this, logMessage);
if (logMessage.Type < LowestLogLevel) return;
if (OutputToFile)
await File.AppendAllTextAsync(
OutputFile!,
(UseShortVersion ? logMessage : logMessage.AsLongString()) + "\n"
);
}
public async void Log(string message = "", Type? source = default, LogType type = LogType.INFO, DateTime throwTime = default)
{
if (!IsEnabled) return;
if (type < LowestLogLevel) return;
if (string.IsNullOrEmpty(message)) return;
if (throwTime == default) throwTime = DateTime.Now;
if (source == default) source = typeof(Log);
await Log(new Log(message, source, type, throwTime));
}
public async void Log(Exception exception, LogType logType = LogType.ERROR, Type? source = null)
{
if (!IsEnabled) return;
if (logType < LowestLogLevel) return;
await Log(new Log(exception.Message, source, logType, DateTime.Now));
}
}

View File

@@ -0,0 +1,139 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PluginManager.Others
{
public class OneOf<T0, T1>
{
public T0 Item0 { get; }
public T1 Item1 { get; }
public object? Value => Item0 != null ? Item0 : Item1;
public OneOf(T0 item0)
{
Item0 = item0;
}
public OneOf(T1 item1)
{
Item1 = item1;
}
public static implicit operator OneOf<T0, T1>(T0 item0) => new OneOf<T0, T1>(item0);
public static implicit operator OneOf<T0, T1>(T1 item1) => new OneOf<T0, T1>(item1);
public void Match(Action<T0> item0, Action<T1> item1)
{
if (Item0 != null)
item0(Item0);
else
item1(Item1);
}
public TResult Match<TResult>(Func<T0, TResult> item0, Func<T1, TResult> item1)
{
return Item0 != null ? item0(Item0) : item1(Item1);
}
public Type GetActualType()
{
return Item0 != null ? Item0.GetType() : Item1.GetType();
}
}
public class OneOf<T0, T1, T2>
{
public T0 Item0 { get; }
public T1 Item1 { get; }
public T2 Item2 { get; }
public OneOf(T0 item0)
{
Item0 = item0;
}
public OneOf(T1 item1)
{
Item1 = item1;
}
public OneOf(T2 item2)
{
Item2 = item2;
}
public static implicit operator OneOf<T0, T1, T2>(T0 item0) => new OneOf<T0, T1, T2>(item0);
public static implicit operator OneOf<T0, T1, T2>(T1 item1) => new OneOf<T0, T1, T2>(item1);
public static implicit operator OneOf<T0, T1, T2>(T2 item2) => new OneOf<T0, T1, T2>(item2);
public void Match(Action<T0> item0, Action<T1> item1, Action<T2> item2)
{
if (Item0 != null)
item0(Item0);
else if (Item1 != null)
item1(Item1);
else
item2(Item2);
}
public TResult Match<TResult>(Func<T0, TResult> item0, Func<T1, TResult> item1, Func<T2, TResult> item2)
{
return Item0 != null ? item0(Item0) : Item1 != null ? item1(Item1) : item2(Item2);
}
}
public class OneOf<T0, T1, T2, T3>
{
public T0 Item0 { get; }
public T1 Item1 { get; }
public T2 Item2 { get; }
public T3 Item3 { get; }
public OneOf(T0 item0)
{
Item0 = item0;
}
public OneOf(T1 item1)
{
Item1 = item1;
}
public OneOf(T2 item2)
{
Item2 = item2;
}
public OneOf(T3 item3)
{
Item3 = item3;
}
public static implicit operator OneOf<T0, T1, T2, T3>(T0 item0) => new OneOf<T0, T1, T2, T3>(item0);
public static implicit operator OneOf<T0, T1, T2, T3>(T1 item1) => new OneOf<T0, T1, T2, T3>(item1);
public static implicit operator OneOf<T0, T1, T2, T3>(T2 item2) => new OneOf<T0, T1, T2, T3>(item2);
public static implicit operator OneOf<T0, T1, T2, T3>(T3 item3) => new OneOf<T0, T1, T2, T3>(item3);
public void Match(Action<T0> item0, Action<T1> item1, Action<T2> item2, Action<T3> item3)
{
if (Item0 != null)
item0(Item0);
else if (Item1 != null)
item1(Item1);
else if (Item2 != null)
item2(Item2);
else
item3(Item3);
}
public TResult Match<TResult>(Func<T0, TResult> item0, Func<T1, TResult> item1, Func<T2, TResult> item2, Func<T3, TResult> item3)
{
return Item0 != null ? item0(Item0) : Item1 != null ? item1(Item1) : Item2 != null ? item2(Item2) : item3(Item3);
}
}
}

View File

@@ -26,7 +26,7 @@ public static class DiscordPermissions
/// <param name="user">The user</param> /// <param name="user">The user</param>
/// <param name="role">The role</param> /// <param name="role">The role</param>
/// <returns></returns> /// <returns></returns>
public static bool hasRole(this SocketGuildUser user, IRole role) public static bool HasRole(this SocketGuildUser user, IRole role)
{ {
return user.Roles.Contains(role); return user.Roles.Contains(role);
} }
@@ -37,7 +37,7 @@ public static class DiscordPermissions
/// <param name="user">The user</param> /// <param name="user">The user</param>
/// <param name="permission">The permission</param> /// <param name="permission">The permission</param>
/// <returns></returns> /// <returns></returns>
public static bool hasPermission(this SocketGuildUser user, GuildPermission permission) 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.Where(role => role.hasPermission(permission)).Any() || user.Guild.Owner == user;
} }
@@ -47,9 +47,9 @@ public static class DiscordPermissions
/// </summary> /// </summary>
/// <param name="user">The user</param> /// <param name="user">The user</param>
/// <returns></returns> /// <returns></returns>
public static bool isAdmin(this SocketGuildUser user) public static bool IsAdmin(this SocketGuildUser user)
{ {
return user.hasPermission(GuildPermission.Administrator); return user.HasPermission(GuildPermission.Administrator);
} }
/// <summary> /// <summary>
@@ -57,8 +57,8 @@ public static class DiscordPermissions
/// </summary> /// </summary>
/// <param name="user">The user</param> /// <param name="user">The user</param>
/// <returns></returns> /// <returns></returns>
public static bool isAdmin(this SocketUser user) public static bool IsAdmin(this SocketUser user)
{ {
return isAdmin((SocketGuildUser)user); return IsAdmin((SocketGuildUser)user);
} }
} }

View File

@@ -0,0 +1,88 @@
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace PluginManager.Others;
public class SettingsDictionary<TKey, TValue>
{
private string _File { get; }
private IDictionary<TKey, TValue> _Dictionary;
public SettingsDictionary(string file)
{
this._File = file;
_Dictionary = null!;
}
public async Task SaveToFile()
{
if (!string.IsNullOrEmpty(_File))
await JsonManager.SaveToJsonFile(_File, _Dictionary);
}
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
{
return _Dictionary.GetEnumerator();
}
public async Task<bool> LoadFromFile()
{
if (string.IsNullOrEmpty(_File))
return false;
if(!File.Exists(_File))
{
_Dictionary = new Dictionary<TKey, TValue>();
return true;
}
string fileAsText = await File.ReadAllTextAsync(_File);
if(string.IsNullOrEmpty(fileAsText) || string.IsNullOrWhiteSpace(fileAsText))
{
_Dictionary = new Dictionary<TKey, TValue>();
return true;
}
_Dictionary = await JsonManager.ConvertFromJson<IDictionary<TKey,TValue>>(fileAsText);
return true;
}
public void Add(TKey key, TValue value)
{
_Dictionary.Add(key, value);
}
public bool ContainsAllKeys(params TKey[] keys)
{
return keys.All(key => _Dictionary.ContainsKey(key));
}
public bool ContainsKey(TKey key)
{
return _Dictionary.ContainsKey(key);
}
public bool Remove(TKey key)
{
return _Dictionary.Remove(key);
}
public TValue this[TKey key]
{
get
{
if(!_Dictionary.ContainsKey(key))
throw new System.Exception($"The key {key} ({typeof(TKey)}) was not present in the dictionary");
if(_Dictionary[key] is not TValue)
throw new System.Exception("The dictionary is corrupted. This error is critical !");
return _Dictionary[key];
}
set => _Dictionary[key] = value;
}
}

View File

@@ -0,0 +1,13 @@
namespace PluginManager.Plugin;
public class OnlineDependencyInfo
{
public string DownloadLink { get; private set; }
public string DownloadLocation { get; private set; }
public OnlineDependencyInfo(string downloadLink, string downloadLocation)
{
DownloadLink = downloadLink;
DownloadLocation = downloadLocation;
}
}

View File

@@ -0,0 +1,39 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using PluginManager.Online.Helpers;
namespace PluginManager.Plugin;
public class PluginInfo
{
public string PluginName { get; private set; }
public PluginVersion PluginVersion { get; private set; }
public string FilePath { get; private set; }
public List<string> ListOfDependancies {get; private set;}
public bool IsMarkedToUninstall {get; internal set;}
[JsonConstructor]
public PluginInfo(string pluginName, PluginVersion pluginVersion, List<string> listOfDependancies, bool isMarkedToUninstall)
{
PluginName = pluginName;
PluginVersion = pluginVersion;
ListOfDependancies = listOfDependancies;
IsMarkedToUninstall = isMarkedToUninstall;
FilePath = $"{Config.AppSettings["PluginFolder"]}/{pluginName}.dll";
}
public PluginInfo(string pluginName, PluginVersion pluginVersion, List<string> listOfDependancies)
{
PluginName = pluginName;
PluginVersion = pluginVersion;
ListOfDependancies = listOfDependancies;
IsMarkedToUninstall = false;
FilePath = $"{Config.AppSettings["PluginFolder"]}/{pluginName}.dll";
}
public static PluginInfo FromOnlineInfo(PluginOnlineInfo onlineInfo)
{
return new PluginInfo(onlineInfo.Name, onlineInfo.Version, onlineInfo.Dependencies.Select(dep => dep.DownloadLocation).ToList());
}
}

View File

@@ -0,0 +1,51 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using PluginManager.Online.Helpers;
using PluginManager.Others;
namespace PluginManager.Plugin;
public class PluginOnlineInfo
{
public string Name { get; private set; }
public PluginVersion Version { get; private set; }
public string DownLoadLink { get; private set; }
public string Description { get; private set; }
public List<OnlineDependencyInfo> Dependencies { get; private set; }
public OSType SupportedOS { get; private set; }
public bool HasDependencies { get; init; }
[JsonConstructor]
public PluginOnlineInfo(string name, PluginVersion version, string description, string downLoadLink, OSType supportedOS, List<OnlineDependencyInfo> dependencies)
{
Name = name;
Version = version;
Description = description;
DownLoadLink = downLoadLink;
SupportedOS = supportedOS;
Dependencies = dependencies;
HasDependencies = dependencies.Count > 0;
}
public PluginOnlineInfo(string name, PluginVersion version, string description, string downLoadLink, OSType supportedOS)
{
Name = name;
Version = version;
Description = description;
DownLoadLink = downLoadLink;
SupportedOS = supportedOS;
Dependencies = new List<OnlineDependencyInfo>();
HasDependencies = false;
}
public static async Task<PluginOnlineInfo> FromRawData(string jsonText)
{
return await JsonManager.ConvertFromJson<PluginOnlineInfo>(jsonText);
}
public override string ToString()
{
return $"{Name} - {Version} ({Description})";
}
}

View File

@@ -1,24 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<PropertyGroup> <TargetFramework>net8.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework> <Nullable>enable</Nullable>
<Nullable>enable</Nullable> </PropertyGroup>
</PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<FileAlignment>512</FileAlignment>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <DebugType>none</DebugType>
<FileAlignment>512</FileAlignment> <DebugSymbols>false</DebugSymbols>
<DebugType>none</DebugType> </PropertyGroup>
<DebugSymbols>false</DebugSymbols> <ItemGroup>
</PropertyGroup> <None Remove="BlankWindow1.xaml" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<None Remove="BlankWindow1.xaml" /> <PackageReference Include="Discord.Net" Version="3.14.1" />
</ItemGroup> <PackageReference Include="System.Data.SQLite.Core" Version="1.0.118" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Discord.Net" Version="3.7.2" /> <UpToDateCheckInput Remove="UI\Controls\MessageBox.axaml" />
<PackageReference Include="System.Data.SQLite" Version="1.0.116" /> </ItemGroup>
<PackageReference Include="Terminal.Gui" Version="1.8.2" />
</ItemGroup>
</Project> </Project>

View File

@@ -0,0 +1,94 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using PluginManager.Interfaces.Updater;
namespace PluginManager.Updater.Application
{
public class AppUpdater
{
private static readonly string _DefaultUpdateUrl = "https://github.com/andreitdr/SethDiscordBot/releases/latest";
private async Task<AppVersion> GetOnlineVersion()
{
HttpClient client = new HttpClient();
var response = await client.GetAsync(_DefaultUpdateUrl);
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
var version = Regex.Match(content, @"<title>.+?v(\d+\.\d+\.\d+.\d+).+?</title>").Groups[1].Value;
return new AppVersion(version);
}
return AppVersion.CurrentAppVersion;
}
public async Task<Update> CheckForUpdates()
{
var latestVersion = await GetOnlineVersion();
if(latestVersion.IsNewerThan(AppVersion.CurrentAppVersion))
{
return new Update(AppVersion.CurrentAppVersion, latestVersion, _DefaultUpdateUrl, await GetUpdateNotes());
}
return Update.None;
}
private async Task<string> GetUpdateNotes()
{
HttpClient client = new HttpClient();
var response = await client.GetAsync(_DefaultUpdateUrl);
if (!response.IsSuccessStatusCode)
{
return string.Empty;
}
var content = await response.Content.ReadAsStringAsync();
var markdownStart = content.IndexOf("<div data-pjax=\"true\" data-test-selector=\"body-content\"");
if(markdownStart == -1)
{
return string.Empty;
}
markdownStart = content.IndexOf(">", markdownStart) + 1; // Move past the opening tag
var markdownEnd = content.IndexOf("</div>", markdownStart);
var markdown = content.Substring(markdownStart, markdownEnd - markdownStart).Trim();
markdown = RemoveHtmlTags(markdown);
markdown = ApplyMarkdownFormatting(markdown);
return markdown;
}
private string RemoveHtmlTags(string text)
{
return Regex.Replace(text, "<.*?>", "").Trim();
}
private string ApplyMarkdownFormatting(string markdown)
{
// Apply markdown formatting
markdown = markdown.Replace("**", "**"); // Bold
markdown = markdown.Replace("*", "*"); // Italic
markdown = markdown.Replace("`", "`"); // Inline code
markdown = markdown.Replace("```", "```"); // Code block
markdown = markdown.Replace("&gt;", ">"); // Greater than symbol
markdown = markdown.Replace("&lt;", "<"); // Less than symbol
markdown = markdown.Replace("&amp;", "&"); // Ampersand
markdown = markdown.Replace("&quot;", "\""); // Double quote
markdown = markdown.Replace("&apos;", "'"); // Single quote
markdown = markdown.Replace(" - ", "\n- "); // Convert bullet points to markdown list items
return markdown;
}
}
}

View File

@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Discord.Commands;
using PluginManager.Interfaces.Updater;
namespace PluginManager.Updater.Application
{
public class Update
{
public readonly static Update None = new Update(AppVersion.CurrentAppVersion, AppVersion.CurrentAppVersion, string.Empty, string.Empty);
public AppVersion UpdateVersion { get; private set; }
public AppVersion CurrentVersion { get; private set; }
public string UpdateUrl { get; private set; }
public string UpdateNotes { get; private set; }
public Update(AppVersion currentVersion, AppVersion updateVersion, string updateUrl, string updateNotes)
{
UpdateVersion = updateVersion;
CurrentVersion = currentVersion;
UpdateUrl = updateUrl;
UpdateNotes = updateNotes;
}
}
}

View File

@@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using PluginManager.Online;
using PluginManager.Plugin;
namespace PluginManager.Updater.Plugins;
public class PluginUpdater
{
private readonly PluginsManager _PluginsManager;
public PluginUpdater(PluginsManager pluginManager)
{
_PluginsManager = pluginManager;
}
public async Task<PluginOnlineInfo> GetPluginInfo(string pluginName)
{
var result = await _PluginsManager.GetPluginDataByName(pluginName);
return result;
}
public async Task<PluginInfo> GetLocalPluginInfo(string pluginName)
{
string pluginsDatabase = File.ReadAllText(Config.AppSettings["PluginDatabase"]);
List<PluginInfo> installedPlugins = await JsonManager.ConvertFromJson<List<PluginInfo>>(pluginsDatabase);
var result = installedPlugins.Find(p => p.PluginName == pluginName);
return result;
}
public async Task UpdatePlugin(string pluginName, IProgress<float>? progressMeter = null)
{
PluginOnlineInfo pluginInfo = await GetPluginInfo(pluginName);
await ServerCom.DownloadFileAsync(pluginInfo.DownLoadLink, $"{Config.AppSettings["PluginFolder"]}/{pluginName}.dll", progressMeter);
foreach(OnlineDependencyInfo dependency in pluginInfo.Dependencies)
await ServerCom.DownloadFileAsync(dependency.DownloadLocation, dependency.DownloadLocation, progressMeter);
await _PluginsManager.RemovePluginFromDatabase(pluginName);
await _PluginsManager.AppendPluginToDatabase(PluginInfo.FromOnlineInfo(pluginInfo));
}
public async Task<bool> HasUpdate(string pluginName)
{
var localPluginInfo = await GetLocalPluginInfo(pluginName);
var pluginInfo = await GetPluginInfo(pluginName);
return pluginInfo.Version.IsNewerThan(localPluginInfo.PluginVersion);
}
}

View File

@@ -1,21 +0,0 @@
using System.IO;
using PluginManager.Database;
namespace PluginManager
{
public class Settings
{
public static class Variables
{
public static string WebsiteURL = "https://wizzy69.github.io/SethDiscordBot";
public static string UpdaterURL = "https://github.com/Wizzy69/installer/releases/download/release-1-discordbot/Updater.zip";
public static TextWriter outputStream;
}
public static SqlDatabase sqlDatabase;
}
}

223
README.md
View File

@@ -3,96 +3,101 @@
This is a Discord Bot made with C# that accepts plugins as extensions for more commands and events. All basic commands are built in already in the PluginManager class library. This is a Discord Bot made with C# that accepts plugins as extensions for more commands and events. All basic commands are built in already in the PluginManager class library.
This project is based on: This project is based on:
- [.NET 6 (C#)](https://dotnet.microsoft.com/en-us/download/dotnet/6.0) - [.NET 8 (C#)](https://dotnet.microsoft.com/en-us/download/dotnet/8.0)
- [Discord.Net](https://github.com/discord-net/Discord.Net) - [Discord.Net](https://github.com/discord-net/Discord.Net)
## Plugins ## Plugins
#### Requirements: - Some plugins can be found in [this repo](https://github.com/andreitdr/SethPlugins).
- [Visual Studio](https://visualstudio.microsoft.com/thank-you-downloading-visual-studio/?sku=Community&channel=Release&version=VS2022&source=VSLandingPage&cid=2030&passive=false)
- .NET 6 (downloaded with Visual Studio)
Plugin Types: Plugin Types:
1. Commands 1. Commands
2. Events 2. Events
3. Slash Commands
### How to create a plugin ### How to create a plugin
First of all, Create a new project (class library) in Visual Studio. #### Requirements:
![Imgur Image](https://i.imgur.com/KUqzKsB.png) - [Visual Studio](https://visualstudio.microsoft.com/thank-you-downloading-visual-studio/?sku=Community&channel=Release&version=VS2022&source=VSLandingPage&cid=2030&passive=false)
- .NET 8 (downloaded with Visual Studio)
![Imgur Image](https://i.imgur.com/JzpEViR.png) First of all, create a new project (class library) in Visual Studio.
Then import the PluginManager as reference to your project.
![Imgur Image](https://i.imgur.com/vtoEepX.png) ## 1. Commands
![Imgur Image](https://i.imgur.com/ceaVR2R.png) Commands are loaded when all plugins are loaded into memory. The Execute method is called whenever any user (that respects the `requireAdmin` propery) calls the command using the bot prefix and the `Command`.
Now, let's add the PluginManager reference. It can be found inside the bot's main folder under
`DiscordBot/bin/Debug/net6.0/PluginManager.dll` or `PluginManager/bin/Debug/net6.0/PluginManager.dll`
after one successfull build.
![Imgur Image](https://i.imgur.com/UMSitk4.png)
![Imgur Image](https://i.imgur.com/GEjShdl.png)
1. Commands
Commands are loaded when all plugins are loaded into memory. When an user executes the command, only then the Execute function is called.
Commands are plugins that allow users to interact with them. Commands are plugins that allow users to interact with them.
Here is an example of class that is a command class Here is an example:
```cs ```cs
using Discord;
using Discord.Commands; using Discord.Commands;
using Discord.WebSocket;
using PluginManager.Interfaces; using PluginManager.Interfaces;
namespace CMD_Utils namespace LevelingSystem;
public class LevelCommand : DBCommand
{ {
class FlipCoin : DBCommand public string Command => "level";
public List<string> Aliases => new() { "lvl" };
public string Description => "Display your current level";
public string Usage => "level";
public bool requireAdmin => false;
public async void ExecuteServer(DBCommandExecutingArguments context)
{ {
public string Command => "flip"; //Variables.database is a sql connection that is defined in an auxiliary file in the same napespace as this class
object[] user = await Variables.database.ReadDataArrayAsync($"SELECT * FROM Levels WHERE UserID='{context.Message.Author.Id}'");
public string Description => "Flip a coin"; if (user is null)
public string Usage => "flip";
public bool canUseDM => true;
public bool canUseServer => true;
public bool requireAdmin => false;
public async void Execute(SocketCommandContext context, SocketMessage message, DiscordSocketClient client, bool isDM)
{ {
System.Random random = new System.Random(); await context.Channel.SendMessageAsync("You are now unranked !");
int r = random.Next(1, 3); return;
if (r == 1)
await message.Channel.SendMessageAsync("Heads");
else await message.Channel.SendMessageAsync("Tails");
} }
int level = (int)user[1];
int exp = (int)user[2];
var builder = new EmbedBuilder();
var r = new Random();
builder.WithColor(r.Next(256), r.Next(256), r.Next(256));
builder.AddField("Current Level", level, true)
.AddField("Current EXP", exp, true)
.AddField("Required Exp", (level * 8 + 24).ToString(), true);
builder.WithTimestamp(DateTimeOffset.Now);
builder.WithAuthor(context.Message.Author.Mention);
await context.Channel.SendMessageAsync(embed: builder.Build());
} }
//Optional method (tell the bot what should it do if the command is executed from a DM channel)
//public async void ExecuteDM(DBCommandExecutingArguments context) {
//
//}
} }
``` ```
#### Code description: #### Code description:
- Command - The keyword that triggers the execution for the command. This is what players must type in order to execute your command - Command - The keyword that triggers the execution for the command. This is what players must type in order to execute your command
- Aliases - The aliases that can be used instead of the full name to execute the command
- Description - The description of your command. Can be anything you like - Description - The description of your command. Can be anything you like
- Usage - The usage of your command. This is what `help [Command]` command will display - Usage - The usage of your command. This is what `help [Command]` command will display
- canUseDM - true if you plan to let users execute this command in DM chat with bot
- canUseServer - true if you plan to let the users execute this command in a server chat
- requireAdmin - true if this command requres an user with Administrator permission in the server - requireAdmin - true if this command requres an user with Administrator permission in the server
- Execute () - the function of your command. - ExecuteServer () - the function that is executed only when the command is invoked in a server channel. (optional)
- context - the command context
- ExecuteDM () - the function that is executed only when the command is invoked in a private (DM) channel. (optional)
- context - the command context - context - the command context
- message - the message itself
- client - the discord bot client
- isDM - true if the message was sent from DM chat
From here on, start coding. When your plugin is done, build it as any DLL project then add it to the following path From here on, start coding. When your plugin is done, build it as any DLL project then add it to the following path
`{bot_executable}/Data/Plugins/Commands/<optional subfolder>/yourDLLName.dll` `{bot_executable}/Data/Plugins/<optional subfolder>/[plugin name].dll`
Then, reload bot and execute command `lp` in bot's console. The plugin should be loaded into memory or an error is thrown if not. If an error is thrown, then Then, reload bot and execute command `plugin load` in the console. The plugin should be loaded into memory or an error is thrown if not. If an error is thrown, then
there is something wrong in your command's code. there is something wrong in your command's code.
2. Events ## 2. Events
Events are loaded when all plugins are loaded. At the moment when they are loaded, the Start function is called. Events are loaded when all plugins are loaded. At the moment when they are loaded, the Start function is called.
Events are used if you want the bot to do something when something happens in server. The following example shows you how to catch when a user joins the server Events are used if you want the bot to do something when something happens in server. The following example shows you how to catch when a user joins the server
@@ -110,8 +115,6 @@ public class OnUserJoin : DBEvent
public async void Start(Discord.WebSocket.DiscordSocketClient client) public async void Start(Discord.WebSocket.DiscordSocketClient client)
{ {
Console.WriteLine($"Hello World from {name}");
client.UserJoined += async (user) => { client.UserJoined += async (user) => {
await (await user.CreateDMChannelAsync()).SendMessageAsync("Welcome to server !"); await (await user.CreateDMChannelAsync()).SendMessageAsync("Welcome to server !");
}; };
@@ -125,3 +128,115 @@ public class OnUserJoin : DBEvent
- Start() - The main body of your event. This is executed when the bot loads all plugins - Start() - The main body of your event. This is executed when the bot loads all plugins
- client - the discord bot client - client - the discord bot client
## 3. Slash Commands
Slash commands are server based commands. They work the same way as normal commands, but they require the `/` prefix as they are integrated
with the UI of Discord.
Here is an example:
```cs
using Discord;
using Discord.WebSocket;
using PluginManager.Interfaces;
namespace SlashCommands
{
public class Random : DBSlashCommand
{
public string Name => "random";
public string Description => "Generates a random number between 2 values";
public bool canUseDM => true;
public List<SlashCommandOptionBuilder> Options => new List<SlashCommandOptionBuilder>()
{
new SlashCommandOptionBuilder() {Name = "min-value", Description = "Minimum value", IsRequired=true, Type = ApplicationCommandOptionType.Integer, MinValue = 0, MaxValue = int.MaxValue-1},
new SlashCommandOptionBuilder() {Name = "max-value", Description = "Maximum value", IsRequired=true, Type=ApplicationCommandOptionType.Integer,MinValue = 0, MaxValue = int.MaxValue-1}
};
public async void ExecuteServer(SocketSlashCommand command)
{
var rnd = new System.Random();
var options = command.Data.Options.ToArray();
if (options.Count() != 2)
{
await command.RespondAsync("Invalid parameters", ephemeral: true);
return;
}
Int64 numberOne = (Int64)options[0].Value;
Int64 numberTwo = (Int64)options[1].Value;
await command.RespondAsync("Your generated number is " + rnd.Next((int)numberOne, (int)numberTwo), ephemeral: true);
}
}
}
```
#### Code description:
- Name - the command name (execute with /{Name})
- Description - The description of the command
- canUseDM - true id this command can be activated in DM chat, false otherwise
- Options - the arguments of the command
- ExecuteServer() - this function will be called if the command is invoked in a server channel (optional)
- context - the command context
- ExecuteDM() - this function will be called if the command is invoked in a DM channel (optional)
- context - the command context
## Note:
You can create multiple commands, events and slash commands into one single plugin (class library). The PluginManager will detect the classes and load them individualy. If there are more commands (normal commands, events or slash commands) into a single project (class library) they can use the same resources (a class for example) that is contained within the plugin.
# Building from source
## Required tools
You must have dotnet 8 installed in order to compile.
You might run this commands with sudo in order to install dotnet successfully.
### On Linux
#### Arch
```sh
pacman -S dotnet-sdk-8.0
```
#### Debian / Ubuntu
```sh
apt install dotnet-sdk-8.0
```
#### Fedora / RHEL
```sh
dnf install dotnet-sdk-8.0
```
### On Windows
#### Default method
Download and install dotnet 8 from the official Microsoft website using [this](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) link.
#### Using Visual Studio
Download and install Visual Studio 2022 and select .NET Desktop Development while installing Visual Studio 2022.
Open Visual Studio and select Clone a repo and paste the following link: `https://github.com/andreitdr/SethDiscordBot`.
Open the solution in Visual Studio and build it.
> Note: You might need to manually restore the NuGet packages, but VS2022 should take care of them automatically for you.
> If not then you will need to click on Dependencies -> Packages for each project that has a yellow sign over the Dependancies tab and click Update.
## Cloning the repository
```sh
git clone https://github.com/andreitdr/SethDiscordBot
cd SethDiscordBot
dotnet build
```
After the build succeeds, check the `/bin/Debug` folders for each project to see the built items.
Follow the on-screen prompts to make the bot run.
> Updated: 01.04.2024

View File

@@ -1,4 +1,3 @@
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17 # Visual Studio Version 17
VisualStudioVersion = 17.1.32421.90 VisualStudioVersion = 17.1.32421.90
@@ -7,11 +6,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscordBot", "DiscordBot\Di
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PluginManager", "PluginManager\PluginManager.csproj", "{EDD4D9B3-98DD-4367-A09F-D1C5ACB61132}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PluginManager", "PluginManager\PluginManager.csproj", "{EDD4D9B3-98DD-4367-A09F-D1C5ACB61132}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MusicLibrary", "..\DiscordBotItems\Plugins\MusicLibrary\MusicLibrary.csproj", "{878DFE01-4596-4EBC-9651-0679598CE794}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SethPlugins", "SethPlugins", "{78B6D390-F61A-453F-B38D-E4C054321615}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SlashCommands", "..\DiscordBotItems\Plugins\SlashCommands\SlashCommands.csproj", "{C2D73BE8-997B-4A4A-8EA5-989BE33EE1DD}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscordBotUI", "DiscordBotUI\DiscordBotUI\DiscordBotUI.csproj", "{71293BF7-79D0-4707-AB4B-FDD16800FA81}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LevelingSystem", "..\DiscordBotItems\Plugins\LevelingSystem\LevelingSystem.csproj", "{0138F343-BBB9-4D5F-B499-D9C2978BE9AA}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscordBotUI.Desktop", "DiscordBotUI\DiscordBotUI.Desktop\DiscordBotUI.Desktop.csproj", "{F23CF852-2042-4BDE-ABFE-D4F5BD9B991D}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -27,18 +26,14 @@ Global
{EDD4D9B3-98DD-4367-A09F-D1C5ACB61132}.Debug|Any CPU.Build.0 = Debug|Any CPU {EDD4D9B3-98DD-4367-A09F-D1C5ACB61132}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EDD4D9B3-98DD-4367-A09F-D1C5ACB61132}.Release|Any CPU.ActiveCfg = Release|Any CPU {EDD4D9B3-98DD-4367-A09F-D1C5ACB61132}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EDD4D9B3-98DD-4367-A09F-D1C5ACB61132}.Release|Any CPU.Build.0 = Release|Any CPU {EDD4D9B3-98DD-4367-A09F-D1C5ACB61132}.Release|Any CPU.Build.0 = Release|Any CPU
{878DFE01-4596-4EBC-9651-0679598CE794}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {71293BF7-79D0-4707-AB4B-FDD16800FA81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{878DFE01-4596-4EBC-9651-0679598CE794}.Debug|Any CPU.Build.0 = Debug|Any CPU {71293BF7-79D0-4707-AB4B-FDD16800FA81}.Debug|Any CPU.Build.0 = Debug|Any CPU
{878DFE01-4596-4EBC-9651-0679598CE794}.Release|Any CPU.ActiveCfg = Release|Any CPU {71293BF7-79D0-4707-AB4B-FDD16800FA81}.Release|Any CPU.ActiveCfg = Release|Any CPU
{878DFE01-4596-4EBC-9651-0679598CE794}.Release|Any CPU.Build.0 = Release|Any CPU {71293BF7-79D0-4707-AB4B-FDD16800FA81}.Release|Any CPU.Build.0 = Release|Any CPU
{C2D73BE8-997B-4A4A-8EA5-989BE33EE1DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F23CF852-2042-4BDE-ABFE-D4F5BD9B991D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C2D73BE8-997B-4A4A-8EA5-989BE33EE1DD}.Debug|Any CPU.Build.0 = Debug|Any CPU {F23CF852-2042-4BDE-ABFE-D4F5BD9B991D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C2D73BE8-997B-4A4A-8EA5-989BE33EE1DD}.Release|Any CPU.ActiveCfg = Release|Any CPU {F23CF852-2042-4BDE-ABFE-D4F5BD9B991D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C2D73BE8-997B-4A4A-8EA5-989BE33EE1DD}.Release|Any CPU.Build.0 = Release|Any CPU {F23CF852-2042-4BDE-ABFE-D4F5BD9B991D}.Release|Any CPU.Build.0 = Release|Any CPU
{0138F343-BBB9-4D5F-B499-D9C2978BE9AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0138F343-BBB9-4D5F-B499-D9C2978BE9AA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0138F343-BBB9-4D5F-B499-D9C2978BE9AA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0138F343-BBB9-4D5F-B499-D9C2978BE9AA}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE