import BasePlugin from '../BasePlugin'; /** * Plugin for handling room-related operations. */ export default class RoomPlugin extends BasePlugin { #rooms = new Map(); #clients = new Map(); #config = { allowMultipleRooms: true, alwaysMainChannel: false, notifyMuteUnmute: false, notifyKick: false, inactivityTimeout: 60_000, }; /** * Initialize the plugin. * @param {Object} wetsock - The WetSock server instance. * @param {Object} config - The configuration options for the plugin. */ initialize(wetsock, config = {}) { super.initialize(wetsock); this.#config = { ...this.#config, ...config }; wetsock.on('message', this.onMessage.bind(this)); wetsock.on('close', this.onClose.bind(this)); setInterval(this.#purge.bind(this), this.#config.inactivityTimeout); } /** * Handle incoming messages from clients. * @param {Object} client - The client object. * @param {Object} message - The message object. * @returns {boolean} - True if the message was handled, false otherwise. */ onMessage(client, message) { if (typeof message === 'object' && message.type && this[message.type]) { return this[message.type](client, message); } return false; } /** * Handle a client disconnecting. * @param {Object} client - The client object. */ onClose(client) { const clientRooms = this.#clients.get(client.id); if (clientRooms) { for (const roomId of clientRooms) { const room = this.#rooms.get(roomId); if (room) { room.members.delete(client.id); this._getServer().publish(roomId, `${client.id} has left the room`); } } this.#clients.delete(client.id); } } actions () { return Object.getOwnPropertyNames( Object.getPrototypeOf(this) ).filter((prop) => { return ![ "constructor", "initialize", "onMessage", "onClose", "actions" ].includes(prop) && typeof this[prop] === 'function' }); } /** * Handle a client joining a room. * @param {Object} client - The client object. * @param {Object} message - The message object. * @param {string} message.roomId - The ID of the room to join. */ join(client, { roomId }) { const room = this.#rooms.get(roomId); if (room) { const memberData = room.members.get(client.id); if (memberData && memberData.kicked) { client.send({ type: 'error', message: 'You have been kicked from this room and cannot rejoin' }); return; } if (!this.#config.allowMultipleRooms && this.#clients.has(client.id)) { client.send({ type: 'error', message: 'You can only be in one room at a time' }); return; } client.ws.subscribe(roomId); room.add(client.id, { muted: memberData ? memberData.muted : false, lastActivity: Date.now() }); this.#clients.set(client.id, (this.#clients.get(client.id) || new Set()).add(roomId)); this._getServer().publish(roomId, `${client.id} has joined the room`); } else { client.send({ type: 'error', message: `Room ${roomId} does not exist` }); } } /** * Handle a client leaving a room. * @param {Object} client - The client object. * @param {Object} message - The message object. * @param {string} message.roomId - The ID of the room to leave. */ leave(client, { roomId }) { const room = this.#rooms.get(roomId); if (room) { client.ws.unsubscribe(roomId); room.members.delete(client.id); const clientRooms = this.#clients.get(client.id); if (clientRooms) { clientRooms.delete(roomId); if (clientRooms.size === 0 && !this.#config.alwaysMainChannel) { this.#clients.delete(client.id); } } this._getServer().publish(roomId, `${client.id} has left the room`); } else { client.send({ type: 'error', message: `Room ${roomId} does not exist` }); } } /** * Handle creating a new room. * @param {Object} client - The client object. * @param {Object} message - The message object. * @param {string} message.roomId - The ID of the room to create. */ create(client, { roomId }) { if (this.#rooms.has(roomId)) { client.send({ type: 'error', message: `Room ${roomId} already exists` }); return; } const room = new Room(roomId); this.#rooms.set(roomId, room); client.ws.subscribe(roomId); room.add(client.id, { muted: false, lastActivity: Date.now() }); this.#clients.set(client.id, (this.#clients.get(client.id) || new Set()).add(roomId)); // Initialize the permissions for the room this._getServer().emit('permissions:init', this.constructor.name, roomId, (permissions) => { room.permissions = permissions; permissions.role(client.id, 'OWNER'); }); client.send({ type: 'success', message: `Room ${roomId} created successfully` }); } /** * Handle sending a message to a room. * @param {Object} client - The client object. * @param {Object} message - The message object. * @param {string} message.roomId - The ID of the room to send the message to. * @param {string} message.content - The content of the message. * @returns {boolean} - True if the message was sent successfully, false otherwise. */ send(client, { roomId, content }) { const room = this.#rooms.get(roomId); if (room && room.get(client.id)) { if (!room.permissions.can(client.id, 'SEND_MESSAGE')) { client.send({ type: 'error', message: 'You do not have permission to send messages in this room' }); return false; } const memberData = room.members.get(client.id); if (memberData.muted) { client.send({ type: 'error', message: 'You are muted and cannot send messages in this room' }); return false; } room.update(client.id); this._getServer().publish(roomId, `${client.id}: ${content}`); this.emit('rooms:message', client, message.roomId, message.content); this.emit(`rooms:message:${message.roomId}`, client, message.content); return true; } else { client.send({ type: 'error', message: 'You are not allowed to send messages in this room' }); return false; } } /** * Handle kicking a member from a room. * @param {Object} client - The client object. * @param {Object} message - The message object. * @param {string} message.roomId - The ID of the room. * @param {string} message.memberId - The ID of the member to kick. */ kick(client, { roomId, memberId }) { const room = this.#rooms.get(roomId); if (room && room.get(memberId)) { if (!room.permissions.can(client.id, 'KICK_MEMBER')) { client.send({ type: 'error', message: 'You do not have permission to kick members from this room' }); return; } const member = this._getServer().getValidClient(memberId); if (member) { member.ws.unsubscribe(roomId); room.members.set(memberId, { id: memberId, muted: true, kicked: Date.now() }); const clientRooms = this.#clients.get(memberId); if (clientRooms) { clientRooms.delete(roomId); if (clientRooms.size === 0 && !this.#config.alwaysMainChannel) { this.#clients.delete(memberId); } } if (this.#config.notifyKick) { this._getServer().publish(roomId, `${memberId} has been kicked from the room`); } member.send({ type: 'kicked', roomId }); } client.send({ type: 'success', message: `Member ${memberId} has been kicked from room ${roomId}` }); } else { client.send({ type: 'error', message: `Cannot kick member ${memberId} from room ${roomId}` }); } } /** * Handle muting a member in a room. * @param {Object} client - The client object. * @param {Object} message - The message object. * @param {string} message.roomId - The ID of the room. * @param {string} message.memberId - The ID of the member to mute. */ mute(client, { roomId, memberId }) { const room = this.#rooms.get(roomId); if (room && room.get(memberId)) { if (!room.permissions.can(client.id, 'MUTE_MEMBER')) { client.send({ type: 'error', message: 'You do not have permission to mute members in this room' }); return; } room.members.get(memberId).muted = true; const member = this._getServer().getValidClient(memberId); if (member && this.#config.notifyMuteUnmute) { member.send({ type: 'muted', roomId }); } client.send({ type: 'success', message: `Member ${memberId} has been muted in room ${roomId}` }); } else { client.send({ type: 'error', message: `Cannot mute member ${memberId} in room ${roomId}` }); } } /** * Handle unmuting a member in a room. * @param {Object} client - The client object. * @param {Object} message - The message object. * @param {string} message.roomId - The ID of the room. * @param {string} message.memberId - The ID of the member to unmute. */ unmute(client, { roomId, memberId }) { const room = this.#rooms.get(roomId); if (room && room.get(memberId)) { if (!room.permissions.can(client.id, 'UNMUTE_MEMBER')) { client.send({ type: 'error', message: 'You do not have permission to unmute members in this room' }); return; } room.members.get(memberId).muted = false; const member = this._getServer().getValidClient(memberId); if (member && this.#config.notifyMuteUnmute) { member.send({ type: 'unmuted', roomId }); } client.send({ type: 'success', message: `Member ${memberId} has been unmuted in room ${roomId}` }); } else { client.send({ type: 'error', message: `Cannot unmute member ${memberId} in room ${roomId}` }); } } /** * Check for inactive members in each room and kick them. */ #purge() { for (const [roomId, room] of this.#rooms.entries()) { const now = Date.now(); for (const [memberId, memberData] of room.members.entries()) { if (now - memberData.lastActivity > this.#config.inactivityTimeout) { this.kick(null, { roomId, memberId }); } } } } /** * Broadcast a message to a specific room or all rooms. * @param {string} roomId - The ID of the room to broadcast to. If not provided, the message will be broadcast to all rooms. * @param {string} message - The message to broadcast. */ #broadcast(message, roomId) { if (this.#rooms.has(roomId)) { this._getServer().publish(roomId, message); return; } if(roomId == 'all') { for (const room of this.#rooms.values()) { this._getServer().publish(room.id, message); } } } }