/* * Copyright (c) 2022. * Author Peter Placzek (tada5hi) * For the full copyright and license information, * view the LICENSE file that was distributed with this source code. */ import {Context} from "@nuxt/types"; import {Socket} from "socket.io-client"; import {SecurityKeyPair} from "~/modules/security"; import {decodeContactMessage, encodeContactMessage} from "~/domains/contact/encryption"; import {decodeChatRoomMessage, encodeChatRoomMessage} from "~/domains/chat-room/encryption"; export interface ChatRoom extends Record { id: string, contactId: string | null, [key: string]: any } export interface ChatRoomRegistration extends Record { subscriptions: number, room: { id: string, contactId: string | null, [key: string]: any } } export class ChatModule { protected ctx: Context; private roomRegistrations: Map; public socket: Socket; constructor(ctx: Context) { this.ctx = ctx; this.roomRegistrations = new Map(); this.socket = this.ctx.$socket.use('/chat'); this.initSocketHandler(); this.subscribeStore(); } //------------------------------------------------------- async joinRoom(room: ChatRoom) { return new Promise(((resolve, reject) => { const connectionTimeout = setTimeout(() => { return reject(new Error('Die Verbindung konnte nicht aufgebaut werden...')); },10000); return this.socket.emit('join', { id: room.id, type: 'chat-room' }, (data) => { clearTimeout(connectionTimeout); if(data.success) { return this.registerRoom(room).then(() => { return resolve(data.data); }).catch(reject); } else { return reject(new Error(data.message)); } }); })); } async leaveRoom(roomId: string) { return new Promise(((resolve, reject) => { const connectionTimeout = setTimeout(() => { return reject(new Error('Die Verbindung konnte nicht getrennt werden...')); },10000); return this.socket.emit('leave', { id: roomId, type: 'chat-room', }, (data) => { clearTimeout(connectionTimeout); this.unregisterRoom(roomId); this.ctx.store.dispatch('chat/dropRoomMemberOnline', roomId).then(r => r); if(data.success) { return resolve(data.success); } else { return reject(new Error(data.message)); } }); })); } public getRegistration(roomId: string) : ChatRoomRegistration { const room : undefined | ChatRoomRegistration = this.roomRegistrations.get(roomId); if(typeof room === 'undefined') { throw new Error('Der Raum #'+roomId+' wurde noch nicht registriert.'); } return room; } private setRegistration(roomId: string, registration: ChatRoomRegistration) { this.roomRegistrations.set(roomId, registration); } public async registerRoom(room: ChatRoom) { if(this.roomRegistrations.has(room.id)) { const registration = this.roomRegistrations.get(room.id); registration.subscriptions++; this.setRegistration(room.id, registration); return; } const registration : ChatRoomRegistration = { subscriptions: 1, room: room }; this.setRegistration(room.id, registration); this.loadSockets(room.id).then(r => r); //await this.initEncryption(room.id); } public unregisterRoom(roomId: string) { if(this.roomRegistrations.has(roomId)) { const room = this.roomRegistrations.get(roomId); room.subscriptions--; if(room.subscriptions === 0) { this.roomRegistrations.delete(roomId); } else { this.setRegistration(roomId, room); } } } //------------------------------------------------------- // Socket //------------------------------------------------------- private initSocketHandler() { const handler = (event, data) => { switch (event) { case 'userOnline': this.handleSocketUserOnlineEvent(data); break; case 'userOffline': this.handleSocketUserOfflineEvent(data); break; case 'reconnect': this.handleSocketReconnect(); break; case 'connectionClose': this.handleSocketConnectionCloseEvent(data); break; } }; handler.bind(this); this.socket.onAny(handler); } private async loadAllSockets() { await this.roomRegistrations.forEach((async (value, key) => { await this.loadSockets(key); })) } private async loadSockets(roomId: string) { return new Promise(((resolve, reject) => { this.socket.emit('onlineUsers', { id: roomId, type: 'chat-room', }, (response) => { if(typeof response === 'object') { if (response.success) { const {data} = response.data; const items : Record[] = []; for (let i = 0; i < data.length; i++) { items.push({id: data[i].id, roomId: roomId}); } this.ctx.store.dispatch('chat/setMembersOnline', items).then(r => r); resolve(data); } else { reject(new Error()); } } }); })); } private handleSocketReconnect() { this.loadAllSockets().then(r => r); } private handleSocketConnectionCloseEvent(data) { this.ctx.store.dispatch('chat/dropRoomMemberOnline', data.id).then(r => r); } private handleSocketUserOnlineEvent(data) { this.ctx.store.dispatch('chat/addMemberOnline', {roomId: data.meta.roomId, id: data.id}).then(r => r); } private handleSocketUserOfflineEvent(data) { this.ctx.store.dispatch('chat/dropMemberOnline', {roomId: data.meta.roomId, id: data.id}).then(r => r); } //------------------------------------------------------- // Security //------------------------------------------------------- encodeMessage(roomId: string, message: string) { const registration = this.getRegistration(roomId); if(registration.room.contactId) { return encodeContactMessage(message, registration.room.contactId); } else { return encodeChatRoomMessage(message, registration.room.id); } } decodeMessage(roomId: string, message: string) { const registration = this.getRegistration(roomId); if(registration.room.contactId) { return decodeContactMessage(message, registration.room.contactId); } else { return decodeChatRoomMessage(message, registration.room.id); } } //------------------------------------------------------- private subscribeStore() { this.ctx.store.subscribe((mutation: any) => { switch (mutation.type) { case 'auth/unsetToken': this.ctx.$socket.reconnect('/chat'); break; case 'auth/setToken': this.ctx.$socket.reconnect('/chat'); break; } }); } }