Source: core/SocketServer.js

const { Server } = require('socket.io');

const Utils     = require('../Utils.js');
const Client    = require('../Client.js');
const Msgs      = require('./Msgs.js');

/** Error sent to an admin socket that is not logged in; admin pages then go to the login page. */
const ADMIN_LOGIN_REQUIRED = 'jtree: admin login required';

/** Handles socket connections */
class SocketServer {

    constructor(jt) {
        // On a server shared with other apps (see jtree.js), leave WebSocket upgrades for other paths alone.
        this.io = new Server(jt.staticServer.server, {
            path: jt.basePath + '/socket.io',
            destroyUpgrade: jt.httpServer == null,
            // socket.io 2 accepted messages up to 100 MB; admins send whole app files.
            maxHttpBufferSize: 1e8,
            // Experiment pages that load an old socket.io client of their own.
            allowEIO3: true,
        });
        this.jt = jt;
        this.ADMIN_TYPE = 'ADMIN';
        this.msgs         = new Msgs.new(jt); // MESSAGES TO LISTEN FOR FROM CLIENTS
        jt.io = this.io;

        // Admin sockets carry the login of the page that opened them (see AdminAuth).
        const auth = jt.staticServer.auth;
        this.io.engine.use(auth.sessionMiddleware);
        this.io.use((socket, next) => {
            if (socket.handshake.query.type === this.ADMIN_TYPE && !auth.isAuthorized(socket.request)) {
                return next(new Error(ADMIN_LOGIN_REQUIRED));
            }
            next();
        });
        this.io.on('connection', this.onConnection.bind(this));
    }

    /**
     * onConnection - called when a socket connects to the server.
     *
     * @param  {Socket} socket description
     */
    onConnection(socket) {

        var query       = socket.handshake.query;
        var id          = query.id; // participant or admin ID
        var type        = query.type; // ADMIN or PARTI
        var sessionId   = query.sessionId;
        var roomId      = query.roomId;

        if (id === null || id === undefined || id === '') {
            id = socket.handshake.address;
            id = id.substring(id.lastIndexOf(':')+1);
            console.log('new id = ' + id);
        }

        this.jt.log('socket connection: ' + socket.id + ', id=' + id + ', type=' + type + ', session=' + sessionId);

        // Only authorized admins get this far with type ADMIN (see the constructor).
        if (type === this.ADMIN_TYPE) {
            this.addAdminClient(socket);
        } else if (roomId !== 'null') {
            this.addRoomClient(socket, id, roomId);
        } else {
            this.addParticipantClient(socket, id, sessionId, roomId);
        }
    }

    addAdminClient(socket) {
        var sock = socket;
        var self = this;
        var log = this.jt.log;
        log('socket connection to ' + this.ADMIN_TYPE);
        socket.join(this.ADMIN_TYPE);
        socket.join('socket_' + sock.id);

        for (const fnI of Msgs.names()) {
            socket.on(fnI, function(d, cb) {
                try {
                    log('received message ' + fnI + ': ' + JSON.stringify(d));
                    self.msgs[fnI](d, sock);
                    if (typeof cb === 'function') {
                        cb(true);
                    }
                } catch (err) {
                    console.log("Error: " + err + "\n" + err.stack);
                    if (typeof cb === 'function') {
                        cb(false);
                    }
                }
            });
        }

        socket.on('refreshAdmin', function(msg) {
            log('Server.refreshAdmin: socket_' + sock.id);
            self.refreshAdmin(null, 'socket_' + sock.id, msg.userId);
        });

        socket.on('reloadApps', function(msg) {
            self.jt.data.reloadApps();
            if (msg == null) {
                msg = {
                    userId: ''
                }
            }
            self.refreshAdmin(null, 'socket_' + sock.id, msg.userId);
        });

        socket.on('get-app', function(id) {
            var toSend = self.jt.data.apps[id].shell();
            self.io.to(sock.id).emit('get-app', toSend);
        });

        socket.on('session-set-active', function(msg) {
            var session = self.jt.data.getSession(msg.sId);
            session.setActive(msg.active);
        });

        socket.on('refresh-apps', function(msg) {
            self.jt.data.apps = self.jt.data.loadApps();
        });

    }

    addRoomClient(socket, pId, roomId) {
        this.jt.log('server.addRoomClient: ' + socket.id + ', pId=' + pId + ', roomId=' + roomId);
        try {
            var room = this.jt.data.room(roomId);
            room.addClient(socket, pId);
        } catch (err) {
            this.jt.log('server.addRoomClient error:' + err + '\n' + err.stack);
        }

    }

    addParticipantClient(socket, pId, sessionId, roomId) {
        this.jt.log('server.addClient: ' + socket.id + ', pId=' + pId + ', sId=' + sessionId + ', roomId=' + roomId);
        try {
            var session = this.jt.data.getActiveSession(sessionId);
            if (session === null) {
                session = this.jt.data.getMostRecentActiveSession();
            }

            let client = null;
            if (session != null) {
                // Add client to session (automatic notification of admins + client).
                client = session.addClient(socket, pId);
            } else {
                // No session, so notify manually.
                client = new Client.new(socket, null);
                client.pId = pId;
                this.sendOrQueueAdminMsg(null, 'addClient', client.shell());
                let participant = {
                    id: pId,
                    session: {
                        id: 'none'
                    }
                };
                socket.emit('logged-in', participant);
                let socketServer = this;
                socket.on('disconnect', function() {
                    console.log('disconnect for ' + JSON.stringify(client.shell()));
                    socketServer.sendOrQueueAdminMsg(null, 'removeClient', client.shell());
                    Utils.deleteById(socketServer.jt.data.participantClients, client.id);
                });
            }

            // If no fixed session ID, add to list of clients to reset.
            if ((session == null || session.id !== sessionId) && client !== null) {
                this.jt.data.participantClients.push(client);
            }

        } catch (err) {
            this.jt.log('server.addClient error:' + err + '\n' + err.stack);
        }
    }

    adminsRefreshApps() {
        this.io.to(this.ADMIN_TYPE).emit('refresh-apps', system.apps);
    }

    refreshAdmin(msgs, id, userId) {
        this.jt.log('refreshAdmin: ' + id + ', userId = ' + userId);

        var ag = {};
        ag.appFolders           = this.jt.settings.appFolders;
        ag.apps                 = this.jt.data.appsMetaData;
        ag.appsFull             = Utils.shells(this.jt.data.apps);
        ag.predefinedQueues     = this.jt.settings.predefinedQueues;
        ag.rooms                = Utils.shells(this.jt.data.rooms);
        ag.queues                = Utils.shells(this.jt.data.queues);
        ag.sessions             = this.jt.data.sessionsForUser(userId);
        ag.jtreeLocalPath       = this.jt.path;
        ag.settings             = this.jt.settings.shell();
        ag.users                = Utils.shells(this.jt.data.users);

        this.sendOrQueueAdminMsg(msgs, 'refreshAdmin', ag, id);
    }

    // If msgs is null, sends the message immediately.
    // Otherwise, the messages is just added to msgs.
    sendOrQueueAdminMsg(msgs, msgName, msgData, channel) {
        if (msgs == null || msgs == undefined) {
            if (channel == null || channel == undefined) {
                channel = this.ADMIN_TYPE;
            }
            this.io.to(channel).emit(msgName, msgData);
        } else {
            // TODO: add channel information
            msgs.push(new jt.Message(msgName, msgData));
        }
    }

    emitMessages(channel, msgs) {
        this.io.to(channel).emit('messages', msgs);
    }

    refreshAdmins(msgs) {
        this.refreshAdmin(msgs, this.ADMIN_TYPE, null);
    }

    emitToAdmins(msgName, msgData) {
        this.io.to(this.ADMIN_TYPE).emit(msgName, msgData);
    }

}

var exports = module.exports = {};
exports.new = SocketServer;