import { ConnectToLineRequest, RtcAnswerRequest, RtcCallRequest, RtcNewUserResponse, RtcReceiveAnswerResponse, ServerRequestChannels, ServerResponseChannels, SomeoneConnectedResponse, SomeoneTunedResponse, SomeoneUntunedFromLineResponse, StartBroadcastingRequest, StopBroadcastingRequest, TuneToLineRequest, UntuneFromLineRequest, UserStartedBroadcastingResponse, UserStoppedBroadcastingResponse, } from '@nirvana/core/sockets/channels'; import GetAllSocketClients from '@nirvana/core/sockets/getAllActiveSocketClients'; import { JwtClaims } from '../middleware/auth'; import { LineMemberState } from '@nirvana/core/models/line.model'; import { LineService } from '../services/line.service'; import ReceiveSignal from '@nirvana/core/sockets/receiveSignal'; import SendSignal from '@nirvana/core/sockets/sendSignal'; import { UserService } from '../services/user.service'; import { UserStatus } from '@nirvana/core/models/user.model'; import { client } from '../services/database.service'; import { loadConfig } from '../config'; const jwt = require('jsonwebtoken'); const config = loadConfig(); // NOTE: client socket connections should never have to deal with socketIds const socketIdsToUserIds: { [socketId: string]: string; } = {}; const userIdsToSocketIds: { [userId: string]: string; } = {}; export default function InitializeWs(io: any) { console.log('initializing web sockets'); return io .use(function (socket: any, next: any) { console.log('authenticating user...'); try { const { token } = socket.handshake.query; console.log(token); // verify jwt token with our api secret var decoded: JwtClaims = jwt.verify(token, config.JWT_TOKEN_SECRET); socket.userInfo = decoded; next(); } catch (error) { console.error(error); next(new Error('WS Authentication Error')); } }) .on('connection', function (socket: any) { const userInfo: JwtClaims = socket.userInfo; socketIdsToUserIds[socket.id] = userInfo.userId.toString(); userIdsToSocketIds[userInfo.userId.toString()] = socket.id; console.log(`a user connected | user Id: ${userInfo.userId} and name: ${userInfo.name}`); socket.on('test', () => { console.log('asdf'); }); // ?verification that user is in a particular line to be tuned into it or just generally in it? /** CONNECT | User wants to subscribe to live emissions of a line */ socket.on(ServerRequestChannels.CONNECT_TO_LINE, (req: ConnectToLineRequest) => { // add this user to the room console.log(`${socket.id} user CONNECTED room for line ${Object.keys(socket.rooms)}`); const roomName = `connectedLine:${req.lineId}`; socket.join(roomName); console.log(`${socket.id} now in rooms ${socket.rooms}`); const clientUserIdsInRoom = [...(io.sockets.adapter.rooms.get(roomName) ?? [])].map( (otherUserSocketId: string) => socketIdsToUserIds[otherUserSocketId], ); io.in(roomName).emit( ServerResponseChannels.SOMEONE_CONNECTED_TO_LINE, new SomeoneConnectedResponse(req.lineId, userInfo.userId, clientUserIdsInRoom), ); }); /** * TODO: handle when user wants to completely leave a line (delete or removed from one) */ socket.on(ServerRequestChannels.DISCONNECT_FROM_LINE, () => console.log('not implemented')); /** TUNE | User tunes into the line either temporarily or toggled in */ socket.on(ServerRequestChannels.TUNE_INTO_LINE, async (req: TuneToLineRequest) => { console.log(`${socket.id} user TUNED into room for line ${req.lineId}`); const roomName = `tunedLine:${req.lineId}`; socket.join(roomName); console.log(`${socket.id} now in rooms ${Object.keys(socket.rooms)}`); // persist tuning in if user is toggle tuning in if (req.keepTunedIn) { await LineService.updateLineMemberState( req.lineId, userInfo.userId, LineMemberState.TUNED, ); } else { await LineService.updateLineMemberState( req.lineId, userInfo.userId, LineMemberState.INBOX, ); } const clientUserIdsInRoom = [...(io.sockets.adapter.rooms.get(roomName) ?? [])].map( (otherUserSocketId: string) => socketIdsToUserIds[otherUserSocketId], ); // we want to notify everyone connected to the line even if they are not tuned in const connectedLineRoomName = `connectedLine:${req.lineId}`; io.in(connectedLineRoomName).emit( ServerResponseChannels.SOMEONE_TUNED_INTO_LINE, new SomeoneTunedResponse( req.lineId, userInfo.userId, clientUserIdsInRoom, req.keepTunedIn, ), ); }); /** * Notify all connected users when someone UNTUNES from a room * ?might not be needed, all users' memory of tuned in users is irrelevant? don't need real time? but UI will show # of users tuned in? */ socket.on(ServerRequestChannels.UNTUNE_FROM_LINE, async (req: UntuneFromLineRequest) => { const roomName = `tunedLine:${req.lineId}`; socket.leave(roomName); console.log('someone left room'); const clientUserIdsInRoom = [...(io.sockets.adapter.rooms.get(roomName) ?? [])].map( (otherUserSocketId: string) => socketIdsToUserIds[otherUserSocketId], ); // we want to notify everyone connected to the line even if they are not tuned in const connectedLineRoomName = `connectedLine:${req.lineId}`; io.in(connectedLineRoomName).emit( ServerResponseChannels.SOMEONE_UNTUNED_FROM_LINE, new SomeoneUntunedFromLineResponse(req.lineId, userInfo.userId, clientUserIdsInRoom), ); }); // TODO: use same pattern as tuning and untuning and send updated fresh list of current broadcasters but using another namespace/room for broadcasters in a line /** BROADCAST UPDATE | tell all connected, not just tuned into, that there is an update to someone broadcasting */ socket.on(ServerRequestChannels.BROADCAST_TO_LINE, (req: StartBroadcastingRequest) => { const roomName = `connectedLine:${req.lineId}`; io.in(roomName).emit( ServerResponseChannels.SOMEONE_STARTED_BROADCASTING, new UserStartedBroadcastingResponse(req.lineId, userInfo.userId), ); }); socket.on(ServerRequestChannels.STOP_BROADCAST_TO_LINE, (req: StopBroadcastingRequest) => { const roomName = `connectedLine:${req.lineId}`; io.in(roomName).emit( ServerResponseChannels.SOMEONE_STOPPED_BROADCASTING, new UserStoppedBroadcastingResponse(req.lineId, userInfo.userId), ); }); // socket.on(SocketChannels.SEND_SIGNAL, async (payload: SendSignal) => { // console.log(payload); // const sendingBackData: ReceiveSignal = { // simplePeerSignal: payload.simplePeerSignal, // senderUserSocketId: socket.id, // isGoingBackToInitiator: payload.isAnswerer ? true : false, // }; // io.to(payload.userSocketIdToSignal).emit( // SocketChannels.RECEIVE_SIGNAL, // sendingBackData // ); // }); // tell the proper other user to create a local peer object for the one on one mesh connection socket.on(ServerRequestChannels.RTC_CALL_REQUEST, (req: RtcCallRequest) => { const userSocketId = userIdsToSocketIds[req.userIdToCall]; io.to(userSocketId).emit( `${ServerResponseChannels.RTC_NEW_USER_JOINED_RESPONSE_PREFIX}:${req.lineId}`, new RtcNewUserResponse(userInfo.userId, req.simplePeerSignal), ); }); socket.on(ServerRequestChannels.RTC_ANSWER_REQUEST, (req: RtcAnswerRequest) => { const userSocketId = userIdsToSocketIds[req.userIdToCall]; io.to(userSocketId).emit( `${ServerResponseChannels.RTC_RECEIVING_ANSWER_RESPONSE_PREFIX}:${req.lineId}`, new RtcReceiveAnswerResponse(userInfo.userId, req.simplePeerSignal), ); }); // TODO: not complete socket.on(ServerRequestChannels.GOING_INTO_FLOW_STATE, () => { // change user object to persist this state // get all of the connectedLine rooms of this person, and tell them that user is going into flow state for (const roomName of socket.rooms) { if (roomName !== socket.id) { const lineId = roomName.split(':')[1]; if (roomName.includes('connectedLine')) { // get fresh list of tuned in folks without me const clientUserIdsInRoom = [...(io.sockets.adapter.rooms.get(roomName) ?? [])] .filter((mappedSocketId) => mappedSocketId !== socket.id) .map((otherUserSocketId: string) => socketIdsToUserIds[otherUserSocketId]); // io.in(roomName).emit( // ServerResponseChannels.SOMEONE_GOING_INTO_FLOW_STATE, // new SomeoneGoingIntoFlowState( // userInfo.userId // ) // ); } } } }); // tell all connected people that I am disconnecting // tell tuned in folks that I am leaving the room // tell the tuned in folks the new list of socket.on('disconnecting', (reason: any) => { console.log(reason); console.log(socket.rooms); for (const roomName of socket.rooms) { if (roomName !== socket.id) { const lineId = roomName.split(':')[1]; if (roomName.includes('tunedLine')) { // get fresh list of tuned in folks without me const clientUserIdsInRoom = [...(io.sockets.adapter.rooms.get(roomName) ?? [])] .filter((mappedSocketId) => mappedSocketId !== socket.id) .map((otherUserSocketId: string) => socketIdsToUserIds[otherUserSocketId]); io.in(roomName).emit( ServerResponseChannels.SOMEONE_UNTUNED_FROM_LINE, new SomeoneUntunedFromLineResponse(lineId, userInfo.userId, clientUserIdsInRoom), ); } else if (roomName.includes('connectedLine')) { //TODO: p3: client doesn't really to know right now in our flow as this list is not really used // io.in(roomName).emit(ServerResponseChannels.SOMEONE_UNTUNED_FROM_LINE, new SomeoneDisconnected(lineId, userInfo.userId, clientUserIdsInRoom)); } } } }); // ==== DISCONNECT ==== socket.on('disconnect', () => { delete socketIdsToUserIds[socket.id]; delete userIdsToSocketIds[userInfo.userId]; // get all of the rooms of this socket // notify everyone of this disconnection console.log('user disconnected'); console.log(`list of sockets mappings in memory: ${socketIdsToUserIds}`); console.log(socket.rooms); // ! NOTIFY ALL CONNECTED LINES SO THAT THEY CAN REMOVE FROM THEIR SESSION CONNECTED USERS ARRAY AND TUNED IN ARRAY }); }); }