From 28ea31ed27f6ca289feb9fb4df423073161f20d9 Mon Sep 17 00:00:00 2001 From: Suiram1701 <110390261+Suiram1701@users.noreply.github.com> Date: Wed, 23 Apr 2025 21:31:28 +0200 Subject: [PATCH 01/12] Added a basic otel instrumentation project and the classes for WebSocket --- Discord.Net.sln | 15 ++++++++++++ .../Discord.Net.OpenTelemetry.csproj | 22 ++++++++++++++++++ src/Discord.Net.OpenTelemetry/Extensions.cs | 23 +++++++++++++++++++ .../Diagnostics/SocketActivitySource.cs | 20 ++++++++++++++++ .../Diagnostics/SocketMeter.cs | 16 +++++++++++++ 5 files changed, 96 insertions(+) create mode 100644 src/Discord.Net.OpenTelemetry/Discord.Net.OpenTelemetry.csproj create mode 100644 src/Discord.Net.OpenTelemetry/Extensions.cs create mode 100644 src/Discord.Net.WebSocket/Diagnostics/SocketActivitySource.cs create mode 100644 src/Discord.Net.WebSocket/Diagnostics/SocketMeter.cs diff --git a/Discord.Net.sln b/Discord.Net.sln index 48c80d54fe..80b92bee7d 100644 --- a/Discord.Net.sln +++ b/Discord.Net.sln @@ -42,6 +42,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{BB59 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net.BuildOverrides", "experiment\Discord.Net.BuildOverrides\Discord.Net.BuildOverrides.csproj", "{115F4921-B44D-4F69-996B-69796959C99D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net.OpenTelemetry", "src\Discord.Net.OpenTelemetry\Discord.Net.OpenTelemetry.csproj", "{88D77C2C-547E-41B8-8AFC-D1C089652767}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -244,6 +246,18 @@ Global {115F4921-B44D-4F69-996B-69796959C99D}.Release|x64.Build.0 = Release|Any CPU {115F4921-B44D-4F69-996B-69796959C99D}.Release|x86.ActiveCfg = Release|Any CPU {115F4921-B44D-4F69-996B-69796959C99D}.Release|x86.Build.0 = Release|Any CPU + {88D77C2C-547E-41B8-8AFC-D1C089652767}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {88D77C2C-547E-41B8-8AFC-D1C089652767}.Debug|Any CPU.Build.0 = Debug|Any CPU + {88D77C2C-547E-41B8-8AFC-D1C089652767}.Debug|x64.ActiveCfg = Debug|Any CPU + {88D77C2C-547E-41B8-8AFC-D1C089652767}.Debug|x64.Build.0 = Debug|Any CPU + {88D77C2C-547E-41B8-8AFC-D1C089652767}.Debug|x86.ActiveCfg = Debug|Any CPU + {88D77C2C-547E-41B8-8AFC-D1C089652767}.Debug|x86.Build.0 = Debug|Any CPU + {88D77C2C-547E-41B8-8AFC-D1C089652767}.Release|Any CPU.ActiveCfg = Release|Any CPU + {88D77C2C-547E-41B8-8AFC-D1C089652767}.Release|Any CPU.Build.0 = Release|Any CPU + {88D77C2C-547E-41B8-8AFC-D1C089652767}.Release|x64.ActiveCfg = Release|Any CPU + {88D77C2C-547E-41B8-8AFC-D1C089652767}.Release|x64.Build.0 = Release|Any CPU + {88D77C2C-547E-41B8-8AFC-D1C089652767}.Release|x86.ActiveCfg = Release|Any CPU + {88D77C2C-547E-41B8-8AFC-D1C089652767}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -264,6 +278,7 @@ Global {B61AAE66-15CC-40E4-873A-C23E697C3411} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B} {4A03840B-9EBE-47E3-89AB-E0914DF21AFB} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B} {115F4921-B44D-4F69-996B-69796959C99D} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A} + {88D77C2C-547E-41B8-8AFC-D1C089652767} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D2404771-EEC8-45F2-9D71-F3373F6C1495} diff --git a/src/Discord.Net.OpenTelemetry/Discord.Net.OpenTelemetry.csproj b/src/Discord.Net.OpenTelemetry/Discord.Net.OpenTelemetry.csproj new file mode 100644 index 0000000000..f417a0a045 --- /dev/null +++ b/src/Discord.Net.OpenTelemetry/Discord.Net.OpenTelemetry.csproj @@ -0,0 +1,22 @@ + + + + + net9.0;net8.0;net6.0;net5.0;net461;netstandard2.0;netstandard2.1 + Discord.OpenTelemetry + Discord.Net.OpenTelemetry + A Discord.Net extension adding support for the OpenTelemetry (otel) Sdk. + 5 + True + true + snupkg + + + + + + + + + + diff --git a/src/Discord.Net.OpenTelemetry/Extensions.cs b/src/Discord.Net.OpenTelemetry/Extensions.cs new file mode 100644 index 0000000000..26132a3d1c --- /dev/null +++ b/src/Discord.Net.OpenTelemetry/Extensions.cs @@ -0,0 +1,23 @@ +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; +using System; + +namespace Discord.OpenTelemetry +{ + /// + /// An extension class which contains methods to added the Discord.Net OpenTelemetry instrumentations. + /// + public static class Extensions + { + public static TracerProviderBuilder AddDiscordNetInstrumentation(this TracerProviderBuilder builder) + { + throw new NotImplementedException(); + } + + public static MeterProviderBuilder AddDiscordNetInstrumentation(this MeterProviderBuilder builder) + { + throw new NotImplementedException(); + } + } + +} diff --git a/src/Discord.Net.WebSocket/Diagnostics/SocketActivitySource.cs b/src/Discord.Net.WebSocket/Diagnostics/SocketActivitySource.cs new file mode 100644 index 0000000000..3f57a8563e --- /dev/null +++ b/src/Discord.Net.WebSocket/Diagnostics/SocketActivitySource.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.WebSocket.Diagnostics +{ + internal static class SocketActivitySource + { +#if NET5_0_OR_GREATER + private static readonly ActivitySource _source = new( + name: "Discord.Net.WebSocket", + version: typeof(DiscordSocketClient).Assembly.GetName().Version.ToString()); +#else +#endif + } +} diff --git a/src/Discord.Net.WebSocket/Diagnostics/SocketMeter.cs b/src/Discord.Net.WebSocket/Diagnostics/SocketMeter.cs new file mode 100644 index 0000000000..bfddf8dfe6 --- /dev/null +++ b/src/Discord.Net.WebSocket/Diagnostics/SocketMeter.cs @@ -0,0 +1,16 @@ +#if NET6_0_OR_GREATER +using System.Diagnostics.Metrics; +#endif + +namespace Discord.WebSocket.Diagnostics +{ + internal static class SocketMeter + { +#if NET6_0_OR_GREATER + private readonly static Meter _meter = new( + name: "Discord.Net.WebSocket", + version: typeof(DiscordSocketClient).Assembly.GetName().Version.ToString()); +#else +#endif + } +} From 05f597f7a395f5b14c1e1ef66248d5102d5801d3 Mon Sep 17 00:00:00 2001 From: Suiram1701 <110390261+Suiram1701@users.noreply.github.com> Date: Thu, 24 Apr 2025 20:06:08 +0200 Subject: [PATCH 02/12] Added the source names to the extensions and added otel project to the Github actions workflow --- .github/workflows/dotnet.yml | 1 + src/Discord.Net.OpenTelemetry/Extensions.cs | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 65f0fe2e3e..e258b67279 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -65,6 +65,7 @@ jobs: dotnet pack "src\Discord.Net.Commands\Discord.Net.Commands.csproj" --no-restore --no-build -v minimal -c Release -o ${{ env.ArtifactStagingDirectory }} /p:BuildNumber=${{ env.Suffix }} /p:IsTagBuild=${{ env.IsTagBuild }} dotnet pack "src\Discord.Net.Webhook\Discord.Net.Webhook.csproj" --no-restore --no-build -v minimal -c Release -o ${{ env.ArtifactStagingDirectory }} /p:BuildNumber=${{ env.Suffix }} /p:IsTagBuild=${{ env.IsTagBuild }} dotnet pack "src\Discord.Net.Interactions\Discord.Net.Interactions.csproj" --no-restore --no-build -v minimal -c Release -o ${{ env.ArtifactStagingDirectory }} /p:BuildNumber=${{ env.Suffix }} /p:IsTagBuild=${{ env.IsTagBuild }} + dotnet pack "src\Discord.Net.OpenTelemetry\Discord.Net.OpenTelemetry.csproj" --no-restore --no-build -v minimal -c Release -o ${{ env.ArtifactStagingDirectory }} /p:BuildNumber=${{ env.Suffix }} /p:IsTagBuild=${{ env.IsTagBuild }} # dotnet pack "experiment\Discord.Net.BuildOverrides\Discord.Net.BuildOverrides.csproj" --no-restore --no-build -v minimal -c Release -o ${{ env.ArtifactStagingDirectory }} /p:BuildNumber=${{ env.Suffix }} /p:IsTagBuild=${{ env.IsTagBuild }} - name: Publish Artifacts diff --git a/src/Discord.Net.OpenTelemetry/Extensions.cs b/src/Discord.Net.OpenTelemetry/Extensions.cs index 26132a3d1c..7ecfe403da 100644 --- a/src/Discord.Net.OpenTelemetry/Extensions.cs +++ b/src/Discord.Net.OpenTelemetry/Extensions.cs @@ -9,14 +9,20 @@ namespace Discord.OpenTelemetry /// public static class Extensions { + private const string SourceName = "Discord.Net.WebSocket"; + public static TracerProviderBuilder AddDiscordNetInstrumentation(this TracerProviderBuilder builder) { - throw new NotImplementedException(); + if (builder is null) + throw new ArgumentNullException(nameof(builder)); + return builder.AddSource(SourceName); } public static MeterProviderBuilder AddDiscordNetInstrumentation(this MeterProviderBuilder builder) { - throw new NotImplementedException(); + if (builder is null) + throw new ArgumentNullException(nameof(builder)); + return builder.AddMeter(SourceName); } } From a3f074c42ad282990dc52ec8d08e0bd816117382 Mon Sep 17 00:00:00 2001 From: Suiram1701 <110390261+Suiram1701@users.noreply.github.com> Date: Thu, 24 Apr 2025 21:48:37 +0200 Subject: [PATCH 03/12] Added metrics and tracing for socket events --- .../Diagnostics/Options.cs | 23 + .../Diagnostics/SocketActivity.cs | 43 + .../Diagnostics/SocketActivitySource.cs | 20 - .../Diagnostics/SocketMeter.cs | 70 +- .../DiscordSocketClient.EventHandling.cs | 3641 +++++++++-------- 5 files changed, 1966 insertions(+), 1831 deletions(-) create mode 100644 src/Discord.Net.WebSocket/Diagnostics/Options.cs create mode 100644 src/Discord.Net.WebSocket/Diagnostics/SocketActivity.cs delete mode 100644 src/Discord.Net.WebSocket/Diagnostics/SocketActivitySource.cs diff --git a/src/Discord.Net.WebSocket/Diagnostics/Options.cs b/src/Discord.Net.WebSocket/Diagnostics/Options.cs new file mode 100644 index 0000000000..060e6d307a --- /dev/null +++ b/src/Discord.Net.WebSocket/Diagnostics/Options.cs @@ -0,0 +1,23 @@ +using Discord.API.Gateway; +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Discord.WebSocket.Diagnostics +{ + internal static class Options + { + internal const string SourceName = "Discord.Net.WebSocket"; + internal static readonly string Version = typeof(Options).Assembly.GetName().Version.ToString(); + +#if NET5_0_OR_GREATER + internal static IEnumerable> CreateTags(GatewayOpCode opCode, string type, DiscordSocketConfig config) => [ + KeyValuePair.Create("client.gateway_host", config.GatewayHost ?? "/gateway"), + KeyValuePair.Create("client.shard_id", config.ShardId ?? 0), + KeyValuePair.Create("event.op_code", opCode), + KeyValuePair.Create("event.type", type) + ]; +#endif + } + +} diff --git a/src/Discord.Net.WebSocket/Diagnostics/SocketActivity.cs b/src/Discord.Net.WebSocket/Diagnostics/SocketActivity.cs new file mode 100644 index 0000000000..db1b2ee52b --- /dev/null +++ b/src/Discord.Net.WebSocket/Diagnostics/SocketActivity.cs @@ -0,0 +1,43 @@ +using Discord.API.Gateway; +using System; +using System.Diagnostics; + +namespace Discord.WebSocket.Diagnostics +{ + internal static class SocketActivity + { +#if NET5_0_OR_GREATER + private static readonly ActivitySource _source = new(Options.SourceName, Options.Version); + + internal static Activity StartSocketDispatchActivity(string type, DiscordSocketConfig config) + { + return _source.StartActivity( + "dispatch socket event", + ActivityKind.Consumer, + null, + tags: Options.CreateTags(GatewayOpCode.Dispatch, type, config)); + } + + internal static void AddExceptionToActivity(this Activity activity, Exception ex) + { +#if NET6_0_OR_GREATER + activity.SetStatus(ActivityStatusCode.Error, ex.Message); +#endif +#if NET9_0_OR_GREATER + activity.AddException(ex); +#else + activity.AddEvent(new("exception", tags: new() + { + { "exception.type", ex.GetType().ToString() }, + { "exception.message", ex.Message }, + { "exception.stacktrace", ex.ToString() } + })); +#endif + } +#else + internal static IDisposable StartSocketDispatchActivity(string type, DiscordSocketConfig config) => null; + + internal static void AddExceptionToActivity(this IDisposable activity, Exception ex) { } +#endif + } +} diff --git a/src/Discord.Net.WebSocket/Diagnostics/SocketActivitySource.cs b/src/Discord.Net.WebSocket/Diagnostics/SocketActivitySource.cs deleted file mode 100644 index 3f57a8563e..0000000000 --- a/src/Discord.Net.WebSocket/Diagnostics/SocketActivitySource.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; - -namespace Discord.WebSocket.Diagnostics -{ - internal static class SocketActivitySource - { -#if NET5_0_OR_GREATER - private static readonly ActivitySource _source = new( - name: "Discord.Net.WebSocket", - version: typeof(DiscordSocketClient).Assembly.GetName().Version.ToString()); -#else -#endif - } -} diff --git a/src/Discord.Net.WebSocket/Diagnostics/SocketMeter.cs b/src/Discord.Net.WebSocket/Diagnostics/SocketMeter.cs index bfddf8dfe6..0f8d9dbdda 100644 --- a/src/Discord.Net.WebSocket/Diagnostics/SocketMeter.cs +++ b/src/Discord.Net.WebSocket/Diagnostics/SocketMeter.cs @@ -1,3 +1,8 @@ +using Discord.API.Gateway; +using System; +using System.Collections.Generic; +using System.Diagnostics; + #if NET6_0_OR_GREATER using System.Diagnostics.Metrics; #endif @@ -7,10 +12,69 @@ namespace Discord.WebSocket.Diagnostics internal static class SocketMeter { #if NET6_0_OR_GREATER - private readonly static Meter _meter = new( - name: "Discord.Net.WebSocket", - version: typeof(DiscordSocketClient).Assembly.GetName().Version.ToString()); + private readonly static Meter _meter = new(Options.SourceName, Options.Version); + + private readonly static Counter _socketEvents; + private readonly static Counter _socketEventExceptions; + private readonly static Counter _socketDispatches; + private readonly static Counter _socketDispatchesExceptions; + private readonly static Histogram _socketDispatchesDuration; + + static SocketMeter() + { + _socketEvents = _meter.CreateCounter( + name: "socket.events_count", + unit: "Events", + description: "The total amount of events sent by the gateway since the application is running."); + _socketEventExceptions = _meter.CreateCounter( + name: "socket.events.exceptions_count", + unit: "Exceptions", + description: "The amount of exceptions occurred while event procession."); + _socketDispatches = _meter.CreateCounter( + name: "socket.dispatches_count", + unit: "Dispatches", + description: "The total amount of dispatches (like 'READY' or 'INTERACTION_CREATE') sent by the gateway since the application is running."); + _socketDispatchesExceptions = _meter.CreateCounter( + name: "socket.dispatches.exceptions_count", + unit: "Exceptions", + description: "The amount of exceptions occurred while handling dispatches (like 'READY' or 'INTERACTION_CREATE')."); + _socketDispatchesDuration = _meter.CreateHistogram( + name: "socket.dispatches.duration", + unit: "Seconds", + description: "The handling duration of dispatches (like 'READY' or 'INTERACTION_CREATE') received from the gateway."); + } + + internal static void RecordSocketEvent(GatewayOpCode opCode, string type, DiscordSocketConfig config) + { + _socketEvents.Add(1, [..Options.CreateTags(opCode, type, config)]); + } + + internal static void RecordSocketEventException(Exception ex, GatewayOpCode opCode, string type, DiscordSocketConfig config) + { + TagList tags = [ + .. Options.CreateTags(opCode, type, config), + KeyValuePair.Create("exception.type", ex.GetType().ToString()), + KeyValuePair.Create("exception.message", ex.Message), + KeyValuePair.Create("exception.stacktrace", ex.ToString()), + ]; + + _socketEventExceptions.Add(1, tags); + if (opCode == GatewayOpCode.Dispatch) + _socketDispatchesExceptions.Add(1, tags); + } + + internal static void RecordSocketDispatch(TimeSpan duration, string type, DiscordSocketConfig config) + { + TagList tags = [..Options.CreateTags(GatewayOpCode.Dispatch, type, config)]; + _socketDispatches.Add(1, tags); + _socketDispatchesDuration.Record(duration.TotalSeconds, tags); + } #else + internal static void RecordSocketEvent(GatewayOpCode opCode, string type, DiscordSocketConfig config) { } + + internal static void RecordSocketEventException(Exception ex, GatewayOpCode opCode, string type, DiscordSocketConfig config) { } + + internal static void RecordSocketDispatch(TimeSpan duration, string type, DiscordSocketConfig config) { } #endif } } diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.EventHandling.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.EventHandling.cs index fdb57e7edd..d4c04ea986 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.EventHandling.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.EventHandling.cs @@ -2,14 +2,14 @@ using Discord.API.Gateway; using Discord.Rest; using Discord.Utils; +using Discord.WebSocket.Diagnostics; 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; +using System.Diagnostics; namespace Discord.WebSocket; @@ -22,6 +22,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty _lastSeq = seq.Value; _lastMessageTime = Environment.TickCount; + SocketMeter.RecordSocketEvent(opCode, type, BaseConfig); try { switch (opCode) @@ -87,141 +88,165 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty } break; case GatewayOpCode.Dispatch: - switch (type) + var activity = SocketActivity.StartSocketDispatchActivity(type, BaseConfig); + var watch = activity is null ? Stopwatch.StartNew() : null; + try { - #region Connection - case "READY": + switch (type) { - 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) + #region Connection + case "READY": { - _connection.CriticalError(new Exception("Processing READY failed", ex)); - return; - } - - _lastGuildAvailableTime = Environment.TickCount; - _guildDownloadTask = WaitForGuildsAsync(_connection.CancelToken, _gatewayLogger) - .ContinueWith(async x => + try { - if (x.IsFaulted) + 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++) { - _connection.Error(x.Exception); - return; + var model = data.Guilds[i]; + var guild = AddGuild(model, state); + if (!guild.IsAvailable) + unavailableGuilds++; + else + await GuildAvailableAsync(guild).ConfigureAwait(false); } - else if (_connection.CancelToken.IsCancellationRequested) - return; + for (int i = 0; i < data.PrivateChannels.Length; i++) + AddPrivateChannel(data.PrivateChannels[i], state); - if (BaseConfig.AlwaysDownloadUsers) - try - { - _ = DownloadUsersAsync(Guilds.Where(x => x.IsAvailable && !x.HasAllMembers)); - } - catch (Exception ex) + _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) { - await _gatewayLogger.WarningAsync(ex); + _connection.Error(x.Exception); + return; } + else if (_connection.CancelToken.IsCancellationRequested) + return; - 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(); + if (BaseConfig.AlwaysDownloadUsers) + try + { + _ = DownloadUsersAsync(Guilds.Where(x => x.IsAvailable && !x.HasAllMembers)); + } + catch (Exception ex) + { + await _gatewayLogger.WarningAsync(ex); + } - //Notify the client that these guilds are available again - foreach (var guild in State.Guilds) - { - if (guild.IsAvailable) - await GuildAvailableAsync(guild).ConfigureAwait(false); + 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); - // Restore the previous sessions current user - CurrentUser = _previousSessionUser; + _ = _connection.CompleteAsync(); - await _gatewayLogger.InfoAsync("Resumed previous session").ConfigureAwait(false); - } - break; - #endregion + //Notify the client that these guilds are available again + foreach (var guild in State.Guilds) + { + if (guild.IsAvailable) + await GuildAvailableAsync(guild).ConfigureAwait(false); + } - #region Guilds - case "GUILD_CREATE": - { - var data = (payload as JToken).ToObject(_serializer); + // Restore the previous sessions current user + CurrentUser = _previousSessionUser; + + await _gatewayLogger.InfoAsync("Resumed previous session").ConfigureAwait(false); + } + break; + #endregion - if (data.Unavailable == false) + #region Guilds + case "GUILD_CREATE": { - type = "GUILD_AVAILABLE"; - _lastGuildAvailableTime = Environment.TickCount; - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_AVAILABLE)").ConfigureAwait(false); + var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.Id); - if (guild != null) + if (data.Unavailable == false) { - guild.Update(State, data); + type = "GUILD_AVAILABLE"; + _lastGuildAvailableTime = Environment.TickCount; + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_AVAILABLE)").ConfigureAwait(false); - if (_unavailableGuildCount != 0) - _unavailableGuildCount--; - await GuildAvailableAsync(guild).ConfigureAwait(false); + var guild = State.GetGuild(data.Id); + if (guild != null) + { + guild.Update(State, data); - if (guild.DownloadedMemberCount >= guild.MemberCount && !guild.DownloaderPromise.IsCompleted) + 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 { - guild.CompleteDownloadUsers(); - await TimedInvokeAsync(_guildMembersDownloadedEvent, nameof(GuildMembersDownloaded), guild).ConfigureAwait(false); + await UnknownGuildAsync(type, data.Id).ConfigureAwait(false); + return; } } else { - await UnknownGuildAsync(type, data.Id).ConfigureAwait(false); - return; + 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; + } } } - else + break; + case "GUILD_UPDATE": { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_CREATE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_UPDATE)").ConfigureAwait(false); - var guild = AddGuild(data, State); + var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.Id); if (guild != null) { - await TimedInvokeAsync(_joinedGuildEvent, nameof(JoinedGuild), guild).ConfigureAwait(false); - await GuildAvailableAsync(guild).ConfigureAwait(false); + var before = guild.Clone(); + guild.Update(State, data); + await TimedInvokeAsync(_guildUpdatedEvent, nameof(GuildUpdated), before, guild).ConfigureAwait(false); } else { @@ -229,2289 +254,2288 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty 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) + break; + case "GUILD_EMOJIS_UPDATE": { - type = "GUILD_UNAVAILABLE"; - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_UNAVAILABLE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_EMOJIS_UPDATE)").ConfigureAwait(false); - var guild = State.GetGuild(data.Id); + var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.GuildId); if (guild != null) { - await GuildUnavailableAsync(guild).ConfigureAwait(false); - _unavailableGuildCount++; + 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); + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); return; } } - else + break; + case "GUILD_SYNC": { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_DELETE)").ConfigureAwait(false); - - var guild = RemoveGuild(data.Id); + 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) { - await GuildUnavailableAsync(guild).ConfigureAwait(false); - await TimedInvokeAsync(_leftGuildEvent, nameof(LeftGuild), guild).ConfigureAwait(false); - (guild as IDisposable).Dispose(); + 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_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 => + break; + case "GUILD_DELETE": { - var s = guild.Stickers.FirstOrDefault(y => y.Id == x.Id); - if (s == null) - return null; - - var e = s.Equals(x); - if (!e) + var data = (payload as JToken).ToObject(_serializer); + if (data.Unavailable == true) { - return (s, x) as (SocketCustomSticker Entity, API.Sticker Model)?; + 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 { - return null; - } - }).Where(x => x.HasValue).Select(x => x.Value).ToArray(); + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_DELETE)").ConfigureAwait(false); - 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); + 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; + } + } } - foreach (var entityModelPair in updatedStickers) + break; + case "GUILD_STICKERS_UPDATE": { - var before = entityModelPair.Entity.Clone(); + await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_STICKERS_UPDATE)").ConfigureAwait(false); - entityModelPair.Entity.Update(entityModelPair.Model); + var data = (payload as JToken).ToObject(_serializer); - await TimedInvokeAsync(_guildStickerUpdated, nameof(GuildStickerUpdated), before, entityModelPair.Entity); - } - } - break; - #endregion + var guild = State.GetGuild(data.GuildId); - #region Channels - case "CHANNEL_CREATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_CREATE)").ConfigureAwait(false); + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } - var data = (payload as JToken).ToObject(_serializer); - SocketChannel channel = null; - if (data.GuildId.IsSpecified) - { - var guild = State.GetGuild(data.GuildId.Value); - if (guild != null) + 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 => { - channel = guild.AddChannel(State, data); + var s = guild.Stickers.FirstOrDefault(y => y.Id == x.Id); + if (s == null) + return null; - if (!guild.IsSynced) + var e = s.Equals(x); + if (!e) { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; + 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); } - else + foreach (var sticker in deletedStickers) { - await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); - return; + var entity = guild.RemoveSticker(sticker.Id); + await TimedInvokeAsync(_guildStickerDeleted, nameof(GuildStickerDeleted), entity); } - } - else - { - channel = State.GetChannel(data.Id); - if (channel != null) - return; //Discord may send duplicate CHANNEL_CREATEs for DMs - channel = AddPrivateChannel(data, State) as SocketChannel; - } + foreach (var entityModelPair in updatedStickers) + { + var before = entityModelPair.Entity.Clone(); - if (channel != null) - await TimedInvokeAsync(_channelCreatedEvent, nameof(ChannelCreated), channel).ConfigureAwait(false); - } - break; - case "CHANNEL_UPDATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_UPDATE)").ConfigureAwait(false); + entityModelPair.Entity.Update(entityModelPair.Model); + + await TimedInvokeAsync(_guildStickerUpdated, nameof(GuildStickerUpdated), before, entityModelPair.Entity); + } + } + break; + #endregion - var data = (payload as JToken).ToObject(_serializer); - var channel = State.GetChannel(data.Id); - if (channel != null) + #region Channels + case "CHANNEL_CREATE": { - var before = channel.Clone(); - channel.Update(State, data); + await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_CREATE)").ConfigureAwait(false); - var guild = (channel as SocketGuildChannel)?.Guild; - if (!(guild?.IsSynced ?? true)) + var data = (payload as JToken).ToObject(_serializer); + SocketChannel channel = null; + if (data.GuildId.IsSpecified) { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; + 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; } - await TimedInvokeAsync(_channelUpdatedEvent, nameof(ChannelUpdated), before, channel).ConfigureAwait(false); + if (channel != null) + await TimedInvokeAsync(_channelCreatedEvent, nameof(ChannelCreated), channel).ConfigureAwait(false); } - else + break; + case "CHANNEL_UPDATE": { - await UnknownChannelAsync(type, data.Id).ConfigureAwait(false); - return; - } - } - break; - case "CHANNEL_DELETE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_DELETE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_UPDATE)").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) + var data = (payload as JToken).ToObject(_serializer); + var channel = State.GetChannel(data.Id); + if (channel != null) { - channel = guild.RemoveChannel(State, data.Id); + var before = channel.Clone(); + channel.Update(State, data); - if (!guild.IsSynced) + 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 UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + await UnknownChannelAsync(type, data.Id).ConfigureAwait(false); return; } } - else - channel = RemovePrivateChannel(data.Id) as SocketChannel; - - if (channel != null) - await TimedInvokeAsync(_channelDestroyedEvent, nameof(ChannelDestroyed), channel).ConfigureAwait(false); - else + break; + case "CHANNEL_DELETE": { - await UnknownChannelAsync(type, data.Id, data.GuildId.GetValueOrDefault(0)).ConfigureAwait(false); - return; - } - } - break; - #endregion + await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_DELETE)").ConfigureAwait(false); - #region Members - case "GUILD_MEMBER_ADD": - { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBER_ADD)").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); - 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; + } + } + else + { + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + return; + } + } + else + channel = RemovePrivateChannel(data.Id) as SocketChannel; - if (!guild.IsSynced) + if (channel != null) + await TimedInvokeAsync(_channelDestroyedEvent, nameof(ChannelDestroyed), channel).ConfigureAwait(false); + else { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + await UnknownChannelAsync(type, data.Id, data.GuildId.GetValueOrDefault(0)).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); + break; + #endregion - var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); - if (guild != null) + #region Members + case "GUILD_MEMBER_ADD": { - var user = guild.GetUser(data.User.Id); + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBER_ADD)").ConfigureAwait(false); - if (!guild.IsSynced) + var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.GuildId); + if (guild != null) { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } + var user = guild.AddOrUpdateUser(data); + guild.MemberCount++; - if (user != null) - { - var before = user.Clone(); - if (user.GlobalUser.Update(State, data.User)) + if (!guild.IsSynced) { - //Global data was updated, trigger UserUpdated - await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), before.GlobalUser, user).ConfigureAwait(false); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; } - user.Update(State, data); - - var cacheableBefore = new Cacheable(before, user.Id, true, () => Task.FromResult(null)); - await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), cacheableBefore, user).ConfigureAwait(false); + await TimedInvokeAsync(_userJoinedEvent, nameof(UserJoined), 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); + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; } } - else + break; + case "GUILD_MEMBER_UPDATE": { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; - } - } - break; - case "GUILD_MEMBER_REMOVE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBER_REMOVE)").ConfigureAwait(false); + 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) - { - SocketUser user = guild.RemoveUser(data.User.Id); - guild.MemberCount--; - - if (!guild.IsSynced) + var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.GuildId); + if (guild != null) { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } - - user ??= State.GetUser(data.User.Id); + var user = guild.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)); + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } - 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); + 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); + } - 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); + user.Update(State, data); - if (guild.DownloadedMemberCount >= guild.MemberCount && !guild.DownloaderPromise.IsCompleted) + 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 { - guild.CompleteDownloadUsers(); - await TimedInvokeAsync(_guildMembersDownloadedEvent, nameof(GuildMembersDownloaded), guild).ConfigureAwait(false); + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; } } - 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) + break; + case "GUILD_MEMBER_REMOVE": { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; - } - - var user = guild.RemoveUser(data.UserId); - guild.MemberCount--; + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBER_REMOVE)").ConfigureAwait(false); - var cacheableUser = new Cacheable(user, data.UserId, user != null, () => Task.FromResult((SocketGuildUser)null)); + 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--; - await TimedInvokeAsync(_guildJoinRequestDeletedEvent, nameof(GuildJoinRequestDeleted), cacheableUser, guild).ConfigureAwait(false); - } - break; - #endregion + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } - #region DM Channels + user ??= State.GetUser(data.User.Id); - case "CHANNEL_RECIPIENT_ADD": - { - await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_RECIPIENT_ADD)").ConfigureAwait(false); + if (user != null) + user.Update(State, data.User); + else + user = State.GetOrAddUser(data.User.Id, (x) => SocketGlobalUser.Create(this, State, data.User)); - 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); + await TimedInvokeAsync(_userLeftEvent, nameof(UserLeft), guild, user).ConfigureAwait(false); + } + else + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } } - else + break; + case "GUILD_MEMBERS_CHUNK": { - await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); - return; - } - } - break; - case "CHANNEL_RECIPIENT_REMOVE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_RECIPIENT_REMOVE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBERS_CHUNK)").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); + 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 UnknownChannelUserAsync(type, data.User.Id, data.ChannelId).ConfigureAwait(false); + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); return; } } - else + break; + case "GUILD_JOIN_REQUEST_DELETE": { - await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); - return; - } - } - break; + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_JOIN_REQUEST_DELETE)").ConfigureAwait(false); - #endregion + var data = (payload as JToken).ToObject(_serializer); - #region Roles - case "GUILD_ROLE_CREATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_ROLE_CREATE)").ConfigureAwait(false); + 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 - var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); - if (guild != null) + case "CHANNEL_RECIPIENT_ADD": { - var role = guild.AddRole(data.Role); + await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_RECIPIENT_ADD)").ConfigureAwait(false); - if (!guild.IsSynced) + var data = (payload as JToken).ToObject(_serializer); + if (State.GetChannel(data.ChannelId) is SocketGroupChannel channel) { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + var user = channel.GetOrAddUser(data.User); + await TimedInvokeAsync(_recipientAddedEvent, nameof(RecipientAdded), user).ConfigureAwait(false); + } + else + { + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); return; } - await TimedInvokeAsync(_roleCreatedEvent, nameof(RoleCreated), role).ConfigureAwait(false); } - else + break; + case "CHANNEL_RECIPIENT_REMOVE": { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; - } - } - break; - case "GUILD_ROLE_UPDATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_ROLE_UPDATE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_RECIPIENT_REMOVE)").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 data = (payload as JToken).ToObject(_serializer); + if (State.GetChannel(data.ChannelId) is SocketGroupChannel channel) { - var before = role.Clone(); - role.Update(State, data.Role); - - if (!guild.IsSynced) + var user = channel.RemoveUser(data.User.Id); + if (user != null) + await TimedInvokeAsync(_recipientRemovedEvent, nameof(RecipientRemoved), user).ConfigureAwait(false); + else { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + await UnknownChannelUserAsync(type, data.User.Id, data.ChannelId).ConfigureAwait(false); return; } - - await TimedInvokeAsync(_roleUpdatedEvent, nameof(RoleUpdated), before, role).ConfigureAwait(false); } else { - await UnknownRoleAsync(type, data.Role.Id, guild.Id).ConfigureAwait(false); + await UnknownChannelAsync(type, data.ChannelId).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); + break; + + #endregion - var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); - if (guild != null) + #region Roles + case "GUILD_ROLE_CREATE": { - var role = guild.RemoveRole(data.RoleId); - if (role != null) + 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(_roleDeletedEvent, nameof(RoleDeleted), role).ConfigureAwait(false); + await TimedInvokeAsync(_roleCreatedEvent, nameof(RoleCreated), role).ConfigureAwait(false); } else { - await UnknownRoleAsync(type, data.RoleId, guild.Id).ConfigureAwait(false); + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); return; } } - else + break; + case "GUILD_ROLE_UPDATE": { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; - } - } - break; - #endregion + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_ROLE_UPDATE)").ConfigureAwait(false); - #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) + { + var role = guild.GetRole(data.Role.Id); + if (role != null) + { + var before = role.Clone(); + role.Update(State, data.Role); - var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); - if (guild != null) - { - if (!guild.IsSynced) + 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 UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + await UnknownGuildAsync(type, data.GuildId).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 + break; + case "GUILD_ROLE_DELETE": { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; - } - } - break; - case "GUILD_BAN_REMOVE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_BAN_REMOVE)").ConfigureAwait(false); + 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) - { - if (!guild.IsSynced) + var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.GuildId); + if (guild != null) { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + 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; } - - 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; + break; + #endregion - var guild = (channel as SocketGuildChannel)?.Guild; - if (guild != null && !guild.IsSynced) + #region Bans + case "GUILD_BAN_ADD": { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_BAN_ADD)").ConfigureAwait(false); - if (channel == null) - { - if (!data.GuildId.IsSpecified) // assume it is a DM + var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.GuildId); + if (guild != null) { - channel = CreateDMChannel(data.ChannelId, data.Author.Value, State); + 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 UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); return; } } - - SocketUser author; - if (guild != null) + break; + case "GUILD_BAN_REMOVE": { - 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); + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_BAN_REMOVE)").ConfigureAwait(false); - if (author == null) - { + var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.GuildId); if (guild != null) { - if (data.Member.IsSpecified) // member isn't always included, but use it when we can + if (!guild.IsSynced) { - data.Member.Value.User = data.Author.Value; - author = guild.AddOrUpdateUser(data.Member.Value); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; } - else - author = guild.AddOrUpdateUser(data.Author.Value); // user has no guild-specific data + + 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 if (channel is SocketGroupChannel groupChannel) - author = groupChannel.GetOrAddUser(data.Author.Value); else { - await UnknownChannelUserAsync(type, data.Author.Value.Id, channel.Id).ConfigureAwait(false); + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); return; } } + break; + #endregion - 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); + #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 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; - } + 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 (channel == null) { - if (guild != null) + if (!data.GuildId.IsSpecified) // assume it is a DM { - if (data.WebhookId.IsSpecified) - author = SocketWebhookUser.Create(guild, State, data.Author.Value, data.WebhookId.Value); - else - author = guild.GetUser(data.Author.Value.Id); + channel = CreateDMChannel(data.ChannelId, data.Author.Value, State); } 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); + 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 - // Message author wasn't specified in the payload, so create a completely anonymous unknown user - author = new SocketUnknownUser(this, id: 0); + author = (channel as SocketChannel).GetUser(data.Author.Value.Id); - if (channel == null) + if (author == null) { - if (!data.GuildId.IsSpecified) // assume it is a DM + if (guild != null) { - if (data.Author.IsSpecified) + if (data.Member.IsSpecified) // member isn't always included, but use it when we can { - var dmChannel = CreateDMChannel(data.ChannelId, data.Author.Value, State); - channel = dmChannel; - author = dmChannel.Recipient; + data.Member.Value.User = data.Author.Value; + author = guild.AddOrUpdateUser(data.Member.Value); } else - channel = CreateDMChannel(data.ChannelId, author, State); + 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 UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); + await UnknownChannelUserAsync(type, data.Author.Value.Id, channel.Id).ConfigureAwait(false); return; } } - after = SocketMessage.Create(this, State, author, channel, data); + var msg = SocketMessage.Create(this, State, author, channel, data); + SocketChannelHelper.AddMessage(channel, this, msg); + await TimedInvokeAsync(_messageReceivedEvent, nameof(MessageReceived), msg).ConfigureAwait(false); } - 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)) + break; + case "MESSAGE_UPDATE": { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_UPDATE)").ConfigureAwait(false); - 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); + var data = (payload as JToken).ToObject(_serializer); + var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; - 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 && !guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } - 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; + 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); - 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); + 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); - var optionalMsg = !isMsgCached - ? Optional.Create() - : Optional.Create(cachedMsg); + 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; + } + } - var optionalUser = user is null - ? Optional.Create() - : Optional.Create(user); + after = SocketMessage.Create(this, State, author, channel, data); + } + var cacheableBefore = new Cacheable(before, data.Id, isCached, async () => await channel.GetMessageAsync(data.Id).ConfigureAwait(false)); - 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 () => + await TimedInvokeAsync(_messageUpdatedEvent, nameof(MessageUpdated), cacheableBefore, after, channel).ConfigureAwait(false); + } + break; + case "MESSAGE_DELETE": { - 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); + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_DELETE)").ConfigureAwait(false); - cachedMsg?.RemoveReaction(reaction); + var data = (payload as JToken).ToObject(_serializer); + var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; - 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 guild = (channel as SocketGuildChannel)?.Guild; + if (!(guild?.IsSynced ?? true)) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } - var data = (payload as JToken).ToObject(_serializer); - var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; + 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); - 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 () => + await TimedInvokeAsync(_messageDeletedEvent, nameof(MessageDeleted), cacheableMsg, cacheableChannel).ConfigureAwait(false); + } + break; + case "MESSAGE_REACTION_ADD": { - var channelObj = await cacheableChannel.GetOrDownloadAsync().ConfigureAwait(false); - return await channelObj.GetMessageAsync(data.MessageId).ConfigureAwait(false) as IUserMessage; - }); + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_ADD)").ConfigureAwait(false); - cachedMsg?.ClearReactions(); + var data = (payload as JToken).ToObject(_serializer); + var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; - 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; + IUser user = null; + if (channel != null) + user = await channel.GetUserAsync(data.UserId, CacheMode.CacheOnly).ConfigureAwait(false); - var cachedMsg = channel?.GetCachedMessage(data.MessageId) as SocketUserMessage; - bool isMsgCached = cachedMsg != null; + var optionalMsg = !isMsgCached + ? Optional.Create() + : Optional.Create(cachedMsg); - var optionalMsg = !isMsgCached - ? Optional.Create() - : Optional.Create(cachedMsg); + if (data.Member.IsSpecified) + { + var guild = (channel as SocketGuildChannel)?.Guild; - 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(); + if (guild != null) + user = guild.AddOrUpdateUser(data.Member.Value); + } + else + user = GetUser(data.UserId); - cachedMsg?.RemoveReactionsForEmote(emote); + var optionalUser = user is null + ? Optional.Create() + : Optional.Create(user); - 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 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); - var data = (payload as JToken).ToObject(_serializer); - var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; + cachedMsg?.AddReaction(reaction); - var guild = (channel as SocketGuildChannel)?.Guild; - if (!(guild?.IsSynced ?? true)) - { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; + await TimedInvokeAsync(_reactionAddedEvent, nameof(ReactionAdded), cacheableMsg, cacheableChannel, reaction).ConfigureAwait(false); } - - 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) + break; + case "MESSAGE_REACTION_REMOVE": { - 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 _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_REMOVE)").ConfigureAwait(false); - await TimedInvokeAsync(_messagesBulkDeletedEvent, nameof(MessagesBulkDeleted), cacheableList, cacheableChannel).ConfigureAwait(false); - } - break; - #endregion + var data = (payload as JToken).ToObject(_serializer); + var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; - #region Polls + 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); - case "MESSAGE_POLL_VOTE_ADD": - { - await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_POLL_VOTE_ADD)").ConfigureAwait(false); + var optionalMsg = !isMsgCached + ? Optional.Create() + : Optional.Create(cachedMsg); - var data = (payload as JToken).ToObject(_serializer); + var optionalUser = user is null + ? Optional.Create() + : Optional.Create(user); - Cacheable? guildCacheable = null; + 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); - Cacheable userCacheable; - Cacheable channelCacheable; - Cacheable messageCacheable; + cachedMsg?.RemoveReaction(reaction); - if (data.GuildId.IsSpecified) + await TimedInvokeAsync(_reactionRemovedEvent, nameof(ReactionRemoved), cacheableMsg, cacheableChannel, reaction).ConfigureAwait(false); + } + break; + case "MESSAGE_REACTION_REMOVE_ALL": { - 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)); + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_REMOVE_ALL)").ConfigureAwait(false); - var channel = guild.GetTextChannel(data.ChannelId); - channelCacheable = new(channel, data.ChannelId, channel is not null, async () => (RestTextChannel)await Rest.GetChannelAsync(data.ChannelId)); + var data = (payload as JToken).ToObject(_serializer); + var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; - 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 + 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 () => { - 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 channelObj = await cacheableChannel.GetOrDownloadAsync().ConfigureAwait(false); + return await channelObj.GetMessageAsync(data.MessageId).ConfigureAwait(false) as IUserMessage; + }); - 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); + cachedMsg?.ClearReactions(); - 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(_reactionsClearedEvent, nameof(ReactionsCleared), cacheableMsg, cacheableChannel).ConfigureAwait(false); } + break; + case "MESSAGE_REACTION_REMOVE_EMOJI": + { + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_REMOVE_EMOJI)").ConfigureAwait(false); - await TimedInvokeAsync(_pollVoteAdded, nameof(PollVoteAdded), userCacheable, channelCacheable, messageCacheable, guildCacheable, data.AnswerId); - } - break; + var data = (payload as JToken).ToObject(_serializer); + var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; - case "MESSAGE_POLL_VOTE_REMOVE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_POLL_VOTE_REMOVE)").ConfigureAwait(false); + var cachedMsg = channel?.GetCachedMessage(data.MessageId) as SocketUserMessage; + bool isMsgCached = cachedMsg != null; - var data = (payload as JToken).ToObject(_serializer); + var optionalMsg = !isMsgCached + ? Optional.Create() + : Optional.Create(cachedMsg); - Cacheable? guildCacheable = null; + 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(); - Cacheable userCacheable; - Cacheable channelCacheable; - Cacheable messageCacheable; + cachedMsg?.RemoveReactionsForEmote(emote); - if (data.GuildId.IsSpecified) + await TimedInvokeAsync(_reactionsRemovedForEmoteEvent, nameof(ReactionsRemovedForEmote), cacheableMsg, cacheableChannel, emote).ConfigureAwait(false); + } + break; + case "MESSAGE_DELETE_BULK": { - 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)); + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_DELETE_BULK)").ConfigureAwait(false); - var channel = guild.GetTextChannel(data.ChannelId); - channelCacheable = new(channel, data.ChannelId, channel is not null, async () => (RestTextChannel)await Rest.GetChannelAsync(data.ChannelId)); + var data = (payload as JToken).ToObject(_serializer); + var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; - 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 + var guild = (channel as SocketGuildChannel)?.Guild; + if (!(guild?.IsSynced ?? true)) { - 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); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; } - } - 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 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); + } - 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(_messagesBulkDeletedEvent, nameof(MessagesBulkDeleted), cacheableList, cacheableChannel).ConfigureAwait(false); } + break; + #endregion - await TimedInvokeAsync(_pollVoteRemoved, nameof(PollVoteRemoved), userCacheable, channelCacheable, messageCacheable, guildCacheable, data.AnswerId); - } - break; + #region Polls - #endregion + case "MESSAGE_POLL_VOTE_ADD": + { + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_POLL_VOTE_ADD)").ConfigureAwait(false); - #region Statuses - case "PRESENCE_UPDATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (PRESENCE_UPDATE)").ConfigureAwait(false); + var data = (payload as JToken).ToObject(_serializer); - var data = (payload as JToken).ToObject(_serializer); + Cacheable? guildCacheable = null; - SocketUser user = null; + Cacheable userCacheable; + Cacheable channelCacheable; + Cacheable messageCacheable; - if (data.GuildId.IsSpecified) - { - var guild = State.GetGuild(data.GuildId.Value); - if (guild == null) + if (data.GuildId.IsSpecified) { - await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); - return; - } - if (!guild.IsSynced) - { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } + var guild = State.GetGuild(data.GuildId.Value); + guildCacheable = new(guild, data.GuildId.Value, guild is not null, () => Rest.GetGuildAsync(data.GuildId.Value)); - user = guild.GetUser(data.User.Id); - if (user == null) - { - if (data.Status == UserStatus.Offline) + if (guild is not null) { - return; + 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); } - user = guild.AddOrUpdateUser(data); - } - else - { - var globalBefore = user.GlobalUser.Clone(); - if (user.GlobalUser.Update(State, data.User)) + else { - //Global data was updated, trigger UserUpdated - await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), globalBefore, user).ConfigureAwait(false); + 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 - { - user = State.GetUser(data.User.Id); - if (user == null) + else { - await UnknownGlobalUserAsync(type, data.User.Id).ConfigureAwait(false); - return; - } - } + var user = State.GetUser(data.UserId); + userCacheable = new(user, data.UserId, user is not null, async () => await GetUserAsync(data.UserId)); - 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 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 data = (payload as JToken).ToObject(_serializer); - var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; + 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); + } - var guild = (channel as SocketGuildChannel)?.Guild; - if (!(guild?.IsSynced ?? true)) - { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; + await TimedInvokeAsync(_pollVoteAdded, nameof(PollVoteAdded), userCacheable, channelCacheable, messageCacheable, guildCacheable, data.AnswerId); } + break; - 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) + case "MESSAGE_POLL_VOTE_REMOVE": { - 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 _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_POLL_VOTE_REMOVE)").ConfigureAwait(false); - await TimedInvokeAsync(_userIsTypingEvent, nameof(UserIsTyping), cacheableUser, cacheableChannel).ConfigureAwait(false); - } - break; - #endregion + var data = (payload as JToken).ToObject(_serializer); - #region Integrations - case "INTEGRATION_CREATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (INTEGRATION_CREATE)").ConfigureAwait(false); + Cacheable? guildCacheable = null; - var data = (payload as JToken).ToObject(_serializer); + Cacheable userCacheable; + Cacheable channelCacheable; + Cacheable messageCacheable; - // Integrations from Gateway should always have guild IDs specified. - if (!data.GuildId.IsSpecified) - return; + 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)); - var guild = State.GetGuild(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)); - if (guild != null) - { - if (!guild.IsSynced) + 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 { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; + 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(_integrationCreated, nameof(IntegrationCreated), RestIntegration.Create(this, guild, data)).ConfigureAwait(false); + await TimedInvokeAsync(_pollVoteRemoved, nameof(PollVoteRemoved), userCacheable, channelCacheable, messageCacheable, guildCacheable, data.AnswerId); } - else + break; + + #endregion + + #region Statuses + case "PRESENCE_UPDATE": { - await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); - return; - } - } - break; - case "INTEGRATION_UPDATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (INTEGRATION_UPDATE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Dispatch (PRESENCE_UPDATE)").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); - // Integrations from Gateway should always have guild IDs specified. - if (!data.GuildId.IsSpecified) - return; + SocketUser user = null; - var guild = State.GetGuild(data.GuildId.Value); + 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; + } - if (guild != null) - { - if (!guild.IsSynced) + 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 { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; + user = State.GetUser(data.User.Id); + if (user == null) + { + await UnknownGlobalUserAsync(type, data.User.Id).ConfigureAwait(false); + return; + } } - await TimedInvokeAsync(_integrationUpdated, nameof(IntegrationUpdated), RestIntegration.Create(this, guild, data)).ConfigureAwait(false); + var before = user.Presence?.Clone(); + user.Update(State, data.User); + user.Update(data); + await TimedInvokeAsync(_presenceUpdated, nameof(PresenceUpdated), user, before, user.Presence).ConfigureAwait(false); } - else + break; + case "TYPING_START": { - 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); + await _gatewayLogger.DebugAsync("Received Dispatch (TYPING_START)").ConfigureAwait(false); - var guild = State.GetGuild(data.GuildId); + var data = (payload as JToken).ToObject(_serializer); + var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; - if (guild != null) - { - if (!guild.IsSynced) + var guild = (channel as SocketGuildChannel)?.Guild; + if (!(guild?.IsSynced ?? true)) { 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 + var cacheableChannel = new Cacheable(channel, data.ChannelId, channel != null, async () => await GetChannelAsync(data.ChannelId).ConfigureAwait(false) as IMessageChannel); - #region Users - case "USER_UPDATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (USER_UPDATE)").ConfigureAwait(false); + 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)); - 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); + await TimedInvokeAsync(_userIsTypingEvent, nameof(UserIsTyping), cacheableUser, cacheableChannel).ConfigureAwait(false); } - else + break; + #endregion + + #region Integrations + case "INTEGRATION_CREATE": { - await _gatewayLogger.WarningAsync("Received USER_UPDATE for wrong user.").ConfigureAwait(false); - return; - } - } - break; - #endregion + await _gatewayLogger.DebugAsync("Received Dispatch (INTEGRATION_CREATE)").ConfigureAwait(false); - #region Voice - case "VOICE_STATE_UPDATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (VOICE_STATE_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 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) + + if (guild != null) { - await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); - return; + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + await TimedInvokeAsync(_integrationCreated, nameof(IntegrationCreated), RestIntegration.Create(this, guild, data)).ConfigureAwait(false); } - else if (!guild.IsSynced) + else { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); return; } + } + break; + case "INTEGRATION_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (INTEGRATION_UPDATE)").ConfigureAwait(false); - if (data.ChannelId != null) + 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) { - before = guild.GetVoiceState(data.UserId)?.Clone() ?? SocketVoiceState.Default; - after = await guild.AddOrUpdateVoiceStateAsync(State, data).ConfigureAwait(false); - /*if (data.UserId == CurrentUser.Id) + if (!guild.IsSynced) { - var _ = guild.FinishJoinAudioChannel().ConfigureAwait(false); - }*/ + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + await TimedInvokeAsync(_integrationUpdated, nameof(IntegrationUpdated), RestIntegration.Create(this, guild, data)).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); + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); return; } } - else + break; + case "INTEGRATION_DELETE": { - var groupChannel = GetChannel(data.ChannelId.Value) as SocketGroupChannel; - if (groupChannel == null) + 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) { - await UnknownChannelAsync(type, data.ChannelId.Value).ConfigureAwait(false); - return; + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + await TimedInvokeAsync(_integrationDeleted, nameof(IntegrationDeleted), guild, data.Id, data.ApplicationID).ConfigureAwait(false); } - if (data.ChannelId != null) + else { - before = groupChannel.GetVoiceState(data.UserId)?.Clone() ?? SocketVoiceState.Default; - after = groupChannel.AddOrUpdateVoiceState(State, data); + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; } - else + } + 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) { - before = groupChannel.RemoveVoiceState(data.UserId) ?? SocketVoiceState.Default; - after = SocketVoiceState.Create(null, data); + var before = CurrentUser.Clone(); + CurrentUser.Update(State, data); + await TimedInvokeAsync(_selfUpdatedEvent, nameof(CurrentUserUpdated), before, CurrentUser).ConfigureAwait(false); } - user = groupChannel.GetUser(data.UserId); - if (user == null) + else { - await UnknownChannelUserAsync(type, data.UserId, groupChannel.Id).ConfigureAwait(false); + await _gatewayLogger.WarningAsync("Received USER_UPDATE for wrong user.").ConfigureAwait(false); return; } } + break; + #endregion - if (user is SocketGuildUser guildUser && data.ChannelId.HasValue) + #region Voice + case "VOICE_STATE_UPDATE": { - SocketStageChannel stage = guildUser.Guild.GetStageChannel(data.ChannelId.Value); + await _gatewayLogger.DebugAsync("Received Dispatch (VOICE_STATE_UPDATE)").ConfigureAwait(false); - if (stage != null && before.VoiceChannel != null && after.VoiceChannel != null) + var data = (payload as JToken).ToObject(_serializer); + SocketUser user; + SocketVoiceState before, after; + if (data.GuildId != null) { - if (!before.RequestToSpeakTimestamp.HasValue && after.RequestToSpeakTimestamp.HasValue) + var guild = State.GetGuild(data.GuildId.Value); + if (guild == null) { - await TimedInvokeAsync(_requestToSpeak, nameof(RequestToSpeak), stage, guildUser); + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); return; } - if (before.IsSuppressed && !after.IsSuppressed) + else if (!guild.IsSynced) { - await TimedInvokeAsync(_speakerAdded, nameof(SpeakerAdded), stage, guildUser); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } - if (!before.IsSuppressed && after.IsSuppressed) + + if (data.ChannelId != null) { - await TimedInvokeAsync(_speakerRemoved, nameof(SpeakerRemoved), stage, guildUser); + 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); } - } - } - 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); + //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; + } + } - 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)); + if (user is SocketGuildUser guildUser && data.ChannelId.HasValue) + { + SocketStageChannel stage = guildUser.Guild.GetStageChannel(data.ChannelId.Value); - var voiceServer = new SocketVoiceServer(cachedGuild, data.Endpoint, data.Token); - await TimedInvokeAsync(_voiceServerUpdatedEvent, nameof(UserVoiceStateUpdated), voiceServer).ConfigureAwait(false); + 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); + } + } + } - if (isCached) + await TimedInvokeAsync(_userVoiceStateUpdatedEvent, nameof(UserVoiceStateUpdated), user, before, after).ConfigureAwait(false); + } + break; + case "VOICE_SERVER_UPDATE": { - var endpoint = data.Endpoint; + await _gatewayLogger.DebugAsync("Received Dispatch (VOICE_SERVER_UPDATE)").ConfigureAwait(false); - //Only strip out the port if the endpoint contains it - var portBegin = endpoint.LastIndexOf(':'); - if (portBegin > 0) - endpoint = endpoint.Substring(0, portBegin); + 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 _ = guild.FinishConnectAudio(endpoint, data.Token).ConfigureAwait(false); - } - else - { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - } + var voiceServer = new SocketVoiceServer(cachedGuild, data.Endpoint, data.Token); + await TimedInvokeAsync(_voiceServerUpdatedEvent, nameof(UserVoiceStateUpdated), voiceServer).ConfigureAwait(false); - } - break; + if (isCached) + { + var endpoint = data.Endpoint; - case "VOICE_CHANNEL_STATUS_UPDATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (VOICE_CHANNEL_STATUS_UPDATE)").ConfigureAwait(false); + //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; - var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); + case "VOICE_CHANNEL_STATUS_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (VOICE_CHANNEL_STATUS_UPDATE)").ConfigureAwait(false); - var channel = State.GetChannel(data.Id) as SocketVoiceChannel; - var channelCacheable = new Cacheable(channel, data.Id, channel is not null, () => null); + var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.GuildId); - var before = (string)channel?.Status?.Clone(); - var after = data.Status; - channel?.UpdateVoiceStatus(data.Status); + var channel = State.GetChannel(data.Id) as SocketVoiceChannel; + var channelCacheable = new Cacheable(channel, data.Id, channel is not null, () => null); - await TimedInvokeAsync(_voiceChannelStatusUpdated, nameof(VoiceChannelStatusUpdated), channelCacheable, before, after); - } - break; - #endregion + var before = (string)channel?.Status?.Clone(); + var after = data.Status; + channel?.UpdateVoiceStatus(data.Status); - #region Invites - case "INVITE_CREATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (INVITE_CREATE)").ConfigureAwait(false); + await TimedInvokeAsync(_voiceChannelStatusUpdated, nameof(VoiceChannelStatusUpdated), channelCacheable, before, after); + } + break; + #endregion - var data = (payload as JToken).ToObject(_serializer); - if (State.GetChannel(data.ChannelId) is SocketGuildChannel channel) + #region Invites + case "INVITE_CREATE": { - var guild = channel.Guild; - if (!guild.IsSynced) + await _gatewayLogger.DebugAsync("Received Dispatch (INVITE_CREATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + if (State.GetChannel(data.ChannelId) is SocketGuildChannel channel) { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } + 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; + 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; + 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); + var invite = SocketInvite.Create(this, guild, channel, inviter, target, data); - await TimedInvokeAsync(_inviteCreatedEvent, nameof(InviteCreated), invite).ConfigureAwait(false); + await TimedInvokeAsync(_inviteCreatedEvent, nameof(InviteCreated), invite).ConfigureAwait(false); + } + else + { + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); + return; + } } - else + break; + case "INVITE_DELETE": { - await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); - return; + 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; - case "INVITE_DELETE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (INVITE_DELETE)").ConfigureAwait(false); + break; + #endregion - var data = (payload as JToken).ToObject(_serializer); - if (State.GetChannel(data.ChannelId) is SocketGuildChannel channel) + #region Interactions + case "INTERACTION_CREATE": { - var guild = channel.Guild; - if (!guild.IsSynced) + 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); - return; } - await TimedInvokeAsync(_inviteDeletedEvent, nameof(InviteDeleted), channel, data.Code).ConfigureAwait(false); - } - else - { - await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); - return; - } - } - break; - #endregion + 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)); - #region Interactions - case "INTERACTION_CREATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (INTERACTION_CREATE)").ConfigureAwait(false); + SocketChannel channel = null; + if (data.ChannelId.IsSpecified) + { + channel = State.GetChannel(data.ChannelId.Value); - var data = (payload as JToken).ToObject(_serializer); + if (channel == null) + { + if (!data.GuildId.IsSpecified) // assume it is a DM + { + channel = CreateDMChannel(data.ChannelId.Value, user, State); + } - var guild = data.GuildId.IsSpecified ? GetGuild(data.GuildId.Value) : null; + // 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); + } - if (guild != null && !guild.IsSynced) - { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - } + var interaction = SocketInteraction.Create(this, data, channel as ISocketMessageChannel, user); - 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)); + await TimedInvokeAsync(_interactionCreatedEvent, nameof(InteractionCreated), interaction).ConfigureAwait(false); - SocketChannel channel = null; - if (data.ChannelId.IsSpecified) + 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": { - channel = State.GetChannel(data.ChannelId.Value); + await _gatewayLogger.DebugAsync("Received Dispatch (APPLICATION_COMMAND_CREATE)").ConfigureAwait(false); - if (channel == null) + var data = (payload as JToken).ToObject(_serializer); + + if (data.GuildId.IsSpecified) { - if (!data.GuildId.IsSpecified) // assume it is a DM + var guild = State.GetGuild(data.GuildId.Value); + if (guild == null) { - channel = CreateDMChannel(data.ChannelId.Value, user, State); + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + return; } - - // 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); + var applicationCommand = SocketApplicationCommand.Create(this, data); - await TimedInvokeAsync(_interactionCreatedEvent, nameof(InteractionCreated), interaction).ConfigureAwait(false); + State.AddCommand(applicationCommand); - 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; + await TimedInvokeAsync(_applicationCommandCreated, nameof(ApplicationCommandCreated), applicationCommand).ConfigureAwait(false); } - } - break; - case "APPLICATION_COMMAND_CREATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (APPLICATION_COMMAND_CREATE)").ConfigureAwait(false); + break; + case "APPLICATION_COMMAND_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (APPLICATION_COMMAND_UPDATE)").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); - if (data.GuildId.IsSpecified) - { - var guild = State.GetGuild(data.GuildId.Value); - if (guild == null) + if (data.GuildId.IsSpecified) { - await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); - return; + 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 applicationCommand = SocketApplicationCommand.Create(this, data); + var data = (payload as JToken).ToObject(_serializer); - State.AddCommand(applicationCommand); + if (data.GuildId.IsSpecified) + { + var guild = State.GetGuild(data.GuildId.Value); + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + return; + } + } - await TimedInvokeAsync(_applicationCommandCreated, nameof(ApplicationCommandCreated), applicationCommand).ConfigureAwait(false); - } - break; - case "APPLICATION_COMMAND_UPDATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (APPLICATION_COMMAND_UPDATE)").ConfigureAwait(false); + var applicationCommand = SocketApplicationCommand.Create(this, data); + + State.RemoveCommand(applicationCommand.Id); - var data = (payload as JToken).ToObject(_serializer); + await TimedInvokeAsync(_applicationCommandDeleted, nameof(ApplicationCommandDeleted), applicationCommand).ConfigureAwait(false); + } + break; + #endregion - if (data.GuildId.IsSpecified) + #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).ConfigureAwait(false); + await UnknownGuildAsync(type, data.GuildId.Value); return; } - } - var applicationCommand = SocketApplicationCommand.Create(this, data); + SocketThreadChannel threadChannel = null; - State.AddCommand(applicationCommand); + if ((threadChannel = guild.ThreadChannels.FirstOrDefault(x => x.Id == data.Id)) != null) + { + threadChannel.Update(State, data); - await TimedInvokeAsync(_applicationCommandUpdated, nameof(ApplicationCommandUpdated), applicationCommand).ConfigureAwait(false); - } - break; - case "APPLICATION_COMMAND_DELETE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (APPLICATION_COMMAND_DELETE)").ConfigureAwait(false); + 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); + } - var data = (payload as JToken).ToObject(_serializer); + await TimedInvokeAsync(_threadCreated, nameof(ThreadCreated), threadChannel).ConfigureAwait(false); + } - if (data.GuildId.IsSpecified) + 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).ConfigureAwait(false); + await UnknownGuildAsync(type, data.GuildId.Value); return; } - } - - var applicationCommand = SocketApplicationCommand.Create(this, data); - State.RemoveCommand(applicationCommand.Id); + 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)); - 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; - } + if (threadChannel != null) + { + threadChannel.Update(State, data); - SocketThreadChannel threadChannel = null; + 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 ((threadChannel = guild.ThreadChannels.FirstOrDefault(x => x.Id == data.Id)) != null) - { - threadChannel.Update(State, data); + if (!(guild?.IsSynced ?? true)) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } - if (data.ThreadMember.IsSpecified) - threadChannel.AddOrUpdateThreadMember(data.ThreadMember.Value, guild.CurrentUser); + await TimedInvokeAsync(_threadUpdated, nameof(ThreadUpdated), before, threadChannel).ConfigureAwait(false); } - else + break; + case "THREAD_DELETE": { - threadChannel = (SocketThreadChannel)guild.AddChannel(State, data); - if (data.ThreadMember.IsSpecified) - threadChannel.AddOrUpdateThreadMember(data.ThreadMember.Value, guild.CurrentUser); - } + await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_DELETE)").ConfigureAwait(false); - await TimedInvokeAsync(_threadCreated, nameof(ThreadCreated), threadChannel).ConfigureAwait(false); - } + var data = (payload as JToken).ToObject(_serializer); - break; - case "THREAD_UPDATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_UPDATE)").ConfigureAwait(false); + var guild = State.GetGuild(data.GuildId.Value); - var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId.Value); - if (guild == null) - { - await UnknownGuildAsync(type, data.GuildId.Value); - return; - } + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + 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)); + var thread = (SocketThreadChannel)guild.RemoveChannel(State, data.Id); - if (threadChannel != null) - { - threadChannel.Update(State, data); + var cacheable = new Cacheable(thread, data.Id, thread != null, null); - 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); + await TimedInvokeAsync(_threadDeleted, nameof(ThreadDeleted), cacheable).ConfigureAwait(false); } - - if (!(guild?.IsSynced ?? true)) + break; + case "THREAD_LIST_SYNC": { - 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); + await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_LIST_SYNC)").ConfigureAwait(false); - var guild = State.GetGuild(data.GuildId.Value); + var data = (payload as JToken).ToObject(_serializer); - if (guild == null) - { - await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); - return; - } + var guild = State.GetGuild(data.GuildId); - var thread = (SocketThreadChannel)guild.RemoveChannel(State, data.Id); - - var cacheable = new Cacheable(thread, data.Id, thread != null, null); + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } - await TimedInvokeAsync(_threadDeleted, nameof(ThreadDeleted), cacheable).ConfigureAwait(false); - } - break; - case "THREAD_LIST_SYNC": - { - await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_LIST_SYNC)").ConfigureAwait(false); + foreach (var thread in data.Threads) + { + var entity = guild.ThreadChannels.FirstOrDefault(x => x.Id == thread.Id); - var data = (payload as JToken).ToObject(_serializer); + if (entity == null) + { + entity = (SocketThreadChannel)guild.AddChannel(State, thread); + } + else + { + entity.Update(State, thread); + } - var guild = State.GetGuild(data.GuildId); + foreach (var member in data.Members.Where(x => x.Id.Value == entity.Id)) + { + var guildMember = guild.GetUser(member.Id.Value); - if (guild == null) - { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; + entity.AddOrUpdateThreadMember(member, guildMember); + } + } } - - foreach (var thread in data.Threads) + break; + case "THREAD_MEMBER_UPDATE": { - var entity = guild.ThreadChannels.FirstOrDefault(x => x.Id == thread.Id); - - if (entity == null) - { - entity = (SocketThreadChannel)guild.AddChannel(State, thread); - } - else - { - entity.Update(State, thread); - } + await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_MEMBER_UPDATE)").ConfigureAwait(false); - foreach (var member in data.Members.Where(x => x.Id.Value == entity.Id)) - { - var guildMember = guild.GetUser(member.Id.Value); + var data = (payload as JToken).ToObject(_serializer); - entity.AddOrUpdateThreadMember(member, guildMember); - } - } - } - break; - case "THREAD_MEMBER_UPDATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_MEMBER_UPDATE)").ConfigureAwait(false); + var thread = (SocketThreadChannel)State.GetChannel(data.Id.Value); - var data = (payload as JToken).ToObject(_serializer); + if (thread == null) + { + await UnknownChannelAsync(type, data.Id.Value); + return; + } - var thread = (SocketThreadChannel)State.GetChannel(data.Id.Value); + thread.AddOrUpdateThreadMember(data, thread.Guild.CurrentUser); + } - if (thread == null) + break; + case "THREAD_MEMBERS_UPDATE": { - await UnknownChannelAsync(type, data.Id.Value); - return; - } + await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_MEMBERS_UPDATE)").ConfigureAwait(false); - thread.AddOrUpdateThreadMember(data, thread.Guild.CurrentUser); - } + var data = (payload as JToken).ToObject(_serializer); - break; - case "THREAD_MEMBERS_UPDATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_MEMBERS_UPDATE)").ConfigureAwait(false); + var guild = State.GetGuild(data.GuildId); - var data = (payload as JToken).ToObject(_serializer); + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } - var guild = State.GetGuild(data.GuildId); + var thread = (SocketThreadChannel)guild.GetChannel(data.Id); - if (guild == null) - { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; - } + if (thread == null) + { + await UnknownChannelAsync(type, data.Id); + return; + } - var thread = (SocketThreadChannel)guild.GetChannel(data.Id); + IReadOnlyCollection leftUsers = null; + IReadOnlyCollection joinUsers = null; - 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; - if (data.RemovedMemberIds.IsSpecified) - { - leftUsers = thread.RemoveUsers(data.RemovedMemberIds.Value); - } + guildMember = guild.GetUser(threadMember.UserId.Value); - if (data.AddedMembers.IsSpecified) - { - List newThreadMembers = new List(); - foreach (var threadMember in data.AddedMembers.Value) - { - SocketGuildUser guildMember; + if (guildMember == null) + { + await UnknownGuildUserAsync("THREAD_MEMBERS_UPDATE", threadMember.UserId.Value, guild.Id); + } + else + newThreadMembers.Add(thread.AddOrUpdateThreadMember(threadMember, guildMember)); + } - guildMember = guild.GetUser(threadMember.UserId.Value); + if (newThreadMembers.Any()) + joinUsers = newThreadMembers.ToImmutableArray(); + } - if (guildMember == null) + if (leftUsers != null) + { + foreach (var threadUser in leftUsers) { - await UnknownGuildUserAsync("THREAD_MEMBERS_UPDATE", threadMember.UserId.Value, guild.Id); + await TimedInvokeAsync(_threadMemberLeft, nameof(ThreadMemberLeft), threadUser).ConfigureAwait(false); } - else - newThreadMembers.Add(thread.AddOrUpdateThreadMember(threadMember, guildMember)); } - if (newThreadMembers.Any()) - joinUsers = newThreadMembers.ToImmutableArray(); - } - - if (leftUsers != null) - { - foreach (var threadUser in leftUsers) + if (joinUsers != null) { - await TimedInvokeAsync(_threadMemberLeft, nameof(ThreadMemberLeft), threadUser).ConfigureAwait(false); + foreach (var threadUser in joinUsers) + { + await TimedInvokeAsync(_threadMemberJoined, nameof(ThreadMemberJoined), threadUser).ConfigureAwait(false); + } } } - if (joinUsers != null) + break; + #endregion + + #region Stage Channels + case "STAGE_INSTANCE_CREATE" or "STAGE_INSTANCE_UPDATE" or "STAGE_INSTANCE_DELETE": { - foreach (var threadUser in joinUsers) + 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 TimedInvokeAsync(_threadMemberJoined, nameof(ThreadMemberJoined), threadUser).ConfigureAwait(false); + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; } - } - } - break; - #endregion + var stageChannel = guild.GetStageChannel(data.ChannelId); - #region Stage Channels - case "STAGE_INSTANCE_CREATE" or "STAGE_INSTANCE_UPDATE" or "STAGE_INSTANCE_DELETE": - { - await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false); + if (stageChannel == null) + { + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); + return; + } - var data = (payload as JToken).ToObject(_serializer); + SocketStageChannel before = type == "STAGE_INSTANCE_UPDATE" ? stageChannel.Clone() : null; - var guild = State.GetGuild(data.GuildId); + stageChannel.Update(data, type == "STAGE_INSTANCE_CREATE"); - if (guild == null) - { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; + 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 - var stageChannel = guild.GetStageChannel(data.ChannelId); - - if (stageChannel == null) + #region Guild Scheduled Events + case "GUILD_SCHEDULED_EVENT_CREATE": { - await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); - return; - } + await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false); - SocketStageChannel before = type == "STAGE_INSTANCE_UPDATE" ? stageChannel.Clone() : null; + var data = (payload as JToken).ToObject(_serializer); - stageChannel.Update(data, type == "STAGE_INSTANCE_CREATE"); + var guild = State.GetGuild(data.GuildId); - 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); + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); return; - } - } - break; - #endregion + } - #region Guild Scheduled Events - case "GUILD_SCHEDULED_EVENT_CREATE": - { - await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false); + var newEvent = guild.AddOrUpdateEvent(data); - var data = (payload as JToken).ToObject(_serializer); + await TimedInvokeAsync(_guildScheduledEventCreated, nameof(GuildScheduledEventCreated), newEvent).ConfigureAwait(false); + } + break; + case "GUILD_SCHEDULED_EVENT_UPDATE": + { + await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false); - var guild = State.GetGuild(data.GuildId); + var data = (payload as JToken).ToObject(_serializer); - if (guild == null) - { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; - } + var guild = State.GetGuild(data.GuildId); - var newEvent = guild.AddOrUpdateEvent(data); + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } - await TimedInvokeAsync(_guildScheduledEventCreated, nameof(GuildScheduledEventCreated), newEvent).ConfigureAwait(false); - } - break; - case "GUILD_SCHEDULED_EVENT_UPDATE": - { - await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false); + var before = guild.GetEvent(data.Id)?.Clone(); - var data = (payload as JToken).ToObject(_serializer); + var beforeCacheable = new Cacheable(before, data.Id, before != null, () => Task.FromResult((SocketGuildEvent)null)); - var guild = State.GetGuild(data.GuildId); + var after = guild.AddOrUpdateEvent(data); - if (guild == null) - { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; + 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 before = guild.GetEvent(data.Id)?.Clone(); + var data = (payload as JToken).ToObject(_serializer); - var beforeCacheable = new Cacheable(before, data.Id, before != null, () => Task.FromResult((SocketGuildEvent)null)); + var guild = State.GetGuild(data.GuildId); + + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } - var after = guild.AddOrUpdateEvent(data); + var guildEvent = guild.RemoveEvent(data.Id) ?? SocketGuildEvent.Create(this, guild, data); - if ((before != null ? before.Status != GuildScheduledEventStatus.Completed : true) && data.Status == GuildScheduledEventStatus.Completed) - { - await TimedInvokeAsync(_guildScheduledEventCompleted, nameof(GuildScheduledEventCompleted), after).ConfigureAwait(false); + await TimedInvokeAsync(_guildScheduledEventCancelled, nameof(GuildScheduledEventCancelled), guildEvent).ConfigureAwait(false); } - else if ((before != null ? before.Status != GuildScheduledEventStatus.Active : false) && data.Status == GuildScheduledEventStatus.Active) + break; + case "GUILD_SCHEDULED_EVENT_USER_ADD" or "GUILD_SCHEDULED_EVENT_USER_REMOVE": { - 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); + await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); + var guild = State.GetGuild(data.GuildId); - if (guild == null) - { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; - } + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } - var guildEvent = guild.RemoveEvent(data.Id) ?? SocketGuildEvent.Create(this, guild, data); + var guildEvent = guild.GetEvent(data.EventId); - 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); + if (guildEvent == null) + { + await UnknownGuildEventAsync(type, data.EventId, data.GuildId).ConfigureAwait(false); + return; + } - var data = (payload as JToken).ToObject(_serializer); + var user = (SocketUser)guild.GetUser(data.UserId) ?? State.GetUser(data.UserId); - var guild = State.GetGuild(data.GuildId); + var cacheableUser = new Cacheable(user, data.UserId, user != null, () => Rest.GetUserAsync(data.UserId)); - if (guild == null) - { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; + 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; - var guildEvent = guild.GetEvent(data.EventId); + #endregion - if (guildEvent == null) - { - await UnknownGuildEventAsync(type, data.EventId, data.GuildId).ConfigureAwait(false); - return; - } + #region Webhooks - var user = (SocketUser)guild.GetUser(data.UserId) ?? State.GetUser(data.UserId); + case "WEBHOOKS_UPDATE": + { + var data = (payload as JToken).ToObject(_serializer); + type = "WEBHOOKS_UPDATE"; + await _gatewayLogger.DebugAsync("Received Dispatch (WEBHOOKS_UPDATE)").ConfigureAwait(false); - var cacheableUser = new Cacheable(user, data.UserId, user != null, () => Rest.GetUserAsync(data.UserId)); + var guild = State.GetGuild(data.GuildId); + var channel = State.GetChannel(data.ChannelId); - 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; + await TimedInvokeAsync(_webhooksUpdated, nameof(WebhooksUpdated), guild, channel); } - } - break; + break; - #endregion + #endregion - #region Webhooks + #region Audit Logs - case "WEBHOOKS_UPDATE": - { - var data = (payload as JToken).ToObject(_serializer); - type = "WEBHOOKS_UPDATE"; - await _gatewayLogger.DebugAsync("Received Dispatch (WEBHOOKS_UPDATE)").ConfigureAwait(false); + 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 channel = State.GetChannel(data.ChannelId); + var guild = State.GetGuild(data.GuildId); + var auditLog = SocketAuditLogEntry.Create(this, data); + guild.AddAuditLog(auditLog); - await TimedInvokeAsync(_webhooksUpdated, nameof(WebhooksUpdated), guild, channel); - } - break; + await TimedInvokeAsync(_auditLogCreated, nameof(AuditLogCreated), auditLog, guild); + } + break; + #endregion - #endregion + #region Auto Moderation - #region Audit Logs + case "AUTO_MODERATION_RULE_CREATE": + { + var data = (payload as JToken).ToObject(_serializer); - 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 guild = State.GetGuild(data.GuildId); - var auditLog = SocketAuditLogEntry.Create(this, data); - guild.AddAuditLog(auditLog); + var rule = guild.AddOrUpdateAutoModRule(data); - await TimedInvokeAsync(_auditLogCreated, nameof(AuditLogCreated), auditLog, guild); - } - break; - #endregion + await TimedInvokeAsync(_autoModRuleCreated, nameof(AutoModRuleCreated), rule); + } + break; - #region Auto Moderation + case "AUTO_MODERATION_RULE_UPDATE": + { + var data = (payload as JToken).ToObject(_serializer); - case "AUTO_MODERATION_RULE_CREATE": - { - var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.GuildId); - 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)); - var rule = guild.AddOrUpdateAutoModRule(data); + await TimedInvokeAsync(_autoModRuleUpdated, nameof(AutoModRuleUpdated), cacheableBefore, guild.AddOrUpdateAutoModRule(data)); + } + break; - await TimedInvokeAsync(_autoModRuleCreated, nameof(AutoModRuleCreated), rule); - } - break; + case "AUTO_MODERATION_RULE_DELETE": + { + var data = (payload as JToken).ToObject(_serializer); - case "AUTO_MODERATION_RULE_UPDATE": - { - var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.GuildId); - var guild = State.GetGuild(data.GuildId); + var rule = guild.RemoveAutoModRule(data); - 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(_autoModRuleDeleted, nameof(AutoModRuleDeleted), rule); + } + break; - await TimedInvokeAsync(_autoModRuleUpdated, nameof(AutoModRuleUpdated), cacheableBefore, guild.AddOrUpdateAutoModRule(data)); - } - 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); + } + ); - case "AUTO_MODERATION_RULE_DELETE": - { - var data = (payload as JToken).ToObject(_serializer); + ISocketMessageChannel channel = null; + if (data.ChannelId.IsSpecified) + channel = GetChannel(data.ChannelId.Value) as ISocketMessageChannel; - var guild = State.GetGuild(data.GuildId); + 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; + }); - var rule = guild.RemoveAutoModRule(data); - await TimedInvokeAsync(_autoModRuleDeleted, nameof(AutoModRuleDeleted), rule); - } - break; + IUserMessage cachedMsg = null; + if (data.MessageId.IsSpecified) + cachedMsg = channel?.GetCachedMessage(data.MessageId.GetValueOrDefault(0)) as IUserMessage; - 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, + var cacheableMessage = new Cacheable(cachedMsg, + data.MessageId.GetValueOrDefault(0), + cachedMsg is not null, async () => { - var model = await ApiClient.GetGuildMemberAsync(data.GuildId, data.UserId); - return guild.AddOrUpdateUser(model); - } - ); + 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; - ISocketMessageChannel channel = null; - if (data.ChannelId.IsSpecified) - channel = GetChannel(data.ChannelId.Value) as ISocketMessageChannel; + #endregion - 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; - }); + #region App Subscriptions + case "ENTITLEMENT_CREATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (ENTITLEMENT_CREATE)").ConfigureAwait(false); + var data = (payload as JToken).ToObject(_serializer); - IUserMessage cachedMsg = null; - if (data.MessageId.IsSpecified) - cachedMsg = channel?.GetCachedMessage(data.MessageId.GetValueOrDefault(0)) as IUserMessage; + var entitlement = SocketEntitlement.Create(this, data); + State.AddEntitlement(data.Id, entitlement); - 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; - }); + await TimedInvokeAsync(_entitlementCreated, nameof(EntitlementCreated), entitlement); + } + break; - 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; + case "ENTITLEMENT_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (ENTITLEMENT_UPDATE)").ConfigureAwait(false); + var data = (payload as JToken).ToObject(_serializer); - #endregion + var entitlement = State.GetEntitlement(data.Id); - #region App Subscriptions + var cacheableBefore = new Cacheable(entitlement?.Clone(), data.Id, + entitlement is not null, () => null); - case "ENTITLEMENT_CREATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (ENTITLEMENT_CREATE)").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); + if (entitlement is null) + { + entitlement = SocketEntitlement.Create(this, data); + State.AddEntitlement(data.Id, entitlement); + } + else + { + entitlement.Update(data); + } - var entitlement = SocketEntitlement.Create(this, data); - State.AddEntitlement(data.Id, entitlement); + await TimedInvokeAsync(_entitlementUpdated, nameof(EntitlementUpdated), cacheableBefore, entitlement); + } + break; - await TimedInvokeAsync(_entitlementCreated, nameof(EntitlementCreated), entitlement); - } - break; + case "ENTITLEMENT_DELETE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (ENTITLEMENT_DELETE)").ConfigureAwait(false); + var data = (payload as JToken).ToObject(_serializer); - case "ENTITLEMENT_UPDATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (ENTITLEMENT_UPDATE)").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); + var entitlement = State.RemoveEntitlement(data.Id); - var entitlement = State.GetEntitlement(data.Id); + if (entitlement is null) + entitlement = SocketEntitlement.Create(this, data); + else + entitlement.Update(data); - var cacheableBefore = new Cacheable(entitlement?.Clone(), data.Id, - entitlement is not null, () => null); + var cacheableEntitlement = new Cacheable(entitlement, 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(_entitlementDeleted, nameof(EntitlementDeleted), cacheableEntitlement); } + break; - await TimedInvokeAsync(_entitlementUpdated, nameof(EntitlementUpdated), cacheableBefore, entitlement); - } - break; + case "SUBSCRIPTION_CREATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (SUBSCRIPTION_CREATE)").ConfigureAwait(false); + var data = (payload as JToken).ToObject(_serializer); - case "ENTITLEMENT_DELETE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (ENTITLEMENT_DELETE)").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); + var subscription = SocketSubscription.Create(this, data); + State.AddSubscription(data.Id, subscription); - var entitlement = State.RemoveEntitlement(data.Id); + await TimedInvokeAsync(_subscriptionCreated, nameof(SubscriptionCreated), subscription); + } + break; - if (entitlement is null) - entitlement = SocketEntitlement.Create(this, data); - else - entitlement.Update(data); + case "SUBSCRIPTION_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (SUBSCRIPTION_UPDATE)").ConfigureAwait(false); + var data = (payload as JToken).ToObject(_serializer); - var cacheableEntitlement = new Cacheable(entitlement, data.Id, - entitlement is not null, () => null); + var subscription = State.GetSubscription(data.Id); - await TimedInvokeAsync(_entitlementDeleted, nameof(EntitlementDeleted), cacheableEntitlement); - } - break; + var cacheableBefore = new Cacheable(subscription?.Clone(), data.Id, + hasValue: subscription is not null, () => null); - case "SUBSCRIPTION_CREATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (SUBSCRIPTION_CREATE)").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); + if (subscription is null) + { + subscription = SocketSubscription.Create(this, data); + State.AddSubscription(data.Id, subscription); + } + else + { + subscription.Update(data); + } - var subscription = SocketSubscription.Create(this, data); - State.AddSubscription(data.Id, subscription); + await TimedInvokeAsync(_subscriptionUpdated, nameof(SubscriptionUpdated), cacheableBefore, subscription); + } + break; - await TimedInvokeAsync(_subscriptionCreated, nameof(SubscriptionCreated), subscription); - } - break; + case "SUBSCRIPTION_DELETE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (SUBSCRIPTION_DELETE)").ConfigureAwait(false); + var data = (payload as JToken).ToObject(_serializer); - case "SUBSCRIPTION_UPDATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (SUBSCRIPTION_UPDATE)").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); + var subscription = State.RemoveSubscription(data.Id); - var subscription = State.GetSubscription(data.Id); + if (subscription is null) + subscription = SocketSubscription.Create(this, data); + else + subscription.Update(data); - var cacheableBefore = new Cacheable(subscription?.Clone(), data.Id, - hasValue: subscription is not null, () => null); + var cacheableSubscription = new Cacheable(subscription, data.Id, + 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(_subscriptionDeleted, nameof(SubscriptionDeleted), cacheableSubscription); } + break; - 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); + #endregion - if (subscription is null) - subscription = SocketSubscription.Create(this, data); - else - subscription.Update(data); + #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 - var cacheableSubscription = new Cacheable(subscription, data.Id, - subscription is not null, () => null); + #region Others + default: + if (!SuppressUnknownDispatchWarnings) + await _gatewayLogger.WarningAsync($"Unknown Dispatch ({type})").ConfigureAwait(false); - await TimedInvokeAsync(_subscriptionDeleted, nameof(SubscriptionDeleted), cacheableSubscription); + await TimedInvokeAsync(_unknownDispatchReceived, nameof(UnknownDispatchReceived), type, (payload as JToken)); + break; + #endregion } 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); - - await TimedInvokeAsync(_unknownDispatchReceived, nameof(UnknownDispatchReceived), type, (payload as JToken)); - break; - #endregion } - break; + catch (Exception ex) + { + activity?.AddExceptionToActivity(ex); + throw; + } + finally + { + activity.Dispose(); + watch?.Stop(); + + SocketMeter.RecordSocketDispatch( +#if NET5_0_OR_GREATER + activity?.Duration ?? watch.Elapsed, +#else + watch.Elapsed, +#endif + type, BaseConfig); + } default: await _gatewayLogger.WarningAsync($"Unknown OpCode ({opCode})").ConfigureAwait(false); break; @@ -2526,6 +2550,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty ex.Data["payload_data"] = (payload as JToken).ToString(); } + SocketMeter.RecordSocketEventException(ex, opCode, type, BaseConfig); await _gatewayLogger.ErrorAsync($"Error handling {opCode}{(type != null ? $" ({type})" : "")}", ex).ConfigureAwait(false); } } From da574bb824b896aec0a5a1c86f88f1a45f58af09 Mon Sep 17 00:00:00 2001 From: Suiram1701 <110390261+Suiram1701@users.noreply.github.com> Date: Fri, 25 Apr 2025 23:20:02 +0200 Subject: [PATCH 04/12] Added metrics for shards and connections --- .../Diagnostics/Options.cs | 8 ++- .../Diagnostics/SocketActivity.cs | 6 +- .../Diagnostics/SocketMeter.cs | 71 ++++++++++++++++++- .../DiscordSocketClient.cs | 25 ++++++- 4 files changed, 101 insertions(+), 9 deletions(-) diff --git a/src/Discord.Net.WebSocket/Diagnostics/Options.cs b/src/Discord.Net.WebSocket/Diagnostics/Options.cs index 060e6d307a..b46c69468c 100644 --- a/src/Discord.Net.WebSocket/Diagnostics/Options.cs +++ b/src/Discord.Net.WebSocket/Diagnostics/Options.cs @@ -11,9 +11,13 @@ internal static class Options internal static readonly string Version = typeof(Options).Assembly.GetName().Version.ToString(); #if NET5_0_OR_GREATER - internal static IEnumerable> CreateTags(GatewayOpCode opCode, string type, DiscordSocketConfig config) => [ + internal static IEnumerable> CreateTags(DiscordSocketConfig config) => [ KeyValuePair.Create("client.gateway_host", config.GatewayHost ?? "/gateway"), - KeyValuePair.Create("client.shard_id", config.ShardId ?? 0), + KeyValuePair.Create("client.shard_id", config.ShardId ?? 0) + ]; + + internal static IEnumerable> CreateTags(GatewayOpCode opCode, string type, DiscordSocketConfig config) => [ + ..CreateTags(config), KeyValuePair.Create("event.op_code", opCode), KeyValuePair.Create("event.type", type) ]; diff --git a/src/Discord.Net.WebSocket/Diagnostics/SocketActivity.cs b/src/Discord.Net.WebSocket/Diagnostics/SocketActivity.cs index db1b2ee52b..18e1b6e8bc 100644 --- a/src/Discord.Net.WebSocket/Diagnostics/SocketActivity.cs +++ b/src/Discord.Net.WebSocket/Diagnostics/SocketActivity.cs @@ -1,6 +1,9 @@ -using Discord.API.Gateway; using System; + +#if NET5_0_OR_GREATER +using Discord.API.Gateway; using System.Diagnostics; +#endif namespace Discord.WebSocket.Diagnostics { @@ -34,6 +37,7 @@ internal static void AddExceptionToActivity(this Activity activity, Exception ex })); #endif } + #else internal static IDisposable StartSocketDispatchActivity(string type, DiscordSocketConfig config) => null; diff --git a/src/Discord.Net.WebSocket/Diagnostics/SocketMeter.cs b/src/Discord.Net.WebSocket/Diagnostics/SocketMeter.cs index 0f8d9dbdda..c51304b56a 100644 --- a/src/Discord.Net.WebSocket/Diagnostics/SocketMeter.cs +++ b/src/Discord.Net.WebSocket/Diagnostics/SocketMeter.cs @@ -1,9 +1,9 @@ using Discord.API.Gateway; using System; -using System.Collections.Generic; -using System.Diagnostics; #if NET6_0_OR_GREATER +using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.Metrics; #endif @@ -14,14 +14,50 @@ internal static class SocketMeter #if NET6_0_OR_GREATER private readonly static Meter _meter = new(Options.SourceName, Options.Version); +#if NET7_0_OR_GREATER + private readonly static UpDownCounter _clientShards; + + private readonly static UpDownCounter _socketConnections; +#endif + private readonly static Histogram _socketConnectionsLatency; + private readonly static Counter _socketEvents; private readonly static Counter _socketEventExceptions; private readonly static Counter _socketDispatches; private readonly static Counter _socketDispatchesExceptions; private readonly static Histogram _socketDispatchesDuration; +#if NET9_0_OR_GREATER + /* + * OTel bucket boundary recommendation for 'http.request.duration': + * [0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10] + * (https://github.com/open-telemetry/semantic-conventions/blob/release/v1.23.x/docs/http/http-metrics.md#metric-httpclientrequestduration) + */ + private readonly static double[] _histogramBoundaries = [0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.125, 0.15, 0.175, 0.2, 0.225, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10]; // Higher resolution in the area from 0.1 to 0.25 in 0.025 steps +#endif + static SocketMeter() { +#if NET7_0_OR_GREATER + _clientShards = _meter.CreateUpDownCounter( + name: "client.shards_count", + unit: "Shards", + description: "The amount of shards that currently exists."); + + _socketConnections = _meter.CreateUpDownCounter( + name: "socket.connections_count", + unit: "Connections", + description: "The total amount of WebSocket connections currently connected (should match the amount of shards)."); +#endif + _socketConnectionsLatency = _meter.CreateHistogram( + name: "socket.connections.latency", + unit: "Seconds", + description: "The latency of the open WebSocket connections." +#if NET9_0_OR_GREATER + , advice: new() { HistogramBucketBoundaries = _histogramBoundaries } +#endif + ); + _socketEvents = _meter.CreateCounter( name: "socket.events_count", unit: "Events", @@ -41,7 +77,30 @@ static SocketMeter() _socketDispatchesDuration = _meter.CreateHistogram( name: "socket.dispatches.duration", unit: "Seconds", - description: "The handling duration of dispatches (like 'READY' or 'INTERACTION_CREATE') received from the gateway."); + description: "The handling duration of dispatches (like 'READY' or 'INTERACTION_CREATE') received from the gateway." +#if NET9_0_OR_GREATER + , advice: new() { HistogramBucketBoundaries = _histogramBoundaries } +#endif + ); + } + + internal static void AddClientShards(int shards, DiscordSocketConfig config) + { +#if NET7_0_OR_GREATER + _clientShards.Add(shards, [.. Options.CreateTags(config)]); +#endif + } + + internal static void AddSocketConnections(int connections, DiscordSocketConfig config) + { +#if NET7_0_OR_GREATER + _socketConnections.Add(connections, [.. Options.CreateTags(config)]); +#endif + } + + internal static void RecordSocketLatency(double seconds, DiscordSocketConfig config) + { + _socketConnectionsLatency.Record(seconds, [.. Options.CreateTags(config)]); } internal static void RecordSocketEvent(GatewayOpCode opCode, string type, DiscordSocketConfig config) @@ -70,6 +129,12 @@ internal static void RecordSocketDispatch(TimeSpan duration, string type, Discor _socketDispatchesDuration.Record(duration.TotalSeconds, tags); } #else + internal static void AddClientShards(int shards, DiscordSocketConfig config) { } + + internal static void AddSocketConnections(int connections, DiscordSocketConfig config) { } + + internal static void RecordSocketLatency(double seconds, DiscordSocketConfig config) { } + internal static void RecordSocketEvent(GatewayOpCode opCode, string type, DiscordSocketConfig config) { } internal static void RecordSocketEventException(Exception ex, GatewayOpCode opCode, string type, DiscordSocketConfig config) { } diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index cb7ba9b9d0..092f66b9fe 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -6,7 +6,7 @@ using Discord.Net.WebSockets; using Discord.Rest; using Discord.Utils; - +using Discord.WebSocket.Diagnostics; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -192,7 +192,23 @@ private DiscordSocketClient(DiscordSocketConfig config, API.DiscordSocketApiClie JoinedGuild += async g => await _gatewayLogger.InfoAsync($"Joined {g.Name}").ConfigureAwait(false); GuildAvailable += async g => await _gatewayLogger.VerboseAsync($"Connected to {g.Name}").ConfigureAwait(false); GuildUnavailable += async g => await _gatewayLogger.VerboseAsync($"Disconnected from {g.Name}").ConfigureAwait(false); - LatencyUpdated += async (old, val) => await _gatewayLogger.DebugAsync($"Latency = {val} ms").ConfigureAwait(false); + + Connected += () => + { + SocketMeter.AddSocketConnections(1, BaseConfig); + return Task.CompletedTask; + }; + Disconnected += _ => + { + SocketMeter.AddSocketConnections(-1, BaseConfig); + return Task.CompletedTask; + + }; + LatencyUpdated += async (old, val) => + { + SocketMeter.RecordSocketLatency((double)val / 1000, BaseConfig); + await _gatewayLogger.DebugAsync($"Latency = {val} ms").ConfigureAwait(false); + }; GuildAvailable += g => { @@ -205,6 +221,8 @@ private DiscordSocketClient(DiscordSocketConfig config, API.DiscordSocketApiClie _largeGuilds = new ConcurrentQueue(); AuditLogCacheSize = config.AuditLogCacheSize; + + SocketMeter.AddClientShards(1, BaseConfig); } private static API.DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config) => new DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent, config.GatewayHost, @@ -220,13 +238,14 @@ internal override void Dispose(bool disposing) ApiClient?.Dispose(); _stateLock?.Dispose(); } + + SocketMeter.AddClientShards(-1, BaseConfig); _isDisposed = true; } base.Dispose(disposing); } - internal override async ValueTask DisposeAsync(bool disposing) { if (!_isDisposed) From b9ed2d194ea8c1f88bf4e86d111991bbcc67a6cb Mon Sep 17 00:00:00 2001 From: Suiram1701 <110390261+Suiram1701@users.noreply.github.com> Date: Sat, 26 Apr 2025 21:11:30 +0200 Subject: [PATCH 05/12] Enchanced existing metrics and tracing --- .../Diagnostics/BufferedUpDownCounter.cs | 65 +++++++++++++++++++ .../Diagnostics/SocketActivity.cs | 1 + .../Diagnostics/SocketMeter.cs | 12 ++-- .../DiscordSocketClient.EventHandling.cs | 18 ++--- 4 files changed, 79 insertions(+), 17 deletions(-) create mode 100644 src/Discord.Net.WebSocket/Diagnostics/BufferedUpDownCounter.cs diff --git a/src/Discord.Net.WebSocket/Diagnostics/BufferedUpDownCounter.cs b/src/Discord.Net.WebSocket/Diagnostics/BufferedUpDownCounter.cs new file mode 100644 index 0000000000..e3fa34118e --- /dev/null +++ b/src/Discord.Net.WebSocket/Diagnostics/BufferedUpDownCounter.cs @@ -0,0 +1,65 @@ +#if NET7_0_OR_GREATER +using System.Threading.Tasks; +using System.Diagnostics; +using System.Diagnostics.Metrics; +using System.Collections.ObjectModel; + +namespace Discord.WebSocket.Diagnostics +{ + /// + /// A wrapper around which buffers values in cause the instrument isn't enabled yet. + /// + internal class BufferedUpDownCounter + { + private readonly Collection<(int value, TagList tags)> _pendingValues = []; + private bool _buffering = false; + + /// + /// The instrument this instance will use. + /// + public UpDownCounter Instrument { get; private set; } + + /// + /// Creates a new instance of which buffers . + /// + /// The instrument to wrap. + public BufferedUpDownCounter(UpDownCounter instrument) + { + Instrument = instrument; + } + + /// + /// Calls as soon as the instrument is enabled. + /// + /// The amount to be added. + /// Tags to associate with the amount. + public void Add(int delta, TagList tags) + { + if (Instrument.Enabled) + { + Instrument.Add(delta, tags); + } + else + { + _pendingValues.Add((delta, tags)); + if (!_buffering) + { + _buffering = true; + _ = Task.Run(FlushWhenEnabled); + } + } + } + + private async Task FlushWhenEnabled() + { + while (!Instrument.Enabled) + { await Task.Delay(50); } + + _buffering = false; + foreach ((var value, var tags) in _pendingValues) + Instrument.Add(value, tags); + _pendingValues.Clear(); + } + } +} +#endif diff --git a/src/Discord.Net.WebSocket/Diagnostics/SocketActivity.cs b/src/Discord.Net.WebSocket/Diagnostics/SocketActivity.cs index 18e1b6e8bc..20fdaf310e 100644 --- a/src/Discord.Net.WebSocket/Diagnostics/SocketActivity.cs +++ b/src/Discord.Net.WebSocket/Diagnostics/SocketActivity.cs @@ -14,6 +14,7 @@ internal static class SocketActivity internal static Activity StartSocketDispatchActivity(string type, DiscordSocketConfig config) { + Activity.Current = null; // This activity doesn't have a parent so it have to be explicitly set return _source.StartActivity( "dispatch socket event", ActivityKind.Consumer, diff --git a/src/Discord.Net.WebSocket/Diagnostics/SocketMeter.cs b/src/Discord.Net.WebSocket/Diagnostics/SocketMeter.cs index c51304b56a..8c44240e53 100644 --- a/src/Discord.Net.WebSocket/Diagnostics/SocketMeter.cs +++ b/src/Discord.Net.WebSocket/Diagnostics/SocketMeter.cs @@ -15,9 +15,9 @@ internal static class SocketMeter private readonly static Meter _meter = new(Options.SourceName, Options.Version); #if NET7_0_OR_GREATER - private readonly static UpDownCounter _clientShards; + private readonly static BufferedUpDownCounter _clientShards; // Buffering is especially here required because Add gets called so early where the instrument isn't enabled yet. - private readonly static UpDownCounter _socketConnections; + private readonly static BufferedUpDownCounter _socketConnections; #endif private readonly static Histogram _socketConnectionsLatency; @@ -39,15 +39,15 @@ internal static class SocketMeter static SocketMeter() { #if NET7_0_OR_GREATER - _clientShards = _meter.CreateUpDownCounter( + _clientShards = new(_meter.CreateUpDownCounter( name: "client.shards_count", unit: "Shards", - description: "The amount of shards that currently exists."); + description: "The amount of shards that currently exists.")); - _socketConnections = _meter.CreateUpDownCounter( + _socketConnections = new(_meter.CreateUpDownCounter( name: "socket.connections_count", unit: "Connections", - description: "The total amount of WebSocket connections currently connected (should match the amount of shards)."); + description: "The total amount of WebSocket connections currently connected (should match the amount of shards).")); #endif _socketConnectionsLatency = _meter.CreateHistogram( name: "socket.connections.latency", diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.EventHandling.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.EventHandling.cs index d4c04ea986..f62c02f0ac 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.EventHandling.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.EventHandling.cs @@ -88,8 +88,10 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty } break; case GatewayOpCode.Dispatch: + // An extra Stopwatch is required due to `activity.Duration` cannot be used because its only useable when its stopped but then the metrics wont be associated with this trace. var activity = SocketActivity.StartSocketDispatchActivity(type, BaseConfig); - var watch = activity is null ? Stopwatch.StartNew() : null; + var watch = Stopwatch.StartNew(); + try { switch (type) @@ -2525,16 +2527,10 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty } finally { - activity.Dispose(); - watch?.Stop(); - - SocketMeter.RecordSocketDispatch( -#if NET5_0_OR_GREATER - activity?.Duration ?? watch.Elapsed, -#else - watch.Elapsed, -#endif - type, BaseConfig); + watch.Stop(); + SocketMeter.RecordSocketDispatch(watch.Elapsed, type, BaseConfig); + + activity?.Dispose(); } default: await _gatewayLogger.WarningAsync($"Unknown OpCode ({opCode})").ConfigureAwait(false); From aec77b93715ae6e54a96f29508ed33b82ceb1a02 Mon Sep 17 00:00:00 2001 From: Suiram1701 <110390261+Suiram1701@users.noreply.github.com> Date: Fri, 2 May 2025 14:50:11 +0200 Subject: [PATCH 06/12] Changed a bit in otel implementation -Removed metrics for con. management events (like HELLO, ...) -Changed Options class into a class for tags (also changed a tag) -Retrieve tags from the client like its done elsewere not from the config --- .../Diagnostics/DiagnosticTags.cs | 19 +++++ .../Diagnostics/Options.cs | 27 ------- .../Diagnostics/SocketActivity.cs | 8 +- .../Diagnostics/SocketMeter.cs | 79 +++++++------------ .../DiscordSocketClient.EventHandling.cs | 7 +- .../DiscordSocketClient.cs | 10 +-- 6 files changed, 61 insertions(+), 89 deletions(-) create mode 100644 src/Discord.Net.WebSocket/Diagnostics/DiagnosticTags.cs delete mode 100644 src/Discord.Net.WebSocket/Diagnostics/Options.cs diff --git a/src/Discord.Net.WebSocket/Diagnostics/DiagnosticTags.cs b/src/Discord.Net.WebSocket/Diagnostics/DiagnosticTags.cs new file mode 100644 index 0000000000..fba7940a10 --- /dev/null +++ b/src/Discord.Net.WebSocket/Diagnostics/DiagnosticTags.cs @@ -0,0 +1,19 @@ +#if NET5_0_OR_GREATER +using System.Collections.Generic; + +namespace Discord.WebSocket.Diagnostics +{ + internal static class DiagnosticTags + { + internal static IEnumerable> Create(DiscordSocketClient client) => [ + KeyValuePair.Create("client.shard_id", client.ShardId), + KeyValuePair.Create("client.api_version", $"v{DiscordConfig.APIVersion}") + ]; + + internal static IEnumerable> Create(string type, DiscordSocketClient client) => [ + ..Create(client), + KeyValuePair.Create("event.type", type) + ]; + } +} +#endif diff --git a/src/Discord.Net.WebSocket/Diagnostics/Options.cs b/src/Discord.Net.WebSocket/Diagnostics/Options.cs deleted file mode 100644 index b46c69468c..0000000000 --- a/src/Discord.Net.WebSocket/Diagnostics/Options.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Discord.API.Gateway; -using System; -using System.Collections.Generic; -using System.Diagnostics; - -namespace Discord.WebSocket.Diagnostics -{ - internal static class Options - { - internal const string SourceName = "Discord.Net.WebSocket"; - internal static readonly string Version = typeof(Options).Assembly.GetName().Version.ToString(); - -#if NET5_0_OR_GREATER - internal static IEnumerable> CreateTags(DiscordSocketConfig config) => [ - KeyValuePair.Create("client.gateway_host", config.GatewayHost ?? "/gateway"), - KeyValuePair.Create("client.shard_id", config.ShardId ?? 0) - ]; - - internal static IEnumerable> CreateTags(GatewayOpCode opCode, string type, DiscordSocketConfig config) => [ - ..CreateTags(config), - KeyValuePair.Create("event.op_code", opCode), - KeyValuePair.Create("event.type", type) - ]; -#endif - } - -} diff --git a/src/Discord.Net.WebSocket/Diagnostics/SocketActivity.cs b/src/Discord.Net.WebSocket/Diagnostics/SocketActivity.cs index 20fdaf310e..463481dad3 100644 --- a/src/Discord.Net.WebSocket/Diagnostics/SocketActivity.cs +++ b/src/Discord.Net.WebSocket/Diagnostics/SocketActivity.cs @@ -10,16 +10,16 @@ namespace Discord.WebSocket.Diagnostics internal static class SocketActivity { #if NET5_0_OR_GREATER - private static readonly ActivitySource _source = new(Options.SourceName, Options.Version); + private static readonly ActivitySource _source = new("Discord.Net.WebSocket", typeof(DiagnosticTags).Assembly.GetName().Version.ToString()); - internal static Activity StartSocketDispatchActivity(string type, DiscordSocketConfig config) + internal static Activity StartSocketDispatchActivity(string type, DiscordSocketClient client) { Activity.Current = null; // This activity doesn't have a parent so it have to be explicitly set return _source.StartActivity( "dispatch socket event", ActivityKind.Consumer, null, - tags: Options.CreateTags(GatewayOpCode.Dispatch, type, config)); + tags: DiagnosticTags.Create(type, client)); } internal static void AddExceptionToActivity(this Activity activity, Exception ex) @@ -40,7 +40,7 @@ internal static void AddExceptionToActivity(this Activity activity, Exception ex } #else - internal static IDisposable StartSocketDispatchActivity(string type, DiscordSocketConfig config) => null; + internal static IDisposable StartSocketDispatchActivity(string type, DiscordSocketClient client) => null; internal static void AddExceptionToActivity(this IDisposable activity, Exception ex) { } #endif diff --git a/src/Discord.Net.WebSocket/Diagnostics/SocketMeter.cs b/src/Discord.Net.WebSocket/Diagnostics/SocketMeter.cs index 8c44240e53..9782f641c8 100644 --- a/src/Discord.Net.WebSocket/Diagnostics/SocketMeter.cs +++ b/src/Discord.Net.WebSocket/Diagnostics/SocketMeter.cs @@ -12,7 +12,7 @@ namespace Discord.WebSocket.Diagnostics internal static class SocketMeter { #if NET6_0_OR_GREATER - private readonly static Meter _meter = new(Options.SourceName, Options.Version); + private readonly static Meter _meter = new("Discord.Net.WebSocket", typeof(DiagnosticTags).Assembly.GetName().Version.ToString()); #if NET7_0_OR_GREATER private readonly static BufferedUpDownCounter _clientShards; // Buffering is especially here required because Add gets called so early where the instrument isn't enabled yet. @@ -22,10 +22,8 @@ internal static class SocketMeter private readonly static Histogram _socketConnectionsLatency; private readonly static Counter _socketEvents; - private readonly static Counter _socketEventExceptions; - private readonly static Counter _socketDispatches; - private readonly static Counter _socketDispatchesExceptions; - private readonly static Histogram _socketDispatchesDuration; + private readonly static Histogram _socketEventsDuration; + private readonly static Counter _socketEventsExceptions; #if NET9_0_OR_GREATER /* @@ -61,85 +59,68 @@ static SocketMeter() _socketEvents = _meter.CreateCounter( name: "socket.events_count", unit: "Events", - description: "The total amount of events sent by the gateway since the application is running."); - _socketEventExceptions = _meter.CreateCounter( - name: "socket.events.exceptions_count", - unit: "Exceptions", - description: "The amount of exceptions occurred while event procession."); - _socketDispatches = _meter.CreateCounter( - name: "socket.dispatches_count", - unit: "Dispatches", - description: "The total amount of dispatches (like 'READY' or 'INTERACTION_CREATE') sent by the gateway since the application is running."); - _socketDispatchesExceptions = _meter.CreateCounter( - name: "socket.dispatches.exceptions_count", - unit: "Exceptions", - description: "The amount of exceptions occurred while handling dispatches (like 'READY' or 'INTERACTION_CREATE')."); - _socketDispatchesDuration = _meter.CreateHistogram( - name: "socket.dispatches.duration", + description: "The total amount of events sent by the gateway since the application has startet."); + _socketEventsDuration = _meter.CreateHistogram( + name: "socket.events.duration", unit: "Seconds", - description: "The handling duration of dispatches (like 'READY' or 'INTERACTION_CREATE') received from the gateway." + description: "The duration to dispatch events received from the gateway." #if NET9_0_OR_GREATER , advice: new() { HistogramBucketBoundaries = _histogramBoundaries } #endif ); + _socketEventsExceptions = _meter.CreateCounter( + name: "socket.events.exceptions_count", + unit: "Exceptions", + description: "The amount of exceptions occurred while dispatching dispatches sent by the gateway."); } - internal static void AddClientShards(int shards, DiscordSocketConfig config) + internal static void AddClientShards(int shards, DiscordSocketClient client) { #if NET7_0_OR_GREATER - _clientShards.Add(shards, [.. Options.CreateTags(config)]); + _clientShards.Add(shards, [.. DiagnosticTags.Create(client)]); #endif } - internal static void AddSocketConnections(int connections, DiscordSocketConfig config) + internal static void AddSocketConnections(int connections, DiscordSocketClient client) { #if NET7_0_OR_GREATER - _socketConnections.Add(connections, [.. Options.CreateTags(config)]); + _socketConnections.Add(connections, [.. DiagnosticTags.Create(client)]); #endif } - internal static void RecordSocketLatency(double seconds, DiscordSocketConfig config) + internal static void RecordConnectionLatency(double seconds, DiscordSocketClient client) { - _socketConnectionsLatency.Record(seconds, [.. Options.CreateTags(config)]); + _socketConnectionsLatency.Record(seconds, [.. DiagnosticTags.Create(client)]); } - internal static void RecordSocketEvent(GatewayOpCode opCode, string type, DiscordSocketConfig config) - { - _socketEvents.Add(1, [..Options.CreateTags(opCode, type, config)]); - } - - internal static void RecordSocketEventException(Exception ex, GatewayOpCode opCode, string type, DiscordSocketConfig config) + internal static void RecordSocketEventException(Exception ex, string type, DiscordSocketClient client) { TagList tags = [ - .. Options.CreateTags(opCode, type, config), + .. DiagnosticTags.Create(type, client), KeyValuePair.Create("exception.type", ex.GetType().ToString()), KeyValuePair.Create("exception.message", ex.Message), KeyValuePair.Create("exception.stacktrace", ex.ToString()), ]; - - _socketEventExceptions.Add(1, tags); - if (opCode == GatewayOpCode.Dispatch) - _socketDispatchesExceptions.Add(1, tags); + _socketEventsExceptions.Add(1, tags); } - internal static void RecordSocketDispatch(TimeSpan duration, string type, DiscordSocketConfig config) + internal static void RecordSocketEvent(TimeSpan duration, string type, DiscordSocketClient client) { - TagList tags = [..Options.CreateTags(GatewayOpCode.Dispatch, type, config)]; - _socketDispatches.Add(1, tags); - _socketDispatchesDuration.Record(duration.TotalSeconds, tags); + TagList tags = [..DiagnosticTags.Create(type, client)]; + + _socketEvents.Add(1, tags); + _socketEventsDuration.Record(duration.TotalSeconds, tags); } #else - internal static void AddClientShards(int shards, DiscordSocketConfig config) { } - - internal static void AddSocketConnections(int connections, DiscordSocketConfig config) { } + internal static void AddClientShards(int shards, DiscordSocketClient client) { } - internal static void RecordSocketLatency(double seconds, DiscordSocketConfig config) { } + internal static void AddSocketConnections(int connections, DiscordSocketClient client) { } - internal static void RecordSocketEvent(GatewayOpCode opCode, string type, DiscordSocketConfig config) { } + internal static void RecordConnectionLatency(double seconds, DiscordSocketClient client) { } - internal static void RecordSocketEventException(Exception ex, GatewayOpCode opCode, string type, DiscordSocketConfig config) { } + internal static void RecordSocketEventException(Exception ex, string type, DiscordSocketClient client) { } - internal static void RecordSocketDispatch(TimeSpan duration, string type, DiscordSocketConfig config) { } + internal static void RecordSocketEvent(TimeSpan duration, string type, DiscordSocketClient client) { } #endif } } diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.EventHandling.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.EventHandling.cs index f62c02f0ac..740e0309c5 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.EventHandling.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.EventHandling.cs @@ -22,7 +22,6 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty _lastSeq = seq.Value; _lastMessageTime = Environment.TickCount; - SocketMeter.RecordSocketEvent(opCode, type, BaseConfig); try { switch (opCode) @@ -89,7 +88,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty break; case GatewayOpCode.Dispatch: // An extra Stopwatch is required due to `activity.Duration` cannot be used because its only useable when its stopped but then the metrics wont be associated with this trace. - var activity = SocketActivity.StartSocketDispatchActivity(type, BaseConfig); + var activity = SocketActivity.StartSocketDispatchActivity(type, this); var watch = Stopwatch.StartNew(); try @@ -2523,12 +2522,13 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty catch (Exception ex) { activity?.AddExceptionToActivity(ex); + SocketMeter.RecordSocketEventException(ex, type, this); throw; } finally { watch.Stop(); - SocketMeter.RecordSocketDispatch(watch.Elapsed, type, BaseConfig); + SocketMeter.RecordSocketEvent(watch.Elapsed, type, this); activity?.Dispose(); } @@ -2546,7 +2546,6 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty ex.Data["payload_data"] = (payload as JToken).ToString(); } - SocketMeter.RecordSocketEventException(ex, opCode, type, BaseConfig); 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 092f66b9fe..07360e43e2 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -195,18 +195,18 @@ private DiscordSocketClient(DiscordSocketConfig config, API.DiscordSocketApiClie Connected += () => { - SocketMeter.AddSocketConnections(1, BaseConfig); + SocketMeter.AddSocketConnections(1, this); return Task.CompletedTask; }; Disconnected += _ => { - SocketMeter.AddSocketConnections(-1, BaseConfig); + SocketMeter.AddSocketConnections(-1, this); return Task.CompletedTask; }; LatencyUpdated += async (old, val) => { - SocketMeter.RecordSocketLatency((double)val / 1000, BaseConfig); + SocketMeter.RecordConnectionLatency((double)val / 1000, this); await _gatewayLogger.DebugAsync($"Latency = {val} ms").ConfigureAwait(false); }; @@ -222,7 +222,7 @@ private DiscordSocketClient(DiscordSocketConfig config, API.DiscordSocketApiClie _largeGuilds = new ConcurrentQueue(); AuditLogCacheSize = config.AuditLogCacheSize; - SocketMeter.AddClientShards(1, BaseConfig); + SocketMeter.AddClientShards(1, this); } private static API.DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config) => new DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent, config.GatewayHost, @@ -239,7 +239,7 @@ internal override void Dispose(bool disposing) _stateLock?.Dispose(); } - SocketMeter.AddClientShards(-1, BaseConfig); + SocketMeter.AddClientShards(-1, this); _isDisposed = true; } From a305c06508f0b0c3a44daa2ca240a02a650e456e Mon Sep 17 00:00:00 2001 From: Suiram1701 <110390261+Suiram1701@users.noreply.github.com> Date: Fri, 2 May 2025 23:34:36 +0200 Subject: [PATCH 07/12] Added UDP audio instrumentation --- src/Discord.Net.OpenTelemetry/Extensions.cs | 6 +- .../Audio/AudioClient.cs | 19 ++++- .../Diagnostics/AudioMeter.cs | 78 +++++++++++++++++++ .../Diagnostics/DiagnosticTags.cs | 16 +++- .../DiscordVoiceApiClient.cs | 4 + 5 files changed, 116 insertions(+), 7 deletions(-) create mode 100644 src/Discord.Net.WebSocket/Diagnostics/AudioMeter.cs diff --git a/src/Discord.Net.OpenTelemetry/Extensions.cs b/src/Discord.Net.OpenTelemetry/Extensions.cs index 7ecfe403da..f100b537c3 100644 --- a/src/Discord.Net.OpenTelemetry/Extensions.cs +++ b/src/Discord.Net.OpenTelemetry/Extensions.cs @@ -9,20 +9,20 @@ namespace Discord.OpenTelemetry /// public static class Extensions { - private const string SourceName = "Discord.Net.WebSocket"; + private static readonly string[] SourceNames = ["Discord.Net.WebSocket", "Discord.Net.Audio"]; public static TracerProviderBuilder AddDiscordNetInstrumentation(this TracerProviderBuilder builder) { if (builder is null) throw new ArgumentNullException(nameof(builder)); - return builder.AddSource(SourceName); + return builder.AddSource(SourceNames); } public static MeterProviderBuilder AddDiscordNetInstrumentation(this MeterProviderBuilder builder) { if (builder is null) throw new ArgumentNullException(nameof(builder)); - return builder.AddMeter(SourceName); + return builder.AddMeter(SourceNames); } } diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.cs index 40ef631dac..024b68da44 100644 --- a/src/Discord.Net.WebSocket/Audio/AudioClient.cs +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.cs @@ -4,6 +4,7 @@ using Discord.Net; using Discord.Net.Converters; using Discord.WebSocket; +using Discord.WebSocket.Diagnostics; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; @@ -58,6 +59,7 @@ public StreamPair(AudioInStream reader, AudioOutStream writer) private StopReason _stopReason; private bool _resuming; + public int ClientId { get; } public SocketGuild Guild { get; } public DiscordVoiceAPIClient ApiClient { get; private set; } public int Latency { get; private set; } @@ -73,13 +75,19 @@ public StreamPair(AudioInStream reader, AudioOutStream writer) internal AudioClient(SocketGuild guild, int clientId, ulong channelId) { Guild = guild; + ClientId = clientId; ChannelId = channelId; _audioLogger = Discord.LogManager.CreateLogger($"Audio #{clientId}"); ApiClient = new DiscordVoiceAPIClient(guild.Id, Discord.WebSocketProvider, Discord.UdpSocketProvider); ApiClient.SentGatewayMessage += async opCode => await _audioLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false); ApiClient.SentDiscovery += async () => await _audioLogger.DebugAsync("Sent Discovery").ConfigureAwait(false); - //ApiClient.SentData += async bytes => await _audioLogger.DebugAsync($"Sent {bytes} Bytes").ConfigureAwait(false); + ApiClient.SentData += bytes => + { + //await _audioLogger.DebugAsync($"Sent {bytes} Bytes").ConfigureAwait(false); + AudioMeter.RecordBytesSent(bytes, this); + return Task.CompletedTask; + }; ApiClient.ReceivedEvent += ProcessMessageAsync; ApiClient.ReceivedPacket += ProcessPacketAsync; @@ -101,7 +109,11 @@ internal AudioClient(SocketGuild guild, int clientId, ulong channelId) }; LatencyUpdated += async (old, val) => await _audioLogger.DebugAsync($"Latency = {val} ms").ConfigureAwait(false); - UdpLatencyUpdated += async (old, val) => await _audioLogger.DebugAsync($"UDP Latency = {val} ms").ConfigureAwait(false); + UdpLatencyUpdated += async (old, val) => + { + await _audioLogger.DebugAsync($"UDP Latency = {val} ms").ConfigureAwait(false); + AudioMeter.RecordUdpLatency((double)val / 1000, this); + }; } internal Task StartAsync(string url, ulong userId, string sessionId, string token) @@ -132,6 +144,7 @@ private async Task OnConnectingAsync() await _audioLogger.DebugAsync($"Connecting ApiClient. Voice server: wss://{_url}").ConfigureAwait(false); await ApiClient.ConnectAsync($"wss://{_url}?v={DiscordConfig.VoiceAPIVersion}").ConfigureAwait(false); await _audioLogger.DebugAsync($"Listening on port {ApiClient.UdpPort}").ConfigureAwait(false); + AudioMeter.AddAudioConnections(1, this); if (!_resuming) { @@ -151,6 +164,7 @@ private async Task OnDisconnectingAsync(Exception ex) { await _audioLogger.DebugAsync("Disconnecting ApiClient").ConfigureAwait(false); await ApiClient.DisconnectAsync().ConfigureAwait(false); + AudioMeter.AddAudioConnections(-1, this); if (_stopReason == StopReason.Unknown && ex.InnerException is WebSocketException exception) { @@ -403,6 +417,7 @@ private async Task ProcessMessageAsync(VoiceOpCode opCode, object payload) } private async Task ProcessPacketAsync(byte[] packet) { + AudioMeter.RecordBytesReceived(packet.Length, this); try { if (_connection.State == ConnectionState.Connecting) diff --git a/src/Discord.Net.WebSocket/Diagnostics/AudioMeter.cs b/src/Discord.Net.WebSocket/Diagnostics/AudioMeter.cs new file mode 100644 index 0000000000..993474793e --- /dev/null +++ b/src/Discord.Net.WebSocket/Diagnostics/AudioMeter.cs @@ -0,0 +1,78 @@ +using Discord.Audio; + +#if NET6_0_OR_GREATER +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.Metrics; +#endif + +namespace Discord.WebSocket.Diagnostics +{ + internal static class AudioMeter + { +#if NET6_0_OR_GREATER + private static readonly Meter _meter = new("Discord.Net.Audio", typeof(DiagnosticTags).Assembly.GetName().Version.ToString()); + +#if NET7_0_OR_GREATER + private static readonly UpDownCounter _audioConnections; +#endif + private static readonly Histogram _udpLatency; + + private static readonly Counter _audioBytesReceived; + private static readonly Counter _audioBytesSent; + + static AudioMeter() + { +#if NET7_0_OR_GREATER + _audioConnections = _meter.CreateUpDownCounter( + name: "audio.connections_count", + unit: "Connections", + description: "The amount of UDP audio connections currently active."); +#endif + _udpLatency = _meter.CreateHistogram( + name: "udp.connections.latency", + unit: "Seconds", + description: "The latency of the open UDP audio connections."); + + _audioBytesReceived = _meter.CreateCounter( + name: "udp.bytes_received", + unit: "Bytes", + description: "The total amount of bytes received from every UDP audio connection."); + _audioBytesSent = _meter.CreateCounter( + name: "udp.bytes_sent", + unit: "Bytes", + description: "The total amount of bytes sent by every UDP audio connection."); + } + + internal static void AddAudioConnections(int connections, AudioClient client) + { +#if NET7_0_OR_GREATER + _audioConnections.Add(connections); +#endif + } + + internal static void RecordUdpLatency(double seconds, AudioClient client) + { + _udpLatency.Record(seconds, [.. DiagnosticTags.CreateUdpTags(client)]); + } + + internal static void RecordBytesReceived(int amount, AudioClient client) + { + _audioBytesReceived.Add(amount, [.. DiagnosticTags.CreateUdpTags(client)]); + } + + internal static void RecordBytesSent(int amount, AudioClient client) + { + _audioBytesSent.Add(amount, [.. DiagnosticTags.CreateUdpTags(client)]); + } +#else + internal static void AddAudioConnections(int connections, AudioClient client) { } + + internal static void RecordUdpLatency(double seconds, AudioClient client) { } + + internal static void RecordBytesReceived(int amount, AudioClient client) { } + + internal static void RecordBytesSent(int amount, AudioClient client) { } +#endif + } +} diff --git a/src/Discord.Net.WebSocket/Diagnostics/DiagnosticTags.cs b/src/Discord.Net.WebSocket/Diagnostics/DiagnosticTags.cs index fba7940a10..f70ef92f52 100644 --- a/src/Discord.Net.WebSocket/Diagnostics/DiagnosticTags.cs +++ b/src/Discord.Net.WebSocket/Diagnostics/DiagnosticTags.cs @@ -1,4 +1,5 @@ #if NET5_0_OR_GREATER +using Discord.Audio; using System.Collections.Generic; namespace Discord.WebSocket.Diagnostics @@ -8,12 +9,23 @@ internal static class DiagnosticTags internal static IEnumerable> Create(DiscordSocketClient client) => [ KeyValuePair.Create("client.shard_id", client.ShardId), KeyValuePair.Create("client.api_version", $"v{DiscordConfig.APIVersion}") - ]; + ]; internal static IEnumerable> Create(string type, DiscordSocketClient client) => [ ..Create(client), KeyValuePair.Create("event.type", type) - ]; + ]; + + internal static IEnumerable> Create(AudioClient client) => [ + KeyValuePair.Create("client.id", client.ClientId) + ]; + + internal static IEnumerable> CreateUdpTags(AudioClient client) => [ + ..Create(client), + KeyValuePair.Create("client.port", client.ApiClient.UdpPort), + KeyValuePair.Create("server.remote_ip", client.ApiClient.UdpRemoteIp), + KeyValuePair.Create("server.remote_port", client.ApiClient.UdpRemotePort) + ]; } } #endif diff --git a/src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs b/src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs index cc810d42dd..d1ffda6240 100644 --- a/src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs +++ b/src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs @@ -48,6 +48,8 @@ internal class DiscordVoiceAPIClient : IDisposable internal IWebSocketClient WebSocketClient { get; } public ConnectionState ConnectionState { get; private set; } + public string UdpRemoteIp { get; private set; } + public int UdpRemotePort { get; private set; } public ushort UdpPort => _udp.Port; internal DiscordVoiceAPIClient(ulong guildId, WebSocketProvider webSocketProvider, UdpSocketProvider udpSocketProvider, JsonSerializer serializer = null) @@ -268,6 +270,8 @@ public async Task SendKeepaliveAsync() public void SetUdpEndpoint(string ip, int port) { + UdpRemoteIp = ip; + UdpRemotePort = port; _udp.SetDestination(ip, port); } #endregion From d3acf5375c69da38e5283811fb551e2bb8af4483 Mon Sep 17 00:00:00 2001 From: Suiram1701 <110390261+Suiram1701@users.noreply.github.com> Date: Wed, 14 May 2025 16:10:45 +0200 Subject: [PATCH 08/12] Added metrics to the audio seocket --- .../Audio/AudioClient.cs | 23 ++++- .../Diagnostics/AudioMeter.cs | 90 ++++++++++++++++++- .../Diagnostics/DiagnosticTags.cs | 6 ++ 3 files changed, 113 insertions(+), 6 deletions(-) diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.cs index 024b68da44..be1d080189 100644 --- a/src/Discord.Net.WebSocket/Audio/AudioClient.cs +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.cs @@ -10,6 +10,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Net.WebSockets; using System.Text; @@ -80,7 +81,11 @@ internal AudioClient(SocketGuild guild, int clientId, ulong channelId) _audioLogger = Discord.LogManager.CreateLogger($"Audio #{clientId}"); ApiClient = new DiscordVoiceAPIClient(guild.Id, Discord.WebSocketProvider, Discord.UdpSocketProvider); - ApiClient.SentGatewayMessage += async opCode => await _audioLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false); + ApiClient.SentGatewayMessage += async opCode => + { + AudioMeter.RecordSocketEventSent(opCode, this); + await _audioLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false); + }; ApiClient.SentDiscovery += async () => await _audioLogger.DebugAsync("Sent Discovery").ConfigureAwait(false); ApiClient.SentData += bytes => { @@ -88,6 +93,7 @@ internal AudioClient(SocketGuild guild, int clientId, ulong channelId) AudioMeter.RecordBytesSent(bytes, this); return Task.CompletedTask; }; + ApiClient.ReceivedEvent += ProcessMessageAsync; ApiClient.ReceivedPacket += ProcessPacketAsync; @@ -108,7 +114,11 @@ internal AudioClient(SocketGuild guild, int clientId, ulong channelId) e.ErrorContext.Handled = true; }; - LatencyUpdated += async (old, val) => await _audioLogger.DebugAsync($"Latency = {val} ms").ConfigureAwait(false); + LatencyUpdated += async (old, val) => + { + AudioMeter.RecordSocketLatency((double)val / 1000, this); + await _audioLogger.DebugAsync($"Latency = {val} ms").ConfigureAwait(false); + }; UdpLatencyUpdated += async (old, val) => { await _audioLogger.DebugAsync($"UDP Latency = {val} ms").ConfigureAwait(false); @@ -317,6 +327,7 @@ private async Task ProcessMessageAsync(VoiceOpCode opCode, object payload) { _lastMessageTime = Environment.TickCount; + var watch = Stopwatch.StartNew(); try { switch (opCode) @@ -402,7 +413,7 @@ private async Task ProcessMessageAsync(VoiceOpCode opCode, object payload) _heartbeatTask = RunHeartbeatAsync(_heartbeatInterval, _connection.CancelToken); _keepaliveTask = RunKeepaliveAsync(_connection.CancelToken); - _ = _connection.CompleteAsync(); + _ = _connection.CompleteAsync(); } break; default: @@ -413,6 +424,12 @@ private async Task ProcessMessageAsync(VoiceOpCode opCode, object payload) catch (Exception ex) { await _audioLogger.ErrorAsync($"Error handling {opCode}", ex).ConfigureAwait(false); + AudioMeter.RecordSocketEventException(opCode, ex, this); + } + finally + { + watch.Stop(); + AudioMeter.RecordSocketEventReceived(opCode, watch.Elapsed, this); } } private async Task ProcessPacketAsync(byte[] packet) diff --git a/src/Discord.Net.WebSocket/Diagnostics/AudioMeter.cs b/src/Discord.Net.WebSocket/Diagnostics/AudioMeter.cs index 993474793e..6c93e7ae61 100644 --- a/src/Discord.Net.WebSocket/Diagnostics/AudioMeter.cs +++ b/src/Discord.Net.WebSocket/Diagnostics/AudioMeter.cs @@ -1,4 +1,6 @@ +using System; using Discord.Audio; +using Discord.API.Voice; #if NET6_0_OR_GREATER using System.Collections.Generic; @@ -16,24 +18,68 @@ internal static class AudioMeter #if NET7_0_OR_GREATER private static readonly UpDownCounter _audioConnections; #endif + private static readonly Histogram _socketLatency; private static readonly Histogram _udpLatency; private static readonly Counter _audioBytesReceived; private static readonly Counter _audioBytesSent; + private static readonly Counter _socketEventsSentCount; + private static readonly Counter _socketEventsReceivedCount; + private static readonly Histogram _socketEventsReceivedDuration; + private static readonly Counter _socketEventsReceivedExceptions; + +#if NET9_0_OR_GREATER + /* + * OTel bucket boundary recommendation for 'http.request.duration': + * [0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10] + * (https://github.com/open-telemetry/semantic-conventions/blob/release/v1.23.x/docs/http/http-metrics.md#metric-httpclientrequestduration) + */ + private readonly static double[] _histogramBoundaries = [0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.125, 0.15, 0.175, 0.2, 0.225, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10]; // Higher resolution in the area from 0.1 to 0.25 in 0.025 steps +#endif + static AudioMeter() { #if NET7_0_OR_GREATER _audioConnections = _meter.CreateUpDownCounter( name: "audio.connections_count", unit: "Connections", - description: "The amount of UDP audio connections currently active."); + description: "The amount of both audio WebSocket and UDP connections currently active."); #endif + + _socketLatency = _meter.CreateHistogram( + name: "socket.connections.latency", + unit: "Seconds", + description: "The latency of the active audio WebSocket connections." +#if NET9_0_OR_GREATER + , advice: new() { HistogramBucketBoundaries = _histogramBoundaries } +#endif + ); + _socketEventsSentCount = _meter.CreateCounter( + name: "socket.events_sent.count", + unit: "Events", + description: "The amount of events sent to the audio gateway."); + _socketEventsReceivedCount = _meter.CreateCounter( + name: "socket.events_received.count", + unit: "Events", + description: "The amount of events received from the audio gateway."); + _socketEventsReceivedDuration = _meter.CreateHistogram( + name: "socket.events_received.duration", + unit: "Seconds", + description: "The duration it took to process events received from the audio gateway."); + _socketEventsReceivedExceptions = _meter.CreateCounter( + name: "socket.event_received.exception_count", + unit: "Exceptions", + description: "The amount of exceptions occurred while processing events received from the audio gateway."); + _udpLatency = _meter.CreateHistogram( name: "udp.connections.latency", unit: "Seconds", - description: "The latency of the open UDP audio connections."); - + description: "The latency of the open UDP audio connections." +#if NET9_0_OR_GREATER + , advice: new() { HistogramBucketBoundaries = _histogramBoundaries } +#endif + ); _audioBytesReceived = _meter.CreateCounter( name: "udp.bytes_received", unit: "Bytes", @@ -51,6 +97,36 @@ internal static void AddAudioConnections(int connections, AudioClient client) #endif } + internal static void RecordSocketLatency(double seconds, AudioClient client) + { + _socketLatency.Record(seconds, [.. DiagnosticTags.Create(client)]); + } + + internal static void RecordSocketEventSent(VoiceOpCode op, AudioClient client) + { + _socketEventsSentCount.Add(1, [.. DiagnosticTags.Create(op, client)]); + } + + internal static void RecordSocketEventReceived(VoiceOpCode op, TimeSpan duration, AudioClient client) + { + TagList tags = [ + .. DiagnosticTags.Create(client), + KeyValuePair.Create("event.op_code", op) + ]; + _socketEventsReceivedCount.Add(1, tags); + _socketEventsReceivedDuration.Record(duration.TotalSeconds, tags); + } + + internal static void RecordSocketEventException(VoiceOpCode op, Exception ex, AudioClient client) + { + _socketEventsReceivedExceptions.Add(1, [ + .. DiagnosticTags.Create(op, client), + KeyValuePair.Create("exception.type", ex.GetType().ToString()), + KeyValuePair.Create("exception.message", ex.Message), + KeyValuePair.Create("exception.stacktrace", ex.ToString()) + ]); + } + internal static void RecordUdpLatency(double seconds, AudioClient client) { _udpLatency.Record(seconds, [.. DiagnosticTags.CreateUdpTags(client)]); @@ -68,6 +144,14 @@ internal static void RecordBytesSent(int amount, AudioClient client) #else internal static void AddAudioConnections(int connections, AudioClient client) { } + internal static void RecordSocketLatency(double seconds, AudioClient client) { } + + internal static void RecordSocketEventSent(VoiceOpCode op, AudioClient client) { } + + internal static void RecordSocketEventReceived(VoiceOpCode op, TimeSpan duration, AudioClient client) { } + + internal static void RecordSocketEventException(VoiceOpCode op, Exception ex, AudioClient client) { } + internal static void RecordUdpLatency(double seconds, AudioClient client) { } internal static void RecordBytesReceived(int amount, AudioClient client) { } diff --git a/src/Discord.Net.WebSocket/Diagnostics/DiagnosticTags.cs b/src/Discord.Net.WebSocket/Diagnostics/DiagnosticTags.cs index f70ef92f52..6e77a7d8a0 100644 --- a/src/Discord.Net.WebSocket/Diagnostics/DiagnosticTags.cs +++ b/src/Discord.Net.WebSocket/Diagnostics/DiagnosticTags.cs @@ -1,4 +1,5 @@ #if NET5_0_OR_GREATER +using Discord.API.Voice; using Discord.Audio; using System.Collections.Generic; @@ -20,6 +21,11 @@ internal static IEnumerable> Create(AudioClient cli KeyValuePair.Create("client.id", client.ClientId) ]; + internal static IEnumerable> Create(VoiceOpCode opCode, AudioClient client) => [ + ..Create(client), + KeyValuePair.Create("event.op_code", opCode) + ]; + internal static IEnumerable> CreateUdpTags(AudioClient client) => [ ..Create(client), KeyValuePair.Create("client.port", client.ApiClient.UdpPort), From c1051f6d4e74317ccc635df3cf929cb511756548 Mon Sep 17 00:00:00 2001 From: Suiram1701 <110390261+Suiram1701@users.noreply.github.com> Date: Sat, 28 Jun 2025 18:17:30 +0200 Subject: [PATCH 09/12] Renamed every meter, activity and tag to be more OTel conform and added a reconnect counter for sockets --- .../Audio/AudioClient.cs | 12 ++- .../Diagnostics/AudioMeter.cs | 95 +++++++++++-------- .../Diagnostics/DiagnosticTags.cs | 31 +++--- .../Diagnostics/SocketActivity.cs | 19 ++-- .../Diagnostics/SocketMeter.cs | 80 ++++++++++------ .../DiscordSocketApiClient.cs | 1 + .../DiscordSocketClient.EventHandling.cs | 9 +- .../DiscordSocketClient.cs | 7 +- 8 files changed, 151 insertions(+), 103 deletions(-) diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.cs index be1d080189..8bb6a603a0 100644 --- a/src/Discord.Net.WebSocket/Audio/AudioClient.cs +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.cs @@ -1,7 +1,6 @@ using Discord.API.Voice; using Discord.Audio.Streams; using Discord.Logging; -using Discord.Net; using Discord.Net.Converters; using Discord.WebSocket; using Discord.WebSocket.Diagnostics; @@ -101,7 +100,12 @@ internal AudioClient(SocketGuild guild, int clientId, ulong channelId) _connection = new ConnectionManager(_stateLock, _audioLogger, ConnectionTimeoutMs, OnConnectingAsync, OnDisconnectingAsync, x => ApiClient.Disconnected += x); _connection.Connected += () => _connectedEvent.InvokeAsync(); - _connection.Disconnected += (exception, _) => _disconnectedEvent.InvokeAsync(exception); + _connection.Disconnected += (exception, reconnect) => + { + if (reconnect) + AudioMeter.AddAudioReconnect(this); + return _disconnectedEvent.InvokeAsync(exception); + }; _heartbeatTimes = new ConcurrentQueue(); _keepaliveTimes = new ConcurrentQueue>(); _ssrcMap = new ConcurrentDictionary(); @@ -424,12 +428,12 @@ private async Task ProcessMessageAsync(VoiceOpCode opCode, object payload) catch (Exception ex) { await _audioLogger.ErrorAsync($"Error handling {opCode}", ex).ConfigureAwait(false); - AudioMeter.RecordSocketEventException(opCode, ex, this); + AudioMeter.RecordSocketEventException(ex, opCode, this); } finally { watch.Stop(); - AudioMeter.RecordSocketEventReceived(opCode, watch.Elapsed, this); + AudioMeter.RecordSocketEventReceived(watch.Elapsed, opCode, this); } } private async Task ProcessPacketAsync(byte[] packet) diff --git a/src/Discord.Net.WebSocket/Diagnostics/AudioMeter.cs b/src/Discord.Net.WebSocket/Diagnostics/AudioMeter.cs index 6c93e7ae61..6c6cad0d1f 100644 --- a/src/Discord.Net.WebSocket/Diagnostics/AudioMeter.cs +++ b/src/Discord.Net.WebSocket/Diagnostics/AudioMeter.cs @@ -13,114 +13,131 @@ namespace Discord.WebSocket.Diagnostics internal static class AudioMeter { #if NET6_0_OR_GREATER - private static readonly Meter _meter = new("Discord.Net.Audio", typeof(DiagnosticTags).Assembly.GetName().Version.ToString()); + private static readonly Meter _meter = new("Discord.Net.Audio", typeof(DiagnosticTags).Assembly.GetName().Version!.ToString()); #if NET7_0_OR_GREATER - private static readonly UpDownCounter _audioConnections; + private static readonly UpDownCounter _socketConnections; #endif + private static readonly Counter _socketReconnects; private static readonly Histogram _socketLatency; - private static readonly Histogram _udpLatency; - - private static readonly Counter _audioBytesReceived; - private static readonly Counter _audioBytesSent; private static readonly Counter _socketEventsSentCount; private static readonly Counter _socketEventsReceivedCount; private static readonly Histogram _socketEventsReceivedDuration; private static readonly Counter _socketEventsReceivedExceptions; + private static readonly Counter _audioBytesReceived; + private static readonly Counter _audioBytesSent; + private static readonly Histogram _udpLatency; + #if NET9_0_OR_GREATER - /* + /* * OTel bucket boundary recommendation for 'http.request.duration': * [0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10] * (https://github.com/open-telemetry/semantic-conventions/blob/release/v1.23.x/docs/http/http-metrics.md#metric-httpclientrequestduration) */ - private readonly static double[] _histogramBoundaries = [0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.125, 0.15, 0.175, 0.2, 0.225, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10]; // Higher resolution in the area from 0.1 to 0.25 in 0.025 steps + private static readonly double[] _histogramBoundaries = [0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.125, 0.15, 0.175, 0.2, 0.225, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10]; // Higher resolution in the area from 0.1 to 0.25 in 0.025 steps #endif static AudioMeter() { + // Audio socket #if NET7_0_OR_GREATER - _audioConnections = _meter.CreateUpDownCounter( - name: "audio.connections_count", + _socketConnections = _meter.CreateUpDownCounter( + name: "discord.audio.connections_count", unit: "Connections", - description: "The amount of both audio WebSocket and UDP connections currently active."); + description: "The amount of WebSocket audio connections currently active."); #endif - + _socketReconnects = _meter.CreateCounter( + name: "discord.audio.reconnects_count", + unit: "Reconnects", + description: "The amount WebSocket audio connections reconnecting."); _socketLatency = _meter.CreateHistogram( - name: "socket.connections.latency", + name: "discord.audio.socket_latency", unit: "Seconds", description: "The latency of the active audio WebSocket connections." #if NET9_0_OR_GREATER - , advice: new() { HistogramBucketBoundaries = _histogramBoundaries } + , advice: new InstrumentAdvice { HistogramBucketBoundaries = _histogramBoundaries } #endif ); + + // Audio socket events _socketEventsSentCount = _meter.CreateCounter( - name: "socket.events_sent.count", + name: "discord.audio.events_sent.count", unit: "Events", description: "The amount of events sent to the audio gateway."); _socketEventsReceivedCount = _meter.CreateCounter( - name: "socket.events_received.count", + name: "discord.audio.events_received.count", unit: "Events", description: "The amount of events received from the audio gateway."); _socketEventsReceivedDuration = _meter.CreateHistogram( - name: "socket.events_received.duration", + name: "discord.audio.events_received.duration", unit: "Seconds", description: "The duration it took to process events received from the audio gateway."); _socketEventsReceivedExceptions = _meter.CreateCounter( - name: "socket.event_received.exception_count", + name: "discord.audio.event_received.exception_count", unit: "Exceptions", description: "The amount of exceptions occurred while processing events received from the audio gateway."); - _udpLatency = _meter.CreateHistogram( - name: "udp.connections.latency", - unit: "Seconds", - description: "The latency of the open UDP audio connections." -#if NET9_0_OR_GREATER - , advice: new() { HistogramBucketBoundaries = _histogramBoundaries } -#endif - ); + // UDP data _audioBytesReceived = _meter.CreateCounter( - name: "udp.bytes_received", + name: "discord.audio.bytes_received", unit: "Bytes", description: "The total amount of bytes received from every UDP audio connection."); _audioBytesSent = _meter.CreateCounter( - name: "udp.bytes_sent", + name: "discord.audio.bytes_sent", unit: "Bytes", description: "The total amount of bytes sent by every UDP audio connection."); + _udpLatency = _meter.CreateHistogram( + name: "discord.audio.udp_latency", + unit: "Seconds", + description: "The latency of the open UDP audio 'connections'." +#if NET9_0_OR_GREATER + , advice: new InstrumentAdvice { HistogramBucketBoundaries = _histogramBoundaries } +#endif + ); } internal static void AddAudioConnections(int connections, AudioClient client) { #if NET7_0_OR_GREATER - _audioConnections.Add(connections); + _socketConnections.Add(connections, [.. DiagnosticTags.CreateAudioClientTags(client)]); #endif } + internal static void AddAudioReconnect(AudioClient client) + { + _socketReconnects.Add(1, [.. DiagnosticTags.CreateAudioClientTags(client)]); + } + internal static void RecordSocketLatency(double seconds, AudioClient client) { - _socketLatency.Record(seconds, [.. DiagnosticTags.Create(client)]); + _socketLatency.Record(seconds, [.. DiagnosticTags.CreateAudioClientTags(client)]); } internal static void RecordSocketEventSent(VoiceOpCode op, AudioClient client) { - _socketEventsSentCount.Add(1, [.. DiagnosticTags.Create(op, client)]); + _socketEventsSentCount.Add(1, [ + .. DiagnosticTags.CreateAudioClientTags(client), + .. DiagnosticTags.CreateAudioEventTags(op) + ]); } - internal static void RecordSocketEventReceived(VoiceOpCode op, TimeSpan duration, AudioClient client) + internal static void RecordSocketEventReceived(TimeSpan duration, VoiceOpCode op, AudioClient client) { TagList tags = [ - .. DiagnosticTags.Create(client), - KeyValuePair.Create("event.op_code", op) + .. DiagnosticTags.CreateAudioClientTags(client), + .. DiagnosticTags.CreateAudioEventTags(op) ]; _socketEventsReceivedCount.Add(1, tags); _socketEventsReceivedDuration.Record(duration.TotalSeconds, tags); } - internal static void RecordSocketEventException(VoiceOpCode op, Exception ex, AudioClient client) + internal static void RecordSocketEventException(Exception ex, VoiceOpCode op, AudioClient client) { _socketEventsReceivedExceptions.Add(1, [ - .. DiagnosticTags.Create(op, client), + .. DiagnosticTags.CreateAudioClientTags(client), + .. DiagnosticTags.CreateAudioEventTags(op), KeyValuePair.Create("exception.type", ex.GetType().ToString()), KeyValuePair.Create("exception.message", ex.Message), KeyValuePair.Create("exception.stacktrace", ex.ToString()) @@ -144,13 +161,15 @@ internal static void RecordBytesSent(int amount, AudioClient client) #else internal static void AddAudioConnections(int connections, AudioClient client) { } + internal static void AddAudioReconnect(AudioClient client) { } + internal static void RecordSocketLatency(double seconds, AudioClient client) { } internal static void RecordSocketEventSent(VoiceOpCode op, AudioClient client) { } - internal static void RecordSocketEventReceived(VoiceOpCode op, TimeSpan duration, AudioClient client) { } + internal static void RecordSocketEventReceived(TimeSpan duration, VoiceOpCode op, AudioClient client) { } - internal static void RecordSocketEventException(VoiceOpCode op, Exception ex, AudioClient client) { } + internal static void RecordSocketEventException(Exception ex, VoiceOpCode op, AudioClient client) { } internal static void RecordUdpLatency(double seconds, AudioClient client) { } diff --git a/src/Discord.Net.WebSocket/Diagnostics/DiagnosticTags.cs b/src/Discord.Net.WebSocket/Diagnostics/DiagnosticTags.cs index 6e77a7d8a0..5ba3c1d19c 100644 --- a/src/Discord.Net.WebSocket/Diagnostics/DiagnosticTags.cs +++ b/src/Discord.Net.WebSocket/Diagnostics/DiagnosticTags.cs @@ -7,30 +7,31 @@ namespace Discord.WebSocket.Diagnostics { internal static class DiagnosticTags { - internal static IEnumerable> Create(DiscordSocketClient client) => [ - KeyValuePair.Create("client.shard_id", client.ShardId), - KeyValuePair.Create("client.api_version", $"v{DiscordConfig.APIVersion}") + internal static IEnumerable> CreateSocketClientTags(DiscordSocketClient client) => [ + KeyValuePair.Create("discord.client.shard_id", client.ShardId), + KeyValuePair.Create("discord.client.api_version", $"v{DiscordConfig.APIVersion}"), + KeyValuePair.Create("discord.client.gateway_url", client.ApiClient.GatewayUrl) ]; - internal static IEnumerable> Create(string type, DiscordSocketClient client) => [ - ..Create(client), - KeyValuePair.Create("event.type", type) + internal static IEnumerable> CreateEventTags(int? seq, string type) => [ + KeyValuePair.Create("discord.event_type", type), + KeyValuePair.Create("discord.event_sequence", seq) ]; - internal static IEnumerable> Create(AudioClient client) => [ - KeyValuePair.Create("client.id", client.ClientId) + internal static IEnumerable> CreateAudioClientTags(AudioClient client) => [ + KeyValuePair.Create("discord.audio.client_id", client.ClientId), + KeyValuePair.Create("discord.guild_id", client.Guild.Id), + KeyValuePair.Create("discord.channel_id", client.ChannelId) ]; - internal static IEnumerable> Create(VoiceOpCode opCode, AudioClient client) => [ - ..Create(client), - KeyValuePair.Create("event.op_code", opCode) + internal static IEnumerable> CreateAudioEventTags(VoiceOpCode opCode) => [ + KeyValuePair.Create("discord.audio.event_opCode", opCode) ]; internal static IEnumerable> CreateUdpTags(AudioClient client) => [ - ..Create(client), - KeyValuePair.Create("client.port", client.ApiClient.UdpPort), - KeyValuePair.Create("server.remote_ip", client.ApiClient.UdpRemoteIp), - KeyValuePair.Create("server.remote_port", client.ApiClient.UdpRemotePort) + KeyValuePair.Create("discord.audio.client_port", client.ApiClient.UdpPort), + KeyValuePair.Create("discord.audio.server.remote_ip", client.ApiClient.UdpRemoteIp), + KeyValuePair.Create("discord.audio.server.remote_port", client.ApiClient.UdpRemotePort) ]; } } diff --git a/src/Discord.Net.WebSocket/Diagnostics/SocketActivity.cs b/src/Discord.Net.WebSocket/Diagnostics/SocketActivity.cs index 463481dad3..3af20197a7 100644 --- a/src/Discord.Net.WebSocket/Diagnostics/SocketActivity.cs +++ b/src/Discord.Net.WebSocket/Diagnostics/SocketActivity.cs @@ -1,7 +1,7 @@ using System; #if NET5_0_OR_GREATER -using Discord.API.Gateway; +using System.Collections.Generic; using System.Diagnostics; #endif @@ -10,16 +10,17 @@ namespace Discord.WebSocket.Diagnostics internal static class SocketActivity { #if NET5_0_OR_GREATER - private static readonly ActivitySource _source = new("Discord.Net.WebSocket", typeof(DiagnosticTags).Assembly.GetName().Version.ToString()); + private static readonly ActivitySource _source = new("Discord.Net.WebSocket", typeof(DiagnosticTags).Assembly.GetName().Version!.ToString()); - internal static Activity StartSocketDispatchActivity(string type, DiscordSocketClient client) + internal static Activity StartSocketDispatchActivity(int? seq, string type, DiscordSocketClient client) { Activity.Current = null; // This activity doesn't have a parent so it have to be explicitly set - return _source.StartActivity( - "dispatch socket event", - ActivityKind.Consumer, - null, - tags: DiagnosticTags.Create(type, client)); + + IEnumerable> tags = [ + .. DiagnosticTags.CreateSocketClientTags(client), + .. DiagnosticTags.CreateEventTags(seq, type), + ]; + return _source.StartActivity($"process {type}", ActivityKind.Consumer, null, tags: tags); } internal static void AddExceptionToActivity(this Activity activity, Exception ex) @@ -40,7 +41,7 @@ internal static void AddExceptionToActivity(this Activity activity, Exception ex } #else - internal static IDisposable StartSocketDispatchActivity(string type, DiscordSocketClient client) => null; + internal static IDisposable StartSocketDispatchActivity(int? seq, string type, DiscordSocketClient client) => null; internal static void AddExceptionToActivity(this IDisposable activity, Exception ex) { } #endif diff --git a/src/Discord.Net.WebSocket/Diagnostics/SocketMeter.cs b/src/Discord.Net.WebSocket/Diagnostics/SocketMeter.cs index 9782f641c8..a3fbd87899 100644 --- a/src/Discord.Net.WebSocket/Diagnostics/SocketMeter.cs +++ b/src/Discord.Net.WebSocket/Diagnostics/SocketMeter.cs @@ -1,4 +1,3 @@ -using Discord.API.Gateway; using System; #if NET6_0_OR_GREATER @@ -12,64 +11,72 @@ namespace Discord.WebSocket.Diagnostics internal static class SocketMeter { #if NET6_0_OR_GREATER - private readonly static Meter _meter = new("Discord.Net.WebSocket", typeof(DiagnosticTags).Assembly.GetName().Version.ToString()); + private static readonly Meter _meter = new("Discord.Net.WebSocket", typeof(DiagnosticTags).Assembly.GetName().Version!.ToString()); #if NET7_0_OR_GREATER - private readonly static BufferedUpDownCounter _clientShards; // Buffering is especially here required because Add gets called so early where the instrument isn't enabled yet. + private static readonly BufferedUpDownCounter _clientShards; // Buffering is especially here required because Add gets called so early where the instrument isn't enabled yet. - private readonly static BufferedUpDownCounter _socketConnections; + private static readonly BufferedUpDownCounter _socketConnections; #endif - private readonly static Histogram _socketConnectionsLatency; + private static readonly Counter _socketReconnects; + private static readonly Histogram _socketConnectionsLatency; - private readonly static Counter _socketEvents; - private readonly static Histogram _socketEventsDuration; - private readonly static Counter _socketEventsExceptions; + private static readonly Counter _socketEvents; + private static readonly Histogram _socketEventsDuration; + private static readonly Counter _socketEventsExceptions; #if NET9_0_OR_GREATER - /* + /* * OTel bucket boundary recommendation for 'http.request.duration': * [0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10] * (https://github.com/open-telemetry/semantic-conventions/blob/release/v1.23.x/docs/http/http-metrics.md#metric-httpclientrequestduration) */ - private readonly static double[] _histogramBoundaries = [0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.125, 0.15, 0.175, 0.2, 0.225, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10]; // Higher resolution in the area from 0.1 to 0.25 in 0.025 steps + private static readonly double[] _histogramBoundaries = [0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.125, 0.15, 0.175, 0.2, 0.225, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10]; // Higher resolution in the area from 0.1 to 0.25 in 0.025 steps #endif static SocketMeter() { + // Shard metrics #if NET7_0_OR_GREATER - _clientShards = new(_meter.CreateUpDownCounter( - name: "client.shards_count", + _clientShards = new BufferedUpDownCounter(_meter.CreateUpDownCounter( + name: "discord.shards_count", unit: "Shards", description: "The amount of shards that currently exists.")); - _socketConnections = new(_meter.CreateUpDownCounter( - name: "socket.connections_count", + // Socket client metrics + _socketConnections = new BufferedUpDownCounter(_meter.CreateUpDownCounter( + name: "discord.socket.connections_count", unit: "Connections", - description: "The total amount of WebSocket connections currently connected (should match the amount of shards).")); + description: "The total amount of WebSocket connections currently connected (should match the amount of 'discord.shards_count').")); #endif + _socketReconnects = _meter.CreateCounter( + name: "discord.socket.reconnects_count", + unit: "Reconnects", + description: "The amount of WebSocket connections reconnecting."); _socketConnectionsLatency = _meter.CreateHistogram( - name: "socket.connections.latency", + name: "discord.socket.latency", unit: "Seconds", description: "The latency of the open WebSocket connections." #if NET9_0_OR_GREATER - , advice: new() { HistogramBucketBoundaries = _histogramBoundaries } + , advice: new InstrumentAdvice { HistogramBucketBoundaries = _histogramBoundaries } #endif ); + // Socket client event metrics _socketEvents = _meter.CreateCounter( - name: "socket.events_count", + name: "discord.events.received_count", unit: "Events", - description: "The total amount of events sent by the gateway since the application has startet."); + description: "The total amount of events received from the gateway since the application has startet."); _socketEventsDuration = _meter.CreateHistogram( - name: "socket.events.duration", + name: "discord.events.duration", unit: "Seconds", description: "The duration to dispatch events received from the gateway." #if NET9_0_OR_GREATER - , advice: new() { HistogramBucketBoundaries = _histogramBoundaries } + , advice: new InstrumentAdvice { HistogramBucketBoundaries = _histogramBoundaries } #endif ); _socketEventsExceptions = _meter.CreateCounter( - name: "socket.events.exceptions_count", + name: "discord.events.exceptions_count", unit: "Exceptions", description: "The amount of exceptions occurred while dispatching dispatches sent by the gateway."); } @@ -77,26 +84,32 @@ static SocketMeter() internal static void AddClientShards(int shards, DiscordSocketClient client) { #if NET7_0_OR_GREATER - _clientShards.Add(shards, [.. DiagnosticTags.Create(client)]); + _clientShards.Add(shards, [.. DiagnosticTags.CreateSocketClientTags(client)]); #endif } internal static void AddSocketConnections(int connections, DiscordSocketClient client) { #if NET7_0_OR_GREATER - _socketConnections.Add(connections, [.. DiagnosticTags.Create(client)]); + _socketConnections.Add(connections, [.. DiagnosticTags.CreateSocketClientTags(client)]); #endif } + internal static void AddSocketReconnect(DiscordSocketClient client) + { + _socketReconnects.Add(1, [.. DiagnosticTags.CreateSocketClientTags(client)]); + } + internal static void RecordConnectionLatency(double seconds, DiscordSocketClient client) { - _socketConnectionsLatency.Record(seconds, [.. DiagnosticTags.Create(client)]); + _socketConnectionsLatency.Record(seconds, [.. DiagnosticTags.CreateSocketClientTags(client)]); } - internal static void RecordSocketEventException(Exception ex, string type, DiscordSocketClient client) + internal static void RecordSocketEventException(Exception ex, int? seq, string type, DiscordSocketClient client) { TagList tags = [ - .. DiagnosticTags.Create(type, client), + .. DiagnosticTags.CreateSocketClientTags(client), + .. DiagnosticTags.CreateEventTags(seq, type), KeyValuePair.Create("exception.type", ex.GetType().ToString()), KeyValuePair.Create("exception.message", ex.Message), KeyValuePair.Create("exception.stacktrace", ex.ToString()), @@ -104,9 +117,12 @@ .. DiagnosticTags.Create(type, client), _socketEventsExceptions.Add(1, tags); } - internal static void RecordSocketEvent(TimeSpan duration, string type, DiscordSocketClient client) + internal static void RecordSocketEvent(TimeSpan duration, int? seq, string type, DiscordSocketClient client) { - TagList tags = [..DiagnosticTags.Create(type, client)]; + TagList tags = [ + .. DiagnosticTags.CreateSocketClientTags(client), + .. DiagnosticTags.CreateEventTags(seq, type) + ]; _socketEvents.Add(1, tags); _socketEventsDuration.Record(duration.TotalSeconds, tags); @@ -116,11 +132,13 @@ internal static void AddClientShards(int shards, DiscordSocketClient client) { } internal static void AddSocketConnections(int connections, DiscordSocketClient client) { } + internal static void AddSocketReconnect(DiscordSocketClient client) { } + internal static void RecordConnectionLatency(double seconds, DiscordSocketClient client) { } - internal static void RecordSocketEventException(Exception ex, string type, DiscordSocketClient client) { } + internal static void RecordSocketEventException(Exception ex, int? seq, string type, DiscordSocketClient client) { } - internal static void RecordSocketEvent(TimeSpan duration, string type, DiscordSocketClient client) { } + internal static void RecordSocketEvent(TimeSpan duration, int? seq, string type, DiscordSocketClient client) { } #endif } } diff --git a/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs b/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs index f33199ba90..197e5b8cc8 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs @@ -46,6 +46,7 @@ internal class DiscordSocketApiClient : DiscordRestApiClient /// public string GatewayUrl { + get => _gatewayUrl; set { // Makes the sharded client not override the custom value. diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.EventHandling.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.EventHandling.cs index 740e0309c5..bd5cfaeaee 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.EventHandling.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.EventHandling.cs @@ -15,7 +15,6 @@ namespace Discord.WebSocket; public partial class DiscordSocketClient { - private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string type, object payload) { if (seq != null) @@ -87,8 +86,8 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty } break; case GatewayOpCode.Dispatch: - // An extra Stopwatch is required due to `activity.Duration` cannot be used because its only useable when its stopped but then the metrics wont be associated with this trace. - var activity = SocketActivity.StartSocketDispatchActivity(type, this); + // An extra Stopwatch is required due to `activity.Duration` cannot be used because its only usable when its stopped but then the metrics won't be associated with this trace. + var activity = SocketActivity.StartSocketDispatchActivity(seq, type, this); var watch = Stopwatch.StartNew(); try @@ -2522,13 +2521,13 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty catch (Exception ex) { activity?.AddExceptionToActivity(ex); - SocketMeter.RecordSocketEventException(ex, type, this); + SocketMeter.RecordSocketEventException(ex, seq, type, this); throw; } finally { watch.Stop(); - SocketMeter.RecordSocketEvent(watch.Elapsed, type, this); + SocketMeter.RecordSocketEvent(watch.Elapsed, seq, type, this); activity?.Dispose(); } diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 07360e43e2..dde511d80a 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -172,7 +172,12 @@ private DiscordSocketClient(DiscordSocketConfig config, API.DiscordSocketApiClie _connection = new ConnectionManager(_stateLock, _gatewayLogger, config.ConnectionTimeout, OnConnectingAsync, OnDisconnectingAsync, x => ApiClient.Disconnected += x); _connection.Connected += () => TimedInvokeAsync(_connectedEvent, nameof(Connected)); - _connection.Disconnected += (ex, recon) => TimedInvokeAsync(_disconnectedEvent, nameof(Disconnected), ex); + _connection.Disconnected += (ex, recon) => + { + if (recon) + SocketMeter.AddSocketReconnect(this); + return TimedInvokeAsync(_disconnectedEvent, nameof(Disconnected), ex); + }; _nextAudioId = 1; _shardedClient = shardedClient; From d25b06e59e14ad41c0d6e0ef7eb6146fb6e4f228 Mon Sep 17 00:00:00 2001 From: Suiram1701 <110390261+Suiram1701@users.noreply.github.com> Date: Sun, 29 Jun 2025 11:28:16 +0200 Subject: [PATCH 10/12] Improved some XML comments and fixed the cref syntax error --- src/Discord.Net.OpenTelemetry/Extensions.cs | 20 +++++++++++++++---- .../Diagnostics/BufferedUpDownCounter.cs | 4 ++-- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/Discord.Net.OpenTelemetry/Extensions.cs b/src/Discord.Net.OpenTelemetry/Extensions.cs index f100b537c3..b90f547cf1 100644 --- a/src/Discord.Net.OpenTelemetry/Extensions.cs +++ b/src/Discord.Net.OpenTelemetry/Extensions.cs @@ -5,24 +5,36 @@ namespace Discord.OpenTelemetry { /// - /// An extension class which contains methods to added the Discord.Net OpenTelemetry instrumentations. + /// An extension class which contains methods to add the Discord.Net OpenTelemetry instrumentation. /// public static class Extensions { - private static readonly string[] SourceNames = ["Discord.Net.WebSocket", "Discord.Net.Audio"]; + private static readonly string[] _sourceNames = ["Discord.Net.WebSocket", "Discord.Net.Audio"]; + /// + /// Adds the trace sources of DNet. + /// + /// The trace provider to add these sources to. + /// The provided trace provider to chain calls. + /// public static TracerProviderBuilder AddDiscordNetInstrumentation(this TracerProviderBuilder builder) { if (builder is null) throw new ArgumentNullException(nameof(builder)); - return builder.AddSource(SourceNames); + return builder.AddSource(_sourceNames); } + /// + /// Adds the meters of DNet. + /// + /// The meter provider to add the meters to. + /// The provided meter builder to chain calls. + /// public static MeterProviderBuilder AddDiscordNetInstrumentation(this MeterProviderBuilder builder) { if (builder is null) throw new ArgumentNullException(nameof(builder)); - return builder.AddMeter(SourceNames); + return builder.AddMeter(_sourceNames); } } diff --git a/src/Discord.Net.WebSocket/Diagnostics/BufferedUpDownCounter.cs b/src/Discord.Net.WebSocket/Diagnostics/BufferedUpDownCounter.cs index e3fa34118e..d9bcc1d653 100644 --- a/src/Discord.Net.WebSocket/Diagnostics/BufferedUpDownCounter.cs +++ b/src/Discord.Net.WebSocket/Diagnostics/BufferedUpDownCounter.cs @@ -7,12 +7,12 @@ namespace Discord.WebSocket.Diagnostics { /// - /// A wrapper around which buffers values in cause the instrument isn't enabled yet. + /// A wrapper around (T is int) which buffers values in cause the instrument isn't enabled yet. /// internal class BufferedUpDownCounter { private readonly Collection<(int value, TagList tags)> _pendingValues = []; - private bool _buffering = false; + private bool _buffering; /// /// The instrument this instance will use. From 1c368c9a1f0dd4093ed09b2255b512eb68f1a06b Mon Sep 17 00:00:00 2001 From: Suiram1701 <110390261+Suiram1701@users.noreply.github.com> Date: Tue, 1 Jul 2025 20:17:41 +0200 Subject: [PATCH 11/12] Added all events to tracing and metrics and removed sequence tag --- .../Diagnostics/DiagnosticTags.cs | 17 +- .../Diagnostics/SocketActivity.cs | 9 +- .../Diagnostics/SocketMeter.cs | 27 +- .../DiscordSocketClient.EventHandling.cs | 3563 ++++++++--------- 4 files changed, 1809 insertions(+), 1807 deletions(-) diff --git a/src/Discord.Net.WebSocket/Diagnostics/DiagnosticTags.cs b/src/Discord.Net.WebSocket/Diagnostics/DiagnosticTags.cs index 5ba3c1d19c..38fda7bee4 100644 --- a/src/Discord.Net.WebSocket/Diagnostics/DiagnosticTags.cs +++ b/src/Discord.Net.WebSocket/Diagnostics/DiagnosticTags.cs @@ -1,7 +1,9 @@ #if NET5_0_OR_GREATER +using Discord.API.Gateway; using Discord.API.Voice; using Discord.Audio; using System.Collections.Generic; +using System.Linq; namespace Discord.WebSocket.Diagnostics { @@ -13,10 +15,15 @@ internal static IEnumerable> CreateSocketClientTags KeyValuePair.Create("discord.client.gateway_url", client.ApiClient.GatewayUrl) ]; - internal static IEnumerable> CreateEventTags(int? seq, string type) => [ - KeyValuePair.Create("discord.event_type", type), - KeyValuePair.Create("discord.event_sequence", seq) - ]; + internal static IEnumerable> CreateEventTags(GatewayOpCode opCode, string type) + { + IEnumerable> tags = [ + KeyValuePair.Create("discord.event_op_code", opCode) + ]; + if (!string.IsNullOrEmpty(type)) + tags = tags.Append(new KeyValuePair("discord.event_op_type", type)); + return tags; + } internal static IEnumerable> CreateAudioClientTags(AudioClient client) => [ KeyValuePair.Create("discord.audio.client_id", client.ClientId), @@ -25,7 +32,7 @@ internal static IEnumerable> CreateAudioClientTags( ]; internal static IEnumerable> CreateAudioEventTags(VoiceOpCode opCode) => [ - KeyValuePair.Create("discord.audio.event_opCode", opCode) + KeyValuePair.Create("discord.audio.event_op_code", opCode) ]; internal static IEnumerable> CreateUdpTags(AudioClient client) => [ diff --git a/src/Discord.Net.WebSocket/Diagnostics/SocketActivity.cs b/src/Discord.Net.WebSocket/Diagnostics/SocketActivity.cs index 3af20197a7..75bd7f84ec 100644 --- a/src/Discord.Net.WebSocket/Diagnostics/SocketActivity.cs +++ b/src/Discord.Net.WebSocket/Diagnostics/SocketActivity.cs @@ -1,3 +1,4 @@ +using Discord.API.Gateway; using System; #if NET5_0_OR_GREATER @@ -12,15 +13,15 @@ internal static class SocketActivity #if NET5_0_OR_GREATER private static readonly ActivitySource _source = new("Discord.Net.WebSocket", typeof(DiagnosticTags).Assembly.GetName().Version!.ToString()); - internal static Activity StartSocketDispatchActivity(int? seq, string type, DiscordSocketClient client) + internal static Activity StartSocketEventActivity(GatewayOpCode opCode, string type, DiscordSocketClient client) { Activity.Current = null; // This activity doesn't have a parent so it have to be explicitly set IEnumerable> tags = [ .. DiagnosticTags.CreateSocketClientTags(client), - .. DiagnosticTags.CreateEventTags(seq, type), + .. DiagnosticTags.CreateEventTags(opCode, type), ]; - return _source.StartActivity($"process {type}", ActivityKind.Consumer, null, tags: tags); + return _source.StartActivity($"process {opCode} {type}", ActivityKind.Consumer, null, tags: tags); } internal static void AddExceptionToActivity(this Activity activity, Exception ex) @@ -41,7 +42,7 @@ internal static void AddExceptionToActivity(this Activity activity, Exception ex } #else - internal static IDisposable StartSocketDispatchActivity(int? seq, string type, DiscordSocketClient client) => null; + internal static IDisposable StartSocketEventActivity(GatewayOpCode opCode, string type, DiscordSocketClient client) => null; internal static void AddExceptionToActivity(this IDisposable activity, Exception ex) { } #endif diff --git a/src/Discord.Net.WebSocket/Diagnostics/SocketMeter.cs b/src/Discord.Net.WebSocket/Diagnostics/SocketMeter.cs index a3fbd87899..5818d1b961 100644 --- a/src/Discord.Net.WebSocket/Diagnostics/SocketMeter.cs +++ b/src/Discord.Net.WebSocket/Diagnostics/SocketMeter.cs @@ -1,3 +1,4 @@ +using Discord.API.Gateway; using System; #if NET6_0_OR_GREATER @@ -105,27 +106,27 @@ internal static void RecordConnectionLatency(double seconds, DiscordSocketClient _socketConnectionsLatency.Record(seconds, [.. DiagnosticTags.CreateSocketClientTags(client)]); } - internal static void RecordSocketEventException(Exception ex, int? seq, string type, DiscordSocketClient client) + internal static void RecordSocketEvent(TimeSpan duration, GatewayOpCode opCode, string type, DiscordSocketClient client) { TagList tags = [ .. DiagnosticTags.CreateSocketClientTags(client), - .. DiagnosticTags.CreateEventTags(seq, type), - KeyValuePair.Create("exception.type", ex.GetType().ToString()), - KeyValuePair.Create("exception.message", ex.Message), - KeyValuePair.Create("exception.stacktrace", ex.ToString()), + .. DiagnosticTags.CreateEventTags(opCode, type) ]; - _socketEventsExceptions.Add(1, tags); + + _socketEvents.Add(1, tags); + _socketEventsDuration.Record(duration.TotalSeconds, tags); } - internal static void RecordSocketEvent(TimeSpan duration, int? seq, string type, DiscordSocketClient client) + internal static void RecordSocketEventException(Exception ex, GatewayOpCode opCode, string type, DiscordSocketClient client) { TagList tags = [ .. DiagnosticTags.CreateSocketClientTags(client), - .. DiagnosticTags.CreateEventTags(seq, type) + .. DiagnosticTags.CreateEventTags(opCode, type), + KeyValuePair.Create("exception.type", ex.GetType().ToString()), + KeyValuePair.Create("exception.message", ex.Message), + KeyValuePair.Create("exception.stacktrace", ex.ToString()), ]; - - _socketEvents.Add(1, tags); - _socketEventsDuration.Record(duration.TotalSeconds, tags); + _socketEventsExceptions.Add(1, tags); } #else internal static void AddClientShards(int shards, DiscordSocketClient client) { } @@ -136,9 +137,9 @@ internal static void AddSocketReconnect(DiscordSocketClient client) { } internal static void RecordConnectionLatency(double seconds, DiscordSocketClient client) { } - internal static void RecordSocketEventException(Exception ex, int? seq, string type, DiscordSocketClient client) { } + internal static void RecordSocketEvent(TimeSpan duration, GatewayOpCode opCode, string type, DiscordSocketClient client) { } - internal static void RecordSocketEvent(TimeSpan duration, int? seq, string type, DiscordSocketClient client) { } + internal static void RecordSocketEventException(Exception ex, GatewayOpCode opCode, string type, DiscordSocketClient client) { } #endif } } diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.EventHandling.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.EventHandling.cs index bd5cfaeaee..4424b18f2c 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.EventHandling.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.EventHandling.cs @@ -21,6 +21,10 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty _lastSeq = seq.Value; _lastMessageTime = Environment.TickCount; + // An extra Stopwatch is required due to `activity.Duration` cannot be used because its only usable when its stopped but then the metrics won't be associated with this trace when stopped. + var activity = SocketActivity.StartSocketEventActivity(opCode, type, this); + var watch = Stopwatch.StartNew(); + try { switch (opCode) @@ -86,167 +90,141 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty } break; case GatewayOpCode.Dispatch: - // An extra Stopwatch is required due to `activity.Duration` cannot be used because its only usable when its stopped but then the metrics won't be associated with this trace. - var activity = SocketActivity.StartSocketDispatchActivity(seq, type, this); - var watch = Stopwatch.StartNew(); - - try + switch (type) { - switch (type) + #region Connection + case "READY": { - #region Connection - case "READY": - { - try - { - await _gatewayLogger.DebugAsync("Received Dispatch (READY)").ConfigureAwait(false); + 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); - var data = (payload as JToken).ToObject(_serializer); - var state = new ClientState(data.Guilds.Length, data.PrivateChannels.Length); + _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; + } - 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++) + _lastGuildAvailableTime = Environment.TickCount; + _guildDownloadTask = WaitForGuildsAsync(_connection.CancelToken, _gatewayLogger) + .ContinueWith(async x => + { + if (x.IsFaulted) { - var model = data.Guilds[i]; - var guild = AddGuild(model, state); - if (!guild.IsAvailable) - unavailableGuilds++; - else - await GuildAvailableAsync(guild).ConfigureAwait(false); + _connection.Error(x.Exception); + return; } - 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; - } + else if (_connection.CancelToken.IsCancellationRequested) + return; - _lastGuildAvailableTime = Environment.TickCount; - _guildDownloadTask = WaitForGuildsAsync(_connection.CancelToken, _gatewayLogger) - .ContinueWith(async x => - { - if (x.IsFaulted) + if (BaseConfig.AlwaysDownloadUsers) + try { - _connection.Error(x.Exception); - return; + _ = DownloadUsersAsync(Guilds.Where(x => x.IsAvailable && !x.HasAllMembers)); + } + catch (Exception ex) + { + await _gatewayLogger.WarningAsync(ex); } - 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(); - } + 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); + case "RESUMED": + { + await _gatewayLogger.DebugAsync("Received Dispatch (RESUMED)").ConfigureAwait(false); - _ = _connection.CompleteAsync(); + _ = _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); - } + //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; + // Restore the previous sessions current user + CurrentUser = _previousSessionUser; - await _gatewayLogger.InfoAsync("Resumed previous session").ConfigureAwait(false); - } + await _gatewayLogger.InfoAsync("Resumed previous session").ConfigureAwait(false); + } break; - #endregion + #endregion + + #region Guilds + case "GUILD_CREATE": + { + var data = (payload as JToken).ToObject(_serializer); - #region Guilds - case "GUILD_CREATE": + if (data.Unavailable == false) { - var data = (payload as JToken).ToObject(_serializer); + type = "GUILD_AVAILABLE"; + _lastGuildAvailableTime = Environment.TickCount; + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_AVAILABLE)").ConfigureAwait(false); - if (data.Unavailable == false) + var guild = State.GetGuild(data.Id); + if (guild != null) { - 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); + guild.Update(State, data); - if (_unavailableGuildCount != 0) - _unavailableGuildCount--; - await GuildAvailableAsync(guild).ConfigureAwait(false); + 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 + if (guild.DownloadedMemberCount >= guild.MemberCount && !guild.DownloaderPromise.IsCompleted) { - await UnknownGuildAsync(type, data.Id).ConfigureAwait(false); - return; + guild.CompleteDownloadUsers(); + await TimedInvokeAsync(_guildMembersDownloadedEvent, nameof(GuildMembersDownloaded), guild).ConfigureAwait(false); } } 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; - } + await UnknownGuildAsync(type, data.Id).ConfigureAwait(false); + return; } } - break; - case "GUILD_UPDATE": + else { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_UPDATE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_CREATE)").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.Id); + var guild = AddGuild(data, State); if (guild != null) { - var before = guild.Clone(); - guild.Update(State, data); - await TimedInvokeAsync(_guildUpdatedEvent, nameof(GuildUpdated), before, guild).ConfigureAwait(false); + await TimedInvokeAsync(_joinedGuildEvent, nameof(JoinedGuild), guild).ConfigureAwait(false); + await GuildAvailableAsync(guild).ConfigureAwait(false); } else { @@ -254,2283 +232,2289 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty return; } } + } break; - case "GUILD_EMOJIS_UPDATE": + 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 _gatewayLogger.DebugAsync("Received Dispatch (GUILD_EMOJIS_UPDATE)").ConfigureAwait(false); + 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; - } + 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_SYNC": + case "GUILD_DELETE": + { + var data = (payload as JToken).ToObject(_serializer); + if (data.Unavailable == true) { - 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); + type = "GUILD_UNAVAILABLE"; + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_UNAVAILABLE)").ConfigureAwait(false); + 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); + await GuildUnavailableAsync(guild).ConfigureAwait(false); + _unavailableGuildCount++; } else { await UnknownGuildAsync(type, data.Id).ConfigureAwait(false); return; - }*/ + } } - break; - case "GUILD_DELETE": + else { - var data = (payload as JToken).ToObject(_serializer); - if (data.Unavailable == true) - { - type = "GUILD_UNAVAILABLE"; - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_UNAVAILABLE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_DELETE)").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; - } + 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 _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; - } + 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); + case "GUILD_STICKERS_UPDATE": + { + await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_STICKERS_UPDATE)").ConfigureAwait(false); - var guild = State.GetGuild(data.GuildId); + var data = (payload as JToken).ToObject(_serializer); - if (guild == null) - { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; - } + var guild = State.GetGuild(data.GuildId); - 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; + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } - 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(); + 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; - foreach (var model in newStickers) + var e = s.Equals(x); + if (!e) { - var entity = guild.AddSticker(model); - await TimedInvokeAsync(_guildStickerCreated, nameof(GuildStickerCreated), entity); + return (s, x) as (SocketCustomSticker Entity, API.Sticker Model)?; } - foreach (var sticker in deletedStickers) + else { - var entity = guild.RemoveSticker(sticker.Id); - await TimedInvokeAsync(_guildStickerDeleted, nameof(GuildStickerDeleted), entity); + return null; } - foreach (var entityModelPair in updatedStickers) - { - var before = entityModelPair.Entity.Clone(); - - entityModelPair.Entity.Update(entityModelPair.Model); + }).Where(x => x.HasValue).Select(x => x.Value).ToArray(); - await TimedInvokeAsync(_guildStickerUpdated, nameof(GuildStickerUpdated), before, entityModelPair.Entity); - } + foreach (var model in newStickers) + { + var entity = guild.AddSticker(model); + await TimedInvokeAsync(_guildStickerCreated, nameof(GuildStickerCreated), entity); } - break; - #endregion - - #region Channels - case "CHANNEL_CREATE": + foreach (var sticker in deletedStickers) { - 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); + var entity = guild.RemoveSticker(sticker.Id); + await TimedInvokeAsync(_guildStickerDeleted, nameof(GuildStickerDeleted), entity); + } + foreach (var entityModelPair in updatedStickers) + { + var before = entityModelPair.Entity.Clone(); - 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; - } + entityModelPair.Entity.Update(entityModelPair.Model); - if (channel != null) - await TimedInvokeAsync(_channelCreatedEvent, nameof(ChannelCreated), channel).ConfigureAwait(false); + await TimedInvokeAsync(_guildStickerUpdated, nameof(GuildStickerUpdated), before, entityModelPair.Entity); } + } break; - case "CHANNEL_UPDATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_UPDATE)").ConfigureAwait(false); + #endregion - var data = (payload as JToken).ToObject(_serializer); - var channel = State.GetChannel(data.Id); - if (channel != null) + #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) { - var before = channel.Clone(); - channel.Update(State, data); + channel = guild.AddChannel(State, data); - var guild = (channel as SocketGuildChannel)?.Guild; - if (!(guild?.IsSynced ?? true)) + if (!guild.IsSynced) { 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); + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); return; } } - break; - case "CHANNEL_DELETE": + else { - await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_DELETE)").ConfigureAwait(false); + channel = State.GetChannel(data.Id); + if (channel != null) + return; //Discord may send duplicate CHANNEL_CREATEs for DMs + channel = AddPrivateChannel(data, State) as SocketChannel; + } - 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 (channel != null) + await TimedInvokeAsync(_channelCreatedEvent, nameof(ChannelCreated), channel).ConfigureAwait(false); + } + break; + case "CHANNEL_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_UPDATE)").ConfigureAwait(false); - 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; + var data = (payload as JToken).ToObject(_serializer); + var channel = State.GetChannel(data.Id); + if (channel != null) + { + var before = channel.Clone(); + channel.Update(State, data); - if (channel != null) - await TimedInvokeAsync(_channelDestroyedEvent, nameof(ChannelDestroyed), channel).ConfigureAwait(false); - else + var guild = (channel as SocketGuildChannel)?.Guild; + if (!(guild?.IsSynced ?? true)) { - await UnknownChannelAsync(type, data.Id, data.GuildId.GetValueOrDefault(0)).ConfigureAwait(false); + 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; - #endregion + case "CHANNEL_DELETE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_DELETE)").ConfigureAwait(false); - #region Members - case "GUILD_MEMBER_ADD": + SocketChannel channel = null; + var data = (payload as JToken).ToObject(_serializer); + if (data.GuildId.IsSpecified) { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBER_ADD)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); + var guild = State.GetGuild(data.GuildId.Value); if (guild != null) { - var user = guild.AddOrUpdateUser(data); - guild.MemberCount++; + channel = guild.RemoveChannel(State, data.Id); 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); + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); return; } } - break; - case "GUILD_MEMBER_UPDATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBER_UPDATE)").ConfigureAwait(false); + else + channel = RemovePrivateChannel(data.Id) as SocketChannel; - 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 (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 - 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); - } + #region Members + case "GUILD_MEMBER_ADD": + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBER_ADD)").ConfigureAwait(false); - user.Update(State, data); + var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.GuildId); + if (guild != null) + { + var user = guild.AddOrUpdateUser(data); + guild.MemberCount++; - 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 + if (!guild.IsSynced) { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + 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_REMOVE": + 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) { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBER_REMOVE)").ConfigureAwait(false); + var user = guild.GetUser(data.User.Id); - var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); - if (guild != null) + if (!guild.IsSynced) { - SocketUser user = guild.RemoveUser(data.User.Id); - guild.MemberCount--; + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } - if (!guild.IsSynced) + if (user != null) + { + var before = user.Clone(); + if (user.GlobalUser.Update(State, data.User)) { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; + //Global data was updated, trigger UserUpdated + await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), before.GlobalUser, user).ConfigureAwait(false); } - 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)); + user.Update(State, data); - await TimedInvokeAsync(_userLeftEvent, nameof(UserLeft), guild, user).ConfigureAwait(false); + var cacheableBefore = new Cacheable(before, user.Id, true, () => Task.FromResult(null)); + await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), cacheableBefore, user).ConfigureAwait(false); } else { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; + user = guild.AddOrUpdateUser(data); + var cacheableBefore = new Cacheable(null, user.Id, false, () => Task.FromResult(null)); + await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), cacheableBefore, user).ConfigureAwait(false); } } - break; - case "GUILD_MEMBERS_CHUNK": + else { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBERS_CHUNK)").ConfigureAwait(false); + 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) - { - foreach (var memberModel in data.Members) - guild.AddOrUpdateUser(memberModel); + 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.DownloadedMemberCount >= guild.MemberCount && !guild.DownloaderPromise.IsCompleted) - { - guild.CompleteDownloadUsers(); - await TimedInvokeAsync(_guildMembersDownloadedEvent, nameof(GuildMembersDownloaded), guild).ConfigureAwait(false); - } - } - else + if (!guild.IsSynced) { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + 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); } - break; - case "GUILD_JOIN_REQUEST_DELETE": + else { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_JOIN_REQUEST_DELETE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + } + break; + case "GUILD_MEMBERS_CHUNK": + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBERS_CHUNK)").ConfigureAwait(false); - var guild = State.GetGuild(data.GuildId); + 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 == null) + if (guild.DownloadedMemberCount >= guild.MemberCount && !guild.DownloaderPromise.IsCompleted) { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; + 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 user = guild.RemoveUser(data.UserId); - guild.MemberCount--; + var data = (payload as JToken).ToObject(_serializer); - var cacheableUser = new Cacheable(user, data.UserId, user != null, () => Task.FromResult((SocketGuildUser)null)); + var guild = State.GetGuild(data.GuildId); - await TimedInvokeAsync(_guildJoinRequestDeletedEvent, nameof(GuildJoinRequestDeleted), cacheableUser, guild).ConfigureAwait(false); + 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 + #endregion + + #region DM Channels - #region DM Channels + case "CHANNEL_RECIPIENT_ADD": + { + await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_RECIPIENT_ADD)").ConfigureAwait(false); - case "CHANNEL_RECIPIENT_ADD": + 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 _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_RECIPIENT_ADD)").ConfigureAwait(false); + 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.GetOrAddUser(data.User); - await TimedInvokeAsync(_recipientAddedEvent, nameof(RecipientAdded), user).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 UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); + await UnknownChannelUserAsync(type, data.User.Id, data.ChannelId).ConfigureAwait(false); return; } } + else + { + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); + return; + } + } break; - case "CHANNEL_RECIPIENT_REMOVE": + + #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) { - await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_RECIPIENT_REMOVE)").ConfigureAwait(false); + var role = guild.AddRole(data.Role); - 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 + if (!guild.IsSynced) { - await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); + 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); - #endregion - - #region Roles - case "GUILD_ROLE_CREATE": + var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.GuildId); + if (guild != null) { - 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.GetRole(data.Role.Id); + if (role != null) { - var role = guild.AddRole(data.Role); + var before = role.Clone(); + role.Update(State, data.Role); if (!guild.IsSynced) { await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } - await TimedInvokeAsync(_roleCreatedEvent, nameof(RoleCreated), role).ConfigureAwait(false); + + await TimedInvokeAsync(_roleUpdatedEvent, nameof(RoleUpdated), before, role).ConfigureAwait(false); } else { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + await UnknownRoleAsync(type, data.Role.Id, guild.Id).ConfigureAwait(false); return; } } - break; - case "GUILD_ROLE_UPDATE": + else { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_ROLE_UPDATE)").ConfigureAwait(false); + 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 data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.GuildId); + if (guild != null) + { + var role = guild.RemoveRole(data.RoleId); + if (role != 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 + if (!guild.IsSynced) { - await UnknownRoleAsync(type, data.Role.Id, guild.Id).ConfigureAwait(false); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } + + await TimedInvokeAsync(_roleDeletedEvent, nameof(RoleDeleted), role).ConfigureAwait(false); } else { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + await UnknownRoleAsync(type, data.RoleId, guild.Id).ConfigureAwait(false); return; } } - break; - case "GUILD_ROLE_DELETE": + else { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_ROLE_DELETE)").ConfigureAwait(false); + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + } + break; + #endregion - 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; - } + #region Bans + case "GUILD_BAN_ADD": + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_BAN_ADD)").ConfigureAwait(false); - await TimedInvokeAsync(_roleDeletedEvent, nameof(RoleDeleted), role).ConfigureAwait(false); - } - else - { - await UnknownRoleAsync(type, data.RoleId, guild.Id).ConfigureAwait(false); - return; - } + 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; } - else + + 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 UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + 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 + #endregion - #region Bans - case "GUILD_BAN_ADD": + #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 _gatewayLogger.DebugAsync("Received Dispatch (GUILD_BAN_ADD)").ConfigureAwait(false); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } - var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); - if (guild != null) + if (channel == null) + { + if (!data.GuildId.IsSpecified) // assume it is a DM { - 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); + channel = CreateDMChannel(data.ChannelId, data.Author.Value, State); } else { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); return; } } - break; - case "GUILD_BAN_REMOVE": + + SocketUser author; + if (guild != null) { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_BAN_REMOVE)").ConfigureAwait(false); + 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); - var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); + if (author == null) + { if (guild != null) { - if (!guild.IsSynced) + if (data.Member.IsSpecified) // member isn't always included, but use it when we can { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; + data.Member.Value.User = data.Author.Value; + author = guild.AddOrUpdateUser(data.Member.Value); } - - 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 + 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 UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + await UnknownChannelUserAsync(type, data.Author.Value.Id, channel.Id).ConfigureAwait(false); return; } } - break; - #endregion - #region Messages - case "MESSAGE_CREATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_CREATE)").ConfigureAwait(false); + 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 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; - } + var guild = (channel as SocketGuildChannel)?.Guild; + if (guild != null && !guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } - if (channel == null) + 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 (!data.GuildId.IsSpecified) // assume it is a DM + if (guild != null) { - channel = CreateDMChannel(data.ChannelId, data.Author.Value, State); + 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) { - await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); - return; + 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); } } - - 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); + // Message author wasn't specified in the payload, so create a completely anonymous unknown user + author = new SocketUnknownUser(this, id: 0); - if (author == null) + if (channel == null) { - if (guild != null) + if (!data.GuildId.IsSpecified) // assume it is a DM { - if (data.Member.IsSpecified) // member isn't always included, but use it when we can + if (data.Author.IsSpecified) { - data.Member.Value.User = data.Author.Value; - author = guild.AddOrUpdateUser(data.Member.Value); + var dmChannel = CreateDMChannel(data.ChannelId, data.Author.Value, State); + channel = dmChannel; + author = dmChannel.Recipient; } else - author = guild.AddOrUpdateUser(data.Author.Value); // user has no guild-specific data + channel = CreateDMChannel(data.ChannelId, author, State); } - else if (channel is SocketGroupChannel groupChannel) - author = groupChannel.GetOrAddUser(data.Author.Value); else { - await UnknownChannelUserAsync(type, data.Author.Value.Id, channel.Id).ConfigureAwait(false); + await UnknownChannelAsync(type, data.ChannelId).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); + 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_UPDATE": + 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 _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_UPDATE)").ConfigureAwait(false); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } - var data = (payload as JToken).ToObject(_serializer); - var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; + 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); - var guild = (channel as SocketGuildChannel)?.Guild; - if (guild != null && !guild.IsSynced) - { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } + await TimedInvokeAsync(_messageDeletedEvent, nameof(MessageDeleted), cacheableMsg, cacheableChannel).ConfigureAwait(false); + } + break; + case "MESSAGE_REACTION_ADD": + { + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_ADD)").ConfigureAwait(false); - 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); + var data = (payload as JToken).ToObject(_serializer); + var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; - 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); + 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); - 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; - } - } + var optionalMsg = !isMsgCached + ? Optional.Create() + : Optional.Create(cachedMsg); - after = SocketMessage.Create(this, State, author, channel, data); - } - var cacheableBefore = new Cacheable(before, data.Id, isCached, async () => await channel.GetMessageAsync(data.Id).ConfigureAwait(false)); + if (data.Member.IsSpecified) + { + var guild = (channel as SocketGuildChannel)?.Guild; - await TimedInvokeAsync(_messageUpdatedEvent, nameof(MessageUpdated), cacheableBefore, after, channel).ConfigureAwait(false); + if (guild != null) + user = guild.AddOrUpdateUser(data.Member.Value); } - break; - case "MESSAGE_DELETE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_DELETE)").ConfigureAwait(false); + else + user = GetUser(data.UserId); - var data = (payload as JToken).ToObject(_serializer); - var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; + var optionalUser = user is null + ? Optional.Create() + : Optional.Create(user); - 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 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); - 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); + cachedMsg?.AddReaction(reaction); - await TimedInvokeAsync(_messageDeletedEvent, nameof(MessageDeleted), cacheableMsg, cacheableChannel).ConfigureAwait(false); - } + await TimedInvokeAsync(_reactionAddedEvent, nameof(ReactionAdded), cacheableMsg, cacheableChannel, reaction).ConfigureAwait(false); + } break; - case "MESSAGE_REACTION_ADD": - { - await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_ADD)").ConfigureAwait(false); + 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 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 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 optionalMsg = !isMsgCached + ? Optional.Create() + : Optional.Create(cachedMsg); - if (data.Member.IsSpecified) - { - var guild = (channel as SocketGuildChannel)?.Guild; + var optionalUser = user is null + ? Optional.Create() + : Optional.Create(user); - if (guild != null) - user = guild.AddOrUpdateUser(data.Member.Value); - } - else - user = GetUser(data.UserId); + 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); - var optionalUser = user is null - ? Optional.Create() - : Optional.Create(user); + cachedMsg?.RemoveReaction(reaction); - 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); + 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); - cachedMsg?.AddReaction(reaction); + var data = (payload as JToken).ToObject(_serializer); + var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; - await TimedInvokeAsync(_reactionAddedEvent, nameof(ReactionAdded), cacheableMsg, cacheableChannel, reaction).ConfigureAwait(false); - } - break; - case "MESSAGE_REACTION_REMOVE": + 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 () => { - await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_REMOVE)").ConfigureAwait(false); + var channelObj = await cacheableChannel.GetOrDownloadAsync().ConfigureAwait(false); + return await channelObj.GetMessageAsync(data.MessageId).ConfigureAwait(false) as IUserMessage; + }); - var data = (payload as JToken).ToObject(_serializer); - var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; + cachedMsg?.ClearReactions(); - 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); + 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 optionalUser = user is null - ? Optional.Create() - : Optional.Create(user); + 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 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); + var cachedMsg = channel?.GetCachedMessage(data.MessageId) as SocketUserMessage; + bool isMsgCached = cachedMsg != null; - cachedMsg?.RemoveReaction(reaction); + var optionalMsg = !isMsgCached + ? Optional.Create() + : Optional.Create(cachedMsg); - await TimedInvokeAsync(_reactionRemovedEvent, nameof(ReactionRemoved), cacheableMsg, cacheableChannel, reaction).ConfigureAwait(false); - } - break; - case "MESSAGE_REACTION_REMOVE_ALL": + 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 () => { - await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_REMOVE_ALL)").ConfigureAwait(false); + var channelObj = await cacheableChannel.GetOrDownloadAsync().ConfigureAwait(false); + return await channelObj.GetMessageAsync(data.MessageId).ConfigureAwait(false) as IUserMessage; + }); + var emote = data.Emoji.ToIEmote(); - var data = (payload as JToken).ToObject(_serializer); - var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; + cachedMsg?.RemoveReactionsForEmote(emote); - 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; - }); + 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); - cachedMsg?.ClearReactions(); + var data = (payload as JToken).ToObject(_serializer); + var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; - await TimedInvokeAsync(_reactionsClearedEvent, nameof(ReactionsCleared), cacheableMsg, cacheableChannel).ConfigureAwait(false); + var guild = (channel as SocketGuildChannel)?.Guild; + if (!(guild?.IsSynced ?? true)) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; } - break; - case "MESSAGE_REACTION_REMOVE_EMOJI": + + 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) { - await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_REMOVE_EMOJI)").ConfigureAwait(false); + 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); + } - var data = (payload as JToken).ToObject(_serializer); - var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; + await TimedInvokeAsync(_messagesBulkDeletedEvent, nameof(MessagesBulkDeleted), cacheableList, cacheableChannel).ConfigureAwait(false); + } + break; + #endregion - var cachedMsg = channel?.GetCachedMessage(data.MessageId) as SocketUserMessage; - bool isMsgCached = cachedMsg != null; + #region Polls - var optionalMsg = !isMsgCached - ? Optional.Create() - : Optional.Create(cachedMsg); + case "MESSAGE_POLL_VOTE_ADD": + { + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_POLL_VOTE_ADD)").ConfigureAwait(false); - 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(); + var data = (payload as JToken).ToObject(_serializer); - cachedMsg?.RemoveReactionsForEmote(emote); + Cacheable? guildCacheable = null; - 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); + Cacheable userCacheable; + Cacheable channelCacheable; + Cacheable messageCacheable; - var data = (payload as JToken).ToObject(_serializer); - var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; + 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)); - var guild = (channel as SocketGuildChannel)?.Guild; - if (!(guild?.IsSynced ?? true)) + if (guild is not null) { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } + 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 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) + 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 { - 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); + 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); - await TimedInvokeAsync(_messagesBulkDeletedEvent, nameof(MessagesBulkDeleted), cacheableList, cacheableChannel).ConfigureAwait(false); + 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; - #endregion - #region Polls + case "MESSAGE_POLL_VOTE_REMOVE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_POLL_VOTE_REMOVE)").ConfigureAwait(false); - case "MESSAGE_POLL_VOTE_ADD": - { - await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_POLL_VOTE_ADD)").ConfigureAwait(false); + var data = (payload as JToken).ToObject(_serializer); - var data = (payload as JToken).ToObject(_serializer); + Cacheable? guildCacheable = null; - Cacheable? guildCacheable = null; + Cacheable userCacheable; + Cacheable channelCacheable; + Cacheable messageCacheable; - 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 (data.GuildId.IsSpecified) + if (guild is not null) { - var guild = State.GetGuild(data.GuildId.Value); - guildCacheable = new(guild, data.GuildId.Value, guild is not null, () => Rest.GetGuildAsync(data.GuildId.Value)); + var user = guild.GetUser(data.UserId); + userCacheable = new(user, data.UserId, user is not null, async () => await Rest.GetGuildUserAsync(data.GuildId.Value, data.UserId)); - 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 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); - } + 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 { - 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); + 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); } - - await TimedInvokeAsync(_pollVoteAdded, nameof(PollVoteAdded), userCacheable, channelCacheable, messageCacheable, guildCacheable, data.AnswerId); } - break; - - case "MESSAGE_POLL_VOTE_REMOVE": + else { - await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_POLL_VOTE_REMOVE)").ConfigureAwait(false); + var user = State.GetUser(data.UserId); + userCacheable = new(user, data.UserId, user is not null, async () => await GetUserAsync(data.UserId)); - var data = (payload as JToken).ToObject(_serializer); + 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); - Cacheable? guildCacheable = null; + 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); + } - Cacheable userCacheable; - Cacheable channelCacheable; - Cacheable messageCacheable; + await TimedInvokeAsync(_pollVoteRemoved, nameof(PollVoteRemoved), userCacheable, channelCacheable, messageCacheable, guildCacheable, data.AnswerId); + } + break; - 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)); + #endregion - 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)); + #region Statuses + case "PRESENCE_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (PRESENCE_UPDATE)").ConfigureAwait(false); - var channel = guild.GetTextChannel(data.ChannelId); - channelCacheable = new(channel, data.ChannelId, channel is not null, async () => (RestTextChannel)await Rest.GetChannelAsync(data.ChannelId)); + var data = (payload as JToken).ToObject(_serializer); - 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); - } + 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; } - else + if (!guild.IsSynced) { - 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 UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; } - 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) + user = guild.GetUser(data.User.Id); + if (user == null) { - var guild = State.GetGuild(data.GuildId.Value); - if (guild == null) + if (data.Status == UserStatus.Offline) { - 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); - } - } + user = guild.AddOrUpdateUser(data); } else { - user = State.GetUser(data.User.Id); - if (user == null) + var globalBefore = user.GlobalUser.Clone(); + if (user.GlobalUser.Update(State, data.User)) { - await UnknownGlobalUserAsync(type, data.User.Id).ConfigureAwait(false); - return; + //Global data was updated, trigger UserUpdated + await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), globalBefore, user).ConfigureAwait(false); } } - - 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": + else { - 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)) + user = State.GetUser(data.User.Id); + if (user == null) { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + await UnknownGlobalUserAsync(type, data.User.Id).ConfigureAwait(false); return; } + } - var cacheableChannel = new Cacheable(channel, data.ChannelId, channel != null, async () => await GetChannelAsync(data.ChannelId).ConfigureAwait(false) as IMessageChannel); + 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 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)); + var data = (payload as JToken).ToObject(_serializer); + var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; - await TimedInvokeAsync(_userIsTypingEvent, nameof(UserIsTyping), cacheableUser, cacheableChannel).ConfigureAwait(false); + var guild = (channel as SocketGuildChannel)?.Guild; + if (!(guild?.IsSynced ?? true)) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; } - break; - #endregion - #region Integrations - case "INTEGRATION_CREATE": + 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) { - await _gatewayLogger.DebugAsync("Received Dispatch (INTEGRATION_CREATE)").ConfigureAwait(false); + 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)); - var data = (payload as JToken).ToObject(_serializer); + await TimedInvokeAsync(_userIsTypingEvent, nameof(UserIsTyping), cacheableUser, cacheableChannel).ConfigureAwait(false); + } + break; + #endregion - // Integrations from Gateway should always have guild IDs specified. - if (!data.GuildId.IsSpecified) - return; + #region Integrations + case "INTEGRATION_CREATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (INTEGRATION_CREATE)").ConfigureAwait(false); - var guild = State.GetGuild(data.GuildId.Value); + var data = (payload as JToken).ToObject(_serializer); - if (guild != null) - { - if (!guild.IsSynced) - { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } + // Integrations from Gateway should always have guild IDs specified. + if (!data.GuildId.IsSpecified) + return; - await TimedInvokeAsync(_integrationCreated, nameof(IntegrationCreated), RestIntegration.Create(this, guild, data)).ConfigureAwait(false); - } - else + var guild = State.GetGuild(data.GuildId.Value); + + if (guild != null) + { + if (!guild.IsSynced) { - await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } + + await TimedInvokeAsync(_integrationCreated, nameof(IntegrationCreated), RestIntegration.Create(this, guild, data)).ConfigureAwait(false); } - break; - case "INTEGRATION_UPDATE": + else { - await _gatewayLogger.DebugAsync("Received Dispatch (INTEGRATION_UPDATE)").ConfigureAwait(false); + 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); + var data = (payload as JToken).ToObject(_serializer); - // Integrations from Gateway should always have guild IDs specified. - if (!data.GuildId.IsSpecified) - return; + // Integrations from Gateway should always have guild IDs specified. + if (!data.GuildId.IsSpecified) + return; - var guild = State.GetGuild(data.GuildId.Value); + var guild = State.GetGuild(data.GuildId.Value); - if (guild != null) + if (guild != null) + { + if (!guild.IsSynced) { - if (!guild.IsSynced) - { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } - - await TimedInvokeAsync(_integrationUpdated, nameof(IntegrationUpdated), RestIntegration.Create(this, guild, data)).ConfigureAwait(false); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; } - else + + 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 UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + 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; - case "INTEGRATION_DELETE": + #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.DebugAsync("Received Dispatch (INTEGRATION_DELETE)").ConfigureAwait(false); + await _gatewayLogger.WarningAsync("Received USER_UPDATE for wrong user.").ConfigureAwait(false); + return; + } + } + break; + #endregion - var data = (payload as JToken).ToObject(_serializer); + #region Voice + case "VOICE_STATE_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (VOICE_STATE_UPDATE)").ConfigureAwait(false); - var guild = State.GetGuild(data.GuildId); + 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 (guild != null) + if (data.ChannelId != null) { - if (!guild.IsSynced) + before = guild.GetVoiceState(data.UserId)?.Clone() ?? SocketVoiceState.Default; + after = await guild.AddOrUpdateVoiceStateAsync(State, data).ConfigureAwait(false); + /*if (data.UserId == CurrentUser.Id) { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } - - await TimedInvokeAsync(_integrationDeleted, nameof(IntegrationDeleted), guild, data.Id, data.ApplicationID).ConfigureAwait(false); + var _ = guild.FinishJoinAudioChannel().ConfigureAwait(false); + }*/ } else { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + 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; } } - break; - #endregion - - #region Users - case "USER_UPDATE": + else { - await _gatewayLogger.DebugAsync("Received Dispatch (USER_UPDATE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - if (data.Id == CurrentUser.Id) + var groupChannel = GetChannel(data.ChannelId.Value) as SocketGroupChannel; + if (groupChannel == null) + { + await UnknownChannelAsync(type, data.ChannelId.Value).ConfigureAwait(false); + return; + } + if (data.ChannelId != null) { - var before = CurrentUser.Clone(); - CurrentUser.Update(State, data); - await TimedInvokeAsync(_selfUpdatedEvent, nameof(CurrentUserUpdated), before, CurrentUser).ConfigureAwait(false); + before = groupChannel.GetVoiceState(data.UserId)?.Clone() ?? SocketVoiceState.Default; + after = groupChannel.AddOrUpdateVoiceState(State, data); } else { - await _gatewayLogger.WarningAsync("Received USER_UPDATE for wrong user.").ConfigureAwait(false); + 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; } } - break; - #endregion - #region Voice - case "VOICE_STATE_UPDATE": + if (user is SocketGuildUser guildUser && data.ChannelId.HasValue) { - 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); - } + SocketStageChannel stage = guildUser.Guild.GetStageChannel(data.ChannelId.Value); - //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 + if (stage != null && before.VoiceChannel != null && after.VoiceChannel != null) { - var groupChannel = GetChannel(data.ChannelId.Value) as SocketGroupChannel; - if (groupChannel == null) + if (!before.RequestToSpeakTimestamp.HasValue && after.RequestToSpeakTimestamp.HasValue) { - await UnknownChannelAsync(type, data.ChannelId.Value).ConfigureAwait(false); + await TimedInvokeAsync(_requestToSpeak, nameof(RequestToSpeak), stage, guildUser); 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) + if (before.IsSuppressed && !after.IsSuppressed) { - await UnknownChannelUserAsync(type, data.UserId, groupChannel.Id).ConfigureAwait(false); + await TimedInvokeAsync(_speakerAdded, nameof(SpeakerAdded), stage, guildUser); 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.IsSuppressed && after.IsSuppressed) { - 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(_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)); + 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 voiceServer = new SocketVoiceServer(cachedGuild, data.Endpoint, data.Token); - await TimedInvokeAsync(_voiceServerUpdatedEvent, nameof(UserVoiceStateUpdated), voiceServer).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)); - if (isCached) - { - var endpoint = data.Endpoint; + var voiceServer = new SocketVoiceServer(cachedGuild, data.Endpoint, data.Token); + await TimedInvokeAsync(_voiceServerUpdatedEvent, nameof(UserVoiceStateUpdated), voiceServer).ConfigureAwait(false); - //Only strip out the port if the endpoint contains it - var portBegin = endpoint.LastIndexOf(':'); - if (portBegin > 0) - endpoint = endpoint.Substring(0, portBegin); + if (isCached) + { + var endpoint = data.Endpoint; - var _ = guild.FinishConnectAudio(endpoint, data.Token).ConfigureAwait(false); - } - else - { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - } + //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); + 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 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 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); + var before = (string)channel?.Status?.Clone(); + var after = data.Status; + channel?.UpdateVoiceStatus(data.Status); - await TimedInvokeAsync(_voiceChannelStatusUpdated, nameof(VoiceChannelStatusUpdated), channelCacheable, before, after); - } + await TimedInvokeAsync(_voiceChannelStatusUpdated, nameof(VoiceChannelStatusUpdated), channelCacheable, before, after); + } break; - #endregion + #endregion - #region Invites - case "INVITE_CREATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (INVITE_CREATE)").ConfigureAwait(false); + #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 data = (payload as JToken).ToObject(_serializer); + if (State.GetChannel(data.ChannelId) is SocketGuildChannel channel) + { + var guild = channel.Guild; + if (!guild.IsSynced) { - var guild = channel.Guild; - if (!guild.IsSynced) - { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } + 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; + 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; + 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); + 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; - } + await TimedInvokeAsync(_inviteCreatedEvent, nameof(InviteCreated), invite).ConfigureAwait(false); } - break; - case "INVITE_DELETE": + else { - 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; - } + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); + return; } + } break; - #endregion + case "INVITE_DELETE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (INVITE_DELETE)").ConfigureAwait(false); - #region Interactions - case "INTERACTION_CREATE": + var data = (payload as JToken).ToObject(_serializer); + if (State.GetChannel(data.ChannelId) is SocketGuildChannel channel) { - 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) + var guild = channel.Guild; + if (!guild.IsSynced) { await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; } - 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)); + await TimedInvokeAsync(_inviteDeletedEvent, nameof(InviteDeleted), channel, data.Code).ConfigureAwait(false); + } + else + { + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); + return; + } + } + break; + #endregion - SocketChannel channel = null; - if (data.ChannelId.IsSpecified) - { - channel = State.GetChannel(data.ChannelId.Value); + #region Interactions + case "INTERACTION_CREATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (INTERACTION_CREATE)").ConfigureAwait(false); - if (channel == null) - { - if (!data.GuildId.IsSpecified) // assume it is a DM - { - channel = CreateDMChannel(data.ChannelId.Value, user, State); - } + var data = (payload as JToken).ToObject(_serializer); - // 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 guild = data.GuildId.IsSpecified ? GetGuild(data.GuildId.Value) : null; - var interaction = SocketInteraction.Create(this, data, channel as ISocketMessageChannel, user); + if (guild != null && !guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + } - await TimedInvokeAsync(_interactionCreatedEvent, nameof(InteractionCreated), interaction).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)); - 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": + SocketChannel channel = null; + if (data.ChannelId.IsSpecified) { - await _gatewayLogger.DebugAsync("Received Dispatch (APPLICATION_COMMAND_CREATE)").ConfigureAwait(false); + channel = State.GetChannel(data.ChannelId.Value); - var data = (payload as JToken).ToObject(_serializer); - - if (data.GuildId.IsSpecified) + if (channel == null) { - var guild = State.GetGuild(data.GuildId.Value); - if (guild == null) + if (!data.GuildId.IsSpecified) // assume it is a DM { - await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); - return; + 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 applicationCommand = SocketApplicationCommand.Create(this, data); + var interaction = SocketInteraction.Create(this, data, channel as ISocketMessageChannel, user); - State.AddCommand(applicationCommand); + await TimedInvokeAsync(_interactionCreatedEvent, nameof(InteractionCreated), interaction).ConfigureAwait(false); - await TimedInvokeAsync(_applicationCommandCreated, nameof(ApplicationCommandCreated), applicationCommand).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_UPDATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (APPLICATION_COMMAND_UPDATE)").ConfigureAwait(false); + case "APPLICATION_COMMAND_CREATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (APPLICATION_COMMAND_CREATE)").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); - if (data.GuildId.IsSpecified) + if (data.GuildId.IsSpecified) + { + var guild = State.GetGuild(data.GuildId.Value); + if (guild == null) { - var guild = State.GetGuild(data.GuildId.Value); - if (guild == null) - { - await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); - return; - } + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + return; } + } - var applicationCommand = SocketApplicationCommand.Create(this, data); + var applicationCommand = SocketApplicationCommand.Create(this, data); - State.AddCommand(applicationCommand); + State.AddCommand(applicationCommand); - await TimedInvokeAsync(_applicationCommandUpdated, nameof(ApplicationCommandUpdated), applicationCommand).ConfigureAwait(false); - } + await TimedInvokeAsync(_applicationCommandCreated, nameof(ApplicationCommandCreated), applicationCommand).ConfigureAwait(false); + } break; - case "APPLICATION_COMMAND_DELETE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (APPLICATION_COMMAND_DELETE)").ConfigureAwait(false); + case "APPLICATION_COMMAND_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (APPLICATION_COMMAND_UPDATE)").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); - if (data.GuildId.IsSpecified) + if (data.GuildId.IsSpecified) + { + var guild = State.GetGuild(data.GuildId.Value); + if (guild == null) { - var guild = State.GetGuild(data.GuildId.Value); - if (guild == null) - { - await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); - return; - } + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + return; } + } - var applicationCommand = SocketApplicationCommand.Create(this, data); + var applicationCommand = SocketApplicationCommand.Create(this, data); - State.RemoveCommand(applicationCommand.Id); + State.AddCommand(applicationCommand); - await TimedInvokeAsync(_applicationCommandDeleted, nameof(ApplicationCommandDeleted), applicationCommand).ConfigureAwait(false); - } + await TimedInvokeAsync(_applicationCommandUpdated, nameof(ApplicationCommandUpdated), applicationCommand).ConfigureAwait(false); + } break; - #endregion - - #region Threads - case "THREAD_CREATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_CREATE)").ConfigureAwait(false); + case "APPLICATION_COMMAND_DELETE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (APPLICATION_COMMAND_DELETE)").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); + 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); + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); return; } + } - SocketThreadChannel threadChannel = null; + var applicationCommand = SocketApplicationCommand.Create(this, data); - if ((threadChannel = guild.ThreadChannels.FirstOrDefault(x => x.Id == data.Id)) != null) - { - threadChannel.Update(State, data); + State.RemoveCommand(applicationCommand.Id); - 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(_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); - await TimedInvokeAsync(_threadCreated, nameof(ThreadCreated), threadChannel).ConfigureAwait(false); + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId.Value); + return; } - break; - case "THREAD_UPDATE": + SocketThreadChannel threadChannel = null; + + if ((threadChannel = guild.ThreadChannels.FirstOrDefault(x => x.Id == data.Id)) != null) { - await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_UPDATE)").ConfigureAwait(false); + threadChannel.Update(State, data); - var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId.Value); - if (guild == null) - { - await UnknownGuildAsync(type, data.GuildId.Value); - return; - } + 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); + } - 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)); + await TimedInvokeAsync(_threadCreated, nameof(ThreadCreated), threadChannel).ConfigureAwait(false); + } - if (threadChannel != null) - { - threadChannel.Update(State, data); + break; + case "THREAD_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_UPDATE)").ConfigureAwait(false); - 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); - } + var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.GuildId.Value); + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId.Value); + return; + } - if (!(guild?.IsSynced ?? true)) - { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - 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)); - await TimedInvokeAsync(_threadUpdated, nameof(ThreadUpdated), before, threadChannel).ConfigureAwait(false); + 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); + case "THREAD_DELETE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_DELETE)").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId.Value); + var guild = State.GetGuild(data.GuildId.Value); - if (guild == null) - { - await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); - return; - } + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + return; + } - var thread = (SocketThreadChannel)guild.RemoveChannel(State, data.Id); + var thread = (SocketThreadChannel)guild.RemoveChannel(State, data.Id); - var cacheable = new Cacheable(thread, data.Id, thread != null, null); + var cacheable = new Cacheable(thread, data.Id, thread != null, null); - await TimedInvokeAsync(_threadDeleted, nameof(ThreadDeleted), cacheable).ConfigureAwait(false); - } + await TimedInvokeAsync(_threadDeleted, nameof(ThreadDeleted), cacheable).ConfigureAwait(false); + } break; - case "THREAD_LIST_SYNC": - { - await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_LIST_SYNC)").ConfigureAwait(false); + case "THREAD_LIST_SYNC": + { + await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_LIST_SYNC)").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); + var guild = State.GetGuild(data.GuildId); - if (guild == null) + 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) { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; + entity = (SocketThreadChannel)guild.AddChannel(State, thread); } - - foreach (var thread in data.Threads) + else { - var entity = guild.ThreadChannels.FirstOrDefault(x => x.Id == thread.Id); - - if (entity == null) - { - entity = (SocketThreadChannel)guild.AddChannel(State, thread); - } - else - { - entity.Update(State, thread); - } + entity.Update(State, thread); + } - foreach (var member in data.Members.Where(x => x.Id.Value == entity.Id)) - { - var guildMember = guild.GetUser(member.Id.Value); + foreach (var member in data.Members.Where(x => x.Id.Value == entity.Id)) + { + var guildMember = guild.GetUser(member.Id.Value); - entity.AddOrUpdateThreadMember(member, guildMember); - } + 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); + case "THREAD_MEMBER_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_MEMBER_UPDATE)").ConfigureAwait(false); - var thread = (SocketThreadChannel)State.GetChannel(data.Id.Value); + var data = (payload as JToken).ToObject(_serializer); - if (thread == null) - { - await UnknownChannelAsync(type, data.Id.Value); - return; - } + var thread = (SocketThreadChannel)State.GetChannel(data.Id.Value); - thread.AddOrUpdateThreadMember(data, thread.Guild.CurrentUser); + 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); + case "THREAD_MEMBERS_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_MEMBERS_UPDATE)").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); + var guild = State.GetGuild(data.GuildId); - if (guild == null) - { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; - } + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } - var thread = (SocketThreadChannel)guild.GetChannel(data.Id); + var thread = (SocketThreadChannel)guild.GetChannel(data.Id); - if (thread == null) - { - await UnknownChannelAsync(type, data.Id); - return; - } + if (thread == null) + { + await UnknownChannelAsync(type, data.Id); + return; + } - IReadOnlyCollection leftUsers = null; - IReadOnlyCollection joinUsers = null; + IReadOnlyCollection leftUsers = null; + IReadOnlyCollection joinUsers = null; - if (data.RemovedMemberIds.IsSpecified) - { - leftUsers = thread.RemoveUsers(data.RemovedMemberIds.Value); - } + if (data.RemovedMemberIds.IsSpecified) + { + leftUsers = thread.RemoveUsers(data.RemovedMemberIds.Value); + } - if (data.AddedMembers.IsSpecified) + if (data.AddedMembers.IsSpecified) + { + List newThreadMembers = new List(); + foreach (var threadMember in data.AddedMembers.Value) { - List newThreadMembers = new List(); - foreach (var threadMember in data.AddedMembers.Value) - { - SocketGuildUser guildMember; + SocketGuildUser guildMember; - guildMember = guild.GetUser(threadMember.UserId.Value); + 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 (guildMember == null) + { + await UnknownGuildUserAsync("THREAD_MEMBERS_UPDATE", threadMember.UserId.Value, guild.Id); } - - if (newThreadMembers.Any()) - joinUsers = newThreadMembers.ToImmutableArray(); + else + newThreadMembers.Add(thread.AddOrUpdateThreadMember(threadMember, guildMember)); } - if (leftUsers != null) + if (newThreadMembers.Any()) + joinUsers = newThreadMembers.ToImmutableArray(); + } + + if (leftUsers != null) + { + foreach (var threadUser in leftUsers) { - foreach (var threadUser in leftUsers) - { - await TimedInvokeAsync(_threadMemberLeft, nameof(ThreadMemberLeft), threadUser).ConfigureAwait(false); - } + await TimedInvokeAsync(_threadMemberLeft, nameof(ThreadMemberLeft), threadUser).ConfigureAwait(false); } + } - if (joinUsers != null) + if (joinUsers != null) + { + foreach (var threadUser in joinUsers) { - foreach (var threadUser in joinUsers) - { - await TimedInvokeAsync(_threadMemberJoined, nameof(ThreadMemberJoined), threadUser).ConfigureAwait(false); - } + await TimedInvokeAsync(_threadMemberJoined, nameof(ThreadMemberJoined), threadUser).ConfigureAwait(false); } } + } break; - #endregion + #endregion - #region Stage Channels - case "STAGE_INSTANCE_CREATE" or "STAGE_INSTANCE_UPDATE" or "STAGE_INSTANCE_DELETE": - { - await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false); + #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 data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); + var guild = State.GetGuild(data.GuildId); - if (guild == null) - { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; - } + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } - var stageChannel = guild.GetStageChannel(data.ChannelId); + var stageChannel = guild.GetStageChannel(data.ChannelId); - if (stageChannel == null) - { - await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); - return; - } + if (stageChannel == null) + { + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); + return; + } - SocketStageChannel before = type == "STAGE_INSTANCE_UPDATE" ? stageChannel.Clone() : null; + SocketStageChannel before = type == "STAGE_INSTANCE_UPDATE" ? stageChannel.Clone() : null; - stageChannel.Update(data, type == "STAGE_INSTANCE_CREATE"); + 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; - } + 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 + #endregion - #region Guild Scheduled Events - case "GUILD_SCHEDULED_EVENT_CREATE": - { - await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false); + #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 data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); + var guild = State.GetGuild(data.GuildId); - if (guild == null) - { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; - } + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } - var newEvent = guild.AddOrUpdateEvent(data); + var newEvent = guild.AddOrUpdateEvent(data); - await TimedInvokeAsync(_guildScheduledEventCreated, nameof(GuildScheduledEventCreated), newEvent).ConfigureAwait(false); - } + await TimedInvokeAsync(_guildScheduledEventCreated, nameof(GuildScheduledEventCreated), newEvent).ConfigureAwait(false); + } break; - case "GUILD_SCHEDULED_EVENT_UPDATE": - { - await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false); + case "GUILD_SCHEDULED_EVENT_UPDATE": + { + await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); + var guild = State.GetGuild(data.GuildId); - if (guild == null) - { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; - } + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } - var before = guild.GetEvent(data.Id)?.Clone(); + var before = guild.GetEvent(data.Id)?.Clone(); - var beforeCacheable = new Cacheable(before, data.Id, before != null, () => Task.FromResult((SocketGuildEvent)null)); + var beforeCacheable = new Cacheable(before, data.Id, before != null, () => Task.FromResult((SocketGuildEvent)null)); - var after = guild.AddOrUpdateEvent(data); + 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); + if ((before != null ? before.Status != GuildScheduledEventStatus.Completed : true) && data.Status == GuildScheduledEventStatus.Completed) + { + await TimedInvokeAsync(_guildScheduledEventCompleted, nameof(GuildScheduledEventCompleted), after).ConfigureAwait(false); } - break; - case "GUILD_SCHEDULED_EVENT_DELETE": + else if ((before != null ? before.Status != GuildScheduledEventStatus.Active : false) && data.Status == GuildScheduledEventStatus.Active) { - 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); + await TimedInvokeAsync(_guildScheduledEventStarted, nameof(GuildScheduledEventStarted), after).ConfigureAwait(false); } + else + await TimedInvokeAsync(_guildScheduledEventUpdated, nameof(GuildScheduledEventUpdated), beforeCacheable, after).ConfigureAwait(false); + } break; - case "GUILD_SCHEDULED_EVENT_USER_ADD" or "GUILD_SCHEDULED_EVENT_USER_REMOVE": - { - await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false); + case "GUILD_SCHEDULED_EVENT_DELETE": + { + await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); + var guild = State.GetGuild(data.GuildId); - if (guild == null) - { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; - } + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } - var guildEvent = guild.GetEvent(data.EventId); + var guildEvent = guild.RemoveEvent(data.Id) ?? SocketGuildEvent.Create(this, guild, data); - if (guildEvent == null) - { - await UnknownGuildEventAsync(type, data.EventId, data.GuildId).ConfigureAwait(false); - return; - } + 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 user = (SocketUser)guild.GetUser(data.UserId) ?? State.GetUser(data.UserId); + var data = (payload as JToken).ToObject(_serializer); - var cacheableUser = new Cacheable(user, data.UserId, user != null, () => Rest.GetUserAsync(data.UserId)); + var guild = State.GetGuild(data.GuildId); - 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; - } + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; } - break; - - #endregion - #region Webhooks + var guildEvent = guild.GetEvent(data.EventId); - case "WEBHOOKS_UPDATE": + if (guildEvent == null) { - var data = (payload as JToken).ToObject(_serializer); - type = "WEBHOOKS_UPDATE"; - await _gatewayLogger.DebugAsync("Received Dispatch (WEBHOOKS_UPDATE)").ConfigureAwait(false); + await UnknownGuildEventAsync(type, data.EventId, data.GuildId).ConfigureAwait(false); + return; + } + + var user = (SocketUser)guild.GetUser(data.UserId) ?? State.GetUser(data.UserId); - var guild = State.GetGuild(data.GuildId); - var channel = State.GetChannel(data.ChannelId); + var cacheableUser = new Cacheable(user, data.UserId, user != null, () => Rest.GetUserAsync(data.UserId)); - await TimedInvokeAsync(_webhooksUpdated, nameof(WebhooksUpdated), guild, channel); + 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 + #endregion - #region Audit Logs + #region Webhooks - 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); + 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 auditLog = SocketAuditLogEntry.Create(this, data); - guild.AddAuditLog(auditLog); + var guild = State.GetGuild(data.GuildId); + var channel = State.GetChannel(data.ChannelId); - await TimedInvokeAsync(_auditLogCreated, nameof(AuditLogCreated), auditLog, guild); - } + await TimedInvokeAsync(_webhooksUpdated, nameof(WebhooksUpdated), guild, channel); + } break; - #endregion - #region Auto Moderation + #endregion - case "AUTO_MODERATION_RULE_CREATE": - { - var data = (payload as JToken).ToObject(_serializer); + #region Audit Logs - var guild = State.GetGuild(data.GuildId); + 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 rule = guild.AddOrUpdateAutoModRule(data); + var guild = State.GetGuild(data.GuildId); + var auditLog = SocketAuditLogEntry.Create(this, data); + guild.AddAuditLog(auditLog); - await TimedInvokeAsync(_autoModRuleCreated, nameof(AutoModRuleCreated), rule); - } + await TimedInvokeAsync(_auditLogCreated, nameof(AuditLogCreated), auditLog, guild); + } break; + #endregion - case "AUTO_MODERATION_RULE_UPDATE": - { - var data = (payload as JToken).ToObject(_serializer); + #region Auto Moderation + + case "AUTO_MODERATION_RULE_CREATE": + { + var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); + 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)); + var rule = guild.AddOrUpdateAutoModRule(data); - await TimedInvokeAsync(_autoModRuleUpdated, nameof(AutoModRuleUpdated), cacheableBefore, guild.AddOrUpdateAutoModRule(data)); - } + await TimedInvokeAsync(_autoModRuleCreated, nameof(AutoModRuleCreated), rule); + } break; - case "AUTO_MODERATION_RULE_DELETE": - { - var data = (payload as JToken).ToObject(_serializer); + case "AUTO_MODERATION_RULE_UPDATE": + { + var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); + var guild = State.GetGuild(data.GuildId); - var rule = guild.RemoveAutoModRule(data); + 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(_autoModRuleDeleted, nameof(AutoModRuleDeleted), rule); - } + await TimedInvokeAsync(_autoModRuleUpdated, nameof(AutoModRuleUpdated), cacheableBefore, guild.AddOrUpdateAutoModRule(data)); + } break; - case "AUTO_MODERATION_ACTION_EXECUTION": - { - var data = (payload as JToken).ToObject(_serializer); + case "AUTO_MODERATION_RULE_DELETE": + { + 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 guild = State.GetGuild(data.GuildId); + var rule = guild.RemoveAutoModRule(data); - var member = guild.GetUser(data.UserId); + await TimedInvokeAsync(_autoModRuleDeleted, nameof(AutoModRuleDeleted), rule); + } + break; - 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); - } - ); + 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; + }); - 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; + }); - IUserMessage cachedMsg = null; - if (data.MessageId.IsSpecified) - cachedMsg = channel?.GetCachedMessage(data.MessageId.GetValueOrDefault(0)) as IUserMessage; + 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; - 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; - }); + #endregion - var cachedRule = guild.GetAutoModRule(data.RuleId); + #region App Subscriptions - var cacheableRule = new Cacheable(cachedRule, - data.RuleId, - cachedRule is not null, - async () => await guild.GetAutoModRuleAsync(data.RuleId)); + case "ENTITLEMENT_CREATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (ENTITLEMENT_CREATE)").ConfigureAwait(false); + var data = (payload as JToken).ToObject(_serializer); - 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); + var entitlement = SocketEntitlement.Create(this, data); + State.AddEntitlement(data.Id, entitlement); - await TimedInvokeAsync(_autoModActionExecuted, nameof(AutoModActionExecuted), guild, action, eventData); - } + await TimedInvokeAsync(_entitlementCreated, nameof(EntitlementCreated), entitlement); + } break; - #endregion + case "ENTITLEMENT_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (ENTITLEMENT_UPDATE)").ConfigureAwait(false); + var data = (payload as JToken).ToObject(_serializer); - #region App Subscriptions + var entitlement = State.GetEntitlement(data.Id); - case "ENTITLEMENT_CREATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (ENTITLEMENT_CREATE)").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); + var cacheableBefore = new Cacheable(entitlement?.Clone(), data.Id, + entitlement is not null, () => null); - var entitlement = SocketEntitlement.Create(this, data); + if (entitlement is null) + { + entitlement = SocketEntitlement.Create(this, data); State.AddEntitlement(data.Id, entitlement); - - await TimedInvokeAsync(_entitlementCreated, nameof(EntitlementCreated), entitlement); } + else + { + entitlement.Update(data); + } + + await TimedInvokeAsync(_entitlementUpdated, nameof(EntitlementUpdated), cacheableBefore, entitlement); + } break; - case "ENTITLEMENT_UPDATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (ENTITLEMENT_UPDATE)").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); + case "ENTITLEMENT_DELETE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (ENTITLEMENT_DELETE)").ConfigureAwait(false); + var data = (payload as JToken).ToObject(_serializer); - var entitlement = State.GetEntitlement(data.Id); + var entitlement = State.RemoveEntitlement(data.Id); - var cacheableBefore = new Cacheable(entitlement?.Clone(), data.Id, - entitlement is not null, () => null); + if (entitlement is null) + entitlement = SocketEntitlement.Create(this, data); + else + entitlement.Update(data); - if (entitlement is null) - { - entitlement = SocketEntitlement.Create(this, data); - State.AddEntitlement(data.Id, entitlement); - } - else - { - entitlement.Update(data); - } + var cacheableEntitlement = new Cacheable(entitlement, data.Id, + entitlement is not null, () => null); - await TimedInvokeAsync(_entitlementUpdated, nameof(EntitlementUpdated), cacheableBefore, entitlement); - } + await TimedInvokeAsync(_entitlementDeleted, nameof(EntitlementDeleted), cacheableEntitlement); + } break; - case "ENTITLEMENT_DELETE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (ENTITLEMENT_DELETE)").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); + case "SUBSCRIPTION_CREATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (SUBSCRIPTION_CREATE)").ConfigureAwait(false); + var data = (payload as JToken).ToObject(_serializer); - var entitlement = State.RemoveEntitlement(data.Id); + var subscription = SocketSubscription.Create(this, data); + State.AddSubscription(data.Id, subscription); - if (entitlement is null) - entitlement = SocketEntitlement.Create(this, data); - else - entitlement.Update(data); + await TimedInvokeAsync(_subscriptionCreated, nameof(SubscriptionCreated), subscription); + } + break; - var cacheableEntitlement = new Cacheable(entitlement, data.Id, - entitlement is not null, () => null); + case "SUBSCRIPTION_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (SUBSCRIPTION_UPDATE)").ConfigureAwait(false); + var data = (payload as JToken).ToObject(_serializer); - await TimedInvokeAsync(_entitlementDeleted, nameof(EntitlementDeleted), cacheableEntitlement); - } - break; + var subscription = State.GetSubscription(data.Id); - case "SUBSCRIPTION_CREATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (SUBSCRIPTION_CREATE)").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); + var cacheableBefore = new Cacheable(subscription?.Clone(), data.Id, + hasValue: subscription is not null, () => null); - var subscription = SocketSubscription.Create(this, data); + if (subscription is null) + { + subscription = SocketSubscription.Create(this, data); State.AddSubscription(data.Id, subscription); - - await TimedInvokeAsync(_subscriptionCreated, nameof(SubscriptionCreated), subscription); } + else + { + subscription.Update(data); + } + + await TimedInvokeAsync(_subscriptionUpdated, nameof(SubscriptionUpdated), cacheableBefore, subscription); + } break; - case "SUBSCRIPTION_UPDATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (SUBSCRIPTION_UPDATE)").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); + case "SUBSCRIPTION_DELETE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (SUBSCRIPTION_DELETE)").ConfigureAwait(false); + var data = (payload as JToken).ToObject(_serializer); - var subscription = State.GetSubscription(data.Id); + var subscription = State.RemoveSubscription(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); + else + subscription.Update(data); - if (subscription is null) - { - subscription = SocketSubscription.Create(this, data); - State.AddSubscription(data.Id, subscription); - } - else - { - subscription.Update(data); - } + var cacheableSubscription = new Cacheable(subscription, data.Id, + subscription is not null, () => null); - await TimedInvokeAsync(_subscriptionUpdated, nameof(SubscriptionUpdated), cacheableBefore, subscription); - } + await TimedInvokeAsync(_subscriptionDeleted, nameof(SubscriptionDeleted), cacheableSubscription); + } break; - case "SUBSCRIPTION_DELETE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (SUBSCRIPTION_DELETE)").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); + #endregion - var subscription = State.RemoveSubscription(data.Id); + #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 - 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); - - await TimedInvokeAsync(_unknownDispatchReceived, nameof(UnknownDispatchReceived), type, (payload as JToken)); - break; - #endregion - } - break; - } - catch (Exception ex) - { - activity?.AddExceptionToActivity(ex); - SocketMeter.RecordSocketEventException(ex, seq, type, this); - throw; - } - finally - { - watch.Stop(); - SocketMeter.RecordSocketEvent(watch.Elapsed, seq, type, this); + #region Others + default: + if (!SuppressUnknownDispatchWarnings) + await _gatewayLogger.WarningAsync($"Unknown Dispatch ({type})").ConfigureAwait(false); - activity?.Dispose(); + await TimedInvokeAsync(_unknownDispatchReceived, nameof(UnknownDispatchReceived), type, (payload as JToken)); + break; + #endregion } + break; default: await _gatewayLogger.WarningAsync($"Unknown OpCode ({opCode})").ConfigureAwait(false); break; @@ -2545,8 +2529,17 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty ex.Data["payload_data"] = (payload as JToken).ToString(); } + activity?.AddExceptionToActivity(ex); + SocketMeter.RecordSocketEventException(ex, opCode, type, this); + await _gatewayLogger.ErrorAsync($"Error handling {opCode}{(type != null ? $" ({type})" : "")}", ex).ConfigureAwait(false); } - } + finally + { + watch.Stop(); + SocketMeter.RecordSocketEvent(watch.Elapsed, opCode, type, this); + activity?.Dispose(); + } + } } From f3ec40d55a1a6e5157630276a9ecc9f767cb1102 Mon Sep 17 00:00:00 2001 From: Suiram1701 <110390261+Suiram1701@users.noreply.github.com> Date: Wed, 2 Jul 2025 15:35:50 +0200 Subject: [PATCH 12/12] Added tracing for received audio events --- .../Audio/AudioClient.cs | 8 ++++- .../Diagnostics/AudioActivity.cs | 32 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 src/Discord.Net.WebSocket/Diagnostics/AudioActivity.cs diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.cs index 8bb6a603a0..1ed7150b88 100644 --- a/src/Discord.Net.WebSocket/Audio/AudioClient.cs +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.cs @@ -331,7 +331,9 @@ private async Task ProcessMessageAsync(VoiceOpCode opCode, object payload) { _lastMessageTime = Environment.TickCount; + var activity = AudioActivity.StartEventReceivedActivity(opCode, this); var watch = Stopwatch.StartNew(); + try { switch (opCode) @@ -427,13 +429,17 @@ private async Task ProcessMessageAsync(VoiceOpCode opCode, object payload) } catch (Exception ex) { - await _audioLogger.ErrorAsync($"Error handling {opCode}", ex).ConfigureAwait(false); + activity?.AddExceptionToActivity(ex); AudioMeter.RecordSocketEventException(ex, opCode, this); + + await _audioLogger.ErrorAsync($"Error handling {opCode}", ex).ConfigureAwait(false); } finally { watch.Stop(); AudioMeter.RecordSocketEventReceived(watch.Elapsed, opCode, this); + + activity?.Dispose(); } } private async Task ProcessPacketAsync(byte[] packet) diff --git a/src/Discord.Net.WebSocket/Diagnostics/AudioActivity.cs b/src/Discord.Net.WebSocket/Diagnostics/AudioActivity.cs new file mode 100644 index 0000000000..63bfb790a7 --- /dev/null +++ b/src/Discord.Net.WebSocket/Diagnostics/AudioActivity.cs @@ -0,0 +1,32 @@ +using Discord.Audio; +using Discord.API.Voice; +using System; + +#if NET5_0_OR_GREATER +using System.Collections.Generic; +using System.Diagnostics; +#endif + +namespace Discord.WebSocket.Diagnostics +{ + public static class AudioActivity + { +#if NET5_0_OR_GREATER + private static readonly ActivitySource _source = new("Discord.Net.Audio", typeof(DiagnosticTags).Assembly.GetName().Version!.ToString()); + + internal static Activity StartEventReceivedActivity(VoiceOpCode opCode, AudioClient client) + { + Activity.Current = null; // This activity doesn't have a parent so it have to be explicitly set + + IEnumerable> tags = [ + .. DiagnosticTags.CreateAudioClientTags(client), + .. DiagnosticTags.CreateAudioEventTags(opCode) + ]; + return _source.StartActivity($"process {opCode}", ActivityKind.Consumer, null, tags: tags); + } + +#else + internal static IDisposable StartEventReceivedActivity(VoiceOpCode opCode, AudioClient client) => null; +#endif + } +}