using System.Collections.Specialized; using System.Text.Json; using Serilog; using Websocket.Client; namespace Discord.API; public class GatewayClient { private const int ApiVersion = 10; private string ApiKey; private WebsocketClient Websocket; private CancellationTokenSource? heartbeat_cts; private ulong? Sequence = null; public GatewayClient(string url, string api_key){ this.ApiKey=api_key; UriBuilder uriBuilder = new(url); NameValueCollection query = System.Web.HttpUtility.ParseQueryString(""); query.Add("v", ApiVersion.ToString()); query.Add("encoding", "json"); uriBuilder.Query=query.ToString(); Websocket = new WebsocketClient(uriBuilder.Uri); Websocket.DisconnectionHappened.Subscribe(DisconnectionHandler); Websocket.ReconnectionHappened.Subscribe(ReconnectionHandler); Websocket.MessageReceived.Subscribe(MessageReceivedHandler); Log.Debug("GATEWAY: Created new gateway, with url: {url}", uriBuilder.ToString()); } private void DisconnectionHandler(DisconnectionInfo info){ Log.Information("GATEWAY: Disconnected. Type: {DisconnectionType}", info.Type); } private void ReconnectionHandler(ReconnectionInfo info){ Log.Information("GATEWAY: (Re)Connected to server. Url: {url}, Type: {Type}", Websocket.Url, info.Type); } private void MessageReceivedHandler(ResponseMessage msg){ if(msg.MessageType != System.Net.WebSockets.WebSocketMessageType.Text) return; try{ GatewayPacket packet = JsonSerializer.Deserialize(msg.Text!, SourceGenerationContext.Default.GatewayPacket) ?? throw new Exception("Failed to deserialize packet"); // This can be optimized //TODO switch(packet){ case HelloPacket helloPacket: HelloPacketHandler(helloPacket); break; } }catch(Exception ex){ Log.Warning(ex, "GATEWAY: Error processing gateway event"); } } private void HelloPacketHandler(HelloPacket packet){ StartHeartbeat((int)packet.Data.HeartbeatInterval); } private void StartHeartbeat(int heartbeat_interval){ heartbeat_cts?.Cancel(); heartbeat_cts = new CancellationTokenSource(); CancellationToken ct = heartbeat_cts.Token; Task.Run(async ()=>{ await Task.Delay(Random.Shared.Next(1, heartbeat_interval)); using PeriodicTimer pd = new(TimeSpan.FromMilliseconds(heartbeat_interval)); do{ HeartbeatPacket packet = new HeartbeatPacket(){ Sequence=this.Sequence }; if(Websocket.IsRunning) if(!Websocket.Send(JsonSerializer.Serialize(packet, SourceGenerationContext.Default.HeartbeatPacket))){ Log.Warning("GATEWAY: Failed to queue heartbeat message"); } }while(await pd.WaitForNextTickAsync(ct) && !ct.IsCancellationRequested); }); } }