const SocketController = require('flitter-socket/Controller') const uuid = require('uuid').v4 function getColor(){ return "hsl(" + 360 * Math.random() + ',' + (25 + 70 * Math.random()) + '%,' + (85 + 10 * Math.random()) + '%)' } class EditorGroup { constructor(resource_id, resource) { this.id = resource_id this.resource = resource this.connections = {} this.connection_id_x_session = {} } join(connection_manager) { this.connections[connection_manager.id] = connection_manager this.connection_id_x_session[connection_manager.id] = {} connection_manager.on_close().then(() => { delete this.connection_id_x_session[connection_manager.id] delete this.connections[connection_manager.id] this.blast_editor_group_users() }) } has(connection_manager) { return !!this.connections[connection_manager.id] } unique_users() { const users = {} const conns = Object.values(this.connections) for ( const conn of conns ) { const session = this.session(conn) const user = conn.request.user if ( user && !users[user.uuid] ) { if ( !session.user_color ) { session.user_color = getColor() // '#' + Math.floor(Math.random() * 16777215).toString(16) while ( session.user_color.length < 7 ) { session.user_color += '5' } } users[user.uuid] = { user, color: session.user_color } } } return Object.values(users) } session(conn) { return this.connection_id_x_session[conn.id] } blast_editor_group_users() { const conns = Object.values(this.connections) const unique_users = this.unique_users() console.log('blast_editor_group_users', unique_users) for ( const conn of conns ) { const users = unique_users.filter(data => data.user.uuid !== conn.request.user.uuid) .map(data => { const { user, color } = data return { uuid: user.uuid, uid: user.uid, display: user.uid, color, } }) conn._request('setEditorGroupUsers', { users }) } } blast_operations(operations, exclude_connection_id) { const conns = Object.values(this.connections) for ( const conn of conns ) { if ( conn.id === exclude_connection_id ) continue; conn._request('applyRemoteOperation', { operations }) } } blast_position(position, uuid, exclude_connection_id) { const conns = Object.values(this.connections) for ( const conn of conns ) { if ( conn.id === exclude_connection_id ) continue; conn._request('updateCursorPosition', { position, uuid }) } } blast_selection(startPosition, endPosition, uuid, exclude_connection_id) { const conns = Object.values(this.connections) for ( const conn of conns ) { if ( conn.id === exclude_connection_id ) continue; conn._request('updateSelection', { startPosition, endPosition, uuid }) } } } class EditorController extends SocketController { editor_groups = {} static get services() { return [...super.services, 'output', 'models'] } ping(transaction, socket) { this.output.info('Got norm editor socket ping.') transaction.status(200) .message('Pong!') .send(transaction.incoming) } async apply(transaction, socket) { const group = this._get_editor_group_or_fail(transaction) if ( !group ) return // FIXME validate const operations = transaction.incoming.operations.map(x => typeof x === 'string' ? JSON.parse(x) : x) await group.blast_operations(operations, transaction.cm.id) transaction.send() } async update_cursor(transaction, socket) { const group = this._get_editor_group_or_fail(transaction) if ( !group ) return // FIXME validate const position = transaction.incoming.position const uuid = transaction.incoming.uuid await group.blast_position(position, uuid, transaction.cm.id) transaction.send() } async update_selection(transaction, socket) { const group = this._get_editor_group_or_fail(transaction) if ( !group ) return // FIXME validate const start = transaction.incoming.startPosition const end = transaction.incoming.endPosition const uuid = transaction.incoming.uuid await group.blast_selection(start, end, uuid, transaction.cm.id) transaction.send() } async subscribe(transaction, socket) { if ( !transaction.incoming.resource_id ) { return transaction.status(400) .message('Missing resource ID.') .send() } const resource = await this._get_resource(transaction.incoming.resource_id) if ( !resource ) { return transaction.status(404) .message('Invalid resource ID.') .send() } // Try to look up an existing editor group let editor_group = this.editor_groups[transaction.incoming.resource_id] if ( !editor_group ) { // create a new editor group editor_group = new EditorGroup(transaction.incoming.resource_id, resource) this.editor_groups[transaction.incoming.resource_id] = editor_group } if ( !editor_group.has(transaction.cm) ) editor_group.join(transaction.cm) transaction.status(200) .message('Joined editor group.') .send({ editor_group_id: transaction.incoming.resource_id, local_user: { uid: transaction.cm.request.user.uid, uuid: transaction.cm.request.user.uuid, color: 'black', display: transaction.cm.request.user.uid, } }) editor_group.blast_editor_group_users() } async _get_resource(id) { const Node = this.models.get('api:Node') return Node.findOne({ UUID: id }) } _get_editor_group_or_fail(transaction) { if ( !transaction.incoming.editor_group_id ) { transaction.status(400) .message('Missing editor_group_id.') .send() return } const editor_group = this.editor_groups[transaction.incoming.editor_group_id] if ( !editor_group || !editor_group.has(transaction.cm) ) { transaction.status(400) .message('Invalid editor_group_id.') .send() return } return editor_group } _client_connected(connection_manager) { super._client_connected(connection_manager) this.output.debug('New client connected to editor socket.') if ( !connection_manager.request?.session?.auth?.user_id ) { delete this.connections[connection_manager.id] connection_manager.socket.close() } } } module.exports = exports = EditorController