diff --git a/DisCatSharp.CommandsNext/Attributes/RequireBotPermissionsAttribute.cs b/DisCatSharp.CommandsNext/Attributes/RequireBotPermissionsAttribute.cs index 5d560528b..f5149579b 100644 --- a/DisCatSharp.CommandsNext/Attributes/RequireBotPermissionsAttribute.cs +++ b/DisCatSharp.CommandsNext/Attributes/RequireBotPermissionsAttribute.cs @@ -1,78 +1,83 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 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; 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 pbot = ctx.Channel.PermissionsFor(bot); + 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/RequirePermissionsAttribute.cs b/DisCatSharp.CommandsNext/Attributes/RequirePermissionsAttribute.cs index 9362133f6..c3ee201a2 100644 --- a/DisCatSharp.CommandsNext/Attributes/RequirePermissionsAttribute.cs +++ b/DisCatSharp.CommandsNext/Attributes/RequirePermissionsAttribute.cs @@ -1,88 +1,94 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 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; 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 = ctx.Channel.PermissionsFor(usr); + var pusr = channel.PermissionsFor(usr); var bot = await ctx.Guild.GetMemberAsync(ctx.Client.CurrentUser.Id).ConfigureAwait(false); if (bot == null) return false; - var pbot = ctx.Channel.PermissionsFor(bot); + 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/EventArgs/CommandContext.cs b/DisCatSharp.CommandsNext/EventArgs/CommandContext.cs index 3fa4d24cf..63ade7030 100644 --- a/DisCatSharp.CommandsNext/EventArgs/CommandContext.cs +++ b/DisCatSharp.CommandsNext/EventArgs/CommandContext.cs @@ -1,207 +1,207 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 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.Channel.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; } /// /// 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/Clients/DiscordClient.cs b/DisCatSharp/Clients/DiscordClient.cs index a257d5a3a..097b3fac5 100644 --- a/DisCatSharp/Clients/DiscordClient.cs +++ b/DisCatSharp/Clients/DiscordClient.cs @@ -1,1587 +1,1591 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 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.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using DisCatSharp.Common.Utilities; using DisCatSharp.Entities; using DisCatSharp.Enums; using DisCatSharp.EventArgs; using DisCatSharp.Exceptions; using DisCatSharp.Net; using DisCatSharp.Net.Abstractions; using DisCatSharp.Net.Models; using DisCatSharp.Net.Serialization; using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; namespace DisCatSharp; /// /// A Discord API wrapper. /// public sealed partial class DiscordClient : BaseDiscordClient { #region Internal Fields/Properties internal bool IsShard = false; /// /// Gets the message cache. /// internal RingBuffer MessageCache { get; } private List _extensions = new(); private StatusUpdate _status; /// /// Gets the connection lock. /// private readonly ManualResetEventSlim _connectionLock = new(true); #endregion #region Public Fields/Properties /// /// Gets the gateway protocol version. /// public int GatewayVersion { get; internal set; } /// /// Gets the gateway session information for this client. /// public GatewayInfo GatewayInfo { get; internal set; } /// /// Gets the gateway URL. /// public Uri GatewayUri { get; internal set; } /// /// Gets the total number of shards the bot is connected to. /// public int ShardCount => this.GatewayInfo != null ? this.GatewayInfo.ShardCount : this.Configuration.ShardCount; /// /// Gets the currently connected shard ID. /// public int ShardId => this.Configuration.ShardId; /// /// Gets the intents configured for this client. /// public DiscordIntents Intents => this.Configuration.Intents; /// /// Gets a dictionary of guilds that this client is in. The dictionary's key is the guild ID. Note that the /// guild objects in this dictionary will not be filled in if the specific guilds aren't available (the /// or events haven't been fired yet) /// public override IReadOnlyDictionary Guilds { get; } internal ConcurrentDictionary GuildsInternal = new(); /// /// Gets the websocket latency for this client. /// public int Ping => Volatile.Read(ref this._ping); private int _ping; /// /// Gets the collection of presences held by this client. /// public IReadOnlyDictionary Presences => this._presencesLazy.Value; internal Dictionary PresencesInternal = new(); private Lazy> _presencesLazy; /// /// Gets the collection of presences held by this client. /// public IReadOnlyDictionary EmbeddedActivities => this._embeddedActivitiesLazy.Value; internal Dictionary EmbeddedActivitiesInternal = new(); private Lazy> _embeddedActivitiesLazy; #endregion #region Constructor/Internal Setup /// /// Initializes a new instance of . /// /// Specifies configuration parameters. public DiscordClient(DiscordConfiguration config) : base(config) { if (this.Configuration.MessageCacheSize > 0) { var intents = this.Configuration.Intents; this.MessageCache = intents.HasIntent(DiscordIntents.GuildMessages) || intents.HasIntent(DiscordIntents.DirectMessages) ? new RingBuffer(this.Configuration.MessageCacheSize) : null; } this.InternalSetup(); this.Guilds = new ReadOnlyConcurrentDictionary(this.GuildsInternal); } /// /// Internal setup of the Client. /// internal void InternalSetup() { this._clientErrored = new AsyncEvent("CLIENT_ERRORED", EventExecutionLimit, this.Goof); this._socketErrored = new AsyncEvent("SOCKET_ERRORED", EventExecutionLimit, this.Goof); this._socketOpened = new AsyncEvent("SOCKET_OPENED", EventExecutionLimit, this.EventErrorHandler); this._socketClosed = new AsyncEvent("SOCKET_CLOSED", EventExecutionLimit, this.EventErrorHandler); this._ready = new AsyncEvent("READY", EventExecutionLimit, this.EventErrorHandler); this._resumed = new AsyncEvent("RESUMED", EventExecutionLimit, this.EventErrorHandler); this._channelCreated = new AsyncEvent("CHANNEL_CREATED", EventExecutionLimit, this.EventErrorHandler); this._channelUpdated = new AsyncEvent("CHANNEL_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._channelDeleted = new AsyncEvent("CHANNEL_DELETED", EventExecutionLimit, this.EventErrorHandler); this._dmChannelDeleted = new AsyncEvent("DM_CHANNEL_DELETED", EventExecutionLimit, this.EventErrorHandler); this._channelPinsUpdated = new AsyncEvent("CHANNEL_PINS_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildCreated = new AsyncEvent("GUILD_CREATED", EventExecutionLimit, this.EventErrorHandler); this._guildAvailable = new AsyncEvent("GUILD_AVAILABLE", EventExecutionLimit, this.EventErrorHandler); this._guildUpdated = new AsyncEvent("GUILD_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildDeleted = new AsyncEvent("GUILD_DELETED", EventExecutionLimit, this.EventErrorHandler); this._guildUnavailable = new AsyncEvent("GUILD_UNAVAILABLE", EventExecutionLimit, this.EventErrorHandler); this._guildDownloadCompletedEv = new AsyncEvent("GUILD_DOWNLOAD_COMPLETED", EventExecutionLimit, this.EventErrorHandler); this._inviteCreated = new AsyncEvent("INVITE_CREATED", EventExecutionLimit, this.EventErrorHandler); this._inviteDeleted = new AsyncEvent("INVITE_DELETED", EventExecutionLimit, this.EventErrorHandler); this._messageCreated = new AsyncEvent("MESSAGE_CREATED", EventExecutionLimit, this.EventErrorHandler); this._presenceUpdated = new AsyncEvent("PRESENCE_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildBanAdded = new AsyncEvent("GUILD_BAN_ADD", EventExecutionLimit, this.EventErrorHandler); this._guildBanRemoved = new AsyncEvent("GUILD_BAN_REMOVED", EventExecutionLimit, this.EventErrorHandler); this._guildEmojisUpdated = new AsyncEvent("GUILD_EMOJI_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildStickersUpdated = new AsyncEvent("GUILD_STICKER_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildIntegrationsUpdated = new AsyncEvent("GUILD_INTEGRATIONS_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildMemberAdded = new AsyncEvent("GUILD_MEMBER_ADD", EventExecutionLimit, this.EventErrorHandler); this._guildMemberRemoved = new AsyncEvent("GUILD_MEMBER_REMOVED", EventExecutionLimit, this.EventErrorHandler); this._guildMemberUpdated = new AsyncEvent("GUILD_MEMBER_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildRoleCreated = new AsyncEvent("GUILD_ROLE_CREATED", EventExecutionLimit, this.EventErrorHandler); this._guildRoleUpdated = new AsyncEvent("GUILD_ROLE_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildRoleDeleted = new AsyncEvent("GUILD_ROLE_DELETED", EventExecutionLimit, this.EventErrorHandler); this._messageAcknowledged = new AsyncEvent("MESSAGE_ACKNOWLEDGED", EventExecutionLimit, this.EventErrorHandler); this._messageUpdated = new AsyncEvent("MESSAGE_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._messageDeleted = new AsyncEvent("MESSAGE_DELETED", EventExecutionLimit, this.EventErrorHandler); this._messagesBulkDeleted = new AsyncEvent("MESSAGE_BULK_DELETED", EventExecutionLimit, this.EventErrorHandler); this._interactionCreated = new AsyncEvent("INTERACTION_CREATED", EventExecutionLimit, this.EventErrorHandler); this._componentInteractionCreated = new AsyncEvent("COMPONENT_INTERACTED", EventExecutionLimit, this.EventErrorHandler); this._contextMenuInteractionCreated = new AsyncEvent("CONTEXT_MENU_INTERACTED", EventExecutionLimit, this.EventErrorHandler); this._typingStarted = new AsyncEvent("TYPING_STARTED", EventExecutionLimit, this.EventErrorHandler); this._userSettingsUpdated = new AsyncEvent("USER_SETTINGS_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._userUpdated = new AsyncEvent("USER_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._voiceStateUpdated = new AsyncEvent("VOICE_STATE_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._voiceServerUpdated = new AsyncEvent("VOICE_SERVER_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildMembersChunked = new AsyncEvent("GUILD_MEMBERS_CHUNKED", EventExecutionLimit, this.EventErrorHandler); this._unknownEvent = new AsyncEvent("UNKNOWN_EVENT", EventExecutionLimit, this.EventErrorHandler); this._messageReactionAdded = new AsyncEvent("MESSAGE_REACTION_ADDED", EventExecutionLimit, this.EventErrorHandler); this._messageReactionRemoved = new AsyncEvent("MESSAGE_REACTION_REMOVED", EventExecutionLimit, this.EventErrorHandler); this._messageReactionsCleared = new AsyncEvent("MESSAGE_REACTIONS_CLEARED", EventExecutionLimit, this.EventErrorHandler); this._messageReactionRemovedEmoji = new AsyncEvent("MESSAGE_REACTION_REMOVED_EMOJI", EventExecutionLimit, this.EventErrorHandler); this._webhooksUpdated = new AsyncEvent("WEBHOOKS_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._heartbeated = new AsyncEvent("HEARTBEATED", EventExecutionLimit, this.EventErrorHandler); this._applicationCommandCreated = new AsyncEvent("APPLICATION_COMMAND_CREATED", EventExecutionLimit, this.EventErrorHandler); this._applicationCommandUpdated = new AsyncEvent("APPLICATION_COMMAND_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._applicationCommandDeleted = new AsyncEvent("APPLICATION_COMMAND_DELETED", EventExecutionLimit, this.EventErrorHandler); this._guildApplicationCommandCountUpdated = new AsyncEvent("GUILD_APPLICATION_COMMAND_COUNTS_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._applicationCommandPermissionsUpdated = new AsyncEvent("APPLICATION_COMMAND_PERMISSIONS_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildIntegrationCreated = new AsyncEvent("INTEGRATION_CREATED", EventExecutionLimit, this.EventErrorHandler); this._guildIntegrationUpdated = new AsyncEvent("INTEGRATION_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildIntegrationDeleted = new AsyncEvent("INTEGRATION_DELETED", EventExecutionLimit, this.EventErrorHandler); this._stageInstanceCreated = new AsyncEvent("STAGE_INSTANCE_CREATED", EventExecutionLimit, this.EventErrorHandler); this._stageInstanceUpdated = new AsyncEvent("STAGE_INSTANCE_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._stageInstanceDeleted = new AsyncEvent("STAGE_INSTANCE_DELETED", EventExecutionLimit, this.EventErrorHandler); this._threadCreated = new AsyncEvent("THREAD_CREATED", EventExecutionLimit, this.EventErrorHandler); this._threadUpdated = new AsyncEvent("THREAD_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._threadDeleted = new AsyncEvent("THREAD_DELETED", EventExecutionLimit, this.EventErrorHandler); this._threadListSynced = new AsyncEvent("THREAD_LIST_SYNCED", EventExecutionLimit, this.EventErrorHandler); this._threadMemberUpdated = new AsyncEvent("THREAD_MEMBER_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._threadMembersUpdated = new AsyncEvent("THREAD_MEMBERS_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._zombied = new AsyncEvent("ZOMBIED", EventExecutionLimit, this.EventErrorHandler); this._payloadReceived = new AsyncEvent("PAYLOAD_RECEIVED", EventExecutionLimit, this.EventErrorHandler); this._guildScheduledEventCreated = new AsyncEvent("GUILD_SCHEDULED_EVENT_CREATED", EventExecutionLimit, this.EventErrorHandler); this._guildScheduledEventUpdated = new AsyncEvent("GUILD_SCHEDULED_EVENT_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildScheduledEventDeleted = new AsyncEvent("GUILD_SCHEDULED_EVENT_DELETED", EventExecutionLimit, this.EventErrorHandler); this._guildScheduledEventUserAdded = new AsyncEvent("GUILD_SCHEDULED_EVENT_USER_ADDED", EventExecutionLimit, this.EventErrorHandler); this._guildScheduledEventUserRemoved = new AsyncEvent("GUILD_SCHEDULED_EVENT_USER_REMOVED", EventExecutionLimit, this.EventErrorHandler); this._embeddedActivityUpdated = new AsyncEvent("EMBEDDED_ACTIVITY_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildMemberTimeoutAdded = new AsyncEvent("GUILD_MEMBER_TIMEOUT_ADDED", EventExecutionLimit, this.EventErrorHandler); this._guildMemberTimeoutChanged = new AsyncEvent("GUILD_MEMBER_TIMEOUT_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildMemberTimeoutRemoved = new AsyncEvent("GUILD_MEMBER_TIMEOUT_REMOVED", EventExecutionLimit, this.EventErrorHandler); this._rateLimitHit = new AsyncEvent("RATELIMIT_HIT", EventExecutionLimit, this.EventErrorHandler); this._automodRuleCreated = new AsyncEvent("AUTO_MODERATION_RULE_CREATED", EventExecutionLimit, this.EventErrorHandler); ; this._automodRuleUpdated = new AsyncEvent("AUTO_MODERATION_RULE_UPDATED", EventExecutionLimit, this.EventErrorHandler); ; this._automodRuleDeleted = new AsyncEvent("AUTO_MODERATION_RULE_DELETED", EventExecutionLimit, this.EventErrorHandler); ; this._automodActionExecuted = new AsyncEvent("AUTO_MODERATION_ACTION_EXECUTED", EventExecutionLimit, this.EventErrorHandler); ; this.GuildsInternal.Clear(); this._presencesLazy = new Lazy>(() => new ReadOnlyDictionary(this.PresencesInternal)); this._embeddedActivitiesLazy = new Lazy>(() => new ReadOnlyDictionary(this.EmbeddedActivitiesInternal)); } #endregion #region Client Extension Methods /// /// Registers an extension with this client. /// /// Extension to register. public void AddExtension(BaseExtension ext) { ext.Setup(this); this._extensions.Add(ext); } /// /// Retrieves a previously registered extension from this client. /// /// The type of extension to retrieve. /// The requested extension. public T GetExtension() where T : BaseExtension => this._extensions.FirstOrDefault(x => x.GetType() == typeof(T)) as T; #endregion #region Public Connection Methods /// /// Connects to the gateway. /// /// The activity to set. Defaults to null. /// The optional status to set. Defaults to null. /// Since when is the client performing the specified activity. Defaults to null. /// Thrown when an invalid token was provided. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task ConnectAsync(DiscordActivity activity = null, UserStatus? status = null, DateTimeOffset? idlesince = null) { // Check if connection lock is already set, and set it if it isn't if (!this._connectionLock.Wait(0)) throw new InvalidOperationException("This client is already connected."); this._connectionLock.Set(); var w = 7500; var i = 5; var s = false; Exception cex = null; if (activity == null && status == null && idlesince == null) this._status = null; else { var sinceUnix = idlesince != null ? (long?)Utilities.GetUnixTime(idlesince.Value) : null; this._status = new StatusUpdate() { Activity = new TransportActivity(activity), Status = status ?? UserStatus.Online, IdleSince = sinceUnix, IsAfk = idlesince != null, ActivityInternal = activity }; } if (!this.IsShard) { if (this.Configuration.TokenType != TokenType.Bot) this.Logger.LogWarning(LoggerEvents.Misc, "You are logging in with a token that is not a bot token. This is not officially supported by Discord, and can result in your account being terminated if you aren't careful."); this.Logger.LogInformation(LoggerEvents.Startup, "Lib {0}, version {1}", this.BotLibrary, this.VersionString); } while (i-- > 0 || this.Configuration.ReconnectIndefinitely) { try { await this.InternalConnectAsync().ConfigureAwait(false); s = true; break; } catch (UnauthorizedException e) { FailConnection(this._connectionLock); throw new Exception("Authentication failed. Check your token and try again.", e); } catch (PlatformNotSupportedException) { FailConnection(this._connectionLock); throw; } catch (NotImplementedException) { FailConnection(this._connectionLock); throw; } catch (Exception ex) { FailConnection(null); cex = ex; if (i <= 0 && !this.Configuration.ReconnectIndefinitely) break; this.Logger.LogError(LoggerEvents.ConnectionFailure, ex, "Connection attempt failed, retrying in {0}s", w / 1000); await Task.Delay(w).ConfigureAwait(false); if (i > 0) w *= 2; } } if (!s && cex != null) { this._connectionLock.Set(); throw new Exception("Could not connect to Discord.", cex); } // non-closure, hence args static void FailConnection(ManualResetEventSlim cl) => // unlock this (if applicable) so we can let others attempt to connect cl?.Set(); } /// /// Reconnects to the gateway. /// /// Whether to start a new session. public Task ReconnectAsync(bool startNewSession = true) => this.InternalReconnectAsync(startNewSession, code: startNewSession ? 1000 : 4002); /// /// Disconnects from the gateway. /// public async Task DisconnectAsync() { this.Configuration.AutoReconnect = false; if (this.WebSocketClient != null) await this.WebSocketClient.DisconnectAsync().ConfigureAwait(false); } #endregion #region Public REST Methods /// /// Gets a user. /// /// Id of the user /// Whether to ignore the cache. Defaults to false. /// The requested user. /// Thrown when the user does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task GetUserAsync(ulong userId, bool fetch = false) { if (!fetch) { return this.TryGetCachedUserInternal(userId, out var usr) ? usr : new DiscordUser { Id = userId, Discord = this }; } else { var usr = await this.ApiClient.GetUserAsync(userId).ConfigureAwait(false); usr = this.UserCache.AddOrUpdate(userId, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; old.BannerHash = usr.BannerHash; old.BannerColorInternal = usr.BannerColorInternal; old.AvatarDecorationHash = usr.AvatarDecorationHash; old.ThemeColorsInternal = usr.ThemeColorsInternal; old.Pronouns = usr.Pronouns; return old; }); return usr; } } /// /// Gets a applications rpc information. /// /// Id of the application /// The requested application. /// Thrown when the application does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task GetRpcApplicationAsync(ulong applicationId) => await this.ApiClient.GetApplicationInfoAsync(applicationId); /// /// Tries to get a user. /// /// Id of the user. /// Whether to ignore the cache. Defaults to true. /// The requested user or null if not found. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. #pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. public async Task TryGetUserAsync(ulong userId, bool fetch = true) #pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. { try { return await this.GetUserAsync(userId, fetch).ConfigureAwait(false); } catch (NotFoundException) { return null; } } /// /// Gets the applications role connection metadata. /// /// A list of metadata records or . public async Task> GetRoleConnectionMetadata() => await this.ApiClient.GetRoleConnectionMetadataRecords(this.CurrentApplication.Id); /// /// Updates the applications role connection metadata. /// /// A list of metadata objects. Max 5. public async Task> UpdateRoleConnectionMetadata(IEnumerable metadata) => await this.ApiClient.UpdateRoleConnectionMetadataRecords(this.CurrentApplication.Id, metadata); /// /// Removes all global application commands. /// public async Task RemoveGlobalApplicationCommandsAsync() => await this.ApiClient.BulkOverwriteGlobalApplicationCommandsAsync(this.CurrentApplication.Id, Array.Empty()); /// /// Removes all global application commands for a specific guild id. /// /// The target guild id. public async Task RemoveGuildApplicationCommandsAsync(ulong guildId) => await this.ApiClient.BulkOverwriteGuildApplicationCommandsAsync(this.CurrentApplication.Id, guildId, Array.Empty()); /// /// Removes all global application commands for a specific guild. /// /// The target guild. public async Task RemoveGuildApplicationCommandsAsync(DiscordGuild guild) => await this.RemoveGuildApplicationCommandsAsync(guild.Id); /// /// Gets a channel. /// /// The id of the channel to get. /// Whether to ignore the cache. Defaults to false. /// The requested channel. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task GetChannelAsync(ulong id, bool fetch = false) => (fetch ? null : this.InternalGetCachedChannel(id)) ?? await this.ApiClient.GetChannelAsync(id).ConfigureAwait(false); /// /// Tries to get a channel. /// /// The id of the channel to get. /// Whether to ignore the cache. Defaults to true. /// The requested channel or null if not found. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. #pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. public async Task TryGetChannelAsync(ulong id, bool fetch = true) #pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. { try { return await this.GetChannelAsync(id, fetch).ConfigureAwait(false); } catch (NotFoundException) { return null; } } /// /// Gets a thread. /// /// The id of the thread to get. /// Whether to ignore the cache. Defaults to false. /// The requested thread. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task GetThreadAsync(ulong id, bool fetch = false) => (fetch ? null : this.InternalGetCachedThread(id)) ?? await this.ApiClient.GetThreadAsync(id).ConfigureAwait(false); /// /// Tries to get a thread. /// /// The id of the thread to get. /// Whether to ignore the cache. Defaults to true. /// The requested thread or null if not found. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. #pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. public async Task TryGetThreadAsync(ulong id, bool fetch = true) #pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. { try { return await this.GetThreadAsync(id, fetch).ConfigureAwait(false); } catch (NotFoundException) { return null; } } /// /// Sends a normal message. /// /// The channel to send to. /// The message content to send. /// The message that was sent. /// Thrown when the client does not have the permission. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task SendMessageAsync(DiscordChannel channel, string content) => this.ApiClient.CreateMessageAsync(channel.Id, content, embeds: null, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false); /// /// Sends a message with an embed. /// /// The channel to send to. /// The embed to attach to the message. /// The message that was sent. /// Thrown when the client does not have the permission. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task SendMessageAsync(DiscordChannel channel, DiscordEmbed embed) => this.ApiClient.CreateMessageAsync(channel.Id, null, embed != null ? new[] { embed } : null, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false); /// /// Sends a message with content and an embed. /// /// Channel to send to. /// The message content to send. /// The embed to attach to the message. /// The message that was sent. /// Thrown when the client does not have the permission. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task SendMessageAsync(DiscordChannel channel, string content, DiscordEmbed embed) => this.ApiClient.CreateMessageAsync(channel.Id, content, embed != null ? new[] { embed } : null, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false); /// /// Sends a message with the . /// /// The channel to send the message to. /// The message builder. /// The message that was sent. /// Thrown when the client does not have the permission if TTS is false and if TTS is true. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task SendMessageAsync(DiscordChannel channel, DiscordMessageBuilder builder) => this.ApiClient.CreateMessageAsync(channel.Id, builder); /// /// Sends a message with an . /// /// The channel to send the message to. /// The message builder. /// The message that was sent. /// Thrown when the client does not have the permission if TTS is false and if TTS is true. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task SendMessageAsync(DiscordChannel channel, Action action) { var builder = new DiscordMessageBuilder(); action(builder); return this.ApiClient.CreateMessageAsync(channel.Id, builder); } /// /// Creates a guild. This requires the bot to be in less than 10 guilds total. /// /// Name of the guild. /// Voice region of the guild. /// Stream containing the icon for the guild. /// Verification level for the guild. /// Default message notification settings for the guild. /// System channel flags for the guild. /// The created guild. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task CreateGuildAsync(string name, string region = null, Optional icon = default, VerificationLevel? verificationLevel = null, DefaultMessageNotifications? defaultMessageNotifications = null, SystemChannelFlags? systemChannelFlags = null) { var iconb64 = ImageTool.Base64FromStream(icon); return this.ApiClient.CreateGuildAsync(name, region, iconb64, verificationLevel, defaultMessageNotifications, systemChannelFlags); } /// /// Creates a guild from a template. This requires the bot to be in less than 10 guilds total. /// /// The template code. /// Name of the guild. /// Stream containing the icon for the guild. /// The created guild. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task CreateGuildFromTemplateAsync(string code, string name, Optional icon = default) { var iconb64 = ImageTool.Base64FromStream(icon); return this.ApiClient.CreateGuildFromTemplateAsync(code, name, iconb64); } /// /// Gets a guild. /// Setting to true will make a REST request. /// /// The guild ID to search for. /// Whether to include approximate presence and member counts in the returned guild. /// Whether to ignore the cache. Defaults to false. /// The requested Guild. /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task GetGuildAsync(ulong id, bool? withCounts = null, bool fetch = false) { if (!fetch && this.GuildsInternal.TryGetValue(id, out var guild) && (!withCounts.HasValue || !withCounts.Value)) return guild; guild = await this.ApiClient.GetGuildAsync(id, withCounts).ConfigureAwait(false); var channels = await this.ApiClient.GetGuildChannelsAsync(guild.Id).ConfigureAwait(false); foreach (var channel in channels) guild.ChannelsInternal[channel.Id] = channel; return guild; } /// /// Tries to get a guild. /// Setting to true will make a REST request. /// /// The guild ID to search for. /// Whether to include approximate presence and member counts in the returned guild. /// Whether to ignore the cache. Defaults to true. /// The requested Guild or null if not found. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. #pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. public async Task TryGetGuildAsync(ulong id, bool? withCounts = null, bool fetch = true) #pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. { try { return await this.GetGuildAsync(id, withCounts, fetch).ConfigureAwait(false); } catch (NotFoundException) { return null; } } /// /// Gets a guild preview. /// /// The guild ID. /// A preview of the requested guild. /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task GetGuildPreviewAsync(ulong id) => this.ApiClient.GetGuildPreviewAsync(id); /// /// Tries to get a guild preview. /// /// The guild ID. /// A preview of the requested guild or null if not found. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. #pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. public async Task TryGetGuildPreviewAsync(ulong id) #pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. { try { return await this.ApiClient.GetGuildPreviewAsync(id).ConfigureAwait(false); } catch (NotFoundException) { return null; } } /// /// Gets a guild widget. /// /// The Guild Id. /// A guild widget. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task GetGuildWidgetAsync(ulong id) => this.ApiClient.GetGuildWidgetAsync(id); /// /// Tries to get a guild widget. /// /// The Guild Id. /// The requested guild widget or null if not found. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. #pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. public async Task TryGetGuildWidgetAsync(ulong id) #pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. { try { return await this.ApiClient.GetGuildWidgetAsync(id); } catch (NotFoundException) { return null; } } /// /// Gets an invite. /// /// The invite code. /// Whether to include presence and total member counts in the returned invite. /// Whether to include the expiration date in the returned invite. /// The scheduled event id. /// The requested invite. /// Thrown when the invite does not exists. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task GetInviteByCodeAsync(string code, bool? withCounts = null, bool? withExpiration = null, ulong? scheduledEventId = null) => this.ApiClient.GetInviteAsync(code, withCounts, withExpiration, scheduledEventId); /// /// Tries to get an invite. /// /// The invite code. /// Whether to include presence and total member counts in the returned invite. /// Whether to include the expiration date in the returned invite. /// The scheduled event id. /// The requested invite or null if not found. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. #pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. public async Task TryGetInviteByCodeAsync(string code, bool? withCounts = null, bool? withExpiration = null, ulong? scheduledEventId = null) #pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. { try { return await this.GetInviteByCodeAsync(code, withCounts, withExpiration, scheduledEventId).ConfigureAwait(false); } catch (NotFoundException) { return null; } } /// /// Gets a list of user connections. /// /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task> GetConnectionsAsync() => this.ApiClient.GetUserConnectionsAsync(); /// /// Gets a sticker. /// /// The requested sticker. /// The id of the sticker. /// Thrown when the sticker does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task GetStickerAsync(ulong id) => this.ApiClient.GetStickerAsync(id); /// /// Tries to get a sticker. /// /// The requested sticker or null if not found. /// The id of the sticker. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. #pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. public async Task TryGetStickerAsync(ulong id) #pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. { try { return await this.GetStickerAsync(id).ConfigureAwait(false); } catch (NotFoundException) { return null; } } /// /// Gets all nitro sticker packs. /// /// List of sticker packs. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task> GetStickerPacksAsync() => this.ApiClient.GetStickerPacksAsync(); /// /// Gets the In-App OAuth Url. /// /// Defaults to . /// Redirect Uri. /// Defaults to . /// The OAuth Url public Uri GetInAppOAuth(Permissions permissions = Permissions.None, OAuthScopes scopes = OAuthScopes.BOT_DEFAULT, string redir = null) { permissions &= PermissionMethods.FullPerms; return new Uri(new QueryUriBuilder($"{DiscordDomain.GetDomain(CoreDomain.Discord).Url}{Endpoints.OAUTH2}{Endpoints.AUTHORIZE}") .AddParameter("client_id", this.CurrentApplication.Id.ToString(CultureInfo.InvariantCulture)) .AddParameter("scope", OAuth.ResolveScopes(scopes)) .AddParameter("permissions", ((long)permissions).ToString(CultureInfo.InvariantCulture)) .AddParameter("state", "") .AddParameter("redirect_uri", redir ?? "") .ToString()); } /// /// Generates an In-App OAuth Url. /// /// The bot to generate the url for. /// Defaults to . /// Redirect Uri. /// Defaults to . /// The OAuth Url public Uri GenerateInAppOauthFor(DiscordUser bot, Permissions permissions = Permissions.None, OAuthScopes scopes = OAuthScopes.BOT_DEFAULT, string redir = null) { if (!bot.IsBot) throw new ArgumentException("The user must be a bot.", nameof(bot)); permissions &= PermissionMethods.FullPerms; return new Uri(new QueryUriBuilder($"{DiscordDomain.GetDomain(CoreDomain.Discord).Url}{Endpoints.OAUTH2}{Endpoints.AUTHORIZE}") .AddParameter("client_id", bot.Id.ToString(CultureInfo.InvariantCulture)) .AddParameter("scope", OAuth.ResolveScopes(scopes)) .AddParameter("permissions", ((long)permissions).ToString(CultureInfo.InvariantCulture)) .AddParameter("state", "") .AddParameter("redirect_uri", redir ?? "") .ToString()); } /// /// Gets a webhook. /// /// The target webhook id. /// The requested webhook. /// Thrown when the webhook does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task GetWebhookAsync(ulong id) => this.ApiClient.GetWebhookAsync(id); /// /// Tries to get a webhook. /// /// The target webhook id. /// The requested webhook or null if not found. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. #pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. public async Task TryGetWebhookAsync(ulong id) #pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. { try { return await this.GetWebhookAsync(id).ConfigureAwait(false); } catch (NotFoundException) { return null; } } /// /// Gets a webhook with a token. /// /// The target webhook id. /// The target webhook token. /// The requested webhook. /// Thrown when the webhook does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task GetWebhookWithTokenAsync(ulong id, string token) => this.ApiClient.GetWebhookWithTokenAsync(id, token); /// /// Tries to get a webhook with a token. /// /// The target webhook id. /// The target webhook token. /// The requested webhook or null if not found. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. #pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. public async Task TryGetWebhookWithTokenAsync(ulong id, string token) #pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. { try { return await this.GetWebhookWithTokenAsync(id, token).ConfigureAwait(false); } catch (NotFoundException) { return null; } } /// /// Updates current user's activity and status. /// /// Activity to set. /// Status of the user. /// Since when is the client performing the specified activity. /// public Task UpdateStatusAsync(DiscordActivity activity = null, UserStatus? userStatus = null, DateTimeOffset? idleSince = null) => this.InternalUpdateStatusAsync(activity, userStatus, idleSince); /// /// Edits current user. /// /// New username. /// New avatar. /// The modified user. /// Thrown when the user does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task UpdateCurrentUserAsync(string username = null, Optional avatar = default) { var av64 = ImageTool.Base64FromStream(avatar); var usr = await this.ApiClient.ModifyCurrentUserAsync(username, av64).ConfigureAwait(false); this.CurrentUser.Username = usr.Username; this.CurrentUser.Discriminator = usr.Discriminator; this.CurrentUser.AvatarHash = usr.AvatarHash; return this.CurrentUser; } /// /// Gets a guild template by the code. /// /// The code of the template. /// The guild template for the code. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task GetTemplateAsync(string code) => this.ApiClient.GetTemplateAsync(code); /// /// Gets all the global application commands for this application. /// /// Whether to get the full localization dict. /// A list of global application commands. public Task> GetGlobalApplicationCommandsAsync(bool withLocalizations = false) => this.ApiClient.GetGlobalApplicationCommandsAsync(this.CurrentApplication.Id, withLocalizations); /// /// Overwrites the existing global application commands. New commands are automatically created and missing commands are automatically deleted. /// /// The list of commands to overwrite with. /// The list of global commands. public Task> BulkOverwriteGlobalApplicationCommandsAsync(IEnumerable commands) => this.ApiClient.BulkOverwriteGlobalApplicationCommandsAsync(this.CurrentApplication.Id, commands); /// /// Creates or overwrites a global application command. /// /// The command to create. /// The created command. public Task CreateGlobalApplicationCommandAsync(DiscordApplicationCommand command) => this.ApiClient.CreateGlobalApplicationCommandAsync(this.CurrentApplication.Id, command); /// /// Gets a global application command by its id. /// /// The id of the command to get. /// The command with the id. public Task GetGlobalApplicationCommandAsync(ulong commandId) => this.ApiClient.GetGlobalApplicationCommandAsync(this.CurrentApplication.Id, commandId); /// /// Edits a global application command. /// /// The id of the command to edit. /// Action to perform. /// The edited command. public async Task EditGlobalApplicationCommandAsync(ulong commandId, Action action) { var mdl = new ApplicationCommandEditModel(); action(mdl); var applicationId = this.CurrentApplication?.Id ?? (await this.GetCurrentApplicationAsync().ConfigureAwait(false)).Id; return await this.ApiClient.EditGlobalApplicationCommandAsync(applicationId, commandId, mdl.Name, mdl.Description, mdl.Options, mdl.NameLocalizations, mdl.DescriptionLocalizations, mdl.DefaultMemberPermissions, mdl.DmPermission, mdl.IsNsfw).ConfigureAwait(false); } /// /// Deletes a global application command. /// /// The id of the command to delete. public Task DeleteGlobalApplicationCommandAsync(ulong commandId) => this.ApiClient.DeleteGlobalApplicationCommandAsync(this.CurrentApplication.Id, commandId); /// /// Gets all the application commands for a guild. /// /// The id of the guild to get application commands for. /// Whether to get the full localization dict. /// A list of application commands in the guild. public Task> GetGuildApplicationCommandsAsync(ulong guildId, bool withLocalizations = false) => this.ApiClient.GetGuildApplicationCommandsAsync(this.CurrentApplication.Id, guildId, withLocalizations); /// /// Overwrites the existing application commands in a guild. New commands are automatically created and missing commands are automatically deleted. /// /// The id of the guild. /// The list of commands to overwrite with. /// The list of guild commands. public Task> BulkOverwriteGuildApplicationCommandsAsync(ulong guildId, IEnumerable commands) => this.ApiClient.BulkOverwriteGuildApplicationCommandsAsync(this.CurrentApplication.Id, guildId, commands); /// /// Creates or overwrites a guild application command. /// /// The id of the guild to create the application command in. /// The command to create. /// The created command. public Task CreateGuildApplicationCommandAsync(ulong guildId, DiscordApplicationCommand command) => this.ApiClient.CreateGuildApplicationCommandAsync(this.CurrentApplication.Id, guildId, command); /// /// Gets a application command in a guild by its id. /// /// The id of the guild the application command is in. /// The id of the command to get. /// The command with the id. public Task GetGuildApplicationCommandAsync(ulong guildId, ulong commandId) => this.ApiClient.GetGuildApplicationCommandAsync(this.CurrentApplication.Id, guildId, commandId); /// /// Edits a application command in a guild. /// /// The id of the guild the application command is in. /// The id of the command to edit. /// Action to perform. /// The edited command. public async Task EditGuildApplicationCommandAsync(ulong guildId, ulong commandId, Action action) { var mdl = new ApplicationCommandEditModel(); action(mdl); var applicationId = this.CurrentApplication?.Id ?? (await this.GetCurrentApplicationAsync().ConfigureAwait(false)).Id; return await this.ApiClient.EditGuildApplicationCommandAsync(applicationId, guildId, commandId, mdl.Name, mdl.Description, mdl.Options, mdl.NameLocalizations, mdl.DescriptionLocalizations, mdl.DefaultMemberPermissions, mdl.DmPermission, mdl.IsNsfw).ConfigureAwait(false); } /// /// Deletes a application command in a guild. /// /// The id of the guild to delete the application command in. /// The id of the command. public Task DeleteGuildApplicationCommandAsync(ulong guildId, ulong commandId) => this.ApiClient.DeleteGuildApplicationCommandAsync(this.CurrentApplication.Id, guildId, commandId); /// /// Gets all command permissions for a guild. /// /// The target guild. public Task> GetGuildApplicationCommandPermissionsAsync(ulong guildId) => this.ApiClient.GetGuildApplicationCommandPermissionsAsync(this.CurrentApplication.Id, guildId); /// /// Gets the permissions for a guild command. /// /// The target guild. /// The target command id. public Task GetApplicationCommandPermissionAsync(ulong guildId, ulong commandId) => this.ApiClient.GetGuildApplicationCommandPermissionAsync(this.CurrentApplication.Id, guildId, commandId); #endregion #region Internal Caching Methods /// /// Gets the internal cached threads. /// /// The target thread id. /// The requested thread. internal DiscordThreadChannel InternalGetCachedThread(ulong threadId) { if (this.Guilds == null) return null; foreach (var guild in this.Guilds.Values) if (guild.Threads.TryGetValue(threadId, out var foundThread)) return foundThread; return null; } /// /// Gets the internal cached scheduled event. /// /// The target scheduled event id. /// The requested scheduled event. internal DiscordScheduledEvent InternalGetCachedScheduledEvent(ulong scheduledEventId) { if (this.Guilds == null) return null; foreach (var guild in this.Guilds.Values) if (guild.ScheduledEvents.TryGetValue(scheduledEventId, out var foundScheduledEvent)) return foundScheduledEvent; return null; } /// /// Gets the internal cached channel. /// /// The target channel id. /// The requested channel. - internal DiscordChannel InternalGetCachedChannel(ulong channelId) + internal DiscordChannel InternalGetCachedChannel(ulong channelId, ulong? guildId = null) { if (this.Guilds == null) return null; foreach (var guild in this.Guilds.Values) if (guild.Channels.TryGetValue(channelId, out var foundChannel)) + { + if (guildId.HasValue) + foundChannel.GuildId = guildId; return foundChannel; + } return null; } /// /// Gets the internal cached guild. /// /// The target guild id. /// The requested guild. internal DiscordGuild InternalGetCachedGuild(ulong? guildId) { if (this.GuildsInternal != null && guildId.HasValue) { if (this.GuildsInternal.TryGetValue(guildId.Value, out var guild)) return guild; } return null; } /// /// Updates a message. /// /// The message to update. /// The author to update. /// The guild to update. /// The member to update. private void UpdateMessage(DiscordMessage message, TransportUser author, DiscordGuild guild, TransportMember member) { if (author != null) { var usr = new DiscordUser(author) { Discord = this }; if (member != null) member.User = author; message.Author = this.UpdateUser(usr, guild?.Id, guild, member); } var channel = this.InternalGetCachedChannel(message.ChannelId); if (channel != null) return; channel = !message.GuildId.HasValue ? new DiscordDmChannel { Id = message.ChannelId, Discord = this, Type = ChannelType.Private } : new DiscordChannel { Id = message.ChannelId, Discord = this }; message.Channel = channel; } /// /// Updates a scheduled event. /// /// The scheduled event to update. /// The guild to update. /// The updated scheduled event. private DiscordScheduledEvent UpdateScheduledEvent(DiscordScheduledEvent scheduledEvent, DiscordGuild guild) { if (scheduledEvent != null) { _ = guild.ScheduledEventsInternal.AddOrUpdate(scheduledEvent.Id, scheduledEvent, (id, old) => { old.Discord = this; old.Description = scheduledEvent.Description; old.ChannelId = scheduledEvent.ChannelId; old.EntityId = scheduledEvent.EntityId; old.EntityType = scheduledEvent.EntityType; old.EntityMetadata = scheduledEvent.EntityMetadata; old.PrivacyLevel = scheduledEvent.PrivacyLevel; old.Name = scheduledEvent.Name; old.Status = scheduledEvent.Status; old.UserCount = scheduledEvent.UserCount; old.ScheduledStartTimeRaw = scheduledEvent.ScheduledStartTimeRaw; old.ScheduledEndTimeRaw = scheduledEvent.ScheduledEndTimeRaw; return old; }); } return scheduledEvent; } /// /// Updates a user. /// /// The user to update. /// The guild id to update. /// The guild to update. /// The member to update. /// The updated user. private DiscordUser UpdateUser(DiscordUser usr, ulong? guildId, DiscordGuild guild, TransportMember mbr) { if (mbr != null) { if (mbr.User != null) { usr = new DiscordUser(mbr.User) { Discord = this }; _ = this.UserCache.AddOrUpdate(usr.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; old.BannerHash = usr.BannerHash; old.BannerColorInternal = usr.BannerColorInternal; old.AvatarDecorationHash = usr.AvatarDecorationHash; old.ThemeColorsInternal = usr.ThemeColorsInternal; old.Pronouns = usr.Pronouns; old.Locale = usr.Locale; return old; }); usr = new DiscordMember(mbr) { Discord = this, GuildId = guildId.Value }; } var intents = this.Configuration.Intents; DiscordMember member = default; if (!intents.HasAllPrivilegedIntents() || guild.IsLarge) // we have the necessary privileged intents, no need to worry about caching here unless guild is large. { if (guild?.MembersInternal.TryGetValue(usr.Id, out member) == false) { if (intents.HasIntent(DiscordIntents.GuildMembers) || this.Configuration.AlwaysCacheMembers) // member can be updated by events, so cache it { guild.MembersInternal.TryAdd(usr.Id, (DiscordMember)usr); } } else if (intents.HasIntent(DiscordIntents.GuildPresences) || this.Configuration.AlwaysCacheMembers) // we can attempt to update it if it's already in cache. { if (!intents.HasIntent(DiscordIntents.GuildMembers)) // no need to update if we already have the member events { _ = guild.MembersInternal.TryUpdate(usr.Id, (DiscordMember)usr, member); } } } } else if (usr.Username != null) // check if not a skeleton user { _ = this.UserCache.AddOrUpdate(usr.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; old.BannerHash = usr.BannerHash; old.BannerColorInternal = usr.BannerColorInternal; old.AvatarDecorationHash = usr.AvatarDecorationHash; old.ThemeColorsInternal = usr.ThemeColorsInternal; old.Pronouns = usr.Pronouns; old.Locale = usr.Locale; return old; }); } return usr; } /// /// Updates the cached scheduled events in a guild. /// /// The guild. /// The raw events. private void UpdateCachedScheduledEvent(DiscordGuild guild, JArray rawEvents) { if (this._disposed) return; if (rawEvents != null) { guild.ScheduledEventsInternal.Clear(); foreach (var xj in rawEvents) { var xtm = xj.ToDiscordObject(); xtm.Discord = this; guild.ScheduledEventsInternal[xtm.Id] = xtm; } } } /// /// Updates the cached guild. /// /// The new guild. /// The raw members. private void UpdateCachedGuild(DiscordGuild newGuild, JArray rawMembers) { if (this._disposed) return; if (!this.GuildsInternal.ContainsKey(newGuild.Id)) this.GuildsInternal[newGuild.Id] = newGuild; var guild = this.GuildsInternal[newGuild.Id]; if (newGuild.ChannelsInternal != null && !newGuild.ChannelsInternal.IsEmpty) { foreach (var channel in newGuild.ChannelsInternal.Values) { if (guild.ChannelsInternal.TryGetValue(channel.Id, out _)) continue; channel.Initialize(this); guild.ChannelsInternal[channel.Id] = channel; } } if (newGuild.ThreadsInternal != null && !newGuild.ThreadsInternal.IsEmpty) { foreach (var thread in newGuild.ThreadsInternal.Values) { if (guild.ThreadsInternal.TryGetValue(thread.Id, out _)) continue; guild.ThreadsInternal[thread.Id] = thread; } } if (newGuild.ScheduledEventsInternal != null && !newGuild.ScheduledEventsInternal.IsEmpty) { foreach (var @event in newGuild.ScheduledEventsInternal.Values) { if (guild.ScheduledEventsInternal.TryGetValue(@event.Id, out _)) continue; guild.ScheduledEventsInternal[@event.Id] = @event; } } foreach (var newEmoji in newGuild.EmojisInternal.Values) _ = guild.EmojisInternal.GetOrAdd(newEmoji.Id, _ => newEmoji); foreach (var newSticker in newGuild.StickersInternal.Values) _ = guild.StickersInternal.GetOrAdd(newSticker.Id, _ => newSticker); foreach (var newStageInstance in newGuild.StageInstancesInternal.Values) _ = guild.StageInstancesInternal.GetOrAdd(newStageInstance.Id, _ => newStageInstance); if (rawMembers != null) { guild.MembersInternal.Clear(); foreach (var xj in rawMembers) { var xtm = xj.ToDiscordObject(); var xu = new DiscordUser(xtm.User) { Discord = this }; _ = this.UserCache.AddOrUpdate(xtm.User.Id, xu, (id, old) => { old.Username = xu.Username; old.Discriminator = xu.Discriminator; old.AvatarHash = xu.AvatarHash; old.PremiumType = xu.PremiumType; return old; }); guild.MembersInternal[xtm.User.Id] = new DiscordMember(xtm) { Discord = this, GuildId = guild.Id }; } } foreach (var role in newGuild.RolesInternal.Values) { if (guild.RolesInternal.TryGetValue(role.Id, out _)) continue; role.GuildId = guild.Id; guild.RolesInternal[role.Id] = role; } guild.Name = newGuild.Name; guild.AfkChannelId = newGuild.AfkChannelId; guild.AfkTimeout = newGuild.AfkTimeout; guild.DefaultMessageNotifications = newGuild.DefaultMessageNotifications; guild.RawFeatures = newGuild.RawFeatures; guild.IconHash = newGuild.IconHash; guild.MfaLevel = newGuild.MfaLevel; guild.OwnerId = newGuild.OwnerId; guild.VoiceRegionId = newGuild.VoiceRegionId; guild.SplashHash = newGuild.SplashHash; guild.VerificationLevel = newGuild.VerificationLevel; guild.WidgetEnabled = newGuild.WidgetEnabled; guild.WidgetChannelId = newGuild.WidgetChannelId; guild.ExplicitContentFilter = newGuild.ExplicitContentFilter; guild.PremiumTier = newGuild.PremiumTier; guild.PremiumSubscriptionCount = newGuild.PremiumSubscriptionCount; guild.PremiumProgressBarEnabled = newGuild.PremiumProgressBarEnabled; guild.BannerHash = newGuild.BannerHash; guild.Description = newGuild.Description; guild.VanityUrlCode = newGuild.VanityUrlCode; guild.SystemChannelId = newGuild.SystemChannelId; guild.SystemChannelFlags = newGuild.SystemChannelFlags; guild.DiscoverySplashHash = newGuild.DiscoverySplashHash; guild.MaxMembers = newGuild.MaxMembers; guild.MaxPresences = newGuild.MaxPresences; guild.ApproximateMemberCount = newGuild.ApproximateMemberCount; guild.ApproximatePresenceCount = newGuild.ApproximatePresenceCount; guild.MaxVideoChannelUsers = newGuild.MaxVideoChannelUsers; guild.PreferredLocale = newGuild.PreferredLocale; guild.RulesChannelId = newGuild.RulesChannelId; guild.PublicUpdatesChannelId = newGuild.PublicUpdatesChannelId; guild.ApplicationId = newGuild.ApplicationId; // fields not sent for update: // - guild.Channels // - voice states // - guild.JoinedAt = new_guild.JoinedAt; // - guild.Large = new_guild.Large; // - guild.MemberCount = Math.Max(new_guild.MemberCount, guild._members.Count); // - guild.Unavailable = new_guild.Unavailable; } /// /// Populates the message reactions and cache. /// /// The message. /// The author. /// The member. private void PopulateMessageReactionsAndCache(DiscordMessage message, TransportUser author, TransportMember member) { var guild = message.Channel?.Guild ?? this.InternalGetCachedGuild(message.GuildId); this.UpdateMessage(message, author, guild, member); message.ReactionsInternal ??= new List(); foreach (var xr in message.ReactionsInternal) xr.Emoji.Discord = this; if (this.Configuration.MessageCacheSize > 0 && message.Channel != null) this.MessageCache?.Add(message); } #endregion #region Disposal ~DiscordClient() { this.Dispose(); } /// /// Whether the client is disposed. /// private bool _disposed; /// /// Disposes the client. /// public override void Dispose() { if (this._disposed) return; this._disposed = true; GC.SuppressFinalize(this); this.DisconnectAsync().ConfigureAwait(false).GetAwaiter().GetResult(); this.ApiClient.Rest.Dispose(); this.CurrentUser = null; var extensions = this._extensions; // prevent _extensions being modified during dispose this._extensions = null; foreach (var extension in extensions) if (extension is IDisposable disposable) disposable.Dispose(); try { this._cancelTokenSource?.Cancel(); this._cancelTokenSource?.Dispose(); } catch { } this.GuildsInternal = null; this._heartbeatTask = null; } #endregion } diff --git a/DisCatSharp/Entities/Message/DiscordMessage.cs b/DisCatSharp/Entities/Message/DiscordMessage.cs index fb7d8ca02..c31fcf48f 100644 --- a/DisCatSharp/Entities/Message/DiscordMessage.cs +++ b/DisCatSharp/Entities/Message/DiscordMessage.cs @@ -1,1072 +1,1083 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 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.Globalization; using System.Linq; using System.Threading.Tasks; using DisCatSharp.Enums; using Newtonsoft.Json; namespace DisCatSharp.Entities; /// /// Represents a Discord text message. /// public class DiscordMessage : SnowflakeObject, IEquatable { /// /// Initializes a new instance of the class. /// internal DiscordMessage() { this._attachmentsLazy = new Lazy>(() => new ReadOnlyCollection(this.AttachmentsInternal)); this._embedsLazy = new Lazy>(() => new ReadOnlyCollection(this.EmbedsInternal)); this._mentionedChannelsLazy = new Lazy>(() => this.MentionedChannelsInternal != null ? new ReadOnlyCollection(this.MentionedChannelsInternal) : Array.Empty()); this._mentionedRolesLazy = new Lazy>(() => this.MentionedRolesInternal != null ? new ReadOnlyCollection(this.MentionedRolesInternal) : Array.Empty()); this.MentionedUsersLazy = new Lazy>(() => new ReadOnlyCollection(this.MentionedUsersInternal)); this._reactionsLazy = new Lazy>(() => new ReadOnlyCollection(this.ReactionsInternal)); this._stickersLazy = new Lazy>(() => new ReadOnlyCollection(this.StickersInternal)); this._jumpLink = new Lazy(() => { string gid = null; if (this.Channel != null) gid = this.Channel is DiscordDmChannel ? "@me" : this.Channel is DiscordThreadChannel ? this.INTERNAL_THREAD.GuildId.Value.ToString(CultureInfo.InvariantCulture) + : this.GuildId.HasValue + ? this.GuildId.Value.ToString(CultureInfo.InvariantCulture) : this.Channel.GuildId.Value.ToString(CultureInfo.InvariantCulture); var cid = this.ChannelId.ToString(CultureInfo.InvariantCulture); var mid = this.Id.ToString(CultureInfo.InvariantCulture); return new Uri($"https://{(this.Discord.Configuration.UseCanary ? "canary.discord.com" : this.Discord.Configuration.UsePtb ? "ptb.discord.com" : "discord.com")}/channels/{gid}/{cid}/{mid}"); }); } /// /// Initializes a new instance of the class. /// /// The other message. internal DiscordMessage(DiscordMessage other) : this() { this.Discord = other.Discord; this.AttachmentsInternal = other.AttachmentsInternal; // the attachments cannot change, thus no need to copy and reallocate. this.EmbedsInternal = new List(other.EmbedsInternal); if (other.MentionedChannelsInternal != null) this.MentionedChannelsInternal = new List(other.MentionedChannelsInternal); if (other.MentionedRolesInternal != null) this.MentionedRolesInternal = new List(other.MentionedRolesInternal); if (other.MentionedRoleIds != null) this.MentionedRoleIds = new List(other.MentionedRoleIds); this.MentionedUsersInternal = new List(other.MentionedUsersInternal); this.ReactionsInternal = new List(other.ReactionsInternal); this.StickersInternal = new List(other.StickersInternal); this.Author = other.Author; this.ChannelId = other.ChannelId; this.Content = other.Content; this.EditedTimestampRaw = other.EditedTimestampRaw; this.Id = other.Id; this.IsTts = other.IsTts; this.MessageType = other.MessageType; this.Pinned = other.Pinned; this.TimestampRaw = other.TimestampRaw; this.WebhookId = other.WebhookId; + this.GuildId = other.GuildId; } /// /// Gets the channel in which the message was sent. /// [JsonIgnore] public DiscordChannel Channel { - get => (this.Discord as DiscordClient)?.InternalGetCachedChannel(this.ChannelId) ?? this._channel; + get => (this.Discord as DiscordClient)?.InternalGetCachedChannel(this.ChannelId, this.GuildId) ?? this._channel; internal set => this._channel = value; } private DiscordChannel _channel; /// /// Gets the thread in which the message was sent. /// [JsonIgnore] private DiscordThreadChannel INTERNAL_THREAD { get => (this.Discord as DiscordClient)?.InternalGetCachedThread(this.ChannelId) ?? this._thread; set => this._thread = value; } private DiscordThreadChannel _thread; /// /// Gets the ID of the channel in which the message was sent. /// [JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)] public ulong ChannelId { get; internal set; } /// /// Gets the components this message was sent with. /// [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] public IReadOnlyCollection Components { get; internal set; } /// /// Gets the user or member that sent the message. /// [JsonProperty("author", NullValueHandling = NullValueHandling.Ignore)] public DiscordUser Author { get; internal set; } /// /// Gets the message's content. /// [JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)] public string Content { get; internal set; } /// /// Gets the message's creation timestamp. /// [JsonIgnore] public DateTimeOffset Timestamp => !string.IsNullOrWhiteSpace(this.TimestampRaw) && DateTimeOffset.TryParse(this.TimestampRaw, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dto) ? dto : this.CreationTimestamp; /// /// Gets the message's creation timestamp as raw string. /// [JsonProperty("timestamp", NullValueHandling = NullValueHandling.Ignore)] internal string TimestampRaw { get; set; } /// /// Gets the message's edit timestamp. Will be null if the message was not edited. /// [JsonIgnore] public DateTimeOffset? EditedTimestamp => !string.IsNullOrWhiteSpace(this.EditedTimestampRaw) && DateTimeOffset.TryParse(this.EditedTimestampRaw, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dto) ? dto : null; /// /// Gets the message's edit timestamp as raw string. Will be null if the message was not edited. /// [JsonProperty("edited_timestamp", NullValueHandling = NullValueHandling.Ignore)] internal string EditedTimestampRaw { get; set; } /// /// Gets whether this message was edited. /// [JsonIgnore] public bool IsEdited => !string.IsNullOrWhiteSpace(this.EditedTimestampRaw); /// /// Gets whether the message is a text-to-speech message. /// [JsonProperty("tts", NullValueHandling = NullValueHandling.Ignore)] public bool IsTts { get; internal set; } /// /// Gets whether the message mentions everyone. /// [JsonProperty("mention_everyone", NullValueHandling = NullValueHandling.Ignore)] public bool MentionEveryone { get; internal set; } /// /// Gets users or members mentioned by this message. /// [JsonIgnore] public IReadOnlyList MentionedUsers => this.MentionedUsersLazy.Value; [JsonProperty("mentions", NullValueHandling = NullValueHandling.Ignore)] internal List MentionedUsersInternal; [JsonIgnore] internal readonly Lazy> MentionedUsersLazy; // TODO: this will probably throw an exception in DMs since it tries to wrap around a null List... // this is probably low priority but need to find out a clean way to solve it... /// /// Gets roles mentioned by this message. /// [JsonIgnore] public IReadOnlyList MentionedRoles => this._mentionedRolesLazy.Value; [JsonIgnore] internal List MentionedRolesInternal; [JsonProperty("mention_roles")] internal List MentionedRoleIds; [JsonIgnore] private readonly Lazy> _mentionedRolesLazy; /// /// Gets channels mentioned by this message. /// [JsonIgnore] public IReadOnlyList MentionedChannels => this._mentionedChannelsLazy.Value; [JsonIgnore] internal List MentionedChannelsInternal; [JsonIgnore] private readonly Lazy> _mentionedChannelsLazy; /// /// Gets files attached to this message. /// [JsonIgnore] public IReadOnlyList Attachments => this._attachmentsLazy.Value; [JsonProperty("attachments", NullValueHandling = NullValueHandling.Ignore)] internal List AttachmentsInternal = new(); [JsonIgnore] private readonly Lazy> _attachmentsLazy; /// /// Gets embeds attached to this message. /// [JsonIgnore] public IReadOnlyList Embeds => this._embedsLazy.Value; [JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)] internal List EmbedsInternal = new(); [JsonIgnore] private readonly Lazy> _embedsLazy; /// /// Gets reactions used on this message. /// [JsonIgnore] public IReadOnlyList Reactions => this._reactionsLazy.Value; [JsonProperty("reactions", NullValueHandling = NullValueHandling.Ignore)] internal List ReactionsInternal = new(); [JsonIgnore] private readonly Lazy> _reactionsLazy; /// /// Gets the nonce sent with the message, if the message was sent by the client. /// [JsonProperty("nonce", NullValueHandling = NullValueHandling.Ignore)] public string Nonce { get; internal set; } /// /// Gets whether the message is pinned. /// [JsonProperty("pinned", NullValueHandling = NullValueHandling.Ignore)] public bool Pinned { get; internal set; } /// /// Gets the id of the webhook that generated this message. /// [JsonProperty("webhook_id", NullValueHandling = NullValueHandling.Ignore)] public ulong? WebhookId { get; internal set; } /// /// Gets the type of the message. /// [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] public MessageType? MessageType { get; internal set; } /// /// Gets the message activity in the Rich Presence embed. /// [JsonProperty("activity", NullValueHandling = NullValueHandling.Ignore)] public DiscordMessageActivity Activity { get; internal set; } /// /// Gets the message application in the Rich Presence embed. /// [JsonProperty("application", NullValueHandling = NullValueHandling.Ignore)] public DiscordMessageApplication Application { get; internal set; } /// /// Gets the message application id in the Rich Presence embed. /// [JsonProperty("application_id", NullValueHandling = NullValueHandling.Ignore)] public ulong ApplicationId { get; internal set; } /// /// Gets the internal reference. /// [JsonProperty("message_reference", NullValueHandling = NullValueHandling.Ignore)] internal InternalDiscordMessageReference? InternalReference { get; set; } /// /// Gets the original message reference from the crossposted message. /// [JsonIgnore] public DiscordMessageReference Reference => this.InternalReference.HasValue ? this?.InternalBuildMessageReference() : null; /// /// Gets the bitwise flags for this message. /// [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] public MessageFlags? Flags { get; internal set; } /// /// Gets whether the message originated from a webhook. /// [JsonIgnore] public bool WebhookMessage => this.WebhookId != null; /// /// Gets the jump link to this message. /// [JsonIgnore] public Uri JumpLink => this._jumpLink.Value; private readonly Lazy _jumpLink; /// /// Gets stickers for this message. /// [JsonIgnore] public IReadOnlyList Stickers => this._stickersLazy.Value; [JsonProperty("sticker_items", NullValueHandling = NullValueHandling.Ignore)] internal List StickersInternal = new(); [JsonIgnore] private readonly Lazy> _stickersLazy; /// /// Gets the guild id. /// [JsonProperty("guild_id", NullValueHandling = NullValueHandling.Ignore)] public ulong? GuildId { get; internal set; } + /// + /// Gets the guild to which this channel belongs. + /// + [JsonIgnore] + public DiscordGuild Guild + => this.GuildId.HasValue && this.Discord.Guilds.TryGetValue(this.GuildId.Value, out var guild) ? guild : null; + + /// /// Gets the message object for the referenced message /// [JsonProperty("referenced_message", NullValueHandling = NullValueHandling.Ignore)] public DiscordMessage ReferencedMessage { get; internal set; } /// /// Gets whether the message is a response to an interaction. /// [JsonProperty("interaction", NullValueHandling = NullValueHandling.Ignore)] public DiscordMessageInteraction Interaction { get; internal set; } /// /// Gets the thread that was started from this message. /// [JsonProperty("thread", NullValueHandling = NullValueHandling.Ignore)] public DiscordThreadChannel Thread { get; internal set; } /// /// Build the message reference. /// internal DiscordMessageReference InternalBuildMessageReference() { var client = this.Discord as DiscordClient; var guildId = this.InternalReference.Value.GuildId; var channelId = this.InternalReference.Value.ChannelId; var messageId = this.InternalReference.Value.MessageId; var reference = new DiscordMessageReference(); if (guildId.HasValue) reference.Guild = client.GuildsInternal.TryGetValue(guildId.Value, out var g) ? g : new DiscordGuild { Id = guildId.Value, Discord = client }; var channel = client.InternalGetCachedChannel(channelId.Value); if (channel == null) { reference.Channel = new DiscordChannel { Id = channelId.Value, Discord = client }; if (guildId.HasValue) reference.Channel.GuildId = guildId.Value; } else reference.Channel = channel; if (client.MessageCache != null && client.MessageCache.TryGet(m => m.Id == messageId.Value && m.ChannelId == channelId, out var msg)) reference.Message = msg; else { reference.Message = new DiscordMessage { ChannelId = this.ChannelId, Discord = client }; if (messageId.HasValue) reference.Message.Id = messageId.Value; } return reference; } /// /// Gets the mentions. /// /// An array of IMentions. private List GetMentions() { var mentions = new List(); if (this.ReferencedMessage != null && this.MentionedUsersInternal.Contains(this.ReferencedMessage.Author)) mentions.Add(new RepliedUserMention()); if (this.MentionedUsersInternal.Any()) mentions.AddRange(this.MentionedUsersInternal.Select(m => (IMention)new UserMention(m))); if (this.MentionedRoleIds.Any()) mentions.AddRange(this.MentionedRoleIds.Select(r => (IMention)new RoleMention(r))); return mentions; } /// /// Populates the mentions. /// internal void PopulateMentions() { var guild = this.Channel?.Guild; this.MentionedUsersInternal ??= new List(); this.MentionedRolesInternal ??= new List(); this.MentionedChannelsInternal ??= new List(); var mentionedUsers = new HashSet(new DiscordUserComparer()); if (guild != null) { foreach (var usr in this.MentionedUsersInternal) { usr.Discord = this.Discord; this.Discord.UserCache.AddOrUpdate(usr.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); mentionedUsers.Add(guild.MembersInternal.TryGetValue(usr.Id, out var member) ? member : usr); } } if (!string.IsNullOrWhiteSpace(this.Content)) { //mentionedUsers.UnionWith(Utilities.GetUserMentions(this).Select(this.Discord.GetCachedOrEmptyUserInternal)); if (guild != null) { //this._mentionedRoles = this._mentionedRoles.Union(Utilities.GetRoleMentions(this).Select(xid => guild.GetRole(xid))).ToList(); this.MentionedRolesInternal = this.MentionedRolesInternal.Union(this.MentionedRoleIds.Select(xid => guild.GetRole(xid))).ToList(); this.MentionedChannelsInternal = this.MentionedChannelsInternal.Union(Utilities.GetChannelMentions(this).Select(xid => guild.GetChannel(xid))).ToList(); } } this.MentionedUsersInternal = mentionedUsers.ToList(); } #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved /// /// Edits the message. /// /// New content. /// Thrown when the client tried to modify a message not sent by them. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task ModifyAsync(Optional content) #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved => this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, content, default, this.GetMentions(), default, default, Array.Empty(), default); #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved /// /// Edits the message. /// /// New embed. /// Thrown when the client tried to modify a message not sent by them. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task ModifyAsync(Optional embed = default) #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved => this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, default, embed.Map(v => new[] { v }).ValueOr(Array.Empty()), this.GetMentions(), default, default, Array.Empty(), default); #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved /// /// Edits the message. /// /// New content. /// New embed. /// Thrown when the client tried to modify a message not sent by them. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task ModifyAsync(Optional content, Optional embed = default) #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved => this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, content, embed.Map(v => new[] { v }).ValueOr(Array.Empty()), this.GetMentions(), default, default, Array.Empty(), default); #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved /// /// Edits the message. /// /// New content. /// New embeds. /// Thrown when the client tried to modify a message not sent by them. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task ModifyAsync(Optional content, Optional> embeds = default) #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved => this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, content, embeds, this.GetMentions(), default, default, Array.Empty(), default); #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved /// /// Edits the message. /// /// The builder of the message to edit. /// Thrown when the client tried to modify a message not sent by them. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task ModifyAsync(DiscordMessageBuilder builder) #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved { builder.Validate(true); return await this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, builder.Content, Optional.Some(builder.Embeds.AsEnumerable()), builder.Mentions, builder.Components, builder.Suppressed, builder.Files, builder.Attachments.Count > 0 ? Optional.Some(builder.Attachments.AsEnumerable()) : builder.KeepAttachmentsInternal.HasValue ? builder.KeepAttachmentsInternal.Value ? Optional.Some(this.Attachments.AsEnumerable()) : Array.Empty() : null); } #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved /// /// Edits the message embed suppression. /// /// Suppress embeds. /// Thrown when the client tried to modify a message not sent by them. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task ModifySuppressionAsync(bool suppress = false) #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved => this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, default, default, default, default, suppress, default, default); /// /// Clears all attachments from the message. /// /// public Task ClearAttachmentsAsync() => this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, default, default, this.GetMentions(), default, default, default, Array.Empty()); #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved /// /// Edits the message. /// /// The builder of the message to edit. /// Thrown when the client tried to modify a message not sent by them. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task ModifyAsync(Action action) #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved { var builder = new DiscordMessageBuilder(); action(builder); builder.Validate(true); return await this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, builder.Content, Optional.Some(builder.Embeds.AsEnumerable()), builder.Mentions, builder.Components, builder.Suppressed, builder.Files, builder.Attachments.Count > 0 ? Optional.Some(builder.Attachments.AsEnumerable()) : builder.KeepAttachmentsInternal.HasValue ? builder.KeepAttachmentsInternal.Value ? Optional.Some(this.Attachments.AsEnumerable()) : Array.Empty() : null); } #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved /// /// Deletes the message. /// /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task DeleteAsync(string reason = null) #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved => this.Discord.ApiClient.DeleteMessageAsync(this.ChannelId, this.Id, reason); #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved /// /// Creates a thread. /// Depending on the of the parent channel it's either a or a . /// /// The name of the thread. /// till it gets archived. Defaults to /// The per user ratelimit, aka slowdown. /// The reason. /// Thrown when the client does not have the or permission. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. /// Thrown when the cannot be modified. public async Task CreateThreadAsync(string name, ThreadAutoArchiveDuration autoArchiveDuration = ThreadAutoArchiveDuration.OneHour, int? rateLimitPerUser = null, string reason = null) => Utilities.CheckThreadAutoArchiveDurationFeature(this.Channel.Guild, autoArchiveDuration) ? await this.Discord.ApiClient.CreateThreadAsync(this.ChannelId, this.Id, name, autoArchiveDuration, this.Channel.Type == ChannelType.News ? ChannelType.NewsThread : ChannelType.PublicThread, rateLimitPerUser, isForum: false, reason: reason) : throw new NotSupportedException($"Cannot modify ThreadAutoArchiveDuration. Guild needs boost tier {(autoArchiveDuration == ThreadAutoArchiveDuration.ThreeDays ? "one" : "two")}."); #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved /// /// Pins the message in its channel. /// /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task PinAsync() #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved => this.Discord.ApiClient.PinMessageAsync(this.ChannelId, this.Id); #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved /// /// Unpins the message in its channel. /// /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task UnpinAsync() #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved => this.Discord.ApiClient.UnpinMessageAsync(this.ChannelId, this.Id); #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved /// /// Responds to the message. This produces a reply. /// /// Message content to respond with. /// The sent message. /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task RespondAsync(string content) #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved => this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, content, null, sticker: null, replyMessageId: this.Id, mentionReply: false, failOnInvalidReply: false); #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved /// /// Responds to the message. This produces a reply. /// /// Embed to attach to the message. /// The sent message. /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task RespondAsync(DiscordEmbed embed) #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved => this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, null, embed != null ? new[] { embed } : null, sticker: null, replyMessageId: this.Id, mentionReply: false, failOnInvalidReply: false); #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved /// /// Responds to the message. This produces a reply. /// /// Message content to respond with. /// Embed to attach to the message. /// The sent message. /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task RespondAsync(string content, DiscordEmbed embed) #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved => this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, content, embed != null ? new[] { embed } : null, sticker: null, replyMessageId: this.Id, mentionReply: false, failOnInvalidReply: false); #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved /// /// Responds to the message. This produces a reply. /// /// The Discord message builder. /// The sent message. /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task RespondAsync(DiscordMessageBuilder builder) #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved => this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, builder.WithReply(this.Id, mention: false, failOnInvalidReply: false)); #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved /// /// Responds to the message. This produces a reply. /// /// The Discord message builder. /// The sent message. /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task RespondAsync(Action action) #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved { var builder = new DiscordMessageBuilder(); action(builder); return this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, builder.WithReply(this.Id, mention: false, failOnInvalidReply: false)); } #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved /// /// Creates a reaction to this message. /// /// The emoji you want to react with, either an emoji or name:id /// Thrown when the client does not have the permission. /// Thrown when the emoji does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task CreateReactionAsync(DiscordEmoji emoji) #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved => this.Discord.ApiClient.CreateReactionAsync(this.ChannelId, this.Id, emoji.ToReactionString()); #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved /// /// Deletes your own reaction /// /// Emoji for the reaction you want to remove, either an emoji or name:id /// Thrown when the emoji does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task DeleteOwnReactionAsync(DiscordEmoji emoji) #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved => this.Discord.ApiClient.DeleteOwnReactionAsync(this.ChannelId, this.Id, emoji.ToReactionString()); #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved /// /// Deletes another user's reaction. /// /// Emoji for the reaction you want to remove, either an emoji or name:id. /// Member you want to remove the reaction for /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the emoji does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task DeleteReactionAsync(DiscordEmoji emoji, DiscordUser user, string reason = null) #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved => this.Discord.ApiClient.DeleteUserReactionAsync(this.ChannelId, this.Id, user.Id, emoji.ToReactionString(), reason); #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved /// /// Gets users that reacted with this emoji. /// /// Emoji to react with. /// Limit of users to fetch. /// Fetch users after this user's id. /// Thrown when the emoji does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task> GetReactionsAsync(DiscordEmoji emoji, int limit = 25, ulong? after = null) #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved => this.GetReactionsInternalAsync(emoji, limit, after); #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved /// /// Deletes all reactions for this message. /// /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the emoji does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task DeleteAllReactionsAsync(string reason = null) #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved => this.Discord.ApiClient.DeleteAllReactionsAsync(this.ChannelId, this.Id, reason); #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved /// /// Deletes all reactions of a specific reaction for this message. /// /// The emoji to clear, either an emoji or name:id. /// Thrown when the client does not have the permission. /// Thrown when the emoji does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task DeleteReactionsEmojiAsync(DiscordEmoji emoji) #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved => this.Discord.ApiClient.DeleteReactionsEmojiAsync(this.ChannelId, this.Id, emoji.ToReactionString()); /// /// Gets the reactions. /// /// The emoji to search for. /// The limit of results. /// Get the reasctions after snowflake. private async Task> GetReactionsInternalAsync(DiscordEmoji emoji, int limit = 25, ulong? after = null) { if (limit < 0) throw new ArgumentException("Cannot get a negative number of reactions' users."); if (limit == 0) return Array.Empty(); var users = new List(limit); var remaining = limit; var last = after; int lastCount; do { var fetchSize = remaining > 100 ? 100 : remaining; var fetch = await this.Discord.ApiClient.GetReactionsAsync(this.Channel.Id, this.Id, emoji.ToReactionString(), last, fetchSize).ConfigureAwait(false); lastCount = fetch.Count; remaining -= lastCount; users.AddRange(fetch); last = fetch.LastOrDefault()?.Id; } while (remaining > 0 && lastCount > 0); return new ReadOnlyCollection(users); } /// /// Returns a string representation of this message. /// /// String representation of this message. public override string ToString() => $"Message {this.Id}; Attachment count: {this.AttachmentsInternal.Count}; Embed count: {this.EmbedsInternal.Count}; Contents: {this.Content}"; /// /// 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 DiscordMessage); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(DiscordMessage e) => e is not null && (ReferenceEquals(this, e) || (this.Id == e.Id && this.ChannelId == e.ChannelId)); /// /// Gets the hash code for this . /// /// The hash code for this . public override int GetHashCode() { var hash = 13; hash = (hash * 7) + this.Id.GetHashCode(); hash = (hash * 7) + this.ChannelId.GetHashCode(); return hash; } /// /// Gets whether the two objects are equal. /// /// First message to compare. /// Second message to compare. /// Whether the two messages are equal. public static bool operator ==(DiscordMessage e1, DiscordMessage e2) { var o1 = e1 as object; var o2 = e2 as object; return (o1 != null || o2 == null) && (o1 == null || o2 != null) && ((o1 == null && o2 == null) || (e1.Id == e2.Id && e1.ChannelId == e2.ChannelId)); } /// /// Gets whether the two objects are not equal. /// /// First message to compare. /// Second message to compare. /// Whether the two messages are not equal. public static bool operator !=(DiscordMessage e1, DiscordMessage e2) => !(e1 == e2); } diff --git a/DisCatSharp/EventArgs/Message/MessageCreateEventArgs.cs b/DisCatSharp/EventArgs/Message/MessageCreateEventArgs.cs index 3f2ff2793..07e5d5e91 100644 --- a/DisCatSharp/EventArgs/Message/MessageCreateEventArgs.cs +++ b/DisCatSharp/EventArgs/Message/MessageCreateEventArgs.cs @@ -1,83 +1,83 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 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 DisCatSharp.Entities; namespace DisCatSharp.EventArgs; /// /// Represents arguments for event. /// public class MessageCreateEventArgs : DiscordEventArgs { /// /// Gets the message that was created. /// public DiscordMessage Message { get; internal set; } /// /// Gets the channel this message belongs to. /// public DiscordChannel Channel => this.Message.Channel; /// /// Gets the guild this message belongs to. /// public DiscordGuild Guild - => this.Channel.Guild; + => this.Message.Guild; /// /// Gets the guild id in case it couldn't convert. /// public ulong? GuildId => this.Message.GuildId; /// /// Gets the author of the message. /// public DiscordUser Author => this.Message.Author; /// /// Gets the collection of mentioned users. /// public IReadOnlyList MentionedUsers { get; internal set; } /// /// Gets the collection of mentioned roles. /// public IReadOnlyList MentionedRoles { get; internal set; } /// /// Gets the collection of mentioned channels. /// public IReadOnlyList MentionedChannels { get; internal set; } /// /// Initializes a new instance of the class. /// internal MessageCreateEventArgs(IServiceProvider provider) : base(provider) { } }