diff --git a/lib/WetSock/Message.js b/lib/WetSock/Message.js new file mode 100644 index 0000000..599cd00 --- /dev/null +++ b/lib/WetSock/Message.js @@ -0,0 +1,33 @@ +/** + * Represents a chat message. + */ +export default class ChatMessage { + #origin; + #data; + #message; + + /** + * Creates an instance of ChatMessage. + * @param {string} rawText - The raw text of the chat message. + */ + constructor(rawText) { + this.#origin = rawText; + try { + this.#data = JSON.parse(rawText.trim()); + this.#message = this.#data.message; + } catch(err) {} + } + + /** + * Emits the properties of the chat message through the provided WebSocket. + * @param {WebSocket} wetsock - The WebSocket instance to emit the properties through. + */ + emitProps(wetsock) { + if (!this.#data || typeof this.#data !== 'object') return; + for (const key in this.#data) { + if (typeof this.#data[key] === 'string' && this.#data[key].indexOf(' ') === -1) { + wetsock.emit(`message:${key}:${this.#data[key]}`, this); + } + } + } +} diff --git a/lib/WetSock/Plugins/BasePlugin.js b/lib/WetSock/Plugins/BasePlugin.js index cc590c4..4c5fd05 100644 --- a/lib/WetSock/Plugins/BasePlugin.js +++ b/lib/WetSock/Plugins/BasePlugin.js @@ -1,17 +1,82 @@ +/** + * BasePlugin class represents the base class for WetSock plugins. + */ export default class BasePlugin { #wetsock = undefined; - constructor() { - } + /** + * Create a new instance of BasePlugin. + */ + constructor() {} + + /** + * Get the WetSock server instance. + * @returns {Object} The WetSock server instance. + * @private + */ _getServer() { return this.#wetsock; } + /** + * Get a list of available actions. + * @returns {Array} The keys of available actions. + */ + actions() { + return []; + } + + /** + * Initialize the plugin with the WetSock server instance. + * @param {Object} wetsock - The WetSock server instance. + */ initialize(wetsock) { this.#wetsock = wetsock; } + /** + * Register an event listener. + * @param {string} event - The event name. + * @param {function} listener - The event listener function. + */ + on(event, listener) { + this.#wetsock.on(event, listener); + } + + /** + * Remove an event listener. + * @param {string} event - The event name. + * @param {function} listener - The event listener function to remove. + */ + off(event, listener) { + this.#wetsock.off(event, listener); + } + + /** + * Emit an event. + * @param {string} event - The event name. + * @param {...*} args - The arguments to pass to the event listeners. + */ + emit(event, ...args) { + this.#wetsock.emit(event, ...args); + } + + /** + * Handle an incoming message from a client. + * @param {string} clientId - The ID of the client. + * @param {*} message - The message received from the client. + */ onMessage(clientId, message) {} + + /** + * Handle a client connection. + * @param {string} clientId - The ID of the connected client. + */ onOpen(clientId) {} + + /** + * Handle a client disconnection. + * @param {string} clientId - The ID of the disconnected client. + */ onClose(clientId) {} } diff --git a/lib/WetSock/SocketClient.js b/lib/WetSock/SocketClient.js new file mode 100644 index 0000000..8638dfd --- /dev/null +++ b/lib/WetSock/SocketClient.js @@ -0,0 +1,27 @@ +/** + * Represents a connected WebSocket client + */ +export default class SocketClient { + #id; + #ws; + + /** + * Create a new websocket server client instance. + * @param {string} id - The unique identifier of the client. + * @param {ServerWebSocket} - The Socket object for sending messages. + */ + constructor(id, ws) { + this.#id = id; + this.#ws = ws; + } + + /** + * Sends a message to the client + * @param {*} message - The data to be sent to the client. Will be converted to string if not a string. + */ + send(message) { + if(typeof(message) != 'string') message = JSON.stringify(message); + + this.#ws.send(message); + } +} diff --git a/lib/WetSock/WetSock.js b/lib/WetSock/WetSock.js index 3b23e5f..27539c2 100644 --- a/lib/WetSock/WetSock.js +++ b/lib/WetSock/WetSock.js @@ -1,61 +1,182 @@ +import SocketClient from './SocketClient'; +import Message from './Message'; +/** + * WetSock class represents the main server class for handling WebSocket connections and plugins. + */ export default class WetSock { #config = undefined; #server = undefined; #clients = new Map(); #plugins = new Map(); + #listeners = new Map(); + + /** + * Create a new instance of WetSock. + * @param {Object} config - The configuration object for the server. + */ constructor(config) { this.#config = config; this.#server = null; } + /** + * Start the server with the provided server instance. + * @param {Object} server - The server instance to start. + */ start(server) { this.#server = server; } - registerPlugin(identifier, plugin) { + /** + * Register a plugin with the specified identifier and priority. + * @param {string} identifier - The identifier for the plugin. + * @param {Object} plugin - The plugin instance to register. + * @param {number} priority - The priority of the plugin (default: 0). + */ + registerPlugin(identifier, plugin, priority = 0) { plugin.initialize(this); - this.#plugins.set(identifier, plugin); + this.#plugins.set(identifier, { plugin, priority }); } + /** + * Get the plugin with the specified identifier. + * @param {string} identifier - The identifier of the plugin. + * @returns {Object} The plugin instance. + */ getPlugin(identifier) { return this.#plugins.get(identifier); } + /** + * Handle the WebSocket upgrade request. + * @param {Object} req - The upgrade request object. + * @returns {Response} The response to send back. + */ handleUpgrade(req) { const clientId = Math.random().toString(36).substring(2); const success = this.#server.upgrade(req, { data: { clientId } }); if (success) { + this.#clients.set(clientId, { id: clientId }); return undefined; } return Response.redirect('/', 302); } - handleMessage(ws, message) { - const client = this.#clients.get(ws.data.clientId); - - try { - const messageData = JSON.parse(message); - if(typeof(messageData) == 'object' && messageData.constructor.name == 'Object') { - message = messageData; - } - } catch (err) {} - - for(const plugin of this.#plugins.values()) { - if(plugin.onMessage(ws.data.clientId, message)) break; // plugin consumes message by returning a truthy value - } - } + /** + * Handle the opening of a WebSocket connection. + * @param {WebSocket} ws - The WebSocket connection. + */ handleOpen(ws) { - const client = this.#clients.get(ws.data.clientId); - this.#plugins.forEach((plugin) => plugin.onOpen(ws.data.clientId)); + const client = new SocketClient(ws.data.clientId, ws); + this.#clients.set(client.id, client); + client.send({ + event: 'plugins', + data: [...this.#plugins.keys()].map(key => { + return { + identifier: key, + actions: this.#plugins.get(key).plugin.actions() + }; + }) + }); + this.emit('open', client); } + /** + * Handle an incoming message from a WebSocket client. + * @param {WebSocket} ws - The WebSocket connection. + * @param {string|Object} message - The message received from the client. + */ + handleMessage(ws, message) { + const client = this.getValidClient(ws); + if (!client) return; + ws.data.messages = Number(ws.data.messages) + 1; + const chatMessage = new Message(message); + chatMessage.emitProps(this); + this.emit('message', client, chatMessage); + } + + + /** + * Handle the closing of a WebSocket connection. + * @param {WebSocket} ws - The WebSocket connection. + */ handleClose(ws) { - const client = this.#clients.get(ws.data.clientId); - this.#plugins.forEach((plugin) => plugin.onClose(ws.data.clientId)); - this.#clients.delete(ws.data.clientId); + const client = this.getValidClient(ws); + if (!client) return; + this.emit('close', client, 'closed'); + this.#clients.delete(client.id); } + /** + * Get the valid client object associated with the WebSocket connection. + * @param {WebSocket} ws - The WebSocket connection. + * @returns {Object|null} The valid client object or null if the client is not valid. + */ + getValidClient(ws) { + const clientId = ws.data?.clientId; + if (!clientId || !this.#clients.has(clientId)) { + ws.close(1011, 'Invalid client'); + this.emit('close', ws, 'invalid'); + return null; + } + return this.#clients.get(clientId); + } + + /** + * Handle the drain event of a WebSocket connection. + * @param {WebSocket} ws - The WebSocket connection. + */ handleDrain(ws) { // Handle socket drain event } + + /** + * Send a message to a specific client. + * @param {string} clientId - The ID of the client to send the message to. + * @param {Object} message - The message to send. + */ + sendTo(clientId, message) { + const client = this.#clients.get(clientId); + if (client) { + client.send(JSON.stringify(message)); + } + } + + /** + * Register an event listener. + * @param {string} event - The event name. + * @param {function} listener - The event listener function. + */ + on(event, listener) { + if (!this.#listeners.has(event)) { + this.#listeners.set(event, []); + } + this.#listeners.get(event).push(listener); + } + + /** + * Remove an event listener. + * @param {string} event - The event name. + * @param {function} listener - The event listener function to remove. + */ + off(event, listener) { + if (this.#listeners.has(event)) { + const listeners = this.#listeners.get(event); + const index = listeners.indexOf(listener); + if (index !== -1) { + listeners.splice(index, 1); + } + } + } + + /** + * Emit an event. + * @param {string} event - The event name. + * @param {...*} args - The arguments to pass to the event listeners. + */ + emit(event, ...args) { + if (this.#listeners.has(event)) { + this.#listeners.get(event).forEach((listener) => listener(...args)); + } + } }