Merged projects with plugins and modules
This commit is contained in:
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
22
Modules/LoggerModule/Entry.cs
Normal file
22
Modules/LoggerModule/Entry.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
79
Modules/LoggerModule/LogMessage.cs
Normal file
79
Modules/LoggerModule/LogMessage.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
122
Modules/LoggerModule/Logger.cs
Normal file
122
Modules/LoggerModule/Logger.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
Modules/LoggerModule/LoggerModule.csproj
Normal file
13
Modules/LoggerModule/LoggerModule.csproj
Normal 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>
|
||||||
13
Plugins/CppWrapper/CppWrapper.csproj
Normal file
13
Plugins/CppWrapper/CppWrapper.csproj
Normal 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>
|
||||||
112
Plugins/CppWrapper/LibraryManagement/ExternLibrary.cs
Normal file
112
Plugins/CppWrapper/LibraryManagement/ExternLibrary.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
Plugins/CppWrapper/Objects/ApplicationStruct.cs
Normal file
34
Plugins/CppWrapper/Objects/ApplicationStruct.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
Plugins/CppWrapper/Objects/ObjectConvertor.cs
Normal file
17
Plugins/CppWrapper/Objects/ObjectConvertor.cs
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
Plugins/DiscordBotUI/Delegates.cs
Normal file
22
Plugins/DiscordBotUI/Delegates.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
13
Plugins/DiscordBotUI/DiscordBotUI.csproj
Normal file
13
Plugins/DiscordBotUI/DiscordBotUI.csproj
Normal 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>
|
||||||
52
Plugins/DiscordBotUI/Entry.cs
Normal file
52
Plugins/DiscordBotUI/Entry.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
52
Plugins/LevelingSystem/LevelCommand.cs
Normal file
52
Plugins/LevelingSystem/LevelCommand.cs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
83
Plugins/LevelingSystem/LevelEvent.cs
Normal file
83
Plugins/LevelingSystem/LevelEvent.cs
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
13
Plugins/LevelingSystem/LevelingSystem.csproj
Normal file
13
Plugins/LevelingSystem/LevelingSystem.csproj
Normal 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>
|
||||||
20
Plugins/LevelingSystem/ReloadAction.cs
Normal file
20
Plugins/LevelingSystem/ReloadAction.cs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
19
Plugins/LevelingSystem/Settings.cs
Normal file
19
Plugins/LevelingSystem/Settings.cs
Normal 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();
|
||||||
|
}
|
||||||
96
Plugins/MusicPlayer/Commands/AddMelody.cs
Normal file
96
Plugins/MusicPlayer/Commands/AddMelody.cs
Normal 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();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
77
Plugins/MusicPlayer/Commands/AddMelodyYoutube.cs
Normal file
77
Plugins/MusicPlayer/Commands/AddMelodyYoutube.cs
Normal 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 !");
|
||||||
|
}
|
||||||
|
}
|
||||||
38
Plugins/MusicPlayer/Commands/SearchMelody.cs
Normal file
38
Plugins/MusicPlayer/Commands/SearchMelody.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
27
Plugins/MusicPlayer/Events/OnLoad.cs
Normal file
27
Plugins/MusicPlayer/Events/OnLoad.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
35
Plugins/MusicPlayer/Events/OnVoiceRemoved.cs
Normal file
35
Plugins/MusicPlayer/Events/OnVoiceRemoved.cs
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
64
Plugins/MusicPlayer/MusicDatabase.cs
Normal file
64
Plugins/MusicPlayer/MusicDatabase.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
Plugins/MusicPlayer/MusicInfo.cs
Normal file
10
Plugins/MusicPlayer/MusicInfo.cs
Normal 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;
|
||||||
|
}
|
||||||
53
Plugins/MusicPlayer/MusicInfoExtensions.cs
Normal file
53
Plugins/MusicPlayer/MusicInfoExtensions.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
175
Plugins/MusicPlayer/MusicPlayer.cs
Normal file
175
Plugins/MusicPlayer/MusicPlayer.cs
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
25
Plugins/MusicPlayer/MusicPlayer.csproj
Normal file
25
Plugins/MusicPlayer/MusicPlayer.csproj
Normal 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>
|
||||||
54
Plugins/MusicPlayer/SlashCommands/Loop.cs
Normal file
54
Plugins/MusicPlayer/SlashCommands/Loop.cs
Normal 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");
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
78
Plugins/MusicPlayer/SlashCommands/Play.cs
Normal file
78
Plugins/MusicPlayer/SlashCommands/Play.cs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
49
Plugins/MusicPlayer/SlashCommands/Queue.cs
Normal file
49
Plugins/MusicPlayer/SlashCommands/Queue.cs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
35
Plugins/MusicPlayer/SlashCommands/Skip.cs
Normal file
35
Plugins/MusicPlayer/SlashCommands/Skip.cs
Normal 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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
11
Plugins/MusicPlayer/Variables.cs
Normal file
11
Plugins/MusicPlayer/Variables.cs
Normal 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;
|
||||||
|
}
|
||||||
42
Plugins/MusicPlayer/YoutubeDLP.cs
Normal file
42
Plugins/MusicPlayer/YoutubeDLP.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
Plugins/MusicPlayer/libs/linux/ffmpeg
Normal file
BIN
Plugins/MusicPlayer/libs/linux/ffmpeg
Normal file
Binary file not shown.
BIN
Plugins/MusicPlayer/libs/linux/libopus.so
Normal file
BIN
Plugins/MusicPlayer/libs/linux/libopus.so
Normal file
Binary file not shown.
BIN
Plugins/MusicPlayer/libs/linux/libsodium.so
Normal file
BIN
Plugins/MusicPlayer/libs/linux/libsodium.so
Normal file
Binary file not shown.
BIN
Plugins/MusicPlayer/libs/windows/ffmpeg.exe
Normal file
BIN
Plugins/MusicPlayer/libs/windows/ffmpeg.exe
Normal file
Binary file not shown.
BIN
Plugins/MusicPlayer/libs/windows/libopus.dll
Normal file
BIN
Plugins/MusicPlayer/libs/windows/libopus.dll
Normal file
Binary file not shown.
BIN
Plugins/MusicPlayer/libs/windows/libsodium.dll
Normal file
BIN
Plugins/MusicPlayer/libs/windows/libsodium.dll
Normal file
Binary file not shown.
BIN
Plugins/MusicPlayer/libs/windows/opus.dll
Normal file
BIN
Plugins/MusicPlayer/libs/windows/opus.dll
Normal file
Binary file not shown.
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user