using System.Collections.ObjectModel; using System.Reflection; using Discord; using Discord.WebSocket; using DiscordBotCore.Configuration; using DiscordBotCore.Logging; using DiscordBotCore.PluginCore.Helpers.Execution.DbEvent; using DiscordBotCore.PluginCore.Interfaces; using DiscordBotCore.PluginManagement.Loading.Exceptions; using DiscordBotCore.Utilities.Responses; namespace DiscordBotCore.PluginManagement.Loading; public class PluginLoader : IPluginLoader { private static readonly string _HelpCommandNamespaceFullName = "DiscordBotCore.Commands.HelpCommand"; private readonly IPluginManager _PluginManager; private readonly ILogger _Logger; private readonly IConfiguration _Configuration; private DiscordSocketClient? _DiscordClient; private PluginLoaderContext? PluginLoaderContext; private readonly List _Commands = new List(); private readonly List _Events = new List(); private readonly List _SlashCommands = new List(); private readonly List _ApplicationCommands = new List(); public PluginLoader(IPluginManager pluginManager, ILogger logger, IConfiguration configuration) { _PluginManager = pluginManager; _Logger = logger; _Configuration = configuration; } public IReadOnlyList Commands => _Commands; public IReadOnlyList Events => _Events; public IReadOnlyList SlashCommands => _SlashCommands; public void SetDiscordClient(DiscordSocketClient discordSocketClient) { if (_DiscordClient is not null && discordSocketClient == _DiscordClient) { _Logger.Log("A client is already set. Please set the client only once.", this, LogType.Warning); return; } if (discordSocketClient.LoginState != LoginState.LoggedIn) { _Logger.Log("The client must be logged in before setting it.", this, LogType.Error); return; } _DiscordClient = discordSocketClient; } public async Task LoadPlugins() { if (PluginLoaderContext is not null) { _Logger.Log("The plugins are already loaded", this, LogType.Error); return; } _Events.Clear(); _Commands.Clear(); _SlashCommands.Clear(); _ApplicationCommands.Clear(); await LoadPluginFiles(); LoadEverythingOfType(); var helpCommand = AppDomain.CurrentDomain.GetAssemblies() .FirstOrDefault(assembly => assembly.DefinedTypes.Any(type => type.FullName == _HelpCommandNamespaceFullName) && assembly.FullName != null && assembly.FullName.StartsWith("DiscordBotCore")); if (helpCommand is not null) { var helpCommandType = helpCommand.DefinedTypes.FirstOrDefault(type => type.FullName == _HelpCommandNamespaceFullName && typeof(IDbCommand).IsAssignableFrom(type)); if (helpCommandType is not null) { InitializeType(helpCommandType); } } LoadEverythingOfType(); LoadEverythingOfType(); _Logger.Log("Loaded plugins", this); } public async Task UnloadAllPlugins() { if (PluginLoaderContext is null) { _Logger.Log("The plugins are not loaded. Please load the plugins before unloading them.", this, LogType.Error); return; } await UnloadSlashCommands(); PluginLoaderContext.Unload(); PluginLoaderContext = null; GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); } private async Task UnloadSlashCommands() { if (_DiscordClient is null) { _Logger.Log("The client is not set. Please set the client before unloading slash commands.", this, LogType.Error); return; } foreach (SocketApplicationCommand command in _ApplicationCommands) { await command.DeleteAsync(); } _ApplicationCommands.Clear(); _Logger.Log("Unloaded all slash commands", this); } private async Task LoadPluginFiles() { var installedPlugins = await _PluginManager.GetInstalledPlugins(); if (installedPlugins.Count == 0) { _Logger.Log("No plugin files found. Please check the plugin files.", this, LogType.Error); return; } var files = installedPlugins.Where(plugin => plugin.IsEnabled).Select(plugin => plugin.FilePath); PluginLoaderContext = new PluginLoaderContext(_Logger, "PluginLoader"); foreach (var file in files) { string fullFilePath = Path.GetFullPath(file); if (string.IsNullOrEmpty(fullFilePath)) { _Logger.Log("The file path is empty. Please check the plugin file path.", PluginLoaderContext, LogType.Error); continue; } if (!File.Exists(fullFilePath)) { _Logger.Log("The file does not exist. Please check the plugin file path.", PluginLoaderContext, LogType.Error); continue; } try { PluginLoaderContext.LoadFromAssemblyPath(fullFilePath); } catch (Exception ex) { _Logger.LogException(ex, this); } } _Logger.Log($"Loaded {PluginLoaderContext.Assemblies.Count()} assemblies", this); } private void LoadEverythingOfType() { if (PluginLoaderContext is null) { _Logger.Log("The plugins are not loaded. Please load the plugins before loading them.", this, LogType.Error); return; } var types = PluginLoaderContext.Assemblies .SelectMany(s => s.GetTypes()) .Where(p => typeof(T).IsAssignableFrom(p) && !p.IsInterface); foreach (var type in types) { InitializeType(type); } } private void InitializeType(Type type) { T? plugin = (T?)Activator.CreateInstance(type); if (plugin is null) { _Logger.Log($"Failed to create instance of plugin with type {type.FullName} [{type.Assembly}]", this, LogType.Error); } switch (plugin) { case IDbEvent dbEvent: InitializeEvent(dbEvent); break; case IDbCommand dbCommand: InitializeDbCommand(dbCommand); break; case IDbSlashCommand dbSlashCommand: InitializeSlashCommand(dbSlashCommand); break; default: throw new PluginNotFoundException($"Unknown plugin type {plugin.GetType().FullName}"); } } private void InitializeDbCommand(IDbCommand command) { _Commands.Add(command); _Logger.Log("Command loaded: " + command.Command, this); } private void InitializeEvent(IDbEvent eEvent) { if (!TryStartEvent(eEvent)) { return; } _Events.Add(eEvent); _Logger.Log("Event loaded: " + eEvent, this); } private async void InitializeSlashCommand(IDbSlashCommand slashCommand) { bool result = await TryStartSlashCommand(slashCommand); if (!result) { return; } if (_DiscordClient is null) { return; } if (slashCommand.HasInteraction) { _DiscordClient.InteractionCreated += interaction => slashCommand.ExecuteInteraction(_Logger, interaction); } _SlashCommands.Add(slashCommand); _Logger.Log("Slash command loaded: " + slashCommand.Name, this); } private bool TryStartEvent(IDbEvent dbEvent) { string? botPrefix = _Configuration.Get("prefix"); if (string.IsNullOrEmpty(botPrefix)) { _Logger.Log("Bot prefix is not set. Please set the bot prefix in the configuration.", this, LogType.Error); return false; } if (_DiscordClient is null) { _Logger.Log("Discord client is not set. Please set the discord client before starting events.", this, LogType.Error); return false; } string? resourcesFolder = _Configuration.Get("ResourcesFolder"); if (string.IsNullOrEmpty(resourcesFolder)) { _Logger.Log("Resources folder is not set. Please set the resources folder in the configuration.", this, LogType.Error); return false; } if (!Directory.Exists(resourcesFolder)) { _Logger.Log("Resources folder does not exist. Please create the resources folder.", this, LogType.Error); return false; } string? eventConfigDirectory = Path.Combine(resourcesFolder, dbEvent.GetType().Assembly.GetName().Name); Directory.CreateDirectory(eventConfigDirectory); IDbEventExecutingArgument args = new DbEventExecutingArgument( _Logger, _DiscordClient, botPrefix, new DirectoryInfo(eventConfigDirectory)); dbEvent.Start(args); return true; } private async Task TryStartSlashCommand(IDbSlashCommand? dbSlashCommand) { if (dbSlashCommand is null) { _Logger.Log("The loaded slash command was null. Please check the plugin.", this, LogType.Error); return false; } if (_DiscordClient is null) { _Logger.Log("The client is not set. Please set the client before starting slash commands.", this, LogType.Error); return false; } if (_DiscordClient.Guilds.Count == 0) { _Logger.Log("The client is not connected to any guilds. Please check the client.", this, LogType.Error); return false; } var builder = new SlashCommandBuilder(); builder.WithName(dbSlashCommand.Name); builder.WithDescription(dbSlashCommand.Description); builder.Options = dbSlashCommand.Options; if (dbSlashCommand.CanUseDm) builder.WithContextTypes(InteractionContextType.BotDm, InteractionContextType.Guild); else builder.WithContextTypes(InteractionContextType.Guild); List serverIds = _Configuration.GetList("ServerIds", new List()); if (serverIds.Any()) { foreach(ulong guildId in serverIds) { IResponse result = await EnableSlashCommandPerGuild(guildId, builder); if (!result.IsSuccess) { _Logger.Log($"Failed to enable slash command {dbSlashCommand.Name} for guild {guildId}", this, LogType.Error); continue; } if (result.Data is null) { continue; } _ApplicationCommands.Add(result.Data); } return true; } var command = await _DiscordClient.CreateGlobalApplicationCommandAsync(builder.Build()); _ApplicationCommands.Add(command); return true; } private async Task> EnableSlashCommandPerGuild(ulong guildId, SlashCommandBuilder builder) { SocketGuild? guild = _DiscordClient?.GetGuild(guildId); if (guild is null) { _Logger.Log("Failed to get guild with ID " + guildId, this, LogType.Error); return Response.Failure("Failed to get guild with ID " + guildId); } var command = await guild.CreateApplicationCommandAsync(builder.Build()); return Response.Success(command); } }