Renamed PluginManager to DiscordBotCore.

Revamped the Logger
This commit is contained in:
2024-05-12 20:10:52 +03:00
parent 413d465d7f
commit 17147d920d
66 changed files with 529 additions and 556 deletions
+99
View File
@@ -0,0 +1,99 @@
using DiscordBotCore.Online;
using DiscordBotCore.Others;
using DiscordBotCore.Others.Actions;
using DiscordBotCore.Others.Logger;
using DiscordBotCore.Plugin;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
namespace DiscordBotCore
{
public class Application
{
public static Application CurrentApplication { get; private set; } = null;
private static readonly string _DefaultLogMessageFormat = "{SenderName} {Message}";
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 readonly string _MaxParallelDownloads = "3";
public string ServerID => ApplicationEnvironmentVariables["ServerID"];
public string PluginDatabase => ApplicationEnvironmentVariables["PluginDatabase"];
public string LogFile => $"{ApplicationEnvironmentVariables["LogFolder"]}/{DateTime.Now.ToLongDateString().Replace(" / ", "")}.log";
public SettingsDictionary<string, string> ApplicationEnvironmentVariables { get; private set; }
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 static async Task CreateApplication()
{
if (CurrentApplication is not null)
return;
CurrentApplication = new Application();
Directory.CreateDirectory(_ResourcesFolder);
Directory.CreateDirectory(_PluginsFolder);
Directory.CreateDirectory(_ArchivesFolder);
Directory.CreateDirectory(_LogsFolder);
CurrentApplication.ApplicationEnvironmentVariables = new SettingsDictionary<string, string>(_ConfigFile);
bool result = await CurrentApplication.ApplicationEnvironmentVariables.LoadFromFile();
if(!result)
{
PopulateEnvWithDefault();
File.Delete(_ConfigFile);
await CurrentApplication.ApplicationEnvironmentVariables.SaveToFile();
}
CurrentApplication.Logger = CurrentApplication.ApplicationEnvironmentVariables.ContainsKey("LogMessageFormat") ?
new Logger(CurrentApplication.ApplicationEnvironmentVariables["LogMessageFormat"]) :
new Logger(_DefaultLogMessageFormat);
if (!File.Exists(_PluginsDatabaseFile))
{
List<PluginInfo> plugins = new();
await JsonManager.SaveToJsonFile(_PluginsDatabaseFile, plugins);
}
CurrentApplication.PluginManager = new PluginManager();
await CurrentApplication.PluginManager.UninstallMarkedPlugins();
await CurrentApplication.PluginManager.CheckForUpdates();
CurrentApplication.InternalActionManager = new InternalActionManager(CurrentApplication.ApplicationEnvironmentVariables["PluginFolder"], "*.dll");
await CurrentApplication.InternalActionManager.Initialize();
}
private static void PopulateEnvWithDefault()
{
if (CurrentApplication is null)
return;
if (CurrentApplication.ApplicationEnvironmentVariables is null)
return;
CurrentApplication.ApplicationEnvironmentVariables["LogFolder"] = _LogsFolder;
CurrentApplication.ApplicationEnvironmentVariables["PluginFolder"] = _PluginsFolder;
CurrentApplication.ApplicationEnvironmentVariables["ArchiveFolder"] = _ArchivesFolder;
CurrentApplication.ApplicationEnvironmentVariables["PluginDatabase"] = _PluginsDatabaseFile;
CurrentApplication.ApplicationEnvironmentVariables["LogMessageFormat"] = _DefaultLogMessageFormat;
CurrentApplication.ApplicationEnvironmentVariables["MaxParallelDownloads"] = _MaxParallelDownloads;
}
}
}
+153
View File
@@ -0,0 +1,153 @@
using System;
using System.Threading.Tasks;
using Discord;
using Discord.Commands;
using Discord.WebSocket;
using DiscordBotCore.Others;
namespace DiscordBotCore.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();
Application.CurrentApplication.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"))
{
Application.CurrentApplication.ApplicationEnvironmentVariables.Remove("token");
Application.CurrentApplication.Logger.Log("The token is invalid. Please restart the bot and follow the instructions", typeof(Boot), LogType.CRITICAL);
await Application.CurrentApplication.ApplicationEnvironmentVariables.SaveToFile();
Environment.Exit(0);
}
}
private async Task Client_LoggedOut()
{
Application.CurrentApplication.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()
{
Application.CurrentApplication.Logger.Log("Successfully Logged In", typeof(Boot));
return Task.CompletedTask;
}
private Task Log(LogMessage message)
{
switch (message.Severity)
{
case LogSeverity.Error:
case LogSeverity.Critical:
Application.CurrentApplication.Logger.Log(message.Message, typeof(Boot), LogType.ERROR);
break;
case LogSeverity.Info:
case LogSeverity.Debug:
Application.CurrentApplication.Logger.Log(message.Message, typeof(Boot), LogType.INFO);
break;
}
return Task.CompletedTask;
}
}
+158
View File
@@ -0,0 +1,158 @@
using System;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Discord.Commands;
using Discord.WebSocket;
using DiscordBotCore.Interfaces;
using DiscordBotCore.Loaders;
using DiscordBotCore.Others;
using DiscordBotCore.Others.Permissions;
namespace DiscordBotCore.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)
{
Application.CurrentApplication.Logger.LogException(ex, this);
}
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);
Application.CurrentApplication.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)
{
Application.CurrentApplication.Logger.LogException(ex, this);
}
}
}
+644
View File
@@ -0,0 +1,644 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SQLite;
using System.IO;
using System.Threading.Tasks;
namespace DiscordBotCore.Database;
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)
{
if (!fileName.StartsWith("./Data/Resources/"))
fileName = Path.Combine("./Data/Resources", fileName);
if (!File.Exists(fileName))
SQLiteConnection.CreateFile(fileName);
var connectionString = $"URI=file:{fileName}";
_connection = new SQLiteConnection(connectionString);
}
/// <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++)
{
query += $"'{values[i]}'";
if (i != values.Length - 1)
query += ", ";
}
query += ")";
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 += ", ";
}
query += ")";
var command = new SQLiteCommand(query, _connection);
command.ExecuteNonQuery();
}
/// <summary>
/// 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}'";
var command = new SQLiteCommand(query, _connection);
await command.ExecuteNonQueryAsync();
}
/// <summary>
/// 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 void RemoveKey(string tableName, string KeyName, string KeyValue)
{
var query = $"DELETE FROM {tableName} WHERE {KeyName} = '{KeyValue}'";
var command = new SQLiteCommand(query, _connection);
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}'";
if (await ReadDataAsync(query) is not null)
return true;
return false;
}
/// <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 bool KeyExists(string tableName, string keyName, string KeyValue)
{
var query = $"SELECT * FROM {tableName} where {keyName} = '{KeyValue}'";
if (ReadData(query) is not null)
return true;
return false;
}
/// <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 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}'"
);
}
/// <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");
Execute($"UPDATE {tableName} SET {ResultColumnName}='{ResultColumnValue}' WHERE {keyName}='{KeyValue}'");
}
/// <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");
return await ReadDataAsync($"SELECT {ResultColumnName} FROM {tableName} WHERE {keyName}='{KeyValue}'");
}
/// <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 string? GetValue(string tableName, string keyName, string KeyValue, string ResultColumnName)
{
if (!TableExists(tableName))
throw new Exception($"Table {tableName} does not exist");
return ReadData($"SELECT {ResultColumnName} FROM {tableName} WHERE {keyName}='{KeyValue}'");
}
/// <summary>
/// Stop the connection to the SQL Database
/// </summary>
/// <returns></returns>
public async void Stop()
{
await _connection.CloseAsync();
}
/// <summary>
/// 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))
{
command.CommandText = $"ALTER TABLE {tableName} ADD COLUMN {column} {TYPE}";
await command.ExecuteNonQueryAsync();
}
}
/// <summary>
/// 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 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));
foreach (var column in columns)
if (!tableColumns.Contains(column))
{
command.CommandText = $"ALTER TABLE {tableName} ADD COLUMN {column} {TYPE}";
command.ExecuteNonQuery();
}
}
/// <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())
{
reader.GetValues(values);
return string.Join<object>(" ", values);
}
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())
{
reader.GetValues(values);
return string.Join<object>(" ", 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 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;
}
}
+24
View File
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<FileAlignment>512</FileAlignment>
<DebugType>portable</DebugType>
<DebugSymbols>false</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Config.cs" />
</ItemGroup>
<ItemGroup>
<None Remove="BlankWindow1.xaml" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Discord.Net" Version="3.14.1" />
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.118" />
</ItemGroup>
<ItemGroup>
<UpToDateCheckInput Remove="UI\Controls\MessageBox.axaml" />
</ItemGroup>
</Project>
+50
View File
@@ -0,0 +1,50 @@
using System.Collections.Generic;
using DiscordBotCore.Others;
namespace DiscordBotCore.Interfaces;
public interface DBCommand
{
/// <summary>
/// Command to be executed
/// It's CaSe SeNsItIvE
/// </summary>
string Command { get; }
/// <summary>
/// Command aliases. Users may use this to execute the command
/// </summary>
List<string>? Aliases { get; }
/// <summary>
/// Command description
/// </summary>
string Description { get; }
/// <summary>
/// The usage for your command.
/// It will be displayed when users type help
/// </summary>
string Usage { get; }
/// <summary>
/// true if the command requre admin, otherwise false
/// </summary>
bool requireAdmin { get; }
/// <summary>
/// The main body of the command. This is what is executed when user calls the command in Server
/// </summary>
/// <param name="args">The disocrd Context</param>
void ExecuteServer(DbCommandExecutingArguments args)
{
}
/// <summary>
/// The main body of the command. This is what is executed when user calls the command in DM
/// </summary>
/// <param name="args">The disocrd Context</param>
void ExecuteDM(DbCommandExecutingArguments args)
{
}
}
+22
View File
@@ -0,0 +1,22 @@
using Discord.WebSocket;
namespace DiscordBotCore.Interfaces;
public interface DBEvent
{
/// <summary>
/// The name of the event
/// </summary>
string Name { get; }
/// <summary>
/// The description of the event
/// </summary>
string Description { get; }
/// <summary>
/// The method that is invoked when the event is loaded into memory
/// </summary>
/// <param name="client">The discord bot client</param>
void Start(DiscordSocketClient client);
}
@@ -0,0 +1,24 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Discord;
using Discord.WebSocket;
namespace DiscordBotCore.Interfaces;
public interface DBSlashCommand
{
string Name { get; }
string Description { get; }
bool canUseDM { get; }
bool HasInteraction { get; }
List<SlashCommandOptionBuilder> Options { get; }
void ExecuteServer(SocketSlashCommand context)
{ }
void ExecuteDM(SocketSlashCommand context) { }
Task ExecuteInteraction(SocketInteraction interaction) => Task.CompletedTask;
}
@@ -0,0 +1,22 @@
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using DiscordBotCore.Others;
using DiscordBotCore.Others.Actions;
namespace DiscordBotCore.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);
}
@@ -0,0 +1,15 @@
using DiscordBotCore.Others;
using System;
namespace DiscordBotCore.Interfaces.Logger
{
public interface ILogMessage
{
public string Message { get; protected set; }
public DateTime ThrowTime { get; protected set; }
public string SenderName { get; protected set; }
public LogType LogMessageType { get; protected set; }
}
}
@@ -0,0 +1,19 @@
using DiscordBotCore.Others;
using System;
using System.Collections.Generic;
namespace DiscordBotCore.Interfaces.Logger
{
public interface ILogger
{
public struct FormattedMessage { public string Message; public LogType Type; }
public string LogMessageFormat { get; set; }
public void Log(ILogMessage message);
public void LogException(Exception exception, object Sender);
public event EventHandler<FormattedMessage> OnFormattedLog;
public event EventHandler<ILogMessage> OnRawLog;
}
}
@@ -0,0 +1,83 @@
using System;
namespace DiscordBotCore.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(Application.CurrentApplication.ApplicationEnvironmentVariables["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();
}
}
}
@@ -0,0 +1,17 @@
namespace DiscordBotCore.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();
}
@@ -0,0 +1,76 @@
using System;
namespace DiscordBotCore.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();
}
}
+72
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 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 = "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;
}
}
@@ -0,0 +1,21 @@
namespace DiscordBotCore.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;
}
}
+89
View File
@@ -0,0 +1,89 @@
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using DiscordBotCore.Interfaces;
using DiscordBotCore.Others;
namespace DiscordBotCore.Loaders;
internal class Loader
{
private readonly string _SearchPath;
private readonly string _FileExtension;
internal delegate void FileLoadedHandler(FileLoaderResult result);
internal delegate void PluginLoadedHandler(PluginLoadResultData result);
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 files = Directory.GetFiles(_SearchPath, $"*.{_FileExtension}", SearchOption.TopDirectoryOnly);
foreach (var file in files)
{
try
{
Assembly.LoadFrom(file);
}
catch
{
OnFileLoadedException?.Invoke(new FileLoaderResult(file, $"Failed to load file {file}"));
}
}
await LoadEverythingOfType<DBEvent>();
await LoadEverythingOfType<DBCommand>();
await LoadEverythingOfType<DBSlashCommand>();
}
private async 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}]");
}
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));
}
}
}
}
@@ -0,0 +1,23 @@
using DiscordBotCore.Others;
namespace DiscordBotCore.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;
}
}
+85
View File
@@ -0,0 +1,85 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Discord.WebSocket;
using DiscordBotCore.Interfaces;
using DiscordBotCore.Others;
namespace DiscordBotCore.Loaders;
public class PluginLoader
{
internal readonly DiscordSocketClient _Client;
public delegate void CommandLoaded(PluginLoadResultData resultData);
public delegate void EventLoaded(PluginLoadResultData resultData);
public delegate void SlashCommandLoaded(PluginLoadResultData resultData);
public CommandLoaded? OnCommandLoaded;
public EventLoaded? OnEventLoaded;
public SlashCommandLoaded? OnSlashCommandLoaded;
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 PluginLoader(DiscordSocketClient discordSocketClient)
{
_Client = discordSocketClient;
}
public async Task LoadPlugins()
{
Application.CurrentApplication.Logger.Log("Loading plugins...", typeof(PluginLoader));
var loader = new Loader(Application.CurrentApplication.ApplicationEnvironmentVariables["PluginFolder"], "dll");
//await this.ResetSlashCommands();
loader.OnFileLoadedException += FileLoadedException;
loader.OnPluginLoaded += OnPluginLoaded;
await loader.Load();
}
private void FileLoadedException(FileLoaderResult result)
{
Application.CurrentApplication.Logger.Log(result.ErrorMessage, typeof(PluginLoader), LogType.ERROR);
}
private async void OnPluginLoaded(PluginLoadResultData result)
{
switch (result.PluginType)
{
case PluginType.COMMAND:
Commands.Add((DBCommand)result.Plugin);
OnCommandLoaded?.Invoke(result);
break;
case PluginType.EVENT:
if (this.TryStartEvent((DBEvent)result.Plugin))
{
Events.Add((DBEvent)result.Plugin);
OnEventLoaded?.Invoke(result);
}
break;
case PluginType.SLASH_COMMAND:
if (await this.TryStartSlashCommand((DBSlashCommand)result.Plugin))
{
if(((DBSlashCommand)result.Plugin).HasInteraction)
_Client.InteractionCreated += ((DBSlashCommand)result.Plugin).ExecuteInteraction;
SlashCommands.Add((DBSlashCommand)result.Plugin);
OnSlashCommandLoaded?.Invoke(result);
}
else
Application.CurrentApplication.Logger.Log($"Failed to start slash command {result.PluginName}", typeof(PluginLoader), LogType.ERROR);
break;
case PluginType.UNKNOWN:
default:
Application.CurrentApplication.Logger.Log("Unknown plugin type", typeof(PluginLoader), LogType.ERROR);
break;
}
}
}
@@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Discord;
using Discord.WebSocket;
using DiscordBotCore.Interfaces;
using DiscordBotCore.Others;
namespace DiscordBotCore.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)
{
Application.CurrentApplication.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(Application.CurrentApplication.ServerID, out _))
{
Application.CurrentApplication.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(Application.CurrentApplication.ServerID));
if(guild is null)
{
Application.CurrentApplication.Logger.Log("Failed to get guild with ID " + Application.CurrentApplication.ServerID, typeof(PluginLoader), LogType.ERROR);
return;
}
await guild.DeleteApplicationCommandsAsync();
Application.CurrentApplication.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(Application.CurrentApplication.ServerID, out uint result))
{
SocketGuild? guild = pluginLoader._Client.GetGuild(result);
if (guild is null)
{
Application.CurrentApplication.Logger.Log("Failed to get guild with ID " + Application.CurrentApplication.ServerID, typeof(PluginLoader), LogType.ERROR);
return false;
}
await guild.CreateApplicationCommandAsync(builder.Build());
}else await pluginLoader._Client.CreateGlobalApplicationCommandAsync(builder.Build());
return true;
}
catch (Exception e)
{
Application.CurrentApplication.Logger.Log($"Error starting slash command {dbSlashCommand.Name}: {e.Message}", typeof(PluginLoader), LogType.ERROR);
return false;
}
}
}
@@ -0,0 +1,72 @@
using System;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using DiscordBotCore.Others;
namespace DiscordBotCore.Online.Helpers;
internal static class OnlineFunctions
{
/// <summary>
/// Downloads a <see cref="Stream" /> and saves it to another <see cref="Stream" />.
/// </summary>
/// <param name="client">The <see cref="HttpClient" /> that is used to download the file</param>
/// <param name="url">The url to the file</param>
/// <param name="destination">The <see cref="Stream" /> to save the downloaded data</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>
/// <returns></returns>
internal static async Task DownloadFileAsync(
this HttpClient client, string url, Stream destination,
IProgress<float>? progress = null,
IProgress<long>? downloadedBytes = null, int bufferSize = 81920,
CancellationToken cancellation = default)
{
using (var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellation))
{
var contentLength = response.Content.Headers.ContentLength;
using (var download = await response.Content.ReadAsStreamAsync(cancellation))
{
// Ignore progress reporting when no progress reporter was
// passed or when the content length is unknown
if (progress == null || !contentLength.HasValue)
{
await download.CopyToAsync(destination, cancellation);
if (!contentLength.HasValue)
progress?.Report(100f);
return;
}
// Convert absolute progress (bytes downloaded) into relative progress (0% - 100%)
// total ... 100%
// downloaded ... x%
// x = downloaded * 100 / total => x = downloaded / total * 100
var relativeProgress = new Progress<long>(totalBytesDownloaded =>
{
progress?.Report(totalBytesDownloaded / (float)contentLength.Value * 100);
downloadedBytes?.Report(totalBytesDownloaded);
}
);
// Use extension method to report progress while downloading
await download.CopyToOtherStreamAsync(destination, bufferSize, relativeProgress, cancellation);
progress.Report(100f);
}
}
}
/// <summary>
/// Read contents of a file as string from specified URL
/// </summary>
/// <param name="url">The URL to read from</param>
/// <param name="cancellation">The cancellation token</param>
/// <returns></returns>
internal static async Task<string> DownloadStringAsync(string url, CancellationToken cancellation = default)
{
using var client = new HttpClient();
return await client.GetStringAsync(url, cancellation);
}
}
@@ -0,0 +1,20 @@
using System.Text.Json.Serialization;
using DiscordBotCore.Interfaces.Updater;
namespace DiscordBotCore.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();
}
}
+172
View File
@@ -0,0 +1,172 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using DiscordBotCore.Others;
using DiscordBotCore.Plugin;
using DiscordBotCore.Updater.Plugins;
namespace DiscordBotCore.Online;
public class PluginManager
{
private static readonly string _DefaultBranch = "releases";
private static readonly string _DefaultBaseUrl = "https://raw.githubusercontent.com/andreitdr/SethPlugins";
private static readonly string _DefaultPluginsLink = "PluginsList.json";
public string Branch { get; init; }
public string BaseUrl { get; init; }
private string PluginsLink => $"{BaseUrl}/{Branch}/{_DefaultPluginsLink}";
public PluginManager(Uri baseUrl, string branch)
{
BaseUrl = baseUrl.ToString();
Branch = branch;
}
public PluginManager(string branch)
{
BaseUrl = _DefaultBaseUrl;
Branch = branch;
}
public PluginManager()
{
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(Application.CurrentApplication.PluginDatabase));
installedPlugins.RemoveAll(p => p.PluginName == pluginName);
await JsonManager.SaveToJsonFile(Application.CurrentApplication.PluginDatabase,installedPlugins);
}
public async Task AppendPluginToDatabase(PluginInfo pluginData)
{
List<PluginInfo> installedPlugins = await JsonManager.ConvertFromJson<List<PluginInfo>>(await File.ReadAllTextAsync(Application.CurrentApplication.PluginDatabase));
installedPlugins.Add(pluginData);
await JsonManager.SaveToJsonFile(Application.CurrentApplication.PluginDatabase, installedPlugins);
}
public async Task<List<PluginInfo>> GetInstalledPlugins()
{
return await JsonManager.ConvertFromJson<List<PluginInfo>>(await File.ReadAllTextAsync(Application.CurrentApplication.PluginDatabase));
}
public async Task<bool> IsPluginInstalled(string pluginName)
{
List<PluginInfo> installedPlugins = await JsonManager.ConvertFromJson<List<PluginInfo>>(await File.ReadAllTextAsync(Application.CurrentApplication.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)
{
if (await pluginUpdater.HasUpdate(plugin.PluginName))
{
Application.CurrentApplication.Logger.Log("Updating plugin: " + plugin.PluginName, typeof(PluginManager), LogType.INFO);
await pluginUpdater.UpdatePlugin(plugin.PluginName);
}
}
}
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, $"{Application.CurrentApplication.ApplicationEnvironmentVariables["PluginFolder"]}/{pluginData.Name}.dll", progress);
foreach (var dependency in pluginData.Dependencies)
{
await ServerCom.DownloadFileAsync(dependency.DownloadLink, dependency.DownloadLocation, progress);
currentProgress += stepProgress;
}
}
}
+78
View File
@@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using DiscordBotCore.Online.Helpers;
namespace DiscordBotCore.Online;
public static class ServerCom
{
/// <summary>
/// Read all lines from a file async
/// </summary>
/// <param name="link">The link of the file</param>
/// <returns></returns>
public static async Task<List<string>> ReadTextFromURL(string link)
{
var response = await OnlineFunctions.DownloadStringAsync(link);
var lines = response.Split('\n');
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>
/// 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>
/// <param name="progress">The <see cref="IProgress{T}" /> to track the download</param>
/// <returns></returns>
public static async Task DownloadFileAsync(
string URL, string location, IProgress<float>? progress,
IProgress<long>? downloadedBytes)
{
using (var client = new HttpClient())
{
client.Timeout = TimeSpan.FromMinutes(5);
using (var file = new FileStream(location, FileMode.Create, FileAccess.Write, FileShare.None))
{
await client.DownloadFileAsync(URL, file, progress, downloadedBytes);
}
}
}
public static async Task DownloadFileAsync(string URl, string location, IProgress<float> progress)
{
await DownloadFileAsync(URl, location, progress, null);
}
public static async Task DownloadFileAsync(string url, string location)
{
await DownloadFileAsync(url, location, null, null);
}
public static Task CreateDownloadTask(string URl, string location)
{
return DownloadFileAsync(URl, location, null, null);
}
public static Task CreateDownloadTask(string URl, string location, IProgress<float> progress)
{
return DownloadFileAsync(URl, location, progress, null);
}
}
@@ -0,0 +1,14 @@
namespace DiscordBotCore.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;
}
}
}
@@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using DiscordBotCore.Interfaces;
using DiscordBotCore.Loaders;
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);
}
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))
{
Application.CurrentApplication.Logger.Log($"Action {actionName} not found", this, LogType.ERROR);
return false;
}
try
{
await Actions[actionName].Execute(args);
return true;
}
catch (Exception e)
{
Application.CurrentApplication.Logger.Log(e.Message, type: LogType.ERROR, Sender: this);
return false;
}
}
}
+167
View File
@@ -0,0 +1,167 @@
using System;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Threading.Tasks;
namespace DiscordBotCore.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 = Application.CurrentApplication.ApplicationEnvironmentVariables["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 = Application.CurrentApplication.ApplicationEnvironmentVariables["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)
{
Application.CurrentApplication.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)
{
Application.CurrentApplication.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)
{
Application.CurrentApplication.Logger.Log(ex.Message, typeof(ArchiveManager), LogType.ERROR);
}
await Task.Delay(10);
if (progress != null)
progress.Report((float)currentSize / zipSize * 100);
}
}
}
}
@@ -0,0 +1,48 @@
using Discord.Commands;
using Discord.WebSocket;
namespace DiscordBotCore.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(Application.CurrentApplication.DiscordBotClient.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;
}
}
+43
View File
@@ -0,0 +1,43 @@
using System;
namespace DiscordBotCore.Others;
/// <summary>
/// The output log type
/// </summary>
public enum LogType
{
INFO,
WARNING,
ERROR,
CRITICAL
}
public enum UnzipProgressType
{
PERCENTAGE_FROM_NUMBER_OF_FILES,
PERCENTAGE_FROM_TOTAL_SIZE
}
public enum InternalActionRunType
{
ON_STARTUP,
ON_CALL
}
[Flags]
public enum OSType: byte
{
NONE = 0,
WINDOWS = 1 << 0,
LINUX = 2 << 1,
MACOSX = 3 << 2
}
public enum PluginType
{
UNKNOWN,
COMMAND,
EVENT,
SLASH_COMMAND
}
+83
View File
@@ -0,0 +1,83 @@
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 readonly string dataFolder = @"./Data/Resources/";
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);
}
}
+53
View File
@@ -0,0 +1,53 @@
using System;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace DiscordBotCore.Others;
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)!;
}
}
@@ -0,0 +1,71 @@
using DiscordBotCore.Interfaces.Logger;
using System;
namespace DiscordBotCore.Others.Logger
{
internal sealed class LogMessage : ILogMessage
{
public string Message { get; set; }
public DateTime ThrowTime { get; set; }
public string SenderName { get; set; }
public LogType LogMessageType { get; set; }
public LogMessage(string message, object sender)
{
Message = message;
SenderName = sender.GetType().FullName ?? sender.GetType().Name;
ThrowTime = DateTime.Now;
LogMessageType = LogType.INFO;
}
public LogMessage(string message, object sender, DateTime throwTime)
{
Message = message;
SenderName = sender.GetType().FullName ?? sender.GetType().Name;
ThrowTime = throwTime;
LogMessageType = LogType.INFO;
}
public LogMessage(string message, object sender, LogType logMessageType)
{
Message = message;
SenderName = sender.GetType().FullName ?? sender.GetType().Name;
ThrowTime = DateTime.Now;
LogMessageType = logMessageType;
}
public LogMessage(string message, DateTime throwTime, object sender, LogType logMessageType)
{
Message = message;
ThrowTime = throwTime;
SenderName = sender.GetType().FullName ?? sender.GetType().Name;
LogMessageType = logMessageType;
}
public LogMessage WithMessage(string message)
{
this.Message = message;
return this;
}
public LogMessage WithCurrentThrowTime()
{
this.ThrowTime = DateTime.Now;
return this;
}
public LogMessage WithMessageType(LogType logType)
{
this.LogMessageType = logType;
return this;
}
public static LogMessage CreateFromException(Exception exception, object Sender)
{
LogMessage message = new LogMessage(exception.Message, Sender, LogType.ERROR);
return message;
}
}
}
+51
View File
@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.Linq;
using DiscordBotCore.Interfaces.Logger;
namespace DiscordBotCore.Others.Logger;
public sealed class Logger : ILogger
{
public List<string> LogMessageProperties = typeof(ILogMessage).GetProperties().Select(p => p.Name).ToList();
public string LogMessageFormat { get ; set; }
public event EventHandler<ILogger.FormattedMessage> OnFormattedLog;
public event EventHandler<ILogMessage> OnRawLog;
public Logger(string logMessageFormat)
{
this.LogMessageFormat = logMessageFormat;
}
/// <summary>
/// Generate a formatted string based on the default parameters of the ILogMessage and a string defined as model
/// </summary>
/// <param name="message">The message</param>
/// <returns>A formatted string with the message values</returns>
private string GenerateLogMessage(ILogMessage message)
{
string messageAsString = new string(LogMessageFormat);
foreach (var prop in LogMessageProperties)
{
Type messageType = typeof(ILogMessage);
messageAsString = messageAsString.Replace("{" + prop + "}", messageType?.GetProperty(prop)?.GetValue(message)?.ToString());
}
return messageAsString;
}
public void Log(ILogMessage message)
{
OnRawLog?.Invoke(this, message);
string messageAsString = GenerateLogMessage(message);
OnFormattedLog?.Invoke(this, new ILogger.FormattedMessage() { Message = messageAsString, Type = message.LogMessageType }) ;
}
public void Log(string message, object Sender) => Log(new LogMessage(message, Sender));
public void Log(string message, object Sender, LogType type) => Log(new LogMessage(message, Sender, type));
public void LogException(Exception exception, object Sender) => Log(LogMessage.CreateFromException(exception, Sender));
}
+135
View File
@@ -0,0 +1,135 @@
using System;
namespace DiscordBotCore.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);
}
}
}
@@ -0,0 +1,64 @@
using System.Linq;
using Discord;
using Discord.WebSocket;
namespace DiscordBotCore.Others.Permissions;
/// <summary>
/// A class whith all discord permissions
/// </summary>
public static class DiscordPermissions
{
/// <summary>
/// Checks if the role has the specified permission
/// </summary>
/// <param name="role">The role</param>
/// <param name="permission">The permission</param>
/// <returns></returns>
public static bool hasPermission(this IRole role, GuildPermission permission)
{
return role.Permissions.Has(permission);
}
/// <summary>
/// Check if user has the specified role
/// </summary>
/// <param name="user">The user</param>
/// <param name="role">The role</param>
/// <returns></returns>
public static bool HasRole(this SocketGuildUser user, IRole role)
{
return user.Roles.Contains(role);
}
/// <summary>
/// Check if user has the specified permission
/// </summary>
/// <param name="user">The user</param>
/// <param name="permission">The permission</param>
/// <returns></returns>
public static bool HasPermission(this SocketGuildUser user, GuildPermission permission)
{
return user.Roles.Where(role => role.hasPermission(permission)).Any() || user.Guild.Owner == user;
}
/// <summary>
/// Check if user is administrator of server
/// </summary>
/// <param name="user">The user</param>
/// <returns></returns>
public static bool IsAdmin(this SocketGuildUser user)
{
return user.HasPermission(GuildPermission.Administrator);
}
/// <summary>
/// Check if user is administrator of server
/// </summary>
/// <param name="user">The user</param>
/// <returns></returns>
public static bool IsAdmin(this SocketUser user)
{
return IsAdmin((SocketGuildUser)user);
}
}
@@ -0,0 +1,87 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace DiscordBotCore.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 false;
}
string fileAsText = await File.ReadAllTextAsync(_File);
if(string.IsNullOrEmpty(fileAsText) || string.IsNullOrWhiteSpace(fileAsText))
{
_Dictionary = new Dictionary<TKey, TValue>();
return false;
}
_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)}) (file: {this._File}) 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;
}
}
@@ -0,0 +1,13 @@
namespace DiscordBotCore.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;
}
}
+39
View File
@@ -0,0 +1,39 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using DiscordBotCore.Online.Helpers;
namespace DiscordBotCore.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 = $"{Application.CurrentApplication.ApplicationEnvironmentVariables["PluginFolder"]}/{pluginName}.dll";
}
public PluginInfo(string pluginName, PluginVersion pluginVersion, List<string> listOfDependancies)
{
PluginName = pluginName;
PluginVersion = pluginVersion;
ListOfDependancies = listOfDependancies;
IsMarkedToUninstall = false;
FilePath = $"{Application.CurrentApplication.ApplicationEnvironmentVariables["PluginFolder"]}/{pluginName}.dll";
}
public static PluginInfo FromOnlineInfo(PluginOnlineInfo onlineInfo)
{
return new PluginInfo(onlineInfo.Name, onlineInfo.Version, onlineInfo.Dependencies.Select(dep => dep.DownloadLocation).ToList());
}
}
+51
View File
@@ -0,0 +1,51 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using DiscordBotCore.Online.Helpers;
using DiscordBotCore.Others;
namespace DiscordBotCore.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})";
}
}
@@ -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 DiscordBotCore.Interfaces.Updater;
namespace DiscordBotCore.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;
}
}
}
@@ -0,0 +1,23 @@
using DiscordBotCore.Interfaces.Updater;
namespace DiscordBotCore.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;
}
}
}
@@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using DiscordBotCore.Online;
using DiscordBotCore.Others;
using DiscordBotCore.Plugin;
namespace DiscordBotCore.Updater.Plugins;
public class PluginUpdater
{
private readonly PluginManager _PluginsManager;
public PluginUpdater(PluginManager 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(DiscordBotCore.Application.CurrentApplication.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, $"{DiscordBotCore.Application.CurrentApplication.ApplicationEnvironmentVariables["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);
}
}