/* * server component for the TimeLimit App * Copyright (C) 2019 - 2022 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, version 3 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ import { EventEmitter } from 'events' import { Server } from 'socket.io' import { ConnectedDevicesManager, VisibleConnectedDevicesManager } from '../connected-devices' import { Database } from '../database' import { deviceByAuthTokenRoom } from './rooms' export const createWebsocketHandler = ({ connectedDevicesManager, database }: { connectedDevicesManager: VisibleConnectedDevicesManager database: Database }): { websocketServer: Server websocketApi: WebsocketApi } => { const events = new EventEmitter() // this disables warnings for many listeners // this is required because every single socket causes listeners events.setMaxListeners(0) const eventTriggerImportantSyncForAll = 'triggerimportantsyncforall' let socketCounter = 0 const server = new Server({ allowEIO3: true }) server.on('connection', (socket) => { socketCounter++ socket.on('disconnect', () => socketCounter--) socket.on('devicelogin', (deviceAuthToken: unknown, ack: unknown) => { socket.rooms.forEach((room) => socket.leave(room)) if (typeof deviceAuthToken !== 'string') { return } socket.join(deviceByAuthTokenRoom(deviceAuthToken)) const importantSyncForAllListener = () => { setTimeout(() => { socket.connected && socket.emit('should sync', { isImportant: true }) }, Math.random() * 1000 * 60 /* wait up to one minute */) } events.on(eventTriggerImportantSyncForAll, importantSyncForAllListener) socket.on('disconnect', () => events.removeListener(eventTriggerImportantSyncForAll, importantSyncForAllListener)) ;(async () => { const deviceEntryUnsafe = await database.device.findOne({ where: { deviceAuthToken }, attributes: ['familyId', 'deviceId'] }) if (deviceEntryUnsafe && socket.connected) { const deviceEntry = { familyId: deviceEntryUnsafe.familyId, deviceId: deviceEntryUnsafe.deviceId } const key = ConnectedDevicesManager.buildKey({ familyId: deviceEntry.familyId, deviceId: deviceEntry.deviceId }) connectedDevicesManager.connectedDevicesManager.reportDeviceConnected({ key }) const { shutdown } = connectedDevicesManager.observeConnectedDevicesOfFamily({ familyId: deviceEntry.familyId, listener: (connectedDevices) => { socket.emit('connected devices', connectedDevices) } }) socket.on('disconnect', () => { connectedDevicesManager.connectedDevicesManager.reportDeviceDisconnected({ key }) shutdown() }) } })().catch(() => { /* ignore */ }) if (typeof ack === 'function') { ack() } }) }) const websocketApi: WebsocketApi = { triggerSyncByDeviceAuthToken: ({ deviceAuthToken, isImportant }: {deviceAuthToken: string, isImportant: boolean}) => { server .to(deviceByAuthTokenRoom(deviceAuthToken)) .emit('should sync', { isImportant }) }, triggerLogoutByDeviceAuthToken: ({ deviceAuthToken }: {deviceAuthToken: string}) => { server .to(deviceByAuthTokenRoom(deviceAuthToken)) .emit('sign out') }, triggerImportantSyncAtAllDevicesInBackground: () => { events.emit(eventTriggerImportantSyncForAll) }, countConnections: () => socketCounter } return { websocketServer: server, websocketApi } } export interface WebsocketApi { triggerSyncByDeviceAuthToken: (params: {deviceAuthToken: string, isImportant: boolean}) => void triggerLogoutByDeviceAuthToken: (params: {deviceAuthToken: string}) => void triggerImportantSyncAtAllDevicesInBackground: () => void countConnections: () => number }