Merged projects with plugins and modules

This commit is contained in:
2024-07-22 01:18:00 +03:00
parent 1fd065f4c2
commit 8ace51c840
59 changed files with 1669 additions and 73 deletions

View File

@@ -23,7 +23,7 @@ namespace DiscordBot.Bot.Actions
new InternalActionOption("fileName", "The file name") new InternalActionOption("fileName", "The file name")
]; ];
public InternalActionRunType RunType => InternalActionRunType.ON_CALL; public InternalActionRunType RunType => InternalActionRunType.OnCall;
public async Task Execute(string[] args) public async Task Execute(string[] args)
{ {

View File

@@ -15,7 +15,7 @@ public class Clear: ICommandAction
public string Usage => "clear"; public string Usage => "clear";
public IEnumerable<InternalActionOption> ListOfOptions => []; public IEnumerable<InternalActionOption> ListOfOptions => [];
public InternalActionRunType RunType => InternalActionRunType.ON_CALL; public InternalActionRunType RunType => InternalActionRunType.OnCall;
public Task Execute(string[] args) public Task Execute(string[] args)
{ {

View File

@@ -18,13 +18,13 @@ public class Exit: ICommandAction
new InternalActionOption("help", "Displays this message"), new InternalActionOption("help", "Displays this message"),
new InternalActionOption("force | -f", "Exits the bot without saving the config") new InternalActionOption("force | -f", "Exits the bot without saving the config")
}; };
public InternalActionRunType RunType => InternalActionRunType.ON_CALL; public InternalActionRunType RunType => InternalActionRunType.OnCall;
public async Task Execute(string[] args) public async Task Execute(string[] args)
{ {
if (args is null || args.Length == 0) if (args is null || args.Length == 0)
{ {
Application.CurrentApplication.Logger.Log("Exiting...", this, LogType.WARNING); Application.CurrentApplication.Logger.Log("Exiting...", this, LogType.Warning);
await Application.CurrentApplication.ApplicationEnvironmentVariables.SaveToFile(); await Application.CurrentApplication.ApplicationEnvironmentVariables.SaveToFile();
Environment.Exit(0); Environment.Exit(0);
} }
@@ -40,7 +40,7 @@ public class Exit: ICommandAction
case "-f": case "-f":
case "force": case "force":
Application.CurrentApplication.Logger.Log("Exiting (FORCE)...", this, LogType.WARNING); Application.CurrentApplication.Logger.Log("Exiting (FORCE)...", this, LogType.Warning);
Environment.Exit(0); Environment.Exit(0);
break; break;

View File

@@ -227,13 +227,13 @@ internal static class PluginMethods
{ {
if (data.IsSuccess) if (data.IsSuccess)
{ {
Application.CurrentApplication.Logger.Log("Successfully loaded command : " + data.PluginName, LogType.INFO, "\t\t > {Message}"); Application.CurrentApplication.Logger.Log("Successfully loaded command : " + data.PluginName, LogType.Info, "\t\t > {Message}");
} }
else else
{ {
Application.CurrentApplication.Logger.Log("Failed to load command : " + data.PluginName + " because " + data.ErrorMessage, Application.CurrentApplication.Logger.Log("Failed to load command : " + data.PluginName + " because " + data.ErrorMessage,
typeof(PluginMethods), LogType.ERROR typeof(PluginMethods), LogType.Error
); );
} }
@@ -243,12 +243,12 @@ internal static class PluginMethods
{ {
if (data.IsSuccess) if (data.IsSuccess)
{ {
Application.CurrentApplication.Logger.Log("Successfully loaded event : " + data.PluginName, LogType.INFO, "\t\t > {Message}"); Application.CurrentApplication.Logger.Log("Successfully loaded event : " + data.PluginName, LogType.Info, "\t\t > {Message}");
} }
else else
{ {
Application.CurrentApplication.Logger.Log("Failed to load event : " + data.PluginName + " because " + data.ErrorMessage, Application.CurrentApplication.Logger.Log("Failed to load event : " + data.PluginName + " because " + data.ErrorMessage,
typeof(PluginMethods), LogType.ERROR typeof(PluginMethods), LogType.Error
); );
} }
@@ -259,12 +259,12 @@ internal static class PluginMethods
{ {
if (data.IsSuccess) if (data.IsSuccess)
{ {
Application.CurrentApplication.Logger.Log("Successfully loaded slash command : " + data.PluginName, LogType.INFO, "\t\t > {Message}"); Application.CurrentApplication.Logger.Log("Successfully loaded slash command : " + data.PluginName, LogType.Info, "\t\t > {Message}");
} }
else else
{ {
Application.CurrentApplication.Logger.Log("Failed to load slash command : " + data.PluginName + " because " + data.ErrorMessage, Application.CurrentApplication.Logger.Log("Failed to load slash command : " + data.PluginName + " because " + data.ErrorMessage,
typeof(PluginMethods), LogType.ERROR typeof(PluginMethods), LogType.Error
); );
} }
@@ -275,12 +275,12 @@ internal static class PluginMethods
{ {
if (data.IsSuccess) if (data.IsSuccess)
{ {
Application.CurrentApplication.Logger.Log("Successfully loaded action : " + data.PluginName, LogType.INFO, "\t\t > {Message}"); Application.CurrentApplication.Logger.Log("Successfully loaded action : " + data.PluginName, LogType.Info, "\t\t > {Message}");
} }
else else
{ {
Application.CurrentApplication.Logger.Log("Failed to load action : " + data.PluginName + " because " + data.ErrorMessage, Application.CurrentApplication.Logger.Log("Failed to load action : " + data.PluginName + " because " + data.ErrorMessage,
typeof(PluginMethods), LogType.ERROR typeof(PluginMethods), LogType.Error
); );
} }

View File

@@ -25,7 +25,7 @@ public class Help: ICommandAction
new InternalActionOption("command", "The command to get help for") new InternalActionOption("command", "The command to get help for")
]; ];
public InternalActionRunType RunType => InternalActionRunType.ON_CALL; public InternalActionRunType RunType => InternalActionRunType.OnCall;
public async Task Execute(string[] args) public async Task Execute(string[] args)
{ {

View File

@@ -18,7 +18,7 @@ namespace DiscordBot.Bot.Actions
public IEnumerable<InternalActionOption> ListOfOptions => []; public IEnumerable<InternalActionOption> ListOfOptions => [];
public InternalActionRunType RunType => InternalActionRunType.ON_CALL; public InternalActionRunType RunType => InternalActionRunType.OnCall;
public Task Execute(string[] args) public Task Execute(string[] args)
{ {

View File

@@ -38,7 +38,7 @@ public class Plugin: ICommandAction
]) ])
}; };
public InternalActionRunType RunType => InternalActionRunType.ON_CALL; public InternalActionRunType RunType => InternalActionRunType.OnCall;
public async Task Execute(string[] args) public async Task Execute(string[] args)
{ {
@@ -126,13 +126,13 @@ public class Plugin: ICommandAction
case "load": case "load":
if (pluginsLoaded) if (pluginsLoaded)
{ {
Application.CurrentApplication.Logger.Log("Plugins already loaded", this, LogType.WARNING); Application.CurrentApplication.Logger.Log("Plugins already loaded", this, LogType.Warning);
break; break;
} }
if (Application.CurrentApplication.DiscordBotClient is null) if (Application.CurrentApplication.DiscordBotClient is null)
{ {
Application.CurrentApplication.Logger.Log("DiscordBot is null", this, LogType.WARNING); Application.CurrentApplication.Logger.Log("DiscordBot is null", this, LogType.Warning);
break; break;
} }

View File

@@ -21,7 +21,7 @@ public class SettingsConfig: ICommandAction
new InternalActionOption("remove", "Remove a setting"), new InternalActionOption("remove", "Remove a setting"),
new InternalActionOption("add", "Add a setting") new InternalActionOption("add", "Add a setting")
}; };
public InternalActionRunType RunType => InternalActionRunType.ON_CALL; public InternalActionRunType RunType => InternalActionRunType.OnCall;
public Task Execute(string[] args) public Task Execute(string[] args)
{ {
if (args is null) if (args is null)

View File

@@ -40,13 +40,13 @@ internal class Help: DBCommand
/// <param name="context">The command context</param> /// <param name="context">The command context</param>
public void ExecuteServer(DbCommandExecutingArguments args) public void ExecuteServer(DbCommandExecutingArguments args)
{ {
if (args.arguments is not null) if (args.Arguments is not null)
{ {
var e = GenerateHelpCommand(args.arguments[0]); var e = GenerateHelpCommand(args.Arguments[0]);
if (e is null) if (e is null)
args.context.Channel.SendMessageAsync("Unknown Command " + args.arguments[0]); args.Context.Channel.SendMessageAsync("Unknown Command " + args.Arguments[0]);
else else
args.context.Channel.SendMessageAsync(embed: e.Build()); args.Context.Channel.SendMessageAsync(embed: e.Build());
return; return;
@@ -68,7 +68,7 @@ internal class Help: DBCommand
embedBuilder.AddField("Admin Commands", adminCommands); embedBuilder.AddField("Admin Commands", adminCommands);
if (normalCommands.Length > 0) if (normalCommands.Length > 0)
embedBuilder.AddField("Normal Commands", normalCommands); embedBuilder.AddField("Normal Commands", normalCommands);
args.context.Channel.SendMessageAsync(embed: embedBuilder.Build()); args.Context.Channel.SendMessageAsync(embed: embedBuilder.Build());
} }
private EmbedBuilder GenerateHelpCommand(string command) private EmbedBuilder GenerateHelpCommand(string command)

View File

@@ -71,7 +71,7 @@ public class Program
} }
catch (Exception ex) catch (Exception ex)
{ {
Application.CurrentApplication.Logger.Log(ex.ToString(), typeof(Program), LogType.CRITICAL); Application.CurrentApplication.Logger.Log(ex.ToString(), typeof(Program), LogType.Critical);
} }
} }

View File

@@ -107,7 +107,7 @@ public class App
if (arg.Message.Contains("401")) if (arg.Message.Contains("401"))
{ {
Application.CurrentApplication.ApplicationEnvironmentVariables.Remove("token"); Application.CurrentApplication.ApplicationEnvironmentVariables.Remove("token");
Application.CurrentApplication.Logger.Log("The token is invalid.", this, LogType.CRITICAL); Application.CurrentApplication.Logger.Log("The token is invalid.", this, LogType.Critical);
await Application.CurrentApplication.ApplicationEnvironmentVariables.SaveToFile(); await Application.CurrentApplication.ApplicationEnvironmentVariables.SaveToFile();
await Task.Delay(3000); await Task.Delay(3000);
@@ -134,12 +134,12 @@ public class App
{ {
case LogSeverity.Error: case LogSeverity.Error:
case LogSeverity.Critical: case LogSeverity.Critical:
Application.CurrentApplication.Logger.Log(message.Message, this, LogType.ERROR); Application.CurrentApplication.Logger.Log(message.Message, this, LogType.Error);
break; break;
case LogSeverity.Info: case LogSeverity.Info:
case LogSeverity.Debug: case LogSeverity.Debug:
Application.CurrentApplication.Logger.Log(message.Message, this, LogType.INFO); Application.CurrentApplication.Logger.Log(message.Message, this, LogType.Info);
break; break;

View File

@@ -141,9 +141,9 @@ internal class CommandHandler
DbCommandExecutingArguments cmd = new(context, cleanMessage, split[0], argsClean); DbCommandExecutingArguments cmd = new(context, cleanMessage, split[0], argsClean);
Application.CurrentApplication.Logger.Log( Application.CurrentApplication.Logger.Log(
$"User ({context.User.Username}) from Guild \"{context.Guild.Name}\" executed command \"{cmd.cleanContent}\"", $"User ({context.User.Username}) from Guild \"{context.Guild.Name}\" executed command \"{cmd.CleanContent}\"",
this, this,
LogType.INFO LogType.Info
); );
if (context.Channel is SocketDMChannel) if (context.Channel is SocketDMChannel)

View File

@@ -41,7 +41,7 @@ public class PluginLoader
if (_Client == null) if (_Client == null)
{ {
Application.CurrentApplication.Logger.Log("Discord client is null", this, LogType.ERROR); Application.CurrentApplication.Logger.Log("Discord client is null", this, LogType.Error);
return; return;
} }
@@ -62,7 +62,7 @@ public class PluginLoader
private void FileLoadedException(FileLoaderResult result) private void FileLoadedException(FileLoaderResult result)
{ {
Application.CurrentApplication.Logger.Log(result.ErrorMessage, this, LogType.ERROR); Application.CurrentApplication.Logger.Log(result.ErrorMessage, this, LogType.Error);
} }
private async void OnPluginLoaded(PluginLoadResultData result) private async void OnPluginLoaded(PluginLoadResultData result)
@@ -71,10 +71,10 @@ public class PluginLoader
{ {
case PluginType.ACTION: case PluginType.ACTION:
ICommandAction action = (ICommandAction)result.Plugin; ICommandAction action = (ICommandAction)result.Plugin;
if (action.RunType == InternalActionRunType.ON_STARTUP || action.RunType == InternalActionRunType.BOTH) if (action.RunType == InternalActionRunType.OnStartup || action.RunType == InternalActionRunType.OnStartupAndCall)
action.Execute(null); action.Execute(null);
if(action.RunType == InternalActionRunType.ON_CALL || action.RunType == InternalActionRunType.BOTH) if(action.RunType == InternalActionRunType.OnCall || action.RunType == InternalActionRunType.OnStartupAndCall)
Actions.Add(action); Actions.Add(action);
OnActionLoaded?.Invoke(result); OnActionLoaded?.Invoke(result);
@@ -101,11 +101,11 @@ public class PluginLoader
OnSlashCommandLoaded?.Invoke(result); OnSlashCommandLoaded?.Invoke(result);
} }
else else
Application.CurrentApplication.Logger.Log($"Failed to start slash command {result.PluginName}", this, LogType.ERROR); Application.CurrentApplication.Logger.Log($"Failed to start slash command {result.PluginName}", this, LogType.Error);
break; break;
case PluginType.UNKNOWN: case PluginType.UNKNOWN:
default: default:
Application.CurrentApplication.Logger.Log("Unknown plugin type", this, LogType.ERROR); Application.CurrentApplication.Logger.Log("Unknown plugin type", this, LogType.Error);
break; break;
} }
} }

View File

@@ -26,7 +26,7 @@ internal static class PluginLoaderExtensions
} }
catch (Exception e) catch (Exception e)
{ {
Application.CurrentApplication.Logger.Log($"Error starting event {dbEvent.Name}: {e.Message}", typeof(PluginLoader), LogType.ERROR); Application.CurrentApplication.Logger.Log($"Error starting event {dbEvent.Name}: {e.Message}", typeof(PluginLoader), LogType.Error);
Application.CurrentApplication.Logger.LogException(e, typeof(PluginLoader)); Application.CurrentApplication.Logger.LogException(e, typeof(PluginLoader));
return false; return false;
} }
@@ -39,14 +39,14 @@ internal static class PluginLoaderExtensions
if(pluginLoader._Client.Guilds.Count == 0) return; if(pluginLoader._Client.Guilds.Count == 0) return;
if (!ulong.TryParse(Application.CurrentApplication.ServerID, out _)) 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); Application.CurrentApplication.Logger.Log("Invalid ServerID in config file. Can not reset specific guild commands", typeof(PluginLoader), LogType.Error);
return; return;
} }
SocketGuild? guild = pluginLoader._Client.GetGuild(ulong.Parse(Application.CurrentApplication.ServerID)); SocketGuild? guild = pluginLoader._Client.GetGuild(ulong.Parse(Application.CurrentApplication.ServerID));
if(guild is null) if(guild is null)
{ {
Application.CurrentApplication.Logger.Log("Failed to get guild with ID " + Application.CurrentApplication.ServerID, typeof(PluginLoader), LogType.ERROR); Application.CurrentApplication.Logger.Log("Failed to get guild with ID " + Application.CurrentApplication.ServerID, typeof(PluginLoader), LogType.Error);
return; return;
} }
@@ -79,7 +79,7 @@ internal static class PluginLoaderExtensions
SocketGuild? guild = pluginLoader._Client.GetGuild(result); SocketGuild? guild = pluginLoader._Client.GetGuild(result);
if (guild is null) if (guild is null)
{ {
Application.CurrentApplication.Logger.Log("Failed to get guild with ID " + Application.CurrentApplication.ServerID, typeof(PluginLoader), LogType.ERROR); Application.CurrentApplication.Logger.Log("Failed to get guild with ID " + Application.CurrentApplication.ServerID, typeof(PluginLoader), LogType.Error);
return false; return false;
} }
@@ -90,7 +90,7 @@ internal static class PluginLoaderExtensions
} }
catch (Exception e) catch (Exception e)
{ {
Application.CurrentApplication.Logger.Log($"Error starting slash command {dbSlashCommand.Name}: {e.Message}", typeof(PluginLoader), LogType.ERROR); Application.CurrentApplication.Logger.Log($"Error starting slash command {dbSlashCommand.Name}: {e.Message}", typeof(PluginLoader), LogType.Error);
return false; return false;
} }
} }

View File

@@ -119,7 +119,7 @@ public class PluginManager : IPluginManager
{ {
if (await pluginUpdater.HasUpdate(plugin.PluginName)) if (await pluginUpdater.HasUpdate(plugin.PluginName))
{ {
Application.CurrentApplication.Logger.Log("Updating plugin: " + plugin.PluginName, this, LogType.INFO); Application.CurrentApplication.Logger.Log("Updating plugin: " + plugin.PluginName, this, LogType.Info);
await pluginUpdater.UpdatePlugin(plugin.PluginName); await pluginUpdater.UpdatePlugin(plugin.PluginName);
} }
} }

View File

@@ -16,12 +16,12 @@ public class InternalActionManager
PluginLoader.Actions.ForEach(action => PluginLoader.Actions.ForEach(action =>
{ {
if (action.RunType == InternalActionRunType.ON_CALL || action.RunType == InternalActionRunType.BOTH) if (action.RunType == InternalActionRunType.OnCall || action.RunType == InternalActionRunType.OnStartupAndCall)
{ {
if (this.Actions.ContainsKey(action.ActionName)) if (this.Actions.ContainsKey(action.ActionName))
{ {
// This should never happen. If it does, log it and return // This should never happen. If it does, log it and return
Application.CurrentApplication.Logger.Log($"Action {action.ActionName} already exists", this, LogType.ERROR); Application.CurrentApplication.Logger.Log($"Action {action.ActionName} already exists", this, LogType.Error);
return; return;
} }
@@ -49,15 +49,15 @@ public class InternalActionManager
{ {
if (!Actions.ContainsKey(actionName)) if (!Actions.ContainsKey(actionName))
{ {
Application.CurrentApplication.Logger.Log($"Action {actionName} not found", this, LogType.ERROR); Application.CurrentApplication.Logger.Log($"Action {actionName} not found", this, LogType.Error);
return false; return false;
} }
try try
{ {
if (Actions[actionName].RunType == InternalActionRunType.ON_STARTUP) if (Actions[actionName].RunType == InternalActionRunType.OnStartup)
{ {
Application.CurrentApplication.Logger.Log($"Action {actionName} is not executable", this, LogType.ERROR); Application.CurrentApplication.Logger.Log($"Action {actionName} is not executable", this, LogType.Error);
return false; return false;
} }
@@ -66,7 +66,7 @@ public class InternalActionManager
} }
catch (Exception e) catch (Exception e)
{ {
Application.CurrentApplication.Logger.Log(e.Message, type: LogType.ERROR, Sender: this); Application.CurrentApplication.Logger.Log(e.Message, type: LogType.Error, Sender: this);
return false; return false;
} }
} }

View File

@@ -87,7 +87,7 @@ public static class ArchiveManager
} }
catch (Exception ex) catch (Exception ex)
{ {
Application.CurrentApplication.Logger.Log(ex.Message, typeof(ArchiveManager), LogType.ERROR); // Write the error to a file Application.CurrentApplication.Logger.Log(ex.Message, typeof(ArchiveManager), LogType.Error); // Write the error to a file
await Task.Delay(100); await Task.Delay(100);
return await ReadFromPakAsync(fileName, archFile); return await ReadFromPakAsync(fileName, archFile);
} }
@@ -123,7 +123,7 @@ public static class ArchiveManager
} }
catch (Exception ex) catch (Exception ex)
{ {
Application.CurrentApplication.Logger.Log(ex.Message, typeof(ArchiveManager), LogType.ERROR); Application.CurrentApplication.Logger.Log(ex.Message, typeof(ArchiveManager), LogType.Error);
} }
currentZipFile++; currentZipFile++;
@@ -158,7 +158,7 @@ public static class ArchiveManager
} }
catch (Exception ex) catch (Exception ex)
{ {
Application.CurrentApplication.Logger.Log(ex.Message, typeof(ArchiveManager), LogType.ERROR); Application.CurrentApplication.Logger.Log(ex.Message, typeof(ArchiveManager), LogType.Error);
} }
await Task.Delay(10); await Task.Delay(10);

View File

@@ -7,42 +7,42 @@ namespace DiscordBotCore.Others;
public class DbCommandExecutingArguments public class DbCommandExecutingArguments
{ {
public SocketCommandContext context { get; init; } public SocketCommandContext Context { get; init; }
public string cleanContent { get; init; } public string CleanContent { get; init; }
public string commandUsed { get; init; } public string CommandUsed { get; init; }
public string[]? arguments { get; init; } public string[]? Arguments { get; init; }
public ISocketMessageChannel Channel => context.Channel; public ISocketMessageChannel Channel => Context.Channel;
public DbCommandExecutingArguments( public DbCommandExecutingArguments(
SocketCommandContext context, string cleanContent, string commandUsed, string[]? arguments) SocketCommandContext context, string cleanContent, string commandUsed, string[]? arguments)
{ {
this.context = context; this.Context = context;
this.cleanContent = cleanContent; this.CleanContent = cleanContent;
this.commandUsed = commandUsed; this.CommandUsed = commandUsed;
this.arguments = arguments; this.Arguments = arguments;
} }
public DbCommandExecutingArguments(SocketUserMessage? message, DiscordSocketClient client) public DbCommandExecutingArguments(SocketUserMessage? message, DiscordSocketClient client)
{ {
context = new SocketCommandContext(client, message); Context = new SocketCommandContext(client, message);
var pos = 0; var pos = 0;
if (message.HasMentionPrefix(client.CurrentUser, ref pos)) if (message.HasMentionPrefix(client.CurrentUser, ref pos))
{ {
var mentionPrefix = "<@" + client.CurrentUser.Id + ">"; var mentionPrefix = "<@" + client.CurrentUser.Id + ">";
cleanContent = message.Content.Substring(mentionPrefix.Length + 1); CleanContent = message.Content.Substring(mentionPrefix.Length + 1);
} }
else else
{ {
cleanContent = message.Content.Substring(Application.CurrentApplication.DiscordBotClient.BotPrefix.Length); CleanContent = message.Content.Substring(Application.CurrentApplication.DiscordBotClient.BotPrefix.Length);
} }
var split = cleanContent.Split(' '); var split = CleanContent.Split(' ');
string[]? argsClean = null; string[]? argsClean = null;
if (split.Length > 1) if (split.Length > 1)
argsClean = string.Join(' ', split, 1, split.Length - 1).Split(' '); argsClean = string.Join(' ', split, 1, split.Length - 1).Split(' ');
commandUsed = split[0]; CommandUsed = split[0];
arguments = argsClean; Arguments = argsClean;
} }
} }

View File

@@ -7,10 +7,10 @@ namespace DiscordBotCore.Others;
/// </summary> /// </summary>
public enum LogType public enum LogType
{ {
INFO, Info,
WARNING, Warning,
ERROR, Error,
CRITICAL Critical
} }
public enum UnzipProgressType public enum UnzipProgressType
@@ -21,9 +21,9 @@ public enum UnzipProgressType
public enum InternalActionRunType public enum InternalActionRunType
{ {
ON_STARTUP, OnStartup,
ON_CALL, OnCall,
BOTH OnStartupAndCall
} }
[Flags] [Flags]

View File

@@ -0,0 +1,22 @@
using DiscordBotCore;
using DiscordBotCore.Interfaces.Logger;
using DiscordBotCore.Interfaces.Modules;
namespace LoggerModule
{
public class Entry : IModule<ILogger>
{
public string Name => "Logger Module";
const string _LogFolder = "./Data/Logs/";
const string _LogFormat = "{ThrowTime} {SenderName} {Message}";
public ILogger Module { get; private set; }
public Task Initialize()
{
ILogger logger = new Logger(_LogFolder, _LogFormat);
Module = logger;
return Task.CompletedTask;
}
}
}

View File

@@ -0,0 +1,79 @@
using DiscordBotCore.Interfaces.Logger;
using DiscordBotCore.Others;
namespace LoggerModule
{
internal sealed class LogMessage : ILogMessage
{
private static readonly string _DefaultLogMessageSender = "\b";
public string Message { get; set; }
public DateTime ThrowTime { get; set; }
public string SenderName { get; set; }
public LogType LogMessageType { get; set; }
public LogMessage(string message, LogType logMessageType)
{
Message = message;
LogMessageType = logMessageType;
ThrowTime = DateTime.Now;
SenderName = string.Empty;
}
public LogMessage(string message, object sender)
{
Message = message;
SenderName = sender is string && sender as string == string.Empty ? _DefaultLogMessageSender : sender.GetType().FullName ?? sender.GetType().Name;
ThrowTime = DateTime.Now;
LogMessageType = LogType.Info;
}
public LogMessage(string message, object sender, DateTime throwTime)
{
Message = message;
SenderName = sender is string && sender as string == string.Empty ? _DefaultLogMessageSender : sender.GetType().FullName ?? sender.GetType().Name;
ThrowTime = throwTime;
LogMessageType = LogType.Info;
}
public LogMessage(string message, object sender, LogType logMessageType)
{
Message = message;
SenderName = sender is string && sender as string == string.Empty ? _DefaultLogMessageSender : 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 is string && sender as string == string.Empty ? _DefaultLogMessageSender : 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, bool logFullStack)
{
LogMessage message = new LogMessage(logFullStack? exception.ToString() : exception.Message, Sender, LogType.Error);
return message;
}
}
}

View File

@@ -0,0 +1,122 @@
using DiscordBotCore.Interfaces.Logger;
using DiscordBotCore.Others;
namespace LoggerModule;
public sealed class Logger : ILogger
{
private readonly FileStream _LogFileStream;
public List<string> LogMessageProperties = typeof(ILogMessage).GetProperties().Select(p => p.Name).ToList();
private Action<string>? _OutFunction;
public string LogMessageFormat { get ; set; }
public Logger(string logFolder, string logMessageFormat, Action<string>? outFunction = null)
{
this.LogMessageFormat = logMessageFormat;
this._OutFunction = outFunction;
var logFile = logFolder + DateTime.Now.ToString("yyyy-MM-dd") + ".log";
_LogFileStream = File.Open(logFile, FileMode.Append, FileAccess.Write, FileShare.Read);
}
/// <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());
}
switch (message.LogMessageType)
{
case LogType.Info:
messageAsString = $"[green]{messageAsString} [/]";
break;
case LogType.Warning:
messageAsString = $"[yellow]{messageAsString} [/]";
break;
case LogType.Error:
messageAsString = $"[red]{messageAsString} [/]";
break;
case LogType.Critical:
messageAsString = $"[red] [bold]{messageAsString} [/][/]";
break;
}
return messageAsString;
}
private async void LogToFile(string message)
{
byte[] messageAsBytes = System.Text.Encoding.ASCII.GetBytes(message);
await _LogFileStream.WriteAsync(messageAsBytes, 0, messageAsBytes.Length);
byte[] newLine = System.Text.Encoding.ASCII.GetBytes(Environment.NewLine);
await _LogFileStream.WriteAsync(newLine, 0, newLine.Length);
await _LogFileStream.FlushAsync();
}
private string GenerateLogMessage(ILogMessage message, string customFormat)
{
string messageAsString = customFormat;
foreach (var prop in LogMessageProperties)
{
Type messageType = typeof(ILogMessage);
messageAsString = messageAsString.Replace("{" + prop + "}", messageType?.GetProperty(prop)?.GetValue(message)?.ToString());
}
switch (message.LogMessageType)
{
case LogType.Info:
messageAsString = $"[green]{messageAsString} [/]";
break;
case LogType.Warning:
messageAsString = $"[yellow]{messageAsString} [/]";
break;
case LogType.Error:
messageAsString = $"[red]{messageAsString} [/]";
break;
case LogType.Critical:
messageAsString = $"[red][bold]{messageAsString} [/][/]";
break;
}
return messageAsString;
}
public void Log(ILogMessage message, string format)
{
string messageAsString = GenerateLogMessage(message, format);
_OutFunction?.Invoke(messageAsString);
LogToFile(messageAsString);
}
public void Log(ILogMessage message)
{
string messageAsString = GenerateLogMessage(message);
_OutFunction?.Invoke(messageAsString);
LogToFile(messageAsString);
}
public void Log(string message) => Log(new LogMessage(message, string.Empty, LogType.Info));
public void Log(string message, LogType logType, string format) => Log(new LogMessage(message, logType), format);
public void Log(string message, LogType logType) => Log(new LogMessage(message, logType));
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, bool logFullStack = false) => Log(LogMessage.CreateFromException(exception, Sender, logFullStack));
public void SetOutFunction(Action<string> outFunction)
{
this._OutFunction = outFunction;
}
}

View File

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

View File

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

View File

@@ -0,0 +1,112 @@
using System.Runtime.InteropServices;
using DiscordBotCore;
namespace CppWrapper.LibraryManagement
{
public sealed class ExternLibrary
{
public string LibraryPath { get; init; }
public IntPtr LibraryHandle { get; private set; }
public ExternLibrary(string libraryPath)
{
LibraryPath = libraryPath;
LibraryHandle = IntPtr.Zero;
}
public void InitializeLibrary()
{
if(LibraryHandle != IntPtr.Zero)
{
return;
}
Application.CurrentApplication.Logger.Log($"Loading library {LibraryPath}");
if(!NativeLibrary.TryLoad(LibraryPath, out IntPtr hModule))
{
throw new DllNotFoundException($"Unable to load library {LibraryPath}");
}
Application.CurrentApplication.Logger.Log($"Library {LibraryPath} loaded successfully [{hModule}]");
LibraryHandle = hModule;
}
public void FreeLibrary()
{
if(LibraryHandle == IntPtr.Zero)
{
return;
}
NativeLibrary.Free(LibraryHandle);
LibraryHandle = IntPtr.Zero;
Application.CurrentApplication.Logger.Log($"Library {LibraryPath} freed successfully");
}
private IntPtr GetFunctionPointer(string functionName)
{
if(LibraryHandle == IntPtr.Zero)
{
throw new InvalidOperationException("Library is not loaded");
}
if(!NativeLibrary.TryGetExport(LibraryHandle, functionName, out IntPtr functionPointer))
{
throw new EntryPointNotFoundException($"Unable to find function {functionName}");
}
return functionPointer;
}
public T GetDelegateForFunctionPointer<T>(string methodName) where T : Delegate
{
IntPtr functionPointer = GetFunctionPointer(methodName);
Application.CurrentApplication.Logger.Log($"Function pointer for {methodName} obtained successfully [address: {functionPointer}]");
T result = (T)Marshal.GetDelegateForFunctionPointer(functionPointer, typeof(T));
Application.CurrentApplication.Logger.Log($"Delegate for {methodName} created successfully");
return result;
}
private IntPtr GetFunctionPointerForDelegate<T>(T functionDelegate) where T : Delegate
{
IntPtr functionPointer = Marshal.GetFunctionPointerForDelegate(functionDelegate);
Application.CurrentApplication.Logger.Log($"Function pointer for delegate {functionDelegate.Method.Name} obtained successfully [address: {functionPointer}]");
return functionPointer;
}
/// <summary>
/// Tells the extern setter function to point its function to this C# function instead.
/// This function takes the name of the extern setter function and the C# function to be executed.
/// <para><b>How it works:</b></para>
/// Find the external setter method by its name. It should take one parameter, which is the pointer to the function to be executed.
/// Take the delegate function that should be executed and get its function pointer.
/// Call the external setter with the new function memory address. This should replace the old C++ function with the new C# function.
/// </summary>
/// <param name="setterExternFunctionName">The setter function name</param>
/// <param name="executableFunction">The function that the C++ setter will make its internal function to point to</param>
/// <typeparam name="ExecuteDelegate">A delegate that reflects the executable function structure</typeparam>
/// <typeparam name="SetDelegate">The Setter delegate </typeparam>
/// <returns>A response if it exists as an object</returns>
public object? SetExternFunctionSetterPointerToCustomDelegate<SetDelegate, ExecuteDelegate>(string setterExternFunctionName, ExecuteDelegate executableFunction) where ExecuteDelegate : Delegate where SetDelegate : Delegate
{
SetDelegate setterDelegate = GetDelegateForFunctionPointer<SetDelegate>(setterExternFunctionName);
IntPtr executableFunctionPtr = GetFunctionPointerForDelegate(executableFunction);
var result = setterDelegate.DynamicInvoke(executableFunctionPtr);
Application.CurrentApplication.Logger.Log($"Function {setterExternFunctionName} bound to local action successfully");
return result;
}
}
}

View File

@@ -0,0 +1,34 @@
using System.Runtime.InteropServices;
namespace CppWrapper.Objects
{
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct ApplicationStruct
{
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 19)]
public string ServerId;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 2)]
public string Prefix;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 71)]
public string Token;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct ComplexObject
{
public int Integer;
public double DoubleValue;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
public string strValue;
public ComplexObject(int integer, double doubleValue, string strValue)
{
Integer = integer;
DoubleValue = doubleValue;
this.strValue = strValue;
}
}
}

View File

@@ -0,0 +1,17 @@
using DiscordBotCore;
namespace CppWrapper.Objects
{
public static class ObjectConvertor
{
public static ApplicationStruct ToApplicationStruct(this Application application)
{
return new ApplicationStruct
{
Token = application.ApplicationEnvironmentVariables["token"],
Prefix = application.ApplicationEnvironmentVariables["prefix"],
ServerId = application.ServerID
};
}
}
}

View File

@@ -0,0 +1,22 @@
using System.Runtime.InteropServices;
using CppWrapper.Objects;
namespace DiscordBotUI
{
public abstract class Delegates
{
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void ProcessApplicationData(ref ApplicationStruct appData);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void ProcessComplexObject(ref ComplexObject complexObject);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void CsharpFunctionDelegate();
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void SetCsharpFunctionPointerDelegate(IntPtr funcPtr);
}
}

View File

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

View File

@@ -0,0 +1,52 @@
using DiscordBotCore.Interfaces;
using DiscordBotCore.Others;
using DiscordBotCore.Others.Actions;
using CppWrapper.Objects;
using CppWrapper.LibraryManagement;
using DiscordBotCore;
using CppWrapper;
namespace DiscordBotUI;
public class Entry : ICommandAction
{
public string ActionName => "cppui";
public string? Description => "A C++ linker to the C++ UI for the bot";
public string? Usage => "cppui";
public IEnumerable<InternalActionOption> ListOfOptions => [];
public InternalActionRunType RunType => InternalActionRunType.OnStartupAndCall;
public async Task Execute(string[]? args)
{
try{
string appUiComponent = "./Data/Test/libtestlib.dll";
ExternLibrary externalLibrary = new ExternLibrary(appUiComponent);
externalLibrary.InitializeLibrary();
externalLibrary.SetExternFunctionSetterPointerToCustomDelegate<Delegates.SetCsharpFunctionPointerDelegate, Delegates.CsharpFunctionDelegate>("setCSharpFunctionPointer", () =>
{
Console.WriteLine("Hello from C#. This code is called from the C# function");
});
Delegates.ProcessComplexObject processObj = externalLibrary.GetDelegateForFunctionPointer<Delegates.ProcessComplexObject>("ProcessComplexObject");
ComplexObject complexObject = new ComplexObject(10, 10.5, "Hello from C#");
processObj(ref complexObject);
Console.WriteLine($"Integer: {complexObject.Integer}");
Console.WriteLine($"Double: {complexObject.DoubleValue}");
Console.WriteLine($"String: {complexObject.strValue}");
externalLibrary.FreeLibrary();
} catch (Exception dllException) {
Application.CurrentApplication.Logger.LogException(dllException, this);
}
}
}

View File

@@ -0,0 +1,52 @@
using Discord;
using DiscordBotCore;
using DiscordBotCore.Interfaces;
using DiscordBotCore.Others;
namespace LevelingSystem;
internal class LevelCommand: DBCommand
{
public string Command => "level";
public List<string> Aliases => ["lvl", "rank"];
public string Description => "Display tour current level";
public string Usage => "level";
public bool requireAdmin => false;
public async void ExecuteServer(DbCommandExecutingArguments args)
{
if(Variables.Database is null)
{
Application.CurrentApplication.Logger.Log("Database is not initialized", this, LogType.Warning);
return;
}
object[]? user = await Variables.Database.ReadDataArrayAsync($"SELECT * FROM Levels WHERE UserID=@userId",
new KeyValuePair<string, object>("userId", args.Context.Message.Author.Id));
if (user is null)
{
await args.Context.Channel.SendMessageAsync("You are now unranked !");
return;
}
var level = (int)user[1];
var exp = (int)user[2];
var builder = new EmbedBuilder();
var r = new Random();
builder.WithColor(r.Next(256), r.Next(256), r.Next(256));
builder.AddField("Current Level", level, true)
.AddField("Current EXP", exp, true)
.AddField("Required Exp", (level * 8 + 24).ToString(), true);
builder.WithTimestamp(DateTimeOffset.Now);
builder.WithAuthor(args.Context.Message.Author);
await args.Context.Channel.SendMessageAsync(embed: builder.Build());
}
}

View File

@@ -0,0 +1,83 @@
using Discord.WebSocket;
using DiscordBotCore;
using DiscordBotCore.Database;
using DiscordBotCore.Interfaces;
using static LevelingSystem.Variables;
namespace LevelingSystem;
internal class LevelEvent : DBEvent
{
public string Name => "Leveling System Event Handler";
public string Description => "The Leveling System Event Handler";
public bool RequireOtherThread => false;
public async void Start(DiscordSocketClient client)
{
Directory.CreateDirectory(DataFolder);
await Task.Delay(200);
Database = new SqlDatabase(DataFolder + "Users.db");
await Database.Open();
if (!File.Exists(DataFolder + "Settings.txt"))
{
GlobalSettings = new Settings
{
SecondsToWaitBetweenMessages = 5,
MaxExp = 7,
MinExp = 1
};
await DiscordBotCore.Others.JsonManager.SaveToJsonFile(DataFolder + "Settings.txt", GlobalSettings);
}
else
GlobalSettings = await DiscordBotCore.Others.JsonManager.ConvertFromJson<Settings>(DataFolder + "Settings.txt");
if (!await Database.TableExistsAsync("Levels"))
await Database.CreateTableAsync("Levels", "UserID VARCHAR(128)", "Level INT", "EXP INT");
if (!await Database.TableExistsAsync("Users"))
await Database.CreateTableAsync("Users", "UserID VARCHAR(128)", "UserMention VARCHAR(128)", "Username VARCHAR(128)", "Discriminator VARCHAR(128)");
client.MessageReceived += ClientOnMessageReceived;
}
private async Task ClientOnMessageReceived(SocketMessage arg)
{
if (arg.Author.IsBot || arg.IsTTS || arg.Content.StartsWith(Application.CurrentApplication.ApplicationEnvironmentVariables["prefix"]))
return;
if (WaitingList.ContainsKey(arg.Author.Id) && WaitingList[arg.Author.Id] > DateTime.Now.AddSeconds(-GlobalSettings.SecondsToWaitBetweenMessages))
return;
var userID = arg.Author.Id.ToString();
object[] userData = await Database.ReadDataArrayAsync($"SELECT * FROM Levels WHERE userID='{userID}'");
if (userData is null)
{
await Database.ExecuteAsync($"INSERT INTO Levels (UserID, Level, EXP) VALUES ('{userID}', 1, 0)");
await Database.ExecuteAsync($"INSERT INTO Users (UserID, UserMention) VALUES ('{userID}', '{arg.Author.Mention}')");
return;
}
var level = (int)userData[1];
var exp = (int)userData[2];
var random = new Random().Next(GlobalSettings.MinExp, GlobalSettings.MaxExp);
if (exp + random >= level * 8 + 24)
{
await Database.ExecuteAsync($"UPDATE Levels SET Level={level + 1}, EXP={random - (level * 8 + 24 - exp)} WHERE UserID='{userID}'");
await arg.Channel.SendMessageAsync($"{arg.Author.Mention} has leveled up to level {level + 1}!");
}
else await Database.ExecuteAsync($"UPDATE Levels SET EXP={exp + random} WHERE UserID='{userID}'");
WaitingList.Add(arg.Author.Id, DateTime.Now);
}
}

View File

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

View File

@@ -0,0 +1,20 @@
using DiscordBotCore.Interfaces;
using DiscordBotCore.Others;
using DiscordBotCore.Others.Actions;
namespace LevelingSystem;
public class ReloadAction: ICommandAction
{
public string ActionName => "LevelingSystemReload";
public string? Description => "Reloads the Leveling System config file";
public string? Usage => "LevelingSystemReload";
public InternalActionRunType RunType => InternalActionRunType.OnCall;
public IEnumerable<InternalActionOption> ListOfOptions => [];
public async Task Execute(string[]? args)
{
Variables.GlobalSettings = await JsonManager.ConvertFromJson<Settings>(Variables.DataFolder + "Settings.txt");
}
}

View File

@@ -0,0 +1,19 @@
using DiscordBotCore;
using DiscordBotCore.Database;
namespace LevelingSystem;
public class Settings
{
public int SecondsToWaitBetweenMessages { get; set; }
public int MinExp { get; set; }
public int MaxExp { get; set; }
}
internal static class Variables
{
internal static readonly string DataFolder = Application.GetResourceFullPath("LevelingSystem/");
internal static SqlDatabase? Database;
internal static readonly Dictionary<ulong, DateTime> WaitingList = new();
internal static Settings GlobalSettings = new();
}

View File

@@ -0,0 +1,96 @@
using Discord;
using DiscordBotCore;
using DiscordBotCore.Interfaces;
using DiscordBotCore.Online;
using DiscordBotCore.Others;
namespace MusicPlayer.Commands;
public class AddMelody: DBCommand
{
public string Command => "add_melody";
public List<string>? Aliases => new()
{
"madd"
};
public string Description => "Add a custom melody to the database";
public string Usage => "add_melody [title],[description?],[aliases],[byteSize]";
public bool requireAdmin => false;
public async void ExecuteServer(DbCommandExecutingArguments args)
{
var arguments = string.Join(" ", args.Arguments);
string[] split = arguments.Split(',');
if (split.Length < 4)
{
var message = "";
message += "Invalid arguments given. Please use the following format:\n";
message += "add_melody [title],[description?],[aliases],[byteSize]\n";
message += "title: The title of the melody\n";
message += "description: The description of the melody\n";
message += "aliases: The aliases of the melody. Use | to separate them\n";
message += "byteSize: The byte size of the melody. Default is 1024. ( & will use default)\n";
await args.Context.Channel.SendMessageAsync(message);
return;
}
if (args.Context.Message.Attachments.Count == 0)
{
await args.Context.Channel.SendMessageAsync("You must upload a valid .mp3 audio or .mp4 video file !!");
return;
}
var file = args.Context.Message.Attachments.FirstOrDefault();
if (!(file.Filename.EndsWith(".mp3") || file.Filename.EndsWith(".mp4")))
{
await args.Context.Channel.SendMessageAsync("Invalid file format !!");
return;
}
var title = split[0];
var description = split[1];
string[]? aliases = split[2]?.Split('|') ?? null;
var byteSize = split[3];
int bsize;
if (!int.TryParse(byteSize, out bsize))
bsize = 1024;
var msg = await args.Context.Channel.SendMessageAsync("Saving melody ...");
Console.WriteLine("Saving melody");
IProgress<float> downloadProgress = new Progress<float>();
var location = Application.GetResourceFullPath($"Music/Melodies/{title}.mp3");
Directory.CreateDirectory(Application.GetResourceFullPath("Music/Melodies"));
await ServerCom.DownloadFileAsync(file.Url, location, downloadProgress);
Console.WriteLine($"Done. Saved at {location}");
await msg.ModifyAsync(a => a.Content = "Done");
var info =
MusicInfoExtensions.CreateMusicInfo(title, location, description ?? "Unknown", aliases.ToList(), bsize);
Variables._MusicDatabase?.Add(title, info);
var builder = new EmbedBuilder();
builder.Title = "A new music was successfully added !";
builder.AddField("Title", info.Title);
builder.AddField("Description", info.Description);
builder.AddField("Aliases", string.Join(" | ", aliases));
await args.Context.Channel.SendMessageAsync(embed: builder.Build());
await Variables._MusicDatabase.SaveToFile();
}
}

View File

@@ -0,0 +1,77 @@
using System.Diagnostics;
using DiscordBotCore;
using DiscordBotCore.Interfaces;
using DiscordBotCore.Others;
namespace MusicPlayer.Commands;
public class AddMelodyYoutube: DBCommand
{
public string Command => "add_melody_youtube";
public List<string>? Aliases => new()
{
"madd-yt"
};
public string Description => "Add melody to the database from a youtube link";
public string Usage => "add_melody_youtube [URL] <alias1|alias2|...>";
public bool requireAdmin => true;
public async void ExecuteServer(DbCommandExecutingArguments args)
{
if (args.Arguments is null)
{
await args.Context.Channel.SendMessageAsync("Invalid arguments given. Please use the following format:\nadd_melody_youtube [URL]");
return;
}
var URL = args.Arguments[0];
if (!URL.StartsWith("https://www.youtube.com/watch?v=") && !URL.StartsWith("https://youtu.be/"))
{
await args.Context.Channel.SendMessageAsync("Invalid URL given. Please use the following format:\nadd_melody_youtube [URL]");
return;
}
if (args.Arguments.Length <= 1)
{
await args.Channel.SendMessageAsync("Please specify at least one alias for the melody !");
return;
}
var msg = await args.Context.Channel.SendMessageAsync("Saving melody ...");
var title = await YoutubeDLP.DownloadMelody(URL);
if (title == null)
{
await msg.ModifyAsync(x => x.Content = "Failed to download melody !");
return;
}
var joinedAliases = string.Join(" ", args.Arguments.Skip(1));
List<string> aliases = joinedAliases.Split('|').ToList();
if (Variables._MusicDatabase.ContainsMelodyWithNameOrAlias(title))
Variables._MusicDatabase.Remove(title);
Variables._MusicDatabase.Add(title, new MusicInfo()
{
Aliases = aliases,
ByteSize = 1024,
Description = "Melody added from youtube link",
Location = Application.GetResourceFullPath($"Music/Melodies/{title}.mp3"),
Title = title
}
);
await Variables._MusicDatabase.SaveToFile();
await msg.ModifyAsync(x => x.Content = "Melody saved !");
}
}

View File

@@ -0,0 +1,38 @@
using Discord;
using DiscordBotCore.Interfaces;
using DiscordBotCore.Others;
namespace MusicPlayer.Commands;
public class SearchMelody: DBCommand
{
public string Command => "search_melody";
public List<string>? Aliases => null;
public string Description => "Search for a melody in the database";
public string Usage => "search_melody [melody name OR one of its aliases]";
public bool requireAdmin => false;
public void ExecuteServer(DbCommandExecutingArguments args)
{
var title = string.Join(" ", args.Arguments);
if (string.IsNullOrWhiteSpace(title))
{
args.Context.Channel.SendMessageAsync("You need to specify a melody name");
return;
}
List<MusicInfo>? info = Variables._MusicDatabase.GetMusicInfoList(title);
if (info == null || info.Count == 0)
{
args.Context.Channel.SendMessageAsync("No melody with that name or alias was found");
return;
}
if (info.Count > 1)
args.Context.Channel.SendMessageAsync(embed: info.ToEmbed(Color.DarkOrange));
else
args.Context.Channel.SendMessageAsync(embed: info[0].ToEmbed(Color.DarkOrange));
}
}

View File

@@ -0,0 +1,27 @@
using Discord.WebSocket;
using DiscordBotCore;
using DiscordBotCore.Interfaces;
namespace MusicPlayer.Events;
public class OnLoad: DBEvent
{
private static readonly string _DefaultMusicPath = "Music/";
private static readonly string _DefaultSaveLocation = "Music/Melodies/";
private static readonly string _DefaultMusicDB = "Music/music_db.json";
public string Name => "Music Commands";
public string Description => "The default music commands event loader";
public bool RequireOtherThread => false;
public async void Start(DiscordSocketClient client)
{
var path1 = Application.GetResourceFullPath(_DefaultMusicPath);
var path2 = Application.GetResourceFullPath(_DefaultSaveLocation);
var path3 = Application.GetResourceFullPath(_DefaultMusicDB);
Directory.CreateDirectory(path1);
Directory.CreateDirectory(path2);
Variables._MusicDatabase = new MusicDatabase(path3);
await Variables._MusicDatabase.LoadFromFile();
}
}

View File

@@ -0,0 +1,35 @@
using Discord.WebSocket;
using DiscordBotCore;
using DiscordBotCore.Interfaces;
using DiscordBotCore.Others;
namespace MusicPlayer.Events;
public class OnVoiceRemoved: DBEvent
{
public string Name => "Event: OnVoiceRemoved";
public string Description => "Called when bot leaves a voice channel";
public bool RequireOtherThread => false;
public void Start(DiscordSocketClient client)
{
client.UserVoiceStateUpdated += async (user, oldState, newState) =>
{
if (user.Id == client.CurrentUser.Id && newState.VoiceChannel == null)
{
Variables._MusicPlayer?.MusicQueue.Clear();
Variables._MusicPlayer?.Skip();
Variables._MusicPlayer?.Stop();
await Variables.audioClient!.StopAsync();
Variables.audioClient = null;
Variables._MusicPlayer = null;
Application.CurrentApplication.Logger.Log("Bot left voice channel.", this, LogType.Info);
}
};
}
}

View File

@@ -0,0 +1,64 @@
using DiscordBotCore.Others;
namespace MusicPlayer;
public class MusicDatabase: SettingsDictionary<string, MusicInfo>
{
public MusicDatabase(string file): base(file)
{
}
/// <summary>
/// Checks if the database contains a melody with the specified name or alias
/// </summary>
/// <param name="melodyName">The name (alias) of the melody</param>
/// <returns></returns>
public bool ContainsMelodyWithNameOrAlias(string melodyName)
{
return ContainsKey(melodyName) || Values.Any(elem => elem.Aliases.Contains(melodyName, StringComparer.CurrentCultureIgnoreCase));
}
/// <summary>
/// Tries to get the music info of a melody with the specified name or alias. Returns the first match or null if no match was found.
/// </summary>
/// <param name="searchQuery">The music name or one of its aliases.</param>
/// <returns></returns>
public MusicInfo? GetMusicInfo(string searchQuery)
{
return FirstOrDefault(kvp => kvp.Key.Contains(searchQuery, StringComparison.InvariantCultureIgnoreCase) ||
kvp.Value.Aliases.Any(alias => alias.Contains(searchQuery, StringComparison.InvariantCultureIgnoreCase))
).Value;
}
/// <summary>
/// Get a list of music info that match the search query. Returns null if an error occurred, or empty list if no match was found.
/// </summary>
/// <param name="searchQuery">The search query</param>
/// <returns>null if an error occured, otherwise a list with songs that match the search query. If no song match the list is empty</returns>
public List<MusicInfo>? GetMusicInfoList(string searchQuery)
{
try
{
return this.Where(kvp =>
kvp.Key.Contains(searchQuery, StringComparison.InvariantCultureIgnoreCase) ||
kvp.Value.Aliases.Any(alias => alias.Contains(searchQuery, StringComparison.InvariantCultureIgnoreCase))
)
.Select(item => item.Value).ToList();
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
return null;
}
}
/// <summary>
/// Adds a new entry to the database based on the music info. It uses the title as the key.
/// </summary>
/// <param name="musicInfo">The music to add to</param>
public void AddNewEntryBasedOnMusicInfo(MusicInfo musicInfo)
{
Add(musicInfo.Title, musicInfo);
}
}

View File

@@ -0,0 +1,10 @@
namespace MusicPlayer;
public class MusicInfo
{
public string Title { get; init; }
public string? Description { get; init; }
public string Location { get; init; }
public List<string>? Aliases { get; init; }
public int? ByteSize { get; init; } = 1024;
}

View File

@@ -0,0 +1,53 @@
using Discord;
namespace MusicPlayer;
public static class MusicInfoExtensions
{
public static void AddAlias(this MusicInfo musicInfo, string alias)
{
musicInfo.Aliases.Add(alias);
}
public static void RemoveAlias(this MusicInfo musicInfo, string alias)
{
musicInfo.Aliases.Remove(alias);
}
public static MusicInfo CreateMusicInfo(string title, string fileLocation, string? Description = "Unknown", List<string>? aliases = null, int? byteSize = 1024)
{
return new MusicInfo()
{
Title = title,
Aliases = aliases,
Description = Description,
Location = fileLocation,
ByteSize = byteSize
};
}
public static Embed ToEmbed(this MusicInfo musicInfo, Color? embedColor = null)
{
var builder = new EmbedBuilder();
builder.Color = embedColor ?? Color.Default;
builder.WithTitle(musicInfo.Title);
builder.WithDescription(musicInfo.Description);
if (musicInfo.Aliases != null)
builder.AddField("Aliases", string.Join(", ", musicInfo.Aliases));
else
builder.AddField("Aliases", "None");
builder.AddField("Location", musicInfo.Location);
builder.AddField("ByteSize", musicInfo.ByteSize);
return builder.Build();
}
public static Embed ToEmbed(this List<MusicInfo> musicInfo, Color? embedColor = null)
{
var builder = new EmbedBuilder();
builder.Color = embedColor ?? Color.Default;
builder.WithTitle("Search results");
builder.WithDescription("Found " + musicInfo.Count + " results");
builder.AddField("Results", string.Join("\n", musicInfo.Select(item => item.Title)));
return builder.Build();
}
}

View File

@@ -0,0 +1,175 @@
using System.Diagnostics;
using Discord.Audio;
using DiscordBotCore;
using DiscordBotCore.Others;
namespace MusicPlayer;
public class MusicPlayer
{
private static int defaultByteSize = 1024;
public Queue<MusicInfo> MusicQueue { get; private set; }
public bool isPaused { get; private set; }
public bool isPlaying { get; private set; }
private bool isQueueRunning;
public int ByteSize { get; private set; }
public MusicInfo? CurrentlyPlaying { get; private set; }
public MusicPlayer()
{
MusicQueue = new Queue<MusicInfo>();
}
public async Task PlayQueue()
{
if (isQueueRunning)
{
Application.CurrentApplication.Logger.Log("Another queue is running !", typeof(MusicPlayer), LogType.Warning);
return;
}
if (Variables.audioClient is null)
{
Application.CurrentApplication.Logger.Log("Audio Client is null", typeof(MusicPlayer), LogType.Warning);
return;
}
isQueueRunning = true;
string? ffmpegPath = await Application.CurrentApplication.PluginManager.GetDependencyLocation("FFMPEG");
if(ffmpegPath is null)
{
Application.CurrentApplication.Logger.Log("FFMPEG is missing. Please install it and try again.", typeof(MusicPlayer), LogType.Error);
isQueueRunning = false;
return;
}
ffmpegPath = ffmpegPath.Replace("\\", "/");
ffmpegPath = Path.GetFullPath(ffmpegPath);
Console.WriteLine("FFMPEG Path: " + ffmpegPath);
while (MusicQueue.TryDequeue(out var dequeuedMusic))
{
CurrentlyPlaying = dequeuedMusic;
await using var dsAudioStream = Variables.audioClient.CreatePCMStream(AudioApplication.Mixed);
using var ffmpeg = CreateStream(ffmpegPath, CurrentlyPlaying.Location);
if (ffmpeg is null)
{
Application.CurrentApplication.Logger.Log($"Failed to start ffmpeg process. FFMPEG is missing or the {CurrentlyPlaying.Location} has an invalid format.", typeof(MusicPlayer), LogType.Error);
continue;
}
await using var ffmpegOut = ffmpeg.StandardOutput.BaseStream;
await PlayCurrentTrack(dsAudioStream, ffmpegOut, CurrentlyPlaying.ByteSize ?? defaultByteSize);
}
isQueueRunning = false;
CurrentlyPlaying = null;
}
public void Loop(int numberOfTimes)
{
if (CurrentlyPlaying is null) return;
Queue<MusicInfo> tempQueue = new();
for (var i = 0; i < numberOfTimes; i++)
{
tempQueue.Enqueue(CurrentlyPlaying);
}
foreach (var musicInfo in MusicQueue)
{
tempQueue.Enqueue(musicInfo);
}
MusicQueue = tempQueue;
}
private async Task PlayCurrentTrack(Stream discordVoiceChannelStream, Stream fileStreamFfmpeg, int byteSize)
{
if (isPlaying) return;
ByteSize = byteSize;
isPlaying = true;
isPaused = false;
while (isPlaying)
{
if (isPaused) continue;
var bits = new byte[byteSize];
var read = await fileStreamFfmpeg.ReadAsync(bits, 0, ByteSize);
if (read == 0) break;
try
{
await discordVoiceChannelStream.WriteAsync(bits, 0, read);
}
catch (Exception ex)
{
Application.CurrentApplication.Logger.LogException(ex, this);
break;
}
}
await discordVoiceChannelStream.FlushAsync();
await fileStreamFfmpeg.FlushAsync();
isPlaying = false;
isPaused = false;
}
public void Pause()
{
isPaused = true;
}
public void Unpause()
{
isPaused = false;
}
public bool Enqueue(string musicName)
{
var minfo = Variables._MusicDatabase.GetMusicInfo(musicName);
if (minfo is null) return false;
MusicQueue.Enqueue(minfo);
return true;
}
public void Skip()
{
isPlaying = false;
}
public void SetVolume(float volume)
{
// set volume
}
private static Process? CreateStream(string? fileName, string path)
{
return Process.Start(new ProcessStartInfo
{
FileName = fileName,
Arguments = $"-hide_banner -loglevel panic -i \"{path}\" -ac 2 -f s16le -ar 48000 pipe:1",
UseShellExecute = false,
RedirectStandardOutput = true
}
);
}
public void Stop()
{
MusicQueue.Clear();
isPlaying = false;
}
}

View File

@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\DiscordBotCore\DiscordBotCore.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Remove="libs\**"/>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Remove="libs\**"/>
</ItemGroup>
<ItemGroup>
<None Remove="libs\**"/>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,54 @@
using Discord;
using Discord.WebSocket;
using DiscordBotCore.Interfaces;
namespace MusicPlayer.SlashCommands;
public class Loop: DBSlashCommand
{
public string Name => "loop";
public string Description => "Loop the current song for a certain amount of times. If no times are specified, it will loop once";
public bool canUseDM => false;
public bool HasInteraction => false;
public List<SlashCommandOptionBuilder> Options => new()
{
new()
{
Type = ApplicationCommandOptionType.Integer,
Name = "times",
Description = "How many times to loop the song",
IsRequired = false
}
};
public void ExecuteServer(SocketSlashCommand context)
{
if (Variables._MusicPlayer.CurrentlyPlaying == null)
{
context.RespondAsync("There is nothing playing right now");
return;
}
var times = context.Data.Options.FirstOrDefault()?.Value.ToString() ?? "1";
if (!int.TryParse(times, out var timesToLoop))
{
context.RespondAsync("Invalid number");
return;
}
if (timesToLoop < 1)
{
context.RespondAsync("You need to specify a number greater than 0");
return;
}
Variables._MusicPlayer.Loop(timesToLoop);
context.RespondAsync($"Looping {Variables._MusicPlayer.CurrentlyPlaying.Title} {timesToLoop} times. Check the queue to see the progress");
}
}

View File

@@ -0,0 +1,78 @@
using Discord;
using Discord.WebSocket;
using DiscordBotCore;
using DiscordBotCore.Interfaces;
using DiscordBotCore.Others;
namespace MusicPlayer.SlashCommands;
public class Play: DBSlashCommand
{
public string Name => "play";
public string Description => "Play music command";
public bool canUseDM => false;
public bool HasInteraction => false;
public List<SlashCommandOptionBuilder> Options => new()
{
new()
{
IsRequired = true,
Description = "The music name to be played",
Name = "music-name",
Type = ApplicationCommandOptionType.String
}
};
public async void ExecuteServer(SocketSlashCommand context)
{
var melodyName = context.Data.Options.First().Value as string;
if (melodyName is null)
{
await context.RespondAsync("Failed to retrieve melody with name " + melodyName);
return;
}
var melody = Variables._MusicDatabase.GetMusicInfo(melodyName);
if (melody is null)
{
await context.RespondAsync("The searched melody does not exists in the database. Sorry :(");
return;
}
var user = context.User as IGuildUser;
if (user is null)
{
await context.RespondAsync("Failed to get user data from channel ! Check error log at " + DateTime.Now.ToLongTimeString());
Application.CurrentApplication.Logger.Log("User is null while trying to convert from context.User to IGuildUser.", typeof(Play), LogType.Error);
return;
}
var voiceChannel = user.VoiceChannel;
if (voiceChannel is null)
{
await context.RespondAsync("Unknown voice channel. Maybe I do not have permission to join it ?");
return;
}
if (Variables.audioClient is null)
{
Variables.audioClient = await voiceChannel.ConnectAsync(true); // self deaf
}
Variables._MusicPlayer ??= new MusicPlayer();
if (!Variables._MusicPlayer.Enqueue(melodyName))
{
await context.RespondAsync("Failed to enqueue your request. Something went wrong !");
return;
}
await context.RespondAsync("Enqueued your request");
await Variables._MusicPlayer.PlayQueue(); //start queue
}
}

View File

@@ -0,0 +1,49 @@
using Discord;
using Discord.WebSocket;
using DiscordBotCore.Interfaces;
namespace MusicPlayer.SlashCommands;
public class Queue: DBSlashCommand
{
public string Name => "queue";
public string Description => "Queue a melody to play";
public bool canUseDM => false;
public bool HasInteraction => false;
public List<SlashCommandOptionBuilder> Options => null;
public async void ExecuteServer(SocketSlashCommand context)
{
if (Variables._MusicPlayer is null)
{
await context.RespondAsync("No music is currently playing.");
return;
}
if (Variables._MusicPlayer.MusicQueue.Count == 0 && Variables._MusicPlayer.CurrentlyPlaying == null)
{
await context.RespondAsync("No music is currently playing");
return;
}
var builder = new EmbedBuilder()
{
Title = "Music Queue",
Description = "Here is the current music queue",
Color = Color.Blue
};
if (Variables._MusicPlayer.CurrentlyPlaying != null)
builder.AddField("Current music", Variables._MusicPlayer.CurrentlyPlaying.Title);
var i = 1;
foreach (var melody in Variables._MusicPlayer.MusicQueue)
{
builder.AddField($"#{i}", melody.Title);
i++;
}
await context.RespondAsync(embed: builder.Build());
}
}

View File

@@ -0,0 +1,35 @@
using Discord;
using Discord.WebSocket;
using DiscordBotCore.Interfaces;
namespace MusicPlayer.SlashCommands;
public class Skip: DBSlashCommand
{
public string Name => "skip";
public string Description => "Skip the current melody";
public bool canUseDM => false;
public bool HasInteraction => false;
public List<SlashCommandOptionBuilder> Options => null;
public async void ExecuteServer(SocketSlashCommand context)
{
if (Variables._MusicPlayer is null)
{
await context.RespondAsync("No music is currently playing.");
return;
}
if (Variables._MusicPlayer.MusicQueue.Count == 0 && Variables._MusicPlayer.CurrentlyPlaying == null)
{
await context.RespondAsync("No music is currently playing");
return;
}
var melodyTitle = Variables._MusicPlayer.CurrentlyPlaying.Title;
await context.RespondAsync($"Skipping {melodyTitle} ...");
Variables._MusicPlayer.Skip();
await context.ModifyOriginalResponseAsync(x => x.Content = $"Skipped {melodyTitle}");
}
}

View File

@@ -0,0 +1,11 @@
using Discord.Audio;
namespace MusicPlayer;
public class Variables
{
public static MusicDatabase? _MusicDatabase;
public static MusicPlayer? _MusicPlayer;
public static IAudioClient? audioClient;
}

View File

@@ -0,0 +1,42 @@
using System.Diagnostics;
namespace MusicPlayer;
public class YoutubeDLP
{
public static async Task<string?> DownloadMelody(string url)
{
Console.WriteLine("Downloading melody: " + url);
var process = new Process();
process.StartInfo.FileName = await DiscordBotCore.Application.CurrentApplication.PluginManager.GetDependencyLocation("yt-dlp");
process.StartInfo.Arguments = $"-x --force-overwrites -o \"{DiscordBotCore.Application.GetResourceFullPath("/Music/Melodies")}/%(title)s.%(ext)s\" --audio-format mp3 {url}";
process.StartInfo.RedirectStandardOutput = true;
var title = "";
process.OutputDataReceived += (sender, args) =>
{
if (args.Data != null)
{
if (args.Data.StartsWith("[ExtractAudio] Destination: "))
{
title = args.Data.Replace("[ExtractAudio] Destination: ", "").Replace(".mp3", "");
title = title.Replace("\\", "/");
title = title.Split('/').Last().Replace(".mp3", "").TrimEnd();
Console.WriteLine("Output title: " + title);
return;
}
Console.WriteLine(args.Data);
}
};
process.Start();
Console.WriteLine("Waiting for process to exit ...");
process.BeginOutputReadLine();
await process.WaitForExitAsync();
return title ?? null;
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -6,6 +6,20 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscordBot", "DiscordBot\Di
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscordBotCore", "DiscordBotCore\DiscordBotCore.csproj", "{5A99BFC3-EB39-4AEF-8D61-3CE22D013B02}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscordBotCore", "DiscordBotCore\DiscordBotCore.csproj", "{5A99BFC3-EB39-4AEF-8D61-3CE22D013B02}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{5CF9AD7B-6BF0-4035-835F-722F989C01E1}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Modules", "Modules", "{EA4FA308-7B2C-458E-8485-8747D745DD59}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CppWrapper", "Plugins\CppWrapper\CppWrapper.csproj", "{B21FC858-C397-4B0B-BFED-218EFA28E3E1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscordBotUI", "Plugins\DiscordBotUI\DiscordBotUI.csproj", "{E1A93D4E-A541-44B1-9DD1-FEF9B503CFD6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LevelingSystem", "Plugins\LevelingSystem\LevelingSystem.csproj", "{FCE9743F-7EB4-4639-A080-FCDDFCC7D689}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MusicPlayer", "Plugins\MusicPlayer\MusicPlayer.csproj", "{F3C61A47-F758-4145-B496-E3ECCF979D89}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LoggerModule", "Modules\LoggerModule\LoggerModule.csproj", "{367F3197-8B9E-4BDC-A6DE-226E721F9ED1}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -20,6 +34,26 @@ Global
{5A99BFC3-EB39-4AEF-8D61-3CE22D013B02}.Debug|Any CPU.Build.0 = Debug|Any CPU {5A99BFC3-EB39-4AEF-8D61-3CE22D013B02}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5A99BFC3-EB39-4AEF-8D61-3CE22D013B02}.Release|Any CPU.ActiveCfg = Release|Any CPU {5A99BFC3-EB39-4AEF-8D61-3CE22D013B02}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5A99BFC3-EB39-4AEF-8D61-3CE22D013B02}.Release|Any CPU.Build.0 = Release|Any CPU {5A99BFC3-EB39-4AEF-8D61-3CE22D013B02}.Release|Any CPU.Build.0 = Release|Any CPU
{B21FC858-C397-4B0B-BFED-218EFA28E3E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B21FC858-C397-4B0B-BFED-218EFA28E3E1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B21FC858-C397-4B0B-BFED-218EFA28E3E1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B21FC858-C397-4B0B-BFED-218EFA28E3E1}.Release|Any CPU.Build.0 = Release|Any CPU
{E1A93D4E-A541-44B1-9DD1-FEF9B503CFD6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E1A93D4E-A541-44B1-9DD1-FEF9B503CFD6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E1A93D4E-A541-44B1-9DD1-FEF9B503CFD6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E1A93D4E-A541-44B1-9DD1-FEF9B503CFD6}.Release|Any CPU.Build.0 = Release|Any CPU
{FCE9743F-7EB4-4639-A080-FCDDFCC7D689}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FCE9743F-7EB4-4639-A080-FCDDFCC7D689}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FCE9743F-7EB4-4639-A080-FCDDFCC7D689}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FCE9743F-7EB4-4639-A080-FCDDFCC7D689}.Release|Any CPU.Build.0 = Release|Any CPU
{F3C61A47-F758-4145-B496-E3ECCF979D89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F3C61A47-F758-4145-B496-E3ECCF979D89}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F3C61A47-F758-4145-B496-E3ECCF979D89}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F3C61A47-F758-4145-B496-E3ECCF979D89}.Release|Any CPU.Build.0 = Release|Any CPU
{367F3197-8B9E-4BDC-A6DE-226E721F9ED1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{367F3197-8B9E-4BDC-A6DE-226E721F9ED1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{367F3197-8B9E-4BDC-A6DE-226E721F9ED1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{367F3197-8B9E-4BDC-A6DE-226E721F9ED1}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -27,4 +61,11 @@ Global
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3FB3C5DE-ED21-4D2E-ABDD-3A00EE4A2FFF} SolutionGuid = {3FB3C5DE-ED21-4D2E-ABDD-3A00EE4A2FFF}
EndGlobalSection EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{B21FC858-C397-4B0B-BFED-218EFA28E3E1} = {5CF9AD7B-6BF0-4035-835F-722F989C01E1}
{E1A93D4E-A541-44B1-9DD1-FEF9B503CFD6} = {5CF9AD7B-6BF0-4035-835F-722F989C01E1}
{FCE9743F-7EB4-4639-A080-FCDDFCC7D689} = {5CF9AD7B-6BF0-4035-835F-722F989C01E1}
{F3C61A47-F758-4145-B496-E3ECCF979D89} = {5CF9AD7B-6BF0-4035-835F-722F989C01E1}
{367F3197-8B9E-4BDC-A6DE-226E721F9ED1} = {EA4FA308-7B2C-458E-8485-8747D745DD59}
EndGlobalSection
EndGlobal EndGlobal