From 88ea2ed9bab6d03a4431d6f040dd14e4b3d19bb6 Mon Sep 17 00:00:00 2001 From: Mihail Gribkov <61027276+Misha-133@users.noreply.github.com> Date: Sat, 14 Sep 2024 00:28:48 +0300 Subject: [PATCH] more monetizatios stuffz (#3002) * uhhhhhhhhhhhhhhhhhhh yes * uhhhhhhhhhhhhhhhhhhh yes * ~~i love git~~ --- src/Discord.Net.Core/DiscordConfig.cs | 5 + .../AppSubscriptions/ISubscription.cs | 56 + .../Entities/AppSubscriptions/SKU.cs | 8 +- .../Entities/AppSubscriptions/SKUFlags.cs | 5 + .../AppSubscriptions/SubscriptionStatus.cs | 19 + src/Discord.Net.Core/IDiscordClient.cs | 13 +- .../API/Common/Entitlement.cs | 2 +- src/Discord.Net.Rest/API/Common/SKU.cs | 3 + .../API/Common/Subscription.cs | 34 + src/Discord.Net.Rest/BaseDiscordClient.cs | 13 +- src/Discord.Net.Rest/ClientHelper.cs | 36 +- src/Discord.Net.Rest/DiscordRestApiClient.cs | 18 + src/Discord.Net.Rest/DiscordRestClient.cs | 17 + .../AppSubscriptions/RestEntitlement.cs | 4 +- .../AppSubscriptions/RestSubscription.cs | 61 + .../BaseSocketClient.Events.cs | 36 + src/Discord.Net.WebSocket/ClientState.cs | 27 + .../DiscordSocketClient.EventHandling.cs | 2531 +++++++++++++++++ .../DiscordSocketClient.cs | 2487 +--------------- .../AppSubscriptions/SocketSubscription.cs | 64 + 20 files changed, 2973 insertions(+), 2466 deletions(-) create mode 100644 src/Discord.Net.Core/Entities/AppSubscriptions/ISubscription.cs create mode 100644 src/Discord.Net.Core/Entities/AppSubscriptions/SubscriptionStatus.cs create mode 100644 src/Discord.Net.Rest/API/Common/Subscription.cs create mode 100644 src/Discord.Net.Rest/Entities/AppSubscriptions/RestSubscription.cs create mode 100644 src/Discord.Net.WebSocket/DiscordSocketClient.EventHandling.cs create mode 100644 src/Discord.Net.WebSocket/Entities/AppSubscriptions/SocketSubscription.cs diff --git a/src/Discord.Net.Core/DiscordConfig.cs b/src/Discord.Net.Core/DiscordConfig.cs index c98b7c44d8..36c4b71e31 100644 --- a/src/Discord.Net.Core/DiscordConfig.cs +++ b/src/Discord.Net.Core/DiscordConfig.cs @@ -265,6 +265,11 @@ public class DiscordConfig /// public const int MaxEntitlementsPerBatch = 100; + /// + /// Returns the maximum number of subscriptions that can be gotten per-batch. + /// + public const int MaxSubscriptionsPerBatch = 100; + /// /// Returns the maximum number of poll answer voters that can be gotten per-batch. /// diff --git a/src/Discord.Net.Core/Entities/AppSubscriptions/ISubscription.cs b/src/Discord.Net.Core/Entities/AppSubscriptions/ISubscription.cs new file mode 100644 index 0000000000..06118fd63d --- /dev/null +++ b/src/Discord.Net.Core/Entities/AppSubscriptions/ISubscription.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; + +namespace Discord; + +/// +/// Represents a subscription object. +/// +public interface ISubscription : ISnowflakeEntity +{ + /// + /// Gets the ID of the user who is subscribed. + /// + ulong UserId { get; } + + /// + /// Gets the SKUs subscribed to. + /// + IReadOnlyCollection SKUIds { get; } + + /// + /// Gets the entitlements granted for this subscription. + /// + IReadOnlyCollection EntitlementIds { get; } + + /// + /// Gets the start of the current subscription period. + /// + DateTimeOffset CurrentPeriodStart { get; } + + /// + /// Gets end of the current subscription period. + /// + DateTimeOffset CurrentPeriodEnd { get; } + + /// + /// Gets the current status of the subscription. + /// + SubscriptionStatus Status { get; } + + /// + /// Gets when the subscription was canceled. + /// + /// + /// if the subscription has not been canceled. + /// + DateTimeOffset? CanceledAt { get; } + + /// + /// Gets country code of the payment source used to purchase the subscription. + /// + /// + /// Requires an oauth scope. + /// + string Country { get; } +} diff --git a/src/Discord.Net.Core/Entities/AppSubscriptions/SKU.cs b/src/Discord.Net.Core/Entities/AppSubscriptions/SKU.cs index 338b14ea0f..e0e33a5351 100644 --- a/src/Discord.Net.Core/Entities/AppSubscriptions/SKU.cs +++ b/src/Discord.Net.Core/Entities/AppSubscriptions/SKU.cs @@ -30,12 +30,18 @@ public struct SKU : ISnowflakeEntity /// public string Slug { get; } - internal SKU(ulong id, SKUType type, ulong applicationId, string name, string slug) + /// + /// Gets the flags for this SKU. + /// + public SKUFlags Flags { get; } + + internal SKU(ulong id, SKUType type, ulong applicationId, string name, string slug, SKUFlags flags) { Id = id; Type = type; ApplicationId = applicationId; Name = name; Slug = slug; + Flags = flags; } } diff --git a/src/Discord.Net.Core/Entities/AppSubscriptions/SKUFlags.cs b/src/Discord.Net.Core/Entities/AppSubscriptions/SKUFlags.cs index 09e5a90ae4..74b24e7098 100644 --- a/src/Discord.Net.Core/Entities/AppSubscriptions/SKUFlags.cs +++ b/src/Discord.Net.Core/Entities/AppSubscriptions/SKUFlags.cs @@ -8,6 +8,11 @@ namespace Discord; [Flags] public enum SKUFlags { + /// + /// The SKU is available for purchase. + /// + IsAvailable = 1 << 2, + /// /// The SKU is a guild subscription. /// diff --git a/src/Discord.Net.Core/Entities/AppSubscriptions/SubscriptionStatus.cs b/src/Discord.Net.Core/Entities/AppSubscriptions/SubscriptionStatus.cs new file mode 100644 index 0000000000..8362433436 --- /dev/null +++ b/src/Discord.Net.Core/Entities/AppSubscriptions/SubscriptionStatus.cs @@ -0,0 +1,19 @@ +namespace Discord; + +public enum SubscriptionStatus +{ + /// + /// Subscription is active and scheduled to renew. + /// + Active = 0, + + /// + /// Subscription is active but will not renew. + /// + Ending = 1, + + /// + /// Subscription is inactive and not being charged. + /// + Inactive = 2 +} diff --git a/src/Discord.Net.Core/IDiscordClient.cs b/src/Discord.Net.Core/IDiscordClient.cs index 43e77020c1..431d58168e 100644 --- a/src/Discord.Net.Core/IDiscordClient.cs +++ b/src/Discord.Net.Core/IDiscordClient.cs @@ -341,7 +341,7 @@ public interface IDiscordClient : IDisposable, IAsyncDisposable /// /// Returns all entitlements for a given app, active and expired. /// - IAsyncEnumerable> GetEntitlementsAsync(int? limit = 100, + IAsyncEnumerable> GetEntitlementsAsync(int limit = 100, ulong? afterId = null, ulong? beforeId = null, bool excludeEnded = false, ulong? guildId = null, ulong? userId = null, ulong[] skuIds = null, RequestOptions options = null); @@ -357,6 +357,17 @@ IAsyncEnumerable> GetEntitlementsAsync(int? li /// The options to be used when sending the request. Task ConsumeEntitlementAsync(ulong entitlementId, RequestOptions options = null); + /// + /// Returns all subscriptions for a given SKU. + /// + IAsyncEnumerable> GetSKUSubscriptionsAsync(ulong skuId, int limit = 100, ulong? afterId = null, + ulong? beforeId = null, ulong? userId = null, RequestOptions options = null); + + /// + /// Gets a subscription by its id. + /// + Task GetSKUSubscriptionAsync(ulong skuId, ulong subscriptionId, RequestOptions options = null); + /// /// Gets an emote for the current application. /// diff --git a/src/Discord.Net.Rest/API/Common/Entitlement.cs b/src/Discord.Net.Rest/API/Common/Entitlement.cs index 01acabd685..218f902687 100644 --- a/src/Discord.Net.Rest/API/Common/Entitlement.cs +++ b/src/Discord.Net.Rest/API/Common/Entitlement.cs @@ -30,5 +30,5 @@ internal class Entitlement public Optional StartsAt { get; set; } [JsonProperty("ends_at")] - public Optional EndsAt { get; set; } + public Optional EndsAt { get; set; } } diff --git a/src/Discord.Net.Rest/API/Common/SKU.cs b/src/Discord.Net.Rest/API/Common/SKU.cs index dd3bb483ff..dd2876e35a 100644 --- a/src/Discord.Net.Rest/API/Common/SKU.cs +++ b/src/Discord.Net.Rest/API/Common/SKU.cs @@ -18,4 +18,7 @@ internal class SKU [JsonProperty("slug")] public string Slug { get; set; } + + [JsonProperty("flags")] + public SKUFlags Flags { get; set; } } diff --git a/src/Discord.Net.Rest/API/Common/Subscription.cs b/src/Discord.Net.Rest/API/Common/Subscription.cs new file mode 100644 index 0000000000..cbcb1b075b --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/Subscription.cs @@ -0,0 +1,34 @@ +using Newtonsoft.Json; +using System; + +namespace Discord.API; + +internal class Subscription +{ + [JsonProperty("id")] + public ulong Id { get; set; } + + [JsonProperty("user_id")] + public ulong UserId { get; set; } + + [JsonProperty("sku_ids")] + public ulong[] SKUIds { get; set; } + + [JsonProperty("entitlement_ids")] + public ulong[] EntitlementIds { get; set; } + + [JsonProperty("current_period_start")] + public DateTimeOffset CurrentPeriodStart { get; set; } + + [JsonProperty("current_period_end")] + public DateTimeOffset CurrentPeriodEnd { get; set; } + + [JsonProperty("status")] + public SubscriptionStatus Status { get; set; } + + [JsonProperty("canceled_at")] + public DateTimeOffset? CanceledAt { get; set; } + + [JsonProperty("country")] + public string Country { get; set; } +} diff --git a/src/Discord.Net.Rest/BaseDiscordClient.cs b/src/Discord.Net.Rest/BaseDiscordClient.cs index 8b95d8b321..c8c18cb920 100644 --- a/src/Discord.Net.Rest/BaseDiscordClient.cs +++ b/src/Discord.Net.Rest/BaseDiscordClient.cs @@ -286,7 +286,7 @@ Task IDiscordClient.DeleteTestEntitlementAsync(ulong entitlementId, RequestOptio /// /// Returns all entitlements for a given app. /// - IAsyncEnumerable> IDiscordClient.GetEntitlementsAsync(int? limit, ulong? afterId, ulong? beforeId, + IAsyncEnumerable> IDiscordClient.GetEntitlementsAsync(int limit, ulong? afterId, ulong? beforeId, bool excludeEnded, ulong? guildId, ulong? userId, ulong[] skuIds, RequestOptions options) => AsyncEnumerable.Empty>(); /// @@ -299,6 +299,17 @@ IAsyncEnumerable> IDiscordClient.GetEntitlemen /// Task IDiscordClient.ConsumeEntitlementAsync(ulong entitlementId, RequestOptions options) => Task.CompletedTask; + /// + /// Returns all subscriptions for a given SKU. + /// + IAsyncEnumerable> IDiscordClient.GetSKUSubscriptionsAsync(ulong skuId, int limit, ulong? afterId, + ulong? beforeId, ulong? userId, RequestOptions options) => AsyncEnumerable.Empty>(); + + /// + /// Gets a subscription by its id. + /// + Task IDiscordClient.GetSKUSubscriptionAsync(ulong skuId, ulong subscriptionId, RequestOptions options) => Task.FromResult(null); + /// Task IDiscordClient.GetApplicationEmoteAsync(ulong emoteId, RequestOptions options) => Task.FromResult(null); diff --git a/src/Discord.Net.Rest/ClientHelper.cs b/src/Discord.Net.Rest/ClientHelper.cs index 6f7654da8d..752b63e5f2 100644 --- a/src/Discord.Net.Rest/ClientHelper.cs +++ b/src/Discord.Net.Rest/ClientHelper.cs @@ -443,12 +443,46 @@ public static async Task> ListSKUsAsync(BaseDiscordClie { var models = await client.ApiClient.ListSKUsAsync(options).ConfigureAwait(false); - return models.Select(x => new SKU(x.Id, x.Type, x.ApplicationId, x.Name, x.Slug)).ToImmutableArray(); + return models.Select(x => new SKU(x.Id, x.Type, x.ApplicationId, x.Name, x.Slug, x.Flags)).ToImmutableArray(); } public static Task ConsumeEntitlementAsync(BaseDiscordClient client, ulong entitlementId, RequestOptions options = null) => client.ApiClient.ConsumeEntitlementAsync(entitlementId, options); + public static async Task GetSKUSubscriptionAsync(BaseDiscordClient client, ulong skuId, ulong subscriptionId, RequestOptions options = null) + { + var model = await client.ApiClient.GetSKUSubscriptionAsync(skuId, subscriptionId, options); + + return RestSubscription.Create(client, model); + } + + public static IAsyncEnumerable> ListSubscriptionsAsync(BaseDiscordClient client, ulong skuId, int limit = 100, + ulong? afterId = null, ulong? beforeId = null, ulong? userId = null, RequestOptions options = null) + { + return new PagedAsyncEnumerable( + DiscordConfig.MaxSubscriptionsPerBatch, + async (info, ct) => + { + var _afterId = afterId; + if (info.Position != null) + _afterId = info.Position.Value; + var models = await client.ApiClient.ListSKUSubscriptionsAsync(skuId, beforeId, _afterId, limit, userId, options).ConfigureAwait(false); + return models + .Select(x => RestSubscription.Create(client, x)) + .ToImmutableArray(); + }, + nextPage: (info, lastPage) => + { + if (lastPage.Count != DiscordConfig.MaxSubscriptionsPerBatch) + return false; + info.Position = lastPage.Max(x => x.Id); + return true; + }, + start: afterId, + count: limit + ); + } + #endregion #region Application Emojis diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index 36558d3772..4c138523a3 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -2852,6 +2852,24 @@ public Task ListSKUsAsync(RequestOptions options = null) public Task ConsumeEntitlementAsync(ulong entitlementId, RequestOptions options = null) => SendAsync("POST", () => $"applications/{CurrentApplicationId}/entitlements/{entitlementId}/consume", new BucketIds(), options: options); + public Task GetSKUSubscriptionAsync(ulong skuId, ulong subscriptionId, RequestOptions options = null) + => SendAsync("GET", () => $"skus/{skuId}/subscriptions/{subscriptionId}", new BucketIds(), options: options); + + public Task ListSKUSubscriptionsAsync(ulong skuId, ulong? before = null, ulong? after = null, int limit = 100, ulong? userId = null, RequestOptions options = null) + { + Preconditions.AtMost(100, limit, "Limit must be less or equal to 100."); + Preconditions.AtLeast(1, limit, "Limit must be greater or equal to 1."); + + var args = $"?limit={limit}"; + if (before is not null) + args += $"&before={before}"; + if (after is not null) + args += $"&after={after}"; + if (userId is not null) + args += $"&user_id={userId}"; + + return SendAsync("GET", () => $"skus/{skuId}/subscriptions{args}", new BucketIds(), options: options); + } #endregion #region Polls diff --git a/src/Discord.Net.Rest/DiscordRestClient.cs b/src/Discord.Net.Rest/DiscordRestClient.cs index 404e84af1c..e2392b0dd7 100644 --- a/src/Discord.Net.Rest/DiscordRestClient.cs +++ b/src/Discord.Net.Rest/DiscordRestClient.cs @@ -300,6 +300,15 @@ public Task> GetSKUsAsync(RequestOptions options = null public Task ConsumeEntitlementAsync(ulong entitlementId, RequestOptions options = null) => ClientHelper.ConsumeEntitlementAsync(this, entitlementId, options); + /// + public Task GetSKUSubscriptionAsync(ulong skuId, ulong subscriptionId, RequestOptions options = null) + => ClientHelper.GetSKUSubscriptionAsync(this, skuId, subscriptionId, options); + + /// + public IAsyncEnumerable> GetSKUSubscriptionsAsync(ulong skuId, int limit = 100, ulong? afterId = null, + ulong? beforeId = null, ulong? userId = null, RequestOptions options = null) + => ClientHelper.ListSubscriptionsAsync(this, skuId, limit, afterId, beforeId, userId, options); + /// public Task GetApplicationEmoteAsync(ulong emoteId, RequestOptions options = null) => ClientHelper.GetApplicationEmojiAsync(this, emoteId, options); @@ -330,6 +339,14 @@ async Task IDiscordClient.CreateTestEntitlementAsync(ulong skuId, async Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) => await GetApplicationInfoAsync(options).ConfigureAwait(false); + /// + async Task IDiscordClient.GetSKUSubscriptionAsync(ulong skuId, ulong subscriptionId, RequestOptions options) + => await GetSKUSubscriptionAsync(skuId, subscriptionId, options); + + /// + IAsyncEnumerable> IDiscordClient.GetSKUSubscriptionsAsync(ulong skuId, int limit, ulong? afterId, + ulong? beforeId, ulong? userId, RequestOptions options) => GetSKUSubscriptionsAsync(skuId, limit, afterId, beforeId, userId, options); + /// async Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) { diff --git a/src/Discord.Net.Rest/Entities/AppSubscriptions/RestEntitlement.cs b/src/Discord.Net.Rest/Entities/AppSubscriptions/RestEntitlement.cs index 5c7f70d8f4..e5fb4ea338 100644 --- a/src/Discord.Net.Rest/Entities/AppSubscriptions/RestEntitlement.cs +++ b/src/Discord.Net.Rest/Entities/AppSubscriptions/RestEntitlement.cs @@ -6,8 +6,8 @@ namespace Discord.Rest; public class RestEntitlement : RestEntity, IEntitlement { - /// - public DateTimeOffset CreatedAt { get; private set; } + /// + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); /// public ulong SkuId { get; private set; } diff --git a/src/Discord.Net.Rest/Entities/AppSubscriptions/RestSubscription.cs b/src/Discord.Net.Rest/Entities/AppSubscriptions/RestSubscription.cs new file mode 100644 index 0000000000..afae876c5a --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AppSubscriptions/RestSubscription.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Discord.Rest; + +/// +/// Represents an application subscription. +/// +public class RestSubscription : RestEntity, ISubscription +{ + /// + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + + /// + public ulong UserId { get; private set; } + + /// + public IReadOnlyCollection SKUIds { get; private set; } + + /// + public IReadOnlyCollection EntitlementIds { get; private set; } + + /// + public DateTimeOffset CurrentPeriodStart { get; private set; } + + /// + public DateTimeOffset CurrentPeriodEnd { get; private set; } + + /// + public SubscriptionStatus Status { get; private set; } + + /// + public DateTimeOffset? CanceledAt { get; private set; } + + /// + public string Country { get; private set; } + + internal RestSubscription(BaseDiscordClient discord, ulong id) : base(discord, id) + { + } + + internal static RestSubscription Create(BaseDiscordClient discord, API.Subscription model) + { + var s = new RestSubscription(discord, model.Id); + s.Update(model); + return s; + } + + internal void Update(API.Subscription model) + { + UserId = model.UserId; + SKUIds = model.SKUIds.ToImmutableArray(); + EntitlementIds = model.EntitlementIds.ToImmutableArray(); + CurrentPeriodStart = model.CurrentPeriodStart; + CurrentPeriodEnd = model.CurrentPeriodEnd; + Status = model.Status; + CanceledAt = model.CanceledAt; + Country = model.Country; + } +} diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs index 06154b0f2a..328f09ed00 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs @@ -1032,6 +1032,42 @@ public event Func, Task> EntitlementDeleted internal readonly AsyncEvent, Task>> _entitlementDeleted = new(); + + /// + /// Fired when a user subscribes to a SKU. + /// + public event Func SubscriptionCreated + { + add { _subscriptionCreated.Add(value); } + remove { _subscriptionCreated.Remove(value); } + } + + internal readonly AsyncEvent> _subscriptionCreated = new(); + + + /// + /// Fired when a subscription to a SKU is updated. + /// + public event Func, SocketSubscription, Task> SubscriptionUpdated + { + add { _subscriptionUpdated.Add(value); } + remove { _subscriptionUpdated.Remove(value); } + } + + internal readonly AsyncEvent, SocketSubscription, Task>> _subscriptionUpdated = new(); + + + /// + /// Fired when a user's subscription is deleted. + /// + public event Func, Task> SubscriptionDeleted + { + add { _subscriptionDeleted.Add(value); } + remove { _subscriptionDeleted.Remove(value); } + } + + internal readonly AsyncEvent, Task>> _subscriptionDeleted = new(); + #endregion } } diff --git a/src/Discord.Net.WebSocket/ClientState.cs b/src/Discord.Net.WebSocket/ClientState.cs index 745d55814c..8bd1fd7fc2 100644 --- a/src/Discord.Net.WebSocket/ClientState.cs +++ b/src/Discord.Net.WebSocket/ClientState.cs @@ -18,6 +18,7 @@ internal class ClientState private readonly ConcurrentHashSet _groupChannels; private readonly ConcurrentDictionary _commands; private readonly ConcurrentDictionary _entitlements; + private readonly ConcurrentDictionary _subscriptions; internal IReadOnlyCollection Channels => _channels.ToReadOnlyCollection(); internal IReadOnlyCollection DMChannels => _dmChannels.ToReadOnlyCollection(); @@ -26,6 +27,7 @@ internal class ClientState internal IReadOnlyCollection Users => _users.ToReadOnlyCollection(); internal IReadOnlyCollection Commands => _commands.ToReadOnlyCollection(); internal IReadOnlyCollection Entitlements => _entitlements.ToReadOnlyCollection(); + internal IReadOnlyCollection Subscriptions => _subscriptions.ToReadOnlyCollection(); internal IReadOnlyCollection PrivateChannels => _dmChannels.Select(x => x.Value as ISocketPrivateChannel).Concat( @@ -43,6 +45,7 @@ public ClientState(int guildCount, int dmChannelCount) _groupChannels = new ConcurrentHashSet(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(10 * CollectionMultiplier)); _commands = new ConcurrentDictionary(); _entitlements = new(); + _subscriptions = new(); } internal SocketChannel GetChannel(ulong id) @@ -197,5 +200,29 @@ internal SocketEntitlement RemoveEntitlement(ulong id) return entitlement; return null; } + + internal void AddSubscription(ulong id, SocketSubscription subscription) + { + _subscriptions.TryAdd(id, subscription); + } + + internal SocketSubscription GetSubscription(ulong id) + { + if (_subscriptions.TryGetValue(id, out var subscription)) + return subscription; + return null; + } + + internal SocketSubscription GetOrAddSubscription(ulong id, Func subscriptionFactory) + { + return _subscriptions.GetOrAdd(id, subscriptionFactory); + } + + internal SocketSubscription RemoveSubscription(ulong id) + { + if (_subscriptions.TryRemove(id, out var subscription)) + return subscription; + return null; + } } } diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.EventHandling.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.EventHandling.cs new file mode 100644 index 0000000000..2758cb32c3 --- /dev/null +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.EventHandling.cs @@ -0,0 +1,2531 @@ +using Discord.API; +using Discord.API.Gateway; +using Discord.Rest; +using Discord.Utils; +using Newtonsoft.Json.Linq; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using System; + +using GameModel = Discord.API.Game; + +namespace Discord.WebSocket; + +public partial class DiscordSocketClient +{ + + private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string type, object payload) + { + if (seq != null) + _lastSeq = seq.Value; + _lastMessageTime = Environment.TickCount; + + try + { + switch (opCode) + { + case GatewayOpCode.Hello: + { + await _gatewayLogger.DebugAsync("Received Hello").ConfigureAwait(false); + var data = (payload as JToken).ToObject(_serializer); + + _heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _connection.CancelToken); + } + break; + case GatewayOpCode.Heartbeat: + { + await _gatewayLogger.DebugAsync("Received Heartbeat").ConfigureAwait(false); + + await ApiClient.SendHeartbeatAsync(_lastSeq).ConfigureAwait(false); + } + break; + case GatewayOpCode.HeartbeatAck: + { + await _gatewayLogger.DebugAsync("Received HeartbeatAck").ConfigureAwait(false); + + if (_heartbeatTimes.TryDequeue(out long time)) + { + int latency = (int)(Environment.TickCount - time); + int before = Latency; + Latency = latency; + + await TimedInvokeAsync(_latencyUpdatedEvent, nameof(LatencyUpdated), before, latency).ConfigureAwait(false); + } + } + break; + case GatewayOpCode.InvalidSession: + { + await _gatewayLogger.DebugAsync("Received InvalidSession").ConfigureAwait(false); + await _gatewayLogger.WarningAsync("Failed to resume previous session").ConfigureAwait(false); + + _sessionId = null; + _lastSeq = 0; + ApiClient.ResumeGatewayUrl = null; + + if (_shardedClient != null) + { + await _shardedClient.AcquireIdentifyLockAsync(ShardId, _connection.CancelToken).ConfigureAwait(false); + try + { + await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards, gatewayIntents: _gatewayIntents, presence: BuildCurrentStatus()).ConfigureAwait(false); + } + finally + { + _shardedClient.ReleaseIdentifyLock(); + } + } + else + await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards, gatewayIntents: _gatewayIntents, presence: BuildCurrentStatus()).ConfigureAwait(false); + } + break; + case GatewayOpCode.Reconnect: + { + await _gatewayLogger.DebugAsync("Received Reconnect").ConfigureAwait(false); + _connection.Error(new GatewayReconnectException("Server requested a reconnect")); + } + break; + case GatewayOpCode.Dispatch: + switch (type) + { + #region Connection + case "READY": + { + try + { + await _gatewayLogger.DebugAsync("Received Dispatch (READY)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var state = new ClientState(data.Guilds.Length, data.PrivateChannels.Length); + + var currentUser = SocketSelfUser.Create(this, state, data.User); + Rest.CreateRestSelfUser(data.User); + var activities = _activity.IsSpecified ? ImmutableList.Create(_activity.Value) : null; + currentUser.Presence = new SocketPresence(Status, null, activities); + ApiClient.CurrentUserId = currentUser.Id; + ApiClient.CurrentApplicationId = data.Application?.Id; + Rest.CurrentUser = RestSelfUser.Create(this, data.User); + int unavailableGuilds = 0; + for (int i = 0; i < data.Guilds.Length; i++) + { + var model = data.Guilds[i]; + var guild = AddGuild(model, state); + if (!guild.IsAvailable) + unavailableGuilds++; + else + await GuildAvailableAsync(guild).ConfigureAwait(false); + } + for (int i = 0; i < data.PrivateChannels.Length; i++) + AddPrivateChannel(data.PrivateChannels[i], state); + + _sessionId = data.SessionId; + ApiClient.ResumeGatewayUrl = data.ResumeGatewayUrl; + _unavailableGuildCount = unavailableGuilds; + CurrentUser = currentUser; + _previousSessionUser = CurrentUser; + State = state; + } + catch (Exception ex) + { + _connection.CriticalError(new Exception("Processing READY failed", ex)); + return; + } + + _lastGuildAvailableTime = Environment.TickCount; + _guildDownloadTask = WaitForGuildsAsync(_connection.CancelToken, _gatewayLogger) + .ContinueWith(async x => + { + if (x.IsFaulted) + { + _connection.Error(x.Exception); + return; + } + else if (_connection.CancelToken.IsCancellationRequested) + return; + + if (BaseConfig.AlwaysDownloadUsers) + try + { + _ = DownloadUsersAsync(Guilds.Where(x => x.IsAvailable && !x.HasAllMembers)); + } + catch (Exception ex) + { + await _gatewayLogger.WarningAsync(ex); + } + + await TimedInvokeAsync(_readyEvent, nameof(Ready)).ConfigureAwait(false); + await _gatewayLogger.InfoAsync("Ready").ConfigureAwait(false); + }); + _ = _connection.CompleteAsync(); + } + break; + case "RESUMED": + { + await _gatewayLogger.DebugAsync("Received Dispatch (RESUMED)").ConfigureAwait(false); + + _ = _connection.CompleteAsync(); + + //Notify the client that these guilds are available again + foreach (var guild in State.Guilds) + { + if (guild.IsAvailable) + await GuildAvailableAsync(guild).ConfigureAwait(false); + } + + // Restore the previous sessions current user + CurrentUser = _previousSessionUser; + + await _gatewayLogger.InfoAsync("Resumed previous session").ConfigureAwait(false); + } + break; + #endregion + + #region Guilds + case "GUILD_CREATE": + { + var data = (payload as JToken).ToObject(_serializer); + + if (data.Unavailable == false) + { + type = "GUILD_AVAILABLE"; + _lastGuildAvailableTime = Environment.TickCount; + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_AVAILABLE)").ConfigureAwait(false); + + var guild = State.GetGuild(data.Id); + if (guild != null) + { + guild.Update(State, data); + + if (_unavailableGuildCount != 0) + _unavailableGuildCount--; + await GuildAvailableAsync(guild).ConfigureAwait(false); + + if (guild.DownloadedMemberCount >= guild.MemberCount && !guild.DownloaderPromise.IsCompleted) + { + guild.CompleteDownloadUsers(); + await TimedInvokeAsync(_guildMembersDownloadedEvent, nameof(GuildMembersDownloaded), guild).ConfigureAwait(false); + } + } + else + { + await UnknownGuildAsync(type, data.Id).ConfigureAwait(false); + return; + } + } + else + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_CREATE)").ConfigureAwait(false); + + var guild = AddGuild(data, State); + if (guild != null) + { + await TimedInvokeAsync(_joinedGuildEvent, nameof(JoinedGuild), guild).ConfigureAwait(false); + await GuildAvailableAsync(guild).ConfigureAwait(false); + } + else + { + await UnknownGuildAsync(type, data.Id).ConfigureAwait(false); + return; + } + } + } + break; + case "GUILD_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.Id); + if (guild != null) + { + var before = guild.Clone(); + guild.Update(State, data); + await TimedInvokeAsync(_guildUpdatedEvent, nameof(GuildUpdated), before, guild).ConfigureAwait(false); + } + else + { + await UnknownGuildAsync(type, data.Id).ConfigureAwait(false); + return; + } + } + break; + case "GUILD_EMOJIS_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_EMOJIS_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.GuildId); + if (guild != null) + { + var before = guild.Clone(); + guild.Update(State, data); + await TimedInvokeAsync(_guildUpdatedEvent, nameof(GuildUpdated), before, guild).ConfigureAwait(false); + } + else + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + } + break; + case "GUILD_SYNC": + { + await _gatewayLogger.DebugAsync("Ignored Dispatch (GUILD_SYNC)").ConfigureAwait(false); + /*await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_SYNC)").ConfigureAwait(false); //TODO remove? userbot related + var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.Id); + if (guild != null) + { + var before = guild.Clone(); + guild.Update(State, data); + //This is treated as an extension of GUILD_AVAILABLE + _unavailableGuildCount--; + _lastGuildAvailableTime = Environment.TickCount; + await GuildAvailableAsync(guild).ConfigureAwait(false); + await TimedInvokeAsync(_guildUpdatedEvent, nameof(GuildUpdated), before, guild).ConfigureAwait(false); + } + else + { + await UnknownGuildAsync(type, data.Id).ConfigureAwait(false); + return; + }*/ + } + break; + case "GUILD_DELETE": + { + var data = (payload as JToken).ToObject(_serializer); + if (data.Unavailable == true) + { + type = "GUILD_UNAVAILABLE"; + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_UNAVAILABLE)").ConfigureAwait(false); + + var guild = State.GetGuild(data.Id); + if (guild != null) + { + await GuildUnavailableAsync(guild).ConfigureAwait(false); + _unavailableGuildCount++; + } + else + { + await UnknownGuildAsync(type, data.Id).ConfigureAwait(false); + return; + } + } + else + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_DELETE)").ConfigureAwait(false); + + var guild = RemoveGuild(data.Id); + if (guild != null) + { + await GuildUnavailableAsync(guild).ConfigureAwait(false); + await TimedInvokeAsync(_leftGuildEvent, nameof(LeftGuild), guild).ConfigureAwait(false); + (guild as IDisposable).Dispose(); + } + else + { + await UnknownGuildAsync(type, data.Id).ConfigureAwait(false); + return; + } + } + } + break; + case "GUILD_STICKERS_UPDATE": + { + await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_STICKERS_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + var guild = State.GetGuild(data.GuildId); + + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + + var newStickers = data.Stickers.Where(x => !guild.Stickers.Any(y => y.Id == x.Id)); + var deletedStickers = guild.Stickers.Where(x => !data.Stickers.Any(y => y.Id == x.Id)); + var updatedStickers = data.Stickers.Select(x => + { + var s = guild.Stickers.FirstOrDefault(y => y.Id == x.Id); + if (s == null) + return null; + + var e = s.Equals(x); + if (!e) + { + return (s, x) as (SocketCustomSticker Entity, API.Sticker Model)?; + } + else + { + return null; + } + }).Where(x => x.HasValue).Select(x => x.Value).ToArray(); + + foreach (var model in newStickers) + { + var entity = guild.AddSticker(model); + await TimedInvokeAsync(_guildStickerCreated, nameof(GuildStickerCreated), entity); + } + foreach (var sticker in deletedStickers) + { + var entity = guild.RemoveSticker(sticker.Id); + await TimedInvokeAsync(_guildStickerDeleted, nameof(GuildStickerDeleted), entity); + } + foreach (var entityModelPair in updatedStickers) + { + var before = entityModelPair.Entity.Clone(); + + entityModelPair.Entity.Update(entityModelPair.Model); + + await TimedInvokeAsync(_guildStickerUpdated, nameof(GuildStickerUpdated), before, entityModelPair.Entity); + } + } + break; + #endregion + + #region Channels + case "CHANNEL_CREATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_CREATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + SocketChannel channel = null; + if (data.GuildId.IsSpecified) + { + var guild = State.GetGuild(data.GuildId.Value); + if (guild != null) + { + channel = guild.AddChannel(State, data); + + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + } + else + { + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + return; + } + } + else + { + channel = State.GetChannel(data.Id); + if (channel != null) + return; //Discord may send duplicate CHANNEL_CREATEs for DMs + channel = AddPrivateChannel(data, State) as SocketChannel; + } + + if (channel != null) + await TimedInvokeAsync(_channelCreatedEvent, nameof(ChannelCreated), channel).ConfigureAwait(false); + } + break; + case "CHANNEL_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var channel = State.GetChannel(data.Id); + if (channel != null) + { + var before = channel.Clone(); + channel.Update(State, data); + + var guild = (channel as SocketGuildChannel)?.Guild; + if (!(guild?.IsSynced ?? true)) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + await TimedInvokeAsync(_channelUpdatedEvent, nameof(ChannelUpdated), before, channel).ConfigureAwait(false); + } + else + { + await UnknownChannelAsync(type, data.Id).ConfigureAwait(false); + return; + } + } + break; + case "CHANNEL_DELETE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_DELETE)").ConfigureAwait(false); + + SocketChannel channel = null; + var data = (payload as JToken).ToObject(_serializer); + if (data.GuildId.IsSpecified) + { + var guild = State.GetGuild(data.GuildId.Value); + if (guild != null) + { + channel = guild.RemoveChannel(State, data.Id); + + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + } + else + { + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + return; + } + } + else + channel = RemovePrivateChannel(data.Id) as SocketChannel; + + if (channel != null) + await TimedInvokeAsync(_channelDestroyedEvent, nameof(ChannelDestroyed), channel).ConfigureAwait(false); + else + { + await UnknownChannelAsync(type, data.Id, data.GuildId.GetValueOrDefault(0)).ConfigureAwait(false); + return; + } + } + break; + #endregion + + #region Members + case "GUILD_MEMBER_ADD": + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBER_ADD)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.GuildId); + if (guild != null) + { + var user = guild.AddOrUpdateUser(data); + guild.MemberCount++; + + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + await TimedInvokeAsync(_userJoinedEvent, nameof(UserJoined), user).ConfigureAwait(false); + } + else + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + } + break; + case "GUILD_MEMBER_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBER_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.GuildId); + if (guild != null) + { + var user = guild.GetUser(data.User.Id); + + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + if (user != null) + { + var before = user.Clone(); + if (user.GlobalUser.Update(State, data.User)) + { + //Global data was updated, trigger UserUpdated + await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), before.GlobalUser, user).ConfigureAwait(false); + } + + user.Update(State, data); + + var cacheableBefore = new Cacheable(before, user.Id, true, () => Task.FromResult(null)); + await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), cacheableBefore, user).ConfigureAwait(false); + } + else + { + user = guild.AddOrUpdateUser(data); + var cacheableBefore = new Cacheable(null, user.Id, false, () => Task.FromResult(null)); + await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), cacheableBefore, user).ConfigureAwait(false); + } + } + else + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + } + break; + case "GUILD_MEMBER_REMOVE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBER_REMOVE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.GuildId); + if (guild != null) + { + SocketUser user = guild.RemoveUser(data.User.Id); + guild.MemberCount--; + + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + user ??= State.GetUser(data.User.Id); + + if (user != null) + user.Update(State, data.User); + else + user = State.GetOrAddUser(data.User.Id, (x) => SocketGlobalUser.Create(this, State, data.User)); + + await TimedInvokeAsync(_userLeftEvent, nameof(UserLeft), guild, user).ConfigureAwait(false); + } + else + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + } + break; + case "GUILD_MEMBERS_CHUNK": + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBERS_CHUNK)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.GuildId); + if (guild != null) + { + foreach (var memberModel in data.Members) + guild.AddOrUpdateUser(memberModel); + + if (guild.DownloadedMemberCount >= guild.MemberCount && !guild.DownloaderPromise.IsCompleted) + { + guild.CompleteDownloadUsers(); + await TimedInvokeAsync(_guildMembersDownloadedEvent, nameof(GuildMembersDownloaded), guild).ConfigureAwait(false); + } + } + else + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + } + break; + case "GUILD_JOIN_REQUEST_DELETE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_JOIN_REQUEST_DELETE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + var guild = State.GetGuild(data.GuildId); + + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + + var user = guild.RemoveUser(data.UserId); + guild.MemberCount--; + + var cacheableUser = new Cacheable(user, data.UserId, user != null, () => Task.FromResult((SocketGuildUser)null)); + + await TimedInvokeAsync(_guildJoinRequestDeletedEvent, nameof(GuildJoinRequestDeleted), cacheableUser, guild).ConfigureAwait(false); + } + break; + #endregion + + #region DM Channels + + case "CHANNEL_RECIPIENT_ADD": + { + await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_RECIPIENT_ADD)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + if (State.GetChannel(data.ChannelId) is SocketGroupChannel channel) + { + var user = channel.GetOrAddUser(data.User); + await TimedInvokeAsync(_recipientAddedEvent, nameof(RecipientAdded), user).ConfigureAwait(false); + } + else + { + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); + return; + } + } + break; + case "CHANNEL_RECIPIENT_REMOVE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_RECIPIENT_REMOVE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + if (State.GetChannel(data.ChannelId) is SocketGroupChannel channel) + { + var user = channel.RemoveUser(data.User.Id); + if (user != null) + await TimedInvokeAsync(_recipientRemovedEvent, nameof(RecipientRemoved), user).ConfigureAwait(false); + else + { + await UnknownChannelUserAsync(type, data.User.Id, data.ChannelId).ConfigureAwait(false); + return; + } + } + else + { + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); + return; + } + } + break; + + #endregion + + #region Roles + case "GUILD_ROLE_CREATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_ROLE_CREATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.GuildId); + if (guild != null) + { + var role = guild.AddRole(data.Role); + + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + await TimedInvokeAsync(_roleCreatedEvent, nameof(RoleCreated), role).ConfigureAwait(false); + } + else + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + } + break; + case "GUILD_ROLE_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_ROLE_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.GuildId); + if (guild != null) + { + var role = guild.GetRole(data.Role.Id); + if (role != null) + { + var before = role.Clone(); + role.Update(State, data.Role); + + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + await TimedInvokeAsync(_roleUpdatedEvent, nameof(RoleUpdated), before, role).ConfigureAwait(false); + } + else + { + await UnknownRoleAsync(type, data.Role.Id, guild.Id).ConfigureAwait(false); + return; + } + } + else + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + } + break; + case "GUILD_ROLE_DELETE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_ROLE_DELETE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.GuildId); + if (guild != null) + { + var role = guild.RemoveRole(data.RoleId); + if (role != null) + { + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + await TimedInvokeAsync(_roleDeletedEvent, nameof(RoleDeleted), role).ConfigureAwait(false); + } + else + { + await UnknownRoleAsync(type, data.RoleId, guild.Id).ConfigureAwait(false); + return; + } + } + else + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + } + break; + #endregion + + #region Bans + case "GUILD_BAN_ADD": + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_BAN_ADD)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.GuildId); + if (guild != null) + { + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + SocketUser user = guild.GetUser(data.User.Id); + if (user == null) + user = SocketUnknownUser.Create(this, State, data.User); + await TimedInvokeAsync(_userBannedEvent, nameof(UserBanned), user, guild).ConfigureAwait(false); + } + else + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + } + break; + case "GUILD_BAN_REMOVE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_BAN_REMOVE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.GuildId); + if (guild != null) + { + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + SocketUser user = State.GetUser(data.User.Id); + if (user == null) + user = SocketUnknownUser.Create(this, State, data.User); + await TimedInvokeAsync(_userUnbannedEvent, nameof(UserUnbanned), user, guild).ConfigureAwait(false); + } + else + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + } + break; + #endregion + + #region Messages + case "MESSAGE_CREATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_CREATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; + + var guild = (channel as SocketGuildChannel)?.Guild; + if (guild != null && !guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + if (channel == null) + { + if (!data.GuildId.IsSpecified) // assume it is a DM + { + channel = CreateDMChannel(data.ChannelId, data.Author.Value, State); + } + else + { + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); + return; + } + } + + SocketUser author; + if (guild != null) + { + if (data.WebhookId.IsSpecified) + author = SocketWebhookUser.Create(guild, State, data.Author.Value, data.WebhookId.Value); + else + author = guild.GetUser(data.Author.Value.Id); + } + else + author = (channel as SocketChannel).GetUser(data.Author.Value.Id); + + if (author == null) + { + if (guild != null) + { + if (data.Member.IsSpecified) // member isn't always included, but use it when we can + { + data.Member.Value.User = data.Author.Value; + author = guild.AddOrUpdateUser(data.Member.Value); + } + else + author = guild.AddOrUpdateUser(data.Author.Value); // user has no guild-specific data + } + else if (channel is SocketGroupChannel groupChannel) + author = groupChannel.GetOrAddUser(data.Author.Value); + else + { + await UnknownChannelUserAsync(type, data.Author.Value.Id, channel.Id).ConfigureAwait(false); + return; + } + } + + var msg = SocketMessage.Create(this, State, author, channel, data); + SocketChannelHelper.AddMessage(channel, this, msg); + await TimedInvokeAsync(_messageReceivedEvent, nameof(MessageReceived), msg).ConfigureAwait(false); + } + break; + case "MESSAGE_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; + + var guild = (channel as SocketGuildChannel)?.Guild; + if (guild != null && !guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + SocketMessage before = null, after = null; + SocketMessage cachedMsg = channel?.GetCachedMessage(data.Id); + bool isCached = cachedMsg != null; + if (isCached) + { + before = cachedMsg.Clone(); + cachedMsg.Update(State, data); + after = cachedMsg; + } + else + { + //Edited message isn't in cache, create a detached one + SocketUser author; + if (data.Author.IsSpecified) + { + if (guild != null) + { + if (data.WebhookId.IsSpecified) + author = SocketWebhookUser.Create(guild, State, data.Author.Value, data.WebhookId.Value); + else + author = guild.GetUser(data.Author.Value.Id); + } + else + author = (channel as SocketChannel)?.GetUser(data.Author.Value.Id); + + if (author == null) + { + if (guild != null) + { + if (data.Member.IsSpecified) // member isn't always included, but use it when we can + { + data.Member.Value.User = data.Author.Value; + author = guild.AddOrUpdateUser(data.Member.Value); + } + else + author = guild.AddOrUpdateUser(data.Author.Value); // user has no guild-specific data + } + else if (channel is SocketGroupChannel groupChannel) + author = groupChannel.GetOrAddUser(data.Author.Value); + } + } + else + // Message author wasn't specified in the payload, so create a completely anonymous unknown user + author = new SocketUnknownUser(this, id: 0); + + if (channel == null) + { + if (!data.GuildId.IsSpecified) // assume it is a DM + { + if (data.Author.IsSpecified) + { + var dmChannel = CreateDMChannel(data.ChannelId, data.Author.Value, State); + channel = dmChannel; + author = dmChannel.Recipient; + } + else + channel = CreateDMChannel(data.ChannelId, author, State); + } + else + { + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); + return; + } + } + + after = SocketMessage.Create(this, State, author, channel, data); + } + var cacheableBefore = new Cacheable(before, data.Id, isCached, async () => await channel.GetMessageAsync(data.Id).ConfigureAwait(false)); + + await TimedInvokeAsync(_messageUpdatedEvent, nameof(MessageUpdated), cacheableBefore, after, channel).ConfigureAwait(false); + } + break; + case "MESSAGE_DELETE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_DELETE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; + + var guild = (channel as SocketGuildChannel)?.Guild; + if (!(guild?.IsSynced ?? true)) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + SocketMessage msg = null; + if (channel != null) + msg = SocketChannelHelper.RemoveMessage(channel, this, data.Id); + var cacheableMsg = new Cacheable(msg, data.Id, msg != null, () => Task.FromResult((IMessage)null)); + var cacheableChannel = new Cacheable(channel, data.ChannelId, channel != null, async () => await GetChannelAsync(data.ChannelId).ConfigureAwait(false) as IMessageChannel); + + await TimedInvokeAsync(_messageDeletedEvent, nameof(MessageDeleted), cacheableMsg, cacheableChannel).ConfigureAwait(false); + } + break; + case "MESSAGE_REACTION_ADD": + { + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_ADD)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; + + var cachedMsg = channel?.GetCachedMessage(data.MessageId) as SocketUserMessage; + bool isMsgCached = cachedMsg != null; + IUser user = null; + if (channel != null) + user = await channel.GetUserAsync(data.UserId, CacheMode.CacheOnly).ConfigureAwait(false); + + var optionalMsg = !isMsgCached + ? Optional.Create() + : Optional.Create(cachedMsg); + + if (data.Member.IsSpecified) + { + var guild = (channel as SocketGuildChannel)?.Guild; + + if (guild != null) + user = guild.AddOrUpdateUser(data.Member.Value); + } + else + user = GetUser(data.UserId); + + var optionalUser = user is null + ? Optional.Create() + : Optional.Create(user); + + var cacheableChannel = new Cacheable(channel, data.ChannelId, channel != null, async () => await GetChannelAsync(data.ChannelId).ConfigureAwait(false) as IMessageChannel); + var cacheableMsg = new Cacheable(cachedMsg, data.MessageId, isMsgCached, async () => + { + var channelObj = await cacheableChannel.GetOrDownloadAsync().ConfigureAwait(false); + return await channelObj.GetMessageAsync(data.MessageId).ConfigureAwait(false) as IUserMessage; + }); + var reaction = SocketReaction.Create(data, channel, optionalMsg, optionalUser); + + cachedMsg?.AddReaction(reaction); + + await TimedInvokeAsync(_reactionAddedEvent, nameof(ReactionAdded), cacheableMsg, cacheableChannel, reaction).ConfigureAwait(false); + } + break; + case "MESSAGE_REACTION_REMOVE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_REMOVE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; + + var cachedMsg = channel?.GetCachedMessage(data.MessageId) as SocketUserMessage; + bool isMsgCached = cachedMsg != null; + IUser user = null; + if (channel != null) + user = await channel.GetUserAsync(data.UserId, CacheMode.CacheOnly).ConfigureAwait(false); + else if (!data.GuildId.IsSpecified) + user = GetUser(data.UserId); + + var optionalMsg = !isMsgCached + ? Optional.Create() + : Optional.Create(cachedMsg); + + var optionalUser = user is null + ? Optional.Create() + : Optional.Create(user); + + var cacheableChannel = new Cacheable(channel, data.ChannelId, channel != null, async () => await GetChannelAsync(data.ChannelId).ConfigureAwait(false) as IMessageChannel); + var cacheableMsg = new Cacheable(cachedMsg, data.MessageId, isMsgCached, async () => + { + var channelObj = await cacheableChannel.GetOrDownloadAsync().ConfigureAwait(false); + return await channelObj.GetMessageAsync(data.MessageId).ConfigureAwait(false) as IUserMessage; + }); + var reaction = SocketReaction.Create(data, channel, optionalMsg, optionalUser); + + cachedMsg?.RemoveReaction(reaction); + + await TimedInvokeAsync(_reactionRemovedEvent, nameof(ReactionRemoved), cacheableMsg, cacheableChannel, reaction).ConfigureAwait(false); + } + break; + case "MESSAGE_REACTION_REMOVE_ALL": + { + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_REMOVE_ALL)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; + + var cacheableChannel = new Cacheable(channel, data.ChannelId, channel != null, async () => await GetChannelAsync(data.ChannelId).ConfigureAwait(false) as IMessageChannel); + var cachedMsg = channel?.GetCachedMessage(data.MessageId) as SocketUserMessage; + bool isMsgCached = cachedMsg != null; + var cacheableMsg = new Cacheable(cachedMsg, data.MessageId, isMsgCached, async () => + { + var channelObj = await cacheableChannel.GetOrDownloadAsync().ConfigureAwait(false); + return await channelObj.GetMessageAsync(data.MessageId).ConfigureAwait(false) as IUserMessage; + }); + + cachedMsg?.ClearReactions(); + + await TimedInvokeAsync(_reactionsClearedEvent, nameof(ReactionsCleared), cacheableMsg, cacheableChannel).ConfigureAwait(false); + } + break; + case "MESSAGE_REACTION_REMOVE_EMOJI": + { + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_REMOVE_EMOJI)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; + + var cachedMsg = channel?.GetCachedMessage(data.MessageId) as SocketUserMessage; + bool isMsgCached = cachedMsg != null; + + var optionalMsg = !isMsgCached + ? Optional.Create() + : Optional.Create(cachedMsg); + + var cacheableChannel = new Cacheable(channel, data.ChannelId, channel != null, async () => await GetChannelAsync(data.ChannelId).ConfigureAwait(false) as IMessageChannel); + var cacheableMsg = new Cacheable(cachedMsg, data.MessageId, isMsgCached, async () => + { + var channelObj = await cacheableChannel.GetOrDownloadAsync().ConfigureAwait(false); + return await channelObj.GetMessageAsync(data.MessageId).ConfigureAwait(false) as IUserMessage; + }); + var emote = data.Emoji.ToIEmote(); + + cachedMsg?.RemoveReactionsForEmote(emote); + + await TimedInvokeAsync(_reactionsRemovedForEmoteEvent, nameof(ReactionsRemovedForEmote), cacheableMsg, cacheableChannel, emote).ConfigureAwait(false); + } + break; + case "MESSAGE_DELETE_BULK": + { + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_DELETE_BULK)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; + + var guild = (channel as SocketGuildChannel)?.Guild; + if (!(guild?.IsSynced ?? true)) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + var cacheableChannel = new Cacheable(channel, data.ChannelId, channel != null, async () => await GetChannelAsync(data.ChannelId).ConfigureAwait(false) as IMessageChannel); + var cacheableList = new List>(data.Ids.Length); + foreach (ulong id in data.Ids) + { + SocketMessage msg = null; + if (channel != null) + msg = SocketChannelHelper.RemoveMessage(channel, this, id); + bool isMsgCached = msg != null; + var cacheableMsg = new Cacheable(msg, id, isMsgCached, () => Task.FromResult((IMessage)null)); + cacheableList.Add(cacheableMsg); + } + + await TimedInvokeAsync(_messagesBulkDeletedEvent, nameof(MessagesBulkDeleted), cacheableList, cacheableChannel).ConfigureAwait(false); + } + break; + #endregion + + #region Polls + + case "MESSAGE_POLL_VOTE_ADD": + { + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_POLL_VOTE_ADD)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + Cacheable? guildCacheable = null; + + Cacheable userCacheable; + Cacheable channelCacheable; + Cacheable messageCacheable; + + if (data.GuildId.IsSpecified) + { + var guild = State.GetGuild(data.GuildId.Value); + guildCacheable = new(guild, data.GuildId.Value, guild is not null, () => Rest.GetGuildAsync(data.GuildId.Value)); + + if (guild is not null) + { + var user = guild.GetUser(data.UserId); + userCacheable = new(user, data.UserId, user is not null, async () => await Rest.GetGuildUserAsync(data.GuildId.Value, data.UserId)); + + var channel = guild.GetTextChannel(data.ChannelId); + channelCacheable = new(channel, data.ChannelId, channel is not null, async () => (RestTextChannel)await Rest.GetChannelAsync(data.ChannelId)); + + var message = channel?.GetCachedMessage(data.MessageId) as IUserMessage; + messageCacheable = new(message, data.MessageId, message is not null, + async () => (channel ?? (ITextChannel)await Rest.GetChannelAsync(data.ChannelId)).GetMessageAsync(data.MessageId) as IUserMessage); + } + else + { + userCacheable = new(null, data.UserId, false, async () => await Rest.GetGuildUserAsync(data.GuildId.Value, data.UserId)); + channelCacheable = new(null, data.ChannelId, false, async () => (RestTextChannel)(await Rest.GetChannelAsync(data.ChannelId))); + messageCacheable = new(null, data.MessageId, false, + async () => await ((ITextChannel)await Rest.GetChannelAsync(data.ChannelId)).GetMessageAsync(data.MessageId) as IUserMessage); + } + } + else + { + var user = State.GetUser(data.UserId); + userCacheable = new(user, data.UserId, user is not null, async () => await GetUserAsync(data.UserId)); + + var channel = State.GetChannel(data.ChannelId) as ISocketMessageChannel; + channelCacheable = new(channel, data.ChannelId, channel is not null, async () => await Rest.GetDMChannelAsync(data.ChannelId) as IRestMessageChannel); + + var message = channel?.GetCachedMessage(data.MessageId) as IUserMessage; + messageCacheable = new(message, data.MessageId, message is not null, async () => await (channel ?? (IMessageChannel)await Rest.GetDMChannelAsync(data.ChannelId)).GetMessageAsync(data.MessageId) as IUserMessage); + } + + await TimedInvokeAsync(_pollVoteAdded, nameof(PollVoteAdded), userCacheable, channelCacheable, messageCacheable, guildCacheable, data.AnswerId); + } + break; + + case "MESSAGE_POLL_VOTE_REMOVE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_POLL_VOTE_REMOVE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + Cacheable? guildCacheable = null; + + Cacheable userCacheable; + Cacheable channelCacheable; + Cacheable messageCacheable; + + if (data.GuildId.IsSpecified) + { + var guild = State.GetGuild(data.GuildId.Value); + guildCacheable = new(guild, data.GuildId.Value, guild is not null, () => Rest.GetGuildAsync(data.GuildId.Value)); + + if (guild is not null) + { + var user = guild.GetUser(data.UserId); + userCacheable = new(user, data.UserId, user is not null, async () => await Rest.GetGuildUserAsync(data.GuildId.Value, data.UserId)); + + var channel = guild.GetTextChannel(data.ChannelId); + channelCacheable = new(channel, data.ChannelId, channel is not null, async () => (RestTextChannel)await Rest.GetChannelAsync(data.ChannelId)); + + var message = channel?.GetCachedMessage(data.MessageId) as IUserMessage; + messageCacheable = new(message, data.MessageId, message is not null, + async () => (channel ?? (ITextChannel)await Rest.GetChannelAsync(data.ChannelId)).GetMessageAsync(data.MessageId) as IUserMessage); + } + else + { + userCacheable = new(null, data.UserId, false, async () => await Rest.GetGuildUserAsync(data.GuildId.Value, data.UserId)); + channelCacheable = new(null, data.ChannelId, false, async () => (RestTextChannel)(await Rest.GetChannelAsync(data.ChannelId))); + messageCacheable = new(null, data.MessageId, false, + async () => await ((ITextChannel)await Rest.GetChannelAsync(data.ChannelId)).GetMessageAsync(data.MessageId) as IUserMessage); + } + } + else + { + var user = State.GetUser(data.UserId); + userCacheable = new(user, data.UserId, user is not null, async () => await GetUserAsync(data.UserId)); + + var channel = State.GetChannel(data.ChannelId) as ISocketMessageChannel; + channelCacheable = new(channel, data.ChannelId, channel is not null, async () => await Rest.GetDMChannelAsync(data.ChannelId) as IRestMessageChannel); + + var message = channel?.GetCachedMessage(data.MessageId) as IUserMessage; + messageCacheable = new(message, data.MessageId, message is not null, async () => await (channel ?? (IMessageChannel)await Rest.GetDMChannelAsync(data.ChannelId)).GetMessageAsync(data.MessageId) as IUserMessage); + } + + await TimedInvokeAsync(_pollVoteRemoved, nameof(PollVoteRemoved), userCacheable, channelCacheable, messageCacheable, guildCacheable, data.AnswerId); + } + break; + + #endregion + + #region Statuses + case "PRESENCE_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (PRESENCE_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + SocketUser user = null; + + if (data.GuildId.IsSpecified) + { + var guild = State.GetGuild(data.GuildId.Value); + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + return; + } + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + user = guild.GetUser(data.User.Id); + if (user == null) + { + if (data.Status == UserStatus.Offline) + { + return; + } + user = guild.AddOrUpdateUser(data); + } + else + { + var globalBefore = user.GlobalUser.Clone(); + if (user.GlobalUser.Update(State, data.User)) + { + //Global data was updated, trigger UserUpdated + await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), globalBefore, user).ConfigureAwait(false); + } + } + } + else + { + user = State.GetUser(data.User.Id); + if (user == null) + { + await UnknownGlobalUserAsync(type, data.User.Id).ConfigureAwait(false); + return; + } + } + + var before = user.Presence?.Clone(); + user.Update(State, data.User); + user.Update(data); + await TimedInvokeAsync(_presenceUpdated, nameof(PresenceUpdated), user, before, user.Presence).ConfigureAwait(false); + } + break; + case "TYPING_START": + { + await _gatewayLogger.DebugAsync("Received Dispatch (TYPING_START)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; + + var guild = (channel as SocketGuildChannel)?.Guild; + if (!(guild?.IsSynced ?? true)) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + var cacheableChannel = new Cacheable(channel, data.ChannelId, channel != null, async () => await GetChannelAsync(data.ChannelId).ConfigureAwait(false) as IMessageChannel); + + var user = (channel as SocketChannel)?.GetUser(data.UserId); + if (user == null) + { + if (guild != null && data.Member.IsSpecified) + user = guild.AddOrUpdateUser(data.Member.Value); + } + var cacheableUser = new Cacheable(user, data.UserId, user != null, async () => await GetUserAsync(data.UserId).ConfigureAwait(false)); + + await TimedInvokeAsync(_userIsTypingEvent, nameof(UserIsTyping), cacheableUser, cacheableChannel).ConfigureAwait(false); + } + break; + #endregion + + #region Integrations + case "INTEGRATION_CREATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (INTEGRATION_CREATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + // Integrations from Gateway should always have guild IDs specified. + if (!data.GuildId.IsSpecified) + return; + + var guild = State.GetGuild(data.GuildId.Value); + + if (guild != null) + { + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + await TimedInvokeAsync(_integrationCreated, nameof(IntegrationCreated), RestIntegration.Create(this, guild, data)).ConfigureAwait(false); + } + else + { + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + return; + } + } + break; + case "INTEGRATION_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (INTEGRATION_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + // Integrations from Gateway should always have guild IDs specified. + if (!data.GuildId.IsSpecified) + return; + + var guild = State.GetGuild(data.GuildId.Value); + + if (guild != null) + { + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + await TimedInvokeAsync(_integrationUpdated, nameof(IntegrationUpdated), RestIntegration.Create(this, guild, data)).ConfigureAwait(false); + } + else + { + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + return; + } + } + break; + case "INTEGRATION_DELETE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (INTEGRATION_DELETE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + var guild = State.GetGuild(data.GuildId); + + if (guild != null) + { + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + await TimedInvokeAsync(_integrationDeleted, nameof(IntegrationDeleted), guild, data.Id, data.ApplicationID).ConfigureAwait(false); + } + else + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + } + break; + #endregion + + #region Users + case "USER_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (USER_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + if (data.Id == CurrentUser.Id) + { + var before = CurrentUser.Clone(); + CurrentUser.Update(State, data); + await TimedInvokeAsync(_selfUpdatedEvent, nameof(CurrentUserUpdated), before, CurrentUser).ConfigureAwait(false); + } + else + { + await _gatewayLogger.WarningAsync("Received USER_UPDATE for wrong user.").ConfigureAwait(false); + return; + } + } + break; + #endregion + + #region Voice + case "VOICE_STATE_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (VOICE_STATE_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + SocketUser user; + SocketVoiceState before, after; + if (data.GuildId != null) + { + var guild = State.GetGuild(data.GuildId.Value); + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + return; + } + else if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + if (data.ChannelId != null) + { + before = guild.GetVoiceState(data.UserId)?.Clone() ?? SocketVoiceState.Default; + after = await guild.AddOrUpdateVoiceStateAsync(State, data).ConfigureAwait(false); + /*if (data.UserId == CurrentUser.Id) + { + var _ = guild.FinishJoinAudioChannel().ConfigureAwait(false); + }*/ + } + else + { + before = await guild.RemoveVoiceStateAsync(data.UserId).ConfigureAwait(false) ?? SocketVoiceState.Default; + after = SocketVoiceState.Create(null, data); + } + + //Per g250k, this should always be sent, but apparently not always + user = guild.GetUser(data.UserId) + ?? (data.Member.IsSpecified ? guild.AddOrUpdateUser(data.Member.Value) : null); + if (user == null) + { + await UnknownGuildUserAsync(type, data.UserId, guild.Id).ConfigureAwait(false); + return; + } + } + else + { + var groupChannel = GetChannel(data.ChannelId.Value) as SocketGroupChannel; + if (groupChannel == null) + { + await UnknownChannelAsync(type, data.ChannelId.Value).ConfigureAwait(false); + return; + } + if (data.ChannelId != null) + { + before = groupChannel.GetVoiceState(data.UserId)?.Clone() ?? SocketVoiceState.Default; + after = groupChannel.AddOrUpdateVoiceState(State, data); + } + else + { + before = groupChannel.RemoveVoiceState(data.UserId) ?? SocketVoiceState.Default; + after = SocketVoiceState.Create(null, data); + } + user = groupChannel.GetUser(data.UserId); + if (user == null) + { + await UnknownChannelUserAsync(type, data.UserId, groupChannel.Id).ConfigureAwait(false); + return; + } + } + + if (user is SocketGuildUser guildUser && data.ChannelId.HasValue) + { + SocketStageChannel stage = guildUser.Guild.GetStageChannel(data.ChannelId.Value); + + if (stage != null && before.VoiceChannel != null && after.VoiceChannel != null) + { + if (!before.RequestToSpeakTimestamp.HasValue && after.RequestToSpeakTimestamp.HasValue) + { + await TimedInvokeAsync(_requestToSpeak, nameof(RequestToSpeak), stage, guildUser); + return; + } + if (before.IsSuppressed && !after.IsSuppressed) + { + await TimedInvokeAsync(_speakerAdded, nameof(SpeakerAdded), stage, guildUser); + return; + } + if (!before.IsSuppressed && after.IsSuppressed) + { + await TimedInvokeAsync(_speakerRemoved, nameof(SpeakerRemoved), stage, guildUser); + } + } + } + + await TimedInvokeAsync(_userVoiceStateUpdatedEvent, nameof(UserVoiceStateUpdated), user, before, after).ConfigureAwait(false); + } + break; + case "VOICE_SERVER_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (VOICE_SERVER_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.GuildId); + var isCached = guild != null; + var cachedGuild = new Cacheable(guild, data.GuildId, isCached, + () => Task.FromResult(State.GetGuild(data.GuildId) as IGuild)); + + var voiceServer = new SocketVoiceServer(cachedGuild, data.Endpoint, data.Token); + await TimedInvokeAsync(_voiceServerUpdatedEvent, nameof(UserVoiceStateUpdated), voiceServer).ConfigureAwait(false); + + if (isCached) + { + var endpoint = data.Endpoint; + + //Only strip out the port if the endpoint contains it + var portBegin = endpoint.LastIndexOf(':'); + if (portBegin > 0) + endpoint = endpoint.Substring(0, portBegin); + + var _ = guild.FinishConnectAudio(endpoint, data.Token).ConfigureAwait(false); + } + else + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + } + + } + break; + + case "VOICE_CHANNEL_STATUS_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (VOICE_CHANNEL_STATUS_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.GuildId); + + var channel = State.GetChannel(data.Id) as SocketVoiceChannel; + var channelCacheable = new Cacheable(channel, data.Id, channel is not null, () => null); + + var before = (string)channel?.Status?.Clone(); + var after = data.Status; + channel?.UpdateVoiceStatus(data.Status); + + await TimedInvokeAsync(_voiceChannelStatusUpdated, nameof(VoiceChannelStatusUpdated), channelCacheable, before, after); + } + break; + #endregion + + #region Invites + case "INVITE_CREATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (INVITE_CREATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + if (State.GetChannel(data.ChannelId) is SocketGuildChannel channel) + { + var guild = channel.Guild; + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + SocketGuildUser inviter = data.Inviter.IsSpecified + ? (guild.GetUser(data.Inviter.Value.Id) ?? guild.AddOrUpdateUser(data.Inviter.Value)) + : null; + + SocketUser target = data.TargetUser.IsSpecified + ? (guild.GetUser(data.TargetUser.Value.Id) ?? (SocketUser)SocketUnknownUser.Create(this, State, data.TargetUser.Value)) + : null; + + var invite = SocketInvite.Create(this, guild, channel, inviter, target, data); + + await TimedInvokeAsync(_inviteCreatedEvent, nameof(InviteCreated), invite).ConfigureAwait(false); + } + else + { + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); + return; + } + } + break; + case "INVITE_DELETE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (INVITE_DELETE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + if (State.GetChannel(data.ChannelId) is SocketGuildChannel channel) + { + var guild = channel.Guild; + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + await TimedInvokeAsync(_inviteDeletedEvent, nameof(InviteDeleted), channel, data.Code).ConfigureAwait(false); + } + else + { + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); + return; + } + } + break; + #endregion + + #region Interactions + case "INTERACTION_CREATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (INTERACTION_CREATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + var guild = data.GuildId.IsSpecified ? GetGuild(data.GuildId.Value) : null; + + if (guild != null && !guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + } + + SocketUser user = data.User.IsSpecified + ? State.GetOrAddUser(data.User.Value.Id, (_) => SocketGlobalUser.Create(this, State, data.User.Value)) + : guild != null + ? guild.AddOrUpdateUser(data.Member.Value) // null if the bot scope isn't set, so the guild cannot be retrieved. + : State.GetOrAddUser(data.Member.Value.User.Id, (_) => SocketGlobalUser.Create(this, State, data.Member.Value.User)); + + SocketChannel channel = null; + if (data.ChannelId.IsSpecified) + { + channel = State.GetChannel(data.ChannelId.Value); + + if (channel == null) + { + if (!data.GuildId.IsSpecified) // assume it is a DM + { + channel = CreateDMChannel(data.ChannelId.Value, user, State); + } + + // The channel isn't required when responding to an interaction, so we can leave the channel null. + } + } + else if (data.User.IsSpecified) + { + channel = State.GetDMChannel(data.User.Value.Id); + } + + var interaction = SocketInteraction.Create(this, data, channel as ISocketMessageChannel, user); + + await TimedInvokeAsync(_interactionCreatedEvent, nameof(InteractionCreated), interaction).ConfigureAwait(false); + + switch (interaction) + { + case SocketSlashCommand slashCommand: + await TimedInvokeAsync(_slashCommandExecuted, nameof(SlashCommandExecuted), slashCommand).ConfigureAwait(false); + break; + case SocketMessageComponent messageComponent: + if (messageComponent.Data.Type.IsSelectType()) + await TimedInvokeAsync(_selectMenuExecuted, nameof(SelectMenuExecuted), messageComponent).ConfigureAwait(false); + if (messageComponent.Data.Type == ComponentType.Button) + await TimedInvokeAsync(_buttonExecuted, nameof(ButtonExecuted), messageComponent).ConfigureAwait(false); + break; + case SocketUserCommand userCommand: + await TimedInvokeAsync(_userCommandExecuted, nameof(UserCommandExecuted), userCommand).ConfigureAwait(false); + break; + case SocketMessageCommand messageCommand: + await TimedInvokeAsync(_messageCommandExecuted, nameof(MessageCommandExecuted), messageCommand).ConfigureAwait(false); + break; + case SocketAutocompleteInteraction autocomplete: + await TimedInvokeAsync(_autocompleteExecuted, nameof(AutocompleteExecuted), autocomplete).ConfigureAwait(false); + break; + case SocketModal modal: + await TimedInvokeAsync(_modalSubmitted, nameof(ModalSubmitted), modal).ConfigureAwait(false); + break; + } + } + break; + case "APPLICATION_COMMAND_CREATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (APPLICATION_COMMAND_CREATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + if (data.GuildId.IsSpecified) + { + var guild = State.GetGuild(data.GuildId.Value); + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + return; + } + } + + var applicationCommand = SocketApplicationCommand.Create(this, data); + + State.AddCommand(applicationCommand); + + await TimedInvokeAsync(_applicationCommandCreated, nameof(ApplicationCommandCreated), applicationCommand).ConfigureAwait(false); + } + break; + case "APPLICATION_COMMAND_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (APPLICATION_COMMAND_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + if (data.GuildId.IsSpecified) + { + var guild = State.GetGuild(data.GuildId.Value); + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + return; + } + } + + var applicationCommand = SocketApplicationCommand.Create(this, data); + + State.AddCommand(applicationCommand); + + await TimedInvokeAsync(_applicationCommandUpdated, nameof(ApplicationCommandUpdated), applicationCommand).ConfigureAwait(false); + } + break; + case "APPLICATION_COMMAND_DELETE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (APPLICATION_COMMAND_DELETE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + if (data.GuildId.IsSpecified) + { + var guild = State.GetGuild(data.GuildId.Value); + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + return; + } + } + + var applicationCommand = SocketApplicationCommand.Create(this, data); + + State.RemoveCommand(applicationCommand.Id); + + await TimedInvokeAsync(_applicationCommandDeleted, nameof(ApplicationCommandDeleted), applicationCommand).ConfigureAwait(false); + } + break; + #endregion + + #region Threads + case "THREAD_CREATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_CREATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + var guild = State.GetGuild(data.GuildId.Value); + + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId.Value); + return; + } + + SocketThreadChannel threadChannel = null; + + if ((threadChannel = guild.ThreadChannels.FirstOrDefault(x => x.Id == data.Id)) != null) + { + threadChannel.Update(State, data); + + if (data.ThreadMember.IsSpecified) + threadChannel.AddOrUpdateThreadMember(data.ThreadMember.Value, guild.CurrentUser); + } + else + { + threadChannel = (SocketThreadChannel)guild.AddChannel(State, data); + if (data.ThreadMember.IsSpecified) + threadChannel.AddOrUpdateThreadMember(data.ThreadMember.Value, guild.CurrentUser); + } + + await TimedInvokeAsync(_threadCreated, nameof(ThreadCreated), threadChannel).ConfigureAwait(false); + } + + break; + case "THREAD_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.GuildId.Value); + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId.Value); + return; + } + + var threadChannel = guild.ThreadChannels.FirstOrDefault(x => x.Id == data.Id); + var before = threadChannel != null + ? new Cacheable(threadChannel.Clone(), data.Id, true, () => Task.FromResult((SocketThreadChannel)null)) + : new Cacheable(null, data.Id, false, () => Task.FromResult((SocketThreadChannel)null)); + + if (threadChannel != null) + { + threadChannel.Update(State, data); + + if (data.ThreadMember.IsSpecified) + threadChannel.AddOrUpdateThreadMember(data.ThreadMember.Value, guild.CurrentUser); + } + else + { + //Thread is updated but was not cached, likely meaning the thread was unarchived. + threadChannel = (SocketThreadChannel)guild.AddChannel(State, data); + if (data.ThreadMember.IsSpecified) + threadChannel.AddOrUpdateThreadMember(data.ThreadMember.Value, guild.CurrentUser); + } + + if (!(guild?.IsSynced ?? true)) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + await TimedInvokeAsync(_threadUpdated, nameof(ThreadUpdated), before, threadChannel).ConfigureAwait(false); + } + break; + case "THREAD_DELETE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_DELETE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + var guild = State.GetGuild(data.GuildId.Value); + + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + return; + } + + var thread = (SocketThreadChannel)guild.RemoveChannel(State, data.Id); + + var cacheable = new Cacheable(thread, data.Id, thread != null, null); + + await TimedInvokeAsync(_threadDeleted, nameof(ThreadDeleted), cacheable).ConfigureAwait(false); + } + break; + case "THREAD_LIST_SYNC": + { + await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_LIST_SYNC)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + var guild = State.GetGuild(data.GuildId); + + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + + foreach (var thread in data.Threads) + { + var entity = guild.ThreadChannels.FirstOrDefault(x => x.Id == thread.Id); + + if (entity == null) + { + entity = (SocketThreadChannel)guild.AddChannel(State, thread); + } + else + { + entity.Update(State, thread); + } + + foreach (var member in data.Members.Where(x => x.Id.Value == entity.Id)) + { + var guildMember = guild.GetUser(member.Id.Value); + + entity.AddOrUpdateThreadMember(member, guildMember); + } + } + } + break; + case "THREAD_MEMBER_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_MEMBER_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + var thread = (SocketThreadChannel)State.GetChannel(data.Id.Value); + + if (thread == null) + { + await UnknownChannelAsync(type, data.Id.Value); + return; + } + + thread.AddOrUpdateThreadMember(data, thread.Guild.CurrentUser); + } + + break; + case "THREAD_MEMBERS_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_MEMBERS_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + var guild = State.GetGuild(data.GuildId); + + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + + var thread = (SocketThreadChannel)guild.GetChannel(data.Id); + + if (thread == null) + { + await UnknownChannelAsync(type, data.Id); + return; + } + + IReadOnlyCollection leftUsers = null; + IReadOnlyCollection joinUsers = null; + + + if (data.RemovedMemberIds.IsSpecified) + { + leftUsers = thread.RemoveUsers(data.RemovedMemberIds.Value); + } + + if (data.AddedMembers.IsSpecified) + { + List newThreadMembers = new List(); + foreach (var threadMember in data.AddedMembers.Value) + { + SocketGuildUser guildMember; + + guildMember = guild.GetUser(threadMember.UserId.Value); + + if (guildMember == null) + { + await UnknownGuildUserAsync("THREAD_MEMBERS_UPDATE", threadMember.UserId.Value, guild.Id); + } + else + newThreadMembers.Add(thread.AddOrUpdateThreadMember(threadMember, guildMember)); + } + + if (newThreadMembers.Any()) + joinUsers = newThreadMembers.ToImmutableArray(); + } + + if (leftUsers != null) + { + foreach (var threadUser in leftUsers) + { + await TimedInvokeAsync(_threadMemberLeft, nameof(ThreadMemberLeft), threadUser).ConfigureAwait(false); + } + } + + if (joinUsers != null) + { + foreach (var threadUser in joinUsers) + { + await TimedInvokeAsync(_threadMemberJoined, nameof(ThreadMemberJoined), threadUser).ConfigureAwait(false); + } + } + } + + break; + #endregion + + #region Stage Channels + case "STAGE_INSTANCE_CREATE" or "STAGE_INSTANCE_UPDATE" or "STAGE_INSTANCE_DELETE": + { + await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + var guild = State.GetGuild(data.GuildId); + + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + + var stageChannel = guild.GetStageChannel(data.ChannelId); + + if (stageChannel == null) + { + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); + return; + } + + SocketStageChannel before = type == "STAGE_INSTANCE_UPDATE" ? stageChannel.Clone() : null; + + stageChannel.Update(data, type == "STAGE_INSTANCE_CREATE"); + + switch (type) + { + case "STAGE_INSTANCE_CREATE": + await TimedInvokeAsync(_stageStarted, nameof(StageStarted), stageChannel).ConfigureAwait(false); + return; + case "STAGE_INSTANCE_DELETE": + await TimedInvokeAsync(_stageEnded, nameof(StageEnded), stageChannel).ConfigureAwait(false); + return; + case "STAGE_INSTANCE_UPDATE": + await TimedInvokeAsync(_stageUpdated, nameof(StageUpdated), before, stageChannel).ConfigureAwait(false); + return; + } + } + break; + #endregion + + #region Guild Scheduled Events + case "GUILD_SCHEDULED_EVENT_CREATE": + { + await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + var guild = State.GetGuild(data.GuildId); + + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + + var newEvent = guild.AddOrUpdateEvent(data); + + await TimedInvokeAsync(_guildScheduledEventCreated, nameof(GuildScheduledEventCreated), newEvent).ConfigureAwait(false); + } + break; + case "GUILD_SCHEDULED_EVENT_UPDATE": + { + await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + var guild = State.GetGuild(data.GuildId); + + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + + var before = guild.GetEvent(data.Id)?.Clone(); + + var beforeCacheable = new Cacheable(before, data.Id, before != null, () => Task.FromResult((SocketGuildEvent)null)); + + var after = guild.AddOrUpdateEvent(data); + + if ((before != null ? before.Status != GuildScheduledEventStatus.Completed : true) && data.Status == GuildScheduledEventStatus.Completed) + { + await TimedInvokeAsync(_guildScheduledEventCompleted, nameof(GuildScheduledEventCompleted), after).ConfigureAwait(false); + } + else if ((before != null ? before.Status != GuildScheduledEventStatus.Active : false) && data.Status == GuildScheduledEventStatus.Active) + { + await TimedInvokeAsync(_guildScheduledEventStarted, nameof(GuildScheduledEventStarted), after).ConfigureAwait(false); + } + else + await TimedInvokeAsync(_guildScheduledEventUpdated, nameof(GuildScheduledEventUpdated), beforeCacheable, after).ConfigureAwait(false); + } + break; + case "GUILD_SCHEDULED_EVENT_DELETE": + { + await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + var guild = State.GetGuild(data.GuildId); + + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + + var guildEvent = guild.RemoveEvent(data.Id) ?? SocketGuildEvent.Create(this, guild, data); + + await TimedInvokeAsync(_guildScheduledEventCancelled, nameof(GuildScheduledEventCancelled), guildEvent).ConfigureAwait(false); + } + break; + case "GUILD_SCHEDULED_EVENT_USER_ADD" or "GUILD_SCHEDULED_EVENT_USER_REMOVE": + { + await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + var guild = State.GetGuild(data.GuildId); + + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + + var guildEvent = guild.GetEvent(data.EventId); + + if (guildEvent == null) + { + await UnknownGuildEventAsync(type, data.EventId, data.GuildId).ConfigureAwait(false); + return; + } + + var user = (SocketUser)guild.GetUser(data.UserId) ?? State.GetUser(data.UserId); + + var cacheableUser = new Cacheable(user, data.UserId, user != null, () => Rest.GetUserAsync(data.UserId)); + + switch (type) + { + case "GUILD_SCHEDULED_EVENT_USER_ADD": + await TimedInvokeAsync(_guildScheduledEventUserAdd, nameof(GuildScheduledEventUserAdd), cacheableUser, guildEvent).ConfigureAwait(false); + break; + case "GUILD_SCHEDULED_EVENT_USER_REMOVE": + await TimedInvokeAsync(_guildScheduledEventUserRemove, nameof(GuildScheduledEventUserRemove), cacheableUser, guildEvent).ConfigureAwait(false); + break; + } + } + break; + + #endregion + + #region Webhooks + + case "WEBHOOKS_UPDATE": + { + var data = (payload as JToken).ToObject(_serializer); + type = "WEBHOOKS_UPDATE"; + await _gatewayLogger.DebugAsync("Received Dispatch (WEBHOOKS_UPDATE)").ConfigureAwait(false); + + var guild = State.GetGuild(data.GuildId); + var channel = State.GetChannel(data.ChannelId); + + await TimedInvokeAsync(_webhooksUpdated, nameof(WebhooksUpdated), guild, channel); + } + break; + + #endregion + + #region Audit Logs + + case "GUILD_AUDIT_LOG_ENTRY_CREATE": + { + var data = (payload as JToken).ToObject(_serializer); + type = "GUILD_AUDIT_LOG_ENTRY_CREATE"; + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_AUDIT_LOG_ENTRY_CREATE)").ConfigureAwait(false); + + var guild = State.GetGuild(data.GuildId); + var auditLog = SocketAuditLogEntry.Create(this, data); + guild.AddAuditLog(auditLog); + + await TimedInvokeAsync(_auditLogCreated, nameof(AuditLogCreated), auditLog, guild); + } + break; + #endregion + + #region Auto Moderation + + case "AUTO_MODERATION_RULE_CREATE": + { + var data = (payload as JToken).ToObject(_serializer); + + var guild = State.GetGuild(data.GuildId); + + var rule = guild.AddOrUpdateAutoModRule(data); + + await TimedInvokeAsync(_autoModRuleCreated, nameof(AutoModRuleCreated), rule); + } + break; + + case "AUTO_MODERATION_RULE_UPDATE": + { + var data = (payload as JToken).ToObject(_serializer); + + var guild = State.GetGuild(data.GuildId); + + var cachedRule = guild.GetAutoModRule(data.Id); + var cacheableBefore = new Cacheable(cachedRule?.Clone(), + data.Id, + cachedRule is not null, + async () => await guild.GetAutoModRuleAsync(data.Id)); + + await TimedInvokeAsync(_autoModRuleUpdated, nameof(AutoModRuleUpdated), cacheableBefore, guild.AddOrUpdateAutoModRule(data)); + } + break; + + case "AUTO_MODERATION_RULE_DELETE": + { + var data = (payload as JToken).ToObject(_serializer); + + var guild = State.GetGuild(data.GuildId); + + var rule = guild.RemoveAutoModRule(data); + + await TimedInvokeAsync(_autoModRuleDeleted, nameof(AutoModRuleDeleted), rule); + } + break; + + case "AUTO_MODERATION_ACTION_EXECUTION": + { + var data = (payload as JToken).ToObject(_serializer); + + var guild = State.GetGuild(data.GuildId); + var action = new AutoModRuleAction(data.Action.Type, + data.Action.Metadata.IsSpecified + ? data.Action.Metadata.Value.ChannelId.IsSpecified + ? data.Action.Metadata.Value.ChannelId.Value + : null + : null, + data.Action.Metadata.IsSpecified + ? data.Action.Metadata.Value.DurationSeconds.IsSpecified + ? data.Action.Metadata.Value.DurationSeconds.Value + : null + : null, + data.Action.Metadata.IsSpecified + ? data.Action.Metadata.Value.CustomMessage.IsSpecified + ? data.Action.Metadata.Value.CustomMessage.Value + : null + : null); + + + var member = guild.GetUser(data.UserId); + + var cacheableUser = new Cacheable(member, + data.UserId, + member is not null, + async () => + { + var model = await ApiClient.GetGuildMemberAsync(data.GuildId, data.UserId); + return guild.AddOrUpdateUser(model); + } + ); + + ISocketMessageChannel channel = null; + if (data.ChannelId.IsSpecified) + channel = GetChannel(data.ChannelId.Value) as ISocketMessageChannel; + + var cacheableChannel = new Cacheable(channel, + data.ChannelId.GetValueOrDefault(0), + channel != null, + async () => + { + if (data.ChannelId.IsSpecified) + return await GetChannelAsync(data.ChannelId.Value).ConfigureAwait(false) as ISocketMessageChannel; + return null; + }); + + + IUserMessage cachedMsg = null; + if (data.MessageId.IsSpecified) + cachedMsg = channel?.GetCachedMessage(data.MessageId.GetValueOrDefault(0)) as IUserMessage; + + var cacheableMessage = new Cacheable(cachedMsg, + data.MessageId.GetValueOrDefault(0), + cachedMsg is not null, + async () => + { + if (data.MessageId.IsSpecified) + return (await channel!.GetMessageAsync(data.MessageId.Value).ConfigureAwait(false)) as IUserMessage; + return null; + }); + + var cachedRule = guild.GetAutoModRule(data.RuleId); + + var cacheableRule = new Cacheable(cachedRule, + data.RuleId, + cachedRule is not null, + async () => await guild.GetAutoModRuleAsync(data.RuleId)); + + var eventData = new AutoModActionExecutedData( + cacheableRule, + data.TriggerType, + cacheableUser, + cacheableChannel, + data.MessageId.IsSpecified ? cacheableMessage : null, + data.AlertSystemMessageId.GetValueOrDefault(0), + data.Content, + data.MatchedContent.IsSpecified + ? data.MatchedContent.Value + : null, + data.MatchedKeyword.IsSpecified + ? data.MatchedKeyword.Value + : null); + + await TimedInvokeAsync(_autoModActionExecuted, nameof(AutoModActionExecuted), guild, action, eventData); + } + break; + + #endregion + + #region App Subscriptions + + case "ENTITLEMENT_CREATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (ENTITLEMENT_CREATE)").ConfigureAwait(false); + var data = (payload as JToken).ToObject(_serializer); + + var entitlement = SocketEntitlement.Create(this, data); + State.AddEntitlement(data.Id, entitlement); + + await TimedInvokeAsync(_entitlementCreated, nameof(EntitlementCreated), entitlement); + } + break; + + case "ENTITLEMENT_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (ENTITLEMENT_UPDATE)").ConfigureAwait(false); + var data = (payload as JToken).ToObject(_serializer); + + var entitlement = State.GetEntitlement(data.Id); + + var cacheableBefore = new Cacheable(entitlement?.Clone(), data.Id, + entitlement is not null, () => null); + + if (entitlement is null) + { + entitlement = SocketEntitlement.Create(this, data); + State.AddEntitlement(data.Id, entitlement); + } + else + { + entitlement.Update(data); + } + + await TimedInvokeAsync(_entitlementUpdated, nameof(EntitlementUpdated), cacheableBefore, entitlement); + } + break; + + case "ENTITLEMENT_DELETE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (ENTITLEMENT_DELETE)").ConfigureAwait(false); + var data = (payload as JToken).ToObject(_serializer); + + var entitlement = State.RemoveEntitlement(data.Id); + + if (entitlement is null) + entitlement = SocketEntitlement.Create(this, data); + else + entitlement.Update(data); + + var cacheableEntitlement = new Cacheable(entitlement, data.Id, + entitlement is not null, () => null); + + await TimedInvokeAsync(_entitlementDeleted, nameof(EntitlementDeleted), cacheableEntitlement); + } + break; + + case "SUBSCRIPTION_CREATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (SUBSCRIPTION_CREATE)").ConfigureAwait(false); + var data = (payload as JToken).ToObject(_serializer); + + var subscription = SocketSubscription.Create(this, data); + State.AddSubscription(data.Id, subscription); + + await TimedInvokeAsync(_subscriptionCreated, nameof(SubscriptionCreated), subscription); + } + break; + + case "SUBSCRIPTION_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (SUBSCRIPTION_UPDATE)").ConfigureAwait(false); + var data = (payload as JToken).ToObject(_serializer); + + var subscription = State.GetSubscription(data.Id); + + var cacheableBefore = new Cacheable(subscription?.Clone(), data.Id, + hasValue: subscription is not null, () => null); + + if (subscription is null) + { + subscription = SocketSubscription.Create(this, data); + State.AddSubscription(data.Id, subscription); + } + else + { + subscription.Update(data); + } + + await TimedInvokeAsync(_subscriptionUpdated, nameof(SubscriptionUpdated), cacheableBefore, subscription); + } + break; + + case "SUBSCRIPTION_DELETE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (SUBSCRIPTION_DELETE)").ConfigureAwait(false); + var data = (payload as JToken).ToObject(_serializer); + + var subscription = State.RemoveSubscription(data.Id); + + if (subscription is null) + subscription = SocketSubscription.Create(this, data); + else + subscription.Update(data); + + var cacheableSubscription = new Cacheable(subscription, data.Id, + subscription is not null, () => null); + + await TimedInvokeAsync(_subscriptionDeleted, nameof(SubscriptionDeleted), cacheableSubscription); + } + break; + + #endregion + + #region Ignored (User only) + case "CHANNEL_PINS_ACK": + await _gatewayLogger.DebugAsync("Ignored Dispatch (CHANNEL_PINS_ACK)").ConfigureAwait(false); + break; + case "CHANNEL_PINS_UPDATE": + await _gatewayLogger.DebugAsync("Ignored Dispatch (CHANNEL_PINS_UPDATE)").ConfigureAwait(false); + break; + case "GUILD_INTEGRATIONS_UPDATE": + await _gatewayLogger.DebugAsync("Ignored Dispatch (GUILD_INTEGRATIONS_UPDATE)").ConfigureAwait(false); + break; + case "MESSAGE_ACK": + await _gatewayLogger.DebugAsync("Ignored Dispatch (MESSAGE_ACK)").ConfigureAwait(false); + break; + case "PRESENCES_REPLACE": + await _gatewayLogger.DebugAsync("Ignored Dispatch (PRESENCES_REPLACE)").ConfigureAwait(false); + break; + case "USER_SETTINGS_UPDATE": + await _gatewayLogger.DebugAsync("Ignored Dispatch (USER_SETTINGS_UPDATE)").ConfigureAwait(false); + break; + #endregion + + #region Others + default: + if (!SuppressUnknownDispatchWarnings) + await _gatewayLogger.WarningAsync($"Unknown Dispatch ({type})").ConfigureAwait(false); + break; + #endregion + } + break; + default: + await _gatewayLogger.WarningAsync($"Unknown OpCode ({opCode})").ConfigureAwait(false); + break; + } + } + catch (Exception ex) + { + if (IncludeRawPayloadOnGatewayErrors) + { + ex.Data["opcode"] = opCode; + ex.Data["type"] = type; + ex.Data["payload_data"] = (payload as JToken).ToString(); + } + + await _gatewayLogger.ErrorAsync($"Error handling {opCode}{(type != null ? $" ({type})" : "")}", ex).ConfigureAwait(false); + } + } + +} diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 161fff3ac8..06eb3628aa 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -453,6 +453,15 @@ public Task> GetSKUsAsync(RequestOptions options = null public Task ConsumeEntitlementAsync(ulong entitlementId, RequestOptions options = null) => ClientHelper.ConsumeEntitlementAsync(this, entitlementId, options); + /// + public Task GetSKUSubscriptionAsync(ulong skuId, ulong subscriptionId, RequestOptions options = null) + => ClientHelper.GetSKUSubscriptionAsync(this, skuId, subscriptionId, options); + + /// + public IAsyncEnumerable> GetSKUSubscriptionsAsync(ulong skuId, int limit = 100, ulong? afterId = null, + ulong? beforeId = null, ulong? userId = null, RequestOptions options = null) + => ClientHelper.ListSubscriptionsAsync(this, skuId, limit, afterId, beforeId, userId, options); + /// public Task GetApplicationEmoteAsync(ulong emoteId, RequestOptions options = null) => ClientHelper.GetApplicationEmojiAsync(this, emoteId, options); @@ -478,12 +487,23 @@ public Task DeleteApplicationEmoteAsync(ulong emoteId, RequestOptions options = /// public IReadOnlyCollection Entitlements => State.Entitlements; + /// + /// Gets subscriptions from cache. + /// + public IReadOnlyCollection Subscription => State.Subscriptions; + /// /// Gets an entitlement from cache. if not found. /// public SocketEntitlement GetEntitlement(ulong id) => State.GetEntitlement(id); + /// + /// Gets a subscription from cache. if not found. + /// + public SocketSubscription GetSubscription(ulong id) + => State.GetSubscription(id); + /// /// Gets a global application command. /// @@ -897,2465 +917,6 @@ _shardedClient is not null && } } - #region ProcessMessageAsync - private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string type, object payload) - { - if (seq != null) - _lastSeq = seq.Value; - _lastMessageTime = Environment.TickCount; - - try - { - switch (opCode) - { - case GatewayOpCode.Hello: - { - await _gatewayLogger.DebugAsync("Received Hello").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); - - _heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _connection.CancelToken); - } - break; - case GatewayOpCode.Heartbeat: - { - await _gatewayLogger.DebugAsync("Received Heartbeat").ConfigureAwait(false); - - await ApiClient.SendHeartbeatAsync(_lastSeq).ConfigureAwait(false); - } - break; - case GatewayOpCode.HeartbeatAck: - { - await _gatewayLogger.DebugAsync("Received HeartbeatAck").ConfigureAwait(false); - - if (_heartbeatTimes.TryDequeue(out long time)) - { - int latency = (int)(Environment.TickCount - time); - int before = Latency; - Latency = latency; - - await TimedInvokeAsync(_latencyUpdatedEvent, nameof(LatencyUpdated), before, latency).ConfigureAwait(false); - } - } - break; - case GatewayOpCode.InvalidSession: - { - await _gatewayLogger.DebugAsync("Received InvalidSession").ConfigureAwait(false); - await _gatewayLogger.WarningAsync("Failed to resume previous session").ConfigureAwait(false); - - _sessionId = null; - _lastSeq = 0; - ApiClient.ResumeGatewayUrl = null; - - if (_shardedClient != null) - { - await _shardedClient.AcquireIdentifyLockAsync(ShardId, _connection.CancelToken).ConfigureAwait(false); - try - { - await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards, gatewayIntents: _gatewayIntents, presence: BuildCurrentStatus()).ConfigureAwait(false); - } - finally - { - _shardedClient.ReleaseIdentifyLock(); - } - } - else - await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards, gatewayIntents: _gatewayIntents, presence: BuildCurrentStatus()).ConfigureAwait(false); - } - break; - case GatewayOpCode.Reconnect: - { - await _gatewayLogger.DebugAsync("Received Reconnect").ConfigureAwait(false); - _connection.Error(new GatewayReconnectException("Server requested a reconnect")); - } - break; - case GatewayOpCode.Dispatch: - switch (type) - { - #region Connection - case "READY": - { - try - { - await _gatewayLogger.DebugAsync("Received Dispatch (READY)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - var state = new ClientState(data.Guilds.Length, data.PrivateChannels.Length); - - var currentUser = SocketSelfUser.Create(this, state, data.User); - Rest.CreateRestSelfUser(data.User); - var activities = _activity.IsSpecified ? ImmutableList.Create(_activity.Value) : null; - currentUser.Presence = new SocketPresence(Status, null, activities); - ApiClient.CurrentUserId = currentUser.Id; - ApiClient.CurrentApplicationId = data.Application?.Id; - Rest.CurrentUser = RestSelfUser.Create(this, data.User); - int unavailableGuilds = 0; - for (int i = 0; i < data.Guilds.Length; i++) - { - var model = data.Guilds[i]; - var guild = AddGuild(model, state); - if (!guild.IsAvailable) - unavailableGuilds++; - else - await GuildAvailableAsync(guild).ConfigureAwait(false); - } - for (int i = 0; i < data.PrivateChannels.Length; i++) - AddPrivateChannel(data.PrivateChannels[i], state); - - _sessionId = data.SessionId; - ApiClient.ResumeGatewayUrl = data.ResumeGatewayUrl; - _unavailableGuildCount = unavailableGuilds; - CurrentUser = currentUser; - _previousSessionUser = CurrentUser; - State = state; - } - catch (Exception ex) - { - _connection.CriticalError(new Exception("Processing READY failed", ex)); - return; - } - - _lastGuildAvailableTime = Environment.TickCount; - _guildDownloadTask = WaitForGuildsAsync(_connection.CancelToken, _gatewayLogger) - .ContinueWith(async x => - { - if (x.IsFaulted) - { - _connection.Error(x.Exception); - return; - } - else if (_connection.CancelToken.IsCancellationRequested) - return; - - if (BaseConfig.AlwaysDownloadUsers) - try - { - _ = DownloadUsersAsync(Guilds.Where(x => x.IsAvailable && !x.HasAllMembers)); - } - catch (Exception ex) - { - await _gatewayLogger.WarningAsync(ex); - } - - await TimedInvokeAsync(_readyEvent, nameof(Ready)).ConfigureAwait(false); - await _gatewayLogger.InfoAsync("Ready").ConfigureAwait(false); - }); - _ = _connection.CompleteAsync(); - } - break; - case "RESUMED": - { - await _gatewayLogger.DebugAsync("Received Dispatch (RESUMED)").ConfigureAwait(false); - - _ = _connection.CompleteAsync(); - - //Notify the client that these guilds are available again - foreach (var guild in State.Guilds) - { - if (guild.IsAvailable) - await GuildAvailableAsync(guild).ConfigureAwait(false); - } - - // Restore the previous sessions current user - CurrentUser = _previousSessionUser; - - await _gatewayLogger.InfoAsync("Resumed previous session").ConfigureAwait(false); - } - break; - #endregion - - #region Guilds - case "GUILD_CREATE": - { - var data = (payload as JToken).ToObject(_serializer); - - if (data.Unavailable == false) - { - type = "GUILD_AVAILABLE"; - _lastGuildAvailableTime = Environment.TickCount; - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_AVAILABLE)").ConfigureAwait(false); - - var guild = State.GetGuild(data.Id); - if (guild != null) - { - guild.Update(State, data); - - if (_unavailableGuildCount != 0) - _unavailableGuildCount--; - await GuildAvailableAsync(guild).ConfigureAwait(false); - - if (guild.DownloadedMemberCount >= guild.MemberCount && !guild.DownloaderPromise.IsCompleted) - { - guild.CompleteDownloadUsers(); - await TimedInvokeAsync(_guildMembersDownloadedEvent, nameof(GuildMembersDownloaded), guild).ConfigureAwait(false); - } - } - else - { - await UnknownGuildAsync(type, data.Id).ConfigureAwait(false); - return; - } - } - else - { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_CREATE)").ConfigureAwait(false); - - var guild = AddGuild(data, State); - if (guild != null) - { - await TimedInvokeAsync(_joinedGuildEvent, nameof(JoinedGuild), guild).ConfigureAwait(false); - await GuildAvailableAsync(guild).ConfigureAwait(false); - } - else - { - await UnknownGuildAsync(type, data.Id).ConfigureAwait(false); - return; - } - } - } - break; - case "GUILD_UPDATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_UPDATE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.Id); - if (guild != null) - { - var before = guild.Clone(); - guild.Update(State, data); - await TimedInvokeAsync(_guildUpdatedEvent, nameof(GuildUpdated), before, guild).ConfigureAwait(false); - } - else - { - await UnknownGuildAsync(type, data.Id).ConfigureAwait(false); - return; - } - } - break; - case "GUILD_EMOJIS_UPDATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_EMOJIS_UPDATE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); - if (guild != null) - { - var before = guild.Clone(); - guild.Update(State, data); - await TimedInvokeAsync(_guildUpdatedEvent, nameof(GuildUpdated), before, guild).ConfigureAwait(false); - } - else - { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; - } - } - break; - case "GUILD_SYNC": - { - await _gatewayLogger.DebugAsync("Ignored Dispatch (GUILD_SYNC)").ConfigureAwait(false); - /*await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_SYNC)").ConfigureAwait(false); //TODO remove? userbot related - var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.Id); - if (guild != null) - { - var before = guild.Clone(); - guild.Update(State, data); - //This is treated as an extension of GUILD_AVAILABLE - _unavailableGuildCount--; - _lastGuildAvailableTime = Environment.TickCount; - await GuildAvailableAsync(guild).ConfigureAwait(false); - await TimedInvokeAsync(_guildUpdatedEvent, nameof(GuildUpdated), before, guild).ConfigureAwait(false); - } - else - { - await UnknownGuildAsync(type, data.Id).ConfigureAwait(false); - return; - }*/ - } - break; - case "GUILD_DELETE": - { - var data = (payload as JToken).ToObject(_serializer); - if (data.Unavailable == true) - { - type = "GUILD_UNAVAILABLE"; - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_UNAVAILABLE)").ConfigureAwait(false); - - var guild = State.GetGuild(data.Id); - if (guild != null) - { - await GuildUnavailableAsync(guild).ConfigureAwait(false); - _unavailableGuildCount++; - } - else - { - await UnknownGuildAsync(type, data.Id).ConfigureAwait(false); - return; - } - } - else - { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_DELETE)").ConfigureAwait(false); - - var guild = RemoveGuild(data.Id); - if (guild != null) - { - await GuildUnavailableAsync(guild).ConfigureAwait(false); - await TimedInvokeAsync(_leftGuildEvent, nameof(LeftGuild), guild).ConfigureAwait(false); - (guild as IDisposable).Dispose(); - } - else - { - await UnknownGuildAsync(type, data.Id).ConfigureAwait(false); - return; - } - } - } - break; - case "GUILD_STICKERS_UPDATE": - { - await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_STICKERS_UPDATE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - - var guild = State.GetGuild(data.GuildId); - - if (guild == null) - { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; - } - - var newStickers = data.Stickers.Where(x => !guild.Stickers.Any(y => y.Id == x.Id)); - var deletedStickers = guild.Stickers.Where(x => !data.Stickers.Any(y => y.Id == x.Id)); - var updatedStickers = data.Stickers.Select(x => - { - var s = guild.Stickers.FirstOrDefault(y => y.Id == x.Id); - if (s == null) - return null; - - var e = s.Equals(x); - if (!e) - { - return (s, x) as (SocketCustomSticker Entity, API.Sticker Model)?; - } - else - { - return null; - } - }).Where(x => x.HasValue).Select(x => x.Value).ToArray(); - - foreach (var model in newStickers) - { - var entity = guild.AddSticker(model); - await TimedInvokeAsync(_guildStickerCreated, nameof(GuildStickerCreated), entity); - } - foreach (var sticker in deletedStickers) - { - var entity = guild.RemoveSticker(sticker.Id); - await TimedInvokeAsync(_guildStickerDeleted, nameof(GuildStickerDeleted), entity); - } - foreach (var entityModelPair in updatedStickers) - { - var before = entityModelPair.Entity.Clone(); - - entityModelPair.Entity.Update(entityModelPair.Model); - - await TimedInvokeAsync(_guildStickerUpdated, nameof(GuildStickerUpdated), before, entityModelPair.Entity); - } - } - break; - #endregion - - #region Channels - case "CHANNEL_CREATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_CREATE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - SocketChannel channel = null; - if (data.GuildId.IsSpecified) - { - var guild = State.GetGuild(data.GuildId.Value); - if (guild != null) - { - channel = guild.AddChannel(State, data); - - if (!guild.IsSynced) - { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } - } - else - { - await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); - return; - } - } - else - { - channel = State.GetChannel(data.Id); - if (channel != null) - return; //Discord may send duplicate CHANNEL_CREATEs for DMs - channel = AddPrivateChannel(data, State) as SocketChannel; - } - - if (channel != null) - await TimedInvokeAsync(_channelCreatedEvent, nameof(ChannelCreated), channel).ConfigureAwait(false); - } - break; - case "CHANNEL_UPDATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_UPDATE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - var channel = State.GetChannel(data.Id); - if (channel != null) - { - var before = channel.Clone(); - channel.Update(State, data); - - var guild = (channel as SocketGuildChannel)?.Guild; - if (!(guild?.IsSynced ?? true)) - { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } - - await TimedInvokeAsync(_channelUpdatedEvent, nameof(ChannelUpdated), before, channel).ConfigureAwait(false); - } - else - { - await UnknownChannelAsync(type, data.Id).ConfigureAwait(false); - return; - } - } - break; - case "CHANNEL_DELETE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_DELETE)").ConfigureAwait(false); - - SocketChannel channel = null; - var data = (payload as JToken).ToObject(_serializer); - if (data.GuildId.IsSpecified) - { - var guild = State.GetGuild(data.GuildId.Value); - if (guild != null) - { - channel = guild.RemoveChannel(State, data.Id); - - if (!guild.IsSynced) - { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } - } - else - { - await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); - return; - } - } - else - channel = RemovePrivateChannel(data.Id) as SocketChannel; - - if (channel != null) - await TimedInvokeAsync(_channelDestroyedEvent, nameof(ChannelDestroyed), channel).ConfigureAwait(false); - else - { - await UnknownChannelAsync(type, data.Id, data.GuildId.GetValueOrDefault(0)).ConfigureAwait(false); - return; - } - } - break; - #endregion - - #region Members - case "GUILD_MEMBER_ADD": - { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBER_ADD)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); - if (guild != null) - { - var user = guild.AddOrUpdateUser(data); - guild.MemberCount++; - - if (!guild.IsSynced) - { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } - - await TimedInvokeAsync(_userJoinedEvent, nameof(UserJoined), user).ConfigureAwait(false); - } - else - { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; - } - } - break; - case "GUILD_MEMBER_UPDATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBER_UPDATE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); - if (guild != null) - { - var user = guild.GetUser(data.User.Id); - - if (!guild.IsSynced) - { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } - - if (user != null) - { - var before = user.Clone(); - if (user.GlobalUser.Update(State, data.User)) - { - //Global data was updated, trigger UserUpdated - await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), before.GlobalUser, user).ConfigureAwait(false); - } - - user.Update(State, data); - - var cacheableBefore = new Cacheable(before, user.Id, true, () => Task.FromResult(null)); - await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), cacheableBefore, user).ConfigureAwait(false); - } - else - { - user = guild.AddOrUpdateUser(data); - var cacheableBefore = new Cacheable(null, user.Id, false, () => Task.FromResult(null)); - await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), cacheableBefore, user).ConfigureAwait(false); - } - } - else - { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; - } - } - break; - case "GUILD_MEMBER_REMOVE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBER_REMOVE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); - if (guild != null) - { - SocketUser user = guild.RemoveUser(data.User.Id); - guild.MemberCount--; - - if (!guild.IsSynced) - { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } - - user ??= State.GetUser(data.User.Id); - - if (user != null) - user.Update(State, data.User); - else - user = State.GetOrAddUser(data.User.Id, (x) => SocketGlobalUser.Create(this, State, data.User)); - - await TimedInvokeAsync(_userLeftEvent, nameof(UserLeft), guild, user).ConfigureAwait(false); - } - else - { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; - } - } - break; - case "GUILD_MEMBERS_CHUNK": - { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBERS_CHUNK)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); - if (guild != null) - { - foreach (var memberModel in data.Members) - guild.AddOrUpdateUser(memberModel); - - if (guild.DownloadedMemberCount >= guild.MemberCount && !guild.DownloaderPromise.IsCompleted) - { - guild.CompleteDownloadUsers(); - await TimedInvokeAsync(_guildMembersDownloadedEvent, nameof(GuildMembersDownloaded), guild).ConfigureAwait(false); - } - } - else - { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; - } - } - break; - case "GUILD_JOIN_REQUEST_DELETE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_JOIN_REQUEST_DELETE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - - var guild = State.GetGuild(data.GuildId); - - if (guild == null) - { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; - } - - var user = guild.RemoveUser(data.UserId); - guild.MemberCount--; - - var cacheableUser = new Cacheable(user, data.UserId, user != null, () => Task.FromResult((SocketGuildUser)null)); - - await TimedInvokeAsync(_guildJoinRequestDeletedEvent, nameof(GuildJoinRequestDeleted), cacheableUser, guild).ConfigureAwait(false); - } - break; - #endregion - - #region DM Channels - - case "CHANNEL_RECIPIENT_ADD": - { - await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_RECIPIENT_ADD)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - if (State.GetChannel(data.ChannelId) is SocketGroupChannel channel) - { - var user = channel.GetOrAddUser(data.User); - await TimedInvokeAsync(_recipientAddedEvent, nameof(RecipientAdded), user).ConfigureAwait(false); - } - else - { - await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); - return; - } - } - break; - case "CHANNEL_RECIPIENT_REMOVE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_RECIPIENT_REMOVE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - if (State.GetChannel(data.ChannelId) is SocketGroupChannel channel) - { - var user = channel.RemoveUser(data.User.Id); - if (user != null) - await TimedInvokeAsync(_recipientRemovedEvent, nameof(RecipientRemoved), user).ConfigureAwait(false); - else - { - await UnknownChannelUserAsync(type, data.User.Id, data.ChannelId).ConfigureAwait(false); - return; - } - } - else - { - await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); - return; - } - } - break; - - #endregion - - #region Roles - case "GUILD_ROLE_CREATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_ROLE_CREATE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); - if (guild != null) - { - var role = guild.AddRole(data.Role); - - if (!guild.IsSynced) - { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } - await TimedInvokeAsync(_roleCreatedEvent, nameof(RoleCreated), role).ConfigureAwait(false); - } - else - { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; - } - } - break; - case "GUILD_ROLE_UPDATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_ROLE_UPDATE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); - if (guild != null) - { - var role = guild.GetRole(data.Role.Id); - if (role != null) - { - var before = role.Clone(); - role.Update(State, data.Role); - - if (!guild.IsSynced) - { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } - - await TimedInvokeAsync(_roleUpdatedEvent, nameof(RoleUpdated), before, role).ConfigureAwait(false); - } - else - { - await UnknownRoleAsync(type, data.Role.Id, guild.Id).ConfigureAwait(false); - return; - } - } - else - { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; - } - } - break; - case "GUILD_ROLE_DELETE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_ROLE_DELETE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); - if (guild != null) - { - var role = guild.RemoveRole(data.RoleId); - if (role != null) - { - if (!guild.IsSynced) - { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } - - await TimedInvokeAsync(_roleDeletedEvent, nameof(RoleDeleted), role).ConfigureAwait(false); - } - else - { - await UnknownRoleAsync(type, data.RoleId, guild.Id).ConfigureAwait(false); - return; - } - } - else - { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; - } - } - break; - #endregion - - #region Bans - case "GUILD_BAN_ADD": - { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_BAN_ADD)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); - if (guild != null) - { - if (!guild.IsSynced) - { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } - - SocketUser user = guild.GetUser(data.User.Id); - if (user == null) - user = SocketUnknownUser.Create(this, State, data.User); - await TimedInvokeAsync(_userBannedEvent, nameof(UserBanned), user, guild).ConfigureAwait(false); - } - else - { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; - } - } - break; - case "GUILD_BAN_REMOVE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_BAN_REMOVE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); - if (guild != null) - { - if (!guild.IsSynced) - { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } - - SocketUser user = State.GetUser(data.User.Id); - if (user == null) - user = SocketUnknownUser.Create(this, State, data.User); - await TimedInvokeAsync(_userUnbannedEvent, nameof(UserUnbanned), user, guild).ConfigureAwait(false); - } - else - { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; - } - } - break; - #endregion - - #region Messages - case "MESSAGE_CREATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_CREATE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; - - var guild = (channel as SocketGuildChannel)?.Guild; - if (guild != null && !guild.IsSynced) - { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } - - if (channel == null) - { - if (!data.GuildId.IsSpecified) // assume it is a DM - { - channel = CreateDMChannel(data.ChannelId, data.Author.Value, State); - } - else - { - await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); - return; - } - } - - SocketUser author; - if (guild != null) - { - if (data.WebhookId.IsSpecified) - author = SocketWebhookUser.Create(guild, State, data.Author.Value, data.WebhookId.Value); - else - author = guild.GetUser(data.Author.Value.Id); - } - else - author = (channel as SocketChannel).GetUser(data.Author.Value.Id); - - if (author == null) - { - if (guild != null) - { - if (data.Member.IsSpecified) // member isn't always included, but use it when we can - { - data.Member.Value.User = data.Author.Value; - author = guild.AddOrUpdateUser(data.Member.Value); - } - else - author = guild.AddOrUpdateUser(data.Author.Value); // user has no guild-specific data - } - else if (channel is SocketGroupChannel groupChannel) - author = groupChannel.GetOrAddUser(data.Author.Value); - else - { - await UnknownChannelUserAsync(type, data.Author.Value.Id, channel.Id).ConfigureAwait(false); - return; - } - } - - var msg = SocketMessage.Create(this, State, author, channel, data); - SocketChannelHelper.AddMessage(channel, this, msg); - await TimedInvokeAsync(_messageReceivedEvent, nameof(MessageReceived), msg).ConfigureAwait(false); - } - break; - case "MESSAGE_UPDATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_UPDATE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; - - var guild = (channel as SocketGuildChannel)?.Guild; - if (guild != null && !guild.IsSynced) - { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } - - SocketMessage before = null, after = null; - SocketMessage cachedMsg = channel?.GetCachedMessage(data.Id); - bool isCached = cachedMsg != null; - if (isCached) - { - before = cachedMsg.Clone(); - cachedMsg.Update(State, data); - after = cachedMsg; - } - else - { - //Edited message isn't in cache, create a detached one - SocketUser author; - if (data.Author.IsSpecified) - { - if (guild != null) - { - if (data.WebhookId.IsSpecified) - author = SocketWebhookUser.Create(guild, State, data.Author.Value, data.WebhookId.Value); - else - author = guild.GetUser(data.Author.Value.Id); - } - else - author = (channel as SocketChannel)?.GetUser(data.Author.Value.Id); - - if (author == null) - { - if (guild != null) - { - if (data.Member.IsSpecified) // member isn't always included, but use it when we can - { - data.Member.Value.User = data.Author.Value; - author = guild.AddOrUpdateUser(data.Member.Value); - } - else - author = guild.AddOrUpdateUser(data.Author.Value); // user has no guild-specific data - } - else if (channel is SocketGroupChannel groupChannel) - author = groupChannel.GetOrAddUser(data.Author.Value); - } - } - else - // Message author wasn't specified in the payload, so create a completely anonymous unknown user - author = new SocketUnknownUser(this, id: 0); - - if (channel == null) - { - if (!data.GuildId.IsSpecified) // assume it is a DM - { - if (data.Author.IsSpecified) - { - var dmChannel = CreateDMChannel(data.ChannelId, data.Author.Value, State); - channel = dmChannel; - author = dmChannel.Recipient; - } - else - channel = CreateDMChannel(data.ChannelId, author, State); - } - else - { - await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); - return; - } - } - - after = SocketMessage.Create(this, State, author, channel, data); - } - var cacheableBefore = new Cacheable(before, data.Id, isCached, async () => await channel.GetMessageAsync(data.Id).ConfigureAwait(false)); - - await TimedInvokeAsync(_messageUpdatedEvent, nameof(MessageUpdated), cacheableBefore, after, channel).ConfigureAwait(false); - } - break; - case "MESSAGE_DELETE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_DELETE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; - - var guild = (channel as SocketGuildChannel)?.Guild; - if (!(guild?.IsSynced ?? true)) - { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } - - SocketMessage msg = null; - if (channel != null) - msg = SocketChannelHelper.RemoveMessage(channel, this, data.Id); - var cacheableMsg = new Cacheable(msg, data.Id, msg != null, () => Task.FromResult((IMessage)null)); - var cacheableChannel = new Cacheable(channel, data.ChannelId, channel != null, async () => await GetChannelAsync(data.ChannelId).ConfigureAwait(false) as IMessageChannel); - - await TimedInvokeAsync(_messageDeletedEvent, nameof(MessageDeleted), cacheableMsg, cacheableChannel).ConfigureAwait(false); - } - break; - case "MESSAGE_REACTION_ADD": - { - await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_ADD)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; - - var cachedMsg = channel?.GetCachedMessage(data.MessageId) as SocketUserMessage; - bool isMsgCached = cachedMsg != null; - IUser user = null; - if (channel != null) - user = await channel.GetUserAsync(data.UserId, CacheMode.CacheOnly).ConfigureAwait(false); - - var optionalMsg = !isMsgCached - ? Optional.Create() - : Optional.Create(cachedMsg); - - if (data.Member.IsSpecified) - { - var guild = (channel as SocketGuildChannel)?.Guild; - - if (guild != null) - user = guild.AddOrUpdateUser(data.Member.Value); - } - else - user = GetUser(data.UserId); - - var optionalUser = user is null - ? Optional.Create() - : Optional.Create(user); - - var cacheableChannel = new Cacheable(channel, data.ChannelId, channel != null, async () => await GetChannelAsync(data.ChannelId).ConfigureAwait(false) as IMessageChannel); - var cacheableMsg = new Cacheable(cachedMsg, data.MessageId, isMsgCached, async () => - { - var channelObj = await cacheableChannel.GetOrDownloadAsync().ConfigureAwait(false); - return await channelObj.GetMessageAsync(data.MessageId).ConfigureAwait(false) as IUserMessage; - }); - var reaction = SocketReaction.Create(data, channel, optionalMsg, optionalUser); - - cachedMsg?.AddReaction(reaction); - - await TimedInvokeAsync(_reactionAddedEvent, nameof(ReactionAdded), cacheableMsg, cacheableChannel, reaction).ConfigureAwait(false); - } - break; - case "MESSAGE_REACTION_REMOVE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_REMOVE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; - - var cachedMsg = channel?.GetCachedMessage(data.MessageId) as SocketUserMessage; - bool isMsgCached = cachedMsg != null; - IUser user = null; - if (channel != null) - user = await channel.GetUserAsync(data.UserId, CacheMode.CacheOnly).ConfigureAwait(false); - else if (!data.GuildId.IsSpecified) - user = GetUser(data.UserId); - - var optionalMsg = !isMsgCached - ? Optional.Create() - : Optional.Create(cachedMsg); - - var optionalUser = user is null - ? Optional.Create() - : Optional.Create(user); - - var cacheableChannel = new Cacheable(channel, data.ChannelId, channel != null, async () => await GetChannelAsync(data.ChannelId).ConfigureAwait(false) as IMessageChannel); - var cacheableMsg = new Cacheable(cachedMsg, data.MessageId, isMsgCached, async () => - { - var channelObj = await cacheableChannel.GetOrDownloadAsync().ConfigureAwait(false); - return await channelObj.GetMessageAsync(data.MessageId).ConfigureAwait(false) as IUserMessage; - }); - var reaction = SocketReaction.Create(data, channel, optionalMsg, optionalUser); - - cachedMsg?.RemoveReaction(reaction); - - await TimedInvokeAsync(_reactionRemovedEvent, nameof(ReactionRemoved), cacheableMsg, cacheableChannel, reaction).ConfigureAwait(false); - } - break; - case "MESSAGE_REACTION_REMOVE_ALL": - { - await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_REMOVE_ALL)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; - - var cacheableChannel = new Cacheable(channel, data.ChannelId, channel != null, async () => await GetChannelAsync(data.ChannelId).ConfigureAwait(false) as IMessageChannel); - var cachedMsg = channel?.GetCachedMessage(data.MessageId) as SocketUserMessage; - bool isMsgCached = cachedMsg != null; - var cacheableMsg = new Cacheable(cachedMsg, data.MessageId, isMsgCached, async () => - { - var channelObj = await cacheableChannel.GetOrDownloadAsync().ConfigureAwait(false); - return await channelObj.GetMessageAsync(data.MessageId).ConfigureAwait(false) as IUserMessage; - }); - - cachedMsg?.ClearReactions(); - - await TimedInvokeAsync(_reactionsClearedEvent, nameof(ReactionsCleared), cacheableMsg, cacheableChannel).ConfigureAwait(false); - } - break; - case "MESSAGE_REACTION_REMOVE_EMOJI": - { - await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_REMOVE_EMOJI)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; - - var cachedMsg = channel?.GetCachedMessage(data.MessageId) as SocketUserMessage; - bool isMsgCached = cachedMsg != null; - - var optionalMsg = !isMsgCached - ? Optional.Create() - : Optional.Create(cachedMsg); - - var cacheableChannel = new Cacheable(channel, data.ChannelId, channel != null, async () => await GetChannelAsync(data.ChannelId).ConfigureAwait(false) as IMessageChannel); - var cacheableMsg = new Cacheable(cachedMsg, data.MessageId, isMsgCached, async () => - { - var channelObj = await cacheableChannel.GetOrDownloadAsync().ConfigureAwait(false); - return await channelObj.GetMessageAsync(data.MessageId).ConfigureAwait(false) as IUserMessage; - }); - var emote = data.Emoji.ToIEmote(); - - cachedMsg?.RemoveReactionsForEmote(emote); - - await TimedInvokeAsync(_reactionsRemovedForEmoteEvent, nameof(ReactionsRemovedForEmote), cacheableMsg, cacheableChannel, emote).ConfigureAwait(false); - } - break; - case "MESSAGE_DELETE_BULK": - { - await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_DELETE_BULK)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; - - var guild = (channel as SocketGuildChannel)?.Guild; - if (!(guild?.IsSynced ?? true)) - { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } - - var cacheableChannel = new Cacheable(channel, data.ChannelId, channel != null, async () => await GetChannelAsync(data.ChannelId).ConfigureAwait(false) as IMessageChannel); - var cacheableList = new List>(data.Ids.Length); - foreach (ulong id in data.Ids) - { - SocketMessage msg = null; - if (channel != null) - msg = SocketChannelHelper.RemoveMessage(channel, this, id); - bool isMsgCached = msg != null; - var cacheableMsg = new Cacheable(msg, id, isMsgCached, () => Task.FromResult((IMessage)null)); - cacheableList.Add(cacheableMsg); - } - - await TimedInvokeAsync(_messagesBulkDeletedEvent, nameof(MessagesBulkDeleted), cacheableList, cacheableChannel).ConfigureAwait(false); - } - break; - #endregion - - #region Polls - - case "MESSAGE_POLL_VOTE_ADD": - { - await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_POLL_VOTE_ADD)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - - Cacheable? guildCacheable = null; - - Cacheable userCacheable; - Cacheable channelCacheable; - Cacheable messageCacheable; - - if (data.GuildId.IsSpecified) - { - var guild = State.GetGuild(data.GuildId.Value); - guildCacheable = new (guild, data.GuildId.Value, guild is not null, () => Rest.GetGuildAsync(data.GuildId.Value)); - - if (guild is not null) - { - var user = guild.GetUser(data.UserId); - userCacheable = new (user, data.UserId, user is not null, async () => await Rest.GetGuildUserAsync(data.GuildId.Value, data.UserId)); - - var channel = guild.GetTextChannel(data.ChannelId); - channelCacheable = new(channel, data.ChannelId, channel is not null, async () => (RestTextChannel)await Rest.GetChannelAsync(data.ChannelId)); - - var message = channel?.GetCachedMessage(data.MessageId) as IUserMessage; - messageCacheable = new (message, data.MessageId, message is not null, - async () => (channel ?? (ITextChannel)await Rest.GetChannelAsync(data.ChannelId)).GetMessageAsync(data.MessageId) as IUserMessage); - } - else - { - userCacheable = new (null, data.UserId, false, async () => await Rest.GetGuildUserAsync(data.GuildId.Value, data.UserId)); - channelCacheable = new(null, data.ChannelId, false, async () => (RestTextChannel)(await Rest.GetChannelAsync(data.ChannelId))); - messageCacheable = new(null, data.MessageId, false, - async () => await ((ITextChannel)await Rest.GetChannelAsync(data.ChannelId)).GetMessageAsync(data.MessageId) as IUserMessage); - } - } - else - { - var user = State.GetUser(data.UserId); - userCacheable = new(user, data.UserId, user is not null, async () => await GetUserAsync(data.UserId)); - - var channel = State.GetChannel(data.ChannelId) as ISocketMessageChannel; - channelCacheable = new(channel, data.ChannelId, channel is not null, async () => await Rest.GetDMChannelAsync(data.ChannelId) as IRestMessageChannel); - - var message = channel?.GetCachedMessage(data.MessageId) as IUserMessage; - messageCacheable = new(message, data.MessageId, message is not null, async () => await (channel ?? (IMessageChannel)await Rest.GetDMChannelAsync(data.ChannelId)).GetMessageAsync(data.MessageId) as IUserMessage); - } - - await TimedInvokeAsync(_pollVoteAdded, nameof(PollVoteAdded), userCacheable, channelCacheable, messageCacheable, guildCacheable, data.AnswerId); - } - break; - - case "MESSAGE_POLL_VOTE_REMOVE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_POLL_VOTE_REMOVE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - - Cacheable? guildCacheable = null; - - Cacheable userCacheable; - Cacheable channelCacheable; - Cacheable messageCacheable; - - if (data.GuildId.IsSpecified) - { - var guild = State.GetGuild(data.GuildId.Value); - guildCacheable = new(guild, data.GuildId.Value, guild is not null, () => Rest.GetGuildAsync(data.GuildId.Value)); - - if (guild is not null) - { - var user = guild.GetUser(data.UserId); - userCacheable = new(user, data.UserId, user is not null, async () => await Rest.GetGuildUserAsync(data.GuildId.Value, data.UserId)); - - var channel = guild.GetTextChannel(data.ChannelId); - channelCacheable = new(channel, data.ChannelId, channel is not null, async () => (RestTextChannel)await Rest.GetChannelAsync(data.ChannelId)); - - var message = channel?.GetCachedMessage(data.MessageId) as IUserMessage; - messageCacheable = new(message, data.MessageId, message is not null, - async () => (channel ?? (ITextChannel)await Rest.GetChannelAsync(data.ChannelId)).GetMessageAsync(data.MessageId) as IUserMessage); - } - else - { - userCacheable = new(null, data.UserId, false, async () => await Rest.GetGuildUserAsync(data.GuildId.Value, data.UserId)); - channelCacheable = new(null, data.ChannelId, false, async () => (RestTextChannel)(await Rest.GetChannelAsync(data.ChannelId))); - messageCacheable = new(null, data.MessageId, false, - async () => await ((ITextChannel)await Rest.GetChannelAsync(data.ChannelId)).GetMessageAsync(data.MessageId) as IUserMessage); - } - } - else - { - var user = State.GetUser(data.UserId); - userCacheable = new(user, data.UserId, user is not null, async () => await GetUserAsync(data.UserId)); - - var channel = State.GetChannel(data.ChannelId) as ISocketMessageChannel; - channelCacheable = new(channel, data.ChannelId, channel is not null, async () => await Rest.GetDMChannelAsync(data.ChannelId) as IRestMessageChannel); - - var message = channel?.GetCachedMessage(data.MessageId) as IUserMessage; - messageCacheable = new(message, data.MessageId, message is not null, async () => await (channel ?? (IMessageChannel)await Rest.GetDMChannelAsync(data.ChannelId)).GetMessageAsync(data.MessageId) as IUserMessage); - } - - await TimedInvokeAsync(_pollVoteRemoved, nameof(PollVoteRemoved), userCacheable, channelCacheable, messageCacheable, guildCacheable, data.AnswerId); - } - break; - - #endregion - - #region Statuses - case "PRESENCE_UPDATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (PRESENCE_UPDATE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - - SocketUser user = null; - - if (data.GuildId.IsSpecified) - { - var guild = State.GetGuild(data.GuildId.Value); - if (guild == null) - { - await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); - return; - } - if (!guild.IsSynced) - { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } - - user = guild.GetUser(data.User.Id); - if (user == null) - { - if (data.Status == UserStatus.Offline) - { - return; - } - user = guild.AddOrUpdateUser(data); - } - else - { - var globalBefore = user.GlobalUser.Clone(); - if (user.GlobalUser.Update(State, data.User)) - { - //Global data was updated, trigger UserUpdated - await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), globalBefore, user).ConfigureAwait(false); - } - } - } - else - { - user = State.GetUser(data.User.Id); - if (user == null) - { - await UnknownGlobalUserAsync(type, data.User.Id).ConfigureAwait(false); - return; - } - } - - var before = user.Presence?.Clone(); - user.Update(State, data.User); - user.Update(data); - await TimedInvokeAsync(_presenceUpdated, nameof(PresenceUpdated), user, before, user.Presence).ConfigureAwait(false); - } - break; - case "TYPING_START": - { - await _gatewayLogger.DebugAsync("Received Dispatch (TYPING_START)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; - - var guild = (channel as SocketGuildChannel)?.Guild; - if (!(guild?.IsSynced ?? true)) - { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } - - var cacheableChannel = new Cacheable(channel, data.ChannelId, channel != null, async () => await GetChannelAsync(data.ChannelId).ConfigureAwait(false) as IMessageChannel); - - var user = (channel as SocketChannel)?.GetUser(data.UserId); - if (user == null) - { - if (guild != null && data.Member.IsSpecified) - user = guild.AddOrUpdateUser(data.Member.Value); - } - var cacheableUser = new Cacheable(user, data.UserId, user != null, async () => await GetUserAsync(data.UserId).ConfigureAwait(false)); - - await TimedInvokeAsync(_userIsTypingEvent, nameof(UserIsTyping), cacheableUser, cacheableChannel).ConfigureAwait(false); - } - break; - #endregion - - #region Integrations - case "INTEGRATION_CREATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (INTEGRATION_CREATE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - - // Integrations from Gateway should always have guild IDs specified. - if (!data.GuildId.IsSpecified) - return; - - var guild = State.GetGuild(data.GuildId.Value); - - if (guild != null) - { - if (!guild.IsSynced) - { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } - - await TimedInvokeAsync(_integrationCreated, nameof(IntegrationCreated), RestIntegration.Create(this, guild, data)).ConfigureAwait(false); - } - else - { - await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); - return; - } - } - break; - case "INTEGRATION_UPDATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (INTEGRATION_UPDATE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - - // Integrations from Gateway should always have guild IDs specified. - if (!data.GuildId.IsSpecified) - return; - - var guild = State.GetGuild(data.GuildId.Value); - - if (guild != null) - { - if (!guild.IsSynced) - { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } - - await TimedInvokeAsync(_integrationUpdated, nameof(IntegrationUpdated), RestIntegration.Create(this, guild, data)).ConfigureAwait(false); - } - else - { - await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); - return; - } - } - break; - case "INTEGRATION_DELETE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (INTEGRATION_DELETE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - - var guild = State.GetGuild(data.GuildId); - - if (guild != null) - { - if (!guild.IsSynced) - { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } - - await TimedInvokeAsync(_integrationDeleted, nameof(IntegrationDeleted), guild, data.Id, data.ApplicationID).ConfigureAwait(false); - } - else - { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; - } - } - break; - #endregion - - #region Users - case "USER_UPDATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (USER_UPDATE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - if (data.Id == CurrentUser.Id) - { - var before = CurrentUser.Clone(); - CurrentUser.Update(State, data); - await TimedInvokeAsync(_selfUpdatedEvent, nameof(CurrentUserUpdated), before, CurrentUser).ConfigureAwait(false); - } - else - { - await _gatewayLogger.WarningAsync("Received USER_UPDATE for wrong user.").ConfigureAwait(false); - return; - } - } - break; - #endregion - - #region Voice - case "VOICE_STATE_UPDATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (VOICE_STATE_UPDATE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - SocketUser user; - SocketVoiceState before, after; - if (data.GuildId != null) - { - var guild = State.GetGuild(data.GuildId.Value); - if (guild == null) - { - await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); - return; - } - else if (!guild.IsSynced) - { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } - - if (data.ChannelId != null) - { - before = guild.GetVoiceState(data.UserId)?.Clone() ?? SocketVoiceState.Default; - after = await guild.AddOrUpdateVoiceStateAsync(State, data).ConfigureAwait(false); - /*if (data.UserId == CurrentUser.Id) - { - var _ = guild.FinishJoinAudioChannel().ConfigureAwait(false); - }*/ - } - else - { - before = await guild.RemoveVoiceStateAsync(data.UserId).ConfigureAwait(false) ?? SocketVoiceState.Default; - after = SocketVoiceState.Create(null, data); - } - - //Per g250k, this should always be sent, but apparently not always - user = guild.GetUser(data.UserId) - ?? (data.Member.IsSpecified ? guild.AddOrUpdateUser(data.Member.Value) : null); - if (user == null) - { - await UnknownGuildUserAsync(type, data.UserId, guild.Id).ConfigureAwait(false); - return; - } - } - else - { - var groupChannel = GetChannel(data.ChannelId.Value) as SocketGroupChannel; - if (groupChannel == null) - { - await UnknownChannelAsync(type, data.ChannelId.Value).ConfigureAwait(false); - return; - } - if (data.ChannelId != null) - { - before = groupChannel.GetVoiceState(data.UserId)?.Clone() ?? SocketVoiceState.Default; - after = groupChannel.AddOrUpdateVoiceState(State, data); - } - else - { - before = groupChannel.RemoveVoiceState(data.UserId) ?? SocketVoiceState.Default; - after = SocketVoiceState.Create(null, data); - } - user = groupChannel.GetUser(data.UserId); - if (user == null) - { - await UnknownChannelUserAsync(type, data.UserId, groupChannel.Id).ConfigureAwait(false); - return; - } - } - - if (user is SocketGuildUser guildUser && data.ChannelId.HasValue) - { - SocketStageChannel stage = guildUser.Guild.GetStageChannel(data.ChannelId.Value); - - if (stage != null && before.VoiceChannel != null && after.VoiceChannel != null) - { - if (!before.RequestToSpeakTimestamp.HasValue && after.RequestToSpeakTimestamp.HasValue) - { - await TimedInvokeAsync(_requestToSpeak, nameof(RequestToSpeak), stage, guildUser); - return; - } - if (before.IsSuppressed && !after.IsSuppressed) - { - await TimedInvokeAsync(_speakerAdded, nameof(SpeakerAdded), stage, guildUser); - return; - } - if (!before.IsSuppressed && after.IsSuppressed) - { - await TimedInvokeAsync(_speakerRemoved, nameof(SpeakerRemoved), stage, guildUser); - } - } - } - - await TimedInvokeAsync(_userVoiceStateUpdatedEvent, nameof(UserVoiceStateUpdated), user, before, after).ConfigureAwait(false); - } - break; - case "VOICE_SERVER_UPDATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (VOICE_SERVER_UPDATE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); - var isCached = guild != null; - var cachedGuild = new Cacheable(guild, data.GuildId, isCached, - () => Task.FromResult(State.GetGuild(data.GuildId) as IGuild)); - - var voiceServer = new SocketVoiceServer(cachedGuild, data.Endpoint, data.Token); - await TimedInvokeAsync(_voiceServerUpdatedEvent, nameof(UserVoiceStateUpdated), voiceServer).ConfigureAwait(false); - - if (isCached) - { - var endpoint = data.Endpoint; - - //Only strip out the port if the endpoint contains it - var portBegin = endpoint.LastIndexOf(':'); - if (portBegin > 0) - endpoint = endpoint.Substring(0, portBegin); - - var _ = guild.FinishConnectAudio(endpoint, data.Token).ConfigureAwait(false); - } - else - { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - } - - } - break; - - case "VOICE_CHANNEL_STATUS_UPDATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (VOICE_CHANNEL_STATUS_UPDATE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); - - var channel = State.GetChannel(data.Id) as SocketVoiceChannel; - var channelCacheable = new Cacheable(channel, data.Id, channel is not null, () => null); - - var before = (string)channel?.Status?.Clone(); - var after = data.Status; - channel?.UpdateVoiceStatus(data.Status); - - await TimedInvokeAsync(_voiceChannelStatusUpdated, nameof(VoiceChannelStatusUpdated), channelCacheable, before, after); - } - break; - #endregion - - #region Invites - case "INVITE_CREATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (INVITE_CREATE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - if (State.GetChannel(data.ChannelId) is SocketGuildChannel channel) - { - var guild = channel.Guild; - if (!guild.IsSynced) - { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } - - SocketGuildUser inviter = data.Inviter.IsSpecified - ? (guild.GetUser(data.Inviter.Value.Id) ?? guild.AddOrUpdateUser(data.Inviter.Value)) - : null; - - SocketUser target = data.TargetUser.IsSpecified - ? (guild.GetUser(data.TargetUser.Value.Id) ?? (SocketUser)SocketUnknownUser.Create(this, State, data.TargetUser.Value)) - : null; - - var invite = SocketInvite.Create(this, guild, channel, inviter, target, data); - - await TimedInvokeAsync(_inviteCreatedEvent, nameof(InviteCreated), invite).ConfigureAwait(false); - } - else - { - await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); - return; - } - } - break; - case "INVITE_DELETE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (INVITE_DELETE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - if (State.GetChannel(data.ChannelId) is SocketGuildChannel channel) - { - var guild = channel.Guild; - if (!guild.IsSynced) - { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } - - await TimedInvokeAsync(_inviteDeletedEvent, nameof(InviteDeleted), channel, data.Code).ConfigureAwait(false); - } - else - { - await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); - return; - } - } - break; - #endregion - - #region Interactions - case "INTERACTION_CREATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (INTERACTION_CREATE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - - var guild = data.GuildId.IsSpecified ? GetGuild(data.GuildId.Value) : null; - - if (guild != null && !guild.IsSynced) - { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - } - - SocketUser user = data.User.IsSpecified - ? State.GetOrAddUser(data.User.Value.Id, (_) => SocketGlobalUser.Create(this, State, data.User.Value)) - : guild != null - ? guild.AddOrUpdateUser(data.Member.Value) // null if the bot scope isn't set, so the guild cannot be retrieved. - : State.GetOrAddUser(data.Member.Value.User.Id, (_) => SocketGlobalUser.Create(this, State, data.Member.Value.User)); - - SocketChannel channel = null; - if (data.ChannelId.IsSpecified) - { - channel = State.GetChannel(data.ChannelId.Value); - - if (channel == null) - { - if (!data.GuildId.IsSpecified) // assume it is a DM - { - channel = CreateDMChannel(data.ChannelId.Value, user, State); - } - - // The channel isn't required when responding to an interaction, so we can leave the channel null. - } - } - else if (data.User.IsSpecified) - { - channel = State.GetDMChannel(data.User.Value.Id); - } - - var interaction = SocketInteraction.Create(this, data, channel as ISocketMessageChannel, user); - - await TimedInvokeAsync(_interactionCreatedEvent, nameof(InteractionCreated), interaction).ConfigureAwait(false); - - switch (interaction) - { - case SocketSlashCommand slashCommand: - await TimedInvokeAsync(_slashCommandExecuted, nameof(SlashCommandExecuted), slashCommand).ConfigureAwait(false); - break; - case SocketMessageComponent messageComponent: - if (messageComponent.Data.Type.IsSelectType()) - await TimedInvokeAsync(_selectMenuExecuted, nameof(SelectMenuExecuted), messageComponent).ConfigureAwait(false); - if (messageComponent.Data.Type == ComponentType.Button) - await TimedInvokeAsync(_buttonExecuted, nameof(ButtonExecuted), messageComponent).ConfigureAwait(false); - break; - case SocketUserCommand userCommand: - await TimedInvokeAsync(_userCommandExecuted, nameof(UserCommandExecuted), userCommand).ConfigureAwait(false); - break; - case SocketMessageCommand messageCommand: - await TimedInvokeAsync(_messageCommandExecuted, nameof(MessageCommandExecuted), messageCommand).ConfigureAwait(false); - break; - case SocketAutocompleteInteraction autocomplete: - await TimedInvokeAsync(_autocompleteExecuted, nameof(AutocompleteExecuted), autocomplete).ConfigureAwait(false); - break; - case SocketModal modal: - await TimedInvokeAsync(_modalSubmitted, nameof(ModalSubmitted), modal).ConfigureAwait(false); - break; - } - } - break; - case "APPLICATION_COMMAND_CREATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (APPLICATION_COMMAND_CREATE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - - if (data.GuildId.IsSpecified) - { - var guild = State.GetGuild(data.GuildId.Value); - if (guild == null) - { - await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); - return; - } - } - - var applicationCommand = SocketApplicationCommand.Create(this, data); - - State.AddCommand(applicationCommand); - - await TimedInvokeAsync(_applicationCommandCreated, nameof(ApplicationCommandCreated), applicationCommand).ConfigureAwait(false); - } - break; - case "APPLICATION_COMMAND_UPDATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (APPLICATION_COMMAND_UPDATE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - - if (data.GuildId.IsSpecified) - { - var guild = State.GetGuild(data.GuildId.Value); - if (guild == null) - { - await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); - return; - } - } - - var applicationCommand = SocketApplicationCommand.Create(this, data); - - State.AddCommand(applicationCommand); - - await TimedInvokeAsync(_applicationCommandUpdated, nameof(ApplicationCommandUpdated), applicationCommand).ConfigureAwait(false); - } - break; - case "APPLICATION_COMMAND_DELETE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (APPLICATION_COMMAND_DELETE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - - if (data.GuildId.IsSpecified) - { - var guild = State.GetGuild(data.GuildId.Value); - if (guild == null) - { - await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); - return; - } - } - - var applicationCommand = SocketApplicationCommand.Create(this, data); - - State.RemoveCommand(applicationCommand.Id); - - await TimedInvokeAsync(_applicationCommandDeleted, nameof(ApplicationCommandDeleted), applicationCommand).ConfigureAwait(false); - } - break; - #endregion - - #region Threads - case "THREAD_CREATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_CREATE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - - var guild = State.GetGuild(data.GuildId.Value); - - if (guild == null) - { - await UnknownGuildAsync(type, data.GuildId.Value); - return; - } - - SocketThreadChannel threadChannel = null; - - if ((threadChannel = guild.ThreadChannels.FirstOrDefault(x => x.Id == data.Id)) != null) - { - threadChannel.Update(State, data); - - if (data.ThreadMember.IsSpecified) - threadChannel.AddOrUpdateThreadMember(data.ThreadMember.Value, guild.CurrentUser); - } - else - { - threadChannel = (SocketThreadChannel)guild.AddChannel(State, data); - if (data.ThreadMember.IsSpecified) - threadChannel.AddOrUpdateThreadMember(data.ThreadMember.Value, guild.CurrentUser); - } - - await TimedInvokeAsync(_threadCreated, nameof(ThreadCreated), threadChannel).ConfigureAwait(false); - } - - break; - case "THREAD_UPDATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_UPDATE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId.Value); - if (guild == null) - { - await UnknownGuildAsync(type, data.GuildId.Value); - return; - } - - var threadChannel = guild.ThreadChannels.FirstOrDefault(x => x.Id == data.Id); - var before = threadChannel != null - ? new Cacheable(threadChannel.Clone(), data.Id, true, () => Task.FromResult((SocketThreadChannel)null)) - : new Cacheable(null, data.Id, false, () => Task.FromResult((SocketThreadChannel)null)); - - if (threadChannel != null) - { - threadChannel.Update(State, data); - - if (data.ThreadMember.IsSpecified) - threadChannel.AddOrUpdateThreadMember(data.ThreadMember.Value, guild.CurrentUser); - } - else - { - //Thread is updated but was not cached, likely meaning the thread was unarchived. - threadChannel = (SocketThreadChannel)guild.AddChannel(State, data); - if (data.ThreadMember.IsSpecified) - threadChannel.AddOrUpdateThreadMember(data.ThreadMember.Value, guild.CurrentUser); - } - - if (!(guild?.IsSynced ?? true)) - { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } - - await TimedInvokeAsync(_threadUpdated, nameof(ThreadUpdated), before, threadChannel).ConfigureAwait(false); - } - break; - case "THREAD_DELETE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_DELETE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - - var guild = State.GetGuild(data.GuildId.Value); - - if (guild == null) - { - await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); - return; - } - - var thread = (SocketThreadChannel)guild.RemoveChannel(State, data.Id); - - var cacheable = new Cacheable(thread, data.Id, thread != null, null); - - await TimedInvokeAsync(_threadDeleted, nameof(ThreadDeleted), cacheable).ConfigureAwait(false); - } - break; - case "THREAD_LIST_SYNC": - { - await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_LIST_SYNC)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - - var guild = State.GetGuild(data.GuildId); - - if (guild == null) - { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; - } - - foreach (var thread in data.Threads) - { - var entity = guild.ThreadChannels.FirstOrDefault(x => x.Id == thread.Id); - - if (entity == null) - { - entity = (SocketThreadChannel)guild.AddChannel(State, thread); - } - else - { - entity.Update(State, thread); - } - - foreach (var member in data.Members.Where(x => x.Id.Value == entity.Id)) - { - var guildMember = guild.GetUser(member.Id.Value); - - entity.AddOrUpdateThreadMember(member, guildMember); - } - } - } - break; - case "THREAD_MEMBER_UPDATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_MEMBER_UPDATE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - - var thread = (SocketThreadChannel)State.GetChannel(data.Id.Value); - - if (thread == null) - { - await UnknownChannelAsync(type, data.Id.Value); - return; - } - - thread.AddOrUpdateThreadMember(data, thread.Guild.CurrentUser); - } - - break; - case "THREAD_MEMBERS_UPDATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_MEMBERS_UPDATE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - - var guild = State.GetGuild(data.GuildId); - - if (guild == null) - { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; - } - - var thread = (SocketThreadChannel)guild.GetChannel(data.Id); - - if (thread == null) - { - await UnknownChannelAsync(type, data.Id); - return; - } - - IReadOnlyCollection leftUsers = null; - IReadOnlyCollection joinUsers = null; - - - if (data.RemovedMemberIds.IsSpecified) - { - leftUsers = thread.RemoveUsers(data.RemovedMemberIds.Value); - } - - if (data.AddedMembers.IsSpecified) - { - List newThreadMembers = new List(); - foreach (var threadMember in data.AddedMembers.Value) - { - SocketGuildUser guildMember; - - guildMember = guild.GetUser(threadMember.UserId.Value); - - if (guildMember == null) - { - await UnknownGuildUserAsync("THREAD_MEMBERS_UPDATE", threadMember.UserId.Value, guild.Id); - } - else - newThreadMembers.Add(thread.AddOrUpdateThreadMember(threadMember, guildMember)); - } - - if (newThreadMembers.Any()) - joinUsers = newThreadMembers.ToImmutableArray(); - } - - if (leftUsers != null) - { - foreach (var threadUser in leftUsers) - { - await TimedInvokeAsync(_threadMemberLeft, nameof(ThreadMemberLeft), threadUser).ConfigureAwait(false); - } - } - - if (joinUsers != null) - { - foreach (var threadUser in joinUsers) - { - await TimedInvokeAsync(_threadMemberJoined, nameof(ThreadMemberJoined), threadUser).ConfigureAwait(false); - } - } - } - - break; - #endregion - - #region Stage Channels - case "STAGE_INSTANCE_CREATE" or "STAGE_INSTANCE_UPDATE" or "STAGE_INSTANCE_DELETE": - { - await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - - var guild = State.GetGuild(data.GuildId); - - if (guild == null) - { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; - } - - var stageChannel = guild.GetStageChannel(data.ChannelId); - - if (stageChannel == null) - { - await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); - return; - } - - SocketStageChannel before = type == "STAGE_INSTANCE_UPDATE" ? stageChannel.Clone() : null; - - stageChannel.Update(data, type == "STAGE_INSTANCE_CREATE"); - - switch (type) - { - case "STAGE_INSTANCE_CREATE": - await TimedInvokeAsync(_stageStarted, nameof(StageStarted), stageChannel).ConfigureAwait(false); - return; - case "STAGE_INSTANCE_DELETE": - await TimedInvokeAsync(_stageEnded, nameof(StageEnded), stageChannel).ConfigureAwait(false); - return; - case "STAGE_INSTANCE_UPDATE": - await TimedInvokeAsync(_stageUpdated, nameof(StageUpdated), before, stageChannel).ConfigureAwait(false); - return; - } - } - break; - #endregion - - #region Guild Scheduled Events - case "GUILD_SCHEDULED_EVENT_CREATE": - { - await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - - var guild = State.GetGuild(data.GuildId); - - if (guild == null) - { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; - } - - var newEvent = guild.AddOrUpdateEvent(data); - - await TimedInvokeAsync(_guildScheduledEventCreated, nameof(GuildScheduledEventCreated), newEvent).ConfigureAwait(false); - } - break; - case "GUILD_SCHEDULED_EVENT_UPDATE": - { - await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - - var guild = State.GetGuild(data.GuildId); - - if (guild == null) - { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; - } - - var before = guild.GetEvent(data.Id)?.Clone(); - - var beforeCacheable = new Cacheable(before, data.Id, before != null, () => Task.FromResult((SocketGuildEvent)null)); - - var after = guild.AddOrUpdateEvent(data); - - if ((before != null ? before.Status != GuildScheduledEventStatus.Completed : true) && data.Status == GuildScheduledEventStatus.Completed) - { - await TimedInvokeAsync(_guildScheduledEventCompleted, nameof(GuildScheduledEventCompleted), after).ConfigureAwait(false); - } - else if ((before != null ? before.Status != GuildScheduledEventStatus.Active : false) && data.Status == GuildScheduledEventStatus.Active) - { - await TimedInvokeAsync(_guildScheduledEventStarted, nameof(GuildScheduledEventStarted), after).ConfigureAwait(false); - } - else - await TimedInvokeAsync(_guildScheduledEventUpdated, nameof(GuildScheduledEventUpdated), beforeCacheable, after).ConfigureAwait(false); - } - break; - case "GUILD_SCHEDULED_EVENT_DELETE": - { - await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - - var guild = State.GetGuild(data.GuildId); - - if (guild == null) - { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; - } - - var guildEvent = guild.RemoveEvent(data.Id) ?? SocketGuildEvent.Create(this, guild, data); - - await TimedInvokeAsync(_guildScheduledEventCancelled, nameof(GuildScheduledEventCancelled), guildEvent).ConfigureAwait(false); - } - break; - case "GUILD_SCHEDULED_EVENT_USER_ADD" or "GUILD_SCHEDULED_EVENT_USER_REMOVE": - { - await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - - var guild = State.GetGuild(data.GuildId); - - if (guild == null) - { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; - } - - var guildEvent = guild.GetEvent(data.EventId); - - if (guildEvent == null) - { - await UnknownGuildEventAsync(type, data.EventId, data.GuildId).ConfigureAwait(false); - return; - } - - var user = (SocketUser)guild.GetUser(data.UserId) ?? State.GetUser(data.UserId); - - var cacheableUser = new Cacheable(user, data.UserId, user != null, () => Rest.GetUserAsync(data.UserId)); - - switch (type) - { - case "GUILD_SCHEDULED_EVENT_USER_ADD": - await TimedInvokeAsync(_guildScheduledEventUserAdd, nameof(GuildScheduledEventUserAdd), cacheableUser, guildEvent).ConfigureAwait(false); - break; - case "GUILD_SCHEDULED_EVENT_USER_REMOVE": - await TimedInvokeAsync(_guildScheduledEventUserRemove, nameof(GuildScheduledEventUserRemove), cacheableUser, guildEvent).ConfigureAwait(false); - break; - } - } - break; - - #endregion - - #region Webhooks - - case "WEBHOOKS_UPDATE": - { - var data = (payload as JToken).ToObject(_serializer); - type = "WEBHOOKS_UPDATE"; - await _gatewayLogger.DebugAsync("Received Dispatch (WEBHOOKS_UPDATE)").ConfigureAwait(false); - - var guild = State.GetGuild(data.GuildId); - var channel = State.GetChannel(data.ChannelId); - - await TimedInvokeAsync(_webhooksUpdated, nameof(WebhooksUpdated), guild, channel); - } - break; - - #endregion - - #region Audit Logs - - case "GUILD_AUDIT_LOG_ENTRY_CREATE": - { - var data = (payload as JToken).ToObject(_serializer); - type = "GUILD_AUDIT_LOG_ENTRY_CREATE"; - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_AUDIT_LOG_ENTRY_CREATE)").ConfigureAwait(false); - - var guild = State.GetGuild(data.GuildId); - var auditLog = SocketAuditLogEntry.Create(this, data); - guild.AddAuditLog(auditLog); - - await TimedInvokeAsync(_auditLogCreated, nameof(AuditLogCreated), auditLog, guild); - } - break; - #endregion - - #region Auto Moderation - - case "AUTO_MODERATION_RULE_CREATE": - { - var data = (payload as JToken).ToObject(_serializer); - - var guild = State.GetGuild(data.GuildId); - - var rule = guild.AddOrUpdateAutoModRule(data); - - await TimedInvokeAsync(_autoModRuleCreated, nameof(AutoModRuleCreated), rule); - } - break; - - case "AUTO_MODERATION_RULE_UPDATE": - { - var data = (payload as JToken).ToObject(_serializer); - - var guild = State.GetGuild(data.GuildId); - - var cachedRule = guild.GetAutoModRule(data.Id); - var cacheableBefore = new Cacheable(cachedRule?.Clone(), - data.Id, - cachedRule is not null, - async () => await guild.GetAutoModRuleAsync(data.Id)); - - await TimedInvokeAsync(_autoModRuleUpdated, nameof(AutoModRuleUpdated), cacheableBefore, guild.AddOrUpdateAutoModRule(data)); - } - break; - - case "AUTO_MODERATION_RULE_DELETE": - { - var data = (payload as JToken).ToObject(_serializer); - - var guild = State.GetGuild(data.GuildId); - - var rule = guild.RemoveAutoModRule(data); - - await TimedInvokeAsync(_autoModRuleDeleted, nameof(AutoModRuleDeleted), rule); - } - break; - - case "AUTO_MODERATION_ACTION_EXECUTION": - { - var data = (payload as JToken).ToObject(_serializer); - - var guild = State.GetGuild(data.GuildId); - var action = new AutoModRuleAction(data.Action.Type, - data.Action.Metadata.IsSpecified - ? data.Action.Metadata.Value.ChannelId.IsSpecified - ? data.Action.Metadata.Value.ChannelId.Value - : null - : null, - data.Action.Metadata.IsSpecified - ? data.Action.Metadata.Value.DurationSeconds.IsSpecified - ? data.Action.Metadata.Value.DurationSeconds.Value - : null - : null, - data.Action.Metadata.IsSpecified - ? data.Action.Metadata.Value.CustomMessage.IsSpecified - ? data.Action.Metadata.Value.CustomMessage.Value - : null - : null); - - - var member = guild.GetUser(data.UserId); - - var cacheableUser = new Cacheable(member, - data.UserId, - member is not null, - async () => - { - var model = await ApiClient.GetGuildMemberAsync(data.GuildId, data.UserId); - return guild.AddOrUpdateUser(model); - } - ); - - ISocketMessageChannel channel = null; - if (data.ChannelId.IsSpecified) - channel = GetChannel(data.ChannelId.Value) as ISocketMessageChannel; - - var cacheableChannel = new Cacheable(channel, - data.ChannelId.GetValueOrDefault(0), - channel != null, - async () => - { - if (data.ChannelId.IsSpecified) - return await GetChannelAsync(data.ChannelId.Value).ConfigureAwait(false) as ISocketMessageChannel; - return null; - }); - - - IUserMessage cachedMsg = null; - if (data.MessageId.IsSpecified) - cachedMsg = channel?.GetCachedMessage(data.MessageId.GetValueOrDefault(0)) as IUserMessage; - - var cacheableMessage = new Cacheable(cachedMsg, - data.MessageId.GetValueOrDefault(0), - cachedMsg is not null, - async () => - { - if (data.MessageId.IsSpecified) - return (await channel!.GetMessageAsync(data.MessageId.Value).ConfigureAwait(false)) as IUserMessage; - return null; - }); - - var cachedRule = guild.GetAutoModRule(data.RuleId); - - var cacheableRule = new Cacheable(cachedRule, - data.RuleId, - cachedRule is not null, - async () => await guild.GetAutoModRuleAsync(data.RuleId)); - - var eventData = new AutoModActionExecutedData( - cacheableRule, - data.TriggerType, - cacheableUser, - cacheableChannel, - data.MessageId.IsSpecified ? cacheableMessage : null, - data.AlertSystemMessageId.GetValueOrDefault(0), - data.Content, - data.MatchedContent.IsSpecified - ? data.MatchedContent.Value - : null, - data.MatchedKeyword.IsSpecified - ? data.MatchedKeyword.Value - : null); - - await TimedInvokeAsync(_autoModActionExecuted, nameof(AutoModActionExecuted), guild, action, eventData); - } - break; - - #endregion - - #region App Subscriptions - - case "ENTITLEMENT_CREATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (ENTITLEMENT_CREATE)").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); - - var entitlement = SocketEntitlement.Create(this, data); - State.AddEntitlement(data.Id, entitlement); - - await TimedInvokeAsync(_entitlementCreated, nameof(EntitlementCreated), entitlement); - } - break; - - case "ENTITLEMENT_UPDATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (ENTITLEMENT_UPDATE)").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); - - var entitlement = State.GetEntitlement(data.Id); - - var cacheableBefore = new Cacheable(entitlement?.Clone(), data.Id, - entitlement is not null, () => null); - - if (entitlement is null) - { - entitlement = SocketEntitlement.Create(this, data); - State.AddEntitlement(data.Id, entitlement); - } - else - { - entitlement.Update(data); - } - - await TimedInvokeAsync(_entitlementUpdated, nameof(EntitlementUpdated), cacheableBefore, entitlement); - } - break; - - case "ENTITLEMENT_DELETE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (ENTITLEMENT_DELETE)").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); - - var entitlement = State.RemoveEntitlement(data.Id); - - if (entitlement is null) - entitlement = SocketEntitlement.Create(this, data); - else - entitlement.Update(data); - - var cacheableEntitlement = new Cacheable(entitlement, data.Id, - entitlement is not null, () => null); - - await TimedInvokeAsync(_entitlementDeleted, nameof(EntitlementDeleted), cacheableEntitlement); - } - break; - - #endregion - - #region Ignored (User only) - case "CHANNEL_PINS_ACK": - await _gatewayLogger.DebugAsync("Ignored Dispatch (CHANNEL_PINS_ACK)").ConfigureAwait(false); - break; - case "CHANNEL_PINS_UPDATE": - await _gatewayLogger.DebugAsync("Ignored Dispatch (CHANNEL_PINS_UPDATE)").ConfigureAwait(false); - break; - case "GUILD_INTEGRATIONS_UPDATE": - await _gatewayLogger.DebugAsync("Ignored Dispatch (GUILD_INTEGRATIONS_UPDATE)").ConfigureAwait(false); - break; - case "MESSAGE_ACK": - await _gatewayLogger.DebugAsync("Ignored Dispatch (MESSAGE_ACK)").ConfigureAwait(false); - break; - case "PRESENCES_REPLACE": - await _gatewayLogger.DebugAsync("Ignored Dispatch (PRESENCES_REPLACE)").ConfigureAwait(false); - break; - case "USER_SETTINGS_UPDATE": - await _gatewayLogger.DebugAsync("Ignored Dispatch (USER_SETTINGS_UPDATE)").ConfigureAwait(false); - break; - #endregion - - #region Others - default: - if (!SuppressUnknownDispatchWarnings) - await _gatewayLogger.WarningAsync($"Unknown Dispatch ({type})").ConfigureAwait(false); - break; - #endregion - } - break; - default: - await _gatewayLogger.WarningAsync($"Unknown OpCode ({opCode})").ConfigureAwait(false); - break; - } - } - catch (Exception ex) - { - if (IncludeRawPayloadOnGatewayErrors) - { - ex.Data["opcode"] = opCode; - ex.Data["type"] = type; - ex.Data["payload_data"] = (payload as JToken).ToString(); - } - - await _gatewayLogger.ErrorAsync($"Error handling {opCode}{(type != null ? $" ({type})" : "")}", ex).ConfigureAwait(false); - } - } - #endregion - private async Task RunHeartbeatAsync(int intervalMillis, CancellationToken cancelToken) { int delayInterval = (int)(intervalMillis * DiscordConfig.HeartbeatIntervalFactor); @@ -3681,6 +1242,14 @@ private Task UnsyncedGuildAsync(string evnt, ulong guildId) #region IDiscordClient + /// + async Task IDiscordClient.GetSKUSubscriptionAsync(ulong skuId, ulong subscriptionId, RequestOptions options) + => await GetSKUSubscriptionAsync(skuId, subscriptionId, options); + + /// + IAsyncEnumerable> IDiscordClient.GetSKUSubscriptionsAsync(ulong skuId, int limit, ulong? afterId, + ulong? beforeId, ulong? userId, RequestOptions options) => GetSKUSubscriptionsAsync(skuId, limit, afterId, beforeId, userId, options); + async Task IDiscordClient.CreateTestEntitlementAsync(ulong skuId, ulong ownerId, SubscriptionOwnerType ownerType, RequestOptions options) => await CreateTestEntitlementAsync(skuId, ownerId, ownerType, options).ConfigureAwait(false); /// diff --git a/src/Discord.Net.WebSocket/Entities/AppSubscriptions/SocketSubscription.cs b/src/Discord.Net.WebSocket/Entities/AppSubscriptions/SocketSubscription.cs new file mode 100644 index 0000000000..f7b4445ae7 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/AppSubscriptions/SocketSubscription.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Discord.WebSocket; + +/// +/// Represents an application subscription. +/// +public class SocketSubscription : SocketEntity, ISubscription +{ + /// + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + + /// + public ulong UserId { get; private set; } + + /// + public IReadOnlyCollection SKUIds { get; private set; } + + /// + public IReadOnlyCollection EntitlementIds { get; private set; } + + /// + public DateTimeOffset CurrentPeriodStart { get; private set; } + + /// + public DateTimeOffset CurrentPeriodEnd { get; private set; } + + /// + public SubscriptionStatus Status { get; private set; } + + /// + public DateTimeOffset? CanceledAt { get; private set; } + + /// + public string Country { get; private set; } + + internal SocketSubscription(DiscordSocketClient discord, ulong id) : base(discord, id) + { + } + + internal static SocketSubscription Create(DiscordSocketClient discord, API.Subscription model) + { + var s = new SocketSubscription(discord, model.Id); + s.Update(model); + return s; + } + + internal void Update(API.Subscription model) + { + UserId = model.UserId; + SKUIds = model.SKUIds.ToImmutableArray(); + EntitlementIds = model.EntitlementIds.ToImmutableArray(); + CurrentPeriodStart = model.CurrentPeriodStart; + CurrentPeriodEnd = model.CurrentPeriodEnd; + Status = model.Status; + CanceledAt = model.CanceledAt; + Country = model.Country; + } + + internal SocketSubscription Clone() + => MemberwiseClone() as SocketSubscription; +}