diff --git a/DisCatSharp/Clients/Rest/BaseMethods/Users/DiscordApiClient.BaseMethods.Users.cs b/DisCatSharp/Clients/Rest/BaseMethods/Users/DiscordApiClient.BaseMethods.Users.cs index a5711100b..a1988b98c 100644 --- a/DisCatSharp/Clients/Rest/BaseMethods/Users/DiscordApiClient.BaseMethods.Users.cs +++ b/DisCatSharp/Clients/Rest/BaseMethods/Users/DiscordApiClient.BaseMethods.Users.cs @@ -1,133 +1,165 @@ // 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.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Threading.Tasks; using DisCatSharp.Entities; using DisCatSharp.Net.Abstractions; using DisCatSharp.Net.Serialization; using Newtonsoft.Json; namespace DisCatSharp.Net; public sealed partial class DiscordApiClient { /// /// Gets the current user async. /// internal Task GetCurrentUserAsync() => this.GetUserAsync("@me"); /// /// Gets the user async. /// /// The user_id. internal Task GetUserAsync(ulong userId) => this.GetUserAsync(userId.ToString(CultureInfo.InvariantCulture)); /// /// Gets the user async. /// /// The user_id. internal async Task GetUserAsync(string userId) { var route = $"{Endpoints.USERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {user_id = userId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var userRaw = JsonConvert.DeserializeObject(res.Response); var duser = new DiscordUser(userRaw) { Discord = this.Discord }; return duser; } /// /// Modifies the current user async. /// /// The username. /// The base64_avatar. internal async Task ModifyCurrentUserAsync(string username, Optional base64Avatar) { var pld = new RestUserUpdateCurrentPayload { Username = username, AvatarBase64 = base64Avatar.ValueOrDefault(), AvatarSet = base64Avatar.HasValue }; var route = $"{Endpoints.USERS}{Endpoints.ME}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var userRaw = JsonConvert.DeserializeObject(res.Response); return userRaw; } /// /// Gets the current user guilds async. /// /// The limit. /// The before. /// The after. internal async Task> GetCurrentUserGuildsAsync(int limit = 100, ulong? before = null, ulong? after = null) { var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.GUILDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { }, out var path); var url = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration) .AddParameter($"limit", limit.ToString(CultureInfo.InvariantCulture)); if (before != null) url.AddParameter("before", before.Value.ToString(CultureInfo.InvariantCulture)); if (after != null) url.AddParameter("after", after.Value.ToString(CultureInfo.InvariantCulture)); var res = await this.DoRequestAsync(this.Discord, bucket, url.Build(), RestRequestMethod.GET, route).ConfigureAwait(false); if (this.Discord is DiscordClient) { var guildsRaw = JsonConvert.DeserializeObject>(res.Response); var glds = guildsRaw.Select(xug => (this.Discord as DiscordClient)?.GuildsInternal[xug.Id]); return new ReadOnlyCollection(new List(glds)); } else { return new ReadOnlyCollection(JsonConvert.DeserializeObject>(res.Response)); } } + + /// + /// Gets the users connections async. + /// + internal async Task> GetCurrentUserConnectionsAsync() + { + var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.CONNECTIONS}"; + var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { }, out var path); + + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); + var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); + + var connectionsRaw = JsonConvert.DeserializeObject>(res.Response).Select(xc => { xc.Discord = this.Discord; return xc; }); + + return new ReadOnlyCollection(new List(connectionsRaw)); + } + + /// + /// Gets the users connections async. + /// + internal async Task> GetUserConnectionsAsync(ulong user_id) + { + var route = $"{Endpoints.USERS}/:user_id{Endpoints.CONNECTIONS}"; + var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { user_id = user_id.ToString() }, out var path); + + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); + var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); + + var connectionsRaw = JsonConvert.DeserializeObject>(res.Response).Select(xc => { xc.Discord = this.Discord; return xc; }); + + return new ReadOnlyCollection(new List(connectionsRaw)); + } } diff --git a/DisCatSharp/Clients/Rest/DiscordApiClient.cs b/DisCatSharp/Clients/Rest/DiscordApiClient.cs index badd33382..b89673ead 100644 --- a/DisCatSharp/Clients/Rest/DiscordApiClient.cs +++ b/DisCatSharp/Clients/Rest/DiscordApiClient.cs @@ -1,2045 +1,2026 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Net; using System.Text; using System.Threading.Tasks; using DisCatSharp.Entities; using DisCatSharp.Enums; using DisCatSharp.Net.Abstractions; using DisCatSharp.Net.Serialization; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace DisCatSharp.Net; /// /// Represents a discord api client. /// public sealed partial class DiscordApiClient { /// /// The audit log reason header name. /// private const string REASON_HEADER_NAME = "X-Audit-Log-Reason"; /// /// Gets the discord client. /// internal BaseDiscordClient Discord { get; } /// /// Gets the rest client. /// internal RestClient Rest { get; } /// /// Initializes a new instance of the class. /// /// The client. internal DiscordApiClient(BaseDiscordClient client) { this.Discord = client; this.Rest = new RestClient(client); } /// /// Initializes a new instance of the class. /// /// The proxy. /// The timeout. /// If true, use relative rate limit. /// The logger. internal DiscordApiClient(IWebProxy proxy, TimeSpan timeout, bool useRelativeRateLimit, ILogger logger) // This is for meta-clients, such as the webhook client { this.Rest = new RestClient(proxy, timeout, useRelativeRateLimit, logger); } /// /// Builds the query string. /// /// The values. /// Whether this query will be transmitted via POST. private static string BuildQueryString(IDictionary values, bool post = false) { if (values == null || values.Count == 0) return string.Empty; var valsCollection = values.Select(xkvp => $"{WebUtility.UrlEncode(xkvp.Key)}={WebUtility.UrlEncode(xkvp.Value)}"); var vals = string.Join("&", valsCollection); return !post ? $"?{vals}" : vals; } /// /// Prepares the message. /// /// The msg_raw. /// A DiscordMessage. private DiscordMessage PrepareMessage(JToken msgRaw) { var author = msgRaw["author"].ToObject(); var ret = msgRaw.ToDiscordObject(); ret.Discord = this.Discord; this.PopulateMessage(author, ret); var referencedMsg = msgRaw["referenced_message"]; if (ret.MessageType == MessageType.Reply && !string.IsNullOrWhiteSpace(referencedMsg?.ToString())) { author = referencedMsg["author"].ToObject(); ret.ReferencedMessage.Discord = this.Discord; this.PopulateMessage(author, ret.ReferencedMessage); } if (ret.Channel != null) return ret; var channel = !ret.GuildId.HasValue ? new DiscordDmChannel { Id = ret.ChannelId, Discord = this.Discord, Type = ChannelType.Private } : new DiscordChannel { Id = ret.ChannelId, GuildId = ret.GuildId, Discord = this.Discord }; ret.Channel = channel; return ret; } /// /// Populates the message. /// /// The author. /// The message. private void PopulateMessage(TransportUser author, DiscordMessage ret) { var guild = ret.Channel?.Guild; //If this is a webhook, it shouldn't be in the user cache. if (author.IsBot && int.Parse(author.Discriminator) == 0) { ret.Author = new DiscordUser(author) { Discord = this.Discord }; } else { if (!this.Discord.UserCache.TryGetValue(author.Id, out var usr)) { this.Discord.UserCache[author.Id] = usr = new DiscordUser(author) { Discord = this.Discord }; } if (guild != null) { if (!guild.Members.TryGetValue(author.Id, out var mbr)) mbr = new DiscordMember(usr) { Discord = this.Discord, GuildId = guild.Id }; ret.Author = mbr; } else { ret.Author = usr; } } ret.PopulateMentions(); if (ret.ReactionsInternal == null) ret.ReactionsInternal = new List(); foreach (var xr in ret.ReactionsInternal) xr.Emoji.Discord = this.Discord; } /// /// Executes a rest request. /// /// The client. /// The bucket. /// The url. /// The method. /// The route. /// The headers. /// The payload. /// The ratelimit wait override. internal Task DoRequestAsync(BaseDiscordClient client, RateLimitBucket bucket, Uri url, RestRequestMethod method, string route, IReadOnlyDictionary headers = null, string payload = null, double? ratelimitWaitOverride = null) { var req = new RestRequest(client, bucket, url, method, route, headers, payload, ratelimitWaitOverride); if (this.Discord != null) this.Rest.ExecuteRequestAsync(req).LogTaskFault(this.Discord.Logger, LogLevel.Error, LoggerEvents.RestError, "Error while executing request"); else _ = this.Rest.ExecuteRequestAsync(req); return req.WaitForCompletionAsync(); } /// /// Executes a multipart rest request for stickers. /// /// The client. /// The bucket. /// The url. /// The method. /// The route. /// The headers. /// The file. /// The sticker name. /// The sticker tag. /// The sticker description. /// The ratelimit wait override. private Task DoStickerMultipartAsync(BaseDiscordClient client, RateLimitBucket bucket, Uri url, RestRequestMethod method, string route, IReadOnlyDictionary headers = null, DiscordMessageFile file = null, string name = "", string tags = "", string description = "", double? ratelimitWaitOverride = null) { var req = new MultipartStickerWebRequest(client, bucket, url, method, route, headers, file, name, tags, description, ratelimitWaitOverride); if (this.Discord != null) this.Rest.ExecuteRequestAsync(req).LogTaskFault(this.Discord.Logger, LogLevel.Error, LoggerEvents.RestError, "Error while executing request"); else _ = this.Rest.ExecuteRequestAsync(req); return req.WaitForCompletionAsync(); } /// /// Executes a multipart request. /// /// The client. /// The bucket. /// The url. /// The method. /// The route. /// The headers. /// The values. /// The files. /// The ratelimit wait override. private Task DoMultipartAsync(BaseDiscordClient client, RateLimitBucket bucket, Uri url, RestRequestMethod method, string route, IReadOnlyDictionary headers = null, IReadOnlyDictionary values = null, IReadOnlyCollection files = null, double? ratelimitWaitOverride = null) { var req = new MultipartWebRequest(client, bucket, url, method, route, headers, values, files, ratelimitWaitOverride); if (this.Discord != null) this.Rest.ExecuteRequestAsync(req).LogTaskFault(this.Discord.Logger, LogLevel.Error, LoggerEvents.RestError, "Error while executing request"); else _ = this.Rest.ExecuteRequestAsync(req); return req.WaitForCompletionAsync(); } - #region Connections - /// - /// Gets the users connections async. - /// - - internal async Task> GetUserConnectionsAsync() - { - var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.CONNECTIONS}"; - var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { }, out var path); - - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); - var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); - - var connectionsRaw = JsonConvert.DeserializeObject>(res.Response).Select(xc => { xc.Discord = this.Discord; return xc; }); - - return new ReadOnlyCollection(new List(connectionsRaw)); - } - #endregion - #region Webhooks /// /// Creates the webhook async. /// /// The channel_id. /// The name. /// The base64_avatar. /// The reason. internal async Task CreateWebhookAsync(ulong channelId, string name, Optional base64Avatar, string reason) { var pld = new RestWebhookPayload { Name = name, AvatarBase64 = base64Avatar.ValueOrDefault(), AvatarSet = base64Avatar.HasValue }; var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.WEBHOOKS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; ret.ApiClient = this; return ret; } /// /// Gets the channel webhooks async. /// /// The channel_id. internal async Task> GetChannelWebhooksAsync(ulong channelId) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.WEBHOOKS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var webhooksRaw = JsonConvert.DeserializeObject>(res.Response).Select(xw => { xw.Discord = this.Discord; xw.ApiClient = this; return xw; }); return new ReadOnlyCollection(new List(webhooksRaw)); } /// /// Gets the guild webhooks async. /// /// The guild_id. internal async Task> GetGuildWebhooksAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WEBHOOKS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var webhooksRaw = JsonConvert.DeserializeObject>(res.Response).Select(xw => { xw.Discord = this.Discord; xw.ApiClient = this; return xw; }); return new ReadOnlyCollection(new List(webhooksRaw)); } /// /// Gets the webhook async. /// /// The webhook_id. internal async Task GetWebhookAsync(ulong webhookId) { var route = $"{Endpoints.WEBHOOKS}/:webhook_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {webhook_id = webhookId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; ret.ApiClient = this; return ret; } /// /// Gets the webhook with token async. /// /// The webhook_id. /// The webhook_token. internal async Task GetWebhookWithTokenAsync(ulong webhookId, string webhookToken) { var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {webhook_id = webhookId, webhook_token = webhookToken }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Token = webhookToken; ret.Id = webhookId; ret.Discord = this.Discord; ret.ApiClient = this; return ret; } /// /// Modifies the webhook async. /// /// The webhook_id. /// The channel id. /// The name. /// The base64_avatar. /// The reason. internal async Task ModifyWebhookAsync(ulong webhookId, ulong channelId, string name, Optional base64Avatar, string reason) { var pld = new RestWebhookPayload { Name = name, AvatarBase64 = base64Avatar.ValueOrDefault(), AvatarSet = base64Avatar.HasValue, ChannelId = channelId }; var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.WEBHOOKS}/:webhook_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {webhook_id = webhookId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; ret.ApiClient = this; return ret; } /// /// Modifies the webhook async. /// /// The webhook_id. /// The name. /// The base64_avatar. /// The webhook_token. /// The reason. internal async Task ModifyWebhookAsync(ulong webhookId, string name, string base64Avatar, string webhookToken, string reason) { var pld = new RestWebhookPayload { Name = name, AvatarBase64 = base64Avatar }; var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {webhook_id = webhookId, webhook_token = webhookToken }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; ret.ApiClient = this; return ret; } /// /// Deletes the webhook async. /// /// The webhook_id. /// The reason. internal Task DeleteWebhookAsync(ulong webhookId, string reason) { var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.WEBHOOKS}/:webhook_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {webhook_id = webhookId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Deletes the webhook async. /// /// The webhook_id. /// The webhook_token. /// The reason. internal Task DeleteWebhookAsync(ulong webhookId, string webhookToken, string reason) { var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {webhook_id = webhookId, webhook_token = webhookToken }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Executes the webhook async. /// /// The webhook_id. /// The webhook_token. /// The builder. /// The thread_id. internal async Task ExecuteWebhookAsync(ulong webhookId, string webhookToken, DiscordWebhookBuilder builder, string threadId) { builder.Validate(); if (builder.Embeds != null) foreach (var embed in builder.Embeds) if (embed.Timestamp != null) embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); var values = new Dictionary(); var pld = new RestWebhookExecutePayload { Content = builder.Content, Username = builder.Username.ValueOrDefault(), AvatarUrl = builder.AvatarUrl.ValueOrDefault(), IsTts = builder.IsTts, Embeds = builder.Embeds, Components = builder.Components }; if (builder.Mentions != null) pld.Mentions = new DiscordMentions(builder.Mentions, builder.Mentions.Any()); if (builder.Files?.Count > 0) { ulong fileId = 0; List attachments = new(); foreach (var file in builder.Files) { DiscordAttachment att = new() { Id = fileId, Discord = this.Discord, Description = file.Description, FileName = file.FileName, FileSize = null }; attachments.Add(att); fileId++; } pld.Attachments = attachments; } if (!string.IsNullOrEmpty(builder.Content) || builder.Embeds?.Count > 0 || builder.Files?.Count > 0 || builder.IsTts == true || builder.Mentions != null) values["payload_json"] = DiscordJson.SerializeObject(pld); var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {webhook_id = webhookId, webhook_token = webhookToken }, out var path); var qub = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration).AddParameter("wait", "true"); if (threadId != null) qub.AddParameter("thread_id", threadId); var url = qub.Build(); var res = await this.DoMultipartAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, values: values, files: builder.Files).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); foreach (var att in ret.Attachments) att.Discord = this.Discord; foreach (var file in builder.Files.Where(x => x.ResetPositionTo.HasValue)) { file.Stream.Position = file.ResetPositionTo.Value; } ret.Discord = this.Discord; return ret; } /// /// Executes the webhook slack async. /// /// The webhook_id. /// The webhook_token. /// The json_payload. /// The thread_id. internal async Task ExecuteWebhookSlackAsync(ulong webhookId, string webhookToken, string jsonPayload, string threadId) { var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token{Endpoints.SLACK}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {webhook_id = webhookId, webhook_token = webhookToken }, out var path); var qub = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration).AddParameter("wait", "true"); if (threadId != null) qub.AddParameter("thread_id", threadId); var url = qub.Build(); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: jsonPayload).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Executes the webhook github async. /// /// The webhook_id. /// The webhook_token. /// The json_payload. /// The thread_id. internal async Task ExecuteWebhookGithubAsync(ulong webhookId, string webhookToken, string jsonPayload, string threadId) { var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token{Endpoints.GITHUB}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {webhook_id = webhookId, webhook_token = webhookToken }, out var path); var qub = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration).AddParameter("wait", "true"); if (threadId != null) qub.AddParameter("thread_id", threadId); var url = qub.Build(); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: jsonPayload).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Edits the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// The builder. /// The thread_id. internal async Task EditWebhookMessageAsync(ulong webhookId, string webhookToken, string messageId, DiscordWebhookBuilder builder, string threadId) { builder.Validate(true); var pld = new RestWebhookMessageEditPayload { Content = builder.Content, Embeds = builder.Embeds, Mentions = builder.Mentions, Components = builder.Components, }; if (builder.Files?.Count > 0) { ulong fileId = 0; List attachments = new(); foreach (var file in builder.Files) { DiscordAttachment att = new() { Id = fileId, Discord = this.Discord, Description = file.Description, FileName = file.FileName, FileSize = null }; attachments.Add(att); fileId++; } if (builder.Attachments != null && builder.Attachments?.Count() > 0) attachments.AddRange(builder.Attachments); pld.Attachments = attachments; var values = new Dictionary { ["payload_json"] = DiscordJson.SerializeObject(pld) }; var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {webhook_id = webhookId, webhook_token = webhookToken, message_id = messageId }, out var path); var qub = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration); if (threadId != null) qub.AddParameter("thread_id", threadId); var url = qub.Build(); var res = await this.DoMultipartAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, values: values, files: builder.Files); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; foreach (var att in ret.AttachmentsInternal) att.Discord = this.Discord; foreach (var file in builder.Files.Where(x => x.ResetPositionTo.HasValue)) { file.Stream.Position = file.ResetPositionTo.Value; } return ret; } else { pld.Attachments = builder.Attachments; var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {webhook_id = webhookId, webhook_token = webhookToken, message_id = messageId }, out var path); var qub = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration); if (threadId != null) qub.AddParameter("thread_id", threadId); var url = qub.Build(); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; foreach (var att in ret.AttachmentsInternal) att.Discord = this.Discord; return ret; } } /// /// Edits the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// The builder. /// The thread_id. internal Task EditWebhookMessageAsync(ulong webhookId, string webhookToken, ulong messageId, DiscordWebhookBuilder builder, ulong threadId) => this.EditWebhookMessageAsync(webhookId, webhookToken, messageId.ToString(), builder, threadId.ToString()); /// /// Gets the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// The thread_id. internal async Task GetWebhookMessageAsync(ulong webhookId, string webhookToken, string messageId, string threadId) { var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {webhook_id = webhookId, webhook_token = webhookToken, message_id = messageId }, out var path); var qub = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration); if (threadId != null) qub.AddParameter("thread_id", threadId); var url = qub.Build(); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Gets the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. internal Task GetWebhookMessageAsync(ulong webhookId, string webhookToken, ulong messageId) => this.GetWebhookMessageAsync(webhookId, webhookToken, messageId.ToString(), null); /// /// Gets the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// The thread_id. internal Task GetWebhookMessageAsync(ulong webhookId, string webhookToken, ulong messageId, ulong threadId) => this.GetWebhookMessageAsync(webhookId, webhookToken, messageId.ToString(), threadId.ToString()); /// /// Deletes the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// The thread_id. internal async Task DeleteWebhookMessageAsync(ulong webhookId, string webhookToken, string messageId, string threadId) { var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {webhook_id = webhookId, webhook_token = webhookToken, message_id = messageId }, out var path); var qub = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration); if (threadId != null) qub.AddParameter("thread_id", threadId); var url = qub.Build(); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } /// /// Deletes the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. internal Task DeleteWebhookMessageAsync(ulong webhookId, string webhookToken, ulong messageId) => this.DeleteWebhookMessageAsync(webhookId, webhookToken, messageId.ToString(), null); /// /// Deletes the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// The thread_id. internal Task DeleteWebhookMessageAsync(ulong webhookId, string webhookToken, ulong messageId, ulong threadId) => this.DeleteWebhookMessageAsync(webhookId, webhookToken, messageId.ToString(), threadId.ToString()); #endregion #region Reactions /// /// Creates the reaction async. /// /// The channel_id. /// The message_id. /// The emoji. internal Task CreateReactionAsync(ulong channelId, ulong messageId, string emoji) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.REACTIONS}/:emoji{Endpoints.ME}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new {channel_id = channelId, message_id = messageId, emoji }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, ratelimitWaitOverride: this.Discord.Configuration.UseRelativeRatelimit ? null : (double?)0.26); } /// /// Deletes the own reaction async. /// /// The channel_id. /// The message_id. /// The emoji. internal Task DeleteOwnReactionAsync(ulong channelId, ulong messageId, string emoji) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.REACTIONS}/:emoji{Endpoints.ME}"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {channel_id = channelId, message_id = messageId, emoji }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, ratelimitWaitOverride: this.Discord.Configuration.UseRelativeRatelimit ? null : (double?)0.26); } /// /// Deletes the user reaction async. /// /// The channel_id. /// The message_id. /// The user_id. /// The emoji. /// The reason. internal Task DeleteUserReactionAsync(ulong channelId, ulong messageId, ulong userId, string emoji, string reason) { var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.REACTIONS}/:emoji/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {channel_id = channelId, message_id = messageId, emoji, user_id = userId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers, ratelimitWaitOverride: this.Discord.Configuration.UseRelativeRatelimit ? null : (double?)0.26); } /// /// Gets the reactions async. /// /// The channel_id. /// The message_id. /// The emoji. /// The after_id. /// The limit. internal async Task> GetReactionsAsync(ulong channelId, ulong messageId, string emoji, ulong? afterId = null, int limit = 25) { var urlParams = new Dictionary(); if (afterId.HasValue) urlParams["after"] = afterId.Value.ToString(CultureInfo.InvariantCulture); urlParams["limit"] = limit.ToString(CultureInfo.InvariantCulture); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.REACTIONS}/:emoji"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {channel_id = channelId, message_id = messageId, emoji }, out var path); var url = Utilities.GetApiUriFor(path, BuildQueryString(urlParams), this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var reactersRaw = JsonConvert.DeserializeObject>(res.Response); var reacters = new List(); foreach (var xr in reactersRaw) { var usr = new DiscordUser(xr) { Discord = this.Discord }; usr = this.Discord.UserCache.AddOrUpdate(xr.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); reacters.Add(usr); } return new ReadOnlyCollection(new List(reacters)); } /// /// Deletes the all reactions async. /// /// The channel_id. /// The message_id. /// The reason. internal Task DeleteAllReactionsAsync(ulong channelId, ulong messageId, string reason) { var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.REACTIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {channel_id = channelId, message_id = messageId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers, ratelimitWaitOverride: this.Discord.Configuration.UseRelativeRatelimit ? null : (double?)0.26); } /// /// Deletes the reactions emoji async. /// /// The channel_id. /// The message_id. /// The emoji. internal Task DeleteReactionsEmojiAsync(ulong channelId, ulong messageId, string emoji) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.REACTIONS}/:emoji"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {channel_id = channelId, message_id = messageId, emoji }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, ratelimitWaitOverride: this.Discord.Configuration.UseRelativeRatelimit ? null : (double?)0.26); } #endregion #region Emoji /// /// Gets the guild emojis async. /// /// The guild_id. internal async Task> GetGuildEmojisAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.EMOJIS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var emojisRaw = JsonConvert.DeserializeObject>(res.Response); this.Discord.Guilds.TryGetValue(guildId, out var gld); var users = new Dictionary(); var emojis = new List(); foreach (var rawEmoji in emojisRaw) { var xge = rawEmoji.ToObject(); xge.Guild = gld; var xtu = rawEmoji["user"]?.ToObject(); if (xtu != null) { if (!users.ContainsKey(xtu.Id)) { var user = gld != null && gld.Members.TryGetValue(xtu.Id, out var member) ? member : new DiscordUser(xtu); users[user.Id] = user; } xge.User = users[xtu.Id]; } emojis.Add(xge); } return new ReadOnlyCollection(emojis); } /// /// Gets the guild emoji async. /// /// The guild_id. /// The emoji_id. internal async Task GetGuildEmojiAsync(ulong guildId, ulong emojiId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.EMOJIS}/:emoji_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId, emoji_id = emojiId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); this.Discord.Guilds.TryGetValue(guildId, out var gld); var emojiRaw = JObject.Parse(res.Response); var emoji = emojiRaw.ToObject(); emoji.Guild = gld; var xtu = emojiRaw["user"]?.ToObject(); if (xtu != null) emoji.User = gld != null && gld.Members.TryGetValue(xtu.Id, out var member) ? member : new DiscordUser(xtu); return emoji; } /// /// Creates the guild emoji async. /// /// The guild_id. /// The name. /// The imageb64. /// The roles. /// The reason. internal async Task CreateGuildEmojiAsync(ulong guildId, string name, string imageb64, IEnumerable roles, string reason) { var pld = new RestGuildEmojiCreatePayload { Name = name, ImageB64 = imageb64, Roles = roles?.ToArray() }; var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.EMOJIS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); this.Discord.Guilds.TryGetValue(guildId, out var gld); var emojiRaw = JObject.Parse(res.Response); var emoji = emojiRaw.ToObject(); emoji.Guild = gld; var xtu = emojiRaw["user"]?.ToObject(); emoji.User = xtu != null ? gld != null && gld.Members.TryGetValue(xtu.Id, out var member) ? member : new DiscordUser(xtu) : this.Discord.CurrentUser; return emoji; } /// /// Modifies the guild emoji async. /// /// The guild_id. /// The emoji_id. /// The name. /// The roles. /// The reason. internal async Task ModifyGuildEmojiAsync(ulong guildId, ulong emojiId, string name, IEnumerable roles, string reason) { var pld = new RestGuildEmojiModifyPayload { Name = name, Roles = roles?.ToArray() }; var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.EMOJIS}/:emoji_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {guild_id = guildId, emoji_id = emojiId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); this.Discord.Guilds.TryGetValue(guildId, out var gld); var emojiRaw = JObject.Parse(res.Response); var emoji = emojiRaw.ToObject(); emoji.Guild = gld; var xtu = emojiRaw["user"]?.ToObject(); if (xtu != null) emoji.User = gld != null && gld.Members.TryGetValue(xtu.Id, out var member) ? member : new DiscordUser(xtu); return emoji; } /// /// Deletes the guild emoji async. /// /// The guild_id. /// The emoji_id. /// The reason. internal Task DeleteGuildEmojiAsync(ulong guildId, ulong emojiId, string reason) { var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.EMOJIS}/:emoji_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {guild_id = guildId, emoji_id = emojiId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } #endregion #region Stickers /// /// Gets a sticker. /// /// The sticker id. internal async Task GetStickerAsync(ulong stickerId) { var route = $"{Endpoints.STICKERS}/:sticker_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {sticker_id = stickerId}, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = JObject.Parse(res.Response).ToDiscordObject(); ret.Discord = this.Discord; return ret; } /// /// Gets the sticker packs. /// internal async Task> GetStickerPacksAsync() { var route = $"{Endpoints.STICKERPACKS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var json = JObject.Parse(res.Response)["sticker_packs"] as JArray; var ret = json.ToDiscordObject(); return ret.ToList(); } /// /// Gets the guild stickers. /// /// The guild id. internal async Task> GetGuildStickersAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.STICKERS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId}, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var json = JArray.Parse(res.Response); var ret = json.ToDiscordObject(); for (var i = 0; i < ret.Length; i++) { var stkr = ret[i]; stkr.Discord = this.Discord; if (json[i]["user"] is JObject obj) // Null = Missing stickers perm // { var tsr = obj.ToDiscordObject(); var usr = new DiscordUser(tsr) {Discord = this.Discord}; usr = this.Discord.UserCache.AddOrUpdate(tsr.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); stkr.User = usr; } } return ret.ToList(); } /// /// Gets a guild sticker. /// /// The guild id. /// The sticker id. internal async Task GetGuildStickerAsync(ulong guildId, ulong stickerId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.STICKERS}/:sticker_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId, sticker_id = stickerId}, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var json = JObject.Parse(res.Response); var ret = json.ToDiscordObject(); if (json["user"] is not null) // Null = Missing stickers perm // { var tsr = json["user"].ToDiscordObject(); var usr = new DiscordUser(tsr) {Discord = this.Discord}; usr = this.Discord.UserCache.AddOrUpdate(tsr.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); ret.User = usr; } ret.Discord = this.Discord; return ret; } /// /// Creates the guild sticker. /// /// The guild id. /// The name. /// The description. /// The tags. /// The file. /// The reason. internal async Task CreateGuildStickerAsync(ulong guildId, string name, string description, string tags, DiscordMessageFile file, string reason) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.STICKERS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {guild_id = guildId}, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var res = await this.DoStickerMultipartAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, file, name, tags, description); var ret = JObject.Parse(res.Response).ToDiscordObject(); ret.Discord = this.Discord; return ret; } /// /// Modifies the guild sticker. /// /// The guild id. /// The sticker id. /// The name. /// The description. /// The tags. /// The reason. internal async Task ModifyGuildStickerAsync(ulong guildId, ulong stickerId, Optional name, Optional description, Optional tags, string reason) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.STICKERS}/:sticker_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {guild_id = guildId, sticker_id = stickerId}, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var pld = new RestStickerModifyPayload() { Name = name, Description = description, Tags = tags }; var values = new Dictionary { ["payload_json"] = DiscordJson.SerializeObject(pld) }; var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route); var ret = JObject.Parse(res.Response).ToDiscordObject(); ret.Discord = this.Discord; return null; } /// /// Deletes the guild sticker async. /// /// The guild id. /// The sticker id. /// The reason. internal async Task DeleteGuildStickerAsync(ulong guildId, ulong stickerId, string reason) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.STICKERS}/:sticker_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {guild_id = guildId, sticker_id = stickerId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } #endregion #region Application Commands /// /// Gets the global application commands. /// /// The application id. /// Whether to get the full localization dict. internal async Task> GetGlobalApplicationCommandsAsync(ulong applicationId, bool withLocalizations = false) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {application_id = applicationId }, out var path); var querydict = new Dictionary { ["with_localizations"] = withLocalizations.ToString().ToLower() }; var url = Utilities.GetApiUriFor(path, BuildQueryString(querydict), this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var ret = JsonConvert.DeserializeObject>(res.Response); foreach (var app in ret) app.Discord = this.Discord; return ret.ToList(); } /// /// Bulk overwrites the global application commands. /// /// The application id. /// The commands. internal async Task> BulkOverwriteGlobalApplicationCommandsAsync(ulong applicationId, IEnumerable commands) { var pld = new List(); foreach (var command in commands) { pld.Add(new RestApplicationCommandCreatePayload { Type = command.Type, Name = command.Name, Description = command.Description, Options = command.Options, NameLocalizations = command.NameLocalizations?.GetKeyValuePairs(), DescriptionLocalizations = command.DescriptionLocalizations?.GetKeyValuePairs(), DefaultMemberPermission = command.DefaultMemberPermissions, DmPermission = command.DmPermission/*, Nsfw = command.IsNsfw*/ }); } this.Discord.Logger.LogDebug(DiscordJson.SerializeObject(pld)); var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new {application_id = applicationId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, payload: DiscordJson.SerializeObject(pld)); var ret = JsonConvert.DeserializeObject>(res.Response); foreach (var app in ret) app.Discord = this.Discord; return ret.ToList(); } /// /// Creates a global application command. /// /// The applicationid. /// The command. internal async Task CreateGlobalApplicationCommandAsync(ulong applicationId, DiscordApplicationCommand command) { var pld = new RestApplicationCommandCreatePayload { Type = command.Type, Name = command.Name, Description = command.Description, Options = command.Options, NameLocalizations = command.NameLocalizations.GetKeyValuePairs(), DescriptionLocalizations = command.DescriptionLocalizations.GetKeyValuePairs(), DefaultMemberPermission = command.DefaultMemberPermissions, DmPermission = command.DmPermission/*, Nsfw = command.IsNsfw*/ }; var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {application_id = applicationId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Gets a global application command. /// /// The application id. /// The command id. internal async Task GetGlobalApplicationCommandAsync(ulong applicationId, ulong commandId) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}/:command_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {application_id = applicationId, command_id = commandId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Edits a global application command. /// /// The application id. /// The command id. /// The name. /// The description. /// The options. /// The localizations of the name. /// The localizations of the description. /// The default member permissions. /// The dm permission. /// Whether this command is marked as NSFW. internal async Task EditGlobalApplicationCommandAsync(ulong applicationId, ulong commandId, Optional name, Optional description, Optional> options, Optional nameLocalization, Optional descriptionLocalization, Optional defaultMemberPermission, Optional dmPermission, Optional isNsfw) { var pld = new RestApplicationCommandEditPayload { Name = name, Description = description, Options = options, DefaultMemberPermission = defaultMemberPermission, DmPermission = dmPermission, NameLocalizations = nameLocalization.Map(l => l.GetKeyValuePairs()).ValueOrDefault(), DescriptionLocalizations = descriptionLocalization.Map(l => l.GetKeyValuePairs()).ValueOrDefault()/*, Nsfw = isNsfw*/ }; var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}/:command_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {application_id = applicationId, command_id = commandId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Deletes a global application command. /// /// The application_id. /// The command_id. internal async Task DeleteGlobalApplicationCommandAsync(ulong applicationId, ulong commandId) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}/:command_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {application_id = applicationId, command_id = commandId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } /// /// Gets the guild application commands. /// /// The application id. /// The guild id. /// Whether to get the full localization dict. internal async Task> GetGuildApplicationCommandsAsync(ulong applicationId, ulong guildId, bool withLocalizations = false) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {application_id = applicationId, guild_id = guildId }, out var path); var querydict = new Dictionary { ["with_localizations"] = withLocalizations.ToString().ToLower() }; var url = Utilities.GetApiUriFor(path, BuildQueryString(querydict), this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var ret = JsonConvert.DeserializeObject>(res.Response); foreach (var app in ret) app.Discord = this.Discord; return ret.ToList(); } /// /// Bulk overwrites the guild application commands. /// /// The application id. /// The guild id. /// The commands. internal async Task> BulkOverwriteGuildApplicationCommandsAsync(ulong applicationId, ulong guildId, IEnumerable commands) { var pld = new List(); foreach (var command in commands) { pld.Add(new RestApplicationCommandCreatePayload { Type = command.Type, Name = command.Name, Description = command.Description, Options = command.Options, NameLocalizations = command.NameLocalizations?.GetKeyValuePairs(), DescriptionLocalizations = command.DescriptionLocalizations?.GetKeyValuePairs(), DefaultMemberPermission = command.DefaultMemberPermissions, DmPermission = command.DmPermission/*, Nsfw = command.IsNsfw*/ }); } this.Discord.Logger.LogDebug(DiscordJson.SerializeObject(pld)); var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new {application_id = applicationId, guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, payload: DiscordJson.SerializeObject(pld)); var ret = JsonConvert.DeserializeObject>(res.Response); foreach (var app in ret) app.Discord = this.Discord; return ret.ToList(); } /// /// Creates a guild application command. /// /// The application id. /// The guild id. /// The command. internal async Task CreateGuildApplicationCommandAsync(ulong applicationId, ulong guildId, DiscordApplicationCommand command) { var pld = new RestApplicationCommandCreatePayload { Type = command.Type, Name = command.Name, Description = command.Description, Options = command.Options, NameLocalizations = command.NameLocalizations.GetKeyValuePairs(), DescriptionLocalizations = command.DescriptionLocalizations.GetKeyValuePairs(), DefaultMemberPermission = command.DefaultMemberPermissions, DmPermission = command.DmPermission/*, Nsfw = command.IsNsfw*/ }; var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {application_id = applicationId, guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Gets a guild application command. /// /// The application id. /// The guild id. /// The command id. internal async Task GetGuildApplicationCommandAsync(ulong applicationId, ulong guildId, ulong commandId) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}/:command_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {application_id = applicationId, guild_id = guildId, command_id = commandId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Edits a guild application command. /// /// The application id. /// The guild id. /// The command id. /// The name. /// The description. /// The options. /// The localizations of the name. /// The localizations of the description. /// The default member permissions. /// The dm permission. /// Whether this command is marked as NSFW. internal async Task EditGuildApplicationCommandAsync(ulong applicationId, ulong guildId, ulong commandId, Optional name, Optional description, Optional> options, Optional nameLocalization, Optional descriptionLocalization, Optional defaultMemberPermission, Optional dmPermission, Optional isNsfw) { var pld = new RestApplicationCommandEditPayload { Name = name, Description = description, Options = options, DefaultMemberPermission = defaultMemberPermission, DmPermission = dmPermission, NameLocalizations = nameLocalization.Map(l => l.GetKeyValuePairs()).ValueOrDefault(), DescriptionLocalizations = descriptionLocalization.Map(l => l.GetKeyValuePairs()).ValueOrDefault()/*, Nsfw = isNsfw*/ }; var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}/:command_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {application_id = applicationId, guild_id = guildId, command_id = commandId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Deletes a guild application command. /// /// The application id. /// The guild id. /// The command id. internal async Task DeleteGuildApplicationCommandAsync(ulong applicationId, ulong guildId, ulong commandId) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}/:command_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {application_id = applicationId, guild_id = guildId, command_id = commandId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } #region Permissions 2.1 /// /// Gets the guild application command permissions. /// /// The target application id. /// The target guild id. internal async Task> GetGuildApplicationCommandPermissionsAsync(ulong applicationId, ulong guildId) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}{Endpoints.PERMISSIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {application_id = applicationId, guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var ret = JsonConvert.DeserializeObject>(res.Response); foreach (var app in ret) app.Discord = this.Discord; return ret.ToList(); } /// /// Gets a guild application command permission. /// /// The target application id. /// The target guild id. /// The target command id. internal async Task GetGuildApplicationCommandPermissionAsync(ulong applicationId, ulong guildId, ulong commandId) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}/:command_id{Endpoints.PERMISSIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {application_id = applicationId, guild_id = guildId, command_id = commandId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } #endregion /// /// Creates the interaction response. /// /// The interaction id. /// The interaction token. /// The type. /// The builder. internal async Task CreateInteractionResponseAsync(ulong interactionId, string interactionToken, InteractionResponseType type, DiscordInteractionResponseBuilder builder) { if (builder?.Embeds != null) foreach (var embed in builder.Embeds) if (embed.Timestamp != null) embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); RestInteractionResponsePayload pld; if (type != InteractionResponseType.AutoCompleteResult) { var data = builder != null ? new DiscordInteractionApplicationCommandCallbackData { Content = builder.Content ?? null, Embeds = builder.Embeds ?? null, IsTts = builder.IsTts, Mentions = builder.Mentions ?? null, Flags = builder.IsEphemeral ? MessageFlags.Ephemeral : null, Components = builder.Components ?? null, Choices = null } : null; pld = new RestInteractionResponsePayload { Type = type, Data = data }; if (builder != null && builder.Files != null && builder.Files.Count > 0) { ulong fileId = 0; List attachments = new(); foreach (var file in builder.Files) { DiscordAttachment att = new() { Id = fileId, Discord = this.Discord, Description = file.Description, FileName = file.FileName, FileSize = null }; attachments.Add(att); fileId++; } pld.Attachments = attachments; pld.Data.Attachments = attachments; } } else { pld = new RestInteractionResponsePayload { Type = type, Data = new DiscordInteractionApplicationCommandCallbackData { Content = null, Embeds = null, IsTts = null, Mentions = null, Flags = null, Components = null, Choices = builder.Choices, Attachments = null }, Attachments = null }; } var values = new Dictionary(); if (builder != null) if (!string.IsNullOrEmpty(builder.Content) || builder.Embeds?.Count > 0 || builder.IsTts == true || builder.Mentions != null || builder.Files?.Count > 0) values["payload_json"] = DiscordJson.SerializeObject(pld); var route = $"{Endpoints.INTERACTIONS}/:interaction_id/:interaction_token{Endpoints.CALLBACK}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {interaction_id = interactionId, interaction_token = interactionToken }, out var path); var url = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration).AddParameter("wait", "false").Build(); if (builder != null) { await this.DoMultipartAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, values: values, files: builder.Files); foreach (var file in builder.Files.Where(x => x.ResetPositionTo.HasValue)) { file.Stream.Position = file.ResetPositionTo.Value; } } else { await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)); } } /// /// Creates the interaction response. /// /// The interaction id. /// The interaction token. /// The type. /// The builder. internal async Task CreateInteractionModalResponseAsync(ulong interactionId, string interactionToken, InteractionResponseType type, DiscordInteractionModalBuilder builder) { if (builder.ModalComponents.Any(mc => mc.Components.Any(c => c.Type != ComponentType.InputText && c.Type != ComponentType.Select))) throw new NotSupportedException("Can't send any other type then Input Text or Select as Modal Component."); var pld = new RestInteractionModalResponsePayload { Type = type, Data = new DiscordInteractionApplicationCommandModalCallbackData { Title = builder.Title, CustomId = builder.CustomId, ModalComponents = builder.ModalComponents } }; var values = new Dictionary(); var route = $"{Endpoints.INTERACTIONS}/:interaction_id/:interaction_token{Endpoints.CALLBACK}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {interaction_id = interactionId, interaction_token = interactionToken }, out var path); var url = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration).AddParameter("wait", "true").Build(); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)); } /// /// Gets the original interaction response. /// /// The application id. /// The interaction token. internal Task GetOriginalInteractionResponseAsync(ulong applicationId, string interactionToken) => this.GetWebhookMessageAsync(applicationId, interactionToken, Endpoints.ORIGINAL, null); /// /// Edits the original interaction response. /// /// The application id. /// The interaction token. /// The builder. internal Task EditOriginalInteractionResponseAsync(ulong applicationId, string interactionToken, DiscordWebhookBuilder builder) => this.EditWebhookMessageAsync(applicationId, interactionToken, Endpoints.ORIGINAL, builder, null); /// /// Deletes the original interaction response. /// /// The application id. /// The interaction token. internal Task DeleteOriginalInteractionResponseAsync(ulong applicationId, string interactionToken) => this.DeleteWebhookMessageAsync(applicationId, interactionToken, Endpoints.ORIGINAL, null); /// /// Creates the followup message. /// /// The application id. /// The interaction token. /// The builder. internal async Task CreateFollowupMessageAsync(ulong applicationId, string interactionToken, DiscordFollowupMessageBuilder builder) { builder.Validate(); if (builder.Embeds != null) foreach (var embed in builder.Embeds) if (embed.Timestamp != null) embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); var values = new Dictionary(); var pld = new RestFollowupMessageCreatePayload { Content = builder.Content, IsTts = builder.IsTts, Embeds = builder.Embeds, Flags = builder.Flags, Components = builder.Components }; if (builder.Files != null && builder.Files.Count > 0) { ulong fileId = 0; List attachments = new(); foreach (var file in builder.Files) { DiscordAttachment att = new() { Id = fileId, Discord = this.Discord, Description = file.Description, FileName = file.FileName, FileSize = null }; attachments.Add(att); fileId++; } pld.Attachments = attachments; } if (builder.Mentions != null) pld.Mentions = new DiscordMentions(builder.Mentions, builder.Mentions.Any()); if (!string.IsNullOrEmpty(builder.Content) || builder.Embeds?.Count > 0 || builder.IsTts == true || builder.Mentions != null || builder.Files?.Count > 0) values["payload_json"] = DiscordJson.SerializeObject(pld); var route = $"{Endpoints.WEBHOOKS}/:application_id/:interaction_token"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {application_id = applicationId, interaction_token = interactionToken }, out var path); var url = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration).AddParameter("wait", "true").Build(); var res = await this.DoMultipartAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, values: values, files: builder.Files).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); foreach (var att in ret.AttachmentsInternal) { att.Discord = this.Discord; } foreach (var file in builder.Files.Where(x => x.ResetPositionTo.HasValue)) { file.Stream.Position = file.ResetPositionTo.Value; } ret.Discord = this.Discord; return ret; } /// /// Gets the followup message. /// /// The application id. /// The interaction token. /// The message id. internal Task GetFollowupMessageAsync(ulong applicationId, string interactionToken, ulong messageId) => this.GetWebhookMessageAsync(applicationId, interactionToken, messageId); /// /// Edits the followup message. /// /// The application id. /// The interaction token. /// The message id. /// The builder. internal Task EditFollowupMessageAsync(ulong applicationId, string interactionToken, ulong messageId, DiscordWebhookBuilder builder) => this.EditWebhookMessageAsync(applicationId, interactionToken, messageId.ToString(), builder, null); /// /// Deletes the followup message. /// /// The application id. /// The interaction token. /// The message id. internal Task DeleteFollowupMessageAsync(ulong applicationId, string interactionToken, ulong messageId) => this.DeleteWebhookMessageAsync(applicationId, interactionToken, messageId); #endregion #region Misc /// /// Gets the current application info async. /// internal Task GetCurrentApplicationInfoAsync() => this.GetApplicationInfoAsync("@me"); /// /// Gets the application info async. /// /// The application_id. internal Task GetApplicationInfoAsync(ulong applicationId) => this.GetApplicationInfoAsync(applicationId.ToString(CultureInfo.InvariantCulture)); /// /// Gets the application info async. /// /// The application_id. private async Task GetApplicationInfoAsync(string applicationId) { var route = $"{Endpoints.OAUTH2}{Endpoints.APPLICATIONS}/:application_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {application_id = applicationId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); return JsonConvert.DeserializeObject(res.Response); } /// /// Gets the application assets async. /// /// The application. internal async Task> GetApplicationAssetsAsync(DiscordApplication application) { var route = $"{Endpoints.OAUTH2}{Endpoints.APPLICATIONS}/:application_id{Endpoints.ASSETS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { application_id = application.Id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var assets = JsonConvert.DeserializeObject>(res.Response); foreach (var asset in assets) { asset.Discord = application.Discord; asset.Application = application; } return new ReadOnlyCollection(new List(assets)); } /// /// Gets the gateway info async. /// internal async Task GetGatewayInfoAsync() { var headers = Utilities.GetBaseHeaders(); var route = Endpoints.GATEWAY; if (this.Discord.Configuration.TokenType == TokenType.Bot) route += Endpoints.BOT; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route, headers).ConfigureAwait(false); var info = JObject.Parse(res.Response).ToObject(); info.SessionBucket.ResetAfter = DateTimeOffset.UtcNow + TimeSpan.FromMilliseconds(info.SessionBucket.ResetAfterInternal); return info; } #endregion } diff --git a/DisCatSharp/Clients/Standalone/DiscordClient.cs b/DisCatSharp/Clients/Standalone/DiscordClient.cs index 3121ce9da..7a8277780 100644 --- a/DisCatSharp/Clients/Standalone/DiscordClient.cs +++ b/DisCatSharp/Clients/Standalone/DiscordClient.cs @@ -1,1303 +1,1303 @@ // 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 = false) => 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. /// The requested user. /// 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) { 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.Pronouns = usr.Pronouns; return old; }); return usr; } } /// /// Gets a channel. /// /// The id of the channel to get. /// 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); /// /// Gets a thread. /// /// The id of the thread to get. /// 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); /// /// 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. /// 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) { if (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; } /// /// Gets a guild preview. /// /// The guild ID. /// /// 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); /// /// Gets an invite. /// /// The invite code. /// Whether to include presence and total member counts in the returned invite. /// Whether to include the expiration date in the returned invite. /// The scheduled event id. /// The requested Invite. /// Thrown when the invite does not exists. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task GetInviteByCodeAsync(string code, bool? withCounts = null, bool? withExpiration = null, ulong? scheduledEventId = null) => this.ApiClient.GetInviteAsync(code, withCounts, withExpiration, scheduledEventId); /// /// 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(); + => this.ApiClient.GetCurrentUserConnectionsAsync(); /// /// 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); /// /// 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. /// /// 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); /// /// 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) { 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) { 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) { 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.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.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; foreach (var overwrite in channel.PermissionOverwritesInternal) { overwrite.Discord = this; overwrite.ChannelId = channel.Id; } 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); if (message.ReactionsInternal == null) 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 }