diff --git a/Directory.Build.props b/Directory.Build.props
index 2944b28..12d08e8 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -20,10 +20,13 @@
+
+
+
+
-
\ No newline at end of file
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 718f88d..10425ef 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -1,22 +1,15 @@
-
-
- true
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+ false
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/DiscordLab.Administration/Commands/SendCommand.cs b/DiscordLab.Administration/Commands/SendCommand.cs
index dd4f7d4..786ed30 100644
--- a/DiscordLab.Administration/Commands/SendCommand.cs
+++ b/DiscordLab.Administration/Commands/SendCommand.cs
@@ -1,60 +1,26 @@
-using CommandSystem;
-using Discord;
-using Discord.WebSocket;
-using DiscordLab.Bot.API.Extensions;
-using DiscordLab.Bot.API.Features;
-using LabApi.Features.Console;
+using DiscordLab.Core.API.Commands;
+using DiscordLab.Core.API.TranslationBuilders;
using LabApi.Features.Wrappers;
-using RemoteAdmin;
namespace DiscordLab.Administration.Commands;
-public class SendCommand : AutocompleteCommand
+public class SendCommand : ICommand
{
public static Config Config => Plugin.Instance.Config;
public static Translation Translation => Plugin.Instance.Translation;
- public override SlashCommandBuilder Data { get; } = new()
- {
- Name = Translation.SendCommandName,
- Description = Translation.SendCommandDescription,
- DefaultMemberPermissions = GuildPermission.ModerateMembers,
- Options =
- [
- new()
- {
- Name = Translation.SendCommandOptionName,
- Description = Translation.SendCommandOptionDescription,
- Type = ApplicationCommandOptionType.String,
- IsRequired = true,
- IsAutocomplete = true
- }
- ]
- };
-
- protected override ulong GuildId { get; } = Config.GuildId;
-
- public override async Task Run(SocketSlashCommand command)
+ public CommandBuilder Data => Translation.SendCommand;
+
+ public async Task Execute(CommandInformation command)
{
- await command.DeferAsync();
+ await command.DeferResponse();
- string response = Server.RunCommand(command.Data.Options.GetOption(Translation.SendCommandOptionName)!);
+ string response = Server.RunCommand(command.OptionsDictionary["command"]);
TranslationBuilder builder = new TranslationBuilder()
.AddCustomReplacer("response", response);
- await Translation.SendCommandResponse.ModifyInteraction(command, builder);
- }
-
- public override async Task Autocomplete(SocketAutocompleteInteraction autocomplete)
- {
- IEnumerable commands =
- [
- ..CommandProcessor.GetAllCommands().Select(x => "/" + x.Command),
- ..QueryProcessor.DotCommandHandler.AllCommands.Select(x => "." + x.Command)
- ];
- await autocomplete.RespondAsync(commands.Where(x => x.Contains((string)autocomplete.Data.Current.Value))
- .Take(25).Select(x => new AutocompleteResult(x, x)));
+ await command.Reply(Translation.SendCommandResponse.Build(builder));
}
}
\ No newline at end of file
diff --git a/DiscordLab.Administration/Config.cs b/DiscordLab.Administration/Config.cs
index d876259..e0f166c 100644
--- a/DiscordLab.Administration/Config.cs
+++ b/DiscordLab.Administration/Config.cs
@@ -4,22 +4,20 @@ namespace DiscordLab.Administration;
public class Config
{
- public ulong GuildId { get; set; } = 0;
-
[Description("The channel to send server start logs")]
- public ulong ServerStartChannelId { get; set; } = 0;
+ public string ServerStartChannel { get; set; } = "default";
[Description("Where server shutdown logs should be sent")]
- public ulong ServerShutdownChannelId { get; set; } = 0;
+ public string ServerShutdownChannel { get; set; } = "default";
[Description("The channel to send error logs")]
- public ulong ErrorLogChannelId { get; set; } = 0;
+ public string ErrorLogChannel { get; set; } = "default";
[Description("The channel to send remote admin logs")]
- public ulong RemoteAdminChannelId { get; set; } = 0;
+ public string RemoteAdminChannel { get; set; } = "default";
[Description("The channel to send normal command logs")]
- public ulong CommandLogChannelId { get; set; } = 0;
+ public string CommandLogChannel { get; set; } = "default";
[Description("Should a secondary translation be used for remote admin commands whose response is a failure?")]
public bool UseSecondaryTranslationRemoteAdmin { get; set; } = false;
diff --git a/DiscordLab.Administration/DiscordLab.Administration.csproj b/DiscordLab.Administration/DiscordLab.Administration.csproj
index 827682d..301e9ae 100644
--- a/DiscordLab.Administration/DiscordLab.Administration.csproj
+++ b/DiscordLab.Administration/DiscordLab.Administration.csproj
@@ -6,11 +6,11 @@
14
x64
true
- 2.6.1
+ 3.0.0
-
+
diff --git a/DiscordLab.Administration/Events.cs b/DiscordLab.Administration/Events.cs
index a7bd251..26909e0 100644
--- a/DiscordLab.Administration/Events.cs
+++ b/DiscordLab.Administration/Events.cs
@@ -1,14 +1,10 @@
-using Discord.WebSocket;
-using DiscordLab.Bot;
-using DiscordLab.Bot.API.Attributes;
-using DiscordLab.Bot.API.Extensions;
-using DiscordLab.Bot.API.Features;
-using DiscordLab.Bot.API.Utilities;
-using LabApi.Events;
+using DiscordLab.Core;
+using DiscordLab.Core.API.Attributes;
+using DiscordLab.Core.API.Extensions;
+using DiscordLab.Core.API.TranslationBuilders;
using LabApi.Events.Arguments.ServerEvents;
using LabApi.Events.CustomHandlers;
using LabApi.Events.Handlers;
-using LabApi.Features.Console;
using LabApi.Features.Enums;
using LabApi.Features.Wrappers;
@@ -42,34 +38,13 @@ public static void Unload()
public static void OnServerQuit()
{
- if (Config.ServerShutdownChannelId == 0)
- return;
-
- if (!Client.TryGetOrAddChannel(Config.ServerShutdownChannelId, out SocketTextChannel channel))
- {
- Logger.Error(LoggingUtils.GenerateMissingChannelMessage("server quit logs", Config.ServerShutdownChannelId, Config.GuildId));
- return;
- }
-
- Translation.ServerShutdown.SendToChannel(channel, new());
+ Translation.ServerShutdown.Send(Config.ServerShutdownChannel, new());
}
public static void OnServerStart()
{
ServerEvents.WaitingForPlayers -= OnServerStart;
- IsSubscribed = false;
-
- if (Config.ServerStartChannelId == 0)
- return;
-
- if (!Client.TryGetOrAddChannel(Config.ServerStartChannelId, out SocketTextChannel channel))
- {
- Logger.Error(LoggingUtils.GenerateMissingChannelMessage("server start logs", Config.ServerStartChannelId,
- Config.GuildId));
- return;
- }
-
- Translation.ServerStart.SendToChannel(channel, new());
+ Translation.ServerStart.Send(Config.ServerStartChannel, new());
}
public override void OnServerCommandExecuted(CommandExecutedEventArgs ev)
@@ -80,8 +55,7 @@ public override void OnServerCommandExecuted(CommandExecutedEventArgs ev)
if (string.IsNullOrEmpty(ev.CommandName))
return;
- SocketTextChannel channel;
- TranslationBuilder builder = new TranslationBuilder("player", player)
+ TranslationBuilder builder = new PlayerTranslationBuilder("player", player)
.AddCustomReplacer("type", ev.CommandType.ToString())
.AddCustomReplacer("arguments", () => !ev.Arguments.Any() ? " " : string.Join(" ", ev.Arguments))
.AddCustomReplacer("command", ev.CommandName)
@@ -91,32 +65,12 @@ public override void OnServerCommandExecuted(CommandExecutedEventArgs ev)
MessageContent translation;
if (ev.CommandType == CommandType.RemoteAdmin)
{
- if (Config.RemoteAdminChannelId == 0)
- return;
-
- if (!Client.TryGetOrAddChannel(Config.RemoteAdminChannelId, out channel))
- {
- Logger.Error(LoggingUtils.GenerateMissingChannelMessage("remote admin logs",
- Config.RemoteAdminChannelId, Config.GuildId));
- return;
- }
-
translation = Config.UseSecondaryTranslationRemoteAdmin && !ev.ExecutedSuccessfully ? Translation.RemoteAdminCommandFailResponse : Translation.RemoteAdmin;
- translation.SendToChannel(channel, builder);
+ translation.Send(Config.RemoteAdminChannel, builder);
return;
}
- if (Config.CommandLogChannelId == 0)
- return;
-
- if (!Client.TryGetOrAddChannel(Config.CommandLogChannelId, out channel))
- {
- Logger.Error(LoggingUtils.GenerateMissingChannelMessage("command logs", Config.CommandLogChannelId,
- Config.GuildId));
- return;
- }
-
translation = Config.UseSecondaryTranslationCommand && !ev.ExecutedSuccessfully ? Translation.CommandLogFailResponse : Translation.CommandLog;
- translation.SendToChannel(channel, builder);
+ translation.Send(Config.CommandLogChannel, builder);
}
}
\ No newline at end of file
diff --git a/DiscordLab.Administration/Patches/ErrorLog.cs b/DiscordLab.Administration/Patches/ErrorLog.cs
index 02d879f..6eb01df 100644
--- a/DiscordLab.Administration/Patches/ErrorLog.cs
+++ b/DiscordLab.Administration/Patches/ErrorLog.cs
@@ -1,11 +1,8 @@
-using Discord;
-using Discord.WebSocket;
-using DiscordLab.Bot;
-using DiscordLab.Bot.API.Extensions;
-using DiscordLab.Bot.API.Features;
-using DiscordLab.Bot.API.Utilities;
+using DiscordLab.Core;
+using DiscordLab.Core.API.TranslationBuilders;
using HarmonyLib;
using LabApi.Features.Console;
+using Attachment = DiscordLab.Core.API.Features.Attachment;
namespace DiscordLab.Administration.Patches;
@@ -14,35 +11,24 @@ public static class ErrorLog
{
public static void Postfix(object message)
{
- if (Plugin.Instance.Config.ErrorLogChannelId == 0)
- return;
+ TranslationBuilder builder = new();
- if (!Client.TryGetOrAddChannel(Plugin.Instance.Config.ErrorLogChannelId, out SocketTextChannel channel))
- {
- Logger.Raw(
- $"[ERROR] [{Plugin.Instance.Name}] {LoggingUtils.GenerateMissingChannelMessage("error logs", Plugin.Instance.Config.ErrorLogChannelId, Plugin.Instance.Config.GuildId)}",
- ConsoleColor.Red);
- return;
- }
-
- TranslationBuilder builder = new TranslationBuilder();
-
- (Embed embed, string content) = Plugin.Instance.Translation.ErrorLog.Build(builder);
+ MessageInformation info = Plugin.Instance.Translation.ErrorLog.Build(builder);
- MemoryStream stream = new MemoryStream();
- StreamWriter writer = new StreamWriter(stream);
+ MemoryStream stream = new();
+ StreamWriter writer = new(stream);
writer.Write(message.ToString());
writer.Flush();
stream.Position = 0;
- FileAttachment attachment = new(stream,
+ Attachment attachment = new(stream,
$"Error {DateTime.UtcNow.ToShortDateString()} {DateTime.UtcNow.ToLongTimeString()}.txt");
Task.Run(async () =>
{
try
{
- await channel.SendFileAsync(attachment, content, embed: embed);
+ await MessageHandler.SendMessages(Plugin.Instance.Config.ErrorLogChannel, info with { Attachment = attachment });
}
catch (Exception ex)
{
diff --git a/DiscordLab.Administration/Plugin.cs b/DiscordLab.Administration/Plugin.cs
index 95c2747..580c9db 100644
--- a/DiscordLab.Administration/Plugin.cs
+++ b/DiscordLab.Administration/Plugin.cs
@@ -1,5 +1,6 @@
-using DiscordLab.Bot.API.Attributes;
-using DiscordLab.Bot.API.Features;
+using DiscordLab.Core.API.Attributes;
+using DiscordLab.Core.API.Commands;
+using DiscordLab.Core.API.Features;
using HarmonyLib;
using LabApi.Events.CustomHandlers;
using LabApi.Features;
@@ -30,7 +31,7 @@ public override void Enable()
CallOnLoadAttribute.Load();
if (Config.AddCommands)
- SlashCommand.FindAll();
+ ICommand.FindAll();
CustomHandlersManager.RegisterEventsHandler(Events);
}
diff --git a/DiscordLab.Administration/Translation.cs b/DiscordLab.Administration/Translation.cs
index 30d05eb..54d55c6 100644
--- a/DiscordLab.Administration/Translation.cs
+++ b/DiscordLab.Administration/Translation.cs
@@ -1,4 +1,5 @@
-using DiscordLab.Bot.API.Features;
+using DiscordLab.Core;
+using DiscordLab.Core.API.Commands;
namespace DiscordLab.Administration;
@@ -20,13 +21,22 @@ public class Translation
public MessageContent CommandLogFailResponse { get; set; } = "Player {player} has attempted to run a command which failed: `{command}`";
- public string SendCommandName { get; set; } = "send";
-
- public string SendCommandDescription { get; set; } = "Sends a command to the server";
-
- public string SendCommandOptionName { get; set; } = "command";
-
- public string SendCommandOptionDescription { get; set; } = "The command to send";
+ public CommandBuilder SendCommand = new()
+ {
+ Name = "send",
+ Description = "Sends a command to the server",
+ DefaultPermission = DefaultCommandPermissions.Admins,
+ Options =
+ [
+ new()
+ {
+ Name = "command",
+ Description = "The command to send",
+ IsRequired = true,
+ Type = CommandOptionType.String
+ }
+ ]
+ };
public MessageContent SendCommandResponse { get; set; } = "The command has been sent, it returned: {response}";
}
\ No newline at end of file
diff --git a/DiscordLab.Bot/API/Attributes/CallOnReadyAttribute.cs b/DiscordLab.Bot/API/Attributes/CallOnReadyAttribute.cs
deleted file mode 100644
index c08f02a..0000000
--- a/DiscordLab.Bot/API/Attributes/CallOnReadyAttribute.cs
+++ /dev/null
@@ -1,77 +0,0 @@
-namespace DiscordLab.Bot.API.Attributes;
-
-using System.Reflection;
-using DiscordLab.Bot.API.Utilities;
-using LabApi.Features.Console;
-
-///
-/// An attribute that when used on a method, will trigger whenever the is ready.
-///
-[AttributeUsage(AttributeTargets.Method)]
-public class CallOnReadyAttribute : Attribute
-{
- private static List instances = [];
-
- ///
- /// Locates all 's in your plugin and prepares them to be called.
- ///
- /// The assembly you wish to check, defaults to the current one.
- public static void Load(Assembly? assembly = null)
- {
- assembly ??= Assembly.GetCallingAssembly();
-
- foreach (Type type in assembly.GetTypes())
- {
- foreach (MethodInfo method in type.GetMethods(BindingFlags.Static | BindingFlags.Public |
- BindingFlags.NonPublic))
- {
- CallOnReadyAttribute attribute = method.GetCustomAttribute();
- if (attribute == null)
- continue;
-
- Logger.Debug($"Loading {type.FullName}:{method.Name} ({nameof(CallOnReadyAttribute)})", Plugin.Instance.Config.Debug);
-
- // Sometimes the client is ready, so we check here to make sure that it isn't, and if it is just run the method directly.
- if (!Client.IsClientReady)
- {
- instances.Add(method);
- }
- else
- {
- try
- {
- LogInvoke(method);
- method.Invoke(null, null);
- }
- catch (Exception ex)
- {
- LoggingUtils.LogMethodError(ex, method);
- }
- }
- }
- }
- }
-
- ///
- /// Called whenever the bot is ready.
- ///
- internal static void Ready()
- {
- foreach (MethodInfo method in instances)
- {
- try
- {
- LogInvoke(method);
- method.Invoke(null, null);
- }
- catch (Exception ex)
- {
- LoggingUtils.LogMethodError(ex, method);
- }
- }
-
- instances.Clear();
- }
-
- private static void LogInvoke(MethodInfo method) => Logger.Debug($"Invoking {LoggingUtils.GetFullName(method)} ({nameof(CallOnReadyAttribute)})", Plugin.Instance.Config.Debug);
-}
\ No newline at end of file
diff --git a/DiscordLab.Bot/API/Extensions/DiscordExtensions.cs b/DiscordLab.Bot/API/Extensions/DiscordExtensions.cs
deleted file mode 100644
index 8357303..0000000
--- a/DiscordLab.Bot/API/Extensions/DiscordExtensions.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-namespace DiscordLab.Bot.API.Extensions;
-
-using System.Diagnostics;
-using System.Reflection;
-using Discord;
-using Discord.WebSocket;
-using DiscordLab.Bot.API.Utilities;
-
-///
-/// Extension methods to help with Discord based tasks.
-///
-public static class DiscordExtensions
-{
- ///
- /// Runs a task that sends a message to the specified channel.
- ///
- /// The channel to send the message to.
- /// The text.
- /// Whether the message is TTS.
- /// The embed.
- /// The embeds.
- /// Text, embed or embeds is required here.
- public static void SendMessage(this SocketTextChannel channel, string? text = null, bool isTts = false, Embed? embed = null, Embed[]? embeds = null)
- {
- MethodBase method = new StackFrame(1).GetMethod();
- Task.RunAndLog(
- async () => await channel.SendMessageAsync(text, isTts, embed, embeds: embeds).ConfigureAwait(false),
- ex =>
- {
- if (ex is not TimeoutException)
- LoggingUtils.LogMethodError(ex, method);
- });
- }
-
- ///
- /// Gets an option from a list of slash command options.
- ///
- /// The options to check from.
- /// The option name to get.
- /// The type that this option should return.
- /// The found item, if any.
- public static T? GetOption(this IReadOnlyCollection options, string name)
- {
- if (options.FirstOrDefault(e => e.Name == name)?.Value is T t)
- {
- return t;
- }
-
- return default;
- }
-}
\ No newline at end of file
diff --git a/DiscordLab.Bot/API/Features/AutocompleteCommand.cs b/DiscordLab.Bot/API/Features/AutocompleteCommand.cs
deleted file mode 100644
index dbedf5b..0000000
--- a/DiscordLab.Bot/API/Features/AutocompleteCommand.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-namespace DiscordLab.Bot.API.Features;
-
-using Discord.WebSocket;
-
-///
-/// Allows you to make autocomplete commands.
-///
-public abstract class AutocompleteCommand : SlashCommand
-{
- ///
- /// The method that is called once an autocomplete request is made.
- ///
- /// The autocomplete instance.
- /// The .
- public abstract Task Autocomplete(SocketAutocompleteInteraction autocomplete);
-}
\ No newline at end of file
diff --git a/DiscordLab.Bot/API/Features/Embed/EmbedAuthorBuilder.cs b/DiscordLab.Bot/API/Features/Embed/EmbedAuthorBuilder.cs
deleted file mode 100644
index 5f57aed..0000000
--- a/DiscordLab.Bot/API/Features/Embed/EmbedAuthorBuilder.cs
+++ /dev/null
@@ -1,74 +0,0 @@
-namespace DiscordLab.Bot.API.Features.Embed;
-
-using YamlDotNet.Serialization;
-
-///
-/// Contains information about an author field in an embed.
-///
-public class EmbedAuthorBuilder
-{
- ///
- /// Initializes a new instance of the class.
- ///
- public EmbedAuthorBuilder()
- {
- }
-
- ///
- /// Initializes a new instance of the class.
- /// Replaces the base with the instance.
- ///
- /// The instance.
- public EmbedAuthorBuilder(Discord.EmbedAuthorBuilder builder)
- {
- Name = builder.Name;
- IconUrl = builder.IconUrl;
- Url = builder.Url;
- }
-
- ///
- /// Gets or sets the author name.
- ///
- [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitNull)]
- public string? Name { get; set; }
-
- ///
- /// Gets or sets the icon URL.
- ///
- [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitNull)]
- public string? IconUrl { get; set; }
-
- ///
- /// Gets or sets the URL.
- ///
- [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitNull)]
- public string? Url { get; set; }
-
- ///
- /// Gets the base of this builder.
- ///
- [YamlIgnore]
- [Obsolete("Please use the properties of the DiscordLab.Bot.API.Features.Embed.EmbedAuthorBuilder instead.")]
- public Discord.EmbedAuthorBuilder Base => this;
-
- ///
- /// Changes a into a instance.
- ///
- /// The instance.
- /// A copy of the instance.
- public static implicit operator Discord.EmbedAuthorBuilder(EmbedAuthorBuilder builder)
- {
- Discord.EmbedAuthorBuilder copy = new();
-
- if (builder.Name != null)
- copy.WithName(builder.Name);
-
- if (builder.Url != null)
- copy.WithUrl(builder.Url);
-
- if (builder.IconUrl != null)
- copy.WithIconUrl(builder.IconUrl);
-
- return copy;
- }
-}
\ No newline at end of file
diff --git a/DiscordLab.Bot/API/Features/Embed/EmbedFieldBuilder.cs b/DiscordLab.Bot/API/Features/Embed/EmbedFieldBuilder.cs
deleted file mode 100644
index ead2016..0000000
--- a/DiscordLab.Bot/API/Features/Embed/EmbedFieldBuilder.cs
+++ /dev/null
@@ -1,72 +0,0 @@
-namespace DiscordLab.Bot.API.Features.Embed;
-
-using YamlDotNet.Serialization;
-
-///
-/// Allows you to create embed fields for a .
-///
-public class EmbedFieldBuilder
-{
- ///
- /// Initializes a new instance of the class.
- ///
- public EmbedFieldBuilder()
- {
- }
-
- ///
- /// Initializes a new instance of the class.
- /// Replaces the base with the instance.
- ///
- /// The instance.
- public EmbedFieldBuilder(Discord.EmbedFieldBuilder builder)
- {
- Name = builder.Name;
- IsInline = builder.IsInline;
-
- if (builder.Value is string value)
- Value = value;
- }
-
- ///
- /// Gets or sets the field name.
- ///
- public string Name { get; set; } = string.Empty;
-
- ///
- /// Gets or sets the field value.
- ///
- public string? Value { get; set; }
-
- ///
- /// Gets or sets a value indicating whether the field is inline.
- ///
- [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)]
- public bool IsInline { get; set; }
-
- ///
- /// Gets the base builder.
- ///
- [YamlIgnore]
- [Obsolete("Please use the properties of the DiscordLab.Bot.API.Features.Embed.EmbedFieldBuilder instead.")]
- public Discord.EmbedFieldBuilder Base => this;
-
- ///
- /// Changes a into a instance.
- ///
- /// The instance.
- /// A copy of the instance.
- public static implicit operator Discord.EmbedFieldBuilder(EmbedFieldBuilder builder)
- {
- Discord.EmbedFieldBuilder copy = new();
-
- copy.WithName(builder.Name);
-
- if (builder.Value != null)
- copy.WithValue(builder.Value);
-
- copy.WithIsInline(builder.IsInline);
-
- return copy;
- }
-}
\ No newline at end of file
diff --git a/DiscordLab.Bot/API/Features/Embed/EmbedFooterBuilder.cs b/DiscordLab.Bot/API/Features/Embed/EmbedFooterBuilder.cs
deleted file mode 100644
index 59ef80f..0000000
--- a/DiscordLab.Bot/API/Features/Embed/EmbedFooterBuilder.cs
+++ /dev/null
@@ -1,64 +0,0 @@
-namespace DiscordLab.Bot.API.Features.Embed;
-
-using YamlDotNet.Serialization;
-
-///
-/// Holds information for an Embed footer.
-///
-public class EmbedFooterBuilder
-{
- ///
- /// Initializes a new instance of the class.
- ///
- public EmbedFooterBuilder()
- {
- }
-
- ///
- /// Initializes a new instance of the class.
- /// Replaces the base with the instance.
- ///
- /// The instance.
- public EmbedFooterBuilder(Discord.EmbedFooterBuilder builder)
- {
- Text = builder.Text;
- IconUrl = builder.IconUrl;
- }
-
- ///
- /// Gets or sets the text for this footer.
- ///
- [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitNull)]
- public string? Text { get; set; }
-
- ///
- /// Gets or sets the icon URl for this footer.
- ///
- [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitNull)]
- public string? IconUrl { get; set; }
-
- ///
- /// Gets the base builder object.
- ///
- [YamlIgnore]
- [Obsolete("Please use the properties of the DiscordLab.Bot.API.Features.Embed.EmbedFooterBuilder instead.")]
- public Discord.EmbedFooterBuilder Base => this;
-
- ///
- /// Changes a into a instance.
- ///
- /// The instance.
- /// A copy of the instance.
- public static implicit operator Discord.EmbedFooterBuilder(EmbedFooterBuilder builder)
- {
- Discord.EmbedFooterBuilder copy = new();
-
- if (builder.Text != null)
- copy.WithText(builder.Text);
-
- if (builder.IconUrl != null)
- copy.WithIconUrl(builder.IconUrl);
-
- return copy;
- }
-}
\ No newline at end of file
diff --git a/DiscordLab.Bot/API/Features/MessageContent.cs b/DiscordLab.Bot/API/Features/MessageContent.cs
deleted file mode 100644
index 4d4f89c..0000000
--- a/DiscordLab.Bot/API/Features/MessageContent.cs
+++ /dev/null
@@ -1,246 +0,0 @@
-// ReSharper disable MemberCanBePrivate.Global
-
-namespace DiscordLab.Bot.API.Features;
-
-using System.ComponentModel;
-using System.Diagnostics;
-using System.Reflection;
-using Discord.Rest;
-using Discord.WebSocket;
-using DiscordLab.Bot.API.Extensions;
-using DiscordLab.Bot.API.Utilities;
-using UnityEngine;
-using YamlDotNet.Serialization;
-
-///
-/// Message config object for either string messages or embeds.
-///
-public class MessageContent
-{
- private const TranslatableMessageField DefaultTranslatableFields =
- TranslatableMessageField.Message |
- TranslatableMessageField.EmbedDescription |
- TranslatableMessageField.EmbedFieldValues |
- TranslatableMessageField.EmbedFooterText;
-
- ///
- /// Gets or sets the embed to send, if any.
- ///
- [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitNull)]
- public Embed.EmbedBuilder? Embed { get; set; }
-
- ///
- /// Gets or sets the string to send, if any.
- ///
- [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitNull)]
- public string? Message { get; set; }
-
- ///
- /// Gets or sets which parts of the message should have their placeholders replaced.
- ///
- [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)]
- [DefaultValue(DefaultTranslatableFields)]
- public TranslatableMessageField TranslatedFields { get; set; } = DefaultTranslatableFields;
-
- ///
- /// Converts an embed into a instance.
- ///
- /// The embed.
- /// The instance.
- public static implicit operator MessageContent(Embed.EmbedBuilder embed) => new() { Embed = embed };
-
- ///
- /// Converts a string into a instance.
- ///
- /// The content.
- /// The instance.
- public static implicit operator MessageContent(string content) => new() { Message = content };
-
- ///
- /// Sends this message to a channel.
- ///
- /// The channel to send the message to.
- /// The instance to utilise.
- public void SendToChannel(SocketTextChannel channel, TranslationBuilder builder)
- {
- if (Embed == null && Message == null)
- throw new ArgumentNullException($"A message failed to send to {channel.Name} ({channel.Id}) because both embed and message contents were undefined.");
-
- if (channel == null)
- throw new ArgumentNullException(nameof(channel));
-
- if (builder == null)
- throw new ArgumentNullException(nameof(builder));
-
- MethodBase method = new StackFrame(1).GetMethod();
- (Discord.Embed? embed, string? content) = Build(builder);
- Task.RunAndLog(async () => await channel.SendMessageAsync(content, embed: embed), ex => LoggingUtils.LogMethodError(ex, method));
- }
-
- ///
- /// Sends this message to a channel, asynchronously.
- ///
- /// The channel to send the message to.
- /// The instance to utilise.
- /// The new message object.
- public async Task SendToChannelAsync(SocketTextChannel channel, TranslationBuilder builder)
- {
- if (Embed == null && Message == null)
- throw new ArgumentNullException($"A message failed to send to {channel.Name} ({channel.Id}) because both embed and message contents were undefined.");
-
- if (channel == null)
- throw new ArgumentNullException(nameof(channel));
-
- if (builder == null)
- throw new ArgumentNullException(nameof(builder));
-
- (Discord.Embed? embed, string? content) = await MainThreadBuild(builder);
-
- return await channel.SendMessageAsync(content, embed: embed);
- }
-
- ///
- /// Modifies the instance with this message and builder.
- ///
- /// The instance.
- /// The instance to utilise.
- public void ModifyMessage(Discord.IUserMessage message, TranslationBuilder builder)
- {
- if (Embed == null && Message == null)
- throw new ArgumentNullException($"Message {message.Id} (in #{message.Channel.Name} ({message.Channel.Id})) failed to be edited because both embed and message contents were undefined.");
-
- (Discord.Embed? embed, string? content) = Build(builder);
-
- Task.RunAndLog(async () => await message.ModifyAsync(msg =>
- {
- if (!string.IsNullOrEmpty(content))
- msg.Content = content;
-
- if (embed != null)
- msg.Embed = embed;
- }).ConfigureAwait(false));
- }
-
- ///
- /// Responds to a with this message and builder.
- ///
- /// The instance.
- /// The instance to utilise.
- /// This task.
- public async Task InteractionRespond(SocketCommandBase command, TranslationBuilder builder)
- {
- if (Embed == null && Message == null)
- throw new ArgumentNullException($"Failed to respond to command {command.CommandName} because both embed and message contents were undefined.");
-
- (Discord.Embed? embed, string? content) = await MainThreadBuild(builder);
-
- await command.RespondAsync(content, embed: embed);
- }
-
- ///
- /// Modifies a 's response with the new message and builder.
- ///
- /// The instance.
- /// The instance to utilise.
- /// This task.
- public async Task ModifyInteraction(SocketCommandBase command, TranslationBuilder builder)
- {
- if (Embed == null && Message == null)
- throw new ArgumentNullException($"Failed to modify command {command.CommandName}'s response because both embed and message contents were undefined.");
-
- (Discord.Embed? embed, string? content) = await MainThreadBuild(builder);
-
- await command.ModifyOriginalResponseAsync(msg =>
- {
- if (!string.IsNullOrEmpty(content))
- msg.Content = content;
-
- if (embed != null)
- msg.Embed = embed;
- });
- }
-
- ///
- /// Checks whether the specified field has been marked as translatable in .
- ///
- /// The field to check.
- /// Whether the field is present in the .
- public bool FieldMarkedTranslatable(TranslatableMessageField field) =>
- (TranslatedFields & field) != 0;
-
- ///
- /// Builds the embed and/or content assigned to this using a .
- ///
- /// The to use.
- /// The embed and content with replaced values.
- /// Throws when message content is too long after being built.
- public (Discord.Embed? Embed, string? Content) Build(TranslationBuilder builder)
- {
- string? content = Message != null && FieldMarkedTranslatable(TranslatableMessageField.Message) ? builder.Build(Message) : Message;
-
- if (content is { Length: > Discord.DiscordConfig.MaxMessageSize })
- throw new ArgumentException($"Message content is too long, length must be less or equal to {Discord.DiscordConfig.MaxMessageSize}. This is after compiling the message.", nameof(Message));
-
- if (Embed == null)
- return (null, content);
-
- Discord.EmbedBuilder embed = Embed;
-
- if (FieldMarkedTranslatable(TranslatableMessageField.EmbedTitle) && !string.IsNullOrEmpty(embed.Title))
- embed.Title = builder.Build(embed.Title);
- if (FieldMarkedTranslatable(TranslatableMessageField.EmbedDescription) && !string.IsNullOrEmpty(embed.Description))
- embed.Description = builder.Build(embed.Description);
- if (FieldMarkedTranslatable(TranslatableMessageField.EmbedUrl) && !string.IsNullOrEmpty(embed.Url))
- embed.Url = builder.Build(embed.Url);
- if (FieldMarkedTranslatable(TranslatableMessageField.EmbedThumbnailUrl) && !string.IsNullOrEmpty(embed.ThumbnailUrl))
- embed.ThumbnailUrl = builder.Build(embed.ThumbnailUrl);
- if (FieldMarkedTranslatable(TranslatableMessageField.EmbedImageUrl) && !string.IsNullOrEmpty(embed.ImageUrl))
- embed.ImageUrl = builder.Build(embed.ImageUrl);
-
- if (embed.Author != null)
- {
- if (FieldMarkedTranslatable(TranslatableMessageField.EmbedAuthorName) && !string.IsNullOrEmpty(embed.Author.Name))
- embed.Author.Name = builder.Build(embed.Author.Name);
- if (FieldMarkedTranslatable(TranslatableMessageField.EmbedAuthorUrl) && !string.IsNullOrEmpty(embed.Author.Url))
- embed.Author.Url = builder.Build(embed.Author.Url);
- if (FieldMarkedTranslatable(TranslatableMessageField.EmbedAuthorIconUrl) && !string.IsNullOrEmpty(embed.Author.IconUrl))
- embed.Author.IconUrl = builder.Build(embed.Author.IconUrl);
- }
-
- if (embed.Footer != null)
- {
- if (FieldMarkedTranslatable(TranslatableMessageField.EmbedFooterText) && !string.IsNullOrEmpty(embed.Footer.Text))
- embed.Footer.Text = builder.Build(embed.Footer.Text);
- if (FieldMarkedTranslatable(TranslatableMessageField.EmbedFooterIconUrl) && !string.IsNullOrEmpty(embed.Footer.IconUrl))
- embed.Footer.IconUrl = builder.Build(embed.Footer.IconUrl);
- }
-
- if (FieldMarkedTranslatable(TranslatableMessageField.EmbedFieldNames))
- {
- foreach (Discord.EmbedFieldBuilder field in embed.Fields)
- {
- if (!string.IsNullOrEmpty(field.Name))
- field.Name = builder.Build(field.Name);
- }
- }
-
- if (FieldMarkedTranslatable(TranslatableMessageField.EmbedFieldValues))
- {
- foreach (Discord.EmbedFieldBuilder field in embed.Fields)
- {
- if (field.Value is string value && !string.IsNullOrEmpty(value))
- field.Value = builder.Build(value);
- }
- }
-
- return (embed.Build(), content);
- }
-
- ///
- /// Does the building on the main thread, use this over Build if you use Task.Run.
- public async Awaitable<(Discord.Embed? Embed, string? Content)> MainThreadBuild(TranslationBuilder builder)
- {
- await Awaitable.MainThreadAsync();
- return Build(builder);
- }
-}
\ No newline at end of file
diff --git a/DiscordLab.Bot/API/Features/SlashCommand.cs b/DiscordLab.Bot/API/Features/SlashCommand.cs
deleted file mode 100644
index 61929a0..0000000
--- a/DiscordLab.Bot/API/Features/SlashCommand.cs
+++ /dev/null
@@ -1,112 +0,0 @@
-namespace DiscordLab.Bot.API.Features;
-
-using System.Collections.ObjectModel;
-using System.Collections.Specialized;
-using System.Reflection;
-using Discord;
-using Discord.WebSocket;
-using DiscordLab.Bot.API.Attributes;
-using DiscordLab.Bot.API.Extensions;
-using LabApi.Features.Console;
-
-///
-/// A wrapper to easily add your own slash commands in your bot.
-///
-public abstract class SlashCommand
-{
- ///
- /// The list of currently active s.
- ///
-#pragma warning disable SA1401
- public static ObservableCollection Commands = [];
-#pragma warning restore SA1401
-
- ///
- /// Gets the slash command data.
- ///
- public abstract SlashCommandBuilder Data { get; }
-
- ///
- /// Gets the guild ID to assign this command to.
- ///
- protected abstract ulong GuildId { get; }
-
- ///
- /// Finds and creates all slash commands in your plugin. There is no method to delete all your commands, as that is handled by the bot itself.
- ///
- /// The assembly you wish to check, defaults to the current one.
- public static void FindAll(Assembly? assembly = null)
- {
- assembly ??= Assembly.GetCallingAssembly();
-
- foreach (Type type in assembly.GetTypes())
- {
- if (type.IsAbstract || !typeof(SlashCommand).IsAssignableFrom(type))
- continue;
-
- if (Activator.CreateInstance(type) is not SlashCommand init)
- continue;
- Commands.Add(init);
- }
- }
-
- ///
- /// What should be called when you run this command.
- ///
- /// The command data.
- /// A .
- public abstract Task Run(SocketSlashCommand command);
-
- [CallOnLoad]
- private static void Start()
- {
- Commands.CollectionChanged += OnCollectionChanged;
- }
-
- [CallOnUnload]
- private static void Unload()
- {
- Commands.CollectionChanged -= OnCollectionChanged;
- Commands.Clear();
- }
-
- [CallOnReady]
- private static void Ready()
- {
- Task.RunAndLog(() => RegisterGuildCommands(Commands));
- }
-
-#pragma warning disable SA1313
- private static void OnCollectionChanged(object _, NotifyCollectionChangedEventArgs ev)
-#pragma warning restore SA1313
- {
- if (!Client.IsClientReady)
- {
- return;
- }
-
- if (ev.Action is not (NotifyCollectionChangedAction.Add or NotifyCollectionChangedAction.Replace))
- {
- return;
- }
-
- Task.RunAndLog(() => RegisterGuildCommands((IEnumerable)ev.NewItems));
- }
-
- private static async Task RegisterGuildCommands(IEnumerable commands)
- {
- foreach (IGrouping cmds in commands.GroupBy(cmd => cmd.GuildId))
- {
- SocketGuild? guild = Client.GetGuild(cmds.Key);
- if (guild == null)
- {
- Logger.Warn(
- $"Could not find guild {cmds.Key}, so could not register the commands {string.Join(",", cmds.Select(cmd => cmd.Data.Name))}");
- continue;
- }
-
- await guild.BulkOverwriteApplicationCommandAsync(cmds.Select(cmd => cmd.Data.Build())
- .ToArray());
- }
- }
-}
\ No newline at end of file
diff --git a/DiscordLab.Bot/API/Features/TranslatableMessageField.cs b/DiscordLab.Bot/API/Features/TranslatableMessageField.cs
deleted file mode 100644
index 7b7040a..0000000
--- a/DiscordLab.Bot/API/Features/TranslatableMessageField.cs
+++ /dev/null
@@ -1,78 +0,0 @@
-namespace DiscordLab.Bot.API.Features;
-
-///
-/// Represents a specific field that the can be executed on.
-///
-[Flags]
-public enum TranslatableMessageField
-{
- ///
- /// Does not mark any fields for translation.
- ///
- None = 0,
-
- ///
- /// Marks the for parsing placeholders.
- ///
- Message = 1 << 0,
-
- ///
- /// Marks the embed's for parsing placeholders.
- ///
- EmbedTitle = 1 << 1,
-
- ///
- /// Marks the embed's for parsing placeholders.
- ///
- EmbedDescription = 1 << 2,
-
- ///
- /// Marks the embed's for parsing placeholders.
- ///
- EmbedThumbnailUrl = 1 << 3,
-
- ///
- /// Marks the embed's for parsing placeholders.
- ///
- EmbedImageUrl = 1 << 4,
-
- ///
- /// Marks the embed's for parsing placeholders.
- ///
- EmbedUrl = 1 << 5,
-
- ///
- /// Marks all the embed's for parsing placeholders.
- ///
- EmbedFieldNames = 1 << 6,
-
- ///
- /// Marks all the embed's for parsing placeholders.
- ///
- EmbedFieldValues = 1 << 7,
-
- ///
- /// Marks the embed's for parsing placeholders.
- ///
- EmbedAuthorName = 1 << 8,
-
- ///
- /// Marks the embed's for parsing placeholders.
- ///
- EmbedAuthorUrl = 1 << 9,
-
- ///
- /// Marks the embed's for parsing placeholders.
- ///
- EmbedAuthorIconUrl = 1 << 10,
-
- ///
- /// Marks the embed's for parsing placeholders.
- ///
- EmbedFooterText = 1 << 11,
-
- ///
- /// Marks the embed's for parsing placeholders.
- ///
- EmbedFooterIconUrl = 1 << 12,
-}
\ No newline at end of file
diff --git a/DiscordLab.Bot/API/Utilities/CommandUtils.cs b/DiscordLab.Bot/API/Utilities/CommandUtils.cs
deleted file mode 100644
index 0511e07..0000000
--- a/DiscordLab.Bot/API/Utilities/CommandUtils.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-namespace DiscordLab.Bot.API.Utilities;
-
-using LabApi.Features.Wrappers;
-
-///
-/// Utility methods for commands.
-///
-public static class CommandUtils
-{
- ///
- /// Gets a player from an unparsed string id, will check if or .
- ///
- /// The ID to check.
- /// The player if found.
- public static Player? GetPlayerFromUnparsed(string id)
- {
- return TryGetPlayerFromUnparsed(id, out Player? player) ? player : null;
- }
-
-#nullable disable
- ///
- /// Tries to get a player from an unparsed string id, will check if or .
- ///
- /// The ID to check.
- /// The player if found.
- /// Whether the player was found.
- public static bool TryGetPlayerFromUnparsed(string id, out Player player)
- {
- player = int.TryParse(id, out int intId) ? Player.Get(intId) : Player.Get(id);
- return player != null;
- }
-}
\ No newline at end of file
diff --git a/DiscordLab.Bot/Client.cs b/DiscordLab.Bot/Client.cs
index 8aec2eb..9ce350b 100644
--- a/DiscordLab.Bot/Client.cs
+++ b/DiscordLab.Bot/Client.cs
@@ -1,5 +1,7 @@
// ReSharper disable MemberCanBePrivate.Global
+using DiscordLab.Core.API.Commands;
+
namespace DiscordLab.Bot;
using System.Net;
@@ -11,9 +13,8 @@ namespace DiscordLab.Bot;
using Discord.Net.Rest;
using Discord.Net.WebSockets;
using Discord.WebSocket;
-using DiscordLab.Bot.API.Attributes;
-using DiscordLab.Bot.API.Extensions;
-using DiscordLab.Bot.API.Features;
+using DiscordLab.Core.API.Attributes;
+using DiscordLab.Core.API.Extensions;
using LabApi.Features.Console;
using NorthwoodLib.Pools;
@@ -106,14 +107,6 @@ internal static void Start()
MessageCacheSize = Config.MessageCacheSize,
};
- if (!string.IsNullOrEmpty(Config.ProxyUrl))
- {
- DebugLog("Proxy is configured.");
- WebProxy proxy = new(Config.ProxyUrl);
- config.RestClientProvider = DefaultRestClientProvider.Create(true, proxy);
- config.WebSocketProvider = DefaultWebSocketProvider.Create(proxy);
- }
-
DebugLog("Done the initial setup...");
try
@@ -124,8 +117,8 @@ internal static void Start()
SocketClient.Log += OnLog;
SocketClient.Ready += OnReady;
- SocketClient.SlashCommandExecuted += SlashCommandHandler;
- SocketClient.AutocompleteExecuted += AutocompleteHandler;
+ SocketClient.SlashCommandExecuted += OnCommand;
+ SocketClient.AutocompleteExecuted += OnAutocomplete;
}
catch (TargetInvocationException ex) when (ex.InnerException is TypeLoadException)
{
@@ -157,8 +150,8 @@ internal static void Disable()
SocketClient.Log -= OnLog;
SocketClient.Ready -= OnReady;
- SocketClient.SlashCommandExecuted -= SlashCommandHandler;
- SocketClient.AutocompleteExecuted -= AutocompleteHandler;
+ SocketClient.SlashCommandExecuted -= OnCommand;
+ SocketClient.AutocompleteExecuted -= OnAutocomplete;
Task.RunAndLog(async () =>
{
await SocketClient.LogoutAsync();
@@ -212,7 +205,6 @@ private static Task OnReady()
DebugLog("Bot is ready");
IsClientReady = true;
DefaultGuild = SocketClient.GetGuild(Config.GuildId);
- CallOnReadyAttribute.Ready();
if (Config.Debug)
{
@@ -222,31 +214,57 @@ private static Task OnReady()
return Task.CompletedTask;
}
- private static string GenerateGuildChannelsMessage(SocketGuild guild) =>
- $"Guild {guild.Name} ({guild.Id}) channels: {string.Join("\n", guild.Channels.Where(channel => channel is SocketTextChannel).Select(GenerateChannelMessage))}";
+ private static IEnumerable LoopThroughOptions(IEnumerable options) =>
+ options.Select(option => new CommandOptionInformation(option.Name, option.Value.ToString(), option.Options.Any() ? LoopThroughOptions(option.Options) : null));
- private static string GenerateChannelMessage(SocketGuildChannel channel) => $"{channel.Name} ({channel.Id})";
-
- private static Task SlashCommandHandler(SocketSlashCommand command)
+ private static async Task OnCommand(SocketSlashCommand cmd)
{
- DebugLog($"{command.Data.Name} requested a response, finding the command...");
- SlashCommand? cmd = SlashCommand.Commands.FirstOrDefault(c => c.Data.Name == command.Data.Name);
+ IEnumerable options = LoopThroughOptions(cmd.Data.Options);
- cmd?.Run(command);
- return Task.CompletedTask;
+ CommandInformation information = new(cmd.CommandName, options)
+ {
+ ReplyFunc = async info =>
+ {
+ if (!cmd.HasResponded)
+ {
+ FileAttachment? attachment = MessageHandler.ToAttachment(info);
+ Embed[]? embeds = MessageHandler.ToEmbeds(info);
+ if (attachment.HasValue)
+ await cmd.RespondWithFileAsync(attachment.Value, info.Content, embeds);
+ else
+ await cmd.RespondAsync(info.Content, embeds);
+ }
+ else
+ {
+ await cmd.ModifyOriginalResponseAsync(msg => MessageHandler.SetFrom(msg, info));
+ }
+ },
+ DeferResponseFunc = async () => await cmd.DeferAsync()
+ };
+
+ await CommandHandler.Instance.ExecuteCommand(information);
}
- private static Task AutocompleteHandler(SocketAutocompleteInteraction autocomplete)
+ private static async Task OnAutocomplete(SocketAutocompleteInteraction autocomplete)
{
- DebugLog($"{autocomplete.Data.CommandName} requested a response, finding the command...");
- AutocompleteCommand? command =
- SlashCommand.Commands.FirstOrDefault(c =>
- c is AutocompleteCommand cmd && cmd.Data.Name == autocomplete.Data.CommandName) as AutocompleteCommand;
+ CommandOptionInformation info = new()
+ {
+ Name = autocomplete.Data.Current.Name,
+ Value = autocomplete.Data.Current.Value.ToString()
+ };
- command?.Autocomplete(autocomplete);
- return Task.CompletedTask;
+ IEnumerable<(string name, string value)> output = CommandHandler.Instance.ExecuteAutocomplete(autocomplete.Data.CommandName, info).Take(25);
+
+ IEnumerable results = output.Select(tuple => new AutocompleteResult(tuple.name, tuple.value));
+
+ await autocomplete.RespondAsync(results);
}
+ private static string GenerateGuildChannelsMessage(SocketGuild guild) =>
+ $"Guild {guild.Name} ({guild.Id}) channels: {string.Join("\n", guild.Channels.Where(channel => channel is SocketTextChannel).Select(GenerateChannelMessage))}";
+
+ private static string GenerateChannelMessage(SocketGuildChannel channel) => $"{channel.Name} ({channel.Id})";
+
private static void DebugLog(object message)
{
Logger.Debug(message, Config.Debug);
diff --git a/DiscordLab.Bot/CommandHandler.cs b/DiscordLab.Bot/CommandHandler.cs
new file mode 100644
index 0000000..871c27e
--- /dev/null
+++ b/DiscordLab.Bot/CommandHandler.cs
@@ -0,0 +1,56 @@
+using Discord;
+using DiscordLab.Core.API.Commands;
+
+namespace DiscordLab.Bot;
+
+public class CommandHandler : Core.CommandHandler
+{
+ public static CommandHandler Instance = null!;
+
+ private static List LoopThroughOptions(IEnumerable? options) =>
+ options?.Select(option =>
+ {
+ ApplicationCommandOptionType type = option.Type switch
+ {
+ CommandOptionType.Subcommand => ApplicationCommandOptionType.SubCommand,
+ CommandOptionType.String => ApplicationCommandOptionType.String,
+ CommandOptionType.Integer => ApplicationCommandOptionType.Integer,
+ CommandOptionType.Boolean => ApplicationCommandOptionType.Boolean,
+ CommandOptionType.User => ApplicationCommandOptionType.User,
+ CommandOptionType.Channel => ApplicationCommandOptionType.Channel,
+ CommandOptionType.Role => ApplicationCommandOptionType.Role,
+ _ => throw new ArgumentOutOfRangeException()
+ };
+
+ return new SlashCommandOptionBuilder
+ {
+ Name = option.Name, Description = option.Description, Type = type,
+ Options = option.Options == null ? [] : LoopThroughOptions(option.Options),
+ IsAutocomplete = option.Autocomplete != null
+ };
+ }).ToList() ?? new List();
+
+ protected override async Task AddCommands(IEnumerable commands)
+ {
+ await Client.DefaultGuild!.BulkOverwriteApplicationCommandAsync(commands.Select(cmd =>
+ {
+ GuildPermission? permission = cmd.Data.DefaultPermission switch
+ {
+ DefaultCommandPermissions.Everyone => null,
+ DefaultCommandPermissions.Moderators => GuildPermission.ModerateMembers,
+ DefaultCommandPermissions.Admins => GuildPermission.Administrator,
+ _ => throw new ArgumentOutOfRangeException()
+ };
+
+ SlashCommandBuilder builder = new()
+ {
+ Name = cmd.Data.Name,
+ Description = cmd.Data.Description,
+ DefaultMemberPermissions = permission,
+ Options = LoopThroughOptions(cmd.Data.Options)
+ };
+
+ return builder.Build();
+ }).ToArray());
+ }
+}
\ No newline at end of file
diff --git a/DiscordLab.Bot/Commands/DiscordCommand.cs b/DiscordLab.Bot/Commands/DiscordCommand.cs
index 6e1163f..5b2c24a 100644
--- a/DiscordLab.Bot/Commands/DiscordCommand.cs
+++ b/DiscordLab.Bot/Commands/DiscordCommand.cs
@@ -1,56 +1,53 @@
-namespace DiscordLab.Bot.Commands;
+using DiscordLab.Core.API.Commands;
+using DiscordLab.Core.API.Updater;
-using Discord;
-using Discord.WebSocket;
-using DiscordLab.Bot.API.Features;
-using DiscordLab.Bot.API.Updates;
+namespace DiscordLab.Bot.Commands;
///
-public class DiscordCommand : AutocompleteCommand
+public class DiscordCommand : ICommand
{
///
- public override SlashCommandBuilder Data { get; } = new()
+ public CommandBuilder Data { get; } = new()
{
Name = "discordlab",
Description = "DiscordLab related commands",
- DefaultMemberPermissions = GuildPermission.Administrator,
+ DefaultPermission = DefaultCommandPermissions.Admins,
Options =
[
new()
{
- Type = ApplicationCommandOptionType.SubCommand,
+ Type = CommandOptionType.Subcommand,
Name = "list",
Description = "List all available DiscordLab modules",
},
new()
{
- Type = ApplicationCommandOptionType.SubCommand,
+ Type = CommandOptionType.Subcommand,
Name = "install",
Description = "The module to install",
Options =
[
new()
{
- Type = ApplicationCommandOptionType.String,
+ Type = CommandOptionType.String,
Name = "module",
Description = "The module to install",
IsRequired = true,
- IsAutocomplete = true,
}
],
},
new()
{
- Type = ApplicationCommandOptionType.SubCommand,
+ Type = CommandOptionType.Subcommand,
Name = "check",
Description = "Check for DiscordLab updates",
},
new()
{
- Type = ApplicationCommandOptionType.SubCommand,
+ Type = CommandOptionType.Subcommand,
Name = "update",
Description = "Update DiscordLab forcefully, skips auto update checks",
}
@@ -58,14 +55,11 @@ public class DiscordCommand : AutocompleteCommand
};
///
- protected override ulong GuildId { get; } = 0;
-
- ///
- public override async Task Run(SocketSlashCommand command)
+ public async Task Execute(CommandInformation command)
{
- await command.DeferAsync(true);
- string subcommand = command.Data.Options.First().Name;
- switch (subcommand)
+ await command.DeferResponse();
+ CommandOptionInformation subcommand = command.Options.First();
+ switch (subcommand.Name)
{
case "list":
{
@@ -73,17 +67,16 @@ public override async Task Run(SocketSlashCommand command)
"\n",
Module.CurrentModules.Where(s => s.Name != "DiscordLab.Bot")
.Select(s => $"{s.Name} (v{s.Version})"));
- await command.ModifyOriginalResponseAsync(m =>
- m.Content = "List of available DiscordLab modules:\n\n" + modules);
+ await command.Reply("List of available DiscordLab modules:\n\n" + modules);
break;
}
case "install":
{
- string moduleName = command.Data.Options.First().Options.First().Value.ToString();
+ string moduleName = subcommand.Options!.First().Value;
if (string.IsNullOrWhiteSpace(moduleName))
{
- await command.ModifyOriginalResponseAsync(m => m.Content = "Please provide a module name.");
+ await command.Reply("Please provide a module name.");
return;
}
@@ -94,14 +87,13 @@ await command.ModifyOriginalResponseAsync(m =>
s.Name.Split('.').Last().Equals(moduleName, StringComparison.CurrentCultureIgnoreCase));
if (module == null || module.Name == "DiscordLab.Bot")
{
- await command.ModifyOriginalResponseAsync(m => m.Content = "Module not found.");
+ await command.Reply("Module not found.");
return;
}
await module.Download();
ServerStatic.StopNextRound = ServerStatic.NextRoundAction.Restart;
- await command.ModifyOriginalResponseAsync(m =>
- m.Content = "Downloaded module. Server will restart next round.");
+ await command.Reply("Downloaded module. Server will restart next round.");
break;
}
@@ -110,12 +102,11 @@ await command.ModifyOriginalResponseAsync(m =>
IEnumerable modules = await Updater.ManageUpdates();
if (!modules.Any())
{
- await command.ModifyOriginalResponseAsync(m => m.Content = "No updates found.");
+ await command.Reply("No updates found.");
return;
}
- await command.ModifyOriginalResponseAsync(m =>
- m.Content = $"Updates found, modules that need updating:\n{Module.GenerateUpdateString(modules)}");
+ await command.Reply($"Updates found, modules that need updating:\n{Module.GenerateUpdateString(modules)}");
break;
}
@@ -125,15 +116,13 @@ await command.ModifyOriginalResponseAsync(m =>
if (!modules.Any())
{
- await command.ModifyOriginalResponseAsync(m => m.Content = "No updates found.");
+ await command.Reply("No updates found.");
return;
}
- if (Plugin.Instance.Config.AutoUpdate)
+ if (!Core.Plugin.Instance.Config.AutoUpdate)
{
- await command.ModifyOriginalResponseAsync(m =>
- m.Content =
- $"Updates found, modules that need updating:\n{Module.GenerateUpdateString(modules)}");
+ await command.Reply($"Updates found, modules that need updating:\n{Module.GenerateUpdateString(modules)}");
return;
}
@@ -143,19 +132,9 @@ await command.ModifyOriginalResponseAsync(m =>
await module.Download();
}
- await command.ModifyOriginalResponseAsync(m =>
- m.Content =
- $"Updates found, modules that need updating:\n{Module.GenerateUpdateString(modules)}");
+ await command.Reply($"Updates found, modules that need updating:\n{Module.GenerateUpdateString(modules)}");
break;
}
}
}
-
- ///
- public override async Task Autocomplete(SocketAutocompleteInteraction autocomplete)
- {
- await autocomplete.RespondAsync(Module.CurrentModules
- .Where(x => x.Name != "DiscordLab.Bot" && x.Name.Contains((string)autocomplete.Data.Current.Value)).Take(25)
- .Select(x => new AutocompleteResult($"{x.Name} (v{x.Version})", x.Name)));
- }
}
\ No newline at end of file
diff --git a/DiscordLab.Bot/Commands/LocalAdminCommand.cs b/DiscordLab.Bot/Commands/LocalAdminCommand.cs
index f75b9ac..4e226c6 100644
--- a/DiscordLab.Bot/Commands/LocalAdminCommand.cs
+++ b/DiscordLab.Bot/Commands/LocalAdminCommand.cs
@@ -1,8 +1,9 @@
+using DiscordLab.Core.API.Extensions;
+using DiscordLab.Core.API.Updater;
+
namespace DiscordLab.Bot.Commands;
using CommandSystem;
-using DiscordLab.Bot.API.Extensions;
-using DiscordLab.Bot.API.Updates;
using LabApi.Features.Console;
///
@@ -77,7 +78,7 @@ public bool Execute(ArraySegment arguments, ICommandSender sender, out s
return;
}
- if (Plugin.Instance.Config.AutoUpdate)
+ if (Core.Plugin.Instance.Config.AutoUpdate)
{
return;
}
diff --git a/DiscordLab.Bot/Config.cs b/DiscordLab.Bot/Config.cs
index a9e71bd..35b9f9b 100644
--- a/DiscordLab.Bot/Config.cs
+++ b/DiscordLab.Bot/Config.cs
@@ -20,28 +20,18 @@ public sealed class Config
[Description("The default guild ID. Each module that has their guild ID set to 0 has their guild ID set to this.")]
public ulong GuildId { get; set; } = 0;
- ///
- /// Gets or sets a value indicating whether the plugin should check for DiscordLab updates.
- ///
- [Description("Whether the plugin should check for DiscordLab updates.")]
- public bool AutoUpdate { get; set; } = true;
-
- ///
- /// Gets or sets the proxy URL. Shouldn't be set if proxy is not needed.
- ///
- [Description(
- "The proxy URL to use. Should only be used in very specific cases like Discord being banned in your country. Please set to empty to not use.")]
- public string ProxyUrl { get; set; } = string.Empty;
-
- ///
- /// Gets or sets a value indicating whether debug logging should be enabled.
- ///
- [Description("Enable debugging mode, useful to enable when needing to debug for developers.")]
- public bool Debug { get; set; } = false;
+ [Description("")]
+ public Dictionary ChannelIds { get; set; } = new()
+ {
+ ["default"] = "0"
+ };
///
/// Gets or sets the number of messages that should be cached from each channel.
///
[YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)]
public int MessageCacheSize { get; set; }
+
+ [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)]
+ public bool Debug { get; set; } = false;
}
\ No newline at end of file
diff --git a/DiscordLab.Bot/DiscordLab.Bot.csproj b/DiscordLab.Bot/DiscordLab.Bot.csproj
index c9a0a81..f42ca5d 100644
--- a/DiscordLab.Bot/DiscordLab.Bot.csproj
+++ b/DiscordLab.Bot/DiscordLab.Bot.csproj
@@ -6,45 +6,21 @@
enable
x64
true
- 2.6.1
+ 3.0.0
-
- true
- true
- LumiFae
- DiscordLab
- A modular Discord bot for SCP:SL servers running LabAPI
- git
- https://github.com/DiscordLabSCP/DiscordLab
- README.md
- AGPL-3.0-only
-
-
-
- ../stylecop.ruleset
-
-
-
-
- True
- \
-
-
-
-
-
-
-
-
-
-
-
-
+
+ DiscordNet
+
+
+
+
+
+
$(MSBuildThisFileDirectory)..\bin\dependencies\
diff --git a/DiscordLab.Bot/MessageConfig.cs b/DiscordLab.Bot/MessageConfig.cs
new file mode 100644
index 0000000..378e781
--- /dev/null
+++ b/DiscordLab.Bot/MessageConfig.cs
@@ -0,0 +1,9 @@
+using System.ComponentModel;
+
+namespace DiscordLab.Bot;
+
+public class MessageConfig
+{
+ [Description("Do not edit unless you know what you are doing")]
+ public Dictionary MessageIds = new();
+}
\ No newline at end of file
diff --git a/DiscordLab.Bot/MessageHandler.cs b/DiscordLab.Bot/MessageHandler.cs
new file mode 100644
index 0000000..f2eada3
--- /dev/null
+++ b/DiscordLab.Bot/MessageHandler.cs
@@ -0,0 +1,229 @@
+namespace DiscordLab.Bot;
+
+using System.Collections.ObjectModel;
+using Discord;
+using Discord.Rest;
+using Discord.Webhook;
+using Discord.WebSocket;
+using DiscordLab.Core;
+using DiscordLab.Core.API.Enums;
+using DiscordLab.Core.API.Extensions;
+using LabApi.Loader;
+
+public class MessageHandler : Core.MessageHandler
+{
+ private bool TryGetFromDestination(string destination, out string channel) =>
+ Plugin.Instance.Config.ChannelIds.TryGetValue(destination, out channel);
+
+ private static Embed FromGeneric(Core.API.Embed.EmbedBuilder embed)
+ {
+ EmbedBuilder builder = new()
+ {
+ Title = embed.Title,
+ Description = embed.Description,
+ Fields = embed.Fields?.Select(field => new EmbedFieldBuilder
+ { IsInline = field.IsInline, Name = field.Name, Value = field.Value }).ToList(),
+ Author = new()
+ {
+ IconUrl = embed.Author?.IconUrl,
+ Name = embed.Author?.Name,
+ Url = embed.Author?.Url
+ },
+ Color = Color.Parse(embed.Color),
+ ThumbnailUrl = embed.ThumbnailUrl,
+ ImageUrl = embed.ImageUrl,
+ Url = embed.Url,
+ Footer = new()
+ {
+ IconUrl = embed.Footer?.IconUrl,
+ Text = embed.Footer?.Text
+ },
+ };
+
+ if (embed.Timestamp)
+ builder.Timestamp = DateTimeOffset.UtcNow;
+
+ return builder.Build();
+ }
+
+ public static Embed[]? ToEmbeds(MessageInformation information) => information.Embeds?.Select(FromGeneric).ToArray();
+
+ public static FileAttachment? ToAttachment(MessageInformation information) => information.Attachment.HasValue
+ ? new FileAttachment(information.Attachment.Value.Stream, information.Attachment.Value.FileName)
+ : null;
+
+ private async Task WebhookSend(DiscordWebhookClient client, string? content, FileAttachment? attachment, Embed[]? embeds)
+ {
+ if (!attachment.HasValue)
+ return await client.SendMessageAsync(content, embeds: embeds);
+
+ ulong msgId = await client.SendFileAsync(attachment.Value, content, embeds: embeds);
+
+ return msgId;
+ }
+
+ internal async Task SendToChannel(SocketTextChannel channel, string? content, Embed[]? embeds, FileAttachment? attachment)
+ {
+ if (!attachment.HasValue)
+ return await channel.SendMessageAsync(content, embeds: embeds).Then(msg => msg.Id);
+
+ RestUserMessage msg = await channel.SendFileAsync(attachment.Value, content, embeds: embeds);
+
+ return msg.Id;
+ }
+
+ public override async Task SendMessage(string destination, MessageInformation information)
+ {
+ if (!TryGetFromDestination(destination, out string channel))
+ return null;
+
+ Embed[]? embeds = ToEmbeds(information);
+ FileAttachment? attachment = ToAttachment(information);
+
+ if (ulong.TryParse(channel, out ulong channelId) && Client.TryGetOrAddChannel(channelId, out SocketTextChannel socket))
+ await SendToChannel(socket, information.Content, embeds, attachment).Then(id => id.ToString());
+
+ // Webhook
+ if (Uri.TryCreate(channel, UriKind.Absolute, out Uri _))
+ {
+ using DiscordWebhookClient client = new(channel);
+
+ ulong msgId = await WebhookSend(client, information.Content, attachment, embeds);
+
+ return msgId.ToString();
+ }
+
+ return null;
+ }
+
+ public override async Task SendToWebhook(string destination, MessageInformation information)
+ {
+ if (!TryGetFromDestination(destination, out string channel))
+ return;
+
+ Embed[]? embeds = ToEmbeds(information);
+ FileAttachment? attachment = ToAttachment(information);
+
+ if (Uri.TryCreate(channel, UriKind.Absolute, out Uri _))
+ {
+ using DiscordWebhookClient client = new(channel);
+
+ await WebhookSend(client, information.Content, attachment, embeds);
+
+ return;
+ }
+
+ if (!ulong.TryParse(channel, out ulong channelId))
+ return;
+
+ if (!Client.TryGetOrAddChannel(channelId, out SocketTextChannel socket))
+ return;
+
+ IReadOnlyCollection sockets = await socket.GetWebhooksAsync();
+ RestWebhook? hook = sockets.FirstOrDefault(h => h.Creator == Client.SocketClient.CurrentUser);
+
+ if (hook == null)
+ return;
+
+ using DiscordWebhookClient channelClient = new(hook);
+
+ await WebhookSend(channelClient, information.Content, attachment, embeds);
+ }
+
+ public static void SetFrom(MessageProperties properties, MessageInformation info)
+ {
+ properties.Content = info.Content;
+ properties.Embeds = ToEmbeds(info);
+
+ FileAttachment? attachment = ToAttachment(info);
+ if (attachment.HasValue)
+ properties.Attachments = new ReadOnlyCollection([attachment.Value]);
+ }
+
+ public override async Task EditMessage(string destination, string identifier, MessageInformation information)
+ {
+ if (!ulong.TryParse(identifier, out ulong messageId))
+ throw new ArgumentException($"Identifier must be a ulong", nameof(identifier));
+
+ if (!TryGetFromDestination(destination, out string channel))
+ return;
+
+ if (!ulong.TryParse(channel, out ulong channelId) || !Client.TryGetOrAddChannel(channelId, out SocketTextChannel socket))
+ return;
+
+ await socket.ModifyMessageAsync(messageId, msg => SetFrom(msg, information));
+ }
+
+ public override async Task DeleteMessage(string destination, string identifier)
+ {
+ if (!ulong.TryParse(identifier, out ulong messageId))
+ throw new ArgumentException($"Identifier must be a ulong", nameof(identifier));
+
+ if (!TryGetFromDestination(destination, out string channel))
+ return;
+
+ if (!ulong.TryParse(channel, out ulong channelId) || !Client.TryGetOrAddChannel(channelId, out SocketTextChannel socket))
+ return;
+
+ await socket.DeleteMessageAsync(messageId);
+ }
+
+ public override async Task ThrowIfMissingMessage(string destination, string identifier)
+ {
+ if (!ulong.TryParse(identifier, out ulong messageId))
+ throw new ArgumentException($"Identifier must be a ulong", nameof(identifier));
+
+ if (!TryGetFromDestination(destination, out string channel))
+ return;
+
+ if (!ulong.TryParse(channel, out ulong channelId) || !Client.TryGetOrAddChannel(channelId, out SocketTextChannel socket))
+ return;
+
+ IMessage msg = await socket.GetMessageAsync(messageId);
+
+ if (msg == null)
+ throw new();
+ }
+
+ public override async Task SetStatus(StatusType status)
+ {
+ UserStatus userStatus = status switch
+ {
+ StatusType.Online => UserStatus.Online,
+ StatusType.Idle => UserStatus.Idle,
+ StatusType.DoNotDisturb => UserStatus.DoNotDisturb,
+ _ => throw new ArgumentOutOfRangeException(nameof(status), status, null)
+ };
+
+ await Client.SocketClient.SetStatusAsync(userStatus);
+ }
+
+ public override async Task EditActivity(string activity, Core.API.Enums.ActivityType type)
+ {
+ Discord.ActivityType activityType = type switch
+ {
+ Core.API.Enums.ActivityType.Playing => Discord.ActivityType.Playing,
+ Core.API.Enums.ActivityType.Custom => Discord.ActivityType.CustomStatus,
+ _ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
+ };
+
+ await Client.SocketClient.SetGameAsync(activity, type: activityType);
+ }
+
+ public override string? GetMessageId(string destination)
+ {
+ if (Plugin.Instance.MessageConfig.MessageIds.TryGetValue(destination, out ulong messageId))
+ return messageId.ToString();
+
+ return null;
+ }
+
+ public override void SaveMessageId(string destination, string identifier)
+ {
+ if(!ulong.TryParse(identifier, out ulong messageId))
+ throw new ArgumentException($"Identifier must be a ulong", nameof(identifier));
+
+ Plugin.Instance.MessageConfig.MessageIds.Add(destination, messageId);
+ Plugin.Instance.SaveConfig(Plugin.Instance.MessageConfig, "message-config.yml");
+ }
+}
\ No newline at end of file
diff --git a/DiscordLab.Bot/Patches/RestClientCreate.cs b/DiscordLab.Bot/Patches/RestClientCreate.cs
index df66b69..c691940 100644
--- a/DiscordLab.Bot/Patches/RestClientCreate.cs
+++ b/DiscordLab.Bot/Patches/RestClientCreate.cs
@@ -47,8 +47,6 @@ public static class RestClientCreate
/// The patched code.
public static IEnumerable Transpiler(IEnumerable instructions)
{
- Logger.Debug("Transpiler start", Plugin.Instance.Config.Debug);
-
CodeMatcher matcher = new CodeMatcher(instructions)
.MatchEndForward(
new CodeMatch(OpCodes.Ldarg_0),
@@ -77,14 +75,11 @@ public static IEnumerable Transpiler(IEnumerable
///
public override LoadPriority Priority { get; } = LoadPriority.Highest;
- ///
- /// Gets the current config for the plugin.
- ///
- public new Config Config { get; private set; } = null!;
-
private Harmony Harmony { get; } = new($"DiscordLab.Bot-{DateTime.Now.Ticks}");
+ public MessageConfig MessageConfig { get; private set; } = null!;
+
///
public override void Enable()
{
Instance = this;
- Config = base.Config!;
+ CommandHandler.Instance = new();
try
{
@@ -61,9 +60,8 @@ public override void Enable()
Harmony.PatchAll();
CallOnLoadAttribute.Load();
- CallOnReadyAttribute.Load();
- SlashCommand.FindAll();
+ ICommand.FindAll();
}
///
@@ -73,7 +71,15 @@ public override void Disable()
CallOnUnloadAttribute.Unload();
- Config = null!;
Instance = null!;
+ MessageConfig = null!;
+ CommandHandler.Instance = null!;
+ }
+
+ public override void LoadConfigs()
+ {
+ MessageConfig = this.LoadConfig("message-config.yml") ?? new();
+
+ base.LoadConfigs();
}
}
\ No newline at end of file
diff --git a/DiscordLab.BotStatus/Config.cs b/DiscordLab.BotStatus/Config.cs
index 201083a..5373e2f 100644
--- a/DiscordLab.BotStatus/Config.cs
+++ b/DiscordLab.BotStatus/Config.cs
@@ -1,10 +1,12 @@
-using Discord;
+using System.ComponentModel;
+using DiscordLab.Core.API.Enums;
namespace DiscordLab.BotStatus;
public class Config
{
- public ActivityType ActivityType { get; set; } = ActivityType.CustomStatus;
-
+ [Description("Whether Custom or Playing activity type")]
+ public ActivityType ActivityType { get; set; } = ActivityType.Custom;
+
public bool IdleOnEmpty { get; set; } = false;
}
\ No newline at end of file
diff --git a/DiscordLab.BotStatus/DiscordLab.BotStatus.csproj b/DiscordLab.BotStatus/DiscordLab.BotStatus.csproj
index 6ad1873..60cfb80 100644
--- a/DiscordLab.BotStatus/DiscordLab.BotStatus.csproj
+++ b/DiscordLab.BotStatus/DiscordLab.BotStatus.csproj
@@ -6,10 +6,10 @@
14
x64
true
- 2.6.1
+ 3.0.0
-
+
\ No newline at end of file
diff --git a/DiscordLab.BotStatus/Plugin.cs b/DiscordLab.BotStatus/Plugin.cs
index b98f555..2f3515d 100644
--- a/DiscordLab.BotStatus/Plugin.cs
+++ b/DiscordLab.BotStatus/Plugin.cs
@@ -1,7 +1,8 @@
-using Discord;
-using DiscordLab.Bot;
-using DiscordLab.Bot.API.Extensions;
-using DiscordLab.Bot.API.Features;
+using DiscordLab.Core;
+using DiscordLab.Core.API.Enums;
+using DiscordLab.Core.API.Extensions;
+using DiscordLab.Core.API.Features;
+using DiscordLab.Core.API.TranslationBuilders;
using LabApi.Events.Arguments.PlayerEvents;
using LabApi.Events.Handlers;
using LabApi.Features;
@@ -69,23 +70,21 @@ private static void UpdateStatus()
TranslationBuilder builder = new(playerCount == 0
? Instance.Translation.EmptyContent
: Instance.Translation.NormalContent);
- Task.RunAndLog(async () => await Client.SocketClient.SetGameAsync(builder, type: Instance.Config.ActivityType)
- .ConfigureAwait(false));
+
+ Task.RunAndLog(() => MessageHandler.EditActivities(builder, Instance.Config.ActivityType));
switch (playerCount)
{
case 0 when Instance.Config.IdleOnEmpty:
- Task.RunAndLog(async () => await Client.SocketClient.SetStatusAsync(UserStatus.Idle).ConfigureAwait(false), OnException);
+ Task.RunAndLog(async () => await MessageHandler.SetStatuses(StatusType.Idle).ConfigureAwait(false), OnException);
break;
- case > 0 when Instance.Config.IdleOnEmpty &&
- Client.SocketClient.Status == UserStatus.Idle:
- Task.RunAndLog(async () => await Client.SocketClient.SetStatusAsync(UserStatus.Online).ConfigureAwait(false), OnException);
+ case > 0 when Instance.Config.IdleOnEmpty:
+ Task.RunAndLog(async () => await MessageHandler.SetStatuses(StatusType.Online).ConfigureAwait(false), OnException);
break;
}
}
private static void OnException(Exception ex)
{
- // Discord.WebSocket.DiscordSocketClient.BuildCurrentStatus() throws an InvalidOperationException sometimes, so this is a blocker.
if (ex is InvalidOperationException)
return;
diff --git a/DiscordLab.ConnectionLogs/Config.cs b/DiscordLab.ConnectionLogs/Config.cs
index 7b7ba22..521d8b4 100644
--- a/DiscordLab.ConnectionLogs/Config.cs
+++ b/DiscordLab.ConnectionLogs/Config.cs
@@ -5,16 +5,14 @@ namespace DiscordLab.ConnectionLogs;
public class Config
{
[Description("The channel where the join logs will be sent.")]
- public ulong JoinChannelId { get; set; } = 0;
+ public string JoinChannel { get; set; } = "default";
[Description("The channel where the leave logs will be sent.")]
- public ulong LeaveChannelId { get; set; } = 0;
+ public string LeaveChannel { get; set; } = "default";
[Description("The channel where the round start logs will be sent.")]
- public ulong RoundStartChannelId { get; set; } = 0;
+ public string RoundStartChannel { get; set; } = "default";
[Description("The channel where the round end logs will be sent. Optional.")]
- public ulong RoundEndChannelId { get; set; } = 0;
-
- public ulong GuildId { get; set; } = 0;
+ public string RoundEndChannel { get; set; } = "default";
}
\ No newline at end of file
diff --git a/DiscordLab.ConnectionLogs/DiscordLab.ConnectionLogs.csproj b/DiscordLab.ConnectionLogs/DiscordLab.ConnectionLogs.csproj
index f737d7f..c381862 100644
--- a/DiscordLab.ConnectionLogs/DiscordLab.ConnectionLogs.csproj
+++ b/DiscordLab.ConnectionLogs/DiscordLab.ConnectionLogs.csproj
@@ -3,13 +3,13 @@
net48
enable
disable
- 12
+ 14
x64
true
- 2.6.1
+ 3.0.0
-
+
\ No newline at end of file
diff --git a/DiscordLab.ConnectionLogs/Events.cs b/DiscordLab.ConnectionLogs/Events.cs
index a56d997..576b7ca 100644
--- a/DiscordLab.ConnectionLogs/Events.cs
+++ b/DiscordLab.ConnectionLogs/Events.cs
@@ -1,8 +1,5 @@
-using Discord.WebSocket;
-using DiscordLab.Bot;
-using DiscordLab.Bot.API.Extensions;
-using DiscordLab.Bot.API.Features;
-using DiscordLab.Bot.API.Utilities;
+using DiscordLab.Core.API.Extensions;
+using DiscordLab.Core.API.TranslationBuilders;
using LabApi.Events.Arguments.PlayerEvents;
using LabApi.Events.Arguments.ServerEvents;
using LabApi.Events.CustomHandlers;
@@ -21,79 +18,31 @@ public override void OnPlayerJoined(PlayerJoinedEventArgs ev)
{
if (!Round.IsRoundInProgress)
return;
-
- if (Config.JoinChannelId == 0)
- return;
-
- if (!Client.TryGetOrAddChannel(Config.JoinChannelId, out SocketTextChannel channel))
- {
- Logger.Error(LoggingUtils.GenerateMissingChannelMessage("join log", Config.JoinChannelId, Config.GuildId));
- return;
- }
-
- if (ev.Player.ReferenceHub.serverRoles.HideFromPlayerList)
- return;
-
- Translation.PlayerJoin.SendToChannel(channel, new("player", ev.Player));
+
+ Translation.PlayerJoin.Send(Config.JoinChannel, new PlayerTranslationBuilder("player", ev.Player));
}
public override void OnPlayerLeft(PlayerLeftEventArgs ev)
{
if (!Round.IsRoundInProgress)
return;
-
- if (Config.LeaveChannelId == 0)
- return;
-
- if (!Client.TryGetOrAddChannel(Config.LeaveChannelId, out SocketTextChannel channel))
- {
- Logger.Error(
- LoggingUtils.GenerateMissingChannelMessage("leave log", Config.LeaveChannelId, Config.GuildId));
- return;
- }
-
+
if (ev.Player.ReferenceHub.serverRoles.HideFromPlayerList)
return;
- Translation.PlayerLeave.SendToChannel(channel, new("player", ev.Player));
+ Translation.PlayerLeave.Send(Config.LeaveChannel, new PlayerTranslationBuilder("player", ev.Player));
}
public override void OnServerRoundStarted()
{
- if (Config.RoundStartChannelId == 0)
- return;
-
- if (!Client.TryGetOrAddChannel(Config.RoundStartChannelId, out SocketTextChannel channel))
- {
- Logger.Error(LoggingUtils.GenerateMissingChannelMessage("round start log", Config.RoundStartChannelId,
- Config.GuildId));
- return;
- }
-
- Translation.RoundStart.SendToChannel(channel, new()
- {
- PlayerListItem = Translation.RoundPlayers
- });
+ Translation.RoundStart.Send(Config.RoundStartChannel, new AllPlayersTranslationBuilder(Translation.RoundPlayers));
}
public override void OnServerRoundEnded(RoundEndedEventArgs ev)
{
- if (Config.RoundEndChannelId == 0)
- return;
-
- if (!Client.TryGetOrAddChannel(Config.RoundEndChannelId, out SocketTextChannel channel))
- {
- Logger.Error(LoggingUtils.GenerateMissingChannelMessage("round start log", Config.RoundEndChannelId,
- Config.GuildId));
- return;
- }
-
- TranslationBuilder builder = new TranslationBuilder()
- {
- PlayerListItem = Translation.RoundPlayers
- }
+ TranslationBuilder builder = new AllPlayersTranslationBuilder(Translation.RoundPlayers)
.AddCustomReplacer("winner", ev.LeadingTeam.ToString());
- Translation.RoundEnd.SendToChannel(channel, builder);
+ Translation.RoundEnd.Send(Config.RoundEndChannel, builder);
}
}
\ No newline at end of file
diff --git a/DiscordLab.ConnectionLogs/Plugin.cs b/DiscordLab.ConnectionLogs/Plugin.cs
index b31a17d..8d35979 100644
--- a/DiscordLab.ConnectionLogs/Plugin.cs
+++ b/DiscordLab.ConnectionLogs/Plugin.cs
@@ -1,4 +1,4 @@
-using DiscordLab.Bot.API.Features;
+using DiscordLab.Core.API.Features;
using LabApi.Events.CustomHandlers;
using LabApi.Features;
diff --git a/DiscordLab.ConnectionLogs/Translation.cs b/DiscordLab.ConnectionLogs/Translation.cs
index 37bf7dc..1f6b80b 100644
--- a/DiscordLab.ConnectionLogs/Translation.cs
+++ b/DiscordLab.ConnectionLogs/Translation.cs
@@ -1,5 +1,5 @@
using System.ComponentModel;
-using DiscordLab.Bot.API.Features;
+using DiscordLab.Core;
namespace DiscordLab.ConnectionLogs;
diff --git a/DiscordLab.Bot/API/Attributes/CallOnLoadAttribute.cs b/DiscordLab.Core/API/Attributes/CallOnLoadAttribute.cs
similarity index 53%
rename from DiscordLab.Bot/API/Attributes/CallOnLoadAttribute.cs
rename to DiscordLab.Core/API/Attributes/CallOnLoadAttribute.cs
index 7c746de..6503936 100644
--- a/DiscordLab.Bot/API/Attributes/CallOnLoadAttribute.cs
+++ b/DiscordLab.Core/API/Attributes/CallOnLoadAttribute.cs
@@ -1,9 +1,9 @@
-namespace DiscordLab.Bot.API.Attributes;
-
using System.Reflection;
-using DiscordLab.Bot.API.Utilities;
+using DiscordLab.Core.API.Utilities;
using LabApi.Features.Console;
+namespace DiscordLab.Core.API.Attributes;
+
///
/// An attribute that when used on a method, will trigger whenever your plugin is loaded. Requires you to run .
///
@@ -40,23 +40,4 @@ public static void Load(Assembly? assembly = null)
}
}
}
-
- ///
- /// Logs an exception that is thrown from a method.
- ///
- /// The exception that got caught.
- /// The method that the exception was thrown from.
- /// The type the method comes from, isn't required but is useful.
- [Obsolete($"Use {nameof(LoggingUtils)}.{nameof(LoggingUtils.LogMethodError)} instead")]
- public static void LogLoadException(Exception ex, MethodInfo method, Type? type = null) =>
- LoggingUtils.LogMethodError(ex, method, type);
-
- ///
- /// Gets the full name of a method from it's and/or .
- ///
- /// The method that you want the name of.
- /// The type that the method is from, isn't required unless dynamic method is called, otherwise just the name of the method will print.
- /// The full method name.
- [Obsolete($"Use {nameof(LoggingUtils)}.{nameof(LoggingUtils.GetFullName)} instead")]
- public static string GetFullName(MethodInfo method, Type? type = null) => LoggingUtils.GetFullName(method, type);
}
\ No newline at end of file
diff --git a/DiscordLab.Bot/API/Attributes/CallOnUnloadAttribute.cs b/DiscordLab.Core/API/Attributes/CallOnUnloadAttribute.cs
similarity index 95%
rename from DiscordLab.Bot/API/Attributes/CallOnUnloadAttribute.cs
rename to DiscordLab.Core/API/Attributes/CallOnUnloadAttribute.cs
index 424057f..2e99588 100644
--- a/DiscordLab.Bot/API/Attributes/CallOnUnloadAttribute.cs
+++ b/DiscordLab.Core/API/Attributes/CallOnUnloadAttribute.cs
@@ -1,9 +1,9 @@
-namespace DiscordLab.Bot.API.Attributes;
-
using System.Reflection;
-using DiscordLab.Bot.API.Utilities;
+using DiscordLab.Core.API.Utilities;
using LabApi.Features.Console;
+namespace DiscordLab.Core.API.Attributes;
+
///
/// An attribute that when used on a method, will trigger whenever your plugin is unloaded. Requires you to run .
///
diff --git a/DiscordLab.Core/API/Commands/BaseCommandBuilder.cs b/DiscordLab.Core/API/Commands/BaseCommandBuilder.cs
new file mode 100644
index 0000000..6c92fed
--- /dev/null
+++ b/DiscordLab.Core/API/Commands/BaseCommandBuilder.cs
@@ -0,0 +1,13 @@
+using YamlDotNet.Serialization;
+
+namespace DiscordLab.Core.API.Commands;
+
+public abstract class BaseCommandBuilder
+{
+ public string Name { get; set; } = string.Empty;
+
+ public string Description { get; set; } = string.Empty;
+
+ [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitNull)]
+ public IEnumerable? Options { get; set; }
+}
\ No newline at end of file
diff --git a/DiscordLab.Core/API/Commands/CommandBuilder.cs b/DiscordLab.Core/API/Commands/CommandBuilder.cs
new file mode 100644
index 0000000..2e1ea1b
--- /dev/null
+++ b/DiscordLab.Core/API/Commands/CommandBuilder.cs
@@ -0,0 +1,9 @@
+using YamlDotNet.Serialization;
+
+namespace DiscordLab.Core.API.Commands;
+
+public class CommandBuilder : BaseCommandBuilder
+{
+ [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)]
+ public DefaultCommandPermissions DefaultPermission { get; set; } = DefaultCommandPermissions.Everyone;
+}
\ No newline at end of file
diff --git a/DiscordLab.Core/API/Commands/CommandInformation.cs b/DiscordLab.Core/API/Commands/CommandInformation.cs
new file mode 100644
index 0000000..83dd945
--- /dev/null
+++ b/DiscordLab.Core/API/Commands/CommandInformation.cs
@@ -0,0 +1,64 @@
+using System.Collections.ObjectModel;
+using System.Text.RegularExpressions;
+using NorthwoodLib.Pools;
+
+namespace DiscordLab.Core.API.Commands;
+
+public struct CommandInformation
+{
+ public string Name { get; init; }
+
+ public IEnumerable Options { get; init; }
+
+ public Dictionary? OptionsDictionary { get; init; }
+
+ public CommandInformation(string name, IEnumerable? options = null)
+ {
+ Name = name;
+ Options = options ?? [];
+ OptionsDictionary = Options.ToDictionary(opt => opt.Name, opt => opt.Value);
+ }
+
+ public CommandInformation(string name, IEnumerable optionNames, string message) : this(name, Compile(optionNames, message)) { }
+
+ private static Regex TokenRegex { get; } = new(
+ "\"([^\"]*)\"|(\\S+)",
+ RegexOptions.Compiled);
+
+ public async Task Reply(MessageInformation info) => await ReplyFunc(info);
+
+ public async Task DeferResponse() => await DeferResponseFunc();
+
+ public Func ReplyFunc { private get; init; }
+
+ public Func DeferResponseFunc { private get; init; } = () => Task.CompletedTask;
+
+ private static IEnumerable Compile(IEnumerable optionNames, string message)
+ {
+ string[] optionNamesArr = optionNames.ToArray();
+
+ List options = ListPool.Shared.Rent();
+
+ MatchCollection matches = TokenRegex.Matches(message);
+
+ int optionNameCount = optionNamesArr.Length;
+
+ if (matches.Count < optionNameCount)
+ throw new ArgumentException("Not enough inputs");
+
+ for (int i = 0; i < optionNameCount; i++)
+ {
+ Match m = matches[i];
+ string name = optionNamesArr[i];
+ string value = m.Groups[1].Success ? m.Groups[1].Value : m.Groups[2].Value;
+
+ options.Add(new(name, value));
+ }
+
+ ReadOnlyCollection returnOptions = new(options);
+
+ ListPool.Shared.Return(options);
+
+ return returnOptions;
+ }
+}
\ No newline at end of file
diff --git a/DiscordLab.Core/API/Commands/CommandOptionBuilder.cs b/DiscordLab.Core/API/Commands/CommandOptionBuilder.cs
new file mode 100644
index 0000000..6d459ed
--- /dev/null
+++ b/DiscordLab.Core/API/Commands/CommandOptionBuilder.cs
@@ -0,0 +1,15 @@
+using YamlDotNet.Serialization;
+
+namespace DiscordLab.Core.API.Commands;
+
+public class CommandOptionBuilder : BaseCommandBuilder
+{
+ [YamlIgnore]
+ public CommandOptionType Type { get; set; }
+
+ [YamlIgnore]
+ public bool IsRequired { get; set; }
+
+ [YamlIgnore]
+ public Func>? Autocomplete;
+}
\ No newline at end of file
diff --git a/DiscordLab.Core/API/Commands/CommandOptionInformation.cs b/DiscordLab.Core/API/Commands/CommandOptionInformation.cs
new file mode 100644
index 0000000..eda6483
--- /dev/null
+++ b/DiscordLab.Core/API/Commands/CommandOptionInformation.cs
@@ -0,0 +1,3 @@
+namespace DiscordLab.Core.API.Commands;
+
+public record struct CommandOptionInformation(string Name, string Value, IEnumerable? Options = null);
\ No newline at end of file
diff --git a/DiscordLab.Core/API/Commands/CommandOptionType.cs b/DiscordLab.Core/API/Commands/CommandOptionType.cs
new file mode 100644
index 0000000..ba10a8f
--- /dev/null
+++ b/DiscordLab.Core/API/Commands/CommandOptionType.cs
@@ -0,0 +1,12 @@
+namespace DiscordLab.Core.API.Commands;
+
+public enum CommandOptionType
+{
+ Subcommand,
+ String,
+ Integer,
+ Boolean,
+ User,
+ Channel,
+ Role,
+}
\ No newline at end of file
diff --git a/DiscordLab.Core/API/Commands/DefaultCommandPermissions.cs b/DiscordLab.Core/API/Commands/DefaultCommandPermissions.cs
new file mode 100644
index 0000000..a100939
--- /dev/null
+++ b/DiscordLab.Core/API/Commands/DefaultCommandPermissions.cs
@@ -0,0 +1,8 @@
+namespace DiscordLab.Core.API.Commands;
+
+public enum DefaultCommandPermissions
+{
+ Everyone,
+ Moderators,
+ Admins
+}
\ No newline at end of file
diff --git a/DiscordLab.Core/API/Commands/GameUserCommandOptionBuilder.cs b/DiscordLab.Core/API/Commands/GameUserCommandOptionBuilder.cs
new file mode 100644
index 0000000..18c9aac
--- /dev/null
+++ b/DiscordLab.Core/API/Commands/GameUserCommandOptionBuilder.cs
@@ -0,0 +1,43 @@
+using LabApi.Features.Wrappers;
+using NorthwoodLib.Pools;
+using YamlDotNet.Serialization;
+
+namespace DiscordLab.Core.API.Commands;
+
+public class GameUserCommandOptionBuilder : CommandOptionBuilder
+{
+ public GameUserCommandOptionBuilder()
+ {
+ Type = CommandOptionType.String;
+ Autocomplete = information =>
+ {
+ List<(string name, string value)> list = new();
+ foreach (Player player in Player.ReadyList)
+ {
+ if (!player.Nickname.Contains(information.Value))
+ continue;
+
+ string name = $"{player.Nickname} ({player.PlayerId})";
+ string value = player.UserId;
+
+ if (ListType == ListType.All)
+ list.Add((name, value));
+ else if (ListType == ListType.Players && !player.IsNpc)
+ list.Add((name, value));
+ else if (ListType == ListType.Dummies && player.IsNpc)
+ list.Add((name, value));
+ }
+
+ return list;
+ };
+ }
+
+ [YamlIgnore] public ListType ListType { get; set; } = ListType.All;
+}
+
+public enum ListType
+{
+ All,
+ Players,
+ Dummies
+}
\ No newline at end of file
diff --git a/DiscordLab.Core/API/Commands/ICommand.cs b/DiscordLab.Core/API/Commands/ICommand.cs
new file mode 100644
index 0000000..558876d
--- /dev/null
+++ b/DiscordLab.Core/API/Commands/ICommand.cs
@@ -0,0 +1,32 @@
+using System.Collections.ObjectModel;
+using System.Reflection;
+
+namespace DiscordLab.Core.API.Commands;
+
+public interface ICommand
+{
+ public CommandBuilder Data { get; }
+
+ public Task Execute(CommandInformation data);
+
+ public static ObservableCollection Commands = new();
+
+ ///
+ /// Finds and creates all slash commands in your plugin. There is no method to delete all your commands, as that is handled by the bot itself.
+ ///
+ /// The assembly you wish to check, defaults to the current one.
+ public static void FindAll(Assembly? assembly = null)
+ {
+ assembly ??= Assembly.GetCallingAssembly();
+
+ foreach (Type type in assembly.GetTypes())
+ {
+ if (type.IsAbstract || !typeof(ICommand).IsAssignableFrom(type))
+ continue;
+
+ if (Activator.CreateInstance(type) is not ICommand init)
+ continue;
+ Commands.Add(init);
+ }
+ }
+}
\ No newline at end of file
diff --git a/DiscordLab.Core/API/Embed/EmbedAuthorBuilder.cs b/DiscordLab.Core/API/Embed/EmbedAuthorBuilder.cs
new file mode 100644
index 0000000..a76af1d
--- /dev/null
+++ b/DiscordLab.Core/API/Embed/EmbedAuthorBuilder.cs
@@ -0,0 +1,29 @@
+using YamlDotNet.Serialization;
+
+namespace DiscordLab.Core.API.Embed;
+
+///
+/// Contains information about an author field in an embed.
+///
+public class EmbedAuthorBuilder
+{
+ ///
+ /// Gets or sets the author name.
+ ///
+ [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitNull)]
+ public string? Name { get; set; }
+
+ ///
+ /// Gets or sets the icon URL.
+ ///
+ [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitNull)]
+ public string? IconUrl { get; set; }
+
+ ///
+ /// Gets or sets the URL.
+ ///
+ [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitNull)]
+ public string? Url { get; set; }
+
+ public EmbedAuthorBuilder Clone() => (EmbedAuthorBuilder)MemberwiseClone();
+}
\ No newline at end of file
diff --git a/DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs b/DiscordLab.Core/API/Embed/EmbedBuilder.cs
similarity index 53%
rename from DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs
rename to DiscordLab.Core/API/Embed/EmbedBuilder.cs
index 4ace171..1b73fed 100644
--- a/DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs
+++ b/DiscordLab.Core/API/Embed/EmbedBuilder.cs
@@ -1,12 +1,14 @@
-namespace DiscordLab.Bot.API.Features.Embed;
+using YamlDotNet.Serialization;
-using YamlDotNet.Serialization;
+namespace DiscordLab.Core.API.Embed;
///
/// Allows you to make an embed. Should be used in translations only.
///
public class EmbedBuilder
{
+ public const int MaxDescriptionLength = 4096;
+
///
/// Gets or sets the embed title.
///
@@ -67,60 +69,27 @@ public class EmbedBuilder
[YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)]
public bool Timestamp { get; set; }
- ///
- /// Changes a into a instance.
- ///
- /// The instance.
- /// A copy of the instance.
- public static implicit operator Discord.EmbedBuilder(EmbedBuilder builder)
+ public EmbedBuilder Clone()
{
if (
- string.IsNullOrEmpty(builder.Title)
- && string.IsNullOrEmpty(builder.Description)
- && string.IsNullOrEmpty(builder.ThumbnailUrl)
- && string.IsNullOrEmpty(builder.ImageUrl)
- && (builder.Author == null || string.IsNullOrEmpty(builder.Author.Name))
- && (builder.Fields == null || !builder.Fields.Any()))
+ string.IsNullOrEmpty(Title)
+ && string.IsNullOrEmpty(Description)
+ && string.IsNullOrEmpty(ThumbnailUrl)
+ && string.IsNullOrEmpty(ImageUrl)
+ && (Author == null || string.IsNullOrEmpty(Author.Name))
+ && (Fields == null || !Fields.Any()))
{
- throw new ArgumentNullException("An embed must contain at least on of the following: title, description, thumbnail, image, author (with a name) or at least 1 field.");
+ throw new ArgumentNullException(nameof(Description), "An embed must contain at least on of the following: title, description, thumbnail, image, author (with a name) or at least 1 field.");
}
- Discord.EmbedBuilder copy = new();
-
- if (builder.Title != null)
- copy.WithTitle(builder.Title);
-
- if (builder.Description != null)
- copy.WithDescription(builder.Description);
-
- if (builder.Color != null)
- copy.WithColor(Discord.Color.Parse(builder.Color));
-
- if (builder.Url != null)
- copy.WithUrl(builder.Url);
-
- if (builder.ImageUrl != null)
- copy.WithImageUrl(builder.ImageUrl);
+ EmbedBuilder copy = (EmbedBuilder)MemberwiseClone();
- if (builder.ThumbnailUrl != null)
- copy.WithThumbnailUrl(builder.ThumbnailUrl);
-
- if (builder.Timestamp)
- copy.WithCurrentTimestamp();
-
- if (builder.Footer != null)
- copy.WithFooter(builder.Footer);
-
- if (builder.Author != null)
- copy.WithAuthor(builder.Author);
-
- if (builder.Fields != null)
- {
- foreach (var field in builder.Fields)
- {
- copy.AddField(field);
- }
- }
+ if(copy.Fields != null)
+ copy.Fields = copy.Fields?.Select(field => field.Clone());
+ if (copy.Author != null)
+ copy.Author = copy.Author.Clone();
+ if (copy.Footer != null)
+ copy.Footer = copy.Footer.Clone();
return copy;
}
diff --git a/DiscordLab.Core/API/Embed/EmbedFieldBuilder.cs b/DiscordLab.Core/API/Embed/EmbedFieldBuilder.cs
new file mode 100644
index 0000000..ddab6a6
--- /dev/null
+++ b/DiscordLab.Core/API/Embed/EmbedFieldBuilder.cs
@@ -0,0 +1,27 @@
+using YamlDotNet.Serialization;
+
+namespace DiscordLab.Core.API.Embed;
+
+///
+/// Allows you to create embed fields for a .
+///
+public class EmbedFieldBuilder
+{
+ ///
+ /// Gets or sets the field name.
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the field value.
+ ///
+ public string? Value { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the field is inline.
+ ///
+ [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)]
+ public bool IsInline { get; set; }
+
+ public EmbedFieldBuilder Clone() => (EmbedFieldBuilder)MemberwiseClone();
+}
\ No newline at end of file
diff --git a/DiscordLab.Core/API/Embed/EmbedFooterBuilder.cs b/DiscordLab.Core/API/Embed/EmbedFooterBuilder.cs
new file mode 100644
index 0000000..abe2788
--- /dev/null
+++ b/DiscordLab.Core/API/Embed/EmbedFooterBuilder.cs
@@ -0,0 +1,23 @@
+using YamlDotNet.Serialization;
+
+namespace DiscordLab.Core.API.Embed;
+
+///
+/// Holds information for an Embed footer.
+///
+public class EmbedFooterBuilder
+{
+ ///
+ /// Gets or sets the text for this footer.
+ ///
+ [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitNull)]
+ public string? Text { get; set; }
+
+ ///
+ /// Gets or sets the icon URl for this footer.
+ ///
+ [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitNull)]
+ public string? IconUrl { get; set; }
+
+ public EmbedFooterBuilder Clone() => (EmbedFooterBuilder)MemberwiseClone();
+}
\ No newline at end of file
diff --git a/DiscordLab.Core/API/Enums/ActivityType.cs b/DiscordLab.Core/API/Enums/ActivityType.cs
new file mode 100644
index 0000000..67a33ca
--- /dev/null
+++ b/DiscordLab.Core/API/Enums/ActivityType.cs
@@ -0,0 +1,7 @@
+namespace DiscordLab.Core.API.Enums;
+
+public enum ActivityType
+{
+ Playing,
+ Custom
+}
\ No newline at end of file
diff --git a/DiscordLab.Core/API/Enums/StatusType.cs b/DiscordLab.Core/API/Enums/StatusType.cs
new file mode 100644
index 0000000..05a64e6
--- /dev/null
+++ b/DiscordLab.Core/API/Enums/StatusType.cs
@@ -0,0 +1,8 @@
+namespace DiscordLab.Core.API.Enums;
+
+public enum StatusType
+{
+ Online,
+ Idle,
+ DoNotDisturb
+}
\ No newline at end of file
diff --git a/DiscordLab.Bot/API/Extensions/BitwiseExtensions.cs b/DiscordLab.Core/API/Extensions/BitwiseExtensions.cs
similarity index 92%
rename from DiscordLab.Bot/API/Extensions/BitwiseExtensions.cs
rename to DiscordLab.Core/API/Extensions/BitwiseExtensions.cs
index 2fac01d..83cb016 100644
--- a/DiscordLab.Bot/API/Extensions/BitwiseExtensions.cs
+++ b/DiscordLab.Core/API/Extensions/BitwiseExtensions.cs
@@ -1,4 +1,4 @@
-namespace DiscordLab.Bot.API.Extensions;
+namespace DiscordLab.Core.API.Extensions;
///
/// Contains extension methods for bitwise operations.
diff --git a/DiscordLab.Core/API/Extensions/CollectionExtensions.cs b/DiscordLab.Core/API/Extensions/CollectionExtensions.cs
new file mode 100644
index 0000000..c329d22
--- /dev/null
+++ b/DiscordLab.Core/API/Extensions/CollectionExtensions.cs
@@ -0,0 +1,11 @@
+namespace DiscordLab.Core.API.Extensions;
+
+public static class CollectionExtensions
+{
+ extension(IEnumerable collection)
+ {
+ public IEnumerable> ChunkBy(int chunkSize) => collection
+ .Select((x, i) => new { Index = i, Value = x }).GroupBy(x => x.Index / chunkSize)
+ .Select(x => x.Select(v => v.Value).ToList());
+ }
+}
\ No newline at end of file
diff --git a/DiscordLab.Core/API/Extensions/EmbedBuilderExtensions.cs b/DiscordLab.Core/API/Extensions/EmbedBuilderExtensions.cs
new file mode 100644
index 0000000..2d02372
--- /dev/null
+++ b/DiscordLab.Core/API/Extensions/EmbedBuilderExtensions.cs
@@ -0,0 +1,76 @@
+using DiscordLab.Core.API.Embed;
+using DiscordLab.Core.API.TranslationBuilders;
+using NorthwoodLib.Pools;
+
+namespace DiscordLab.Core.API.Extensions;
+
+public static class EmbedBuilderExtensions
+{
+ extension(EmbedBuilder builder)
+ {
+ public EmbedBuilder CloneWithTranslation(TranslationBuilder translation)
+ {
+ List contents = ListPool.Shared.Rent();
+
+ contents.Add(builder.Title.OrIfEmpty());
+ contents.Add(builder.Description.OrIfEmpty());
+ contents.Add(builder.Url.OrIfEmpty());
+ contents.Add(builder.ImageUrl.OrIfEmpty());
+ contents.Add(builder.ThumbnailUrl.OrIfEmpty());
+ contents.Add(builder.Footer?.Text.OrIfEmpty()!);
+ contents.Add(builder.Footer?.IconUrl.OrIfEmpty()!);
+
+ foreach (EmbedFieldBuilder field in builder.Fields ?? [])
+ {
+ contents.Add(field.Name.OrIfEmpty());
+ contents.Add(field.Value.OrIfEmpty());
+ }
+
+ string delimiter = $"|{Guid.NewGuid()}|";
+ string combined = string.Join(delimiter, contents);
+ ListPool.Shared.Return(contents);
+ string replaced = translation.Build(combined);
+ if (replaced.Split(delimiter) is not
+ [
+ { } embedTitle, { } embedDescription, { } embedUrl, { } imageUrl, { } thumbnailUrl,
+ { } footer, { } iconUrl, .. { } fields
+ ])
+ throw new("Failed to build out the message content");
+
+ EmbedBuilder embed = new()
+ {
+ Title = embedTitle,
+ Description = embedDescription,
+ Url = embedUrl,
+ ImageUrl = imageUrl,
+ ThumbnailUrl = thumbnailUrl,
+ Footer = new()
+ {
+ Text = footer,
+ IconUrl = iconUrl
+ },
+ Fields = new List()
+ };
+
+ IEnumerable[] fieldsChunked = fields.ChunkBy(2).ToArray();
+
+ for (int i = 0; i < fieldsChunked.Length; i++)
+ {
+ string[] fieldsArr = fieldsChunked[i].ToArray();
+
+ if (fieldsArr is not [{ } fieldName, { } fieldValue])
+ continue;
+
+ if (embed.Fields is not List fieldList)
+ {
+ fieldList = new();
+ embed.Fields = fieldList;
+ }
+
+ fieldList.Add(new() { Name = fieldName, Value = fieldValue, IsInline = builder.Fields!.ElementAt(i).IsInline });
+ }
+
+ return embed;
+ }
+ }
+}
\ No newline at end of file
diff --git a/DiscordLab.Core/API/Extensions/HttpExtensions.cs b/DiscordLab.Core/API/Extensions/HttpExtensions.cs
new file mode 100644
index 0000000..418f30e
--- /dev/null
+++ b/DiscordLab.Core/API/Extensions/HttpExtensions.cs
@@ -0,0 +1,17 @@
+using System.Net.Http;
+using System.Text.Json;
+
+namespace DiscordLab.Core.API.Extensions;
+
+public static class HttpExtensions
+{
+ extension(HttpContent content)
+ {
+ public async Task ReadFromJson()
+ {
+ string raw = await content.ReadAsStringAsync();
+
+ return JsonSerializer.Deserialize(raw);
+ }
+ }
+}
\ No newline at end of file
diff --git a/DiscordLab.Core/API/Extensions/MessageContentExtensions.cs b/DiscordLab.Core/API/Extensions/MessageContentExtensions.cs
new file mode 100644
index 0000000..574b188
--- /dev/null
+++ b/DiscordLab.Core/API/Extensions/MessageContentExtensions.cs
@@ -0,0 +1,23 @@
+using DiscordLab.Core.API.Commands;
+using DiscordLab.Core.API.TranslationBuilders;
+
+namespace DiscordLab.Core.API.Extensions;
+
+public static class MessageContentExtensions
+{
+ extension(MessageContent content)
+ {
+ public Task Send(string destination, TranslationBuilder? builder = null, bool saveIds = false) =>
+ Task.RunAndLog(async () => await content.SendAsync(destination, builder ?? new(), saveIds));
+
+ public Task Modify(string destination, TranslationBuilder? builder = null) =>
+ Task.RunAndLog(async () => await content.ModifyAsync(destination, builder ?? new()));
+
+ public async Task SendAsync(string destination, TranslationBuilder builder, bool saveIds = false) => await MessageHandler.SendMessages(destination, content.Build(builder), saveIds);
+
+ public async Task ModifyAsync(string destination, TranslationBuilder builder) => await MessageHandler.EditMessages(destination, content.Build(builder));
+
+ public async Task InteractionResponseAsync(CommandInformation information, TranslationBuilder builder) =>
+ await information.Reply(content.Build(builder));
+ }
+}
\ No newline at end of file
diff --git a/DiscordLab.Bot/API/Extensions/RegexExtensions.cs b/DiscordLab.Core/API/Extensions/RegexExtensions.cs
similarity index 94%
rename from DiscordLab.Bot/API/Extensions/RegexExtensions.cs
rename to DiscordLab.Core/API/Extensions/RegexExtensions.cs
index 09794c6..d119558 100644
--- a/DiscordLab.Bot/API/Extensions/RegexExtensions.cs
+++ b/DiscordLab.Core/API/Extensions/RegexExtensions.cs
@@ -1,7 +1,7 @@
-namespace DiscordLab.Bot.API.Extensions;
-
using System.Text.RegularExpressions;
+namespace DiscordLab.Core.API.Extensions;
+
///
/// Utility extension methods for Regex.
///
diff --git a/DiscordLab.Core/API/Extensions/StringExtensions.cs b/DiscordLab.Core/API/Extensions/StringExtensions.cs
new file mode 100644
index 0000000..f0dd1af
--- /dev/null
+++ b/DiscordLab.Core/API/Extensions/StringExtensions.cs
@@ -0,0 +1,9 @@
+namespace DiscordLab.Core.API.Extensions;
+
+public static class StringExtensions
+{
+ extension(string? str)
+ {
+ public string OrIfEmpty(string? replacer = null) => string.IsNullOrEmpty(str) ? replacer ?? string.Empty : str!;
+ }
+}
\ No newline at end of file
diff --git a/DiscordLab.Bot/API/Extensions/TaskExtensions.cs b/DiscordLab.Core/API/Extensions/TaskExtensions.cs
similarity index 84%
rename from DiscordLab.Bot/API/Extensions/TaskExtensions.cs
rename to DiscordLab.Core/API/Extensions/TaskExtensions.cs
index b1e3247..932467b 100644
--- a/DiscordLab.Bot/API/Extensions/TaskExtensions.cs
+++ b/DiscordLab.Core/API/Extensions/TaskExtensions.cs
@@ -1,6 +1,6 @@
-namespace DiscordLab.Bot.API.Extensions;
+using LabApi.Features.Console;
-using LabApi.Features.Console;
+namespace DiscordLab.Core.API.Extensions;
///
/// Extension methods to help with Tasks.
@@ -46,6 +46,21 @@ public static Task RunAndLog(Func task, Action? onException = n
});
}
+ extension(Task task)
+ {
+ public async Task Then(Func then)
+ {
+ T result = await task;
+ return then(result);
+ }
+
+ public async Task Then(Action then)
+ {
+ T result = await task;
+ then(result);
+ }
+ }
+
private static bool IsDiscordException(Exception ex) => ex.Source?.StartsWith("Discord.Net") == true ||
ex.TargetSite?.DeclaringType?.Namespace?.StartsWith(
"Discord") == true ||
diff --git a/DiscordLab.Core/API/Features/Attachment.cs b/DiscordLab.Core/API/Features/Attachment.cs
new file mode 100644
index 0000000..d3bcfbf
--- /dev/null
+++ b/DiscordLab.Core/API/Features/Attachment.cs
@@ -0,0 +1,3 @@
+namespace DiscordLab.Core.API.Features;
+
+public record struct Attachment(MemoryStream Stream, string FileName);
\ No newline at end of file
diff --git a/DiscordLab.Bot/API/Features/Plugin.cs b/DiscordLab.Core/API/Features/Plugin.cs
similarity index 96%
rename from DiscordLab.Bot/API/Features/Plugin.cs
rename to DiscordLab.Core/API/Features/Plugin.cs
index 6a15d8c..b80638d 100644
--- a/DiscordLab.Bot/API/Features/Plugin.cs
+++ b/DiscordLab.Core/API/Features/Plugin.cs
@@ -1,7 +1,7 @@
-namespace DiscordLab.Bot.API.Features;
-
using LabApi.Loader;
+namespace DiscordLab.Core.API.Features;
+
///
/// Allows users to easily make plugins with translations also, not just configs.
///
diff --git a/DiscordLab.Bot/API/Features/Queue.cs b/DiscordLab.Core/API/Features/Queue.cs
similarity index 97%
rename from DiscordLab.Bot/API/Features/Queue.cs
rename to DiscordLab.Core/API/Features/Queue.cs
index 02275bf..a75146e 100644
--- a/DiscordLab.Bot/API/Features/Queue.cs
+++ b/DiscordLab.Core/API/Features/Queue.cs
@@ -1,7 +1,7 @@
-namespace DiscordLab.Bot.API.Features;
-
using MEC;
+namespace DiscordLab.Core.API.Features;
+
///
/// A utility class that helps with dealing with Discord rate limits by introducing a queue.
///
diff --git a/DiscordLab.Core/API/TranslationBuilders/AllPlayersTranslationBuilder.cs b/DiscordLab.Core/API/TranslationBuilders/AllPlayersTranslationBuilder.cs
new file mode 100644
index 0000000..970b017
--- /dev/null
+++ b/DiscordLab.Core/API/TranslationBuilders/AllPlayersTranslationBuilder.cs
@@ -0,0 +1,33 @@
+using LabApi.Features.Enums;
+using LabApi.Features.Wrappers;
+
+namespace DiscordLab.Core.API.TranslationBuilders;
+
+public class AllPlayersTranslationBuilder : PlayerListTranslationBuilder
+{
+ public static Dictionary>> PlayerLists = new()
+ {
+ // All players, including dummies/NPCs
+ ["players"] = static () => Player.ReadyList,
+ // Only real players
+ ["players no npcs"] = static () => Player.GetAll(PlayerSearchFlags.AuthenticatedPlayers),
+ // Only NPCs/Dummies.
+ ["npcs"] = static () => Player.NpcList,
+ // Everything except NPCs hidden on player list
+ ["players hidden npcs"] = static () => Player.ReadyList.Where(player => !player.IsDummy || !player.ReferenceHub.serverRoles.HideFromPlayerList)
+ };
+
+ public AllPlayersTranslationBuilder(string entry)
+ {
+ foreach (KeyValuePair>> pair in PlayerLists)
+ {
+ AddPlayersList(pair.Value, entry, pair.Key);
+ }
+ }
+
+ public AllPlayersTranslationBuilder(string translation, string entry)
+ : this(entry)
+ {
+ Translation = translation;
+ }
+}
\ No newline at end of file
diff --git a/DiscordLab.Core/API/TranslationBuilders/PlayerListTranslationBuilder.cs b/DiscordLab.Core/API/TranslationBuilders/PlayerListTranslationBuilder.cs
new file mode 100644
index 0000000..334b2a6
--- /dev/null
+++ b/DiscordLab.Core/API/TranslationBuilders/PlayerListTranslationBuilder.cs
@@ -0,0 +1,40 @@
+using System.Text;
+using LabApi.Features.Wrappers;
+using NorthwoodLib.Pools;
+
+namespace DiscordLab.Core.API.TranslationBuilders;
+
+public class PlayerListTranslationBuilder : PlayerTranslationBuilder
+{
+ protected PlayerListTranslationBuilder() {}
+
+ protected PlayerListTranslationBuilder(string translation) : base(translation) {}
+
+ public PlayerListTranslationBuilder(IEnumerable players, string entry)
+ {
+ AddPlayersList(players, entry);
+ }
+
+ public PlayerListTranslationBuilder(string translation, IEnumerable players, string entry)
+ : this(players, entry)
+ {
+ Translation = translation;
+ }
+
+ public void AddPlayersList(IEnumerable players, string entry, string customReplaceName = "players") =>
+ AddPlayersList(() => players, entry, customReplaceName);
+
+
+ public void AddPlayersList(Func> playersFunc, string entry, string customReplaceName = "players") =>
+ CustomReplacers.Add(CreateRegex(customReplaceName), () =>
+ {
+ StringBuilder builder = StringBuilderPool.Shared.Rent();
+
+ foreach (Player player in playersFunc())
+ {
+ builder.AppendLine(ReplacePlayer(entry, new("player", player)));
+ }
+
+ return StringBuilderPool.Shared.ToStringReturn(builder);
+ });
+}
\ No newline at end of file
diff --git a/DiscordLab.Core/API/TranslationBuilders/PlayerTranslationBuilder.cs b/DiscordLab.Core/API/TranslationBuilders/PlayerTranslationBuilder.cs
new file mode 100644
index 0000000..5ef5988
--- /dev/null
+++ b/DiscordLab.Core/API/TranslationBuilders/PlayerTranslationBuilder.cs
@@ -0,0 +1,133 @@
+using System.Globalization;
+using System.Text.RegularExpressions;
+using DiscordLab.Core.API.Extensions;
+using LabApi.Features.Wrappers;
+using Mirror.LiteNetLib4Mirror;
+using PlayerRoles;
+
+namespace DiscordLab.Core.API.TranslationBuilders;
+
+public class PlayerTranslationBuilder : TranslationBuilder
+{
+ ///
+ /// Gets player based replacements.
+ ///
+ public static Dictionary> PlayerReplacers { get; } = new()
+ {
+ ["name"] = static player =>
+ player.Nickname.Replace("@everyone", "@\u200beveryone").Replace("@here", "@\u200bhere").Trim(),
+ ["nickname"] = static player =>
+ player.Nickname.Replace("@everyone", "@\u200beveryone").Replace("@here", "@\u200bhere").Trim(),
+ ["displayname"] = static player => player.DisplayName,
+ ["id"] = static player => player.UserId,
+ ["ip"] = static player => player.IpAddress,
+ ["userid"] = static player => player.PlayerId.ToString(),
+ ["role"] = static player => player.RoleBase.RoleName,
+ ["roletype"] = static player => player.Role.ToString(),
+ ["team"] = static player => player.Team.ToString(),
+ ["faction"] = static player => player.Team.GetFaction().ToString(),
+ ["health"] = static player => player.Health.ToString(CultureInfo.CurrentCulture),
+ ["maxhealth"] = static player => player.MaxHealth.ToString(CultureInfo.CurrentCulture),
+ ["group"] = static player => player.GroupName,
+ ["badgecolor"] = static player => player.GroupColor.ToString(),
+ ["hasdnt"] = static player => player.DoNotTrack.ToString(),
+ ["hasra"] = static player => player.RemoteAdminAccess.ToString(),
+ ["isnorthwood"] = static player => player.IsNorthwoodStaff.ToString(),
+ ["room"] = static player => player.Room?.ToString() ?? "None",
+ ["zone"] = static player => player.Zone.ToString(),
+ ["position"] = static player => player.Position.ToString(),
+ ["ping"] = static player => LiteNetLib4MirrorServer.GetPing(player.Connection.connectionId).ToString(),
+ ["isglobalmod"] = static player => player.IsGlobalModerator.ToString(),
+ ["permissiongroup"] = static player => player.PermissionsGroupName ?? "None",
+ };
+
+ public PlayerTranslationBuilder() {}
+
+ public PlayerTranslationBuilder(string translation) : base(translation) {}
+
+ public PlayerTranslationBuilder(string playerKey, Player player)
+ {
+ AddPlayer(playerKey, player);
+ }
+
+ public PlayerTranslationBuilder(string translation, string playerKey, Player player) : this(playerKey, player)
+ {
+ Translation = translation;
+ }
+
+ ///
+ /// Gets or sets the players that need to be translated for, if any.
+ ///
+ public Dictionary Players { get; set; } = new();
+
+ ///
+ /// Adds multiple players to the list.
+ ///
+ /// The players to add.
+ /// The instance.
+ public PlayerTranslationBuilder AddPlayers(Dictionary players)
+ {
+ foreach (KeyValuePair pair in players)
+ {
+ Players.Add(pair.Key, pair.Value);
+ }
+
+ return this;
+ }
+
+ ///
+ /// Adds a player to the list.
+ ///
+ /// The prefix for the player.
+ /// The to add.
+ /// The instance.
+ public PlayerTranslationBuilder AddPlayer(string prefix, Player player)
+ {
+ Players.Add(prefix, player);
+
+ return this;
+ }
+
+ ///
+ /// Builds this instance.
+ ///
+ /// The translation to build from, isn't needed if is defined.
+ /// The translation built.
+ public new string Build(string? translation = null)
+ {
+ string output = base.Build(translation);
+
+ foreach (KeyValuePair player in Players)
+ {
+ if (player.Value is not { IsReady: true })
+ continue;
+
+ Regex baseRegex = CachedRegex.GetOrAdd(player.Key, () => CreateRegex(player.Key));
+
+ output = baseRegex.Replace(output, player.Value.Nickname);
+
+ output = ReplacePlayer(output, player);
+ }
+
+ return output;
+ }
+
+ protected string ReplacePlayer(string message, KeyValuePair pair)
+ {
+ string output = message;
+
+ foreach (KeyValuePair> replacer in PlayerReplacers)
+ {
+ Regex regex = CachedRegex.GetOrAdd(
+ $"{pair.Key}{replacer.Key}",
+ () => CreateRegex($"{pair.Key}{replacer.Key}"));
+
+ output = regex.CheckReplace(output, Replacement);
+ continue;
+
+ string Replacement() => GetReplacer(() => replacer.Value(pair.Value));
+ }
+
+ return output;
+ }
+}
\ No newline at end of file
diff --git a/DiscordLab.Bot/API/Features/TranslationBuilder.cs b/DiscordLab.Core/API/TranslationBuilders/TranslationBuilder.cs
similarity index 59%
rename from DiscordLab.Bot/API/Features/TranslationBuilder.cs
rename to DiscordLab.Core/API/TranslationBuilders/TranslationBuilder.cs
index 953e113..1019a02 100644
--- a/DiscordLab.Bot/API/Features/TranslationBuilder.cs
+++ b/DiscordLab.Core/API/TranslationBuilders/TranslationBuilder.cs
@@ -1,19 +1,16 @@
// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable PropertyCanBeMadeInitOnly.Global
-namespace DiscordLab.Bot.API.Features;
-
using System.Globalization;
using System.Text.RegularExpressions;
-using Discord;
-using DiscordLab.Bot.API.Extensions;
+using DiscordLab.Core.API.Extensions;
using LabApi.Features.Wrappers;
using LightContainmentZoneDecontamination;
-using Mirror.LiteNetLib4Mirror;
-using PlayerRoles;
using RoundRestarting;
using UnityEngine;
+namespace DiscordLab.Core.API.TranslationBuilders;
+
///
/// Allows you to create translations with placeholders being replaced.
///
@@ -31,16 +28,6 @@ public TranslationBuilder()
{
}
- ///
- /// Initializes a new instance of the class with a person added.
- ///
- /// The player prefix.
- /// The player to use for the prefix.
- public TranslationBuilder(string playerPrefix, Player player)
- {
- AddPlayer(playerPrefix, player);
- }
-
///
/// Initializes a new instance of the class.
///
@@ -50,18 +37,6 @@ public TranslationBuilder(string translation)
Translation = translation;
}
- ///
- /// Initializes a new instance of the class with a person added.
- ///
- /// The translation to modify.
- /// The player prefix.
- /// The player to use for the prefix.
- public TranslationBuilder(string translation, string playerPrefix, Player player)
- {
- Translation = translation;
- AddPlayer(playerPrefix, player);
- }
-
///
/// Gets the dictionary of replacers that have no argument.
///
@@ -129,48 +104,11 @@ public TranslationBuilder(string translation, string playerPrefix, Player player
[CreateRegex("minutessince")] = static time => TimeSince(time).Minutes.ToString(CultureInfo.InvariantCulture),
};
- ///
- /// Gets player based replacements.
- ///
- public static Dictionary> PlayerReplacers { get; } = new()
- {
- ["name"] = static player =>
- player.Nickname.Replace("@everyone", "@\u200beveryone").Replace("@here", "@\u200bhere").Trim(),
- ["nickname"] = static player =>
- player.Nickname.Replace("@everyone", "@\u200beveryone").Replace("@here", "@\u200bhere").Trim(),
- ["displayname"] = static player => player.DisplayName,
- ["id"] = static player => player.UserId,
- ["ip"] = static player => player.IpAddress,
- ["userid"] = static player => player.PlayerId.ToString(),
- ["role"] = static player => player.RoleBase.RoleName,
- ["roletype"] = static player => player.Role.ToString(),
- ["team"] = static player => player.Team.ToString(),
- ["faction"] = static player => player.Team.GetFaction().ToString(),
- ["health"] = static player => player.Health.ToString(CultureInfo.CurrentCulture),
- ["maxhealth"] = static player => player.MaxHealth.ToString(CultureInfo.CurrentCulture),
- ["group"] = static player => player.GroupName,
- ["badgecolor"] = static player => player.GroupColor.ToString(),
- ["hasdnt"] = static player => player.DoNotTrack.ToString(),
- ["hasra"] = static player => player.RemoteAdminAccess.ToString(),
- ["isnorthwood"] = static player => player.IsNorthwoodStaff.ToString(),
- ["room"] = static player => player.Room?.ToString() ?? "None",
- ["zone"] = static player => player.Zone.ToString(),
- ["position"] = static player => player.Position.ToString(),
- ["ping"] = static player => LiteNetLib4MirrorServer.GetPing(player.Connection.connectionId).ToString(),
- ["isglobalmod"] = static player => player.IsGlobalModerator.ToString(),
- ["permissiongroup"] = static player => player.PermissionsGroupName ?? "None",
- };
-
///
/// Gets or sets a Dictionary of custom replacers. Key is the text to replace and value is the factory to replace with.
///
public Dictionary> CustomReplacers { get; set; } = new();
- ///
- /// Gets or sets the players that need to be translated for, if any.
- ///
- public Dictionary Players { get; set; } = new();
-
///
/// Gets or sets the time that this translation will use.
///
@@ -181,26 +119,10 @@ public TranslationBuilder(string translation, string playerPrefix, Player player
///
public string? Translation { get; set; }
- ///
- /// Gets or sets the item that will show for each player when the {players} placeholder is used. Defaults to null.
- ///
- /// If you want the {players} placeholder to not work, set this to null.
- public string? PlayerListItem { get; set; }
-
- ///
- /// Gets or sets the separator between items in .
- ///
- public string PlayerListSeparator { get; set; } = "\n";
-
- ///
- /// Gets or sets the player list that will be used for .
- ///
- public IEnumerable? PlayerList { get; set; }
-
///
/// Gets a Dictionary of cached regexes that are unknown.
///
- private static Dictionary CachedRegex { get; } = new();
+ protected static Dictionary CachedRegex { get; } = new();
///
/// .
@@ -210,14 +132,6 @@ public TranslationBuilder(string translation, string playerPrefix, Player player
public static implicit operator string(TranslationBuilder builder) =>
builder.Build();
- ///
- /// .
- ///
- /// The instance.
- ///
- public static implicit operator Optional(TranslationBuilder builder) =>
- builder.Build();
-
///
/// Creates a compatible placeholder regex.
///
@@ -226,34 +140,6 @@ public static implicit operator Optional(TranslationBuilder builder) =>
public static Regex CreateRegex(string placeholder) =>
new(ToParameterString(placeholder), RegexOptions.IgnoreCase | RegexOptions.Compiled);
- ///
- /// Adds multiple players to the list.
- ///
- /// The players to add.
- /// The instance.
- public TranslationBuilder AddPlayers(Dictionary players)
- {
- foreach (KeyValuePair pair in players)
- {
- Players.Add(pair.Key, pair.Value);
- }
-
- return this;
- }
-
- ///
- /// Adds a player to the list.
- ///
- /// The prefix for the player.
- /// The to add.
- /// The instance.
- public TranslationBuilder AddPlayer(string prefix, Player player)
- {
- Players.Add(prefix, player);
-
- return this;
- }
-
///
/// Adds a custom replacer to the dictionary.
///
@@ -297,9 +183,6 @@ public string Build(string? translation = null)
if (string.IsNullOrEmpty(translation))
throw new ArgumentNullException($"{nameof(TranslationBuilder)} failed to build because of no valid translation.");
- if (PlayerListItem != null && CustomReplacers.All(replacer => replacer.Key.ToString() != "{players}"))
- SetupPlayerList();
-
string returnTranslation = translation!;
foreach (KeyValuePair> replacer in CustomReplacers)
@@ -324,38 +207,16 @@ public string Build(string? translation = null)
string Replacement() => GetReplacer(() => replacer.Value(unix));
}
- foreach (KeyValuePair player in Players)
- {
- if (player.Value is not { IsReady: true })
- continue;
-
- Regex baseRegex = CachedRegex.GetOrAdd(player.Key, () => CreateRegex(player.Key));
-
- returnTranslation = baseRegex.Replace(returnTranslation, player.Value.Nickname);
-
- foreach (KeyValuePair> replacer in PlayerReplacers)
- {
- Regex regex = CachedRegex.GetOrAdd(
- $"{player.Key}{replacer.Key}",
- () => CreateRegex($"{player.Key}{replacer.Key}"));
-
- returnTranslation = regex.CheckReplace(returnTranslation, Replacement);
- continue;
-
- string Replacement() => GetReplacer(() => replacer.Value(player.Value));
- }
- }
-
return returnTranslation;
}
- private static string ToParameterString(string str) => "{" + str + "}";
+ private static string ToParameterString(string str) => "{" + str.Replace(" ", string.Empty) + "}";
#pragma warning disable SA1118
private static string GetRemainingDecontaminationTime() => Mathf.Min(
0,
(float)(DecontaminationController.Singleton
- .DecontaminationPhases[DecontaminationController.Singleton.DecontaminationPhases.Length - 1]
+ .DecontaminationPhases[^1]
.TimeTrigger - DecontaminationController.GetServerTime))
.ToString(CultureInfo.InvariantCulture);
#pragma warning restore SA1118
@@ -363,7 +224,7 @@ private static string GetRemainingDecontaminationTime() => Mathf.Min(
private static TimeSpan TimeSince(long time) =>
Round.Duration - (DateTimeOffset.Now - DateTimeOffset.FromUnixTimeSeconds(time));
- private static string GetReplacer(Func func)
+ protected static string GetReplacer(Func func)
{
try
{
@@ -380,28 +241,4 @@ private static string GetReplacer(Func func)
return "Unknown";
}
}
-
- private void SetupPlayerList()
- {
- if (string.IsNullOrEmpty(PlayerListItem))
- throw new ArgumentException($"Invalid {nameof(PlayerListItem)} provided, it was either null or empty.");
-
- Player[] readyPlayers = (PlayerList ?? Player.ReadyList).ToArray();
-
- int length = readyPlayers.Length;
-
- List playerItems = new(length);
- Dictionary playerDictionary = new(length);
-
- for (int i = 0; i < length; i++)
- {
- string playerKey = $"player{i}";
- playerItems.Add(PlayerListItem!.Replace("{player", "{" + $"{playerKey}"));
- playerDictionary[playerKey] = readyPlayers[i];
- }
-
- AddCustomReplacer("players", string.Join(PlayerListSeparator, playerItems));
-
- AddPlayers(playerDictionary);
- }
}
\ No newline at end of file
diff --git a/DiscordLab.Bot/API/Updates/GitHubRelease.cs b/DiscordLab.Core/API/Updater/GitHubRelease.cs
similarity index 79%
rename from DiscordLab.Bot/API/Updates/GitHubRelease.cs
rename to DiscordLab.Core/API/Updater/GitHubRelease.cs
index b9fa1b4..0899b2f 100644
--- a/DiscordLab.Bot/API/Updates/GitHubRelease.cs
+++ b/DiscordLab.Core/API/Updater/GitHubRelease.cs
@@ -1,8 +1,8 @@
#nullable disable
-namespace DiscordLab.Bot.API.Updates;
+using System.Text.Json.Serialization;
-using Newtonsoft.Json;
+namespace DiscordLab.Core.API.Updater;
///
/// Contains data for a GitHub release object.
@@ -12,7 +12,7 @@ public class GitHubRelease
///
/// Gets or sets the tag name for this release.
///
- [JsonProperty("tag_name")]
+ [JsonPropertyName("tag_name")]
public string TagName { get; set; }
// ReSharper disable CollectionNeverUpdated.Global
@@ -20,7 +20,7 @@ public class GitHubRelease
///
/// Gets or sets the assets for this release.
///
- [JsonProperty("assets")]
+ [JsonPropertyName("assets")]
public List Assets { get; set; }
// ReSharper restore CollectionNeverUpdated.Global
@@ -28,12 +28,12 @@ public class GitHubRelease
///
/// Gets or sets a value indicating whether this is a prerelease release.
///
- [JsonProperty("prerelease")]
+ [JsonPropertyName("prerelease")]
public bool Prerelease { get; set; }
///
/// Gets or sets a value indicating whether this is a draft release.
///
- [JsonProperty("draft")]
+ [JsonPropertyName("draft")]
public bool Draft { get; set; }
}
\ No newline at end of file
diff --git a/DiscordLab.Bot/API/Updates/GitHubReleaseAsset.cs b/DiscordLab.Core/API/Updater/GitHubReleaseAsset.cs
similarity index 71%
rename from DiscordLab.Bot/API/Updates/GitHubReleaseAsset.cs
rename to DiscordLab.Core/API/Updater/GitHubReleaseAsset.cs
index da8b935..f432a96 100644
--- a/DiscordLab.Bot/API/Updates/GitHubReleaseAsset.cs
+++ b/DiscordLab.Core/API/Updater/GitHubReleaseAsset.cs
@@ -1,8 +1,8 @@
#nullable disable
-namespace DiscordLab.Bot.API.Updates;
+using System.Text.Json.Serialization;
-using Newtonsoft.Json;
+namespace DiscordLab.Core.API.Updater;
///
/// Gets details for a GitHub release asset.
@@ -12,12 +12,12 @@ public class GitHubReleaseAsset
///
/// Gets or sets the name of the asset.
///
- [JsonProperty("name")]
+ [JsonPropertyName("name")]
public string Name { get; set; }
///
/// Gets or sets the download name of the asset.
///
- [JsonProperty("browser_download_url")]
+ [JsonPropertyName("browser_download_url")]
public string DownloadUrl { get; set; }
}
\ No newline at end of file
diff --git a/DiscordLab.Bot/API/Updates/Module.cs b/DiscordLab.Core/API/Updater/Module.cs
similarity index 97%
rename from DiscordLab.Bot/API/Updates/Module.cs
rename to DiscordLab.Core/API/Updater/Module.cs
index 8dd862b..f11ae6b 100644
--- a/DiscordLab.Bot/API/Updates/Module.cs
+++ b/DiscordLab.Core/API/Updater/Module.cs
@@ -1,9 +1,9 @@
-namespace DiscordLab.Bot.API.Updates;
-
using LabApi.Features.Console;
using LabApi.Loader;
using LabApi.Loader.Features.Paths;
+namespace DiscordLab.Core.API.Updater;
+
///
/// Contains information about a DiscordLab module.
///
@@ -69,7 +69,7 @@ public static string GenerateUpdateString(IEnumerable modules) => string
/// Downloads this module.
///
/// A .
- internal async Task Download()
+ public async Task Download()
{
string filePath = Path.Combine(PathManager.Plugins.FullName, "global", Asset.Name);
diff --git a/DiscordLab.Bot/API/Updates/Updater.cs b/DiscordLab.Core/API/Updater/Updater.cs
similarity index 92%
rename from DiscordLab.Bot/API/Updates/Updater.cs
rename to DiscordLab.Core/API/Updater/Updater.cs
index d858f3a..86c436f 100644
--- a/DiscordLab.Bot/API/Updates/Updater.cs
+++ b/DiscordLab.Core/API/Updater/Updater.cs
@@ -1,10 +1,9 @@
-namespace DiscordLab.Bot.API.Updates;
-
using System.Net.Http;
-using DiscordLab.Bot.API.Attributes;
-using DiscordLab.Bot.API.Extensions;
-using LabApi.Features.Console;
-using Newtonsoft.Json;
+using DiscordLab.Core.API.Attributes;
+using DiscordLab.Core.API.Extensions;
+using Logger = LabApi.Features.Console.Logger;
+
+namespace DiscordLab.Core.API.Updater;
///
/// Handle updates within DiscordLab.
@@ -38,9 +37,7 @@ public static async Task> CheckForUpdates()
return [];
}
- string str = await response.Content.ReadAsStringAsync();
-
- GitHubRelease[] releases = JsonConvert.DeserializeObject(str) ?? [];
+ GitHubRelease[] releases = await response.Content.ReadFromJson() ?? [];
List modules = [];
diff --git a/DiscordLab.Bot/API/Utilities/LoggingUtils.cs b/DiscordLab.Core/API/Utilities/LoggingUtils.cs
similarity index 68%
rename from DiscordLab.Bot/API/Utilities/LoggingUtils.cs
rename to DiscordLab.Core/API/Utilities/LoggingUtils.cs
index 518da39..1a0d9ee 100644
--- a/DiscordLab.Bot/API/Utilities/LoggingUtils.cs
+++ b/DiscordLab.Core/API/Utilities/LoggingUtils.cs
@@ -1,31 +1,15 @@
-namespace DiscordLab.Bot.API.Utilities;
-
using System.Reflection;
using System.Text;
using LabApi.Features.Console;
using NorthwoodLib.Pools;
+namespace DiscordLab.Core.API.Utilities;
+
///
/// Contains utilities for logging related tasks.
///
public static class LoggingUtils
{
- ///
- /// Generates a message that will tell the user that the channel was not found.
- ///
- /// The submodule that this error comes from.
- /// The channel ID that was missing.
- /// The related guild ID, goes to the default guild ID if 0.
- /// The error string.
- public static string GenerateMissingChannelMessage(string type, ulong channelId, ulong guildId)
- {
- if (guildId == 0)
- guildId = Plugin.Instance.Config.GuildId;
-
- return
- $"Could not find channel {channelId} under the guild {guildId}, please make sure the bot has access and you put in the right IDs. This was triggered from {type}.";
- }
-
///
/// Logs an exception that is thrown from a method.
///
diff --git a/DiscordLab.Core/CommandHandler.cs b/DiscordLab.Core/CommandHandler.cs
new file mode 100644
index 0000000..ba0abed
--- /dev/null
+++ b/DiscordLab.Core/CommandHandler.cs
@@ -0,0 +1,80 @@
+using System.Collections.Specialized;
+using DiscordLab.Core.API.Commands;
+using DiscordLab.Core.API.Extensions;
+
+namespace DiscordLab.Core;
+
+public abstract class CommandHandler
+{
+ protected CommandHandler()
+ {
+ ICommand.Commands.CollectionChanged += CollectionChanged;
+ }
+
+ ~CommandHandler()
+ {
+ ICommand.Commands.CollectionChanged -= CollectionChanged;
+ }
+
+ private void CollectionChanged(object _, NotifyCollectionChangedEventArgs ev)
+ {
+ if (ev.Action is not (NotifyCollectionChangedAction.Add or NotifyCollectionChangedAction.Replace))
+ return;
+
+ Task.RunAndLog(() => AddCommands((IEnumerable)ev.NewItems));
+ }
+
+ public async Task ExecuteCommand(CommandInformation information)
+ {
+ ICommand? command = ICommand.Commands.FirstOrDefault(cmd => cmd.Data.Name == information.Name);
+ if (command == null)
+ return;
+
+ await command.Execute(information);
+ }
+
+ private CommandOptionBuilder? FindBuilder(CommandOptionBuilder builder, string name)
+ {
+ if (builder.Name == name)
+ return builder;
+
+ if (builder.Options == null)
+ return null;
+
+ CommandOptionBuilder? retOpt = null;
+
+ foreach (CommandOptionBuilder commandOptionBuilder in builder.Options)
+ {
+ retOpt = FindBuilder(commandOptionBuilder, name);
+ if (retOpt != null)
+ break;
+ }
+
+ return retOpt;
+ }
+
+ public IEnumerable<(string name, string value)> ExecuteAutocomplete(string commandName, CommandOptionInformation information)
+ {
+ ICommand? command = ICommand.Commands.FirstOrDefault(cmd => cmd.Data.Name == information.Name);
+ if (command == null)
+ return [];
+
+ CommandOptionBuilder? builder = null;
+
+ foreach (CommandOptionBuilder commandOptionBuilder in command.Data.Options ?? [])
+ {
+ builder = FindBuilder(commandOptionBuilder, information.Name);
+ if (builder != null)
+ break;
+ }
+
+ if (builder == null)
+ return [];
+
+ IEnumerable<(string name, string value)> output = builder.Autocomplete!(information);
+
+ return output;
+ }
+
+ protected abstract Task AddCommands(IEnumerable commands);
+}
\ No newline at end of file
diff --git a/DiscordLab.Core/Config.cs b/DiscordLab.Core/Config.cs
new file mode 100644
index 0000000..7368ec9
--- /dev/null
+++ b/DiscordLab.Core/Config.cs
@@ -0,0 +1,8 @@
+namespace DiscordLab.Core;
+
+public class Config
+{
+ public bool AutoUpdate { get; set; } = true;
+
+ public bool Debug { get; set; } = false;
+}
\ No newline at end of file
diff --git a/DiscordLab.Core/DiscordLab.Core.csproj b/DiscordLab.Core/DiscordLab.Core.csproj
new file mode 100644
index 0000000..82da54f
--- /dev/null
+++ b/DiscordLab.Core/DiscordLab.Core.csproj
@@ -0,0 +1,35 @@
+
+
+ net48
+ enable
+ enable
+ 14
+ x64
+ true
+ 3.0.0
+
+
+
+ true
+ true
+ LumiFae
+ DiscordLab
+ A modular Discord bot for SCP:SL servers running LabAPI
+ git
+ https://github.com/DiscordLabSCP/DiscordLab
+ README.md
+ AGPL-3.0-only
+
+
+
+
+ True
+ \
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/DiscordLab.Core/MessageContent.cs b/DiscordLab.Core/MessageContent.cs
new file mode 100644
index 0000000..587dc1f
--- /dev/null
+++ b/DiscordLab.Core/MessageContent.cs
@@ -0,0 +1,91 @@
+// ReSharper disable MemberCanBePrivate.Global
+
+using System.ComponentModel;
+using DiscordLab.Core.API.Embed;
+using DiscordLab.Core.API.Extensions;
+using DiscordLab.Core.API.TranslationBuilders;
+using NorthwoodLib.Pools;
+using UnityEngine;
+using YamlDotNet.Serialization;
+
+namespace DiscordLab.Core;
+
+///
+/// Message config object for either string messages or embeds.
+///
+public class MessageContent
+{
+ private const TranslatableMessageField DefaultTranslatableFields =
+ TranslatableMessageField.Message |
+ TranslatableMessageField.EmbedDescription |
+ TranslatableMessageField.EmbedFieldValues |
+ TranslatableMessageField.EmbedFooterText;
+
+ ///
+ /// Gets or sets the embed to send, if any.
+ ///
+ [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitNull)]
+ public EmbedBuilder? Embed { get; set; }
+
+ ///
+ /// Gets or sets the string to send, if any.
+ ///
+ [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitNull)]
+ public string? Message { get; set; }
+
+ ///
+ /// Gets or sets which parts of the message should have their placeholders replaced.
+ ///
+ [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)]
+ [DefaultValue(DefaultTranslatableFields)]
+ public TranslatableMessageField TranslatedFields { get; set; } = DefaultTranslatableFields;
+
+ ///
+ /// Converts an embed into a instance.
+ ///
+ /// The embed.
+ /// The instance.
+ public static implicit operator MessageContent(EmbedBuilder embed) => new() { Embed = embed };
+
+ ///
+ /// Converts a string into a instance.
+ ///
+ /// The content.
+ /// The instance.
+ public static implicit operator MessageContent(string content) => new() { Message = content };
+
+ ///
+ /// Checks whether the specified field has been marked as translatable in .
+ ///
+ /// The field to check.
+ /// Whether the field is present in the .
+ public bool FieldMarkedTranslatable(TranslatableMessageField field) =>
+ (TranslatedFields & field) != 0;
+
+ ///
+ /// Builds the embed and/or content assigned to this using a .
+ ///
+ /// The to use.
+ /// The embed and content with replaced values.
+ /// Throws when message content is too long after being built.
+ public MessageInformation Build(TranslationBuilder builder)
+ {
+ string? message = builder.Build(Message.OrIfEmpty());
+ EmbedBuilder? embed = Embed?.CloneWithTranslation(builder);
+
+ if (string.IsNullOrEmpty(message))
+ message = null;
+ if (string.IsNullOrEmpty(embed?.Title) && string.IsNullOrEmpty(embed?.Description) && !(embed?.Fields?.Any() ?? false))
+ embed = null;
+
+ return new(message, embed);
+ }
+
+ ///
+ /// Does the building on the main thread, use this over Build if you use Task.Run.
+ public async Awaitable MainThreadBuild(TranslationBuilder builder)
+ {
+ await Awaitable.MainThreadAsync();
+ return Build(builder);
+ }
+}
\ No newline at end of file
diff --git a/DiscordLab.Core/MessageHandler.cs b/DiscordLab.Core/MessageHandler.cs
new file mode 100644
index 0000000..c5a6af5
--- /dev/null
+++ b/DiscordLab.Core/MessageHandler.cs
@@ -0,0 +1,83 @@
+using System.Diagnostics.CodeAnalysis;
+using DiscordLab.Core.API.Enums;
+using DiscordLab.Core.API.Extensions;
+
+namespace DiscordLab.Core;
+
+public abstract class MessageHandler
+{
+ public static HashSet Handlers = new();
+
+ public static async Task SendMessages(string destination, MessageInformation information,
+ bool saveIds = false) => await Task.WhenAll(Handlers.Select(async handler =>
+ {
+ string? messageId = await handler.SendMessage(destination, information);
+ if (saveIds && messageId != null)
+ handler.SaveMessageId(destination, messageId);
+ }));
+
+ public static async Task SendMessagesCheckExists(string destination, MessageInformation information) =>
+ await Task.WhenAll(Handlers.Select(async handler =>
+ {
+ if (!handler.TryGetMessageId(destination, out string? id))
+ return handler.SendMessage(destination, information)
+ .Then(result => handler.SaveMessageId(destination, result!));
+
+ try
+ {
+ await handler.ThrowIfMissingMessage(destination, id);
+ }
+ catch (Exception)
+ {
+ return handler.SendMessage(destination, information)
+ .Then(result => handler.SaveMessageId(destination, result!));
+ }
+
+ return Task.CompletedTask;
+ }));
+
+ public static async Task SendToWebhooks(string destination, MessageInformation information) =>
+ await Task.WhenAll(Handlers.Select(handler => handler.SendToWebhook(destination, information)));
+
+ public static async Task EditMessages(string destination, MessageInformation information) => await Task.WhenAll(
+ Handlers.Select(handler => handler.TryGetMessageId(destination, out string? id) ? handler.EditMessage(destination, id, information) : Task.CompletedTask));
+
+ public static async Task DeleteMessages(string destination) => await Task.WhenAll(
+ Handlers.Select(handler => handler.TryGetMessageId(destination, out string? id) ? handler.DeleteMessage(destination, id) : Task.CompletedTask));
+
+ public static async Task SetStatuses(StatusType status) =>
+ await Task.WhenAll(Handlers.Select(handler => handler.SetStatus(status)));
+
+ public static async Task EditActivities(string activity, ActivityType type = ActivityType.Custom) =>
+ await Task.WhenAll(Handlers.Select(handler => handler.EditActivity(activity, type)));
+
+ public abstract Task SendMessage(string destination, MessageInformation information);
+
+ public abstract Task SendToWebhook(string destination, MessageInformation information);
+
+ public abstract Task EditMessage(string destination, string identifier, MessageInformation information);
+
+ public abstract Task DeleteMessage(string destination, string identifier);
+
+ public abstract Task ThrowIfMissingMessage(string destination, string identifier);
+
+ public bool TryGetMessageId(string destination, [NotNullWhen(true)] out string? messageId)
+ {
+ messageId = GetMessageId(destination);
+ return messageId != null;
+ }
+
+ public abstract string? GetMessageId(string destination);
+
+ public abstract void SaveMessageId(string destination, string identifier);
+
+ public virtual Task SetStatus(StatusType status)
+ {
+ return Task.CompletedTask;
+ }
+
+ public virtual Task EditActivity(string activity, ActivityType type)
+ {
+ return Task.CompletedTask;
+ }
+}
\ No newline at end of file
diff --git a/DiscordLab.Core/MessageInformation.cs b/DiscordLab.Core/MessageInformation.cs
new file mode 100644
index 0000000..9661cd2
--- /dev/null
+++ b/DiscordLab.Core/MessageInformation.cs
@@ -0,0 +1,18 @@
+using DiscordLab.Core.API.Embed;
+using DiscordLab.Core.API.Features;
+using DiscordLab.Core.API.TranslationBuilders;
+
+namespace DiscordLab.Core;
+
+public record struct MessageInformation(string? Content, IEnumerable? Embeds, Attachment? Attachment = null)
+{
+ public MessageInformation(string? content, EmbedBuilder? embed = null) : this(content, embed == null ? null : [embed]) {}
+
+ public static implicit operator MessageInformation((string? content, EmbedBuilder? embed) tuple) => new(tuple.content, tuple.embed);
+
+ public static implicit operator MessageInformation(string content) => new(content);
+
+ public static implicit operator MessageInformation(EmbedBuilder embed) => new(null, embed);
+
+ public static implicit operator MessageInformation(TranslationBuilder builder) => new(builder.Build());
+}
\ No newline at end of file
diff --git a/DiscordLab.Core/Plugin.cs b/DiscordLab.Core/Plugin.cs
new file mode 100644
index 0000000..b21f673
--- /dev/null
+++ b/DiscordLab.Core/Plugin.cs
@@ -0,0 +1,30 @@
+using DiscordLab.Core.API.Attributes;
+using LabApi.Features;
+using LabApi.Loader.Features.Plugins;
+
+namespace DiscordLab.Core;
+
+public class Plugin : Plugin
+{
+ public static Plugin Instance { get; set; } = null!;
+
+ public override string Name { get; } = "DiscordLab";
+ public override string Description { get; } = "The core plugin for DiscordLab";
+ public override string Author { get; } = "LumiFae";
+ public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion);
+ public override Version Version => field ??= GetType().Assembly.GetName().Version;
+
+ public override void Enable()
+ {
+ Instance = this;
+
+ CallOnLoadAttribute.Load();
+ }
+
+ public override void Disable()
+ {
+ Instance = null!;
+
+ CallOnUnloadAttribute.Unload();
+ }
+}
\ No newline at end of file
diff --git a/DiscordLab.Core/TranslatableMessageField.cs b/DiscordLab.Core/TranslatableMessageField.cs
new file mode 100644
index 0000000..9e6abe6
--- /dev/null
+++ b/DiscordLab.Core/TranslatableMessageField.cs
@@ -0,0 +1,81 @@
+using DiscordLab.Core.API.Embed;
+using DiscordLab.Core.API.TranslationBuilders;
+
+namespace DiscordLab.Core;
+
+///
+/// Represents a specific field that the can be executed on.
+///
+[Flags]
+public enum TranslatableMessageField
+{
+ ///
+ /// Does not mark any fields for translation.
+ ///
+ None = 0,
+
+ ///
+ /// Marks the for parsing placeholders.
+ ///
+ Message = 1 << 0,
+
+ ///
+ /// Marks the embed's for parsing placeholders.
+ ///
+ EmbedTitle = 1 << 1,
+
+ ///
+ /// Marks the embed's for parsing placeholders.
+ ///
+ EmbedDescription = 1 << 2,
+
+ ///
+ /// Marks the embed's for parsing placeholders.
+ ///
+ EmbedThumbnailUrl = 1 << 3,
+
+ ///
+ /// Marks the embed's for parsing placeholders.
+ ///
+ EmbedImageUrl = 1 << 4,
+
+ ///
+ /// Marks the embed's for parsing placeholders.
+ ///
+ EmbedUrl = 1 << 5,
+
+ ///
+ /// Marks all the embed's for parsing placeholders.
+ ///
+ EmbedFieldNames = 1 << 6,
+
+ ///
+ /// Marks all the embed's for parsing placeholders.
+ ///
+ EmbedFieldValues = 1 << 7,
+
+ ///
+ /// Marks the embed's for parsing placeholders.
+ ///
+ EmbedAuthorName = 1 << 8,
+
+ ///
+ /// Marks the embed's for parsing placeholders.
+ ///
+ EmbedAuthorUrl = 1 << 9,
+
+ ///
+ /// Marks the embed's for parsing placeholders.
+ ///
+ EmbedAuthorIconUrl = 1 << 10,
+
+ ///
+ /// Marks the embed's for parsing placeholders.
+ ///
+ EmbedFooterText = 1 << 11,
+
+ ///
+ /// Marks the embed's for parsing placeholders.
+ ///
+ EmbedFooterIconUrl = 1 << 12,
+}
\ No newline at end of file
diff --git a/DiscordLab.DeathLogs/Config.cs b/DiscordLab.DeathLogs/Config.cs
index 9a57f5a..bcefcee 100644
--- a/DiscordLab.DeathLogs/Config.cs
+++ b/DiscordLab.DeathLogs/Config.cs
@@ -5,34 +5,32 @@ namespace DiscordLab.DeathLogs;
public class Config
{
[Description("The channel where the normal death logs will be sent.")]
- public ulong ChannelId { get; set; } = 0;
+ public string Channel { get; set; } = "default";
[Description(
"The channel where the death logs of cuffed players will be sent. Keep as default value to disable. Disabling this will make it so logs are only sent to the normal death logs channel, but without the cuffed identifier.")]
- public ulong CuffedChannelId { get; set; } = 0;
+ public string CuffedChannel { get; set; } = "default";
[Description(
"The channel where logs will be sent when a player dies by their own actions, or just they died because of something else.")]
- public ulong SelfChannelId { get; set; } = 0;
+ public string SelfChannel { get; set; } = "default";
[Description("The channel where logs will be sent when a player dies by a teamkill.")]
- public ulong TeamKillChannelId { get; set; } = 0;
+ public string TeamKillChannel { get; set; } = "default";
[Description(
"If this is true, then the plugin will ignore the cuff state of the player and send the death logs to the normal death logs channel.")]
public bool ScpIgnoreCuffed { get; set; } = true;
[Description("The channel to send damage logs to, if any.")]
- public ulong DamageLogChannelId { get; set; } = 0;
+ public string DamageLogChannel { get; set; } = "default";
[Description("The channel to send team damage logs to, if any.")]
- public ulong TeamDamageLogChannelId { get; set; } = 0;
+ public string TeamDamageLogChannel { get; set; } = "default";
[Description("Whether damage logs shouldn't be tracked if the attacker is an SCP.")]
public bool IgnoreScpDamage { get; set; } = false;
[Description("If your server turns on friendly fire at round end, or people are allowed to RDM at round end, enable this to avoid rate limits and spam.")]
public bool IgnoreRoundEndDamage { get; set; } = false;
-
- public ulong GuildId { get; set; } = 0;
}
\ No newline at end of file
diff --git a/DiscordLab.DeathLogs/DamageLogs.cs b/DiscordLab.DeathLogs/DamageLogs.cs
index 5b340a9..699c84f 100644
--- a/DiscordLab.DeathLogs/DamageLogs.cs
+++ b/DiscordLab.DeathLogs/DamageLogs.cs
@@ -1,70 +1,45 @@
using System.Globalization;
-using System.Net.Http;
using CustomPlayerEffects;
-using Discord;
-using Discord.Rest;
-using Discord.WebSocket;
-using DiscordLab.Bot;
-using DiscordLab.Bot.API.Attributes;
-using DiscordLab.Bot.API.Extensions;
-using DiscordLab.Bot.API.Features;
-using DiscordLab.Bot.API.Utilities;
+using DiscordLab.Core;
+using DiscordLab.Core.API.Attributes;
+using DiscordLab.Core.API.Embed;
+using DiscordLab.Core.API.Extensions;
+using DiscordLab.Core.API.TranslationBuilders;
using LabApi.Events.Arguments.PlayerEvents;
using LabApi.Events.Handlers;
using LabApi.Features.Wrappers;
using NorthwoodLib.Pools;
using PlayerStatsSystem;
using UnityEngine;
-using Logger = LabApi.Features.Console.Logger;
+using Queue = DiscordLab.Core.API.Features.Queue;
namespace DiscordLab.DeathLogs;
public static class DamageLogs
{
+ private static Config Config => Plugin.Instance.Config;
+
+ private static Translation Translation => Plugin.Instance.Translation;
+
public static List DamageLogEntries { get; set; } = new();
public static List TeamDamageLogEntries { get; set; } = new();
- public static RestWebhook Webhook;
-
- public static RestWebhook TeamWebhook;
-
- private static Queue queue = new(5, SendLog);
-
- private static bool hasDependency;
+ private static Queue Queue { get; } = new(5, SendLog);
[CallOnLoad]
public static void Register()
{
- if (Plugin.Instance.Config.DamageLogChannelId == 0 && Plugin.Instance.Config.TeamDamageLogChannelId == 0) return;
-
- try
- {
-#pragma warning disable CS0219 // Variable is assigned but its value is never used
- string _ = nameof(Discord.Webhook.DiscordWebhookClient);
-#pragma warning restore CS0219 // Variable is assigned but its value is never used
- hasDependency = true;
- }
- catch (Exception)
- {
- Logger.Error("You do not have your dependencies updated, so damage logs will not work, please update the dependencies manually from the DiscordLab GitHub repository.");
- }
-
PlayerEvents.Hurt += OnHurt;
}
[CallOnUnload]
public static void Unregister()
{
- if (Plugin.Instance.Config.DamageLogChannelId == 0 && Plugin.Instance.Config.TeamDamageLogChannelId == 0) return;
- if (!hasDependency) return;
-
PlayerEvents.Hurt -= OnHurt;
DamageLogEntries = null;
TeamDamageLogEntries = null;
- Webhook = null;
- TeamWebhook = null;
}
public static void OnHurt(PlayerHurtEventArgs ev)
@@ -97,7 +72,7 @@ public static void OnHurt(PlayerHurtEventArgs ev)
if (ev.Player.IsSCP && ev.Attacker.IsSCP && Plugin.Instance.Config.IgnoreScpDamage)
return;
- string log = new TranslationBuilder(Plugin.Instance.Translation.DamageLogEntry)
+ string log = new PlayerTranslationBuilder(Plugin.Instance.Translation.DamageLogEntry)
.AddPlayer("target", ev.Player)
.AddPlayer("player", ev.Attacker)
.AddCustomReplacer("damage", handler.Damage.ToString(CultureInfo.InvariantCulture))
@@ -108,78 +83,38 @@ public static void OnHurt(PlayerHurtEventArgs ev)
else
DamageLogEntries.Add(log);
- queue.Process();
+ Queue.Process();
}
public static void SendLog() => Task.RunAndLog(async () =>
{
- ulong guildId = Plugin.Instance.Config.GuildId;
- ulong channelId = Plugin.Instance.Config.DamageLogChannelId;
-
List damageLogEntries = DamageLogEntries.ToList();
List teamDamageLogEntries = TeamDamageLogEntries.ToList();
TeamDamageLogEntries.Clear();
DamageLogEntries.Clear();
-
- if (Webhook == null && Client.TryGetOrAddChannel(channelId, out SocketTextChannel channel))
- Webhook = await GetOrCreateWebhook(channel);
- if (Webhook != null)
- {
- Discord.Webhook.DiscordWebhookClient client = new(Webhook);
-
- foreach (Embed embed in CreateEmbeds(damageLogEntries, Plugin.Instance.Translation.DamageLogEmbed))
- {
- await client.SendMessageAsync(embeds: [embed]);
- }
-
- client.Dispose();
- }
- else if (channelId != 0 && Webhook == null)
- Logger.Error(
- LoggingUtils.GenerateMissingChannelMessage(
- "damage logs",
- channelId,
- guildId));
-
- ulong teamChannelId = Plugin.Instance.Config.TeamDamageLogChannelId;
- if (TeamWebhook == null && Client.TryGetOrAddChannel(teamChannelId, out SocketTextChannel teamChannel))
- TeamWebhook = await GetOrCreateWebhook(teamChannel);
-
- if (TeamWebhook != null)
- {
- Discord.Webhook.DiscordWebhookClient client = new(TeamWebhook);
-
- foreach (Embed embed in CreateEmbeds(teamDamageLogEntries, Plugin.Instance.Translation.TeamDamageLogEmbed))
- {
- await client.SendMessageAsync(embeds: [embed]);
- }
+ await MessageHandler.SendToWebhooks(Config.DamageLogChannel,
+ new(null, CreateEmbeds(damageLogEntries, Translation.DamageLogEmbed)));
- client.Dispose();
- }
- else if (teamChannelId != 0 && TeamWebhook == null)
- Logger.Error(
- LoggingUtils.GenerateMissingChannelMessage(
- "team damage logs",
- teamChannelId,
- guildId));
+ await MessageHandler.SendToWebhooks(Config.TeamDamageLogChannel,
+ new(null, CreateEmbeds(teamDamageLogEntries, Translation.TeamDamageLogEmbed)));
});
- private static IEnumerable