From a4b6c40094357346ffb03551763fb254901f36af Mon Sep 17 00:00:00 2001 From: Lorenzo Miniero Date: Wed, 11 Jun 2025 10:42:47 +0200 Subject: [PATCH 1/5] Add support for TextRoom plugin (Janus API only) --- README.md | 3 +- package.json | 1 + src/plugins/textroom-plugin.js | 515 +++++++++++++++++++++++++++++++++ 3 files changed, 518 insertions(+), 1 deletion(-) create mode 100644 src/plugins/textroom-plugin.js diff --git a/README.md b/README.md index 95d1c30..1d71f9b 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ The supported Janus plugins are: - Streaming - VideoRoom - SIP +- TextRoom (Janus API only) The library is available on [npm](https://www.npmjs.com/package/janode) and the source code is on [github](https://github.com/meetecho/janode). @@ -159,4 +160,4 @@ Then use the npm script: npm run build-docs ``` -Documentation in HTML format will be built under the `docs` folder. \ No newline at end of file +Documentation in HTML format will be built under the `docs` folder. diff --git a/package.json b/package.json index 202daaa..f9fe5ca 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "./plugins/echotest": "./src/plugins/echotest-plugin.js", "./plugins/sip": "./src/plugins/sip-plugin.js", "./plugins/streaming": "./src/plugins/streaming-plugin.js", + "./plugins/textroom": "./src/plugins/textroom-plugin.js", "./plugins/videoroom": "./src/plugins/videoroom-plugin.js" }, "files": [ diff --git a/src/plugins/textroom-plugin.js b/src/plugins/textroom-plugin.js new file mode 100644 index 0000000..2929cd2 --- /dev/null +++ b/src/plugins/textroom-plugin.js @@ -0,0 +1,515 @@ +'use strict'; + +/** + * This module contains the implementation of the TextRoom plugin (ref. {@link https://janus.conf.meetecho.com/docs/textroom.html}). + * Notice this only covers what's possible via the Janus API: messages only sent via datachannels are not covered by this module. + * @module textroom-plugin + */ + +import Handle from '../handle.js'; + +/* The plugin ID exported in the plugin descriptor */ +const PLUGIN_ID = 'janus.plugin.textroom'; + +/* These are the requests defined for the Janus TextRoom API */ +const REQUEST_SETUP = 'setup'; +const REQUEST_ACK = 'ack'; +const REQUEST_RESTART = 'restart'; +const REQUEST_LIST_ROOMS = 'list'; +const REQUEST_LIST_PARTICIPANTS = 'listparticipants'; +const REQUEST_EXISTS = 'exists'; +const REQUEST_CREATE = 'create'; +const REQUEST_ALLOW = 'allowed'; +const REQUEST_ANNOUNCEMENT = 'announcement'; +const REQUEST_KICK = 'kick'; +const REQUEST_DESTROY = 'destroy'; + +/* These are the events/responses that the Janode plugin will manage */ +/* Some of them will be exported in the plugin descriptor */ +const PLUGIN_EVENT = { + ROOMS_LIST: 'textroom_list', + PARTICIPANTS_LIST: 'textroom_participants_list', + EXISTS: 'textroom_exists', + CREATED: 'textroom_created', + DESTROYED: 'textroom_destroyed', + SUCCESS: 'textroom_success', + ERROR: 'textroom_error', +}; + +/** + * The class implementing the TextRoom plugin (ref. {@link https://janus.conf.meetecho.com/docs/textroom.html}). + * Notice this only covers what's possible via the Janus API: messages only sent via datachannels are not covered by this module.
+ * + * It extends the base Janode Handle class and overrides the base "handleMessage" method.
+ * + * Moreover it defines many methods to support TextRoom operations. + * + * @hideconstructor + * @extends module:handle~Handle + */ +class TextRoomHandle extends Handle { + /** + * Create a Janode TextRoom handle. + * + * @param {module:session~Session} session - A reference to the parent session + * @param {number} id - The handle identifier + */ + constructor(session, id) { + super(session, id); + } + + /** + * The custom "handleMessage" needed for handling TextRoom messages. + * + * @private + * @param {Object} janus_message + * @returns {Object} A falsy value for unhandled events, a truthy value for handled events + */ + handleMessage(janus_message) { + const { plugindata, transaction } = janus_message; + if (plugindata && plugindata.data && plugindata.data.textroom) { + /** + * @type {TextRoomData} + */ + const message_data = plugindata.data; + const { textroom, error, error_code, room } = message_data; + + /* Prepare an object for the output Janode event */ + const janode_event = this._newPluginEvent(janus_message); + + /* Add room information if available */ + if (room) janode_event.data.room = room; + + /* The plugin will emit an event only if the handle does not own the transaction */ + /* That means that a transaction has already been closed or this is an async event */ + const emit = (this.ownsTransaction(transaction) === false); + + switch (textroom) { + + /* success response */ + case 'success': + /* Room exists API */ + if (typeof message_data.exists !== 'undefined') { + janode_event.data.exists = message_data.exists; + janode_event.event = PLUGIN_EVENT.EXISTS; + break; + } + /* Room list API */ + if (typeof message_data.rooms !== 'undefined') { + janode_event.data.rooms = message_data.rooms; + janode_event.event = PLUGIN_EVENT.ROOMS_LIST; + break; + } + /* Participants list API */ + if (typeof message_data.participants !== 'undefined') { + janode_event.data.participants = message_data.participants; + janode_event.event = PLUGIN_EVENT.PARTICIPANTS_LIST; + break; + } + + /* Generic success (might be token disable) */ + if (typeof message_data.allowed !== 'undefined') { + janode_event.data.list = message_data.allowed; + } + /* In this case the "event" field of the Janode event is "success" */ + janode_event.event = PLUGIN_EVENT.SUCCESS; + break; + + /* Audio bridge room created */ + case 'created': + janode_event.event = PLUGIN_EVENT.CREATED; + janode_event.data.permanent = message_data.permanent; + break; + + /* Audio bridge room destroyed */ + case 'destroyed': + janode_event.event = PLUGIN_EVENT.DESTROYED; + break; + + /* Generic event (e.g. errors) */ + case 'event': + /* TextRoom error */ + if (error) { + janode_event.event = PLUGIN_EVENT.ERROR; + janode_event.data = new Error(`${error_code} ${error}`); + /* In case of error, close a transaction */ + this.closeTransactionWithError(transaction, janode_event.data); + break; + } + /* Configuration success for this handle */ + if (typeof message_data.result !== 'undefined') { + if (message_data.result === 'ok') { + janode_event.event = PLUGIN_EVENT.SUCCESS; + } + break; + } + } + + /* The event has been handled */ + if (janode_event.event) { + /* Try to close the transaction */ + this.closeTransactionWithSuccess(transaction, janus_message); + /* If the transaction was not owned, emit the event */ + if (emit) this.emit(janode_event.event, janode_event.data); + return janode_event; + } + } + + /* The event has not been handled, return a falsy value */ + return null; + } + + /*----------*/ + /* USER API */ + /*----------*/ + + /* These are the APIs that users need to work with the textroom plugin */ + + /** + * Setup a datachannel connection. + * + * @returns {Promise} + */ + async setup() { + const body = { + request: REQUEST_SETUP, + }; + + const response = await this.message(body); + const { event, data: evtdata } = this._getPluginEvent(response); + if (event === PLUGIN_EVENT.SUCCESS) + return evtdata; + const error = new Error(`unexpected response to ${body.request} request`); + throw (error); + } + + /** + * Complete the setup or restart of a datachannel connection. + * + * @param {Object} params + * @param {RTCSessionDescription} params.jsep + * @returns {Promise} + */ + async ack(jsep) { + const body = { + request: REQUEST_ACK, + }; + + const response = await this.message(body, jsep); + const { event, data: evtdata } = this._getPluginEvent(response); + if (event === PLUGIN_EVENT.SUCCESS) + return evtdata; + const error = new Error(`unexpected response to ${body.request} request`); + throw (error); + } + + /** + * Restart the setup of a datachannel connection. + * + * @returns {Promise} + */ + async restart() { + const body = { + request: REQUEST_RESTART, + }; + + const response = await this.message(body); + const { event, data: evtdata } = this._getPluginEvent(response); + if (event === PLUGIN_EVENT.SUCCESS) + return evtdata; + const error = new Error(`unexpected response to ${body.request} request`); + throw (error); + } + + /*----------------*/ + /* Management API */ + /*----------------*/ + + /* These are the APIs needed to manage textroom resources (rooms, forwarders ...) */ + + /** + * List available textroom rooms. + * + * @param {Object} params + * @param {string} [params.admin_key] - The admin key needed for invoking the API + * @returns {Promise} + */ + async list({ admin_key }) { + const body = { + request: REQUEST_LIST_ROOMS, + }; + if (typeof admin_key === 'string') body.admin_key = admin_key; + + const response = await this.message(body); + const { event, data: evtdata } = this._getPluginEvent(response); + if (event === PLUGIN_EVENT.ROOMS_LIST) + return evtdata; + const error = new Error(`unexpected response to ${body.request} request`); + throw (error); + } + + /** + * List participants inside a room. + * + * @param {Object} params + * @param {number|string} params.room - The room where to execute the list + * @returns {Promise} + */ + async listParticipants({ room, secret }) { + const body = { + request: REQUEST_LIST_PARTICIPANTS, + room, + }; + if (typeof secret === 'string') body.secret = secret; + + const response = await this.message(body); + const { event, data: evtdata } = this._getPluginEvent(response); + if (event === PLUGIN_EVENT.PARTICIPANTS_LIST) + return evtdata; + const error = new Error(`unexpected response to ${body.request} request`); + throw (error); + } + + /** + * Check if a room exists. + * + * @param {Object} params + * @param {number|string} params.room - The involved room + * @returns {Promise} + */ + async exists({ room }) { + const body = { + request: REQUEST_EXISTS, + room, + }; + + const response = await this.message(body); + const { event, data: evtdata } = this._getPluginEvent(response); + if (event === PLUGIN_EVENT.EXISTS) + return evtdata; + const error = new Error(`unexpected response to ${body.request} request`); + throw (error); + } + + /** + * Create an textroom room. + * + * @param {Object} params + * @param {number|string} params.room - The room identifier + * @param {string} [params.admin_key] - The admin key needed for invoking the API + * @param {string} [params.description] - A room description + * @param {string} [params.secret] - The secret to be used when managing the room + * @param {string} [params.pin] - The ping needed for joining the room + * @param {boolean} [params.is_private] - Set room as private (hidden in list) + * @param {boolean} [params.history] - Set number of messages to store as a history + * @param {boolean} [params.post] - Set HTTP backend to forward incoming chat messages to + * @param {boolean} [params.permanent] - Set to true to persist the room in the Janus config file + * @returns {Promise} + */ + async create({ room, admin_key, description, secret, pin, is_private, history, post, permanent }) { + const body = { + request: REQUEST_CREATE, + room, + }; + if (typeof admin_key === 'string') body.admin_key = admin_key; + if (typeof description === 'string') body.description = description; + if (typeof secret === 'string') body.secret = secret; + if (typeof pin === 'string') body.pin = pin; + if (typeof is_private === 'boolean') body.is_private = is_private; + if (typeof history === 'number') body.history = history; + if (typeof post === 'string') body.post = post; + if (typeof permanent === 'boolean') body.permanent = permanent; + + const response = await this.message(body); + const { event, data: evtdata } = this._getPluginEvent(response); + if (event === PLUGIN_EVENT.CREATED) + return evtdata; + const error = new Error(`unexpected response to ${body.request} request`); + throw (error); + } + + /** + * Edit an textroom token list. + * + * @param {Object} params + * @param {number|string} params.room - The involved room + * @param {"enable"|"disable"|"add"|"remove"} params.action - The action to perform + * @param {string[]} params.list - The list of tokens to add/remove + * @param {string} [params.secret] - The optional secret needed to manage the room + * @returns {Promise} + */ + async allow({ room, action, list, secret }) { + const body = { + request: REQUEST_ALLOW, + room, + action, + }; + if (list && list.length > 0) body.allowed = list; + if (typeof secret === 'string') body.secret = secret; + + const response = await this.message(body); + const { event, data: evtdata } = this._getPluginEvent(response); + if (event === PLUGIN_EVENT.SUCCESS) + return evtdata; + const error = new Error(`unexpected response to ${body.request} request`); + throw (error); + } + + /** + * Send an announcement to a textroom room. + * + * @param {Object} params + * @param {number|string} params.room - The involved room + * @param {string} params.text - The content of the announcement, as text + * @param {string} [params.secret] - The optional secret needed to manage the room + * @returns {Promise} + */ + async announcement({ room, text, secret }) { + const body = { + request: REQUEST_ANNOUNCEMENT, + room, + text, + }; + if (typeof secret === 'string') body.secret = secret; + + const response = await this.message(body); + const { event, data: evtdata } = this._getPluginEvent(response); + if (event === PLUGIN_EVENT.SUCCESS) + return evtdata; + const error = new Error(`unexpected response to ${body.request} request`); + throw (error); + } + + /** + * Kick an user out from a room. + * + * @param {Object} params + * @param {number|string} params.room - The involved room + * @param {string} params.username - The user to kick out + * @param {string} [params.secret] - The optional secret needed for managing the room + * @returns {Promise} + */ + async kick({ room, feed, secret }) { + const body = { + request: REQUEST_KICK, + room, + id: feed, + }; + if (typeof secret === 'string') body.secret = secret; + + const response = await this.message(body); + const { event, data: evtdata } = this._getPluginEvent(response); + if (event === PLUGIN_EVENT.SUCCESS) { + /* Add data missing from Janus response */ + evtdata.room = body.room; + evtdata.feed = body.id; + return evtdata; + } + const error = new Error(`unexpected response to ${body.request} request`); + throw (error); + } + + /** + * Destroy an textroom room. + * + * @param {Object} params + * @param {number|string} params.room - The room to destroy + * @param {boolean} [params.permanent] - Set to true to remove the room from the Janus config file + * @param {string} [params.secret] - The optional secret needed to manage the room + * @returns {Promise} + */ + async destroy({ room, permanent, secret }) { + const body = { + request: REQUEST_DESTROY, + room, + }; + if (typeof permanent === 'boolean') body.permanent = permanent; + if (typeof secret === 'string') body.secret = secret; + + const response = await this.message(body); + const { event, data: evtdata } = this._getPluginEvent(response); + if (event === PLUGIN_EVENT.DESTROYED) + return evtdata; + const error = new Error(`unexpected response to ${body.request} request`); + throw (error); + } + +} + +/** + * The payload of the plugin message (cfr. Janus docs). + * {@link https://janus.conf.meetecho.com/docs/textroom.html} + * + * @private + * @typedef {Object} TextRoomData + */ + +/** + * The response event for textroom room list request. + * + * @typedef {Object} TEXTROOM_EVENT_ROOMS_LIST + * @property {object[]} list - The list of the rooms as returned by Janus + */ + +/** + * The response event for textroom participants list request. + * + * @typedef {Object} TEXTROOM_EVENT_PARTICIPANTS_LIST + * @property {number|string} room - The involved room + * @property {object[]} participants - The list of participants as returned by Janus + */ + +/** + * The response event for textroom room exists request. + * + * @typedef {Object} TEXTROOM_EVENT_EXISTS + * @property {number|string} room - The involved room + * @property {boolean} exists - True if the rooms exists + */ + +/** + * The response event for textroom room create request. + * + * @typedef {Object} TEXTROOM_EVENT_CREATED + * @property {number|string} room - The created room + * @property {boolean} permanent - True if the room is being persisted in the Janus config file + */ + +/** + * The response event for textroom room destroy request. + * + * @typedef {Object} TEXTROOM_EVENT_DESTROYED + * @property {number|string} room - The destroyed room + */ + +/** + * The response event for textroom ACL token edit request. + * + * @typedef {Object} TEXTROOM_EVENT_ALLOWED + * @property {number|string} room - The involved room + * @property {string[]} list - The updated, complete, list of allowed tokens + */ + +/** + * The exported plugin descriptor. + * + * @type {Object} + * @property {string} id - The plugin identifier used when attaching to Janus + * @property {module:textroom-plugin~TextRoomHandle} Handle - The custom class implementing the plugin + * @property {Object} EVENT - The events emitted by the plugin + * @property {string} EVENT.TEXTROOM_ERROR {@link module:textroom-plugin~TextRoomHandle#event:TEXTROOM_ERROR TEXTROOM_ERROR} + */ +export default { + id: PLUGIN_ID, + Handle: TextRoomHandle, + + EVENT: { + /** + * Generic textroom error. + * + * @event module:textroom-plugin~TextRoomHandle#event:TEXTROOM_ERROR + * @type {Error} + */ + TEXTROOM_ERROR: PLUGIN_EVENT.ERROR, + }, +}; From d2282b5751ae4280650a6e100784048f813de0cd Mon Sep 17 00:00:00 2001 From: Lorenzo Miniero Date: Wed, 11 Jun 2025 10:47:35 +0200 Subject: [PATCH 2/5] Fixed typos --- src/plugins/textroom-plugin.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/textroom-plugin.js b/src/plugins/textroom-plugin.js index 2929cd2..a8df80f 100644 --- a/src/plugins/textroom-plugin.js +++ b/src/plugins/textroom-plugin.js @@ -115,13 +115,13 @@ class TextRoomHandle extends Handle { janode_event.event = PLUGIN_EVENT.SUCCESS; break; - /* Audio bridge room created */ + /* TextRoom room created */ case 'created': janode_event.event = PLUGIN_EVENT.CREATED; janode_event.data.permanent = message_data.permanent; break; - /* Audio bridge room destroyed */ + /* TextRoom room destroyed */ case 'destroyed': janode_event.event = PLUGIN_EVENT.DESTROYED; break; From db1c0c1dfdc3cdf5e5dd6740f21d2b376221c5de Mon Sep 17 00:00:00 2001 From: Lorenzo Miniero Date: Thu, 12 Jun 2025 11:20:49 +0200 Subject: [PATCH 3/5] Addressed comments in review --- src/plugins/textroom-plugin.js | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/src/plugins/textroom-plugin.js b/src/plugins/textroom-plugin.js index a8df80f..66c8a64 100644 --- a/src/plugins/textroom-plugin.js +++ b/src/plugins/textroom-plugin.js @@ -124,6 +124,7 @@ class TextRoomHandle extends Handle { /* TextRoom room destroyed */ case 'destroyed': janode_event.event = PLUGIN_EVENT.DESTROYED; + janode_event.data.permanent = message_data.permanent; break; /* Generic event (e.g. errors) */ @@ -292,7 +293,7 @@ class TextRoomHandle extends Handle { } /** - * Create an textroom room. + * Create a textroom room. * * @param {Object} params * @param {number|string} params.room - The room identifier @@ -329,14 +330,14 @@ class TextRoomHandle extends Handle { } /** - * Edit an textroom token list. + * Edit a textroom token list. * * @param {Object} params * @param {number|string} params.room - The involved room * @param {"enable"|"disable"|"add"|"remove"} params.action - The action to perform * @param {string[]} params.list - The list of tokens to add/remove * @param {string} [params.secret] - The optional secret needed to manage the room - * @returns {Promise} + * @returns {Promise} */ async allow({ room, action, list, secret }) { const body = { @@ -389,11 +390,11 @@ class TextRoomHandle extends Handle { * @param {string} [params.secret] - The optional secret needed for managing the room * @returns {Promise} */ - async kick({ room, feed, secret }) { + async kick({ room, username, secret }) { const body = { request: REQUEST_KICK, room, - id: feed, + username: username, }; if (typeof secret === 'string') body.secret = secret; @@ -402,7 +403,7 @@ class TextRoomHandle extends Handle { if (event === PLUGIN_EVENT.SUCCESS) { /* Add data missing from Janus response */ evtdata.room = body.room; - evtdata.feed = body.id; + evtdata.username = body.username; return evtdata; } const error = new Error(`unexpected response to ${body.request} request`); @@ -410,7 +411,7 @@ class TextRoomHandle extends Handle { } /** - * Destroy an textroom room. + * Destroy a textroom room. * * @param {Object} params * @param {number|string} params.room - The room to destroy @@ -444,6 +445,12 @@ class TextRoomHandle extends Handle { * @typedef {Object} TextRoomData */ +/** + * The response event for textroom WebRTC establishment. + * + * @typedef {Object} TEXTROOM_EVENT_SUCCESS + */ + /** * The response event for textroom room list request. * @@ -480,12 +487,21 @@ class TextRoomHandle extends Handle { * * @typedef {Object} TEXTROOM_EVENT_DESTROYED * @property {number|string} room - The destroyed room + * @property {boolean} permanent - True if the room removal is being persisted in the Janus config file + */ + +/** + * The response event for textroom participant kick request. + * + * @typedef {Object} TEXTROOM_EVENT_KICK_RESPONSE + * @property {number|string} room - The involved room + * @property {string} username - The username that has been kicked out */ /** * The response event for textroom ACL token edit request. * - * @typedef {Object} TEXTROOM_EVENT_ALLOWED + * @typedef {Object} TEXTROOM_EVENT_ALLOWED_RESPONSE * @property {number|string} room - The involved room * @property {string[]} list - The updated, complete, list of allowed tokens */ From ca85da66013fa0210aca89184151c124c1a84732 Mon Sep 17 00:00:00 2001 From: Lorenzo Miniero Date: Thu, 12 Jun 2025 11:35:44 +0200 Subject: [PATCH 4/5] Minor fixes --- src/plugins/textroom-plugin.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/textroom-plugin.js b/src/plugins/textroom-plugin.js index 66c8a64..ce1e7ba 100644 --- a/src/plugins/textroom-plugin.js +++ b/src/plugins/textroom-plugin.js @@ -363,7 +363,7 @@ class TextRoomHandle extends Handle { * @param {number|string} params.room - The involved room * @param {string} params.text - The content of the announcement, as text * @param {string} [params.secret] - The optional secret needed to manage the room - * @returns {Promise} + * @returns {Promise} */ async announcement({ room, text, secret }) { const body = { @@ -394,7 +394,7 @@ class TextRoomHandle extends Handle { const body = { request: REQUEST_KICK, room, - username: username, + username, }; if (typeof secret === 'string') body.secret = secret; From 3c72435f9f8ecd6328454388c4b80c1794aeeb82 Mon Sep 17 00:00:00 2001 From: Lorenzo Miniero Date: Thu, 12 Jun 2025 12:22:55 +0200 Subject: [PATCH 5/5] Fixed management of 'list' response --- src/plugins/textroom-plugin.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/textroom-plugin.js b/src/plugins/textroom-plugin.js index ce1e7ba..6cb30d1 100644 --- a/src/plugins/textroom-plugin.js +++ b/src/plugins/textroom-plugin.js @@ -95,8 +95,8 @@ class TextRoomHandle extends Handle { break; } /* Room list API */ - if (typeof message_data.rooms !== 'undefined') { - janode_event.data.rooms = message_data.rooms; + if (typeof message_data.list !== 'undefined') { + janode_event.data.list = message_data.list; janode_event.event = PLUGIN_EVENT.ROOMS_LIST; break; }