diff --git a/DisCatSharp/Clients/DiscordClient.cs b/DisCatSharp/Clients/DiscordClient.cs index 739119e5b..3a81f2ccf 100644 --- a/DisCatSharp/Clients/DiscordClient.cs +++ b/DisCatSharp/Clients/DiscordClient.cs @@ -1,1333 +1,1520 @@ // 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.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 fetch the user again. Defaults to true. + /// 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 = true) + 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; } } + /// + /// 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. + public async Task TryGetUserAsync(ulong userId, bool fetch = true) + { + try + { + return await this.GetUserAsync(userId, fetch).ConfigureAwait(false); + } + catch (NotFoundException) + { + return null; + } + } + /// /// 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) - => this.InternalGetCachedChannel(id) ?? await this.ApiClient.GetChannelAsync(id).ConfigureAwait(false); + 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. + public async Task TryGetChannelAsync(ulong id, bool fetch = true) + { + 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) - => this.InternalGetCachedThread(id) ?? await this.ApiClient.GetThreadAsync(id).ConfigureAwait(false); + 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. + public async Task TryGetThreadAsync(ulong id, bool fetch = true) + { + 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); } /// /// Executes a raw request. /// /// /// /// var request = await Client.ExecuteRawRequestAsync(RestRequestMethod.GET, $"{Endpoints.CHANNELS}/243184972190742178964/{Endpoints.INVITES}"); /// List<DiscordInvite> invites = DiscordJson.ToDiscordObject<List<DiscordInvite>>(request.Response); /// /// /// The method. /// The route. /// The route parameters. /// The json body. /// The additional headers. /// The ratelimit wait override. /// Thrown when the resource does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. /// A awaitable RestResponse [Obsolete("This is no longer needed. Use DiscordClient.RestClient instead.", false)] public async Task ExecuteRawRequestAsync(RestRequestMethod method, string route, object routeParams, string jsonBody = null, Dictionary additionalHeaders = null, double? ratelimitWaitOverride = null) { var bucket = this.ApiClient.Rest.GetBucket(method, route, routeParams, out var path); var url = Utilities.GetApiUriFor(path, this.Configuration); var res = await this.ApiClient.DoRequestAsync(this, bucket, url, method, route, additionalHeaders, DiscordJson.SerializeObject(jsonBody), ratelimitWaitOverride); return res; } /// /// 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) + public async Task GetGuildAsync(ulong id, bool? withCounts = null, bool fetch = false) { - if (this.GuildsInternal.TryGetValue(id, out var guild) && (!withCounts.HasValue || !withCounts.Value)) + 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. + public async Task TryGetGuildAsync(ulong id, bool? withCounts = null, bool fetch = true) + { + 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. + public async Task TryGetGuildPreviewAsync(ulong id) + { + try + { + return await this.ApiClient.GetGuildPreviewAsync(id).ConfigureAwait(false); + } + 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. + /// 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. + public async Task TryGetInviteByCodeAsync(string code, bool? withCounts = null, bool? withExpiration = null, ulong? scheduledEventId = null) + { + 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. + public async Task TryGetStickerAsync(ulong id) + { + 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; // hey look, it's not all annoying and blue :P 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()); } /// /// 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); /// - /// Gets a webhook. + /// 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. + public async Task TryGetWebhookAsync(ulong id) + { + 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. + public async Task TryGetWebhookWithTokenAsync(ulong id, string token) + { + 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) { if (this.Guilds == null) return null; foreach (var guild in this.Guilds.Values) if (guild.Channels.TryGetValue(channelId, out var foundChannel)) 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; 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; 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/Channel/DiscordChannel.cs b/DisCatSharp/Entities/Channel/DiscordChannel.cs index ec3f23caf..79ae760f3 100644 --- a/DisCatSharp/Entities/Channel/DiscordChannel.cs +++ b/DisCatSharp/Entities/Channel/DiscordChannel.cs @@ -1,1424 +1,1460 @@ // 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.IO; using System.Linq; using System.Threading.Tasks; using DisCatSharp.Enums; using DisCatSharp.Exceptions; using DisCatSharp.Net.Abstractions; using DisCatSharp.Net.Models; using Newtonsoft.Json; namespace DisCatSharp.Entities; /// /// Represents a discord channel. /// public class DiscordChannel : SnowflakeObject, IEquatable { /// /// Gets ID of the guild to which this channel belongs. /// [JsonProperty("guild_id", NullValueHandling = NullValueHandling.Ignore)] public ulong? GuildId { get; internal set; } /// /// Gets ID of the category that contains this channel. /// [JsonProperty("parent_id", NullValueHandling = NullValueHandling.Include)] public ulong? ParentId { get; internal set; } /// /// Gets the category that contains this channel. /// [JsonIgnore] public DiscordChannel Parent => this.ParentId.HasValue ? this.Guild.GetChannel(this.ParentId.Value) : null; /// /// Gets the name of this channel. /// [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public string Name { get; internal set; } /// /// Gets the type of this channel. /// [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] public ChannelType Type { get; internal set; } /// /// Gets the template for new posts in this channel. /// Applicable if forum channel. /// [JsonProperty("template", NullValueHandling = NullValueHandling.Ignore)] public string Template { get; internal set; } /// /// Gets the position of this channel. /// [JsonProperty("position", NullValueHandling = NullValueHandling.Ignore)] public int Position { get; internal set; } /// /// Gets the flags of this channel. /// [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] public ChannelFlags Flags { get; internal set; } /// /// Gets the maximum available position to move the channel to. /// This can contain outdated information. /// public int GetMaxPosition() { var channels = this.Guild.Channels.Values; return this.ParentId != null ? this.Type == ChannelType.Text || this.Type == ChannelType.News ? channels.Where(xc => xc.ParentId == this.ParentId && (xc.Type == ChannelType.Text || xc.Type == ChannelType.News)).OrderBy(xc => xc.Position).Last().Position : this.Type == ChannelType.Voice || this.Type == ChannelType.Stage ? channels.Where(xc => xc.ParentId == this.ParentId && (xc.Type == ChannelType.Voice || xc.Type == ChannelType.Stage)).OrderBy(xc => xc.Position).Last().Position : channels.Where(xc => xc.ParentId == this.ParentId && xc.Type == this.Type).OrderBy(xc => xc.Position).Last().Position : channels.Where(xc => xc.ParentId == null && xc.Type == this.Type).OrderBy(xc => xc.Position).Last().Position; } /// /// Gets the minimum available position to move the channel to. /// public int GetMinPosition() { var channels = this.Guild.Channels.Values; return this.ParentId != null ? this.Type == ChannelType.Text || this.Type == ChannelType.News ? channels.Where(xc => xc.ParentId == this.ParentId && (xc.Type == ChannelType.Text || xc.Type == ChannelType.News)).OrderBy(xc => xc.Position).First().Position : this.Type == ChannelType.Voice || this.Type == ChannelType.Stage ? channels.Where(xc => xc.ParentId == this.ParentId && (xc.Type == ChannelType.Voice || xc.Type == ChannelType.Stage)).OrderBy(xc => xc.Position).First().Position : channels.Where(xc => xc.ParentId == this.ParentId && xc.Type == this.Type).OrderBy(xc => xc.Position).First().Position : channels.Where(xc => xc.ParentId == null && xc.Type == this.Type).OrderBy(xc => xc.Position).First().Position; } /// /// Gets whether this channel is a DM channel. /// [JsonIgnore] public bool IsPrivate => this.Type is ChannelType.Private or ChannelType.Group; /// /// Gets whether this channel is a channel category. /// [JsonIgnore] public bool IsCategory => this.Type == ChannelType.Category; /// /// Gets whether this channel is a stage channel. /// [JsonIgnore] public bool IsStage => this.Type == ChannelType.Stage; /// /// 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 a collection of permission overwrites for this channel. /// [JsonIgnore] public IReadOnlyList PermissionOverwrites => this._permissionOverwritesLazy.Value; [JsonProperty("permission_overwrites", NullValueHandling = NullValueHandling.Ignore)] internal List PermissionOverwritesInternal = new(); [JsonIgnore] private readonly Lazy> _permissionOverwritesLazy; /// /// Gets the channel's topic. This is applicable to text channels only. /// [JsonProperty("topic", NullValueHandling = NullValueHandling.Ignore)] public string Topic { get; internal set; } /// /// Gets the ID of the last message sent in this channel. This is applicable to text channels only. /// [JsonProperty("last_message_id", NullValueHandling = NullValueHandling.Ignore)] public ulong? LastMessageId { get; internal set; } /// /// Gets this channel's bitrate. This is applicable to voice channels only. /// [JsonProperty("bitrate", NullValueHandling = NullValueHandling.Ignore)] public int? Bitrate { get; internal set; } /// /// Gets this channel's user limit. This is applicable to voice channels only. /// [JsonProperty("user_limit", NullValueHandling = NullValueHandling.Ignore)] public int? UserLimit { get; internal set; } /// /// Gets the slow mode delay configured for this channel. /// All bots, as well as users with or permissions in the channel are exempt from slow mode. /// [JsonProperty("rate_limit_per_user", NullValueHandling = NullValueHandling.Ignore)] public int? PerUserRateLimit { get; internal set; } /// /// Gets the slow mode delay configured for this channel for post creations. /// All bots, as well as users with or permissions in the channel are exempt from slow mode. /// [JsonProperty("default_thread_rate_limit_per_user", NullValueHandling = NullValueHandling.Ignore)] public int? PostCreateUserRateLimit { get; internal set; } /// /// Gets this channel's video quality mode. This is applicable to voice channels only. /// [JsonProperty("video_quality_mode", NullValueHandling = NullValueHandling.Ignore)] public VideoQualityMode? QualityMode { get; internal set; } /// /// List of available tags for forum posts. /// [JsonIgnore] public IReadOnlyList AvailableTags => this.InternalAvailableTags; /// /// List of available tags for forum posts. /// [JsonProperty("available_tags", NullValueHandling = NullValueHandling.Ignore)] internal List InternalAvailableTags { get; set; } = new(); /// /// List of available tags for forum posts. /// [JsonProperty("default_reaction_emoji", NullValueHandling = NullValueHandling.Ignore)] public ForumReactionEmoji DefaultReactionEmoji { get; internal set; } [JsonProperty("default_sort_order", NullValueHandling = NullValueHandling.Include)] public ForumPostSortOrder? DefaultSortOrder { get; internal set; } /// /// Gets when the last pinned message was pinned. /// [JsonIgnore] public DateTimeOffset? LastPinTimestamp => !string.IsNullOrWhiteSpace(this.LastPinTimestampRaw) && DateTimeOffset.TryParse(this.LastPinTimestampRaw, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dto) ? dto : null; /// /// Gets when the last pinned message was pinned as raw string. /// [JsonProperty("last_pin_timestamp", NullValueHandling = NullValueHandling.Ignore)] internal string LastPinTimestampRaw { get; set; } /// /// Gets this channel's default duration for newly created threads, in minutes, to automatically archive the thread after recent activity. /// [JsonProperty("default_auto_archive_duration", NullValueHandling = NullValueHandling.Ignore)] public ThreadAutoArchiveDuration? DefaultAutoArchiveDuration { get; internal set; } /// /// Gets this channel's mention string. /// [JsonIgnore] public string Mention => Formatter.Mention(this); /// /// Gets this channel's children. This applies only to channel categories. /// [JsonIgnore] public IReadOnlyList Children => !this.IsCategory ? throw new ArgumentException("Only channel categories contain children.") : this.Guild.ChannelsInternal.Values.Where(e => e.ParentId == this.Id).ToList(); /// /// Gets the list of members currently in the channel (if voice channel), or members who can see the channel (otherwise). /// [JsonIgnore] public virtual IReadOnlyList Users => this.Guild == null ? throw new InvalidOperationException("Cannot query users outside of guild channels.") : this.IsVoiceJoinable() ? this.Guild.Members.Values.Where(x => x.VoiceState?.ChannelId == this.Id).ToList() : this.Guild.Members.Values.Where(x => (this.PermissionsFor(x) & Permissions.AccessChannels) == Permissions.AccessChannels).ToList(); /// /// Gets whether this channel is an NSFW channel. /// [JsonProperty("nsfw")] public bool IsNsfw { get; internal set; } /// /// Gets this channel's region id (if voice channel). /// [JsonProperty("rtc_region", NullValueHandling = NullValueHandling.Ignore)] internal string RtcRegionId { get; set; } /// /// Gets this channel's region override (if voice channel). /// [JsonIgnore] public DiscordVoiceRegion RtcRegion => this.RtcRegionId != null ? this.Discord.VoiceRegions[this.RtcRegionId] : null; /// /// Only sent on the resolved channels of interaction responses for application commands. /// Gets the permissions of the user in this channel who invoked the command. /// [JsonProperty("permissions", NullValueHandling = NullValueHandling.Ignore)] public Permissions? UserPermissions { get; internal set; } /// /// Initializes a new instance of the class. /// internal DiscordChannel() { this._permissionOverwritesLazy = new Lazy>(() => new ReadOnlyCollection(this.PermissionOverwritesInternal)); } #region Methods /// /// Sends a message to this channel. /// /// Content of the message to send. /// The sent message. /// Thrown when the client does not have the permission if TTS is true 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(string content) => !this.IsWritable() ? throw new ArgumentException("Cannot send a text message to a non-text channel.") : this.Discord.ApiClient.CreateMessageAsync(this.Id, content, null, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false); /// /// Sends a message to this channel. /// /// Embed to attach to the message. /// The sent message. /// Thrown when the client does not have the permission 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(DiscordEmbed embed) => !this.IsWritable() ? throw new ArgumentException("Cannot send a text message to a non-text channel.") : this.Discord.ApiClient.CreateMessageAsync(this.Id, null, embed != null ? new[] { embed } : null, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false); /// /// Sends a message to this channel. /// /// Embed to attach to the message. /// Content of the message to send. /// The sent message. /// Thrown when the client does not have the permission if TTS is true 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(string content, DiscordEmbed embed) => !this.IsWritable() ? throw new ArgumentException("Cannot send a text message to a non-text channel.") : this.Discord.ApiClient.CreateMessageAsync(this.Id, content, embed != null ? new[] { embed } : null, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false); /// /// Sends a message to this channel. /// /// The builder with all the items to send. /// The sent message. /// Thrown when the client does not have the permission TTS is true 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(DiscordMessageBuilder builder) => this.Discord.ApiClient.CreateMessageAsync(this.Id, builder); /// /// Sends a message to this channel. /// /// The builder with all the items to send. /// The sent message. /// Thrown when the client does not have the permission TTS is true 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(Action action) { var builder = new DiscordMessageBuilder(); action(builder); return !this.IsWritable() ? throw new ArgumentException("Cannot send a text message to a non-text channel.") : this.Discord.ApiClient.CreateMessageAsync(this.Id, builder); } /// /// Deletes a guild channel /// /// Reason for audit logs. /// 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 DeleteAsync(string reason = null) => this.Discord.ApiClient.DeleteChannelAsync(this.Id, reason); /// /// Clones this channel. This operation will create a channel with identical settings to this one. Note that this will not copy messages. /// /// Reason for audit logs. /// Newly-created channel. /// 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 async Task CloneAsync(string reason = null) { if (this.Guild == null) throw new InvalidOperationException("Non-guild channels cannot be cloned."); var ovrs = new List(); foreach (var ovr in this.PermissionOverwritesInternal) ovrs.Add(await new DiscordOverwriteBuilder().FromAsync(ovr).ConfigureAwait(false)); // TODO: Add forum tags option missing? var bitrate = this.Bitrate; var userLimit = this.UserLimit; Optional perUserRateLimit = this.PerUserRateLimit; if (!this.IsVoiceJoinable()) { bitrate = null; userLimit = null; } if (this.Type == ChannelType.Stage) { userLimit = null; } if (!this.IsWritable()) { perUserRateLimit = Optional.None; } return await this.Guild.CreateChannelAsync(this.Name, this.Type, this.Parent, this.Topic, bitrate, userLimit, ovrs, this.IsNsfw, perUserRateLimit, this.QualityMode, this.DefaultAutoArchiveDuration, this.Flags, reason).ConfigureAwait(false); } /// - /// Returns a specific message + /// Gets a specific message. /// /// The id of the message - /// Whether to bypass the message cache + /// Whether to bypass the cache. Defaults to false. /// 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 async Task GetMessageAsync(ulong id, bool bypassCache = false) => + public async Task GetMessageAsync(ulong id, bool fetch = false) => this.Discord.Configuration.MessageCacheSize > 0 - && !bypassCache + && !fetch && this.Discord is DiscordClient dc && dc.MessageCache != null && dc.MessageCache.TryGet(xm => xm.Id == id && xm.ChannelId == this.Id, out var msg) ? msg : await this.Discord.ApiClient.GetMessageAsync(this.Id, id).ConfigureAwait(false); + /// + /// Tries to get a specific message. + /// + /// The id of the message + /// Whether to bypass the cache. Defaults to true. + /// Thrown when the client does not have the permission. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task TryGetMessageAsync(ulong id, bool fetch = true) + { + try + { + return await this.GetMessageAsync(id, fetch).ConfigureAwait(false); + } + catch (NotFoundException) + { + return null; + } + } + /// /// Modifies the current channel. /// /// Action to perform on this channel /// Thrown when the client does not have the . /// Thrown when the client does not have the correct for modifying the . /// 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 ModifyAsync(Action action) { if (this.Type == ChannelType.Forum) throw new NotSupportedException("Cannot execute this request on a forum channel."); var mdl = new ChannelEditModel(); action(mdl); if (mdl.DefaultAutoArchiveDuration.HasValue) if (!Utilities.CheckThreadAutoArchiveDurationFeature(this.Guild, mdl.DefaultAutoArchiveDuration.Value)) throw new NotSupportedException($"Cannot modify DefaultAutoArchiveDuration. Guild needs boost tier {(mdl.DefaultAutoArchiveDuration.Value == ThreadAutoArchiveDuration.ThreeDays ? "one" : "two")}."); return this.Discord.ApiClient.ModifyChannelAsync(this.Id, mdl.Name, mdl.Position, mdl.Topic, mdl.Nsfw, mdl.Parent.Map(p => p?.Id), mdl.Bitrate, mdl.UserLimit, mdl.PerUserRateLimit, mdl.RtcRegion.Map(r => r?.Id), mdl.QualityMode, mdl.DefaultAutoArchiveDuration, mdl.Type, mdl.PermissionOverwrites, mdl.Flags, mdl.AuditLogReason); } /// /// Modifies the current forum channel. /// /// Action to perform on this channel /// Thrown when the client does not have the . /// Thrown when the client does not have the correct for modifying the . /// 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 ModifyForumAsync(Action action) { if (this.Type != ChannelType.Forum) throw new NotSupportedException("Cannot execute this request on a non-forum channel."); var mdl = new ForumChannelEditModel(); action(mdl); if (mdl.DefaultAutoArchiveDuration.HasValue) if (!Utilities.CheckThreadAutoArchiveDurationFeature(this.Guild, mdl.DefaultAutoArchiveDuration.Value)) throw new NotSupportedException($"Cannot modify DefaultAutoArchiveDuration. Guild needs boost tier {(mdl.DefaultAutoArchiveDuration.Value == ThreadAutoArchiveDuration.ThreeDays ? "one" : "two")}."); return this.Discord.ApiClient.ModifyForumChannelAsync(this.Id, mdl.Name, mdl.Position, mdl.Topic, mdl.Template, mdl.Nsfw, mdl.Parent.Map(p => p?.Id), mdl.DefaultReactionEmoji, mdl.PerUserRateLimit, mdl.PostCreateUserRateLimit, mdl.DefaultSortOrder, mdl.DefaultAutoArchiveDuration, mdl.PermissionOverwrites, mdl.Flags, mdl.AuditLogReason); } /// /// Updates the channel position when it doesn't have a category. /// /// Use for moving to other categories. /// Use to move out of a category. /// Use for moving within a category. /// /// Position the channel should be moved to. /// Reason for audit logs. /// 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 ModifyPositionAsync(int position, string reason = null) { if (this.Guild == null) throw new ArgumentException("Cannot modify order of non-guild channels."); if (!this.IsMovable()) throw new NotSupportedException("You can't move this type of channel in categories."); if (this.ParentId != null) throw new ArgumentException("Cannot modify order of channels within a category. Use ModifyPositionInCategoryAsync instead."); var pmds = this.Guild.ChannelsInternal.Values.Where(xc => xc.Type == this.Type).OrderBy(xc => xc.Position) .Select(x => new RestGuildChannelReorderPayload { ChannelId = x.Id, Position = x.Id == this.Id ? position : x.Position >= position ? x.Position + 1 : x.Position }); return this.Discord.ApiClient.ModifyGuildChannelPositionAsync(this.Guild.Id, pmds, reason); } /// /// Updates the channel position within it's own category. /// /// Use for moving to other categories. /// Use to move out of a category. /// Use to move channels outside a category. /// /// The position. /// The reason. /// 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. /// Thrown when is out of range. /// Thrown when function is called on a channel without a parent channel. public async Task ModifyPositionInCategoryAsync(int position, string reason = null) { if (!this.IsMovableInParent()) throw new NotSupportedException("You can't move this type of channel in categories."); var isUp = position > this.Position; var channels = await this.InternalRefreshChannelsAsync(); var chns = this.ParentId != null ? this.Type == ChannelType.Text || this.Type == ChannelType.News ? channels.Where(xc => xc.ParentId == this.ParentId && (xc.Type == ChannelType.Text || xc.Type == ChannelType.News)) : this.Type == ChannelType.Voice || this.Type == ChannelType.Stage ? channels.Where(xc => xc.ParentId == this.ParentId && (xc.Type == ChannelType.Voice || xc.Type == ChannelType.Stage)) : channels.Where(xc => xc.ParentId == this.ParentId && xc.Type == this.Type) : this.Type == ChannelType.Text || this.Type == ChannelType.News ? channels.Where(xc => xc.ParentId == null && (xc.Type == ChannelType.Text || xc.Type == ChannelType.News)) : this.Type == ChannelType.Voice || this.Type == ChannelType.Stage ? channels.Where(xc => xc.ParentId == null && (xc.Type == ChannelType.Voice || xc.Type == ChannelType.Stage)) : channels.Where(xc => xc.ParentId == null && xc.Type == this.Type); var ochns = chns.OrderBy(xc => xc.Position).ToArray(); var min = ochns.First().Position; var max = ochns.Last().Position; if (position > max || position < min) throw new IndexOutOfRangeException($"Position is not in range. {position} is {(position > max ? "greater then the maximal" : "lower then the minimal")} position."); var pmds = ochns.Select(x => new RestGuildChannelReorderPayload { ChannelId = x.Id, Position = x.Id == this.Id ? position : isUp ? x.Position <= position && x.Position > this.Position ? x.Position - 1 : x.Position : x.Position >= position && x.Position < this.Position ? x.Position + 1 : x.Position } ); await this.Discord.ApiClient.ModifyGuildChannelPositionAsync(this.Guild.Id, pmds, reason).ConfigureAwait(false); } /// /// Internally refreshes the channel list. /// private async Task> InternalRefreshChannelsAsync() { await this.RefreshPositionsAsync(); return this.Guild.Channels.Values.ToList().AsReadOnly(); } internal void Initialize(BaseDiscordClient client) { this.Discord = client; foreach (var xo in this.PermissionOverwritesInternal) { xo.Discord = this.Discord; xo.ChannelId = this.Id; } if (this.InternalAvailableTags != null) { foreach (var xo in this.InternalAvailableTags) { xo.Discord = this.Discord; xo.ChannelId = this.Id; } } } /// /// Refreshes the positions. /// public async Task RefreshPositionsAsync() { var channels = await this.Discord.ApiClient.GetGuildChannelsAsync(this.Guild.Id); this.Guild.ChannelsInternal.Clear(); foreach (var channel in channels.ToList()) { channel.Initialize(this.Discord); this.Guild.ChannelsInternal[channel.Id] = channel; } } /// /// Updates the channel position within it's own category. /// Valid modes: '+' or 'down' to move a channel down | '-' or 'up' to move a channel up. /// /// Use for moving to other categories. /// Use to move out of a category. /// Use to move channels outside a category. /// /// The mode. Valid: '+' or 'down' to move a channel down | '-' or 'up' to move a channel up /// The position. /// The reason. /// 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. /// Thrown when is out of range. /// Thrown when function is called on a channel without a parent channel, a wrong mode is given or given position is zero. public Task ModifyPositionInCategorySmartAsync(string mode, int position, string reason = null) { if (!this.IsMovableInParent()) throw new NotSupportedException("You can't move this type of channel in categories."); if (mode != "+" && mode != "-" && mode != "down" && mode != "up") throw new ArgumentException("Error with the selected mode: Valid is '+' or 'down' to move a channel down and '-' or 'up' to move a channel up"); var positive = mode == "+" || mode == "positive" || mode == "down"; var negative = mode == "-" || mode == "negative" || mode == "up"; return positive ? position < this.GetMaxPosition() ? this.ModifyPositionInCategoryAsync(this.Position + position, reason) : throw new IndexOutOfRangeException($"Position is not in range of category.") : negative ? position > this.GetMinPosition() ? this.ModifyPositionInCategoryAsync(this.Position - position, reason) : throw new IndexOutOfRangeException($"Position is not in range of category.") : throw new ArgumentException("You can only modify with +X or -X. 0 is not valid."); } /// /// Updates the channel parent, moving the channel to the bottom of the new category. /// /// New parent for channel. Use to remove from parent. /// Sync permissions with parent. Defaults to null. /// Reason for audit logs. /// 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 ModifyParentAsync(DiscordChannel newParent, bool? lockPermissions = null, string reason = null) { if (this.Guild == null) throw new ArgumentException("Cannot modify parent of non-guild channels."); if (!this.IsMovableInParent()) throw new NotSupportedException("You can't move this type of channel in categories."); if (newParent.Type is not ChannelType.Category) throw new ArgumentException("Only category type channels can be parents."); var position = this.Guild.ChannelsInternal.Values.Where(xc => xc.Type == this.Type && xc.ParentId == newParent.Id) // gets list same type channels in parent .Select(xc => xc.Position).DefaultIfEmpty(-1).Max() + 1; // returns highest position of list +1, default val: 0 var pmds = this.Guild.ChannelsInternal.Values.Where(xc => xc.Type == this.Type) .OrderBy(xc => xc.Position) .Select(x => { var pmd = new RestGuildChannelNewParentPayload { ChannelId = x.Id, Position = x.Position >= position ? x.Position + 1 : x.Position, }; if (x.Id == this.Id) { pmd.Position = position; pmd.ParentId = newParent?.Id; pmd.LockPermissions = lockPermissions; } return pmd; }); return this.Discord.ApiClient.ModifyGuildChannelParentAsync(this.Guild.Id, pmds, reason); } /// /// Moves the channel out of a category. /// /// Reason for audit logs. /// 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 RemoveParentAsync(string reason = null) { if (this.Guild == null) throw new ArgumentException("Cannot modify parent of non-guild channels."); if (!this.IsMovableInParent()) throw new NotSupportedException("You can't move this type of channel in categories."); var pmds = this.Guild.ChannelsInternal.Values.Where(xc => xc.Type == this.Type) .OrderBy(xc => xc.Position) .Select(x => { var pmd = new RestGuildChannelNoParentPayload { ChannelId = x.Id }; if (x.Id == this.Id) { pmd.Position = 1; pmd.ParentId = null; } else { pmd.Position = x.Position < this.Position ? x.Position + 1 : x.Position; } return pmd; }); return this.Discord.ApiClient.DetachGuildChannelParentAsync(this.Guild.Id, pmds, reason); } /// /// Returns a list of messages before a certain message. /// The amount of messages to fetch. /// Message to fetch before from. /// /// 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> GetMessagesBeforeAsync(ulong before, int limit = 100) => this.GetMessagesInternalAsync(limit, before, null, null); /// /// Returns a list of messages after a certain message. /// The amount of messages to fetch. /// Message to fetch after from. /// /// 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> GetMessagesAfterAsync(ulong after, int limit = 100) => this.GetMessagesInternalAsync(limit, null, after, null); /// /// Returns a list of messages around a certain message. /// The amount of messages to fetch. /// Message to fetch around from. /// /// 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> GetMessagesAroundAsync(ulong around, int limit = 100) => this.GetMessagesInternalAsync(limit, null, null, around); /// /// Returns a list of messages from the last message in the channel. /// The amount of messages to fetch. /// /// 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> GetMessagesAsync(int limit = 100) => this.GetMessagesInternalAsync(limit, null, null, null); /// /// Returns a list of messages /// /// How many messages should be returned. /// Get messages before snowflake. /// Get messages after snowflake. /// Get messages around snowflake. private async Task> GetMessagesInternalAsync(int limit = 100, ulong? before = null, ulong? after = null, ulong? around = null) { if (!this.IsWritable()) throw new ArgumentException("Cannot get the messages of a non-text channel."); if (limit < 0) throw new ArgumentException("Cannot get a negative number of messages."); if (limit == 0) return Array.Empty(); //return this.Discord.ApiClient.GetChannelMessagesAsync(this.Id, limit, before, after, around); if (limit > 100 && around != null) throw new InvalidOperationException("Cannot get more than 100 messages around the specified ID."); var msgs = new List(limit); var remaining = limit; ulong? last = null; var isAfter = after != null; int lastCount; do { var fetchSize = remaining > 100 ? 100 : remaining; var fetch = await this.Discord.ApiClient.GetChannelMessagesAsync(this.Id, fetchSize, !isAfter ? last ?? before : null, isAfter ? last ?? after : null, around).ConfigureAwait(false); lastCount = fetch.Count; remaining -= lastCount; if (!isAfter) { msgs.AddRange(fetch); last = fetch.LastOrDefault()?.Id; } else { msgs.InsertRange(0, fetch); last = fetch.FirstOrDefault()?.Id; } } while (remaining > 0 && lastCount > 0); return new ReadOnlyCollection(msgs); } /// /// Deletes multiple messages if they are less than 14 days old. If they are older, none of the messages will be deleted and you will receive a error. /// /// A collection of messages to delete. /// Reason for audit logs. /// 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 async Task DeleteMessagesAsync(IEnumerable messages, string reason = null) { // don't enumerate more than once var msgs = messages.Where(x => x.Channel.Id == this.Id).Select(x => x.Id).ToArray(); if (messages == null || !msgs.Any()) throw new ArgumentException("You need to specify at least one message to delete."); if (msgs.Length < 2) { await this.Discord.ApiClient.DeleteMessageAsync(this.Id, msgs.Single(), reason).ConfigureAwait(false); return; } for (var i = 0; i < msgs.Length; i += 100) await this.Discord.ApiClient.DeleteMessagesAsync(this.Id, msgs.Skip(i).Take(100), reason).ConfigureAwait(false); } /// /// Deletes a message /// /// The message to be deleted. /// Reason for audit logs. /// 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 DeleteMessageAsync(DiscordMessage message, string reason = null) => this.Discord.ApiClient.DeleteMessageAsync(this.Id, message.Id, reason); /// /// Returns a list of invite objects /// /// 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> GetInvitesAsync() => this.Guild == null ? throw new ArgumentException("Cannot get the invites of a channel that does not belong to a guild.") : this.Discord.ApiClient.GetChannelInvitesAsync(this.Id); /// /// Create a new invite object /// /// Duration of invite in seconds before expiry, or 0 for never. Defaults to 86400. /// Max number of uses or 0 for unlimited. Defaults to 0 /// Whether this invite should be temporary. Defaults to false. /// Whether this invite should be unique. Defaults to false. /// The target type. Defaults to null. /// The target activity. Defaults to null. /// The target user id. Defaults to null. /// The audit log reason. /// 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 CreateInviteAsync(int maxAge = 86400, int maxUses = 0, bool temporary = false, bool unique = false, TargetType? targetType = null, TargetActivity? targetApplication = null, ulong? targetUser = null, string reason = null) => this.Discord.ApiClient.CreateChannelInviteAsync(this.Id, maxAge, maxUses, targetType, targetApplication, targetUser, temporary, unique, reason); #region Stage /// /// Opens a stage. /// /// Topic of the stage. /// Whether @everyone should be notified. /// Privacy level of the stage (Defaults to . /// Audit log reason. /// Stage instance /// 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 async Task OpenStageAsync(string topic, bool sendStartNotification = false, StagePrivacyLevel privacyLevel = StagePrivacyLevel.GuildOnly, string reason = null) => await this.Discord.ApiClient.CreateStageInstanceAsync(this.Id, topic, sendStartNotification, privacyLevel, reason); /// /// Modifies a stage topic. /// /// New topic of the stage. /// New privacy level of the stage. /// Audit log reason. /// 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 async Task ModifyStageAsync(Optional topic, Optional privacyLevel, string reason = null) => await this.Discord.ApiClient.ModifyStageInstanceAsync(this.Id, topic, privacyLevel, reason); /// /// Closes a stage. /// /// Audit log reason. /// 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 async Task CloseStageAsync(string reason = null) => await this.Discord.ApiClient.DeleteStageInstanceAsync(this.Id, reason); /// /// Gets a stage. /// /// The requested stage. /// 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. public async Task GetStageAsync() => await this.Discord.ApiClient.GetStageInstanceAsync(this.Id); #endregion #region Scheduled Events /// /// Creates a scheduled event based on the channel type. /// /// The name. /// The scheduled start time. /// The description. /// The cover image. /// The reason. /// A scheduled event. /// Thrown when the resource does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task CreateScheduledEventAsync(string name, DateTimeOffset scheduledStartTime, string description = null, Optional coverImage = default, string reason = null) { if (!this.IsVoiceJoinable()) throw new NotSupportedException("Cannot create a scheduled event for this type of channel. Channel type must be either voice or stage."); var type = this.Type == ChannelType.Voice ? ScheduledEventEntityType.Voice : ScheduledEventEntityType.StageInstance; return await this.Guild.CreateScheduledEventAsync(name, scheduledStartTime, null, this, null, description, type, coverImage, reason); } #endregion #region Threads /// /// Creates a thread. /// Depending on whether it is created inside an or an it is either an or an . /// Depending on whether the is set to it is either an or an (default). /// /// The name of the thread. /// till it gets archived. Defaults to . /// Can be either an , or an . /// The per user ratelimit, aka slowdown. /// Audit log reason. /// The created thread. /// Thrown when the client does not have the or or if creating a private thread the permission. /// Thrown when the guild hasn't enabled threads atm. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. /// Thrown when the cannot be modified. This happens, when the guild hasn't reached a certain boost . Or if is not enabled for guild. This happens, if the guild does not have public async Task CreateThreadAsync(string name, ThreadAutoArchiveDuration autoArchiveDuration = ThreadAutoArchiveDuration.OneHour, ChannelType type = ChannelType.PublicThread, int? rateLimitPerUser = null, string reason = null) => type != ChannelType.NewsThread && type != ChannelType.PublicThread && type != ChannelType.PrivateThread ? throw new NotSupportedException("Wrong thread type given.") : !this.IsThreadHolder() ? throw new NotSupportedException("Parent channel can't have threads.") : type == ChannelType.PrivateThread ? Utilities.CheckThreadPrivateFeature(this.Guild) ? Utilities.CheckThreadAutoArchiveDurationFeature(this.Guild, autoArchiveDuration) ? await this.Discord.ApiClient.CreateThreadAsync(this.Id, null, name, autoArchiveDuration, type, rateLimitPerUser, isForum: false, reason: reason) : throw new NotSupportedException($"Cannot modify ThreadAutoArchiveDuration. Guild needs boost tier {(autoArchiveDuration == ThreadAutoArchiveDuration.ThreeDays ? "one" : "two")}.") : throw new NotSupportedException($"Cannot create a private thread. Guild needs to be boost tier two.") : Utilities.CheckThreadAutoArchiveDurationFeature(this.Guild, autoArchiveDuration) ? await this.Discord.ApiClient.CreateThreadAsync(this.Id, null, name, autoArchiveDuration, this.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")}."); /// /// Creates a forum post. /// /// The name of the post. /// The message of the post. /// The per user ratelimit, aka slowdown. /// The tags to add on creation. /// Audit log reason. /// The created thread. /// Thrown when the client does not have the permission. /// Thrown when the guild hasn't enabled threads atm. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task CreatePostAsync(string name, DiscordMessageBuilder builder, int? rateLimitPerUser = null, IEnumerable? tags = null, string reason = null) { if (this.Type != ChannelType.Forum) throw new NotSupportedException("Parent channel must be forum."); else return await this.Discord.ApiClient.CreateThreadAsync(this.Id, null, name, null, null, rateLimitPerUser, tags, builder, true, reason); } /// /// Gets joined archived private threads. Can contain more threads. /// If the result's value 'HasMore' is true, you need to recall this function to get older threads. /// /// Get threads created before this thread id. /// Defines the limit of returned . /// 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 async Task GetJoinedPrivateArchivedThreadsAsync(ulong? before, int? limit) => await this.Discord.ApiClient.GetJoinedPrivateArchivedThreadsAsync(this.Id, before, limit); /// /// Gets archived public threads. Can contain more threads. /// If the result's value 'HasMore' is true, you need to recall this function to get older threads. /// /// Get threads created before this thread id. /// Defines the limit of returned . /// 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 async Task GetPublicArchivedThreadsAsync(ulong? before, int? limit) => await this.Discord.ApiClient.GetPublicArchivedThreadsAsync(this.Id, before, limit); /// /// Gets archived private threads. Can contain more threads. /// If the result's value 'HasMore' is true, you need to recall this function to get older threads. /// /// Get threads created before this thread id. /// Defines the limit of returned . /// 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. public async Task GetPrivateArchivedThreadsAsync(ulong? before, int? limit) => await this.Discord.ApiClient.GetPrivateArchivedThreadsAsync(this.Id, before, limit); /// /// Gets a forum channel tag. /// /// The id of the tag to get. + /// Thrown when the tag does not exist. public ForumPostTag GetForumPostTag(ulong id) { var tag = this.InternalAvailableTags.First(x => x.Id == id); tag.Discord = this.Discord; tag.ChannelId = this.Id; return tag; } + /// + /// Tries to get a forum channel tag. + /// + /// The id of the tag to get or null if not found. + public ForumPostTag? TryGetForumPostTag(ulong id) + { + var tag = this.InternalAvailableTags.FirstOrDefault(x => x.Id == id); + if (tag is not null) + { + tag.Discord = this.Discord; + tag.ChannelId = this.Id; + } + return tag; + } + /// /// Creates a forum channel tag. /// /// The name of the tag. /// The emoji of the tag. Has to be either a of the current guild or a . /// Whether only moderators should be able to apply this tag. /// The audit log reason. /// Thrown when the client does not have the permission. /// Thrown when the tag does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task CreateForumPostTagAsync(string name, DiscordEmoji emoji, bool moderated = false, string reason = null) => await this.Discord.ApiClient.CreateForumTagAsync(this.Id, name, emoji, moderated, reason); /// /// Deletes a forum channel tag. /// /// The id of the tag to delete. /// The audit log reason. /// Thrown when the client does not have the permission. /// Thrown when the tag does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task DeleteForumPostTag(ulong id, string reason = null) => this.Discord.ApiClient.DeleteForumTagAsync(id, this.Id, reason); #endregion /// /// Adds a channel permission overwrite for specified role. /// /// The role to have the permission added. /// The permissions to allow. /// The permissions to deny. /// Reason for audit logs. /// 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 AddOverwriteAsync(DiscordRole role, Permissions allow = Permissions.None, Permissions deny = Permissions.None, string reason = null) => this.Discord.ApiClient.EditChannelPermissionsAsync(this.Id, role.Id, allow, deny, "role", reason); /// /// Adds a channel permission overwrite for specified member. /// /// The member to have the permission added. /// The permissions to allow. /// The permissions to deny. /// Reason for audit logs. /// 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 AddOverwriteAsync(DiscordMember member, Permissions allow = Permissions.None, Permissions deny = Permissions.None, string reason = null) => this.Discord.ApiClient.EditChannelPermissionsAsync(this.Id, member.Id, allow, deny, "member", reason); /// /// Deletes a channel permission overwrite for specified member. /// /// The member to have the permission deleted. /// Reason for audit logs. /// 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 DeleteOverwriteAsync(DiscordMember member, string reason = null) => this.Discord.ApiClient.DeleteChannelPermissionAsync(this.Id, member.Id, reason); /// /// Deletes a channel permission overwrite for specified role. /// /// The role to have the permission deleted. /// Reason for audit logs. /// 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 DeleteOverwriteAsync(DiscordRole role, string reason = null) => this.Discord.ApiClient.DeleteChannelPermissionAsync(this.Id, role.Id, reason); /// /// Post a typing indicator. /// /// 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 TriggerTypingAsync() => !this.IsWritable() ? throw new ArgumentException("Cannot start typing in a non-text channel.") : this.Discord.ApiClient.TriggerTypingAsync(this.Id); /// /// Returns all pinned messages. /// /// 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> GetPinnedMessagesAsync() => !this.IsWritable() ? throw new ArgumentException("A non-text channel does not have pinned messages.") : this.Discord.ApiClient.GetPinnedMessagesAsync(this.Id); /// /// Create a new webhook. /// /// The name of the webhook. /// The image for the default webhook avatar. /// Reason for audit logs. /// 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 async Task CreateWebhookAsync(string name, Optional avatar = default, string reason = null) => await this.Discord.ApiClient.CreateWebhookAsync(this.IsThread() ? this.ParentId!.Value : this.Id, name, ImageTool.Base64FromStream(avatar), reason).ConfigureAwait(false); /// /// Returns a list of webhooks. /// /// Thrown when the client does not have the permission. /// Thrown when the channel does not exist. /// Thrown when Discord is unable to process the request. public Task> GetWebhooksAsync() => this.Discord.ApiClient.GetChannelWebhooksAsync(this.IsThread() ? this.ParentId!.Value : this.Id); /// /// Moves a member to this voice channel. /// /// The member to be moved. /// Thrown when the client does not have the permission. /// Thrown when the channel does not exists or if the Member does not exists. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task PlaceMemberAsync(DiscordMember member) { if (!this.IsVoiceJoinable()) throw new ArgumentException("Cannot place a member in a non-voice channel."); await this.Discord.ApiClient.ModifyGuildMemberAsync(this.Guild.Id, member.Id, default, default, default, default, this.Id, null).ConfigureAwait(false); } /// /// Follows a news channel. /// /// Channel to crosspost messages to. /// Thrown when trying to follow a non-news channel. /// Thrown when the current user doesn't have on the target channel. public Task FollowAsync(DiscordChannel targetChannel) => this.Type != ChannelType.News ? throw new ArgumentException("Cannot follow a non-news channel.") : this.Discord.ApiClient.FollowChannelAsync(this.Id, targetChannel.Id); /// /// Publishes a message in a news channel to following channels. /// /// Message to publish. /// Thrown when the message has already been crossposted. /// /// Thrown when the current user doesn't have and/or /// public Task CrosspostMessageAsync(DiscordMessage message) => (message.Flags & MessageFlags.Crossposted) == MessageFlags.Crossposted ? throw new ArgumentException("Message is already crossposted.") : this.Discord.ApiClient.CrosspostMessageAsync(this.Id, message.Id); /// /// Updates the current user's suppress state in this channel, if stage channel. /// /// Toggles the suppress state. /// Sets the time the user requested to speak. /// Thrown when the channel is not a stage channel. public async Task UpdateCurrentUserVoiceStateAsync(bool? suppress, DateTimeOffset? requestToSpeakTimestamp = null) { if (this.Type != ChannelType.Stage) throw new ArgumentException("Voice state can only be updated in a stage channel."); await this.Discord.ApiClient.UpdateCurrentUserVoiceStateAsync(this.GuildId.Value, this.Id, suppress, requestToSpeakTimestamp).ConfigureAwait(false); } /// /// Calculates permissions for a given member. /// /// Member to calculate permissions for. /// Calculated permissions for a given member. public Permissions PermissionsFor(DiscordMember mbr) { // user > role > everyone // allow > deny > undefined // => // user allow > user deny > role allow > role deny > everyone allow > everyone deny if (this.IsPrivate || this.Guild == null) return Permissions.None; if (this.Guild.OwnerId == mbr.Id) return PermissionMethods.FullPerms; Permissions perms; // assign @everyone permissions var everyoneRole = this.Guild.EveryoneRole; perms = everyoneRole.Permissions; // roles that member is in var mbRoles = mbr.Roles.Where(xr => xr.Id != everyoneRole.Id); // assign permissions from member's roles (in order) perms |= mbRoles.Aggregate(Permissions.None, (c, role) => c | role.Permissions); // Administrator grants all permissions and cannot be overridden if ((perms & Permissions.Administrator) == Permissions.Administrator) return PermissionMethods.FullPerms; // channel overrides for roles that member is in var mbRoleOverrides = mbRoles .Select(xr => this.PermissionOverwritesInternal.FirstOrDefault(xo => xo.Id == xr.Id)) .Where(xo => xo != null) .ToList(); // assign channel permission overwrites for @everyone pseudo-role var everyoneOverwrites = this.PermissionOverwritesInternal.FirstOrDefault(xo => xo.Id == everyoneRole.Id); if (everyoneOverwrites != null) { perms &= ~everyoneOverwrites.Denied; perms |= everyoneOverwrites.Allowed; } // assign channel permission overwrites for member's roles (explicit deny) perms &= ~mbRoleOverrides.Aggregate(Permissions.None, (c, overs) => c | overs.Denied); // assign channel permission overwrites for member's roles (explicit allow) perms |= mbRoleOverrides.Aggregate(Permissions.None, (c, overs) => c | overs.Allowed); // channel overrides for just this member var mbOverrides = this.PermissionOverwritesInternal.FirstOrDefault(xo => xo.Id == mbr.Id); if (mbOverrides == null) return perms; // assign channel permission overwrites for just this member perms &= ~mbOverrides.Denied; perms |= mbOverrides.Allowed; return perms; } /// /// Returns a string representation of this channel. /// /// String representation of this channel. public override string ToString() => this.Type == ChannelType.Category ? $"Channel Category {this.Name} ({this.Id})" : this.Type == ChannelType.Text || this.Type == ChannelType.News || this.IsThread() ? $"Channel #{this.Name} ({this.Id})" : this.IsVoiceJoinable() ? $"Channel #!{this.Name} ({this.Id})" : !string.IsNullOrWhiteSpace(this.Name) ? $"Channel {this.Name} ({this.Id})" : $"Channel {this.Id}"; #endregion /// /// 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 DiscordChannel); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(DiscordChannel e) => e is not null && (ReferenceEquals(this, e) || this.Id == e.Id); /// /// Gets the hash code for this . /// /// The hash code for this . public override int GetHashCode() => this.Id.GetHashCode(); /// /// Gets whether the two objects are equal. /// /// First channel to compare. /// Second channel to compare. /// Whether the two channels are equal. public static bool operator ==(DiscordChannel e1, DiscordChannel 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); } /// /// Gets whether the two objects are not equal. /// /// First channel to compare. /// Second channel to compare. /// Whether the two channels are not equal. public static bool operator !=(DiscordChannel e1, DiscordChannel e2) => !(e1 == e2); } diff --git a/DisCatSharp/Entities/Guild/DiscordGuild.cs b/DisCatSharp/Entities/Guild/DiscordGuild.cs index c23de73f1..eabbd16f3 100644 --- a/DisCatSharp/Entities/Guild/DiscordGuild.cs +++ b/DisCatSharp/Entities/Guild/DiscordGuild.cs @@ -1,2028 +1,2103 @@ // 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.Tasks; using DisCatSharp.Enums; using DisCatSharp.Exceptions; using DisCatSharp.Net; using DisCatSharp.Net.Abstractions; using DisCatSharp.Net.Models; using DisCatSharp.Net.Serialization; using Newtonsoft.Json; namespace DisCatSharp.Entities; /// /// Represents a Discord guild. /// public partial class DiscordGuild : SnowflakeObject, IEquatable { /// /// Gets the guild's name. /// [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public string Name { get; internal set; } /// /// Gets the guild icon's hash. /// [JsonProperty("icon", NullValueHandling = NullValueHandling.Ignore)] public string IconHash { get; internal set; } /// /// Gets the guild icon's url. /// [JsonIgnore] public string IconUrl => !string.IsNullOrWhiteSpace(this.IconHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.ICONS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.IconHash}.{(this.IconHash.StartsWith("a_") ? "gif" : "png")}?size=1024" : null; /// /// Gets the guild splash's hash. /// [JsonProperty("splash", NullValueHandling = NullValueHandling.Ignore)] public string SplashHash { get; internal set; } /// /// Gets the guild splash's url. /// [JsonIgnore] public string SplashUrl => !string.IsNullOrWhiteSpace(this.SplashHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.SPLASHES}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.SplashHash}.png?size=1024" : null; /// /// Gets the guild discovery splash's hash. /// [JsonProperty("discovery_splash", NullValueHandling = NullValueHandling.Ignore)] public string DiscoverySplashHash { get; internal set; } /// /// Gets the guild discovery splash's url. /// [JsonIgnore] public string DiscoverySplashUrl => !string.IsNullOrWhiteSpace(this.DiscoverySplashHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.GUILD_DISCOVERY_SPLASHES}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.DiscoverySplashHash}.png?size=1024" : null; /// /// Gets the preferred locale of this guild. /// This is used for server discovery, interactions and notices from Discord. Defaults to en-US. /// [JsonProperty("preferred_locale", NullValueHandling = NullValueHandling.Ignore)] public string PreferredLocale { get; internal set; } /// /// Gets the ID of the guild's owner. /// [JsonProperty("owner_id", NullValueHandling = NullValueHandling.Ignore)] public ulong OwnerId { get; internal set; } /// /// Gets the guild's owner. /// [JsonIgnore] public DiscordMember Owner => this.Members.TryGetValue(this.OwnerId, out var owner) ? owner : this.Discord.ApiClient.GetGuildMemberAsync(this.Id, this.OwnerId).ConfigureAwait(false).GetAwaiter().GetResult(); /// /// Gets permissions for the user in the guild (does not include channel overrides) /// [JsonProperty("permissions", NullValueHandling = NullValueHandling.Ignore)] public Permissions? Permissions { get; set; } /// /// Gets the guild's voice region ID. /// [JsonProperty("region", NullValueHandling = NullValueHandling.Ignore)] internal string VoiceRegionId { get; set; } /// /// Gets the guild's voice region. /// [JsonIgnore] public DiscordVoiceRegion VoiceRegion => this.Discord.VoiceRegions[this.VoiceRegionId]; /// /// Gets the guild's AFK voice channel ID. /// [JsonProperty("afk_channel_id", NullValueHandling = NullValueHandling.Ignore)] internal ulong AfkChannelId { get; set; } /// /// Gets the guild's AFK voice channel. /// [JsonIgnore] public DiscordChannel AfkChannel => this.GetChannel(this.AfkChannelId); /// /// List of . /// Null if DisCatSharp.ApplicationCommands is not used or no guild commands are registered. /// [JsonIgnore] public ReadOnlyCollection RegisteredApplicationCommands => new(this.InternalRegisteredApplicationCommands); [JsonIgnore] internal List InternalRegisteredApplicationCommands { get; set; } = null; /// /// Gets the guild's AFK timeout. /// [JsonProperty("afk_timeout", NullValueHandling = NullValueHandling.Ignore)] public int AfkTimeout { get; internal set; } /// /// Gets the guild's verification level. /// [JsonProperty("verification_level", NullValueHandling = NullValueHandling.Ignore)] public VerificationLevel VerificationLevel { get; internal set; } /// /// Gets the guild's default notification settings. /// [JsonProperty("default_message_notifications", NullValueHandling = NullValueHandling.Ignore)] public DefaultMessageNotifications DefaultMessageNotifications { get; internal set; } /// /// Gets the guild's explicit content filter settings. /// [JsonProperty("explicit_content_filter")] public ExplicitContentFilter ExplicitContentFilter { get; internal set; } /// /// Gets the guild's nsfw level. /// [JsonProperty("nsfw_level")] public NsfwLevel NsfwLevel { get; internal set; } /// /// Gets the system channel id. /// [JsonProperty("system_channel_id", NullValueHandling = NullValueHandling.Include)] internal ulong? SystemChannelId { get; set; } /// /// Gets the channel where system messages (such as boost and welcome messages) are sent. /// [JsonIgnore] public DiscordChannel SystemChannel => this.SystemChannelId.HasValue ? this.GetChannel(this.SystemChannelId.Value) : null; /// /// Gets the settings for this guild's system channel. /// [JsonProperty("system_channel_flags")] public SystemChannelFlags SystemChannelFlags { get; internal set; } /// /// Gets whether this guild's widget is enabled. /// [JsonProperty("widget_enabled", NullValueHandling = NullValueHandling.Ignore)] public bool? WidgetEnabled { get; internal set; } /// /// Gets the widget channel id. /// [JsonProperty("widget_channel_id", NullValueHandling = NullValueHandling.Ignore)] internal ulong? WidgetChannelId { get; set; } /// /// Gets the widget channel for this guild. /// [JsonIgnore] public DiscordChannel WidgetChannel => this.WidgetChannelId.HasValue ? this.GetChannel(this.WidgetChannelId.Value) : null; /// /// Gets the rules channel id. /// [JsonProperty("rules_channel_id")] internal ulong? RulesChannelId { get; set; } /// /// Gets the rules channel for this guild. /// This is only available if the guild is considered "discoverable". /// [JsonIgnore] public DiscordChannel RulesChannel => this.RulesChannelId.HasValue ? this.GetChannel(this.RulesChannelId.Value) : null; /// /// Gets the public updates channel id. /// [JsonProperty("public_updates_channel_id")] internal ulong? PublicUpdatesChannelId { get; set; } /// /// Gets the public updates channel (where admins and moderators receive messages from Discord) for this guild. /// This is only available if the guild is considered "discoverable". /// [JsonIgnore] public DiscordChannel PublicUpdatesChannel => this.PublicUpdatesChannelId.HasValue ? this.GetChannel(this.PublicUpdatesChannelId.Value) : null; /// /// Gets the application id of this guild if it is bot created. /// [JsonProperty("application_id")] public ulong? ApplicationId { get; internal set; } /// /// Gets a collection of this guild's roles. /// [JsonIgnore] public IReadOnlyDictionary Roles => new ReadOnlyConcurrentDictionary(this.RolesInternal); [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary RolesInternal; /// /// Gets a collection of this guild's stickers. /// [JsonIgnore] public IReadOnlyDictionary Stickers => new ReadOnlyConcurrentDictionary(this.StickersInternal); [JsonProperty("stickers", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary StickersInternal; /// /// Gets a collection of this guild's emojis. /// [JsonIgnore] public IReadOnlyDictionary Emojis => new ReadOnlyConcurrentDictionary(this.EmojisInternal); [JsonProperty("emojis", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary EmojisInternal; /// /// Gets a collection of this guild's features. /// [JsonProperty("features", NullValueHandling = NullValueHandling.Ignore)] public IReadOnlyList RawFeatures { get; internal set; } /// /// Gets the guild's features. /// [JsonIgnore] public GuildFeatures Features => new(this); /// /// Gets the required multi-factor authentication level for this guild. /// [JsonProperty("mfa_level", NullValueHandling = NullValueHandling.Ignore)] public MfaLevel MfaLevel { get; internal set; } /// /// Gets this guild's join date. /// [JsonProperty("joined_at", NullValueHandling = NullValueHandling.Ignore)] public DateTimeOffset JoinedAt { get; internal set; } /// /// Gets whether this guild is considered to be a large guild. /// [JsonProperty("large", NullValueHandling = NullValueHandling.Ignore)] public bool IsLarge { get; internal set; } /// /// Gets whether this guild is unavailable. /// [JsonProperty("unavailable", NullValueHandling = NullValueHandling.Ignore)] public bool IsUnavailable { get; internal set; } /// /// Gets the total number of members in this guild. /// [JsonProperty("member_count", NullValueHandling = NullValueHandling.Ignore)] public int MemberCount { get; internal set; } /// /// Gets the maximum amount of members allowed for this guild. /// [JsonProperty("max_members")] public int? MaxMembers { get; internal set; } /// /// Gets the maximum amount of presences allowed for this guild. /// [JsonProperty("max_presences")] public int? MaxPresences { get; internal set; } /// /// Gets the approximate number of members in this guild, when using and having withCounts set to true. /// [JsonProperty("approximate_member_count", NullValueHandling = NullValueHandling.Ignore)] public int? ApproximateMemberCount { get; internal set; } /// /// Gets the approximate number of presences in this guild, when using and having withCounts set to true. /// [JsonProperty("approximate_presence_count", NullValueHandling = NullValueHandling.Ignore)] public int? ApproximatePresenceCount { get; internal set; } /// /// Gets the maximum amount of users allowed per video channel. /// [JsonProperty("max_video_channel_users", NullValueHandling = NullValueHandling.Ignore)] public int? MaxVideoChannelUsers { get; internal set; } /// /// Gets the maximum amount of users allowed per video stage channel. /// [JsonProperty("max_stage_video_channel_users", NullValueHandling = NullValueHandling.Ignore)] public int? MaxStageVideoChannelUsers { get; internal set; } /// /// Gets a dictionary of all the voice states for this guilds. The key for this dictionary is the ID of the user /// the voice state corresponds to. /// [JsonIgnore] public IReadOnlyDictionary VoiceStates => new ReadOnlyConcurrentDictionary(this.VoiceStatesInternal); [JsonProperty("voice_states", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary VoiceStatesInternal; /// /// Gets a dictionary of all the members that belong to this guild. The dictionary's key is the member ID. /// [JsonIgnore] public IReadOnlyDictionary Members => new ReadOnlyConcurrentDictionary(this.MembersInternal); [JsonProperty("members", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary MembersInternal; /// /// Gets a dictionary of all the channels associated with this guild. The dictionary's key is the channel ID. /// [JsonIgnore] public IReadOnlyDictionary Channels => new ReadOnlyConcurrentDictionary(this.ChannelsInternal); [JsonProperty("channels", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary ChannelsInternal; internal ConcurrentDictionary Invites; /// /// Gets a dictionary of all the active threads associated with this guild the user has permission to view. The dictionary's key is the channel ID. /// [JsonIgnore] public IReadOnlyDictionary Threads { get; internal set; } [JsonProperty("threads", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary ThreadsInternal = new(); /// /// Gets a dictionary of all active stage instances. The dictionary's key is the stage ID. /// [JsonIgnore] public IReadOnlyDictionary StageInstances { get; internal set; } [JsonProperty("stage_instances", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary StageInstancesInternal = new(); /// /// Gets a dictionary of all scheduled events. /// [JsonIgnore] public IReadOnlyDictionary ScheduledEvents { get; internal set; } [JsonProperty("guild_scheduled_events", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary ScheduledEventsInternal = new(); /// /// Gets the guild member for current user. /// [JsonIgnore] public DiscordMember CurrentMember => this._currentMemberLazy.Value; [JsonIgnore] private readonly Lazy _currentMemberLazy; /// /// Gets the @everyone role for this guild. /// [JsonIgnore] public DiscordRole EveryoneRole => this.GetRole(this.Id); [JsonIgnore] internal bool IsOwnerInternal; /// /// Gets whether the current user is the guild's owner. /// [JsonProperty("owner", NullValueHandling = NullValueHandling.Ignore)] public bool IsOwner { get => this.IsOwnerInternal || this.OwnerId == this.Discord.CurrentUser.Id; internal set => this.IsOwnerInternal = value; } /// /// Gets the vanity URL code for this guild, when applicable. /// [JsonProperty("vanity_url_code")] public string VanityUrlCode { get; internal set; } /// /// Gets the guild description, when applicable. /// [JsonProperty("description")] public string Description { get; internal set; } /// /// Gets this guild's banner hash, when applicable. /// [JsonProperty("banner")] public string BannerHash { get; internal set; } /// /// Gets this guild's banner in url form. /// [JsonIgnore] public string BannerUrl => !string.IsNullOrWhiteSpace(this.BannerHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Uri}{Endpoints.BANNERS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.BannerHash}.{(this.BannerHash.StartsWith("a_") ? "gif" : "png")}" : null; /// /// Whether this guild has the community feature enabled. /// [JsonIgnore] public bool IsCommunity => this.Features.HasCommunityEnabled; /// /// Whether this guild has enabled the welcome screen. /// [JsonIgnore] public bool HasWelcomeScreen => this.Features.HasWelcomeScreenEnabled; /// /// Whether this guild has enabled membership screening. /// [JsonIgnore] public bool HasMemberVerificationGate => this.Features.HasMembershipScreeningEnabled; /// /// Gets this guild's premium tier (Nitro boosting). /// [JsonProperty("premium_tier")] public PremiumTier PremiumTier { get; internal set; } /// /// Gets the amount of members that boosted this guild. /// [JsonProperty("premium_subscription_count", NullValueHandling = NullValueHandling.Ignore)] public int? PremiumSubscriptionCount { get; internal set; } /// /// Whether the premium progress bar is enabled. /// [JsonProperty("premium_progress_bar_enabled", NullValueHandling = NullValueHandling.Ignore)] public bool PremiumProgressBarEnabled { get; internal set; } /// /// Gets whether this guild is designated as NSFW. /// [JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)] public bool IsNsfw { get; internal set; } /// /// Gets this guild's hub type, if applicable. /// [JsonProperty("hub_type", NullValueHandling = NullValueHandling.Ignore)] public HubType HubType { get; internal set; } /// /// Gets a dictionary of all by position ordered channels associated with this guild. The dictionary's key is the channel ID. /// [JsonIgnore] public IReadOnlyDictionary OrderedChannels => new ReadOnlyDictionary(this.InternalSortChannels()); /// /// Sorts the channels. /// private Dictionary InternalSortChannels() { Dictionary keyValuePairs = new(); var ochannels = this.GetOrderedChannels(); foreach (var ochan in ochannels) { if (ochan.Key != 0) keyValuePairs.Add(ochan.Key, this.GetChannel(ochan.Key)); foreach (var chan in ochan.Value) keyValuePairs.Add(chan.Id, chan); } return keyValuePairs; } /// /// Gets an ordered list out of the channel cache. /// Returns a Dictionary where the key is an ulong and can be mapped to s. /// Ignore the 0 key here, because that indicates that this is the "has no category" list. /// Each value contains a ordered list of text/news and voice/stage channels as . /// /// A ordered list of categories with its channels public Dictionary> GetOrderedChannels() { IReadOnlyList rawChannels = this.ChannelsInternal.Values.ToList(); Dictionary> orderedChannels = new() { { 0, new List() } }; foreach (var channel in rawChannels.Where(c => c.Type == ChannelType.Category).OrderBy(c => c.Position)) { orderedChannels.Add(channel.Id, new List()); } foreach (var channel in rawChannels.Where(c => c.ParentId.HasValue && (c.Type == ChannelType.Text || c.Type == ChannelType.News || c.Type == ChannelType.Forum)).OrderBy(c => c.Position)) { orderedChannels[channel.ParentId.Value].Add(channel); } foreach (var channel in rawChannels.Where(c => c.ParentId.HasValue && (c.Type == ChannelType.Voice || c.Type == ChannelType.Stage)).OrderBy(c => c.Position)) { orderedChannels[channel.ParentId.Value].Add(channel); } foreach (var channel in rawChannels.Where(c => !c.ParentId.HasValue && c.Type != ChannelType.Category && (c.Type == ChannelType.Text || c.Type == ChannelType.News || c.Type == ChannelType.Forum)).OrderBy(c => c.Position)) { orderedChannels[0].Add(channel); } foreach (var channel in rawChannels.Where(c => !c.ParentId.HasValue && c.Type != ChannelType.Category && (c.Type == ChannelType.Voice || c.Type == ChannelType.Stage)).OrderBy(c => c.Position)) { orderedChannels[0].Add(channel); } return orderedChannels; } /// /// Gets an ordered list. /// Returns a Dictionary where the key is an ulong and can be mapped to s. /// Ignore the 0 key here, because that indicates that this is the "has no category" list. /// Each value contains a ordered list of text/news and voice/stage channels as . /// /// A ordered list of categories with its channels public async Task>> GetOrderedChannelsAsync() { var rawChannels = await this.Discord.ApiClient.GetGuildChannelsAsync(this.Id); Dictionary> orderedChannels = new() { { 0, new List() } }; foreach (var channel in rawChannels.Where(c => c.Type == ChannelType.Category).OrderBy(c => c.Position)) { orderedChannels.Add(channel.Id, new List()); } foreach (var channel in rawChannels.Where(c => c.ParentId.HasValue && (c.Type == ChannelType.Text || c.Type == ChannelType.News || c.Type == ChannelType.Forum)).OrderBy(c => c.Position)) { orderedChannels[channel.ParentId.Value].Add(channel); } foreach (var channel in rawChannels.Where(c => c.ParentId.HasValue && (c.Type == ChannelType.Voice || c.Type == ChannelType.Stage)).OrderBy(c => c.Position)) { orderedChannels[channel.ParentId.Value].Add(channel); } foreach (var channel in rawChannels.Where(c => !c.ParentId.HasValue && c.Type != ChannelType.Category && (c.Type == ChannelType.Text || c.Type == ChannelType.News || c.Type == ChannelType.Forum)).OrderBy(c => c.Position)) { orderedChannels[0].Add(channel); } foreach (var channel in rawChannels.Where(c => !c.ParentId.HasValue && c.Type != ChannelType.Category && (c.Type == ChannelType.Voice || c.Type == ChannelType.Stage)).OrderBy(c => c.Position)) { orderedChannels[0].Add(channel); } return orderedChannels; } /// /// Whether it is synced. /// [JsonIgnore] internal bool IsSynced { get; set; } /// /// Initializes a new instance of the class. /// internal DiscordGuild() { this._currentMemberLazy = new Lazy(() => this.MembersInternal != null && this.MembersInternal.TryGetValue(this.Discord.CurrentUser.Id, out var member) ? member : null); this.Invites = new ConcurrentDictionary(); this.Threads = new ReadOnlyConcurrentDictionary(this.ThreadsInternal); this.StageInstances = new ReadOnlyConcurrentDictionary(this.StageInstancesInternal); this.ScheduledEvents = new ReadOnlyConcurrentDictionary(this.ScheduledEventsInternal); } #region Guild Methods /// /// Searches the current guild for members who's display name start with the specified name. /// /// The name to search for. /// The maximum amount of members to return. Max 1000. Defaults to 1. /// The members found, if any. public Task> SearchMembersAsync(string name, int? limit = 1) => this.Discord.ApiClient.SearchMembersAsync(this.Id, name, limit); /// /// Adds a new member to this guild /// /// User to add /// User's access token (OAuth2) /// new nickname /// new roles /// whether this user has to be muted /// whether this user has to be deafened /// Thrown when the client does not have the permission. /// Thrown when the or is not found. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task AddMemberAsync(DiscordUser user, string accessToken, string nickname = null, IEnumerable roles = null, bool muted = false, bool deaf = false) => this.Discord.ApiClient.AddGuildMemberAsync(this.Id, user.Id, accessToken, nickname, roles, muted, deaf); /// /// Deletes this guild. Requires the caller to be the owner of the guild. /// /// Thrown when the client is not the owner of the guild. /// Thrown when Discord is unable to process the request. public Task DeleteAsync() => this.Discord.ApiClient.DeleteGuildAsync(this.Id); /// /// Enables the mfa requirement for this guild. /// /// The audit log reason. /// Thrown when the current user is not the guilds owner. /// Thrown when the guild does not exist. /// Thrown when Discord is unable to process the request. public Task EnableMfaAsync(string reason = null) => this.IsOwner ? this.Discord.ApiClient.EnableGuildMfaAsync(this.Id, reason) : throw new Exception("The current user does not own the guild."); /// /// Disables the mfa requirement for this guild. /// /// The audit log reason. /// Thrown when the current user is not the guilds owner. /// Thrown when the guild does not exist. /// Thrown when Discord is unable to process the request. public Task DisableMfaAsync(string reason = null) => this.IsOwner ? this.Discord.ApiClient.DisableGuildMfaAsync(this.Id, reason) : throw new Exception("The current user does not own the guild."); /// /// Modifies this guild. /// /// Action to perform on this guild. /// The modified guild object. /// Thrown when the client does not have the permission. /// 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 ModifyAsync(Action action) { var mdl = new GuildEditModel(); action(mdl); var afkChannelId = mdl.PublicUpdatesChannel .MapOrNull(c => c.Type != ChannelType.Voice ? throw new ArgumentException("AFK channel needs to be a text channel.") : c.Id); static Optional ChannelToId(Optional ch, string name) => ch.MapOrNull(c => c.Type != ChannelType.Text && c.Type != ChannelType.News ? throw new ArgumentException($"{name} channel needs to be a text channel.") : c.Id); var rulesChannelId = ChannelToId(mdl.RulesChannel, "Rules"); var publicUpdatesChannelId = ChannelToId(mdl.PublicUpdatesChannel, "Public updates"); var systemChannelId = ChannelToId(mdl.SystemChannel, "System"); var iconb64 = ImageTool.Base64FromStream(mdl.Icon); var splashb64 = ImageTool.Base64FromStream(mdl.Splash); var bannerb64 = ImageTool.Base64FromStream(mdl.Banner); var discoverySplash64 = ImageTool.Base64FromStream(mdl.DiscoverySplash); return await this.Discord.ApiClient.ModifyGuildAsync(this.Id, mdl.Name, mdl.VerificationLevel, mdl.DefaultMessageNotifications, mdl.MfaLevel, mdl.ExplicitContentFilter, afkChannelId, mdl.AfkTimeout, iconb64, mdl.Owner.Map(e => e.Id), splashb64, systemChannelId, mdl.SystemChannelFlags, publicUpdatesChannelId, rulesChannelId, mdl.Description, bannerb64, discoverySplash64, mdl.PreferredLocale, mdl.PremiumProgressBarEnabled, mdl.AuditLogReason).ConfigureAwait(false); } /// /// Modifies the community settings async. /// This sets if not highest and . /// /// If true, enable . /// The rules channel. /// The public updates channel. /// The preferred locale. Defaults to en-US. /// The description. /// The default message notifications. Defaults to /// The audit log reason. /// Thrown when the client does not have the permission. /// 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 ModifyCommunitySettingsAsync(bool enabled, DiscordChannel rulesChannel = null, DiscordChannel publicUpdatesChannel = null, string preferredLocale = "en-US", string description = null, DefaultMessageNotifications defaultMessageNotifications = DefaultMessageNotifications.MentionsOnly, string reason = null) { var verificationLevel = this.VerificationLevel; if (this.VerificationLevel != VerificationLevel.Highest) { verificationLevel = VerificationLevel.High; } var explicitContentFilter = ExplicitContentFilter.AllMembers; static Optional ChannelToId(DiscordChannel ch, string name) => ch == null ? null : ch.Type != ChannelType.Text && ch.Type != ChannelType.News ? throw new ArgumentException($"{name} channel needs to be a text channel.") : ch.Id; var rulesChannelId = ChannelToId(rulesChannel, "Rules"); var publicUpdatesChannelId = ChannelToId(publicUpdatesChannel, "Public updates"); List features = new(); var rfeatures = this.RawFeatures.ToList(); if (!this.RawFeatures.Contains("COMMUNITY") && enabled) { rfeatures.Add("COMMUNITY"); } else if (this.RawFeatures.Contains("COMMUNITY") && !enabled) { rfeatures.Remove("COMMUNITY"); } features = rfeatures; return await this.Discord.ApiClient.ModifyGuildCommunitySettingsAsync(this.Id, features, rulesChannelId, publicUpdatesChannelId, preferredLocale, description, defaultMessageNotifications, explicitContentFilter, verificationLevel, reason).ConfigureAwait(false); } /// /// Enables invites for the guild. /// /// The audit log reason. /// Thrown when the client does not have the permission. /// 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 EnableInvitesAsync(string reason = null) { List features = new(); var rfeatures = this.RawFeatures.ToList(); if (this.Features.InvitesDisabled) rfeatures.Remove("INVITES_DISABLED"); features = rfeatures; return await this.Discord.ApiClient.ModifyGuildFeaturesAsync(this.Id, features, reason); } /// /// Disables invites for the guild. /// /// /// /// Thrown when the client does not have the permission. /// 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 DisableInvitesAsync(string reason = null) { List features = new(); var rfeatures = this.RawFeatures.ToList(); if (!this.Features.InvitesDisabled) rfeatures.Add("INVITES_DISABLED"); features = rfeatures; return await this.Discord.ApiClient.ModifyGuildFeaturesAsync(this.Id, features, reason); } /// /// Timeout a specified member in this guild. /// /// Member to timeout. /// The datetime offset to time out the user. Up to 28 days. /// Reason for audit logs. /// 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 TimeoutAsync(ulong memberId, DateTimeOffset until, string reason = null) => until.Subtract(DateTimeOffset.UtcNow).Days > 28 ? throw new ArgumentException("Timeout can not be longer than 28 days") : this.Discord.ApiClient.ModifyTimeoutAsync(this.Id, memberId, until, reason); /// /// Timeout a specified member in this guild. /// /// Member to timeout. /// The timespan to time out the user. Up to 28 days. /// Reason for audit logs. /// 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 TimeoutAsync(ulong memberId, TimeSpan until, string reason = null) => this.TimeoutAsync(memberId, DateTimeOffset.UtcNow + until, reason); /// /// Timeout a specified member in this guild. /// /// Member to timeout. /// The datetime to time out the user. Up to 28 days. /// Reason for audit logs. /// 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 TimeoutAsync(ulong memberId, DateTime until, string reason = null) => this.TimeoutAsync(memberId, until.ToUniversalTime() - DateTime.UtcNow, reason); /// /// Removes the timeout from a specified member in this guild. /// /// Member to remove the timeout from. /// Reason for audit logs. /// 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 RemoveTimeoutAsync(ulong memberId, string reason = null) => this.Discord.ApiClient.ModifyTimeoutAsync(this.Id, memberId, null, reason); /// /// Bans a specified member from this guild. /// /// Member to ban. /// How many days to remove messages from. /// Reason for audit logs. /// 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 BanMemberAsync(DiscordMember member, int deleteMessageDays = 0, string reason = null) => this.Discord.ApiClient.CreateGuildBanAsync(this.Id, member.Id, deleteMessageDays, reason); /// /// Bans a specified user by ID. This doesn't require the user to be in this guild. /// /// ID of the user to ban. /// How many days to remove messages from. /// Reason for audit logs. /// 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 BanMemberAsync(ulong userId, int deleteMessageDays = 0, string reason = null) => this.Discord.ApiClient.CreateGuildBanAsync(this.Id, userId, deleteMessageDays, reason); /// /// Unbans a user from this guild. /// /// User to unban. /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the user does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task UnbanMemberAsync(DiscordUser user, string reason = null) => this.Discord.ApiClient.RemoveGuildBanAsync(this.Id, user.Id, reason); /// /// Unbans a user by ID. /// /// ID of the user to unban. /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the user does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task UnbanMemberAsync(ulong userId, string reason = null) => this.Discord.ApiClient.RemoveGuildBanAsync(this.Id, userId, reason); /// /// Leaves this guild. /// /// Thrown when Discord is unable to process the request. public Task LeaveAsync() => this.Discord.ApiClient.LeaveGuildAsync(this.Id); /// /// Gets the bans for this guild, allowing for pagination. /// /// Maximum number of bans to fetch. Max 1000. Defaults to 1000. /// The Id of the user before which to fetch the bans. Overrides if both are present. /// The Id of the user after which to fetch the bans. /// Collection of bans in this guild in ascending order by user id. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. public Task> GetBansAsync(int? limit = null, ulong? before = null, ulong? after = null) => this.Discord.ApiClient.GetGuildBansAsync(this.Id, limit, before, after); /// /// Gets a ban for a specific user. /// /// The Id of the user to get the ban for. /// The requested ban object. /// Thrown when the specified user is not banned. public Task GetBanAsync(ulong userId) => this.Discord.ApiClient.GetGuildBanAsync(this.Id, userId); + /// + /// Tries to get a ban for a specific user. + /// + /// The Id of the user to get the ban for. + /// The requested ban object or null if not found. + public async Task TryGetBanAsync(ulong userId) + { + try + { + return await this.GetBanAsync(userId).ConfigureAwait(false); + } + catch (NotFoundException) + { + return null; + } + } + + /// /// Gets a ban for a specific user. /// /// The user to get the ban for. /// The requested ban object. /// Thrown when the specified user is not banned. public Task GetBanAsync(DiscordUser user) => this.GetBanAsync(user.Id); + /// + /// Tries to get a ban for a specific user. + /// + /// The user to get the ban for. + /// The requested ban object or null if not found. + public async Task TryGetBanAsync(DiscordUser user) + { + try + { + return await this.GetBanAsync(user).ConfigureAwait(false); + } + catch (NotFoundException) + { + return null; + } + } + #region Scheduled Events /// /// Creates a scheduled event. /// /// The name. /// The scheduled start time. /// The scheduled end time. /// The channel. /// The metadata. /// The description. /// The type. /// The cover image. /// The reason. /// A scheduled event. /// 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 CreateScheduledEventAsync(string name, DateTimeOffset scheduledStartTime, DateTimeOffset? scheduledEndTime = null, DiscordChannel channel = null, DiscordScheduledEventEntityMetadata metadata = null, string description = null, ScheduledEventEntityType type = ScheduledEventEntityType.StageInstance, Optional coverImage = default, string reason = null) { var coverb64 = ImageTool.Base64FromStream(coverImage); return await this.Discord.ApiClient.CreateGuildScheduledEventAsync(this.Id, type == ScheduledEventEntityType.External ? null : channel?.Id, type == ScheduledEventEntityType.External ? metadata : null, name, scheduledStartTime, scheduledEndTime.HasValue && type == ScheduledEventEntityType.External ? scheduledEndTime.Value : null, description, type, coverb64, reason); } /// /// Creates a scheduled event with type . /// /// The name. /// The scheduled start time. /// The scheduled end time. /// The location of the external event. /// The description. /// The cover image. /// The reason. /// A scheduled event. /// 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 CreateExternalScheduledEventAsync(string name, DateTimeOffset scheduledStartTime, DateTimeOffset scheduledEndTime, string location, string description = null, Optional coverImage = default, string reason = null) { var coverb64 = ImageTool.Base64FromStream(coverImage); return await this.Discord.ApiClient.CreateGuildScheduledEventAsync(this.Id, null, new DiscordScheduledEventEntityMetadata(location), name, scheduledStartTime, scheduledEndTime, description, ScheduledEventEntityType.External, coverb64, reason); } /// /// Gets a specific scheduled events. /// /// The Id of the event to get. /// Whether to include user count. /// A scheduled event. /// 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 GetScheduledEventAsync(ulong scheduledEventId, bool? withUserCount = null) => this.ScheduledEventsInternal.TryGetValue(scheduledEventId, out var ev) ? ev : await this.Discord.ApiClient.GetGuildScheduledEventAsync(this.Id, scheduledEventId, withUserCount); + /// + /// Tries to get a specific scheduled events. + /// + /// The Id of the event to get. + /// Whether to include user count. + /// A scheduled event or null if not found. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task TryGetScheduledEventAsync(ulong scheduledEventId, bool? withUserCount = null) + { + try + { + return await this.GetScheduledEventAsync(scheduledEventId, withUserCount).ConfigureAwait(false); + } + catch (NotFoundException) + { + return null; + } + } + /// /// Gets a specific scheduled events. /// /// The event to get. /// Whether to include user count. /// A scheduled event. /// 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 GetScheduledEventAsync(DiscordScheduledEvent scheduledEvent, bool? withUserCount = null) => await this.GetScheduledEventAsync(scheduledEvent.Id, withUserCount); + /// + /// Tries to get a specific scheduled events. + /// + /// The event to get. + /// Whether to include user count. + /// A scheduled event or null if not found. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task TryGetScheduledEventAsync(DiscordScheduledEvent scheduledEvent, bool? withUserCount = null) + { + try + { + return await this.GetScheduledEventAsync(scheduledEvent, withUserCount).ConfigureAwait(false); + } + catch (NotFoundException) + { + return null; + } + } + /// /// Gets the guilds scheduled events. /// /// Whether to include user count. /// A list of the guilds scheduled events. /// 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> GetScheduledEventsAsync(bool? withUserCount = null) => await this.Discord.ApiClient.ListGuildScheduledEventsAsync(this.Id, withUserCount); #endregion /// /// Creates a new text channel in this guild. /// /// Name of the new channel. /// Category to put this channel in. /// Topic of the channel. /// Permission overwrites for this channel. /// Whether the channel is to be flagged as not safe for work. /// Slow mode timeout for users. /// The default auto archive duration for new threads. /// Reason for audit logs. /// The newly-created channel. /// Thrown when the client does not have the permission. /// 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 CreateTextChannelAsync(string name, DiscordChannel parent = null, Optional topic = default, IEnumerable overwrites = null, bool? nsfw = null, Optional perUserRateLimit = default, ThreadAutoArchiveDuration defaultAutoArchiveDuration = ThreadAutoArchiveDuration.OneDay, Optional flags = default, string reason = null) => this.CreateChannelAsync(name, ChannelType.Text, parent, topic, null, null, overwrites, nsfw, perUserRateLimit, null, defaultAutoArchiveDuration, flags, reason); /// /// Creates a new forum channel in this guild. /// The field template is not yet released, so it won't applied. /// /// Name of the new channel. /// Category to put this channel in. /// Topic of the channel. /// Permission overwrites for this channel. /// Whether the channel is to be flagged as not safe for work. /// The default reaction emoji for posts. /// Slow mode timeout for users. /// Slow mode timeout for user post creations. /// The default auto archive duration for new threads. /// Reason for audit logs. /// The newly-created channel. /// Thrown when the client does not have the permission or the guild does not have the forum channel feature. /// 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 CreateForumChannelAsync(string name, DiscordChannel parent = null, Optional topic = default, IEnumerable overwrites = null, bool? nsfw = null, Optional defaultReactionEmoji = default, Optional perUserRateLimit = default, Optional postCreateUserRateLimit = default, ThreadAutoArchiveDuration defaultAutoArchiveDuration = ThreadAutoArchiveDuration.OneDay, Optional defaultSortOrder = default, Optional flags = default, string reason = null) => this.Discord.ApiClient.CreateForumChannelAsync(this.Id, name, parent?.Id, topic, null, nsfw, defaultReactionEmoji, perUserRateLimit, postCreateUserRateLimit, defaultSortOrder, defaultAutoArchiveDuration, overwrites, flags, reason); /// /// Creates a new channel category in this guild. /// /// Name of the new category. /// Permission overwrites for this category. /// Reason for audit logs. /// The newly-created channel category. /// Thrown when the client does not have the permission. /// 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 CreateChannelCategoryAsync(string name, IEnumerable overwrites = null, string reason = null) => this.CreateChannelAsync(name, ChannelType.Category, null, Optional.None, null, null, overwrites, null, Optional.None, null, null, Optional.None, reason); /// /// Creates a new stage channel in this guild. /// /// Name of the new stage channel. /// Permission overwrites for this stage channel. /// Reason for audit logs. /// The newly-created stage channel. /// Thrown when the client does not have the . /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. /// Thrown when the guilds has not enabled community. public Task CreateStageChannelAsync(string name, IEnumerable overwrites = null, string reason = null) => this.Features.HasCommunityEnabled ? this.CreateChannelAsync(name, ChannelType.Stage, null, Optional.None, null, null, overwrites, null, Optional.None, null, null, Optional.None, reason) : throw new NotSupportedException("Guild has not enabled community. Can not create a stage channel."); /// /// Creates a new news channel in this guild. /// /// Name of the new news channel. /// Permission overwrites for this news channel. /// The default auto archive duration for new threads. /// Reason for audit logs. /// The newly-created news channel. /// Thrown when the client does not have the . /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. /// Thrown when the guilds has not enabled community. public Task CreateNewsChannelAsync(string name, IEnumerable overwrites = null, string reason = null, ThreadAutoArchiveDuration defaultAutoArchiveDuration = ThreadAutoArchiveDuration.OneDay, Optional flags = default) => this.Features.HasCommunityEnabled ? this.CreateChannelAsync(name, ChannelType.News, null, Optional.None, null, null, overwrites, null, Optional.None, null, defaultAutoArchiveDuration, flags, reason) : throw new NotSupportedException("Guild has not enabled community. Can not create a news channel."); /// /// Creates a new voice channel in this guild. /// /// Name of the new channel. /// Category to put this channel in. /// Bitrate of the channel. /// Maximum number of users in the channel. /// Permission overwrites for this channel. /// Video quality mode of the channel. /// Reason for audit logs. /// The newly-created channel. /// Thrown when the client does not have the permission. /// 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 CreateVoiceChannelAsync(string name, DiscordChannel parent = null, int? bitrate = null, int? userLimit = null, IEnumerable overwrites = null, VideoQualityMode? qualityMode = null, Optional flags = default, string reason = null) => this.CreateChannelAsync(name, ChannelType.Voice, parent, Optional.None, bitrate, userLimit, overwrites, null, Optional.None, qualityMode, null, flags, reason); /// /// Creates a new channel in this guild. /// /// Name of the new channel. /// Type of the new channel. /// Category to put this channel in. /// Topic of the channel. /// Bitrate of the channel. Applies to voice only. /// Maximum number of users in the channel. Applies to voice only. /// Permission overwrites for this channel. /// Whether the channel is to be flagged as not safe for work. Applies to text only. /// Slow mode timeout for users. /// Video quality mode of the channel. Applies to voice only. /// The default auto archive duration for new threads. /// Reason for audit logs. /// The newly-created channel. /// Thrown when the client does not have the permission. /// 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 CreateChannelAsync(string name, ChannelType type, DiscordChannel parent = null, Optional topic = default, int? bitrate = null, int? userLimit = null, IEnumerable overwrites = null, bool? nsfw = null, Optional perUserRateLimit = default, VideoQualityMode? qualityMode = null, ThreadAutoArchiveDuration? defaultAutoArchiveDuration = null, Optional flags = default, string reason = null) => // technically you can create news/store channels but not always type != ChannelType.Text && type != ChannelType.Voice && type != ChannelType.Category && type != ChannelType.News && type != ChannelType.Store && type != ChannelType.Stage ? throw new ArgumentException("Channel type must be text, voice, stage, or category.", nameof(type)) : type == ChannelType.Category && parent != null ? throw new ArgumentException("Cannot specify parent of a channel category.", nameof(parent)) : this.Discord.ApiClient.CreateGuildChannelAsync(this.Id, name, type, parent?.Id, topic, bitrate, userLimit, overwrites, nsfw, perUserRateLimit, qualityMode, defaultAutoArchiveDuration, flags, reason); /// /// Gets active threads. Can contain more threads. /// If the result's value 'HasMore' is true, you need to recall this function to get older threads. /// /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task GetActiveThreadsAsync() => this.Discord.ApiClient.GetActiveThreadsAsync(this.Id); /// /// Deletes all channels in this guild. /// Note that this is irreversible. Use carefully! /// /// public Task DeleteAllChannelsAsync() { var tasks = this.Channels.Values.Select(xc => xc.DeleteAsync()); return Task.WhenAll(tasks); } /// /// Estimates the number of users to be pruned. /// /// Minimum number of inactivity days required for users to be pruned. Defaults to 7. /// The roles to be included in the prune. /// Number of users that will be pruned. /// Thrown when the client does not have the permission. /// 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 GetPruneCountAsync(int days = 7, IEnumerable includedRoles = null) { if (includedRoles != null) { includedRoles = includedRoles.Where(r => r != null); var rawRoleIds = includedRoles .Where(x => this.RolesInternal.ContainsKey(x.Id)) .Select(x => x.Id); return this.Discord.ApiClient.GetGuildPruneCountAsync(this.Id, days, rawRoleIds); } return this.Discord.ApiClient.GetGuildPruneCountAsync(this.Id, days, null); } /// /// Prunes inactive users from this guild. /// /// Minimum number of inactivity days required for users to be pruned. Defaults to 7. /// Whether to return the prune count after this method completes. This is discouraged for larger guilds. /// The roles to be included in the prune. /// Reason for audit logs. /// Number of users pruned. /// Thrown when the client does not have the permission. /// 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 PruneAsync(int days = 7, bool computePruneCount = true, IEnumerable includedRoles = null, string reason = null) { if (includedRoles != null) { includedRoles = includedRoles.Where(r => r != null); var rawRoleIds = includedRoles .Where(x => this.RolesInternal.ContainsKey(x.Id)) .Select(x => x.Id); return this.Discord.ApiClient.BeginGuildPruneAsync(this.Id, days, computePruneCount, rawRoleIds, reason); } return this.Discord.ApiClient.BeginGuildPruneAsync(this.Id, days, computePruneCount, null, reason); } /// /// Gets integrations attached to this guild. /// /// Collection of integrations attached to this guild. /// Thrown when the client does not have the permission. /// 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> GetIntegrationsAsync() => this.Discord.ApiClient.GetGuildIntegrationsAsync(this.Id); /// /// Attaches an integration from current user to this guild. /// /// Integration to attach. /// The integration after being attached to the guild. /// Thrown when the client does not have the permission. /// 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 AttachUserIntegrationAsync(DiscordIntegration integration) => this.Discord.ApiClient.CreateGuildIntegrationAsync(this.Id, integration.Type, integration.Id); /// /// Modifies an integration in this guild. /// /// Integration to modify. /// Number of days after which the integration expires. /// Length of grace period which allows for renewing the integration. /// Whether emotes should be synced from this integration. /// The modified integration. /// Thrown when the client does not have the permission. /// 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 ModifyIntegrationAsync(DiscordIntegration integration, int expireBehaviour, int expireGracePeriod, bool enableEmoticons) => this.Discord.ApiClient.ModifyGuildIntegrationAsync(this.Id, integration.Id, expireBehaviour, expireGracePeriod, enableEmoticons); /// /// Removes an integration from this guild. /// /// Integration to remove. /// Thrown when the client does not have the permission. /// 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 DeleteIntegrationAsync(DiscordIntegration integration) => this.Discord.ApiClient.DeleteGuildIntegrationAsync(this.Id, integration); /// /// Forces re-synchronization of an integration for this guild. /// /// Integration to synchronize. /// Thrown when the client does not have the permission. /// 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 SyncIntegrationAsync(DiscordIntegration integration) => this.Discord.ApiClient.SyncGuildIntegrationAsync(this.Id, integration.Id); /// /// Gets the voice regions for this guild. /// /// Voice regions available for this guild. /// Thrown when Discord is unable to process the request. public async Task> ListVoiceRegionsAsync() { var vrs = await this.Discord.ApiClient.GetGuildVoiceRegionsAsync(this.Id).ConfigureAwait(false); foreach (var xvr in vrs) this.Discord.InternalVoiceRegions.TryAdd(xvr.Id, xvr); return vrs; } /// /// Gets an invite from this guild from an invite code. /// /// The invite code /// An invite, or null if not in cache. public DiscordInvite GetInvite(string code) => this.Invites.TryGetValue(code, out var invite) ? invite : null; /// /// Gets all the invites created for all the channels in this guild. /// /// A collection of invites. /// Thrown when Discord is unable to process the request. public async Task> GetInvitesAsync() { var res = await this.Discord.ApiClient.GetGuildInvitesAsync(this.Id).ConfigureAwait(false); var intents = this.Discord.Configuration.Intents; if (!intents.HasIntent(DiscordIntents.GuildInvites)) { foreach (var r in res) this.Invites[r.Code] = r; } return res; } /// /// Gets the vanity invite for this guild. /// /// A partial vanity invite. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. public Task GetVanityInviteAsync() => this.Discord.ApiClient.GetGuildVanityUrlAsync(this.Id); /// /// Gets all the webhooks created for all the channels in this guild. /// /// A collection of webhooks this guild has. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. public Task> GetWebhooksAsync() => this.Discord.ApiClient.GetGuildWebhooksAsync(this.Id); /// /// Gets this guild's widget image. /// /// The format of the widget. /// The URL of the widget image. public string GetWidgetImage(WidgetType bannerType = WidgetType.Shield) { var param = bannerType switch { WidgetType.Banner1 => "banner1", WidgetType.Banner2 => "banner2", WidgetType.Banner3 => "banner3", WidgetType.Banner4 => "banner4", _ => "shield", }; return $"{Endpoints.BASE_URI}{Endpoints.GUILDS}/{this.Id}{Endpoints.WIDGET_PNG}?style={param}"; } /// /// Gets a member of this guild by their user ID. /// /// ID of the member to get. /// The requested member. /// Thrown when Discord is unable to process the request. public async Task GetMemberAsync(ulong userId, bool fetch = false) { if (!fetch && this.MembersInternal != null && this.MembersInternal.TryGetValue(userId, out var mbr)) return mbr; mbr = await this.Discord.ApiClient.GetGuildMemberAsync(this.Id, userId).ConfigureAwait(false); var intents = this.Discord.Configuration.Intents; if (intents.HasIntent(DiscordIntents.GuildMembers)) { if (this.MembersInternal != null) { this.MembersInternal[userId] = mbr; } } return mbr; } /// /// Retrieves a full list of members from Discord. This method will bypass cache. /// /// A collection of all members in this guild. /// Thrown when Discord is unable to process the request. public async Task> GetAllMembersAsync() { var recmbr = new HashSet(); var recd = 1000; var last = 0ul; while (recd > 0) { var tms = await this.Discord.ApiClient.ListGuildMembersAsync(this.Id, 1000, last == 0 ? null : (ulong?)last).ConfigureAwait(false); recd = tms.Count; foreach (var xtm in tms) { var usr = new DiscordUser(xtm.User) { Discord = this.Discord }; usr = this.Discord.UserCache.AddOrUpdate(xtm.User.Id, usr, (id, old) => { old.Username = usr.Username; old.Discord = usr.Discord; old.AvatarHash = usr.AvatarHash; return old; }); recmbr.Add(new DiscordMember(xtm) { Discord = this.Discord, GuildId = this.Id }); } var tm = tms.LastOrDefault(); last = tm?.User.Id ?? 0; } return new ReadOnlySet(recmbr); } /// /// Requests that Discord send a list of guild members based on the specified arguments. This method will fire the event. /// If no arguments aside from and are specified, this will request all guild members. /// /// Filters the returned members based on what the username starts with. Either this or must not be null. /// The must also be greater than 0 if this is specified. /// Total number of members to request. This must be greater than 0 if is specified. /// Whether to include the associated with the fetched members. /// Whether to limit the request to the specified user ids. Either this or must not be null. /// The unique string to identify the response. public async Task RequestMembersAsync(string query = "", int limit = 0, bool? presences = null, IEnumerable userIds = null, string nonce = null) { if (this.Discord is not DiscordClient client) throw new InvalidOperationException("This operation is only valid for regular Discord clients."); if (query == null && userIds == null) throw new ArgumentException("The query and user IDs cannot both be null."); if (query != null && userIds != null) query = null; var grgm = new GatewayRequestGuildMembers(this) { Query = query, Limit = limit >= 0 ? limit : 0, Presences = presences, UserIds = userIds, Nonce = nonce }; var payload = new GatewayPayload { OpCode = GatewayOpCode.RequestGuildMembers, Data = grgm }; var payloadStr = JsonConvert.SerializeObject(payload, Formatting.None); await client.WsSendAsync(payloadStr).ConfigureAwait(false); } /// /// Gets all the channels this guild has. /// /// A collection of this guild's channels. /// Thrown when Discord is unable to process the request. public Task> GetChannelsAsync() => this.Discord.ApiClient.GetGuildChannelsAsync(this.Id); /// /// Creates a new role in this guild. /// /// Name of the role. /// Permissions for the role. /// Color for the role. /// Whether the role is to be hoisted. /// Whether the role is to be mentionable. /// Reason for audit logs. /// The newly-created role. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. public Task CreateRoleAsync(string name = null, Permissions? permissions = null, DiscordColor? color = null, bool? hoist = null, bool? mentionable = null, string reason = null) => this.Discord.ApiClient.CreateGuildRoleAsync(this.Id, name, permissions, color?.Value, hoist, mentionable, reason); /// /// Gets a role from this guild by its ID. /// /// ID of the role to get. /// Requested role. /// Thrown when Discord is unable to process the request. public DiscordRole GetRole(ulong id) => this.RolesInternal.TryGetValue(id, out var role) ? role : null; /// /// Gets a channel from this guild by its ID. /// /// ID of the channel to get. /// Requested channel. /// Thrown when Discord is unable to process the request. public DiscordChannel GetChannel(ulong id) => this.ChannelsInternal != null && this.ChannelsInternal.TryGetValue(id, out var channel) ? channel : null; /// /// Gets a thread from this guild by its ID. /// /// ID of the thread to get. /// Requested thread. /// Thrown when Discord is unable to process the request. public DiscordThreadChannel GetThread(ulong id) => this.ThreadsInternal != null && this.ThreadsInternal.TryGetValue(id, out var thread) ? thread : null; /// /// Gets all of this guild's custom emojis. /// /// All of this guild's custom emojis. /// Thrown when Discord is unable to process the request. public Task> GetEmojisAsync() => this.Discord.ApiClient.GetGuildEmojisAsync(this.Id); /// /// Gets this guild's specified custom emoji. /// /// ID of the emoji to get. /// The requested custom emoji. /// Thrown when Discord is unable to process the request. public Task GetEmojiAsync(ulong id) => this.Discord.ApiClient.GetGuildEmojiAsync(this.Id, id); /// /// Creates a new custom emoji for this guild. /// /// Name of the new emoji. /// Image to use as the emoji. /// Roles for which the emoji will be available. This works only if your application is whitelisted as integration. /// Reason for audit log. /// The newly-created emoji. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. public Task CreateEmojiAsync(string name, Stream image, IEnumerable roles = null, string reason = null) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name)); name = name.Trim(); if (name.Length < 2 || name.Length > 50) throw new ArgumentException("Emoji name needs to be between 2 and 50 characters long."); if (image == null) throw new ArgumentNullException(nameof(image)); var image64 = ImageTool.Base64FromStream(image); return this.Discord.ApiClient.CreateGuildEmojiAsync(this.Id, name, image64, roles?.Select(xr => xr.Id), reason); } /// /// Modifies a this guild's custom emoji. /// /// Emoji to modify. /// New name for the emoji. /// Roles for which the emoji will be available. This works only if your application is whitelisted as integration. /// Reason for audit log. /// The modified emoji. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. public Task ModifyEmojiAsync(DiscordGuildEmoji emoji, string name, IEnumerable roles = null, string reason = null) { if (emoji == null) throw new ArgumentNullException(nameof(emoji)); if (emoji.Guild.Id != this.Id) throw new ArgumentException("This emoji does not belong to this guild."); if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name)); name = name.Trim(); return name.Length < 2 || name.Length > 50 ? throw new ArgumentException("Emoji name needs to be between 2 and 50 characters long.") : this.Discord.ApiClient.ModifyGuildEmojiAsync(this.Id, emoji.Id, name, roles?.Select(xr => xr.Id), reason); } /// /// Deletes this guild's custom emoji. /// /// Emoji to delete. /// Reason for audit log. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. public Task DeleteEmojiAsync(DiscordGuildEmoji emoji, string reason = null) => emoji == null ? throw new ArgumentNullException(nameof(emoji)) : emoji.Guild.Id != this.Id ? throw new ArgumentException("This emoji does not belong to this guild.") : this.Discord.ApiClient.DeleteGuildEmojiAsync(this.Id, emoji.Id, reason); /// /// Gets all of this guild's custom stickers. /// /// All of this guild's custom stickers. /// Thrown when Discord is unable to process the request. public async Task> GetStickersAsync() { var stickers = await this.Discord.ApiClient.GetGuildStickersAsync(this.Id); foreach (var xstr in stickers) { this.StickersInternal.AddOrUpdate(xstr.Id, xstr, (id, old) => { old.Name = xstr.Name; old.Description = xstr.Description; old.InternalTags = xstr.InternalTags; return old; }); } return stickers; } /// /// Gets a sticker /// /// Thrown when the sticker could not be found. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. /// Sticker does not belong to a guild. public Task GetStickerAsync(ulong stickerId) => this.Discord.ApiClient.GetGuildStickerAsync(this.Id, stickerId); /// /// Creates a sticker /// /// The name of the sticker. /// The optional description of the sticker. /// The emoji to associate the sticker with. /// The file format the sticker is written in. /// The sticker. /// Audit log reason /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. public Task CreateStickerAsync(string name, string description, DiscordEmoji emoji, Stream file, StickerFormat format, string reason = null) { var fileExt = format switch { StickerFormat.Png => "png", StickerFormat.Apng => "png", StickerFormat.Lottie => "json", _ => throw new InvalidOperationException("This format is not supported.") }; var contentType = format switch { StickerFormat.Png => "image/png", StickerFormat.Apng => "image/png", StickerFormat.Lottie => "application/json", _ => throw new InvalidOperationException("This format is not supported.") }; return emoji.Id is not 0 ? throw new InvalidOperationException("Only unicode emoji can be used for stickers.") : name.Length < 2 || name.Length > 30 ? throw new ArgumentOutOfRangeException(nameof(name), "Sticker name needs to be between 2 and 30 characters long.") : description.Length < 1 || description.Length > 100 ? throw new ArgumentOutOfRangeException(nameof(description), "Sticker description needs to be between 1 and 100 characters long.") : this.Discord.ApiClient.CreateGuildStickerAsync(this.Id, name, description, emoji.GetDiscordName().Replace(":", ""), new DiscordMessageFile("sticker", file, null, fileExt, contentType), reason); } /// /// Modifies a sticker /// /// The id of the sticker to modify /// The name of the sticker /// The description of the sticker /// The emoji to associate with this sticker. /// Audit log reason /// A sticker object /// Thrown when the sticker could not be found. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. /// Sticker does not belong to a guild. public async Task ModifyStickerAsync(ulong sticker, Optional name, Optional description, Optional emoji, string reason = null) { if (!this.StickersInternal.TryGetValue(sticker, out var stickerobj) || stickerobj.Guild.Id != this.Id) throw new ArgumentException("This sticker does not belong to this guild."); if (name.HasValue && (name.Value.Length < 2 || name.Value.Length > 30)) throw new ArgumentException("Sticker name needs to be between 2 and 30 characters long."); if (description.HasValue && (description.Value.Length < 1 || description.Value.Length > 100)) throw new ArgumentException("Sticker description needs to be between 1 and 100 characters long."); if (emoji.HasValue && emoji.Value.Id > 0) throw new ArgumentException("Only unicode emojis can be used with stickers."); string uemoji = null; if (emoji.HasValue) uemoji = emoji.Value.GetDiscordName().Replace(":", ""); var usticker = await this.Discord.ApiClient.ModifyGuildStickerAsync(this.Id, sticker, name, description, uemoji, reason).ConfigureAwait(false); if (this.StickersInternal.TryGetValue(usticker.Id, out var old)) this.StickersInternal.TryUpdate(usticker.Id, usticker, old); return usticker; } /// /// Modifies a sticker /// /// The sticker to modify /// The name of the sticker /// The description of the sticker /// The emoji to associate with this sticker. /// Audit log reason /// A sticker object /// Thrown when the sticker could not be found. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. /// Sticker does not belong to a guild. public Task ModifyStickerAsync(DiscordSticker sticker, Optional name, Optional description, Optional emoji, string reason = null) => this.ModifyStickerAsync(sticker.Id, name, description, emoji, reason); /// /// Deletes a sticker /// /// Id of sticker to delete /// Audit log reason /// Thrown when the sticker could not be found. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. /// Sticker does not belong to a guild. public Task DeleteStickerAsync(ulong sticker, string reason = null) => !this.StickersInternal.TryGetValue(sticker, out var stickerobj) ? throw new ArgumentNullException(nameof(sticker)) : stickerobj.Guild.Id != this.Id ? throw new ArgumentException("This sticker does not belong to this guild.") : this.Discord.ApiClient.DeleteGuildStickerAsync(this.Id, sticker, reason); /// /// Deletes a sticker /// /// Sticker to delete /// Audit log reason /// Thrown when the sticker could not be found. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. /// Sticker does not belong to a guild. public Task DeleteStickerAsync(DiscordSticker sticker, string reason = null) => this.DeleteStickerAsync(sticker.Id, reason); /// /// Gets the default channel for this guild. /// Default channel is the first channel current member can see. /// /// This member's default guild. /// Thrown when Discord is unable to process the request. public DiscordChannel GetDefaultChannel() => this.ChannelsInternal?.Values.Where(xc => xc.Type == ChannelType.Text) .OrderBy(xc => xc.Position) .FirstOrDefault(xc => (xc.PermissionsFor(this.CurrentMember) & DisCatSharp.Enums.Permissions.AccessChannels) == DisCatSharp.Enums.Permissions.AccessChannels); /// /// Gets the guild's widget /// /// The guild's widget public Task GetWidgetAsync() => this.Discord.ApiClient.GetGuildWidgetAsync(this.Id); /// /// Gets the guild's widget settings /// /// The guild's widget settings public Task GetWidgetSettingsAsync() => this.Discord.ApiClient.GetGuildWidgetSettingsAsync(this.Id); /// /// Modifies the guild's widget settings /// /// If the widget is enabled or not /// Widget channel /// Reason the widget settings were modified /// The newly modified widget settings public Task ModifyWidgetSettingsAsync(bool? isEnabled = null, DiscordChannel channel = null, string reason = null) => this.Discord.ApiClient.ModifyGuildWidgetSettingsAsync(this.Id, isEnabled, channel?.Id, reason); /// /// Gets all of this guild's templates. /// /// All of the guild's templates. /// Throws when the client does not have the permission. /// Thrown when Discord is unable to process the request. public Task> GetTemplatesAsync() => this.Discord.ApiClient.GetGuildTemplatesAsync(this.Id); /// /// Creates a guild template. /// /// Name of the template. /// Description of the template. /// The template created. /// Throws when a template already exists for the guild or a null parameter is provided for the name. /// Throws when the client does not have the permission. /// Thrown when Discord is unable to process the request. public Task CreateTemplateAsync(string name, string description = null) => this.Discord.ApiClient.CreateGuildTemplateAsync(this.Id, name, description); /// /// Syncs the template to the current guild's state. /// /// The code of the template to sync. /// The template synced. /// Throws when the template for the code cannot be found /// Throws when the client does not have the permission. /// Thrown when Discord is unable to process the request. public Task SyncTemplateAsync(string code) => this.Discord.ApiClient.SyncGuildTemplateAsync(this.Id, code); /// /// Modifies the template's metadata. /// /// The template's code. /// Name of the template. /// Description of the template. /// The template modified. /// Throws when the template for the code cannot be found /// Throws when the client does not have the permission. /// Thrown when Discord is unable to process the request. public Task ModifyTemplateAsync(string code, string name = null, string description = null) => this.Discord.ApiClient.ModifyGuildTemplateAsync(this.Id, code, name, description); /// /// Deletes the template. /// /// The code of the template to delete. /// The deleted template. /// Throws when the template for the code cannot be found /// Throws when the client does not have the permission. /// Thrown when Discord is unable to process the request. public Task DeleteTemplateAsync(string code) => this.Discord.ApiClient.DeleteGuildTemplateAsync(this.Id, code); /// /// Gets this guild's membership screening form. /// /// This guild's membership screening form. /// Thrown when Discord is unable to process the request. public Task GetMembershipScreeningFormAsync() => this.Discord.ApiClient.GetGuildMembershipScreeningFormAsync(this.Id); /// /// Modifies this guild's membership screening form. /// /// Action to perform /// The modified screening form. /// Thrown when the client doesn't have the permission, or community is not enabled on this guild. /// Thrown when Discord is unable to process the request. public async Task ModifyMembershipScreeningFormAsync(Action action) { var mdl = new MembershipScreeningEditModel(); action(mdl); return await this.Discord.ApiClient.ModifyGuildMembershipScreeningFormAsync(this.Id, mdl.Enabled, mdl.Fields, mdl.Description); } /// /// Gets all the application commands in this guild. /// /// A list of application commands in this guild. public Task> GetApplicationCommandsAsync() => this.Discord.ApiClient.GetGuildApplicationCommandsAsync(this.Discord.CurrentApplication.Id, this.Id); /// /// Overwrites the existing application commands in this guild. New commands are automatically created and missing commands are automatically delete /// /// The list of commands to overwrite with. /// The list of guild commands public Task> BulkOverwriteApplicationCommandsAsync(IEnumerable commands) => this.Discord.ApiClient.BulkOverwriteGuildApplicationCommandsAsync(this.Discord.CurrentApplication.Id, this.Id, commands); /// /// Creates or overwrites a application command in this guild. /// /// The command to create. /// The created command. public Task CreateApplicationCommandAsync(DiscordApplicationCommand command) => this.Discord.ApiClient.CreateGuildApplicationCommandAsync(this.Discord.CurrentApplication.Id, this.Id, command); /// /// Edits a application command in this guild. /// /// The id of the command to edit. /// Action to perform. /// The edit command. public async Task EditApplicationCommandAsync(ulong commandId, Action action) { var mdl = new ApplicationCommandEditModel(); action(mdl); return await this.Discord.ApiClient.EditGuildApplicationCommandAsync(this.Discord.CurrentApplication.Id, this.Id, commandId, mdl.Name, mdl.Description, mdl.Options, mdl.NameLocalizations, mdl.DescriptionLocalizations, mdl.DefaultMemberPermissions, mdl.DmPermission, mdl.IsNsfw).ConfigureAwait(false); } /// /// Gets this guild's welcome screen. /// /// This guild's welcome screen object. /// Thrown when Discord is unable to process the request. public Task GetWelcomeScreenAsync() => this.Discord.ApiClient.GetGuildWelcomeScreenAsync(this.Id); /// /// Modifies this guild's welcome screen. /// /// Action to perform. /// The modified welcome screen. /// Thrown when the client doesn't have the permission, or community is not enabled on this guild. /// Thrown when Discord is unable to process the request. public async Task ModifyWelcomeScreenAsync(Action action) { var mdl = new WelcomeScreenEditModel(); action(mdl); return await this.Discord.ApiClient.ModifyGuildWelcomeScreenAsync(this.Id, mdl.Enabled, mdl.WelcomeChannels, mdl.Description).ConfigureAwait(false); } #endregion /// /// Returns a string representation of this guild. /// /// String representation of this guild. public override string ToString() => $"Guild {this.Id}; {this.Name}"; /// /// 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 DiscordGuild); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(DiscordGuild e) => e is not null && (ReferenceEquals(this, e) || this.Id == e.Id); /// /// Gets the hash code for this . /// /// The hash code for this . public override int GetHashCode() => this.Id.GetHashCode(); /// /// Gets whether the two objects are equal. /// /// First guild to compare. /// Second guild to compare. /// Whether the two guilds are equal. public static bool operator ==(DiscordGuild e1, DiscordGuild 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); } /// /// Gets whether the two objects are not equal. /// /// First guild to compare. /// Second guild to compare. /// Whether the two guilds are not equal. public static bool operator !=(DiscordGuild e1, DiscordGuild e2) => !(e1 == e2); } diff --git a/DisCatSharp/Entities/ThreadAndForum/DiscordThreadChannel.cs b/DisCatSharp/Entities/ThreadAndForum/DiscordThreadChannel.cs index 04267d221..ff19e7036 100644 --- a/DisCatSharp/Entities/ThreadAndForum/DiscordThreadChannel.cs +++ b/DisCatSharp/Entities/ThreadAndForum/DiscordThreadChannel.cs @@ -1,342 +1,380 @@ // 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.Linq; using System.Threading.Tasks; using DisCatSharp.Enums; +using DisCatSharp.Exceptions; using DisCatSharp.Net.Models; using DisCatSharp.Net.Serialization; using Newtonsoft.Json; namespace DisCatSharp.Entities; /// /// Represents a discord thread channel. /// public class DiscordThreadChannel : DiscordChannel { /// /// Gets ID of the owner that started this thread. /// [JsonProperty("owner_id", NullValueHandling = NullValueHandling.Ignore)] public ulong OwnerId { get; internal set; } [JsonProperty("total_message_sent", DefaultValueHandling = DefaultValueHandling.Ignore)] public int TotalMessagesSent { get; internal set; } /// /// Gets an approximate count of messages in a thread, stops counting at 50. /// [JsonProperty("message_count", NullValueHandling = NullValueHandling.Ignore)] public int? MessageCount { get; internal set; } /// /// Gets an approximate count of users in a thread, stops counting at 50. /// [JsonProperty("member_count", NullValueHandling = NullValueHandling.Ignore)] public int? MemberCount { get; internal set; } /// /// Represents the current member for this thread. This will have a value if the user has joined the thread. /// [JsonProperty("member", NullValueHandling = NullValueHandling.Ignore)] public DiscordThreadChannelMember CurrentMember { get; internal set; } /// /// Gets the threads metadata. /// [JsonProperty("thread_metadata", NullValueHandling = NullValueHandling.Ignore)] public DiscordThreadChannelMetadata ThreadMetadata { get; internal set; } /// /// Gets the thread members object. /// [JsonIgnore] public IReadOnlyDictionary ThreadMembers => new ReadOnlyConcurrentDictionary(this.ThreadMembersInternal); [JsonProperty("thread_member", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary ThreadMembersInternal; /// /// List of applied tag ids. /// [JsonIgnore] internal IReadOnlyList AppliedTagIds => this.AppliedTagIdsInternal; /// /// List of applied tag ids. /// [JsonProperty("applied_tags", NullValueHandling = NullValueHandling.Ignore)] internal List AppliedTagIdsInternal; /// /// Gets the list of applied tags. /// Only applicable for forum channel posts. /// [JsonIgnore] public IEnumerable AppliedTags => this.AppliedTagIds?.Select(id => this.Parent.GetForumPostTag(id)).Where(x => x != null); /// /// Initializes a new instance of the class. /// internal DiscordThreadChannel() { } #region Methods /// /// Modifies the current thread. /// /// Action to perform on this thread /// Thrown when the client does not have the permission. /// Thrown when the thread 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. This happens, when the guild hasn't reached a certain boost . public Task ModifyAsync(Action action) { var mdl = new ThreadEditModel(); action(mdl); var canContinue = !mdl.AutoArchiveDuration.HasValue || !mdl.AutoArchiveDuration.Value.HasValue || Utilities.CheckThreadAutoArchiveDurationFeature(this.Guild, mdl.AutoArchiveDuration.Value.Value); if (mdl.Invitable.HasValue) { canContinue = this.Guild.Features.CanCreatePrivateThreads; } return canContinue ? this.Discord.ApiClient.ModifyThreadAsync(this.Id, this.Parent.Type, mdl.Name, mdl.Locked, mdl.Archived, mdl.PerUserRateLimit, mdl.AutoArchiveDuration, mdl.Invitable, mdl.AppliedTags, mdl.AuditLogReason) : throw new NotSupportedException($"Cannot modify ThreadAutoArchiveDuration. Guild needs boost tier {(mdl.AutoArchiveDuration.Value.Value == ThreadAutoArchiveDuration.ThreeDays ? "one" : "two")}."); } /// /// Add a tag to the current thread. /// /// The tag to add. /// The reason for the audit logs. /// Thrown when the client does not have the permission. /// 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 AddTagAsync(ForumPostTag tag, string reason = null) => await this.Discord.ApiClient.ModifyThreadAsync(this.Id, this.Parent.Type, null, null, null, null, null, null, new List(this.AppliedTags) { tag }, reason: reason); /// /// Remove a tag from the current thread. /// /// The tag to remove. /// The reason for the audit logs. /// Thrown when the client does not have the permission. /// 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 RemoveTagAsync(ForumPostTag tag, string reason = null) => await this.Discord.ApiClient.ModifyThreadAsync(this.Id, this.Parent.Type, null, null, null, null, null, null, new List(this.AppliedTags).Where(x => x != tag).ToList(), reason: reason); /// /// Archives a thread. /// /// Whether the thread should be locked. /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task ArchiveAsync(bool locked = true, string reason = null) => this.Discord.ApiClient.ModifyThreadAsync(this.Id, this.Parent.Type, null, locked, true, null, null, null, null, reason: reason); /// /// Unarchives a thread. /// /// Reason for audit logs. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task UnarchiveAsync(string reason = null) => this.Discord.ApiClient.ModifyThreadAsync(this.Id, this.Parent.Type, null, null, false, null, null, null, null, reason: reason); /// /// Gets the members of a thread. Needs the intent. /// /// 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> GetMembersAsync() => await this.Discord.ApiClient.GetThreadMembersAsync(this.Id); /// /// Adds a member to this thread. /// /// The member id to be added. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task AddMemberAsync(ulong memberId) => this.Discord.ApiClient.AddThreadMemberAsync(this.Id, memberId); /// /// Adds a member to this thread. /// /// The member to be added. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task AddMemberAsync(DiscordMember member) => this.AddMemberAsync(member.Id); /// /// Gets a member in this thread. /// - /// The member to be added. + /// The id of the member to get. /// Thrown when the member is not part of the thread. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task GetMemberAsync(ulong memberId) => this.Discord.ApiClient.GetThreadMemberAsync(this.Id, memberId); + + /// + /// Tries to get a member in this thread. + /// + /// The id of the member to get. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task TryGetMemberAsync(ulong memberId) + { + try + { + return await this.GetMemberAsync(memberId).ConfigureAwait(false); + } + catch (NotFoundException) + { + return null; + } + } + /// /// Gets a member in this thread. /// - /// The member to be added. + /// The member to get. /// Thrown when the member is not part of the thread. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task GetMemberAsync(DiscordMember member) => this.Discord.ApiClient.GetThreadMemberAsync(this.Id, member.Id); + /// + /// Tries to get a member in this thread. + /// + /// The member to get. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task TryGetMemberAsync(DiscordMember member) + { + try + { + return await this.GetMemberAsync(member).ConfigureAwait(false); + } + catch (NotFoundException) + { + return null; + } + } + /// /// Removes a member from this thread. /// /// The member id to be removed. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task RemoveMemberAsync(ulong memberId) => this.Discord.ApiClient.RemoveThreadMemberAsync(this.Id, memberId); /// /// Removes a member from this thread. Only applicable to private threads. /// /// The member to be removed. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task RemoveMemberAsync(DiscordMember member) => this.RemoveMemberAsync(member.Id); /// /// Adds a role to this thread. Only applicable to private threads. /// /// The role id to be added. /// 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 AddRoleAsync(ulong roleId) { var role = this.Guild.GetRole(roleId); var members = await this.Guild.GetAllMembersAsync(); var roleMembers = members.Where(m => m.Roles.Contains(role)); foreach (var member in roleMembers) { await this.Discord.ApiClient.AddThreadMemberAsync(this.Id, member.Id); } } /// /// Adds a role to this thread. Only applicable to private threads. /// /// The role to be added. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task AddRoleAsync(DiscordRole role) => this.AddRoleAsync(role.Id); /// /// Removes a role from this thread. Only applicable to private threads. /// /// The role id to be removed. /// 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 RemoveRoleAsync(ulong roleId) { var role = this.Guild.GetRole(roleId); var members = await this.Guild.GetAllMembersAsync(); var roleMembers = members.Where(m => m.Roles.Contains(role)); foreach (var member in roleMembers) { await this.Discord.ApiClient.RemoveThreadMemberAsync(this.Id, member.Id); } } /// /// Removes a role from this thread. Only applicable to private threads. /// /// The role to be removed. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task RemoveRoleAsync(DiscordRole role) => this.RemoveRoleAsync(role.Id); /// /// Joins a thread. /// /// Thrown when the client has no access to this 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 Task JoinAsync() => this.Discord.ApiClient.JoinThreadAsync(this.Id); /// /// Leaves a thread. /// /// Thrown when the client has no access to this 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 Task LeaveAsync() => this.Discord.ApiClient.LeaveThreadAsync(this.Id); /// /// Returns a string representation of this thread. /// /// String representation of this thread. public override string ToString() => this.Type switch { ChannelType.NewsThread => $"News thread {this.Name} ({this.Id})", ChannelType.PublicThread => $"Thread {this.Name} ({this.Id})", ChannelType.PrivateThread => $"Private thread {this.Name} ({this.Id})", _ => $"Thread {this.Name} ({this.Id})", }; #endregion } diff --git a/DisCatSharp/Entities/Webhook/DiscordWebhook.cs b/DisCatSharp/Entities/Webhook/DiscordWebhook.cs index 0852aade4..f9e51ea80 100644 --- a/DisCatSharp/Entities/Webhook/DiscordWebhook.cs +++ b/DisCatSharp/Entities/Webhook/DiscordWebhook.cs @@ -1,279 +1,314 @@ // 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.IO; using System.Threading.Tasks; using DisCatSharp.Enums; +using DisCatSharp.Exceptions; using DisCatSharp.Net; using Newtonsoft.Json; namespace DisCatSharp.Entities; /// /// Represents information about a Discord webhook. /// public class DiscordWebhook : SnowflakeObject, IEquatable { /// /// Gets the api client. /// internal DiscordApiClient ApiClient { get; set; } /// /// Gets the id of the guild this webhook belongs to. /// [JsonProperty("guild_id", NullValueHandling = NullValueHandling.Ignore)] public ulong GuildId { get; internal set; } /// /// Gets the ID of the channel this webhook belongs to. /// [JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)] public ulong ChannelId { get; internal set; } /// /// Gets the user this webhook was created by. /// [JsonProperty("user", NullValueHandling = NullValueHandling.Ignore)] public DiscordUser User { get; internal set; } /// /// Gets the default name of this webhook. /// [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public string Name { get; internal set; } /// /// Gets hash of the default avatar for this webhook. /// [JsonProperty("avatar", NullValueHandling = NullValueHandling.Ignore)] internal string AvatarHash { get; set; } /// /// Gets the partial source guild for this webhook (For Channel Follower Webhooks). /// [JsonProperty("source_guild", NullValueHandling = NullValueHandling.Ignore)] public DiscordGuild SourceGuild { get; set; } /// /// Gets the partial source channel for this webhook (For Channel Follower Webhooks). /// [JsonProperty("source_channel", NullValueHandling = NullValueHandling.Ignore)] public DiscordChannel SourceChannel { get; set; } /// /// Gets the url used for executing the webhook. /// [JsonProperty("url", NullValueHandling = NullValueHandling.Ignore)] public string Url { get; set; } /// /// Gets the default avatar url for this webhook. /// public string AvatarUrl => !string.IsNullOrWhiteSpace(this.AvatarHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.AVATARS}/{this.Id}/{this.AvatarHash}.png?size=1024" : null; /// /// Gets the secure token of this webhook. /// [JsonProperty("token", NullValueHandling = NullValueHandling.Ignore)] public string Token { get; internal set; } /// /// Initializes a new instance of the class. /// internal DiscordWebhook() { } /// /// Modifies this webhook. /// /// New default name for this webhook. /// New avatar for this webhook. /// The new channel id to move the webhook to. /// Reason for audit logs. /// The modified webhook. /// Thrown when the client does not have the permission. /// 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 ModifyAsync(string name = null, Optional avatar = default, ulong? channelId = null, string reason = null) { var avatarb64 = ImageTool.Base64FromStream(avatar); var newChannelId = channelId ?? this.ChannelId; return this.Discord.ApiClient.ModifyWebhookAsync(this.Id, newChannelId, name, avatarb64, reason); } /// /// Gets a previously-sent webhook message. /// /// Thrown when the webhook does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public async Task GetMessageAsync(ulong messageId) - => await (this.Discord?.ApiClient ?? this.ApiClient).GetWebhookMessageAsync(this.Id, this.Token, messageId).ConfigureAwait(false); + public Task GetMessageAsync(ulong messageId) + => (this.Discord?.ApiClient ?? this.ApiClient).GetWebhookMessageAsync(this.Id, this.Token, messageId); + + /// + /// Tries to get a previously-sent webhook message. + /// + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task TryGetMessageAsync(ulong messageId) + { + try + { + return await this.GetMessageAsync(messageId).ConfigureAwait(false); + } + catch (NotFoundException) + { + return null; + } + } /// /// Gets a previously-sent webhook message. /// /// Thrown when the webhook does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public async Task GetMessageAsync(ulong messageId, ulong threadId) - => await (this.Discord?.ApiClient ?? this.ApiClient).GetWebhookMessageAsync(this.Id, this.Token, messageId, threadId).ConfigureAwait(false); + public Task GetMessageAsync(ulong messageId, ulong threadId) + => (this.Discord?.ApiClient ?? this.ApiClient).GetWebhookMessageAsync(this.Id, this.Token, messageId, threadId); + /// + /// Tries to get a previously-sent webhook message. + /// + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task TryGetMessageAsync(ulong messageId, ulong threadId) + { + try + { + return await this.GetMessageAsync(messageId, threadId).ConfigureAwait(false); + } + catch (NotFoundException) + { + return null; + } + } + /// /// Permanently deletes this webhook. /// /// Thrown when the client does not have the permission. /// 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 DeleteAsync() => this.Discord.ApiClient.DeleteWebhookAsync(this.Id, this.Token); /// /// Executes this webhook with the given . /// /// Webhook builder filled with data to send. /// Target thread id (Optional). Defaults to null. /// 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 ExecuteAsync(DiscordWebhookBuilder builder, string threadId = null) => (this.Discord?.ApiClient ?? this.ApiClient).ExecuteWebhookAsync(this.Id, this.Token, builder, threadId); /// /// Executes this webhook in Slack compatibility mode. /// /// JSON containing Slack-compatible payload for this webhook. /// Target thread id (Optional). Defaults to null. /// 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 ExecuteSlackAsync(string json, string threadId = null) => (this.Discord?.ApiClient ?? this.ApiClient).ExecuteWebhookSlackAsync(this.Id, this.Token, json, threadId); /// /// Executes this webhook in GitHub compatibility mode. /// /// JSON containing GitHub-compatible payload for this webhook. /// Target thread id (Optional). Defaults to null. /// 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 ExecuteGithubAsync(string json, string threadId = null) => (this.Discord?.ApiClient ?? this.ApiClient).ExecuteWebhookGithubAsync(this.Id, this.Token, json, threadId); /// /// Edits a previously-sent webhook message. /// /// The id of the message to edit. /// The builder of the message to edit. /// Target thread id (Optional). Defaults to null. /// The modified /// Thrown when the webhook does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task EditMessageAsync(ulong messageId, DiscordWebhookBuilder builder, string threadId = null) { builder.Validate(true); if (builder.KeepAttachmentsInternal.HasValue && builder.KeepAttachmentsInternal.Value) { builder.AttachmentsInternal.AddRange(this.ApiClient.GetWebhookMessageAsync(this.Id, this.Token, messageId.ToString(), threadId).Result.Attachments); } else if (builder.KeepAttachmentsInternal.HasValue) { builder.AttachmentsInternal.Clear(); } return await (this.Discord?.ApiClient ?? this.ApiClient).EditWebhookMessageAsync(this.Id, this.Token, messageId.ToString(), builder, threadId).ConfigureAwait(false); } /// /// Deletes a message that was created by the webhook. /// /// The id of the message to delete /// 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 DeleteMessageAsync(ulong messageId) => (this.Discord?.ApiClient ?? this.ApiClient).DeleteWebhookMessageAsync(this.Id, this.Token, messageId); /// /// Deletes a message that was created by the webhook. /// /// The id of the message to delete /// Target thread id (Optional). Defaults to null. /// 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 DeleteMessageAsync(ulong messageId, ulong threadId) => (this.Discord?.ApiClient ?? this.ApiClient).DeleteWebhookMessageAsync(this.Id, this.Token, messageId, threadId); /// /// 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 DiscordWebhook); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(DiscordWebhook e) => e is not null && (ReferenceEquals(this, e) || this.Id == e.Id); /// /// Gets the hash code for this . /// /// The hash code for this . public override int GetHashCode() => this.Id.GetHashCode(); /// /// Gets whether the two objects are equal. /// /// First webhook to compare. /// Second webhook to compare. /// Whether the two webhooks are equal. public static bool operator ==(DiscordWebhook e1, DiscordWebhook 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); } /// /// Gets whether the two objects are not equal. /// /// First webhook to compare. /// Second webhook to compare. /// Whether the two webhooks are not equal. public static bool operator !=(DiscordWebhook e1, DiscordWebhook e2) => !(e1 == e2); }