diff --git a/DisCatSharp.CommandsNext/Attributes/CheckBaseAttribute.cs b/DisCatSharp.CommandsNext/Attributes/CheckBaseAttribute.cs index 6fa659c4b..688c52d88 100644 --- a/DisCatSharp.CommandsNext/Attributes/CheckBaseAttribute.cs +++ b/DisCatSharp.CommandsNext/Attributes/CheckBaseAttribute.cs @@ -1,41 +1,51 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-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 NONINFRINGEMENT. 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.Threading.Tasks; +using DisCatSharp.HybridCommands.Entities; + namespace DisCatSharp.CommandsNext.Attributes; /// /// Represents a base for all command pre-execution check attributes. /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] public abstract class CheckBaseAttribute : Attribute { /// /// Asynchronously checks whether this command can be executed within given context. /// /// Context to check execution ability for. /// Whether this check is being executed from help or not. This can be used to probe whether command can be run without setting off certain fail conditions (such as cooldowns). /// Whether the command can be executed in given context. public abstract Task ExecuteCheckAsync(CommandContext ctx, bool help); + + /// + /// Asynchronously checks whether this command can be executed within given context. + /// + /// Context to check execution ability for. + /// Whether this check is being executed from help or not. This can be used to probe whether command can be run without setting off certain fail conditions (such as cooldowns). + /// Whether the command can be executed in given context. + public abstract Task ExecuteCheckAsync(HybridCommandContext ctx, bool help); } diff --git a/DisCatSharp.CommandsNext/Attributes/CooldownAttribute.cs b/DisCatSharp.CommandsNext/Attributes/CooldownAttribute.cs index 5ffaa467a..686073aa0 100644 --- a/DisCatSharp.CommandsNext/Attributes/CooldownAttribute.cs +++ b/DisCatSharp.CommandsNext/Attributes/CooldownAttribute.cs @@ -1,332 +1,378 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-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 NONINFRINGEMENT. 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.Concurrent; using System.Globalization; using System.Threading; using System.Threading.Tasks; +using DisCatSharp.HybridCommands.Entities; + namespace DisCatSharp.CommandsNext.Attributes; /// /// Defines a cooldown for this command. This allows you to define how many times can users execute a specific command /// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = false)] public sealed class CooldownAttribute : CheckBaseAttribute { /// /// Gets the maximum number of uses before this command triggers a cooldown for its bucket. /// public int MaxUses { get; } /// /// Gets the time after which the cooldown is reset. /// public TimeSpan Reset { get; } /// /// Gets the type of the cooldown bucket. This determines how cooldowns are applied. /// public CooldownBucketType BucketType { get; } /// /// Gets the cooldown buckets for this command. /// private readonly ConcurrentDictionary _buckets; /// /// Defines a cooldown for this command. This means that users will be able to use the command a specific number of times before they have to wait to use it again. /// /// Number of times the command can be used before triggering a cooldown. /// Number of seconds after which the cooldown is reset. /// Type of cooldown bucket. This allows controlling whether the bucket will be cooled down per user, guild, channel, or globally. public CooldownAttribute(int maxUses, double resetAfter, CooldownBucketType bucketType) { this.MaxUses = maxUses; this.Reset = TimeSpan.FromSeconds(resetAfter); this.BucketType = bucketType; this._buckets = new ConcurrentDictionary(); } /// /// Gets a cooldown bucket for given command context. /// /// Command context to get cooldown bucket for. /// Requested cooldown bucket, or null if one wasn't present. public CommandCooldownBucket GetBucket(CommandContext ctx) { var bid = this.GetBucketId(ctx, out _, out _, out _); this._buckets.TryGetValue(bid, out var bucket); return bucket; } /// /// Calculates the cooldown remaining for given command context. /// /// Context for which to calculate the cooldown. /// Remaining cooldown, or zero if no cooldown is active. public TimeSpan GetRemainingCooldown(CommandContext ctx) { var bucket = this.GetBucket(ctx); return bucket == null ? TimeSpan.Zero : bucket.RemainingUses > 0 ? TimeSpan.Zero : bucket.ResetsAt - DateTimeOffset.UtcNow; } /// /// Calculates bucket ID for given command context. /// /// Context for which to calculate bucket ID for. /// ID of the user with which this bucket is associated. /// ID of the channel with which this bucket is associated. /// ID of the guild with which this bucket is associated. /// Calculated bucket ID. private string GetBucketId(CommandContext ctx, out ulong userId, out ulong channelId, out ulong guildId) { userId = 0ul; if ((this.BucketType & CooldownBucketType.User) != 0) userId = ctx.User.Id; channelId = 0ul; if ((this.BucketType & CooldownBucketType.Channel) != 0) channelId = ctx.Channel.Id; if ((this.BucketType & CooldownBucketType.Guild) != 0 && ctx.Guild == null) channelId = ctx.Channel.Id; guildId = 0ul; if (ctx.Guild != null && (this.BucketType & CooldownBucketType.Guild) != 0) guildId = ctx.Guild.Id; var bid = CommandCooldownBucket.MakeId(userId, channelId, guildId); return bid; } + /// + /// Calculates bucket ID for given command context. + /// + /// Context for which to calculate bucket ID for. + /// ID of the user with which this bucket is associated. + /// ID of the channel with which this bucket is associated. + /// ID of the guild with which this bucket is associated. + /// Calculated bucket ID. + private string GetBucketId(HybridCommandContext ctx, out ulong userId, out ulong channelId, out ulong guildId) + { + userId = 0ul; + if ((this.BucketType & CooldownBucketType.User) != 0) + userId = ctx.User.Id; + + channelId = 0ul; + if ((this.BucketType & CooldownBucketType.Channel) != 0) + channelId = ctx.Channel.Id; + if ((this.BucketType & CooldownBucketType.Guild) != 0 && ctx.Guild == null) + channelId = ctx.Channel.Id; + + guildId = 0ul; + if (ctx.Guild != null && (this.BucketType & CooldownBucketType.Guild) != 0) + guildId = ctx.Guild.Id; + + var bid = CommandCooldownBucket.MakeId(userId, channelId, guildId); + return bid; + } + /// /// Executes a check. /// /// The command context. /// If true, help - returns true. public override async Task ExecuteCheckAsync(CommandContext ctx, bool help) { if (help) return true; var bid = this.GetBucketId(ctx, out var usr, out var chn, out var gld); if (!this._buckets.TryGetValue(bid, out var bucket)) { bucket = new CommandCooldownBucket(this.MaxUses, this.Reset, usr, chn, gld); this._buckets.AddOrUpdate(bid, bucket, (k, v) => bucket); } return await bucket.DecrementUseAsync().ConfigureAwait(false); } + + public override async Task ExecuteCheckAsync(HybridCommandContext ctx, bool help) + { + if (help) + return true; + + var bid = this.GetBucketId(ctx, out var usr, out var chn, out var gld); + if (!this._buckets.TryGetValue(bid, out var bucket)) + { + bucket = new CommandCooldownBucket(this.MaxUses, this.Reset, usr, chn, gld); + this._buckets.AddOrUpdate(bid, bucket, (k, v) => bucket); + } + + return await bucket.DecrementUseAsync().ConfigureAwait(false); + } + } /// /// Defines how are command cooldowns applied. /// public enum CooldownBucketType : int { /// /// Denotes that the command will have its cooldown applied per-user. /// User = 1, /// /// Denotes that the command will have its cooldown applied per-channel. /// Channel = 2, /// /// Denotes that the command will have its cooldown applied per-guild. In DMs, this applies the cooldown per-channel. /// Guild = 4, /// /// Denotes that the command will have its cooldown applied globally. /// Global = 0 } /// /// Represents a cooldown bucket for commands. /// public sealed class CommandCooldownBucket : IEquatable { /// /// Gets the ID of the user with whom this cooldown is associated. /// public ulong UserId { get; } /// /// Gets the ID of the channel with which this cooldown is associated. /// public ulong ChannelId { get; } /// /// Gets the ID of the guild with which this cooldown is associated. /// public ulong GuildId { get; } /// /// Gets the ID of the bucket. This is used to distinguish between cooldown buckets. /// public string BucketId { get; } /// /// Gets the remaining number of uses before the cooldown is triggered. /// public int RemainingUses => Volatile.Read(ref this._remainingUses); private int _remainingUses; /// /// Gets the maximum number of times this command can be used in given timespan. /// public int MaxUses { get; } /// /// Gets the date and time at which the cooldown resets. /// public DateTimeOffset ResetsAt { get; internal set; } /// /// Gets the time after which this cooldown resets. /// public TimeSpan Reset { get; internal set; } /// /// Gets the semaphore used to lock the use value. /// private readonly SemaphoreSlim _usageSemaphore; /// /// Creates a new command cooldown bucket. /// /// Maximum number of uses for this bucket. /// Time after which this bucket resets. /// ID of the user with which this cooldown is associated. /// ID of the channel with which this cooldown is associated. /// ID of the guild with which this cooldown is associated. internal CommandCooldownBucket(int maxUses, TimeSpan resetAfter, ulong userId = 0, ulong channelId = 0, ulong guildId = 0) { this._remainingUses = maxUses; this.MaxUses = maxUses; this.ResetsAt = DateTimeOffset.UtcNow + resetAfter; this.Reset = resetAfter; this.UserId = userId; this.ChannelId = channelId; this.GuildId = guildId; this.BucketId = MakeId(userId, channelId, guildId); this._usageSemaphore = new SemaphoreSlim(1, 1); } /// /// Decrements the remaining use counter. /// /// Whether decrement succeeded or not. internal async Task DecrementUseAsync() { await this._usageSemaphore.WaitAsync().ConfigureAwait(false); // if we're past reset time... var now = DateTimeOffset.UtcNow; if (now >= this.ResetsAt) { // ...do the reset and set a new reset time Interlocked.Exchange(ref this._remainingUses, this.MaxUses); this.ResetsAt = now + this.Reset; } // check if we have any uses left, if we do... var success = false; if (this.RemainingUses > 0) { // ...decrement, and return success... Interlocked.Decrement(ref this._remainingUses); success = true; } // ...otherwise just fail this._usageSemaphore.Release(); return success; } /// /// Returns a string representation of this command cooldown bucket. /// /// String representation of this command cooldown bucket. public override string ToString() => $"Command bucket {this.BucketId}"; /// /// Checks whether this is equal to another object. /// /// Object to compare to. /// Whether the object is equal to this . public override bool Equals(object obj) => this.Equals(obj as CommandCooldownBucket); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(CommandCooldownBucket other) => other is not null && (ReferenceEquals(this, other) || (this.UserId == other.UserId && this.ChannelId == other.ChannelId && this.GuildId == other.GuildId)); /// /// Gets the hash code for this . /// /// The hash code for this . public override int GetHashCode() => HashCode.Combine(this.UserId, this.ChannelId, this.GuildId); /// /// Gets whether the two objects are equal. /// /// First bucket to compare. /// Second bucket to compare. /// Whether the two buckets are equal. public static bool operator ==(CommandCooldownBucket bucket1, CommandCooldownBucket bucket2) { var null1 = bucket1 is null; var null2 = bucket2 is null; return (null1 && null2) || (null1 == null2 && null1.Equals(null2)); } /// /// Gets whether the two objects are not equal. /// /// First bucket to compare. /// Second bucket to compare. /// Whether the two buckets are not equal. public static bool operator !=(CommandCooldownBucket bucket1, CommandCooldownBucket bucket2) => !(bucket1 == bucket2); /// /// Creates a bucket ID from given bucket parameters. /// /// ID of the user with which this cooldown is associated. /// ID of the channel with which this cooldown is associated. /// ID of the guild with which this cooldown is associated. /// Generated bucket ID. public static string MakeId(ulong userId = 0, ulong channelId = 0, ulong guildId = 0) => $"{userId.ToString(CultureInfo.InvariantCulture)}:{channelId.ToString(CultureInfo.InvariantCulture)}:{guildId.ToString(CultureInfo.InvariantCulture)}"; } diff --git a/DisCatSharp.CommandsNext/Attributes/RequireBoostingAttribute.cs b/DisCatSharp.CommandsNext/Attributes/RequireBoostingAttribute.cs index a4f50571a..261600c3e 100644 --- a/DisCatSharp.CommandsNext/Attributes/RequireBoostingAttribute.cs +++ b/DisCatSharp.CommandsNext/Attributes/RequireBoostingAttribute.cs @@ -1,83 +1,99 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-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 NONINFRINGEMENT. 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.Threading.Tasks; +using DisCatSharp.HybridCommands.Entities; + namespace DisCatSharp.CommandsNext.Attributes; /// /// Defines that usage of this command is restricted to boosters. /// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false)] public sealed class RequireBoostingAttribute : CheckBaseAttribute { /// /// Gets the required boost time. /// public int Since { get; } /// /// Gets the required guild. /// public ulong GuildId { get; } /// /// Initializes a new instance of the class. /// /// Boosting since days. public RequireBoostingAttribute(int days = 0) { this.GuildId = 0; this.Since = days; } /// /// Initializes a new instance of the class. /// /// Target guild id. /// Boosting since days. public RequireBoostingAttribute(ulong guildId, int days = 0) { this.GuildId = guildId; this.Since = days; } /// /// Executes the a check. /// /// The command context. /// If true, help - returns true. public override async Task ExecuteCheckAsync(CommandContext ctx, bool help) { if (this.GuildId != 0) { var guild = await ctx.Client.GetGuildAsync(this.GuildId); var member = await guild.GetMemberAsync(ctx.User.Id); return member != null && member.PremiumSince.HasValue ? await Task.FromResult(member.PremiumSince.Value.UtcDateTime.Date < DateTime.UtcNow.Date.AddDays(-this.Since)) : await Task.FromResult(false); } else { return ctx.Member != null && ctx.Member.PremiumSince.HasValue ? await Task.FromResult(ctx.Member.PremiumSince.Value.UtcDateTime.Date < DateTime.UtcNow.Date.AddDays(-this.Since)) : await Task.FromResult(false); } } + + public override async Task ExecuteCheckAsync(HybridCommandContext ctx, bool help) + { + if (this.GuildId != 0) + { + var guild = await ctx.Client.GetGuildAsync(this.GuildId); + var member = await guild.GetMemberAsync(ctx.User.Id); + return member != null && member.PremiumSince.HasValue ? await Task.FromResult(member.PremiumSince.Value.UtcDateTime.Date < DateTime.UtcNow.Date.AddDays(-this.Since)) : await Task.FromResult(false); + } + else + { + return ctx.Member != null && ctx.Member.PremiumSince.HasValue ? await Task.FromResult(ctx.Member.PremiumSince.Value.UtcDateTime.Date < DateTime.UtcNow.Date.AddDays(-this.Since)) : await Task.FromResult(false); + } + } } diff --git a/DisCatSharp.CommandsNext/Attributes/RequireBotPermissionsAttribute.cs b/DisCatSharp.CommandsNext/Attributes/RequireBotPermissionsAttribute.cs index 17a13c233..af94551d6 100644 --- a/DisCatSharp.CommandsNext/Attributes/RequireBotPermissionsAttribute.cs +++ b/DisCatSharp.CommandsNext/Attributes/RequireBotPermissionsAttribute.cs @@ -1,83 +1,106 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-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 NONINFRINGEMENT. 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.Threading.Tasks; using DisCatSharp.Enums; +using DisCatSharp.HybridCommands.Entities; namespace DisCatSharp.CommandsNext.Attributes; /// /// Defines that usage of this command is only possible when the bot is granted a specific permission. /// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false)] public sealed class RequireBotPermissionsAttribute : CheckBaseAttribute { /// /// Gets the permissions required by this attribute. /// public Permissions Permissions { get; } /// /// Gets or sets this check's behaviour in DMs. True means the check will always pass in DMs, whereas false means that it will always fail. /// public bool IgnoreDms { get; } = true; /// /// Defines that usage of this command is only possible when the bot is granted a specific permission. /// /// Permissions required to execute this command. /// Sets this check's behaviour in DMs. True means the check will always pass in DMs, whereas false means that it will always fail. public RequireBotPermissionsAttribute(Permissions permissions, bool ignoreDms = true) { this.Permissions = permissions; this.IgnoreDms = ignoreDms; } /// /// Executes the a check. /// /// The command context. /// If true, help - returns true. public override async Task ExecuteCheckAsync(CommandContext ctx, bool help) { if (ctx.Guild == null) return this.IgnoreDms; var bot = await ctx.Guild.GetMemberAsync(ctx.Client.CurrentUser.Id).ConfigureAwait(false); if (bot == null) return false; if (bot.Id == ctx.Guild.OwnerId) return true; var channel = ctx.Channel; if (ctx.Channel.GuildId == null) { channel = await ctx.Client.GetChannelAsync(ctx.Channel.Id, true); } var pbot = channel.PermissionsFor(bot); return (pbot & Permissions.Administrator) != 0 || (pbot & this.Permissions) == this.Permissions; } + + public override async Task ExecuteCheckAsync(HybridCommandContext ctx, bool help) + { + if (ctx.Guild == null) + return this.IgnoreDms; + + var bot = await ctx.Guild.GetMemberAsync(ctx.Client.CurrentUser.Id).ConfigureAwait(false); + if (bot == null) + return false; + + if (bot.Id == ctx.Guild.OwnerId) + return true; + + var channel = ctx.Channel; + if (ctx.Channel.GuildId == null) + { + channel = await ctx.Client.GetChannelAsync(ctx.Channel.Id, true); + } + var pbot = channel.PermissionsFor(bot); + + return (pbot & Permissions.Administrator) != 0 || (pbot & this.Permissions) == this.Permissions; + } } diff --git a/DisCatSharp.CommandsNext/Attributes/RequireCertifiedModeratorAttribute.cs b/DisCatSharp.CommandsNext/Attributes/RequireCertifiedModeratorAttribute.cs index bbeee7cd3..ed21f6450 100644 --- a/DisCatSharp.CommandsNext/Attributes/RequireCertifiedModeratorAttribute.cs +++ b/DisCatSharp.CommandsNext/Attributes/RequireCertifiedModeratorAttribute.cs @@ -1,42 +1,44 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-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 NONINFRINGEMENT. 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.Threading.Tasks; using DisCatSharp.Enums; +using DisCatSharp.HybridCommands.Entities; namespace DisCatSharp.CommandsNext.Attributes; /// /// Defines that usage of this command is restricted to discord certified moderators. /// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false)] public sealed class RequireCertifiedModeratorAttribute : CheckBaseAttribute { /// /// Executes the a check. /// /// The command context. /// If true, help - returns true. public override Task ExecuteCheckAsync(CommandContext ctx, bool help) => ctx.User.Flags.HasValue ? Task.FromResult(ctx.User.Flags.Value.HasFlag(UserFlags.CertifiedModerator)) : Task.FromResult(false); + public override Task ExecuteCheckAsync(HybridCommandContext ctx, bool help) => ctx.User.Flags.HasValue ? Task.FromResult(ctx.User.Flags.Value.HasFlag(UserFlags.CertifiedModerator)) : Task.FromResult(false); } diff --git a/DisCatSharp.CommandsNext/Attributes/RequireCommunityAttribute.cs b/DisCatSharp.CommandsNext/Attributes/RequireCommunityAttribute.cs index 5a0a0f76f..189a60ac0 100644 --- a/DisCatSharp.CommandsNext/Attributes/RequireCommunityAttribute.cs +++ b/DisCatSharp.CommandsNext/Attributes/RequireCommunityAttribute.cs @@ -1,47 +1,50 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-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 NONINFRINGEMENT. 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.Threading.Tasks; +using DisCatSharp.HybridCommands.Entities; + namespace DisCatSharp.CommandsNext.Attributes; /// /// Defines that a command is only usable within a community-enabled guild. /// /// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false)] public sealed class RequireCommunityAttribute : CheckBaseAttribute { /// /// Defines that this command is only usable within a community-enabled guild. /// public RequireCommunityAttribute() { } /// /// Executes the a check. /// /// The command context. /// If true, help - returns true. public override Task ExecuteCheckAsync(CommandContext ctx, bool help) => Task.FromResult(ctx.Guild != null && ctx.Guild.IsCommunity); + public override Task ExecuteCheckAsync(HybridCommandContext ctx, bool help) => Task.FromResult(ctx.Guild != null && ctx.Guild.IsCommunity); } diff --git a/DisCatSharp.CommandsNext/Attributes/RequireDirectMessageAttribute.cs b/DisCatSharp.CommandsNext/Attributes/RequireDirectMessageAttribute.cs index 97c00a2de..1efc42699 100644 --- a/DisCatSharp.CommandsNext/Attributes/RequireDirectMessageAttribute.cs +++ b/DisCatSharp.CommandsNext/Attributes/RequireDirectMessageAttribute.cs @@ -1,50 +1,53 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-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 NONINFRINGEMENT. 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.Threading.Tasks; using DisCatSharp.Entities; +using DisCatSharp.HybridCommands.Entities; namespace DisCatSharp.CommandsNext.Attributes; /// /// Defines that a command is only usable within a direct message channel. /// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false)] public sealed class RequireDirectMessageAttribute : CheckBaseAttribute { /// /// Defines that this command is only usable within a direct message channel. /// public RequireDirectMessageAttribute() { } /// /// Executes the a check. /// /// The command context. /// If true, help - returns true. public override Task ExecuteCheckAsync(CommandContext ctx, bool help) => Task.FromResult(ctx.Channel is DiscordDmChannel); + public override Task ExecuteCheckAsync(HybridCommandContext ctx, bool help) + => Task.FromResult(ctx.Channel is DiscordDmChannel); } diff --git a/DisCatSharp.CommandsNext/Attributes/RequireDisCatSharpDeveloperAttribute.cs b/DisCatSharp.CommandsNext/Attributes/RequireDisCatSharpDeveloperAttribute.cs index a9c31bc99..b6c9f6460 100644 --- a/DisCatSharp.CommandsNext/Attributes/RequireDisCatSharpDeveloperAttribute.cs +++ b/DisCatSharp.CommandsNext/Attributes/RequireDisCatSharpDeveloperAttribute.cs @@ -1,41 +1,45 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-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 NONINFRINGEMENT. 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.Threading.Tasks; +using DisCatSharp.HybridCommands.Entities; + namespace DisCatSharp.CommandsNext.Attributes; /// /// Defines that usage of this command is restricted to DisCatSharp Developers. /// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false)] public sealed class RequireDisCatSharpDeveloperAttribute : CheckBaseAttribute { /// /// Executes the a check. /// /// The command context. /// If true, help - returns true. public override Task ExecuteCheckAsync(CommandContext ctx, bool help) => Task.FromResult(true); + public override Task ExecuteCheckAsync(HybridCommandContext ctx, bool help) + => Task.FromResult(true); } diff --git a/DisCatSharp.CommandsNext/Attributes/RequireGuildAttribute.cs b/DisCatSharp.CommandsNext/Attributes/RequireGuildAttribute.cs index da9146b5c..b592015a6 100644 --- a/DisCatSharp.CommandsNext/Attributes/RequireGuildAttribute.cs +++ b/DisCatSharp.CommandsNext/Attributes/RequireGuildAttribute.cs @@ -1,45 +1,49 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-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 NONINFRINGEMENT. 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.Entities; + namespace DisCatSharp.CommandsNext.Attributes; /// /// Defines that a command is only usable within a guild. /// public sealed class RequireGuildAttribute : CheckBaseAttribute { /// /// Defines that this command is only usable within a guild. /// public RequireGuildAttribute() { } /// /// Executes the a check. /// /// The command context. /// If true, help - returns true. public override Task ExecuteCheckAsync(CommandContext ctx, bool help) => Task.FromResult(ctx.Guild != null); + public override Task ExecuteCheckAsync(HybridCommandContext ctx, bool help) + => Task.FromResult(ctx.Guild != null); } diff --git a/DisCatSharp.CommandsNext/Attributes/RequireGuildOwnerAttribute.cs b/DisCatSharp.CommandsNext/Attributes/RequireGuildOwnerAttribute.cs index 9c7743596..85444941d 100644 --- a/DisCatSharp.CommandsNext/Attributes/RequireGuildOwnerAttribute.cs +++ b/DisCatSharp.CommandsNext/Attributes/RequireGuildOwnerAttribute.cs @@ -1,53 +1,70 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-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 NONINFRINGEMENT. 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.Threading.Tasks; +using DisCatSharp.HybridCommands.Entities; + namespace DisCatSharp.CommandsNext.Attributes; /// /// Defines that usage of this command is restricted to the guild owner. /// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false)] public sealed class RequireGuildOwnerAttribute : CheckBaseAttribute { /// /// Executes the a check. /// /// The command context. /// If true, help - returns true. public override async Task ExecuteCheckAsync(CommandContext ctx, bool help) { var guild = await Task.FromResult(ctx.Guild != null); if (guild) { var owner = await Task.FromResult(ctx.Member == ctx.Guild.Owner); return owner; } else { return false; } } + + public async override Task ExecuteCheckAsync(HybridCommandContext ctx, bool help) + { + var guild = await Task.FromResult(ctx.Guild != null); + if (guild) + { + var owner = await Task.FromResult(ctx.Member == ctx.Guild.Owner); + + return owner; + } + else + { + return false; + } + } } diff --git a/DisCatSharp.CommandsNext/Attributes/RequireMemberVerificationGateAttribute.cs b/DisCatSharp.CommandsNext/Attributes/RequireMemberVerificationGateAttribute.cs index 49c265bf0..158474b42 100644 --- a/DisCatSharp.CommandsNext/Attributes/RequireMemberVerificationGateAttribute.cs +++ b/DisCatSharp.CommandsNext/Attributes/RequireMemberVerificationGateAttribute.cs @@ -1,44 +1,47 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-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 NONINFRINGEMENT. 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.Entities; + namespace DisCatSharp.CommandsNext.Attributes; /// /// Defines that a command is only usable within a guild which has enabled the member verification gate. /// public sealed class RequireMemberVerificationGateAttribute : CheckBaseAttribute { /// /// Defines that this command is only usable within guild which has enabled the member verification gate. /// public RequireMemberVerificationGateAttribute() { } /// /// Executes the a check. /// /// The command context. /// If true, help - returns true. public override Task ExecuteCheckAsync(CommandContext ctx, bool help) => Task.FromResult(ctx.Guild != null && ctx.Guild.HasMemberVerificationGate); + public override Task ExecuteCheckAsync(HybridCommandContext ctx, bool help) => Task.FromResult(ctx.Guild != null && ctx.Guild.HasMemberVerificationGate); } diff --git a/DisCatSharp.CommandsNext/Attributes/RequireNsfwAttribute.cs b/DisCatSharp.CommandsNext/Attributes/RequireNsfwAttribute.cs index 2e5bb224e..8200a881f 100644 --- a/DisCatSharp.CommandsNext/Attributes/RequireNsfwAttribute.cs +++ b/DisCatSharp.CommandsNext/Attributes/RequireNsfwAttribute.cs @@ -1,41 +1,45 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-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 NONINFRINGEMENT. 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.Threading.Tasks; +using DisCatSharp.HybridCommands.Entities; + namespace DisCatSharp.CommandsNext.Attributes; /// /// Defines that usage of this command is restricted to NSFW channels. /// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false)] public sealed class RequireNsfwAttribute : CheckBaseAttribute { /// /// Executes the a check. /// /// The command context. /// If true, help - returns true. public override Task ExecuteCheckAsync(CommandContext ctx, bool help) => Task.FromResult(ctx.Channel.Guild == null || ctx.Channel.IsNsfw); + public override Task ExecuteCheckAsync(HybridCommandContext ctx, bool help) + => Task.FromResult(ctx.Channel.Guild == null || ctx.Channel.IsNsfw); } diff --git a/DisCatSharp.CommandsNext/Attributes/RequireOwnerAttribute.cs b/DisCatSharp.CommandsNext/Attributes/RequireOwnerAttribute.cs index fd6d6231b..be4408c2f 100644 --- a/DisCatSharp.CommandsNext/Attributes/RequireOwnerAttribute.cs +++ b/DisCatSharp.CommandsNext/Attributes/RequireOwnerAttribute.cs @@ -1,47 +1,57 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-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 NONINFRINGEMENT. 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.Linq; using System.Threading.Tasks; +using DisCatSharp.HybridCommands.Entities; + namespace DisCatSharp.CommandsNext.Attributes; /// /// Defines that usage of this command is restricted to the owner of the bot. /// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false)] public sealed class RequireOwnerAttribute : CheckBaseAttribute { /// /// Executes the a check. /// /// The command context. /// If true, help - returns true. public override Task ExecuteCheckAsync(CommandContext ctx, bool help) { var app = ctx.Client.CurrentApplication; var me = ctx.Client.CurrentUser; return app != null ? Task.FromResult(app.Owners.Any(x => x.Id == ctx.User.Id)) : Task.FromResult(ctx.User.Id == me.Id); } + + public override Task ExecuteCheckAsync(HybridCommandContext ctx, bool help) + { + var app = ctx.Client.CurrentApplication; + var me = ctx.Client.CurrentUser; + + return app != null ? Task.FromResult(app.Owners.Any(x => x.Id == ctx.User.Id)) : Task.FromResult(ctx.User.Id == me.Id); + } } diff --git a/DisCatSharp.CommandsNext/Attributes/RequireOwnerOrIdAttribute.cs b/DisCatSharp.CommandsNext/Attributes/RequireOwnerOrIdAttribute.cs index e2b081423..a3120a4be 100644 --- a/DisCatSharp.CommandsNext/Attributes/RequireOwnerOrIdAttribute.cs +++ b/DisCatSharp.CommandsNext/Attributes/RequireOwnerOrIdAttribute.cs @@ -1,68 +1,83 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-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 NONINFRINGEMENT. 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.ObjectModel; using System.Linq; using System.Threading.Tasks; +using DisCatSharp.HybridCommands.Entities; + namespace DisCatSharp.CommandsNext.Attributes; /// /// Requires ownership of the bot or a whitelisted id to execute this command. /// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false)] public sealed class RequireOwnerOrIdAttribute : CheckBaseAttribute { /// /// Allowed user ids /// public IReadOnlyList UserIds { get; } /// /// Defines that usage of this command is restricted to the owner or whitelisted ids of the bot. /// /// List of allowed user ids public RequireOwnerOrIdAttribute(params ulong[] userIds) { this.UserIds = new ReadOnlyCollection(userIds); } /// /// Executes the a check. /// /// The command context. /// If true, help - returns true. public override async Task ExecuteCheckAsync(CommandContext ctx, bool help) { var app = ctx.Client.CurrentApplication; var me = ctx.Client.CurrentUser; var owner = app != null ? await Task.FromResult(app.Owners.Any(x => x.Id == ctx.User.Id)) : await Task.FromResult(ctx.User.Id == me.Id); var allowed = this.UserIds.Contains(ctx.User.Id); return owner || allowed; } + + public override async Task ExecuteCheckAsync(HybridCommandContext ctx, bool help) + { + var app = ctx.Client.CurrentApplication; + var me = ctx.Client.CurrentUser; + + var owner = app != null ? await Task.FromResult(app.Owners.Any(x => x.Id == ctx.User.Id)) : await Task.FromResult(ctx.User.Id == me.Id); + + var allowed = this.UserIds.Contains(ctx.User.Id); + + return owner || allowed; + + } } diff --git a/DisCatSharp.CommandsNext/Attributes/RequirePermissionsAttribute.cs b/DisCatSharp.CommandsNext/Attributes/RequirePermissionsAttribute.cs index e5eac535c..112f297e0 100644 --- a/DisCatSharp.CommandsNext/Attributes/RequirePermissionsAttribute.cs +++ b/DisCatSharp.CommandsNext/Attributes/RequirePermissionsAttribute.cs @@ -1,94 +1,128 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-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 NONINFRINGEMENT. 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.Threading.Tasks; using DisCatSharp.Enums; +using DisCatSharp.HybridCommands.Entities; namespace DisCatSharp.CommandsNext.Attributes; /// /// Defines that usage of this command is restricted to members with specified permissions. This check also verifies that the bot has the same permissions. /// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false)] public sealed class RequirePermissionsAttribute : CheckBaseAttribute { /// /// Gets the permissions required by this attribute. /// public Permissions Permissions { get; } /// /// Gets or sets this check's behaviour in DMs. True means the check will always pass in DMs, whereas false means that it will always fail. /// public bool IgnoreDms { get; } = true; /// /// Defines that usage of this command is restricted to members with specified permissions. This check also verifies that the bot has the same permissions. /// /// Permissions required to execute this command. /// Sets this check's behaviour in DMs. True means the check will always pass in DMs, whereas false means that it will always fail. public RequirePermissionsAttribute(Permissions permissions, bool ignoreDms = true) { this.Permissions = permissions; this.IgnoreDms = ignoreDms; } /// /// Executes the a check. /// /// The command context. /// If true, help - returns true. public override async Task ExecuteCheckAsync(CommandContext ctx, bool help) { if (ctx.Guild == null) return this.IgnoreDms; var channel = ctx.Channel; if (ctx.Channel.GuildId == null) { channel = await ctx.Client.GetChannelAsync(ctx.Channel.Id, true); } var usr = ctx.Member; if (usr == null) return false; var pusr = channel.PermissionsFor(usr); var bot = await ctx.Guild.GetMemberAsync(ctx.Client.CurrentUser.Id).ConfigureAwait(false); if (bot == null) return false; var pbot = channel.PermissionsFor(bot); var usrok = ctx.Guild.OwnerId == usr.Id; var botok = ctx.Guild.OwnerId == bot.Id; if (!usrok) usrok = (pusr & Permissions.Administrator) != 0 || (pusr & this.Permissions) == this.Permissions; if (!botok) botok = (pbot & Permissions.Administrator) != 0 || (pbot & this.Permissions) == this.Permissions; return usrok && botok; } + + public override async Task ExecuteCheckAsync(HybridCommandContext ctx, bool help) + { + if (ctx.Guild == null) + return this.IgnoreDms; + + var channel = ctx.Channel; + if (ctx.Channel.GuildId == null) + { + channel = await ctx.Client.GetChannelAsync(ctx.Channel.Id, true); + } + + var usr = ctx.Member; + if (usr == null) + return false; + var pusr = channel.PermissionsFor(usr); + + var bot = await ctx.Guild.GetMemberAsync(ctx.Client.CurrentUser.Id).ConfigureAwait(false); + if (bot == null) + return false; + var pbot = channel.PermissionsFor(bot); + + var usrok = ctx.Guild.OwnerId == usr.Id; + var botok = ctx.Guild.OwnerId == bot.Id; + + if (!usrok) + usrok = (pusr & Permissions.Administrator) != 0 || (pusr & this.Permissions) == this.Permissions; + + if (!botok) + botok = (pbot & Permissions.Administrator) != 0 || (pbot & this.Permissions) == this.Permissions; + + return usrok && botok; + } } diff --git a/DisCatSharp.CommandsNext/Attributes/RequirePrefixesAttribute.cs b/DisCatSharp.CommandsNext/Attributes/RequirePrefixesAttribute.cs index 30cb8dc33..6ed3a2a3a 100644 --- a/DisCatSharp.CommandsNext/Attributes/RequirePrefixesAttribute.cs +++ b/DisCatSharp.CommandsNext/Attributes/RequirePrefixesAttribute.cs @@ -1,65 +1,69 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-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 NONINFRINGEMENT. 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.Linq; using System.Threading.Tasks; +using DisCatSharp.HybridCommands.Entities; + namespace DisCatSharp.CommandsNext.Attributes; /// /// Defines that usage of this command is only allowed with specific prefixes. /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = false)] public sealed class RequirePrefixesAttribute : CheckBaseAttribute { /// /// Gets the array of prefixes with which execution of this command is allowed. /// public string[] Prefixes { get; } /// /// Gets or sets default help behaviour for this check. When this is enabled, invoking help without matching prefix will show the commands. /// Defaults to false. /// public bool ShowInHelp { get; set; } = false; /// /// Defines that usage of this command is only allowed with specific prefixes. /// /// Prefixes with which the execution of this command is allowed. public RequirePrefixesAttribute(params string[] prefixes) { if (prefixes?.Any() != true) throw new ArgumentNullException(nameof(prefixes), "The allowed prefix collection cannot be null or empty."); this.Prefixes = prefixes; } /// /// Executes the a check. /// /// The command context. /// If true, help - returns true. public override Task ExecuteCheckAsync(CommandContext ctx, bool help) => Task.FromResult((help && this.ShowInHelp) || this.Prefixes.Contains(ctx.Prefix, ctx.CommandsNext.GetStringComparer())); + public override Task ExecuteCheckAsync(HybridCommandContext ctx, bool help) + => Task.FromResult((help && this.ShowInHelp) || this.Prefixes.Contains(ctx.Prefix, ctx.Client.GetCommandsNext().GetStringComparer())); } diff --git a/DisCatSharp.CommandsNext/Attributes/RequireReferencedMessageAttribute.cs b/DisCatSharp.CommandsNext/Attributes/RequireReferencedMessageAttribute.cs index 15eb210a7..5f079f4fc 100644 --- a/DisCatSharp.CommandsNext/Attributes/RequireReferencedMessageAttribute.cs +++ b/DisCatSharp.CommandsNext/Attributes/RequireReferencedMessageAttribute.cs @@ -1,40 +1,44 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-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 NONINFRINGEMENT. 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.Entities; + namespace DisCatSharp.CommandsNext.Attributes; /// /// Defines that a command is only usable when sent in reply. Command will appear in help regardless of this attribute. /// public sealed class RequireReferencedMessageAttribute : CheckBaseAttribute { /// /// Defines that a command is only usable when sent in reply. Command will appear in help regardless of this attribute. /// public RequireReferencedMessageAttribute() { } public override Task ExecuteCheckAsync(CommandContext ctx, bool help) => Task.FromResult(help || ctx.Message.ReferencedMessage != null); + public override Task ExecuteCheckAsync(HybridCommandContext ctx, bool help) + => Task.FromResult(help || ctx.Message.ReferencedMessage != null); } diff --git a/DisCatSharp.CommandsNext/Attributes/RequireRolesAttribute.cs b/DisCatSharp.CommandsNext/Attributes/RequireRolesAttribute.cs index 704ac3655..dfcc870bd 100644 --- a/DisCatSharp.CommandsNext/Attributes/RequireRolesAttribute.cs +++ b/DisCatSharp.CommandsNext/Attributes/RequireRolesAttribute.cs @@ -1,107 +1,128 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-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 NONINFRINGEMENT. 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.ObjectModel; using System.Linq; using System.Threading.Tasks; +using DisCatSharp.HybridCommands.Entities; + namespace DisCatSharp.CommandsNext.Attributes; /// /// Defines that usage of this command is restricted to members with specified role. Note that it's much preferred to restrict access using . /// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false)] public sealed class RequireRolesAttribute : CheckBaseAttribute { /// /// Gets the name of the role required to execute this command. /// public IReadOnlyList RoleNames { get; } /// /// Gets the role checking mode. Refer to for more information. /// public RoleCheckMode CheckMode { get; } /// /// Defines that usage of this command is restricted to members with specified role. Note that it's much preferred to restrict access using . /// /// Role checking mode. /// Names of the role to be verified by this check. public RequireRolesAttribute(RoleCheckMode checkMode, params string[] roleNames) { this.CheckMode = checkMode; this.RoleNames = new ReadOnlyCollection(roleNames); } /// /// Executes the a check. /// /// The command context. /// If true, help - returns true. public override Task ExecuteCheckAsync(CommandContext ctx, bool help) { if (ctx.Guild == null || ctx.Member == null) return Task.FromResult(false); var rns = ctx.Member.Roles.Select(xr => xr.Name); var rnc = rns.Count(); var ins = rns.Intersect(this.RoleNames, ctx.CommandsNext.GetStringComparer()); var inc = ins.Count(); return this.CheckMode switch { RoleCheckMode.All => Task.FromResult(this.RoleNames.Count == inc), RoleCheckMode.SpecifiedOnly => Task.FromResult(rnc == inc), RoleCheckMode.None => Task.FromResult(inc == 0), _ => Task.FromResult(inc > 0), }; } + + public override Task ExecuteCheckAsync(HybridCommandContext ctx, bool help) + { + if (ctx.Guild == null || ctx.Member == null) + return Task.FromResult(false); + + var rns = ctx.Member.Roles.Select(xr => xr.Name); + var rnc = rns.Count(); + var ins = rns.Intersect(this.RoleNames, ctx.Client.GetCommandsNext().GetStringComparer()); + var inc = ins.Count(); + + return this.CheckMode switch + { + RoleCheckMode.All => Task.FromResult(this.RoleNames.Count == inc), + RoleCheckMode.SpecifiedOnly => Task.FromResult(rnc == inc), + RoleCheckMode.None => Task.FromResult(inc == 0), + _ => Task.FromResult(inc > 0), + }; + } } /// /// Specifies how does check for roles. /// public enum RoleCheckMode { /// /// Member is required to have any of the specified roles. /// Any, /// /// Member is required to have all of the specified roles. /// All, /// /// Member is required to have exactly the same roles as specified; no extra roles may be present. /// SpecifiedOnly, /// /// Member is required to have none of the specified roles. /// None } diff --git a/DisCatSharp.CommandsNext/Attributes/RequireStaffAttribute.cs b/DisCatSharp.CommandsNext/Attributes/RequireStaffAttribute.cs index 9f1994d4f..fdd555ed0 100644 --- a/DisCatSharp.CommandsNext/Attributes/RequireStaffAttribute.cs +++ b/DisCatSharp.CommandsNext/Attributes/RequireStaffAttribute.cs @@ -1,42 +1,44 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-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 NONINFRINGEMENT. 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.Threading.Tasks; using DisCatSharp.Enums; +using DisCatSharp.HybridCommands.Entities; namespace DisCatSharp.CommandsNext.Attributes; /// /// Defines that usage of this command is restricted to discord employees. /// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false)] public sealed class RequireStaffAttribute : CheckBaseAttribute { /// /// Executes the a check. /// /// The command context. /// If true, help - returns true. public override Task ExecuteCheckAsync(CommandContext ctx, bool help) => ctx.User.Flags.HasValue ? Task.FromResult(ctx.User.Flags.Value.HasFlag(UserFlags.Staff)) : Task.FromResult(false); + public override Task ExecuteCheckAsync(HybridCommandContext ctx, bool help) => ctx.User.Flags.HasValue ? Task.FromResult(ctx.User.Flags.Value.HasFlag(UserFlags.Staff)) : Task.FromResult(false); } diff --git a/DisCatSharp.CommandsNext/Attributes/RequireUserPermissionsAttribute.cs b/DisCatSharp.CommandsNext/Attributes/RequireUserPermissionsAttribute.cs index 442540ce0..e8ca68a56 100644 --- a/DisCatSharp.CommandsNext/Attributes/RequireUserPermissionsAttribute.cs +++ b/DisCatSharp.CommandsNext/Attributes/RequireUserPermissionsAttribute.cs @@ -1,82 +1,103 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-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 NONINFRINGEMENT. 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.Threading.Tasks; using DisCatSharp.Enums; +using DisCatSharp.HybridCommands.Entities; namespace DisCatSharp.CommandsNext.Attributes; /// /// Defines that usage of this command is restricted to members with specified permissions. /// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false)] public sealed class RequireUserPermissionsAttribute : CheckBaseAttribute { /// /// Gets the permissions required by this attribute. /// public Permissions Permissions { get; } /// /// Gets or sets this check's behaviour in DMs. True means the check will always pass in DMs, whereas false means that it will always fail. /// public bool IgnoreDms { get; } = true; /// /// Defines that usage of this command is restricted to members with specified permissions. /// /// Permissions required to execute this command. /// Sets this check's behaviour in DMs. True means the check will always pass in DMs, whereas false means that it will always fail. public RequireUserPermissionsAttribute(Permissions permissions, bool ignoreDms = true) { this.Permissions = permissions; this.IgnoreDms = ignoreDms; } /// /// Executes the a check. /// /// The command context. /// If true, help - returns true. public override Task ExecuteCheckAsync(CommandContext ctx, bool help) { if (ctx.Guild == null) return Task.FromResult(this.IgnoreDms); var usr = ctx.Member; if (usr == null) return Task.FromResult(false); if (usr.Id == ctx.Guild.OwnerId) return Task.FromResult(true); var pusr = ctx.Channel.PermissionsFor(usr); if ((pusr & Permissions.Administrator) != 0) return Task.FromResult(true); return (pusr & this.Permissions) == this.Permissions ? Task.FromResult(true) : Task.FromResult(false); } + + public override Task ExecuteCheckAsync(HybridCommandContext ctx, bool help) + { + if (ctx.Guild == null) + return Task.FromResult(this.IgnoreDms); + + var usr = ctx.Member; + if (usr == null) + return Task.FromResult(false); + + if (usr.Id == ctx.Guild.OwnerId) + return Task.FromResult(true); + + var pusr = ctx.Channel.PermissionsFor(usr); + + if ((pusr & Permissions.Administrator) != 0) + return Task.FromResult(true); + + return (pusr & this.Permissions) == this.Permissions ? Task.FromResult(true) : Task.FromResult(false); + } } diff --git a/DisCatSharp.CommandsNext/Attributes/RequireWelcomeScreenAttribute.cs b/DisCatSharp.CommandsNext/Attributes/RequireWelcomeScreenAttribute.cs index ac3ceebfd..90ea4634e 100644 --- a/DisCatSharp.CommandsNext/Attributes/RequireWelcomeScreenAttribute.cs +++ b/DisCatSharp.CommandsNext/Attributes/RequireWelcomeScreenAttribute.cs @@ -1,46 +1,49 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-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 NONINFRINGEMENT. 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.Threading.Tasks; +using DisCatSharp.HybridCommands.Entities; + namespace DisCatSharp.CommandsNext.Attributes; /// /// Defines that a command is only usable within a guild which has enabled the welcome screen. /// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false)] public sealed class RequireWelcomeScreenAttribute : CheckBaseAttribute { /// /// Defines that this command is only usable within a guild which has enabled the welcome screen. /// public RequireWelcomeScreenAttribute() { } /// /// Executes a check. /// /// The command context. /// If true, help - returns true. public override Task ExecuteCheckAsync(CommandContext ctx, bool help) => Task.FromResult(ctx.Guild != null && ctx.Guild.HasWelcomeScreen); + public override Task ExecuteCheckAsync(HybridCommandContext ctx, bool help) => Task.FromResult(ctx.Guild != null && ctx.Guild.HasWelcomeScreen); } diff --git a/DisCatSharp.CommandsNext/BaseCommandModule.cs b/DisCatSharp.CommandsNext/BaseCommandModule.cs index 6465ce13b..f56475f95 100644 --- a/DisCatSharp.CommandsNext/BaseCommandModule.cs +++ b/DisCatSharp.CommandsNext/BaseCommandModule.cs @@ -1,47 +1,65 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-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 NONINFRINGEMENT. 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.Entities; + namespace DisCatSharp.CommandsNext; /// /// Represents a base class for all command modules. /// public abstract class BaseCommandModule { /// /// Called before a command in the implementing module is executed. /// /// Context in which the method is being executed. /// public virtual Task BeforeExecutionAsync(CommandContext ctx) => Task.Delay(0); /// /// Called after a command in the implementing module is successfully executed. /// /// Context in which the method is being executed. /// public virtual Task AfterExecutionAsync(CommandContext ctx) => Task.Delay(0); + + /// + /// Called before a command in the implementing module is executed. + /// + /// Context in which the method is being executed. + /// + public virtual Task BeforeExecutionAsync(HybridCommandContext ctx) + => Task.Delay(0); + + /// + /// Called after a command in the implementing module is successfully executed. + /// + /// Context in which the method is being executed. + /// + public virtual Task AfterExecutionAsync(HybridCommandContext ctx) + => Task.Delay(0); } diff --git a/DisCatSharp.CommandsNext/CommandsNextExtension.cs b/DisCatSharp.CommandsNext/CommandsNextExtension.cs index 9a03591a6..246a82e79 100644 --- a/DisCatSharp.CommandsNext/CommandsNextExtension.cs +++ b/DisCatSharp.CommandsNext/CommandsNextExtension.cs @@ -1,1086 +1,1213 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-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 NONINFRINGEMENT. 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.ObjectModel; using System.Linq; using System.Reflection; using System.Threading.Tasks; using DisCatSharp.CommandsNext.Attributes; using DisCatSharp.CommandsNext.Builders; using DisCatSharp.CommandsNext.Converters; using DisCatSharp.CommandsNext.Entities; using DisCatSharp.CommandsNext.Exceptions; using DisCatSharp.Common.Utilities; using DisCatSharp.Entities; using DisCatSharp.EventArgs; +using DisCatSharp.HybridCommands.Entities; +using DisCatSharp.HybridCommands.Enums; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace DisCatSharp.CommandsNext; /// /// This is the class which handles command registration, management, and execution. /// public class CommandsNextExtension : BaseExtension { /// /// Gets the config. /// - private readonly CommandsNextConfiguration _config; + internal readonly CommandsNextConfiguration _config; /// /// Gets the help formatter. /// private readonly HelpFormatterFactory _helpFormatter; /// /// Gets the convert generic. /// private readonly MethodInfo _convertGeneric; /// /// Gets the user friendly type names. /// private readonly Dictionary _userFriendlyTypeNames; /// /// Gets the argument converters. /// internal Dictionary ArgumentConverters { get; } /// /// Gets the service provider this CommandsNext module was configured with. /// public IServiceProvider Services => this._config.ServiceProvider; /// /// Initializes a new instance of the class. /// /// The cfg. internal CommandsNextExtension(CommandsNextConfiguration cfg) { this._config = new CommandsNextConfiguration(cfg); this._topLevelCommands = new Dictionary(); this._registeredCommandsLazy = new Lazy>(() => new ReadOnlyDictionary(this._topLevelCommands)); this._helpFormatter = new HelpFormatterFactory(); this._helpFormatter.SetFormatterType(); this.ArgumentConverters = new Dictionary { [typeof(string)] = new StringConverter(), [typeof(bool)] = new BoolConverter(), [typeof(sbyte)] = new Int8Converter(), [typeof(byte)] = new Uint8Converter(), [typeof(short)] = new Int16Converter(), [typeof(ushort)] = new Uint16Converter(), [typeof(int)] = new Int32Converter(), [typeof(uint)] = new Uint32Converter(), [typeof(long)] = new Int64Converter(), [typeof(ulong)] = new Uint64Converter(), [typeof(float)] = new Float32Converter(), [typeof(double)] = new Float64Converter(), [typeof(decimal)] = new Float128Converter(), [typeof(DateTime)] = new DateTimeConverter(), [typeof(DateTimeOffset)] = new DateTimeOffsetConverter(), [typeof(TimeSpan)] = new TimeSpanConverter(), [typeof(Uri)] = new UriConverter(), [typeof(DiscordUser)] = new DiscordUserConverter(), [typeof(DiscordMember)] = new DiscordMemberConverter(), [typeof(DiscordRole)] = new DiscordRoleConverter(), [typeof(DiscordChannel)] = new DiscordChannelConverter(), [typeof(DiscordGuild)] = new DiscordGuildConverter(), [typeof(DiscordMessage)] = new DiscordMessageConverter(), [typeof(DiscordEmoji)] = new DiscordEmojiConverter(), [typeof(DiscordThreadChannel)] = new DiscordThreadChannelConverter(), [typeof(DiscordInvite)] = new DiscordInviteConverter(), [typeof(DiscordColor)] = new DiscordColorConverter(), [typeof(DiscordScheduledEvent)] = new DiscordScheduledEventConverter(), }; this._userFriendlyTypeNames = new Dictionary() { [typeof(string)] = "string", [typeof(bool)] = "boolean", [typeof(sbyte)] = "signed byte", [typeof(byte)] = "byte", [typeof(short)] = "short", [typeof(ushort)] = "unsigned short", [typeof(int)] = "int", [typeof(uint)] = "unsigned int", [typeof(long)] = "long", [typeof(ulong)] = "unsigned long", [typeof(float)] = "float", [typeof(double)] = "double", [typeof(decimal)] = "decimal", [typeof(DateTime)] = "date and time", [typeof(DateTimeOffset)] = "date and time", [typeof(TimeSpan)] = "time span", [typeof(Uri)] = "URL", [typeof(DiscordUser)] = "user", [typeof(DiscordMember)] = "member", [typeof(DiscordRole)] = "role", [typeof(DiscordChannel)] = "channel", [typeof(DiscordGuild)] = "guild", [typeof(DiscordMessage)] = "message", [typeof(DiscordEmoji)] = "emoji", [typeof(DiscordThreadChannel)] = "thread", [typeof(DiscordInvite)] = "invite", [typeof(DiscordColor)] = "color", [typeof(DiscordScheduledEvent)] = "event" }; foreach (var xt in this.ArgumentConverters.Keys.ToArray()) { var xti = xt.GetTypeInfo(); if (!xti.IsValueType) continue; var xcvt = typeof(NullableConverter<>).MakeGenericType(xt); var xnt = typeof(Nullable<>).MakeGenericType(xt); if (this.ArgumentConverters.ContainsKey(xcvt)) continue; var xcv = Activator.CreateInstance(xcvt) as IArgumentConverter; this.ArgumentConverters[xnt] = xcv; this._userFriendlyTypeNames[xnt] = this._userFriendlyTypeNames[xt]; } var t = this.GetType(); var ms = t.GetTypeInfo().DeclaredMethods; var m = ms.FirstOrDefault(xm => xm.Name == "ConvertArgumentToObj" && xm.ContainsGenericParameters && !xm.IsStatic && xm.IsPrivate); this._convertGeneric = m; } /// /// Sets the help formatter to use with the default help command. /// /// Type of the formatter to use. public void SetHelpFormatter() where T : BaseHelpFormatter => this._helpFormatter.SetFormatterType(); #region DiscordClient Registration /// /// DO NOT USE THIS MANUALLY. /// /// DO NOT USE THIS MANUALLY. /// protected internal override void Setup(DiscordClient client) { if (this.Client != null) throw new InvalidOperationException("What did I tell you?"); this.Client = client; this._executed = new AsyncEvent("COMMAND_EXECUTED", TimeSpan.Zero, this.Client.EventErrorHandler); this._error = new AsyncEvent("COMMAND_ERRORED", TimeSpan.Zero, this.Client.EventErrorHandler); if (this._config.UseDefaultCommandHandler) this.Client.MessageCreated += this.HandleCommandsAsync; else this.Client.Logger.LogWarning(CommandsNextEvents.Misc, "Not attaching default command handler - if this is intentional, you can ignore this message"); if (this._config.EnableDefaultHelp) { this.RegisterCommands(typeof(DefaultHelpModule), null, null, out var tcmds); if (this._config.DefaultHelpChecks != null) { var checks = this._config.DefaultHelpChecks.ToArray(); foreach (var cb in tcmds) cb.WithExecutionChecks(checks); } if (tcmds != null) foreach (var xc in tcmds) this.AddToCommandDictionary(xc.Build(null)); } } #endregion #region Command Handling /// /// Handles the commands async. /// /// The sender. /// The e. /// A Task. private async Task HandleCommandsAsync(DiscordClient sender, MessageCreateEventArgs e) { if (e.Author.IsBot) // bad bot return; if (!this._config.EnableDms && e.Channel.IsPrivate) return; var mpos = -1; if (this._config.EnableMentionPrefix) mpos = e.Message.GetMentionPrefixLength(this.Client.CurrentUser); if (this._config.StringPrefixes?.Any() == true) foreach (var pfix in this._config.StringPrefixes) if (mpos == -1 && !string.IsNullOrWhiteSpace(pfix)) mpos = e.Message.GetStringPrefixLength(pfix, this._config.CaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase); if (mpos == -1 && this._config.PrefixResolver != null) mpos = await this._config.PrefixResolver(e.Message).ConfigureAwait(false); if (mpos == -1) return; var pfx = e.Message.Content[..mpos]; var cnt = e.Message.Content[mpos..]; var __ = 0; var fname = cnt.ExtractNextArgument(ref __); var cmd = this.FindCommand(cnt, out var args); - var ctx = this.CreateContext(e.Message, pfx, cmd, args); - if (cmd == null) + if (cmd?.IsHybrid ?? false) { - await this._error.InvokeAsync(this, new CommandErrorEventArgs(this.Client.ServiceProvider) { Context = ctx, Exception = new CommandNotFoundException(fname) }).ConfigureAwait(false); - return; + var ctx = this.CreateHybridContext(e.Message, pfx, cmd); + if (cmd == null) + { + return; + } + + _ = Task.Run(async () => await this.ExecuteCommandAsync(ctx, cmd, args).ConfigureAwait(false)); } + else + { + var ctx = this.CreateCnextContext(e.Message, pfx, cmd, args); + if (cmd == null) + { + await this._error.InvokeAsync(this, new CommandErrorEventArgs(this.Client.ServiceProvider) { Context = ctx, Exception = new CommandNotFoundException(fname) }).ConfigureAwait(false); + return; + } - _ = Task.Run(async () => await this.ExecuteCommandAsync(ctx).ConfigureAwait(false)); + _ = Task.Run(async () => await this.ExecuteCommandAsync(ctx).ConfigureAwait(false)); + } } /// /// Finds a specified command by its qualified name, then separates arguments. /// /// Qualified name of the command, optionally with arguments. /// Separated arguments. /// Found command or null if none was found. public Command FindCommand(string commandString, out string rawArguments) { rawArguments = null; var ignoreCase = !this._config.CaseSensitive; var pos = 0; var next = commandString.ExtractNextArgument(ref pos); if (next == null) return null; if (!this.RegisteredCommands.TryGetValue(next, out var cmd)) { if (!ignoreCase) return null; next = next.ToLowerInvariant(); var cmdKvp = this.RegisteredCommands.FirstOrDefault(x => x.Key.ToLowerInvariant() == next); if (cmdKvp.Value == null) return null; cmd = cmdKvp.Value; } if (cmd is not CommandGroup) { rawArguments = commandString[pos..].Trim(); return cmd; } while (cmd is CommandGroup) { var cm2 = cmd as CommandGroup; var oldPos = pos; next = commandString.ExtractNextArgument(ref pos); if (next == null) break; if (ignoreCase) { next = next.ToLowerInvariant(); cmd = cm2.Children.FirstOrDefault(x => x.Name.ToLowerInvariant() == next || x.Aliases?.Any(xx => xx.ToLowerInvariant() == next) == true); } else { cmd = cm2.Children.FirstOrDefault(x => x.Name == next || x.Aliases?.Contains(next) == true); } if (cmd == null) { cmd = cm2; pos = oldPos; break; } } rawArguments = commandString[pos..].Trim(); return cmd; } /// /// Creates a command execution context from specified arguments. /// /// Message to use for context. /// Command prefix, used to execute commands. /// Command to execute. /// Raw arguments to pass to command. /// Created command execution context. - public CommandContext CreateContext(DiscordMessage msg, string prefix, Command cmd, string rawArguments = null) + public CommandContext CreateCnextContext(DiscordMessage msg, string prefix, Command cmd, string rawArguments = null) { var ctx = new CommandContext { Client = this.Client, Command = cmd, Message = msg, Config = this._config, RawArgumentString = rawArguments ?? "", Prefix = prefix, CommandsNext = this, Services = this.Services }; if (cmd != null && (cmd.Module is TransientCommandModule || cmd.Module == null)) { var scope = ctx.Services.CreateScope(); ctx.ServiceScopeContext = new CommandContext.ServiceContext(ctx.Services, scope); ctx.Services = scope.ServiceProvider; } return ctx; } + /// + /// Creates a command execution context from specified arguments. + /// + /// Message to use for context. + /// Command prefix, used to execute commands. + /// Command to execute. + /// Raw arguments to pass to command. + /// Created command execution context. + public HybridCommandContext CreateHybridContext(DiscordMessage msg, string prefix, Command cmd) + { + var ctx = new HybridCommandContext + { + CommandType = HybridCommandType.Prefix, + Client = this.Client, + CommandName = cmd.Name, + Message = msg, + Prefix = prefix, + Services = this.Services + }; + + if (cmd != null && (cmd.Module is TransientCommandModule || cmd.Module == null)) + { + var scope = ctx.Services.CreateScope(); + ctx.Services = scope.ServiceProvider; + } + + return ctx; + } + /// /// Executes specified command from given context. /// /// Context to execute command from. /// public async Task ExecuteCommandAsync(CommandContext ctx) { try { var cmd = ctx.Command; await this.RunAllChecksAsync(cmd, ctx).ConfigureAwait(false); var res = await cmd.ExecuteAsync(ctx).ConfigureAwait(false); if (res.IsSuccessful) - await this._executed.InvokeAsync(this, new CommandExecutionEventArgs(this.Client.ServiceProvider) { Context = res.Context }).ConfigureAwait(false); + await this._executed.InvokeAsync(this, new CommandExecutionEventArgs(this.Client.ServiceProvider) { Context = res.CommandContext }).ConfigureAwait(false); else - await this._error.InvokeAsync(this, new CommandErrorEventArgs(this.Client.ServiceProvider) { Context = res.Context, Exception = res.Exception }).ConfigureAwait(false); + await this._error.InvokeAsync(this, new CommandErrorEventArgs(this.Client.ServiceProvider) { Context = res.CommandContext, Exception = res.Exception }).ConfigureAwait(false); } catch (Exception ex) { await this._error.InvokeAsync(this, new CommandErrorEventArgs(this.Client.ServiceProvider) { Context = ctx, Exception = ex }).ConfigureAwait(false); } finally { if (ctx.ServiceScopeContext.IsInitialized) ctx.ServiceScopeContext.Dispose(); } } + /// + /// Executes specified command from given context. + /// + /// Context to execute command from. + /// + public async Task ExecuteCommandAsync(HybridCommandContext ctx, Command cmd, string rawArgs) + { + try + { + await this.RunAllChecksAsync(cmd, ctx).ConfigureAwait(false); + + var res = await cmd.ExecuteAsync(ctx, cmd, rawArgs).ConfigureAwait(false); + + // todo: handle commands stuff + + //if (res.IsSuccessful) + // await this._executed.InvokeAsync(this, new CommandExecutionEventArgs(this.Client.ServiceProvider) { Context = res.HybridContext }).ConfigureAwait(false); + //else + // await this._error.InvokeAsync(this, new CommandErrorEventArgs(this.Client.ServiceProvider) { Context = res.HybridContext, Exception = res.Exception }).ConfigureAwait(false); + } + catch (Exception) + { + // todo: handle exceptions + } + } + /// /// Runs the all checks async. /// /// The cmd. /// The ctx. /// A Task. private async Task RunAllChecksAsync(Command cmd, CommandContext ctx) { if (cmd.Parent != null) await this.RunAllChecksAsync(cmd.Parent, ctx).ConfigureAwait(false); var fchecks = await cmd.RunChecksAsync(ctx, false).ConfigureAwait(false); if (fchecks.Any()) throw new ChecksFailedException(cmd, ctx, fchecks); } + + /// + /// Runs the all checks async. + /// + /// The cmd. + /// The ctx. + /// A Task. + private async Task RunAllChecksAsync(Command cmd, HybridCommandContext ctx) + { + if (cmd.Parent != null) + await this.RunAllChecksAsync(cmd.Parent, ctx).ConfigureAwait(false); + + var fchecks = await cmd.RunChecksAsync(ctx, false).ConfigureAwait(false); + if (fchecks.Any()) + throw new ChecksFailedException(cmd, ctx, fchecks); + } #endregion #region Command Registration /// /// Gets a dictionary of registered top-level commands. /// public IReadOnlyDictionary RegisteredCommands => this._registeredCommandsLazy.Value; /// /// Gets or sets the top level commands. /// private readonly Dictionary _topLevelCommands; private readonly Lazy> _registeredCommandsLazy; /// /// Registers all commands from a given assembly. The command classes need to be public to be considered for registration. /// /// Assembly to register commands from. public void RegisterCommands(Assembly assembly) { var types = assembly.ExportedTypes.Where(xt => { var xti = xt.GetTypeInfo(); return xti.IsModuleCandidateType() && !xti.IsNested; }); foreach (var xt in types) this.RegisterCommands(xt); } /// /// Registers all commands from a given command class. /// /// Class which holds commands to register. public void RegisterCommands() where T : BaseCommandModule { var t = typeof(T); this.RegisterCommands(t); } /// /// Registers all commands from a given command class. /// /// Type of the class which holds commands to register. public void RegisterCommands(Type t) { if (t == null) throw new ArgumentNullException(nameof(t), "Type cannot be null."); if (!t.IsModuleCandidateType()) throw new ArgumentNullException(nameof(t), "Type must be a class, which cannot be abstract or static."); this.RegisterCommands(t, null, null, out var tempCommands); if (tempCommands != null) foreach (var command in tempCommands) this.AddToCommandDictionary(command.Build(null)); } /// /// Registers the commands. /// /// The type. /// The current parent. /// The inherited checks. /// The found commands. private void RegisterCommands(Type t, CommandGroupBuilder currentParent, IEnumerable inheritedChecks, out List foundCommands) { var ti = t.GetTypeInfo(); var lifespan = ti.GetCustomAttribute(); var moduleLifespan = lifespan != null ? lifespan.Lifespan : ModuleLifespan.Singleton; var module = new CommandModuleBuilder() .WithType(t) .WithLifespan(moduleLifespan) .Build(this.Services); // restrict parent lifespan to more or equally restrictive if (currentParent?.Module is TransientCommandModule && moduleLifespan != ModuleLifespan.Transient) throw new InvalidOperationException("In a transient module, child modules can only be transient."); // check if we are anything var groupBuilder = new CommandGroupBuilder(module); var isModule = false; var moduleAttributes = ti.GetCustomAttributes(); var moduleHidden = false; var moduleChecks = new List(); foreach (var xa in moduleAttributes) { switch (xa) { case GroupAttribute g: isModule = true; var moduleName = g.Name; if (moduleName == null) { moduleName = ti.Name; if (moduleName.EndsWith("Group") && moduleName != "Group") moduleName = moduleName[0..^5]; else if (moduleName.EndsWith("Module") && moduleName != "Module") moduleName = moduleName[0..^6]; else if (moduleName.EndsWith("Commands") && moduleName != "Commands") moduleName = moduleName[0..^8]; } if (!this._config.CaseSensitive) moduleName = moduleName.ToLowerInvariant(); groupBuilder.WithName(moduleName); if (inheritedChecks != null) foreach (var chk in inheritedChecks) groupBuilder.WithExecutionCheck(chk); - foreach (var mi in ti.DeclaredMethods.Where(x => x.IsCommandCandidate(out _) && x.GetCustomAttribute() != null)) + foreach (var mi in ti.DeclaredMethods.Where(x => x.IsCommandCandidate(out _, out _) && x.GetCustomAttribute() != null)) groupBuilder.WithOverload(new CommandOverloadBuilder(mi)); break; case AliasesAttribute a: foreach (var xalias in a.Aliases) groupBuilder.WithAlias(this._config.CaseSensitive ? xalias : xalias.ToLowerInvariant()); break; case HiddenAttribute h: groupBuilder.WithHiddenStatus(true); moduleHidden = true; break; case DescriptionAttribute d: groupBuilder.WithDescription(d.Description); break; case CheckBaseAttribute c: moduleChecks.Add(c); groupBuilder.WithExecutionCheck(c); break; default: groupBuilder.WithCustomAttribute(xa); break; } } if (!isModule) { groupBuilder = null; if (inheritedChecks != null) moduleChecks.AddRange(inheritedChecks); } // candidate methods var methods = ti.DeclaredMethods; var commands = new List(); var commandBuilders = new Dictionary(); foreach (var m in methods) { - if (!m.IsCommandCandidate(out _)) + if (!m.IsCommandCandidate(out _, out var isHybrid)) continue; var attrs = m.GetCustomAttributes(); if (attrs.FirstOrDefault(xa => xa is CommandAttribute) is not CommandAttribute cattr) continue; var commandName = cattr.Name; if (commandName == null) { commandName = m.Name; if (commandName.EndsWith("Async") && commandName != "Async") commandName = commandName[0..^5]; } if (!this._config.CaseSensitive) commandName = commandName.ToLowerInvariant(); if (!commandBuilders.TryGetValue(commandName, out var commandBuilder)) { - commandBuilders.Add(commandName, commandBuilder = new CommandBuilder(module).WithName(commandName)); + commandBuilders.Add(commandName, commandBuilder = new CommandBuilder(module).WithName(commandName).AsHybrid(isHybrid)); if (!isModule) if (currentParent != null) currentParent.WithChild(commandBuilder); else commands.Add(commandBuilder); else groupBuilder.WithChild(commandBuilder); } + commandBuilder.IsHybrid = isHybrid; commandBuilder.WithOverload(new CommandOverloadBuilder(m)); if (!isModule && moduleChecks.Any()) foreach (var chk in moduleChecks) commandBuilder.WithExecutionCheck(chk); foreach (var xa in attrs) { switch (xa) { case AliasesAttribute a: foreach (var xalias in a.Aliases) commandBuilder.WithAlias(this._config.CaseSensitive ? xalias : xalias.ToLowerInvariant()); break; case CheckBaseAttribute p: commandBuilder.WithExecutionCheck(p); break; case DescriptionAttribute d: commandBuilder.WithDescription(d.Description); break; case HiddenAttribute h: commandBuilder.WithHiddenStatus(true); break; default: commandBuilder.WithCustomAttribute(xa); break; } } if (!isModule && moduleHidden) commandBuilder.WithHiddenStatus(true); } // candidate types var types = ti.DeclaredNestedTypes .Where(xt => xt.IsModuleCandidateType() && xt.DeclaredConstructors.Any(xc => xc.IsPublic)); foreach (var type in types) { this.RegisterCommands(type.AsType(), groupBuilder, !isModule ? moduleChecks : null, out var tempCommands); if (isModule) foreach (var chk in moduleChecks) groupBuilder.WithExecutionCheck(chk); if (isModule && tempCommands != null) foreach (var xtcmd in tempCommands) groupBuilder.WithChild(xtcmd); else if (tempCommands != null) commands.AddRange(tempCommands); } if (isModule && currentParent == null) commands.Add(groupBuilder); else if (isModule) currentParent.WithChild(groupBuilder); foundCommands = commands; } /// /// Builds and registers all supplied commands. /// /// Commands to build and register. public void RegisterCommands(params CommandBuilder[] cmds) { foreach (var cmd in cmds) this.AddToCommandDictionary(cmd.Build(null)); } /// /// Unregister specified commands from CommandsNext. /// /// Commands to unregister. public void UnregisterCommands(params Command[] cmds) { if (cmds.Any(x => x.Parent != null)) throw new InvalidOperationException("Cannot unregister nested commands."); var keys = this.RegisteredCommands.Where(x => cmds.Contains(x.Value)).Select(x => x.Key).ToList(); foreach (var key in keys) this._topLevelCommands.Remove(key); } /// /// Adds the to command dictionary. /// /// The cmd. private void AddToCommandDictionary(Command cmd) { if (cmd.Parent != null) return; if (this._topLevelCommands.ContainsKey(cmd.Name) || (cmd.Aliases != null && cmd.Aliases.Any(xs => this._topLevelCommands.ContainsKey(xs)))) throw new DuplicateCommandException(cmd.QualifiedName); this._topLevelCommands[cmd.Name] = cmd; if (cmd.Aliases != null) foreach (var xs in cmd.Aliases) this._topLevelCommands[xs] = cmd; } #endregion #region Default Help /// /// Represents the default help module. /// [ModuleLifespan(ModuleLifespan.Transient)] public class DefaultHelpModule : BaseCommandModule { /// /// Defaults the help async. /// /// The ctx. /// The command. /// A Task. [Command("help"), Description("Displays command help.")] public async Task DefaultHelpAsync(CommandContext ctx, [Description("Command to provide help for.")] params string[] command) { var topLevel = ctx.CommandsNext._topLevelCommands.Values.Distinct(); var helpBuilder = ctx.CommandsNext._helpFormatter.Create(ctx); if (command != null && command.Any()) { Command cmd = null; var searchIn = topLevel; foreach (var c in command) { if (searchIn == null) { cmd = null; break; } cmd = ctx.Config.CaseSensitive ? searchIn.FirstOrDefault(xc => xc.Name == c || (xc.Aliases != null && xc.Aliases.Contains(c))) : searchIn.FirstOrDefault(xc => xc.Name.ToLowerInvariant() == c.ToLowerInvariant() || (xc.Aliases != null && xc.Aliases.Select(xs => xs.ToLowerInvariant()).Contains(c.ToLowerInvariant()))); if (cmd == null) break; var failedChecks = await cmd.RunChecksAsync(ctx, true).ConfigureAwait(false); if (failedChecks.Any()) throw new ChecksFailedException(cmd, ctx, failedChecks); searchIn = cmd is CommandGroup ? (cmd as CommandGroup).Children : null; } if (cmd == null) throw new CommandNotFoundException(string.Join(" ", command)); helpBuilder.WithCommand(cmd); if (cmd is CommandGroup group) { var commandsToSearch = group.Children.Where(xc => !xc.IsHidden); var eligibleCommands = new List(); foreach (var candidateCommand in commandsToSearch) { if (candidateCommand.ExecutionChecks == null || !candidateCommand.ExecutionChecks.Any()) { eligibleCommands.Add(candidateCommand); continue; } var candidateFailedChecks = await candidateCommand.RunChecksAsync(ctx, true).ConfigureAwait(false); if (!candidateFailedChecks.Any()) eligibleCommands.Add(candidateCommand); } if (eligibleCommands.Any()) helpBuilder.WithSubcommands(eligibleCommands.OrderBy(xc => xc.Name)); } } else { var commandsToSearch = topLevel.Where(xc => !xc.IsHidden); var eligibleCommands = new List(); foreach (var sc in commandsToSearch) { if (sc.ExecutionChecks == null || !sc.ExecutionChecks.Any()) { eligibleCommands.Add(sc); continue; } var candidateFailedChecks = await sc.RunChecksAsync(ctx, true).ConfigureAwait(false); if (!candidateFailedChecks.Any()) eligibleCommands.Add(sc); } if (eligibleCommands.Any()) helpBuilder.WithSubcommands(eligibleCommands.OrderBy(xc => xc.Name)); } var helpMessage = helpBuilder.Build(); var builder = new DiscordMessageBuilder().WithContent(helpMessage.Content).WithEmbed(helpMessage.Embed); if (!ctx.Config.DmHelp || ctx.Channel is DiscordDmChannel || ctx.Guild == null) await ctx.RespondAsync(builder).ConfigureAwait(false); else await ctx.Member.SendMessageAsync(builder).ConfigureAwait(false); } } #endregion #region Sudo /// /// Creates a fake command context to execute commands with. /// /// The user or member to use as message author. /// The channel the message is supposed to appear from. /// Contents of the message. /// Command prefix, used to execute commands. /// Command to execute. /// Raw arguments to pass to command. /// Created fake context. public CommandContext CreateFakeContext(DiscordUser actor, DiscordChannel channel, string messageContents, string prefix, Command cmd, string rawArguments = null) { var epoch = new DateTimeOffset(2015, 1, 1, 0, 0, 0, TimeSpan.Zero); var now = DateTimeOffset.UtcNow; var timeSpan = (ulong)(now - epoch).TotalMilliseconds; // create fake message var msg = new DiscordMessage { Discord = this.Client, Author = actor, ChannelId = channel.Id, Content = messageContents, Id = timeSpan << 22, Pinned = false, MentionEveryone = messageContents.Contains("@everyone"), IsTts = false, AttachmentsInternal = new List(), EmbedsInternal = new List(), TimestampRaw = now.ToString("yyyy-MM-ddTHH:mm:sszzz"), ReactionsInternal = new List() }; var mentionedUsers = new List(); var mentionedRoles = msg.Channel.Guild != null ? new List() : null; var mentionedChannels = msg.Channel.Guild != null ? new List() : null; if (!string.IsNullOrWhiteSpace(msg.Content)) { if (msg.Channel.Guild != null) { mentionedUsers = Utilities.GetUserMentions(msg).Select(xid => msg.Channel.Guild.MembersInternal.TryGetValue(xid, out var member) ? member : null).Cast().ToList(); mentionedRoles = Utilities.GetRoleMentions(msg).Select(xid => msg.Channel.Guild.GetRole(xid)).ToList(); mentionedChannels = Utilities.GetChannelMentions(msg).Select(xid => msg.Channel.Guild.GetChannel(xid)).ToList(); } else { mentionedUsers = Utilities.GetUserMentions(msg).Select(this.Client.GetCachedOrEmptyUserInternal).ToList(); } } msg.MentionedUsersInternal = mentionedUsers; msg.MentionedRolesInternal = mentionedRoles; msg.MentionedChannelsInternal = mentionedChannels; var ctx = new CommandContext { Client = this.Client, Command = cmd, Message = msg, Config = this._config, RawArgumentString = rawArguments ?? "", Prefix = prefix, CommandsNext = this, Services = this.Services }; if (cmd != null && (cmd.Module is TransientCommandModule || cmd.Module == null)) { var scope = ctx.Services.CreateScope(); ctx.ServiceScopeContext = new CommandContext.ServiceContext(ctx.Services, scope); ctx.Services = scope.ServiceProvider; } return ctx; } #endregion #region Type Conversion /// /// Converts a string to specified type. /// /// Type to convert to. /// Value to convert. /// Context in which to convert to. /// Converted object. public async Task ConvertArgument(string value, CommandContext ctx) { var t = typeof(T); if (!this.ArgumentConverters.ContainsKey(t)) throw new ArgumentException("There is no converter specified for given type.", nameof(T)); if (this.ArgumentConverters[t] is not IArgumentConverter cv) throw new ArgumentException("Invalid converter registered for this type.", nameof(T)); var cvr = await cv.ConvertAsync(value, ctx).ConfigureAwait(false); return !cvr.HasValue ? throw new ArgumentException("Could not convert specified value to given type.", nameof(value)) : cvr.Value; } + /// + /// Converts a string to specified type. + /// + /// Type to convert to. + /// Value to convert. + /// Context in which to convert to. + /// Converted object. + public async Task ConvertArgument(string value, HybridCommandContext ctx) + { + var t = typeof(T); + if (!this.ArgumentConverters.ContainsKey(t)) + throw new ArgumentException("There is no converter specified for given type.", nameof(T)); + + if (this.ArgumentConverters[t] is not IArgumentConverter cv) + throw new ArgumentException("Invalid converter registered for this type.", nameof(T)); + + var cvr = await cv.ConvertAsync(value, ctx).ConfigureAwait(false); + return !cvr.HasValue ? throw new ArgumentException("Could not convert specified value to given type.", nameof(value)) : cvr.Value; + } + /// /// Converts a string to specified type. /// /// Value to convert. /// Context in which to convert to. /// Type to convert to. /// Converted object. public async Task ConvertArgument(string value, CommandContext ctx, Type type) { var m = this._convertGeneric.MakeGenericMethod(type); try { return await (m.Invoke(this, new object[] { value, ctx }) as Task).ConfigureAwait(false); } catch (TargetInvocationException ex) { throw ex.InnerException; } } + /// + /// Converts a string to specified type. + /// + /// Value to convert. + /// Context in which to convert to. + /// Type to convert to. + /// Converted object. + public async Task ConvertArgument(string value, HybridCommandContext ctx, Type type) + { + var m = this._convertGeneric.MakeGenericMethod(type); + try + { + return await (m.Invoke(this, new object[] { value, ctx }) as Task).ConfigureAwait(false); + } + catch (TargetInvocationException ex) + { + throw ex.InnerException; + } + } + /// /// Registers an argument converter for specified type. /// /// Type for which to register the converter. /// Converter to register. public void RegisterConverter(IArgumentConverter converter) { if (converter == null) throw new ArgumentNullException(nameof(converter), "Converter cannot be null."); var t = typeof(T); var ti = t.GetTypeInfo(); this.ArgumentConverters[t] = converter; if (!ti.IsValueType) return; var nullableConverterType = typeof(NullableConverter<>).MakeGenericType(t); var nullableType = typeof(Nullable<>).MakeGenericType(t); if (this.ArgumentConverters.ContainsKey(nullableType)) return; var nullableConverter = Activator.CreateInstance(nullableConverterType) as IArgumentConverter; this.ArgumentConverters[nullableType] = nullableConverter; } /// /// Unregister an argument converter for specified type. /// /// Type for which to unregister the converter. public void UnregisterConverter() { var t = typeof(T); var ti = t.GetTypeInfo(); if (this.ArgumentConverters.ContainsKey(t)) this.ArgumentConverters.Remove(t); if (this._userFriendlyTypeNames.ContainsKey(t)) this._userFriendlyTypeNames.Remove(t); if (!ti.IsValueType) return; var nullableType = typeof(Nullable<>).MakeGenericType(t); if (!this.ArgumentConverters.ContainsKey(nullableType)) return; this.ArgumentConverters.Remove(nullableType); this._userFriendlyTypeNames.Remove(nullableType); } /// /// Registers a user-friendly type name. /// /// Type to register the name for. /// Name to register. public void RegisterUserFriendlyTypeName(string value) { if (string.IsNullOrWhiteSpace(value)) throw new ArgumentNullException(nameof(value), "Name cannot be null or empty."); var t = typeof(T); var ti = t.GetTypeInfo(); if (!this.ArgumentConverters.ContainsKey(t)) throw new InvalidOperationException("Cannot register a friendly name for a type which has no associated converter."); this._userFriendlyTypeNames[t] = value; if (!ti.IsValueType) return; var nullableType = typeof(Nullable<>).MakeGenericType(t); this._userFriendlyTypeNames[nullableType] = value; } /// /// Converts a type into user-friendly type name. /// /// Type to convert. /// User-friendly type name. public string GetUserFriendlyTypeName(Type t) { if (this._userFriendlyTypeNames.ContainsKey(t)) return this._userFriendlyTypeNames[t]; var ti = t.GetTypeInfo(); if (ti.IsGenericTypeDefinition && t.GetGenericTypeDefinition() == typeof(Nullable<>)) { var tn = ti.GenericTypeArguments[0]; return this._userFriendlyTypeNames.ContainsKey(tn) ? this._userFriendlyTypeNames[tn] : tn.Name; } return t.Name; } #endregion #region Helpers /// /// Allows easier interoperability with reflection by turning the returned by /// into a task containing , using the provided generic type information. /// private async Task ConvertArgumentToObj(string value, CommandContext ctx) => await this.ConvertArgument(value, ctx).ConfigureAwait(false); /// /// Gets the configuration-specific string comparer. This returns or , /// depending on whether is set to or . /// /// A string comparer. internal IEqualityComparer GetStringComparer() => this._config.CaseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase; #endregion #region Events /// /// Triggered whenever a command executes successfully. /// public event AsyncEventHandler CommandExecuted { add => this._executed.Register(value); remove => this._executed.Unregister(value); } private AsyncEvent _executed; /// /// Triggered whenever a command throws an exception during execution. /// public event AsyncEventHandler CommandErrored { add => this._error.Register(value); remove => this._error.Unregister(value); } private AsyncEvent _error; /// /// Fires when a command gets executed. /// /// The command execution event arguments. private Task OnCommandExecuted(CommandExecutionEventArgs e) => this._executed.InvokeAsync(this, e); /// /// Fires when a command fails. /// /// The command error event arguments. private Task OnCommandErrored(CommandErrorEventArgs e) => this._error.InvokeAsync(this, e); #endregion } diff --git a/DisCatSharp.CommandsNext/CommandsNextUtilities.cs b/DisCatSharp.CommandsNext/CommandsNextUtilities.cs index 06da1ecb1..31f885e10 100644 --- a/DisCatSharp.CommandsNext/CommandsNextUtilities.cs +++ b/DisCatSharp.CommandsNext/CommandsNextUtilities.cs @@ -1,430 +1,533 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-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 NONINFRINGEMENT. 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.Globalization; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Text.RegularExpressions; using System.Threading.Tasks; using DisCatSharp.CommandsNext.Attributes; using DisCatSharp.CommandsNext.Converters; using DisCatSharp.Common.RegularExpressions; using DisCatSharp.Entities; +using DisCatSharp.HybridCommands.Entities; using Microsoft.Extensions.DependencyInjection; namespace DisCatSharp.CommandsNext; /// /// Various CommandsNext-related utilities. /// public static class CommandsNextUtilities { /// /// Gets the user regex. /// private static Regex s_userRegex { get; } = DiscordRegEx.User; /// /// Checks whether the message has a specified string prefix. /// /// Message to check. /// String to check for. /// Method of string comparison for the purposes of finding prefixes. /// Positive number if the prefix is present, -1 otherwise. public static int GetStringPrefixLength(this DiscordMessage msg, string str, StringComparison comparisonType = StringComparison.Ordinal) { var content = msg.Content; return str.Length >= content.Length ? -1 : !content.StartsWith(str, comparisonType) ? -1 : str.Length; } /// /// Checks whether the message contains a specified mention prefix. /// /// Message to check. /// User to check for. /// Positive number if the prefix is present, -1 otherwise. public static int GetMentionPrefixLength(this DiscordMessage msg, DiscordUser user) { var content = msg.Content; if (!content.StartsWith("<@")) return -1; var cni = content.IndexOf('>'); if (cni == -1 || content.Length <= cni + 1) return -1; var cnp = content[..(cni + 1)]; var m = s_userRegex.Match(cnp); if (!m.Success) return -1; var userId = ulong.Parse(m.Groups[1].Value, CultureInfo.InvariantCulture); return user.Id != userId ? -1 : m.Value.Length; } //internal static string ExtractNextArgument(string str, out string remainder) /// /// Extracts the next argument. /// /// The string. /// The start position. internal static string ExtractNextArgument(this string str, ref int startPos) { if (string.IsNullOrWhiteSpace(str)) return null; var inBacktick = false; var inTripleBacktick = false; var inQuote = false; var inEscape = false; var removeIndices = new List(str.Length - startPos); var i = startPos; for (; i < str.Length; i++) if (!char.IsWhiteSpace(str[i])) break; startPos = i; var endPosition = -1; var startPosition = startPos; for (i = startPosition; i < str.Length; i++) { if (char.IsWhiteSpace(str[i]) && !inQuote && !inTripleBacktick && !inBacktick && !inEscape) endPosition = i; if (str[i] == '\\' && str.Length > i + 1) { if (!inEscape && !inBacktick && !inTripleBacktick) { inEscape = true; if (str.IndexOf("\\`", i) == i || str.IndexOf("\\\"", i) == i || str.IndexOf("\\\\", i) == i || (str.Length >= i && char.IsWhiteSpace(str[i + 1]))) removeIndices.Add(i - startPosition); i++; } else if ((inBacktick || inTripleBacktick) && str.IndexOf("\\`", i) == i) { inEscape = true; removeIndices.Add(i - startPosition); i++; } } if (str[i] == '`' && !inEscape) { var tripleBacktick = str.IndexOf("```", i) == i; if (inTripleBacktick && tripleBacktick) { inTripleBacktick = false; i += 2; } else if (!inBacktick && tripleBacktick) { inTripleBacktick = true; i += 2; } if (inBacktick && !tripleBacktick) inBacktick = false; else if (!inTripleBacktick && tripleBacktick) inBacktick = true; } if (str[i] == '"' && !inEscape && !inBacktick && !inTripleBacktick) { removeIndices.Add(i - startPosition); inQuote = !inQuote; } if (inEscape) inEscape = false; if (endPosition != -1) { startPos = endPosition; return startPosition != endPosition ? str[startPosition..endPosition].CleanupString(removeIndices) : null; } } startPos = str.Length; return startPos != startPosition ? str[startPosition..].CleanupString(removeIndices) : null; } /// /// Cleanups the string. /// /// The string. /// The indices. internal static string CleanupString(this string s, IList indices) { if (!indices.Any()) return s; var li = indices.Last(); var ll = 1; for (var x = indices.Count - 2; x >= 0; x--) { if (li - indices[x] == ll) { ll++; continue; } s = s.Remove(li - ll + 1, ll); li = indices[x]; ll = 1; } return s.Remove(li - ll + 1, ll); } /// /// Binds the arguments. /// /// The command context. /// If true, ignore further text in string. internal static async Task BindArguments(CommandContext ctx, bool ignoreSurplus) { var command = ctx.Command; var overload = ctx.Overload; var args = new object[overload.Arguments.Count + 2]; args[1] = ctx; var rawArgumentList = new List(overload.Arguments.Count); var argString = ctx.RawArgumentString; var foundAt = 0; foreach (var arg in overload.Arguments) { string argValue; if (arg.IsCatchAll) { if (arg.IsArray) { while (true) { argValue = ExtractNextArgument(argString, ref foundAt); if (argValue == null) break; rawArgumentList.Add(argValue); } break; } else { if (argString == null) break; argValue = argString[foundAt..].Trim(); argValue = argValue == "" ? null : argValue; foundAt = argString.Length; rawArgumentList.Add(argValue); break; } } else { argValue = ExtractNextArgument(argString, ref foundAt); rawArgumentList.Add(argValue); } if (argValue == null && !arg.IsOptional && !arg.IsCatchAll) return new ArgumentBindingResult(new ArgumentException("Not enough arguments supplied to the command.")); else if (argValue == null) rawArgumentList.Add(null); } if (!ignoreSurplus && foundAt < argString.Length) return new ArgumentBindingResult(new ArgumentException("Too many arguments were supplied to this command.")); for (var i = 0; i < overload.Arguments.Count; i++) { var arg = overload.Arguments[i]; if (arg.IsCatchAll && arg.IsArray) { var array = Array.CreateInstance(arg.Type, rawArgumentList.Count - i); var start = i; while (i < rawArgumentList.Count) { try { array.SetValue(await ctx.CommandsNext.ConvertArgument(rawArgumentList[i], ctx, arg.Type).ConfigureAwait(false), i - start); } catch (Exception ex) { return new ArgumentBindingResult(ex); } i++; } args[start + 2] = array; break; } else { try { args[i + 2] = rawArgumentList[i] != null ? await ctx.CommandsNext.ConvertArgument(rawArgumentList[i], ctx, arg.Type).ConfigureAwait(false) : arg.DefaultValue; } catch (Exception ex) { return new ArgumentBindingResult(ex); } } } return new ArgumentBindingResult(args, rawArgumentList); } + /// + /// Binds the arguments. + /// + /// The command context. + /// If true, ignore further text in string. + internal static async Task BindArguments(HybridCommandContext ctx, Command command, CommandOverload overload, string rawString, bool ignoreSurplus) + { + var args = new object[overload.Arguments.Count + 2]; + args[1] = ctx; + var rawArgumentList = new List(overload.Arguments.Count); + + var argString = rawString; + var foundAt = 0; + foreach (var arg in overload.Arguments) + { + string argValue; + if (arg.IsCatchAll) + { + if (arg.IsArray) + { + while (true) + { + argValue = ExtractNextArgument(argString, ref foundAt); + if (argValue == null) + break; + + rawArgumentList.Add(argValue); + } + + break; + } + else + { + if (argString == null) + break; + + argValue = argString[foundAt..].Trim(); + argValue = argValue == "" ? null : argValue; + foundAt = argString.Length; + + rawArgumentList.Add(argValue); + break; + } + } + else + { + argValue = ExtractNextArgument(argString, ref foundAt); + rawArgumentList.Add(argValue); + } + + if (argValue == null && !arg.IsOptional && !arg.IsCatchAll) + return new ArgumentBindingResult(new ArgumentException("Not enough arguments supplied to the command.")); + else if (argValue == null) + rawArgumentList.Add(null); + } + + if (!ignoreSurplus && foundAt < argString.Length) + return new ArgumentBindingResult(new ArgumentException("Too many arguments were supplied to this command.")); + + for (var i = 0; i < overload.Arguments.Count; i++) + { + var arg = overload.Arguments[i]; + if (arg.IsCatchAll && arg.IsArray) + { + var array = Array.CreateInstance(arg.Type, rawArgumentList.Count - i); + var start = i; + while (i < rawArgumentList.Count) + { + try + { + array.SetValue(await ctx.Client.GetCommandsNext().ConvertArgument(rawArgumentList[i], ctx, arg.Type).ConfigureAwait(false), i - start); + } + catch (Exception ex) + { + return new ArgumentBindingResult(ex); + } + i++; + } + + args[start + 2] = array; + break; + } + else + { + try + { + args[i + 2] = rawArgumentList[i] != null ? await ctx.Client.GetCommandsNext().ConvertArgument(rawArgumentList[i], ctx, arg.Type).ConfigureAwait(false) : arg.DefaultValue; + } + catch (Exception ex) + { + return new ArgumentBindingResult(ex); + } + } + } + + return new ArgumentBindingResult(args, rawArgumentList); + } + /// /// 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 TypeInfo ti) { // check if compiler-generated if (ti.GetCustomAttribute(false) != null) return false; // check if derives from the required base class var tmodule = typeof(BaseCommandModule); var timodule = tmodule.GetTypeInfo(); if (!timodule.IsAssignableFrom(ti)) return false; // check if anonymous if (ti.IsGenericType && ti.Name.Contains("AnonymousType") && (ti.Name.StartsWith("<>") || ti.Name.StartsWith("VB$")) && (ti.Attributes & TypeAttributes.NotPublic) == TypeAttributes.NotPublic) return false; // check if abstract, static, or not a class if (!ti.IsClass || ti.IsAbstract) return false; // check if delegate type var tdelegate = typeof(Delegate).GetTypeInfo(); if (tdelegate.IsAssignableFrom(ti)) return false; // qualifies if any method or type qualifies - return ti.DeclaredMethods.Any(xmi => xmi.IsCommandCandidate(out _)) || ti.DeclaredNestedTypes.Any(xti => xti.IsModuleCandidateType()); + return ti.DeclaredMethods.Any(xmi => xmi.IsCommandCandidate(out _, out _)) || ti.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) + internal static bool IsCommandCandidate(this MethodInfo method, out ParameterInfo[] parameters, out bool IsHybrid) { parameters = null; + IsHybrid = false; // check if exists if (method == null) return false; // check if static, non-public, abstract, a constructor, or a special name if (method.IsStatic || method.IsAbstract || method.IsConstructor || method.IsSpecialName) return false; // check if appropriate return and arguments parameters = method.GetParameters(); - if (!parameters.Any() || parameters.First().ParameterType != typeof(CommandContext) || method.ReturnType != typeof(Task)) + if (!parameters.Any() || (parameters.First().ParameterType != typeof(CommandContext) && parameters.First().ParameterType != typeof(HybridCommandContext)) || method.ReturnType != typeof(Task)) return false; + if (parameters.First().ParameterType == typeof(HybridCommandContext)) + IsHybrid = true; + // qualifies return true; } /// /// Creates the instance. /// /// The type. /// The services provider. internal static object CreateInstance(this Type t, IServiceProvider services) { var ti = t.GetTypeInfo(); var constructors = ti.DeclaredConstructors .Where(xci => xci.IsPublic) .ToArray(); if (constructors.Length != 1) throw new ArgumentException("Specified type does not contain a public constructor or contains more than one public constructor."); var constructor = constructors[0]; var constructorArgs = constructor.GetParameters(); var args = new object[constructorArgs.Length]; if (constructorArgs.Length != 0 && services == null) throw new InvalidOperationException("Dependency collection needs to be specified for parameterized constructors."); // inject via constructor if (constructorArgs.Length != 0) for (var i = 0; i < args.Length; i++) args[i] = services.GetRequiredService(constructorArgs[i].ParameterType); var moduleInstance = Activator.CreateInstance(t, args); // inject into properties var props = t.GetRuntimeProperties().Where(xp => xp.CanWrite && xp.SetMethod != null && !xp.SetMethod.IsStatic && xp.SetMethod.IsPublic); foreach (var prop in props) { if (prop.GetCustomAttribute() != null) continue; var service = services.GetService(prop.PropertyType); if (service == null) continue; prop.SetValue(moduleInstance, service); } // inject into fields var fields = t.GetRuntimeFields().Where(xf => !xf.IsInitOnly && !xf.IsStatic && xf.IsPublic); foreach (var field in fields) { if (field.GetCustomAttribute() != null) continue; var service = services.GetService(field.FieldType); if (service == null) continue; field.SetValue(moduleInstance, service); } return moduleInstance; } } diff --git a/DisCatSharp.CommandsNext/Converters/EntityConverters.cs b/DisCatSharp.CommandsNext/Converters/EntityConverters.cs index 8baf4c179..960da4025 100644 --- a/DisCatSharp.CommandsNext/Converters/EntityConverters.cs +++ b/DisCatSharp.CommandsNext/Converters/EntityConverters.cs @@ -1,455 +1,758 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-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 NONINFRINGEMENT. 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.Globalization; using System.Linq; using System.Threading.Tasks; using DisCatSharp.Common.RegularExpressions; using DisCatSharp.Entities; +using DisCatSharp.HybridCommands.Entities; namespace DisCatSharp.CommandsNext.Converters; /// /// Represents a discord user converter. /// public class DiscordUserConverter : IArgumentConverter { + public async Task> ConvertAsync(string value, HybridCommandContext ctx) + { + if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var uid)) + { + var result = await ctx.Client.GetUserAsync(uid).ConfigureAwait(false); + return Optional.FromNullable(result); + } + + var m = DiscordRegEx.User.Match(value); + if (m.Success && ulong.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out uid)) + { + var result = await ctx.Client.GetUserAsync(uid).ConfigureAwait(false); + return Optional.FromNullable(result); + } + + var cs = ctx.Client.GetCommandsNext()._config.CaseSensitive; + if (!cs) + value = value.ToLowerInvariant(); + + var di = value.IndexOf('#'); + var un = di != -1 ? value[..di] : value; + var dv = di != -1 ? value[(di + 1)..] : null; + + var us = ctx.Client.Guilds.Values + .SelectMany(xkvp => xkvp.Members.Values) + .Where(xm => (cs ? xm.Username : xm.Username.ToLowerInvariant()) == un && ((dv != null && xm.Discriminator == dv) || dv == null)); + + var usr = us.FirstOrDefault(); + return Optional.FromNullable(usr); + } + /// /// Converts a string. /// /// The string to convert. /// The command context. async Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) { if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var uid)) { var result = await ctx.Client.GetUserAsync(uid).ConfigureAwait(false); return Optional.FromNullable(result); } var m = DiscordRegEx.User.Match(value); if (m.Success && ulong.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out uid)) { var result = await ctx.Client.GetUserAsync(uid).ConfigureAwait(false); return Optional.FromNullable(result); } var cs = ctx.Config.CaseSensitive; if (!cs) value = value.ToLowerInvariant(); var di = value.IndexOf('#'); var un = di != -1 ? value[..di] : value; var dv = di != -1 ? value[(di + 1)..] : null; var us = ctx.Client.Guilds.Values .SelectMany(xkvp => xkvp.Members.Values) .Where(xm => (cs ? xm.Username : xm.Username.ToLowerInvariant()) == un && ((dv != null && xm.Discriminator == dv) || dv == null)); var usr = us.FirstOrDefault(); return Optional.FromNullable(usr); } } /// /// Represents a discord member converter. /// public class DiscordMemberConverter : IArgumentConverter { + public async Task> ConvertAsync(string value, HybridCommandContext ctx) + { + if (ctx.Guild == null) + return Optional.None; + + if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var uid)) + { + var result = await ctx.Guild.GetMemberAsync(uid).ConfigureAwait(false); + return Optional.FromNullable(result); + } + + var m = DiscordRegEx.User.Match(value); + if (m.Success && ulong.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out uid)) + { + var result = await ctx.Guild.GetMemberAsync(uid).ConfigureAwait(false); + return Optional.FromNullable(result); + } + + var searchResult = await ctx.Guild.SearchMembersAsync(value).ConfigureAwait(false); + if (searchResult.Any()) + return Optional.Some(searchResult.First()); + + var cs = ctx.Client.GetCommandsNext()._config.CaseSensitive; + if (!cs) + value = value.ToLowerInvariant(); + + var di = value.IndexOf('#'); + var un = di != -1 ? value[..di] : value; + var dv = di != -1 ? value[(di + 1)..] : null; + + var us = ctx.Guild.Members.Values + .Where(xm => ((cs ? xm.Username : xm.Username.ToLowerInvariant()) == un && ((dv != null && xm.Discriminator == dv) || dv == null)) + || (cs ? xm.Nickname : xm.Nickname?.ToLowerInvariant()) == value); + + return Optional.FromNullable(us.FirstOrDefault()); + } + /// /// Converts a string. /// /// The string to convert. /// The command context. async Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) { if (ctx.Guild == null) return Optional.None; if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var uid)) { var result = await ctx.Guild.GetMemberAsync(uid).ConfigureAwait(false); return Optional.FromNullable(result); } var m = DiscordRegEx.User.Match(value); if (m.Success && ulong.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out uid)) { var result = await ctx.Guild.GetMemberAsync(uid).ConfigureAwait(false); return Optional.FromNullable(result); } var searchResult = await ctx.Guild.SearchMembersAsync(value).ConfigureAwait(false); if (searchResult.Any()) return Optional.Some(searchResult.First()); var cs = ctx.Config.CaseSensitive; if (!cs) value = value.ToLowerInvariant(); var di = value.IndexOf('#'); var un = di != -1 ? value[..di] : value; var dv = di != -1 ? value[(di + 1)..] : null; var us = ctx.Guild.Members.Values .Where(xm => ((cs ? xm.Username : xm.Username.ToLowerInvariant()) == un && ((dv != null && xm.Discriminator == dv) || dv == null)) || (cs ? xm.Nickname : xm.Nickname?.ToLowerInvariant()) == value); return Optional.FromNullable(us.FirstOrDefault()); } } /// /// Represents a discord channel converter. /// public class DiscordChannelConverter : IArgumentConverter { + public async Task> ConvertAsync(string value, HybridCommandContext ctx) + { + if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var cid)) + { + var result = await ctx.Client.GetChannelAsync(cid).ConfigureAwait(false); + return Optional.FromNullable(result); + } + + var m = DiscordRegEx.Channel.Match(value); + if (m.Success && ulong.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out cid)) + { + var result = await ctx.Client.GetChannelAsync(cid).ConfigureAwait(false); + return Optional.FromNullable(result); + } + + var cs = ctx.Client.GetCommandsNext()._config.CaseSensitive; + if (!cs) + value = value.ToLowerInvariant(); + + var chn = ctx.Guild?.Channels.Values.FirstOrDefault(xc => (cs ? xc.Name : xc.Name.ToLowerInvariant()) == value); + return Optional.FromNullable(chn); + } + /// /// Converts a string. /// /// The string to convert. /// The command context. async Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) { if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var cid)) { var result = await ctx.Client.GetChannelAsync(cid).ConfigureAwait(false); return Optional.FromNullable(result); } var m = DiscordRegEx.Channel.Match(value); if (m.Success && ulong.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out cid)) { var result = await ctx.Client.GetChannelAsync(cid).ConfigureAwait(false); return Optional.FromNullable(result); } var cs = ctx.Config.CaseSensitive; if (!cs) value = value.ToLowerInvariant(); var chn = ctx.Guild?.Channels.Values.FirstOrDefault(xc => (cs ? xc.Name : xc.Name.ToLowerInvariant()) == value); return Optional.FromNullable(chn); } } /// /// Represents a discord thread channel converter. /// public class DiscordThreadChannelConverter : IArgumentConverter { + public async Task> ConvertAsync(string value, HybridCommandContext ctx) + { + if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var tid)) + { + var result = await ctx.Client.GetThreadAsync(tid).ConfigureAwait(false); + return Optional.FromNullable(result); + } + + var m = DiscordRegEx.Channel.Match(value); + if (m.Success && ulong.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out tid)) + { + var result = await ctx.Client.GetThreadAsync(tid).ConfigureAwait(false); + return Optional.FromNullable(result); + } + + var cs = ctx.Client.GetCommandsNext()._config.CaseSensitive; + if (!cs) + value = value.ToLowerInvariant(); + + var tchn = ctx.Guild?.Threads.Values.FirstOrDefault(xc => (cs ? xc.Name : xc.Name.ToLowerInvariant()) == value); + return Optional.FromNullable(tchn); + } + /// /// Converts a string. /// /// The string to convert. /// The command context. async Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) { if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var tid)) { var result = await ctx.Client.GetThreadAsync(tid).ConfigureAwait(false); return Optional.FromNullable(result); } var m = DiscordRegEx.Channel.Match(value); if (m.Success && ulong.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out tid)) { var result = await ctx.Client.GetThreadAsync(tid).ConfigureAwait(false); return Optional.FromNullable(result); } var cs = ctx.Config.CaseSensitive; if (!cs) value = value.ToLowerInvariant(); var tchn = ctx.Guild?.Threads.Values.FirstOrDefault(xc => (cs ? xc.Name : xc.Name.ToLowerInvariant()) == value); return Optional.FromNullable(tchn); } } /// /// Represents a discord role converter. /// public class DiscordRoleConverter : IArgumentConverter { + public Task> ConvertAsync(string value, HybridCommandContext ctx) + { + if (ctx.Guild == null) + return Task.FromResult(Optional.None); + + if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rid)) + { + var result = ctx.Guild.GetRole(rid); + return Task.FromResult(Optional.FromNullable(result)); + } + + var m = DiscordRegEx.Role.Match(value); + if (m.Success && ulong.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out rid)) + { + var result = ctx.Guild.GetRole(rid); + return Task.FromResult(Optional.FromNullable(result)); + } + + var cs = ctx.Client.GetCommandsNext()._config.CaseSensitive; + if (!cs) + value = value.ToLowerInvariant(); + + var rol = ctx.Guild.Roles.Values.FirstOrDefault(xr => (cs ? xr.Name : xr.Name.ToLowerInvariant()) == value); + return Task.FromResult(Optional.FromNullable(rol)); + } + /// /// Converts a string. /// /// The string to convert. /// The command context. Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) { if (ctx.Guild == null) return Task.FromResult(Optional.None); if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rid)) { var result = ctx.Guild.GetRole(rid); return Task.FromResult(Optional.FromNullable(result)); } var m = DiscordRegEx.Role.Match(value); if (m.Success && ulong.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out rid)) { var result = ctx.Guild.GetRole(rid); return Task.FromResult(Optional.FromNullable(result)); } var cs = ctx.Config.CaseSensitive; if (!cs) value = value.ToLowerInvariant(); var rol = ctx.Guild.Roles.Values.FirstOrDefault(xr => (cs ? xr.Name : xr.Name.ToLowerInvariant()) == value); return Task.FromResult(Optional.FromNullable(rol)); } } /// /// Represents a discord guild converter. /// public class DiscordGuildConverter : IArgumentConverter { + public Task> ConvertAsync(string value, HybridCommandContext ctx) + { + if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var gid)) + { + return ctx.Client.Guilds.TryGetValue(gid, out var result) + ? Task.FromResult(Optional.Some(result)) + : Task.FromResult(Optional.None); + } + + var cs = ctx.Client.GetCommandsNext()._config.CaseSensitive; + if (!cs) + value = value?.ToLowerInvariant(); + + var gld = ctx.Client.Guilds.Values.FirstOrDefault(xg => (cs ? xg.Name : xg.Name.ToLowerInvariant()) == value); + return Task.FromResult(Optional.FromNullable(gld)); + } + /// /// Converts a string. /// /// The string to convert. /// The command context. Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) { if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var gid)) { return ctx.Client.Guilds.TryGetValue(gid, out var result) ? Task.FromResult(Optional.Some(result)) : Task.FromResult(Optional.None); } var cs = ctx.Config.CaseSensitive; if (!cs) value = value?.ToLowerInvariant(); var gld = ctx.Client.Guilds.Values.FirstOrDefault(xg => (cs ? xg.Name : xg.Name.ToLowerInvariant()) == value); return Task.FromResult(Optional.FromNullable(gld)); } } /// /// Represents a discord invite converter. /// public class DiscordInviteConverter : IArgumentConverter { + public async Task> ConvertAsync(string value, HybridCommandContext ctx) + { + var m = DiscordRegEx.Invite.Match(value); + if (m.Success) + { + ulong? eventId = ulong.TryParse(m.Groups["event"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, + out var eid) ? eid : null; + var result = await ctx.Client.GetInviteByCodeAsync(m.Groups["code"].Value, scheduledEventId: eventId).ConfigureAwait(false); + return Optional.FromNullable(result); + } + + var inv = await ctx.Client.GetInviteByCodeAsync(value); + return Optional.FromNullable(inv); + } + /// /// Converts a string. /// /// The string to convert. /// The command context. async Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) { var m = DiscordRegEx.Invite.Match(value); if (m.Success) { ulong? eventId = ulong.TryParse(m.Groups["event"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var eid) ? eid : null; var result = await ctx.Client.GetInviteByCodeAsync(m.Groups["code"].Value, scheduledEventId: eventId).ConfigureAwait(false); return Optional.FromNullable(result); } var inv = await ctx.Client.GetInviteByCodeAsync(value); return Optional.FromNullable(inv); } } /// /// Represents a discord message converter. /// public class DiscordMessageConverter : IArgumentConverter { + public async Task> ConvertAsync(string value, HybridCommandContext ctx) + { + if (string.IsNullOrWhiteSpace(value)) + return Optional.None; + + var msguri = value.StartsWith("<") && value.EndsWith(">") ? value[1..^1] : value; + ulong mid; + if (Uri.TryCreate(msguri, UriKind.Absolute, out var uri)) + { + var uripath = DiscordRegEx.MessageLink.Match(uri.AbsoluteUri); + if (!uripath.Success + || !ulong.TryParse(uripath.Groups["channel"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var cid) + || !ulong.TryParse(uripath.Groups["message"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out mid)) + return Optional.None; + + var chn = await ctx.Client.GetChannelAsync(cid).ConfigureAwait(false); + if (chn == null) + return Optional.None; + + var msg = await chn.GetMessageAsync(mid).ConfigureAwait(false); + return Optional.FromNullable(msg); + } + + if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out mid)) + { + var result = await ctx.Channel.GetMessageAsync(mid).ConfigureAwait(false); + return Optional.FromNullable(result); + } + + return Optional.None; + } + /// /// Converts a string. /// /// The string to convert. /// The command context. async Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) { if (string.IsNullOrWhiteSpace(value)) return Optional.None; var msguri = value.StartsWith("<") && value.EndsWith(">") ? value[1..^1] : value; ulong mid; if (Uri.TryCreate(msguri, UriKind.Absolute, out var uri)) { var uripath = DiscordRegEx.MessageLink.Match(uri.AbsoluteUri); if (!uripath.Success || !ulong.TryParse(uripath.Groups["channel"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var cid) || !ulong.TryParse(uripath.Groups["message"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out mid)) return Optional.None; var chn = await ctx.Client.GetChannelAsync(cid).ConfigureAwait(false); if (chn == null) return Optional.None; var msg = await chn.GetMessageAsync(mid).ConfigureAwait(false); return Optional.FromNullable(msg); } if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out mid)) { var result = await ctx.Channel.GetMessageAsync(mid).ConfigureAwait(false); return Optional.FromNullable(result); } return Optional.None; } } /// /// Represents a discord scheduled event converter. /// public class DiscordScheduledEventConverter : IArgumentConverter { + public async Task> ConvertAsync(string value, HybridCommandContext ctx) + { + if (string.IsNullOrWhiteSpace(value)) + return Optional.None; + + var msguri = value.StartsWith("<") && value.EndsWith(">") ? value[1..^1] : value; + ulong seid; + if (Uri.TryCreate(msguri, UriKind.Absolute, out var uri)) + { + var uripath = DiscordRegEx.Event.Match(uri.AbsoluteUri); + if (uripath.Success + && ulong.TryParse(uripath.Groups["guild"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, + out var gid) + && ulong.TryParse(uripath.Groups["event"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, + out seid)) + { + var guild = await ctx.Client.GetGuildAsync(gid).ConfigureAwait(false); + if (guild == null) + return Optional.None; + + var ev = await guild.GetScheduledEventAsync(seid).ConfigureAwait(false); + return Optional.FromNullable(ev); + } + + try + { + var invite = await ctx.Client.GetCommandsNext().ConvertArgument(value, ctx).ConfigureAwait(false); + return Optional.FromNullable(invite.GuildScheduledEvent); + } + catch + { + return Optional.None; + } + } + + if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out seid)) + { + var result = await ctx.Guild.GetScheduledEventAsync(seid).ConfigureAwait(false); + return Optional.FromNullable(result); + } + + return Optional.None; + } + /// /// Converts a string. /// /// The string to convert. /// The command context. async Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) { if (string.IsNullOrWhiteSpace(value)) return Optional.None; var msguri = value.StartsWith("<") && value.EndsWith(">") ? value[1..^1] : value; ulong seid; if (Uri.TryCreate(msguri, UriKind.Absolute, out var uri)) { var uripath = DiscordRegEx.Event.Match(uri.AbsoluteUri); if (uripath.Success && ulong.TryParse(uripath.Groups["guild"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var gid) && ulong.TryParse(uripath.Groups["event"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out seid)) { var guild = await ctx.Client.GetGuildAsync(gid).ConfigureAwait(false); if (guild == null) return Optional.None; var ev = await guild.GetScheduledEventAsync(seid).ConfigureAwait(false); return Optional.FromNullable(ev); } try { var invite = await ctx.CommandsNext.ConvertArgument(value, ctx).ConfigureAwait(false); return Optional.FromNullable(invite.GuildScheduledEvent); } catch { return Optional.None; } } if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out seid)) { var result = await ctx.Guild.GetScheduledEventAsync(seid).ConfigureAwait(false); return Optional.FromNullable(result); } return Optional.None; } } /// /// Represents a discord emoji converter. /// public class DiscordEmojiConverter : IArgumentConverter { + public Task> ConvertAsync(string value, HybridCommandContext ctx) + { + if (DiscordEmoji.TryFromUnicode(ctx.Client, value, out var emoji)) + { + var result = emoji; + return Task.FromResult(Optional.Some(result)); + } + + var m = DiscordRegEx.Emoji.Match(value); + if (m.Success) + { + var sid = m.Groups["id"].Value; + var name = m.Groups["name"].Value; + var anim = m.Groups["animated"].Success; + + return !ulong.TryParse(sid, NumberStyles.Integer, CultureInfo.InvariantCulture, out var id) + ? Task.FromResult(Optional.None) + : DiscordEmoji.TryFromGuildEmote(ctx.Client, id, out emoji) + ? Task.FromResult(Optional.Some(emoji)) + : Task.FromResult(Optional.Some(new DiscordEmoji + { + Discord = ctx.Client, + Id = id, + Name = name, + IsAnimated = anim, + RequiresColons = true, + IsManaged = false + })); + } + + return Task.FromResult(Optional.None); + } + /// /// Converts a string. /// /// The string to convert. /// The command context. Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) { if (DiscordEmoji.TryFromUnicode(ctx.Client, value, out var emoji)) { var result = emoji; return Task.FromResult(Optional.Some(result)); } var m = DiscordRegEx.Emoji.Match(value); if (m.Success) { var sid = m.Groups["id"].Value; var name = m.Groups["name"].Value; var anim = m.Groups["animated"].Success; return !ulong.TryParse(sid, NumberStyles.Integer, CultureInfo.InvariantCulture, out var id) ? Task.FromResult(Optional.None) : DiscordEmoji.TryFromGuildEmote(ctx.Client, id, out emoji) ? Task.FromResult(Optional.Some(emoji)) : Task.FromResult(Optional.Some(new DiscordEmoji { Discord = ctx.Client, Id = id, Name = name, IsAnimated = anim, RequiresColons = true, IsManaged = false })); } return Task.FromResult(Optional.None); } } /// /// Represents a discord color converter. /// public class DiscordColorConverter : IArgumentConverter { + public Task> ConvertAsync(string value, HybridCommandContext ctx) + { + var m = CommonRegEx.HexColorString.Match(value); + if (m.Success && int.TryParse(m.Groups[1].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var clr)) + return Task.FromResult(Optional.Some(clr)); + + m = CommonRegEx.RgbColorString.Match(value); + if (m.Success) + { + var p1 = byte.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var r); + var p2 = byte.TryParse(m.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var g); + var p3 = byte.TryParse(m.Groups[3].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var b); + + return !(p1 && p2 && p3) + ? Task.FromResult(Optional.None) + : Task.FromResult(Optional.Some(new DiscordColor(r, g, b))); + } + + return Task.FromResult(Optional.None); + } + /// /// Converts a string. /// /// The string to convert. /// The command context. Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) { var m = CommonRegEx.HexColorString.Match(value); if (m.Success && int.TryParse(m.Groups[1].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var clr)) return Task.FromResult(Optional.Some(clr)); m = CommonRegEx.RgbColorString.Match(value); if (m.Success) { var p1 = byte.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var r); var p2 = byte.TryParse(m.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var g); var p3 = byte.TryParse(m.Groups[3].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var b); return !(p1 && p2 && p3) ? Task.FromResult(Optional.None) : Task.FromResult(Optional.Some(new DiscordColor(r, g, b))); } return Task.FromResult(Optional.None); } } diff --git a/DisCatSharp.CommandsNext/Converters/EnumConverter.cs b/DisCatSharp.CommandsNext/Converters/EnumConverter.cs index b9759427b..b93595b2f 100644 --- a/DisCatSharp.CommandsNext/Converters/EnumConverter.cs +++ b/DisCatSharp.CommandsNext/Converters/EnumConverter.cs @@ -1,51 +1,63 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-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 NONINFRINGEMENT. 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.Threading.Tasks; using DisCatSharp.Entities; +using DisCatSharp.HybridCommands.Entities; namespace DisCatSharp.CommandsNext.Converters; /// /// Represents a enum converter. /// public class EnumConverter : IArgumentConverter where T : struct, IComparable, IConvertible, IFormattable { + public Task> ConvertAsync(string value, HybridCommandContext ctx) + { + var t = typeof(T); + var ti = t.GetTypeInfo(); + return !ti.IsEnum + ? throw new InvalidOperationException("Cannot convert non-enum value to an enum.") + : Enum.TryParse(value, !ctx.Client.GetCommandsNext()._config.CaseSensitive, out T ev) + ? Task.FromResult(Optional.Some(ev)) + : Task.FromResult(Optional.None); + } + /// /// Converts a string. /// /// The string to convert. /// The command context. Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) { var t = typeof(T); var ti = t.GetTypeInfo(); return !ti.IsEnum ? throw new InvalidOperationException("Cannot convert non-enum value to an enum.") : Enum.TryParse(value, !ctx.Config.CaseSensitive, out T ev) ? Task.FromResult(Optional.Some(ev)) : Task.FromResult(Optional.None); } } diff --git a/DisCatSharp.CommandsNext/Converters/IArgumentConverter.cs b/DisCatSharp.CommandsNext/Converters/IArgumentConverter.cs index 965689195..ed1c153f2 100644 --- a/DisCatSharp.CommandsNext/Converters/IArgumentConverter.cs +++ b/DisCatSharp.CommandsNext/Converters/IArgumentConverter.cs @@ -1,48 +1,50 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-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 NONINFRINGEMENT. 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.Entities; +using DisCatSharp.HybridCommands.Entities; namespace DisCatSharp.CommandsNext.Converters; /// /// Argument converter abstract. /// public interface IArgumentConverter { } /// /// Represents a converter for specific argument type. /// /// Type for which the converter is to be registered. public interface IArgumentConverter : IArgumentConverter { /// /// Converts the raw value into the specified type. /// /// Value to convert. /// Context in which the value will be converted. /// A structure containing information whether the value was converted, and, if so, the converted value. Task> ConvertAsync(string value, CommandContext ctx); + Task> ConvertAsync(string value, HybridCommandContext ctx); } diff --git a/DisCatSharp.CommandsNext/Converters/NullableConverter.cs b/DisCatSharp.CommandsNext/Converters/NullableConverter.cs index b1668f57f..89c122967 100644 --- a/DisCatSharp.CommandsNext/Converters/NullableConverter.cs +++ b/DisCatSharp.CommandsNext/Converters/NullableConverter.cs @@ -1,56 +1,74 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-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 NONINFRINGEMENT. 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.Entities; +using DisCatSharp.HybridCommands.Entities; namespace DisCatSharp.CommandsNext.Converters; /// /// Represents a nullable converter. /// public class NullableConverter : IArgumentConverter where T : struct { + public async Task> ConvertAsync(string value, HybridCommandContext ctx) + { + value = value.ToLowerInvariant(); + + if (value == "null") + return null; + + if (ctx.Client.GetCommandsNext().ArgumentConverters.TryGetValue(typeof(T), out var cv)) + { + var cvx = cv as IArgumentConverter; + var val = await cvx.ConvertAsync(value, ctx).ConfigureAwait(false); + return val.Map(x => x); + } + + return Optional.None; + } + /// /// Converts a string. /// /// The string to convert. /// The command context. async Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) { if (!ctx.Config.CaseSensitive) value = value.ToLowerInvariant(); if (value == "null") return null; if (ctx.CommandsNext.ArgumentConverters.TryGetValue(typeof(T), out var cv)) { var cvx = cv as IArgumentConverter; var val = await cvx.ConvertAsync(value, ctx).ConfigureAwait(false); return val.Map(x => x); } return Optional.None; } } diff --git a/DisCatSharp.CommandsNext/Converters/NumericConverters.cs b/DisCatSharp.CommandsNext/Converters/NumericConverters.cs index fa699d23d..20af3aedd 100644 --- a/DisCatSharp.CommandsNext/Converters/NumericConverters.cs +++ b/DisCatSharp.CommandsNext/Converters/NumericConverters.cs @@ -1,220 +1,281 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-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 NONINFRINGEMENT. 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.Globalization; using System.Threading.Tasks; using DisCatSharp.Entities; +using DisCatSharp.HybridCommands.Entities; namespace DisCatSharp.CommandsNext.Converters; /// /// The bool converter. /// public class BoolConverter : IArgumentConverter { + public Task> ConvertAsync(string value, HybridCommandContext ctx) => + bool.TryParse(value, out var result) + ? Task.FromResult(Optional.Some(result)) + : Task.FromResult(Optional.None); + /// /// Converts a string. /// /// The string to convert. /// The command context. Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => bool.TryParse(value, out var result) ? Task.FromResult(Optional.Some(result)) : Task.FromResult(Optional.None); } /// /// The int8 converter. /// public class Int8Converter : IArgumentConverter { + public Task> ConvertAsync(string value, HybridCommandContext ctx) => + sbyte.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) + ? Task.FromResult(Optional.Some(result)) + : Task.FromResult(Optional.None); + /// /// Converts a string. /// /// The string to convert. /// The command context. Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => sbyte.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) ? Task.FromResult(Optional.Some(result)) : Task.FromResult(Optional.None); } /// /// The uint8 converter. /// public class Uint8Converter : IArgumentConverter { + public Task> ConvertAsync(string value, HybridCommandContext ctx) => + byte.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) + ? Task.FromResult(Optional.Some(result)) + : Task.FromResult(Optional.None); + /// /// Converts a string. /// /// The string to convert. /// The command context. Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => byte.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) ? Task.FromResult(Optional.Some(result)) : Task.FromResult(Optional.None); } /// /// The int16 converter. /// public class Int16Converter : IArgumentConverter { + public Task> ConvertAsync(string value, HybridCommandContext ctx) => + short.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) + ? Task.FromResult(Optional.Some(result)) + : Task.FromResult(Optional.None); + /// /// Converts a string. /// /// The string to convert. /// The command context. Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => short.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) ? Task.FromResult(Optional.Some(result)) : Task.FromResult(Optional.None); } /// /// The uint16 converter. /// public class Uint16Converter : IArgumentConverter { + public Task> ConvertAsync(string value, HybridCommandContext ctx) => + ushort.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) + ? Task.FromResult(Optional.Some(result)) + : Task.FromResult(Optional.None); + /// /// Converts a string. /// /// The string to convert. /// The command context. Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => ushort.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) ? Task.FromResult(Optional.Some(result)) : Task.FromResult(Optional.None); } /// /// The int32 converter. /// public class Int32Converter : IArgumentConverter { + public Task> ConvertAsync(string value, HybridCommandContext ctx) => + int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) + ? Task.FromResult(Optional.Some(result)) + : Task.FromResult(Optional.None); + /// /// Converts a string. /// /// The string to convert. /// The command context. Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) ? Task.FromResult(Optional.Some(result)) : Task.FromResult(Optional.None); } /// /// The uint32 converter. /// public class Uint32Converter : IArgumentConverter { + public Task> ConvertAsync(string value, HybridCommandContext ctx) => + uint.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) + ? Task.FromResult(Optional.Some(result)) + : Task.FromResult(Optional.None); + /// /// Converts a string. /// /// The string to convert. /// The command context. Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => uint.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) ? Task.FromResult(Optional.Some(result)) : Task.FromResult(Optional.None); } /// /// The int64 converter. /// public class Int64Converter : IArgumentConverter { + public Task> ConvertAsync(string value, HybridCommandContext ctx) => + long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) + ? Task.FromResult(Optional.Some(result)) + : Task.FromResult(Optional.None); + /// /// Converts a string. /// /// The string to convert. /// The command context. Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) ? Task.FromResult(Optional.Some(result)) : Task.FromResult(Optional.None); } /// /// The uint64 converter. /// public class Uint64Converter : IArgumentConverter { + public Task> ConvertAsync(string value, HybridCommandContext ctx) => + ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) + ? Task.FromResult(Optional.Some(result)) + : Task.FromResult(Optional.None); + /// /// Converts a string. /// /// The string to convert. /// The command context. Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) ? Task.FromResult(Optional.Some(result)) : Task.FromResult(Optional.None); } /// /// The float32 converter. /// public class Float32Converter : IArgumentConverter { + public Task> ConvertAsync(string value, HybridCommandContext ctx) => + float.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out var result) + ? Task.FromResult(Optional.Some(result)) + : Task.FromResult(Optional.None); + /// /// Converts a string. /// /// The string to convert. /// The command context. Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => float.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out var result) ? Task.FromResult(Optional.Some(result)) : Task.FromResult(Optional.None); } /// /// The float64 converter. /// public class Float64Converter : IArgumentConverter { + public Task> ConvertAsync(string value, HybridCommandContext ctx) => + double.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out var result) + ? Task.FromResult(Optional.Some(result)) + : Task.FromResult(Optional.None); + /// /// Converts a string. /// /// The string to convert. /// The command context. Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => double.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out var result) ? Task.FromResult(Optional.Some(result)) : Task.FromResult(Optional.None); } /// /// The float128 converter. /// public class Float128Converter : IArgumentConverter { + public Task> ConvertAsync(string value, HybridCommandContext ctx) => + decimal.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out var result) + ? Task.FromResult(Optional.Some(result)) + : Task.FromResult(Optional.None); + /// /// Converts a string. /// /// The string to convert. /// The command context. Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => decimal.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out var result) ? Task.FromResult(Optional.Some(result)) : Task.FromResult(Optional.None); } diff --git a/DisCatSharp.CommandsNext/Converters/StringConverter.cs b/DisCatSharp.CommandsNext/Converters/StringConverter.cs index d29ee723c..e9ac4bf3a 100644 --- a/DisCatSharp.CommandsNext/Converters/StringConverter.cs +++ b/DisCatSharp.CommandsNext/Converters/StringConverter.cs @@ -1,68 +1,87 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-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 NONINFRINGEMENT. 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.Threading.Tasks; using DisCatSharp.Entities; +using DisCatSharp.HybridCommands.Entities; namespace DisCatSharp.CommandsNext.Converters; /// /// Represents a string converter. /// public class StringConverter : IArgumentConverter { + public Task> ConvertAsync(string value, HybridCommandContext ctx) + => Task.FromResult(Optional.Some(value)); + /// /// Converts a string. /// /// The string to convert. /// The command context. Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => Task.FromResult(Optional.Some(value)); } /// /// Represents a uri converter. /// public class UriConverter : IArgumentConverter { + public Task> ConvertAsync(string value, HybridCommandContext ctx) + { + try + { + if (value.StartsWith("<") && value.EndsWith(">")) + value = value[1..^1]; + + return Task.FromResult(Optional.Some(new Uri(value))); + } + catch + { + return Task.FromResult(Optional.None); + } + } + /// /// Converts a string. /// /// The string to convert. /// The command context. Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) { try { if (value.StartsWith("<") && value.EndsWith(">")) value = value[1..^1]; return Task.FromResult(Optional.Some(new Uri(value))); } catch { return Task.FromResult(Optional.None); } } } diff --git a/DisCatSharp.CommandsNext/Converters/TimeConverters.cs b/DisCatSharp.CommandsNext/Converters/TimeConverters.cs index 173f97455..bd4452f5f 100644 --- a/DisCatSharp.CommandsNext/Converters/TimeConverters.cs +++ b/DisCatSharp.CommandsNext/Converters/TimeConverters.cs @@ -1,141 +1,206 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-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 NONINFRINGEMENT. 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.Globalization; using System.Text.RegularExpressions; using System.Threading.Tasks; using DisCatSharp.Common.RegularExpressions; using DisCatSharp.Entities; +using DisCatSharp.HybridCommands.Entities; namespace DisCatSharp.CommandsNext.Converters; /// /// Represents a date time converter. /// public class DateTimeConverter : IArgumentConverter { + public Task> ConvertAsync(string value, HybridCommandContext ctx) => + DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.None, out var result) + ? Task.FromResult(Optional.Some(result)) + : Task.FromResult(Optional.None); + /// /// Converts a string. /// /// The string to convert. /// The command context. Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.None, out var result) ? Task.FromResult(Optional.Some(result)) : Task.FromResult(Optional.None); } /// /// Represents a date time offset converter. /// public class DateTimeOffsetConverter : IArgumentConverter { + public Task> ConvertAsync(string value, HybridCommandContext ctx) => + DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.None, out var result) + ? Task.FromResult(Optional.Some(result)) + : Task.FromResult(Optional.None); + /// /// Converts a string. /// /// The string to convert. /// The command context. Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.None, out var result) ? Task.FromResult(Optional.Some(result)) : Task.FromResult(Optional.None); } /// /// Represents a time span converter. /// public class TimeSpanConverter : IArgumentConverter { /// /// Gets or sets the time span regex. /// private static Regex s_timeSpanRegex { get; set; } /// /// Initializes a new instance of the class. /// static TimeSpanConverter() { s_timeSpanRegex = CommonRegEx.TimeSpan; } /// /// Converts a string. /// /// The string to convert. /// The command context. Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) { if (value == "0") return Task.FromResult(Optional.Some(TimeSpan.Zero)); if (int.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out _)) return Task.FromResult(Optional.None); if (!ctx.Config.CaseSensitive) value = value.ToLowerInvariant(); if (TimeSpan.TryParse(value, CultureInfo.InvariantCulture, out var result)) return Task.FromResult(Optional.Some(result)); var gps = new string[] { "days", "hours", "minutes", "seconds" }; var mtc = s_timeSpanRegex.Match(value); if (!mtc.Success) return Task.FromResult(Optional.None); var d = 0; var h = 0; var m = 0; var s = 0; foreach (var gp in gps) { var gpc = mtc.Groups[gp].Value; if (string.IsNullOrWhiteSpace(gpc)) continue; var gpt = gpc[^1]; int.TryParse(gpc[0..^1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var val); switch (gpt) { case 'd': d = val; break; case 'h': h = val; break; case 'm': m = val; break; case 's': s = val; break; } } result = new TimeSpan(d, h, m, s); return Task.FromResult(Optional.Some(result)); } + + public Task> ConvertAsync(string value, HybridCommandContext ctx) + { + if (value == "0") + return Task.FromResult(Optional.Some(TimeSpan.Zero)); + + if (int.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out _)) + return Task.FromResult(Optional.None); + + if (!ctx.Client.GetCommandsNext()._config.CaseSensitive) + value = value.ToLowerInvariant(); + + if (TimeSpan.TryParse(value, CultureInfo.InvariantCulture, out var result)) + return Task.FromResult(Optional.Some(result)); + + var gps = new string[] { "days", "hours", "minutes", "seconds" }; + var mtc = s_timeSpanRegex.Match(value); + if (!mtc.Success) + return Task.FromResult(Optional.None); + + var d = 0; + var h = 0; + var m = 0; + var s = 0; + foreach (var gp in gps) + { + var gpc = mtc.Groups[gp].Value; + if (string.IsNullOrWhiteSpace(gpc)) + continue; + + var gpt = gpc[^1]; + int.TryParse(gpc[0..^1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var val); + switch (gpt) + { + case 'd': + d = val; + break; + + case 'h': + h = val; + break; + + case 'm': + m = val; + break; + + case 's': + s = val; + break; + } + } + result = new TimeSpan(d, h, m, s); + return Task.FromResult(Optional.Some(result)); + } } diff --git a/DisCatSharp.CommandsNext/Entities/Builders/CommandBuilder.cs b/DisCatSharp.CommandsNext/Entities/Builders/CommandBuilder.cs index 081c7ca0d..81dac1840 100644 --- a/DisCatSharp.CommandsNext/Entities/Builders/CommandBuilder.cs +++ b/DisCatSharp.CommandsNext/Entities/Builders/CommandBuilder.cs @@ -1,305 +1,314 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-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 NONINFRINGEMENT. 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.ObjectModel; using System.Linq; using DisCatSharp.CommandsNext.Attributes; using DisCatSharp.CommandsNext.Entities; using DisCatSharp.CommandsNext.Exceptions; namespace DisCatSharp.CommandsNext.Builders; /// /// Represents an interface to build a command. /// public class CommandBuilder { /// /// Gets the name set for this command. /// public string Name { get; private set; } /// /// Gets the aliases set for this command. /// public IReadOnlyList Aliases { get; } /// /// Gets the alias list. /// private readonly List _aliasList; /// /// Gets the description set for this command. /// public string Description { get; private set; } /// /// Gets whether this command will be hidden or not. /// public bool IsHidden { get; private set; } /// /// Gets the execution checks defined for this command. /// public IReadOnlyList ExecutionChecks { get; } /// /// Gets the execution check list. /// private readonly List _executionCheckList; /// /// Gets the collection of this command's overloads. /// public IReadOnlyList Overloads { get; } /// /// Gets the overload list. /// private readonly List _overloadList; /// /// Gets the overload argument sets. /// private readonly HashSet _overloadArgumentSets; /// /// Gets the module on which this command is to be defined. /// public ICommandModule Module { get; } /// /// Gets custom attributes defined on this command. /// public IReadOnlyList CustomAttributes { get; } /// /// Gets the custom attribute list. /// private readonly List _customAttributeList; + internal bool IsHybrid { get; set; } + /// /// Creates a new module-less command builder. /// public CommandBuilder() : this(null) { } /// /// Creates a new command builder. /// /// Module on which this command is to be defined. public CommandBuilder(ICommandModule module) { this._aliasList = new List(); this.Aliases = new ReadOnlyCollection(this._aliasList); this._executionCheckList = new List(); this.ExecutionChecks = new ReadOnlyCollection(this._executionCheckList); this._overloadArgumentSets = new HashSet(); this._overloadList = new List(); this.Overloads = new ReadOnlyCollection(this._overloadList); this.Module = module; this._customAttributeList = new List(); this.CustomAttributes = new ReadOnlyCollection(this._customAttributeList); } + public CommandBuilder AsHybrid(bool a) + { + this.IsHybrid = a; + return this; + } + /// /// Sets the name for this command. /// /// Name for this command. /// This builder. public CommandBuilder WithName(string name) { if (name == null || name.ToCharArray().Any(xc => char.IsWhiteSpace(xc))) throw new ArgumentException("Command name cannot be null or contain any whitespace characters.", nameof(name)); if (this.Name != null) throw new InvalidOperationException("This command already has a name."); if (this._aliasList.Contains(name)) throw new ArgumentException("Command name cannot be one of its aliases.", nameof(name)); this.Name = name; return this; } /// /// Adds aliases to this command. /// /// Aliases to add to the command. /// This builder. public CommandBuilder WithAliases(params string[] aliases) { if (aliases == null || !aliases.Any()) throw new ArgumentException("You need to pass at least one alias.", nameof(aliases)); foreach (var alias in aliases) this.WithAlias(alias); return this; } /// /// Adds an alias to this command. /// /// Alias to add to the command. /// This builder. public CommandBuilder WithAlias(string alias) { if (alias.ToCharArray().Any(xc => char.IsWhiteSpace(xc))) throw new ArgumentException("Aliases cannot contain whitespace characters or null strings.", nameof(alias)); if (this.Name == alias || this._aliasList.Contains(alias)) throw new ArgumentException("Aliases cannot contain the command name, and cannot be duplicate.", nameof(alias)); this._aliasList.Add(alias); return this; } /// /// Sets the description for this command. /// /// Description to use for this command. /// This builder. public CommandBuilder WithDescription(string description) { this.Description = description; return this; } /// /// Sets whether this command is to be hidden. /// /// Whether the command is to be hidden. /// This builder. public CommandBuilder WithHiddenStatus(bool hidden) { this.IsHidden = hidden; return this; } /// /// Adds pre-execution checks to this command. /// /// Pre-execution checks to add to this command. /// This builder. public CommandBuilder WithExecutionChecks(params CheckBaseAttribute[] checks) { this._executionCheckList.AddRange(checks.Except(this._executionCheckList)); return this; } /// /// Adds a pre-execution check to this command. /// /// Pre-execution check to add to this command. /// This builder. public CommandBuilder WithExecutionCheck(CheckBaseAttribute check) { if (!this._executionCheckList.Contains(check)) this._executionCheckList.Add(check); return this; } /// /// Adds overloads to this command. An executable command needs to have at least one overload. /// /// Overloads to add to this command. /// This builder. public CommandBuilder WithOverloads(params CommandOverloadBuilder[] overloads) { foreach (var overload in overloads) this.WithOverload(overload); return this; } /// /// Adds an overload to this command. An executable command needs to have at least one overload. /// /// Overload to add to this command. /// This builder. public CommandBuilder WithOverload(CommandOverloadBuilder overload) { if (this._overloadArgumentSets.Contains(overload.ArgumentSet)) throw new DuplicateOverloadException(this.Name, overload.Arguments.Select(x => x.Type).ToList(), overload.ArgumentSet); this._overloadArgumentSets.Add(overload.ArgumentSet); this._overloadList.Add(overload); return this; } /// /// Adds a custom attribute to this command. This can be used to indicate various custom information about a command. /// /// Attribute to add. /// This builder. public CommandBuilder WithCustomAttribute(Attribute attribute) { this._customAttributeList.Add(attribute); return this; } /// /// Adds multiple custom attributes to this command. This can be used to indicate various custom information about a command. /// /// Attributes to add. /// This builder. public CommandBuilder WithCustomAttributes(params Attribute[] attributes) { foreach (var attr in attributes) this.WithCustomAttribute(attr); return this; } /// /// Builds the command. /// /// The parent command group. internal virtual Command Build(CommandGroup parent) { var cmd = new Command { Name = this.Name, Description = this.Description, Aliases = this.Aliases, ExecutionChecks = this.ExecutionChecks, IsHidden = this.IsHidden, Parent = parent, Overloads = new ReadOnlyCollection(this.Overloads.Select(xo => xo.Build()).ToList()), Module = this.Module, - CustomAttributes = this.CustomAttributes + CustomAttributes = this.CustomAttributes, + IsHybrid = this.IsHybrid, }; return cmd; } } diff --git a/DisCatSharp.CommandsNext/Entities/Builders/CommandOverloadBuilder.cs b/DisCatSharp.CommandsNext/Entities/Builders/CommandOverloadBuilder.cs index d129cd2d1..df35d8215 100644 --- a/DisCatSharp.CommandsNext/Entities/Builders/CommandOverloadBuilder.cs +++ b/DisCatSharp.CommandsNext/Entities/Builders/CommandOverloadBuilder.cs @@ -1,193 +1,198 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-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 NONINFRINGEMENT. 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.ObjectModel; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Text; using DisCatSharp.CommandsNext.Attributes; using DisCatSharp.CommandsNext.Exceptions; +using DisCatSharp.HybridCommands.Entities; namespace DisCatSharp.CommandsNext.Builders; /// /// Represents an interface to build a command overload. /// public sealed class CommandOverloadBuilder { /// /// Gets a value that uniquely identifies an overload. /// internal string ArgumentSet { get; } /// /// Gets the collection of arguments this overload takes. /// public IReadOnlyList Arguments { get; } /// /// Gets this overload's priority when picking a suitable one for execution. /// public int Priority { get; set; } /// /// Gets the overload's callable delegate. /// public Delegate Callable { get; set; } /// /// Gets the invocation target. /// private readonly object _invocationTarget; /// /// Creates a new command overload builder from specified method. /// /// Method to use for this overload. public CommandOverloadBuilder(MethodInfo method) : this(method, null) { } /// /// Creates a new command overload builder from specified delegate. /// /// Delegate to use for this overload. public CommandOverloadBuilder(Delegate method) : this(method.GetMethodInfo(), method.Target) { } /// /// Prevents a default instance of the class from being created. /// /// The method. /// The target. private CommandOverloadBuilder(MethodInfo method, object target) { - if (!method.IsCommandCandidate(out var prms)) + if (!method.IsCommandCandidate(out var prms, out var isHybrid)) throw new ArgumentException("Specified method is not suitable for a command.", nameof(method)); this._invocationTarget = target; // create the argument array var ea = new ParameterExpression[prms.Length + 1]; var iep = Expression.Parameter(target?.GetType() ?? method.DeclaringType, "instance"); ea[0] = iep; - ea[1] = Expression.Parameter(typeof(CommandContext), "ctx"); + + if (isHybrid) + ea[1] = Expression.Parameter(typeof(HybridCommandContext), "ctx"); + else + ea[1] = Expression.Parameter(typeof(CommandContext), "ctx"); var pri = method.GetCustomAttribute(); if (pri != null) this.Priority = pri.Priority; var i = 2; var args = new List(prms.Length - 1); var setb = new StringBuilder(); foreach (var arg in prms.Skip(1)) { setb.Append(arg.ParameterType).Append(';'); var ca = new CommandArgument { Name = arg.Name, Type = arg.ParameterType, IsOptional = arg.IsOptional, DefaultValue = arg.IsOptional ? arg.DefaultValue : null }; var attrsCustom = new List(); var attrs = arg.GetCustomAttributes(); var isParams = false; foreach (var xa in attrs) { switch (xa) { case DescriptionAttribute d: ca.Description = d.Description; break; case RemainingTextAttribute r: ca.IsCatchAll = true; break; case ParamArrayAttribute p: ca.IsCatchAll = true; ca.Type = arg.ParameterType.GetElementType(); ca.IsArray = true; isParams = true; break; default: attrsCustom.Add(xa); break; } } if (i > 2 && !ca.IsOptional && !ca.IsCatchAll && args[i - 3].IsOptional) throw new InvalidOverloadException("Non-optional argument cannot appear after an optional one", method, arg); if (arg.ParameterType.IsArray && !isParams) throw new InvalidOverloadException("Cannot use array arguments without params modifier.", method, arg); ca.CustomAttributes = new ReadOnlyCollection(attrsCustom); args.Add(ca); ea[i++] = Expression.Parameter(arg.ParameterType, arg.Name); } //var ec = Expression.Call(iev, method, ea.Skip(2)); var ec = Expression.Call(iep, method, ea.Skip(1)); var el = Expression.Lambda(ec, ea); this.ArgumentSet = setb.ToString(); this.Arguments = new ReadOnlyCollection(args); this.Callable = el.Compile(); } /// /// Sets the priority for this command overload. /// /// Priority for this command overload. /// This builder. public CommandOverloadBuilder WithPriority(int priority) { this.Priority = priority; return this; } /// /// Builds the command overload. /// internal CommandOverload Build() { var ovl = new CommandOverload() { Arguments = this.Arguments, Priority = this.Priority, Callable = this.Callable, InvocationTarget = this._invocationTarget }; return ovl; } } diff --git a/DisCatSharp.CommandsNext/Entities/Command.cs b/DisCatSharp.CommandsNext/Entities/Command.cs index 3b2352209..0c8b95641 100644 --- a/DisCatSharp.CommandsNext/Entities/Command.cs +++ b/DisCatSharp.CommandsNext/Entities/Command.cs @@ -1,232 +1,305 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-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 NONINFRINGEMENT. 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.Linq; using System.Threading.Tasks; using DisCatSharp.CommandsNext.Attributes; using DisCatSharp.CommandsNext.Entities; +using DisCatSharp.HybridCommands.Entities; namespace DisCatSharp.CommandsNext; /// /// Represents a command. /// public class Command { /// /// Gets this command's name. /// public string Name { get; internal set; } /// /// Gets this command's qualified name (i.e. one that includes all module names). /// public string QualifiedName => this.Parent != null ? string.Concat(this.Parent.QualifiedName, " ", this.Name) : this.Name; /// /// Gets this command's aliases. /// public IReadOnlyList Aliases { get; internal set; } /// /// Gets this command's parent module, if any. /// public CommandGroup Parent { get; internal set; } /// /// Gets this command's description. /// public string Description { get; internal set; } /// /// Gets whether this command is hidden. /// public bool IsHidden { get; internal set; } /// /// Gets a collection of pre-execution checks for this command. /// public IReadOnlyList ExecutionChecks { get; internal set; } /// /// Gets a collection of this command's overloads. /// public IReadOnlyList Overloads { get; internal set; } /// /// Gets the module in which this command is defined. /// public ICommandModule Module { get; internal set; } /// /// Gets the custom attributes defined on this command. /// public IReadOnlyList CustomAttributes { get; internal set; } + internal bool IsHybrid { get; set; } + /// /// Initializes a new instance of the class. /// internal Command() { } /// /// Executes this command with specified context. /// /// Context to execute the command in. /// Command's execution results. public virtual async Task ExecuteAsync(CommandContext ctx) { CommandResult res = default; try { var executed = false; foreach (var ovl in this.Overloads.OrderByDescending(x => x.Priority)) { ctx.Overload = ovl; var args = await CommandsNextUtilities.BindArguments(ctx, ctx.Config.IgnoreExtraArguments).ConfigureAwait(false); if (!args.IsSuccessful) continue; ctx.RawArguments = args.Raw; var mdl = ovl.InvocationTarget ?? this.Module?.GetInstance(ctx.Services); if (mdl is BaseCommandModule bcmBefore) await bcmBefore.BeforeExecutionAsync(ctx).ConfigureAwait(false); args.Converted[0] = mdl; var ret = (Task)ovl.Callable.DynamicInvoke(args.Converted); await ret.ConfigureAwait(false); executed = true; res = new CommandResult { IsSuccessful = true, - Context = ctx + CommandContext = ctx + }; + + if (mdl is BaseCommandModule bcmAfter) + await bcmAfter.AfterExecutionAsync(ctx).ConfigureAwait(false); + break; + } + + if (!executed) + throw new ArgumentException("Could not find a suitable overload for the command."); + } + catch (Exception ex) + { + res = new CommandResult + { + IsSuccessful = false, + Exception = ex, + CommandContext = ctx + }; + } + + return res; + } + + /// + /// Executes this command with specified context. + /// + /// Context to execute the command in. + /// Command's execution results. + public virtual async Task ExecuteAsync(HybridCommandContext ctx, Command cmd, string rawArgs) + { + CommandResult res = default; + try + { + var executed = false; + foreach (var ovl in this.Overloads.OrderByDescending(x => x.Priority)) + { + var args = await CommandsNextUtilities.BindArguments(ctx, cmd, ovl, rawArgs, true).ConfigureAwait(false); + + if (!args.IsSuccessful) + continue; + + var mdl = ovl.InvocationTarget ?? this.Module?.GetInstance(ctx.Services); + if (mdl is BaseCommandModule bcmBefore) + await bcmBefore.BeforeExecutionAsync(ctx).ConfigureAwait(false); + + args.Converted[0] = mdl; + var ret = (Task)ovl.Callable.DynamicInvoke(args.Converted); + await ret.ConfigureAwait(false); + executed = true; + res = new CommandResult + { + IsSuccessful = true, + HybridContext = ctx }; if (mdl is BaseCommandModule bcmAfter) await bcmAfter.AfterExecutionAsync(ctx).ConfigureAwait(false); break; } if (!executed) throw new ArgumentException("Could not find a suitable overload for the command."); } catch (Exception ex) { res = new CommandResult { IsSuccessful = false, Exception = ex, - Context = ctx + HybridContext = ctx }; } return res; } /// /// Runs pre-execution checks for this command and returns any that fail for given context. /// /// Context in which the command is executed. /// Whether this check is being executed from help or not. This can be used to probe whether command can be run without setting off certain fail conditions (such as cooldowns). /// Pre-execution checks that fail for given context. public async Task> RunChecksAsync(CommandContext ctx, bool help) { var fchecks = new List(); if (this.ExecutionChecks != null && this.ExecutionChecks.Any()) foreach (var ec in this.ExecutionChecks) if (!await ec.ExecuteCheckAsync(ctx, help).ConfigureAwait(false)) fchecks.Add(ec); return fchecks; } + /// + /// Runs pre-execution checks for this command and returns any that fail for given context. + /// + /// Context in which the command is executed. + /// Whether this check is being executed from help or not. This can be used to probe whether command can be run without setting off certain fail conditions (such as cooldowns). + /// Pre-execution checks that fail for given context. + public async Task> RunChecksAsync(HybridCommandContext ctx, bool help) + { + var fchecks = new List(); + if (this.ExecutionChecks != null && this.ExecutionChecks.Any()) + foreach (var ec in this.ExecutionChecks) + if (!await ec.ExecuteCheckAsync(ctx, help).ConfigureAwait(false)) + fchecks.Add(ec); + + return fchecks; + } + /// /// Checks whether this command is equal to another one. /// /// Command to compare to. /// Command to compare. /// Whether the two commands are equal. public static bool operator ==(Command cmd1, Command cmd2) { var o1 = cmd1 as object; var o2 = cmd2 as object; if (o1 == null && o2 != null) return false; else if (o1 != null && o2 == null) return false; else if (o1 == null && o2 == null) return true; return cmd1.QualifiedName == cmd2.QualifiedName; } /// /// Checks whether this command is not equal to another one. /// /// Command to compare to. /// Command to compare. /// Whether the two commands are not equal. public static bool operator !=(Command cmd1, Command cmd2) => !(cmd1 == cmd2); /// /// Checks whether this command equals another object. /// /// Object to compare to. /// Whether this command is equal to another object. public override bool Equals(object obj) { var o1 = obj; var o2 = this as object; if (o1 == null && o2 != null) return false; else if (o1 != null && o2 == null) return false; else if (o1 == null && o2 == null) return true; return obj is Command cmd && cmd.QualifiedName == this.QualifiedName; } /// /// Gets this command's hash code. /// /// This command's hash code. public override int GetHashCode() => this.QualifiedName.GetHashCode(); /// /// Returns a string representation of this command. /// /// String representation of this command. public override string ToString() => this is CommandGroup g ? $"Command Group: {this.QualifiedName}, {g.Children.Count} top-level children" : $"Command: {this.QualifiedName}"; } diff --git a/DisCatSharp.CommandsNext/Entities/CommandGroup.cs b/DisCatSharp.CommandsNext/Entities/CommandGroup.cs index 765741eba..f9417792b 100644 --- a/DisCatSharp.CommandsNext/Entities/CommandGroup.cs +++ b/DisCatSharp.CommandsNext/Entities/CommandGroup.cs @@ -1,103 +1,103 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-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 NONINFRINGEMENT. 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.Linq; using System.Threading.Tasks; using DisCatSharp.CommandsNext.Exceptions; namespace DisCatSharp.CommandsNext; /// /// Represents a command group. /// public class CommandGroup : Command { /// /// Gets all the commands that belong to this module. /// public IReadOnlyList Children { get; internal set; } /// /// Gets whether this command is executable without subcommands. /// public bool IsExecutableWithoutSubcommands => this.Overloads?.Any() == true; /// /// Initializes a new instance of the class. /// internal CommandGroup() : base() { } /// /// Executes this command or its subcommand with specified context. /// /// Context to execute the command in. /// Command's execution results. public override async Task ExecuteAsync(CommandContext ctx) { var findPos = 0; var cn = CommandsNextUtilities.ExtractNextArgument(ctx.RawArgumentString, ref findPos); if (cn != null) { var cmd = ctx.Config.CaseSensitive ? this.Children.FirstOrDefault(xc => xc.Name == cn || (xc.Aliases != null && xc.Aliases.Contains(cn))) : this.Children.FirstOrDefault(xc => xc.Name.ToLowerInvariant() == cn.ToLowerInvariant() || (xc.Aliases != null && xc.Aliases.Select(xs => xs.ToLowerInvariant()).Contains(cn.ToLowerInvariant()))); if (cmd != null) { // pass the execution on var xctx = new CommandContext { Client = ctx.Client, Message = ctx.Message, Command = cmd, Config = ctx.Config, RawArgumentString = ctx.RawArgumentString[findPos..], Prefix = ctx.Prefix, CommandsNext = ctx.CommandsNext, Services = ctx.Services }; var fchecks = await cmd.RunChecksAsync(xctx, false).ConfigureAwait(false); return fchecks.Any() ? new CommandResult { IsSuccessful = false, Exception = new ChecksFailedException(cmd, xctx, fchecks), - Context = xctx + CommandContext = xctx } : await cmd.ExecuteAsync(xctx).ConfigureAwait(false); } } return !this.IsExecutableWithoutSubcommands ? new CommandResult { IsSuccessful = false, Exception = new InvalidOperationException("No matching subcommands were found, and this group is not executable."), - Context = ctx + CommandContext = ctx } : await base.ExecuteAsync(ctx).ConfigureAwait(false); } } diff --git a/DisCatSharp.CommandsNext/Entities/CommandResult.cs b/DisCatSharp.CommandsNext/Entities/CommandResult.cs index 2b33aa04c..a9413af45 100644 --- a/DisCatSharp.CommandsNext/Entities/CommandResult.cs +++ b/DisCatSharp.CommandsNext/Entities/CommandResult.cs @@ -1,46 +1,53 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-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 NONINFRINGEMENT. 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.Entities; + namespace DisCatSharp.CommandsNext; /// /// Represents a command's execution result. /// public struct CommandResult { /// /// Gets whether the command execution succeeded. /// public bool IsSuccessful { get; internal set; } /// /// Gets the exception (if any) that occurred when executing the command. /// public Exception Exception { get; internal set; } /// /// Gets the context in which the command was executed. /// - public CommandContext Context { get; internal set; } + public CommandContext CommandContext { get; internal set; } + + /// + /// Gets the context in which the command was executed. + /// + public HybridCommandContext HybridContext { get; internal set; } } diff --git a/DisCatSharp.CommandsNext/EventArgs/CommandContext.cs b/DisCatSharp.CommandsNext/EventArgs/CommandContext.cs index cf64c5344..af188ba8d 100644 --- a/DisCatSharp.CommandsNext/EventArgs/CommandContext.cs +++ b/DisCatSharp.CommandsNext/EventArgs/CommandContext.cs @@ -1,207 +1,209 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-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 NONINFRINGEMENT. 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.Entities; using Microsoft.Extensions.DependencyInjection; namespace DisCatSharp.CommandsNext; /// /// Represents a context in which a command is executed. /// public sealed class CommandContext { /// /// Gets the client which received the message. /// public DiscordClient Client { get; internal set; } /// /// Gets the message that triggered the execution. /// public DiscordMessage Message { get; internal set; } /// /// Gets the channel in which the execution was triggered, /// public DiscordChannel Channel => this.Message.Channel; /// /// Gets the guild in which the execution was triggered. This property is null for commands sent over direct messages. /// public DiscordGuild Guild => this.Message.GuildId.HasValue ? this.Message.Guild : null; /// /// Gets the user who triggered the execution. /// public DiscordUser User => this.Message.Author; /// /// Gets the member who triggered the execution. This property is null for commands sent over direct messages. /// public DiscordMember Member => this._lazyMember.Value; private readonly Lazy _lazyMember; /// /// Gets the CommandsNext service instance that handled this command. /// public CommandsNextExtension CommandsNext { get; internal set; } /// /// Gets the service provider for this CNext instance. /// public IServiceProvider Services { get; internal set; } /// /// Gets the command that is being executed. /// public Command Command { get; internal set; } /// /// Gets the overload of the command that is being executed. /// public CommandOverload Overload { get; internal set; } /// /// Gets the list of raw arguments passed to the command. /// public IReadOnlyList RawArguments { get; internal set; } /// /// Gets the raw string from which the arguments were extracted. /// public string RawArgumentString { get; internal set; } /// /// Gets the prefix used to invoke the command. /// public string Prefix { get; internal set; } /// /// Gets or sets the config. /// internal CommandsNextConfiguration Config { get; set; } /// /// Gets or sets the service scope context. /// internal ServiceContext ServiceScopeContext { get; set; } + internal bool IsHybrid { get; set; } + /// /// Initializes a new instance of the class. /// internal CommandContext() { this._lazyMember = new Lazy(() => this.Guild != null && this.Guild.Members.TryGetValue(this.User.Id, out var member) ? member : this.Guild?.GetMemberAsync(this.User.Id).ConfigureAwait(false).GetAwaiter().GetResult()); } /// /// Quickly respond to the message that triggered the command. /// /// Message to respond with. /// public Task RespondAsync(string content) => this.Message.RespondAsync(content); /// /// Quickly respond to the message that triggered the command. /// /// Embed to attach. /// public Task RespondAsync(DiscordEmbed embed) => this.Message.RespondAsync(embed); /// /// Quickly respond to the message that triggered the command. /// /// Message to respond with. /// Embed to attach. /// public Task RespondAsync(string content, DiscordEmbed embed) => this.Message.RespondAsync(content, embed); /// /// Quickly respond to the message that triggered the command. /// /// The Discord Message builder. /// public Task RespondAsync(DiscordMessageBuilder builder) => this.Message.RespondAsync(builder); /// /// Quickly respond to the message that triggered the command. /// /// The Discord Message builder. /// public Task RespondAsync(Action action) => this.Message.RespondAsync(action); /// /// Triggers typing in the channel containing the message that triggered the command. /// /// public Task TriggerTypingAsync() => this.Channel.TriggerTypingAsync(); internal readonly struct ServiceContext : IDisposable { /// /// Gets the provider. /// public IServiceProvider Provider { get; } /// /// Gets the scope. /// public IServiceScope Scope { get; } /// /// Gets a value indicating whether is initialized. /// public bool IsInitialized { get; } /// /// Initializes a new instance of the class. /// /// The services. /// The scope. public ServiceContext(IServiceProvider services, IServiceScope scope) { this.Provider = services; this.Scope = scope; this.IsInitialized = true; } /// /// Disposes the command context. /// public void Dispose() => this.Scope?.Dispose(); } } diff --git a/DisCatSharp.CommandsNext/Exceptions/ChecksFailedException.cs b/DisCatSharp.CommandsNext/Exceptions/ChecksFailedException.cs index b2763f8b9..38d903f99 100644 --- a/DisCatSharp.CommandsNext/Exceptions/ChecksFailedException.cs +++ b/DisCatSharp.CommandsNext/Exceptions/ChecksFailedException.cs @@ -1,64 +1,84 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-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 NONINFRINGEMENT. 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.ObjectModel; using DisCatSharp.CommandsNext.Attributes; +using DisCatSharp.HybridCommands.Entities; namespace DisCatSharp.CommandsNext.Exceptions; /// /// Indicates that one or more checks for given command have failed. /// public class ChecksFailedException : Exception { /// /// Gets the command that was executed. /// public Command Command { get; } /// /// Gets the context in which given command was executed. /// - public CommandContext Context { get; } + public CommandContext CommandContext { get; } + + /// + /// Gets the context in which given command was executed. + /// + public HybridCommandContext HybridContext { get; } /// /// Gets the checks that failed. /// public IReadOnlyList FailedChecks { get; } /// /// Creates a new . /// /// Command that failed to execute. /// Context in which the command was executed. /// A collection of checks that failed. public ChecksFailedException(Command command, CommandContext ctx, IEnumerable failedChecks) : base("One or more pre-execution checks failed.") { this.Command = command; - this.Context = ctx; + this.CommandContext = ctx; + this.FailedChecks = new ReadOnlyCollection(new List(failedChecks)); + } + + /// + /// Creates a new . + /// + /// Command that failed to execute. + /// Context in which the command was executed. + /// A collection of checks that failed. + public ChecksFailedException(Command command, HybridCommandContext ctx, IEnumerable failedChecks) + : base("One or more pre-execution checks failed.") + { + this.Command = command; + this.HybridContext = ctx; this.FailedChecks = new ReadOnlyCollection(new List(failedChecks)); } }