public async Task SendFcmNotification(long accountId)
        {
            Session[] sessions;

            using (IServiceScope scope = serviceProvider.CreateScope())
            {
                var database = scope.ServiceProvider.GetRequiredService <DatabaseContext>();

                sessions = await database.Sessions.AsQueryable()
                           .Where(s => s.AccountId == accountId && s.FcmToken != null &&
                                  (s.LastFcmMessage < s.LastConnected || options.Value.NotifyForEveryMessage))
                           .ToArrayAsync().ConfigureAwait(false);
            }

            async Task process(Session session)
            {
                using IServiceScope scope = serviceProvider.CreateScope();
                var database = scope.ServiceProvider.GetRequiredService <DatabaseContext>();
                var injector = scope.ServiceProvider.GetRequiredService <MessageInjectionService>();
                var delivery = scope.ServiceProvider.GetRequiredService <DeliveryService>();
                var logger   = scope.ServiceProvider.GetRequiredService <ILogger <NotificationService> >();

                try
                {
                    await firebase.SendAsync(session.FcmToken).ConfigureAwait(false);

                    session.LastFcmMessage = DateTime.Now;
                    database.Entry(session).Property(s => s.LastFcmMessage).IsModified = true;
                    await database.SaveChangesAsync().ConfigureAwait(false);

                    logger.LogInformation($"Successfully sent FCM message to {session.FcmToken.Remove(16)}... last connected {session.LastConnected}");
                }
                catch (FirebaseMessagingException ex) when(ex.MessagingErrorCode == MessagingErrorCode.Unregistered)
                {
                    logger.LogWarning($"Failed to send FCM message to {session.FcmToken.Remove(16)}... {ex.Message}");
                    if (options.Value.DeleteSessionOnError)
                    {
                        // Prevent quick re-login after kick
                        session.SessionTokenHash = Array.Empty <byte>();
                        database.Entry(session).Property(s => s.SessionTokenHash).IsModified = true;
                        await database.SaveChangesAsync().ConfigureAwait(false);

                        // Kick client if connected to avoid conflicting information in RAM vs DB
                        if (connections.TryGet(session.SessionId, out IClient client))
                        {
                            await client.DisposeAsync().ConfigureAwait(false);
                        }
                        database.Sessions.Remove(session);
                        await database.SaveChangesAsync().ConfigureAwait(false);

                        Message deviceList = await injector.CreateDeviceList(session.AccountId).ConfigureAwait(false);

                        await delivery.StartSendMessage(deviceList, null).ConfigureAwait(false);
                    }
                }
            }

            await Task.WhenAll(sessions.Select(s => process(s))).ConfigureAwait(false);
        }
        public async Task StartSetActive(IClient client, bool active)
        {
            long[] sessions = await database.Sessions.AsQueryable()
                              .Where(s => s.AccountId == client.AccountId)
                              .Select(s => s.SessionId)
                              .ToArrayAsync().ConfigureAwait(false);

            client.SoonActive = active;

            bool notify = true;

            foreach (long sessionId in sessions)
            {
                if (connections.TryGet(sessionId, out IClient _client) &&
                    !ReferenceEquals(_client, client) &&
                    _client.SoonActive && (_client.Active || !active))
                {
                    // If going online: No need to notify if another session is already online and staying online
                    // If going offline: No need to notify if another session is online or coming online in the meantime
                    notify = false;
                }
            }

            client.Active = active;

            if (!notify)
            {
                return;
            }

            long accountChannelId = await database.Channels.AsQueryable()
                                    .Where(c => c.OwnerId == client.AccountId && c.ChannelType == ChannelType.AccountData)
                                    .Select(c => c.ChannelId)
                                    .SingleAsync().ConfigureAwait(false);

            var packet = packets.New <P2BOnlineState>();

            packet.OnlineState  = active ? OnlineState.Active : OnlineState.Inactive;
            packet.LastActive   = DateTime.Now;
            packet.MessageFlags = MessageFlags.Unencrypted | MessageFlags.NoSenderSync;

            Message message = await injector.CreateMessage(packet, accountChannelId, client.AccountId).ConfigureAwait(false);

            await delivery.StartSendMessage(message, client).ConfigureAwait(false);
        }
        public async Task StartSendToAccount(Packet packet, long accountId, IClient exclude)
        {
            long[] sessions = await database.Sessions.AsQueryable()
                              .Where(s => s.AccountId == accountId)
                              .Select(s => s.SessionId)
                              .ToArrayAsync().ConfigureAwait(false);

            foreach (long sessionId in sessions)
            {
                if (connections.TryGet(sessionId, out IClient client) && !ReferenceEquals(client, exclude))
                {
                    _ = client.Send(packet);
                }
            }
        }