Redesigned the DiscordBotCore by splitting it into multiple projects. Created a WebUI and preparing to remove the DiscordBot application

This commit is contained in:
2025-04-04 22:07:30 +03:00
parent 62ba5ec63d
commit a4afb28f36
2290 changed files with 76694 additions and 17052 deletions

View File

@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\DiscordBotCore.Configuration\DiscordBotCore.Configuration.csproj" />
<ProjectReference Include="..\DiscordBotCore.Logging\DiscordBotCore.Logging.csproj" />
<ProjectReference Include="..\DiscordBotCore.PluginCore\DiscordBotCore.PluginCore.csproj" />
<ProjectReference Include="..\DiscordBotCore.PluginManagement\DiscordBotCore.PluginManagement.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,9 @@
namespace DiscordBotCore.PluginManagement.Loading.Exceptions;
public class PluginNotFoundException : Exception
{
public PluginNotFoundException(string pluginName) : base($"Plugin {pluginName} was not found") { }
public PluginNotFoundException(string pluginName, string url, string branch) :
base ($"Plugin {pluginName} was not found on {url} (branch: {branch}") { }
}

View File

@@ -0,0 +1,12 @@
using DiscordBotCore.PluginCore;
using DiscordBotCore.PluginCore.Interfaces;
namespace DiscordBotCore.PluginManagement.Loading;
public interface IPluginLoader
{
List<IDbCommand> Commands { get; }
List<IDbEvent> Events { get; }
List<IDbSlashCommand> SlashCommands { get; }
Task LoadPlugins();
}

View File

@@ -0,0 +1,81 @@
using System.Reflection;
using DiscordBotCore.PluginCore;
using DiscordBotCore.PluginCore.Interfaces;
using DiscordBotCore.PluginManagement.Loading.Exceptions;
namespace DiscordBotCore.PluginManagement.Loading;
internal class Loader
{
internal delegate void FileLoadedHandler(string fileName, Exception exception);
internal delegate void PluginLoadedHandler(PluginLoaderResult result);
internal event FileLoadedHandler? OnFileLoadedException;
internal event PluginLoadedHandler? OnPluginLoaded;
private readonly IPluginManager _pluginManager;
internal Loader(IPluginManager manager)
{
_pluginManager = manager;
}
internal async Task Load()
{
var installedPlugins = await _pluginManager.GetInstalledPlugins();
var files = installedPlugins.Where(plugin => plugin.IsEnabled).Select(plugin => plugin.FilePath).ToArray();
foreach (var file in files)
{
try
{
Assembly.LoadFrom(file);
}
catch
{
OnFileLoadedException?.Invoke(file, new Exception($"Failed to load plugin from file {file}"));
}
}
await LoadEverythingOfType<IDbEvent>();
await LoadEverythingOfType<IDbCommand>();
await LoadEverythingOfType<IDbSlashCommand>();
}
private Task LoadEverythingOfType<T>()
{
var types = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(s => s.GetTypes())
.Where(p => typeof(T).IsAssignableFrom(p) && !p.IsInterface);
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}]");
}
PluginLoaderResult result = plugin switch
{
IDbEvent @event => PluginLoaderResult.FromIDbEvent(@event),
IDbCommand command => PluginLoaderResult.FromIDbCommand(command),
IDbSlashCommand command => PluginLoaderResult.FromIDbSlashCommand(command),
_ => PluginLoaderResult.FromException(new PluginNotFoundException($"Unknown plugin type {plugin.GetType().FullName}"))
};
OnPluginLoaded?.Invoke(result);
}
catch (Exception ex)
{
OnPluginLoaded?.Invoke(PluginLoaderResult.FromException(ex));
}
}
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,192 @@
using System.Net.Mime;
using Discord;
using Discord.WebSocket;
using DiscordBotCore.Configuration;
using DiscordBotCore.Logging;
using DiscordBotCore.PluginCore;
using DiscordBotCore.PluginCore.Helpers;
using DiscordBotCore.PluginCore.Helpers.Execution.DbEvent;
using DiscordBotCore.PluginCore.Interfaces;
using DiscordBotCore.Utilities;
namespace DiscordBotCore.PluginManagement.Loading;
public sealed class PluginLoader : IPluginLoader
{
private readonly DiscordSocketClient _DiscordClient;
private readonly IPluginManager _PluginManager;
private readonly ILogger _Logger;
private readonly IConfiguration _Configuration;
public delegate void CommandLoaded(IDbCommand eCommand);
public delegate void EventLoaded(IDbEvent eEvent);
public delegate void SlashCommandLoaded(IDbSlashCommand eSlashCommand);
public CommandLoaded? OnCommandLoaded;
public EventLoaded? OnEventLoaded;
public SlashCommandLoaded? OnSlashCommandLoaded;
public List<IDbCommand> Commands { get; private set; } = new List<IDbCommand>();
public List<IDbEvent> Events { get; private set; } = new List<IDbEvent>();
public List<IDbSlashCommand> SlashCommands { get; private set; } = new List<IDbSlashCommand>();
public PluginLoader(IPluginManager pluginManager, ILogger logger, IConfiguration configuration, DiscordSocketClient discordSocketDiscordClient)
{
_PluginManager = pluginManager;
_DiscordClient = discordSocketDiscordClient;
_Logger = logger;
_Configuration = configuration;
}
public async Task LoadPlugins()
{
Commands.Clear();
Events.Clear();
SlashCommands.Clear();
_Logger.Log("Loading plugins...", this);
var loader = new Loader(_PluginManager);
loader.OnFileLoadedException += FileLoadedException;
loader.OnPluginLoaded += OnPluginLoaded;
await loader.Load();
}
private void FileLoadedException(string fileName, Exception exception)
{
_Logger.LogException(exception, this);
}
private void InitializeDbCommand(IDbCommand command)
{
Commands.Add(command);
OnCommandLoaded?.Invoke(command);
}
private void InitializeEvent(IDbEvent eEvent)
{
if (!TryStartEvent(eEvent))
{
return;
}
Events.Add(eEvent);
OnEventLoaded?.Invoke(eEvent);
}
private async void InitializeSlashCommand(IDbSlashCommand slashCommand)
{
Result result = await TryStartSlashCommand(slashCommand);
result.Match(
() =>
{
if (slashCommand.HasInteraction)
_DiscordClient.InteractionCreated += interaction => slashCommand.ExecuteInteraction(_Logger, interaction);
SlashCommands.Add(slashCommand);
OnSlashCommandLoaded?.Invoke(slashCommand);
},
HandleError
);
}
private void HandleError(Exception exception)
{
_Logger.LogException(exception, this);
}
private void OnPluginLoaded(PluginLoaderResult result)
{
result.Match(
InitializeDbCommand,
InitializeEvent,
InitializeSlashCommand,
HandleError
);
}
private bool TryStartEvent(IDbEvent? dbEvent)
{
try
{
if (dbEvent is null)
{
throw new ArgumentNullException(nameof(dbEvent));
}
IDbEventExecutingArgument args = new DbEventExecutingArgument(
_Logger,
_DiscordClient,
_Configuration.Get<string>("prefix"),
new DirectoryInfo(Path.Combine(_Configuration.Get<string>("ResourcesPath"), dbEvent.Name)));
dbEvent.Start(args);
return true;
}
catch (Exception e)
{
_Logger.Log($"Error starting event {dbEvent.Name}: {e.Message}", typeof(PluginLoader), LogType.Error);
_Logger.LogException(e, typeof(PluginLoader));
return false;
}
}
private async Task<Result> TryStartSlashCommand(IDbSlashCommand? dbSlashCommand)
{
try
{
if (dbSlashCommand is null)
{
return Result.Failure(new Exception("dbSlashCommand is null"));
}
if (_DiscordClient.Guilds.Count == 0)
{
return Result.Failure(new Exception("No guilds found"));
}
var builder = new SlashCommandBuilder();
builder.WithName(dbSlashCommand.Name);
builder.WithDescription(dbSlashCommand.Description);
builder.Options = dbSlashCommand.Options;
if (dbSlashCommand.CanUseDm)
builder.WithContextTypes(InteractionContextType.BotDm, InteractionContextType.Guild);
else
builder.WithContextTypes(InteractionContextType.Guild);
List<ulong> serverIds = _Configuration.GetList("ServerIds", new List<ulong>());
foreach(ulong guildId in serverIds)
{
bool result = await EnableSlashCommandPerGuild(guildId, builder);
if (!result)
{
return Result.Failure($"Failed to enable slash command {dbSlashCommand.Name} for guild {guildId}");
}
}
await _DiscordClient.CreateGlobalApplicationCommandAsync(builder.Build());
return Result.Success();
}
catch (Exception e)
{
return Result.Failure("Error starting slash command");
}
}
private async Task<bool> EnableSlashCommandPerGuild(ulong guildId, SlashCommandBuilder builder)
{
SocketGuild? guild = _DiscordClient.GetGuild(guildId);
if (guild is null)
{
_Logger.Log("Failed to get guild with ID " + guildId, typeof(PluginLoader), LogType.Error);
return false;
}
await guild.CreateApplicationCommandAsync(builder.Build());
return true;
}
}

View File

@@ -0,0 +1,37 @@
using DiscordBotCore.PluginCore;
using DiscordBotCore.PluginCore.Interfaces;
using DiscordBotCore.Utilities;
namespace DiscordBotCore.PluginManagement.Loading;
public class PluginLoaderResult
{
private Option3<IDbCommand, IDbEvent, IDbSlashCommand, Exception> _Result;
public static PluginLoaderResult FromIDbCommand(IDbCommand command) => new PluginLoaderResult(new Option3<IDbCommand, IDbEvent, IDbSlashCommand, Exception>(command));
public static PluginLoaderResult FromIDbEvent(IDbEvent dbEvent) => new PluginLoaderResult(new Option3<IDbCommand, IDbEvent, IDbSlashCommand, Exception>(dbEvent));
public static PluginLoaderResult FromIDbSlashCommand(IDbSlashCommand slashCommand) => new PluginLoaderResult(new Option3<IDbCommand, IDbEvent, IDbSlashCommand, Exception>(slashCommand));
public static PluginLoaderResult FromException(Exception exception) => new PluginLoaderResult(new Option3<IDbCommand, IDbEvent, IDbSlashCommand, Exception>(exception));
private PluginLoaderResult(Option3<IDbCommand, IDbEvent, IDbSlashCommand, Exception> result)
{
_Result = result;
}
public void Match(Action<IDbCommand> commandAction, Action<IDbEvent> eventAction, Action<IDbSlashCommand> slashCommandAction,
Action<Exception> exceptionAction)
{
_Result.Match(commandAction, eventAction, slashCommandAction, exceptionAction);
}
public TResult Match<TResult>(Func<IDbCommand, TResult> commandFunc, Func<IDbEvent, TResult> eventFunc,
Func<IDbSlashCommand, TResult> slashCommandFunc,
Func<Exception, TResult> exceptionFunc)
{
return _Result.Match(commandFunc, eventFunc, slashCommandFunc, exceptionFunc);
}
}