From f8975b2e41d956af96470106a94cd596f2f0c363 Mon Sep 17 00:00:00 2001 From: Mira Date: Sat, 10 Jun 2023 07:19:26 +0200 Subject: [PATCH] feat: hybrid commands --- DisCatSharp.HybridCommands/.editorconfig | 14 + .../Attributes/HybridCommandAttribute.cs | 110 ++++ .../Attributes/OptionAttribute.cs | 58 ++ .../RestrictedExecutionTypesAttribute.cs | 48 ++ .../Context/HybridCommandContext.cs | 349 +++++++++++ .../DisCatSharp.HybridCommands.csproj | 44 ++ .../Entities/CacheConfig.cs | 31 + .../Enums/HybridExecutionType.cs | 30 + .../Exceptions/AssemblyLoadException.cs | 34 + .../ExtensionMethods.cs | 71 +++ .../GlobalSuppressions.cs | 8 + .../HybridCommandsConfiguration.cs | 125 ++++ .../HybridCommandsExtension.cs | 273 ++++++++ .../HybridCommandsModule.cs | 45 ++ .../HybridCommandsUtilities.cs | 591 ++++++++++++++++++ .../Utilities/CommandLoadContext.cs | 52 ++ .../InternalsVisibleTo.targets | 1 + DisCatSharp.sln | 6 + 18 files changed, 1890 insertions(+) create mode 100644 DisCatSharp.HybridCommands/.editorconfig create mode 100644 DisCatSharp.HybridCommands/Attributes/HybridCommandAttribute.cs create mode 100644 DisCatSharp.HybridCommands/Attributes/OptionAttribute.cs create mode 100644 DisCatSharp.HybridCommands/Attributes/RestrictedExecutionTypesAttribute.cs create mode 100644 DisCatSharp.HybridCommands/Context/HybridCommandContext.cs create mode 100644 DisCatSharp.HybridCommands/DisCatSharp.HybridCommands.csproj create mode 100644 DisCatSharp.HybridCommands/Entities/CacheConfig.cs create mode 100644 DisCatSharp.HybridCommands/Enums/HybridExecutionType.cs create mode 100644 DisCatSharp.HybridCommands/Exceptions/AssemblyLoadException.cs create mode 100644 DisCatSharp.HybridCommands/ExtensionMethods.cs create mode 100644 DisCatSharp.HybridCommands/GlobalSuppressions.cs create mode 100644 DisCatSharp.HybridCommands/HybridCommandsConfiguration.cs create mode 100644 DisCatSharp.HybridCommands/HybridCommandsExtension.cs create mode 100644 DisCatSharp.HybridCommands/HybridCommandsModule.cs create mode 100644 DisCatSharp.HybridCommands/HybridCommandsUtilities.cs create mode 100644 DisCatSharp.HybridCommands/Utilities/CommandLoadContext.cs diff --git a/DisCatSharp.HybridCommands/.editorconfig b/DisCatSharp.HybridCommands/.editorconfig new file mode 100644 index 0000000000..1d67f940d1 --- /dev/null +++ b/DisCatSharp.HybridCommands/.editorconfig @@ -0,0 +1,14 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = false + +#### Core EditorConfig Options #### + +# All files +[*] + +# General +charset = utf-8 +trim_trailing_whitespace = true + +[*.cs] +file_header_template = This file is part of the DisCatSharp project.\n\nCopyright (c) 2023 AITSYS\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the "Software"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE. diff --git a/DisCatSharp.HybridCommands/Attributes/HybridCommandAttribute.cs b/DisCatSharp.HybridCommands/Attributes/HybridCommandAttribute.cs new file mode 100644 index 0000000000..50993407a9 --- /dev/null +++ b/DisCatSharp.HybridCommands/Attributes/HybridCommandAttribute.cs @@ -0,0 +1,110 @@ +// This file is part of the DisCatSharp project, based off DSharpPlus. +// +// Copyright (c) 2023 AITSYS +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; +using DisCatSharp.Enums; + +namespace DisCatSharp.HybridCommands.Attributes; + +/// +/// Marks this method as a hybrid command +/// +[AttributeUsage(AttributeTargets.Method)] +public class HybridCommandAttribute : Attribute +{ + /// + /// Gets the name of this command + /// + public string Name { get; set; } + + /// + /// Gets the description of this command + /// + public string Description { get; set; } + + /// + /// Gets the needed permission of this command + /// + public Permissions? DefaultMemberPermissions { get; set; } + + /// + /// Gets the dm permission of this command + /// + public bool? DmPermission { get; set; } + + /// + /// Gets whether this command is marked as NSFW + /// + public bool IsNsfw { get; set; } + + /// + /// Marks this method as hybrid command. + /// + /// The name of this hybrid command. + /// The description of this hybrid command. + /// + /// The default permissions required for execution. + /// This can be overriden by a guild and is not enforced via prefix commands. + /// + /// Whether this command should be available in direct messages. + /// + /// Whether this command should be marked as nsfw. + /// Does not protect this command from being ran as prefix command from users under 18. + /// + public HybridCommandAttribute(string name, string description, Permissions? defaultMemberPermissions = null, bool dmPermission = true, bool isNsfw = false) + { + this.Name = name; + this.Description = description; + this.DefaultMemberPermissions = defaultMemberPermissions; + this.DmPermission = dmPermission; + this.IsNsfw = isNsfw; + } + + /// + /// Marks this method as hybrid command. + /// + /// The name of this hybrid command. + /// The description of this hybrid command. + /// Whether this command should be available in direct messages. + /// + /// Whether this command should be marked as nsfw. + /// Does not protect this command from being ran as prefix command from users under 18. + /// + public HybridCommandAttribute(string name, string description, bool dmPermission, bool isNsfw = false) + { + this.Name = name; + this.Description = description; + this.DmPermission = dmPermission; + this.IsNsfw = isNsfw; + } + + /// + /// Marks this method as hybrid command. + /// + /// The name of this hybrid command. + /// The description of this hybrid command. + public HybridCommandAttribute(string name, string description) + { + this.Name = name; + this.Description = description; + } +} diff --git a/DisCatSharp.HybridCommands/Attributes/OptionAttribute.cs b/DisCatSharp.HybridCommands/Attributes/OptionAttribute.cs new file mode 100644 index 0000000000..fd2086deee --- /dev/null +++ b/DisCatSharp.HybridCommands/Attributes/OptionAttribute.cs @@ -0,0 +1,58 @@ +// This file is part of the DisCatSharp project, based off DSharpPlus. +// +// Copyright (c) 2023 AITSYS +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; + +namespace DisCatSharp.HybridCommands.Attributes; + +/// +/// Marks this parameter as an option for a hybrid command +/// +[AttributeUsage(AttributeTargets.Parameter)] +public class OptionAttribute : Attribute +{ + /// + /// Gets the name of this option. + /// + public string Name; + + /// + /// Gets the description of this option. + /// + public string Description; + + /// + /// Initializes a new instance of the class. + /// + /// The name. + /// The description. + public OptionAttribute(string name, string description) + { + if (name.Length > 32) + throw new ArgumentException("Hybrid command option names cannot go over 32 characters."); + else if (description.Length > 100) + throw new ArgumentException("Hybrid command option descriptions cannot go over 100 characters."); + + this.Name = name.ToLower(); + this.Description = description; + } +} diff --git a/DisCatSharp.HybridCommands/Attributes/RestrictedExecutionTypesAttribute.cs b/DisCatSharp.HybridCommands/Attributes/RestrictedExecutionTypesAttribute.cs new file mode 100644 index 0000000000..48a1946a80 --- /dev/null +++ b/DisCatSharp.HybridCommands/Attributes/RestrictedExecutionTypesAttribute.cs @@ -0,0 +1,48 @@ +// This file is part of the DisCatSharp project, based off DSharpPlus. +// +// Copyright (c) 2023 AITSYS +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; + +using DisCatSharp.HybridCommands.Enums; + +namespace DisCatSharp.HybridCommands.Attributes; + +/// +/// Restricts a hybrid command to be only executable by certain execution types. +/// +[AttributeUsage(AttributeTargets.Method)] +public class RestrictedExecutionTypesAttribute : Attribute +{ + /// + /// Gets which execution types are allowed for this hybrid command. + /// + public HybridExecutionType[] Types { get; set; } + + /// + /// Restricts a hybrid command to be only executable by certain execution types. + /// + /// The types of execution that are allowed. + public RestrictedExecutionTypesAttribute(params HybridExecutionType[] allowedTypes) + { + this.Types = allowedTypes; + } +} diff --git a/DisCatSharp.HybridCommands/Context/HybridCommandContext.cs b/DisCatSharp.HybridCommands/Context/HybridCommandContext.cs new file mode 100644 index 0000000000..a193740798 --- /dev/null +++ b/DisCatSharp.HybridCommands/Context/HybridCommandContext.cs @@ -0,0 +1,349 @@ +// This file is part of the DisCatSharp project, based off DSharpPlus. +// +// Copyright (c) 2023 AITSYS +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +using DisCatSharp.ApplicationCommands.Context; +using DisCatSharp.ApplicationCommands.Entities; +using DisCatSharp.CommandsNext; +using DisCatSharp.Entities; +using DisCatSharp.EventArgs; +using DisCatSharp.HybridCommands.Enums; + +namespace DisCatSharp.HybridCommands.Context; +public class HybridCommandContext +{ + /// + /// Do not use, required by RunTime-compiled classes. + /// + public HybridCommandContext(CommandContext ctx) + { + this.ExcutionType = HybridExecutionType.PrefixCommand; + + this.Member = ctx.Member; + this.User = ctx.User; + this.Guild = ctx.Guild; + this.Channel = ctx.Channel; + this.Client = ctx.Client; + + this.CurrentMember = ctx.Guild?.CurrentMember; + this.CurrentUser = ctx.Client.CurrentUser; + + this.OriginalCommandContext = ctx; + + this.Prefix = ctx.Prefix; + this.CommandName = ctx.Command.Name; + +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. + if (ctx.Command.Parent != null) + this.CommandName = this.CommandName.Insert(0, $"{ctx.Command.Parent.Name} "); +#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. + } + + /// + /// Do not use, required by RunTime-compiled classes. + /// + public HybridCommandContext(InteractionContext ctx) + { + this.ExcutionType = HybridExecutionType.SlashCommand; + + this.Member = ctx.Member; + this.User = ctx.User; + this.Guild = ctx.Guild; + this.Channel = ctx.Channel; + this.Client = ctx.Client; + + this.CurrentMember = ctx.Guild?.CurrentMember; + this.CurrentUser = ctx.Client.CurrentUser; + + this.OriginalInteractionContext = ctx; + + this.Prefix = "/"; + this.CommandName = ctx.FullCommandName; + this.ParentCommandName = ctx.CommandName; + } + + /// + /// Do not use, required by RunTime-compiled classes. + /// + public HybridCommandContext(ContextMenuContext ctx) + { + this.ExcutionType = HybridExecutionType.ContextMenuCommand; + + this.Member = ctx.Member; + this.User = ctx.User; + this.Guild = ctx.Guild; + this.Channel = ctx.Channel; + this.Client = ctx.Client; + + this.CurrentMember = ctx.Guild?.CurrentMember; + this.CurrentUser = ctx.Client.CurrentUser; + + this.OriginalContextMenuContext = ctx; + + this.Prefix = ""; + this.CommandName = ctx.FullCommandName; + this.ParentCommandName = ctx.CommandName; + } + + /// + /// From what kind of source this command originated from. + /// + public HybridExecutionType ExcutionType { get; internal set; } + + /// + /// The that execution originated from. + /// + public DiscordClient Client { get; internal set; } + + /// + /// The prefix that was used to execute this command. + /// + public string Prefix { get; internal set; } + + /// + /// The name of the command used. + /// + public string CommandName { get; internal set; } + + /// + /// The name of the parent command used, if command is a subcommand. + /// Null if command is not subcommand. + /// + public string? ParentCommandName { get; internal set; } + + /// + /// The that executed this command. + /// + public DiscordUser User { get; internal set; } + + /// + /// The that executed this command. + /// Null if command was not ran on guild. + /// + public DiscordMember? Member { get; internal set; } + + /// + /// The that the uses. + /// + public DiscordUser CurrentUser { get; internal set; } + + /// + /// The that the uses. + /// Null if command was not ran on guild. + /// + public DiscordMember? CurrentMember { get; internal set; } + + /// + /// The channel this command was executed in. + /// + public DiscordChannel Channel { get; internal set; } + + /// + /// The guild this command was executed on. + /// Null if command was not ran on guild. + /// + public DiscordGuild? Guild { get; internal set; } + + + /// + /// The message that's being used to interact with the user. + /// Null if RespondOrEditAsync was not used. + /// + public DiscordMessage? ResponseMessage { get; internal set; } + + /// + /// The original . + /// Null if is not . + /// + public CommandContext? OriginalCommandContext { get; internal set; } + + /// + /// The original . + /// Null if is not . + /// + public InteractionContext? OriginalInteractionContext { get; internal set; } + + /// + /// The original . + /// Null if is not . + /// + public ContextMenuContext? OriginalContextMenuContext { get; internal set; } + + /// + /// The original interaction that started this command. + /// + public DiscordInteraction? Interaction + => this.ExcutionType switch + { + HybridExecutionType.SlashCommand => this.OriginalInteractionContext?.Interaction, + HybridExecutionType.ContextMenuCommand => this.OriginalContextMenuContext?.Interaction, + _ => null + }; + + #region Methods + + /// + /// + /// + /// The message content to send. + public async Task RespondOrEditAsync(string content) + => await this.RespondOrEditAsync(new DiscordMessageBuilder().WithContent(content)); + + /// + /// + /// + /// The to send. + public async Task RespondOrEditAsync(DiscordEmbed embed) + => await this.RespondOrEditAsync(new DiscordMessageBuilder().WithEmbed(embed)); + + /// + public async Task RespondOrEditAsync(DiscordEmbedBuilder embed) + => await this.RespondOrEditAsync(new DiscordMessageBuilder().WithEmbed(embed.Build())); + + /// + /// Responds or edits the response to this command. + /// Automatically adjusts how to respond or edit the response based on the . + /// + /// The to send. + /// The that contains the reply. + /// + public async Task RespondOrEditAsync(DiscordMessageBuilder discordMessageBuilder) + { + switch (this.ExcutionType) + { + case HybridExecutionType.SlashCommand: + { + DiscordWebhookBuilder discordWebhookBuilder = new(); + + var files = new Dictionary(); + + foreach (var b in discordMessageBuilder.Files) + files.Add(b.FileName, b.Stream); + + discordWebhookBuilder.AddComponents(discordMessageBuilder.Components); + discordWebhookBuilder.AddEmbeds(discordMessageBuilder.Embeds); + discordWebhookBuilder.AddFiles(files); + discordWebhookBuilder.Content = discordMessageBuilder.Content; + + if (this.OriginalInteractionContext is null) + throw new InvalidOperationException("The OriginalInteractionContext is null but ExcutionType is set to SlashCommand."); + + var msg = await this.OriginalInteractionContext.EditResponseAsync(discordWebhookBuilder); + this.ResponseMessage = msg; + return msg; + } + + case HybridExecutionType.ContextMenuCommand: + { + DiscordWebhookBuilder discordWebhookBuilder = new(); + + var files = new Dictionary(); + + foreach (var b in discordMessageBuilder.Files) + files.Add(b.FileName, b.Stream); + + discordWebhookBuilder.AddComponents(discordMessageBuilder.Components); + discordWebhookBuilder.AddEmbeds(discordMessageBuilder.Embeds); + discordWebhookBuilder.AddFiles(files); + discordWebhookBuilder.Content = discordMessageBuilder.Content; + + if (this.OriginalContextMenuContext is null) + throw new InvalidOperationException("The OriginalContextMenuContext is null but ExcutionType is set to SlashCommand."); + + var msg = await this.OriginalContextMenuContext.EditResponseAsync(discordWebhookBuilder); + this.ResponseMessage = msg; + return msg; + } + + case HybridExecutionType.Unknown: + case HybridExecutionType.PrefixCommand: + { + if (this.ResponseMessage is not null) + { + if (discordMessageBuilder.Files?.Any() ?? false) + { + await this.ResponseMessage.DeleteAsync(); + var msg1 = await this.Channel.SendMessageAsync(discordMessageBuilder); + this.ResponseMessage = msg1; + return this.ResponseMessage; + } + + await this.ResponseMessage.ModifyAsync(discordMessageBuilder); + this.ResponseMessage = await this.ResponseMessage.Channel.GetMessageAsync(this.ResponseMessage.Id, true); + + return this.ResponseMessage; + } + + var msg = await this.Channel.SendMessageAsync(discordMessageBuilder); + + this.ResponseMessage = msg; + return this.ResponseMessage; + } + + default: + throw new NotImplementedException(); + } + } + + /// + /// Deletes the response to this command. + /// Automatically adjusts how to delete the response based on the . + /// Only works when RespondOrEditAsync was used. + /// + public async Task DeleteResponseAsync() + { + switch (this.ExcutionType) + { + case HybridExecutionType.SlashCommand: + { + if (this.OriginalInteractionContext is null) + throw new InvalidOperationException("The OriginalInteractionContext is null but ExcutionType is set to SlashCommand."); + + _ = this.OriginalInteractionContext.DeleteResponseAsync(); + return; + } + case HybridExecutionType.ContextMenuCommand: + { + if (this.OriginalContextMenuContext is null) + throw new InvalidOperationException("The OriginalContextMenuContext is null but ExcutionType is set to SlashCommand."); + + await this.OriginalContextMenuContext.DeleteResponseAsync(); + return; + } + default: + { + if (this.ResponseMessage is null) + return; + + await this.ResponseMessage.DeleteAsync(); + return; + } + } + } + + #endregion Methods +} diff --git a/DisCatSharp.HybridCommands/DisCatSharp.HybridCommands.csproj b/DisCatSharp.HybridCommands/DisCatSharp.HybridCommands.csproj new file mode 100644 index 0000000000..e35bbf1106 --- /dev/null +++ b/DisCatSharp.HybridCommands/DisCatSharp.HybridCommands.csproj @@ -0,0 +1,44 @@ + + + + + + + + + + + enable + + + + DisCatSharp.HybridCommands + + DisCatSharp Hybrid Commands Extension + + Use it on top of your DisCatSharp powered bot and unleash the power of both application commands and prefix commands in discord. + + Documentation: TODO + + DisCatSharp,Discord API Wrapper,Discord,Bots,Discord Bots,AITSYS,Net6,Net7,Application Commands,Prefix Commands,Hybrid Commands + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + diff --git a/DisCatSharp.HybridCommands/Entities/CacheConfig.cs b/DisCatSharp.HybridCommands/Entities/CacheConfig.cs new file mode 100644 index 0000000000..e72dc68f07 --- /dev/null +++ b/DisCatSharp.HybridCommands/Entities/CacheConfig.cs @@ -0,0 +1,31 @@ +// This file is part of the DisCatSharp project, based off DSharpPlus. +// +// Copyright (c) 2023 AITSYS +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System.Collections.Generic; + +namespace DisCatSharp.HybridCommands.Entities; +public sealed class CacheConfig +{ + public string? LastKnownHybridCommandHash { get; set; } + + public List LastKnownTypeHashes { get; set; } = new(); +} diff --git a/DisCatSharp.HybridCommands/Enums/HybridExecutionType.cs b/DisCatSharp.HybridCommands/Enums/HybridExecutionType.cs new file mode 100644 index 0000000000..01b91fe952 --- /dev/null +++ b/DisCatSharp.HybridCommands/Enums/HybridExecutionType.cs @@ -0,0 +1,30 @@ +// This file is part of the DisCatSharp project, based off DSharpPlus. +// +// Copyright (c) 2023 AITSYS +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +namespace DisCatSharp.HybridCommands.Enums; +public enum HybridExecutionType +{ + Unknown = 0, + PrefixCommand = 1, + SlashCommand = 2, + ContextMenuCommand = 3, +} diff --git a/DisCatSharp.HybridCommands/Exceptions/AssemblyLoadException.cs b/DisCatSharp.HybridCommands/Exceptions/AssemblyLoadException.cs new file mode 100644 index 0000000000..9d8a43cd4d --- /dev/null +++ b/DisCatSharp.HybridCommands/Exceptions/AssemblyLoadException.cs @@ -0,0 +1,34 @@ +// This file is part of the DisCatSharp project, based off DSharpPlus. +// +// Copyright (c) 2023 AITSYS +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; + +namespace DisCatSharp.HybridCommands.Exceptions; +public class AssemblyLoadException : Exception +{ + public string FilePath { get; set; } + + internal AssemblyLoadException(string filePath, string? message, Exception? innerException = null) : base(message, innerException) + { + this.FilePath = filePath; + } +} diff --git a/DisCatSharp.HybridCommands/ExtensionMethods.cs b/DisCatSharp.HybridCommands/ExtensionMethods.cs new file mode 100644 index 0000000000..a8b55b0a64 --- /dev/null +++ b/DisCatSharp.HybridCommands/ExtensionMethods.cs @@ -0,0 +1,71 @@ +// This file is part of the DisCatSharp project, based off DSharpPlus. +// +// Copyright (c) 2023 AITSYS +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +using DisCatSharp.ApplicationCommands; +using DisCatSharp.CommandsNext; + +namespace DisCatSharp.HybridCommands; +public static class ExtensionMethods +{ + /// + /// Enables hybrid commands on this . + /// Do not initialize or . This module uses it's own settings and initializes them with proper settings for you. + /// + /// Client to enable hybrid commands for. + /// Configuration to use. + /// Created . + public static HybridCommandsExtension UseHybridCommands(this DiscordClient client, HybridCommandsConfiguration? config = null) + { + if (client.GetExtension() != null) + throw new InvalidOperationException("Hybrid commands are already enabled for that client."); + + var scomm = new HybridCommandsExtension(config); + client.AddExtension(scomm); + return scomm; + } + + /// + /// Gets the hybrid commands module for this client. + /// + /// Client to get hybrid commands for. + /// The module, or null if not activated. + public static HybridCommandsExtension GetHybridCommands(this DiscordClient client) + => client.GetExtension(); + + /// + /// Gets the hybrid commands from this . + /// + /// Client to get hybrid commands from. + /// A dictionary of current with the key being the shard id. + public static async Task> GetHybridCommandsAsync(this DiscordShardedClient client) + { + await client.InitializeShardsAsync().ConfigureAwait(false); + var modules = new Dictionary(); + foreach (var shard in client.ShardClients.Values) + modules.Add(shard.ShardId, shard.GetExtension()); + return modules; + } +} diff --git a/DisCatSharp.HybridCommands/GlobalSuppressions.cs b/DisCatSharp.HybridCommands/GlobalSuppressions.cs new file mode 100644 index 0000000000..423fb18648 --- /dev/null +++ b/DisCatSharp.HybridCommands/GlobalSuppressions.cs @@ -0,0 +1,8 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "")] diff --git a/DisCatSharp.HybridCommands/HybridCommandsConfiguration.cs b/DisCatSharp.HybridCommands/HybridCommandsConfiguration.cs new file mode 100644 index 0000000000..86037e586e --- /dev/null +++ b/DisCatSharp.HybridCommands/HybridCommandsConfiguration.cs @@ -0,0 +1,125 @@ +// This file is part of the DisCatSharp project, based off DSharpPlus. +// +// Copyright (c) 2023 AITSYS +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; +using System.Collections.Generic; + +using DisCatSharp.CommandsNext; + +using Microsoft.Extensions.DependencyInjection; + +namespace DisCatSharp.HybridCommands; + +/// +/// Represents a configuration for . +/// +public sealed class HybridCommandsConfiguration +{ + /// + /// Whether to enable debug logs. + /// Defaults to false. + /// + public bool DebugEnabled { internal get; set; } = false; + + /// + /// Sets the string prefixes used for prefix commands. + /// Defaults to no value (disabled). + /// + public List? StringPrefixes { internal get; set; } + + /// + /// Sets the custom prefix resolver used for prefix commands. + /// Defaults to none (disabled). + /// + public PrefixResolverDelegate? PrefixResolver { internal get; set; } + + /// + /// Sets whether to allow mentioning the bot to be used as command prefix. + /// Defaults to true. + /// + public bool EnableMentionPrefix { internal get; set; } = true; + + /// + /// Sets whether to enable default help command. + /// Disabling this will allow you to make your own help command. + /// Defaults to true. + /// + public bool EnableDefaultHelp { internal get; set; } = true; + + /// + /// Sets the service provider for this instance. + /// Objects in this provider are used when instantiating command modules. This allows passing data around without resorting to static members. + /// Defaults to an empty service provider. + /// + public IServiceProvider? ServiceProvider { internal get; set; } + + /// + /// This option enables the localization feature. + /// Defaults to false. + /// + public bool EnableLocalization { internal get; set; } = false; + + /// + /// Whether to entirely disable usage of cached command assemblies. + /// This will cause commands to be recompiled on every startup. This will take significant amount of time if a lot of commands are registered. + /// Defaults to false. + /// + public bool DisableCompilationCache { internal get; set; } = false; + + /// + /// Whether the generated classes should be output to a sub-directory. Used for debugging-purposes. + /// Defaults to false. + /// + public bool OutputGeneratedClasses { internal get; set; } = false; + + /// + /// Creates a new instance of . + /// + public HybridCommandsConfiguration() { } + + /// + /// Initializes a new instance of the class. + /// + /// The service provider. + [ActivatorUtilitiesConstructor] + public HybridCommandsConfiguration(IServiceProvider provider) + { + this.ServiceProvider = provider; + } + + /// + /// Creates a new instance of , copying the properties of another configuration. + /// + /// Configuration the properties of which are to be copied. + public HybridCommandsConfiguration(HybridCommandsConfiguration other) + { + this.EnableDefaultHelp = other.EnableDefaultHelp; + this.EnableLocalization = other.EnableLocalization; + this.EnableMentionPrefix = other.EnableMentionPrefix; + this.PrefixResolver = other.PrefixResolver; + this.ServiceProvider = other.ServiceProvider; + this.StringPrefixes = other.StringPrefixes; + this.DisableCompilationCache = other.DisableCompilationCache; + this.OutputGeneratedClasses = other.OutputGeneratedClasses; + this.DebugEnabled = other.DebugEnabled; + } +} diff --git a/DisCatSharp.HybridCommands/HybridCommandsExtension.cs b/DisCatSharp.HybridCommands/HybridCommandsExtension.cs new file mode 100644 index 0000000000..1427e5cf96 --- /dev/null +++ b/DisCatSharp.HybridCommands/HybridCommandsExtension.cs @@ -0,0 +1,273 @@ +// This file is part of the DisCatSharp project, based off DSharpPlus. +// +// Copyright (c) 2023 AITSYS +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; + +using Microsoft.Extensions.Logging; +using DisCatSharp.ApplicationCommands; +using DisCatSharp.CommandsNext; +using System.Reflection; +using System.Linq; +using DisCatSharp.ApplicationCommands.Context; +using System.Threading.Tasks; +using DisCatSharp.HybridCommands.Context; +using DisCatSharp.HybridCommands.Attributes; +using System.Collections.Generic; +using DisCatSharp.Enums; +using DisCatSharp.Entities; +using System.Diagnostics; +using DisCatSharp.EventArgs; + +namespace DisCatSharp.HybridCommands; +public sealed class HybridCommandsExtension : BaseExtension +{ + internal static HybridCommandsConfiguration? Configuration; + + internal static ILogger? Logger { get; set; } + + internal static string? ExecutionDirectory { get; set; } + + internal List registeredCommands { get; set; } = new(); + + public static bool DebugEnabled + => Configuration?.DebugEnabled ?? false; + + internal protected override async void Setup(DiscordClient client) + { + if (this.Client != null) + throw new InvalidOperationException("What did I tell you?"); + + if (Configuration is null) + throw new InvalidOperationException("Configuration was not initialized."); + + this.Client = client; + Logger = client.Logger; + +#pragma warning disable CS8601 // Possible null reference assignment. + client.UseApplicationCommands(new ApplicationCommandsConfiguration + { + EnableLocalization = Configuration.EnableLocalization, + ServiceProvider = Configuration.ServiceProvider ?? null, + EnableDefaultHelp = false, + AutoDefer = true + }); + + + client.UseCommandsNext(new CommandsNextConfiguration + { + EnableMentionPrefix = Configuration.EnableMentionPrefix, + PrefixResolver = Configuration.PrefixResolver ?? null, + ServiceProvider = Configuration.ServiceProvider ?? null, + StringPrefixes = Configuration.StringPrefixes ?? null, + EnableDefaultHelp = false + }); +#pragma warning restore CS8601 // Possible null reference assignment. + + if (HybridCommandsExtension.Configuration.EnableDefaultHelp) + await this.RegisterGlobalCommands(); + } + + /// + /// Registers all commands from a given assembly. The command classes need to be public to be considered for registration. + /// + /// Assembly to register commands from. + /// The guild id to register it on. + /// A callback to setup app command translations with. + public async Task RegisterGuildCommands(Assembly assembly, ulong guildId, Action? translationSetup = null) + { + var types = assembly.GetTypes().Where(xt => + { + var xti = xt.GetTypeInfo(); + return xti.IsModuleCandidateType() && !xti.IsNested; + }); + foreach (var xt in types) + await this.RegisterGuildCommands(xt, guildId, translationSetup); + } + + /// + /// Registers all commands from a given assembly. The command classes need to be public to be considered for registration. + /// + /// Assembly to register commands from. + /// A callback to setup app command translations with. + public async Task RegisterGlobalCommands(Assembly assembly, Action? translationSetup = null) + { + var types = assembly.GetTypes().Where(xt => + { + var xti = xt.GetTypeInfo(); + return xti.IsModuleCandidateType() && !xti.IsNested; + }); + foreach (var xt in types) + await this.RegisterGlobalCommands(xt, translationSetup); + } + + /// + /// Registers a hybrid command class with optional translation setup globally. + /// + /// The of the command class to register. + /// A callback to setup app command translations with. + public async Task RegisterGlobalCommands(Type type, Action? translationSetup = null) + { + if (!type.IsModuleCandidateType()) + throw new ArgumentException("Command Class is not a valid module candidate."); + + foreach (var assembly in await type.CompileAndLoadCommands()) + { + var commandsNextModule = assembly.DefinedTypes.FirstOrDefault(x => typeof(BaseCommandModule).IsAssignableTo(x), null); + if (commandsNextModule is not null) + { + this.Client.GetCommandsNext().RegisterCommands(commandsNextModule); + } + + var applicationCommandsModule = assembly.DefinedTypes.FirstOrDefault(x => typeof(ApplicationCommandsModule).IsAssignableTo(x), null); + if (applicationCommandsModule is not null) + { +#pragma warning disable CS8604 // Possible null reference argument. + this.Client.GetApplicationCommands().RegisterGlobalCommands(applicationCommandsModule, translationSetup); +#pragma warning restore CS8604 // Possible null reference argument. + } + } + } + + /// + /// Registers a hybrid command class with optional translation setup for a guild. + /// + /// The of the command class to register. + /// The guild id to register it on. + /// A callback to setup app command translations with. + public async Task RegisterGuildCommands(Type type, ulong guildId, Action? translationSetup = null) + { + if (!type.IsModuleCandidateType()) + throw new ArgumentException("Command Class is not a valid module candidate."); + + foreach (var assembly in await type.CompileAndLoadCommands(guildId)) + { + var commandsNextModule = assembly.DefinedTypes.FirstOrDefault(x => typeof(BaseCommandModule).IsAssignableFrom(x), null); + if (commandsNextModule is not null) + { + this.Client.GetCommandsNext().RegisterCommands(commandsNextModule); + } + + var applicationCommandsModule = assembly.DefinedTypes.FirstOrDefault(x => typeof(ApplicationCommandsModule).IsAssignableFrom(x), null); + if (applicationCommandsModule is not null) + { +#pragma warning disable CS8604 // Possible null reference argument. + this.Client.GetApplicationCommands().RegisterGuildCommands(applicationCommandsModule, guildId, translationSetup); +#pragma warning restore CS8604 // Possible null reference argument. + } + } + } + + /// + /// The command class to register. + public async Task RegisterGlobalCommands(Action? translationSetup = null) where T : HybridCommandsModule + => await this.RegisterGlobalCommands(typeof(T), translationSetup); + + /// + /// The command class to register. + public async Task RegisterGuildCommands(ulong guildId, Action? translationSetup = null) where T : HybridCommandsModule + => await this.RegisterGuildCommands(typeof(T), guildId, translationSetup); + + internal HybridCommandsExtension(HybridCommandsConfiguration? configuration = null) + { + configuration ??= new HybridCommandsConfiguration(); + Configuration = new HybridCommandsConfiguration(configuration); + } + + private HybridCommandsExtension() { } +} + +public class DefaultHybridHelp : HybridCommandsModule +{ + [HybridCommand("help", "Displays all commands, their usage and description", true, false)] + public async Task HelpAsync(HybridCommandContext ctx) + { + var hybridModule = ctx.Client.GetHybridCommands(); + var commandDescriptions = new List(); + + foreach (var command in hybridModule.registeredCommands) + commandDescriptions.Add($"`{command.Name}` - _{command.Description}_{(command.IsNsfw ? " (**NSFW**)" : "")}"); + + var splitDescriptions = new List(); + + var currentBuild = ""; + foreach (var description in commandDescriptions) + { + var add = $"{description}\n"; + + if (add.Length + currentBuild.Length > 2048) + { + splitDescriptions.Add(currentBuild); + currentBuild = ""; + } + + currentBuild += add; + } + + if (currentBuild.Length > 0) + { + splitDescriptions.Add(currentBuild); + currentBuild = ""; + } + + var embeds = splitDescriptions.Select(x => new DiscordEmbedBuilder + { + Description = x, + Title = "Command list", + Timestamp = DateTime.UtcNow + }).ToList(); + + var PrevPage = new DiscordButtonComponent(ButtonStyle.Primary, Guid.NewGuid().ToString(), "Previous page", false, new DiscordComponentEmoji(DiscordEmoji.FromUnicode("◀"))); + var NextPage = new DiscordButtonComponent(ButtonStyle.Primary, Guid.NewGuid().ToString(), "Next page", false, new DiscordComponentEmoji(DiscordEmoji.FromUnicode("▶"))); + + Task EditMessage() + => ctx.RespondOrEditAsync(new DiscordMessageBuilder() + .WithEmbed(embeds![0]) + .AddComponents(PrevPage!.Disable(), NextPage!)); + + await EditMessage(); + + uint currentIndex = 0; + var sw = Stopwatch.StartNew(); + ctx.Client.ComponentInteractionCreated += RunInteraction; + + async Task RunInteraction(DiscordClient sender, ComponentInteractionCreateEventArgs e) + { + if (e.Id == PrevPage.CustomId) + currentIndex--; + else if (e.Id == NextPage.CustomId) + currentIndex++; + + PrevPage!.SetState(currentIndex <= 0); + NextPage.SetState(currentIndex >= embeds.Count); + + await EditMessage(); + } + + while (sw.Elapsed < TimeSpan.FromSeconds(60)) + { + await Task.Delay(1000); + } + + ctx.Client.ComponentInteractionCreated -= RunInteraction; + await ctx.RespondOrEditAsync(embeds[(int)currentIndex].WithFooter("Interaction timed out")); + } +} diff --git a/DisCatSharp.HybridCommands/HybridCommandsModule.cs b/DisCatSharp.HybridCommands/HybridCommandsModule.cs new file mode 100644 index 0000000000..a48071e44e --- /dev/null +++ b/DisCatSharp.HybridCommands/HybridCommandsModule.cs @@ -0,0 +1,45 @@ +// This file is part of the DisCatSharp project, based off DSharpPlus. +// +// Copyright (c) 2023 AITSYS +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System.Threading.Tasks; + +using DisCatSharp.HybridCommands.Context; + +namespace DisCatSharp.HybridCommands; +public class HybridCommandsModule +{ + /// + /// Called before the execution of a slash command in the module. + /// + /// The context. + /// Whether or not to execute the slash command. + public virtual Task BeforeExecutionAsync(HybridCommandContext ctx) + => Task.FromResult(true); + + /// + /// Called after the execution of a slash command in the module. + /// + /// The context. + /// + public virtual Task AfterExecutionAsync(HybridCommandContext ctx) + => Task.CompletedTask; +} diff --git a/DisCatSharp.HybridCommands/HybridCommandsUtilities.cs b/DisCatSharp.HybridCommands/HybridCommandsUtilities.cs new file mode 100644 index 0000000000..b6ad3c7b80 --- /dev/null +++ b/DisCatSharp.HybridCommands/HybridCommandsUtilities.cs @@ -0,0 +1,591 @@ +// This file is part of the DisCatSharp project, based off DSharpPlus. +// +// Copyright (c) 2023 AITSYS +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using DisCatSharp.ApplicationCommands; +using DisCatSharp.ApplicationCommands.Attributes; +using DisCatSharp.ApplicationCommands.Context; +using DisCatSharp.CommandsNext; +using DisCatSharp.CommandsNext.Attributes; +using DisCatSharp.HybridCommands.Attributes; +using DisCatSharp.HybridCommands.Context; +using DisCatSharp.HybridCommands.Entities; +using DisCatSharp.HybridCommands.Utilities; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Emit; +using Microsoft.Extensions.Logging; + +using Newtonsoft.Json; + +namespace DisCatSharp.HybridCommands; +internal static class HybridCommandsUtilities +{ + /// + /// Whether this module is a candidate type. + /// + /// The type. + internal static bool IsModuleCandidateType(this Type type) + => type.GetTypeInfo().IsModuleCandidateType(); + + /// + /// Whether this module is a candidate type. + /// + /// The type info. + internal static bool IsModuleCandidateType(this System.Reflection.TypeInfo typeInfo) + { + if (typeInfo.GetCustomAttribute(false) != null) + return false; + + if (!typeof(HybridCommandsModule).GetTypeInfo().IsAssignableFrom(typeInfo)) + { + if (HybridCommandsExtension.DebugEnabled) + HybridCommandsExtension.Logger?.LogDebug("Not assignable from type"); + return false; + } + + if (typeInfo.IsGenericType && typeInfo.Name.Contains("AnonymousType") && (typeInfo.Name.StartsWith("<>") || typeInfo.Name.StartsWith("VB$")) && (typeInfo.Attributes & TypeAttributes.NotPublic) == TypeAttributes.NotPublic) + { + if (HybridCommandsExtension.DebugEnabled) + HybridCommandsExtension.Logger?.LogDebug("Anonymous"); + return false; + } + + if (!typeInfo.IsClass || typeInfo.IsAbstract) + return false; + + var tdelegate = typeof(Delegate).GetTypeInfo(); + if (tdelegate.IsAssignableFrom(typeInfo)) + { + if (HybridCommandsExtension.DebugEnabled) + HybridCommandsExtension.Logger?.LogDebug("Delegated"); + return false; + } + + if (HybridCommandsExtension.DebugEnabled) + HybridCommandsExtension.Logger?.LogDebug("Checking qualifying methods"); + + return typeInfo.DeclaredMethods.Any(xmi => xmi.IsCommandCandidate(out _)) || typeInfo.DeclaredNestedTypes.Any(xti => xti.IsModuleCandidateType()); + } + + /// + /// Whether this is a command candidate. + /// + /// The method. + /// The parameters. + internal static bool IsCommandCandidate(this MethodInfo method, out ParameterInfo[]? parameters) + { + parameters = null; + + if (method == null) + { + if (HybridCommandsExtension.DebugEnabled) + HybridCommandsExtension.Logger?.LogDebug("Not existent"); + return false; + } + if (HybridCommandsExtension.DebugEnabled) + HybridCommandsExtension.Logger?.LogDebug("Checking method {name}", method.Name); + + if (method.IsAbstract || method.IsConstructor || method.IsSpecialName) + { + if (HybridCommandsExtension.DebugEnabled) + HybridCommandsExtension.Logger?.LogDebug("Abstract, Constructor or Special name"); + return false; + } + + if (!method.GetCustomAttributes().Any(x => x.GetType() == typeof(HybridCommandAttribute))) + { + if (HybridCommandsExtension.DebugEnabled) + HybridCommandsExtension.Logger?.LogDebug("Does not have HybridCommandAttribute"); + return false; + } + + parameters = method.GetParameters(); + if (!parameters.Any() || parameters.First().ParameterType != typeof(HybridCommandContext) || method.ReturnType != typeof(Task)) + { + if (HybridCommandsExtension.DebugEnabled) + HybridCommandsExtension.Logger?.LogDebug("Missing first parameter with type HybridCommandContext"); + return false; + } + + if (HybridCommandsExtension.DebugEnabled) + HybridCommandsExtension.Logger?.LogDebug("Qualifies"); + + return true; + } + + internal static async Task CompileAndLoadCommands(this Type type, ulong? guildId = null) + { + if (HybridCommandsExtension.Configuration is null) + throw new InvalidOperationException("Configuration is null"); + + HybridCommandsExtension.ExecutionDirectory ??= new FileInfo(Assembly.GetExecutingAssembly().Location).DirectoryName; + + if (HybridCommandsExtension.DebugEnabled) + HybridCommandsExtension.Logger?.LogDebug("ExecutionDirectory: {Dir}", HybridCommandsExtension.ExecutionDirectory); + + var CacheDirectoryPath = $"{HybridCommandsExtension.ExecutionDirectory}/CachedHybridCommands"; + var CacheConfigPath = $"{CacheDirectoryPath}/cache.json"; + + if (HybridCommandsExtension.DebugEnabled) + HybridCommandsExtension.Logger?.LogDebug("CacheDirectory: {Dir}", CacheDirectoryPath); + + if (HybridCommandsExtension.DebugEnabled) + HybridCommandsExtension.Logger?.LogDebug("CacheDirectory: {Dir}", CacheConfigPath); + + if (!Directory.Exists(CacheDirectoryPath)) + Directory.CreateDirectory(CacheDirectoryPath); + + var typeHash = JsonConvert.SerializeObject(type.GetTypeInfo(), new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore }).ComputeSHA256Hash(); + + if (HybridCommandsExtension.DebugEnabled) + HybridCommandsExtension.Logger?.LogDebug("TypeHash: {Hash}", typeHash); + + var cacheConfig = File.Exists(CacheConfigPath) ? JsonConvert.DeserializeObject(File.ReadAllText(CacheConfigPath)) ?? new() : new(); + + var fileInfos = new DirectoryInfo(CacheDirectoryPath).GetFiles(); + + Dictionary loadedAssemblies = new(); + + void PopulateFields() + { + foreach (var assembly in loadedAssemblies.Keys) + { + if (HybridCommandsExtension.DebugEnabled) + HybridCommandsExtension.Logger?.LogDebug("Populating assembly {assembly}", assembly.FullName); + + foreach (var parentType in assembly.GetTypes()) + { + if (HybridCommandsExtension.DebugEnabled) + HybridCommandsExtension.Logger?.LogDebug("Populating type {type}", parentType.FullName); + + foreach (var method in parentType.GetMethods()) + { + if (method.Name.EndsWith("_Populate")) + { + if (HybridCommandsExtension.DebugEnabled) + HybridCommandsExtension.Logger?.LogDebug("Populating via {method} in {type}", method.Name, parentType.Name); + + method.Invoke(null, Array.Empty()); + } + } + + foreach (var subType in parentType.GetNestedTypes()) + { + foreach (var method in subType.GetMethods()) + { + if (method.Name.EndsWith("_Populate")) + { + if (HybridCommandsExtension.DebugEnabled) + HybridCommandsExtension.Logger?.LogDebug("Populating via {method} in {type}", method.Name, subType.Name); + + method.Invoke(null, Array.Empty()); + } + } + } + } + } + } + + if (!HybridCommandsExtension.Configuration.DisableCompilationCache && cacheConfig.LastKnownTypeHashes.Contains(typeHash)) + foreach (var file in fileInfos) + { + if ($"{file.Name.Replace(".dll", "")}" == $"{typeHash}-app") + { + var loadContext = new CommandLoadContext(file.FullName); + var assembly = loadContext.LoadFromAssemblyName(new AssemblyName(Path.GetFileNameWithoutExtension(file.FullName))); + + if (assembly.GetTypes().Any(x => typeof(ApplicationCommandsModule).IsAssignableFrom(x))) + loadedAssemblies.Add(assembly, $"{typeHash}-app"); + } + + if ($"{file.Name.Replace(".dll", "")}" == $"{typeHash}-prefix") + { + var loadContext = new CommandLoadContext(file.FullName); + var assembly = loadContext.LoadFromAssemblyName(new AssemblyName(Path.GetFileNameWithoutExtension(file.FullName))); + + if (assembly.GetTypes().Any(x => typeof(BaseCommandModule).IsAssignableFrom(x))) + loadedAssemblies.Add(assembly, $"{typeHash}-prefix"); + } + } + + if (loadedAssemblies.Values.Any(x => x == $"{typeHash}-prefix") && loadedAssemblies.Values.Any(x => x == $"{typeHash}-app")) + { + PopulateFields(); + return loadedAssemblies.Keys.ToArray(); + } + + void RenderError(ImmutableArray diagnostics, string generatedClass) + { + if (!HybridCommandsExtension.DebugEnabled) + return; + + Thread.Sleep(1000); + + for (var i = 0; i < generatedClass.Length; i++) + { + var foundDiagnostic = diagnostics.FirstOrDefault(x => x!.Location.SourceSpan.Start <= i && x.Location.SourceSpan.End >= i, null); + + if (foundDiagnostic is not null) + switch (foundDiagnostic.Severity) + { + case DiagnosticSeverity.Info: + Console.ForegroundColor = ConsoleColor.Cyan; + break; + case DiagnosticSeverity.Warning: + Console.ForegroundColor = ConsoleColor.Yellow; + break; + case DiagnosticSeverity.Error: + Console.ForegroundColor = ConsoleColor.Red; + break; + } + else + Console.ResetColor(); + + Console.Write(generatedClass[i]); + } + + Console.WriteLine(); + } + + string[] GenerateAppCommandMethods() + { + List generatedMethods = new(); + + foreach (var method in type.GetMethods()) + { + if (!method.IsCommandCandidate(out var parameters)) + continue; + + var cmdInfo = method.GetCustomAttribute(); + var restrictedTypes = method.GetCustomAttributes().Any(x => x is RestrictedExecutionTypesAttribute) ? method.GetCustomAttributes() : null; + + if (!restrictedTypes?.First().Types.Any(y => y == Enums.HybridExecutionType.SlashCommand) ?? false) + { + if (HybridCommandsExtension.DebugEnabled) + HybridCommandsExtension.Logger?.LogDebug("Command is limited to execution with: {types}", string.Join("\n", restrictedTypes?.First().Types.Select(x => Enum.GetName(x)).ToArray() ?? Array.Empty())); + continue; + } + + if (parameters is null || + parameters.Length == 0 || + parameters[0].ParameterType != typeof(HybridCommandContext) || + (parameters.Length > 1 && parameters.Where(x => x.ParameterType != typeof(HybridCommandContext)).All(x => x.GetCustomAttributes().Any(y => y is Attributes.OptionAttribute)))) + { + if (HybridCommandsExtension.DebugEnabled) + HybridCommandsExtension.Logger?.LogDebug("Method has no HybridCommandContext or is missing OptionAttributes"); + + continue; + } + + if (cmdInfo is null) + { + if (HybridCommandsExtension.DebugEnabled) + HybridCommandsExtension.Logger?.LogDebug("Method has no HybridCommandAttribute"); + + continue; + } + + var MethodName = $"a{Guid.NewGuid().ToString().ToLower().Replace("-", "")}"; + + var filteredParams = parameters.Where(x => x.ParameterType != typeof(HybridCommandContext)); + string MakeParameters() + { + List generatedParams = new(); + + foreach (var param in filteredParams) + { + var option = param.GetCustomAttribute() ?? throw new InvalidOperationException("Method Parameter is missing OptionAttribute."); + var useRemainingString = param.GetCustomAttributes().Any(x => x is RemainingTextAttribute); + generatedParams.Add($"[{typeof(ApplicationCommands.Attributes.OptionAttribute).FullName}(\"{option.Name.SanitzeForString()}\", \"{option.Description.SanitzeForString()}\")]" + + $"{param.ParameterType.FullName} {param.Name}"); + } + + return string.Join(", ", generatedParams); + } + + generatedMethods.Add($$""" + public static {{typeof(MethodInfo).FullName}} {{MethodName}}_CommandMethod { get; set; } + + public static void {{MethodName}}_Populate() + { + var callingAssembly = {{typeof(AppDomain).FullName}}.CurrentDomain.GetAssemblies().SingleOrDefault(assembly => assembly.GetName().Name == "{{type.Assembly.GetName().Name}}"); + var type = callingAssembly.GetType("{{type.FullName}}"); + var methods = type.GetMethods(); + {{MethodName}}_CommandMethod = methods.First(x => x.Name == "{{method.Name}}"); + } + + [{{typeof(SlashCommandAttribute).FullName}}("{{cmdInfo.Name.SanitzeForString()}}", "{{cmdInfo.Description.SanitzeForString()}}")] + public {{typeof(Task).FullName}} {{MethodName}}_Execute({{typeof(InteractionContext).FullName}} ctx{{MakeParameters()}}) + { + {{(guildId is not null ? $"if ((ctx.Guild?.Id ?? 0) != {guildId}) return {typeof(Task).FullName}.CompletedTask;" : "")}} + + {{MethodName}}_CommandMethod.Invoke({{typeof(Activator).FullName}}.CreateInstance({{MethodName}}_CommandMethod.DeclaringType), new object[] + { + new {{typeof(HybridCommandContext).FullName}}(ctx){{(filteredParams.Any() ? $", {string.Join(", ", filteredParams.Select(x => x.Name))}" : "")}} + }); + return {{typeof(Task).FullName}}.CompletedTask; + } + """); + } + + return generatedMethods.ToArray(); + } + + var AppClass = + $$""" + // This class has been auto-generated by DisCatSharp.HybridCommands. + + using System.Linq; + + namespace DisCatSharp.RunTimeCompiled; + + public sealed class a{{typeHash}}_AppCommands : {{typeof(ApplicationCommandsModule).FullName}} + { + {{string.Join("\n\n", GenerateAppCommandMethods())}} + } + """; + + string[] GeneratePrefixCommandMethods() + { + List generatedMethods = new(); + + foreach (var method in type.GetMethods()) + { + if (!method.IsCommandCandidate(out var parameters)) + continue; + + var cmdInfo = method.GetCustomAttribute(); + var restrictedTypes = method.GetCustomAttributes().Any(x => x is RestrictedExecutionTypesAttribute) ? method.GetCustomAttributes() : null; + + if (!restrictedTypes?.First().Types.Any(y => y == Enums.HybridExecutionType.PrefixCommand) ?? false) + { + if (HybridCommandsExtension.DebugEnabled) + HybridCommandsExtension.Logger?.LogDebug("Command is limited to execution with: {types}", string.Join("\n", restrictedTypes?.First().Types.Select(x => Enum.GetName(x)).ToArray() ?? Array.Empty())); + continue; + } + + if (parameters is null || + parameters.Length == 0 || + parameters[0].ParameterType != typeof(HybridCommandContext) || + (parameters.Length > 1 && parameters.Where(x => x.ParameterType != typeof(HybridCommandContext)).All(x => x.GetCustomAttributes().Any(y => y is Attributes.OptionAttribute)))) + { + if (HybridCommandsExtension.DebugEnabled) + HybridCommandsExtension.Logger?.LogDebug("Method has no HybridCommandContext or is missing OptionAttributes"); + + continue; + } + + if (cmdInfo is null) + { + if (HybridCommandsExtension.DebugEnabled) + HybridCommandsExtension.Logger?.LogDebug("Method has no HybridCommandAttribute"); + + continue; + } + + var MethodName = $"a{Guid.NewGuid().ToString().ToLower().Replace("-", "")}"; + + var filteredParams = parameters.Where(x => x.ParameterType != typeof(HybridCommandContext)); + string MakeParameters() + { + List generatedParams = new(); + + foreach (var param in filteredParams) + { + var option = param.GetCustomAttribute() ?? throw new InvalidOperationException("Method Parameter is missing OptionAttribute."); + var useRemainingString = param.GetCustomAttributes().Any(x => x is RemainingTextAttribute); + generatedParams.Add($"[{typeof(DescriptionAttribute).FullName}(\"{option.Description.SanitzeForString()}\")]" + + $"{(useRemainingString ? $"[{typeof(RemainingTextAttribute).FullName}]" : "")} {param.ParameterType.FullName} {param.Name}"); + } + + return string.Join(", ", generatedParams); + } + + generatedMethods.Add($$""" + public static {{typeof(MethodInfo).FullName}} {{MethodName}}_CommandMethod { get; set; } + + public static void {{MethodName}}_Populate() + { + var callingAssembly = {{typeof(AppDomain).FullName}}.CurrentDomain.GetAssemblies().SingleOrDefault(assembly => assembly.GetName().Name == "{{type.Assembly.GetName().Name}}"); + var type = callingAssembly.GetType("{{type.FullName}}"); + var methods = type.GetMethods(); + {{MethodName}}_CommandMethod = methods.First(x => x.Name == "{{method.Name}}"); + } + + [{{typeof(CommandAttribute).FullName}}("{{cmdInfo.Name.SanitzeForString()}}"), + {{typeof(DescriptionAttribute).FullName}}("{{cmdInfo.Description.SanitzeForString()}}")] + public {{typeof(Task).FullName}} {{MethodName}}_Execute({{typeof(CommandContext).FullName}} ctx{{MakeParameters()}}) + { + {{(guildId is not null ? $"if ((ctx.Guild?.Id ?? 0) != {guildId}) return {typeof(Task).FullName}.CompletedTask;" : "")}} + + {{MethodName}}_CommandMethod.Invoke({{typeof(Activator).FullName}}.CreateInstance({{MethodName}}_CommandMethod.DeclaringType), new object[] + { + new {{typeof(HybridCommandContext).FullName}}(ctx){{(filteredParams.Any() ? $", {string.Join(", ", filteredParams.Select(x => x.Name))}" : "")}} + }); + return {{typeof(Task).FullName}}.CompletedTask; + } + """); + } + + return generatedMethods.ToArray(); + } + + var PrefixClass = + $$""" + // This class has been auto-generated by DisCatSharp.HybridCommands. + + using System.Linq; + + namespace DisCatSharp.RunTimeCompiled; + + public sealed class a{{typeHash}}_PrefixCommands : {{typeof(BaseCommandModule).FullName}} + { + {{string.Join("\n\n", GeneratePrefixCommandMethods())}} + } + """; + + if (HybridCommandsExtension.Configuration.OutputGeneratedClasses) + { + File.WriteAllText($"{CacheDirectoryPath}/{typeHash}-app.cs", AppClass); + File.WriteAllText($"{CacheDirectoryPath}/{typeHash}-prefix.cs", PrefixClass); + } + + var options = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary).WithOptimizationLevel(OptimizationLevel.Debug); + var references = AppDomain.CurrentDomain.GetAssemblies().Where(x => !x.IsDynamic && !string.IsNullOrWhiteSpace(x.Location)).Select(x => MetadataReference.CreateFromFile(x.Location)); + + try + { + if (!loadedAssemblies.Values.Any(x => x == $"{typeHash}-app")) + { + using var stream = new MemoryStream(); + HybridCommandsExtension.Logger?.LogDebug("Compiling Application Commands Class '{class}'..", type.Name); + + var result = CSharpCompilation.Create($"{typeHash}-app") + .AddSyntaxTrees(SyntaxFactory.ParseSyntaxTree(AppClass)) + .AddReferences(references) + .WithOptions(options).Emit(stream); + if (!result.Success) + { + RenderError(result.Diagnostics, AppClass); + + Exception exception = new(); + exception.Data.Add("diagnostics", result.Diagnostics); + throw exception; + } + + var assemblyBytes = stream.ToArray(); + var assembly = Assembly.Load(assemblyBytes); + loadedAssemblies.Add(assembly, $"{typeHash}-app"); + + var path = $"{CacheDirectoryPath}/{assembly.GetName().Name}.dll"; + using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.ReadWrite)) + { + stream.Seek(0, SeekOrigin.Begin); + await stream.CopyToAsync(fileStream); + await fileStream.FlushAsync(); + + if (!cacheConfig.LastKnownTypeHashes.Contains(typeHash)) + cacheConfig.LastKnownTypeHashes.Add(typeHash); + } + + HybridCommandsExtension.Logger?.LogDebug("Compiled Application Commands Class '{class}'..", type.Name); + } + + } + catch (Exception ex) + { + HybridCommandsExtension.Logger?.LogError(ex, "Failed to compile Application Commands Class '{class}'.", type.Name); + } + + try + { + if (!loadedAssemblies.Values.Any(x => x == $"{typeHash}-prefix")) + { + using var stream = new MemoryStream(); + HybridCommandsExtension.Logger?.LogDebug("Compiling Prefix Commands Class '{class}'..", type.Name); + + var result = CSharpCompilation.Create($"{typeHash}-prefix") + .AddSyntaxTrees(SyntaxFactory.ParseSyntaxTree(PrefixClass)) + .AddReferences(references) + .WithOptions(options).Emit(stream); + if (!result.Success) + { + RenderError(result.Diagnostics, PrefixClass); + + Exception exception = new(); + exception.Data.Add("diagnostics", result.Diagnostics); + throw exception; + } + + var assemblyBytes = stream.ToArray(); + var assembly = Assembly.Load(assemblyBytes); + loadedAssemblies.Add(assembly, $"{typeHash}-prefix"); + + var path = $"{CacheDirectoryPath}/{assembly.GetName().Name}.dll"; + using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.ReadWrite)) + { + stream.Seek(0, SeekOrigin.Begin); + await stream.CopyToAsync(fileStream); + await fileStream.FlushAsync(); + + if (!cacheConfig.LastKnownTypeHashes.Contains(typeHash)) + cacheConfig.LastKnownTypeHashes.Add(typeHash); + } + + HybridCommandsExtension.Logger?.LogDebug("Compiled Prefix Commands Class '{class}'..", type.Name); + } + + } + catch (Exception ex) + { + HybridCommandsExtension.Logger?.LogError(ex, "Failed to compile Prefix Commands Class '{class}'.", type.Name); + } + + File.WriteAllText(CacheConfigPath, JsonConvert.SerializeObject(cacheConfig, Formatting.Indented)); + PopulateFields(); + return loadedAssemblies.Keys.ToArray(); + } + + /// + /// Compute the SHA256-Hash for the given string + /// + /// + /// + private static string ComputeSHA256Hash(this string str) + => BitConverter.ToString(SHA256.HashData(Encoding.ASCII.GetBytes(str))).Replace("-", "").ToLowerInvariant(); + + private static string SanitzeForString(this string str) + => str.Replace("\"", "\\\""); +} diff --git a/DisCatSharp.HybridCommands/Utilities/CommandLoadContext.cs b/DisCatSharp.HybridCommands/Utilities/CommandLoadContext.cs new file mode 100644 index 0000000000..de8cbcd373 --- /dev/null +++ b/DisCatSharp.HybridCommands/Utilities/CommandLoadContext.cs @@ -0,0 +1,52 @@ +// This file is part of the DisCatSharp project, based off DSharpPlus. +// +// Copyright (c) 2023 AITSYS +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; +using System.Reflection; +using System.Runtime.Loader; + +using DisCatSharp.HybridCommands.Exceptions; + +namespace DisCatSharp.HybridCommands.Utilities; +internal class CommandLoadContext : AssemblyLoadContext +{ + private readonly string _filePath; + private readonly AssemblyDependencyResolver _resolver; + + public CommandLoadContext(string path) + { + this._filePath = path; + this._resolver = new AssemblyDependencyResolver(path); + } + + protected override Assembly Load(AssemblyName assemblyName) + { + var assemblyPath = this._resolver.ResolveAssemblyToPath(assemblyName); + return assemblyPath != null ? this.LoadFromAssemblyPath(assemblyPath) : throw new AssemblyLoadException(this._filePath, "Failed to load assembly."); + } + + protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) + { + var libraryPath = this._resolver.ResolveUnmanagedDllToPath(unmanagedDllName); + return libraryPath != null ? this.LoadUnmanagedDllFromPath(libraryPath) : IntPtr.Zero; + } +} diff --git a/DisCatSharp.Targets/InternalsVisibleTo.targets b/DisCatSharp.Targets/InternalsVisibleTo.targets index 01a7f6c6ff..00f5965893 100644 --- a/DisCatSharp.Targets/InternalsVisibleTo.targets +++ b/DisCatSharp.Targets/InternalsVisibleTo.targets @@ -4,6 +4,7 @@ + diff --git a/DisCatSharp.sln b/DisCatSharp.sln index f830cccaaf..0ed29b9c8b 100644 --- a/DisCatSharp.sln +++ b/DisCatSharp.sln @@ -93,6 +93,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp.SafetyTests", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp.Lavalink", "DisCatSharp.Lavalink\DisCatSharp.Lavalink.csproj", "{1ADC1D06-3DB8-4741-B740-13161B40CBA3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DisCatSharp.HybridCommands", "DisCatSharp.HybridCommands\DisCatSharp.HybridCommands.csproj", "{8D7751F6-FF33-4AA7-B05E-39EDA531D264}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -163,6 +165,10 @@ Global {1ADC1D06-3DB8-4741-B740-13161B40CBA3}.Debug|Any CPU.Build.0 = Debug|Any CPU {1ADC1D06-3DB8-4741-B740-13161B40CBA3}.Release|Any CPU.ActiveCfg = Release|Any CPU {1ADC1D06-3DB8-4741-B740-13161B40CBA3}.Release|Any CPU.Build.0 = Release|Any CPU + {8D7751F6-FF33-4AA7-B05E-39EDA531D264}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8D7751F6-FF33-4AA7-B05E-39EDA531D264}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8D7751F6-FF33-4AA7-B05E-39EDA531D264}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8D7751F6-FF33-4AA7-B05E-39EDA531D264}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE