diff --git a/DisCatSharp/Clients/BaseDiscordClient.cs b/DisCatSharp/Clients/BaseDiscordClient.cs
index 9cce61e61..2a9949032 100644
--- a/DisCatSharp/Clients/BaseDiscordClient.cs
+++ b/DisCatSharp/Clients/BaseDiscordClient.cs
@@ -1,354 +1,358 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2023 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#pragma warning disable CS0618
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Threading.Tasks;
using DisCatSharp.Entities;
using DisCatSharp.Enums;
using DisCatSharp.Exceptions;
using DisCatSharp.Net;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
+using Sentry;
+
namespace DisCatSharp;
///
/// Represents a common base for various Discord Client implementations.
///
public abstract class BaseDiscordClient : IDisposable
{
///
/// Gets the api client.
///
internal protected DiscordApiClient ApiClient { get; }
+ internal SentryClient Sentry { get; set; }
+
///
/// Gets the configuration.
///
internal protected DiscordConfiguration Configuration { get; }
///
/// Gets the instance of the logger for this client.
///
public ILogger Logger { get; internal set; }
///
/// Gets the string representing the version of bot lib.
///
public string VersionString { get; }
///
/// Gets the bot library name.
///
public string BotLibrary { get; }
[Obsolete("Use GetLibraryDeveloperTeamAsync")]
public DisCatSharpTeam LibraryDeveloperTeamAsync
=> this.GetLibraryDevelopmentTeamAsync().Result;
///
/// Gets the current user.
///
public DiscordUser CurrentUser { get; internal set; }
///
/// Gets the current application.
///
public DiscordApplication CurrentApplication { get; internal set; }
///
/// Exposes a Http Client for custom operations.
///
public HttpClient RestClient { get; internal set; }
///
/// Gets the cached guilds for this client.
///
public abstract IReadOnlyDictionary Guilds { get; }
///
/// Gets the cached users for this client.
///
public ConcurrentDictionary UserCache { get; internal set; }
///
/// Gets the service provider.
/// This allows passing data around without resorting to static members.
/// Defaults to null.
///
internal IServiceProvider ServiceProvider { get; set; }
///
/// Gets the list of available voice regions. Note that this property will not contain VIP voice regions.
///
public IReadOnlyDictionary VoiceRegions
=> this.VoiceRegionsLazy.Value;
///
/// Gets the list of available voice regions. This property is meant as a way to modify .
///
protected internal ConcurrentDictionary InternalVoiceRegions { get; set; }
internal Lazy> VoiceRegionsLazy;
///
/// Initializes this Discord API client.
///
/// Configuration for this client.
protected BaseDiscordClient(DiscordConfiguration config)
{
this.Configuration = new DiscordConfiguration(config);
this.ServiceProvider = config.ServiceProvider;
if (this.ServiceProvider != null)
{
this.Configuration.LoggerFactory ??= config.ServiceProvider.GetService();
this.Logger = config.ServiceProvider.GetService>();
}
if (this.Configuration.LoggerFactory == null)
{
this.Configuration.LoggerFactory = new DefaultLoggerFactory();
this.Configuration.LoggerFactory.AddProvider(new DefaultLoggerProvider(this));
if (this.Configuration.EnableSentry)
- this.Configuration.LoggerFactory.AddSentry(x => x.DiagnosticLevel = Sentry.SentryLevel.Error);
+ this.Configuration.LoggerFactory.AddSentry(x => x.DiagnosticLevel = SentryLevel.Error);
}
this.Logger ??= this.Configuration.LoggerFactory.CreateLogger();
this.ApiClient = new DiscordApiClient(this);
this.UserCache = new ConcurrentDictionary();
this.InternalVoiceRegions = new ConcurrentDictionary();
this.VoiceRegionsLazy = new Lazy>(() => new ReadOnlyDictionary(this.InternalVoiceRegions));
this.RestClient = new();
this.RestClient.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", Utilities.GetUserAgent());
this.RestClient.DefaultRequestHeaders.TryAddWithoutValidation("X-Discord-Locale", this.Configuration.Locale);
var a = typeof(DiscordClient).GetTypeInfo().Assembly;
var iv = a.GetCustomAttribute();
if (iv != null)
{
this.VersionString = iv.InformationalVersion;
}
else
{
var v = a.GetName().Version;
var vs = v.ToString(3);
if (v.Revision > 0)
this.VersionString = $"{vs}, CI build {v.Revision}";
}
this.BotLibrary = "DisCatSharp";
}
///
/// Gets the current API application.
///
public async Task GetCurrentApplicationAsync()
{
var tapp = await this.ApiClient.GetCurrentApplicationOauth2InfoAsync().ConfigureAwait(false);
var app = new DiscordApplication
{
Discord = this,
Id = tapp.Id,
Name = tapp.Name,
Description = tapp.Description,
Summary = tapp.Summary,
IconHash = tapp.IconHash,
RpcOrigins = tapp.RpcOrigins != null ? new ReadOnlyCollection(tapp.RpcOrigins) : null,
Flags = tapp.Flags,
IsHook = tapp.IsHook,
Type = tapp.Type,
PrivacyPolicyUrl = tapp.PrivacyPolicyUrl,
TermsOfServiceUrl = tapp.TermsOfServiceUrl,
CustomInstallUrl = tapp.CustomInstallUrl,
InstallParams = tapp.InstallParams,
RoleConnectionsVerificationUrl = tapp.RoleConnectionsVerificationUrl,
Tags = (tapp.Tags ?? Enumerable.Empty()).ToArray()
};
if (tapp.Team == null)
{
app.Owners = new List(new[] { new DiscordUser(tapp.Owner) });
app.Team = null;
app.TeamName = null;
}
else
{
app.Team = new DiscordTeam(tapp.Team);
var members = tapp.Team.Members
.Select(x => new DiscordTeamMember(x) { TeamId = app.Team.Id, TeamName = app.Team.Name, User = new DiscordUser(x.User) })
.ToArray();
var owners = members
.Where(x => x.MembershipStatus == DiscordTeamMembershipStatus.Accepted)
.Select(x => x.User)
.ToArray();
app.Owners = new List(owners);
app.Team.Owner = owners.FirstOrDefault(x => x.Id == tapp.Team.OwnerId);
app.Team.Members = new List(members);
app.TeamName = app.Team.Name;
}
app.GuildId = tapp.GuildId.ValueOrDefault();
app.Slug = tapp.Slug.ValueOrDefault();
app.PrimarySkuId = tapp.PrimarySkuId.ValueOrDefault();
app.VerifyKey = tapp.VerifyKey.ValueOrDefault();
app.CoverImageHash = tapp.CoverImageHash.ValueOrDefault();
app.Guild = tapp.Guild.ValueOrDefault();
app.ApproximateGuildCount = tapp.ApproximateGuildCount.ValueOrDefault();
app.RequiresCodeGrant = tapp.BotRequiresCodeGrant.ValueOrDefault();
app.IsPublic = tapp.IsPublicBot.ValueOrDefault();
app.RedirectUris = tapp.RedirectUris.ValueOrDefault();
app.InteractionsEndpointUrl = tapp.InteractionsEndpointUrl.ValueOrDefault();
return app;
}
///
/// Updates the current API application.
///
/// The new description.
/// The new interactions endpoint url.
/// The new role connections verification url.
/// The new tags.
/// The new application icon.
/// The updated application.
public async Task UpdateCurrentApplicationInfoAsync(Optional description, Optional interactionsEndpointUrl, Optional roleConnectionsVerificationUrl, Optional?> tags, Optional icon)
{
var iconb64 = ImageTool.Base64FromStream(icon);
if (tags != null && tags.HasValue && tags.Value != null)
if (tags.Value.Any(x => x.Length > 20))
throw new InvalidOperationException("Tags can not exceed 20 chars.");
_ = await this.ApiClient.ModifyCurrentApplicationInfoAsync(description, interactionsEndpointUrl, roleConnectionsVerificationUrl, tags, iconb64);
// We use GetCurrentApplicationAsync because modify returns internal data not meant for developers.
var app = await this.GetCurrentApplicationAsync();
this.CurrentApplication = app;
return app;
}
///
/// Gets a list of voice regions.
///
/// Thrown when Discord is unable to process the request.
public Task> ListVoiceRegionsAsync()
=> this.ApiClient.ListVoiceRegionsAsync();
///
/// Initializes this client. This method fetches information about current user, application, and voice regions.
///
public virtual async Task InitializeAsync()
{
if (this.CurrentUser == null)
{
this.CurrentUser = await this.ApiClient.GetCurrentUserAsync().ConfigureAwait(false);
this.UserCache.AddOrUpdate(this.CurrentUser.Id, this.CurrentUser, (id, xu) => this.CurrentUser);
}
if (this.Configuration.TokenType == TokenType.Bot && this.CurrentApplication == null)
this.CurrentApplication = await this.GetCurrentApplicationAsync().ConfigureAwait(false);
if (this.Configuration.TokenType != TokenType.Bearer && this.InternalVoiceRegions.IsEmpty)
{
var vrs = await this.ListVoiceRegionsAsync().ConfigureAwait(false);
foreach (var xvr in vrs)
this.InternalVoiceRegions.TryAdd(xvr.Id, xvr);
}
}
///
/// Gets the current gateway info for the provided token.
/// If no value is provided, the configuration value will be used instead.
///
/// A gateway info object.
public async Task GetGatewayInfoAsync(string token = null)
{
if (this.Configuration.TokenType != TokenType.Bot)
throw new InvalidOperationException("Only bot tokens can access this info.");
if (string.IsNullOrEmpty(this.Configuration.Token))
{
if (string.IsNullOrEmpty(token))
throw new InvalidOperationException("Could not locate a valid token.");
this.Configuration.Token = token;
var res = await this.ApiClient.GetGatewayInfoAsync().ConfigureAwait(false);
this.Configuration.Token = null;
return res;
}
return await this.ApiClient.GetGatewayInfoAsync().ConfigureAwait(false);
}
///
/// Gets some information about the development team behind DisCatSharp.
/// Can be used for crediting etc.
/// Note: This call contacts servers managed by the DCS team, no information is collected.
/// The team, or null with errors being logged on failure.
///
[Obsolete("Don't use this right now, inactive")]
public async Task GetLibraryDevelopmentTeamAsync()
=> await DisCatSharpTeam.Get(this.RestClient, this.Logger, this.ApiClient).ConfigureAwait(false);
///
/// Gets a cached user.
///
/// The user id.
internal DiscordUser GetCachedOrEmptyUserInternal(ulong userId)
{
this.TryGetCachedUserInternal(userId, out var user);
return user;
}
///
/// Tries the get a cached user.
///
/// The user id.
/// The user.
internal bool TryGetCachedUserInternal(ulong userId, out DiscordUser user)
{
if (this.UserCache.TryGetValue(userId, out user))
return true;
user = new DiscordUser { Id = userId, Discord = this };
return false;
}
///
/// Disposes this client.
///
public abstract void Dispose();
}
diff --git a/DisCatSharp/Clients/DiscordClient.WebSocket.cs b/DisCatSharp/Clients/DiscordClient.WebSocket.cs
index aef6535d7..60f9b3fde 100644
--- a/DisCatSharp/Clients/DiscordClient.WebSocket.cs
+++ b/DisCatSharp/Clients/DiscordClient.WebSocket.cs
@@ -1,634 +1,653 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2023 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using DisCatSharp.Entities;
using DisCatSharp.Enums;
using DisCatSharp.EventArgs;
using DisCatSharp.Net.Abstractions;
using DisCatSharp.Net.WebSocket;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Sentry;
namespace DisCatSharp;
///
/// Represents a discord websocket client.
///
public sealed partial class DiscordClient
{
#region Private Fields
private int _heartbeatInterval;
private DateTimeOffset _lastHeartbeat;
private Task _heartbeatTask;
internal static DateTimeOffset DiscordEpoch = new(2015, 1, 1, 0, 0, 0, TimeSpan.Zero);
private int _skippedHeartbeats;
private long _lastSequence;
internal IWebSocketClient WebSocketClient;
private PayloadDecompressor _payloadDecompressor;
private CancellationTokenSource _cancelTokenSource;
private CancellationToken _cancelToken;
#endregion
#region Connection Semaphore
///
/// Gets the socket locks.
///
private static ConcurrentDictionary s_socketLocks { get; } = new();
///
/// Gets the session lock.
///
private readonly ManualResetEventSlim _sessionLock = new(true);
#endregion
#region Internal Connection Methods
///
/// Reconnects the websocket client.
///
/// Whether to start a new session.
/// The reconnect code.
/// The reconnect message.
private Task InternalReconnectAsync(bool startNewSession = false, int code = 1000, string message = "")
{
if (startNewSession)
this._sessionId = null;
_ = this.WebSocketClient.DisconnectAsync(code, message);
return Task.CompletedTask;
}
///
/// Connects the websocket client.
///
internal async Task InternalConnectAsync()
{
+ var a = typeof(DiscordClient).GetTypeInfo().Assembly;
+ var vs = "";
+ var iv = a.GetCustomAttribute();
+ if (iv != null)
+ vs = iv.InformationalVersion;
+ else
+ {
+ var v = a.GetName().Version;
+ vs = v.ToString(3);
+ }
+
using (SentrySdk.Init(o => {
o.DetectStartupTime = StartupTimeDetectionMode.Fast;
o.DiagnosticLevel = SentryLevel.Debug;
o.Environment = "prod";
o.IsGlobalModeEnabled = true;
o.TracesSampleRate = 1.0;
o.ReportAssembliesMode = ReportAssembliesMode.InformationalVersion;
o.Dsn = "https://1da216e26a2741b99e8ccfccea1b7ac8@o1113828.ingest.sentry.io/4504901362515968";
o.AddInAppInclude("DisCatSharp");
o.AttachStacktrace = true;
o.AutoSessionTracking = this.Configuration.EnableSentry;
o.StackTraceMode = StackTraceMode.Enhanced;
- var a = typeof(DiscordClient).GetTypeInfo().Assembly;
- var vs = "";
- var iv = a.GetCustomAttribute();
- if (iv != null)
- vs = iv.InformationalVersion;
- else
- {
- var v = a.GetName().Version;
- vs = v.ToString(3);
- }
o.Release = $"{this.BotLibrary}@{vs}";
o.SendClientReports = true;
}))
{
if (this.Configuration.EnableSentry)
+ {
+ this.Sentry = new SentryClient(new SentryOptions()
+ {
+ DetectStartupTime = StartupTimeDetectionMode.Fast,
+ DiagnosticLevel = SentryLevel.Debug,
+ Environment = "prod",
+ IsGlobalModeEnabled = true,
+ TracesSampleRate = 1.0,
+ ReportAssembliesMode = ReportAssembliesMode.InformationalVersion,
+ Dsn = "https://1da216e26a2741b99e8ccfccea1b7ac8@o1113828.ingest.sentry.io/4504901362515968",
+ AttachStacktrace = true,
+ AutoSessionTracking = this.Configuration.EnableSentry,
+ StackTraceMode = StackTraceMode.Enhanced,
+ SendClientReports = true,
+ Release = $"{this.BotLibrary}@{vs}"
+ });
+ SentrySdk.BindClient(this.Sentry);
SentrySdk.StartSession();
+ }
SocketLock socketLock = null;
try
{
if (this.GatewayInfo == null)
await this.InternalUpdateGatewayAsync().ConfigureAwait(false);
await this.InitializeAsync().ConfigureAwait(false);
socketLock = this.GetSocketLock();
await socketLock.LockAsync().ConfigureAwait(false);
SentrySdk.ConfigureScope(o => o.User = new User()
{
Id = this.CurrentApplication.Id.ToString(),
Username = this.CurrentUser.UsernameWithDiscriminator
});
//SentrySdk.CaptureMessage($"Testing {DateTime.UtcNow.Ticks}");
}
catch
{
socketLock?.UnlockAfter(TimeSpan.Zero);
throw;
}
if (!this.Presences.ContainsKey(this.CurrentUser.Id))
{
this.PresencesInternal[this.CurrentUser.Id] = new DiscordPresence
{
Discord = this,
RawActivity = new TransportActivity(),
Activity = new DiscordActivity(),
Status = UserStatus.Online,
InternalUser = new UserWithIdOnly()
{
Id = this.CurrentUser.Id
}
};
}
else
{
var pr = this.PresencesInternal[this.CurrentUser.Id];
pr.RawActivity = new TransportActivity();
pr.Activity = new DiscordActivity();
pr.Status = UserStatus.Online;
}
Volatile.Write(ref this._skippedHeartbeats, 0);
this.WebSocketClient = this.Configuration.WebSocketClientFactory(this.Configuration.Proxy, this.ServiceProvider);
this._payloadDecompressor = this.Configuration.GatewayCompressionLevel != GatewayCompressionLevel.None
? new PayloadDecompressor(this.Configuration.GatewayCompressionLevel)
: null;
this._cancelTokenSource = new CancellationTokenSource();
this._cancelToken = this._cancelTokenSource.Token;
this.WebSocketClient.Connected += SocketOnConnect;
this.WebSocketClient.Disconnected += SocketOnDisconnect;
this.WebSocketClient.MessageReceived += SocketOnMessage;
this.WebSocketClient.ExceptionThrown += SocketOnException;
var gwuri = new QueryUriBuilder(this.GatewayUri)
.AddParameter("v", this.Configuration.ApiVersion)
.AddParameter("encoding", "json");
if (this.Configuration.GatewayCompressionLevel == GatewayCompressionLevel.Stream)
gwuri.AddParameter("compress", "zlib-stream");
this.Logger.LogDebug(LoggerEvents.Startup, "Connecting to {gw}", this.GatewayUri.AbsoluteUri);
await this.WebSocketClient.ConnectAsync(gwuri.Build()).ConfigureAwait(false);
Task SocketOnConnect(IWebSocketClient sender, SocketEventArgs e)
=> this._socketOpened.InvokeAsync(this, e);
async Task SocketOnMessage(IWebSocketClient sender, SocketMessageEventArgs e)
{
string msg = null;
if (e is SocketTextMessageEventArgs etext)
{
msg = etext.Message;
}
else if (e is SocketBinaryMessageEventArgs ebin)
{
using var ms = new MemoryStream();
if (!this._payloadDecompressor.TryDecompress(new ArraySegment(ebin.Message), ms))
{
this.Logger.LogError(LoggerEvents.WebSocketReceiveFailure, "Payload decompression failed");
return;
}
ms.Position = 0;
using var sr = new StreamReader(ms, Utilities.UTF8);
msg = await sr.ReadToEndAsync().ConfigureAwait(false);
}
try
{
this.Logger.LogTrace(LoggerEvents.GatewayWsRx, msg);
await this.HandleSocketMessageAsync(msg).ConfigureAwait(false);
}
catch (Exception ex)
{
this.Logger.LogError(LoggerEvents.WebSocketReceiveFailure, ex, "Socket handler suppressed an exception");
if (this.Configuration.EnableSentry)
SentrySdk.CaptureException(ex);
}
}
Task SocketOnException(IWebSocketClient sender, SocketErrorEventArgs e)
=> this._socketErrored.InvokeAsync(this, e);
async Task SocketOnDisconnect(IWebSocketClient sender, SocketCloseEventArgs e)
{
// release session and connection
this._connectionLock.Set();
this._sessionLock.Set();
if (!this._disposed)
this._cancelTokenSource.Cancel();
this.Logger.LogDebug(LoggerEvents.ConnectionClose, "Connection closed ({0}, '{1}')", e.CloseCode, e.CloseMessage);
await this._socketClosed.InvokeAsync(this, e).ConfigureAwait(false);
if (this.Configuration.AutoReconnect && (e.CloseCode < 4001 || e.CloseCode >= 5000))
{
this.Logger.LogCritical(LoggerEvents.ConnectionClose, "Connection terminated ({0}, '{1}'), reconnecting", e.CloseCode, e.CloseMessage);
if (this._status == null)
await this.ConnectAsync().ConfigureAwait(false);
else
if (this._status.IdleSince.HasValue)
await this.ConnectAsync(this._status.ActivityInternal, this._status.Status, Utilities.GetDateTimeOffsetFromMilliseconds(this._status.IdleSince.Value)).ConfigureAwait(false);
else
await this.ConnectAsync(this._status.ActivityInternal, this._status.Status).ConfigureAwait(false);
}
else
{
this.Logger.LogCritical(LoggerEvents.ConnectionClose, "Connection terminated ({0}, '{1}')", e.CloseCode, e.CloseMessage);
}
}
}
}
#endregion
#region WebSocket (Events)
///
/// Handles the socket message.
///
/// The data.
internal async Task HandleSocketMessageAsync(string data)
{
var payload = JsonConvert.DeserializeObject(data);
this._lastSequence = payload.Sequence ?? this._lastSequence;
switch (payload.OpCode)
{
case GatewayOpCode.Dispatch:
await Task.Run(async () => await this.HandleDispatchAsync(payload).ConfigureAwait(false));
break;
case GatewayOpCode.Heartbeat:
await this.OnHeartbeatAsync((long)payload.Data).ConfigureAwait(false);
break;
case GatewayOpCode.Reconnect:
await this.OnReconnectAsync().ConfigureAwait(false);
break;
case GatewayOpCode.InvalidSession:
await this.OnInvalidateSessionAsync((bool)payload.Data).ConfigureAwait(false);
break;
case GatewayOpCode.Hello:
await this.OnHelloAsync((payload.Data as JObject).ToObject()).ConfigureAwait(false);
break;
case GatewayOpCode.HeartbeatAck:
await this.OnHeartbeatAckAsync().ConfigureAwait(false);
break;
default:
this.Logger.LogWarning(LoggerEvents.WebSocketReceive, "Unknown Discord opcode: {0}\nPayload: {1}", payload.OpCode, payload.Data);
break;
}
}
///
/// Handles the heartbeat.
///
/// The sequence.
internal async Task OnHeartbeatAsync(long seq)
{
this.Logger.LogTrace(LoggerEvents.WebSocketReceive, "Received HEARTBEAT (OP1)");
await this.SendHeartbeatAsync(seq).ConfigureAwait(false);
}
///
/// Handles the reconnect event.
///
internal async Task OnReconnectAsync()
{
this.Logger.LogTrace(LoggerEvents.WebSocketReceive, "Received RECONNECT (OP7)");
await this.InternalReconnectAsync(code: 4000, message: "OP7 acknowledged").ConfigureAwait(false);
}
///
/// Handles the invalidate session event
///
/// Unknown. Please fill documentation.
internal async Task OnInvalidateSessionAsync(bool data)
{
// begin a session if one is not open already
if (this._sessionLock.Wait(0))
this._sessionLock.Reset();
// we are sending a fresh resume/identify, so lock the socket
var socketLock = this.GetSocketLock();
await socketLock.LockAsync().ConfigureAwait(false);
socketLock.UnlockAfter(TimeSpan.FromSeconds(5));
if (data)
{
this.Logger.LogTrace(LoggerEvents.WebSocketReceive, "Received INVALID_SESSION (OP9, true)");
await Task.Delay(6000).ConfigureAwait(false);
await this.SendResumeAsync().ConfigureAwait(false);
}
else
{
this.Logger.LogTrace(LoggerEvents.WebSocketReceive, "Received INVALID_SESSION (OP9, false)");
this._sessionId = null;
await this.SendIdentifyAsync(this._status).ConfigureAwait(false);
}
}
///
/// Handles the hello event.
///
/// The gateway hello payload.
internal async Task OnHelloAsync(GatewayHello hello)
{
this.Logger.LogTrace(LoggerEvents.WebSocketReceive, "Received HELLO (OP10)");
if (this._sessionLock.Wait(0))
{
this._sessionLock.Reset();
this.GetSocketLock().UnlockAfter(TimeSpan.FromSeconds(5));
}
else
{
this.Logger.LogWarning(LoggerEvents.SessionUpdate, "Attempt to start a session while another session is active");
return;
}
Interlocked.CompareExchange(ref this._skippedHeartbeats, 0, 0);
this._heartbeatInterval = hello.HeartbeatInterval;
this._heartbeatTask = Task.Run(this.HeartbeatLoopAsync, this._cancelToken);
if (string.IsNullOrEmpty(this._sessionId))
await this.SendIdentifyAsync(this._status).ConfigureAwait(false);
else
await this.SendResumeAsync().ConfigureAwait(false);
}
///
/// Handles the heartbeat acknowledge event.
///
internal async Task OnHeartbeatAckAsync()
{
Interlocked.Decrement(ref this._skippedHeartbeats);
var ping = (int)(DateTime.Now - this._lastHeartbeat).TotalMilliseconds;
this.Logger.LogTrace(LoggerEvents.WebSocketReceive, "Received HEARTBEAT_ACK (OP11, {0}ms)", ping);
Volatile.Write(ref this._ping, ping);
var args = new HeartbeatEventArgs(this.ServiceProvider)
{
Ping = this.Ping,
Timestamp = DateTimeOffset.Now
};
await this._heartbeated.InvokeAsync(this, args).ConfigureAwait(false);
}
///
/// Handles the heartbeat loop.
///
internal async Task HeartbeatLoopAsync()
{
this.Logger.LogDebug(LoggerEvents.Heartbeat, "Heartbeat task started");
var token = this._cancelToken;
try
{
while (true)
{
await this.SendHeartbeatAsync(this._lastSequence).ConfigureAwait(false);
await Task.Delay(this._heartbeatInterval, token).ConfigureAwait(false);
token.ThrowIfCancellationRequested();
}
}
catch (OperationCanceledException) { }
}
#endregion
#region Internal Gateway Methods
///
/// Updates the status.
///
/// The activity.
/// The optional user status.
/// Since when is the client performing the specified activity.
internal async Task InternalUpdateStatusAsync(DiscordActivity activity, UserStatus? userStatus, DateTimeOffset? idleSince)
{
if (activity != null && activity.Name != null && activity.Name.Length > 128)
throw new Exception("Game name can't be longer than 128 characters!");
var sinceUnix = idleSince != null ? (long?)Utilities.GetUnixTime(idleSince.Value) : null;
var act = activity ?? new DiscordActivity();
var status = new StatusUpdate
{
Activity = new TransportActivity(act),
IdleSince = sinceUnix,
IsAfk = idleSince != null,
Status = userStatus ?? UserStatus.Online
};
// Solution to have status persist between sessions
this._status = status;
var statusUpdate = new GatewayPayload
{
OpCode = GatewayOpCode.StatusUpdate,
Data = status
};
var statusstr = JsonConvert.SerializeObject(statusUpdate);
await this.WsSendAsync(statusstr).ConfigureAwait(false);
if (!this.PresencesInternal.ContainsKey(this.CurrentUser.Id))
{
this.PresencesInternal[this.CurrentUser.Id] = new DiscordPresence
{
Discord = this,
Activity = act,
Status = userStatus ?? UserStatus.Online,
InternalUser = new UserWithIdOnly { Id = this.CurrentUser.Id }
};
}
else
{
var pr = this.PresencesInternal[this.CurrentUser.Id];
pr.Activity = act;
pr.Status = userStatus ?? pr.Status;
}
}
///
/// Sends the heartbeat.
///
/// The sequenze.
internal async Task SendHeartbeatAsync(long seq)
{
var moreThan5 = Volatile.Read(ref this._skippedHeartbeats) > 5;
var guildsComp = Volatile.Read(ref this._guildDownloadCompleted);
if (guildsComp && moreThan5)
{
this.Logger.LogCritical(LoggerEvents.HeartbeatFailure, "Server failed to acknowledge more than 5 heartbeats - connection is zombie");
var args = new ZombiedEventArgs(this.ServiceProvider)
{
Failures = Volatile.Read(ref this._skippedHeartbeats),
GuildDownloadCompleted = true
};
await this._zombied.InvokeAsync(this, args).ConfigureAwait(false);
await this.InternalReconnectAsync(code: 4001, message: "Too many heartbeats missed").ConfigureAwait(false);
return;
}
else if (!guildsComp && moreThan5)
{
var args = new ZombiedEventArgs(this.ServiceProvider)
{
Failures = Volatile.Read(ref this._skippedHeartbeats),
GuildDownloadCompleted = false
};
await this._zombied.InvokeAsync(this, args).ConfigureAwait(false);
this.Logger.LogWarning(LoggerEvents.HeartbeatFailure, "Server failed to acknowledge more than 5 heartbeats, but the guild download is still running - check your connection speed");
}
Volatile.Write(ref this._lastSequence, seq);
this.Logger.LogTrace(LoggerEvents.Heartbeat, "Sending heartbeat");
var heartbeat = new GatewayPayload
{
OpCode = GatewayOpCode.Heartbeat,
Data = seq
};
var heartbeatStr = JsonConvert.SerializeObject(heartbeat);
await this.WsSendAsync(heartbeatStr).ConfigureAwait(false);
this._lastHeartbeat = DateTimeOffset.Now;
Interlocked.Increment(ref this._skippedHeartbeats);
}
///
/// Sends the identify payload.
///
/// The status update payload.
internal async Task SendIdentifyAsync(StatusUpdate status)
{
var identify = new GatewayIdentify
{
Token = Utilities.GetFormattedToken(this),
Compress = this.Configuration.GatewayCompressionLevel == GatewayCompressionLevel.Payload,
LargeThreshold = this.Configuration.LargeThreshold,
ShardInfo = new ShardInfo
{
ShardId = this.Configuration.ShardId,
ShardCount = this.Configuration.ShardCount
},
Presence = status,
Intents = this.Configuration.Intents,
Discord = this
};
var payload = new GatewayPayload
{
OpCode = GatewayOpCode.Identify,
Data = identify
};
var payloadstr = JsonConvert.SerializeObject(payload);
await this.WsSendAsync(payloadstr).ConfigureAwait(false);
this.Logger.LogDebug(LoggerEvents.Intents, "Registered gateway intents ({0})", this.Configuration.Intents);
}
///
/// Sends the resume payload.
///
internal async Task SendResumeAsync()
{
var resume = new GatewayResume
{
Token = Utilities.GetFormattedToken(this),
SessionId = this._sessionId,
SequenceNumber = Volatile.Read(ref this._lastSequence)
};
var resumePayload = new GatewayPayload
{
OpCode = GatewayOpCode.Resume,
Data = resume
};
var resumestr = JsonConvert.SerializeObject(resumePayload);
this.GatewayUri = new Uri(this._resumeGatewayUrl);
this.Logger.LogDebug(LoggerEvents.ConnectionClose, "Request to resume via {gw}", this.GatewayUri.AbsoluteUri);
await this.WsSendAsync(resumestr).ConfigureAwait(false);
}
///
/// Internals the update gateway async.
///
/// A Task.
internal async Task InternalUpdateGatewayAsync()
{
var info = await this.GetGatewayInfoAsync().ConfigureAwait(false);
this.GatewayInfo = info;
this.GatewayUri = new Uri(info.Url);
}
///
/// Sends a websocket message.
///
/// The payload to send.
internal async Task WsSendAsync(string payload)
{
this.Logger.LogTrace(LoggerEvents.GatewayWsTx, payload);
await this.WebSocketClient.SendMessageAsync(payload).ConfigureAwait(false);
}
#endregion
#region Semaphore Methods
///
/// Gets the socket lock.
///
/// The added socket lock.
private SocketLock GetSocketLock()
=> s_socketLocks.GetOrAdd(this.CurrentApplication.Id, appId => new SocketLock(appId, this.GatewayInfo.SessionBucket.MaxConcurrency));
#endregion
}
diff --git a/DisCatSharp/Net/Serialization/DiscordJson.cs b/DisCatSharp/Net/Serialization/DiscordJson.cs
index a81506b09..f2155aef4 100644
--- a/DisCatSharp/Net/Serialization/DiscordJson.cs
+++ b/DisCatSharp/Net/Serialization/DiscordJson.cs
@@ -1,191 +1,189 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2023 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using DisCatSharp.Entities;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Sentry;
namespace DisCatSharp.Net.Serialization;
///
/// Represents discord json.
///
public static class DiscordJson
{
private static readonly JsonSerializer s_serializer = JsonSerializer.CreateDefault(new JsonSerializerSettings
{
ContractResolver = new OptionalJsonContractResolver()
});
/// Serializes the specified object to a JSON string.
/// The object to serialize.
/// A JSON string representation of the object.
public static string SerializeObject(object value)
=> SerializeObjectInternal(value, null!, s_serializer);
public static T DeserializeObject(string json, BaseDiscordClient discord) where T : ObservableApiObject
=> DeserializeObjectInternal(json, discord);
public static T DeserializeIEnumerableObject(string json, BaseDiscordClient discord) where T : IEnumerable
=> DeserializeIEnumerableObjectInternal(json, discord);
/// Populates an object with the values from a JSON node.
/// The token to populate the object with.
/// The object to populate.
public static void PopulateObject(JToken value, object target)
{
using var reader = value.CreateReader();
s_serializer.Populate(reader, target);
}
///
/// Converts this token into an object, passing any properties through extra s if needed.
///
/// The token to convert
/// Type to convert to
/// The converted token
public static T ToDiscordObject(this JToken token)
=> token.ToObject(s_serializer)!;
///
/// Serializes the object.
///
/// The value.
/// The type.
/// The json serializer.
private static string SerializeObjectInternal(object value, Type type, JsonSerializer jsonSerializer)
{
var stringWriter = new StringWriter(new(256), CultureInfo.InvariantCulture);
using (var jsonTextWriter = new JsonTextWriter(stringWriter))
{
jsonTextWriter.Formatting = jsonSerializer.Formatting;
jsonSerializer.Serialize(jsonTextWriter, value, type);
}
return stringWriter.ToString();
}
private static T DeserializeObjectInternal(string json, BaseDiscordClient discord) where T : ObservableApiObject
{
var obj = JsonConvert.DeserializeObject(json, new JsonSerializerSettings()
{
ContractResolver = new OptionalJsonContractResolver()
})!;
obj.Discord = discord;
if (!discord.Configuration.ReportMissingFields || !obj.AdditionalProperties.Any()) return obj;
var sentryMessage = "Found missing properties in api response for " + obj.GetType().Name;
List sentryFields = new();
var vals = 0;
foreach (var ap in obj.AdditionalProperties)
{
vals++;
if (obj._ignoredJsonKeys.Count == 0 || !obj._ignoredJsonKeys.Any(x => x == ap.Key))
{
if (vals == 1)
{
discord.Logger.LogInformation("{sentry}", sentryMessage);
discord.Logger.LogDebug(json);
}
sentryFields.Add(ap.Key);
discord.Logger.LogInformation("Found field {field} on {object}", ap.Key, obj.GetType().Name);
}
}
if (!discord.Configuration.EnableSentry || sentryFields.Count == 0) return obj;
var sentryJson = JsonConvert.SerializeObject(sentryFields);
sentryMessage += "\n\nNew fields: " + sentryJson;
SentryEvent sentryEvent = new()
{
Level = SentryLevel.Warning,
Logger = nameof(DiscordJson),
Message = sentryMessage
};
sentryEvent.SetExtra("Found Fields", sentryJson);
- var sid = SentrySdk.CaptureEvent(sentryEvent);
- _ = Task.Run(SentrySdk.FlushAsync);
+ var sid = discord.Sentry.CaptureEvent(sentryEvent);
+ _ = Task.Run(discord.Sentry.FlushAsync);
discord.Logger.LogInformation("Reported to sentry with id {sid}", sid.ToString());
return obj;
}
private static T DeserializeIEnumerableObjectInternal(string json, BaseDiscordClient discord) where T : IEnumerable
{
var obj = JsonConvert.DeserializeObject(json, new JsonSerializerSettings()
{
ContractResolver = new OptionalJsonContractResolver()
})!;
foreach (var ob in obj)
ob.Discord = discord;
if (!discord.Configuration.ReportMissingFields || !obj.Any(x => x.AdditionalProperties.Any())) return obj;
var first = obj.First();
var sentryMessage = "Found missing properties in api response for " + first.GetType().Name;
List sentryFields = new();
var vals = 0;
foreach (var ap in first.AdditionalProperties)
{
vals++;
if (first._ignoredJsonKeys.Count == 0 || !first._ignoredJsonKeys.Any(x => x == ap.Key))
{
if (vals == 1)
{
discord.Logger.LogInformation("{sentry}", sentryMessage);
discord.Logger.LogDebug(json);
}
sentryFields.Add(ap.Key);
discord.Logger.LogInformation("Found field {field} on {object}", ap.Key, first.GetType().Name);
}
}
- discord.Logger.LogDebug(json);
-
if (!discord.Configuration.EnableSentry || sentryFields.Count == 0) return obj;
var sentryJson = JsonConvert.SerializeObject(sentryFields);
sentryMessage += "\n\nNew fields: " + sentryJson;
SentryEvent sentryEvent = new()
{
Level = SentryLevel.Warning,
Logger = nameof(DiscordJson),
Message = sentryMessage
};
sentryEvent.SetExtra("Found Fields", sentryJson);
- var sid = SentrySdk.CaptureEvent(sentryEvent);
- _ = Task.Run(SentrySdk.FlushAsync);
+ var sid = discord.Sentry.CaptureEvent(sentryEvent);
+ _ = Task.Run(discord.Sentry.FlushAsync);
discord.Logger.LogInformation("Reported to sentry with id {sid}", sid.ToString());
return obj;
}
}