Actions are now loaded together with all plugins. Called the LoadPlugins at startup

This commit is contained in:
2024-06-08 19:17:15 +03:00
parent 9a8ddb5388
commit d9d5c05313
14 changed files with 148 additions and 236 deletions

View File

@@ -40,8 +40,9 @@ internal static class PluginMethods
internal static async Task RefreshPlugins(bool quiet)
{
await Application.CurrentApplication.InternalActionManager.Execute("plugin", "load", quiet ? "-q" : string.Empty);
await Application.CurrentApplication.InternalActionManager.Refresh();
await LoadPlugins(quiet ? ["-q"] : null);
await Application.CurrentApplication.InternalActionManager.Initialize();
}
internal static async Task DownloadPlugin(PluginManager manager, string pluginName)
@@ -156,7 +157,7 @@ internal static class PluginMethods
internal static async Task<bool> LoadPlugins(string[] args)
{
var loader = new PluginLoader(Application.CurrentApplication.DiscordBotClient.Client);
if (args.Length == 2 && args[1] == "-q")
if (args != null && (args.Length == 2 && args[1] == "-q"))
{
await loader.LoadPlugins();
return true;
@@ -211,6 +212,22 @@ internal static class PluginMethods
Console.ForegroundColor = cc;
};
loader.OnActionLoaded += (data) =>
{
if (data.IsSuccess)
{
Application.CurrentApplication.Logger.Log("Successfully loaded action : " + data.PluginName, LogType.INFO, "\t\t > {Message}");
}
else
{
Application.CurrentApplication.Logger.Log("Failed to load action : " + data.PluginName + " because " + data.ErrorMessage,
typeof(PluginMethods), LogType.ERROR
);
}
Console.ForegroundColor = cc;
};
await loader.LoadPlugins();
Console.ForegroundColor = cc;
return true;

View File

@@ -38,15 +38,15 @@ public class Help: ICommandAction
tableData.Columns = ["Command", "Usage", "Description", "Options"];
foreach (var a in Application.CurrentApplication.InternalActionManager.Actions)
foreach (var a in Application.CurrentApplication.InternalActionManager.GetActions())
{
Markup actionName = new Markup($"[bold]{a.Key}[/]");
Markup usage = new Markup($"[italic]{a.Value.Usage}[/]");
Markup description = new Markup($"[dim]{a.Value.Description}[/]");
Markup actionName = new Markup($"[bold]{a.ActionName}[/]");
Markup usage = new Markup($"[italic]{a.Usage}[/]");
Markup description = new Markup($"[dim]{a.Description}[/]");
if (a.Value.ListOfOptions.Any())
if (a.ListOfOptions.Any())
{
tableData.AddRow([actionName, usage, description, CreateTableWithSubOptions(a.Value.ListOfOptions)]);
tableData.AddRow([actionName, usage, description, CreateTableWithSubOptions(a.ListOfOptions)]);
}
else
{
@@ -64,13 +64,13 @@ public class Help: ICommandAction
return;
}
if (!Application.CurrentApplication.InternalActionManager.Actions.ContainsKey(args[0]))
if (!Application.CurrentApplication.InternalActionManager.Exists(args[0]))
{
Console.WriteLine("Command not found");
return;
}
var action = Application.CurrentApplication.InternalActionManager.Actions[args[0]];
var action = Application.CurrentApplication.InternalActionManager.GetAction(args[0]);
tableData.Columns = ["Command", "Usage", "Description"];
tableData.AddRow([action.ActionName, action.Usage, action.Description]);

View File

@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Discord;
@@ -31,7 +32,11 @@ public class Help: DBSlashCommand
EmbedBuilder embedBuilder = new();
embedBuilder.WithTitle("Help Command");
embedBuilder.WithColor(Functions.RandomColor);
var random = new Random();
Color c = new Color(random.Next(0, 255), random.Next(0, 255), random.Next(0, 255));
embedBuilder.WithColor(c);
var slashCommands = PluginLoader.SlashCommands;
var options = context.Data.Options;

View File

@@ -4,6 +4,8 @@ using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using DiscordBot.Bot.Actions.Extra;
using DiscordBotCore;
using DiscordBotCore.Bot;
using DiscordBotCore.Others;
@@ -22,6 +24,8 @@ public class Program
{
await LoadComponents(args);
await PrepareConsole();
await PluginMethods.LoadPlugins(null);
await Application.CurrentApplication.InternalActionManager.Initialize();
await ConsoleInputHandler();
}
@@ -30,7 +34,6 @@ public class Program
/// </summary>
private static async Task ConsoleInputHandler()
{
await Application.CurrentApplication.InternalActionManager.Execute("plugin", "load");
while (true)
{
@@ -65,7 +68,7 @@ public class Program
{
var token = Application.CurrentApplication.ApplicationEnvironmentVariables["token"];
var prefix = Application.CurrentApplication.ApplicationEnvironmentVariables["prefix"];
var discordbooter = new Boot(token, prefix);
var discordbooter = new App(token, prefix);
await discordbooter.Awake();
}
catch (Exception ex)
@@ -118,6 +121,8 @@ public class Program
!Application.CurrentApplication.ApplicationEnvironmentVariables.ContainsKey("token") ||
!Application.CurrentApplication.ApplicationEnvironmentVariables.ContainsKey("prefix"))
await Installer.GenerateStartupConfig();
}
}

View File

@@ -36,7 +36,7 @@ namespace DiscordBotCore
public InternalActionManager InternalActionManager { get; private set; }
public PluginManager PluginManager { get; private set; }
public Logger Logger { get; private set; }
public Bot.Boot DiscordBotClient { get; internal set; }
public Bot.App DiscordBotClient { get; internal set; }
public static async Task CreateApplication()
{
@@ -78,7 +78,7 @@ namespace DiscordBotCore
await CurrentApplication.PluginManager.UninstallMarkedPlugins();
await CurrentApplication.PluginManager.CheckForUpdates();
CurrentApplication.InternalActionManager = new InternalActionManager(CurrentApplication.ApplicationEnvironmentVariables["PluginFolder"], "dll");
CurrentApplication.InternalActionManager = new InternalActionManager();
await CurrentApplication.InternalActionManager.Initialize();
}

View File

@@ -9,7 +9,7 @@ using DiscordBotCore.Others;
namespace DiscordBotCore.Bot;
public class Boot
public class App
{
/// <summary>
/// The bot prefix
@@ -41,7 +41,7 @@ public class Boot
/// </summary>
/// <param name="botToken">The bot token</param>
/// <param name="botPrefix">The bot prefix</param>
public Boot(string botToken, string botPrefix)
public App(string botToken, string botPrefix)
{
this.BotPrefix = botPrefix;
this.BotToken = botToken;

View File

@@ -1,72 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using DiscordBotCore.Interfaces;
using DiscordBotCore.Others;
namespace DiscordBotCore.Loaders;
public class ActionsLoader
{
public delegate void ActionLoaded(string name, string typeName, bool success, Exception? e = null);
private readonly string _actionExtension;
private readonly string _actionFolder;
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_CALL)
action.ExecuteStartup();
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

@@ -10,8 +10,6 @@ namespace DiscordBotCore.Loaders;
internal class Loader
{
private readonly string _SearchPath;
private readonly string _FileExtension;
internal delegate void FileLoadedHandler(FileLoaderResult result);
@@ -20,24 +18,11 @@ internal class Loader
internal event FileLoadedHandler? OnFileLoadedException;
internal event PluginLoadedHandler? OnPluginLoaded;
internal Loader(string searchPath, string fileExtension)
{
_SearchPath = searchPath;
_FileExtension = fileExtension;
}
internal async Task Load()
{
if (!Directory.Exists(_SearchPath))
{
Directory.CreateDirectory(_SearchPath);
return;
}
var installedPlugins = await Application.CurrentApplication.PluginManager.GetInstalledPlugins();
var files = installedPlugins.Select(plugin => plugin.FilePath).ToArray();
//var files = Directory.GetFiles(_SearchPath, $"*.{_FileExtension}", SearchOption.TopDirectoryOnly);
foreach (var file in files)
{
try
@@ -53,6 +38,7 @@ internal class Loader
await LoadEverythingOfType<DBEvent>();
await LoadEverythingOfType<DBCommand>();
await LoadEverythingOfType<DBSlashCommand>();
await LoadEverythingOfType<ICommandAction>();
}
private async Task LoadEverythingOfType<T>()
@@ -77,6 +63,7 @@ internal class Loader
DBEvent => PluginType.EVENT,
DBCommand => PluginType.COMMAND,
DBSlashCommand => PluginType.SLASH_COMMAND,
ICommandAction => PluginType.ACTION,
_ => PluginType.UNKNOWN
};

View File

@@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using Discord.WebSocket;
using DiscordBotCore.Interfaces;
@@ -17,13 +19,17 @@ public class PluginLoader
public delegate void SlashCommandLoaded(PluginLoadResultData resultData);
public delegate void ActionLoaded(PluginLoadResultData resultData);
public CommandLoaded? OnCommandLoaded;
public EventLoaded? OnEventLoaded;
public SlashCommandLoaded? OnSlashCommandLoaded;
public ActionLoaded? OnActionLoaded;
public static List<DBCommand> Commands { get; private set; } = new List<DBCommand>();
public static List<DBEvent> Events { get; private set; } = new List<DBEvent>();
public static List<DBSlashCommand> SlashCommands { get; private set; } = new List<DBSlashCommand>();
public static List<ICommandAction> Actions { get; private set; } = new List<ICommandAction>();
public PluginLoader(DiscordSocketClient discordSocketClient)
{
@@ -34,9 +40,7 @@ public class PluginLoader
{
Application.CurrentApplication.Logger.Log("Loading plugins...", this);
var loader = new Loader(Application.CurrentApplication.ApplicationEnvironmentVariables["PluginFolder"], "dll");
//await this.ResetSlashCommands();
var loader = new Loader();
loader.OnFileLoadedException += FileLoadedException;
loader.OnPluginLoaded += OnPluginLoaded;
@@ -53,6 +57,17 @@ public class PluginLoader
{
switch (result.PluginType)
{
case PluginType.ACTION:
ICommandAction action = (ICommandAction)result.Plugin;
if (action.RunType == InternalActionRunType.ON_STARTUP || action.RunType == InternalActionRunType.BOTH)
action.ExecuteStartup();
if(action.RunType == InternalActionRunType.ON_CALL || action.RunType == InternalActionRunType.BOTH)
Actions.Add(action);
OnActionLoaded?.Invoke(result);
break;
case PluginType.COMMAND:
Commands.Add((DBCommand)result.Plugin);
OnCommandLoaded?.Invoke(result);

View File

@@ -9,6 +9,44 @@ namespace DiscordBotCore.Online.Helpers;
internal static class OnlineFunctions
{
/// <summary>
/// Copy one Stream to another <see langword="async" />
/// </summary>
/// <param name="stream">The base stream</param>
/// <param name="destination">The destination stream</param>
/// <param name="bufferSize">The buffer to read</param>
/// <param name="progress">The progress</param>
/// <param name="cancellationToken">The cancellation token</param>
/// <exception cref="ArgumentNullException">Triggered if any <see cref="Stream" /> is empty</exception>
/// <exception cref="ArgumentOutOfRangeException">Triggered if <paramref name="bufferSize" /> is less then or equal to 0</exception>
/// <exception cref="InvalidOperationException">Triggered if <paramref name="stream" /> is not readable</exception>
/// <exception cref="ArgumentException">Triggered in <paramref name="destination" /> is not writable</exception>
public static async Task CopyToOtherStreamAsync(
this Stream stream, Stream destination, int bufferSize,
IProgress<long>? progress = null,
CancellationToken cancellationToken = default)
{
if (stream == null) throw new ArgumentNullException(nameof(stream));
if (destination == null) throw new ArgumentNullException(nameof(destination));
if (bufferSize <= 0) throw new ArgumentOutOfRangeException(nameof(bufferSize));
if (!stream.CanRead) throw new InvalidOperationException("The stream is not readable.");
if (!destination.CanWrite)
throw new ArgumentException("Destination stream is not writable", nameof(destination));
var buffer = new byte[bufferSize];
long totalBytesRead = 0;
int bytesRead;
while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken)
.ConfigureAwait(false)) != 0)
{
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
totalBytesRead += bytesRead;
progress?.Report(totalBytesRead);
}
}
/// <summary>
/// Downloads a <see cref="Stream" /> and saves it to another <see cref="Stream" />.
/// </summary>

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -88,7 +89,11 @@ public class PluginManager
public async Task AppendPluginToDatabase(PluginInfo pluginData)
{
List<PluginInfo> installedPlugins = await JsonManager.ConvertFromJson<List<PluginInfo>>(await File.ReadAllTextAsync(Application.CurrentApplication.PluginDatabase));
foreach (var dependency in pluginData.ListOfDependancies)
{
pluginData.ListOfDependancies[dependency.Key] = GenerateDependencyLocation(pluginData.PluginName, dependency.Value);
}
installedPlugins.Add(pluginData);
await JsonManager.SaveToJsonFile(Application.CurrentApplication.PluginDatabase, installedPlugins);
}
@@ -123,15 +128,19 @@ public class PluginManager
public async Task<bool> MarkPluginToUninstall(string pluginName)
{
List<PluginInfo> installedPlugins = await GetInstalledPlugins();
PluginInfo? info = installedPlugins.Find(info => info.PluginName == pluginName);
IEnumerable<PluginInfo> installedPlugins = await GetInstalledPlugins();
IEnumerable<PluginInfo> info = installedPlugins.Where(info => info.PluginName == pluginName).AsEnumerable();
if(info == null)
if(!info.Any())
return false;
await RemovePluginFromDatabase(pluginName);
info.IsMarkedToUninstall = true;
await AppendPluginToDatabase(info);
foreach (var item in info)
{
await RemovePluginFromDatabase(item.PluginName);
item.IsMarkedToUninstall = true;
await AppendPluginToDatabase(item);
}
return true;
@@ -139,11 +148,11 @@ public class PluginManager
public async Task UninstallMarkedPlugins()
{
List<PluginInfo> installedPlugins = await GetInstalledPlugins();
foreach(PluginInfo plugin in installedPlugins)
{
if(!plugin.IsMarkedToUninstall) continue;
IEnumerable<PluginInfo> installedPlugins = (await GetInstalledPlugins()).AsEnumerable();
IEnumerable<PluginInfo> pluginsToRemove = installedPlugins.Where(plugin => plugin.IsMarkedToUninstall).AsEnumerable();
foreach (var plugin in pluginsToRemove)
{
await UninstallPlugin(plugin);
}
}
@@ -171,21 +180,6 @@ public class PluginManager
throw new Exception("Dependency not found");
}
public async Task<string> GetDependencyLocation(string pluginName, string dependencyName)
{
PluginOnlineInfo? pluginData = await GetPluginDataByName(pluginName);
if(pluginData == null)
throw new Exception("Plugin not found");
var dependency = pluginData.Dependencies.Find(dep => dep.DependencyName == dependencyName);
if(dependency == null)
throw new Exception("Dependency not found");
return dependency.DownloadLocation;
}
public string GenerateDependencyLocation(string pluginName, string dependencyName)
{
return Path.Combine(Environment.CurrentDirectory, $"Libraries/{pluginName}/{dependencyName}");

View File

@@ -8,31 +8,36 @@ namespace DiscordBotCore.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);
}
private Dictionary<string, ICommandAction> Actions = new();
public async Task Initialize()
{
var loadedActions = await _loader.Load();
Actions.Clear();
PluginLoader.Actions.ForEach(action =>
{
if (action.RunType == InternalActionRunType.ON_CALL || action.RunType == InternalActionRunType.BOTH)
{
if (this.Actions.ContainsKey(action.ActionName))
return; // ingore duplicates
if (loadedActions == null)
return;
foreach (var action in loadedActions)
Actions.TryAdd(action.ActionName, action);
this.Actions.Add(action.ActionName, action);
}
});
}
public async Task Refresh()
public IReadOnlyCollection<ICommandAction> GetActions()
{
Actions.Clear();
await Initialize();
return Actions.Values;
}
public bool Exists(string actionName)
{
return Actions.ContainsKey(actionName);
}
public ICommandAction GetAction(string actionName)
{
return Actions[actionName];
}
public async Task<bool> Execute(string actionName, params string[]? args)

View File

@@ -40,5 +40,6 @@ public enum PluginType
UNKNOWN,
COMMAND,
EVENT,
SLASH_COMMAND
SLASH_COMMAND,
ACTION
}

View File

@@ -1,83 +0,0 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Discord;
namespace DiscordBotCore.Others;
/// <summary>
/// A special class with functions
/// </summary>
public static class Functions
{
/// <summary>
/// The location for the Resources folder
/// String: ./Data/Resources/
/// </summary>
public static string dataFolder => Application.CurrentApplication.DataFolder;
public static Color RandomColor
{
get
{
var random = new Random();
return new Color(random.Next(0, 255), random.Next(0, 255), random.Next(0, 255));
}
}
/// <summary>
/// Copy one Stream to another <see langword="async" />
/// </summary>
/// <param name="stream">The base stream</param>
/// <param name="destination">The destination stream</param>
/// <param name="bufferSize">The buffer to read</param>
/// <param name="progress">The progress</param>
/// <param name="cancellationToken">The cancellation token</param>
/// <exception cref="ArgumentNullException">Triggered if any <see cref="Stream" /> is empty</exception>
/// <exception cref="ArgumentOutOfRangeException">Triggered if <paramref name="bufferSize" /> is less then or equal to 0</exception>
/// <exception cref="InvalidOperationException">Triggered if <paramref name="stream" /> is not readable</exception>
/// <exception cref="ArgumentException">Triggered in <paramref name="destination" /> is not writable</exception>
public static async Task CopyToOtherStreamAsync(
this Stream stream, Stream destination, int bufferSize,
IProgress<long>? progress = null,
CancellationToken cancellationToken = default)
{
if (stream == null) throw new ArgumentNullException(nameof(stream));
if (destination == null) throw new ArgumentNullException(nameof(destination));
if (bufferSize <= 0) throw new ArgumentOutOfRangeException(nameof(bufferSize));
if (!stream.CanRead) throw new InvalidOperationException("The stream is not readable.");
if (!destination.CanWrite)
throw new ArgumentException("Destination stream is not writable", nameof(destination));
var buffer = new byte[bufferSize];
long totalBytesRead = 0;
int bytesRead;
while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken)
.ConfigureAwait(false)) != 0)
{
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
totalBytesRead += bytesRead;
progress?.Report(totalBytesRead);
}
}
public static T SelectRandomValueOf<T>()
{
var enums = Enum.GetValues(typeof(T));
var random = new Random();
return (T)enums.GetValue(random.Next(enums.Length));
}
public static T RandomValue<T>(this T[] values)
{
Random random = new();
return values[random.Next(values.Length)];
}
public static string ToResourcesPath(this string path)
{
return Path.Combine(dataFolder, path);
}
}