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

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

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);
}
}
}
}

View File

@@ -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;
}
}

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
}

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);
}
}

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)!;
}
}

View File

@@ -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;
}
}
}

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));
}

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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}