Пример #1
0
        private async Task InitializeMonitoring(CancellationToken cancellationToken)
        {
            await this.monitorCreationTask.Task;

            this.state = GatewayState.Initializing;

            try
            {
                ServiceEventSource.Current.ServiceMessage(this.Context, "Initializing monitoring {0}", this.myInfo.Id.InstanceId);

                await gatewayMonitor_.InitializeAsync(
                    this.myInfo,
                    this.heartbeatInterval,
                    cancellationToken);

                ServiceEventSource.Current.ServiceMessage(this.Context, "Monitoring initialized");

                this.state = GatewayState.Monitored;
            }
            catch (Exception e)
            {
                ServiceEventSource.Current.ServiceMessage(this.Context, "Failed to initialize monitoring - {0}", e.ToString());
                this.state = GatewayState.Failed;
                this.OnMonitoringFailed();
                throw e;
            }
        }
Пример #2
0
        /// <summary>
        /// Warning: Do not call from the context of the connect loop! A deadlock will occur!
        /// </summary>
        async Task FullDisconnect()
        {
            log.LogVerbose("Disconnecting...");
            state = GatewayState.Disconnected;

            handshakeCompleteCancellationSource.Cancel();

            if (connectTask != null)
            {
                // Cancel any automatic reconnection
                connectTaskCancellationSource?.Cancel();

                // Wait for the automatic reconnection to end
                try
                {
                    await connectTask.ConfigureAwait(false);
                }
                catch (OperationCanceledException) { /* Expected to happen. */ }
                catch (Exception ex)
                {
                    // Should never happen, but there isn't anything we can do here.
                    log.LogError($"[DisconnectAsync] Uncaught exception found in connect task: {ex}");
                }
            }

            // Disconnect the socket if needed
            if (socket.CanBeDisconnected)
            {
                await socket.DisconnectAsync(WebSocketCloseStatus.NormalClosure, "Disconnecting...", CancellationToken.None)
                .ConfigureAwait(false);
            }

            log.LogVerbose("Disconnected.");
        }
Пример #3
0
        private void SetGatewayState(GatewayState newState)
        {
            switch (newState)
            {
            case GatewayState.Disconnected:
                LogInfo("Disconnected.");
                break;

            case GatewayState.ConnectingToPort:
                LogInfo("Trying to connect...");
                break;

            case GatewayState.ConnectingToGateway:
                LogInfo("Trying to communicate...");
                break;

            case GatewayState.Connected:
                LogInfo("Gateway connected.");
                break;
            }

            if (gatewayState == GatewayState.Connected && newState != GatewayState.Connected)
            {
                OnDisconnected?.Invoke();
            }
            else if (gatewayState != GatewayState.Connected && newState == GatewayState.Connected)
            {
                OnConnected?.Invoke();
            }

            gatewayState = newState;
            OnGatewayStateChanged?.Invoke(gatewayState);
        }
Пример #4
0
        private async Task SendHeartbeatAsync(CancellationToken cancellationToken)
        {
            ServiceEventSource.Current.ServiceMessage(this.Context, "Sending heartbeat");

            try
            {
                var cts = CancellationTokenSource.CreateLinkedTokenSource(
                    cancellationToken,
                    new CancellationTokenSource(heartbeatInterval).Token);

                var gatewayMonitor = ServiceProxy.Create <IGatewayMonitor>(
                    this.monitorServiceAddress,
                    this.monitorServicePartitionKey);
                await gatewayMonitor.HeartbeatAsync(this.myInfo.Id, cts.Token);

                ServiceEventSource.Current.ServiceMessage(this.Context, "Heartbeat sent");
            }
            catch (Exception e)
            {
                ServiceEventSource.Current.ServiceMessage(this.Context, "Failed to send heartbeat - {0}", e.ToString());
                this.state = GatewayState.Failed;
                this.OnMonitoringFailed();
                throw e;
            }
        }
Пример #5
0
        private async Task InitializeMonitoring(CancellationToken cancellationToken)
        {
            this.state = GatewayState.Initializing;

            try
            {
                ServiceEventSource.Current.ServiceMessage(this.Context, "Initializing monitoring");

                var gatewayMonitor = ServiceProxy.Create <IGatewayMonitor>(
                    this.monitorServiceAddress,
                    this.monitorServicePartitionKey);

                await gatewayMonitor.InitializeAsync(
                    this.myInfo,
                    this.heartbeatInterval,
                    cancellationToken);

                ServiceEventSource.Current.ServiceMessage(this.Context, "Monitoring initialized");
                this.state = GatewayState.Monitored;
            }
            catch (Exception e)
            {
                ServiceEventSource.Current.ServiceMessage(this.Context, "Failed to initialize monitoring - {0}", e.ToString());
                this.state = GatewayState.Failed;
                this.OnMonitoringFailed();
                throw e;
            }
        }
Пример #6
0
        void _OnClose(int code = 0)
        {
            this._CancelHeartBeat();

            if (this.m_CloseRequired)
            {
                this.m_ReadyState = GatewayState.Init;
                return;
            }

            if (this.m_Socket != null)
            {
                //https://github.com/sta/websocket-sharp/issues/219
                if (code == 1015 && this.m_Socket.checkSslProtocolHackFlag())
                {
                    this.m_SslHandShakeError = true;
                }

                this.m_Socket.Close();
                this.m_Socket = null;
            }

            this._Reset(isInit: false);

            bool reset = !this.resumable || this.m_BackOff.fail > 0;

            if (code != 0)
            {
                this.m_CandidateIndex++;
            }
            var delay = this._OnFail(reset);

            NetworkStatusManager.isConnected = false;
            DebugerUtils.DebugAssert(false, $"connection failed, retry in {delay / 1000f} seconds");
        }
Пример #7
0
 public GatewayService(StatelessServiceContext context)
     : base(context)
 {
     this.state                      = GatewayState.Created;
     this.myInfo                     = CreateMyInfo(context);
     this.heartbeatInterval          = GetHeartbeatDuration(context.CodePackageActivationContext);
     this.monitorServiceAddress      = GetMonitorServiceAddress(context.CodePackageActivationContext);
     this.monitorServicePartitionKey = new ServicePartitionKey(this.myInfo.Id.Name);
 }
Пример #8
0
 public GatewayStateFormalizer(GatewayState gatewayState)
 {
     this.id         = gatewayState.Id;
     this.stateName  = gatewayState.StateName;
     this.remark     = gatewayState.Remark;
     this.createTime = gatewayState.CreateTime
                       .ToString(ConstantService.DateTimeFormat);
     this.updateTime = gatewayState.UpdateTime
                       .ToString(ConstantService.DateTimeFormat);
 }
Пример #9
0
 void _Reset(bool isInit, bool reset = true)
 {
     this.m_ReadyState = isInit ? GatewayState.Init : GatewayState.CLOSED;
     this.m_PayloadQueue.Clear();
     if (reset)
     {
         this.m_SessionId = null;
         this.m_Sequence  = 0;
     }
 }
Пример #10
0
 public GatewayService(StatelessServiceContext context)
     : base(context)
 {
     this.state                      = GatewayState.Created;
     this.myInfo                     = CreateMyInfo(context);
     serviceDownToken                = new CancellationTokenSource();
     this.heartbeatInterval          = GetHeartbeatDuration(context.CodePackageActivationContext);
     this.monitorServiceAddress      = GetMonitorServiceAddress(context.CodePackageActivationContext);
     this.monitorServicePartitionKey = new ServicePartitionKey(this.myInfo.Id.Name);
     this.monitorCreationTask        = new TaskCompletionSource <bool>();
 }
Пример #11
0
        private void Socket_OnFatalDisconnection(object sender, GatewayCloseCode e)
        {
            if (isDisposed)
            {
                return;
            }

            log.LogVerbose("Fatal disconnection occured, setting state to Disconnected.");
            state = GatewayState.Disconnected;

            (string message, ShardFailureReason reason) = GatewayCloseCodeToReason(e);
            gatewayFailureData = new GatewayFailureData(message, reason, null);
            handshakeCompleteEvent.Set();

            OnFailure?.Invoke(this, gatewayFailureData);
        }
        public async void Execute(Gateway gateway)
        {
            // 通信実装を毎フレーム呼び出し
            switch (State)
            {
            case GatewayState.GETTING_DREAMS_LIST:
                _existsFiles = FilerOperator.getExistsTextures();
                _dream       = await gateway.GetDreamsData();

                if (_dream != null)
                {
                    State = GatewayState.GETTING_DREAM_TEXTURE_INIT;
                }
                break;

            case GatewayState.GETTING_DREAM_TEXTURE_INIT:
                _image = _dream.images[_counter];
                // 既にローカルにテクスチャが存在する場合はスキップ
                if (_existsFiles != null && _existsFiles.Contains(_image))
                {
                    State = GatewayState.GETTING_DREAM_TEXTURE_FINISHED;
                    break;
                }
                //gateway.SetUrlFromFileName(_image);
                //State = gateway.GetDreamTexture(_image);
                break;

            case GatewayState.GETTING_DREAM_TEXTURE:
                //State = gateway.GetDreamTexture(_image);
                break;

            case GatewayState.GETTING_DREAM_TEXTURE_FINISHED:
                _counter++;
                if (_counter < _dream.images.Length)
                {
                    State = GatewayState.GETTING_DREAM_TEXTURE_INIT;
                }
                else
                {
                    State = GatewayState.GETTING_DREAM_TEXTURES_COMPLETED;
                }
                break;

            case GatewayState.GETTING_DREAM_TEXTURES_COMPLETED:
                break;
            }
        }
Пример #13
0
        internal Gateway(string botToken, Shard shard, int totalShards)
        {
            this.botToken    = botToken;
            this.shard       = shard;
            this.totalShards = totalShards;

            http = new DiscordHttpClient(botToken);

            cache = shard.Cache;

            log   = new DiscoreLogger($"Gateway#{shard.Id}");
            state = GatewayState.Disconnected;

            handshakeCompleteEvent = new AsyncManualResetEvent();
            handshakeCompleteCancellationSource = new CancellationTokenSource();

            // Up-to-date rate limit parameters: https://discord.com/developers/docs/topics/gateway#rate-limiting
            identifyRateLimiter         = new GatewayRateLimiter(5, 1);    // 1 IDENTIFY per 5 seconds
            outboundPayloadRateLimiter  = new GatewayRateLimiter(60, 120); // 120 outbound payloads every 60 seconds
            gameStatusUpdateRateLimiter = new GatewayRateLimiter(60, 5);   // 5 status updates per minute

            InitializeDispatchHandlers();
        }
Пример #14
0
        public override async Task StartAsync(CancellationToken cancellationToken)
        {
            _logger.LogInformation("Started Discord Service");
            _logger.LogDebug("Mod role: {ModRole}", _settings.Value.ModRole);


            _webSocket = new ClientWebSocket();

            var url = (await _restApi.GetAsync <GetGatewayResponse>("/gateway", cancellationToken))?.Url ?? string.Empty;

            if (string.IsNullOrWhiteSpace(url))
            {
                _logger.LogError("Unable to get Gateway URL");
                return;
            }

            _logger.LogDebug("Got Gateway Url '{Url}'", url);

            _state     = GatewayState.Connecting;
            _webSocket = new ClientWebSocket();
            await _webSocket.ConnectAsync(new Uri($"{url}?v=9&encoding=json"), cancellationToken);

            await base.StartAsync(cancellationToken);
        }
Пример #15
0
        private void SetGatewayState(GatewayState newState)
        {

            switch (newState)
            {
                case GatewayState.Disconnected:
                    LogInfo("Disconnected.");
                    break;
                case GatewayState.ConnectingToPort:
                    LogInfo("Trying to connect...");
                    break;
                case GatewayState.ConnectingToGateway:
                    LogInfo("Trying to communicate...");
                    break;
                case GatewayState.Connected:
                    LogInfo("Gateway connected.");
                    break;
            }

            if (gatewayState == GatewayState.Connected && newState != GatewayState.Connected)
                OnDisconnected?.Invoke();
            else if (gatewayState != GatewayState.Connected && newState == GatewayState.Connected)
                OnConnected?.Invoke();

            gatewayState = newState;
            OnGatewayStateChanged?.Invoke(gatewayState);
        }
Пример #16
0
        /// <exception cref="OperationCanceledException"></exception>
        async Task ConnectLoop(bool resume, CancellationToken cancellationToken)
        {
            // Keep track of whether this is a resume or new session so
            // we can respond to the HELLO payload appropriately.
            isConnectionResuming = resume;

            log.LogVerbose($"[ConnectLoop] resume = {resume}");

            handshakeCompleteEvent.Reset();
            handshakeCompleteCancellationSource = new CancellationTokenSource();

            while (!cancellationToken.IsCancellationRequested)
            {
                // Ensure previous socket has been closed
                if (socket != null)
                {
                    UnsubscribeSocketEvents();

                    if (resume)
                    {
                        // Store previous sequence
                        lastSequence = socket.Sequence;
                    }

                    if (socket.CanBeDisconnected)
                    {
                        log.LogVerbose("[ConnectLoop] Disconnecting previous socket...");

                        // If for some reason the socket cannot be disconnected gracefully,
                        // DisconnectAsync will abort the socket after 5s.

                        if (resume)
                        {
                            // Make sure to disconnect with a non 1000 code to ensure Discord doesn't
                            // force us to make a new session since we are resuming.
                            await socket.DisconnectAsync(WebSocketCloseStatus.EndpointUnavailable,
                                                         "Reconnecting to resume...", cancellationToken).ConfigureAwait(false);
                        }
                        else
                        {
                            await socket.DisconnectAsync(WebSocketCloseStatus.NormalClosure,
                                                         "Starting new session...", cancellationToken).ConfigureAwait(false);
                        }
                    }

                    socket.Dispose();
                }

                if (!resume)
                {
                    // If not resuming, reset gateway session state.
                    lastSequence = 0;
                }

                // Create a new socket
                socket = new GatewaySocket($"GatewaySocket#{shard.Id}", lastSequence,
                                           outboundPayloadRateLimiter, gameStatusUpdateRateLimiter, identifyRateLimiter);

                socket.OnHello = async() =>
                {
                    if (isDisposed)
                    {
                        return;
                    }

                    if (isConnectionResuming)
                    {
                        // Resume
                        await socket.SendResumePayload(botToken, sessionId, lastSequence);
                    }
                    else
                    {
                        // Identify
                        await socket.SendIdentifyPayload(botToken, lastShardStartConfig.GatewayLargeThreshold,
                                                         shard.Id, totalShards);
                    }
                };

                SubscribeSocketEvents();

                // Get the gateway URL if we don't have one
                string gatewayUrl = GatewayUrlMemoryCache.GatewayUrl;

                if (gatewayUrl == null)
                {
                    try
                    {
                        log.LogVerbose("[ConnectLoop] Retrieving Gateway URL...");

                        gatewayUrl = await http.GetGateway().ConfigureAwait(false);

                        GatewayUrlMemoryCache.UpdateUrl(gatewayUrl);
                    }
                    catch (Exception ex) when(ex is DiscordHttpApiException || ex is HttpRequestException || ex is OperationCanceledException)
                    {
                        log.LogError($"[ConnectLoop:GetGateway] {ex}");
                        log.LogError("[ConnectLoop] No gateway URL to connect with, trying again in 10s...");

                        await Task.Delay(10 * 1000, cancellationToken).ConfigureAwait(false);

                        continue;
                    }
                    catch (Exception ex)
                    {
                        // This should never-ever happen, but we need to handle it just in-case.

                        log.LogError(ex);
                        log.LogError("[ConnectLoop] Uncaught severe error occured while getting the Gateway URL, setting state to Disconnected.");

                        state = GatewayState.Disconnected;

                        gatewayFailureData = new GatewayFailureData(
                            "Failed to retrieve the Gateway URL because of an unknown error.",
                            ShardFailureReason.Unknown, ex);

                        handshakeCompleteEvent.Set();

                        OnFailure?.Invoke(this, gatewayFailureData);

                        break;
                    }
                }

                log.LogVerbose($"[ConnectLoop] gatewayUrl = {gatewayUrl}");

                // Wait if necessary
                if (nextConnectionDelayMs > 0)
                {
                    log.LogVerbose($"[ConnectLoop] Waiting {nextConnectionDelayMs}ms before connecting socket...");

                    await Task.Delay(nextConnectionDelayMs, cancellationToken).ConfigureAwait(false);

                    nextConnectionDelayMs = 0;
                }

                try
                {
                    // Attempt to connect
                    await socket.ConnectAsync(new Uri($"{gatewayUrl}?v={GATEWAY_VERSION}&encoding=json"), cancellationToken)
                    .ConfigureAwait(false);

                    // At this point the socket has successfully connected
                    log.LogVerbose("[ConnectLoop] Socket connected successfully.");
                    break;
                }
                catch (WebSocketException wsex)
                {
                    UnsubscribeSocketEvents();

                    // Failed to connect
                    log.LogError("[ConnectLoop] Failed to connect: " +
                                 $"{wsex.WebSocketErrorCode} ({(int)wsex.WebSocketErrorCode}), {wsex.Message}");

                    // Invalidate the cached URL since we failed to connect the socket
                    log.LogVerbose("[ConnectLoop] Invalidating Gateway URL...");
                    GatewayUrlMemoryCache.InvalidateUrl();

                    // Wait 5s then retry
                    log.LogVerbose("[ConnectLoop] Waiting 5s before retrying...");
                    await Task.Delay(5000, cancellationToken).ConfigureAwait(false);
                }
            }

            // If the token is cancelled between the socket successfully connecting and the loop exiting,
            // do not throw an exception because the connection did technically complete before the cancel.
            if (socket == null || !socket.IsConnected)
            {
                // If the loop stopped from the token being cancelled, ensure an exception is still thrown.
                cancellationToken.ThrowIfCancellationRequested();
            }

            // If this is an automatic reconnection, fire OnReconnected event
            if (state == GatewayState.Connected)
            {
                if (resume)
                {
                    log.LogInfo("[ConnectLoop:Reconnection] Successfully started a resume.");
                }
                else
                {
                    log.LogInfo("[ConnectLoop:Reconnection] Successfully started creating a new session.");
                }

                OnReconnected?.Invoke(this, new GatewayReconnectedEventArgs(!resume));
            }
        }
Пример #17
0
        public void Connect(Action <string> onIdentify, Action <string, SocketResponseDataBase> onMessage,
                            bool reconnect = false)
        {
            if (this.m_ReadyState != GatewayState.CLOSED && this.m_ReadyState != GatewayState.Init)
            {
                return;
            }

            if (!reconnect)
            {
                this.m_OnIdentify    = onIdentify;
                this.m_OnMessage     = onMessage;
                this.m_CloseRequired = false;
            }
            else
            {
                DebugerUtils.DebugAssert(this.m_OnIdentify != null, "fatal error: reconnect before initial connect !");
                DebugerUtils.DebugAssert(this.m_OnMessage != null, "fatal error: reconnect before initial connect !");
            }

            this.m_ReadyState = GatewayState.CONNECTING;
            this.m_BackOff.Cancel();

            this._SelectGateway(
                createWebSocketFunc:
                url => {
                if (url != null)
                {
                    this._Reset(isInit: true);
                    this.m_Socket?.Close();
                    this.m_Socket = null;

                    this.m_Socket = new WebSocket(this.m_Host, this.m_SslHandShakeError);
                    this.m_Socket.Connect(url,
                                          OnError: msg => { this._OnClose(1); },
                                          OnClose: this._OnClose,
                                          OnConnected: () => {
                        NetworkStatusManager.isConnected = true;
                        DebugerUtils.DebugAssert(this.m_CommitId != null,
                                                 "fatal error: commit Id is not correctly set before connection!");

                        this.m_ReadyState = GatewayState.OPEN;
                        if (!this._Resume())
                        {
                            this.m_OnIdentify.Invoke(this.m_CommitId);
                        }

                        this._StartHeartBeat();
                    },
                                          OnMessage: bytes => {
                        NetworkStatusManager.isConnected = true;
                        string type = "";
                        var data    = this._OnMessage(bytes, ref type);
                        if (data != null)
                        {
                            this.m_OnMessage?.Invoke(type, data);
                        }
                    }
                                          );
                }
                else
                {
                    NetworkStatusManager.isConnected = false;
                    var delay = this._OnFail();
                    DebugerUtils.DebugAssert(false, $"gateway discovery failed, retry in {delay / 1000f} seconds");
                }
            });
        }
Пример #18
0
        /// <exception cref="GatewayHandshakeException"></exception>
        /// <exception cref="InvalidOperationException"></exception>
        /// <exception cref="ObjectDisposedException"></exception>
        public async Task ConnectAsync(ShardStartConfig config, CancellationToken cancellationToken)
        {
            if (isDisposed)
            {
                throw new ObjectDisposedException(GetType().FullName);
            }
            if (state == GatewayState.Connected)
            {
                throw new InvalidOperationException("The gateway is already connected!");
            }
            if (state == GatewayState.Connecting)
            {
                throw new InvalidOperationException("The gateway is already connecting!");
            }

            // Begin connecting
            state = GatewayState.Connecting;
            lastShardStartConfig = config;

            gatewayFailureData = null;
            handshakeCompleteEvent.Reset();

            connectTask = ConnectLoop(false, cancellationToken);

            try
            {
                // Connect socket
                await connectTask.ConfigureAwait(false);

                try
                {
                    // Wait for the handshake to complete
                    await handshakeCompleteEvent.WaitAsync(cancellationToken).ConfigureAwait(false);
                }
                catch (OperationCanceledException)
                {
                    // Since this was cancelled after the socket connected,
                    // we need to do a full disconnect.
                    await FullDisconnect();

                    throw;
                }

                // Check for errors
                if (gatewayFailureData != null)
                {
                    throw new GatewayHandshakeException(gatewayFailureData);
                }

                // Connection successful
                log.LogVerbose("[ConnectAsync] Setting state to Connected.");
                state = GatewayState.Connected;
            }
            catch
            {
                // Reset to disconnected if cancelled or failed
                log.LogVerbose("[ConnectAsync] Setting state to Disconnected.");
                state = GatewayState.Disconnected;

                throw;
            }
        }
Пример #19
0
        private async Task HandlePacket(string json, CancellationToken cancellationToken)
        {
            var genericGateway = JsonSerializer.Deserialize <GatewayPacket <object> >(json, _jsonOptions);

            if (genericGateway is null)
            {
                _logger.LogError("Unable to parse generic gateway packet");
                return;
            }

            if (genericGateway.SequenceNumber is not null)
            {
                _lastSequenceNumber = genericGateway.SequenceNumber;
            }

            switch (_state)
            {
            case GatewayState.Connecting:
                if (genericGateway.Opcode == Opcode.Hello)
                {
                    // We got a Hello, start sending heartbeats!
                    var helloPacket = DeserializePacket <HelloData>(json);
                    if (helloPacket is null || helloPacket.Data is null)
                    {
                        _logger.LogError("Invalid hello packet");
                        return;
                    }
                    _heartbeatInterval = TimeSpan.FromMilliseconds(helloPacket.Data.HeartbeatInterval);
                    _logger.LogDebug("Heartbeat interval set to '{Interval}'", _heartbeatInterval);
                    if (_heartbeatTask is null)
                    {
                        _heartbeatTask = new BackgroundTask(SendHeartbeatAsync);
                    }
                    // Queue up an Identify packet
                    var identifyPacket = new GatewayPacket <IdentifyData>
                    {
                        Data = new IdentifyData
                        {
                            // TODO: Intents
                            Intents    = 1,
                            Token      = _settings.Value.Token,
                            Properties = new IdentifyDataProperties
                            {
                                Browser = "octantis",
                                Device  = "octantis",
                                Os      = "linux"
                            }
                        },
                        Opcode = Opcode.Identify
                    };

                    await TransmitGatewayAsync(identifyPacket, cancellationToken);
                }
                else if (genericGateway.Opcode == Opcode.Dispatch && genericGateway.EventName == Events.Ready)
                {
                    var readyPacket = DeserializePacket <ReadyData>(json);
                    if (readyPacket is null || readyPacket.Data is null)
                    {
                        _logger.LogError("Invalid ready packet");
                        return;
                    }
                    ApplicationId = readyPacket.Data.User?.Id ?? 0;
                    _sessionId    = readyPacket.Data.SessionId;
                    _logger.LogInformation("Connected to gateway v{Version}, application id '{Id}', session id '{Id}'", readyPacket.Data.GatewayVersion, ApplicationId, _sessionId);
                    foreach (var guild in readyPacket.Data.Guilds)
                    {
                        _logger.LogDebug("Guild '{Id}'", guild.Id);
                    }

                    // Got the ready event, move up to connected state!
                    _state = GatewayState.Connected;
                }
                else
                {
                    _logger.LogWarning("Unhandled opcode '{Opcode}'", genericGateway.Opcode);
                }
                break;

            case GatewayState.Connected:
            {
                // Handle events
                if (genericGateway.Opcode == Opcode.Heartbeat)
                {
                    // Force heartbeat send?
                    _logger.LogError("TODO: SEND HEARTBEAT");
                }
                else if (genericGateway.Opcode == Opcode.Dispatch)
                {
                    // EVENTS :D
                    var type = Events.FromString(genericGateway.EventName !);
                    List <Registration>?registrationsForEvent;
                    lock (_registrations)
                    {
                        _registrations.TryGetValue(type, out registrationsForEvent);
                    }
                    if (registrationsForEvent is not null)
                    {
                        foreach (var registration in registrationsForEvent)
                        {
                            var     packetType         = typeof(GatewayPacket <>).MakeGenericType(registration.ArgumentType);
                            dynamic deserializedAsType = JsonSerializer.Deserialize(json, packetType, _jsonOptions) !;
                            registration.Action(deserializedAsType.Data);
                        }
                    }
                    else
                    {
                        _logger.LogWarning("Unhandled event type '{Type}'", type);
                    }
                }
            }
            break;
            }
        }