diff --git a/DisCatSharp/Clients/DiscordClient.WebSocket.cs b/DisCatSharp/Clients/DiscordClient.WebSocket.cs index 3b74506c4..7df07d238 100644 --- a/DisCatSharp/Clients/DiscordClient.WebSocket.cs +++ b/DisCatSharp/Clients/DiscordClient.WebSocket.cs @@ -1,601 +1,601 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.IO; using System.Threading; using System.Threading.Tasks; using DisCatSharp.Entities; using DisCatSharp.EventArgs; using DisCatSharp.Net.Abstractions; using DisCatSharp.Net.WebSocket; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace DisCatSharp { /// /// Represents a discord websocket client. /// public sealed partial class DiscordClient { #region Private Fields private int _heartbeatInterval; private DateTimeOffset _lastHeartbeat; [System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0052:Remove unread private members", Justification = "")] private Task _heartbeatTask; internal static DateTimeOffset _discordEpoch = new(2015, 1, 1, 0, 0, 0, TimeSpan.Zero); private int _skippedHeartbeats = 0; private long _lastSequence; internal IWebSocketClient _webSocketClient; private PayloadDecompressor _payloadDecompressor; private CancellationTokenSource _cancelTokenSource; private CancellationToken _cancelToken; #endregion #region Connection Semaphore /// /// Gets the socket locks. /// private static ConcurrentDictionary SocketLocks { get; } = new ConcurrentDictionary(); /// /// Gets the session lock. /// private ManualResetEventSlim SessionLock { get; } = new ManualResetEventSlim(true); #endregion #region Internal Connection Methods /// /// Internals the reconnect async. /// /// If true, start new session. /// The code. /// The message. /// A Task. private Task InternalReconnectAsync(bool startNewSession = false, int code = 1000, string message = "") { if (startNewSession) this._sessionId = null; _ = this._webSocketClient.DisconnectAsync(code, message); return Task.CompletedTask; } /// /// Internals the connect async. /// /// A Task. internal async Task InternalConnectAsync() { SocketLock socketLock = null; try { if (this.GatewayInfo == null) await this.InternalUpdateGatewayAsync().ConfigureAwait(false); await this.InitializeAsync().ConfigureAwait(false); socketLock = this.GetSocketLock(); await socketLock.LockAsync().ConfigureAwait(false); } catch { socketLock?.UnlockAfter(TimeSpan.Zero); throw; } if (!this.Presences.ContainsKey(this.CurrentUser.Id)) { this._presences[this.CurrentUser.Id] = new DiscordPresence { Discord = this, RawActivity = new TransportActivity(), Activity = new DiscordActivity(), Status = UserStatus.Online, InternalUser = new TransportUser { Id = this.CurrentUser.Id, Username = this.CurrentUser.Username, Discriminator = this.CurrentUser.Discriminator, AvatarHash = this.CurrentUser.AvatarHash } }; } else { var pr = this._presences[this.CurrentUser.Id]; pr.RawActivity = new TransportActivity(); pr.Activity = new DiscordActivity(); pr.Status = UserStatus.Online; } Volatile.Write(ref this._skippedHeartbeats, 0); this._webSocketClient = this.Configuration.WebSocketClientFactory(this.Configuration.Proxy); this._payloadDecompressor = this.Configuration.GatewayCompressionLevel != GatewayCompressionLevel.None ? new PayloadDecompressor(this.Configuration.GatewayCompressionLevel) : null; this._cancelTokenSource = new CancellationTokenSource(); this._cancelToken = this._cancelTokenSource.Token; this._webSocketClient.Connected += SocketOnConnect; this._webSocketClient.Disconnected += SocketOnDisconnect; this._webSocketClient.MessageReceived += SocketOnMessage; this._webSocketClient.ExceptionThrown += SocketOnException; var gwuri = new QueryUriBuilder(this.GatewayUri) - .AddParameter("v", "9") + .AddParameter("v", this.Configuration.ApiVersion) .AddParameter("encoding", "json"); if (this.Configuration.GatewayCompressionLevel == GatewayCompressionLevel.Stream) gwuri.AddParameter("compress", "zlib-stream"); await this._webSocketClient.ConnectAsync(gwuri.Build()).ConfigureAwait(false); Task SocketOnConnect(IWebSocketClient sender, SocketEventArgs e) => this._socketOpened.InvokeAsync(this, e); async Task SocketOnMessage(IWebSocketClient sender, SocketMessageEventArgs e) { string msg = null; if (e is SocketTextMessageEventArgs etext) { msg = etext.Message; } else if (e is SocketBinaryMessageEventArgs ebin) // :DDDD { using var ms = new MemoryStream(); if (!this._payloadDecompressor.TryDecompress(new ArraySegment(ebin.Message), ms)) { this.Logger.LogError(LoggerEvents.WebSocketReceiveFailure, "Payload decompression failed"); return; } ms.Position = 0; using var sr = new StreamReader(ms, Utilities.UTF8); msg = await sr.ReadToEndAsync().ConfigureAwait(false); } try { this.Logger.LogTrace(LoggerEvents.GatewayWsRx, msg); await this.HandleSocketMessageAsync(msg).ConfigureAwait(false); } catch (Exception ex) { this.Logger.LogError(LoggerEvents.WebSocketReceiveFailure, ex, "Socket handler suppressed an exception"); } } Task SocketOnException(IWebSocketClient sender, SocketErrorEventArgs e) => this._socketErrored.InvokeAsync(this, e); async Task SocketOnDisconnect(IWebSocketClient sender, SocketCloseEventArgs e) { // release session and connection this.ConnectionLock.Set(); this.SessionLock.Set(); if (!this._disposed) this._cancelTokenSource.Cancel(); this.Logger.LogDebug(LoggerEvents.ConnectionClose, "Connection closed ({0}, '{1}')", e.CloseCode, e.CloseMessage); await this._socketClosed.InvokeAsync(this, e).ConfigureAwait(false); if (this.Configuration.AutoReconnect && (e.CloseCode < 4001 || e.CloseCode >= 5000)) { this.Logger.LogCritical(LoggerEvents.ConnectionClose, "Connection terminated ({0}, '{1}'), reconnecting", e.CloseCode, e.CloseMessage); if (this._status == null) await this.ConnectAsync().ConfigureAwait(false); else if (this._status.IdleSince.HasValue) await this.ConnectAsync(this._status._activity, this._status.Status, Utilities.GetDateTimeOffsetFromMilliseconds(this._status.IdleSince.Value)).ConfigureAwait(false); else await this.ConnectAsync(this._status._activity, this._status.Status).ConfigureAwait(false); } else { this.Logger.LogCritical(LoggerEvents.ConnectionClose, "Connection terminated ({0}, '{1}')", e.CloseCode, e.CloseMessage); } } } #endregion #region WebSocket (Events) /// /// Handles the socket message async. /// /// The data. /// A Task. internal async Task HandleSocketMessageAsync(string data) { var payload = JsonConvert.DeserializeObject(data); this._lastSequence = payload.Sequence ?? this._lastSequence; switch (payload.OpCode) { case GatewayOpCode.Dispatch: await this.HandleDispatchAsync(payload).ConfigureAwait(false); break; case GatewayOpCode.Heartbeat: await this.OnHeartbeatAsync((long)payload.Data).ConfigureAwait(false); break; case GatewayOpCode.Reconnect: await this.OnReconnectAsync().ConfigureAwait(false); break; case GatewayOpCode.InvalidSession: await this.OnInvalidateSessionAsync((bool)payload.Data).ConfigureAwait(false); break; case GatewayOpCode.Hello: await this.OnHelloAsync((payload.Data as JObject).ToObject()).ConfigureAwait(false); break; case GatewayOpCode.HeartbeatAck: await this.OnHeartbeatAckAsync().ConfigureAwait(false); break; default: this.Logger.LogWarning(LoggerEvents.WebSocketReceive, "Unknown Discord opcode: {0}\nPayload: {1}", payload.OpCode, payload.Data); break; } } /// /// Ons the heartbeat async. /// /// The seq. /// A Task. internal async Task OnHeartbeatAsync(long seq) { this.Logger.LogTrace(LoggerEvents.WebSocketReceive, "Received HEARTBEAT (OP1)"); await this.SendHeartbeatAsync(seq).ConfigureAwait(false); } /// /// Ons the reconnect async. /// /// A Task. internal async Task OnReconnectAsync() { this.Logger.LogTrace(LoggerEvents.WebSocketReceive, "Received RECONNECT (OP7)"); await this.InternalReconnectAsync(code: 4000, message: "OP7 acknowledged").ConfigureAwait(false); } /// /// Ons the invalidate session async. /// /// If true, data. /// A Task. internal async Task OnInvalidateSessionAsync(bool data) { // begin a session if one is not open already if (this.SessionLock.Wait(0)) this.SessionLock.Reset(); // we are sending a fresh resume/identify, so lock the socket var socketLock = this.GetSocketLock(); await socketLock.LockAsync().ConfigureAwait(false); socketLock.UnlockAfter(TimeSpan.FromSeconds(5)); if (data) { this.Logger.LogTrace(LoggerEvents.WebSocketReceive, "Received INVALID_SESSION (OP9, true)"); await Task.Delay(6000).ConfigureAwait(false); await this.SendResumeAsync().ConfigureAwait(false); } else { this.Logger.LogTrace(LoggerEvents.WebSocketReceive, "Received INVALID_SESSION (OP9, false)"); this._sessionId = null; await this.SendIdentifyAsync(this._status).ConfigureAwait(false); } } /// /// Ons the hello async. /// /// The hello. /// A Task. internal async Task OnHelloAsync(GatewayHello hello) { this.Logger.LogTrace(LoggerEvents.WebSocketReceive, "Received HELLO (OP10)"); if (this.SessionLock.Wait(0)) { this.SessionLock.Reset(); this.GetSocketLock().UnlockAfter(TimeSpan.FromSeconds(5)); } else { this.Logger.LogWarning(LoggerEvents.SessionUpdate, "Attempt to start a session while another session is active"); return; } Interlocked.CompareExchange(ref this._skippedHeartbeats, 0, 0); this._heartbeatInterval = hello.HeartbeatInterval; this._heartbeatTask = Task.Run(this.HeartbeatLoopAsync, this._cancelToken); if (string.IsNullOrEmpty(this._sessionId)) await this.SendIdentifyAsync(this._status).ConfigureAwait(false); else await this.SendResumeAsync().ConfigureAwait(false); } /// /// Ons the heartbeat ack async. /// /// A Task. internal async Task OnHeartbeatAckAsync() { Interlocked.Decrement(ref this._skippedHeartbeats); var ping = (int)(DateTime.Now - this._lastHeartbeat).TotalMilliseconds; this.Logger.LogTrace(LoggerEvents.WebSocketReceive, "Received HEARTBEAT_ACK (OP11, {0}ms)", ping); Volatile.Write(ref this._ping, ping); var args = new HeartbeatEventArgs { Ping = this.Ping, Timestamp = DateTimeOffset.Now }; await this._heartbeated.InvokeAsync(this, args).ConfigureAwait(false); } /// /// Heartbeats the loop async. /// /// A Task. internal async Task HeartbeatLoopAsync() { this.Logger.LogDebug(LoggerEvents.Heartbeat, "Heartbeat task started"); var token = this._cancelToken; try { while (true) { await this.SendHeartbeatAsync(this._lastSequence).ConfigureAwait(false); await Task.Delay(this._heartbeatInterval, token).ConfigureAwait(false); token.ThrowIfCancellationRequested(); } } catch (OperationCanceledException) { } } #endregion #region Internal Gateway Methods /// /// Internals the update status async. /// /// The activity. /// The user status. /// The idle since. /// A Task. internal async Task InternalUpdateStatusAsync(DiscordActivity activity, UserStatus? userStatus, DateTimeOffset? idleSince) { if (activity != null && activity.Name != null && activity.Name.Length > 128) throw new Exception("Game name can't be longer than 128 characters!"); var since_unix = idleSince != null ? (long?)Utilities.GetUnixTime(idleSince.Value) : null; var act = activity ?? new DiscordActivity(); var status = new StatusUpdate { Activity = new TransportActivity(act), IdleSince = since_unix, IsAFK = idleSince != null, Status = userStatus ?? UserStatus.Online }; // Solution to have status persist between sessions this._status = status; var status_update = new GatewayPayload { OpCode = GatewayOpCode.StatusUpdate, Data = status }; var statusstr = JsonConvert.SerializeObject(status_update); await this.WsSendAsync(statusstr).ConfigureAwait(false); if (!this._presences.ContainsKey(this.CurrentUser.Id)) { this._presences[this.CurrentUser.Id] = new DiscordPresence { Discord = this, Activity = act, Status = userStatus ?? UserStatus.Online, InternalUser = new TransportUser { Id = this.CurrentUser.Id } }; } else { var pr = this._presences[this.CurrentUser.Id]; pr.Activity = act; pr.Status = userStatus ?? pr.Status; } } /// /// Sends the heartbeat async. /// /// The seq. /// A Task. internal async Task SendHeartbeatAsync(long seq) { var more_than_5 = Volatile.Read(ref this._skippedHeartbeats) > 5; var guilds_comp = Volatile.Read(ref this._guildDownloadCompleted); if (guilds_comp && more_than_5) { this.Logger.LogCritical(LoggerEvents.HeartbeatFailure, "Server failed to acknowledge more than 5 heartbeats - connection is zombie"); var args = new ZombiedEventArgs { Failures = Volatile.Read(ref this._skippedHeartbeats), GuildDownloadCompleted = true }; await this._zombied.InvokeAsync(this, args).ConfigureAwait(false); await this.InternalReconnectAsync(code: 4001, message: "Too many heartbeats missed").ConfigureAwait(false); return; } else if (!guilds_comp && more_than_5) { var args = new ZombiedEventArgs { Failures = Volatile.Read(ref this._skippedHeartbeats), GuildDownloadCompleted = false }; await this._zombied.InvokeAsync(this, args).ConfigureAwait(false); this.Logger.LogWarning(LoggerEvents.HeartbeatFailure, "Server failed to acknowledge more than 5 heartbeats, but the guild download is still running - check your connection speed"); } Volatile.Write(ref this._lastSequence, seq); this.Logger.LogTrace(LoggerEvents.Heartbeat, "Sending heartbeat"); var heartbeat = new GatewayPayload { OpCode = GatewayOpCode.Heartbeat, Data = seq }; var heartbeat_str = JsonConvert.SerializeObject(heartbeat); await this.WsSendAsync(heartbeat_str).ConfigureAwait(false); this._lastHeartbeat = DateTimeOffset.Now; Interlocked.Increment(ref this._skippedHeartbeats); } /// /// Sends the identify async. /// /// The status. /// A Task. internal async Task SendIdentifyAsync(StatusUpdate status) { var identify = new GatewayIdentify { Token = Utilities.GetFormattedToken(this), Compress = this.Configuration.GatewayCompressionLevel == GatewayCompressionLevel.Payload, LargeThreshold = this.Configuration.LargeThreshold, ShardInfo = new ShardInfo { ShardId = this.Configuration.ShardId, ShardCount = this.Configuration.ShardCount }, Presence = status, Intents = this.Configuration.Intents, Discord = this }; var payload = new GatewayPayload { OpCode = GatewayOpCode.Identify, Data = identify }; var payloadstr = JsonConvert.SerializeObject(payload); await this.WsSendAsync(payloadstr).ConfigureAwait(false); this.Logger.LogDebug(LoggerEvents.Intents, "Registered gateway intents ({0})", this.Configuration.Intents); } /// /// Sends the resume async. /// /// A Task. internal async Task SendResumeAsync() { var resume = new GatewayResume { Token = Utilities.GetFormattedToken(this), SessionId = this._sessionId, SequenceNumber = Volatile.Read(ref this._lastSequence) }; var resume_payload = new GatewayPayload { OpCode = GatewayOpCode.Resume, Data = resume }; var resumestr = JsonConvert.SerializeObject(resume_payload); await this.WsSendAsync(resumestr).ConfigureAwait(false); } /// /// Internals the update gateway async. /// /// A Task. internal async Task InternalUpdateGatewayAsync() { var info = await this.GetGatewayInfoAsync().ConfigureAwait(false); this.GatewayInfo = info; this.GatewayUri = new Uri(info.Url); } /// /// Ws the send async. /// /// The payload. /// A Task. internal async Task WsSendAsync(string payload) { this.Logger.LogTrace(LoggerEvents.GatewayWsTx, payload); await this._webSocketClient.SendMessageAsync(payload).ConfigureAwait(false); } #endregion #region Semaphore Methods /// /// Gets the socket lock. /// /// A SocketLock. private SocketLock GetSocketLock() => SocketLocks.GetOrAdd(this.CurrentApplication.Id, appId => new SocketLock(appId, this.GatewayInfo.SessionBucket.MaxConcurrency)); #endregion } } diff --git a/DisCatSharp/Clients/DiscordShardedClient.cs b/DisCatSharp/Clients/DiscordShardedClient.cs index 9fde84b82..5efc8f4dc 100644 --- a/DisCatSharp/Clients/DiscordShardedClient.cs +++ b/DisCatSharp/Clients/DiscordShardedClient.cs @@ -1,732 +1,732 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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. #pragma warning disable CS0618 using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Net.Http; using System.Reflection; using System.Threading.Tasks; using DisCatSharp.Entities; using DisCatSharp.EventArgs; using DisCatSharp.Net; using DisCatSharp.Common.Utilities; using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; namespace DisCatSharp { /// /// A Discord client that shards automatically. /// public sealed partial class DiscordShardedClient { #region Public Properties /// /// Gets the logger for this client. /// public ILogger Logger { get; } /// /// Gets all client shards. /// public IReadOnlyDictionary ShardClients { get; } /// /// Gets the gateway info for the client's session. /// public GatewayInfo GatewayInfo { get; private set; } /// /// Gets the current user. /// public DiscordUser CurrentUser { get; private set; } /// /// Gets the current application. /// public DiscordApplication CurrentApplication { get; private set; } /// /// Gets the library team. /// public DisCatSharpTeam LibraryDeveloperTeam => this.GetShard(0).LibraryDeveloperTeam; /// /// Gets the list of available voice regions. Note that this property will not contain VIP voice regions. /// public IReadOnlyDictionary VoiceRegions => this._voiceRegionsLazy?.Value; #endregion #region Private Properties/Fields /// /// Gets the configuration. /// private DiscordConfiguration Configuration { get; } /// /// Gets the list of available voice regions. This property is meant as a way to modify . /// private ConcurrentDictionary _internalVoiceRegions; private readonly ConcurrentDictionary _shards = new(); private Lazy> _voiceRegionsLazy; private bool _isStarted; private readonly bool _manuallySharding; #endregion #region Constructor /// /// Initializes new auto-sharding Discord client. /// /// Configuration to use. public DiscordShardedClient(DiscordConfiguration config) { this.InternalSetup(); if (config.ShardCount > 1) this._manuallySharding = true; this.Configuration = config; this.ShardClients = new ReadOnlyConcurrentDictionary(this._shards); if (this.Configuration.LoggerFactory == null) { this.Configuration.LoggerFactory = new DefaultLoggerFactory(); this.Configuration.LoggerFactory.AddProvider(new DefaultLoggerProvider(this.Configuration.MinimumLogLevel, this.Configuration.LogTimestampFormat)); } this.Logger = this.Configuration.LoggerFactory.CreateLogger(); } #endregion #region Public Methods /// /// Initializes and connects all shards. /// /// /// /// public async Task StartAsync() { if (this._isStarted) throw new InvalidOperationException("This client has already been started."); this._isStarted = true; try { 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.Value); var shardc = await this.InitializeShardsAsync().ConfigureAwait(false); var connectTasks = new List(); this.Logger.LogInformation(LoggerEvents.ShardStartup, "Booting {0} shards.", shardc); for (var i = 0; i < shardc; i++) { //This should never happen, but in case it does... if (this.GatewayInfo.SessionBucket.MaxConcurrency < 1) this.GatewayInfo.SessionBucket.MaxConcurrency = 1; if (this.GatewayInfo.SessionBucket.MaxConcurrency == 1) await this.ConnectShardAsync(i).ConfigureAwait(false); else { //Concurrent login. connectTasks.Add(this.ConnectShardAsync(i)); if (connectTasks.Count == this.GatewayInfo.SessionBucket.MaxConcurrency) { await Task.WhenAll(connectTasks).ConfigureAwait(false); connectTasks.Clear(); } } } } catch (Exception ex) { await this.InternalStopAsync(false).ConfigureAwait(false); var message = $"Shard initialization failed, check inner exceptions for details: "; this.Logger.LogCritical(LoggerEvents.ShardClientError, $"{message}\n{ex}"); throw new AggregateException(message, ex); } } /// /// Disconnects and disposes of all shards. /// /// /// public Task StopAsync() => this.InternalStopAsync(); /// /// Gets a shard from a guild ID. /// /// If automatically sharding, this will use the method. /// Otherwise if manually sharding, it will instead iterate through each shard's guild caches. /// /// /// The guild ID for the shard. /// The found shard. Otherwise if the shard was not found for the guild ID. public DiscordClient GetShard(ulong guildId) { var index = this._manuallySharding ? this.GetShardIdFromGuilds(guildId) : Utilities.GetShardId(guildId, this.ShardClients.Count); return index != -1 ? this._shards[index] : null; } /// /// Gets a shard from a guild. /// /// If automatically sharding, this will use the method. /// Otherwise if manually sharding, it will instead iterate through each shard's guild caches. /// /// /// The guild for the shard. /// The found shard. Otherwise if the shard was not found for the guild. public DiscordClient GetShard(DiscordGuild guild) => this.GetShard(guild.Id); /// /// Updates playing statuses on all shards. /// /// Activity to set. /// Status of the user. /// Since when is the client performing the specified activity. /// Asynchronous operation. public async Task UpdateStatusAsync(DiscordActivity activity = null, UserStatus? userStatus = null, DateTimeOffset? idleSince = null) { var tasks = new List(); foreach (var client in this._shards.Values) tasks.Add(client.UpdateStatusAsync(activity, userStatus, idleSince)); await Task.WhenAll(tasks).ConfigureAwait(false); } #endregion #region Internal Methods /// /// Initializes the shards async. /// /// A Task. internal async Task InitializeShardsAsync() { if (this._shards.Count != 0) return this._shards.Count; this.GatewayInfo = await this.GetGatewayInfoAsync().ConfigureAwait(false); var shardc = this.Configuration.ShardCount == 1 ? this.GatewayInfo.ShardCount : this.Configuration.ShardCount; var lf = new ShardedLoggerFactory(this.Logger); for (var i = 0; i < shardc; i++) { var cfg = new DiscordConfiguration(this.Configuration) { ShardId = i, ShardCount = shardc, LoggerFactory = lf }; var client = new DiscordClient(cfg); if (!this._shards.TryAdd(i, client)) throw new InvalidOperationException("Could not initialize shards."); } return shardc; } #endregion #region Private Methods/Version Property /// /// Gets the gateway info async. /// /// A Task. private async Task GetGatewayInfoAsync() { - var url = $"{Utilities.GetApiBaseUri()}{Endpoints.GATEWAY}{Endpoints.BOT}"; + var url = $"{Utilities.GetApiBaseUri(this.Configuration)}{Endpoints.GATEWAY}{Endpoints.BOT}"; var http = new HttpClient(); http.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", Utilities.GetUserAgent()); http.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", Utilities.GetFormattedToken(this.Configuration)); this.Logger.LogDebug(LoggerEvents.ShardRest, $"Obtaining gateway information from GET {Endpoints.GATEWAY}{Endpoints.BOT}..."); var resp = await http.GetAsync(url).ConfigureAwait(false); http.Dispose(); if (!resp.IsSuccessStatusCode) { var ratelimited = await HandleHttpError(url, resp).ConfigureAwait(false); if (ratelimited) return await this.GetGatewayInfoAsync().ConfigureAwait(false); } var timer = new Stopwatch(); timer.Start(); var jo = JObject.Parse(await resp.Content.ReadAsStringAsync().ConfigureAwait(false)); var info = jo.ToObject(); //There is a delay from parsing here. timer.Stop(); info.SessionBucket.resetAfter -= (int)timer.ElapsedMilliseconds; info.SessionBucket.ResetAfter = DateTimeOffset.UtcNow + TimeSpan.FromMilliseconds(info.SessionBucket.resetAfter); return info; async Task HandleHttpError(string reqUrl, HttpResponseMessage msg) { var code = (int)msg.StatusCode; if (code == 401 || code == 403) { throw new Exception($"Authentication failed, check your token and try again: {code} {msg.ReasonPhrase}"); } else if (code == 429) { this.Logger.LogError(LoggerEvents.ShardClientError, $"Ratelimit hit, requeuing request to {reqUrl}"); var hs = msg.Headers.ToDictionary(xh => xh.Key, xh => string.Join("\n", xh.Value), StringComparer.OrdinalIgnoreCase); var waitInterval = 0; if (hs.TryGetValue("Retry-After", out var retry_after_raw)) waitInterval = int.Parse(retry_after_raw, CultureInfo.InvariantCulture); await Task.Delay(waitInterval).ConfigureAwait(false); return true; } else if (code >= 500) { throw new Exception($"Internal Server Error: {code} {msg.ReasonPhrase}"); } else { throw new Exception($"An unsuccessful HTTP status code was encountered: {code} {msg.ReasonPhrase}"); } } } private readonly Lazy _versionString = new(() => { var a = typeof(DiscordShardedClient).GetTypeInfo().Assembly; var iv = a.GetCustomAttribute(); if (iv != null) return iv.InformationalVersion; var v = a.GetName().Version; var vs = v.ToString(3); if (v.Revision > 0) vs = $"{vs}, CI build {v.Revision}"; return vs; }); private readonly string _botLibrary = "DisCatSharp"; #endregion #region Private Connection Methods /// /// Connects the shard async. /// /// The i. /// A Task. private async Task ConnectShardAsync(int i) { if (!this._shards.TryGetValue(i, out var client)) throw new Exception($"Could not initialize shard {i}."); if (this.GatewayInfo != null) { client.GatewayInfo = this.GatewayInfo; client.GatewayUri = new Uri(client.GatewayInfo.Url); } if (this.CurrentUser != null) client.CurrentUser = this.CurrentUser; if (this.CurrentApplication != null) client.CurrentApplication = this.CurrentApplication; if (this._internalVoiceRegions != null) { client.InternalVoiceRegions = this._internalVoiceRegions; client._voice_regions_lazy = new Lazy>(() => new ReadOnlyDictionary(client.InternalVoiceRegions)); } this.HookEventHandlers(client); client._isShard = true; await client.ConnectAsync().ConfigureAwait(false); this.Logger.LogInformation(LoggerEvents.ShardStartup, "Booted shard {0}.", i); if (this.CurrentUser == null) this.CurrentUser = client.CurrentUser; if (this.CurrentApplication == null) this.CurrentApplication = client.CurrentApplication; if (this._internalVoiceRegions == null) { this._internalVoiceRegions = client.InternalVoiceRegions; this._voiceRegionsLazy = new Lazy>(() => new ReadOnlyDictionary(this._internalVoiceRegions)); } } /// /// Internals the stop async. /// /// If true, enable logger. /// A Task. private Task InternalStopAsync(bool enableLogger = true) { if (!this._isStarted) throw new InvalidOperationException("This client has not been started."); if (enableLogger) this.Logger.LogInformation(LoggerEvents.ShardShutdown, "Disposing {0} shards.", this._shards.Count); this._isStarted = false; this._voiceRegionsLazy = null; this.GatewayInfo = null; this.CurrentUser = null; this.CurrentApplication = null; for (var i = 0; i < this._shards.Count; i++) { if (this._shards.TryGetValue(i, out var client)) { this.UnhookEventHandlers(client); client.Dispose(); if (enableLogger) this.Logger.LogInformation(LoggerEvents.ShardShutdown, "Disconnected shard {0}.", i); } } this._shards.Clear(); return Task.CompletedTask; } #endregion #region Event Handler Initialization/Registering /// /// Internals the setup. /// private void InternalSetup() { this._clientErrored = new AsyncEvent("CLIENT_ERRORED", DiscordClient.EventExecutionLimit, this.Goof); this._socketErrored = new AsyncEvent("SOCKET_ERRORED", DiscordClient.EventExecutionLimit, this.Goof); this._socketOpened = new AsyncEvent("SOCKET_OPENED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._socketClosed = new AsyncEvent("SOCKET_CLOSED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._ready = new AsyncEvent("READY", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._resumed = new AsyncEvent("RESUMED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._channelCreated = new AsyncEvent("CHANNEL_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._channelUpdated = new AsyncEvent("CHANNEL_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._channelDeleted = new AsyncEvent("CHANNEL_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._dmChannelDeleted = new AsyncEvent("DM_CHANNEL_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._channelPinsUpdated = new AsyncEvent("CHANNEL_PINS_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildCreated = new AsyncEvent("GUILD_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildAvailable = new AsyncEvent("GUILD_AVAILABLE", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildUpdated = new AsyncEvent("GUILD_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildDeleted = new AsyncEvent("GUILD_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildUnavailable = new AsyncEvent("GUILD_UNAVAILABLE", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildDownloadCompleted = new AsyncEvent("GUILD_DOWNLOAD_COMPLETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._inviteCreated = new AsyncEvent("INVITE_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._inviteDeleted = new AsyncEvent("INVITE_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._messageCreated = new AsyncEvent("MESSAGE_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._presenceUpdated = new AsyncEvent("PRESENCE_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildBanAdded = new AsyncEvent("GUILD_BAN_ADDED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildBanRemoved = new AsyncEvent("GUILD_BAN_REMOVED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildEmojisUpdated = new AsyncEvent("GUILD_EMOJI_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildStickersUpdated = new AsyncEvent("GUILD_STICKER_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildIntegrationsUpdated = new AsyncEvent("GUILD_INTEGRATIONS_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildMemberAdded = new AsyncEvent("GUILD_MEMBER_ADDED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildMemberRemoved = new AsyncEvent("GUILD_MEMBER_REMOVED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildMemberUpdated = new AsyncEvent("GUILD_MEMBER_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildRoleCreated = new AsyncEvent("GUILD_ROLE_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildRoleUpdated = new AsyncEvent("GUILD_ROLE_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildRoleDeleted = new AsyncEvent("GUILD_ROLE_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._messageUpdated = new AsyncEvent("MESSAGE_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._messageDeleted = new AsyncEvent("MESSAGE_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._messageBulkDeleted = new AsyncEvent("MESSAGE_BULK_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._interactionCreated = new AsyncEvent("INTERACTION_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._componentInteractionCreated = new AsyncEvent("COMPONENT_INTERACTED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._contextMenuInteractionCreated = new AsyncEvent("CONTEXT_MENU_INTERACTED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._typingStarted = new AsyncEvent("TYPING_STARTED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._userSettingsUpdated = new AsyncEvent("USER_SETTINGS_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._userUpdated = new AsyncEvent("USER_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._voiceStateUpdated = new AsyncEvent("VOICE_STATE_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._voiceServerUpdated = new AsyncEvent("VOICE_SERVER_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildMembersChunk = new AsyncEvent("GUILD_MEMBERS_CHUNKED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._unknownEvent = new AsyncEvent("UNKNOWN_EVENT", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._messageReactionAdded = new AsyncEvent("MESSAGE_REACTION_ADDED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._messageReactionRemoved = new AsyncEvent("MESSAGE_REACTION_REMOVED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._messageReactionsCleared = new AsyncEvent("MESSAGE_REACTIONS_CLEARED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._messageReactionRemovedEmoji = new AsyncEvent("MESSAGE_REACTION_REMOVED_EMOJI", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._webhooksUpdated = new AsyncEvent("WEBHOOKS_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._heartbeated = new AsyncEvent("HEARTBEATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._applicationCommandCreated = new AsyncEvent("APPLICATION_COMMAND_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._applicationCommandUpdated = new AsyncEvent("APPLICATION_COMMAND_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._applicationCommandDeleted = new AsyncEvent("APPLICATION_COMMAND_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildApplicationCommandCountUpdated = new AsyncEvent("GUILD_APPLICATION_COMMAND_COUNTS_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._applicationCommandPermissionsUpdated = new AsyncEvent("APPLICATION_COMMAND_PERMISSIONS_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildIntegrationCreated = new AsyncEvent("INTEGRATION_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildIntegrationUpdated = new AsyncEvent("INTEGRATION_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildIntegrationDeleted = new AsyncEvent("INTEGRATION_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._stageInstanceCreated = new AsyncEvent("STAGE_INSTANCE_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._stageInstanceUpdated = new AsyncEvent("STAGE_INSTANCE_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._stageInstanceDeleted = new AsyncEvent("STAGE_INSTANCE_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._threadCreated = new AsyncEvent("THREAD_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._threadUpdated = new AsyncEvent("THREAD_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._threadDeleted = new AsyncEvent("THREAD_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._threadListSynced = new AsyncEvent("THREAD_LIST_SYNCED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._threadMemberUpdated = new AsyncEvent("THREAD_MEMBER_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._threadMembersUpdated = new AsyncEvent("THREAD_MEMBERS_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._zombied = new AsyncEvent("ZOMBIED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._payloadReceived = new AsyncEvent("PAYLOAD_RECEIVED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildScheduledEventCreated = new AsyncEvent("GUILD_SCHEDULED_EVENT_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildScheduledEventUpdated = new AsyncEvent("GUILD_SCHEDULED_EVENT_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildScheduledEventDeleted = new AsyncEvent("GUILD_SCHEDULED_EVENT_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); } /// /// Hooks the event handlers. /// /// The client. private void HookEventHandlers(DiscordClient client) { client.ClientErrored += this.Client_ClientError; client.SocketErrored += this.Client_SocketError; client.SocketOpened += this.Client_SocketOpened; client.SocketClosed += this.Client_SocketClosed; client.Ready += this.Client_Ready; client.Resumed += this.Client_Resumed; client.ChannelCreated += this.Client_ChannelCreated; client.ChannelUpdated += this.Client_ChannelUpdated; client.ChannelDeleted += this.Client_ChannelDeleted; client.DmChannelDeleted += this.Client_DMChannelDeleted; client.ChannelPinsUpdated += this.Client_ChannelPinsUpdated; client.GuildCreated += this.Client_GuildCreated; client.GuildAvailable += this.Client_GuildAvailable; client.GuildUpdated += this.Client_GuildUpdated; client.GuildDeleted += this.Client_GuildDeleted; client.GuildUnavailable += this.Client_GuildUnavailable; client.GuildDownloadCompleted += this.Client_GuildDownloadCompleted; client.InviteCreated += this.Client_InviteCreated; client.InviteDeleted += this.Client_InviteDeleted; client.MessageCreated += this.Client_MessageCreated; client.PresenceUpdated += this.Client_PresenceUpdate; client.GuildBanAdded += this.Client_GuildBanAdd; client.GuildBanRemoved += this.Client_GuildBanRemove; client.GuildEmojisUpdated += this.Client_GuildEmojisUpdate; client.GuildStickersUpdated += this.Client_GuildStickersUpdate; client.GuildIntegrationsUpdated += this.Client_GuildIntegrationsUpdate; client.GuildMemberAdded += this.Client_GuildMemberAdd; client.GuildMemberRemoved += this.Client_GuildMemberRemove; client.GuildMemberUpdated += this.Client_GuildMemberUpdate; client.GuildRoleCreated += this.Client_GuildRoleCreate; client.GuildRoleUpdated += this.Client_GuildRoleUpdate; client.GuildRoleDeleted += this.Client_GuildRoleDelete; client.MessageUpdated += this.Client_MessageUpdate; client.MessageDeleted += this.Client_MessageDelete; client.MessagesBulkDeleted += this.Client_MessageBulkDelete; client.InteractionCreated += this.Client_InteractionCreate; client.ComponentInteractionCreated += this.Client_ComponentInteractionCreate; client.ContextMenuInteractionCreated += this.Client_ContextMenuInteractionCreate; client.TypingStarted += this.Client_TypingStart; client.UserSettingsUpdated += this.Client_UserSettingsUpdate; client.UserUpdated += this.Client_UserUpdate; client.VoiceStateUpdated += this.Client_VoiceStateUpdate; client.VoiceServerUpdated += this.Client_VoiceServerUpdate; client.GuildMembersChunked += this.Client_GuildMembersChunk; client.UnknownEvent += this.Client_UnknownEvent; client.MessageReactionAdded += this.Client_MessageReactionAdd; client.MessageReactionRemoved += this.Client_MessageReactionRemove; client.MessageReactionsCleared += this.Client_MessageReactionRemoveAll; client.MessageReactionRemovedEmoji += this.Client_MessageReactionRemovedEmoji; client.WebhooksUpdated += this.Client_WebhooksUpdate; client.Heartbeated += this.Client_HeartBeated; client.ApplicationCommandCreated += this.Client_ApplicationCommandCreated; client.ApplicationCommandUpdated += this.Client_ApplicationCommandUpdated; client.ApplicationCommandDeleted += this.Client_ApplicationCommandDeleted; client.GuildApplicationCommandCountUpdated += this.Client_GuildApplicationCommandCountUpdated; client.ApplicationCommandPermissionsUpdated += this.Client_ApplicationCommandPermissionsUpdated; client.GuildIntegrationCreated += this.Client_GuildIntegrationCreated; client.GuildIntegrationUpdated += this.Client_GuildIntegrationUpdated; client.GuildIntegrationDeleted += this.Client_GuildIntegrationDeleted; client.StageInstanceCreated += this.Client_StageInstanceCreated; client.StageInstanceUpdated += this.Client_StageInstanceUpdated; client.StageInstanceDeleted += this.Client_StageInstanceDeleted; client.ThreadCreated += this.Client_ThreadCreated; client.ThreadUpdated += this.Client_ThreadUpdated; client.ThreadDeleted += this.Client_ThreadDeleted; client.ThreadListSynced += this.Client_ThreadListSynced; client.ThreadMemberUpdated += this.Client_ThreadMemberUpdated; client.ThreadMembersUpdated += this.Client_ThreadMembersUpdated; client.Zombied += this.Client_Zombied; client.PayloadReceived += this.Client_PayloadReceived; client.GuildScheduledEventCreated += this.Client_GuildScheduledEventCreated; client.GuildScheduledEventUpdated += this.Client_GuildScheduledEventUpdated; client.GuildScheduledEventDeleted += this.Client_GuildScheduledEventDeleted; } /// /// Unhooks the event handlers. /// /// The client. private void UnhookEventHandlers(DiscordClient client) { client.ClientErrored -= this.Client_ClientError; client.SocketErrored -= this.Client_SocketError; client.SocketOpened -= this.Client_SocketOpened; client.SocketClosed -= this.Client_SocketClosed; client.Ready -= this.Client_Ready; client.Resumed -= this.Client_Resumed; client.ChannelCreated -= this.Client_ChannelCreated; client.ChannelUpdated -= this.Client_ChannelUpdated; client.ChannelDeleted -= this.Client_ChannelDeleted; client.DmChannelDeleted -= this.Client_DMChannelDeleted; client.ChannelPinsUpdated -= this.Client_ChannelPinsUpdated; client.GuildCreated -= this.Client_GuildCreated; client.GuildAvailable -= this.Client_GuildAvailable; client.GuildUpdated -= this.Client_GuildUpdated; client.GuildDeleted -= this.Client_GuildDeleted; client.GuildUnavailable -= this.Client_GuildUnavailable; client.GuildDownloadCompleted -= this.Client_GuildDownloadCompleted; client.InviteCreated -= this.Client_InviteCreated; client.InviteDeleted -= this.Client_InviteDeleted; client.MessageCreated -= this.Client_MessageCreated; client.PresenceUpdated -= this.Client_PresenceUpdate; client.GuildBanAdded -= this.Client_GuildBanAdd; client.GuildBanRemoved -= this.Client_GuildBanRemove; client.GuildEmojisUpdated -= this.Client_GuildEmojisUpdate; client.GuildStickersUpdated -= this.Client_GuildStickersUpdate; client.GuildIntegrationsUpdated -= this.Client_GuildIntegrationsUpdate; client.GuildMemberAdded -= this.Client_GuildMemberAdd; client.GuildMemberRemoved -= this.Client_GuildMemberRemove; client.GuildMemberUpdated -= this.Client_GuildMemberUpdate; client.GuildRoleCreated -= this.Client_GuildRoleCreate; client.GuildRoleUpdated -= this.Client_GuildRoleUpdate; client.GuildRoleDeleted -= this.Client_GuildRoleDelete; client.MessageUpdated -= this.Client_MessageUpdate; client.MessageDeleted -= this.Client_MessageDelete; client.MessagesBulkDeleted -= this.Client_MessageBulkDelete; client.InteractionCreated -= this.Client_InteractionCreate; client.ComponentInteractionCreated -= this.Client_ComponentInteractionCreate; client.ContextMenuInteractionCreated -= this.Client_ContextMenuInteractionCreate; client.TypingStarted -= this.Client_TypingStart; client.UserSettingsUpdated -= this.Client_UserSettingsUpdate; client.UserUpdated -= this.Client_UserUpdate; client.VoiceStateUpdated -= this.Client_VoiceStateUpdate; client.VoiceServerUpdated -= this.Client_VoiceServerUpdate; client.GuildMembersChunked -= this.Client_GuildMembersChunk; client.UnknownEvent -= this.Client_UnknownEvent; client.MessageReactionAdded -= this.Client_MessageReactionAdd; client.MessageReactionRemoved -= this.Client_MessageReactionRemove; client.MessageReactionsCleared -= this.Client_MessageReactionRemoveAll; client.MessageReactionRemovedEmoji -= this.Client_MessageReactionRemovedEmoji; client.WebhooksUpdated -= this.Client_WebhooksUpdate; client.Heartbeated -= this.Client_HeartBeated; client.ApplicationCommandCreated -= this.Client_ApplicationCommandCreated; client.ApplicationCommandUpdated -= this.Client_ApplicationCommandUpdated; client.ApplicationCommandDeleted -= this.Client_ApplicationCommandDeleted; client.GuildApplicationCommandCountUpdated -= this.Client_GuildApplicationCommandCountUpdated; client.ApplicationCommandPermissionsUpdated -= this.Client_ApplicationCommandPermissionsUpdated; client.GuildIntegrationCreated -= this.Client_GuildIntegrationCreated; client.GuildIntegrationUpdated -= this.Client_GuildIntegrationUpdated; client.GuildIntegrationDeleted -= this.Client_GuildIntegrationDeleted; client.StageInstanceCreated -= this.Client_StageInstanceCreated; client.StageInstanceUpdated -= this.Client_StageInstanceUpdated; client.StageInstanceDeleted -= this.Client_StageInstanceDeleted; client.ThreadCreated -= this.Client_ThreadCreated; client.ThreadUpdated -= this.Client_ThreadUpdated; client.ThreadDeleted -= this.Client_ThreadDeleted; client.ThreadListSynced -= this.Client_ThreadListSynced; client.ThreadMemberUpdated -= this.Client_ThreadMemberUpdated; client.ThreadMembersUpdated -= this.Client_ThreadMembersUpdated; client.Zombied -= this.Client_Zombied; client.PayloadReceived -= this.Client_PayloadReceived; client.GuildScheduledEventCreated -= this.Client_GuildScheduledEventCreated; client.GuildScheduledEventUpdated -= this.Client_GuildScheduledEventUpdated; client.GuildScheduledEventDeleted -= this.Client_GuildScheduledEventDeleted; } /// /// Gets the shard id from guilds. /// /// The id. /// An int. private int GetShardIdFromGuilds(ulong id) { foreach (var s in this._shards.Values) { if (s._guilds.TryGetValue(id, out _)) { return s.ShardId; } } return -1; } #endregion #region Destructor ~DiscordShardedClient() => this.InternalStopAsync(false).GetAwaiter().GetResult(); #endregion } } diff --git a/DisCatSharp/DiscordConfiguration.cs b/DisCatSharp/DiscordConfiguration.cs index c8c8616a1..feab27ed8 100644 --- a/DisCatSharp/DiscordConfiguration.cs +++ b/DisCatSharp/DiscordConfiguration.cs @@ -1,242 +1,249 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Net; using DisCatSharp.Net.Udp; using DisCatSharp.Net.WebSocket; using Microsoft.Extensions.Logging; namespace DisCatSharp { /// /// Represents configuration for and . /// public sealed class DiscordConfiguration { /// /// Sets the token used to identify the client. /// public string Token { internal get => this._token; set { if (string.IsNullOrWhiteSpace(value)) throw new ArgumentNullException(nameof(value), "Token cannot be null, empty, or all whitespace."); this._token = value.Trim(); } } private string _token = ""; /// /// Sets the type of the token used to identify the client. /// Defaults to . /// public TokenType TokenType { internal get; set; } = TokenType.Bot; /// /// Sets the minimum logging level for messages. /// Typically, the default value of is ok for most uses. /// public LogLevel MinimumLogLevel { internal get; set; } = LogLevel.Information; + /// + /// Overwrites the api version. + /// Defaults to 9. + /// + public string ApiVersion { internal get; set; } = "9"; + /// /// Sets whether to rely on Discord for NTP (Network Time Protocol) synchronization with the "X-Ratelimit-Reset-After" header. /// If the system clock is unsynced, setting this to true will ensure ratelimits are synced with Discord and reduce the risk of hitting one. /// This should only be set to false if the system clock is synced with NTP. /// Defaults to true. /// public bool UseRelativeRatelimit { internal get; set; } = true; /// /// Allows you to overwrite the time format used by the internal debug logger. /// Only applicable when is set left at default value. Defaults to ISO 8601-like format. /// public string LogTimestampFormat { internal get; set; } = "yyyy-MM-dd HH:mm:ss zzz"; /// /// Sets the member count threshold at which guilds are considered large. /// Defaults to 250. /// public int LargeThreshold { internal get; set; } = 250; /// /// Sets whether to automatically reconnect in case a connection is lost. /// Defaults to true. /// public bool AutoReconnect { internal get; set; } = true; /// /// Sets the ID of the shard to connect to. /// If not sharding, or sharding automatically, this value should be left with the default value of 0. /// public int ShardId { internal get; set; } = 0; /// /// Sets the total number of shards the bot is on. If not sharding, this value should be left with a default value of 1. /// If sharding automatically, this value will indicate how many shards to boot. If left default for automatic sharding, the client will determine the shard count automatically. /// public int ShardCount { internal get; set; } = 1; /// /// Sets the level of compression for WebSocket traffic. /// Disabling this option will increase the amount of traffic sent via WebSocket. Setting will enable compression for READY and GUILD_CREATE payloads. Setting will enable compression for the entire WebSocket stream, drastically reducing amount of traffic. /// Defaults to . /// public GatewayCompressionLevel GatewayCompressionLevel { internal get; set; } = GatewayCompressionLevel.Stream; /// /// Sets the size of the global message cache. /// Setting this to 0 will disable message caching entirely. Defaults to 1024. /// public int MessageCacheSize { internal get; set; } = 1024; /// /// Sets the proxy to use for HTTP and WebSocket connections to Discord. /// Defaults to null. /// public IWebProxy Proxy { internal get; set; } = null; /// /// Sets the timeout for HTTP requests. /// Set to to disable timeouts. /// Defaults to 10 seconds. /// public TimeSpan HttpTimeout { internal get; set; } = TimeSpan.FromSeconds(100); /// /// Defines that the client should attempt to reconnect indefinitely. /// This is typically a very bad idea to set to true, as it will swallow all connection errors. /// Defaults to false. /// public bool ReconnectIndefinitely { internal get; set; } = false; /// /// Sets whether the client should attempt to cache members if exclusively using unprivileged intents. /// /// This will only take effect if there are no or /// intents specified. Otherwise, this will always be overwritten to true. /// /// Defaults to true. /// public bool AlwaysCacheMembers { internal get; set; } = true; /// /// Sets the gateway intents for this client. /// If set, the client will only receive events that they specify with intents. /// Defaults to . /// public DiscordIntents Intents { internal get; set; } = DiscordIntents.AllUnprivileged; /// /// Sets the factory method used to create instances of WebSocket clients. /// Use and equivalents on other implementations to switch out client implementations. /// Defaults to . /// public WebSocketClientFactoryDelegate WebSocketClientFactory { internal get => this._webSocketClientFactory; set { if (value == null) throw new InvalidOperationException("You need to supply a valid WebSocket client factory method."); this._webSocketClientFactory = value; } } private WebSocketClientFactoryDelegate _webSocketClientFactory = WebSocketClient.CreateNew; /// /// Sets the factory method used to create instances of UDP clients. /// Use and equivalents on other implementations to switch out client implementations. /// Defaults to . /// public UdpClientFactoryDelegate UdpClientFactory { internal get => this._udpClientFactory; set => this._udpClientFactory = value ?? throw new InvalidOperationException("You need to supply a valid UDP client factory method."); } private UdpClientFactoryDelegate _udpClientFactory = DCSUdpClient.CreateNew; /// /// Sets the logger implementation to use. /// To create your own logger, implement the instance. /// Defaults to built-in implementation. /// public ILoggerFactory LoggerFactory { internal get; set; } = null; /// /// Sets if the bot's status should show the mobile icon. /// Defaults to false. /// public bool MobileStatus { internal get; set; } = false; /// /// Use canary. /// Defaults to false. /// public bool UseCanary { internal get; set; } = false; /// /// Refresh full guild channel cache. /// Defaults to false. /// public bool AutoRefreshChannelCache { internal get; set; } = false; /// /// Creates a new configuration with default values. /// public DiscordConfiguration() { } /// /// Creates a clone of another discord configuration. /// /// Client configuration to clone. public DiscordConfiguration(DiscordConfiguration other) { this.Token = other.Token; this.TokenType = other.TokenType; this.MinimumLogLevel = other.MinimumLogLevel; this.UseRelativeRatelimit = other.UseRelativeRatelimit; this.LogTimestampFormat = other.LogTimestampFormat; this.LargeThreshold = other.LargeThreshold; this.AutoReconnect = other.AutoReconnect; this.ShardId = other.ShardId; this.ShardCount = other.ShardCount; this.GatewayCompressionLevel = other.GatewayCompressionLevel; this.MessageCacheSize = other.MessageCacheSize; this.WebSocketClientFactory = other.WebSocketClientFactory; this.UdpClientFactory = other.UdpClientFactory; this.Proxy = other.Proxy; this.HttpTimeout = other.HttpTimeout; this.ReconnectIndefinitely = other.ReconnectIndefinitely; this.Intents = other.Intents; this.LoggerFactory = other.LoggerFactory; this.MobileStatus = other.MobileStatus; this.UseCanary = other.UseCanary; this.AutoRefreshChannelCache = other.AutoRefreshChannelCache; + this.ApiVersion = other.ApiVersion; } } } diff --git a/DisCatSharp/Net/Abstractions/StatusUpdate.cs b/DisCatSharp/Net/Abstractions/StatusUpdate.cs index 7a8f369be..aefeb8517 100644 --- a/DisCatSharp/Net/Abstractions/StatusUpdate.cs +++ b/DisCatSharp/Net/Abstractions/StatusUpdate.cs @@ -1,78 +1,79 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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 DisCatSharp.Entities; using Newtonsoft.Json; namespace DisCatSharp.Net.Abstractions { /// /// Represents data for websocket status update payload. /// internal sealed class StatusUpdate { /// /// Gets or sets the unix millisecond timestamp of when the user went idle. /// [JsonProperty("since", NullValueHandling = NullValueHandling.Include)] public long? IdleSince { get; set; } /// /// Gets or sets whether the user is AFK. /// [JsonProperty("afk")] public bool IsAFK { get; set; } /// /// Gets or sets the status of the user. /// [JsonIgnore] public UserStatus Status { get; set; } = UserStatus.Online; /// /// Gets the status string of the user. /// [JsonProperty("status")] internal string StatusString { get { return this.Status switch { UserStatus.Online => "online", UserStatus.Idle => "idle", UserStatus.DoNotDisturb => "dnd", UserStatus.Invisible or UserStatus.Offline => "invisible", + UserStatus.Streaming => "streaming", _ => "online", }; } } /// /// Gets or sets the game the user is playing. /// [JsonProperty("game", NullValueHandling = NullValueHandling.Ignore)] public TransportActivity Activity { get; set; } internal DiscordActivity _activity; } } diff --git a/DisCatSharp/Net/Rest/DiscordApiClient.cs b/DisCatSharp/Net/Rest/DiscordApiClient.cs index e4a74b184..b85320c6e 100644 --- a/DisCatSharp/Net/Rest/DiscordApiClient.cs +++ b/DisCatSharp/Net/Rest/DiscordApiClient.cs @@ -1,4787 +1,4787 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.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 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. /// If true, post. /// A string. private static string BuildQueryString(IDictionary values, bool post = false) { if (values == null || values.Count == 0) return string.Empty; var vals_collection = values.Select(xkvp => $"{WebUtility.UrlEncode(xkvp.Key)}={WebUtility.UrlEncode(xkvp.Value)}"); var vals = string.Join("&", vals_collection); return !post ? $"?{vals}" : vals; } /// /// Prepares the message. /// /// The msg_raw. /// A DiscordMessage. private DiscordMessage PrepareMessage(JToken msg_raw) { var author = msg_raw["author"].ToObject(); var ret = msg_raw.ToDiscordObject(); ret.Discord = this.Discord; this.PopulateMessage(author, ret); var referencedMsg = msg_raw["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 ret. 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, _guild_id = guild.Id }; ret.Author = mbr; } else { ret.Author = usr; } } ret.PopulateMentions(); if (ret._reactions == null) ret._reactions = new List(); foreach (var xr in ret._reactions) 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. /// A Task. private 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. /// A Task. 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. /// A Task. 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 Guild /// /// Searches the members async. /// /// The guild_id. /// The name. /// The limit. /// A Task. internal async Task> SearchMembersAsync(ulong guild_id, string name, int? limit) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}{Endpoints.SEARCH}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); var querydict = new Dictionary { ["query"] = name, ["limit"] = limit.ToString() }; - var url = Utilities.GetApiUriFor(path, BuildQueryString(querydict), this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, BuildQueryString(querydict), this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var json = JArray.Parse(res.Response); var tms = json.ToObject>(); var mbrs = new List(); foreach (var xtm in tms) { var usr = new DiscordUser(xtm.User) { Discord = this.Discord }; this.Discord.UserCache.AddOrUpdate(xtm.User.Id, usr, (id, old) => { old.Username = usr.Username; old.Discord = usr.Discord; old.AvatarHash = usr.AvatarHash; return old; }); mbrs.Add(new DiscordMember(xtm) { Discord = this.Discord, _guild_id = guild_id }); } return mbrs; } /// /// Gets the guild ban async. /// /// The guild_id. /// The user_id. /// A Task. internal async Task GetGuildBanAsync(ulong guild_id, ulong user_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.BANS}/:user_id"; - var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id, user_id}, out var url); - var uri = Utilities.GetApiUriFor(url, this.Discord.Configuration.UseCanary); + var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id, user_id}, out var path); + var uri = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, uri, RestRequestMethod.GET, route).ConfigureAwait(false); var json = JObject.Parse(res.Response); var ban = json.ToObject(); return ban; } /// /// Creates the guild async. /// /// The name. /// The region_id. /// The iconb64. /// The verification_level. /// The default_message_notifications. /// The system_channel_flags. internal async Task CreateGuildAsync(string name, string region_id, Optional iconb64, VerificationLevel? verification_level, DefaultMessageNotifications? default_message_notifications, SystemChannelFlags? system_channel_flags) { var pld = new RestGuildCreatePayload { Name = name, RegionId = region_id, DefaultMessageNotifications = default_message_notifications, VerificationLevel = verification_level, IconBase64 = iconb64, SystemChannelFlags = system_channel_flags }; var route = $"{Endpoints.GUILDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var json = JObject.Parse(res.Response); var raw_members = (JArray)json["members"]; var guild = json.ToDiscordObject(); if (this.Discord is DiscordClient dc) await dc.OnGuildCreateEventAsync(guild, raw_members, null).ConfigureAwait(false); return guild; } /// /// Creates the guild from template async. /// /// The template_code. /// The name. /// The iconb64. internal async Task CreateGuildFromTemplateAsync(string template_code, string name, Optional iconb64) { var pld = new RestGuildCreateFromTemplatePayload { Name = name, IconBase64 = iconb64 }; var route = $"{Endpoints.GUILDS}{Endpoints.TEMPLATES}/:template_code"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { template_code }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var json = JObject.Parse(res.Response); var raw_members = (JArray)json["members"]; var guild = json.ToDiscordObject(); if (this.Discord is DiscordClient dc) await dc.OnGuildCreateEventAsync(guild, raw_members, null).ConfigureAwait(false); return guild; } /// /// Deletes the guild async. /// /// The guild_id. internal async Task DeleteGuildAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route).ConfigureAwait(false); if (this.Discord is DiscordClient dc) { var gld = dc._guilds[guild_id]; await dc.OnGuildDeleteEventAsync(gld).ConfigureAwait(false); } } /// /// Modifies the guild. /// /// The guild id. /// The name. /// The region. /// The verification level. /// The default message notifications. /// The mfa level. /// The explicit content filter. /// The afk channel id. /// The afk timeout. /// The iconb64. /// The owner id. /// The splashb64. /// The system channel id. /// The system channel flags. /// The public updates channel id. /// The rules channel id. /// The description. /// The banner base64. /// The discovery base64. /// The preferred locale. /// The reason. internal async Task ModifyGuildAsync(ulong guildId, Optional name, Optional region, Optional verificationLevel, Optional defaultMessageNotifications, Optional mfaLevel, Optional explicitContentFilter, Optional afkChannelId, Optional afkTimeout, Optional iconb64, Optional ownerId, Optional splashb64, Optional systemChannelId, Optional systemChannelFlags, Optional publicUpdatesChannelId, Optional rulesChannelId, Optional description, Optional bannerb64, Optional discorverySplashb64, Optional preferredLocale, string reason) { var pld = new RestGuildModifyPayload { Name = name, RegionId = region, VerificationLevel = verificationLevel, DefaultMessageNotifications = defaultMessageNotifications, MfaLevel = mfaLevel, ExplicitContentFilter = explicitContentFilter, AfkChannelId = afkChannelId, AfkTimeout = afkTimeout, IconBase64 = iconb64, SplashBase64 = splashb64, BannerBase64 = bannerb64, DiscoverySplashBase64 = discorverySplashb64, OwnerId = ownerId, SystemChannelId = systemChannelId, SystemChannelFlags = systemChannelFlags, RulesChannelId = rulesChannelId, PublicUpdatesChannelId = publicUpdatesChannelId, PreferredLocale = preferredLocale, Description = description }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id = guildId }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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 json = JObject.Parse(res.Response); var rawMembers = (JArray)json["members"]; var guild = json.ToDiscordObject(); foreach (var r in guild._roles.Values) r._guild_id = guild.Id; if (this.Discord is DiscordClient dc) await dc.OnGuildUpdateEventAsync(guild, rawMembers).ConfigureAwait(false); return guild; } /// /// Modifies the guild community settings. /// /// The guild id. /// The guild features. /// The rules channel id. /// The public updates channel id. /// The preferred locale. /// The description. /// The default message notifications. /// The explicit content filter. /// The verification level. /// The reason. internal async Task ModifyGuildCommunitySettingsAsync(ulong guildId, List features, Optional rulesChannelId, Optional publicUpdatesChannelId, string preferredLocale, string description, DefaultMessageNotifications defaultMessageNotifications, ExplicitContentFilter explicitContentFilter, VerificationLevel verificationLevel, string reason) { var pld = new RestGuildCommunityModifyPayload { VerificationLevel = verificationLevel, DefaultMessageNotifications = defaultMessageNotifications, ExplicitContentFilter = explicitContentFilter, RulesChannelId = rulesChannelId, PublicUpdatesChannelId = publicUpdatesChannelId, PreferredLocale = preferredLocale, Description = description ?? Optional.FromNoValue(), Features = features }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id = guildId }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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 json = JObject.Parse(res.Response); var rawMembers = (JArray)json["members"]; var guild = json.ToDiscordObject(); foreach (var r in guild._roles.Values) r._guild_id = guild.Id; if (this.Discord is DiscordClient dc) await dc.OnGuildUpdateEventAsync(guild, rawMembers).ConfigureAwait(false); return guild; } /// /// Gets the guild bans async. /// /// The guild_id. /// A Task. internal async Task> GetGuildBansAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.BANS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var bans_raw = JsonConvert.DeserializeObject>(res.Response).Select(xb => { if (!this.Discord.TryGetCachedUserInternal(xb.RawUser.Id, out var usr)) { usr = new DiscordUser(xb.RawUser) { Discord = this.Discord }; usr = this.Discord.UserCache.AddOrUpdate(usr.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); } xb.User = usr; return xb; }); var bans = new ReadOnlyCollection(new List(bans_raw)); return bans; } /// /// Creates the guild ban async. /// /// The guild_id. /// The user_id. /// The delete_message_days. /// The reason. /// A Task. internal Task CreateGuildBanAsync(ulong guild_id, ulong user_id, int delete_message_days, string reason) { if (delete_message_days < 0 || delete_message_days > 7) throw new ArgumentException("Delete message days must be a number between 0 and 7.", nameof(delete_message_days)); var urlparams = new Dictionary { ["delete_message_days"] = delete_message_days.ToString(CultureInfo.InvariantCulture) }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.BANS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { guild_id, user_id }, out var path); - var url = Utilities.GetApiUriFor(path, BuildQueryString(urlparams), this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, BuildQueryString(urlparams), this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, headers); } /// /// Removes the guild ban async. /// /// The guild_id. /// The user_id. /// The reason. /// A Task. internal Task RemoveGuildBanAsync(ulong guild_id, ulong user_id, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.BANS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id, user_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Leaves the guild async. /// /// The guild_id. /// A Task. internal Task LeaveGuildAsync(ulong guild_id) { var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.GUILDS}/:guild_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } /// /// Adds the guild member async. /// /// The guild_id. /// The user_id. /// The access_token. /// The nick. /// The roles. /// If true, muted. /// If true, deafened. /// A Task. internal async Task AddGuildMemberAsync(ulong guild_id, ulong user_id, string access_token, string nick, IEnumerable roles, bool muted, bool deafened) { var pld = new RestGuildMemberAddPayload { AccessToken = access_token, Nickname = nick ?? "", Roles = roles ?? new List(), Deaf = deafened, Mute = muted }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { guild_id, user_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var tm = JsonConvert.DeserializeObject(res.Response); return new DiscordMember(tm) { Discord = this.Discord, _guild_id = guild_id }; } /// /// Lists the guild members async. /// /// The guild_id. /// The limit. /// The after. /// A Task. internal async Task> ListGuildMembersAsync(ulong guild_id, int? limit, ulong? after) { var urlparams = new Dictionary(); if (limit != null && limit > 0) urlparams["limit"] = limit.Value.ToString(CultureInfo.InvariantCulture); if (after != null) urlparams["after"] = after.Value.ToString(CultureInfo.InvariantCulture); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); - var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var members_raw = JsonConvert.DeserializeObject>(res.Response); return new ReadOnlyCollection(members_raw); } /// /// Adds the guild member role async. /// /// The guild_id. /// The user_id. /// The role_id. /// The reason. /// A Task. internal Task AddGuildMemberRoleAsync(ulong guild_id, ulong user_id, ulong role_id, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}/:user_id{Endpoints.ROLES}/:role_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { guild_id, user_id, role_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, headers); } /// /// Removes the guild member role async. /// /// The guild_id. /// The user_id. /// The role_id. /// The reason. /// A Task. internal Task RemoveGuildMemberRoleAsync(ulong guild_id, ulong user_id, ulong role_id, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}/:user_id{Endpoints.ROLES}/:role_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id, user_id, role_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Modifies the guild channel position async. /// /// The guild_id. /// The pld. /// The reason. /// A Task. internal Task ModifyGuildChannelPositionAsync(ulong guild_id, IEnumerable pld, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.CHANNELS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Modifies the guild channel parent async. /// /// The guild_id. /// The pld. /// The reason. /// A Task. internal Task ModifyGuildChannelParentAsync(ulong guild_id, IEnumerable pld, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.CHANNELS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Detaches the guild channel parent async. /// /// The guild_id. /// The pld. /// The reason. /// A Task. internal Task DetachGuildChannelParentAsync(ulong guild_id, IEnumerable pld, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.CHANNELS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Modifies the guild role position async. /// /// The guild_id. /// The pld. /// The reason. /// A Task. internal Task ModifyGuildRolePositionAsync(ulong guild_id, IEnumerable pld, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.ROLES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Gets the audit logs async. /// /// The guild_id. /// The limit. /// The after. /// The before. /// The responsible. /// The action_type. /// A Task. internal async Task GetAuditLogsAsync(ulong guild_id, int limit, ulong? after, ulong? before, ulong? responsible, int? action_type) { var urlparams = new Dictionary { ["limit"] = limit.ToString(CultureInfo.InvariantCulture) }; if (after != null) urlparams["after"] = after?.ToString(CultureInfo.InvariantCulture); if (before != null) urlparams["before"] = before?.ToString(CultureInfo.InvariantCulture); if (responsible != null) urlparams["user_id"] = responsible?.ToString(CultureInfo.InvariantCulture); if (action_type != null) urlparams["action_type"] = action_type?.ToString(CultureInfo.InvariantCulture); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.AUDIT_LOGS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); - var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var audit_log_data_raw = JsonConvert.DeserializeObject(res.Response); return audit_log_data_raw; } /// /// Gets the guild vanity url async. /// /// The guild_id. /// A Task. internal async Task GetGuildVanityUrlAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.VANITY_URL}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var invite = JsonConvert.DeserializeObject(res.Response); return invite; } /// /// Gets the guild widget async. /// /// The guild_id. /// A Task. internal async Task GetGuildWidgetAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WIDGET_JSON}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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 rawChannels = (JArray)json["channels"]; var ret = json.ToDiscordObject(); ret.Discord = this.Discord; ret.Guild = this.Discord.Guilds[guild_id]; ret.Channels = ret.Guild == null ? rawChannels.Select(r => new DiscordChannel { Id = (ulong)r["id"], Name = r["name"].ToString(), Position = (int)r["position"] }).ToList() : rawChannels.Select(r => { var c = ret.Guild.GetChannel((ulong)r["id"]); c.Position = (int)r["position"]; return c; }).ToList(); return ret; } /// /// Gets the guild widget settings async. /// /// The guild_id. /// A Task. internal async Task GetGuildWidgetSettingsAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WIDGET}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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.Guild = this.Discord.Guilds[guild_id]; return ret; } /// /// Modifies the guild widget settings async. /// /// The guild_id. /// If true, is enabled. /// The channel id. /// The reason. /// A Task. internal async Task ModifyGuildWidgetSettingsAsync(ulong guild_id, bool? isEnabled, ulong? channelId, string reason) { var pld = new RestGuildWidgetSettingsPayload { Enabled = isEnabled, ChannelId = channelId }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WIDGET}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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.Guild = this.Discord.Guilds[guild_id]; return ret; } /// /// Gets the guild templates async. /// /// The guild_id. /// A Task. internal async Task> GetGuildTemplatesAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.TEMPLATES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var templates_raw = JsonConvert.DeserializeObject>(res.Response); return new ReadOnlyCollection(new List(templates_raw)); } /// /// Creates the guild template async. /// /// The guild_id. /// The name. /// The description. /// A Task. internal async Task CreateGuildTemplateAsync(ulong guild_id, string name, string description) { var pld = new RestGuildTemplateCreateOrModifyPayload { Name = name, Description = description }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.TEMPLATES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { guild_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); return ret; } /// /// Syncs the guild template async. /// /// The guild_id. /// The template_code. /// A Task. internal async Task SyncGuildTemplateAsync(ulong guild_id, string template_code) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.TEMPLATES}/:template_code"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { guild_id, template_code }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route).ConfigureAwait(false); var template_raw = JsonConvert.DeserializeObject(res.Response); return template_raw; } /// /// Modifies the guild template async. /// /// The guild_id. /// The template_code. /// The name. /// The description. /// A Task. internal async Task ModifyGuildTemplateAsync(ulong guild_id, string template_code, string name, string description) { var pld = new RestGuildTemplateCreateOrModifyPayload { Name = name, Description = description }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.TEMPLATES}/:template_code"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id, template_code }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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 template_raw = JsonConvert.DeserializeObject(res.Response); return template_raw; } /// /// Deletes the guild template async. /// /// The guild_id. /// The template_code. /// A Task. internal async Task DeleteGuildTemplateAsync(ulong guild_id, string template_code) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.TEMPLATES}/:template_code"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id, template_code }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route).ConfigureAwait(false); var template_raw = JsonConvert.DeserializeObject(res.Response); return template_raw; } /// /// Gets the guild membership screening form async. /// /// The guild_id. /// A Task. internal async Task GetGuildMembershipScreeningFormAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBER_VERIFICATION}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var screening_raw = JsonConvert.DeserializeObject(res.Response); return screening_raw; } /// /// Modifies the guild membership screening form async. /// /// The guild_id. /// The enabled. /// The fields. /// The description. /// A Task. internal async Task ModifyGuildMembershipScreeningFormAsync(ulong guild_id, Optional enabled, Optional fields, Optional description) { var pld = new RestGuildMembershipScreeningFormModifyPayload { Enabled = enabled, Description = description, Fields = fields }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBER_VERIFICATION}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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 screening_raw = JsonConvert.DeserializeObject(res.Response); return screening_raw; } /// /// Gets the guild welcome screen async. /// /// The guild_id. /// A Task. internal async Task GetGuildWelcomeScreenAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WELCOME_SCREEN}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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); return ret; } /// /// Modifies the guild welcome screen async. /// /// The guild_id. /// The enabled. /// The welcome channels. /// The description. /// A Task. internal async Task ModifyGuildWelcomeScreenAsync(ulong guild_id, Optional enabled, Optional> welcomeChannels, Optional description) { var pld = new RestGuildWelcomeScreenModifyPayload { Enabled = enabled, WelcomeChannels = welcomeChannels, Description = description }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WELCOME_SCREEN}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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); return ret; } /// /// Updates the current user voice state async. /// /// The guild_id. /// The channel id. /// If true, suppress. /// The request to speak timestamp. /// A Task. internal async Task UpdateCurrentUserVoiceStateAsync(ulong guild_id, ulong channelId, bool? suppress, DateTimeOffset? requestToSpeakTimestamp) { var pld = new RestGuildUpdateCurrentUserVoiceStatePayload { ChannelId = channelId, Suppress = suppress, RequestToSpeakTimestamp = requestToSpeakTimestamp }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.VOICE_STATES}/@me"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)); } /// /// Updates the user voice state async. /// /// The guild_id. /// The user_id. /// The channel id. /// If true, suppress. /// A Task. internal async Task UpdateUserVoiceStateAsync(ulong guild_id, ulong user_id, ulong channelId, bool? suppress) { var pld = new RestGuildUpdateUserVoiceStatePayload { ChannelId = channelId, Suppress = suppress }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.VOICE_STATES}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id, user_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)); } #endregion #region Channel /// /// Creates the guild channel async. /// /// The guild_id. /// The name. /// The type. /// The parent. /// The topic. /// The bitrate. /// The user_limit. /// The overwrites. /// If true, nsfw. /// The per user rate limit. /// The quality mode. /// The reason. /// A Task. internal async Task CreateGuildChannelAsync(ulong guild_id, string name, ChannelType type, ulong? parent, Optional topic, int? bitrate, int? user_limit, IEnumerable overwrites, bool? nsfw, Optional perUserRateLimit, VideoQualityMode? qualityMode, string reason) { var restoverwrites = new List(); if (overwrites != null) foreach (var ow in overwrites) restoverwrites.Add(ow.Build()); var pld = new RestChannelCreatePayload { Name = name, Type = type, Parent = parent, Topic = topic, Bitrate = bitrate, UserLimit = user_limit, PermissionOverwrites = restoverwrites, Nsfw = nsfw, PerUserRateLimit = perUserRateLimit, QualityMode = qualityMode }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.CHANNELS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { guild_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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; foreach (var xo in ret._permissionOverwrites) { xo.Discord = this.Discord; xo._channel_id = ret.Id; } return ret; } /// /// Modifies the channel async. /// /// The channel_id. /// The name. /// The position. /// The topic. /// If true, nsfw. /// The parent. /// The bitrate. /// The user_limit. /// The per user rate limit. /// The rtc region. /// The quality mode. /// The default auto archive duration. /// The type. /// The permission overwrites /// The reason. internal Task ModifyChannelAsync(ulong channel_id, string name, int? position, Optional topic, bool? nsfw, Optional parent, int? bitrate, int? user_limit, Optional perUserRateLimit, Optional rtcRegion, VideoQualityMode? qualityMode, ThreadAutoArchiveDuration? autoArchiveDuration, Optional type, IEnumerable permissionOverwrites, string reason) { List restoverwrites = null; if (permissionOverwrites != null) { restoverwrites = new List(); foreach (var ow in permissionOverwrites) restoverwrites.Add(ow.Build()); } var pld = new RestChannelModifyPayload { Name = name, Position = position, Topic = topic, Nsfw = nsfw, Parent = parent, Bitrate = bitrate, UserLimit = user_limit, PerUserRateLimit = perUserRateLimit, RtcRegion = rtcRegion, QualityMode = qualityMode, DefaultAutoArchiveDuration = autoArchiveDuration, Type = type, PermissionOverwrites = restoverwrites }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { channel_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Gets the channel async. /// /// The channel_id. /// A Task. internal async Task GetChannelAsync(ulong channel_id) { var route = $"{Endpoints.CHANNELS}/:channel_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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; foreach (var xo in ret._permissionOverwrites) { xo.Discord = this.Discord; xo._channel_id = ret.Id; } return ret; } /// /// Deletes the channel async. /// /// The channel_id. /// The reason. /// A Task. internal Task DeleteChannelAsync(ulong channel_id, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Gets the message async. /// /// The channel_id. /// The message_id. /// A Task. internal async Task GetMessageAsync(ulong channel_id, ulong message_id) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id, message_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = this.PrepareMessage(JObject.Parse(res.Response)); return ret; } /// /// Creates the message async. /// /// The channel_id. /// The content. /// The embeds. /// The sticker. /// The reply message id. /// If true, mention reply. /// If true, fail on invalid reply. /// A Task. internal async Task CreateMessageAsync(ulong channel_id, string content, IEnumerable embeds, DiscordSticker sticker, ulong? replyMessageId, bool mentionReply, bool failOnInvalidReply) { if (content != null && content.Length > 2000) throw new ArgumentException("Message content length cannot exceed 2000 characters."); if (!embeds?.Any() ?? true) { if (content == null && sticker == null) throw new ArgumentException("You must specify message content, a sticker or an embed."); if (content.Length == 0) throw new ArgumentException("Message content must not be empty."); } if (embeds != null) foreach (var embed in embeds) if (embed.Timestamp != null) embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); var pld = new RestChannelMessageCreatePayload { HasContent = content != null, Content = content, StickersIds = sticker is null ? Array.Empty() : new[] {sticker.Id}, IsTTS = false, HasEmbed = embeds?.Any() ?? false, Embeds = embeds }; if (replyMessageId != null) pld.MessageReference = new InternalDiscordMessageReference { MessageId = replyMessageId, FailIfNotExists = failOnInvalidReply }; if (replyMessageId != null) pld.Mentions = new DiscordMentions(Mentions.All, true, mentionReply); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = this.PrepareMessage(JObject.Parse(res.Response)); return ret; } /// /// Creates the message async. /// /// The channel_id. /// The builder. /// A Task. internal async Task CreateMessageAsync(ulong channel_id, DiscordMessageBuilder builder) { builder.Validate(); if (builder.Embeds != null) foreach (var embed in builder.Embeds) if (embed?.Timestamp != null) embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); var pld = new RestChannelMessageCreatePayload { HasContent = builder.Content != null, Content = builder.Content, StickersIds = builder.Sticker is null ? Array.Empty() : new[] {builder.Sticker.Id}, IsTTS = builder.IsTTS, HasEmbed = builder.Embeds != null, Embeds = builder.Embeds, Components = builder.Components }; if (builder.ReplyId != null) pld.MessageReference = new InternalDiscordMessageReference { MessageId = builder.ReplyId, FailIfNotExists = builder.FailOnInvalidReply }; pld.Mentions = new DiscordMentions(builder.Mentions ?? Mentions.All, builder.Mentions?.Any() ?? false, builder.MentionOnReply); if (builder.Files.Count == 0) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = this.PrepareMessage(JObject.Parse(res.Response)); return ret; } else { var values = new Dictionary { ["payload_json"] = DiscordJson.SerializeObject(pld) }; var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoMultipartAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, values: values, files: builder.Files).ConfigureAwait(false); var ret = this.PrepareMessage(JObject.Parse(res.Response)); foreach (var file in builder._files.Where(x => x.ResetPositionTo.HasValue)) { file.Stream.Position = file.ResetPositionTo.Value; } return ret; } } /// /// Gets the guild channels async. /// /// The guild_id. /// A Task. internal async Task> GetGuildChannelsAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.CHANNELS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var channels_raw = JsonConvert.DeserializeObject>(res.Response).Select(xc => { xc.Discord = this.Discord; return xc; }); foreach (var ret in channels_raw) foreach (var xo in ret._permissionOverwrites) { xo.Discord = this.Discord; xo._channel_id = ret.Id; } return new ReadOnlyCollection(new List(channels_raw)); } /// /// Creates the stage instance async. /// /// The channel_id. /// The topic. /// Whether everyone should be notified about the stage. /// The privacy_level. /// The reason. internal async Task CreateStageInstanceAsync(ulong channel_id, string topic, bool send_start_notification, StagePrivacyLevel privacy_level, string reason) { var pld = new RestStageInstanceCreatePayload { ChannelId = channel_id, Topic = topic, PrivacyLevel = privacy_level, SendStartNotification = send_start_notification }; var route = $"{Endpoints.STAGE_INSTANCES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { }, out var path); var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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 stageInstance = JsonConvert.DeserializeObject(res.Response); return stageInstance; } /// /// Gets the stage instance async. /// /// The channel_id. internal async Task GetStageInstanceAsync(ulong channel_id) { var route = $"{Endpoints.STAGE_INSTANCES}/:channel_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var stageInstance = JsonConvert.DeserializeObject(res.Response); return stageInstance; } /// /// Modifies the stage instance async. /// /// The channel_id. /// The topic. /// The privacy_level. /// The reason. internal Task ModifyStageInstanceAsync(ulong channel_id, Optional topic, Optional privacy_level, string reason) { var pld = new RestStageInstanceModifyPayload { Topic = topic, PrivacyLevel = privacy_level }; var route = $"{Endpoints.STAGE_INSTANCES}/:channel_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { channel_id }, out var path); var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Deletes the stage instance async. /// /// The channel_id. /// The reason. internal Task DeleteStageInstanceAsync(ulong channel_id, string reason) { var route = $"{Endpoints.STAGE_INSTANCES}/:channel_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id }, out var path); var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Gets the channel messages async. /// /// The channel id. /// The limit. /// The before. /// The after. /// The around. /// A Task. internal async Task> GetChannelMessagesAsync(ulong channel_id, int limit, ulong? before, ulong? after, ulong? around) { var urlparams = new Dictionary(); if (around != null) urlparams["around"] = around?.ToString(CultureInfo.InvariantCulture); if (before != null) urlparams["before"] = before?.ToString(CultureInfo.InvariantCulture); if (after != null) urlparams["after"] = after?.ToString(CultureInfo.InvariantCulture); if (limit > 0) urlparams["limit"] = limit.ToString(CultureInfo.InvariantCulture); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id }, out var path); - var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var msgs_raw = JArray.Parse(res.Response); var msgs = new List(); foreach (var xj in msgs_raw) msgs.Add(this.PrepareMessage(xj)); return new ReadOnlyCollection(new List(msgs)); } /// /// Gets the channel message async. /// /// The channel_id. /// The message_id. /// A Task. internal async Task GetChannelMessageAsync(ulong channel_id, ulong message_id) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id, message_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = this.PrepareMessage(JObject.Parse(res.Response)); return ret; } /// /// Edits the message async. /// /// The channel_id. /// The message_id. /// The content. /// The embeds. /// The mentions. /// The components. /// The suppress_embed. /// The files. /// A Task. internal async Task EditMessageAsync(ulong channel_id, ulong message_id, Optional content, Optional> embeds, IEnumerable mentions, IReadOnlyList components, Optional suppress_embed, IReadOnlyCollection files) { if (embeds.HasValue && embeds.Value != null) foreach (var embed in embeds.Value) if (embed.Timestamp != null) embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); var pld = new RestChannelMessageEditPayload { HasContent = content.HasValue, Content = content.HasValue ? (string)content : null, HasEmbed = embeds.HasValue && (embeds.Value?.Any() ?? false), Embeds = embeds.HasValue && (embeds.Value?.Any() ?? false) ? embeds.Value : null, Components = components, Flags = suppress_embed.HasValue ? (bool)suppress_embed ? MessageFlags.SuppressedEmbeds : null : null }; pld.Mentions = new DiscordMentions(mentions ?? Mentions.None, false, mentions?.OfType().Any() ?? false); var values = new Dictionary { ["payload_json"] = DiscordJson.SerializeObject(pld) }; var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { channel_id, message_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoMultipartAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, values: values, files: files).ConfigureAwait(false); var ret = this.PrepareMessage(JObject.Parse(res.Response)); foreach (var file in files.Where(x => x.ResetPositionTo.HasValue)) { file.Stream.Position = file.ResetPositionTo.Value; } return ret; } /// /// Deletes the message async. /// /// The channel_id. /// The message_id. /// The reason. /// A Task. internal Task DeleteMessageAsync(ulong channel_id, ulong message_id, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id, message_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Deletes the messages async. /// /// The channel_id. /// The message_ids. /// The reason. /// A Task. internal Task DeleteMessagesAsync(ulong channel_id, IEnumerable message_ids, string reason) { var pld = new RestChannelMessageBulkDeletePayload { Messages = message_ids }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}{Endpoints.BULK_DELETE}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Gets the channel invites async. /// /// The channel_id. /// A Task. internal async Task> GetChannelInvitesAsync(ulong channel_id) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.INVITES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var invites_raw = JsonConvert.DeserializeObject>(res.Response).Select(xi => { xi.Discord = this.Discord; return xi; }); return new ReadOnlyCollection(new List(invites_raw)); } /// /// Creates the channel invite async. /// /// The channel_id. /// The max_age. /// The max_uses. /// The target_type. /// The target_application. /// If true, temporary. /// If true, unique. /// The reason. /// A Task. internal async Task CreateChannelInviteAsync(ulong channel_id, int max_age, int max_uses, TargetType? target_type, TargetActivity? target_application, bool temporary, bool unique, string reason) { var pld = new RestChannelInviteCreatePayload { MaxAge = max_age, MaxUses = max_uses, TargetType = target_type, TargetApplication = target_application, Temporary = temporary, Unique = unique }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.INVITES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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; return ret; } /// /// Deletes the channel permission async. /// /// The channel_id. /// The overwrite_id. /// The reason. /// A Task. internal Task DeleteChannelPermissionAsync(ulong channel_id, ulong overwrite_id, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.PERMISSIONS}/:overwrite_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id, overwrite_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Edits the channel permissions async. /// /// The channel_id. /// The overwrite_id. /// The allow. /// The deny. /// The type. /// The reason. /// A Task. internal Task EditChannelPermissionsAsync(ulong channel_id, ulong overwrite_id, Permissions allow, Permissions deny, string type, string reason) { var pld = new RestChannelPermissionEditPayload { Type = type, Allow = allow & PermissionMethods.FULL_PERMS, Deny = deny & PermissionMethods.FULL_PERMS }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.PERMISSIONS}/:overwrite_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { channel_id, overwrite_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Triggers the typing async. /// /// The channel_id. /// A Task. internal Task TriggerTypingAsync(ulong channel_id) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.TYPING}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route); } /// /// Gets the pinned messages async. /// /// The channel_id. /// A Task. internal async Task> GetPinnedMessagesAsync(ulong channel_id) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.PINS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var msgs_raw = JArray.Parse(res.Response); var msgs = new List(); foreach (var xj in msgs_raw) msgs.Add(this.PrepareMessage(xj)); return new ReadOnlyCollection(new List(msgs)); } /// /// Pins the message async. /// /// The channel_id. /// The message_id. /// A Task. internal Task PinMessageAsync(ulong channel_id, ulong message_id) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.PINS}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { channel_id, message_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route); } /// /// Unpins the message async. /// /// The channel_id. /// The message_id. /// A Task. internal Task UnpinMessageAsync(ulong channel_id, ulong message_id) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.PINS}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id, message_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } /// /// Adds the group dm recipient async. /// /// The channel_id. /// The user_id. /// The access_token. /// The nickname. /// A Task. internal Task AddGroupDmRecipientAsync(ulong channel_id, ulong user_id, string access_token, string nickname) { var pld = new RestChannelGroupDmRecipientAddPayload { AccessToken = access_token, Nickname = nickname }; var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.CHANNELS}/:channel_id{Endpoints.RECIPIENTS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { channel_id, user_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, payload: DiscordJson.SerializeObject(pld)); } /// /// Removes the group dm recipient async. /// /// The channel_id. /// The user_id. /// A Task. internal Task RemoveGroupDmRecipientAsync(ulong channel_id, ulong user_id) { var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.CHANNELS}/:channel_id{Endpoints.RECIPIENTS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id, user_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } /// /// Creates the group dm async. /// /// The access_tokens. /// The nicks. /// A Task. internal async Task CreateGroupDmAsync(IEnumerable access_tokens, IDictionary nicks) { var pld = new RestUserGroupDmCreatePayload { AccessTokens = access_tokens, Nicknames = nicks }; var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.CHANNELS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Creates the dm async. /// /// The recipient_id. /// A Task. internal async Task CreateDmAsync(ulong recipient_id) { var pld = new RestUserDmCreatePayload { Recipient = recipient_id }; var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.CHANNELS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Follows the channel async. /// /// The channel_id. /// The webhook_channel_id. /// A Task. internal async Task FollowChannelAsync(ulong channel_id, ulong webhook_channel_id) { var pld = new FollowedChannelAddPayload { WebhookChannelId = webhook_channel_id }; var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.FOLLOWERS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var response = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); return JsonConvert.DeserializeObject(response.Response); } /// /// Crossposts the message async. /// /// The channel_id. /// The message_id. /// A Task. internal async Task CrosspostMessageAsync(ulong channel_id, ulong message_id) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.CROSSPOST}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id, message_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var response = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route).ConfigureAwait(false); return JsonConvert.DeserializeObject(response.Response); } #endregion #region Member /// /// Gets the current user async. /// /// A Task. internal Task GetCurrentUserAsync() => this.GetUserAsync("@me"); /// /// Gets the user async. /// /// The user_id. /// A Task. internal Task GetUserAsync(ulong user_id) => this.GetUserAsync(user_id.ToString(CultureInfo.InvariantCulture)); /// /// Gets the user async. /// /// The user_id. /// A Task. internal async Task GetUserAsync(string user_id) { var route = $"{Endpoints.USERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { user_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var user_raw = JsonConvert.DeserializeObject(res.Response); var duser = new DiscordUser(user_raw) { Discord = this.Discord }; return duser; } /// /// Gets the guild member async. /// /// The guild_id. /// The user_id. /// A Task. internal async Task GetGuildMemberAsync(ulong guild_id, ulong user_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id, user_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var tm = JsonConvert.DeserializeObject(res.Response); var usr = new DiscordUser(tm.User) { Discord = this.Discord }; usr = this.Discord.UserCache.AddOrUpdate(tm.User.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); return new DiscordMember(tm) { Discord = this.Discord, _guild_id = guild_id }; } /// /// Removes the guild member async. /// /// The guild_id. /// The user_id. /// The reason. /// A Task. internal Task RemoveGuildMemberAsync(ulong guild_id, ulong user_id, string reason) { var urlparams = new Dictionary(); if (reason != null) urlparams["reason"] = reason; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id, user_id }, out var path); - var url = Utilities.GetApiUriFor(path, BuildQueryString(urlparams), this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, BuildQueryString(urlparams), this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } /// /// Modifies the current user async. /// /// The username. /// The base64_avatar. /// A Task. internal async Task ModifyCurrentUserAsync(string username, Optional base64_avatar) { var pld = new RestUserUpdateCurrentPayload { Username = username, AvatarBase64 = base64_avatar.HasValue ? base64_avatar.Value : null, AvatarSet = base64_avatar.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.UseCanary); + 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 user_raw = JsonConvert.DeserializeObject(res.Response); return user_raw; } /// /// Gets the current user guilds async. /// /// The limit. /// The before. /// The after. /// A Task. 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.UseCanary) + 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 guilds_raw = JsonConvert.DeserializeObject>(res.Response); var glds = guilds_raw.Select(xug => (this.Discord as DiscordClient)?._guilds[xug.Id]); return new ReadOnlyCollection(new List(glds)); } else { return new ReadOnlyCollection(JsonConvert.DeserializeObject>(res.Response)); } } /// /// Modifies the guild member async. /// /// The guild_id. /// The user_id. /// The nick. /// The role_ids. /// The mute. /// The deaf. /// The voice_channel_id. /// The reason. /// A Task. internal Task ModifyGuildMemberAsync(ulong guild_id, ulong user_id, Optional nick, Optional> role_ids, Optional mute, Optional deaf, Optional voice_channel_id, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var pld = new RestGuildMemberModifyPayload { Nickname = nick, RoleIds = role_ids, Deafen = deaf, Mute = mute, VoiceChannelId = voice_channel_id }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id, user_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, payload: DiscordJson.SerializeObject(pld)); } /// /// Modifies the current member nickname async. /// /// The guild_id. /// The nick. /// The reason. /// A Task. internal Task ModifyCurrentMemberNicknameAsync(ulong guild_id, string nick, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var pld = new RestGuildMemberModifyPayload { Nickname = nick }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}{Endpoints.ME}{Endpoints.NICK}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, payload: DiscordJson.SerializeObject(pld)); } #endregion #region Roles /// /// Gets the guild roles async. /// /// The guild_id. /// A Task. internal async Task> GetGuildRolesAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.ROLES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var roles_raw = JsonConvert.DeserializeObject>(res.Response).Select(xr => { xr.Discord = this.Discord; xr._guild_id = guild_id; return xr; }); return new ReadOnlyCollection(new List(roles_raw)); } /// /// Gets the guild async. /// /// The guild id. /// If true, with_counts. /// A Task. internal async Task GetGuildAsync(ulong guildId, bool? with_counts) { var urlparams = new Dictionary(); if (with_counts.HasValue) urlparams["with_counts"] = with_counts?.ToString(); var route = $"{Endpoints.GUILDS}/:guild_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id = guildId }, out var path); - var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route, urlparams).ConfigureAwait(false); var json = JObject.Parse(res.Response); var rawMembers = (JArray)json["members"]; var guildRest = json.ToDiscordObject(); foreach (var r in guildRest._roles.Values) r._guild_id = guildRest.Id; if (this.Discord is DiscordClient dc) { await dc.OnGuildUpdateEventAsync(guildRest, rawMembers).ConfigureAwait(false); return dc._guilds[guildRest.Id]; } else { guildRest.Discord = this.Discord; return guildRest; } } /// /// Modifies the guild role async. /// /// The guild_id. /// The role_id. /// The name. /// The permissions. /// The color. /// If true, hoist. /// If true, mentionable. /// The icon. /// The unicode emoji icon. /// The reason. internal async Task ModifyGuildRoleAsync(ulong guild_id, ulong role_id, string name, Permissions? permissions, int? color, bool? hoist, bool? mentionable, Optional iconb64, Optional emoji, string reason) { var pld = new RestGuildRolePayload { Name = name, Permissions = permissions & PermissionMethods.FULL_PERMS, Color = color, Hoist = hoist, Mentionable = mentionable, }; if (emoji.HasValue && !iconb64.HasValue) pld.UnicodeEmoji = emoji; if (emoji.HasValue && iconb64.HasValue) { pld.IconBase64 = null; pld.UnicodeEmoji = emoji; } if (iconb64.HasValue) pld.IconBase64 = iconb64; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.ROLES}/:role_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id, role_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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._guild_id = guild_id; return ret; } /// /// Deletes the role async. /// /// The guild_id. /// The role_id. /// The reason. /// A Task. internal Task DeleteRoleAsync(ulong guild_id, ulong role_id, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.ROLES}/:role_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id, role_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Creates the guild role async. /// /// The guild_id. /// The name. /// The permissions. /// The color. /// If true, hoist. /// If true, mentionable. /// The reason. /// A Task. internal async Task CreateGuildRoleAsync(ulong guild_id, string name, Permissions? permissions, int? color, bool? hoist, bool? mentionable, string reason) { var pld = new RestGuildRolePayload { Name = name, Permissions = permissions & PermissionMethods.FULL_PERMS, Color = color, Hoist = hoist, Mentionable = mentionable }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.ROLES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { guild_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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._guild_id = guild_id; return ret; } #endregion #region Prune /// /// Gets the guild prune count async. /// /// The guild_id. /// The days. /// The include_roles. /// A Task. internal async Task GetGuildPruneCountAsync(ulong guild_id, int days, IEnumerable include_roles) { if (days < 0 || days > 30) throw new ArgumentException("Prune inactivity days must be a number between 0 and 30.", nameof(days)); var urlparams = new Dictionary { ["days"] = days.ToString(CultureInfo.InvariantCulture) }; var sb = new StringBuilder(); if (include_roles != null) { var roleArray = include_roles.ToArray(); var roleArrayCount = roleArray.Count(); for (var i = 0; i < roleArrayCount; i++) sb.Append($"&include_roles={roleArray[i]}"); } var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.PRUNE}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); - var url = Utilities.GetApiUriFor(path, $"{BuildQueryString(urlparams)}{sb}", this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, $"{BuildQueryString(urlparams)}{sb}", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var pruned = JsonConvert.DeserializeObject(res.Response); return pruned.Pruned.Value; } /// /// Begins the guild prune async. /// /// The guild_id. /// The days. /// If true, compute_prune_count. /// The include_roles. /// The reason. /// A Task. internal async Task BeginGuildPruneAsync(ulong guild_id, int days, bool compute_prune_count, IEnumerable include_roles, string reason) { if (days < 0 || days > 30) throw new ArgumentException("Prune inactivity days must be a number between 0 and 30.", nameof(days)); var urlparams = new Dictionary { ["days"] = days.ToString(CultureInfo.InvariantCulture), ["compute_prune_count"] = compute_prune_count.ToString() }; var sb = new StringBuilder(); if (include_roles != null) { var roleArray = include_roles.ToArray(); var roleArrayCount = roleArray.Count(); for (var i = 0; i < roleArrayCount; i++) sb.Append($"&include_roles={roleArray[i]}"); } var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.PRUNE}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { guild_id }, out var path); - var url = Utilities.GetApiUriFor(path, $"{BuildQueryString(urlparams)}{sb}", this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, $"{BuildQueryString(urlparams)}{sb}", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers).ConfigureAwait(false); var pruned = JsonConvert.DeserializeObject(res.Response); return pruned.Pruned; } #endregion #region GuildVarious /// /// Gets the template async. /// /// The code. /// A Task. internal async Task GetTemplateAsync(string code) { var route = $"{Endpoints.GUILDS}{Endpoints.TEMPLATES}/:code"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { code }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var templates_raw = JsonConvert.DeserializeObject(res.Response); return templates_raw; } /// /// Gets the guild integrations async. /// /// The guild_id. /// A Task. internal async Task> GetGuildIntegrationsAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INTEGRATIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var integrations_raw = JsonConvert.DeserializeObject>(res.Response).Select(xi => { xi.Discord = this.Discord; return xi; }); return new ReadOnlyCollection(new List(integrations_raw)); } /// /// Gets the guild preview async. /// /// The guild_id. /// A Task. internal async Task GetGuildPreviewAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.PREVIEW}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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; return ret; } /// /// Creates the guild integration async. /// /// The guild_id. /// The type. /// The id. /// A Task. internal async Task CreateGuildIntegrationAsync(ulong guild_id, string type, ulong id) { var pld = new RestGuildIntegrationAttachPayload { Type = type, Id = id }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INTEGRATIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { guild_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Modifies the guild integration async. /// /// The guild_id. /// The integration_id. /// The expire_behaviour. /// The expire_grace_period. /// If true, enable_emoticons. /// A Task. internal async Task ModifyGuildIntegrationAsync(ulong guild_id, ulong integration_id, int expire_behaviour, int expire_grace_period, bool enable_emoticons) { var pld = new RestGuildIntegrationModifyPayload { ExpireBehavior = expire_behaviour, ExpireGracePeriod = expire_grace_period, EnableEmoticons = enable_emoticons }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INTEGRATIONS}/:integration_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id, integration_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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 ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Deletes the guild integration async. /// /// The guild_id. /// The integration. /// A Task. internal Task DeleteGuildIntegrationAsync(ulong guild_id, DiscordIntegration integration) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INTEGRATIONS}/:integration_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id, integration_id = integration.Id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, payload: DiscordJson.SerializeObject(integration)); } /// /// Syncs the guild integration async. /// /// The guild_id. /// The integration_id. /// A Task. internal Task SyncGuildIntegrationAsync(ulong guild_id, ulong integration_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INTEGRATIONS}/:integration_id{Endpoints.SYNC}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { guild_id, integration_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route); } /// /// Gets the guild voice regions async. /// /// The guild_id. /// A Task. internal async Task> GetGuildVoiceRegionsAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.REGIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var regions_raw = JsonConvert.DeserializeObject>(res.Response); return new ReadOnlyCollection(new List(regions_raw)); } /// /// Gets the guild invites async. /// /// The guild_id. /// A Task. internal async Task> GetGuildInvitesAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INVITES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var invites_raw = JsonConvert.DeserializeObject>(res.Response).Select(xi => { xi.Discord = this.Discord; return xi; }); return new ReadOnlyCollection(new List(invites_raw)); } #endregion #region Invite /// /// Gets the invite async. /// /// The invite_code. /// If true, with_counts. /// If true, with_expiration. /// The scheduled event id to get. /// A Task. internal async Task GetInviteAsync(string invite_code, bool? with_counts, bool? with_expiration, ulong? guild_scheduled_event_id) { var urlparams = new Dictionary(); if (with_counts.HasValue) urlparams["with_counts"] = with_counts?.ToString(); if (with_expiration.HasValue) urlparams["with_expiration"] = with_expiration?.ToString(); if (guild_scheduled_event_id.HasValue) urlparams["guild_scheduled_event_id"] = guild_scheduled_event_id?.ToString(); var route = $"{Endpoints.INVITES}/:invite_code"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { invite_code }, out var path); - var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", 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; return ret; } /// /// Deletes the invite async. /// /// The invite_code. /// The reason. /// A Task. internal async Task DeleteInviteAsync(string invite_code, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.INVITES}/:invite_code"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { invite_code }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /* * Disabled due to API restrictions * * internal async Task InternalAcceptInvite(string invite_code) * { * this.Discord.DebugLogger.LogMessage(LogLevel.Warning, "REST API", "Invite accept endpoint was used; this account is now likely unverified", DateTime.Now); * - * var url = new Uri($"{Utils.GetApiBaseUri(), Endpoints.INVITES}/{invite_code)); + * var url = new Uri($"{Utils.GetApiBaseUri(this.Configuration), Endpoints.INVITES}/{invite_code)); * var bucket = this.Rest.GetBucket(0, MajorParameterType.Unbucketed, url, HttpRequestMethod.POST); * var res = await this.DoRequestAsync(this.Discord, bucket, url, HttpRequestMethod.POST).ConfigureAwait(false); * * var ret = JsonConvert.DeserializeObject(res.Response); * ret.Discord = this.Discord; * * return ret; * } */ #endregion #region Connections /// /// Gets the users connections async. /// /// A Task. internal async Task> GetUsersConnectionsAsync() { 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.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var connections_raw = JsonConvert.DeserializeObject>(res.Response).Select(xc => { xc.Discord = this.Discord; return xc; }); return new ReadOnlyCollection(new List(connections_raw)); } #endregion #region Voice /// /// Lists the voice regions async. /// /// A Task. internal async Task> ListVoiceRegionsAsync() { var route = $"{Endpoints.VOICE}{Endpoints.REGIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var regions = JsonConvert.DeserializeObject>(res.Response); return new ReadOnlyCollection(new List(regions)); } #endregion #region Webhooks /// /// Creates the webhook async. /// /// The channel_id. /// The name. /// The base64_avatar. /// The reason. /// A Task. internal async Task CreateWebhookAsync(ulong channel_id, string name, Optional base64_avatar, string reason) { var pld = new RestWebhookPayload { Name = name, AvatarBase64 = base64_avatar.HasValue ? base64_avatar.Value : null, AvatarSet = base64_avatar.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 }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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. /// A Task. internal async Task> GetChannelWebhooksAsync(ulong channel_id) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.WEBHOOKS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var webhooks_raw = JsonConvert.DeserializeObject>(res.Response).Select(xw => { xw.Discord = this.Discord; xw.ApiClient = this; return xw; }); return new ReadOnlyCollection(new List(webhooks_raw)); } /// /// Gets the guild webhooks async. /// /// The guild_id. /// A Task. internal async Task> GetGuildWebhooksAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WEBHOOKS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var webhooks_raw = JsonConvert.DeserializeObject>(res.Response).Select(xw => { xw.Discord = this.Discord; xw.ApiClient = this; return xw; }); return new ReadOnlyCollection(new List(webhooks_raw)); } /// /// Gets the webhook async. /// /// The webhook_id. /// A Task. internal async Task GetWebhookAsync(ulong webhook_id) { var route = $"{Endpoints.WEBHOOKS}/:webhook_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { webhook_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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; } // Auth header not required /// /// Gets the webhook with token async. /// /// The webhook_id. /// The webhook_token. /// A Task. internal async Task GetWebhookWithTokenAsync(ulong webhook_id, string webhook_token) { var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { webhook_id, webhook_token }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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 = webhook_token; ret.Id = webhook_id; 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. /// A Task. internal async Task ModifyWebhookAsync(ulong webhook_id, ulong channelId, string name, Optional base64_avatar, string reason) { var pld = new RestWebhookPayload { Name = name, AvatarBase64 = base64_avatar.HasValue ? base64_avatar.Value : null, AvatarSet = base64_avatar.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 }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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. /// A Task. internal async Task ModifyWebhookAsync(ulong webhook_id, string name, string base64_avatar, string webhook_token, string reason) { var pld = new RestWebhookPayload { Name = name, AvatarBase64 = base64_avatar }; 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, webhook_token }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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. /// A Task. internal Task DeleteWebhookAsync(ulong webhook_id, 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 }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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. /// A Task. internal Task DeleteWebhookAsync(ulong webhook_id, string webhook_token, 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, webhook_token }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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. /// A Task. internal async Task ExecuteWebhookAsync(ulong webhook_id, string webhook_token, DiscordWebhookBuilder builder, string thread_id) { 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.HasValue ? builder.Username.Value : null, AvatarUrl = builder.AvatarUrl.HasValue ? builder.AvatarUrl.Value : null, IsTTS = builder.IsTTS, Embeds = builder.Embeds }; 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) 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, webhook_token }, out var path); - var qub = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration.UseCanary).AddParameter("wait", "true"); + var qub = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration).AddParameter("wait", "true"); if (thread_id != null) qub.AddParameter("thread_id", thread_id); 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 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. /// A Task. internal async Task ExecuteWebhookSlackAsync(ulong webhook_id, string webhook_token, string json_payload) { var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token{Endpoints.SLACK}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { webhook_id, webhook_token }, out var path); - var url = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration.UseCanary).AddParameter("wait", "true").Build(); + var url = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration).AddParameter("wait", "true").Build(); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: json_payload).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. /// A Task. internal async Task ExecuteWebhookGithubAsync(ulong webhook_id, string webhook_token, string json_payload) { var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token{Endpoints.GITHUB}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { webhook_id, webhook_token }, out var path); - var url = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration.UseCanary).AddParameter("wait", "true").Build(); + var url = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration).AddParameter("wait", "true").Build(); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: json_payload).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. /// A Task. internal async Task EditWebhookMessageAsync(ulong webhook_id, string webhook_token, string message_id, DiscordWebhookBuilder builder) { builder.Validate(true); var pld = new RestWebhookMessageEditPayload { Content = builder.Content, Embeds = builder.Embeds, Mentions = builder.Mentions, Components = builder.Components, Attachments = builder.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, webhook_token, message_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); 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 file in builder.Files.Where(x => x.ResetPositionTo.HasValue)) { file.Stream.Position = file.ResetPositionTo.Value; } return ret; } /// /// Edits the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// The builder. /// A Task. internal Task EditWebhookMessageAsync(ulong webhook_id, string webhook_token, ulong message_id, DiscordWebhookBuilder builder) => this.EditWebhookMessageAsync(webhook_id, webhook_token, message_id.ToString(), builder); /// /// Gets the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// A Task. internal async Task GetWebhookMessageAsync(ulong webhook_id, string webhook_token, string message_id) { var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { webhook_id, webhook_token, message_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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; } /// /// Gets the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// A Task. internal Task GetWebhookMessageAsync(ulong webhook_id, string webhook_token, ulong message_id) => this.GetWebhookMessageAsync(webhook_id, webhook_token, message_id.ToString()); /// /// Deletes the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// A Task. internal async Task DeleteWebhookMessageAsync(ulong webhook_id, string webhook_token, string message_id) { var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { webhook_id, webhook_token, message_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } /// /// Deletes the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// A Task. internal Task DeleteWebhookMessageAsync(ulong webhook_id, string webhook_token, ulong message_id) => this.DeleteWebhookMessageAsync(webhook_id, webhook_token, message_id.ToString()); #endregion #region Reactions /// /// Creates the reaction async. /// /// The channel_id. /// The message_id. /// The emoji. /// A Task. internal Task CreateReactionAsync(ulong channel_id, ulong message_id, 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, message_id, emoji }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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. /// A Task. internal Task DeleteOwnReactionAsync(ulong channel_id, ulong message_id, 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, message_id, emoji }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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. /// A Task. internal Task DeleteUserReactionAsync(ulong channel_id, ulong message_id, ulong user_id, 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, message_id, emoji, user_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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. /// A Task. internal async Task> GetReactionsAsync(ulong channel_id, ulong message_id, string emoji, ulong? after_id = null, int limit = 25) { var urlparams = new Dictionary(); if (after_id.HasValue) urlparams["after"] = after_id.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, message_id, emoji }, out var path); - var url = Utilities.GetApiUriFor(path, BuildQueryString(urlparams), this.Discord.Configuration.UseCanary); + 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 reacters_raw = JsonConvert.DeserializeObject>(res.Response); var reacters = new List(); foreach (var xr in reacters_raw) { 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. /// A Task. internal Task DeleteAllReactionsAsync(ulong channel_id, ulong message_id, 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, message_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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. /// A Task. internal Task DeleteReactionsEmojiAsync(ulong channel_id, ulong message_id, 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, message_id, emoji }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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 Threads /// /// Creates the thread with message. /// /// The channel id to create the thread in. /// The message id to create the thread from. /// The name of the thread. /// The auto_archive_duration for the thread. /// The reason. internal async Task CreateThreadWithMessageAsync(ulong channel_id, ulong message_id, string name, ThreadAutoArchiveDuration auto_archive_duration, string reason = null) { var pld = new RestThreadChannelCreatePayload { Name = name, AutoArchiveDuration = auto_archive_duration }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.THREADS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id, message_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)); var thread_channel = JsonConvert.DeserializeObject(res.Response); return thread_channel; } /// /// Creates the thread without a message. /// /// The channel id to create the thread in. /// The name of the thread. /// The auto_archive_duration for the thread. /// Can be either or . /// The reason. internal async Task CreateThreadWithoutMessageAsync(ulong channel_id, string name, ThreadAutoArchiveDuration auto_archive_duration, ChannelType type = ChannelType.PublicThread, string reason = null) { var pld = new RestThreadChannelCreatePayload { Name = name, AutoArchiveDuration = auto_archive_duration, Type = type }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREADS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)); var thread_channel = JsonConvert.DeserializeObject(res.Response); return thread_channel; } /// /// Gets the thread. /// /// The thread id. internal async Task GetThreadAsync(ulong thread_id) { var route = $"{Endpoints.CHANNELS}/:channel_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { thread_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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; return ret; } /// /// Joins the thread. /// /// The channel id. internal async Task JoinThreadAsync(ulong channel_id) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREAD_MEMBERS}{Endpoints.ME}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { channel_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route); } /// /// Leaves the thread. /// /// The channel id. internal async Task LeaveThreadAsync(ulong channel_id) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREAD_MEMBERS}{Endpoints.ME}"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } /// /// Adds the thread member. /// /// The channel id to add the member to. /// The user id to add. internal async Task AddThreadMemberAsync(ulong channel_id, ulong user_id) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREAD_MEMBERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { channel_id, user_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route); } /// /// Removes the thread member. /// /// The channel id to remove the member from. /// The user id to remove. internal async Task RemoveThreadMemberAsync(ulong channel_id, ulong user_id) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREAD_MEMBERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id, user_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } /// /// Gets the thread members. /// /// The thread id. internal async Task> GetThreadMembersAsync(ulong thread_id) { var route = $"{Endpoints.CHANNELS}/:thread_id{Endpoints.THREAD_MEMBERS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { thread_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var thread_members_raw = JsonConvert.DeserializeObject>(res.Response); return new ReadOnlyCollection(thread_members_raw); } /// /// Gets the active threads in a guild. /// /// The guild id. internal async Task GetActiveThreadsAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.THREADS}{Endpoints.THREAD_ACTIVE}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var thread_return = JsonConvert.DeserializeObject(res.Response); return thread_return; } /// /// Gets the joined private archived threads in a channel. /// /// The channel id. /// Get threads before snowflake. /// Limit the results. internal async Task GetJoinedPrivateArchivedThreadsAsync(ulong channel_id, ulong? before, int? limit) { var urlparams = new Dictionary(); if (before != null) urlparams["before"] = before.Value.ToString(CultureInfo.InvariantCulture); if (limit != null && limit > 0) urlparams["limit"] = limit.Value.ToString(CultureInfo.InvariantCulture); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.USERS}{Endpoints.ME}{Endpoints.THREADS}{Endpoints.THREAD_ARCHIVED}{Endpoints.THREAD_PRIVATE}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id }, out var path); - var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var thread_return = JsonConvert.DeserializeObject(res.Response); return thread_return; } /// /// Gets the public archived threads in a channel. /// /// The channel id. /// Get threads before snowflake. /// Limit the results. internal async Task GetPublicArchivedThreadsAsync(ulong channel_id, ulong? before, int? limit) { var urlparams = new Dictionary(); if (before != null) urlparams["before"] = before.Value.ToString(CultureInfo.InvariantCulture); if (limit != null && limit > 0) urlparams["limit"] = limit.Value.ToString(CultureInfo.InvariantCulture); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREADS}{Endpoints.THREAD_ARCHIVED}{Endpoints.THREAD_PUBLIC}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id }, out var path); - var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var thread_return = JsonConvert.DeserializeObject(res.Response); return thread_return; } /// /// Gets the private archived threads in a channel. /// /// The channel id. /// Get threads before snowflake. /// Limit the results. internal async Task GetPrivateArchivedThreadsAsync(ulong channel_id, ulong? before, int? limit) { var urlparams = new Dictionary(); if (before != null) urlparams["before"] = before.Value.ToString(CultureInfo.InvariantCulture); if (limit != null && limit > 0) urlparams["limit"] = limit.Value.ToString(CultureInfo.InvariantCulture); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREADS}{Endpoints.THREAD_ARCHIVED}{Endpoints.THREAD_PRIVATE}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id }, out var path); - var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var thread_return = JsonConvert.DeserializeObject(res.Response); return thread_return; } /// /// Modifies a thread. /// /// The thread to modify. /// The new name. /// The new locked state. /// The new archived state. /// The new auto archive duration. /// The new per user rate limit. /// The new user invitable state. /// The reason for the modification. internal Task ModifyThreadAsync(ulong thread_id, string name, Optional locked, Optional archived, Optional autoArchiveDuration, Optional perUserRateLimit, Optional invitable, string reason) { var pld = new RestThreadChannelModifyPayload { Name = name, Archived = archived, AutoArchiveDuration = autoArchiveDuration, Locked = locked, PerUserRateLimit = perUserRateLimit, Invitable = invitable }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:thread_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { thread_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Deletes a thread. /// /// The thread to delete. /// The reason for deletion. internal Task DeleteThreadAsync(ulong thread_id, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:thread_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { thread_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } #endregion #region Emoji /// /// Gets the guild emojis async. /// /// The guild_id. /// A Task. internal async Task> GetGuildEmojisAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.EMOJIS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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(guild_id, 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. /// A Task. internal async Task GetGuildEmojiAsync(ulong guild_id, ulong emoji_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.EMOJIS}/:emoji_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id, emoji_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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(guild_id, out var gld); var emoji_raw = JObject.Parse(res.Response); var emoji = emoji_raw.ToObject(); emoji.Guild = gld; var xtu = emoji_raw["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. /// A Task. internal async Task CreateGuildEmojiAsync(ulong guild_id, 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 }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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(guild_id, out var gld); var emoji_raw = JObject.Parse(res.Response); var emoji = emoji_raw.ToObject(); emoji.Guild = gld; var xtu = emoji_raw["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. /// A Task. internal async Task ModifyGuildEmojiAsync(ulong guild_id, ulong emoji_id, 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, emoji_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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(guild_id, out var gld); var emoji_raw = JObject.Parse(res.Response); var emoji = emoji_raw.ToObject(); emoji.Guild = gld; var xtu = emoji_raw["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. /// A Task. internal Task DeleteGuildEmojiAsync(ulong guild_id, ulong emoji_id, 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, emoji_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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 sticker_id) { var route = $"{Endpoints.STICKERS}/:sticker_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {sticker_id}, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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.UseCanary); + 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 guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.STICKERS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id}, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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 guild_id, ulong sticker_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.STICKERS}/:sticker_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id, sticker_id}, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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 guild_id, 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}, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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 guild_id, ulong sticker_id, 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, sticker_id}, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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 guild_id, ulong sticker_id, string reason) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.STICKERS}/:sticker_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id, sticker_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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 async. /// /// The application_id. /// A Task. internal async Task> GetGlobalApplicationCommandsAsync(ulong application_id) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { application_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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(); } /// /// Bulks the overwrite global application commands async. /// /// The application_id. /// The commands. /// A Task. internal async Task> BulkOverwriteGlobalApplicationCommandsAsync(ulong application_id, 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, DefaultPermission = command.DefaultPermission }); } var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { application_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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 the global application command async. /// /// The application_id. /// The command. /// A Task. internal async Task CreateGlobalApplicationCommandAsync(ulong application_id, DiscordApplicationCommand command) { var pld = new RestApplicationCommandCreatePayload { Type = command.Type, Name = command.Name, Description = command.Description, Options = command.Options, DefaultPermission = command.DefaultPermission }; var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { application_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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 the global application command async. /// /// The application_id. /// The command_id. /// A Task. internal async Task GetGlobalApplicationCommandAsync(ulong application_id, ulong command_id) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}/:command_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { application_id, command_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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 the global application command async. /// /// The application_id. /// The command_id. /// The name. /// The description. /// The options. /// The default_permission. /// A Task. internal async Task EditGlobalApplicationCommandAsync(ulong application_id, ulong command_id, Optional name, Optional description, Optional> options, Optional default_permission) { var pld = new RestApplicationCommandEditPayload { Name = name, Description = description, Options = options, DefaultPermission = default_permission }; var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}/:command_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { application_id, command_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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 the global application command async. /// /// The application_id. /// The command_id. /// A Task. internal async Task DeleteGlobalApplicationCommandAsync(ulong application_id, ulong command_id) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}/:command_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { application_id, command_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } /// /// Gets the guild application commands async. /// /// The application_id. /// The guild_id. /// A Task. internal async Task> GetGuildApplicationCommandsAsync(ulong application_id, ulong guild_id) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { application_id, guild_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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(); } /// /// Bulks the overwrite guild application commands async. /// /// The application_id. /// The guild_id. /// The commands. /// A Task. internal async Task> BulkOverwriteGuildApplicationCommandsAsync(ulong application_id, ulong guild_id, 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, DefaultPermission = command.DefaultPermission }); } var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { application_id, guild_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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 the guild application command async. /// /// The application_id. /// The guild_id. /// The command. /// A Task. internal async Task CreateGuildApplicationCommandAsync(ulong application_id, ulong guild_id, DiscordApplicationCommand command) { var pld = new RestApplicationCommandCreatePayload { Type = command.Type, Name = command.Name, Description = command.Description, Options = command.Options, DefaultPermission = command.DefaultPermission }; var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { application_id, guild_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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 the guild application command async. /// /// The application_id. /// The guild_id. /// The command_id. internal async Task GetGuildApplicationCommandAsync(ulong application_id, ulong guild_id, ulong command_id) { 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, guild_id, command_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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 the guild application command async. /// /// The application_id. /// The guild_id. /// The command_id. /// The name. /// The description. /// The options. /// The default_permission. internal async Task EditGuildApplicationCommandAsync(ulong application_id, ulong guild_id, ulong command_id, Optional name, Optional description, Optional> options, Optional default_permission) { var pld = new RestApplicationCommandEditPayload { Name = name, Description = description, Options = options, DefaultPermission = default_permission }; 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, guild_id, command_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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 the guild application command async. /// /// The application_id. /// The guild_id. /// The command_id. internal async Task DeleteGuildApplicationCommandAsync(ulong application_id, ulong guild_id, ulong command_id) { 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, guild_id, command_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } /// /// Gets the guild application command permissions. /// /// The target application id. /// The target guild id. internal async Task> GetGuildApplicationCommandPermissionsAsync(ulong application_id, ulong guild_id) { 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, guild_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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 the application command permission. /// /// The target application id. /// The target guild id. /// The target command id. internal async Task GetApplicationCommandPermissionAsync(ulong application_id, ulong guild_id, ulong command_id) { 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, guild_id, command_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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; } /// /// Overwrites the guild application command permissions. /// /// The target application id. /// The target guild id. /// The target command id. /// Array of permissions. internal async Task OverwriteGuildApplicationCommandPermissionsAsync(ulong application_id, ulong guild_id, ulong command_id, IEnumerable permissions) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}/:command_id{Endpoints.PERMISSIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { application_id, guild_id, command_id }, out var path); if (permissions.ToArray().Length > 10) throw new NotSupportedException("You can add only up to 10 permission overwrites per command."); var pld = new RestApplicationCommandPermissionEditPayload { Permissions = permissions }; - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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); ret.Discord = this.Discord; return ret; } /// /// Bulks overwrite the application command permissions. /// /// The target application id. /// The target guild id. /// internal async Task> BulkOverwriteApplicationCommandPermissionsAsync(ulong application_id, ulong guild_id, IEnumerable permission_overwrites) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}{Endpoints.PERMISSIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { application_id, guild_id }, out var path); var pld = new List(); foreach (var overwrite in permission_overwrites) { if (overwrite.Permissions.Count > 10) throw new NotSupportedException("You can add only up to 10 permission overwrites per command."); pld.Add(new RestGuildApplicationCommandPermissionEditPayload { CommandId = overwrite.Id, Permissions = overwrite.Permissions }); } - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, payload: DiscordJson.SerializeObject(pld.ToArray())); var ret = JsonConvert.DeserializeObject>(res.Response); foreach (var app in ret) app.Discord = this.Discord; return ret.ToList(); } /// /// Creates the interaction response async. /// /// The interaction_id. /// The interaction_token. /// The type. /// The builder. /// A Task. internal async Task CreateInteractionResponseAsync(ulong interaction_id, string interaction_token, InteractionResponseType type, DiscordInteractionResponseBuilder builder) { try { if (builder?.Embeds != null) foreach (var embed in builder.Embeds) if (embed.Timestamp != null) embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); var pld = type == InteractionResponseType.AutoCompleteResult ? new RestInteractionResponsePayload { Type = type, Data = new DiscordInteractionApplicationCommandCallbackData { Content = null, Embeds = null, IsTTS = null, Mentions = null, Flags = null, Components = null, Choices = builder.Choices } } : new RestInteractionResponsePayload { Type = type, Data = builder != null ? new DiscordInteractionApplicationCommandCallbackData { Content = builder.Content, Embeds = builder.Embeds, IsTTS = builder.IsTTS, Mentions = builder.Mentions, Flags = builder.IsEphemeral ? MessageFlags.Ephemeral : 0, Components = builder.Components, Choices = null } : null }; var values = new Dictionary(); if (builder != null) if (!string.IsNullOrEmpty(builder.Content) || builder.Embeds?.Count() > 0 || builder.IsTTS == true || builder.Mentions != null) 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, interaction_token }, out var path); - var url = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration.UseCanary).AddParameter("wait", "true").Build(); + var url = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration).AddParameter("wait", "true").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)); } } catch(Exception ex) { this.Discord.Logger.LogDebug(ex, ex.Message); } } /// /// Gets the original interaction response async. /// /// The application_id. /// The interaction_token. /// A Task. internal Task GetOriginalInteractionResponseAsync(ulong application_id, string interaction_token) => this.GetWebhookMessageAsync(application_id, interaction_token, Endpoints.ORIGINAL); /// /// Edits the original interaction response async. /// /// The application_id. /// The interaction_token. /// The builder. /// A Task. internal Task EditOriginalInteractionResponseAsync(ulong application_id, string interaction_token, DiscordWebhookBuilder builder) => this.EditWebhookMessageAsync(application_id, interaction_token, Endpoints.ORIGINAL, builder); /// /// Deletes the original interaction response async. /// /// The application_id. /// The interaction_token. /// A Task. internal Task DeleteOriginalInteractionResponseAsync(ulong application_id, string interaction_token) => this.DeleteWebhookMessageAsync(application_id, interaction_token, Endpoints.ORIGINAL); /// /// Creates the followup message async. /// /// The application_id. /// The interaction_token. /// The builder. /// A Task. internal async Task CreateFollowupMessageAsync(ulong application_id, string interaction_token, 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.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) 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, interaction_token }, out var path); - var url = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration.UseCanary).AddParameter("wait", "true").Build(); + 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 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 async. /// /// The application_id. /// The interaction_token. /// The message_id. /// A Task. internal Task GetFollowupMessageAsync(ulong application_id, string interaction_token, ulong message_id) => this.GetWebhookMessageAsync(application_id, interaction_token, message_id); /// /// Edits the followup message async. /// /// The application_id. /// The interaction_token. /// The message_id. /// The builder. /// A Task. internal Task EditFollowupMessageAsync(ulong application_id, string interaction_token, ulong message_id, DiscordWebhookBuilder builder) => this.EditWebhookMessageAsync(application_id, interaction_token, message_id, builder); /// /// Deletes the followup message async. /// /// The application_id. /// The interaction_token. /// The message_id. /// A Task. internal Task DeleteFollowupMessageAsync(ulong application_id, string interaction_token, ulong message_id) => this.DeleteWebhookMessageAsync(application_id, interaction_token, message_id); //TODO: edit, delete, follow up #endregion #region Misc /// /// Gets the current application info async. /// /// A Task. internal Task GetCurrentApplicationInfoAsync() => this.GetApplicationInfoAsync("@me"); /// /// Gets the application info async. /// /// The application_id. /// A Task. internal Task GetApplicationInfoAsync(ulong application_id) => this.GetApplicationInfoAsync(application_id.ToString(CultureInfo.InvariantCulture)); /// /// Gets the application info async. /// /// The application_id. /// A Task. private async Task GetApplicationInfoAsync(string application_id) { var route = $"{Endpoints.OAUTH2}{Endpoints.APPLICATIONS}/:application_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { application_id }, out var path); - var url = Utilities.GetApiUriFor(path, this.Discord.Configuration.UseCanary); + 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. /// A Task. 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.UseCanary); + 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. /// /// A Task. 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.UseCanary); + 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.resetAfter); return info; } #endregion /// /// Gets the DisCatSharp team. /// > internal async Task GetDisCatSharpTeamAsync() { try { var wc = new WebClient(); var dcs = await wc.DownloadStringTaskAsync(new Uri("https://dcs.aitsys.dev/api/devs/")); var dcs_guild = await wc.DownloadStringTaskAsync(new Uri("https://dcs.aitsys.dev/api/guild/")); var app = JsonConvert.DeserializeObject(dcs); var guild = JsonConvert.DeserializeObject(dcs_guild); var dcst = new DisCatSharpTeam { IconHash = app.Team.IconHash, TeamName = app.Team.Name, PrivacyPolicyUrl = app.PrivacyPolicyUrl, TermsOfServiceUrl = app.TermsOfServiceUrl, RepoUrl = "https://github.com/Aiko-IT-Systems/DisCatSharp", DocsUrl = "https://docs.dcs.aitsys.dev", Id = app.Team.Id, BannerHash = guild.BannerHash, LogoHash = guild.IconHash, GuildId = guild.Id, Guild = guild, SupportInvite = await this.GetInviteAsync("discatsharp", true, true, null) }; List team = new(); DisCatSharpTeamMember owner = new(); foreach (var mb in app.Team.Members.OrderBy(m => m.User.Username)) { var tuser = await this.GetUserAsync(mb.User.Id); var user = mb.User; if (mb.User.Id == 856780995629154305) { owner.Id = user.Id; owner.Username = user.Username; owner.Discriminator = user.Discriminator; owner.AvatarHash = user.AvatarHash; owner.BannerHash = tuser.BannerHash; owner._bannerColor = tuser._bannerColor; team.Add(owner); } else { team.Add(new() { Id = user.Id, Username = user.Username, Discriminator = user.Discriminator, AvatarHash = user.AvatarHash, BannerHash = tuser.BannerHash, _bannerColor = tuser._bannerColor }); } } dcst.Owner = owner; dcst.Developers = team; return dcst; } catch(Exception ex) { this.Discord.Logger.LogDebug(ex.Message); this.Discord.Logger.LogDebug(ex.StackTrace); return null; } } } } diff --git a/DisCatSharp/Net/Rest/Endpoints.cs b/DisCatSharp/Net/Rest/Endpoints.cs index 342a09727..0a298a88f 100644 --- a/DisCatSharp/Net/Rest/Endpoints.cs +++ b/DisCatSharp/Net/Rest/Endpoints.cs @@ -1,378 +1,382 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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 DisCatSharp; namespace DisCatSharp.Net { /// /// The discord endpoints. /// internal static class Endpoints { /// /// The base discord api uri. /// - public const string BASE_URI = "https://discord.com/api/v9"; + public const string BASE_URI = "https://discord.com/api/v"; + + /// + /// The base discord canary api uri. + /// + public const string CANARY_URI = "https://canary.discord.com/api/v"; /// /// The oauth2 endpoint. /// public const string OAUTH2 = "/oauth2"; /// /// The oauth2 authorize endpoint. /// public const string AUTHORIZE = "/authorize"; /// /// The applications endpoint. /// public const string APPLICATIONS = "/applications"; /// /// The message reactions endpoint. /// public const string REACTIONS = "/reactions"; /// /// The self (@me) endpoint. /// public const string ME = "/@me"; /// /// The @original endpoint. /// public const string ORIGINAL = "/@original"; /// /// The permissions endpoint. /// public const string PERMISSIONS = "/permissions"; /// /// The recipients endpoint. /// public const string RECIPIENTS = "/recipients"; /// /// The bulk-delete endpoint. /// public const string BULK_DELETE = "/bulk-delete"; /// /// The integrations endpoint. /// public const string INTEGRATIONS = "/integrations"; /// /// The applications endpoint. /// public const string SYNC = "/sync"; /// /// The prune endpoint. /// Used for user removal. /// public const string PRUNE = "/prune"; /// /// The regions endpoint. /// public const string REGIONS = "/regions"; /// /// The connections endpoint. /// public const string CONNECTIONS = "/connections"; /// /// The icons endpoint. /// public const string ICONS = "/icons"; /// /// The gateway endpoint. /// public const string GATEWAY = "/gateway"; /// /// The oauth2 auth endpoint. /// public const string AUTH = "/auth"; /// /// The oauth2 login endpoint. /// public const string LOGIN = "/login"; /// /// The channels endpoint. /// public const string CHANNELS = "/channels"; /// /// The messages endpoint. /// public const string MESSAGES = "/messages"; /// /// The pinned messages endpoint. /// public const string PINS = "/pins"; /// /// The users endpoint. /// public const string USERS = "/users"; /// /// The guilds endpoint. /// public const string GUILDS = "/guilds"; /// /// The guild discovery splash endpoint. /// public const string GUILD_DISCOVERY_SPLASHES = "/discovery-splashes"; /// /// The guild splash endpoint. /// public const string SPLASHES = "/splashes"; /// /// The search endpoint. /// public const string SEARCH = "/search"; /// /// The invites endpoint. /// public const string INVITES = "/invites"; /// /// The roles endpoint. /// public const string ROLES = "/roles"; /// /// The members endpoint. /// public const string MEMBERS = "/members"; /// /// The typing endpoint. /// Triggers a typing indicator inside a channel. /// public const string TYPING = "/typing"; /// /// The avatars endpoint. /// public const string AVATARS = "/avatars"; /// /// The bans endpoint. /// public const string BANS = "/bans"; /// /// The webhook endpoint. /// public const string WEBHOOKS = "/webhooks"; /// /// The slack endpoint. /// Used for . /// public const string SLACK = "/slack"; /// /// The github endpoint. /// Used for . /// public const string GITHUB = "/github"; /// /// The bot endpoint. /// public const string BOT = "/bot"; /// /// The voice endpoint. /// public const string VOICE = "/voice"; /// /// The audit logs endpoint. /// public const string AUDIT_LOGS = "/audit-logs"; /// /// The acknowledge endpoint. /// Indicates that a message is read. /// public const string ACK = "/ack"; /// /// The nickname endpoint. /// public const string NICK = "/nick"; /// /// The assets endpoint. /// public const string ASSETS = "/assets"; /// /// The embed endpoint. /// public const string EMBED = "/embed"; /// /// The emojis endpoint. /// public const string EMOJIS = "/emojis"; /// /// The vanity url endpoint. /// public const string VANITY_URL = "/vanity-url"; /// /// The guild preview endpoint. /// public const string PREVIEW = "/preview"; /// /// The followers endpoint. /// public const string FOLLOWERS = "/followers"; /// /// The crosspost endpoint. /// public const string CROSSPOST = "/crosspost"; /// /// The guild widget endpoint. /// public const string WIDGET = "/widget"; /// /// The guild widget json endpoint. /// public const string WIDGET_JSON = "/widget.json"; /// /// The guild widget png endpoint. /// public const string WIDGET_PNG = "/widget.png"; /// /// The templates endpoint. /// public const string TEMPLATES = "/templates"; /// /// The member verification gate endpoint. /// public const string MEMBER_VERIFICATION = "/member-verification"; /// /// The slash commands endpoint. /// public const string COMMANDS = "/commands"; /// /// The interactions endpoint. /// public const string INTERACTIONS = "/interactions"; /// /// The interaction/command callback endpoint. /// public const string CALLBACK = "/callback"; /// /// The welcome screen endpoint. /// public const string WELCOME_SCREEN = "/welcome-screen"; /// /// The voice states endpoint. /// public const string VOICE_STATES = "/voice-states"; /// /// The stage instances endpoint. /// public const string STAGE_INSTANCES = "/stage-instances"; /// /// The threads endpoint. /// public const string THREADS = "/threads"; /// /// The public threads endpoint. /// public const string THREAD_PUBLIC = "/public"; /// /// The private threads endpoint. /// public const string THREAD_PRIVATE = "/private"; /// /// The active threads endpoint. /// public const string THREAD_ACTIVE = "/active"; /// /// The archived threads endpoint. /// public const string THREAD_ARCHIVED = "/archived"; /// /// The thread members endpoint. /// public const string THREAD_MEMBERS = "/thread-members"; /// /// The guild events endpoint. /// Used for stage events. /// public const string EVENTS = "/events"; /// /// The stickers endpoint. /// public const string STICKERS = "/stickers"; /// /// The sticker packs endpoint. /// Global nitro sticker packs. /// public const string STICKERPACKS = "/sticker-packs"; /// /// The store endpoint. /// public const string STORE = "/store"; /// /// The app assets endpoint. /// public const string APP_ASSETS = "/app-assets"; /// /// The app icons endpoint. /// public const string APP_ICONS = "/app-icons"; /// /// The team icons endpoint. /// public const string TEAM_ICONS = "/team-icons"; /// /// The channel icons endpoint. /// public const string CHANNEL_ICONS = "/channel-icons"; /// /// The user banners endpoint. /// public const string BANNERS = "/banners"; /// /// The sticker endpoint. /// This endpoint is the static nitro sticker application. /// public const string STICKER_APPLICATION = "/710982414301790216"; /// /// The role subscription endpoint. /// public const string ROLE_SUBSCRIPTIONS = "/role-subscriptions"; /// /// The group listings endpoint. /// public const string GROUP_LISTINGS = "/group-listings"; /// /// The subscription listings endpoint. /// public const string SUBSCRIPTION_LISTINGS = "/subscription-listings"; /// /// The directory entries endpoint. /// public const string DIRECTORY_ENTRIES = "/directory-entries"; /// /// The counts endpoint. /// public const string COUNTS = "/counts"; /// /// The list endpoint. /// public const string LIST = "/list"; /// /// The role icons endpoint. /// public const string ROLE_ICONS = "/role-icons"; /// /// The activities endpoint. /// - public const string ACTIVITIES = "/activities "; + public const string ACTIVITIES = "/activities"; /// /// The config endpoint. /// public const string CONFIG = "/config"; } } diff --git a/DisCatSharp/Net/Rest/RestClient.cs b/DisCatSharp/Net/Rest/RestClient.cs index 66fa3c646..b0bc033da 100644 --- a/DisCatSharp/Net/Rest/RestClient.cs +++ b/DisCatSharp/Net/Rest/RestClient.cs @@ -1,840 +1,840 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Globalization; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Reflection; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using DisCatSharp.Exceptions; using Microsoft.Extensions.Logging; namespace DisCatSharp.Net { /// /// Represents a client used to make REST requests. /// internal sealed class RestClient : IDisposable { /// /// Gets the route argument regex. /// private static Regex RouteArgumentRegex { get; } = new Regex(@":([a-z_]+)"); /// /// Gets the http client. /// private HttpClient HttpClient { get; } /// /// Gets the discord. /// [System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0052:Remove unread private members", Justification = "")] private BaseDiscordClient Discord { get; } /// /// Gets a value indicating whether debug is enabled. /// internal bool Debug { get; set; } /// /// Gets the logger. /// private ILogger Logger { get; } /// /// Gets the routes to hashes. /// private ConcurrentDictionary RoutesToHashes { get; } /// /// Gets the hashes to buckets. /// private ConcurrentDictionary HashesToBuckets { get; } /// /// Gets the request queue. /// private ConcurrentDictionary RequestQueue { get; } /// /// Gets the global rate limit event. /// private AsyncManualResetEvent GlobalRateLimitEvent { get; } /// /// Gets a value indicating whether use reset after. /// private bool UseResetAfter { get; } private CancellationTokenSource _bucketCleanerTokenSource; private TimeSpan _bucketCleanupDelay = TimeSpan.FromSeconds(60); private volatile bool _cleanerRunning; private Task _cleanerTask; private volatile bool _disposed; /// /// Initializes a new instance of the class. /// /// The client. internal RestClient(BaseDiscordClient client) : this(client.Configuration.Proxy, client.Configuration.HttpTimeout, client.Configuration.UseRelativeRatelimit, client.Logger) { this.Discord = client; this.HttpClient.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", Utilities.GetFormattedToken(client)); } /// /// Initializes a new instance of the class. /// /// The proxy. /// The timeout. /// If true, use relative ratelimit. /// The logger. internal RestClient(IWebProxy proxy, TimeSpan timeout, bool useRelativeRatelimit, ILogger logger) // This is for meta-clients, such as the webhook client { this.Logger = logger; var httphandler = new HttpClientHandler { UseCookies = false, AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip, UseProxy = proxy != null, Proxy = proxy }; this.HttpClient = new HttpClient(httphandler) { - BaseAddress = new Uri(Utilities.GetApiBaseUri()), + BaseAddress = new Uri(Utilities.GetApiBaseUri(this.Discord.Configuration)), Timeout = timeout }; this.HttpClient.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", Utilities.GetUserAgent()); this.RoutesToHashes = new ConcurrentDictionary(); this.HashesToBuckets = new ConcurrentDictionary(); this.RequestQueue = new ConcurrentDictionary(); this.GlobalRateLimitEvent = new AsyncManualResetEvent(true); this.UseResetAfter = useRelativeRatelimit; } /// /// Gets a bucket. /// /// The method. /// The route. /// The route paramaters. /// The url. /// A ratelimit bucket. public RateLimitBucket GetBucket(RestRequestMethod method, string route, object route_params, out string url) { var rparams_props = route_params.GetType() .GetTypeInfo() .DeclaredProperties; var rparams = new Dictionary(); foreach (var xp in rparams_props) { var val = xp.GetValue(route_params); rparams[xp.Name] = val is string xs ? xs : val is DateTime dt ? dt.ToString("yyyy-MM-ddTHH:mm:sszzz", CultureInfo.InvariantCulture) : val is DateTimeOffset dto ? dto.ToString("yyyy-MM-ddTHH:mm:sszzz", CultureInfo.InvariantCulture) : val is IFormattable xf ? xf.ToString(null, CultureInfo.InvariantCulture) : val.ToString(); } var guild_id = rparams.ContainsKey("guild_id") ? rparams["guild_id"] : ""; var channel_id = rparams.ContainsKey("channel_id") ? rparams["channel_id"] : ""; var webhook_id = rparams.ContainsKey("webhook_id") ? rparams["webhook_id"] : ""; // Create a generic route (minus major params) key // ex: POST:/channels/channel_id/messages var hashKey = RateLimitBucket.GenerateHashKey(method, route); // We check if the hash is present, using our generic route (without major params) // ex: in POST:/channels/channel_id/messages, out 80c17d2f203122d936070c88c8d10f33 // If it doesn't exist, we create an unlimited hash as our initial key in the form of the hash key + the unlimited constant // and assign this to the route to hash cache // ex: this.RoutesToHashes[POST:/channels/channel_id/messages] = POST:/channels/channel_id/messages:unlimited var hash = this.RoutesToHashes.GetOrAdd(hashKey, RateLimitBucket.GenerateUnlimitedHash(method, route)); // Next we use the hash to generate the key to obtain the bucket. // ex: 80c17d2f203122d936070c88c8d10f33:guild_id:506128773926879242:webhook_id // or if unlimited: POST:/channels/channel_id/messages:unlimited:guild_id:506128773926879242:webhook_id var bucketId = RateLimitBucket.GenerateBucketId(hash, guild_id, channel_id, webhook_id); // If it's not in cache, create a new bucket and index it by its bucket id. var bucket = this.HashesToBuckets.GetOrAdd(bucketId, new RateLimitBucket(hash, guild_id, channel_id, webhook_id)); bucket.LastAttemptAt = DateTimeOffset.UtcNow; // Cache the routes for each bucket so it can be used for GC later. if (!bucket.RouteHashes.Contains(bucketId)) bucket.RouteHashes.Add(bucketId); // Add the current route to the request queue, which indexes the amount // of requests occurring to the bucket id. _ = this.RequestQueue.TryGetValue(bucketId, out var count); // Increment by one atomically due to concurrency this.RequestQueue[bucketId] = Interlocked.Increment(ref count); // Start bucket cleaner if not already running. if (!this._cleanerRunning) { this._cleanerRunning = true; this._bucketCleanerTokenSource = new CancellationTokenSource(); this._cleanerTask = Task.Run(this.CleanupBucketsAsync, this._bucketCleanerTokenSource.Token); this.Logger.LogDebug(LoggerEvents.RestCleaner, "Bucket cleaner task started."); } url = RouteArgumentRegex.Replace(route, xm => rparams[xm.Groups[1].Value]); return bucket; } /// /// Executes the request async. /// /// The request to be executed. public Task ExecuteRequestAsync(BaseRestRequest request) => request == null ? throw new ArgumentNullException(nameof(request)) : this.ExecuteRequestAsync(request, null, null); /// /// Executes the request async. /// This is to allow proper rescheduling of the first request from a bucket. /// /// The request to be executed. /// The bucket. /// The ratelimit task completion source. private async Task ExecuteRequestAsync(BaseRestRequest request, RateLimitBucket bucket, TaskCompletionSource ratelimitTcs) { if (this._disposed) return; HttpResponseMessage res = default; try { await this.GlobalRateLimitEvent.WaitAsync().ConfigureAwait(false); if (bucket == null) bucket = request.RateLimitBucket; if (ratelimitTcs == null) ratelimitTcs = await this.WaitForInitialRateLimit(bucket).ConfigureAwait(false); if (ratelimitTcs == null) // ckeck rate limit only if we are not the probe request { var now = DateTimeOffset.UtcNow; await bucket.TryResetLimitAsync(now).ConfigureAwait(false); // Decrement the remaining number of requests as there can be other concurrent requests before this one finishes and has a chance to update the bucket if (Interlocked.Decrement(ref bucket._remaining) < 0) { this.Logger.LogDebug(LoggerEvents.RatelimitDiag, "Request for {0} is blocked", bucket.ToString()); var delay = bucket.Reset - now; var resetDate = bucket.Reset; if (this.UseResetAfter) { delay = bucket.ResetAfter.Value; resetDate = bucket.ResetAfterOffset; } if (delay < new TimeSpan(-TimeSpan.TicksPerMinute)) { this.Logger.LogError(LoggerEvents.RatelimitDiag, "Failed to retrieve ratelimits - giving up and allowing next request for bucket"); bucket._remaining = 1; } if (delay < TimeSpan.Zero) delay = TimeSpan.FromMilliseconds(100); this.Logger.LogWarning(LoggerEvents.RatelimitPreemptive, "Pre-emptive ratelimit triggered - waiting until {0:yyyy-MM-dd HH:mm:ss zzz} ({1:c}).", resetDate, delay); Task.Delay(delay) .ContinueWith(_ => this.ExecuteRequestAsync(request, null, null)) .LogTaskFault(this.Logger, LogLevel.Error, LoggerEvents.RestError, "Error while executing request"); return; } this.Logger.LogDebug(LoggerEvents.RatelimitDiag, "Request for {0} is allowed", bucket.ToString()); } else this.Logger.LogDebug(LoggerEvents.RatelimitDiag, "Initial request for {0} is allowed", bucket.ToString()); var req = this.BuildRequest(request); if(this.Debug) this.Logger.LogTrace(LoggerEvents.Misc, await req.Content.ReadAsStringAsync()); var response = new RestResponse(); try { if (this._disposed) return; res = await this.HttpClient.SendAsync(req, HttpCompletionOption.ResponseContentRead, CancellationToken.None).ConfigureAwait(false); var bts = await res.Content.ReadAsByteArrayAsync().ConfigureAwait(false); var txt = Utilities.UTF8.GetString(bts, 0, bts.Length); this.Logger.LogTrace(LoggerEvents.RestRx, txt); response.Headers = res.Headers.ToDictionary(xh => xh.Key, xh => string.Join("\n", xh.Value), StringComparer.OrdinalIgnoreCase); response.Response = txt; response.ResponseCode = (int)res.StatusCode; } catch (HttpRequestException httpex) { this.Logger.LogError(LoggerEvents.RestError, httpex, "Request to {0} triggered an HttpException", request.Url); request.SetFaulted(httpex); this.FailInitialRateLimitTest(request, ratelimitTcs); return; } this.UpdateBucket(request, response, ratelimitTcs); Exception ex = null; switch (response.ResponseCode) { case 400: case 405: ex = new BadRequestException(request, response); break; case 401: case 403: ex = new UnauthorizedException(request, response); break; case 404: ex = new NotFoundException(request, response); break; case 413: ex = new RequestSizeException(request, response); break; case 429: ex = new RateLimitException(request, response); // check the limit info and requeue this.Handle429(response, out var wait, out var global); if (wait != null) { if (global) { this.Logger.LogError(LoggerEvents.RatelimitHit, "Global ratelimit hit, cooling down"); try { this.GlobalRateLimitEvent.Reset(); await wait.ConfigureAwait(false); } finally { // we don't want to wait here until all the blocked requests have been run, additionally Set can never throw an exception that could be suppressed here _ = this.GlobalRateLimitEvent.SetAsync(); } this.ExecuteRequestAsync(request, bucket, ratelimitTcs) .LogTaskFault(this.Logger, LogLevel.Error, LoggerEvents.RestError, "Error while retrying request"); } else { this.Logger.LogError(LoggerEvents.RatelimitHit, "Ratelimit hit, requeueing request to {0}", request.Url); await wait.ConfigureAwait(false); this.ExecuteRequestAsync(request, bucket, ratelimitTcs) .LogTaskFault(this.Logger, LogLevel.Error, LoggerEvents.RestError, "Error while retrying request"); } return; } break; case 500: case 502: case 503: case 504: ex = new ServerErrorException(request, response); break; } if (ex != null) request.SetFaulted(ex); else request.SetCompleted(response); } catch (Exception ex) { this.Logger.LogError(LoggerEvents.RestError, ex, "Request to {0} triggered an exception", request.Url); // if something went wrong and we couldn't get rate limits for the first request here, allow the next request to run if (bucket != null && ratelimitTcs != null && bucket._limitTesting != 0) this.FailInitialRateLimitTest(request, ratelimitTcs); if (!request.TrySetFaulted(ex)) throw; } finally { res?.Dispose(); // Get and decrement active requests in this bucket by 1. _ = this.RequestQueue.TryGetValue(bucket.BucketId, out var count); this.RequestQueue[bucket.BucketId] = Interlocked.Decrement(ref count); // If it's 0 or less, we can remove the bucket from the active request queue, // along with any of its past routes. if (count <= 0) { foreach (var r in bucket.RouteHashes) { if (this.RequestQueue.ContainsKey(r)) { _ = this.RequestQueue.TryRemove(r, out _); } } } } } /// /// Fails the initial rate limit test. /// /// The request. /// The ratelimit task completion source. /// If true, reset to initial. private void FailInitialRateLimitTest(BaseRestRequest request, TaskCompletionSource ratelimitTcs, bool resetToInitial = false) { if (ratelimitTcs == null && !resetToInitial) return; var bucket = request.RateLimitBucket; bucket._limitValid = false; bucket._limitTestFinished = null; bucket._limitTesting = 0; //Reset to initial values. if (resetToInitial) { this.UpdateHashCaches(request, bucket); bucket.Maximum = 0; bucket._remaining = 0; return; } // no need to wait on all the potentially waiting tasks _ = Task.Run(() => ratelimitTcs.TrySetResult(false)); } /// /// Waits for the initial rate limit. /// /// The bucket. private async Task> WaitForInitialRateLimit(RateLimitBucket bucket) { while (!bucket._limitValid) { if (bucket._limitTesting == 0) { if (Interlocked.CompareExchange(ref bucket._limitTesting, 1, 0) == 0) { // if we got here when the first request was just finishing, we must not create the waiter task as it would signel ExecureRequestAsync to bypass rate limiting if (bucket._limitValid) return null; // allow exactly one request to go through without having rate limits available var ratelimitsTcs = new TaskCompletionSource(); bucket._limitTestFinished = ratelimitsTcs.Task; return ratelimitsTcs; } } // it can take a couple of cycles for the task to be allocated, so wait until it happens or we are no longer probing for the limits Task waitTask = null; while (bucket._limitTesting != 0 && (waitTask = bucket._limitTestFinished) == null) await Task.Yield(); if (waitTask != null) await waitTask.ConfigureAwait(false); // if the request failed and the response did not have rate limit headers we have allow the next request and wait again, thus this is a loop here } return null; } /// /// Builds the request. /// /// The request. /// A http request message. private HttpRequestMessage BuildRequest(BaseRestRequest request) { var req = new HttpRequestMessage(new HttpMethod(request.Method.ToString()), request.Url); if (request.Headers != null && request.Headers.Any()) foreach (var kvp in request.Headers) req.Headers.Add(kvp.Key, kvp.Value); if (request is RestRequest nmprequest && !string.IsNullOrWhiteSpace(nmprequest.Payload)) { this.Logger.LogTrace(LoggerEvents.RestTx, nmprequest.Payload); req.Content = new StringContent(nmprequest.Payload); req.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); } if (request is MultipartWebRequest mprequest) { this.Logger.LogTrace(LoggerEvents.RestTx, ""); var boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x"); req.Headers.Add("Connection", "keep-alive"); req.Headers.Add("Keep-Alive", "600"); var content = new MultipartFormDataContent(boundary); if (mprequest.Values != null && mprequest.Values.Any()) foreach (var kvp in mprequest.Values) content.Add(new StringContent(kvp.Value), kvp.Key); if (mprequest.Files != null && mprequest.Files.Any()) { var i = 1; foreach (var f in mprequest.Files) content.Add(new StreamContent(f.Value), $"file{i++.ToString(CultureInfo.InvariantCulture)}", f.Key); } req.Content = content; } if (request is MultipartStickerWebRequest mpsrequest) { this.Logger.LogTrace(LoggerEvents.RestTx, ""); var boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x"); req.Headers.Add("Connection", "keep-alive"); req.Headers.Add("Keep-Alive", "600"); var sc = new StreamContent(mpsrequest.File.Stream); if (mpsrequest.File.ContentType != null) sc.Headers.ContentType = new MediaTypeHeaderValue(mpsrequest.File.ContentType); var fileName = mpsrequest.File.FileName; if (mpsrequest.File.FileType != null) fileName += '.' + mpsrequest.File.FileType; var content = new MultipartFormDataContent(boundary) { { new StringContent(mpsrequest.Name), "name" }, { new StringContent(mpsrequest.Tags), "tags" }, { new StringContent(mpsrequest.Description), "description" }, { sc, "file", fileName } }; req.Content = content; } return req; } /// /// Handles the http 429 status. /// /// The response. /// The wait task. /// If true, global. private void Handle429(RestResponse response, out Task wait_task, out bool global) { wait_task = null; global = false; if (response.Headers == null) return; var hs = response.Headers; // handle the wait if (hs.TryGetValue("Retry-After", out var retry_after_raw)) { var retry_after = TimeSpan.FromSeconds(int.Parse(retry_after_raw, CultureInfo.InvariantCulture)); wait_task = Task.Delay(retry_after); } // check if global b1nzy if (hs.TryGetValue("X-RateLimit-Global", out var isglobal) && isglobal.ToLowerInvariant() == "true") { // global global = true; } } /// /// Updates the bucket. /// /// The request. /// The response. /// The ratelimit task completion source. private void UpdateBucket(BaseRestRequest request, RestResponse response, TaskCompletionSource ratelimitTcs) { var bucket = request.RateLimitBucket; if (response.Headers == null) { if (response.ResponseCode != 429) // do not fail when ratelimit was or the next request will be scheduled hitting the rate limit again this.FailInitialRateLimitTest(request, ratelimitTcs); return; } var hs = response.Headers; if (hs.TryGetValue("X-RateLimit-Global", out var isglobal) && isglobal.ToLowerInvariant() == "true") { if (response.ResponseCode != 429) this.FailInitialRateLimitTest(request, ratelimitTcs); return; } var r1 = hs.TryGetValue("X-RateLimit-Limit", out var usesmax); var r2 = hs.TryGetValue("X-RateLimit-Remaining", out var usesleft); var r3 = hs.TryGetValue("X-RateLimit-Reset", out var reset); var r4 = hs.TryGetValue("X-Ratelimit-Reset-After", out var resetAfter); var r5 = hs.TryGetValue("X-Ratelimit-Bucket", out var hash); if (!r1 || !r2 || !r3 || !r4) { //If the limits were determined before this request, make the bucket initial again. if (response.ResponseCode != 429) this.FailInitialRateLimitTest(request, ratelimitTcs, ratelimitTcs == null); return; } var clienttime = DateTimeOffset.UtcNow; var resettime = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero).AddSeconds(double.Parse(reset, CultureInfo.InvariantCulture)); var servertime = clienttime; if (hs.TryGetValue("Date", out var raw_date)) servertime = DateTimeOffset.Parse(raw_date, CultureInfo.InvariantCulture).ToUniversalTime(); var resetdelta = resettime - servertime; //var difference = clienttime - servertime; //if (Math.Abs(difference.TotalSeconds) >= 1) //// this.Logger.LogMessage(LogLevel.DebugBaseDiscordClient.RestEventId, $"Difference between machine and server time: {difference.TotalMilliseconds.ToString("#,##0.00", CultureInfo.InvariantCulture)}ms", DateTime.Now); //else // difference = TimeSpan.Zero; if (request.RateLimitWaitOverride.HasValue) resetdelta = TimeSpan.FromSeconds(request.RateLimitWaitOverride.Value); var newReset = clienttime + resetdelta; if (this.UseResetAfter) { bucket.ResetAfter = TimeSpan.FromSeconds(double.Parse(resetAfter, CultureInfo.InvariantCulture)); newReset = clienttime + bucket.ResetAfter.Value + (request.RateLimitWaitOverride.HasValue ? resetdelta : TimeSpan.Zero); bucket.ResetAfterOffset = newReset; } else bucket.Reset = newReset; var maximum = int.Parse(usesmax, CultureInfo.InvariantCulture); var remaining = int.Parse(usesleft, CultureInfo.InvariantCulture); if (ratelimitTcs != null) { // initial population of the ratelimit data bucket.SetInitialValues(maximum, remaining, newReset); _ = Task.Run(() => ratelimitTcs.TrySetResult(true)); } else { // only update the bucket values if this request was for a newer interval than the one // currently in the bucket, to avoid issues with concurrent requests in one bucket // remaining is reset by TryResetLimit and not the response, just allow that to happen when it is time if (bucket._nextReset == 0) bucket._nextReset = newReset.UtcTicks; } this.UpdateHashCaches(request, bucket, hash); } /// /// Updates the hash caches. /// /// The request. /// The bucket. /// The new hash. private void UpdateHashCaches(BaseRestRequest request, RateLimitBucket bucket, string newHash = null) { var hashKey = RateLimitBucket.GenerateHashKey(request.Method, request.Route); if (!this.RoutesToHashes.TryGetValue(hashKey, out var oldHash)) return; // This is an unlimited bucket, which we don't need to keep track of. if (newHash == null) { _ = this.RoutesToHashes.TryRemove(hashKey, out _); _ = this.HashesToBuckets.TryRemove(bucket.BucketId, out _); return; } // Only update the hash once, due to a bug on Discord's end. // This will cause issues if the bucket hashes are dynamically changed from the API while running, // in which case, Dispose will need to be called to clear the caches. if (bucket._isUnlimited && newHash != oldHash) { this.Logger.LogDebug(LoggerEvents.RestHashMover, "Updating hash in {0}: \"{1}\" -> \"{2}\"", hashKey, oldHash, newHash); var bucketId = RateLimitBucket.GenerateBucketId(newHash, bucket.GuildId, bucket.ChannelId, bucket.WebhookId); _ = this.RoutesToHashes.AddOrUpdate(hashKey, newHash, (key, oldHash) => { bucket.Hash = newHash; var oldBucketId = RateLimitBucket.GenerateBucketId(oldHash, bucket.GuildId, bucket.ChannelId, bucket.WebhookId); // Remove the old unlimited bucket. _ = this.HashesToBuckets.TryRemove(oldBucketId, out _); _ = this.HashesToBuckets.AddOrUpdate(bucketId, bucket, (key, oldBucket) => bucket); return newHash; }); } return; } /// /// Cleanups the buckets. /// private async Task CleanupBucketsAsync() { while (!this._bucketCleanerTokenSource.IsCancellationRequested) { try { await Task.Delay(this._bucketCleanupDelay, this._bucketCleanerTokenSource.Token).ConfigureAwait(false); } catch { } if (this._disposed) return; //Check and clean request queue first in case it wasn't removed properly during requests. foreach (var key in this.RequestQueue.Keys) { var bucket = this.HashesToBuckets.Values.FirstOrDefault(x => x.RouteHashes.Contains(key)); if (bucket == null || (bucket != null && bucket.LastAttemptAt.AddSeconds(5) < DateTimeOffset.UtcNow)) _ = this.RequestQueue.TryRemove(key, out _); } var removedBuckets = 0; StringBuilder bucketIdStrBuilder = default; foreach (var kvp in this.HashesToBuckets) { if (bucketIdStrBuilder == null) bucketIdStrBuilder = new StringBuilder(); var key = kvp.Key; var value = kvp.Value; // Don't remove the bucket if it's currently being handled by the rest client, unless it's an unlimited bucket. if (this.RequestQueue.ContainsKey(value.BucketId) && !value._isUnlimited) continue; var resetOffset = this.UseResetAfter ? value.ResetAfterOffset : value.Reset; // Don't remove the bucket if it's reset date is less than now + the additional wait time, unless it's an unlimited bucket. if (resetOffset != null && !value._isUnlimited && (resetOffset > DateTimeOffset.UtcNow || DateTimeOffset.UtcNow - resetOffset < this._bucketCleanupDelay)) continue; _ = this.HashesToBuckets.TryRemove(key, out _); removedBuckets++; bucketIdStrBuilder.Append(value.BucketId + ", "); } if (removedBuckets > 0) this.Logger.LogDebug(LoggerEvents.RestCleaner, "Removed {0} unused bucket{1}: [{2}]", removedBuckets, removedBuckets > 1 ? "s" : string.Empty, bucketIdStrBuilder.ToString().TrimEnd(',', ' ')); if (this.HashesToBuckets.Count == 0) break; } if (!this._bucketCleanerTokenSource.IsCancellationRequested) this._bucketCleanerTokenSource.Cancel(); this._cleanerRunning = false; this.Logger.LogDebug(LoggerEvents.RestCleaner, "Bucket cleaner task stopped."); } ~RestClient() => this.Dispose(); /// /// Disposes the rest client. /// public void Dispose() { if (this._disposed) return; this._disposed = true; this.GlobalRateLimitEvent.Reset(); if (this._bucketCleanerTokenSource?.IsCancellationRequested == false) { this._bucketCleanerTokenSource?.Cancel(); this.Logger.LogDebug(LoggerEvents.RestCleaner, "Bucket cleaner task stopped."); } try { this._cleanerTask?.Dispose(); this._bucketCleanerTokenSource?.Dispose(); this.HttpClient?.Dispose(); } catch { } this.RoutesToHashes.Clear(); this.HashesToBuckets.Clear(); this.RequestQueue.Clear(); } } } diff --git a/DisCatSharp/Utilities.cs b/DisCatSharp/Utilities.cs index 7c1590cd5..cd4f9a95f 100644 --- a/DisCatSharp/Utilities.cs +++ b/DisCatSharp/Utilities.cs @@ -1,465 +1,465 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using DisCatSharp.Entities; using DisCatSharp.Net; using Microsoft.Extensions.Logging; -using static System.Net.WebRequestMethods; namespace DisCatSharp { /// /// Various Discord-related utilities. /// public static class Utilities { /// /// Gets the version of the library /// private static string VersionHeader { get; set; } /// /// Gets or sets the permission strings. /// private static Dictionary PermissionStrings { get; set; } /// /// Gets the utf8 encoding /// internal static UTF8Encoding UTF8 { get; } = new UTF8Encoding(false); /// /// Initializes a new instance of the class. /// static Utilities() { PermissionStrings = new Dictionary(); var t = typeof(Permissions); var ti = t.GetTypeInfo(); var vals = Enum.GetValues(t).Cast(); foreach (var xv in vals) { var xsv = xv.ToString(); var xmv = ti.DeclaredMembers.FirstOrDefault(xm => xm.Name == xsv); var xav = xmv.GetCustomAttribute(); PermissionStrings[xv] = xav.String; } var a = typeof(DiscordClient).GetTypeInfo().Assembly; var vs = ""; var iv = a.GetCustomAttribute(); if (iv != null) vs = iv.InformationalVersion; else { var v = a.GetName().Version; vs = v.ToString(3); } VersionHeader = $"DiscordBot (https://github.com/Aiko-IT-Systems/DisCatSharp, v{vs})"; } /// /// Gets the api base uri. /// + /// The config /// A string. - internal static string GetApiBaseUri(bool canary = false) - => canary ? "https://canary.discord.com/api/v9" : Endpoints.BASE_URI; + internal static string GetApiBaseUri(DiscordConfiguration config) + => config.UseCanary ? Endpoints.CANARY_URI + config.ApiVersion : Endpoints.BASE_URI + config.ApiVersion; /// /// Gets the api uri for. /// /// The path. - /// Use canary + /// The config /// An Uri. - internal static Uri GetApiUriFor(string path, bool canary = false) - => new($"{GetApiBaseUri(canary)}{path}"); + internal static Uri GetApiUriFor(string path, DiscordConfiguration config) + => new($"{GetApiBaseUri(config)}{path}"); /// /// Gets the api uri for. /// /// The path. /// The query string. - /// Use canary + /// The config /// An Uri. - internal static Uri GetApiUriFor(string path, string queryString, bool canary = false) - => new($"{GetApiBaseUri(canary)}{path}{queryString}"); + internal static Uri GetApiUriFor(string path, string queryString, DiscordConfiguration config) + => new($"{GetApiBaseUri(config)}{path}{queryString}"); /// /// Gets the api uri builder for. /// /// The path. - /// Use canary + /// The config /// A QueryUriBuilder. - internal static QueryUriBuilder GetApiUriBuilderFor(string path, bool canary = false) - => new($"{GetApiBaseUri(canary)}{path}"); + internal static QueryUriBuilder GetApiUriBuilderFor(string path, DiscordConfiguration config) + => new($"{GetApiBaseUri(config)}{path}"); /// /// Gets the formatted token. /// /// The client. /// A string. internal static string GetFormattedToken(BaseDiscordClient client) => GetFormattedToken(client.Configuration); /// /// Gets the formatted token. /// /// The config. /// A string. internal static string GetFormattedToken(DiscordConfiguration config) { return config.TokenType switch { TokenType.Bearer => $"Bearer {config.Token}", TokenType.Bot => $"Bot {config.Token}", _ => throw new ArgumentException("Invalid token type specified.", nameof(config.Token)), }; } /// /// Gets the base headers. /// /// A Dictionary. internal static Dictionary GetBaseHeaders() => new(); /// /// Gets the user agent. /// /// A string. internal static string GetUserAgent() => VersionHeader; /// /// Contains the user mentions. /// /// The message. /// A bool. internal static bool ContainsUserMentions(string message) { var pattern = @"<@(\d+)>"; var regex = new Regex(pattern, RegexOptions.ECMAScript); return regex.IsMatch(message); } /// /// Contains the nickname mentions. /// /// The message. /// A bool. internal static bool ContainsNicknameMentions(string message) { var pattern = @"<@!(\d+)>"; var regex = new Regex(pattern, RegexOptions.ECMAScript); return regex.IsMatch(message); } /// /// Contains the channel mentions. /// /// The message. /// A bool. internal static bool ContainsChannelMentions(string message) { var pattern = @"<#(\d+)>"; var regex = new Regex(pattern, RegexOptions.ECMAScript); return regex.IsMatch(message); } /// /// Contains the role mentions. /// /// The message. /// A bool. internal static bool ContainsRoleMentions(string message) { var pattern = @"<@&(\d+)>"; var regex = new Regex(pattern, RegexOptions.ECMAScript); return regex.IsMatch(message); } /// /// Contains the emojis. /// /// The message. /// A bool. internal static bool ContainsEmojis(string message) { var pattern = @""; var regex = new Regex(pattern, RegexOptions.ECMAScript); return regex.IsMatch(message); } /// /// Gets the user mentions. /// /// The message. /// A list of ulong. internal static IEnumerable GetUserMentions(DiscordMessage message) { var regex = new Regex(@"<@!?(\d+)>", RegexOptions.ECMAScript); var matches = regex.Matches(message.Content); foreach (Match match in matches) yield return ulong.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); } /// /// Gets the role mentions. /// /// The message. /// A list of ulong. internal static IEnumerable GetRoleMentions(DiscordMessage message) { var regex = new Regex(@"<@&(\d+)>", RegexOptions.ECMAScript); var matches = regex.Matches(message.Content); foreach (Match match in matches) yield return ulong.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); } /// /// Gets the channel mentions. /// /// The message. /// A list of ulong. internal static IEnumerable GetChannelMentions(DiscordMessage message) { var regex = new Regex(@"<#(\d+)>", RegexOptions.ECMAScript); var matches = regex.Matches(message.Content); foreach (Match match in matches) yield return ulong.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); } /// /// Gets the emojis. /// /// The message. /// A list of ulong. internal static IEnumerable GetEmojis(DiscordMessage message) { var regex = new Regex(@"", RegexOptions.ECMAScript); var matches = regex.Matches(message.Content); foreach (Match match in matches) yield return ulong.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture); } /// /// Are the valid slash command name. /// /// The name. /// A bool. internal static bool IsValidSlashCommandName(string name) { var regex = new Regex(@"^[\w-]{1,32}$", RegexOptions.ECMAScript); return regex.IsMatch(name); } /// /// Checks the thread auto archive duration feature. /// /// The guild. /// The taad. /// A bool. internal static bool CheckThreadAutoArchiveDurationFeature(DiscordGuild guild, ThreadAutoArchiveDuration taad) { return taad == ThreadAutoArchiveDuration.ThreeDays ? (guild.PremiumTier.HasFlag(PremiumTier.Tier_1) || guild.Features.CanSetThreadArchiveDurationThreeDays) : taad != ThreadAutoArchiveDuration.OneWeek || guild.PremiumTier.HasFlag(PremiumTier.Tier_2) || guild.Features.CanSetThreadArchiveDurationSevenDays; } /// /// Checks the thread private feature. /// /// The guild. /// A bool. internal static bool CheckThreadPrivateFeature(DiscordGuild guild) => guild.PremiumTier.HasFlag(PremiumTier.Tier_2) || guild.Features.CanCreatePrivateThreads; /// /// Have the message intents. /// /// The intents. /// A bool. internal static bool HasMessageIntents(DiscordIntents intents) => intents.HasIntent(DiscordIntents.GuildMessages) || intents.HasIntent(DiscordIntents.DirectMessages); /// /// Have the reaction intents. /// /// The intents. /// A bool. internal static bool HasReactionIntents(DiscordIntents intents) => intents.HasIntent(DiscordIntents.GuildMessageReactions) || intents.HasIntent(DiscordIntents.DirectMessageReactions); /// /// Have the typing intents. /// /// The intents. /// A bool. internal static bool HasTypingIntents(DiscordIntents intents) => intents.HasIntent(DiscordIntents.GuildMessageTyping) || intents.HasIntent(DiscordIntents.DirectMessageTyping); // https://discord.com/developers/docs/topics/gateway#sharding-sharding-formula /// /// Gets a shard id from a guild id and total shard count. /// /// The guild id the shard is on. /// The total amount of shards. /// The shard id. public static int GetShardId(ulong guildId, int shardCount) => (int)(guildId >> 22) % shardCount; /// /// Helper method to create a from Unix time seconds for targets that do not support this natively. /// /// Unix time seconds to convert. /// Whether the method should throw on failure. Defaults to true. /// Calculated . public static DateTimeOffset GetDateTimeOffset(long unixTime, bool shouldThrow = true) { try { return DateTimeOffset.FromUnixTimeSeconds(unixTime); } catch (Exception) { if (shouldThrow) throw; return DateTimeOffset.MinValue; } } /// /// Helper method to create a from Unix time milliseconds for targets that do not support this natively. /// /// Unix time milliseconds to convert. /// Whether the method should throw on failure. Defaults to true. /// Calculated . public static DateTimeOffset GetDateTimeOffsetFromMilliseconds(long unixTime, bool shouldThrow = true) { try { return DateTimeOffset.FromUnixTimeMilliseconds(unixTime); } catch (Exception) { if (shouldThrow) throw; return DateTimeOffset.MinValue; } } /// /// Helper method to calculate Unix time seconds from a for targets that do not support this natively. /// /// to calculate Unix time for. /// Calculated Unix time. public static long GetUnixTime(DateTimeOffset dto) => dto.ToUnixTimeMilliseconds(); /// /// Computes a timestamp from a given snowflake. /// /// Snowflake to compute a timestamp from. /// Computed timestamp. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static DateTimeOffset GetSnowflakeTime(this ulong snowflake) => DiscordClient._discordEpoch.AddMilliseconds(snowflake >> 22); /// /// Converts this into human-readable format. /// /// Permissions enumeration to convert. /// Human-readable permissions. public static string ToPermissionString(this Permissions perm) { if (perm == Permissions.None) return PermissionStrings[perm]; perm &= PermissionMethods.FULL_PERMS; var strs = PermissionStrings .Where(xkvp => xkvp.Key != Permissions.None && (perm & xkvp.Key) == xkvp.Key) .Select(xkvp => xkvp.Value); return string.Join(", ", strs.OrderBy(xs => xs)); } /// /// Checks whether this string contains given characters. /// /// String to check. /// Characters to check for. /// Whether the string contained these characters. public static bool Contains(this string str, params char[] characters) { foreach (var xc in str) if (characters.Contains(xc)) return true; return false; } /// /// Logs the task fault. /// /// The task. /// The logger. /// The level. /// The event id. /// The message. internal static void LogTaskFault(this Task task, ILogger logger, LogLevel level, EventId eventId, string message) { if (task == null) throw new ArgumentNullException(nameof(task)); if (logger == null) return; task.ContinueWith(t => logger.Log(level, eventId, t.Exception, message), TaskContinuationOptions.OnlyOnFaulted); } /// /// Deconstructs the. /// /// The kvp. /// The key. /// The value. internal static void Deconstruct(this KeyValuePair kvp, out TKey key, out TValue value) { key = kvp.Key; value = kvp.Value; } } }