From 7ee9021d8e6de0a4f5ac74a4068cb150b506bb30 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Tue, 26 Sep 2023 11:23:43 -0300 Subject: [PATCH 001/154] !refactor: initial v3 with modules system --- .eslintignore | 3 + .eslintrc.yml | 16 + .nvmrc | 2 +- Dockerfile | 19 +- app.js | 5 +- application.js | 69 +- config/custom-environment-variables.yml | 40 +- config/default.example.yml | 120 +- hook.js | 338 -- id_mapping.js | 215 - logger.js | 16 - messageMapping.js | 512 -- package-lock.json | 4604 +++++++++++++---- package.json | 26 +- responses.js | 46 - src/common/logger.js | 115 + utils.js => src/common/utils.js | 58 +- src/db/redis/base-storage.js | 261 + src/db/redis/hooks.js | 155 + src/db/redis/id-mapping.js | 128 + src/db/redis/index.js | 124 + src/db/redis/user-mapping.js | 81 + src/in/redis/index.js | 111 + src/modules/context.js | 41 + src/modules/definitions.js | 71 + src/modules/index.js | 132 + src/modules/module-wrapper.js | 197 + src/modules/queue.js | 78 + src/out/webhooks/api/api.js | 227 + src/out/webhooks/api/responses.js | 67 + .../out/webhooks/callback-emitter.js | 86 +- src/out/webhooks/index.js | 63 + src/out/webhooks/web-hooks.js | 128 + src/process/event-processor.js | 121 + src/process/event.js | 436 ++ userMapping.js | 200 - web_hooks.js | 149 - web_server.js | 177 - 38 files changed, 6470 insertions(+), 2767 deletions(-) create mode 100644 .eslintignore create mode 100644 .eslintrc.yml delete mode 100644 hook.js delete mode 100644 id_mapping.js delete mode 100644 logger.js delete mode 100644 messageMapping.js delete mode 100644 responses.js create mode 100644 src/common/logger.js rename utils.js => src/common/utils.js (66%) create mode 100644 src/db/redis/base-storage.js create mode 100644 src/db/redis/hooks.js create mode 100644 src/db/redis/id-mapping.js create mode 100644 src/db/redis/index.js create mode 100644 src/db/redis/user-mapping.js create mode 100644 src/in/redis/index.js create mode 100644 src/modules/context.js create mode 100644 src/modules/definitions.js create mode 100644 src/modules/index.js create mode 100644 src/modules/module-wrapper.js create mode 100644 src/modules/queue.js create mode 100644 src/out/webhooks/api/api.js create mode 100644 src/out/webhooks/api/responses.js rename callback_emitter.js => src/out/webhooks/callback-emitter.js (65%) create mode 100644 src/out/webhooks/index.js create mode 100644 src/out/webhooks/web-hooks.js create mode 100644 src/process/event-processor.js create mode 100644 src/process/event.js delete mode 100644 userMapping.js delete mode 100644 web_hooks.js delete mode 100644 web_server.js diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..7c8251d --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +node_modules +*.log +*~ diff --git a/.eslintrc.yml b/.eslintrc.yml new file mode 100644 index 0000000..254f8e6 --- /dev/null +++ b/.eslintrc.yml @@ -0,0 +1,16 @@ +env: + node: true + es2023: true +extends: + - eslint:recommended + - plugin:import/recommended +parserOptions: + sourceType: module +rules: + #quotes: ["warn", "single"] + no-console: "warn" + consistent-return: "warn" + no-trailing-spaces: "warn" + no-whitespace-before-property: "warn" + no-multiple-empty-lines: ["warn", { max: 1 }] + import/no-extraneous-dependencies: "error" diff --git a/.nvmrc b/.nvmrc index a2f28f4..a77793e 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -8.4.0 +lts/hydrogen diff --git a/Dockerfile b/Dockerfile index 13b93b1..5b2fdf6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,18 +1,19 @@ -FROM node:18-slim - -ENV NODE_ENV production +FROM node:20-alpine WORKDIR /app -ADD package.json package-lock.json /app/ +COPY package.json package-lock.json ./ -RUN npm install \ - && npm cache clear --force +ENV NODE_ENV production -ADD . /app +RUN npm ci --omit=dev -RUN cp config/default.example.yml config/default.yml +COPY . . + +RUN cp config/default.example.yml config/local.yml EXPOSE 3005 -CMD ["node", "app.js"] +USER node + +CMD [ "npm", "start" ] diff --git a/app.js b/app.js index 0a47b27..4edc02e 100755 --- a/app.js +++ b/app.js @@ -1,5 +1,4 @@ -// This is a simple wrapper to run the app with 'node app.js' +import Application from './application.js'; -Application = require('./application.js'); -application = new Application(); +const application = new Application(); application.start(); diff --git a/application.js b/application.js index f36223d..557bd2f 100644 --- a/application.js +++ b/application.js @@ -1,50 +1,33 @@ -const config = require('config'); -const Hook = require("./hook.js"); -const IDMapping = require("./id_mapping.js"); -const WebHooks = require("./web_hooks.js"); -const WebServer = require("./web_server.js"); -const redis = require("redis"); -const UserMapping = require("./userMapping.js"); -const async = require("async"); - -// Class that defines the application. Listens for events on redis and starts the -// process to perform the callback calls. -// TODO: add port (-p) and log level (-l) to the command line args -module.exports = class Application { +import config from 'config'; +import Logger from './src/common/logger.js'; +import ModuleManager from './src/modules/index.js'; +import EventProcessor from './src/process/event-processor.js'; +export default class Application { constructor() { - this.webHooks = new WebHooks(); - this.webServer = new WebServer(); - } + this.moduleManager = new ModuleManager(config.get("modules")); + this.eventProcessor = null; - start(callback) { - Hook.initialize(() => { - UserMapping.initialize(() => { - IDMapping.initialize(()=> { - async.parallel([ - (callback) => { this.webServer.start(config.get("server.port"), config.get("server.bind"), callback) }, - (callback) => { this.webServer.createPermanents(callback) }, - (callback) => { this.webHooks.start(callback) } - ], (err,results) => { - if(err != null) {} - typeof callback === 'function' ? callback() : undefined; - }); - }); - }); - }); + this._initialized = false; } - static redisPubSubClient() { - if (!Application._redisPubSubClient) { - Application._redisPubSubClient = redis.createClient( { host: config.get("redis.host"), port: config.get("redis.port") } ); - } - return Application._redisPubSubClient; - } + async start() { + if (this._initialized) return Promise.resolve(); + + await this.moduleManager.load(); + this.eventProcessor = new EventProcessor( + this.moduleManager.getInputModules(), + this.moduleManager.getOutputModules(), + ); + await this.eventProcessor.start(); - static redisClient() { - if (!Application._redisClient) { - Application._redisClient = redis.createClient( { host: config.get("redis.host"), port: config.get("redis.port") } ); - } - return Application._redisClient; + return Promise.all([ + ]).then(() => { + Logger.info("bbb-webhooks started"); + this._initialized = true; + }).catch((error) => { + Logger.error("Error starting bbb-webhooks", error); + process.exit(1); + }); } -}; +} diff --git a/config/custom-environment-variables.yml b/config/custom-environment-variables.yml index 2468014..b191581 100644 --- a/config/custom-environment-variables.yml +++ b/config/custom-environment-variables.yml @@ -2,16 +2,36 @@ bbb: serverDomain: SERVER_DOMAIN sharedSecret: SHARED_SECRET auth2_0: BEARER_AUTH -hooks: - permanentURLs: - __name: PERMANENT_HOOKS - __format: json - requestTimeout: - __name: REQUEST_TIMEOUT - __format: json + redis: host: REDIS_HOST port: REDIS_PORT -server: - bind: SERVER_BIND_IP - port: SERVER_PORT + +log: + level: LOG_LEVEL + filename: LOG_FILE + stdout: LOG_STDOUT + +modules: + ../db/redis/index.js: + config: + host: REDIS_HOST + port: REDIS_PORT + ../in/redis/index.js: + config: + redis: + host: REDIS_HOST + port: REDIS_PORT + ../out/webhooks/index.js: + config: + api: + bind: SERVER_BIND_IP + port: SERVER_PORT + queue: + enabled: ENABLE_WH_QUEUE + permanentURLs: + __name: PERMANENT_HOOKS + __format: json + requestTimeout: + __name: REQUEST_TIMEOUT + __format: json diff --git a/config/default.example.yml b/config/default.example.yml index 30117c2..c65e146 100644 --- a/config/default.example.yml +++ b/config/default.example.yml @@ -1,3 +1,8 @@ +log: + level: debug + filename: /var/log/bigbluebutton/bbb-webhooks.log + stdout: true + # Shared secret of your BigBlueButton server. bbb: serverDomain: myserver.com @@ -6,51 +11,6 @@ bbb: auth2_0: false apiPath: /bigbluebutton/api -# The port in which the API server will run. -server: - bind: 127.0.0.1 - port: 3005 - -# Web hooks configs -hooks: - channels: - - from-akka-apps-redis-channel - - from-bbb-web-redis-channel - - from-akka-apps-chat-redis-channel - - from-akka-apps-pres-redis-channel - - bigbluebutton:from-bbb-apps:meeting - - bigbluebutton:from-bbb-apps:users - - bigbluebutton:from-rap - # IP where permanent hook will post data (more than 1 URL means more than 1 permanent hook) - permanentURLs: [] - # How many messages will be enqueued to be processed at the same time - queueSize: 10000 - # Allow permanent hooks to receive raw message, which is the message straight from BBB - getRaw: true - # If set to higher than 1, will send events on the format: - # "event=[{event1},{event2}],timestamp=000" or "[{event1},{event2}]" (based on using auth2_0 or not) - # when there are more than 1 event on the queue at the moment of processing the queue. - multiEvent: 1 - # Retry intervals for failed attempts for perform callback calls. - # In ms. Totals to around 5min. - retryIntervals: - - 100 - - 500 - - 1000 - - 2000 - - 4000 - - 8000 - - 10000 - - 30000 - - 60000 - - 60000 - - 60000 - - 60000 - # Reset permanent interval when exceeding maximum attemps - permanentIntervalReset: 8 - # Hook's request module timeout for socket conn establishment and/or responses (ms) - requestTimeout: 5000 - # Mappings of internal to external meeting IDs mappings: cleanupInterval: 10000 # 10 secs, in ms @@ -68,3 +28,73 @@ redis: eventsPrefix: bigbluebutton:webhooks:events userMaps: bigbluebutton:webhooks:userMaps userMapPrefix: bigbluebutton:webhooks:userMap + +# Basic module config entry template: +# key: , +# type: in|out|db, +# config: (optional) +# +modules: + ../db/redis/index.js: + type: db + config: + host: 127.0.0.1 + port: 6379 + ../in/redis/index.js: + type: in + config: + redis: + host: 127.0.0.1 + port: 6379 + #password: foobar + inboundChannels: + - from-akka-apps-redis-channel + - from-bbb-web-redis-channel + - from-akka-apps-chat-redis-channel + - from-akka-apps-pres-redis-channel + - bigbluebutton:from-bbb-apps:meeting + - bigbluebutton:from-bbb-apps:users + - bigbluebutton:from-rap + ../out/webhooks/index.js: + type: out + config: + api: + bind: 127.0.0.1 + port: 3005 + queue: + enabled: false + maxSize: 10000 + concurrency: 1 + # IP where permanent hook will post data (more than 1 URL means more than 1 permanent hook) + permanentURLs: [] + # How many messages will be enqueued to be processed at the same time + queueSize: 10000 + # Allow permanent hooks to receive raw message, which is the message straight from BBB + getRaw: true + # If set to higher than 1, will send events on the format: + # "event=[{event1},{event2}],timestamp=000" or "[{event1},{event2}]" (based on using auth2_0 or not) + # when there are more than 1 event on the queue at the moment of processing the queue. + multiEvent: 1 + # Retry intervals for failed attempts for perform callback calls. + # In ms. Totals to around 5min. + retryIntervals: + - 100 + - 500 + - 1000 + - 2000 + - 4000 + - 8000 + - 10000 + - 30000 + - 60000 + - 60000 + - 60000 + - 60000 + # Reset permanent interval when exceeding maximum attemps + permanentIntervalReset: 8 + # Hook's request module timeout for socket conn establishment and/or responses (ms) + requestTimeout: 5000 + retry: + attempts: 12 + initialInterval: 1 + increaseFactor: 2 diff --git a/hook.js b/hook.js deleted file mode 100644 index 972d50e..0000000 --- a/hook.js +++ /dev/null @@ -1,338 +0,0 @@ -const _ = require("lodash"); -const async = require("async"); -const redis = require("redis"); - -const config = require('config'); -const CallbackEmitter = require("./callback_emitter.js"); -const IDMapping = require("./id_mapping.js"); -const Logger = require("./logger.js"); - -// The database of hooks. -// Used always from memory, but saved to redis for persistence. -// -// Format: -// { id: Hook } -// Format on redis: -// * a SET "...:hooks" with all ids -// * a HASH "...:hook:" for each hook with some of its attributes -let db = {}; -let nextID = 1; - -// The representation of a hook and its properties. Stored in memory and persisted -// to redis. -// Hooks can be global, receiving callback calls for events from all meetings on the -// server, or for a specific meeting. If an `externalMeetingID` is set in the hook, -// it will only receive calls related to this meeting, otherwise it will be global. -// Events are kept in a queue to be sent in the order they are received. -// But if the requests are going by but taking too long, the queue might be increasing -// faster than the callbacks are made. In this case the events will be concatenated -// and send up to 10 events in every post - -module.exports = class Hook { - - constructor() { - this.id = null; - this.callbackURL = null; - this.externalMeetingID = null; - this.eventID = null; - this.queue = []; - this.emitter = null; - this.redisClient = Application.redisClient(); - this.permanent = false; - this.getRaw = false; - } - - save(callback) { - this.redisClient.hmset(config.get("redis.keys.hookPrefix") + ":" + this.id, this.toRedis(), (error, reply) => { - if (error != null) { Logger.error(`[Hook] error saving hook to redis: ${error} ${reply}`); } - this.redisClient.sadd(config.get("redis.keys.hooks"), this.id, (error, reply) => { - if (error != null) { Logger.error(`[Hook] error saving hookID to the list of hooks: ${error} ${reply}`); } - - db[this.id] = this; - (typeof callback === 'function' ? callback(error, db[this.id]) : undefined); - }); - }); - } - - destroy(callback) { - this.redisClient.srem(config.get("redis.keys.hooks"), this.id, (error, reply) => { - if (error != null) { Logger.error(`[Hook] error removing hookID from the list of hooks: ${error} ${reply}`); } - this.redisClient.del(config.get("redis.keys.hookPrefix") + ":" + this.id, error => { - if (error != null) { Logger.error(`[Hook] error removing hook from redis: ${error}`); } - - if (db[this.id]) { - delete db[this.id]; - (typeof callback === 'function' ? callback(error, true) : undefined); - } else { - (typeof callback === 'function' ? callback(error, false) : undefined); - } - }); - }); - } - - // Is this a global hook? - isGlobal() { - return (this.externalMeetingID == null); - } - - // The meeting from which this hook should receive events. - targetMeetingID() { - return this.externalMeetingID; - } - - // Puts a new message in the queue. Will also trigger a processing in the queue so this - // message might be processed instantly. - enqueue(message) { - // If the event is not in the hook's event list, skip it. - if ((this.eventID != null) && ((message == null) || (message.data == null) || (message.data.id == null) || (!this.eventID.some( ev => ev == message.data.id.toLowerCase() )))) { - Logger.info(`[Hook] ${this.callbackURL} skipping message from queue because not in event list for hook: ${JSON.stringify(message)}`); - } - else { - this.redisClient.llen(config.get("redis.keys.eventsPrefix") + ":" + this.id, (error, reply) => { - const length = reply; - if (length < config.get("hooks.queueSize") && this.queue.length < config.get("hooks.queueSize")) { - Logger.info(`[Hook] ${this.callbackURL} enqueueing message: ${JSON.stringify(message)}`); - // Add message to redis queue - this.redisClient.rpush(config.get("redis.keys.eventsPrefix") + ":" + this.id, JSON.stringify(message), (error,reply) => { - if (error != null) { Logger.error(`[Hook] error pushing event to redis queue: ${JSON.stringify(message)} ${error}`); } - }); - this.queue.push(JSON.stringify(message)); - this._processQueue(); - } else { - Logger.warn(`[Hook] ${this.callbackURL} queue size exceed, event: ${JSON.stringify(message)}`); - } - }); - } - } - - toRedis() { - const r = { - "hookID": this.id, - "callbackURL": this.callbackURL, - "permanent": this.permanent, - "getRaw": this.getRaw - }; - if (this.externalMeetingID != null) { r.externalMeetingID = this.externalMeetingID; } - if (this.eventID != null) { r.eventID = this.eventID.join(); } - return r; - } - - fromRedis(redisData) { - this.id = parseInt(redisData.hookID); - this.callbackURL = redisData.callbackURL; - this.permanent = redisData.permanent.toLowerCase() == 'true'; - this.getRaw = redisData.getRaw.toLowerCase() == 'true'; - if (redisData.externalMeetingID != null) { - this.externalMeetingID = redisData.externalMeetingID; - } else { - this.externalMeetingID = null; - } - if (redisData.eventID != null) { - this.eventID = redisData.eventID.toLowerCase().split(','); - } else { - this.eventID = null; - } - } - - // Gets the first message in the queue and start an emitter to send it. Will only do it - // if there is no emitter running already and if there is a message in the queue. - _processQueue() { - // Will try to send up to a defined number of messages together if they're enqueued (defined on config.get("hooks.multiEvent")) - const lengthIn = this.queue.length > config.get("hooks.multiEvent") ? config.get("hooks.multiEvent") : this.queue.length; - let num = lengthIn + 1; - // Concat messages - let message = this.queue.slice(0,lengthIn); - message = message.join(","); - - if ((message == null) || (this.emitter != null) || (lengthIn <= 0)) { return; } - // Add params so emitter will 'know' when a hook is permanent and have backupURLs - this.emitter = new CallbackEmitter(this.callbackURL, message, this.permanent); - this.emitter.start(); - - this.emitter.on("success", () => { - delete this.emitter; - while ((num -= 1)) { - // Remove the sent message from redis - this.redisClient.lpop(config.get("redis.keys.eventsPrefix") + ":" + this.id, (error, reply) => { - if (error != null) { return Logger.error(`[Hook] error removing event from redis queue: ${error}`); } - }); - this.queue.shift(); - } // pop the first message just sent - this._processQueue(); // go to the next message - }); - - // gave up trying to perform the callback, remove the hook forever if the hook's not permanent (emmiter will validate that) - return this.emitter.on("stopped", error => { - Logger.warn(`[Hook] too many failed attempts to perform a callback call, removing the hook for: ${this.callbackURL}`); - this.destroy(); - }); - } - - static addSubscription(callbackURL, meetingID, eventID, getRaw, callback) { - let hook = Hook.findByCallbackURLSync(callbackURL); - if (hook != null) { - return (typeof callback === 'function' ? callback(new Error("There is already a subscription for this callback URL"), hook) : undefined); - } else { - let msg = `[Hook] adding a hook with callback URL: [${callbackURL}],`; - if (meetingID != null) { msg += ` for the meeting: [${meetingID}]`; } - Logger.info(msg); - - hook = new Hook(); - hook.callbackURL = callbackURL; - hook.externalMeetingID = meetingID; - if (eventID != null) { - hook.eventID = eventID.toLowerCase().split(','); - } - hook.getRaw = getRaw; - hook.permanent = config.get("hooks.permanentURLs").some( obj => { - return obj.url === callbackURL - }); - if (hook.permanent) { - hook.id = config.get("hooks.permanentURLs").map(obj => obj.url).indexOf(callbackURL) + 1; - nextID = config.get("hooks.permanentURLs").length + 1; - } else { - hook.id = nextID++; - } - // Sync permanent queue - if (hook.permanent) { - hook.redisClient.llen(config.get("redis.keys.eventsPrefix") + ":" + hook.id, (error, len) => { - if (len > 0) { - const length = len; - hook.redisClient.lrange(config.get("redis.keys.eventsPrefix") + ":" + hook.id, 0, len, (error, elements) => { - elements.forEach(element => { - hook.queue.push(element); - }); - if (hook.queue.length > 0) { return hook._processQueue(); } - }); - } - }); - } - hook.save((error, hook) => { typeof callback === 'function' ? callback(error, hook) : undefined }); - } - } - - static removeSubscription(hookID, callback) { - let hook = Hook.getSync(hookID); - if (hook != null && !hook.permanent) { - let msg = `[Hook] removing the hook with callback URL: [${hook.callbackURL}],`; - if (hook.externalMeetingID != null) { msg += ` for the meeting: [${hook.externalMeetingID}]`; } - Logger.info(msg); - - hook.destroy((error, removed) => { typeof callback === 'function' ? callback(error, removed) : undefined }); - } else { - return (typeof callback === 'function' ? callback(null, false) : undefined); - } - } - - static countSync() { - return Object.keys(db).length; - } - - static getSync(id) { - return db[id]; - } - - static firstSync() { - const keys = Object.keys(db); - if (keys.length > 0) { - return db[keys[0]]; - } else { - return null; - } - } - - static findByExternalMeetingIDSync(externalMeetingID) { - const hooks = Hook.allSync(); - return _.filter(hooks, hook => (externalMeetingID != null) && (externalMeetingID === hook.externalMeetingID)); - } - - static allGlobalSync() { - const hooks = Hook.allSync(); - return _.filter(hooks, hook => hook.isGlobal()); - } - - static allSync() { - let arr = Object.keys(db).reduce(function(arr, id) { - arr.push(db[id]); - return arr; - } - , []); - return arr; - } - - static clearSync() { - for (let id in db) { - delete db[id]; - } - return db = {}; - } - - static findByCallbackURLSync(callbackURL) { - for (let id in db) { - if (db[id].callbackURL === callbackURL) { - return db[id]; - } - } - } - - static initialize(callback) { - Hook.resync(callback); - } - - // Gets all hooks from redis to populate the local database. - // Calls `callback()` when done. - static resync(callback) { - let client = Application.redisClient(); - // Remove previous permanent hooks - for (let hk = 1; hk <= config.get("hooks.permanentURLs").length; hk++) { - client.srem(config.get("redis.keys.hooks"), hk, (error, reply) => { - if (error != null) { Logger.error(`[Hook] error removing previous permanent hook from list: ${error}`); } - client.del(config.get("redis.keys.hookPrefix") + ":" + hk, error => { - if (error != null) { Logger.error(`[Hook] error removing previous permanent hook from redis: ${error}`); } - }); - }); - } - - let tasks = []; - - client.smembers(config.get("redis.keys.hooks"), (error, hooks) => { - if (error != null) { Logger.error(`[Hook] error getting list of hooks from redis: ${error}`); } - hooks.forEach(id => { - tasks.push(done => { - client.hgetall(config.get("redis.keys.hookPrefix") + ":" + id, function(error, hookData) { - if (error != null) { Logger.error(`[Hook] error getting information for a hook from redis: ${error}`); } - - if (hookData != null) { - let length; - let hook = new Hook(); - hook.fromRedis(hookData); - // sync events queue - client.llen(config.get("redis.keys.eventsPrefix") + ":" + hook.id, (error, len) => { - length = len; - client.lrange(config.get("redis.keys.eventsPrefix") + ":" + hook.id, 0, len, (error, elements) => { - elements.forEach(element => { - hook.queue.push(element); - }); - }); - }); - // Persist hook to redis - hook.save( (error, hook) => { - if (hook.id >= nextID) { nextID = hook.id + 1; } - if (hook.queue.length > 0) { hook._processQueue(); } - done(null, hook); - }); - } else { - done(null, null); - } - }); - }); - }); - - async.series(tasks, function(errors, result) { - hooks = _.map(Hook.allSync(), hook => `[${hook.id}] ${hook.callbackURL}`); - Logger.info(`[Hook] finished resync, hooks registered: ${hooks}`); - (typeof callback === 'function' ? callback() : undefined); - }); - }); - } -}; diff --git a/id_mapping.js b/id_mapping.js deleted file mode 100644 index 7c7764f..0000000 --- a/id_mapping.js +++ /dev/null @@ -1,215 +0,0 @@ -const _ = require("lodash"); -const async = require("async"); -const redis = require("redis"); - -const config = require('config'); -const Logger = require("./logger.js"); -const UserMapping = require("./userMapping.js"); - -// The database of mappings. Uses the internal ID as key because it is unique -// unlike the external ID. -// Used always from memory, but saved to redis for persistence. -// -// Format: -// { -// internalMeetingID: { -// id: @id -// externalMeetingID: @externalMeetingID -// internalMeetingID: @internalMeetingID -// lastActivity: @lastActivity -// } -// } -// Format on redis: -// * a SET "...:mappings" with all ids (not meeting ids, the object id) -// * a HASH "...:mapping:" for each mapping with all its attributes -const db = {}; -let nextID = 1; - -// A simple model to store mappings for meeting IDs. -module.exports = class IDMapping { - - constructor() { - this.id = null; - this.externalMeetingID = null; - this.internalMeetingID = null; - this.lastActivity = null; - this.redisClient = Application.redisClient(); - } - - save(callback) { - this.redisClient.hmset(config.get("redis.keys.mappingPrefix") + ":" + this.id, this.toRedis(), (error, reply) => { - if (error != null) { Logger.error(`[IDMapping] error saving mapping to redis: ${error} ${reply}`); } - this.redisClient.sadd(config.get("redis.keys.mappings"), this.id, (error, reply) => { - if (error != null) { Logger.error(`[IDMapping] error saving mapping ID to the list of mappings: ${error} ${reply}`); } - - db[this.internalMeetingID] = this; - (typeof callback === 'function' ? callback(error, db[this.internalMeetingID]) : undefined); - }); - }); - } - - destroy(callback) { - this.redisClient.srem(config.get("redis.keys.mappings"), this.id, (error, reply) => { - if (error != null) { Logger.error(`[IDMapping] error removing mapping ID from the list of mappings: ${error} ${reply}`); } - this.redisClient.del(config.get("redis.keys.mappingPrefix") + ":" + this.id, error => { - if (error != null) { Logger.error(`[IDMapping] error removing mapping from redis: ${error}`); } - - if (db[this.internalMeetingID]) { - delete db[this.internalMeetingID]; - (typeof callback === 'function' ? callback(error, true) : undefined); - } else { - (typeof callback === 'function' ? callback(error, false) : undefined); - } - }); - }); - } - - toRedis() { - const r = { - "id": this.id, - "internalMeetingID": this.internalMeetingID, - "externalMeetingID": this.externalMeetingID, - "lastActivity": this.lastActivity - }; - return r; - } - - fromRedis(redisData) { - this.id = parseInt(redisData.id); - this.externalMeetingID = redisData.externalMeetingID; - this.internalMeetingID = redisData.internalMeetingID; - this.lastActivity = redisData.lastActivity; - } - - print() { - return JSON.stringify(this.toRedis()); - } - - static addOrUpdateMapping(internalMeetingID, externalMeetingID, callback) { - let mapping = new IDMapping(); - mapping.id = nextID++; - mapping.internalMeetingID = internalMeetingID; - mapping.externalMeetingID = externalMeetingID; - mapping.lastActivity = new Date().getTime(); - mapping.save(function(error, result) { - Logger.info(`[IDMapping] added or changed meeting mapping to the list ${externalMeetingID}: ${mapping.print()}`); - (typeof callback === 'function' ? callback(error, result) : undefined); - }); - } - - static removeMapping(internalMeetingID, callback) { - return (() => { - let result = []; - for (let internal in db) { - var mapping = db[internal]; - if (mapping.internalMeetingID === internalMeetingID) { - result.push(mapping.destroy( (error, result) => { - Logger.info(`[IDMapping] removing meeting mapping from the list ${external}: ${mapping.print()}`); - return (typeof callback === 'function' ? callback(error, result) : undefined); - })); - } else { - result.push(undefined); - } - } - return result; - })(); - } - - static getInternalMeetingID(externalMeetingID) { - const mapping = IDMapping.findByExternalMeetingID(externalMeetingID); - return (mapping != null ? mapping.internalMeetingID : undefined); - } - - static getExternalMeetingID(internalMeetingID) { - if (db[internalMeetingID]){ - return db[internalMeetingID].externalMeetingID; - } - } - - static findByExternalMeetingID(externalMeetingID) { - if (externalMeetingID != null) { - for (let internal in db) { - const mapping = db[internal]; - if (mapping.externalMeetingID === externalMeetingID) { - return mapping; - } - } - } - return null; - } - - static allSync() { - let arr = Object.keys(db).reduce(function(arr, id) { - arr.push(db[id]); - return arr; - } - , []); - return arr; - } - - // Sets the last activity of the mapping for `internalMeetingID` to now. - static reportActivity(internalMeetingID) { - let mapping = db[internalMeetingID]; - if (mapping != null) { - mapping.lastActivity = new Date().getTime(); - return mapping.save(); - } - } - - // Checks all current mappings for their last activity and removes the ones that - // are "expired", that had their last activity too long ago. - static cleanup() { - const now = new Date().getTime(); - const all = IDMapping.allSync(); - const toRemove = _.filter(all, mapping => mapping.lastActivity < (now - config.get("mappings.timeout"))); - if (!_.isEmpty(toRemove)) { - Logger.info(`[IDMapping] expiring the mappings: ${_.map(toRemove, map => map.print())}`); - toRemove.forEach(mapping => { - UserMapping.removeMappingMeetingId(mapping.internalMeetingID); - mapping.destroy() - }); - } - } - - // Initializes global methods for this model. - static initialize(callback) { - IDMapping.resync(callback); - IDMapping.cleanupInterval = setInterval(IDMapping.cleanup, config.get("mappings.cleanupInterval")); - } - - // Gets all mappings from redis to populate the local database. - // Calls `callback()` when done. - static resync(callback) { - let client = Application.redisClient(); - let tasks = []; - - return client.smembers(config.get("redis.keys.mappings"), (error, mappings) => { - if (error != null) { Logger.error(`[IDMapping] error getting list of mappings from redis: ${error}`); } - - mappings.forEach(id => { - tasks.push(done => { - client.hgetall(config.get("redis.keys.mappingPrefix") + ":" + id, function(error, mappingData) { - if (error != null) { Logger.error(`[IDMapping] error getting information for a mapping from redis: ${error}`); } - - if (mappingData != null) { - let mapping = new IDMapping(); - mapping.fromRedis(mappingData); - mapping.save(function(error, hook) { - if (mapping.id >= nextID) { nextID = mapping.id + 1; } - done(null, mapping); - }); - } else { - done(null, null); - } - }); - }); - }); - - return async.series(tasks, function(errors, result) { - mappings = _.map(IDMapping.allSync(), m => m.print()); - Logger.info(`[IDMapping] finished resync, mappings registered: ${mappings}`); - return (typeof callback === 'function' ? callback() : undefined); - }); - }); - } -}; diff --git a/logger.js b/logger.js deleted file mode 100644 index 399660e..0000000 --- a/logger.js +++ /dev/null @@ -1,16 +0,0 @@ -const { createLogger, format, transports } = require('winston'); - -const logger = createLogger({ - transports: [ - new transports.Console({ - format: format.combine(format.timestamp(), format.splat(), format.json()), - }), - new transports.File({ - filename: 'log/application.log', - format: format.combine(format.timestamp(), format.splat(), format.json()), - }), - ], -}); - -module.exports = logger; - diff --git a/messageMapping.js b/messageMapping.js deleted file mode 100644 index cac3375..0000000 --- a/messageMapping.js +++ /dev/null @@ -1,512 +0,0 @@ -const Logger = require("./logger.js"); -const IDMapping = require("./id_mapping.js"); -const UserMapping = require("./userMapping.js"); -module.exports = class MessageMapping { - - constructor() { - this.mappedObject = {}; - this.mappedMessage = {}; - this.meetingEvents = [ - "MeetingCreatedEvtMsg", - "MeetingDestroyedEvtMsg", - "ScreenshareRtmpBroadcastStartedEvtMsg", - "ScreenshareRtmpBroadcastStoppedEvtMsg", - "SetCurrentPresentationEvtMsg", - "RecordingStatusChangedEvtMsg", - ]; - this.userEvents = [ - "UserJoinedMeetingEvtMsg", - "UserLeftMeetingEvtMsg", - "UserJoinedVoiceConfToClientEvtMsg", - "UserLeftVoiceConfToClientEvtMsg", - "PresenterAssignedEvtMsg", - "PresenterUnassignedEvtMsg", - "UserBroadcastCamStartedEvtMsg", - "UserBroadcastCamStoppedEvtMsg", - "UserEmojiChangedEvtMsg", - ]; - this.chatEvents = [ - "GroupChatMessageBroadcastEvtMsg", - ]; - this.rapEvents = [ - "PublishedRecordingSysMsg", - "UnpublishedRecordingSysMsg", - "DeletedRecordingSysMsg", - ]; - this.compMeetingEvents = [ - "meeting_created_message", - "meeting_destroyed_event", - ]; - this.compUserEvents = [ - "user_joined_message", - "user_left_message", - "user_listening_only", - "user_joined_voice_message", - "user_left_voice_message", - "user_shared_webcam_message", - "user_unshared_webcam_message", - "user_status_changed_message", - ]; - this.compRapEvents = [ - "archive_started", - "archive_ended", - "sanity_started", - "sanity_ended", - "post_archive_started", - "post_archive_ended", - "process_started", - "process_ended", - "post_process_started", - "post_process_ended", - "publish_started", - "publish_ended", - "post_publish_started", - "post_publish_ended", - "published", - "unpublished", - "deleted", - ]; - this.padEvents = [ - "PadContentEvtMsg" - ]; - } - - // Map internal message based on it's type - mapMessage(messageObj) { - if (this.mappedEvent(messageObj,this.meetingEvents)) { - this.meetingTemplate(messageObj); - } else if (this.mappedEvent(messageObj,this.userEvents)) { - this.userTemplate(messageObj); - } else if (this.mappedEvent(messageObj,this.chatEvents)) { - this.chatTemplate(messageObj); - } else if (this.mappedEvent(messageObj,this.rapEvents)) { - this.rapTemplate(messageObj); - } else if (this.mappedEvent(messageObj,this.compMeetingEvents)) { - this.compMeetingTemplate(messageObj); - } else if (this.mappedEvent(messageObj,this.compUserEvents)) { - this.compUserTemplate(messageObj); - } else if (this.mappedEvent(messageObj,this.compRapEvents)) { - this.compRapTemplate(messageObj); - } else if (this.mappedEvent(messageObj,this.padEvents)) { - this.padTemplate(messageObj); - } - } - - mappedEvent(messageObj,events) { - return events.some( event => { - if ((messageObj.header != null ? messageObj.header.name : undefined) === event) { - return true; - } - if ((messageObj.envelope != null ? messageObj.envelope.name : undefined) === event) { - return true; - } - return false; - }); - } - - // Map internal to external message for meeting information - meetingTemplate(messageObj) { - const props = messageObj.core.body.props; - const meetingId = messageObj.core.body.meetingId || messageObj.core.header.meetingId; - this.mappedObject.data = { - "type": "event", - "id": this.mapInternalMessage(messageObj), - "attributes":{ - "meeting":{ - "internal-meeting-id": meetingId, - "external-meeting-id": IDMapping.getExternalMeetingID(meetingId) - } - }, - "event":{ - "ts": Date.now() - } - }; - if (messageObj.envelope.name === "MeetingCreatedEvtMsg") { - this.mappedObject.data.attributes = { - "meeting":{ - "internal-meeting-id": props.meetingProp.intId, - "external-meeting-id": props.meetingProp.extId, - "name": props.meetingProp.name, - "is-breakout": props.meetingProp.isBreakout, - "duration": props.durationProps.duration, - "create-time": props.durationProps.createdTime, - "create-date": props.durationProps.createdDate, - "moderator-pass": props.password.moderatorPass, - "viewer-pass": props.password.viewerPass, - "record": props.recordProp.record, - "voice-conf": props.voiceProp.voiceConf, - "dial-number": props.voiceProp.dialNumber, - "max-users": props.usersProp.maxUsers, - "metadata": props.metadataProp.metadata - } - }; - } - if (messageObj.envelope.name === "SetCurrentPresentationEvtMsg") { - this.mappedObject.data.attributes = { - "meeting":{ - "internal-meeting-id": meetingId, - "external-meeting-id": IDMapping.getExternalMeetingID(meetingId), - "presentation-id": messageObj.core.body.presentationId - } - }; - } - this.mappedMessage = JSON.stringify(this.mappedObject); - Logger.info(`[MessageMapping] Mapped message: ${this.mappedMessage}`); - } - - compMeetingTemplate(messageObj) { - const props = messageObj.payload; - const meetingId = props.meeting_id; - this.mappedObject.data = { - "type": "event", - "id": this.mapInternalMessage(messageObj), - "attributes":{ - "meeting":{ - "internal-meeting-id": meetingId, - "external-meeting-id": IDMapping.getExternalMeetingID(meetingId) - } - }, - "event":{ - "ts": Date.now() - } - }; - if (messageObj.header.name === "meeting_created_message") { - this.mappedObject.data.attributes = { - "meeting":{ - "internal-meeting-id": meetingId, - "external-meeting-id": props.external_meeting_id, - "name": props.name, - "is-breakout": props.is_breakout, - "duration": props.duration, - "create-time": props.create_time, - "create-date": props.create_date, - "moderator-pass": props.moderator_pass, - "viewer-pass": props.viewer_pass, - "record": props.recorded, - "voice-conf": props.voice_conf, - "dial-number": props.dial_number, - "max-users": props.max_users, - "metadata": props.metadata - } - }; - } - this.mappedMessage = JSON.stringify(this.mappedObject); - Logger.info(`[MessageMapping] Mapped message: ${this.mappedMessage}`); - } - - // Map internal to external message for user information - userTemplate(messageObj) { - const msgBody = messageObj.core.body; - const msgHeader = messageObj.core.header; - const extId = UserMapping.getExternalUserID(msgHeader.userId) || msgBody.extId || ""; - this.mappedObject.data = { - "type": "event", - "id": this.mapInternalMessage(messageObj), - "attributes":{ - "meeting":{ - "internal-meeting-id": messageObj.envelope.routing.meetingId, - "external-meeting-id": IDMapping.getExternalMeetingID(messageObj.envelope.routing.meetingId) - }, - "user":{ - "internal-user-id": msgHeader.userId, - "external-user-id": extId, - "name": msgBody.name, - "role": msgBody.role, - "presenter": msgBody.presenter, - "stream": msgBody.stream - } - }, - "event":{ - "ts": Date.now() - } - }; - if (this.mappedObject.data["id"] === "user-audio-voice-enabled") { - this.mappedObject.data["attributes"]["user"]["listening-only"] = msgBody.listenOnly; - this.mappedObject.data["attributes"]["user"]["sharing-mic"] = ! msgBody.listenOnly; - } else if (this.mappedObject.data["id"] === "user-audio-voice-disabled") { - this.mappedObject.data["attributes"]["user"]["listening-only"] = false; - this.mappedObject.data["attributes"]["user"]["sharing-mic"] = false; - } - this.mappedMessage = JSON.stringify(this.mappedObject); - Logger.info(`[MessageMapping] Mapped message: ${this.mappedMessage}`); - } - - // Map internal to external message for user information - compUserTemplate(messageObj) { - const msgBody = messageObj.payload; - const msgHeader = messageObj.header; - - let user; - if (msgHeader.name === "user_joined_message") { - user = { - "internal-user-id": msgBody.user.userid, - "external-user-id": msgBody.user.extern_userid, - "sharing-mic": msgBody.user.voiceUser.joined, - "name": msgBody.user.name, - "role": msgBody.user.role, - "presenter": msgBody.user.presenter, - "stream": msgBody.user.webcam_stream, - "listening-only": msgBody.user.listenOnly - } - } - else { - user = UserMapping.getUser(msgBody.userid) || { "internal-user-id": msgBody.userid || msgBody.user.userid }; - if (msgHeader.name === "user_status_changed_message") { - if (msgBody.status === "presenter") { - user["presenter"] = msgBody.value; - } - } - else if (msgHeader.name === "user_listening_only") { - user["listening-only"] = msgBody.listen_only; - } - else if (msgHeader.name === "user_joined_voice_message" || msgHeader.name === "user_left_voice_message") { - user["sharing-mic"] = msgBody.user.voiceUser.joined; - } - else if (msgHeader.name === "user_shared_webcam_message") { - user["stream"].push(msgBody.stream); - } - else if (msgHeader.name === "user_unshared_webcam_message") { - let streams = user["stream"]; - let index = streams.indexOf(msgBody.stream); - if (index != -1) { - streams.splice(index,1); - } - user["stream"] = streams; - } - } - - this.mappedObject.data = { - "type": "event", - "id": this.mapInternalMessage(messageObj), - "attributes":{ - "meeting":{ - "internal-meeting-id": msgBody.meeting_id, - "external-meeting-id": IDMapping.getExternalMeetingID(msgBody.meeting_id) - }, - "user": user - }, - "event":{ - "ts": Date.now() - } - }; - this.mappedMessage = JSON.stringify(this.mappedObject); - Logger.info(`[MessageMapping] Mapped message: ${this.mappedMessage}`); - } - - // Map internal to external message for chat information - chatTemplate(messageObj) { - const { body } = messageObj.core; - // Ignore private chats - if (body.chatId !== 'MAIN-PUBLIC-GROUP-CHAT') return; - - this.mappedObject.data = { - "type": "event", - "id": this.mapInternalMessage(messageObj), - "attributes":{ - "meeting":{ - "internal-meeting-id": messageObj.envelope.routing.meetingId, - "external-meeting-id": IDMapping.getExternalMeetingID(messageObj.envelope.routing.meetingId) - }, - "chat-message":{ - "message": body.msg.message, - "sender":{ - "internal-user-id": body.msg.sender.id, - "external-user-id": body.msg.sender.name, - "timezone-offset": body.msg.fromTimezoneOffset, - "time": body.msg.timestamp - } - }, - "chat-id": body.chatId - }, - "event":{ - "ts": Date.now() - } - }; - this.mappedMessage = JSON.stringify(this.mappedObject); - Logger.info(`[MessageMapping] Mapped message: ${this.mappedMessage}`); - } - - rapTemplate(messageObj) { - const data = messageObj.core.body; - this.mappedObject.data = { - "type": "event", - "id": this.mapInternalMessage(messageObj), - "attributes": { - "meeting": { - "internal-meeting-id": data.recordId, - "external-meeting-id": IDMapping.getExternalMeetingID(data.recordId) - } - }, - "event": { - "ts": Date.now() - } - }; - this.mappedMessage = JSON.stringify(this.mappedObject); - Logger.info(`[MessageMapping] Mapped message: ${this.mappedMessage}`); - } - - compRapTemplate(messageObj) { - const data = messageObj.payload; - this.mappedObject.data = { - "type": "event", - "id": this.mapInternalMessage(messageObj), - "attributes": { - "meeting": { - "internal-meeting-id": data.meeting_id, - "external-meeting-id": data.external_meeting_id || IDMapping.getExternalMeetingID(data.meeting_id) - } - }, - "event": { - "ts": messageObj.header.current_time - } - }; - - if (this.mappedObject.data.id === "published" || - this.mappedObject.data.id === "unpublished" || - this.mappedObject.data.id === "deleted") { - this.mappedObject.data.attributes["record-id"] = data.meeting_id; - this.mappedObject.data.attributes["format"] = data.format; - } else { - this.mappedObject.data.attributes["record-id"] = data.record_id; - this.mappedObject.data.attributes["success"] = data.success; - this.mappedObject.data.attributes["step-time"] = data.step_time; - } - - if (data.workflow) { - this.mappedObject.data.attributes.workflow = data.workflow; - } - - if (this.mappedObject.data.id === "rap-publish-ended") { - this.mappedObject.data.attributes.recording = { - "name": data.metadata.meetingName, - "is-breakout": data.metadata.isBreakout, - "start-time": data.startTime, - "end-time": data.endTime, - "size": data.playback.size, - "raw-size": data.rawSize, - "metadata": data.metadata, - "playback": data.playback, - "download": data.download - } - } - this.mappedMessage = JSON.stringify(this.mappedObject); - Logger.info(`[MessageMapping] Mapped message: ${this.mappedMessage}`); - } - - handleRecordingStatusChanged(message) { - const event = "meeting-recording"; - const { core } = message; - if (core && core.body) { - const { recording } = core.body; - if (typeof recording === 'boolean') { - if (recording) return `${event}-started`; - return `${event}-stopped`; - } - } - return `${event}-unhandled`; - } - - handleUserListeningOnly(message) { - const event = "user-audio-listen-only"; - if (message.payload.listen_only) return `${event}-enabled`; - return `${event}-disabled`; - } - - handleUserStatusChanged(message) { - const event = "user-presenter"; - if (message.payload.status === "presenter") { - if (message.payload.value === "true") return `${event}-assigned`; - return `${event}-unassigned`; - } - } - - padTemplate(messageObj) { - const { - body, - header, - } = messageObj.core; - this.mappedObject.data = { - "type": "event", - "id": this.mapInternalMessage(messageObj), - "attributes":{ - "meeting":{ - "internal-meeting-id": header.meetingId, - "external-meeting-id": IDMapping.getExternalMeetingID(header.meetingId) - }, - "pad":{ - "id": body.padId, - "external-pad-id": body.externalId, - "rev": body.rev, - "start": body.start, - "end": body.end, - "text": body.text - } - }, - "event":{ - "ts": Date.now() - } - }; - this.mappedMessage = JSON.stringify(this.mappedObject); - Logger.info(`[MessageMapping] Mapped message: ${this.mappedMessage}`); - } - - mapInternalMessage(message) { - let name; - if (message.envelope) { - name = message.envelope.name - } - else if (message.header) { - name = message.header.name - } - const mappedMsg = (() => { switch (name) { - case "MeetingCreatedEvtMsg": return "meeting-created"; - case "MeetingDestroyedEvtMsg": return "meeting-ended"; - case "RecordingStatusChangedEvtMsg": return this.handleRecordingStatusChanged(message); - case "ScreenshareRtmpBroadcastStartedEvtMsg": return "meeting-screenshare-started"; - case "ScreenshareRtmpBroadcastStoppedEvtMsg": return "meeting-screenshare-stopped"; - case "SetCurrentPresentationEvtMsg": return "meeting-presentation-changed"; - case "UserJoinedMeetingEvtMsg": return "user-joined"; - case "UserLeftMeetingEvtMsg": return "user-left"; - case "UserJoinedVoiceConfToClientEvtMsg": return "user-audio-voice-enabled"; - case "UserLeftVoiceConfToClientEvtMsg": return "user-audio-voice-disabled"; - case "UserBroadcastCamStartedEvtMsg": return "user-cam-broadcast-start"; - case "UserBroadcastCamStoppedEvtMsg": return "user-cam-broadcast-end"; - case "PresenterAssignedEvtMsg": return "user-presenter-assigned"; - case "PresenterUnassignedEvtMsg": return "user-presenter-unassigned"; - case "UserEmojiChangedEvtMsg": return "user-emoji-changed"; - case "GroupChatMessageBroadcastEvtMsg": return "chat-group-message-sent"; - case "archive_started": return "rap-archive-started"; - case "archive_ended": return "rap-archive-ended"; - case "sanity_started": return "rap-sanity-started"; - case "sanity_ended": return "rap-sanity-ended"; - case "post_archive_started": return "rap-post-archive-started"; - case "post_archive_ended": return "rap-post-archive-ended"; - case "process_started": return "rap-process-started"; - case "process_ended": return "rap-process-ended"; - case "post_process_started": return "rap-post-process-started"; - case "post_process_ended": return "rap-post-process-ended"; - case "publish_started": return "rap-publish-started"; - case "publish_ended": return "rap-publish-ended"; - case "published": return "rap-published"; - case "unpublished": return "rap-unpublished"; - case "deleted": return "rap-deleted"; - case "PublishedRecordingSysMsg": return "rap-published"; - case "UnpublishedRecordingSysMsg": return "rap-unpublished"; - case "DeletedRecordingSysMsg": return "rap-deleted"; - case "post_publish_started": return "rap-post-publish-started"; - case "post_publish_ended": return "rap-post-publish-ended"; - case "meeting_created_message": return "meeting-created"; - case "meeting_destroyed_event": return "meeting-ended"; - case "user_joined_message": return "user-joined"; - case "user_left_message": return "user-left"; - case "user_listening_only": return this.handleUserListeningOnly(message); - case "user_joined_voice_message": return "user-audio-voice-enabled"; - case "user_left_voice_message": return "user-audio-voice-disabled"; - case "user_shared_webcam_message": return "user-cam-broadcast-start"; - case "video_stream_unpublished": return "user-cam-broadcast-end"; - case "user_status_changed_message": return this.handleUserStatusChanged(message); - case "PadContentEvtMsg": return "pad-content"; - } })(); - return mappedMsg; - } -}; diff --git a/package-lock.json b/package-lock.json index c6dd9fd..5e44971 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,1188 +1,2962 @@ { "name": "bbb-webhooks", "version": "2.6.0", - "lockfileVersion": 1, + "lockfileVersion": 3, "requires": true, - "dependencies": { - "@colors/colors": { + "packages": { + "": { + "name": "bbb-webhooks", + "version": "2.6.0", + "dependencies": { + "body-parser": "^1.20.0", + "bullmq": "^4.11.4", + "config": "^3.3.7", + "express": "^4.18.2", + "js-yaml": "^4.1.0", + "nock": "^13.2.4", + "redis": "^4.6.8", + "request": "^2.88.2", + "sha1": "^1.1.1", + "uuid": "^9.0.1", + "winston": "^3.10.0" + }, + "devDependencies": { + "eslint": "^8.49.0", + "eslint-plugin-import": "^2.28.1", + "mocha": "^9.2.2", + "sinon": "^12.0.1", + "supertest": "^3.4.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", - "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==" + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "engines": { + "node": ">=0.1.90" + } }, - "@dabh/diagnostics": { + "node_modules/@dabh/diagnostics": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", - "requires": { + "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, - "@sinonjs/commons": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", - "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", - "requires": { + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.2.tgz", + "integrity": "sha512-0MGxAVt1m/ZK+LTJp/j0qF7Hz97D9O/FH9Ms3ltnyIdDD57cbb1ACIQTkbHvNXtWDv5TPq7w5Kq56+cNukbo7g==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", + "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/eslintrc/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@eslint/js": { + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.50.0.tgz", + "integrity": "sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", + "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "node_modules/@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.2.tgz", + "integrity": "sha512-9bfjwDxIDWmmOKusUcqdS4Rw+SETlp9Dy39Xui9BEGEk19dDwH0jhipwFzEff/pFg95NKymc6TOTbRKcWeRqyQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.2.tgz", + "integrity": "sha512-lwriRAHm1Yg4iDf23Oxm9n/t5Zpw1lVnxYU3HnJPTi2lJRkKTrps1KVgvL6m7WvmhYVt/FIsssWay+k45QHeuw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.2.tgz", + "integrity": "sha512-MOI9Dlfrpi2Cuc7i5dXdxPbFIgbDBGgKR5F2yWEa6FVEtSWncfVNKW5AKjImAQ6CZlBK9tympdsZJ2xThBiWWA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.2.tgz", + "integrity": "sha512-FU20Bo66/f7He9Fp9sP2zaJ1Q8L9uLPZQDub/WlUip78JlPeMbVL8546HbZfcW9LNciEXc8d+tThSJjSC+tmsg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.2.tgz", + "integrity": "sha512-gsWNDCklNy7Ajk0vBBf9jEx04RUxuDQfBse918Ww+Qb9HCPoGzS+XJTLe96iN3BVK7grnLiYghP/M4L8VsaHeA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.2.tgz", + "integrity": "sha512-O+6Gs8UeDbyFpbSh2CPEz/UOrrdWPTBYNblZK5CxxLisYt4kGX3Sc+czffFonyjiGSq3jWLwJS/CCJc7tBr4sQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@redis/bloom": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", + "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/client": { + "version": "1.5.11", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.11.tgz", + "integrity": "sha512-cV7yHcOAtNQ5x/yQl7Yw1xf53kO0FNDTdDU6bFIMbW6ljB7U7ns0YRM+QIkpoqTAt6zK5k9Fq0QWlUbLcq9AvA==", + "dependencies": { + "cluster-key-slot": "1.1.2", + "generic-pool": "3.9.0", + "yallist": "4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@redis/graph": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.0.tgz", + "integrity": "sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/json": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.6.tgz", + "integrity": "sha512-rcZO3bfQbm2zPRpqo82XbW8zg4G/w4W3tI7X8Mqleq9goQjAGLL7q/1n1ZX4dXEAmORVZ4s1+uKLaUOg7LrUhw==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/search": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.5.tgz", + "integrity": "sha512-hPP8w7GfGsbtYEJdn4n7nXa6xt6hVZnnDktKW4ArMaFQ/m/aR7eFvsLQmG/mn1Upq99btPJk+F27IQ2dYpCoUg==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/time-series": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.5.tgz", + "integrity": "sha512-IFjIgTusQym2B5IZJG3XKr5llka7ey84fw/NOYqESP5WUfQs9zz1ww/9+qoz4ka/S6KcGBodzlCeZ5UImKbscg==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@sinonjs/commons": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", + "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", + "dev": true, + "dependencies": { "type-detect": "4.0.8" } }, - "@sinonjs/fake-timers": { + "node_modules/@sinonjs/fake-timers": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz", "integrity": "sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==", - "requires": { + "dev": true, + "dependencies": { "@sinonjs/commons": "^1.7.0" } }, - "@sinonjs/samsam": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.0.2.tgz", - "integrity": "sha512-jxPRPp9n93ci7b8hMfJOFDPRLFYadN6FSpeROFTR4UNF4i5b+EK6m4QXPO46BDhFgRy1JuS87zAnFOzCUwMJcQ==", - "requires": { + "node_modules/@sinonjs/samsam": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.1.3.tgz", + "integrity": "sha512-nhOb2dWPeb1sd3IQXL/dVPnKHDOAFfvichtBf4xV00/rU1QbPCQqKMbvIheIjqwVjh7qIgf2AHTHi391yMOMpQ==", + "dev": true, + "dependencies": { "@sinonjs/commons": "^1.6.0", "lodash.get": "^4.4.2", "type-detect": "^4.0.8" } }, - "@sinonjs/text-encoding": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", - "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==" + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "node_modules/@types/triple-beam": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.3.tgz", + "integrity": "sha512-6tOUG+nVHn0cJbVp25JFayS5UE6+xlbcNF9Lo9mU7U0zk3zeUShZied4YEQZjy1JBF043FSkdXw8YkUJuVtB5g==" }, - "@ungap/promise-all-settled": { + "node_modules/@ungap/promise-all-settled": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", "dev": true }, - "accepts": { + "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "requires": { + "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" }, - "dependencies": { - "mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" - }, - "mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "requires": { - "mime-db": "1.52.0" - } - } + "engines": { + "node": ">= 0.6" } }, - "ajv": { + "node_modules/acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "requires": { + "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "ansi-colors": { + "node_modules/ansi-colors": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true + "dev": true, + "engines": { + "node": ">=6" + } }, - "ansi-regex": { + "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true + "dev": true, + "engines": { + "node": ">=8" + } }, - "ansi-styles": { + "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "requires": { + "dependencies": { "color-convert": "^2.0.1" }, - "dependencies": { - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - } + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, - "requires": { + "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" } }, - "argparse": { + "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, - "array-flatten": { + "node_modules/array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/array-includes": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz", + "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz", + "integrity": "sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", + "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "asn1": { + "node_modules/asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "requires": { + "dependencies": { "safer-buffer": "~2.1.0" } }, - "assert-plus": { + "node_modules/assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "engines": { + "node": ">=0.8" + } }, - "async": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", - "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==" + "node_modules/async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" }, - "asynckit": { + "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "aws-sign2": { + "node_modules/aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "engines": { + "node": "*" + } }, - "aws4": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", - "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" + "node_modules/aws4": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", + "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" }, - "balanced-match": { + "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, - "bcrypt-pbkdf": { + "node_modules/bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "requires": { + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dependencies": { "tweetnacl": "^0.14.3" } }, - "binary-extensions": { + "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true + "dev": true, + "engines": { + "node": ">=8" + } }, - "body-parser": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", - "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==", - "requires": { + "node_modules/body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "dependencies": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.10.3", - "raw-body": "2.5.1", + "qs": "6.11.0", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, - "dependencies": { - "qs": { - "version": "6.10.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", - "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", - "requires": { - "side-channel": "^1.0.4" - } - } + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" } }, - "brace-expansion": { + "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "requires": { + "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, - "braces": { + "node_modules/braces": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "dev": true, - "requires": { + "dependencies": { "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" } }, - "browser-stdout": { + "node_modules/browser-stdout": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, - "bytes": { + "node_modules/bullmq": { + "version": "4.11.4", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-4.11.4.tgz", + "integrity": "sha512-LuCR3ILngYa3CLC5jyf8DU4Yokj9T12MWwBogP3S4IiJUtbJsQ9GTGFxho3imRxXfcd9DUfrABT/pSoqVigXiQ==", + "dependencies": { + "cron-parser": "^4.6.0", + "glob": "^8.0.3", + "ioredis": "^5.3.2", + "lodash": "^4.17.21", + "msgpackr": "^1.6.2", + "node-abort-controller": "^3.1.1", + "semver": "^7.5.4", + "tslib": "^2.0.0", + "uuid": "^9.0.0" + } + }, + "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } }, - "call-bind": { + "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "requires": { + "dependencies": { "function-bind": "^1.1.1", "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "camelcase": { + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "caseless": { + "node_modules/caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" }, - "chalk": { + "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, - "requires": { + "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "charenc": { + "node_modules/charenc": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", - "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=" + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "engines": { + "node": "*" + } }, - "chokidar": { + "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", "dev": true, - "requires": { + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", - "fsevents": "~2.3.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" } }, - "cliui": { + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dev": true, - "requires": { + "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, - "color": { + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", - "requires": { + "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, - "color-convert": { + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color/node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { + "dependencies": { "color-name": "1.1.3" } }, - "color-name": { + "node_modules/color/node_modules/color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, - "color-string": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.0.tgz", - "integrity": "sha512-9Mrz2AQLefkH1UvASKj6v6hj/7eWgjnT/cVsR8CumieLoT+g900exWeNogqtweI8dxloXN9BDQTYro1oWu/5CQ==", - "requires": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "colorspace": { + "node_modules/colorspace": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", - "requires": { + "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" } }, - "combined-stream": { + "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "requires": { + "dependencies": { "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" } }, - "component-emitter": { + "node_modules/component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", "dev": true }, - "concat-map": { + "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, - "config": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/config/-/config-3.3.7.tgz", - "integrity": "sha512-mX/n7GKDYZMqvvkY6e6oBY49W8wxdmQt+ho/5lhwFDXqQW9gI+Ahp8EKp8VAbISPnmf2+Bv5uZK7lKXZ6pf1aA==", - "requires": { - "json5": "^2.1.1" + "node_modules/config": { + "version": "3.3.9", + "resolved": "https://registry.npmjs.org/config/-/config-3.3.9.tgz", + "integrity": "sha512-G17nfe+cY7kR0wVpc49NCYvNtelm/pPy8czHoFkAgtV1lkmcp7DHtWCdDu+C9Z7gb2WVqa9Tm3uF9aKaPbCfhg==", + "dependencies": { + "json5": "^2.2.3" + }, + "engines": { + "node": ">= 10.0.0" } }, - "content-disposition": { + "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "requires": { + "dependencies": { "safe-buffer": "5.2.1" }, - "dependencies": { - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - } + "engines": { + "node": ">= 0.6" } }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } }, - "cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "engines": { + "node": ">= 0.6" + } }, - "cookie-signature": { + "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, - "cookiejar": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", - "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==", + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", "dev": true }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "node_modules/cron-parser": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", + "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", + "dependencies": { + "luxon": "^3.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } }, - "crypt": { + "node_modules/crypt": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", - "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=" + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "engines": { + "node": "*" + } }, - "dashdash": { + "node_modules/dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "requires": { + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dependencies": { "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" } }, - "debug": { + "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { + "dependencies": { "ms": "2.0.0" } }, - "decamelize": { + "node_modules/decamelize": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, - "delayed-stream": { + "node_modules/define-data-property": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.0.tgz", + "integrity": "sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } }, - "denque": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.0.tgz", - "integrity": "sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ==" + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } }, - "depd": { + "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } }, - "destroy": { + "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } }, - "diff": { + "node_modules/diff": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==" + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } }, - "ecc-jsbn": { + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "requires": { + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dependencies": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" } }, - "ee-first": { + "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, - "emoji-regex": { + "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, - "enabled": { + "node_modules/enabled": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" }, - "encodeurl": { + "node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-abstract": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.2.tgz", + "integrity": "sha512-YoxfFcDmhjOgWPWsV13+2RNjq1F6UQnfs+8TftwNqtzlmFzEXvlUwdrNrYeaizfjQzRMxkZ6ElWMOJIFKdVqwA==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.2", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.1", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.12", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "safe-array-concat": "^1.0.1", + "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.8", + "string.prototype.trimend": "^1.0.7", + "string.prototype.trimstart": "^1.0.7", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.11" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", + "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3", + "has": "^1.0.3", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } }, - "escalade": { + "node_modules/es-shim-unscopables": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", + "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true + "dev": true, + "engines": { + "node": ">=6" + } }, - "escape-html": { + "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, - "escape-string-regexp": { + "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.50.0.tgz", + "integrity": "sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "8.50.0", + "@humanwhocodes/config-array": "^0.11.11", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/eslint-module-utils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", + "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", + "dev": true, + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/eslint-plugin-import": { + "version": "2.28.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.28.1.tgz", + "integrity": "sha512-9I9hFlITvOV55alzoKBI+K9q74kv0iKMeY6av5+umsNwayt59fz692daGyjR+oStBQgx6nwR9rXldDev3Clw+A==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.findlastindex": "^1.2.2", + "array.prototype.flat": "^1.3.1", + "array.prototype.flatmap": "^1.3.1", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.7", + "eslint-module-utils": "^2.8.0", + "has": "^1.0.3", + "is-core-module": "^2.13.0", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.6", + "object.groupby": "^1.0.0", + "object.values": "^1.1.6", + "semver": "^6.3.1", + "tsconfig-paths": "^3.14.2" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "etag": { + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } }, - "express": { - "version": "4.17.3", - "resolved": "https://registry.npmjs.org/express/-/express-4.17.3.tgz", - "integrity": "sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg==", - "requires": { + "node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.19.2", + "body-parser": "1.20.1", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.4.2", + "cookie": "0.5.0", "cookie-signature": "1.0.6", "debug": "2.6.9", - "depd": "~1.1.2", + "depd": "2.0.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "~1.1.2", + "finalhandler": "1.2.0", "fresh": "0.5.2", + "http-errors": "2.0.0", "merge-descriptors": "1.0.1", "methods": "~1.1.2", - "on-finished": "~2.3.0", + "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.7", "proxy-addr": "~2.0.7", - "qs": "6.9.7", + "qs": "6.11.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.17.2", - "serve-static": "1.14.2", + "send": "0.18.0", + "serve-static": "1.15.0", "setprototypeof": "1.2.0", - "statuses": "~1.5.0", + "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", "dependencies": { - "body-parser": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.2.tgz", - "integrity": "sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw==", - "requires": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "1.8.1", - "iconv-lite": "0.4.24", - "on-finished": "~2.3.0", - "qs": "6.9.7", - "raw-body": "2.4.3", - "type-is": "~1.6.18" - } - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" - }, - "http-errors": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", - "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "requires": { - "ee-first": "1.1.1" - } - }, - "qs": { - "version": "6.9.7", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==" - }, - "raw-body": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.3.tgz", - "integrity": "sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g==", - "requires": { - "bytes": "3.1.2", - "http-errors": "1.8.1", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - }, - "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" - } + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" } }, - "extend": { + "node_modules/express/node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, - "extsprintf": { + "node_modules/extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "engines": [ + "node >=0.6.0" + ] }, - "fast-deep-equal": { + "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, - "fast-json-stable-stringify": { + "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, - "fecha": { + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fecha": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" }, - "fill-range": { + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "dev": true, - "requires": { + "dependencies": { "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", - "requires": { + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { "debug": "2.6.9", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", - "on-finished": "~2.3.0", + "on-finished": "2.4.1", "parseurl": "~1.3.3", - "statuses": "~1.5.0", + "statuses": "2.0.1", "unpipe": "~1.0.0" }, - "dependencies": { - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "requires": { - "ee-first": "1.1.1" - } - }, - "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" - } + "engines": { + "node": ">= 0.8" } }, - "find-up": { + "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, - "requires": { + "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "flat": { + "node_modules/flat": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz", + "integrity": "sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==", + "dev": true, + "dependencies": { + "flatted": "^3.2.7", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, - "fn.name": { + "node_modules/fn.name": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" }, - "forever-agent": { + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "engines": { + "node": "*" + } }, - "form-data": { + "node_modules/form-data": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "requires": { + "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.6", "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" } }, - "formidable": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz", - "integrity": "sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q==", - "dev": true + "node_modules/formidable": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.6.tgz", + "integrity": "sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==", + "deprecated": "Please upgrade to latest, formidable@v2 or formidable@v3! Check these notes: https://bit.ly/2ZEqIau", + "dev": true, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } }, - "forwarded": { + "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } }, - "fresh": { + "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } }, - "fs.realpath": { + "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, - "optional": true + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } }, - "function-bind": { + "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, - "get-caller-file": { + "node_modules/function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generic-pool": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } }, - "get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "requires": { + "node_modules/get-intrinsic": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "dependencies": { "function-bind": "^1.1.1", "has": "^1.0.3", - "has-symbols": "^1.0.1" + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "getpass": { + "node_modules/getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "requires": { + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dependencies": { "assert-plus": "^1.0.0" } }, - "glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "dev": true, - "requires": { + "node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dependencies": { - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - } + "balanced-match": "^1.0.0" } }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "node_modules/glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/globals": { + "version": "13.22.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.22.0.tgz", + "integrity": "sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==", "dev": true, - "requires": { - "is-glob": "^4.0.1" + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "growl": { + "node_modules/globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/growl": { "version": "1.10.5", "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", - "dev": true + "dev": true, + "engines": { + "node": ">=4.x" + } }, - "har-schema": { + "node_modules/har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "engines": { + "node": ">=4" + } }, - "har-validator": { + "node_modules/har-validator": { "version": "5.1.5", "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "requires": { + "deprecated": "this library is no longer supported", + "dependencies": { "ajv": "^6.12.3", "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" } }, - "has": { + "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { + "dependencies": { "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" } }, - "has-flag": { + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "has-symbols": { + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "he": { + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true + "dev": true, + "bin": { + "he": "bin/he" + } }, - "http-errors": { + "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "requires": { + "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" }, - "dependencies": { - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - } + "engines": { + "node": ">= 0.8" } }, - "http-signature": { + "node_modules/http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "requires": { + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "dependencies": { "assert-plus": "^1.0.0", "jsprim": "^1.2.2", "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" } }, - "inflight": { + "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/internal-slot": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", + "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ioredis": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.2.tgz", + "integrity": "sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==", + "dependencies": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/ioredis/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } }, - "ipaddr.js": { + "node_modules/ioredis/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "is-arrayish": { + "node_modules/is-arrayish": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" }, - "is-binary-path": { + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, - "requires": { + "dependencies": { "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", + "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "is-extglob": { + "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "is-fullwidth-code-point": { + "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true + "dev": true, + "engines": { + "node": ">=8" + } }, - "is-glob": { + "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, - "requires": { + "dependencies": { "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "is-number": { + "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } }, - "is-plain-obj": { + "node_modules/is-plain-obj": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", - "dev": true + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "is-stream": { + "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "dev": true, + "dependencies": { + "which-typed-array": "^1.1.11" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "is-typedarray": { + "node_modules/is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" }, - "is-unicode-supported": { + "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "isexe": { + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, - "isstream": { + "node_modules/isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" }, - "js-yaml": { + "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "requires": { + "dependencies": { "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "jsbn": { + "node_modules/jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true }, - "json-schema": { + "node_modules/json-schema": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" }, - "json-schema-traverse": { + "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, - "json-stringify-safe": { + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "json5": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", - "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", - "requires": { - "minimist": "^1.2.5" + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" } }, - "jsprim": { + "node_modules/jsprim": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", - "requires": { + "dependencies": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", "json-schema": "0.4.0", "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" } }, - "just-extend": { + "node_modules/just-extend": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", - "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==" + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true }, - "kuler": { + "node_modules/keyv": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", + "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kuler": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" }, - "locate-path": { + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, - "requires": { + "dependencies": { "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "lodash": { + "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, - "lodash.get": { + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + }, + "node_modules/lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" }, - "lodash.set": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", - "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=" + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true }, - "log-symbols": { + "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "dev": true, - "requires": { + "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "logform": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.4.0.tgz", - "integrity": "sha512-CPSJw4ftjf517EhXZGGvTHHkYobo7ZCc0kvwUoOYcjfR2UVrI66RHj8MCrfAdEitdmFqbu2BYdYs8FHHZSb6iw==", - "requires": { + "node_modules/logform": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.5.1.tgz", + "integrity": "sha512-9FyqAm9o9NKKfiAKfZoYo9bGXXuwMkxQiQttkT4YjjVtQVIQtK6LmVtlxmCaFswo6N4AfEkHqZTV0taDtPotNg==", + "dependencies": { "@colors/colors": "1.5.0", + "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" - }, + } + }, + "node_modules/logform/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dependencies": { - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - } + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/luxon": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.3.tgz", + "integrity": "sha512-tFWBiv3h7z+T/tDaoxA8rqTxy1CHV6gHS//QdaH4pulbq/JuBSGgQspQQqcgnwdAx6pNI7cmvz5Sv/addzHmUg==", + "engines": { + "node": ">=12" } }, - "media-typer": { + "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } }, - "merge-descriptors": { + "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" }, - "methods": { + "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } }, - "mime": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", - "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==", - "dev": true + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } }, - "mime-db": { - "version": "1.47.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.47.0.tgz", - "integrity": "sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw==" + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } }, - "mime-types": { - "version": "2.1.30", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.30.tgz", - "integrity": "sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg==", - "requires": { - "mime-db": "1.47.0" + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" } }, - "minimatch": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz", - "integrity": "sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==", + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, - "requires": { + "dependencies": { "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, - "minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "mocha": { + "node_modules/mocha": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz", "integrity": "sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==", "dev": true, - "requires": { + "dependencies": { "@ungap/promise-all-settled": "1.1.2", "ansi-colors": "4.1.1", "browser-stdout": "1.3.1", @@ -1208,272 +2982,638 @@ "yargs-parser": "20.2.4", "yargs-unparser": "2.0.0" }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" + } + }, + "node_modules/mocha/node_modules/debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mocha/node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/mocha/node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz", + "integrity": "sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, "dependencies": { - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - }, - "dependencies": { - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "ms": { + "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/msgpackr": { + "version": "1.9.9", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.9.9.tgz", + "integrity": "sha512-sbn6mioS2w0lq1O6PpGtsv6Gy8roWM+o3o4Sqjd6DudrL/nOugY+KyJUimoWzHnf9OkO0T6broHFnYE/R05t9A==", + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.2.tgz", + "integrity": "sha512-SdzXp4kD/Qf8agZ9+iTu6eql0m3kWm1A2y1hkpTeVNENutaB0BwHlSvAIaMxwntmRUAUjon2V4L8Z/njd0Ct8A==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.0.7" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.2", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.2", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.2", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.2", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.2", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.2" + } }, - "nanoid": { + "node_modules/nanoid": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "negotiator": { + "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" - }, - "nise": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.0.tgz", - "integrity": "sha512-W5WlHu+wvo3PaKLsJJkgPup2LrsXCcm7AWwyNZkUnn5rwPkuPBi3Iwk5SQtN0mv+K65k7nKKjwNQ30wg3wLAQQ==", - "requires": { - "@sinonjs/commons": "^1.7.0", - "@sinonjs/fake-timers": "^7.0.4", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nise": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.4.tgz", + "integrity": "sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "^10.0.2", "@sinonjs/text-encoding": "^0.7.1", "just-extend": "^4.0.2", "path-to-regexp": "^1.7.0" - }, + } + }, + "node_modules/nise/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, "dependencies": { - "@sinonjs/fake-timers": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz", - "integrity": "sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==", - "requires": { - "@sinonjs/commons": "^1.7.0" - } - }, - "path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", - "requires": { - "isarray": "0.0.1" - } - } + "type-detect": "4.0.8" + } + }, + "node_modules/nise/node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/nise/node_modules/@sinonjs/fake-timers/node_modules/@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/nise/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true + }, + "node_modules/nise/node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "dependencies": { + "isarray": "0.0.1" } }, - "nock": { - "version": "13.2.4", - "resolved": "https://registry.npmjs.org/nock/-/nock-13.2.4.tgz", - "integrity": "sha512-8GPznwxcPNCH/h8B+XZcKjYPXnUV5clOKCjAqyjsiqA++MpNx9E9+t8YPp0MbThO+KauRo7aZJ1WuIZmOrT2Ug==", - "requires": { + "node_modules/nock": { + "version": "13.3.3", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.3.3.tgz", + "integrity": "sha512-z+KUlILy9SK/RjpeXDiDUEAq4T94ADPHE3qaRkf66mpEhzc/ytOMm3Bwdrbq6k1tMWkbdujiKim3G2tfQARuJw==", + "dependencies": { "debug": "^4.1.0", "json-stringify-safe": "^5.0.1", - "lodash.set": "^4.3.2", + "lodash": "^4.17.21", "propagate": "^2.0.0" }, + "engines": { + "node": ">= 10.13" + } + }, + "node_modules/nock/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dependencies": { - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true } } }, - "normalize-path": { + "node_modules/nock/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==" + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.7.tgz", + "integrity": "sha512-YlCCc6Wffkx0kHkmam79GKvDQ6x+QZkMjFGrIMxgFNILFvGSbCp2fCBC55pGTT9gVaz8Na5CLmxt/urtzRv36w==", + "optional": true, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "oauth-sign": { + "node_modules/oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "engines": { + "node": "*" + } }, - "object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==" + "node_modules/object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", + "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz", + "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1" + } + }, + "node_modules/object.values": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", + "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "on-finished": { + "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "requires": { + "dependencies": { "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" } }, - "once": { + "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { "wrappy": "1" } }, - "one-time": { + "node_modules/one-time": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", - "requires": { + "dependencies": { "fn.name": "1.x.x" } }, - "p-limit": { + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, - "requires": { + "dependencies": { "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "p-locate": { + "node_modules/p-locate": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, - "requires": { + "dependencies": { "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" } }, - "parseurl": { + "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } }, - "path-exists": { + "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true + "dev": true, + "engines": { + "node": ">=8" + } }, - "path-is-absolute": { + "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, - "path-to-regexp": { + "node_modules/path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, - "performance-now": { + "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" }, - "picomatch": { + "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } }, - "process-nextick-args": { + "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, - "propagate": { + "node_modules/propagate": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", - "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==" + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "engines": { + "node": ">= 8" + } }, - "proxy-addr": { + "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "requires": { + "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" } }, - "psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "engines": { + "node": ">=6" + } }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "qs": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", - "dev": true + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] }, - "randombytes": { + "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, - "requires": { + "dependencies": { "safe-buffer": "^5.1.0" } }, - "range-parser": { + "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } }, - "raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "requires": { + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" } }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, - "requires": { + "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", @@ -1481,59 +3621,87 @@ "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - } } }, - "readdirp": { + "node_modules/readable-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, - "requires": { + "dependencies": { "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" } }, - "redis": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/redis/-/redis-3.1.2.tgz", - "integrity": "sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==", - "requires": { - "denque": "^1.5.0", - "redis-commands": "^1.7.0", - "redis-errors": "^1.2.0", - "redis-parser": "^3.0.0" + "node_modules/redis": { + "version": "4.6.10", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.6.10.tgz", + "integrity": "sha512-mmbyhuKgDiJ5TWUhiKhBssz+mjsuSI/lSZNPI9QvZOYzWvYGejtb+W3RlDDf8LD6Bdl5/mZeG8O1feUGhXTxEg==", + "dependencies": { + "@redis/bloom": "1.2.0", + "@redis/client": "1.5.11", + "@redis/graph": "1.1.0", + "@redis/json": "1.0.6", + "@redis/search": "1.1.5", + "@redis/time-series": "1.0.5" } }, - "redis-commands": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz", - "integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==" - }, - "redis-errors": { + "node_modules/redis-errors": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", - "integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=" + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "engines": { + "node": ">=4" + } }, - "redis-parser": { + "node_modules/redis-parser": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", - "integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=", - "requires": { + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "dependencies": { "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" } }, - "request": { + "node_modules/regexp.prototype.flags": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", + "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "set-function-name": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/request": { "version": "2.88.2", "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "requires": { + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "dependencies": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", "caseless": "~0.12.0", @@ -1555,177 +3723,355 @@ "tunnel-agent": "^0.6.0", "uuid": "^3.3.2" }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/request/node_modules/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/request/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz", + "integrity": "sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", + "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dev": true, "dependencies": { - "qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==" - } + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "safe-stable-stringify": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz", - "integrity": "sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg==" + "node_modules/safe-stable-stringify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "engines": { + "node": ">=10" + } }, - "safer-buffer": { + "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, - "send": { - "version": "0.17.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz", - "integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==", - "requires": { + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", + "depd": "2.0.0", + "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", - "http-errors": "1.8.1", + "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", - "on-finished": "~2.3.0", + "on-finished": "2.4.1", "range-parser": "~1.2.1", - "statuses": "~1.5.0" + "statuses": "2.0.1" }, - "dependencies": { - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" - }, - "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" - }, - "http-errors": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", - "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "requires": { - "ee-first": "1.1.1" - } - }, - "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" - } + "engines": { + "node": ">= 0.8.0" } }, - "serialize-javascript": { + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serialize-javascript": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", "dev": true, - "requires": { + "dependencies": { "randombytes": "^2.1.0" } }, - "serve-static": { - "version": "1.14.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz", - "integrity": "sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==", - "requires": { + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.17.2" + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", + "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" } }, - "setprototypeof": { + "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, - "sha1": { + "node_modules/sha1": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/sha1/-/sha1-1.1.1.tgz", - "integrity": "sha1-rdqnqTFo85PxnrKxUJFhjicA+Eg=", - "requires": { + "integrity": "sha512-dZBS6OrMjtgVkopB1Gmo4RQCDKiZsqcpAQpkV/aaj+FCrCg8r4I4qMkDPQjBgLIxlmu9k4nUbWq6ohXahOneYA==", + "dependencies": { "charenc": ">= 0.0.1", "crypt": ">= 0.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" } }, - "side-channel": { + "node_modules/side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "requires": { + "dependencies": { "call-bind": "^1.0.0", "get-intrinsic": "^1.0.2", "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "simple-swizzle": { + "node_modules/simple-swizzle": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", - "requires": { + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dependencies": { "is-arrayish": "^0.3.1" } }, - "sinon": { + "node_modules/sinon": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/sinon/-/sinon-12.0.1.tgz", "integrity": "sha512-iGu29Xhym33ydkAT+aNQFBINakjq69kKO6ByPvTsm3yyIACfyQttRTP03aBP/I8GfhFmLzrnKwNNkr0ORb1udg==", - "requires": { + "dev": true, + "dependencies": { "@sinonjs/commons": "^1.8.3", "@sinonjs/fake-timers": "^8.1.0", "@sinonjs/samsam": "^6.0.2", "diff": "^5.0.0", "nise": "^5.1.0", "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" } }, - "sshpk": { + "node_modules/sshpk": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", - "requires": { + "dependencies": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", "bcrypt-pbkdf": "^1.0.0", @@ -1735,58 +4081,149 @@ "jsbn": "~0.1.0", "safer-buffer": "^2.0.2", "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" } }, - "stack-trace": { + "node_modules/stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "engines": { + "node": "*" + } }, - "statuses": { + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" + }, + "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, - "string-width": { + "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, - "requires": { + "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" } }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" + "node_modules/string.prototype.trim": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", + "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", + "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", + "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "strip-ansi": { + "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, - "requires": { + "dependencies": { "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" } }, - "strip-json-comments": { + "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "superagent": { + "node_modules/superagent": { "version": "3.8.3", "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", + "deprecated": "Please upgrade to v7.0.2+ of superagent. We have fixed numerous issues with streams, form-data, attach(), filesystem errors not bubbling up (ENOENT on attach()), and all tests are now passing. See the releases tab for more information at .", "dev": true, - "requires": { + "dependencies": { "component-emitter": "^1.2.0", "cookiejar": "^2.1.0", "debug": "^3.1.0", @@ -1798,159 +4235,406 @@ "qs": "^6.5.1", "readable-stream": "^2.3.5" }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/superagent/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - } + "ms": "^2.1.1" } }, - "supertest": { + "node_modules/superagent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/supertest": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/supertest/-/supertest-3.4.2.tgz", "integrity": "sha512-WZWbwceHUo2P36RoEIdXvmqfs47idNNZjCuJOqDz6rvtkk8ym56aU5oglORCpPeXGxT7l9rkJ41+O1lffQXYSA==", "dev": true, - "requires": { + "dependencies": { "methods": "^1.1.2", "superagent": "^3.8.3" + }, + "engines": { + "node": ">=6.0.0" } }, - "supports-color": { + "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "requires": { + "dev": true, + "dependencies": { "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "text-hex": { + "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" }, - "to-regex-range": { + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, - "requires": { + "dependencies": { "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" } }, - "toidentifier": { + "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } }, - "tough-cookie": { + "node_modules/tough-cookie": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "requires": { + "dependencies": { "psl": "^1.1.28", "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" } }, - "triple-beam": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", - "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", + "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, - "tunnel-agent": { + "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dependencies": { "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" } }, - "tweetnacl": { + "node_modules/tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } }, - "type-detect": { + "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "type-is": { + "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "requires": { + "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", + "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", + "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", + "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "unpipe": { + "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } }, - "uri-js": { + "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "requires": { + "dependencies": { "punycode": "^2.1.0" } }, - "util-deprecate": { + "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, - "utils-merge": { + "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } }, - "vary": { + "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } }, - "verror": { + "node_modules/verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "requires": { + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, - "which": { + "node_modules/verror/node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" + }, + "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, - "requires": { + "dependencies": { "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "winston": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.7.2.tgz", - "integrity": "sha512-QziIqtojHBoyzUOdQvQiar1DH0Xp9nF1A1y7NVy2DGEsz82SBDtOalS0ulTRGVT14xPX3WRWkCsdcJKqNflKng==", - "requires": { + "node_modules/which-typed-array": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", + "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/winston": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.10.0.tgz", + "integrity": "sha512-nT6SIDaE9B7ZRO0u3UvdrimG0HkB7dSTAgInQnNR2SOPJ4bvq5q79+pXLftKmP52lJGW15+H5MCK0nM9D3KB/g==", + "dependencies": { + "@colors/colors": "1.5.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", @@ -1962,76 +4646,97 @@ "triple-beam": "^1.3.0", "winston-transport": "^4.5.0" }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } + "engines": { + "node": ">= 12.0.0" } }, - "winston-transport": { + "node_modules/winston-transport": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz", "integrity": "sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==", - "requires": { + "dependencies": { "logform": "^2.3.2", "readable-stream": "^3.6.0", "triple-beam": "^1.3.0" }, + "engines": { + "node": ">= 6.4.0" + } + }, + "node_modules/winston-transport/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" } }, - "workerpool": { + "node_modules/winston/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/workerpool": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==", "dev": true }, - "wrap-ansi": { + "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, - "requires": { + "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "wrappy": { + "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, - "y18n": { + "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, - "yargs": { + "node_modules/yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, - "requires": { + "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", @@ -2039,31 +4744,46 @@ "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" } }, - "yargs-parser": { + "node_modules/yargs-parser": { "version": "20.2.4", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", - "dev": true + "dev": true, + "engines": { + "node": ">=10" + } }, - "yargs-unparser": { + "node_modules/yargs-unparser": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", "dev": true, - "requires": { + "dependencies": { "camelcase": "^6.0.0", "decamelize": "^4.0.0", "flat": "^5.0.2", "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" } }, - "yocto-queue": { + "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index 86e0ce8..4bc2683 100644 --- a/package.json +++ b/package.json @@ -2,32 +2,38 @@ "name": "bbb-webhooks", "version": "2.6.0", "description": "A BigBlueButton mudule for events WebHooks", + "type": "module", + "scripts": { + "start": "node app.js", + "test": "mocha", + "lint": "./node_modules/.bin/eslint ./", + "lint:file": "./node_modules/.bin/eslint" + }, "keywords": [ "bigbluebutton", "webhooks" ], "dependencies": { - "async": "^3.2.3", "body-parser": "^1.20.0", + "bullmq": "^4.11.4", "config": "^3.3.7", - "express": "^4.17.3", + "express": "^4.18.2", "js-yaml": "^4.1.0", - "lodash": "^4.17.21", "nock": "^13.2.4", - "redis": "^3.1.2", + "redis": "^4.6.8", "request": "^2.88.2", "sha1": "^1.1.1", - "sinon": "^12.0.1", - "winston": "^3.7.2" + "uuid": "^9.0.1", + "winston": "^3.10.0" }, "engines": { - "node": "^12.22" + "node": ">=18" }, "devDependencies": { + "eslint": "^8.49.0", + "eslint-plugin-import": "^2.28.1", "mocha": "^9.2.2", + "sinon": "^12.0.1", "supertest": "^3.4.2" - }, - "scripts": { - "test": "mocha" } } diff --git a/responses.js b/responses.js deleted file mode 100644 index fd814d0..0000000 --- a/responses.js +++ /dev/null @@ -1,46 +0,0 @@ -responses = {}; -responses.failure = (key, msg) => - ` \ -FAILED \ -${key} \ -${msg} \ -` -; -responses.checksumError = - responses.failure("checksumError", "You did not pass the checksum security check."); - -responses.createSuccess = (id, permanent, getRaw) => - ` \ -SUCCESS \ -${id} \ -${permanent} \ -${getRaw} \ -` -; -responses.createFailure = - responses.failure("createHookError", "An error happened while creating your hook. Check the logs."); -responses.createDuplicated = id => - ` \ -SUCCESS \ -${id} \ -duplicateWarning \ -There is already a hook for this callback URL. \ -` -; - -responses.destroySuccess = - ` \ -SUCCESS \ -true \ -`; -responses.destroyFailure = - responses.failure("destroyHookError", "An error happened while removing your hook. Check the logs."); -responses.destroyNoHook = - responses.failure("destroyMissingHook", "The hook informed was not found."); - -responses.missingParamCallbackURL = - responses.failure("missingParamCallbackURL", "You must specify a callbackURL in the parameters."); -responses.missingParamHookID = - responses.failure("missingParamHookID", "You must specify a hookID in the parameters."); - -module.exports = responses; diff --git a/src/common/logger.js b/src/common/logger.js new file mode 100644 index 0000000..5f3fda9 --- /dev/null +++ b/src/common/logger.js @@ -0,0 +1,115 @@ +'use strict'; + +import { addColors, format, createLogger, transports } from 'winston'; +import config from 'config'; +import jsonStringify from 'safe-stable-stringify'; + +const LOG_CONFIG = config.get('log') || {}; +const { + level: DEFAULT_LEVEL, + filename: DEFAULT_FILENAME, + stdout: STDOUT = true +} = LOG_CONFIG; +const { combine, colorize, timestamp, json, printf, errors, splat } = format; + +addColors({ + error: 'red', + warn: 'yellow', + info: 'green', + verbose: 'cyan', + debug: 'magenta', + trace: 'gray' +}); + +const LEVELS = { + error: 0, + warn: 1, + info: 2, + verbose: 3, + debug: 4, + trace: 5, +}; + +const shimmerLoggerWithLabel = (logger, label) => { + const shimmeredLogger = Object.assign({}, logger); + Object.keys(LEVELS).forEach((level) => { + shimmeredLogger[level] = (message, meta) => { + logger[level](`[${label}] ${message}`, meta); + } + }); + + return shimmeredLogger; +}; + +const _newLogger = ({ + filename, + level, + stdout, +}) => { + const loggingTransports = []; + + if (stdout) { + if (process.env.NODE_ENV !== 'production') { + // Development logging - fancier, more human readable stuff + loggingTransports.push(new transports.Console({ + format: combine( + colorize(), + timestamp(), + errors({ stack: true }), + printf(({ level, message, timestamp, ...meta}) => { + const stringifiedRest = jsonStringify(Object.assign({}, meta, { + splat: undefined + })); + + if (stringifiedRest !== '{}') { + return `${timestamp} - ${level}: ${message} ${stringifiedRest}`; + } else { + return `${timestamp} - ${level}: ${message}`; + } + }), + ) + })); + } else { + loggingTransports.push(new transports.Console({ + format: combine( + timestamp(), + splat(), + json(), + errors({ stack: true }), + ) + })); + } + } + + const logger = createLogger({ + levels: LEVELS, + level, + transports: loggingTransports, + exitOnError: false, + }); + + logger.on('error', (error) => { + // eslint-disable-next-line no-console + console.error("Logger failure", error); + }); + + return logger; +} + +const BASE_LOGGER = _newLogger({ + filename: DEFAULT_FILENAME, + level: DEFAULT_LEVEL, + stdout: STDOUT, +}); + +const logger = shimmerLoggerWithLabel(BASE_LOGGER, 'bbb-webhooks'); + +const newLogger = (label) => { + return shimmerLoggerWithLabel(BASE_LOGGER, label); +} + +export default logger; + +export { + newLogger, +}; diff --git a/utils.js b/src/common/utils.js similarity index 66% rename from utils.js rename to src/common/utils.js index 77c8442..7de4afa 100644 --- a/utils.js +++ b/src/common/utils.js @@ -1,27 +1,24 @@ -const sha1 = require("sha1"); -const url = require("url"); - -const config = require("config"); - -const Utils = exports; +import sha1 from "sha1"; +import url from "url"; +import config from "config"; // Calculates the checksum given a url `fullUrl` and a `salt`, as calculate by bbb-web. -Utils.checksumAPI = function(fullUrl, salt) { - const query = Utils.queryFromUrl(fullUrl); - const method = Utils.methodFromUrl(fullUrl); - return Utils.checksum(method + query + salt); +const checksumAPI = function(fullUrl, salt) { + const query = queryFromUrl(fullUrl); + const method = methodFromUrl(fullUrl); + return checksum(method + query + salt); }; // Calculates the checksum for a string. // Just a wrapper for the method that actually does it. -Utils.checksum = string => sha1(string); +const checksum = string => sha1(string); // Get the query of an API call from the url object (from url.parse()) // Example: // // * `fullUrl` = `http://bigbluebutton.org/bigbluebutton/api/create?name=Demo+Meeting&meetingID=Demo` // * returns: `name=Demo+Meeting&meetingID=Demo` -Utils.queryFromUrl = function(fullUrl) { +const queryFromUrl = function(fullUrl) { // Returns the query without the checksum. // We can't use url.parse() because it would change the encoding @@ -43,14 +40,14 @@ Utils.queryFromUrl = function(fullUrl) { // // * `fullUrl` = `http://mconf.org/bigbluebutton/api/create?name=Demo+Meeting&meetingID=Demo` // * returns: `create` -Utils.methodFromUrl = function(fullUrl) { +const methodFromUrl = function(fullUrl) { const urlObj = url.parse(fullUrl, true); return urlObj.pathname.substr((config.get("bbb.apiPath") + "/").length); }; // Returns the IP address of the client that made a request `req`. // If can not determine the IP, returns `127.0.0.1`. -Utils.ipFromRequest = function(req) { +const ipFromRequest = function(req) { // the first ip in the list if the ip of the client // the others are proxys between him and us @@ -66,3 +63,36 @@ Utils.ipFromRequest = function(req) { if (!ipAddress) { ipAddress = "127.0.0.1"; } return ipAddress; }; + +const isEmpty = (obj) => [Object, Array].includes((obj || {}).constructor) + && !Object.entries((obj || {})).length; + +const sortBy = (key) => (a, b) => { + if (a[key] > b[key]) return 1; + if (a[key] < b[key]) return -1; + return 0; +}; + +const stringify = (obj) => { + if (obj == null) return obj; + + switch (typeof obj) { + case "string": + return obj; + case "object": + return JSON.stringify(obj); + default: + return obj.toString(); + } +} + +export default { + checksumAPI, + checksum, + queryFromUrl, + methodFromUrl, + ipFromRequest, + isEmpty, + sortBy, + stringify, +}; diff --git a/src/db/redis/base-storage.js b/src/db/redis/base-storage.js new file mode 100644 index 0000000..87bf68d --- /dev/null +++ b/src/db/redis/base-storage.js @@ -0,0 +1,261 @@ +import { newLogger } from '../../common/logger.js'; +import { v4 as uuidv4 } from 'uuid'; + +const stringifyValues = (o) => { + Object.keys(o).forEach(k => { + // Make all values strings, but ignore nullish/undefined values. + if (typeof o[k] === 'object') { + o[k] = JSON.stringify(stringifyValues(o[k])); + } else if (o[k] == null) { + delete o[k]; + } else { + o[k] = '' + o[k]; + } + }); + + return o; +} + +class StorageItem { + constructor(client, prefix, setId, payload, { + id = uuidv4(), + alias, + ...appOptions + }) { + this.client = client; + this.prefix = prefix; + this.setId = setId; + this.id = id; + this.alias = alias; + this.payload = payload; + this.appOptions = appOptions; + this.redisClient = client; + this.logger = newLogger(`db:${this.prefix}|${this.setId}`); + } + + async save() { + try { + await this.redisClient.hSet(this.prefix + ":" + this.id, this.serialize()); + } catch (error) { + this.logger.error(`error saving mapping to redis: ${error}`); + throw error; + } + + try { + await this.redisClient.sAdd(this.setId, (this.id).toString()); + } catch (error) { + this.logger.error(`error saving mapping ID to the list of mappings: ${error}`); + throw error; + } + + return true; + } + + async destroy() { + try { + await this.redisClient.sRem(this.setId, (this.id).toString()); + } catch (error) { + this.logger.error(`error removing mapping ID from the list of mappings: ${error}`); + } + + try { + await this.redisClient.del(this.prefix + ":" + this.id); + } catch (error) { + this.logger.error(`error removing mapping from redis: ${error}`); + } + + return true; + } + + serialize() { + const r = { + id: this.id, + ...this.payload, + }; + + const s = Object.entries(stringifyValues(r)).flat(); + return s; + } + + deserialize(data) { + const { id, ...payload } = data; + this.id = id; + this.payload = payload; + } + + print() { + return this.serialize(); + } +} + +class StorageCompartmentKV { + constructor (client, prefix, setId, { + itemClass = StorageItem, + aliasField, + ...appOptions + } = {}) { + this.redisClient = client; + this.prefix = prefix; + this.setId = setId; + this.itemClass = itemClass; + this.localStorage = {} + this.aliasField = aliasField; + this.appOptions = appOptions; + + this.logger = newLogger(`db:${this.prefix}|${this.setId}`); + } + + async save(payload, { + id = uuidv4(), + alias, + }) { + if (alias == null) { + alias = payload[this.aliasField]; + } + + let mapping = new this.itemClass(this.redisClient, this.prefix, this.setId, payload, { + id, + alias, + }); + + await mapping.save(); + this.localStorage[mapping.id] = mapping; + if (mapping.alias) this.localStorage[mapping.alias] = mapping; + + return mapping; + } + + async find(id) { + return this.localStorage[id]; + } + + async destroy(id) { + const mapping = this.localStorage[id]; + if (mapping) { + await mapping.destroy(); + delete this.localStorage[id]; + if (mapping.alias && this.localStorage[mapping.alias]) { + delete this.localStorage[mapping.alias]; + } + return mapping; + } + + return false; + } + + count() { + return Object.keys(this.localStorage).length; + } + + async destroyWithField(field, value) { + return Promise.all( + Object.keys(this.localStorage).map(internal => { + let mapping = this.localStorage[internal]; + if (mapping.payload[field] === value) { + return mapping.destroy() + .then(() => { + return mapping; + }) + .catch(() => { + return false; + }).finally(() => { + if (this.localStorage[mapping.id]) { + delete this.localStorage[mapping.id]; + } + + if (mapping.alias && this.localStorage[mapping.alias]) { + delete this.localStorage[mapping.alias]; + } + }); + } else { + return false; + } + }) + ); + } + + findByField(field, value) { + if (field != null && value != null) { + for (let internal in this.localStorage) { + const mapping = this.localStorage[internal]; + if (mapping != null && mapping.payload[field] === value) { + return mapping; + } + } + } + + return null; + } + + updateWithField(field, value, payload) { + if (field != null && value != null) { + for (let internal in this.localStorage) { + const mapping = this.localStorage[internal]; + if (mapping != null && mapping.payload[field] === value) { + mapping.payload = { ...mapping.payload, ...payload }; + return mapping.save(); + } + } + } + + return Promise.resolve(false); + } + + getAll() { + const allWithAliases = Object.keys(this.localStorage).reduce((arr, id) => { + arr.push(this.localStorage[id]); + return arr; + }, []); + + return [...new Set(allWithAliases)]; + } + + // Initializes global methods for this model. + async initialize() { + return this.resync() + } + + // Gets all mappings from redis to populate the local database. + async resync() { + try { + const mappings = await this.redisClient.sMembers(this.setId); + + if (mappings != null && mappings.length > 0) { + return Promise.all(mappings.map(async (id) => { + try { + const kek = await this.redisClient.hGetAll(this.prefix + ":" + id); + const { id: rId, ...mappingData } = kek; + + if (mappingData && Object.keys(mappingData).length > 0) { + await this.save(mappingData, { + id: rId, + alias: mappingData[this.aliasField], + }); + } + + return Promise.resolve(); + } catch (error) { + this.logger.error(`error getting information for a mapping from redis: ${error}`); + return Promise.resolve(); + } + })).then(() => { + const stringifiedMappings = this.getAll().map(m => m.print()); + this.logger.info(`finished resync, mappings registered: [${stringifiedMappings}]`); + }).catch((error) => { + this.logger.error(`error getting list of mappings from redis: ${error}`); + }); + } + + this.logger.info(`finished resync, no mappings registered`); + return Promise.resolve(); + } catch (error) { + this.logger.error(`error getting list of mappings from redis: ${error}`); + return Promise.resolve(); + } + } +} + +export { + StorageItem, + StorageCompartmentKV, +}; diff --git a/src/db/redis/hooks.js b/src/db/redis/hooks.js new file mode 100644 index 0000000..f66bd3d --- /dev/null +++ b/src/db/redis/hooks.js @@ -0,0 +1,155 @@ +import config from 'config'; +import { + v4 as uuidv4, + v5 as uuidv5, +} from 'uuid'; +import { StorageItem, StorageCompartmentKV } from './base-storage.js'; + +// The database of hooks. +// Used always from memory, but saved to redis for persistence. +// +// Format: +// { id: Hook } +// Format on redis: +// * a SET "...:hooks" with all ids +// * a HASH "...:hook:" for each hook with some of its attributes + +// The representation of a hook and its properties. Stored in memory and persisted +// to redis. +// Hooks can be global, receiving callback calls for events from all meetings on the +// server, or for a specific meeting. If an `externalMeetingID` is set in the hook, +// it will only receive calls related to this meeting, otherwise it will be global. +// faster than the callbacks are made. In this case the events will be concatenated +// and send up to 10 events in every post + +let REDIS_CLIENT = null; + +class HookCompartment extends StorageCompartmentKV { + constructor(client, prefix, setId, options = {}) { + super(client, prefix, setId, options); + } + + setOptions(options) { + this.permanentURLs = options.permanentURLs; + } + + getHook(id) { + return this.find(id); + } + + isGlobal(item) { + return item?.externalMeetingID == null; + } + + getExternalMeetingID(id) { + const hook = this.getHook(id); + return hook?.externalMeetingID; + } + + findByExternalMeetingID(externalMeetingID) { + return this.findByField('externalMeetingID', externalMeetingID); + } + + async addSubscription({ + callbackURL, + meetingID, + eventID, + permanent, + getRaw, + }) { + const payload = { + callbackURL, + externalMeetingID: meetingID, + eventID: eventID?.toLowerCase().split(','), + permanent, + getRaw, + }; + + let hook = this.findByField('callbackURL', callbackURL); + + if (hook != null) { + return { + duplicated: true, + hook, + } + } + + let logMsg = `adding a hook with callback URL: [${callbackURL}],`; + if (meetingID != null) { logMsg += ` for the meeting: [${meetingID}]`; } + this.logger.info(logMsg); + const id = permanent ? uuidv5(callbackURL, uuidv5.URL) : uuidv4(); + hook = await this.save(payload, { + id, + alias: callbackURL, + }); + + return { + duplicated: false, + hook, + } + } + + async removeSubscription(hookID) { + const hook = await this.getSync(hookID); + + if (hook != null && !hook.payload.permanent) { + let msg = `removing the hook with callback URL: [${hook.payload.callbackURL}],`; + if (hook.externalMeetingID != null) msg += ` for the meeting: [${hook.payload.externalMeetingID}]`; + this.logger.info(msg); + return this.destroy(hookID); + } + + return Promise.resolve(false); + } + + countSync() { + return this.count(); + } + + getSync(id) { + return this.find(id); + } + + firstSync() { + const keys = Object.keys(this.localStorage); + if (keys.length > 0) return this.localStorage[keys[0]]; + return null; + } + + allGlobalSync() { + return this.getAll().filter(hook => this.isGlobal(hook)); + } + + // Initializes global methods for this model. + initialize() { + return this.resync(); + } +} + +let Hooks = null; + +const init = (redisClient, prefix, setId) => { + if (Hooks == null) { + Hooks = new HookCompartment( + redisClient, + prefix, + setId, + ); + return Hooks.initialize(); + } + + return Promise.resolve(Hooks); +} + +const get = () => { + if (Hooks == null) { + throw new Error('Hooks not initialized'); + } + + return Hooks; +} + +export default { + init, + get, +} diff --git a/src/db/redis/id-mapping.js b/src/db/redis/id-mapping.js new file mode 100644 index 0000000..3d897ee --- /dev/null +++ b/src/db/redis/id-mapping.js @@ -0,0 +1,128 @@ +import config from 'config'; +import { StorageItem, StorageCompartmentKV } from './base-storage.js'; +import UserMapping from './user-mapping.js'; +import { v4 as uuidv4 } from 'uuid'; + +// The database of mappings. Uses the internal ID as key because it is unique +// unlike the external ID. +// Used always from memory, but saved to redis for persistence. +// +// Format: +// { +// internalMeetingID: { +// id: @id +// externalMeetingID: @externalMeetingID +// internalMeetingID: @internalMeetingID +// lastActivity: @lastActivity +// } +// } +// Format on redis: +// * a SET "...:mappings" with all ids (not meeting ids, the object id) +// * a HASH "...:mapping:" for each mapping with all its attributes + +// A simple model to store mappings for meeting IDs. +class IDMappingCompartment extends StorageCompartmentKV { + constructor(client, prefix, setId) { + super(client, prefix, setId, { + aliasField: 'internalMeetingID', + }); + } + + async addOrUpdateMapping(internalMeetingID, externalMeetingID) { + const payload = { + internalMeetingID: internalMeetingID, + externalMeetingID: externalMeetingID, + lastActivity: new Date().getTime(), + }; + + const mapping = await this.save(payload, { + alias: internalMeetingID, + }); + this.logger.info(`added or changed meeting mapping to the list ${externalMeetingID}: ${mapping.print()}`); + + return mapping; + } + + async removeMapping(internalMeetingID) { + const result = await this.destroyWithField('internalMeetingID', internalMeetingID); + return result; + } + + getInternalMeetingID(externalMeetingID) { + const mapping = this.findByField('externalMeetingID', externalMeetingID); + return (mapping != null ? mapping.payload?.internalMeetingID : undefined); + } + + getExternalMeetingID(internalMeetingID) { + if (this.localStorage[internalMeetingID]) { + return this.localStorage[internalMeetingID].payload?.externalMeetingID; + } + } + + findByExternalMeetingID(externalMeetingID) { + return this.findByField('externalMeetingID', externalMeetingID); + } + + allSync() { + return this.getAll(); + } + + // Sets the last activity of the mapping for `internalMeetingID` to now. + reportActivity(internalMeetingID) { + let mapping = this.localStorage[internalMeetingID]; + if (mapping != null) { + mapping.payload.lastActivity = new Date().getTime(); + return mapping.save(); + } + } + + // Checks all current mappings for their last activity and removes the ones that + // are "expired", that had their last activity too long ago. + cleanup() { + const now = new Date().getTime(); + const all = this.getAll(); + const toRemove = all.filter(mapping => mapping?.payload.lastActivity < (now - config.get("mappings.timeout"))); + + if (toRemove && toRemove.length > 0) { + this.logger.info(`expiring the mappings: ${toRemove.map(map => map.print())}`); + toRemove.forEach(mapping => { + UserMapping.removeMappingMeetingId(mapping.internalMeetingID); + mapping.destroy() + }); + } + } + + // Initializes global methods for this model. + initialize() { + this.cleanupInterval = setInterval(this.cleanup.bind(this), config.get("mappings.cleanupInterval")); + return this.resync(); + } +} + +let IDMapping = null; + +const init = (redisClient) => { + if (IDMapping == null) { + IDMapping = new IDMappingCompartment( + redisClient, + config.get('redis.keys.mappingPrefix'), + config.get('redis.keys.mappings') + ); + return IDMapping.initialize(); + } + + return Promise.resolve(IDMapping); +} + +const get = () => { + if (IDMapping == null) { + throw new Error('IDMapping not initialized'); + } + + return IDMapping; +} + +export default { + init, + get, +} diff --git a/src/db/redis/index.js b/src/db/redis/index.js new file mode 100644 index 0000000..dd73a81 --- /dev/null +++ b/src/db/redis/index.js @@ -0,0 +1,124 @@ +import redis from 'redis'; +import config from 'config'; +import HookCompartment from './hooks.js'; +import IDMappingC from './id-mapping.js'; +import UserMappingC from './user-mapping.js'; + +/* + * + * [MODULE_TYPES.db]: { + * load: 'function', + * unload: 'function', + * setContext: 'function', + * save: 'function', + * read: 'function', + * remove: 'function', + * clear: 'function', + * } + * + */ + +export default class RedisDB { + constructor (context, config = {}) { + this.name = 'db-redis'; + this.type = 'db'; + this.context = this.setContext(context); + this.config = config; + this.logger = context.getLogger(this.name); + this.loaded = false; + + this._redisClient = null; + } + + _onRedisError(error) { + this.logger.error("Redis error: ", { error }); + } + + async load() { + const { host, port, password } = this.config; + + this._redisClient = redis.createClient({ + host, + port, + password, + }); + this._redisClient.on("error", this._onRedisError.bind(this)); + await this._redisClient.connect(); + await IDMappingC.init(this._redisClient); + await UserMappingC.init(this._redisClient); + await HookCompartment.init(this._redisClient, + config.get('redis.keys.hookPrefix'), + config.get('redis.keys.hooks'), + ); + } + + async unload() { + if (this._redisClient) { + await this._redisClient.disconnect(); + this._redisClient = null; + } + + this.loaded = false; + } + + setContext(context) { + this.context = context; + this.logger = context.getLogger(this.name); + } + + async save(key, value) { + if (!this.loaded) { + throw new Error('DB not loaded'); + } + + try { + await this._redisClient.set(key, JSON.stringify(value)); + return true; + } catch (error) { + this.logger.error(error); + return false; + } + } + + async read(key) { + if (!this.loaded) { + throw new Error('DB not loaded'); + } + + try { + const value = await this._redisClient.get(key); + return JSON.parse(value); + } catch (error) { + this.logger.error(error); + throw error; + } + } + + async remove(key) { + if (!this.loaded) { + throw new Error('DB not loaded'); + } + + try { + await this._redisClient.del(key); + return true; + } catch (error) { + this.logger.error(error); + return false; + } + } + + async clear() { + if (!this.loaded) { + throw new Error('DB not loaded'); + } + + try { + await this._redisClient.flushall(); + return true; + } catch (error) { + this.logger.error(error); + return false; + } + } +} diff --git a/src/db/redis/user-mapping.js b/src/db/redis/user-mapping.js new file mode 100644 index 0000000..617762a --- /dev/null +++ b/src/db/redis/user-mapping.js @@ -0,0 +1,81 @@ +import config from 'config'; +import Logger from '../../common/logger.js'; +import { v4 as uuidv4 } from 'uuid'; +import { StorageItem, StorageCompartmentKV } from './base-storage.js'; + +class UserMappingCompartment extends StorageCompartmentKV { + constructor(client, prefix, setId) { + super(client, prefix, setId, { + aliasField: 'internalUserID', + }); + } + + async addOrUpdateMapping(internalUserID, externalUserID, meetingId, user) { + const payload = { + internalUserID, + externalUserID, + meetingId, + user, + }; + + const mapping = await this.save(payload, { + alias: internalUserID, + }); + this.logger.info(`added user mapping to the list ${internalUserID}: ${mapping.print()}`); + + return mapping; + } + + async removeMapping(internalUserID) { + const result = await this.destroyWithField('internalUserID', internalUserID); + return result; + } + + async getInternalMeetingID(externalMeetingID) { + const mapping = await this.findByField('externalMeetingID', externalMeetingID); + return (mapping != null ? mapping.payload?.internalMeetingID : undefined); + } + + getUser(internalUserID) { + const mapping = this.findByField('internalUserID', internalUserID); + return (mapping != null ? mapping.payload?.user : undefined); + } + + getExternalUserID(internalUserID) { + const mapping = this.findByField('internalUserID', internalUserID); + return (mapping != null ? mapping.payload?.externalUserID : undefined); + } + + // Initializes global methods for this model. + initialize() { + return this.resync(); + } +} + +let UserMapping = null; + +const init = (redisClient) => { + if (UserMapping == null) { + UserMapping = new UserMappingCompartment( + redisClient, + config.get('redis.keys.userMapPrefix'), + config.get('redis.keys.userMaps') + ); + return UserMapping.initialize(); + } + + return Promise.resolve(UserMapping); +} + +const get = () => { + if (UserMapping == null) { + throw new Error('UserMapping not initialized'); + } + + return UserMapping; +} + +export default { + init, + get, +} diff --git a/src/in/redis/index.js b/src/in/redis/index.js new file mode 100644 index 0000000..4c2802c --- /dev/null +++ b/src/in/redis/index.js @@ -0,0 +1,111 @@ +import redis from 'redis'; +import Utils from '../../common/utils.js'; + +/* + * [MODULE_TYPES.in]: { + * load: 'function', + * unload: 'function', + * setContext: 'function', + * setCollector: 'function', + * }, + * + */ + +export default class InRedis { + static _defaultCollector () { + throw new Error('Collector not set'); + } + + constructor (context, config = {}) { + this.type = "in"; + this.config = config; + this.setContext(context); + + this.pubsub = null; + } + + _validateConfig () { + if (this.config == null) { + throw new Error("config not set"); + } + + if (this.config.redis == null) { + throw new Error("config.redis not set"); + } + + if (this.config.redis.host == null) { + throw new Error("config.host not set"); + } + + if (this.config.redis.port == null) { + throw new Error("config.port not set"); + } + + if (this.config.redis.inboundChannels == null || this.config.redis.inboundChannels.length == 0) { + throw new Error("config.inboundChannels not set"); + } + + return true; + } + + _onPubsubEvent(message, channel) { + this.logger.trace('Received message on pubsub', { message }); + + try { + const event = JSON.parse(message); + + if (Utils.isEmpty(event)) return; + + this._collector(event); + } catch (error) { + this.logger.error(`Error processing message on [${channel}]: ${error}`); + } + } + + _subscribeToEvents() { + if (this.pubsub == null) { + throw new Error("pubsub not initialized"); + } + + return Promise.all( + this.config.redis.inboundChannels.map((channel) => { + return this.pubsub.subscribe(channel, this._onPubsubEvent.bind(this)) + .then(() => this.logger.info(`subscribed to: ${channel}`)) + .catch((error) => this.logger.error(`error subscribing to: ${channel}: ${error}`)); + }) + ); + } + + async load () { + if (this._validateConfig()) { + this.pubsub = redis.createClient({ + host: this.config.host, + port: this.config.port, + password: this.config.password, + }); + + await this.pubsub.connect(); + await this._subscribeToEvents(); + } + } + + async unload () { + if (this.pubsub != null) { + await this.pubsub.disconnect(); + this.pubsub = null; + } + + this.setCollector(InRedis._defaultCollector); + } + + setContext (context) { + this.context = context; + this.logger = context.getLogger(); + + return context; + } + + async setCollector (event) { + this._collector = event; + } +} diff --git a/src/modules/context.js b/src/modules/context.js new file mode 100644 index 0000000..7504b2c --- /dev/null +++ b/src/modules/context.js @@ -0,0 +1,41 @@ +import { newLogger } from '../common/logger.js'; +import config from 'config'; +import { StorageCompartmentKV, StorageItem } from '../db/redis/base-storage.js'; + +export default class Context { + constructor(configuration) { + this.name = configuration.name; + this.configuration = config.util.cloneDeep(configuration); + this._loggers = new Map(); + } + + getLogger(label = this.name) { + if (!this._loggers.has(label)) { + this._loggers.set(label, newLogger(label)); + } + + return this._loggers.get(label); + } + + destroy () { + this._loggers.clear(); + } + + keyValueCompartmentConstructor () { + return StorageCompartmentKV; + } + + keyValueItemConstructor () { + return StorageItem; + } + + keyValueStorageFactory (prefix, setId, { + itemClass = StorageItem, + aliasField, + } = {}) { + return new StorageCompartmentKV(this.redisClient, prefix, setId, { + itemClass, + aliasField, + }); + } +} diff --git a/src/modules/definitions.js b/src/modules/definitions.js new file mode 100644 index 0000000..00b994d --- /dev/null +++ b/src/modules/definitions.js @@ -0,0 +1,71 @@ +export const MODULE_TYPES = { + in: 'in', + out: 'out', + db: 'db', +}; + +export const MODULE_DEFINITION_SCHEMA = { + [MODULE_TYPES.in]: { + load: 'function', + unload: 'function', + setContext: 'function', + setCollector: 'function', + }, + [MODULE_TYPES.out]: { + load: 'function', + unload: 'function', + setContext: 'function', + onEvent: 'function', + }, + [MODULE_TYPES.db]: { + load: 'function', + unload: 'function', + setContext: 'function', + save: 'function', + read: 'function', + remove: 'function', + clear: 'function', + }, +} + +export function validateModuleDefinition(module) { + if (!module.type || !MODULE_TYPES[module.type]) { + throw new Error('Module spec must be one of in | out | db'); + } + + Object.keys(MODULE_DEFINITION_SCHEMA[module.type]).forEach((key) => { + if (typeof module[key] !== MODULE_DEFINITION_SCHEMA[module.type][key]) { + throw new Error(`Module spec must have ${key} of type ${MODULE_DEFINITION_SCHEMA[module.type][key]}`); + } + }); + + return true; +} + +export function validateModuleConf (conf) { + if (!conf.name) { + throw new Error('Module spec must have a name'); + } + + if (!conf.type) { + throw new Error('Module spec must have a type'); + } + + if (!MODULE_TYPES[conf.type]) { + throw new Error(`Module spec has invalid type ${conf.type}`); + } + + return true; +} + +export function validateModulesConf(conf) { + if (typeof conf !== 'object') { + throw new Error('Module spec must be an object'); + } + + if (Object.keys(conf).length === 0) { + throw new Error('Module spec must have at least one element'); + } + + return true; +} diff --git a/src/modules/index.js b/src/modules/index.js new file mode 100644 index 0000000..bf75815 --- /dev/null +++ b/src/modules/index.js @@ -0,0 +1,132 @@ +'use strict'; + +import config from 'config'; +import { newLogger } from '../common/logger.js'; +import ModuleWrapper from './module-wrapper.js'; +import Context from './context.js'; +import { + MODULE_TYPES, + validateModulesConf, + validateModuleConf +} from './definitions.js'; + + +const UNEXPECTED_TERMINATION_SIGNALS = ['SIGABRT', 'SIGBUS', 'SIGSEGV', 'SIGILL']; +const BASE_CONFIGURATION = { + server: { + domain: config.get('bbb.serverDomain'), + secret: config.get('bbb.sharedSecret'), + auth2_0: config.get('bbb.auth2_0'), + }, + redis: { + host: config.get('redis.host'), + port: config.get('redis.port'), + password: config.has('redis.password') ? config.get('redis.password') : undefined, + }, +} + +export default class ModuleManager { + static moduleTypes = MODULE_TYPES; + + constructor(modulesConfig) { + this.modulesConfig = modulesConfig; + this.modules = {}; + validateModulesConf(modulesConfig); + this.logger = newLogger('module-manager'); + } + + _buildContext(configuration) { + configuration.config = { ...BASE_CONFIGURATION, ...configuration.config }; + return new Context(configuration); + } + + getModulesByType(type) { + return Object.values(this.modules).filter((module) => module.type === type); + } + + getInputModules() { + return this.getModulesByType(ModuleManager.moduleTypes.in); + } + + getOutputModules() { + return this.getModulesByType(ModuleManager.moduleTypes.out); + } + + getDBModules() { + return this.getModulesByType(ModuleManager.moduleTypes.db); + } + + async load() { + const loaders = Object.entries(this.modulesConfig).map(([name, description]) => { + try { + const fullConfiguration = { name, ...description }; + validateModuleConf(fullConfiguration); + const context = this._buildContext(fullConfiguration); + const module = new ModuleWrapper(name, description.type, context, context.configuration.config); + return module.load().then(() => { + this.modules[name] = module; + this.logger.info(`module ${name} loaded`); + }).catch((error) => { + this.logger.error(`failed to load module ${name}`, { error: error.stack }); + }); + } catch (error) { + this.logger.error(`failed to load module ${name} configuration`, { error: error.stack }); + return Promise.resolve(); + } + }); + + await Promise.all(loaders); + + process.on('SIGTERM', async () => { + await this.stopModules(); + process.exit(0); + }); + + process.on('SIGINT', async () => { + await this.stopModules(); + process.exit(0); + }); + + process.on('uncaughtException', async (error) => { + this.logger.error("CRITICAL: uncaught exception, shutdown", { error: error.stack }); + await this.stopModules(); + process.exit('1'); + }); + + // Added this listener to identify unhandled promises, but we should start making + // sense of those as we find them + process.on('unhandledRejection', (reason) => { + this.logger.error("CRITICAL: Unhandled promise rejection", { reason: reason.toString(), stack: reason.stack }); + }); + } + + trackModuleShutdown (proc) { + // Tries to restart process on unsucessful exit + proc.process.on('exit', (code, signal) => { + const shouldRestart = this.runningState === 'RUNNING' + && (code === 1 || UNEXPECTED_TERMINATION_SIGNALS.includes(signal)); + if (shouldRestart) { + this.logger.error("received exit event from child process, restarting it", + { code, signal, pid: proc.process.pid, process: proc.path }); + proc.restart(); + } else { + this.logger.warn("received final exit event from child process, process shutdown", + { code, signal, pid: proc.process.pid, process: proc.path }); + proc.stop(); + } + }); + } + + async stopModules () { + this.runningState = "STOPPING"; + + for (var proc in this.modules) { + if (Object.prototype.hasOwnProperty.call(this.modules, proc)) { + let procObj = this.modules[proc]; + if (typeof procObj.stop === 'function') procObj.stop() + } + } + + this.runningState = "STOPPED"; + } +} diff --git a/src/modules/module-wrapper.js b/src/modules/module-wrapper.js new file mode 100644 index 0000000..400a452 --- /dev/null +++ b/src/modules/module-wrapper.js @@ -0,0 +1,197 @@ +'use strict'; + +import { newLogger } from '../common/logger.js'; +import { MODULE_TYPES, validateModuleDefinition } from './definitions.js'; +import { createQueue, getQueue, deleteQueue } from './queue.js'; + +// [MODULE_TYPES.INPUT]: { +// load: 'function', +// setContext: 'function', +// setCollector: 'function', +// }, +// [MODULE_TYPES.OUTPUT]: { +// load: 'function', +// setContext: 'function', +// onEvent: 'function', +// }, +// [MODULE_TYPES.DB]: { +// load: 'function', +// setContext: 'function', +// create: 'function', +// read: 'function', +// update: 'function', +// delete: 'function', +// }, + +export default class ModuleWrapper { + static _defaultCollector () { + throw new Error('Collector not set'); + } + + constructor (name, type, context, config = {}) { + this.name = name; + this.context = context; + this.config = config; + this.logger = newLogger('module-wrapper'); + this.logger.debug(`created module wrapper for ${name}`, { type, config }); + + this._module = null; + } + + set config(config) { + if (config.queue == null) { + config.queue = { + enabled: false, + }; + } + + // Configuration enrichment - extend with defaults if some specific things + // are not set (eg redis host/port for queues) + if (config.queue?.enabled) { + if (!config.queue.host) { + config.queue.host = config.redis.host; + } + + if (!config.queue.port) { + config.queue.port = config.redis.port; + } + + if (!config.queue.password) { + config.queue.password = config.redis.password; + } + } + + this._config = config; + } + + get config() { + return this._config; + } + + get type() { + return this._module?.type; + } + + _setupOutboundQueues() { + if (this.type !== MODULE_TYPES.out) return; + + if (this.config.queue.enabled) { + this.logger.debug(`setting up outbound queues for module ${this.name}`, this.config.queue); + const queueId = this.config.queue.id || `${this.name}-out-queue`; + const processor = async (job) => { + if (job.name !== 'event') { + this.logger.error(`job ${job.name}:${job.id} is not an event`); + return; + } + const { event, raw } = job.data; + await this._onEvent(event, raw); + }; + const { queue, worker } = createQueue(queueId, processor, { + host: this.config.queue.host, + port: this.config.queue.port, + password: this.config.queue.password, + concurrency: this.config.queue.concurrency || 1, + }); + + this.logger.info(`created queue ${queueId} for module ${this.name}`, { + queueId, + queueConcurrency: this.config.queue.concurrency || 1, + }); + } + } + + _bootstrap() { + if (!this._module) { + throw new Error(`module ${this.name} is not loaded`); + } + + switch (this.type) { + case MODULE_TYPES.in: + this.setContext(this.context); + this.setCollector(this.context.collector || ModuleWrapper._defaultCollector); + break; + case MODULE_TYPES.out: + this.setContext(this.context); + this._setupOutboundQueues(); + break; + case MODULE_TYPES.db: + this.setContext(this.context); + break; + default: + throw new Error(`module ${this.name} has an invalid type`); + } + } + + setCollector(collector) { + if (typeof collector !== 'function') { + throw new Error(`collector must be a function`); + } + + if (this._module?.setCollector) { + this._module.setCollector(collector); + } else { + throw new Error(`module ${this.name} does not support setCollector`); + } + } + + async load() { + // Dynamically import the module provided via this.name + // and instantiate it with the context and config provided + this._module = await import(this.name).then((module) => { + return new module.default(this.context, this.config); + }).catch((error) => { + this.logger.error(`error loading module ${this.name}`, error); + throw error; + }); + + + // Validate the module + if (!validateModuleDefinition(this._module)) { + throw new Error(`module ${this.name} is not valid`); + } + + this._bootstrap(); + // Call the module's load() method + await this._module.load(); + + this.logger.info(`module ${this.name} loaded`); + + return this; + } + + unload() { + if (this._module?.unload) { + return this._module.unload(); + } + } + + setContext(context) { + if (this._module?.setContext) { + return this._module.setContext(context); + } + + throw new Error("Not implemented"); + } + + _onEvent(event, raw) { + if (this._module?.onEvent) { + return this._module.onEvent(event, raw); + } + + throw new Error("Not implemented"); + } + + async onEvent(event, raw) { + if (this.type !== MODULE_TYPES.out) { + throw new Error(`module ${this.name} is not an output module`); + } + + if (this.config.queue.enabled) { + const queueId = this.config.queue.id || `${this.name}-out-queue`; + const { queue } = getQueue(queueId); + const job = await queue.add('event', { event, raw }); + } else { + return this._onEvent(event, raw); + } + } +} diff --git a/src/modules/queue.js b/src/modules/queue.js new file mode 100644 index 0000000..60612a0 --- /dev/null +++ b/src/modules/queue.js @@ -0,0 +1,78 @@ +import { Queue, Worker } from 'bullmq' + +const queues = new Map(); + +// Create a new connection in every instance +const createQueue = (id, processor, { + host, + port, + password, + concurrency = 1, +}) => { + if (queues.has(id)) return queues.get(id); + + const queue = new Queue(id, { + connection: { + host, + port, + password, + }, + concurrency, + }); + + const worker = new Worker(id, processor, { + connection: { + host, + port, + password, + }, + }); + + queues.set(id, { + queue, + worker, + }); + + return { + queue, + worker, + }; +}; + +const addJob = (id, job) => { + const queue = queues.get(id); + + if (queue) { + queue.queue.add(job); + } + + return queue; +} + +const getQueue = (id) => { + return queues.get(id); +} + +const getQueues = () => { + return queues; +} + +const deleteQueue = (id) => { + const queue = queues.get(id); + + if (queue) { + queue.queue.close(); + queue.worker.close(); + queues.delete(id); + } + + return queue; +} + +export { + createQueue, + addJob, + getQueue, + getQueues, + deleteQueue, +}; diff --git a/src/out/webhooks/api/api.js b/src/out/webhooks/api/api.js new file mode 100644 index 0000000..6d30b6e --- /dev/null +++ b/src/out/webhooks/api/api.js @@ -0,0 +1,227 @@ +import express from 'express'; +import url from 'url'; +import { newLogger } from '../../../common/logger.js'; +import Utils from '../../../common/utils.js'; +import responses from './responses.js'; + +// Returns a simple string with a description of the client that made +// the request. It includes the IP address and the user agent. +const clientDataSimple = req => `ip ${Utils.ipFromRequest(req)}, using ${req.headers["user-agent"]}`; + +// Cleans up a string with an XML in it removing spaces and new lines from between the tags. +const cleanupXML = string => string.trim().replace(/>\s*/g, '>'); + +// Was this request made by monit? +// TODO remove/review +const fromMonit = req => (req.headers["user-agent"] != null) && req.headers["user-agent"].match(/^monit/); + +// Web server that listens for API calls and process them. +export default class API { + static logger = newLogger('api'); + static setStorage (storage) { + API.storage = storage; + } + + static respondWithXML(res, msg) { + msg = cleanupXML(msg); + API.logger.info(`respond with: ${msg}`); + res.setHeader("Content-Type", "text/xml"); + res.send(msg); + } + + constructor(options = {}) { + this.app = express(); + + this._permanentURLs = options.permanentURLs || []; + this._secret = options.secret; + this._validateChecksum = this._validateChecksum.bind(this); + + this._registerRoutes(); + } + + start(port, bind) { + return new Promise((resolve, reject) => { + this.server = this.app.listen(port, bind, () => { + if (this.server.address() == null) { + API.logger.error(`aborting, could not bind to port ${port}`); + return reject(new Error(`API failed to start, EARADDRINUSE`)); + } + API.logger.info(`listening on port ${port} in ${this.app.settings.env.toUpperCase()} mode`); + return resolve(); + }); + }); + } + + _registerRoutes() { + // Request logger + this.app.all("*", (req, res, next) => { + if (!fromMonit(req)) { + API.logger.info(`${req.method} request to ${req.url} from: ${clientDataSimple(req)}`); + } + next(); + }); + + this.app.get("/bigbluebutton/api/hooks/create", this._validateChecksum, this._create.bind(this)); + this.app.get("/bigbluebutton/api/hooks/destroy", this._validateChecksum, this._destroy); + this.app.get("/bigbluebutton/api/hooks/list", this._validateChecksum, this._list); + this.app.get("/bigbluebutton/api/hooks/ping", (req, res) => { + res.write("bbb-webhooks up!"); + res.end(); + }); + } + + _isHookPermanent(callbackURL) { + return this._permanentURLs.some(obj => { + return obj.url === callbackURL + }); + } + + async _create(req, res, next) { + const urlObj = url.parse(req.url, true); + const callbackURL = urlObj.query["callbackURL"]; + const meetingID = urlObj.query["meetingID"]; + const eventID = urlObj.query["eventID"]; + let getRaw = urlObj.query["getRaw"]; + + if (getRaw) { + getRaw = JSON.parse(getRaw.toLowerCase()); + } else { + getRaw = false; + } + + if (callbackURL == null) { + API.respondWithXML(res, responses.missingParamCallbackURL); + return; + } + + try { + const { hook, duplicated } = await API.storage.get().addSubscription({ + callbackURL, + meetingID, + eventID, + permanent: this._isHookPermanent(callbackURL), + getRaw, + }); + + let msg; + + if (duplicated) { + msg = responses.createDuplicated(hook.id); + } else if (hook != null) { + const { id, payload } = hook; + const { permanent, getRaw } = payload; + msg = responses.createSuccess(hook.id, permanent, getRaw); + } else { + msg = responses.createFailure; + } + + API.respondWithXML(res, msg); + } catch (error) { + API.logger.error(`error creating hook ${error}`); + API.respondWithXML(res, responses.createFailure); + } + } + + // Create a permanent hook. Permanent hooks can't be deleted via API and will try to emit a message until it succeed + async createPermanents() { + for (let i = 0; i < this._permanentURLs.length; i++) { + try { + const { url: callbackURL, getRaw } = this._permanentURLs[i].url; + const { hook, duplicated } = await API.storage.get().addSubscription({ + callbackURL, + permanent: this._isHookPermanent(callbackURL), + getRaw, + }); + + if (duplicated) { + API.logger.warn(`duplicated permanent hook ${hook.id}`); + } else if (hook != null) { + API.logger.info('permanent hook created successfully'); + } else { + API.logger.error('error creating permanent hook'); + } + } catch (error) { + API.logger.error(`error creating permanent hook ${error}`); + } + } + } + + async _destroy(req, res, next) { + const urlObj = url.parse(req.url, true); + const hookID = urlObj.query["hookID"]; + + if (hookID == null) { + API.respondWithXML(res, responses.missingParamHookID); + } else { + let removed, failed; + try { + removed = await API.storage.get().removeSubscription(hookID); + } catch (error) { + API.logger.error('error removing hook', error); + failed = true; + } finally { + if (removed) { + API.respondWithXML(res, responses.destroySuccess); + } else if (failed) { + API.respondWithXML(res, responses.destroyFailure); + } else { + API.respondWithXML(res, responses.destroyNoHook); + } + } + } + } + + _list(req, res, next) { + let hooks; + const urlObj = url.parse(req.url, true); + const meetingID = urlObj.query["meetingID"]; + + if (meetingID != null) { + // all the hooks that receive events from this meeting + hooks = API.storage.get().allGlobalSync(); + hooks = hooks.concat(API.storage.get().findByExternalMeetingID(meetingID)); + hooks = Utils.sortBy(hooks, hook => hook.id); + } else { + // no meetingID, return all hooks + hooks = API.storage.get().getAll(); + } + + let msg = "SUCCESS"; + hooks.forEach((hook) => { + const { + eventID, + externalMeetingID, + callbackURL, + permanent, + getRaw, + } = hook.payload; + msg += ""; + msg += `${hook.id}`; + msg += ``; + if (!API.storage.get().isGlobal(hook)) { msg += ``; } + if (eventID != null) { msg += `${eventID}`; } + msg += `${permanent}`; + msg += `${getRaw}`; + msg += ""; + }); + msg += ""; + + API.respondWithXML(res, msg); + } + + // Validates the checksum in the request `req`. + // If it doesn't match BigBlueButton's shared secret, will send an XML response + // with an error code just like BBB does. + _validateChecksum(req, res, next) { + const urlObj = url.parse(req.url, true); + const checksum = urlObj.query["checksum"]; + + if (checksum === Utils.checksumAPI(req.url, this._secret)) { + next(); + } else { + API.logger.info('checksum check failed, sending a checksumError response', responses.checksumError); + res.setHeader("Content-Type", "text/xml"); + res.send(cleanupXML(responses.checksumError)); + } + } +} diff --git a/src/out/webhooks/api/responses.js b/src/out/webhooks/api/responses.js new file mode 100644 index 0000000..8849c76 --- /dev/null +++ b/src/out/webhooks/api/responses.js @@ -0,0 +1,67 @@ +const failure = (key, msg) => + ` \ + FAILED \ + ${key} \ + ${msg} \ + `; +const checksumError = failure( + "checksumError", + "You did not pass the checksum security check.", +); +const createSuccess = (id, permanent, getRaw) => + ` \ + SUCCESS \ + ${id} \ + ${permanent} \ + ${getRaw} \ + `; + +const createFailure = failure( + "createHookError", + "An error happened while creating your hook. Check the logs." +); + +const createDuplicated = (id) => + ` \ + SUCCESS \ + ${id} \ + duplicateWarning \ + There is already a hook for this callback URL. \ + `; + +const destroySuccess = + ` \ + SUCCESS \ + true \ + `; + +const destroyFailure = failure( + "destroyHookError", + "An error happened while removing your hook. Check the logs." +); + +const destroyNoHook = failure( + "destroyMissingHook", + "The hook informed was not found." +); + +const missingParamCallbackURL = failure( + "missingParamCallbackURL", + "You must specify a callbackURL in the parameters." +); +const missingParamHookID = failure( + "missingParamHookID", + "You must specify a hookID in the parameters." +); + +export default { + checksumError, + createSuccess, + createFailure, + createDuplicated, + destroySuccess, + destroyFailure, + destroyNoHook, + missingParamCallbackURL, + missingParamHookID, +}; diff --git a/callback_emitter.js b/src/out/webhooks/callback-emitter.js similarity index 65% rename from callback_emitter.js rename to src/out/webhooks/callback-emitter.js index aa3a721..3736591 100644 --- a/callback_emitter.js +++ b/src/out/webhooks/callback-emitter.js @@ -1,26 +1,51 @@ -const _ = require('lodash'); -const request = require("request"); -const url = require('url'); -const EventEmitter = require('events').EventEmitter; +import request from 'request'; +import url from 'url'; +import { EventEmitter } from 'node:events'; +import { newLogger } from '../../common/logger.js'; +import Utils from '../../common/utils.js'; -const config = require('config'); -const Logger = require("./logger.js"); -const Utils = require("./utils.js"); +const Logger = newLogger('callback-emitter'); + +// A simple string that identifies the event +const simplifiedEvent = (event) => { + if (event.event != null) { + event = event.event + } + try { + const eventJs = JSON.parse(event); + return `event: { name: ${(eventJs.data != null ? eventJs.data.id : undefined)}, timestamp: ${(eventJs.data.event != null ? eventJs.data.event.ts : undefined)} }`; + } catch (e) { + return `event: ${event}`; + } +}; // Use to perform a callback. Will try several times until the callback is // properly emitted and stop when successful (or after a given number of tries). // Used to emit a single callback. Destroy it and create a new class for a new callback. // Emits "success" on success, "failure" on error and "stopped" when gave up trying // to perform the callback. -module.exports = class CallbackEmitter extends EventEmitter { - - constructor(callbackURL, message, permanent) { +export default class CallbackEmitter extends EventEmitter { + constructor(callbackURL, event, permanent, options = {}) { super(); this.callbackURL = callbackURL; - this.message = message; + this.event = event; + this.message = JSON.stringify(event); this.nextInterval = 0; this.timestamp = 0; this.permanent = permanent; + + this._permanentIntervalReset = options.permanentIntervalReset || 8; + this._serverDomain = options.domain; + this._secret = options.secret; + this._bearerAuth = options.auth2_0; + this._requestTimeout = options.requestTimeout; + this._retryIntervals = options.retryIntervals || [ + 1000, + 2000, + 5000, + 10000, + 30000, + ]; } start() { @@ -38,20 +63,20 @@ module.exports = class CallbackEmitter extends EventEmitter { this.emit("failure", error); // get the next interval we have to wait and schedule a new try - const interval = config.get("hooks.retryIntervals")[this.nextInterval]; + const interval = this.retryIntervals[this.nextInterval]; if (interval != null) { - Logger.warn(`[Emitter] trying the callback again in ${interval/1000.0} secs`); + Logger.warn(`trying the callback again in ${interval/1000.0} secs`); this.nextInterval++; this._scheduleNext(interval); // no intervals anymore, time to give up } else { - this.nextInterval = config.get("hooks.permanentIntervalReset"); // Reset interval to permanent hooks + this.nextInterval = this._permanentIntervalReset; if(this.permanent){ this._scheduleNext(this.nextInterval); } else { - return this.emit("stopped"); + this.emit("stopped"); } } } @@ -62,10 +87,10 @@ module.exports = class CallbackEmitter extends EventEmitter { _emitMessage(callback) { let data, requestOptions; - const serverDomain = config.get("bbb.serverDomain"); - const sharedSecret = config.get("bbb.sharedSecret"); - const bearerAuth = config.get("bbb.auth2_0"); - const timeout = config.get('hooks.requestTimeout'); + const serverDomain = this._serverDomain; + const sharedSecret = this._secret; + const bearerAuth = this._bearerAuth; + const timeout = this._requestTimeout; // data to be sent // note: keep keys in alphabetical order @@ -97,7 +122,7 @@ module.exports = class CallbackEmitter extends EventEmitter { // get the final callback URL, including the checksum const urlObj = url.parse(this.callbackURL, true); let callbackURL = this.callbackURL; - callbackURL += _.isEmpty(urlObj.search) ? "?" : "&"; + callbackURL += Utils.isEmpty(urlObj.search) ? "?" : "&"; callbackURL += `checksum=${checksum}`; requestOptions = { @@ -116,27 +141,14 @@ module.exports = class CallbackEmitter extends EventEmitter { return !((statusCode >= 200 && statusCode < 300) || statusCode == 401) }; - request(requestOptions, function(error, response, body) { + request(requestOptions, (error, response) => { if ((error != null) || responseFailed(response)) { - Logger.warn(`[Emitter] error in the callback call to: [${requestOptions.uri}] for ${simplifiedEvent(data)} error: ${error} status: ${response != null ? response.statusCode : undefined}`); + Logger.warn(`error in the callback call to: [${requestOptions.uri}] for ${simplifiedEvent(data)} error: ${error} status: ${response != null ? response.statusCode : undefined}`); callback(error, false); } else { - Logger.info(`[Emitter] successful callback call to: [${requestOptions.uri}] for ${simplifiedEvent(data)}`); + Logger.info(`successful callback call to: [${requestOptions.uri}] for ${simplifiedEvent(data)}`); callback(null, true); } }); } -}; - -// A simple string that identifies the event -var simplifiedEvent = function(event) { - if (event.event != null) { - event = event.event - } - try { - const eventJs = JSON.parse(event); - return `event: { name: ${(eventJs.data != null ? eventJs.data.id : undefined)}, timestamp: ${(eventJs.data.event != null ? eventJs.data.event.ts : undefined)} }`; - } catch (e) { - return `event: ${event}`; - } -}; +} diff --git a/src/out/webhooks/index.js b/src/out/webhooks/index.js new file mode 100644 index 0000000..7e3444c --- /dev/null +++ b/src/out/webhooks/index.js @@ -0,0 +1,63 @@ +import WebHooks from './web-hooks.js'; +import API from './api/api.js'; +import HookCompartment from '../../db/redis/hooks.js'; + +/* + * [MODULE_TYPES.OUTPUT]: { + * load: 'function', + * unload: 'function', + * setContext: 'function', + * onEvent: 'function', + * }, + */ + +export default class OutWebHooks { + static _defaultCollector () { + throw new Error('Collector not set'); + } + + constructor (context, config = {}) { + this.type = "out"; + this.config = config; + this.setContext(context); + this.api = new API({ + permanentURLs: this.config.permanentURLs, + secret: this.config.server.secret, + }); + API.setStorage(HookCompartment); + this.webHooks = new WebHooks(this.context, this.config); + this.loaded = false; + } + + async load () { + await this.webHooks.start(), + await this.api.start(this.config.api.port, this.config.api.bind); + await this.api.createPermanents(); + + this.loaded = true; + } + + async unload () { + if (this.webHooks) { + this.webHooks = null; + } + + this.setCollector(OutWebHooks._defaultCollector); + this.loaded = false; + } + + setContext (context) { + this.context = context; + this.logger = context.getLogger(); + + return context; + } + + async onEvent (event, raw) { + if (!this.loaded || !this.webHooks) { + throw new Error("OutWebHooks not loaded"); + } + + return this.webHooks.onEvent(event, raw); + } +} diff --git a/src/out/webhooks/web-hooks.js b/src/out/webhooks/web-hooks.js new file mode 100644 index 0000000..e073e80 --- /dev/null +++ b/src/out/webhooks/web-hooks.js @@ -0,0 +1,128 @@ +import config from 'config'; +import CallbackEmitter from './callback-emitter.js'; +import HookCompartment from '../../db/redis/hooks.js'; + +// Web hooks will listen for events on redis coming from BigBlueButton and +// perform HTTP calls with them to all registered hooks. +export default class WebHooks { + constructor(context, config) { + this.context = context; + this.logger = context.getLogger(); + this.config = config; + } + + start() { + return Promise.resolve(); + } + + _processRaw(devent) { + let meetingID; + let hooks = HookCompartment.get().allGlobalSync(); + + // Add hooks for the specific meeting that expect raw data + // Get meetingId for a raw message that was previously mapped by another webhook application or if it's straight from redis + meetingID = this._extractIntMeetingID(event); + + if (meetingID != null) { + const eMeetingID = this._extractExternalMeetingID(event); + hooks = hooks.concat(HookCompartment.get().findByExternalMeeting(eMeetingID)); + // Notify the hooks that expect raw data + return Promise.all(hooks.map((hook) => { + if (hook.getRaw) { + this.logger.info('enqueueing a raw event in the hook', { callbackURL: hook.payload.callbackURL }); + return this.dispatch(event, hook).catch((error) => { + this.logger.error('failed to enqueue', { calbackURL: hook.callbackURL, error: error.stack }); + }); + } + + return Promise.resolve(); + })); + } + + return Promise.resolve(); + } + + _extractIntMeetingID(message) { + // Webhooks events + return message?.data?.attributes?.meeting["internal-meeting-id"] + // Raw messages from BBB + || message?.envelope?.routing?.meetingId + || message?.header?.body?.meetingId + || message?.core?.body?.props?.meetingProp?.intId + || message?.core?.body?.meetingId; + } + + _extractExternalMeetingID(message) { + return message?.data?.attributes?.meeting["external-meeting-id"]; + } + + dispatch(event, hook) { + return new Promise((resolve, reject) => { + if (event == null) return; + + if (hook.payload.eventID != null + && (event == null + || event.data == null + || event.data.id == null + || (!hook.payload.eventID.some((ev) => ev == event.data.id.toLowerCase()))) + ) { + Logger.info(`[Hook] ${hook.payload.callbackURL} skipping event from queue because not in event list for hook: ${JSON.stringify(event)}`); + return ; + } + + const emitter = new CallbackEmitter( + hook.payload.callbackURL, + event, + hook.payload.permanent, { + permanentIntervalReset: this.config.permanentIntervalReset, + domain: this.config.domain, + secret: this.config.secret, + auth2_0: this.config.auth2_0, + requestTimeout: this.config.requestTimeout, + retryIntervals: this.config.retryIntervals, + } + ); + + emitter.start(); + emitter.on("success", resolve); + emitter.once("stopped", () => { + this.logger.warn(`too many failed attempts to perform a callback call, removing the hook for: ${hook.payload.callbackURL}`); + // TODO just disable + return hook.destroy().then(resolve).catch(resolve); + }); + }); + } + + onEvent(event, raw) { + let hooks = HookCompartment.get().allGlobalSync(); + // filter the hooks that need to receive this event + // add hooks that are registered for this specific meeting + const meetingID = this._extractIntMeetingID(event); + if (meetingID != null) { + const eMeetingID = this._extractExternalMeetingID(event); + hooks = hooks.concat(HookCompartment.get().findByExternalMeetingID(eMeetingID)); + } + + if (hooks == null || hooks.length === 0) { + this.logger.info('no hooks registered for this event'); + return Promise.resolve(); + } + + return Promise.all(hooks.map((hook) => { + if (hook == null) return Promise.resolve(); + if (!hook.getRaw) { + this.logger.info('dispatching raw event in the hook', { callbackURL: hook.payload.callbackURL }); + return this.dispatch(event, hook).catch((error) => { + this.logger.error('failed to enqueue', { calbackURL: hook.callbackURL, error: error.stack }); + }); + } + + return Promise.resolve(); + })).then(() => {; + const sendRaw = hooks.some(hook => hook && hook.getRaw); + if (sendRaw && config.get("hooks.getRaw")) return this._processRaw(raw); + + return Promise.resolve(); + }); + } +} diff --git a/src/process/event-processor.js b/src/process/event-processor.js new file mode 100644 index 0000000..baa2ee4 --- /dev/null +++ b/src/process/event-processor.js @@ -0,0 +1,121 @@ +import IDMapping from '../db/redis/id-mapping.js'; +import { newLogger } from '../common/logger.js'; +import WebhooksEvent from '../process/event.js'; +import UserMapping from '../db/redis/user-mapping.js'; +import Utils from '../common/utils.js'; + +const Logger = newLogger('event-processor'); + +export default class EventProcessor { + static _defaultCollector () { + throw new Error('Collector not set'); + } + + constructor( + inputs, + outputs, + ) { + this.inputs = inputs; + this.outputs = outputs; + } + + start() { + this.inputs.forEach((input) => { + input.setCollector(this.processInputEvent.bind(this)); + }); + + return Promise.resolve(); + } + + stop() { + this.inputs.forEach((input) => { + input.setCollector(EventProcessor._defaultCollector); + }); + } + + _parseEvent(event) { + let parsedEvent = event; + + if (typeof event === 'string') parsedEvent = JSON.parse(event); + + return parsedEvent; + } + + processInputEvent(event) { + + try { + const rawEvent = this._parseEvent(event); + const eventInstance = new WebhooksEvent(rawEvent); + const outputEvent = eventInstance.outputEvent; + + if (!Utils.isEmpty(outputEvent)) { + Logger.debug('raw event succesfully parsed', { rawEvent }); + const intId = outputEvent.data.attributes.meeting["internal-meeting-id"]; + IDMapping.get().reportActivity(intId); + + // First treat meeting events to add/remove ID mappings + switch (outputEvent.data.id) { + case "meeting-created": + IDMapping.get().addOrUpdateMapping(intId, + outputEvent.data.attributes.meeting["external-meeting-id"] + ).catch((error) => { + Logger.error(`error adding mapping: ${error}`, { + error: error.stack, + event, + }); + }).finally(() => { + // has to be here, after the meeting was created, otherwise create calls won't generate + // callback calls for meeting hooks + this._notifyOutputModules(outputEvent, rawEvent); + }); + break; + case "user-joined": + UserMapping.get().addOrUpdateMapping( + outputEvent.data.attributes.user["internal-user-id"], + outputEvent.data.attributes.user["external-user-id"], + intId, + outputEvent.data.attributes.user + ).catch((error) => { + Logger.error(`error adding mapping: ${error}`, { + error: error.stack, + event, + }) + }).finally(() => { + this._notifyOutputModules(outputEvent, rawEvent); + }); + break; + case "user-left": + UserMapping.get().removeMapping(outputEvent.data.attributes.user["internal-user-id"], () => { + this._notifyOutputModules(outputEvent, rawEvent); + }); + break; + case "meeting-ended": + IDMapping.get().removeMapping(intId, () => { + this._notifyOutputModules(outputEvent, rawEvent); + }); + break; + default: + this._notifyOutputModules(outputEvent, rawEvent); + } + } + } catch (error) { + Logger.error(`error processing event: ${error}`, { + error: error.stack, + event, + }); + } + } + + // Processes an event received from redis. Will get all hook URLs that + // should receive this event and start the process to perform the callback. + _notifyOutputModules(message, raw) { + if (this.outputs == null || this.outputs.length === 0) { + Logger.warn('no output modules registered'); + return; + } + + this.outputs.forEach((output) => { + output.onEvent(message, raw); + }); + } +} diff --git a/src/process/event.js b/src/process/event.js new file mode 100644 index 0000000..e7d4906 --- /dev/null +++ b/src/process/event.js @@ -0,0 +1,436 @@ +import { newLogger } from '../common/logger.js'; +import IDMapping from '../db/redis/id-mapping.js'; +import UserMapping from '../db/redis/user-mapping.js'; + +const logger = newLogger('webhook-event'); + +export default class WebhooksEvent { + static OUTPUT_EVENTS = [ + "meeting-created", + "meeting-ended", + "meeting-recording-started", + "meeting-recording-stopped", + "meeting-recording-unhandled", + "meeting-screenshare-started", + "meeting-screenshare-stopped", + "meeting-presentation-changed", + "user-joined", + "user-left", + "user-audio-voice-enabled", + "user-audio-voice-disabled", + "user-cam-broadcast-start", + "user-cam-broadcast-end", + "user-presenter-assigned", + "user-presenter-unassigned", + "user-emoji-changed", + "chat-group-message-sent", + "rap-published", + "rap-unpublished", + "rap-deleted", + "pad-content", + "rap-archive-started", + "rap-archive-ended", + "rap-sanity-started", + "rap-sanity-ended", + "rap-post-archive-started", + "rap-post-archive-ended", + "rap-process-started", + "rap-process-ended", + "rap-post-process-started", + "rap-post-process-ended", + "rap-publish-started", + "rap-publish-ended", + "rap-published", + "rap-unpublished", + "rap-deleted", + "rap-post-publish-started", + "rap-post-publish-ended", + ]; + + static RAW = { + MEETING_EVENTS: [ + "MeetingCreatedEvtMsg", + "MeetingDestroyedEvtMsg", + "ScreenshareRtmpBroadcastStartedEvtMsg", + "ScreenshareRtmpBroadcastStoppedEvtMsg", + "SetCurrentPresentationEvtMsg", + "RecordingStatusChangedEvtMsg", + ], + USER_EVENTS: [ + "UserJoinedMeetingEvtMsg", + "UserLeftMeetingEvtMsg", + "UserJoinedVoiceConfToClientEvtMsg", + "UserLeftVoiceConfToClientEvtMsg", + "PresenterAssignedEvtMsg", + "PresenterUnassignedEvtMsg", + "UserBroadcastCamStartedEvtMsg", + "UserBroadcastCamStoppedEvtMsg", + "UserEmojiChangedEvtMsg", + ], + CHAT_EVENTS: [ + "GroupChatMessageBroadcastEvtMsg", + ], + RAP_EVENTS: [ + "PublishedRecordingSysMsg", + "UnpublishedRecordingSysMsg", + "DeletedRecordingSysMsg", + ], + COMP_RAP_EVENTS: [ + "archive_started", + "archive_ended", + "sanity_started", + "sanity_ended", + "post_archive_started", + "post_archive_ended", + "process_started", + "process_ended", + "post_process_started", + "post_process_ended", + "publish_started", + "publish_ended", + "post_publish_started", + "post_publish_ended", + "published", + "unpublished", + "deleted", + ], + PAD_EVENTS: [ + "PadContentEvtMsg" + ], + } + + constructor(inputEvent) { + this.inputEvent = inputEvent; + this.outputEvent = this.map(); + } + + // Map internal message based on it's type + map() { + if (this.inputEvent) { + if (this.mappedEvent(this.inputEvent, WebhooksEvent.RAW.MEETING_EVENTS)) { + this.meetingTemplate(this.inputEvent); + } else if (this.mappedEvent(this.inputEvent, WebhooksEvent.RAW.USER_EVENTS)) { + this.userTemplate(this.inputEvent); + } else if (this.mappedEvent(this.inputEvent, WebhooksEvent.RAW.CHAT_EVENTS)) { + this.chatTemplate(this.inputEvent); + } else if (this.mappedEvent(this.inputEvent, WebhooksEvent.RAW.RAP_EVENTS)) { + this.rapTemplate(this.inputEvent); + } else if (this.mappedEvent(this.inputEvent, WebhooksEvent.RAW.COMP_RAP_EVENTS)) { + this.compRapTemplate(this.inputEvent); + } else if (this.mappedEvent(this.inputEvent, WebhooksEvent.RAW.PAD_EVENTS)) { + this.padTemplate(this.inputEvent); + } else if (this.mappedEvent(this.inputEvent, WebhooksEvent.OUTPUT_EVENTS)) { + // Check if input is already a mapped event and return it + this.outputEvent = this.inputEvent; + } else { + this.outputEvent = null; + } + + if (this.outputEvent) { + logger.info('output event mapped', this.outputEvent); + } + + return this.outputEvent; + } + + logger.warn('invalid input event', this.inputEvent); + + return null; + } + + mappedEvent(messageObj, events) { + return events.some(event => { + if (messageObj?.header?.name === event) { + return true; + } + + if (messageObj?.envelope?.name === event) { + return true; + } + + if (messageObj?.data?.id === event) { + return true; + } + + return false; + }); + } + + // Map internal to external message for meeting information + meetingTemplate(messageObj) { + const props = messageObj.core.body.props; + const meetingId = messageObj.core.body.meetingId || messageObj.core.header.meetingId; + this.outputEvent = { + data: { + "type": "event", + "id": this.mapInternalMessage(messageObj), + "attributes":{ + "meeting":{ + "internal-meeting-id": meetingId, + "external-meeting-id": IDMapping.get().getExternalMeetingID(meetingId) + } + }, + "event":{ + "ts": Date.now() + } + } + } + + if (messageObj.envelope.name === "MeetingCreatedEvtMsg") { + this.outputEvent.data.attributes = { + "meeting":{ + "internal-meeting-id": props.meetingProp.intId, + "external-meeting-id": props.meetingProp.extId, + "name": props.meetingProp.name, + "is-breakout": props.meetingProp.isBreakout, + "duration": props.durationProps.duration, + "create-time": props.durationProps.createdTime, + "create-date": props.durationProps.createdDate, + "moderator-pass": props.password.moderatorPass, + "viewer-pass": props.password.viewerPass, + "record": props.recordProp.record, + "voice-conf": props.voiceProp.voiceConf, + "dial-number": props.voiceProp.dialNumber, + "max-users": props.usersProp.maxUsers, + "metadata": props.metadataProp.metadata + } + }; + } + if (messageObj.envelope.name === "SetCurrentPresentationEvtMsg") { + this.outputEvent.data.attributes = { + "meeting":{ + "internal-meeting-id": meetingId, + "external-meeting-id": IDMapping.get().getExternalMeetingID(meetingId), + "presentation-id": messageObj.core.body.presentationId + } + }; + } + } + + // Map internal to external message for user information + userTemplate(messageObj) { + const msgBody = messageObj.core.body; + const msgHeader = messageObj.core.header; + const extId = UserMapping.get().getExternalUserID(msgHeader.userId) || msgBody.extId || ""; + this.outputEvent = { + data: { + "type": "event", + "id": this.mapInternalMessage(messageObj), + "attributes":{ + "meeting":{ + "internal-meeting-id": messageObj.envelope.routing.meetingId, + "external-meeting-id": IDMapping.get().getExternalMeetingID(messageObj.envelope.routing.meetingId) + }, + "user":{ + "internal-user-id": msgHeader.userId, + "external-user-id": extId, + "name": msgBody.name, + "role": msgBody.role, + "presenter": msgBody.presenter, + "stream": msgBody.stream + } + }, + "event":{ + "ts": Date.now() + } + } + }; + if (this.outputEvent.data["id"] === "user-audio-voice-enabled") { + this.outputEvent.data["attributes"]["user"]["listening-only"] = msgBody.listenOnly; + this.outputEvent.data["attributes"]["user"]["sharing-mic"] = ! msgBody.listenOnly; + } else if (this.outputEvent.data["id"] === "user-audio-voice-disabled") { + this.outputEvent.data["attributes"]["user"]["listening-only"] = false; + this.outputEvent.data["attributes"]["user"]["sharing-mic"] = false; + } + } + + // Map internal to external message for chat information + chatTemplate(messageObj) { + const { body } = messageObj.core; + // Ignore private chats + if (body.chatId !== 'MAIN-PUBLIC-GROUP-CHAT') return; + + this.outputEvent = { + data: { + "type": "event", + "id": this.mapInternalMessage(messageObj), + "attributes":{ + "meeting":{ + "internal-meeting-id": messageObj.envelope.routing.meetingId, + "external-meeting-id": IDMapping.get().getExternalMeetingID(messageObj.envelope.routing.meetingId) + }, + "chat-message":{ + "message": body.msg.message, + "sender":{ + "internal-user-id": body.msg.sender.id, + "external-user-id": body.msg.sender.name, + "timezone-offset": body.msg.fromTimezoneOffset, + "time": body.msg.timestamp + } + }, + "chat-id": body.chatId + }, + "event":{ + "ts": Date.now() + } + } + }; + } + + rapTemplate(messageObj) { + const data = messageObj.core.body; + this.outputEvent = { + data: { + "type": "event", + "id": this.mapInternalMessage(messageObj), + "attributes": { + "meeting": { + "internal-meeting-id": data.recordId, + "external-meeting-id": IDMapping.get().getExternalMeetingID(data.recordId) + } + }, + "event": { + "ts": Date.now() + } + } + }; + } + + compRapTemplate(messageObj) { + const data = messageObj.payload; + this.outputEvent = { + data: { + "type": "event", + "id": this.mapInternalMessage(messageObj), + "attributes": { + "meeting": { + "internal-meeting-id": data.meeting_id, + "external-meeting-id": data.external_meeting_id || IDMapping.get().getExternalMeetingID(data.meeting_id) + } + }, + "event": { + "ts": messageObj.header.current_time + } + }, + }; + + if (this.outputEvent.data.id === "published" || + this.outputEvent.data.id === "unpublished" || + this.outputEvent.data.id === "deleted") { + this.outputEvent.data.attributes["record-id"] = data.meeting_id; + this.outputEvent.data.attributes["format"] = data.format; + } else { + this.outputEvent.data.attributes["record-id"] = data.record_id; + this.outputEvent.data.attributes["success"] = data.success; + this.outputEvent.data.attributes["step-time"] = data.step_time; + } + + if (data.workflow) { + this.outputEvent.data.attributes.workflow = data.workflow; + } + + if (this.outputEvent.data.id === "rap-publish-ended") { + this.outputEvent.data.attributes.recording = { + "name": data.metadata.meetingName, + "is-breakout": data.metadata.isBreakout, + "start-time": data.startTime, + "end-time": data.endTime, + "size": data.playback.size, + "raw-size": data.rawSize, + "metadata": data.metadata, + "playback": data.playback, + "download": data.download + } + } + } + + handleRecordingStatusChanged(message) { + const event = "meeting-recording"; + const { core } = message; + if (core && core.body) { + const { recording } = core.body; + if (typeof recording === 'boolean') { + if (recording) return `${event}-started`; + return `${event}-stopped`; + } + } + return `${event}-unhandled`; + } + + padTemplate(messageObj) { + const { + body, + header, + } = messageObj.core; + this.outputEvent = { + data: { + "type": "event", + "id": this.mapInternalMessage(messageObj), + "attributes":{ + "meeting":{ + "internal-meeting-id": header.meetingId, + "external-meeting-id": IDMapping.get().getExternalMeetingID(header.meetingId) + }, + "pad":{ + "id": body.padId, + "external-pad-id": body.externalId, + "rev": body.rev, + "start": body.start, + "end": body.end, + "text": body.text + } + }, + "event":{ + "ts": Date.now() + } + } + }; + } + + mapInternalMessage(message) { + const name = message?.envelope?.name || message?.header?.name; + + const mappedMsg = (() => { switch (name) { + case "MeetingCreatedEvtMsg": return "meeting-created"; + case "MeetingDestroyedEvtMsg": return "meeting-ended"; + case "RecordingStatusChangedEvtMsg": return this.handleRecordingStatusChanged(message); + case "ScreenshareRtmpBroadcastStartedEvtMsg": return "meeting-screenshare-started"; + case "ScreenshareRtmpBroadcastStoppedEvtMsg": return "meeting-screenshare-stopped"; + case "SetCurrentPresentationEvtMsg": return "meeting-presentation-changed"; + case "UserJoinedMeetingEvtMsg": return "user-joined"; + case "UserLeftMeetingEvtMsg": return "user-left"; + case "UserJoinedVoiceConfToClientEvtMsg": return "user-audio-voice-enabled"; + case "UserLeftVoiceConfToClientEvtMsg": return "user-audio-voice-disabled"; + case "UserBroadcastCamStartedEvtMsg": return "user-cam-broadcast-start"; + case "UserBroadcastCamStoppedEvtMsg": return "user-cam-broadcast-end"; + case "PresenterAssignedEvtMsg": return "user-presenter-assigned"; + case "PresenterUnassignedEvtMsg": return "user-presenter-unassigned"; + case "UserEmojiChangedEvtMsg": return "user-emoji-changed"; + case "GroupChatMessageBroadcastEvtMsg": return "chat-group-message-sent"; + case "PublishedRecordingSysMsg": return "rap-published"; + case "UnpublishedRecordingSysMsg": return "rap-unpublished"; + case "DeletedRecordingSysMsg": return "rap-deleted"; + case "PadContentEvtMsg": return "pad-content"; + // RAP + case "archive_started": return "rap-archive-started"; + case "archive_ended": return "rap-archive-ended"; + case "sanity_started": return "rap-sanity-started"; + case "sanity_ended": return "rap-sanity-ended"; + case "post_archive_started": return "rap-post-archive-started"; + case "post_archive_ended": return "rap-post-archive-ended"; + case "process_started": return "rap-process-started"; + case "process_ended": return "rap-process-ended"; + case "post_process_started": return "rap-post-process-started"; + case "post_process_ended": return "rap-post-process-ended"; + case "publish_started": return "rap-publish-started"; + case "publish_ended": return "rap-publish-ended"; + case "published": return "rap-published"; + case "unpublished": return "rap-unpublished"; + case "deleted": return "rap-deleted"; + case "post_publish_started": return "rap-post-publish-started"; + case "post_publish_ended": return "rap-post-publish-ended"; + } })(); + + return mappedMsg; + } +} diff --git a/userMapping.js b/userMapping.js deleted file mode 100644 index b003d38..0000000 --- a/userMapping.js +++ /dev/null @@ -1,200 +0,0 @@ -const _ = require("lodash"); -const async = require("async"); -const redis = require("redis"); - -const config = require("config"); -const Logger = require("./logger.js"); - -// The database of mappings. Uses the internal ID as key because it is unique -// unlike the external ID. -// Used always from memory, but saved to redis for persistence. -// -// Format: -// { -// internalMeetingID: { -// id: @id -// externalMeetingID: @externalMeetingID -// internalMeetingID: @internalMeetingID -// lastActivity: @lastActivity -// } -// } -// Format on redis: -// * a SET "...:mappings" with all ids (not meeting ids, the object id) -// * a HASH "...:mapping:" for each mapping with all its attributes -const db = {}; -let nextID = 1; - -// A simple model to store mappings for user extIDs. -module.exports = class UserMapping { - - constructor() { - this.id = null; - this.externalUserID = null; - this.internalUserID = null; - this.meetingId = null; - this.user = null; - this.redisClient = Application.redisClient(); - } - - save(callback) { - db[this.internalUserID] = this; - - this.redisClient.hmset(config.get("redis.keys.userMapPrefix") + ":" + this.id, this.toRedis(), (error, reply) => { - if (error != null) { Logger.error(`[UserMapping] error saving mapping to redis: ${error} ${reply}`); } - this.redisClient.sadd(config.get("redis.keys.userMaps"), this.id, (error, reply) => { - if (error != null) { Logger.error(`[UserMapping] error saving mapping ID to the list of mappings: ${error} ${reply}`); } - - (typeof callback === 'function' ? callback(error, db[this.internalUserID]) : undefined); - }); - }); - } - - destroy(callback) { - this.redisClient.srem(config.get("redis.keys.userMaps"), this.id, (error, reply) => { - if (error != null) { Logger.error(`[UserMapping] error removing mapping ID from the list of mappings: ${error} ${reply}`); } - this.redisClient.del(config.get("redis.keys.userMapPrefix") + ":" + this.id, error => { - if (error != null) { Logger.error(`[UserMapping] error removing mapping from redis: ${error}`); } - - if (db[this.internalUserID]) { - delete db[this.internalUserID]; - (typeof callback === 'function' ? callback(error, true) : undefined); - } else { - (typeof callback === 'function' ? callback(error, false) : undefined); - } - }); - }); - } - - toRedis() { - const r = { - "id": this.id, - "internalUserID": this.internalUserID, - "externalUserID": this.externalUserID, - "meetingId": this.meetingId, - "user": this.user - }; - return r; - } - - fromRedis(redisData) { - this.id = parseInt(redisData.id); - this.externalUserID = redisData.externalUserID; - this.internalUserID = redisData.internalUserID; - this.meetingId = redisData.meetingId; - this.user = redisData.user; - } - - print() { - return JSON.stringify(this.toRedis()); - } - - static addOrUpdateMapping(internalUserID, externalUserID, meetingId, user, callback) { - let mapping = new UserMapping(); - mapping.id = nextID++; - mapping.internalUserID = internalUserID; - mapping.externalUserID = externalUserID; - mapping.meetingId = meetingId; - mapping.user = user; - mapping.save(function(error, result) { - Logger.info(`[UserMapping] added user mapping to the list ${internalUserID}: ${mapping.print()}`); - (typeof callback === 'function' ? callback(error, result) : undefined); - }); - } - - static removeMapping(internalUserID, callback) { - return (() => { - let result = []; - for (let internal in db) { - var mapping = db[internal]; - if (mapping.internalUserID === internalUserID) { - result.push(mapping.destroy( (error, result) => { - Logger.info(`[UserMapping] removing user mapping from the list ${internalUserID}: ${mapping.print()}`); - return (typeof callback === 'function' ? callback(error, result) : undefined); - })); - } else { - result.push(undefined); - } - } - return result; - })(); - } - - static removeMappingMeetingId(meetingId, callback) { - return (() => { - let result = []; - for (let internal in db) { - var mapping = db[internal]; - if (mapping.meetingId === meetingId) { - result.push(mapping.destroy( (error, result) => { - Logger.info(`[UserMapping] removing user mapping from the list ${mapping.internalUserID}: ${mapping.print()}`); - })); - } else { - result.push(undefined); - } - } - return (typeof callback === 'function' ? callback() : undefined); - })(); - } - - static getUser(internalUserID) { - if (db[internalUserID]){ - return db[internalUserID].user; - } - } - - static getExternalUserID(internalUserID) { - if (db[internalUserID]){ - return db[internalUserID].externalUserID; - } - } - - static allSync() { - let arr = Object.keys(db).reduce(function(arr, id) { - arr.push(db[id]); - return arr; - } - , []); - return arr; - } - - // Initializes global methods for this model. - static initialize(callback) { - UserMapping.resync(callback); - } - - // Gets all mappings from redis to populate the local database. - // Calls `callback()` when done. - static resync(callback) { - let client = Application.redisClient(); - let tasks = []; - - return client.smembers(config.get("redis.keys.userMaps"), (error, mappings) => { - if (error != null) { Logger.error(`[UserMapping] error getting list of mappings from redis: ${error}`); } - - mappings.forEach(id => { - tasks.push(done => { - client.hgetall(config.get("redis.keys.userMapPrefix") + ":" + id, function(error, mappingData) { - if (error != null) { Logger.error(`[UserMapping] error getting information for a mapping from redis: ${error}`); } - - if (mappingData != null) { - let mapping = new UserMapping(); - mapping.fromRedis(mappingData); - mapping.save(function(error, hook) { - if (mapping.id >= nextID) { nextID = mapping.id + 1; } - done(null, mapping); - }); - } else { - done(null, null); - } - }); - }); - }); - - return async.series(tasks, function(errors, result) { - mappings = _.map(UserMapping.allSync(), m => m.print()); - Logger.info(`[UserMapping] finished resync, mappings registered: ${mappings}`); - return (typeof callback === 'function' ? callback() : undefined); - }); - }); - } -}; diff --git a/web_hooks.js b/web_hooks.js deleted file mode 100644 index ca55409..0000000 --- a/web_hooks.js +++ /dev/null @@ -1,149 +0,0 @@ -const _ = require("lodash"); -const async = require("async"); -const redis = require("redis"); -const request = require("request"); -const config = require("config"); -const Hook = require("./hook.js"); -const IDMapping = require("./id_mapping.js"); -const Logger = require("./logger.js"); -const MessageMapping = require("./messageMapping.js"); -const UserMapping = require("./userMapping.js"); - -// Web hooks will listen for events on redis coming from BigBlueButton and -// perform HTTP calls with them to all registered hooks. -module.exports = class WebHooks { - - constructor() { - this.subscriberEvents = Application.redisPubSubClient(); - } - - start(callback) { - this._subscribeToEvents(); - typeof callback === 'function' ? callback(null,"w") : undefined; - } - - // Subscribe to the events on pubsub that might need to be sent in callback calls. - _subscribeToEvents() { - this.subscriberEvents.on("psubscribe", (channel, count) => Logger.info(`[WebHooks] subscribed to: ${channel}`)); - - this.subscriberEvents.on("pmessage", (pattern, channel, message) => { - - let raw; - const processMessage = () => { - Logger.info(`[WebHooks] processing message on [${channel}]: ${JSON.stringify(message)}`); - this._processEvent(message, raw); - }; - - try { - raw = JSON.parse(message); - let messageMapped = new MessageMapping(); - messageMapped.mapMessage(JSON.parse(message)); - message = messageMapped.mappedObject; - if (!_.isEmpty(message)) { - const intId = message.data.attributes.meeting["internal-meeting-id"]; - IDMapping.reportActivity(intId); - - // First treat meeting events to add/remove ID mappings - switch (message.data.id) { - case "meeting-created": - Logger.info(`[WebHooks] got create message on meetings channel [${channel}]: ${message}`); - IDMapping.addOrUpdateMapping(intId, message.data.attributes.meeting["external-meeting-id"], (error, result) => { - // has to be here, after the meeting was created, otherwise create calls won't generate - // callback calls for meeting hooks - processMessage(); - }); - break; - case "user-joined": - UserMapping.addOrUpdateMapping(message.data.attributes.user["internal-user-id"],message.data.attributes.user["external-user-id"], intId, message.data.attributes.user, () => { - processMessage(); - }); - break; - case "user-left": - UserMapping.removeMapping(message.data.attributes.user["internal-user-id"], () => { processMessage(); }); - break; - case "meeting-ended": - UserMapping.removeMappingMeetingId(intId, () => { processMessage(); }); - break; - default: - processMessage(); - } - } - } catch (e) { - Logger.error(`[WebHooks] error processing the message ${JSON.stringify(raw)}: ${e.message}`); - } - }); - - config.get("hooks.channels").forEach((channel) => { - this.subscriberEvents.psubscribe(channel); - }); - - } - - // Send raw data to hooks that are not expecting mapped messages - _processRaw(message) { - let idFromMessage; - let hooks = Hook.allGlobalSync(); - - // Add hooks for the specific meeting that expect raw data - // Get meetingId for a raw message that was previously mapped by another webhook application or if it's straight from redis - idFromMessage = this._findMeetingID(message); - if (idFromMessage != null) { - const eMeetingID = IDMapping.getExternalMeetingID(idFromMessage); - hooks = hooks.concat(Hook.findByExternalMeetingIDSync(eMeetingID)); - // Notify the hooks that expect raw data - async.forEach(hooks, (hook) => { - if (hook.getRaw) { - Logger.info(`[WebHooks] enqueueing a raw message in the hook: ${hook.callbackURL}`); - hook.enqueue(message); - } - }); - } // Put foreach inside the if to avoid pingpong events - } - - _findMeetingID(message) { - if (message.data) { - return message.data.attributes.meeting["internal-meeting-id"]; - } - if (message.payload) { - return message.payload.meeting_id; - } - if (message.envelope && message.envelope.routing && message.envelope.routing.meetingId) { - return message.envelope.routing.meetingId; - } - if (message.header && message.header.body && message.header.body.meetingId) { - return message.header.body.meetingId; - } - if (message.core && message.core.body) { - return message.core.body.props ? message.core.body.props.meetingProp.intId : message.core.body.meetingId; - } - return undefined; - } - - // Processes an event received from redis. Will get all hook URLs that - // should receive this event and start the process to perform the callback. - _processEvent(message, raw) { - // Get all global hooks - let hooks = Hook.allGlobalSync(); - - // filter the hooks that need to receive this event - // add hooks that are registered for this specific meeting - const idFromMessage = message.data != null ? message.data.attributes.meeting["internal-meeting-id"] : undefined; - if (idFromMessage != null) { - const eMeetingID = IDMapping.getExternalMeetingID(idFromMessage); - hooks = hooks.concat(Hook.findByExternalMeetingIDSync(eMeetingID)); - } - - // Notify every hook asynchronously, if hook N fails, it won't block hook N+k from receiving its message - async.forEach(hooks, (hook) => { - if (!hook.getRaw) { - Logger.info(`[WebHooks] enqueueing a message in the hook: ${hook.callbackURL}`); - hook.enqueue(message); - } - }); - - const sendRaw = hooks.some(hook => { return hook.getRaw }); - if (sendRaw && config.get("hooks.getRaw")) { - this._processRaw(raw); - } - } -}; diff --git a/web_server.js b/web_server.js deleted file mode 100644 index 5807c77..0000000 --- a/web_server.js +++ /dev/null @@ -1,177 +0,0 @@ -const _ = require("lodash"); -const express = require("express"); -const url = require("url"); - -const config = require("config"); -const Hook = require("./hook.js"); -const Logger = require("./logger.js"); -const Utils = require("./utils.js"); -const responses = require("./responses.js") - -// Web server that listens for API calls and process them. -module.exports = class WebServer { - - constructor() { - this._validateChecksum = this._validateChecksum.bind(this); - this.app = express(); - this._registerRoutes(); - } - - start(port, bind, callback) { - this.server = this.app.listen(port, bind, () => { - if (this.server.address() == null) { - Logger.error(`[WebServer] aborting, could not bind to port ${port} ${process.exit(1)}`); - } - Logger.info(`[WebServer] listening on port ${port} in ${this.app.settings.env.toUpperCase()} mode`); - typeof callback === 'function' ? callback(null,"k") : undefined; - }); - } - - _registerRoutes() { - // Request logger - this.app.all("*", function(req, res, next) { - if (!fromMonit(req)) { - Logger.info(`[WebServer] ${req.method} request to ${req.url} from: ${clientDataSimple(req)}`); - } - next(); - }); - - this.app.get("/bigbluebutton/api/hooks/create", this._validateChecksum, this._create); - this.app.get("/bigbluebutton/api/hooks/destroy", this._validateChecksum, this._destroy); - this.app.get("/bigbluebutton/api/hooks/list", this._validateChecksum, this._list); - this.app.get("/bigbluebutton/api/hooks/ping", function(req, res) { - res.write("bbb-webhooks up!"); - res.end(); - }); - } - - _create(req, res, next) { - const urlObj = url.parse(req.url, true); - const callbackURL = urlObj.query["callbackURL"]; - const meetingID = urlObj.query["meetingID"]; - const eventID = urlObj.query["eventID"]; - let getRaw = urlObj.query["getRaw"]; - if (getRaw){ - getRaw = JSON.parse(getRaw.toLowerCase()); - } else { - getRaw = false; - } - - if (callbackURL == null) { - respondWithXML(res, responses.missingParamCallbackURL); - } else { - Hook.addSubscription(callbackURL, meetingID, eventID, getRaw, function(error, hook) { - let msg; - if (error != null) { // the only error for now is for duplicated callbackURL - msg = responses.createDuplicated(hook.id); - } else if (hook != null) { - msg = responses.createSuccess(hook.id, hook.permanent, hook.getRaw); - } else { - msg = responses.createFailure; - } - respondWithXML(res, msg); - }); - } - } - // Create a permanent hook. Permanent hooks can't be deleted via API and will try to emit a message until it succeed - createPermanents(callback) { - for (let i = 0; i < config.get("hooks.permanentURLs").length; i++) { - Hook.addSubscription(config.get("hooks.permanentURLs")[i].url, null, null, config.get("hooks.permanentURLs")[i].getRaw, function(error, hook) { - if (error != null) { // there probably won't be any errors here - Logger.info(`[WebServer] duplicated permanent hook ${error}`); - } else if (hook != null) { - Logger.info('[WebServer] permanent hook created successfully'); - } else { - Logger.info('[WebServer] error creating permanent hook'); - } - }); - } - typeof callback === 'function' ? callback(null,"p") : undefined; - } - - _destroy(req, res, next) { - const urlObj = url.parse(req.url, true); - const hookID = urlObj.query["hookID"]; - - if (hookID == null) { - respondWithXML(res, responses.missingParamHookID); - } else { - Hook.removeSubscription(hookID, function(error, result) { - let msg; - if (error != null) { - msg = responses.destroyFailure; - } else if (!result) { - msg = responses.destroyNoHook; - } else { - msg = responses.destroySuccess; - } - respondWithXML(res, msg); - }); - } - } - - _list(req, res, next) { - let hooks; - const urlObj = url.parse(req.url, true); - const meetingID = urlObj.query["meetingID"]; - - if (meetingID != null) { - // all the hooks that receive events from this meeting - hooks = Hook.allGlobalSync(); - hooks = hooks.concat(Hook.findByExternalMeetingIDSync(meetingID)); - hooks = _.sortBy(hooks, hook => hook.id); - } else { - // no meetingID, return all hooks - hooks = Hook.allSync(); - } - - let msg = "SUCCESS"; - hooks.forEach(function(hook) { - msg += ""; - msg += `${hook.id}`; - msg += ``; - if (!hook.isGlobal()) { msg += ``; } - if (hook.eventID != null) { msg += `${hook.eventID.join()}`; } - msg += `${hook.permanent}`; - msg += `${hook.getRaw}`; - msg += ""; - }); - msg += ""; - - respondWithXML(res, msg); - } - - // Validates the checksum in the request `req`. - // If it doesn't match BigBlueButton's shared secret, will send an XML response - // with an error code just like BBB does. - _validateChecksum(req, res, next) { - const urlObj = url.parse(req.url, true); - const checksum = urlObj.query["checksum"]; - const sharedSecret = config.get("bbb.sharedSecret"); - - if (checksum === Utils.checksumAPI(req.url, sharedSecret)) { - next(); - } else { - Logger.info('[WebServer] checksum check failed, sending a checksumError response'); - res.setHeader("Content-Type", "text/xml"); - res.send(cleanupXML(responses.checksumError)); - } - } -}; - -var respondWithXML = function(res, msg) { - msg = cleanupXML(msg); - Logger.info(`[WebServer] respond with: ${msg}`); - res.setHeader("Content-Type", "text/xml"); - res.send(msg); -}; - -// Returns a simple string with a description of the client that made -// the request. It includes the IP address and the user agent. -var clientDataSimple = req => `ip ${Utils.ipFromRequest(req)}, using ${req.headers["user-agent"]}`; - -// Cleans up a string with an XML in it removing spaces and new lines from between the tags. -var cleanupXML = string => string.trim().replace(/>\s*/g, '>'); - -// Was this request made by monit? -var fromMonit = req => (req.headers["user-agent"] != null) && req.headers["user-agent"].match(/^monit/); From d1ecc4e6b1285943b470c6c16c9ae1383dd11a00 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Tue, 26 Sep 2023 11:41:21 -0300 Subject: [PATCH 002/154] feat: add xapi output module template --- src/out/xapi/index.js | 45 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/out/xapi/index.js diff --git a/src/out/xapi/index.js b/src/out/xapi/index.js new file mode 100644 index 0000000..681e5f2 --- /dev/null +++ b/src/out/xapi/index.js @@ -0,0 +1,45 @@ +/* + * [MODULE_TYPES.OUTPUT]: { + * load: 'function', + * unload: 'function', + * setContext: 'function', + * onEvent: 'function', + * }, + */ + +export default class OutXAPI { + static _defaultCollector () { + throw new Error('Collector not set'); + } + + constructor (context, config = {}) { + this.type = "out"; + this.config = config; + this.setContext(context); + this.loaded = false; + } + + async load () { + this.loaded = true; + } + + async unload () { + this.setCollector(OutXAPI._defaultCollector); + this.loaded = false; + } + + setContext (context) { + this.context = context; + this.logger = context.getLogger(); + + return context; + } + + async onEvent (event, raw) { + if (!this.loaded) { + throw new Error("OutXAPI not loaded"); + } + + this.logger.debug('OutXAPI.onEvent:', event); + } +} From 174ef2ecbe08cd9d98b27934e5a4f3957bcb1a85 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Tue, 26 Sep 2023 12:04:43 -0300 Subject: [PATCH 003/154] refactor: updated util to capture inbound events --- extra/events.js | 100 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 69 insertions(+), 31 deletions(-) diff --git a/extra/events.js b/extra/events.js index a0997b4..06fb153 100644 --- a/extra/events.js +++ b/extra/events.js @@ -1,46 +1,84 @@ +/* eslint no-console: "off" */ + +import redis from 'redis'; +import fs from 'node:fs'; + // Lists all the events that happen in a meeting. Run with 'node events.js'. // Uses the first meeting started after the application runs and will list all // events, but only the first time they happen. +const eventsPrinted = []; +const REDIS_HOST = process.env.REDIS_HOST || '127.0.0.1'; +const REDIS_PORT = process.env.REDIS_PORT || 6379; +const REDIS_PASSWORD = process.env.REDIS_PASSWORD || ''; +const FILENAME = process.env.FILENAME; +const DEDUPE = (process.env.DEDUPE && process.env.DEDUPE === 'true') || false; +const PRETTY_PRINT = (process.env.PRETTY_PRINT && process.env.PRETTY_PRINT === 'true') || false; +const CHANNELS = process.env.CHANNELS || [ + 'from-akka-apps-redis-channel', + 'from-bbb-web-redis-channel', + 'from-akka-apps-chat-redis-channel', + 'from-akka-apps-pres-redis-channel', + 'bigbluebutton:from-rap', +]; -const redis = require("redis"); -const config = require('config'); -var target_meeting = null; -var events_printed = []; -var subscriber = redis.createClient(config.get(redis.port), config.get(redis.host)); - -subscriber.on("psubscribe", function(channel, count) { - console.log("subscribed to " + channel); -}); +const containsOrAdd = (list, value) => { + for (let i = 0; i <= list.length-1; i++) { + if (list[i] === value) { + return true; + } + } + list.push(value); + return false; +} -subscriber.on("pmessage", function(pattern, channel, message) { +const onMessage = (_message) => { try { - message = JSON.parse(message); - if (message.hasOwnProperty('envelope')) { - - var message_name = message.envelope.name; + const message = JSON.parse(_message); + if (Object.prototype.hasOwnProperty.call(message, 'envelope')) { + const messageName = message.envelope.name; - if (!containsOrAdd(events_printed, message_name)) { - console.log("\n###", message_name, "\n"); + if (!DEDUPE || !containsOrAdd(eventsPrinted, messageName)) { + console.log("\n###", messageName, "\n"); console.log(message); console.log("\n"); + + // Write events as a pretty-printed JSON object to FILENAME, always appending + // Each JSON is isolated and separated by a newline + if (FILENAME) { + const writableMessage = PRETTY_PRINT + ? JSON.stringify(message, null, 2) + : JSON.stringify(message); + fs.appendFile(FILENAME, writableMessage + "\n", (err) => { + if (err) console.error(err); + }); + } } } - } catch(e) { - console.log("error processing the message", message, ":", e); + } catch(error) { + console.error(`error processing ${_message}`, error); } -}); - -for (i = 0; i < config.get(hooks.channels); ++i) { - const channel = config.get(hooks.channels)[i]; - subscriber.psubscribe(channel); } -var containsOrAdd = function(list, value) { - for (i = 0; i <= list.length-1; i++) { - if (list[i] === value) { - return true; - } +const subscribe = (client, channels, messageHandler) => { + if (client == null) { + throw new Error("client not initialized"); } - list.push(value); - return false; -} + + return Promise.all( + channels.map((channel) => { + return client.subscribe(channel, messageHandler) + .then(() => console.info(`subscribed to: ${channel}`)) + .catch((error) => console.error(`error subscribing to: ${channel}: ${error}`)); + }) + ); +}; + +const client = redis.createClient({ + host: REDIS_HOST, + port: REDIS_PORT, + password: REDIS_PASSWORD, +}); + +client.connect() + .then(() => subscribe(client, CHANNELS, onMessage)) + .catch((error) => console.error("error connecting to redis:", error)); From 8f15b40d0481fcf8d69e73456ebc40efbed71499 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Tue, 26 Sep 2023 14:23:57 -0300 Subject: [PATCH 004/154] chore: add in and out module examples --- example/modules/in-file.js | 120 ++++++++++++++++++++++++++++++++++ example/modules/out-file.js | 127 ++++++++++++++++++++++++++++++++++++ 2 files changed, 247 insertions(+) create mode 100644 example/modules/in-file.js create mode 100644 example/modules/out-file.js diff --git a/example/modules/in-file.js b/example/modules/in-file.js new file mode 100644 index 0000000..f5125b8 --- /dev/null +++ b/example/modules/in-file.js @@ -0,0 +1,120 @@ +import { open } from 'node:fs/promises'; + +/* + * [MODULE_TYPES.in]: { + * load: 'function', + * unload: 'function', + * setContext: 'function', + * setCollector: 'function', + * }, + * + * This is an example input module that reads from a file (config.filename). + * The file should contain a JSON object per line, each object representing + * an input event (either BBB/raw format or webhooks format). + * + * To enable it, add the following entry to the `modules` section of your + * config.yml: + * ``` + * ../../example/modules/in-file.js: + * type: in + * config: + * fileName: + * delay: 1000 + * ``` + * + */ + +const timeout = (ms) => { + return new Promise(resolve => setTimeout(resolve, ms)); +}; + +export default class InFile { + static _defaultCollector () { + throw new Error('Collector not set'); + } + + constructor (context, config = {}) { + this.type = "in"; + this.config = config; + this.setContext(context); + this.loaded = false; + + this._fileHandle = null; + } + + _validateConfig () { + if (this.config == null) { + throw new Error("config not set"); + } + + if (this.config.fileName == null) { + throw new Error("config.fileName not set"); + } + + if (this.config.delay == null) { + this.config.delay = 1000; + } + + return true; + } + + async _readFile () { + if (this._fileHandle == null) { + this._fileHandle = await open(this.config.fileName, 'r'); + } + + const lines = this._fileHandle.readLines(); + + for await (const line of lines) { + await timeout(this.config.delay); + this._dispatch(line); + } + } + + _dispatch (event) { + this.logger.debug(`read event from file: ${event}`); + + try { + const parsedEvent = JSON.parse(event); + this._collector(parsedEvent); + } catch (error) { + this.logger.error('error processing message:', error); + } + } + + async load () { + try { + this._validateConfig(); + setTimeout(() => { + this._readFile().catch((error) => { + this.logger.error('error reading file:', error); + }); + }, 0); + this.loaded = true; + } catch (error) { + this.logger.error('error loading InFile:', error); + throw error; + } + } + + async unload () { + if (this._fileHandle != null) { + await this._fileHandle.close(); + this._fileHandle = null; + } + + this.setCollector(InFile._defaultCollector); + } + + setContext (context) { + this.context = context; + this.logger = context.getLogger(); + + return context; + } + + setCollector (collector) { + this.logger.debug('InFile.setCollector:', { collector }); + this._collector = collector; + } +} diff --git a/example/modules/out-file.js b/example/modules/out-file.js new file mode 100644 index 0000000..dbb9605 --- /dev/null +++ b/example/modules/out-file.js @@ -0,0 +1,127 @@ +import { open } from 'node:fs/promises'; + +/* + * [MODULE_TYPES.OUTPUT]: { + * load: 'function', + * unload: 'function', + * setContext: 'function', + * onEvent: 'function', + * }, + * + * This is an example output module that writes events to a file (config.filename). + * The file should contain one JSON object per line, each object representing + * an bbb-webhooks event. + * + * To enable it, add the following entry to the `modules` section of your + * config.yml: + * ``` + * ../../example/modules/out-file.js: + * type: out + * config: + * fileName: + * ``` + * + */ + +const timeout = (ms) => { + return new Promise(resolve => setTimeout(resolve, ms)); +}; + +export default class OutFile { + static _defaultCollector () { + throw new Error('Collector not set'); + } + + constructor (context, config = {}) { + this.type = "out"; + this.config = config; + this.setContext(context); + this.loaded = false; + + this._fileHandle = null; + } + + _validateConfig () { + if (this.config == null) { + throw new Error("config not set"); + } + + if (this.config.fileName == null) { + throw new Error("config.fileName not set"); + } + + if (this.config.delay == null) { + this.config.delay = 1000; + } + + return true; + } + + async _writeEvent() { + if (this._fileHandle == null) { + this._fileHandle = await open(this.config.fileName, 'r'); + } + + const lines = this._fileHandle.readLines(); + + for await (const line of lines) { + await timeout(this.config.delay); + this._dispatch(line); + } + } + + _dispatch (event) { + this.logger.debug(`read event from file: ${event}`); + + try { + const parsedEvent = JSON.parse(event); + this._collector(parsedEvent); + } catch (error) { + this.logger.error('error processing message:', error); + } + } + + async load () { + try { + this._validateConfig(); + this._fileHandle = await open(this.config.fileName, 'a'); + this.loaded = true; + } catch (error) { + this.logger.error('error loading OutFile:', error); + throw error; + } + } + + async unload () { + if (this._fileHandle != null) { + await this._fileHandle.close(); + this._fileHandle = null; + } + + this.setCollector(OutFile._defaultCollector); + } + + setContext (context) { + this.context = context; + this.logger = context.getLogger(); + + return context; + } + + async onEvent (event, raw) { + if (!this.loaded || this._fileHandle == null) { + throw new Error("OutFile not loaded"); + } + + this.logger.debug('OutFile.onEvent:', event); + + // Write events as JSON objects to FILENAME, always appending each JSON + // is isolated and separated by a newline + // JSON is pretty-printed if this.config.prettyPrint is true + const writableMessage = this.config.prettyPrint + ? JSON.stringify(event, null, 2) + : JSON.stringify(event); + + await this._fileHandle.appendFile(writableMessage + "\n"); + } +} From 308fafbfa8623643c4d70b32ccc917ec97fbb93c Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Tue, 26 Sep 2023 15:04:59 -0300 Subject: [PATCH 005/154] docs: minor updates to README running instructions --- README.md | 43 +++++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 91c68dd..de98697 100644 --- a/README.md +++ b/README.md @@ -2,42 +2,45 @@ This is a node.js application that listens for all events on BigBlueButton and sends POST requests with details about these events to hooks registered via an API. A hook is any external URL that can receive HTTP POST requests. -You can read the full documentation at: https://docs.bigbluebutton.org/dev/webhooks.html +You can read the full documentation at: https://docs.bigbluebutton.org/development/webhooks ## Development -With a [working development](https://docs.bigbluebutton.org/2.2/dev.html#setup-a-development-environment) environment, follow the commands below from within the `bigbluebutton/bbb-webhooks` directory. +With a [working development environment](https://docs.bigbluebutton.org/development/guide), follow the commands below from within the `bigbluebutton/bbb-webhooks` directory. 1. Install the node dependencies: - `npm install` + * See the recommended node version in the `.nvmrc` file or `package.json`'s `engines.node` property. -2. Copy the configuration file: +2. Configure the application: - `cp config/default.example.yml config/default.yml` - - Update the `serverDomain` and `sharedSecret` values to match your BBB server configuration in the newly copied `config/default.yml`. + * This sets up the default configuration values for the application. + - `touch config/development.yml` + * Create a new configuration file for your development environment where you will be able to override specific values from the default configuration file: + - Add the `bbb.serverDomain` and `bbb.sharedSecret` values to match your BBB server configuration in the newly created `config/development.yml`. 3. Stop the bbb-webhook service: - - `sudo service bbb-webhooks stop` + - `sudo systemctl stop bbb-webhooks` 4. Run the application: - - `node app.js` - - **Note:** If the `node app.js` script stops, it's likely an issue with the configuration, or the `bbb-webhooks` service may have not been terminated. - + - `npm start` ### Persistent Hooks If you want all webhooks from a BBB server to post to your 3rd party application/s, you can modify the configuration file to include `permanentURLs` and define one or more server urls which you would like the webhooks to post back to. -To add these permanent urls, do the follow: - - `sudo nano config/default.yml` - - Locate `hooks.permanentURLs` in your config/default.yml and modify it as follows: +To add these permanent urls, do the following: + - `sudo nano config/development.yml` + - Add the `modules."../out/webhhooks/index.js".config.permanentURLs` property to the configuration file, and add one or more urls to the `url` property. You can also add a `getRaw` property to the object to specify whether or not you want the raw data to be sent to the url. If you do not specify this property, it will default to `false`. - ``` - hooks: - permanentURLs: - - url: 'https://staging.example.com/webhook-post-route', - getRaw: false - - url: 'https://app.example.com/webhook-post-route', + ../out/webhooks/index.js: + config: + permanentURLs: + - url: 'https://staging.example.com/webhook-post-route' getRaw: false + - url: 'https://app.example.com/webhook-post-route' + getRaw: true ``` Once you have adjusted your configuration file, you will need to restart your development/app server to adapt to the new configuration. @@ -51,10 +54,10 @@ If you are editing these permanent urls after they have already been committed t Follow the commands below starting within the `bigbluebutton/bbb-webhooks` directory. -1. Copy the entire webhooks directory: +1. Copy the entire webhooks directory: - `sudo cp -r . /usr/local/bigbluebutton/bbb-webhooks` - -2. Move into the directory we just copied the files to: + +2. Move into the directory we just copied the files to: - `cd /usr/local/bigbluebutton/bbb-webhooks` 3. Install the dependencies: @@ -66,4 +69,4 @@ Follow the commands below starting within the `bigbluebutton/bbb-webhooks` direc - `sudo nano config/default.yml` 9. Start the bbb-webhooks service: - - `sudo service bbb-webhooks restart` \ No newline at end of file + - `sudo systemctl bbb-webhooks restart` From 7974291dee5f0e73472a0cdd6fa5338713dfa0fd Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Tue, 26 Sep 2023 18:26:15 -0300 Subject: [PATCH 006/154] fix: shutdown on uncaught/unhandled errors in dev envs --- src/modules/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/modules/index.js b/src/modules/index.js index bf75815..bfbfea2 100644 --- a/src/modules/index.js +++ b/src/modules/index.js @@ -90,13 +90,14 @@ export default class ModuleManager { process.on('uncaughtException', async (error) => { this.logger.error("CRITICAL: uncaught exception, shutdown", { error: error.stack }); await this.stopModules(); - process.exit('1'); + if (process.env.NODE_ENV !== 'production') process.exit(1); }); // Added this listener to identify unhandled promises, but we should start making // sense of those as we find them process.on('unhandledRejection', (reason) => { this.logger.error("CRITICAL: Unhandled promise rejection", { reason: reason.toString(), stack: reason.stack }); + if (process.env.NODE_ENV !== 'production') process.exit(1); }); } From 4c30e87566e36ad45f546b0be6fa8251d301a032 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Tue, 26 Sep 2023 18:38:37 -0300 Subject: [PATCH 007/154] chore: set ecmaVersion to 2020 in eslint --- .eslintrc.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index 254f8e6..06f1612 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -1,11 +1,12 @@ env: node: true - es2023: true -extends: + es2020: true +extends: - eslint:recommended - plugin:import/recommended parserOptions: sourceType: module + ecmaVersion: 2020 rules: #quotes: ["warn", "single"] no-console: "warn" From d3a5cc1f03185c26e800d83c7ac195145fc0e3d5 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Tue, 26 Sep 2023 18:39:18 -0300 Subject: [PATCH 008/154] chore: update util to intercept events --- extra/interceptor.js | 86 +++++++++++++++++++++++++++++++++++++++++++ extra/post_catcher.js | 55 --------------------------- 2 files changed, 86 insertions(+), 55 deletions(-) create mode 100644 extra/interceptor.js delete mode 100644 extra/post_catcher.js diff --git a/extra/interceptor.js b/extra/interceptor.js new file mode 100644 index 0000000..b0097b4 --- /dev/null +++ b/extra/interceptor.js @@ -0,0 +1,86 @@ +/* eslint no-console: "off" */ + +// Lists all the events that happen in a meeting. Run with 'node events.js'. +// Uses the first meeting started after the application runs and will list all +// events, but only the first time they happen. +import express from "express"; +import request from "request"; +import sha1 from "sha1"; +import bodyParser from 'body-parser'; + +// server configs +const eject = (reason) => { throw new Error(reason); } +const port = process.env.PORT || 3006; +const sharedSecret = process.env.SHARED_SECRET || eject("SHARED_SECRET not set"); +const catcherDomain = process.env.CATCHER_DOMAIN || eject("CATCHER_DOMAIN not set"); +const bbbDomain = process.env.BBB_DOMAIN || eject("BBB_DOMAIN not set"); +let server = null; + +const encodeForUrl = (value) => { + return encodeURIComponent(value) + .replace(/%20/g, '+') + .replace(/[!'()]/g, escape) + .replace(/\*/g, "%2A") +}; + +const shutdown = (code) => { + console.log(`Shutting down server, code ${code}`); + if (server) server.close(); + process.exit(code); +} + +// create a server to listen for callbacks +const app = express(); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ + extended: true +})); +server = app.listen(port); +app.post("/callback", (req, res) => { + try { + console.log("-------------------------------------"); + console.log("* Received:", req.url); + console.log("* Body:", req.body); + console.log("-------------------------------------\n"); + res.statusCode = 200; + res.send(); + } catch (error) { + console.error("Error processing callback:", error); + res.statusCode = 500; + res.send(); + } +}); +console.log("Server listening on port", port); + +// registers a global hook on the webhooks app +const myUrl = "http://" + catcherDomain + ":" + port + "/callback"; +const params = "callbackURL=" + encodeForUrl(myUrl); +const checksum = sha1("hooks/create" + params + sharedSecret); +const fullUrl = "http://" + bbbDomain + "/bigbluebutton/api/hooks/create?" + + params + "&checksum=" + checksum +const requestOptions = { + uri: fullUrl, + method: "GET" +} +console.log("Registering a hook with", fullUrl); +request(requestOptions, (error, response, body) => { + const statusCode = response?.statusCode; + // consider 401 as success, because the callback worked but was denied by the recipient + if (statusCode >= 200 && statusCode < 300) { + console.debug("Hook registed - response from hook/create:", body); + } else { + console.log("Hook registration failed - response from hook/create:", body); + shutdown(1); + } +}); + +process.on('SIGINT', () => shutdown(0)); +process.on('SIGTERM', () => shutdown(0)); +process.on('uncaughtException', (error) => { + console.error('uncaughtException:', error); + shutdown(1); +}); +process.on('unhandledRejection', (reason, promise) => { + console.error('unhandledRejection:', reason, promise); + shutdown(1); +}); diff --git a/extra/post_catcher.js b/extra/post_catcher.js deleted file mode 100644 index 2ea9fa7..0000000 --- a/extra/post_catcher.js +++ /dev/null @@ -1,55 +0,0 @@ -// Lists all the events that happen in a meeting. Run with 'node events.js'. -// Uses the first meeting started after the application runs and will list all -// events, but only the first time they happen. - -var redis = require("redis"); -var express = require("express"); -var request = require("request"); -var sha1 = require("sha1"); -var bodyParser = require('body-parser'); - -// server configs -var port = 3006; // port in which to run this app -var shared_secret = "33e06642a13942004fd83b3ba6e4104a"; // shared secret of your server -var domain = "127.0.0.1"; // address of your server -var target_domain = "127.0.0.1:3005"; // address of the webhooks app - -var encodeForUrl = function(value) { - return encodeURIComponent(value) - .replace(/%20/g, '+') - .replace(/[!'()]/g, escape) - .replace(/\*/g, "%2A") -} - -// create a server to listen for callbacks -var app = express(); -app.use(bodyParser.json()); -app.use(bodyParser.urlencoded({ - extended: true -})); -app.listen(port); -app.post("/callback", function(req, res, next) { - console.log("-------------------------------------"); - console.log("* Received:", req.url); - console.log("* Body:", req.body); - console.log("-------------------------------------\n"); - res.statusCode = 200; - res.send(); -}); -console.log("Server listening on port", port); - -// registers a global hook on the webhooks app -var myurl = "http://" + domain + ":" + port + "/callback"; -var params = "callbackURL=" + encodeForUrl(myurl); -var checksum = sha1("hooks/create" + params + shared_secret); -var fullurl = "http://" + target_domain + "/bigbluebutton/api/hooks/create?" + - params + "&checksum=" + checksum - -var requestOptions = { - uri: fullurl, - method: "GET" -} -console.log("Registering a hook with", fullurl); -request(requestOptions, function(error, response, body) { - console.log("Response from hook/create:", body); -}); From a3e4ada2c40853d5acae71e4f708b7bbb759fc62 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Tue, 26 Sep 2023 20:37:48 -0300 Subject: [PATCH 009/154] chore: set ecmaVersion to 2022 in eslint --- .eslintrc.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index 06f1612..2be4362 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -1,12 +1,12 @@ env: node: true - es2020: true + es2022: true extends: - eslint:recommended - plugin:import/recommended parserOptions: sourceType: module - ecmaVersion: 2020 + ecmaVersion: 2022 rules: #quotes: ["warn", "single"] no-console: "warn" From 967c23f83cc179f4a221dc7b9382e8fbc31ff62b Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Tue, 26 Sep 2023 20:54:21 -0300 Subject: [PATCH 010/154] fix: restore raw event hooks --- src/db/redis/hooks.js | 6 +----- src/out/webhooks/web-hooks.js | 29 +++++++++++++++-------------- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/src/db/redis/hooks.js b/src/db/redis/hooks.js index f66bd3d..a662622 100644 --- a/src/db/redis/hooks.js +++ b/src/db/redis/hooks.js @@ -1,9 +1,8 @@ -import config from 'config'; import { v4 as uuidv4, v5 as uuidv5, } from 'uuid'; -import { StorageItem, StorageCompartmentKV } from './base-storage.js'; +import { StorageCompartmentKV } from './base-storage.js'; // The database of hooks. // Used always from memory, but saved to redis for persistence. @@ -21,9 +20,6 @@ import { StorageItem, StorageCompartmentKV } from './base-storage.js'; // it will only receive calls related to this meeting, otherwise it will be global. // faster than the callbacks are made. In this case the events will be concatenated // and send up to 10 events in every post - -let REDIS_CLIENT = null; - class HookCompartment extends StorageCompartmentKV { constructor(client, prefix, setId, options = {}) { super(client, prefix, setId, options); diff --git a/src/out/webhooks/web-hooks.js b/src/out/webhooks/web-hooks.js index e073e80..7053a64 100644 --- a/src/out/webhooks/web-hooks.js +++ b/src/out/webhooks/web-hooks.js @@ -1,4 +1,3 @@ -import config from 'config'; import CallbackEmitter from './callback-emitter.js'; import HookCompartment from '../../db/redis/hooks.js'; @@ -15,7 +14,7 @@ export default class WebHooks { return Promise.resolve(); } - _processRaw(devent) { + _processRaw(event) { let meetingID; let hooks = HookCompartment.get().allGlobalSync(); @@ -25,13 +24,15 @@ export default class WebHooks { if (meetingID != null) { const eMeetingID = this._extractExternalMeetingID(event); - hooks = hooks.concat(HookCompartment.get().findByExternalMeeting(eMeetingID)); + hooks = hooks.concat(HookCompartment.get().findByExternalMeetingID(eMeetingID)); // Notify the hooks that expect raw data return Promise.all(hooks.map((hook) => { - if (hook.getRaw) { - this.logger.info('enqueueing a raw event in the hook', { callbackURL: hook.payload.callbackURL }); + if (hook == null) return Promise.resolve(); + + if (hook.payload.getRaw) { + this.logger.info('dispatching raw event to hook', { callbackURL: hook.payload.callbackURL }); return this.dispatch(event, hook).catch((error) => { - this.logger.error('failed to enqueue', { calbackURL: hook.callbackURL, error: error.stack }); + this.logger.error('failed to enqueue', { calbackURL: hook.payload.callbackURL, error: error.stack }); }); } @@ -66,7 +67,7 @@ export default class WebHooks { || event.data.id == null || (!hook.payload.eventID.some((ev) => ev == event.data.id.toLowerCase()))) ) { - Logger.info(`[Hook] ${hook.payload.callbackURL} skipping event from queue because not in event list for hook: ${JSON.stringify(event)}`); + this.logger.info(`${hook.payload.callbackURL} skipping event because not in event list for hook: ${JSON.stringify(event)}`); return ; } @@ -88,7 +89,7 @@ export default class WebHooks { emitter.once("stopped", () => { this.logger.warn(`too many failed attempts to perform a callback call, removing the hook for: ${hook.payload.callbackURL}`); // TODO just disable - return hook.destroy().then(resolve).catch(resolve); + return hook.destroy().then(resolve).catch(reject); }); }); } @@ -110,17 +111,17 @@ export default class WebHooks { return Promise.all(hooks.map((hook) => { if (hook == null) return Promise.resolve(); - if (!hook.getRaw) { - this.logger.info('dispatching raw event in the hook', { callbackURL: hook.payload.callbackURL }); + if (!hook.payload.getRaw) { + this.logger.info('dispatching event to hook', { callbackURL: hook.payload.callbackURL }); return this.dispatch(event, hook).catch((error) => { - this.logger.error('failed to enqueue', { calbackURL: hook.callbackURL, error: error.stack }); + this.logger.error('failed to enqueue', { calbackURL: hook.payload.callbackURL, error: error.stack }); }); } return Promise.resolve(); - })).then(() => {; - const sendRaw = hooks.some(hook => hook && hook.getRaw); - if (sendRaw && config.get("hooks.getRaw")) return this._processRaw(raw); + })).then(() => { + const sendRaw = hooks.some(hook => hook && hook.payload.getRaw); + if (sendRaw && this.config.getRaw) return this._processRaw(raw); return Promise.resolve(); }); From b026d4431c11956a90034bbbafb64a3d001d626f Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Wed, 27 Sep 2023 00:15:29 -0300 Subject: [PATCH 011/154] fix: restore permanent hooks --- src/out/webhooks/api/api.js | 5 ++--- src/out/webhooks/callback-emitter.js | 15 +++++++++++---- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/out/webhooks/api/api.js b/src/out/webhooks/api/api.js index 6d30b6e..a7697f5 100644 --- a/src/out/webhooks/api/api.js +++ b/src/out/webhooks/api/api.js @@ -108,8 +108,7 @@ export default class API { if (duplicated) { msg = responses.createDuplicated(hook.id); } else if (hook != null) { - const { id, payload } = hook; - const { permanent, getRaw } = payload; + const { permanent, getRaw } = hook.payload; msg = responses.createSuccess(hook.id, permanent, getRaw); } else { msg = responses.createFailure; @@ -126,7 +125,7 @@ export default class API { async createPermanents() { for (let i = 0; i < this._permanentURLs.length; i++) { try { - const { url: callbackURL, getRaw } = this._permanentURLs[i].url; + const { url: callbackURL, getRaw } = this._permanentURLs[i]; const { hook, duplicated } = await API.storage.get().addSubscription({ callbackURL, permanent: this._isHookPermanent(callbackURL), diff --git a/src/out/webhooks/callback-emitter.js b/src/out/webhooks/callback-emitter.js index 3736591..5cb91ef 100644 --- a/src/out/webhooks/callback-emitter.js +++ b/src/out/webhooks/callback-emitter.js @@ -63,7 +63,7 @@ export default class CallbackEmitter extends EventEmitter { this.emit("failure", error); // get the next interval we have to wait and schedule a new try - const interval = this.retryIntervals[this.nextInterval]; + const interval = this._retryIntervals[this.nextInterval]; if (interval != null) { Logger.warn(`trying the callback again in ${interval/1000.0} secs`); this.nextInterval++; @@ -120,10 +120,17 @@ export default class CallbackEmitter extends EventEmitter { const checksum = Utils.checksum(`${this.callbackURL}${JSON.stringify(data)}${sharedSecret}`); // get the final callback URL, including the checksum - const urlObj = url.parse(this.callbackURL, true); + let callbackURL = this.callbackURL; - callbackURL += Utils.isEmpty(urlObj.search) ? "?" : "&"; - callbackURL += `checksum=${checksum}`; + try { + const urlObj = url.parse(this.callbackURL, true); + callbackURL += Utils.isEmpty(urlObj.search) ? "?" : "&"; + callbackURL += `checksum=${checksum}`; + } catch (e) { + Logger.error(`error parsing callback URL: ${this.callbackURL}`); + callback(e, false); + return; + } requestOptions = { followRedirect: true, From 2695161bd213c9290dbf654926f71c639da9bdff Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Wed, 27 Sep 2023 00:18:37 -0300 Subject: [PATCH 012/154] fix: partially restore tests --- package.json | 2 +- test/helpers.js | 13 +- test/test.js | 350 +++++++++++++++++++----------------------------- 3 files changed, 144 insertions(+), 221 deletions(-) diff --git a/package.json b/package.json index 4bc2683..803dccd 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "start": "node app.js", - "test": "mocha", + "test": "ALLOW_CONFIG_MUTATIONS=true mocha", "lint": "./node_modules/.bin/eslint ./", "lint:file": "./node_modules/.bin/eslint" }, diff --git a/test/helpers.js b/test/helpers.js index d4eb17e..f8d6e5c 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -1,8 +1,7 @@ - const helpers = {}; -helpers.url = 'http://10.0.3.179'; //serverUrl -helpers.port = ':3005' +helpers.url = 'http://127.0.0.1'; +helpers.port = ':3005'; helpers.callback = 'http://we2bh.requestcatcher.com' helpers.callbackURL = '?callbackURL=' + helpers.callback helpers.apiPath = '/bigbluebutton/api/hooks/' @@ -10,6 +9,7 @@ helpers.createUrl = helpers.port + helpers.apiPath + 'create/' + helpers.callbac helpers.destroyUrl = (id) => { return helpers.port + helpers.apiPath + 'destroy/' + '?hookID=' + id } helpers.destroyPermanent = helpers.port + helpers.apiPath + 'destroy/' + '?hookID=1' helpers.createRaw = '&getRaw=true' +helpers.createPermanent = '&permanent=true' helpers.listUrl = 'list/' helpers.rawMessage = { envelope: { @@ -35,12 +35,11 @@ helpers.rawMessage = { }; helpers.flushall = (rClient) => { - let client = rClient; - client.flushdb() + rClient.flushDb() } helpers.flushredis = (hook) => { - hook.redisClient.flushdb(); + if (hook?.client) hook.client.flushDb(); } -module.exports = helpers; +export default helpers; diff --git a/test/test.js b/test/test.js index 03299e7..66d9ee5 100644 --- a/test/test.js +++ b/test/test.js @@ -1,145 +1,132 @@ -const request = require('supertest'); -const nock = require("nock"); -const Logger = require('../logger.js'); -const utils = require('../utils.js'); -const config = require('config'); -const Hook = require('../hook.js'); -const Helpers = require('./helpers.js') -const sinon = require('sinon'); -const winston = require('winston'); +import { describe, it, before, after, beforeEach } from 'mocha'; +import request from 'supertest'; +import nock from "nock"; +import utils from '../src/common/utils.js'; +import config from 'config'; +import Hook from '../src/db/redis/hooks.js'; +import Helpers from './helpers.js' +import Application from '../application.js'; +import redis from 'redis'; + +const TEST_CHANNEL = 'test-channel'; +const SHARED_SECRET = process.env.SHARED_SECRET || function () { throw new Error('SHARED_SECRET not set'); }(); +const MODULES = config.get('modules'); +const WH_CONFIG = MODULES['../out/webhooks/index.js'].config; +const IN_REDIS_CONFIG = MODULES['../in/redis/index.js'].config.redis; -Application = require('../application.js'); - -const sharedSecret = process.env.SHARED_SECRET || sharedSecret; - -let globalHooks = config.get('hooks'); - -// Block winston from logging -Logger.remove(winston.transports.Console); describe('bbb-webhooks tests', () => { - before( (done) => { - globalHooks.queueSize = 10; - globalHooks.permanentURLs = [ { url: "http://wh.requestcatcher.com", getRaw: true } ]; - application = new Application(); - application.start( () => { - done(); - }); + const application = new Application(); + const redisClient = redis.createClient({ + host: config.get('redis.host'), + port: config.get('redis.port'), + password: config.has('redis.password') ? config.get('redis.password') : undefined, + }); + + before((done) => { + WH_CONFIG.queueSize = 10; + WH_CONFIG.permanentURLs = [ { url: "https://wh.requestcatcher.com", getRaw: true } ]; + IN_REDIS_CONFIG.inboundChannels = [...IN_REDIS_CONFIG.inboundChannels, TEST_CHANNEL]; + application.start() + .then(redisClient.connect()) + .then(() => { done(); }) + .catch(done); }); - beforeEach( (done) => { - const hooks = Hook.allGlobalSync(); - Helpers.flushall(Application.redisClient()); - hooks.forEach( hook => { + beforeEach((done) => { + const hooks = Hook.get().allGlobalSync(); + Helpers.flushall(redisClient); + hooks.forEach((hook) => { Helpers.flushredis(hook); - }) + }); + done(); }) - after( () => { - const hooks = Hook.allGlobalSync(); - Helpers.flushall(Application.redisClient()); - hooks.forEach( hook => { + after(() => { + const hooks = Hook.get().allGlobalSync(); + Helpers.flushall(redisClient); + hooks.forEach((hook) => { Helpers.flushredis(hook); - }) + }); }); describe('GET /hooks/list permanent', () => { it('should list permanent hook', (done) => { - let getUrl = utils.checksumAPI(Helpers.url + Helpers.listUrl, sharedSecret); + let getUrl = utils.checksumAPI(Helpers.url + Helpers.listUrl, SHARED_SECRET); getUrl = Helpers.listUrl + '?checksum=' + getUrl - request(Helpers.url) - .get(getUrl) - .expect('Content-Type', /text\/xml/) - .expect(200, (res) => { - const hooks = Hook.allGlobalSync(); - if (hooks && hooks.some( hook => { return hook.permanent }) ) { - done(); - } - else { - done(new Error ("permanent hook was not created")); - } - }) - }) - }); - - describe('GET /hooks/create', () => { - after( (done) => { - const hooks = Hook.allGlobalSync(); - Hook.removeSubscription(hooks[hooks.length-1].id, () => { done(); }); - }); - it('should create a hook', (done) => { - let getUrl = utils.checksumAPI(Helpers.url + Helpers.createUrl, sharedSecret); - getUrl = Helpers.createUrl + '&checksum=' + getUrl - request(Helpers.url) .get(getUrl) .expect('Content-Type', /text\/xml/) - .expect(200, (res) => { - const hooks = Hook.allGlobalSync(); - if (hooks && hooks.some( hook => { return !hook.permanent }) ) { + .expect(200, () => { + const hooks = Hook.get().allGlobalSync(); + if (hooks && hooks.some(hook => hook.payload.permanent)) { done(); - } - else { - done(new Error ("hook was not created")); + } else { + done(new Error ("permanent hook was not created")); } }) }) }); describe('GET /hooks/destroy', () => { - before( (done) => { - Hook.addSubscription(Helpers.callback,null,null,false,() => { done(); }); + before((done) => { + Hook.get().addSubscription({ + callbackURL: Helpers.callback, + permanent: false, + getRaw: false, + }).then(() => { done(); }).catch(done); }); it('should destroy a hook', (done) => { - const hooks = Hook.allGlobalSync(); + const hooks = Hook.get().allGlobalSync(); const hook = hooks[hooks.length-1].id; - let getUrl = utils.checksumAPI(Helpers.url + Helpers.destroyUrl(hook), sharedSecret); + let getUrl = utils.checksumAPI(Helpers.url + Helpers.destroyUrl(hook), SHARED_SECRET); getUrl = Helpers.destroyUrl(hook) + '&checksum=' + getUrl request(Helpers.url) .get(getUrl) .expect('Content-Type', /text\/xml/) - .expect(200, (res) => { - const hooks = Hook.allGlobalSync(); - if(hooks && hooks.every( hook => { return hook.callbackURL != Helpers.callback })) - done(); + .expect(200, () => { + const hooks = Hook.get().allGlobalSync(); + if (hooks && hooks.every(hook => hook.payload.callbackURL != Helpers.callback)) done(); }) }) }); describe('GET /hooks/destroy permanent hook', () => { it('should not destroy the permanent hook', (done) => { - let getUrl = utils.checksumAPI(Helpers.url + Helpers.destroyPermanent, sharedSecret); + let getUrl = utils.checksumAPI(Helpers.url + Helpers.destroyPermanent, SHARED_SECRET); getUrl = Helpers.destroyPermanent + '&checksum=' + getUrl request(Helpers.url) - .get(getUrl) - .expect('Content-Type', /text\/xml/) - .expect(200, (res) => { - const hooks = Hook.allGlobalSync(); - if (hooks && hooks[0].callbackURL == globalHooks.permanentURLs[0].url) { - done(); - } - else { - done(new Error("should not delete permanent")); - } - }) + .get(getUrl) + .expect('Content-Type', /text\/xml/) + .expect(200, () => { + const hooks = Hook.get().allGlobalSync(); + if (hooks && hooks[0].payload.callbackURL == WH_CONFIG.permanentURLs[0].url) { + done(); + } + else { + done(new Error("should not delete permanent")); + } + }) }) }); describe('GET /hooks/create getRaw hook', () => { after( (done) => { - const hooks = Hook.allGlobalSync(); - Hook.removeSubscription(hooks[hooks.length-1].id, () => { done(); }); + const hooks = Hook.get().allGlobalSync(); + Hook.get().removeSubscription(hooks[hooks.length-1].id) + .then(() => { done(); }) + .catch(done); }); it('should create a hook with getRaw=true', (done) => { - let getUrl = utils.checksumAPI(Helpers.url + Helpers.createUrl + Helpers.createRaw, sharedSecret); + let getUrl = utils.checksumAPI(Helpers.url + Helpers.createUrl + Helpers.createRaw, SHARED_SECRET); getUrl = Helpers.createUrl + '&checksum=' + getUrl + Helpers.createRaw request(Helpers.url) .get(getUrl) .expect('Content-Type', /text\/xml/) - .expect(200, (res) => { - const hooks = Hook.allGlobalSync(); - if (hooks && hooks.some( (hook) => { return hook.getRaw })) { + .expect(200, () => { + const hooks = Hook.get().allGlobalSync(); + if (hooks && hooks.some((hook) => { return hook.payload.getRaw })) { done(); } else { @@ -149,159 +136,96 @@ describe('bbb-webhooks tests', () => { }) }); - describe('Hook queues', () => { - before( () => { - Application.redisPubSubClient().psubscribe("test-channel"); - Hook.addSubscription(Helpers.callback,null,null,false, (err,reply) => { - const hooks = Hook.allGlobalSync(); - const hook = hooks[0]; - const hook2 = hooks[hooks.length -1]; - sinon.stub(hook, '_processQueue'); - sinon.stub(hook2, '_processQueue'); - }); - }); - after( () => { - const hooks = Hook.allGlobalSync(); - const hook = hooks[0]; - const hook2 = hooks[hooks.length -1]; - - hook._processQueue.restore(); - hook2._processQueue.restore(); - Hook.removeSubscription(hooks[hooks.length-1].id); - Application.redisPubSubClient().unsubscribe("test-channel"); - }); - it('should have different queues for each hook', (done) => { - Application.redisClient().publish("test-channel", JSON.stringify(Helpers.rawMessage)); - const hooks = Hook.allGlobalSync(); - - if (hooks && hooks[0].queue != hooks[hooks.length-1].queue) { - done(); - } - else { - done(new Error("hooks using same queue")) - } - }) - }); - // reduce queue size, fill queue with requests, try to add another one, if queue does not exceed, OK - describe('Hook queues', () => { - before( () => { - const hooks = Hook.allGlobalSync(); - const hook = hooks[0]; - sinon.stub(hook, '_processQueue'); - }); - after( () => { - const hooks = Hook.allGlobalSync(); - const hook = hooks[0]; - hook._processQueue.restore(); - Helpers.flushredis(hook); - }) - it('should limit queue size to defined in config', (done) => { - let hook = Hook.allGlobalSync(); - hook = hook[0]; - for(i=0;i<=9;i++) { hook.enqueue("message" + i); } - - if (hook && hook.queue.length <= globalHooks.queueSize) { - done(); - } - else { - done(new Error("hooks exceeded max queue size")) - } - }) - }); - describe('/POST mapped message', () => { - before( () => { - Application.redisPubSubClient().psubscribe("test-channel"); - const hooks = Hook.allGlobalSync(); + before((done) => { + const hooks = Hook.get().allGlobalSync(); const hook = hooks[0]; - hook.queue = []; Helpers.flushredis(hook); + done(); }); - after( () => { - const hooks = Hook.allGlobalSync(); + after(() => { + const hooks = Hook.get().allGlobalSync(); const hook = hooks[0]; Helpers.flushredis(hook); - Application.redisPubSubClient().unsubscribe("test-channel"); }) it('should post mapped message ', (done) => { - const hooks = Hook.allGlobalSync(); + const hooks = Hook.get().allGlobalSync(); const hook = hooks[0]; - - const getpost = nock(globalHooks.permanentURLs[0].url) - .filteringRequestBody( (body) => { - let parsed = JSON.parse(body) - return parsed[0].data.id ? "mapped" : "not mapped"; - }) - .post("/", "mapped") - .reply(200, (res) => { - done(); - }); - Application.redisClient().publish("test-channel", JSON.stringify(Helpers.rawMessage)); + const getpost = nock(WH_CONFIG.permanentURLs[0].url) + .filteringRequestBody((body) => { + let parsed = JSON.parse(body) + return parsed[0].data.id ? "mapped" : "not mapped"; + }) + .post("/", "mapped") + .reply(200, (res) => { + done(); + }); + redisClient.publish(TEST_CHANNEL, JSON.stringify(Helpers.rawMessage)); }) }); - describe('/POST raw message', () => { - before( () => { - Application.redisPubSubClient().psubscribe("test-channel"); - Hook.addSubscription(Helpers.callback,null,null,true, (err,hook) => { - Helpers.flushredis(hook); - }) + before((done) => { + const hooks = Hook.get().allGlobalSync(); + const hook = hooks[0]; + Helpers.flushredis(hook); + done(); }); - after( () => { - const hooks = Hook.allGlobalSync(); - Hook.removeSubscription(hooks[hooks.length-1].id); + after((done) => { + const hooks = Hook.get().allGlobalSync(); + Hook.get().removeSubscription(hooks[hooks.length-1].id) + .then(() => { done(); }) + .catch(done); Helpers.flushredis(hooks[hooks.length-1]); - Application.redisPubSubClient().unsubscribe("test-channel"); }); it('should post raw message ', (done) => { - const hooks = Hook.allGlobalSync(); + const hooks = Hook.get().allGlobalSync(); const hook = hooks[0]; const getpost = nock(Helpers.callback) - .filteringRequestBody( (body) => { - if (body.indexOf("PresenterAssignedEvtMsg")) { - return "raw message"; - } - else { return "not raw"; } - }) - .post("/", "raw message") - .reply(200, () => { - done(); - }); - const permanent = nock(globalHooks.permanentURLs[0].url) - .post("/") - .reply(200) - Application.redisClient().publish("test-channel", JSON.stringify(Helpers.rawMessage)); + .filteringRequestBody( (body) => { + if (body.indexOf("PresenterAssignedEvtMsg")) { + return "raw message"; + } + else { return "not raw"; } + }) + .post("/", "raw message") + .reply(200, () => { + done(); + }); + const permanent = nock(WH_CONFIG.permanentURLs[0].url) + .post("/") + .reply(200) + redisClient.publish(TEST_CHANNEL, JSON.stringify(Helpers.rawMessage)); }) }); describe('/POST multi message', () => { before( () =>{ - const hooks = Hook.allGlobalSync(); + const hooks = Hook.get().allGlobalSync(); const hook = hooks[0]; Helpers.flushredis(hook); hook.queue = ["multiMessage1"]; }); it('should post multi message ', (done) => { - const hooks = Hook.allGlobalSync(); + const hooks = Hook.get().allGlobalSync(); const hook = hooks[0]; hook.enqueue("multiMessage2") - const getpost = nock(globalHooks.permanentURLs[0].url) - .filteringPath( (path) => { - return path.split('?')[0]; - }) - .filteringRequestBody( (body) => { - if (body.indexOf("multiMessage1") != -1 && body.indexOf("multiMessage2") != -1) { - return "multiMess" - } - else { - return "not multi" - } - }) - .post("/", "multiMess") - .reply(200, (res) => { - done(); - }); + const getpost = nock(WH_CONFIG.permanentURLs[0].url) + .filteringPath( (path) => { + return path.split('?')[0]; + }) + .filteringRequestBody( (body) => { + if (body.indexOf("multiMessage1") != -1 && body.indexOf("multiMessage2") != -1) { + return "multiMess" + } + else { + return "not multi" + } + }) + .post("/", "multiMess") + .reply(200, (res) => { + done(); + }); }) }); }); From 296899577b6075a89fcad871cd387401e1e4392a Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Wed, 27 Sep 2023 10:47:11 -0300 Subject: [PATCH 013/154] fix: sync module loading with priority order to avoid boot race conditions --- application.js | 6 ++--- src/modules/index.js | 45 +++++++++++++++++++++++++---------- src/modules/module-wrapper.js | 10 ++++---- 3 files changed, 40 insertions(+), 21 deletions(-) diff --git a/application.js b/application.js index 557bd2f..3e55853 100644 --- a/application.js +++ b/application.js @@ -14,10 +14,10 @@ export default class Application { async start() { if (this._initialized) return Promise.resolve(); - await this.moduleManager.load(); + const { inputModules, outputModules } = await this.moduleManager.load(); this.eventProcessor = new EventProcessor( - this.moduleManager.getInputModules(), - this.moduleManager.getOutputModules(), + inputModules, + outputModules, ); await this.eventProcessor.start(); diff --git a/src/modules/index.js b/src/modules/index.js index bfbfea2..40756de 100644 --- a/src/modules/index.js +++ b/src/modules/index.js @@ -10,7 +10,6 @@ import { validateModuleConf } from './definitions.js'; - const UNEXPECTED_TERMINATION_SIGNALS = ['SIGABRT', 'SIGBUS', 'SIGSEGV', 'SIGILL']; const BASE_CONFIGURATION = { server: { @@ -56,26 +55,40 @@ export default class ModuleManager { return this.getModulesByType(ModuleManager.moduleTypes.db); } + _sortModulesByPriority(a, b) { + // Sort modules by priority: db modules first, then input modules, then output modules + const aD = a[1] + const bD = b[1] + + if (aD.type === ModuleManager.moduleTypes.db && bD.type !== ModuleManager.moduleTypes.db) { + return -1; + } else if (aD.type !== ModuleManager.moduleTypes.db && bD.type === ModuleManager.moduleTypes.db) { + return 1; + } else if (aD.type === ModuleManager.moduleTypes.in && bD.type === ModuleManager.moduleTypes.out) { + return -1; + } else if (aD.type === ModuleManager.moduleTypes.out && bD.type === ModuleManager.moduleTypes.in) { + return 1; + } else { + return 0; + } + } + async load() { - const loaders = Object.entries(this.modulesConfig).map(([name, description]) => { + const sortedModules = Object.entries(this.modulesConfig).sort(this._sortModulesByPriority); + + for (const [name, description] of sortedModules) { try { const fullConfiguration = { name, ...description }; validateModuleConf(fullConfiguration); const context = this._buildContext(fullConfiguration); const module = new ModuleWrapper(name, description.type, context, context.configuration.config); - return module.load().then(() => { - this.modules[name] = module; - this.logger.info(`module ${name} loaded`); - }).catch((error) => { - this.logger.error(`failed to load module ${name}`, { error: error.stack }); - }); + await module.load() + this.modules[name] = module; + this.logger.info(`module ${name} loaded`); } catch (error) { - this.logger.error(`failed to load module ${name} configuration`, { error: error.stack }); - return Promise.resolve(); + this.logger.error(`failed to load module ${name}`, error); } - }); - - await Promise.all(loaders); + } process.on('SIGTERM', async () => { await this.stopModules(); @@ -99,6 +112,12 @@ export default class ModuleManager { this.logger.error("CRITICAL: Unhandled promise rejection", { reason: reason.toString(), stack: reason.stack }); if (process.env.NODE_ENV !== 'production') process.exit(1); }); + + return { + inputModules: this.getInputModules(), + outputModules: this.getOutputModules(), + dbModules: this.getDBModules(), + }; } trackModuleShutdown (proc) { diff --git a/src/modules/module-wrapper.js b/src/modules/module-wrapper.js index 400a452..63efd1b 100644 --- a/src/modules/module-wrapper.js +++ b/src/modules/module-wrapper.js @@ -2,7 +2,7 @@ import { newLogger } from '../common/logger.js'; import { MODULE_TYPES, validateModuleDefinition } from './definitions.js'; -import { createQueue, getQueue, deleteQueue } from './queue.js'; +import { createQueue, getQueue } from './queue.js'; // [MODULE_TYPES.INPUT]: { // load: 'function', @@ -109,14 +109,14 @@ export default class ModuleWrapper { case MODULE_TYPES.in: this.setContext(this.context); this.setCollector(this.context.collector || ModuleWrapper._defaultCollector); - break; + return Promise.resolve(); case MODULE_TYPES.out: this.setContext(this.context); this._setupOutboundQueues(); - break; + return Promise.resolve(); case MODULE_TYPES.db: this.setContext(this.context); - break; + return Promise.resolve(); default: throw new Error(`module ${this.name} has an invalid type`); } @@ -150,7 +150,7 @@ export default class ModuleWrapper { throw new Error(`module ${this.name} is not valid`); } - this._bootstrap(); + await this._bootstrap(); // Call the module's load() method await this._module.load(); From 84729e049b12aeea9bbd29636009f751a15e9e7b Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Wed, 27 Sep 2023 13:11:41 -0300 Subject: [PATCH 014/154] feat: allow multiple module types per entry in module system e.g.: let an user define input and output modules in the same module config/pkg --- example/modules/in-file.js | 4 +++- example/modules/out-file.js | 4 +++- src/db/redis/index.js | 4 +++- src/in/redis/index.js | 4 +++- src/modules/definitions.js | 7 +++---- src/modules/index.js | 24 +++++++++++++++++++----- src/modules/module-wrapper.js | 23 +++++++++++++++++++++-- src/out/webhooks/index.js | 4 +++- src/out/xapi/index.js | 4 +++- src/process/event-processor.js | 1 - 10 files changed, 61 insertions(+), 18 deletions(-) diff --git a/example/modules/in-file.js b/example/modules/in-file.js index f5125b8..c4e924b 100644 --- a/example/modules/in-file.js +++ b/example/modules/in-file.js @@ -29,12 +29,14 @@ const timeout = (ms) => { }; export default class InFile { + static type = "in"; + static _defaultCollector () { throw new Error('Collector not set'); } constructor (context, config = {}) { - this.type = "in"; + this.type = InFile.type; this.config = config; this.setContext(context); this.loaded = false; diff --git a/example/modules/out-file.js b/example/modules/out-file.js index dbb9605..ba7d9ef 100644 --- a/example/modules/out-file.js +++ b/example/modules/out-file.js @@ -28,12 +28,14 @@ const timeout = (ms) => { }; export default class OutFile { + static type = "out"; + static _defaultCollector () { throw new Error('Collector not set'); } constructor (context, config = {}) { - this.type = "out"; + this.type = OutFile.type; this.config = config; this.setContext(context); this.loaded = false; diff --git a/src/db/redis/index.js b/src/db/redis/index.js index dd73a81..35daa3d 100644 --- a/src/db/redis/index.js +++ b/src/db/redis/index.js @@ -19,9 +19,11 @@ import UserMappingC from './user-mapping.js'; */ export default class RedisDB { + static type = "db"; + constructor (context, config = {}) { this.name = 'db-redis'; - this.type = 'db'; + this.type = RedisDB.type; this.context = this.setContext(context); this.config = config; this.logger = context.getLogger(this.name); diff --git a/src/in/redis/index.js b/src/in/redis/index.js index 4c2802c..b36c267 100644 --- a/src/in/redis/index.js +++ b/src/in/redis/index.js @@ -12,12 +12,14 @@ import Utils from '../../common/utils.js'; */ export default class InRedis { + static type = "in"; + static _defaultCollector () { throw new Error('Collector not set'); } constructor (context, config = {}) { - this.type = "in"; + this.type = InRedis.type; this.config = config; this.setContext(context); diff --git a/src/modules/definitions.js b/src/modules/definitions.js index 00b994d..b88c60d 100644 --- a/src/modules/definitions.js +++ b/src/modules/definitions.js @@ -6,25 +6,24 @@ export const MODULE_TYPES = { export const MODULE_DEFINITION_SCHEMA = { [MODULE_TYPES.in]: { + type: 'string', load: 'function', unload: 'function', setContext: 'function', setCollector: 'function', }, [MODULE_TYPES.out]: { + type: 'string', load: 'function', unload: 'function', setContext: 'function', onEvent: 'function', }, [MODULE_TYPES.db]: { + type: 'string', load: 'function', unload: 'function', setContext: 'function', - save: 'function', - read: 'function', - remove: 'function', - clear: 'function', }, } diff --git a/src/modules/index.js b/src/modules/index.js index 40756de..508ca44 100644 --- a/src/modules/index.js +++ b/src/modules/index.js @@ -27,14 +27,28 @@ const BASE_CONFIGURATION = { export default class ModuleManager { static moduleTypes = MODULE_TYPES; + static flattenModulesConfig(config) { + // A configuration entry can either be an object (single module) or an array + // (multiple modules of different types, eg input and output) + // Need to flatten those into a single array of [name, description] tuples + // so we can sort them by priority + return Object.entries(config).flatMap(([name, description]) => { + if (Array.isArray(description)) { + return description.map((d) => [name, d]); + } else { + return [[name, description]]; + } + }); + } + constructor(modulesConfig) { - this.modulesConfig = modulesConfig; + this.modulesConfig = ModuleManager.flattenModulesConfig(modulesConfig); this.modules = {}; - validateModulesConf(modulesConfig); + validateModulesConf(this.modulesConfig); this.logger = newLogger('module-manager'); } - _buildContext(configuration) { + _buildContext(configuration) { configuration.config = { ...BASE_CONFIGURATION, ...configuration.config }; return new Context(configuration); } @@ -74,7 +88,7 @@ export default class ModuleManager { } async load() { - const sortedModules = Object.entries(this.modulesConfig).sort(this._sortModulesByPriority); + const sortedModules = this.modulesConfig.sort(this._sortModulesByPriority); for (const [name, description] of sortedModules) { try { @@ -83,7 +97,7 @@ export default class ModuleManager { const context = this._buildContext(fullConfiguration); const module = new ModuleWrapper(name, description.type, context, context.configuration.config); await module.load() - this.modules[name] = module; + this.modules[module.id] = module; this.logger.info(`module ${name} loaded`); } catch (error) { this.logger.error(`failed to load module ${name}`, error); diff --git a/src/modules/module-wrapper.js b/src/modules/module-wrapper.js index 63efd1b..0578392 100644 --- a/src/modules/module-wrapper.js +++ b/src/modules/module-wrapper.js @@ -30,6 +30,8 @@ export default class ModuleWrapper { constructor (name, type, context, config = {}) { this.name = name; + this.type = type; + this.id = `${name}-${type}`; this.context = context; this.config = config; this.logger = newLogger('module-wrapper'); @@ -68,8 +70,16 @@ export default class ModuleWrapper { return this._config; } + set type(type) { + if (this._module) { + throw new Error(`module ${this.name} already loaded`); + } + + this._type = type; + } + get type() { - return this._module?.type; + return this._module?.type || this._type; } _setupOutboundQueues() { @@ -138,13 +148,20 @@ export default class ModuleWrapper { // Dynamically import the module provided via this.name // and instantiate it with the context and config provided this._module = await import(this.name).then((module) => { + // Check if the module is an array of modules + // If so, select just the one that matches the provided type (this.type) + if (Array.isArray(module.default)) { + module = module.default.find((m) => m.type === this.type); + if (!module) throw new Error(`module ${this.name} does not exist or is badly defined`); + return new module(this.context, this.config); + } + return new module.default(this.context, this.config); }).catch((error) => { this.logger.error(`error loading module ${this.name}`, error); throw error; }); - // Validate the module if (!validateModuleDefinition(this._module)) { throw new Error(`module ${this.name} is not valid`); @@ -163,6 +180,8 @@ export default class ModuleWrapper { if (this._module?.unload) { return this._module.unload(); } + + return Promise.resolve(); } setContext(context) { diff --git a/src/out/webhooks/index.js b/src/out/webhooks/index.js index 7e3444c..0fdab25 100644 --- a/src/out/webhooks/index.js +++ b/src/out/webhooks/index.js @@ -12,12 +12,14 @@ import HookCompartment from '../../db/redis/hooks.js'; */ export default class OutWebHooks { + static type = "out"; + static _defaultCollector () { throw new Error('Collector not set'); } constructor (context, config = {}) { - this.type = "out"; + this.type = OutWebHooks.type; this.config = config; this.setContext(context); this.api = new API({ diff --git a/src/out/xapi/index.js b/src/out/xapi/index.js index 681e5f2..4c7a507 100644 --- a/src/out/xapi/index.js +++ b/src/out/xapi/index.js @@ -8,12 +8,14 @@ */ export default class OutXAPI { + static type = "out"; + static _defaultCollector () { throw new Error('Collector not set'); } constructor (context, config = {}) { - this.type = "out"; + this.type = OutXAPI.type; this.config = config; this.setContext(context); this.loaded = false; diff --git a/src/process/event-processor.js b/src/process/event-processor.js index baa2ee4..870d5ed 100644 --- a/src/process/event-processor.js +++ b/src/process/event-processor.js @@ -42,7 +42,6 @@ export default class EventProcessor { } processInputEvent(event) { - try { const rawEvent = this._parseEvent(event); const eventInstance = new WebhooksEvent(rawEvent); From 6a11f1f51d6c83679f5ff62b779713c2bd7d3b87 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Wed, 27 Sep 2023 15:19:53 -0300 Subject: [PATCH 015/154] fix: deserialization of hook data to parse attributes with adequate types --- src/db/redis/base-storage.js | 78 +++++++++++++++++++++++++----------- src/db/redis/hooks.js | 49 +++++++++++++++++++--- src/out/webhooks/api/api.js | 2 +- 3 files changed, 98 insertions(+), 31 deletions(-) diff --git a/src/db/redis/base-storage.js b/src/db/redis/base-storage.js index 87bf68d..5a574c8 100644 --- a/src/db/redis/base-storage.js +++ b/src/db/redis/base-storage.js @@ -4,11 +4,11 @@ import { v4 as uuidv4 } from 'uuid'; const stringifyValues = (o) => { Object.keys(o).forEach(k => { // Make all values strings, but ignore nullish/undefined values. - if (typeof o[k] === 'object') { - o[k] = JSON.stringify(stringifyValues(o[k])); - } else if (o[k] == null) { + if (o[k] == null) { delete o[k]; - } else { + } else if (typeof o[k] === 'object') { + o[k] = JSON.stringify(stringifyValues(o[k])); + } else { o[k] = '' + o[k]; } }); @@ -17,9 +17,13 @@ const stringifyValues = (o) => { } class StorageItem { + static stringifyValues = stringifyValues; + constructor(client, prefix, setId, payload, { id = uuidv4(), alias, + serializer, + deserializer, ...appOptions }) { this.client = client; @@ -28,6 +32,8 @@ class StorageItem { this.id = id; this.alias = alias; this.payload = payload; + if (typeof serializer === 'function') this.serialize = serializer.bind(this); + if (typeof deserializer === 'function') this.deserialize = deserializer.bind(this); this.appOptions = appOptions; this.redisClient = client; this.logger = newLogger(`db:${this.prefix}|${this.setId}`); @@ -35,16 +41,16 @@ class StorageItem { async save() { try { - await this.redisClient.hSet(this.prefix + ":" + this.id, this.serialize()); + await this.redisClient.hSet(this.prefix + ":" + this.id, this.serialize(this)); } catch (error) { - this.logger.error(`error saving mapping to redis: ${error}`); + this.logger.error('error saving mapping to redis', error); throw error; } try { await this.redisClient.sAdd(this.setId, (this.id).toString()); } catch (error) { - this.logger.error(`error saving mapping ID to the list of mappings: ${error}`); + this.logger.error('error saving mapping ID to the list of mappings', error); throw error; } @@ -55,22 +61,22 @@ class StorageItem { try { await this.redisClient.sRem(this.setId, (this.id).toString()); } catch (error) { - this.logger.error(`error removing mapping ID from the list of mappings: ${error}`); + this.logger.error('error removing mapping ID from the list of mappings', error); } try { await this.redisClient.del(this.prefix + ":" + this.id); } catch (error) { - this.logger.error(`error removing mapping from redis: ${error}`); + this.logger.error('error removing mapping from redis', error); } return true; } - serialize() { + serialize(data) { const r = { - id: this.id, - ...this.payload, + id: data.id, + ...data.payload, }; const s = Object.entries(stringifyValues(r)).flat(); @@ -78,20 +84,22 @@ class StorageItem { } deserialize(data) { - const { id, ...payload } = data; - this.id = id; - this.payload = payload; + return JSON.parse(data); } print() { - return this.serialize(); + return this.serialize(this); } } class StorageCompartmentKV { + static stringifyValues = stringifyValues; + constructor (client, prefix, setId, { itemClass = StorageItem, aliasField, + serializer, + deserializer, ...appOptions } = {}) { this.redisClient = client; @@ -101,10 +109,27 @@ class StorageCompartmentKV { this.localStorage = {} this.aliasField = aliasField; this.appOptions = appOptions; + if (typeof serializer === 'function') this.serialize = serializer.bind(this); + if (typeof deserializer === 'function') this.deserialize = deserializer.bind(this); this.logger = newLogger(`db:${this.prefix}|${this.setId}`); } + serialize(data) { + const r = { + id: data.id, + ...data.payload, + }; + + const s = Object.entries(stringifyValues(r)).flat(); + return s; + } + + deserialize(data) { + return data; + } + + async save(payload, { id = uuidv4(), alias, @@ -116,6 +141,9 @@ class StorageCompartmentKV { let mapping = new this.itemClass(this.redisClient, this.prefix, this.setId, payload, { id, alias, + serializer: this.serialize, + deserializer: this.deserialize, + ...this.appOptions, }); await mapping.save(); @@ -220,36 +248,38 @@ class StorageCompartmentKV { try { const mappings = await this.redisClient.sMembers(this.setId); + this.logger.info(`starting resync, mappings registered: [${mappings}]`); if (mappings != null && mappings.length > 0) { return Promise.all(mappings.map(async (id) => { try { - const kek = await this.redisClient.hGetAll(this.prefix + ":" + id); - const { id: rId, ...mappingData } = kek; + const data = await this.redisClient.hGetAll(this.prefix + ":" + id); + const { id: rId, ...payload } = this.deserialize(data); - if (mappingData && Object.keys(mappingData).length > 0) { - await this.save(mappingData, { + if (payload && Object.keys(payload).length > 0) { + await this.save(payload, { id: rId, - alias: mappingData[this.aliasField], + alias: payload[this.aliasField], + itemClass: this.itemClass }); } return Promise.resolve(); } catch (error) { - this.logger.error(`error getting information for a mapping from redis: ${error}`); + this.logger.error('error getting information for a mapping from redis', error); return Promise.resolve(); } })).then(() => { const stringifiedMappings = this.getAll().map(m => m.print()); this.logger.info(`finished resync, mappings registered: [${stringifiedMappings}]`); }).catch((error) => { - this.logger.error(`error getting list of mappings from redis: ${error}`); + this.logger.error('error getting list of mappings from redis', error); }); } this.logger.info(`finished resync, no mappings registered`); return Promise.resolve(); } catch (error) { - this.logger.error(`error getting list of mappings from redis: ${error}`); + this.logger.error('error getting list of mappings from redis', error); return Promise.resolve(); } } diff --git a/src/db/redis/hooks.js b/src/db/redis/hooks.js index a662622..bb4d139 100644 --- a/src/db/redis/hooks.js +++ b/src/db/redis/hooks.js @@ -21,10 +21,47 @@ import { StorageCompartmentKV } from './base-storage.js'; // faster than the callbacks are made. In this case the events will be concatenated // and send up to 10 events in every post class HookCompartment extends StorageCompartmentKV { + static itemDeserializer(data) { + const { id, ...payload } = data; + const parsedPayload = { + callbackURL: payload.callbackURL, + externalMeetingID: payload.externalMeetingID, + // Should be an array + eventID: payload.eventID?.split(','), + // Should be a boolean + permanent: payload.permanent === 'true', + // Should be a boolean + getRaw: payload.getRaw === 'true', + }; + + return { + id, + ...parsedPayload, + }; + } + constructor(client, prefix, setId, options = {}) { super(client, prefix, setId, options); } + _buildPayload({ + callbackURL, + meetingID = null, + eventID = null, + permanent = false, + getRaw = false, + }) { + if (callbackURL == null) throw new Error('callbackURL is required'); + + return { + callbackURL, + externalMeetingID: meetingID, + eventID, + permanent, + getRaw, + } + } + setOptions(options) { this.permanentURLs = options.permanentURLs; } @@ -53,13 +90,13 @@ class HookCompartment extends StorageCompartmentKV { permanent, getRaw, }) { - const payload = { + const payload = this._buildPayload({ callbackURL, externalMeetingID: meetingID, eventID: eventID?.toLowerCase().split(','), permanent, getRaw, - }; + }); let hook = this.findByField('callbackURL', callbackURL); @@ -70,9 +107,7 @@ class HookCompartment extends StorageCompartmentKV { } } - let logMsg = `adding a hook with callback URL: [${callbackURL}],`; - if (meetingID != null) { logMsg += ` for the meeting: [${meetingID}]`; } - this.logger.info(logMsg); + this.logger.info(`adding a hook with callback URL: [${callbackURL}]`, { payload }); const id = permanent ? uuidv5(callbackURL, uuidv5.URL) : uuidv4(); hook = await this.save(payload, { id, @@ -129,7 +164,9 @@ const init = (redisClient, prefix, setId) => { Hooks = new HookCompartment( redisClient, prefix, - setId, + setId, { + deserializer: HookCompartment.itemDeserializer, + }, ); return Hooks.initialize(); } diff --git a/src/out/webhooks/api/api.js b/src/out/webhooks/api/api.js index a7697f5..fbb66ec 100644 --- a/src/out/webhooks/api/api.js +++ b/src/out/webhooks/api/api.js @@ -133,7 +133,7 @@ export default class API { }); if (duplicated) { - API.logger.warn(`duplicated permanent hook ${hook.id}`); + API.logger.info(`permanent hook already set ${hook.id}`, { hook: hook.payload }); } else if (hook != null) { API.logger.info('permanent hook created successfully'); } else { From 9d59b32e8698778e1d0fb123410d153a506e4746 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Wed, 27 Sep 2023 18:48:33 -0300 Subject: [PATCH 016/154] feat: add poll-started/poll-responded events --- src/process/event.js | 48 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/process/event.js b/src/process/event.js index e7d4906..fc9ddae 100644 --- a/src/process/event.js +++ b/src/process/event.js @@ -97,6 +97,10 @@ export default class WebhooksEvent { PAD_EVENTS: [ "PadContentEvtMsg" ], + POLL_EVENTS: [ + "PollStartedEvtMsg", + "UserRespondedToPollRespMsg", + ], } constructor(inputEvent) { @@ -119,6 +123,8 @@ export default class WebhooksEvent { this.compRapTemplate(this.inputEvent); } else if (this.mappedEvent(this.inputEvent, WebhooksEvent.RAW.PAD_EVENTS)) { this.padTemplate(this.inputEvent); + } else if (this.mappedEvent(this.inputEvent, WebhooksEvent.RAW.POLL_EVENTS)) { + this.pollTemplate(this.inputEvent); } else if (this.mappedEvent(this.inputEvent, WebhooksEvent.OUTPUT_EVENTS)) { // Check if input is already a mapped event and return it this.outputEvent = this.inputEvent; @@ -387,6 +393,46 @@ export default class WebhooksEvent { }; } + pollTemplate(messageObj) { + const { + body, + header, + } = messageObj.core; + const extId = UserMapping.get().getExternalUserID(header.userId) || body.extId || ""; + + this.outputEvent = { + data: { + type: "event", + id: this.mapInternalMessage(messageObj), + attributes:{ + meeting:{ + "internal-meeting-id": messageObj.envelope.routing.meetingId, + "external-meeting-id": IDMapping.get().getExternalMeetingID(messageObj.envelope.routing.meetingId) + }, + user:{ + "internal-user-id": header.userId, + "external-user-id": extId, + }, + poll: { + "id": body.pollId + } + }, + event: { + "ts": Date.now() + } + } + }; + + if (this.outputEvent.data.id === "poll-started") { + this.outputEvent.data.attributes.poll = { + question: body.question, + answers: body.poll.answers, + }; + } else if (this.outputEvent.data.id === "poll-responded") { + this.outputEvent.data.attributes.poll.answerIds = body.answerIds; + } + } + mapInternalMessage(message) { const name = message?.envelope?.name || message?.header?.name; @@ -411,6 +457,8 @@ export default class WebhooksEvent { case "UnpublishedRecordingSysMsg": return "rap-unpublished"; case "DeletedRecordingSysMsg": return "rap-deleted"; case "PadContentEvtMsg": return "pad-content"; + case "PollStartedEvtMsg": return "poll-started"; + case "UserRespondedToPollRespMsg": return "poll-responded"; // RAP case "archive_started": return "rap-archive-started"; case "archive_ended": return "rap-archive-ended"; From d3bf4853aa12185fc67dd344bbd468d8de134d80 Mon Sep 17 00:00:00 2001 From: Felipe Cecagno Date: Tue, 4 Apr 2023 11:27:23 -0300 Subject: [PATCH 017/154] chore: increase mapping timeout from 1 day to 1 week --- config/default.example.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/default.example.yml b/config/default.example.yml index c65e146..84f7c69 100644 --- a/config/default.example.yml +++ b/config/default.example.yml @@ -14,7 +14,7 @@ bbb: # Mappings of internal to external meeting IDs mappings: cleanupInterval: 10000 # 10 secs, in ms - timeout: 86400000 # 24 hours, in ms + timeout: 604800000 # 1 week, in ms # Redis redis: From 070f400a3111a9b4d7761b55b72d369b9ab0dd8a Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Wed, 27 Sep 2023 18:50:18 -0300 Subject: [PATCH 018/154] chore: updates to .gitignore and .dockerignore --- .dockerignore | 7 +++++++ .gitignore | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/.dockerignore b/.dockerignore index c2658d7..5eeabec 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1 +1,8 @@ +.git/ node_modules/ +*swn +*swo +*swp +*~ +*log.* +.env diff --git a/.gitignore b/.gitignore index 814f225..cc8e9bb 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,9 @@ node_modules/ log/* config/default.yml +*swn +*swo +*swp +*log.* +.env +*.orig From 0f924aa7b3454b81748310544063aa6397b75078 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Wed, 27 Sep 2023 18:53:09 -0300 Subject: [PATCH 019/154] chore: add MAPPINGS_TIMEOUT env var for mappings.timeout --- config/custom-environment-variables.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/custom-environment-variables.yml b/config/custom-environment-variables.yml index b191581..2976c49 100644 --- a/config/custom-environment-variables.yml +++ b/config/custom-environment-variables.yml @@ -12,6 +12,9 @@ log: filename: LOG_FILE stdout: LOG_STDOUT +mappings: + timeout: MAPPINGS_TIMEOUT + modules: ../db/redis/index.js: config: From 9051d90f52b22dd774415473eb1bb61212e791f3 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Thu, 28 Sep 2023 09:41:55 -0300 Subject: [PATCH 020/154] chore: minor updates to existing messages Porting 445e30c7c113f091364c884f1fe633501c7642a6 into v3 - add parent-id to meeting-created - add userdata to user-joined - add emoji to user-emoji-changed - add id and name to chat-group-message-sent - add recorded and duration to rap-archive-ended - fix start-time, end-time and raw-size on rap-publish-ended --- src/process/event.js | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/process/event.js b/src/process/event.js index fc9ddae..b3f840e 100644 --- a/src/process/event.js +++ b/src/process/event.js @@ -66,6 +66,7 @@ export default class WebhooksEvent { "UserBroadcastCamStartedEvtMsg", "UserBroadcastCamStoppedEvtMsg", "UserEmojiChangedEvtMsg", + "UserReactionEmojiChangedEvtMsg", ], CHAT_EVENTS: [ "GroupChatMessageBroadcastEvtMsg", @@ -189,6 +190,7 @@ export default class WebhooksEvent { "external-meeting-id": props.meetingProp.extId, "name": props.meetingProp.name, "is-breakout": props.meetingProp.isBreakout, + "parent-id": props.breakoutProps.parentId, "duration": props.durationProps.duration, "create-time": props.durationProps.createdTime, "create-date": props.durationProps.createdDate, @@ -233,6 +235,7 @@ export default class WebhooksEvent { "name": msgBody.name, "role": msgBody.role, "presenter": msgBody.presenter, + "userdata": msgBody.userdata, "stream": msgBody.stream } }, @@ -247,6 +250,11 @@ export default class WebhooksEvent { } else if (this.outputEvent.data["id"] === "user-audio-voice-disabled") { this.outputEvent.data["attributes"]["user"]["listening-only"] = false; this.outputEvent.data["attributes"]["user"]["sharing-mic"] = false; + } else if (this.outputEvent.data["id"] === "user-emoji-changed") { + const emoji = msgBody.emoji || msgBody.reactionEmoji; + if (emoji && emoji !== "none") { + this.outputEvent.data["attributes"]["user"]["emoji"] = emoji; + } } } @@ -266,11 +274,11 @@ export default class WebhooksEvent { "external-meeting-id": IDMapping.get().getExternalMeetingID(messageObj.envelope.routing.meetingId) }, "chat-message":{ + "id": body.msg.id, "message": body.msg.message, "sender":{ "internal-user-id": body.msg.sender.id, - "external-user-id": body.msg.sender.name, - "timezone-offset": body.msg.fromTimezoneOffset, + "name": body.msg.sender.name, "time": body.msg.timestamp } }, @@ -331,6 +339,11 @@ export default class WebhooksEvent { this.outputEvent.data.attributes["step-time"] = data.step_time; } + if (this.outputEvent.data.id === "rap-archive-ended") { + this.outputEvent.data.attributes["recorded"] = data.recorded || false; + this.outputEvent.data.attributes["duration"] = data.duration || 0; + } + if (data.workflow) { this.outputEvent.data.attributes.workflow = data.workflow; } @@ -339,10 +352,10 @@ export default class WebhooksEvent { this.outputEvent.data.attributes.recording = { "name": data.metadata.meetingName, "is-breakout": data.metadata.isBreakout, - "start-time": data.startTime, - "end-time": data.endTime, + "start-time": data.start_time, + "end-time": data.end_time, "size": data.playback.size, - "raw-size": data.rawSize, + "raw-size": data.raw_size, "metadata": data.metadata, "playback": data.playback, "download": data.download @@ -451,7 +464,9 @@ export default class WebhooksEvent { case "UserBroadcastCamStoppedEvtMsg": return "user-cam-broadcast-end"; case "PresenterAssignedEvtMsg": return "user-presenter-assigned"; case "PresenterUnassignedEvtMsg": return "user-presenter-unassigned"; - case "UserEmojiChangedEvtMsg": return "user-emoji-changed"; + case "UserEmojiChangedEvtMsg": + case "UserReactionEmojiChangedEvtMsg": + return "user-emoji-changed"; case "GroupChatMessageBroadcastEvtMsg": return "chat-group-message-sent"; case "PublishedRecordingSysMsg": return "rap-published"; case "UnpublishedRecordingSysMsg": return "rap-unpublished"; From 4425a6b9db68470539c77c235454c29d6e4671a7 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Thu, 28 Sep 2023 09:56:41 -0300 Subject: [PATCH 021/154] chore: add example files with output events (mapped and raw --- example/events/mapped-events.json | 16 ++++++++++++++++ example/events/raw-events.json | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 example/events/mapped-events.json create mode 100644 example/events/raw-events.json diff --git a/example/events/mapped-events.json b/example/events/mapped-events.json new file mode 100644 index 0000000..51c08a3 --- /dev/null +++ b/example/events/mapped-events.json @@ -0,0 +1,16 @@ +{"data":{"type":"event","id":"meeting-created","attributes":{"meeting":{"internal-meeting-id":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","external-meeting-id":"random-6425101","name":"random-6425101","is-breakout":false,"parent-id":"bbb-none","duration":0,"create-time":1695905527931,"create-date":"Thu Sep 28 09:52:07 BRT 2023","moderator-pass":"mp","viewer-pass":"ap","record":false,"voice-conf":"76996","dial-number":"613-555-1234","max-users":0,"metadata":{}}},"event":{"ts":1695905527939}}} +{"data":{"type":"event","id":"user-presenter-assigned","attributes":{"meeting":{"internal-meeting-id":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","external-meeting-id":"random-6425101"},"user":{"internal-user-id":"w_zgrip4yqisad","external-user-id":""}},"event":{"ts":1695905530603}}} +{"data":{"type":"event","id":"user-joined","attributes":{"meeting":{"internal-meeting-id":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","external-meeting-id":"random-6425101"},"user":{"internal-user-id":"w_zgrip4yqisad","external-user-id":"w_zgrip4yqisad","name":"User 8259790","role":"MODERATOR","presenter":"false"}},"event":{"ts":1695905530601}}} +{"data":{"type":"event","id":"user-presenter-assigned","attributes":{"meeting":{"internal-meeting-id":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","external-meeting-id":"random-6425101"},"user":{"internal-user-id":"w_zgrip4yqisad","external-user-id":"w_zgrip4yqisad"}},"event":{"ts":1695905530623}}} +{"data":{"type":"event","id":"user-presenter-assigned","attributes":{"meeting":{"internal-meeting-id":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","external-meeting-id":"random-6425101"},"user":{"internal-user-id":"w_zgrip4yqisad","external-user-id":"w_zgrip4yqisad"}},"event":{"ts":1695905530636}}} +{"data":{"type":"event","id":"user-joined","attributes":{"meeting":{"internal-meeting-id":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","external-meeting-id":"random-6425101"},"user":{"internal-user-id":"w_oyvspjajpxt3","external-user-id":"w_oyvspjajpxt3","name":"User 8259790","role":"VIEWER","presenter":"false"}},"event":{"ts":1695905532777}}} +{"data":{"type":"event","id":"user-audio-voice-enabled","attributes":{"meeting":{"internal-meeting-id":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","external-meeting-id":"random-6425101"},"user":{"internal-user-id":"w_zgrip4yqisad","external-user-id":"w_zgrip4yqisad","listening-only":false,"sharing-mic":true}},"event":{"ts":1695905536376}}} +{"data":{"type":"event","id":"user-audio-voice-disabled","attributes":{"meeting":{"internal-meeting-id":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","external-meeting-id":"random-6425101"},"user":{"internal-user-id":"w_zgrip4yqisad","external-user-id":"w_zgrip4yqisad","listening-only":false,"sharing-mic":false}},"event":{"ts":1695905538277}}} +{"data":{"type":"event","id":"user-cam-broadcast-start","attributes":{"meeting":{"internal-meeting-id":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","external-meeting-id":"random-6425101"},"user":{"internal-user-id":"w_zgrip4yqisad","external-user-id":"w_zgrip4yqisad","stream":"w_zgrip4yqisad_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}},"event":{"ts":1695905540891}}} +{"data":{"type":"event","id":"user-cam-broadcast-end","attributes":{"meeting":{"internal-meeting-id":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","external-meeting-id":"random-6425101"},"user":{"internal-user-id":"w_zgrip4yqisad","external-user-id":"w_zgrip4yqisad","stream":"w_zgrip4yqisad_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}},"event":{"ts":1695905542911}}} +{"data":{"type":"event","id":"meeting-screenshare-started","attributes":{"meeting":{"internal-meeting-id":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","external-meeting-id":"random-6425101"}},"event":{"ts":1695905546799}}} +{"data":{"type":"event","id":"meeting-screenshare-stopped","attributes":{"meeting":{"internal-meeting-id":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","external-meeting-id":"random-6425101"}},"event":{"ts":1695905549651}}} +{"data":{"type":"event","id":"chat-group-message-sent","attributes":{"meeting":{"internal-meeting-id":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","external-meeting-id":"random-6425101"},"chat-message":{"id":"1695905554378-70x5gmn9","message":"Public chat test","sender":{"internal-user-id":"w_zgrip4yqisad","name":"User 8259790","time":1695905554378}},"chat-id":"MAIN-PUBLIC-GROUP-CHAT"},"event":{"ts":1695905554381}}} +{"data":{"type":"event","id":"user-emoji-changed","attributes":{"meeting":{"internal-meeting-id":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","external-meeting-id":"random-6425101"},"user":{"internal-user-id":"w_zgrip4yqisad","external-user-id":"w_zgrip4yqisad","emoji":"🙁"}},"event":{"ts":1695905560067}}} +{"data":{"type":"event","id":"poll-started","attributes":{"meeting":{"internal-meeting-id":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","external-meeting-id":"random-6425101"},"user":{"internal-user-id":"w_zgrip4yqisad","external-user-id":"w_zgrip4yqisad"},"poll":{"question":"ABCD poll test (public), no custom input","answers":[{"id":0,"key":"A"},{"id":1,"key":"B"},{"id":2,"key":"C"},{"id":3,"key":"D"}]}},"event":{"ts":1695905577268}}} +{"data":{"type":"event","id":"poll-responded","attributes":{"meeting":{"internal-meeting-id":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","external-meeting-id":"random-6425101"},"user":{"internal-user-id":"w_zgrip4yqisad","external-user-id":"w_zgrip4yqisad"},"poll":{"id":"37d6334834d5f897b842bd70081cc4caff3a59be-1695905527932/1/1695905577266","answerIds":[0]}},"event":{"ts":1695905580744}}} diff --git a/example/events/raw-events.json b/example/events/raw-events.json new file mode 100644 index 0000000..88223b6 --- /dev/null +++ b/example/events/raw-events.json @@ -0,0 +1,16 @@ +{"envelope":{"name":"MeetingCreatedEvtMsg","routing":{"sender":"bbb-apps-akka"},"timestamp":1695905527936},"core":{"header":{"name":"MeetingCreatedEvtMsg"},"body":{"props":{"meetingProp":{"name":"random-6425101","extId":"random-6425101","intId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","meetingCameraCap":50,"maxPinnedCameras":3,"isBreakout":false,"disabledFeatures":[],"notifyRecordingIsOn":false,"presentationUploadExternalDescription":"","presentationUploadExternalUrl":""},"breakoutProps":{"parentId":"bbb-none","sequence":0,"freeJoin":false,"breakoutRooms":[],"record":false,"privateChatEnabled":true,"captureNotes":false,"captureSlides":false,"captureNotesFilename":"%%CONFNAME%%","captureSlidesFilename":"%%CONFNAME%%"},"durationProps":{"duration":0,"createdTime":1695905527931,"createdDate":"Thu Sep 28 09:52:07 BRT 2023","meetingExpireIfNoUserJoinedInMinutes":5,"meetingExpireWhenLastUserLeftInMinutes":1,"userInactivityInspectTimerInMinutes":0,"userInactivityThresholdInMinutes":30,"userActivitySignResponseDelayInMinutes":5,"endWhenNoModerator":false,"endWhenNoModeratorDelayInMinutes":1},"password":{"moderatorPass":"mp","viewerPass":"ap","learningDashboardAccessToken":"kn0vzjab3orw"},"recordProp":{"record":false,"autoStartRecording":false,"allowStartStopRecording":true,"recordFullDurationMedia":false,"keepEvents":true},"welcomeProp":{"welcomeMsgTemplate":"
Welcome to %%CONFNAME%%!","welcomeMsg":"
Welcome to random-6425101!","modOnlyMessage":""},"voiceProp":{"telVoice":"76996","voiceConf":"76996","dialNumber":"613-555-1234","muteOnStart":false},"usersProp":{"maxUsers":0,"maxUserConcurrentAccesses":3,"webcamsOnlyForModerator":false,"userCameraCap":3,"guestPolicy":"ASK_MODERATOR","meetingLayout":"CUSTOM_LAYOUT","allowModsToUnmuteUsers":true,"allowModsToEjectCameras":true,"authenticatedGuest":false},"metadataProp":{"metadata":{}},"lockSettingsProps":{"disableCam":false,"disableMic":false,"disablePrivateChat":false,"disablePublicChat":false,"disableNotes":false,"hideUserList":false,"lockOnJoin":true,"lockOnJoinConfigurable":false,"hideViewersCursor":false,"hideViewersAnnotation":false},"systemProps":{"html5InstanceId":1},"groups":[]}}}} +{"envelope":{"name":"PresenterAssignedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"timestamp":1695905530596},"core":{"header":{"name":"PresenterAssignedEvtMsg","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"body":{"presenterId":"w_zgrip4yqisad","presenterName":"User 8259790","assignedBy":"w_zgrip4yqisad"}}} +{"envelope":{"name":"UserJoinedMeetingEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"timestamp":1695905530595},"core":{"header":{"name":"UserJoinedMeetingEvtMsg","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"body":{"intId":"w_zgrip4yqisad","extId":"w_zgrip4yqisad","name":"User 8259790","role":"MODERATOR","guest":false,"authed":true,"guestStatus":"ALLOW","emoji":"none","reactionEmoji":"none","raiseHand":false,"away":false,"pin":false,"presenter":false,"locked":true,"avatar":"","color":"#7b1fa2","clientType":"HTML5"}}} +{"envelope":{"name":"PresenterAssignedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"timestamp":1695905530620},"core":{"header":{"name":"PresenterAssignedEvtMsg","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"body":{"presenterId":"w_zgrip4yqisad","presenterName":"User 8259790","assignedBy":"w_zgrip4yqisad"}}} +{"envelope":{"name":"PresenterAssignedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"timestamp":1695905530632},"core":{"header":{"name":"PresenterAssignedEvtMsg","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"body":{"presenterId":"w_zgrip4yqisad","presenterName":"User 8259790","assignedBy":"w_zgrip4yqisad"}}} +{"envelope":{"name":"UserJoinedMeetingEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_oyvspjajpxt3"},"timestamp":1695905532773},"core":{"header":{"name":"UserJoinedMeetingEvtMsg","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_oyvspjajpxt3"},"body":{"intId":"w_oyvspjajpxt3","extId":"w_oyvspjajpxt3","name":"User 8259790","role":"VIEWER","guest":false,"authed":true,"guestStatus":"ALLOW","emoji":"none","reactionEmoji":"none","raiseHand":false,"away":false,"pin":false,"presenter":false,"locked":true,"avatar":"","color":"#6a1b9a","clientType":"HTML5"}}} +{"envelope":{"name":"UserJoinedVoiceConfToClientEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"timestamp":1695905536374},"core":{"header":{"name":"UserJoinedVoiceConfToClientEvtMsg","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"body":{"voiceConf":"76996","intId":"w_zgrip4yqisad","voiceUserId":"2","callerName":"User+8259790","callerNum":"w_zgrip4yqisad_1-bbbID-User+8259790","color":"#4a148c","muted":false,"talking":false,"callingWith":"none","listenOnly":false}}} +{"envelope":{"name":"UserLeftVoiceConfToClientEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"timestamp":1695905538274},"core":{"header":{"name":"UserLeftVoiceConfToClientEvtMsg","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"body":{"voiceConf":"76996","intId":"w_zgrip4yqisad","voiceUserId":"w_zgrip4yqisad"}}} +{"envelope":{"name":"UserBroadcastCamStartedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"timestamp":1695905540890},"core":{"header":{"name":"UserBroadcastCamStartedEvtMsg","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"body":{"userId":"w_zgrip4yqisad","stream":"w_zgrip4yqisad_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}}} +{"envelope":{"name":"UserBroadcastCamStoppedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"timestamp":1695905542909},"core":{"header":{"name":"UserBroadcastCamStoppedEvtMsg","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"body":{"userId":"w_zgrip4yqisad","stream":"w_zgrip4yqisad_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}}} +{"envelope":{"name":"ScreenshareRtmpBroadcastStartedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"not-used"},"timestamp":1695905546798},"core":{"header":{"name":"ScreenshareRtmpBroadcastStartedEvtMsg","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"not-used"},"body":{"voiceConf":"76996","screenshareConf":"76996","stream":"2f4d8e8b-1db5-4477-92f4-515b965c20d3","vidWidth":0,"vidHeight":0,"timestamp":"1695905546796","hasAudio":false,"contentType":"screenshare"}}} +{"envelope":{"name":"ScreenshareRtmpBroadcastStoppedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"not-used"},"timestamp":1695905549650},"core":{"header":{"name":"ScreenshareRtmpBroadcastStoppedEvtMsg","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"not-used"},"body":{"voiceConf":"","screenshareConf":"","stream":"","vidWidth":0,"vidHeight":0,"timestamp":""}}} +{"envelope":{"name":"GroupChatMessageBroadcastEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"timestamp":1695905554379},"core":{"header":{"name":"GroupChatMessageBroadcastEvtMsg","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"body":{"chatId":"MAIN-PUBLIC-GROUP-CHAT","msg":{"id":"1695905554378-70x5gmn9","timestamp":1695905554378,"correlationId":"w_zgrip4yqisad-1695905554362","sender":{"id":"w_zgrip4yqisad","name":"User 8259790","role":"MODERATOR"},"chatEmphasizedText":true,"message":"Public chat test"}}}} +{"envelope":{"name":"UserReactionEmojiChangedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"timestamp":1695905560065},"core":{"header":{"name":"UserReactionEmojiChangedEvtMsg","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"body":{"userId":"w_zgrip4yqisad","reactionEmoji":"🙁"}}} +{"envelope":{"name":"PollStartedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"timestamp":1695905577266},"core":{"header":{"name":"PollStartedEvtMsg","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"body":{"userId":"w_zgrip4yqisad","pollId":"37d6334834d5f897b842bd70081cc4caff3a59be-1695905527932/1/1695905577266","pollType":"A-4","secretPoll":false,"question":"ABCD poll test (public), no custom input","poll":{"id":"37d6334834d5f897b842bd70081cc4caff3a59be-1695905527932/1/1695905577266","isMultipleResponse":false,"answers":[{"id":0,"key":"A"},{"id":1,"key":"B"},{"id":2,"key":"C"},{"id":3,"key":"D"}]}}}} +{"envelope":{"name":"UserRespondedToPollRespMsg","routing":{"msgType":"DIRECT","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"timestamp":1695905580723},"core":{"header":{"name":"UserRespondedToPollRespMsg","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"body":{"pollId":"37d6334834d5f897b842bd70081cc4caff3a59be-1695905527932/1/1695905577266","userId":"w_oyvspjajpxt3","answerIds":[0]}}} From c55044fa42512d51c5fc10ceb6dbe7f833435fb1 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Thu, 28 Sep 2023 10:45:06 -0300 Subject: [PATCH 022/154] chore: allow writing raw events to file in out-file example --- example/modules/out-file.js | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/example/modules/out-file.js b/example/modules/out-file.js index ba7d9ef..03f6649 100644 --- a/example/modules/out-file.js +++ b/example/modules/out-file.js @@ -59,19 +59,6 @@ export default class OutFile { return true; } - async _writeEvent() { - if (this._fileHandle == null) { - this._fileHandle = await open(this.config.fileName, 'r'); - } - - const lines = this._fileHandle.readLines(); - - for await (const line of lines) { - await timeout(this.config.delay); - this._dispatch(line); - } - } - _dispatch (event) { this.logger.debug(`read event from file: ${event}`); @@ -87,6 +74,9 @@ export default class OutFile { try { this._validateConfig(); this._fileHandle = await open(this.config.fileName, 'a'); + if (this.config.rawFileName != null) { + this._rawFileHandle = await open(this.config.rawFileName, 'a'); + } this.loaded = true; } catch (error) { this.logger.error('error loading OutFile:', error); @@ -125,5 +115,11 @@ export default class OutFile { : JSON.stringify(event); await this._fileHandle.appendFile(writableMessage + "\n"); + if (this._rawFileHandle != null) { + const writableRaw = this.config.prettyPrint + ? JSON.stringify(raw, null, 2) + : JSON.stringify(raw); + await this._rawFileHandle.appendFile(writableRaw + "\n"); + } } } From 27e5f996e36c0cdd8559ee6601cc5da7eed97e95 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Thu, 28 Sep 2023 12:04:43 -0300 Subject: [PATCH 023/154] fix: load output modules before input --- src/modules/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/modules/index.js b/src/modules/index.js index 508ca44..b0b7bf9 100644 --- a/src/modules/index.js +++ b/src/modules/index.js @@ -70,7 +70,7 @@ export default class ModuleManager { } _sortModulesByPriority(a, b) { - // Sort modules by priority: db modules first, then input modules, then output modules + // Sort modules by priority: db modules first, then output modules, then input modules const aD = a[1] const bD = b[1] @@ -78,9 +78,9 @@ export default class ModuleManager { return -1; } else if (aD.type !== ModuleManager.moduleTypes.db && bD.type === ModuleManager.moduleTypes.db) { return 1; - } else if (aD.type === ModuleManager.moduleTypes.in && bD.type === ModuleManager.moduleTypes.out) { + } else if (aD.type === ModuleManager.moduleTypes.out && bD.type !== ModuleManager.moduleTypes.out) { return -1; - } else if (aD.type === ModuleManager.moduleTypes.out && bD.type === ModuleManager.moduleTypes.in) { + } else if (aD.type !== ModuleManager.moduleTypes.out && bD.type === ModuleManager.moduleTypes.out) { return 1; } else { return 0; From 5b7f6de9f69fd0e05009088223fb0b2b9cf2ab64 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Thu, 28 Sep 2023 15:27:11 -0300 Subject: [PATCH 024/154] fix: dispatch user-left/meeting-ended events --- src/db/redis/base-storage.js | 6 ++++-- src/process/event-processor.js | 28 ++++++++++++++++++++-------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/db/redis/base-storage.js b/src/db/redis/base-storage.js index 5a574c8..00d9a37 100644 --- a/src/db/redis/base-storage.js +++ b/src/db/redis/base-storage.js @@ -177,7 +177,9 @@ class StorageCompartmentKV { async destroyWithField(field, value) { return Promise.all( - Object.keys(this.localStorage).map(internal => { + Object.keys(this.localStorage).filter(internal => { + return this.localStorage[internal] && this.localStorage[internal]?.payload[field] === value; + }).map(internal => { let mapping = this.localStorage[internal]; if (mapping.payload[field] === value) { return mapping.destroy() @@ -196,7 +198,7 @@ class StorageCompartmentKV { } }); } else { - return false; + return Promise.resolve(false); } }) ); diff --git a/src/process/event-processor.js b/src/process/event-processor.js index 870d5ed..32812ef 100644 --- a/src/process/event-processor.js +++ b/src/process/event-processor.js @@ -49,16 +49,16 @@ export default class EventProcessor { if (!Utils.isEmpty(outputEvent)) { Logger.debug('raw event succesfully parsed', { rawEvent }); - const intId = outputEvent.data.attributes.meeting["internal-meeting-id"]; - IDMapping.get().reportActivity(intId); + const internalMeetingId = outputEvent.data.attributes.meeting["internal-meeting-id"]; + IDMapping.get().reportActivity(internalMeetingId); // First treat meeting events to add/remove ID mappings switch (outputEvent.data.id) { case "meeting-created": - IDMapping.get().addOrUpdateMapping(intId, + IDMapping.get().addOrUpdateMapping(internalMeetingId, outputEvent.data.attributes.meeting["external-meeting-id"] ).catch((error) => { - Logger.error(`error adding mapping: ${error}`, { + Logger.error(`error adding meeting mapping: ${error}`, { error: error.stack, event, }); @@ -72,10 +72,10 @@ export default class EventProcessor { UserMapping.get().addOrUpdateMapping( outputEvent.data.attributes.user["internal-user-id"], outputEvent.data.attributes.user["external-user-id"], - intId, + internalMeetingId, outputEvent.data.attributes.user ).catch((error) => { - Logger.error(`error adding mapping: ${error}`, { + Logger.error(`error adding user mapping: ${error}`, { error: error.stack, event, }) @@ -84,12 +84,24 @@ export default class EventProcessor { }); break; case "user-left": - UserMapping.get().removeMapping(outputEvent.data.attributes.user["internal-user-id"], () => { + UserMapping.get().removeMapping( + outputEvent.data.attributes.user["internal-user-id"] + ).catch((error) => { + Logger.error(`error removing user mapping: ${error}`, { + error: error.stack, + event, + }); + }).finally(() => { this._notifyOutputModules(outputEvent, rawEvent); }); break; case "meeting-ended": - IDMapping.get().removeMapping(intId, () => { + IDMapping.get().removeMapping(internalMeetingId).catch((error) => { + Logger.error(`error removing meeting mapping: ${error}`, { + error: error.stack, + event, + }); + }).finally(() => { this._notifyOutputModules(outputEvent, rawEvent); }); break; From 653fc383d446ba0561749d6256c986cbf7439bba Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Thu, 28 Sep 2023 15:29:18 -0300 Subject: [PATCH 025/154] chore: update event examples with meeting-ended/user-left --- example/events/mapped-events.json | 34 ++++++++++++++++--------------- example/events/raw-events.json | 34 ++++++++++++++++--------------- 2 files changed, 36 insertions(+), 32 deletions(-) diff --git a/example/events/mapped-events.json b/example/events/mapped-events.json index 51c08a3..9be7e0c 100644 --- a/example/events/mapped-events.json +++ b/example/events/mapped-events.json @@ -1,16 +1,18 @@ -{"data":{"type":"event","id":"meeting-created","attributes":{"meeting":{"internal-meeting-id":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","external-meeting-id":"random-6425101","name":"random-6425101","is-breakout":false,"parent-id":"bbb-none","duration":0,"create-time":1695905527931,"create-date":"Thu Sep 28 09:52:07 BRT 2023","moderator-pass":"mp","viewer-pass":"ap","record":false,"voice-conf":"76996","dial-number":"613-555-1234","max-users":0,"metadata":{}}},"event":{"ts":1695905527939}}} -{"data":{"type":"event","id":"user-presenter-assigned","attributes":{"meeting":{"internal-meeting-id":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","external-meeting-id":"random-6425101"},"user":{"internal-user-id":"w_zgrip4yqisad","external-user-id":""}},"event":{"ts":1695905530603}}} -{"data":{"type":"event","id":"user-joined","attributes":{"meeting":{"internal-meeting-id":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","external-meeting-id":"random-6425101"},"user":{"internal-user-id":"w_zgrip4yqisad","external-user-id":"w_zgrip4yqisad","name":"User 8259790","role":"MODERATOR","presenter":"false"}},"event":{"ts":1695905530601}}} -{"data":{"type":"event","id":"user-presenter-assigned","attributes":{"meeting":{"internal-meeting-id":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","external-meeting-id":"random-6425101"},"user":{"internal-user-id":"w_zgrip4yqisad","external-user-id":"w_zgrip4yqisad"}},"event":{"ts":1695905530623}}} -{"data":{"type":"event","id":"user-presenter-assigned","attributes":{"meeting":{"internal-meeting-id":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","external-meeting-id":"random-6425101"},"user":{"internal-user-id":"w_zgrip4yqisad","external-user-id":"w_zgrip4yqisad"}},"event":{"ts":1695905530636}}} -{"data":{"type":"event","id":"user-joined","attributes":{"meeting":{"internal-meeting-id":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","external-meeting-id":"random-6425101"},"user":{"internal-user-id":"w_oyvspjajpxt3","external-user-id":"w_oyvspjajpxt3","name":"User 8259790","role":"VIEWER","presenter":"false"}},"event":{"ts":1695905532777}}} -{"data":{"type":"event","id":"user-audio-voice-enabled","attributes":{"meeting":{"internal-meeting-id":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","external-meeting-id":"random-6425101"},"user":{"internal-user-id":"w_zgrip4yqisad","external-user-id":"w_zgrip4yqisad","listening-only":false,"sharing-mic":true}},"event":{"ts":1695905536376}}} -{"data":{"type":"event","id":"user-audio-voice-disabled","attributes":{"meeting":{"internal-meeting-id":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","external-meeting-id":"random-6425101"},"user":{"internal-user-id":"w_zgrip4yqisad","external-user-id":"w_zgrip4yqisad","listening-only":false,"sharing-mic":false}},"event":{"ts":1695905538277}}} -{"data":{"type":"event","id":"user-cam-broadcast-start","attributes":{"meeting":{"internal-meeting-id":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","external-meeting-id":"random-6425101"},"user":{"internal-user-id":"w_zgrip4yqisad","external-user-id":"w_zgrip4yqisad","stream":"w_zgrip4yqisad_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}},"event":{"ts":1695905540891}}} -{"data":{"type":"event","id":"user-cam-broadcast-end","attributes":{"meeting":{"internal-meeting-id":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","external-meeting-id":"random-6425101"},"user":{"internal-user-id":"w_zgrip4yqisad","external-user-id":"w_zgrip4yqisad","stream":"w_zgrip4yqisad_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}},"event":{"ts":1695905542911}}} -{"data":{"type":"event","id":"meeting-screenshare-started","attributes":{"meeting":{"internal-meeting-id":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","external-meeting-id":"random-6425101"}},"event":{"ts":1695905546799}}} -{"data":{"type":"event","id":"meeting-screenshare-stopped","attributes":{"meeting":{"internal-meeting-id":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","external-meeting-id":"random-6425101"}},"event":{"ts":1695905549651}}} -{"data":{"type":"event","id":"chat-group-message-sent","attributes":{"meeting":{"internal-meeting-id":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","external-meeting-id":"random-6425101"},"chat-message":{"id":"1695905554378-70x5gmn9","message":"Public chat test","sender":{"internal-user-id":"w_zgrip4yqisad","name":"User 8259790","time":1695905554378}},"chat-id":"MAIN-PUBLIC-GROUP-CHAT"},"event":{"ts":1695905554381}}} -{"data":{"type":"event","id":"user-emoji-changed","attributes":{"meeting":{"internal-meeting-id":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","external-meeting-id":"random-6425101"},"user":{"internal-user-id":"w_zgrip4yqisad","external-user-id":"w_zgrip4yqisad","emoji":"🙁"}},"event":{"ts":1695905560067}}} -{"data":{"type":"event","id":"poll-started","attributes":{"meeting":{"internal-meeting-id":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","external-meeting-id":"random-6425101"},"user":{"internal-user-id":"w_zgrip4yqisad","external-user-id":"w_zgrip4yqisad"},"poll":{"question":"ABCD poll test (public), no custom input","answers":[{"id":0,"key":"A"},{"id":1,"key":"B"},{"id":2,"key":"C"},{"id":3,"key":"D"}]}},"event":{"ts":1695905577268}}} -{"data":{"type":"event","id":"poll-responded","attributes":{"meeting":{"internal-meeting-id":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","external-meeting-id":"random-6425101"},"user":{"internal-user-id":"w_zgrip4yqisad","external-user-id":"w_zgrip4yqisad"},"poll":{"id":"37d6334834d5f897b842bd70081cc4caff3a59be-1695905527932/1/1695905577266","answerIds":[0]}},"event":{"ts":1695905580744}}} +{"data":{"type":"event","id":"meeting-created","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","external-meeting-id":"random-9301511","name":"random-9301511","is-breakout":false,"parent-id":"bbb-none","duration":0,"create-time":1695925641462,"create-date":"Thu Sep 28 15:27:21 BRT 2023","moderator-pass":"mp","viewer-pass":"ap","record":false,"voice-conf":"74692","dial-number":"613-555-1234","max-users":0,"metadata":{}}},"event":{"ts":1695925641468}}} +{"data":{"type":"event","id":"user-presenter-assigned","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_hc16wzmqcevt","external-user-id":""}},"event":{"ts":1695925645387}}} +{"data":{"type":"event","id":"user-joined","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_hc16wzmqcevt","external-user-id":"w_hc16wzmqcevt","name":"User 9165940","role":"MODERATOR","presenter":"false"}},"event":{"ts":1695925645383}}} +{"data":{"type":"event","id":"user-presenter-assigned","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_hc16wzmqcevt","external-user-id":"w_hc16wzmqcevt"}},"event":{"ts":1695925645402}}} +{"data":{"type":"event","id":"user-presenter-assigned","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_hc16wzmqcevt","external-user-id":"w_hc16wzmqcevt"}},"event":{"ts":1695925645408}}} +{"data":{"type":"event","id":"user-joined","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_s3vx2orgmloh","external-user-id":"w_s3vx2orgmloh","name":"User 9165940","role":"VIEWER","presenter":"false"}},"event":{"ts":1695925647058}}} +{"data":{"type":"event","id":"user-audio-voice-enabled","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_hc16wzmqcevt","external-user-id":"w_hc16wzmqcevt","listening-only":false,"sharing-mic":true}},"event":{"ts":1695925651878}}} +{"data":{"type":"event","id":"user-audio-voice-disabled","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_hc16wzmqcevt","external-user-id":"w_hc16wzmqcevt","listening-only":false,"sharing-mic":false}},"event":{"ts":1695925655433}}} +{"data":{"type":"event","id":"user-cam-broadcast-start","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_hc16wzmqcevt","external-user-id":"w_hc16wzmqcevt","stream":"w_hc16wzmqcevt_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}},"event":{"ts":1695925659251}}} +{"data":{"type":"event","id":"user-cam-broadcast-end","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_hc16wzmqcevt","external-user-id":"w_hc16wzmqcevt","stream":"w_hc16wzmqcevt_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}},"event":{"ts":1695925660679}}} +{"data":{"type":"event","id":"meeting-screenshare-started","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","external-meeting-id":"random-9301511"}},"event":{"ts":1695925664606}}} +{"data":{"type":"event","id":"meeting-screenshare-stopped","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","external-meeting-id":"random-9301511"}},"event":{"ts":1695925666666}}} +{"data":{"type":"event","id":"chat-group-message-sent","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","external-meeting-id":"random-9301511"},"chat-message":{"id":"1695925670472-plwf615o","message":"Public chat test","sender":{"internal-user-id":"w_hc16wzmqcevt","name":"User 9165940","time":1695925670472}},"chat-id":"MAIN-PUBLIC-GROUP-CHAT"},"event":{"ts":1695925670474}}} +{"data":{"type":"event","id":"user-emoji-changed","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_hc16wzmqcevt","external-user-id":"w_hc16wzmqcevt","emoji":"🙁"}},"event":{"ts":1695925675998}}} +{"data":{"type":"event","id":"poll-started","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_hc16wzmqcevt","external-user-id":"w_hc16wzmqcevt"},"poll":{"question":"ABCD Poll test (public)","answers":[{"id":0,"key":"A"},{"id":1,"key":"B"},{"id":2,"key":"C"},{"id":3,"key":"D"}]}},"event":{"ts":1695925693465}}} +{"data":{"type":"event","id":"poll-responded","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_hc16wzmqcevt","external-user-id":"w_hc16wzmqcevt"},"poll":{"id":"cbabaab6246e315678ba0a357009337966843a60-1695925641463/1/1695925693463","answerIds":[0]}},"event":{"ts":1695925696673}}} +{"data":{"type":"event","id":"user-left","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_s3vx2orgmloh","external-user-id":"w_s3vx2orgmloh"}},"event":{"ts":1695925716477}}} +{"data":{"type":"event","id":"meeting-ended","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","external-meeting-id":"random-9301511"}},"event":{"ts":1695925722658}}} diff --git a/example/events/raw-events.json b/example/events/raw-events.json index 88223b6..e7b74db 100644 --- a/example/events/raw-events.json +++ b/example/events/raw-events.json @@ -1,16 +1,18 @@ -{"envelope":{"name":"MeetingCreatedEvtMsg","routing":{"sender":"bbb-apps-akka"},"timestamp":1695905527936},"core":{"header":{"name":"MeetingCreatedEvtMsg"},"body":{"props":{"meetingProp":{"name":"random-6425101","extId":"random-6425101","intId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","meetingCameraCap":50,"maxPinnedCameras":3,"isBreakout":false,"disabledFeatures":[],"notifyRecordingIsOn":false,"presentationUploadExternalDescription":"","presentationUploadExternalUrl":""},"breakoutProps":{"parentId":"bbb-none","sequence":0,"freeJoin":false,"breakoutRooms":[],"record":false,"privateChatEnabled":true,"captureNotes":false,"captureSlides":false,"captureNotesFilename":"%%CONFNAME%%","captureSlidesFilename":"%%CONFNAME%%"},"durationProps":{"duration":0,"createdTime":1695905527931,"createdDate":"Thu Sep 28 09:52:07 BRT 2023","meetingExpireIfNoUserJoinedInMinutes":5,"meetingExpireWhenLastUserLeftInMinutes":1,"userInactivityInspectTimerInMinutes":0,"userInactivityThresholdInMinutes":30,"userActivitySignResponseDelayInMinutes":5,"endWhenNoModerator":false,"endWhenNoModeratorDelayInMinutes":1},"password":{"moderatorPass":"mp","viewerPass":"ap","learningDashboardAccessToken":"kn0vzjab3orw"},"recordProp":{"record":false,"autoStartRecording":false,"allowStartStopRecording":true,"recordFullDurationMedia":false,"keepEvents":true},"welcomeProp":{"welcomeMsgTemplate":"
Welcome to %%CONFNAME%%!","welcomeMsg":"
Welcome to random-6425101!","modOnlyMessage":""},"voiceProp":{"telVoice":"76996","voiceConf":"76996","dialNumber":"613-555-1234","muteOnStart":false},"usersProp":{"maxUsers":0,"maxUserConcurrentAccesses":3,"webcamsOnlyForModerator":false,"userCameraCap":3,"guestPolicy":"ASK_MODERATOR","meetingLayout":"CUSTOM_LAYOUT","allowModsToUnmuteUsers":true,"allowModsToEjectCameras":true,"authenticatedGuest":false},"metadataProp":{"metadata":{}},"lockSettingsProps":{"disableCam":false,"disableMic":false,"disablePrivateChat":false,"disablePublicChat":false,"disableNotes":false,"hideUserList":false,"lockOnJoin":true,"lockOnJoinConfigurable":false,"hideViewersCursor":false,"hideViewersAnnotation":false},"systemProps":{"html5InstanceId":1},"groups":[]}}}} -{"envelope":{"name":"PresenterAssignedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"timestamp":1695905530596},"core":{"header":{"name":"PresenterAssignedEvtMsg","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"body":{"presenterId":"w_zgrip4yqisad","presenterName":"User 8259790","assignedBy":"w_zgrip4yqisad"}}} -{"envelope":{"name":"UserJoinedMeetingEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"timestamp":1695905530595},"core":{"header":{"name":"UserJoinedMeetingEvtMsg","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"body":{"intId":"w_zgrip4yqisad","extId":"w_zgrip4yqisad","name":"User 8259790","role":"MODERATOR","guest":false,"authed":true,"guestStatus":"ALLOW","emoji":"none","reactionEmoji":"none","raiseHand":false,"away":false,"pin":false,"presenter":false,"locked":true,"avatar":"","color":"#7b1fa2","clientType":"HTML5"}}} -{"envelope":{"name":"PresenterAssignedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"timestamp":1695905530620},"core":{"header":{"name":"PresenterAssignedEvtMsg","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"body":{"presenterId":"w_zgrip4yqisad","presenterName":"User 8259790","assignedBy":"w_zgrip4yqisad"}}} -{"envelope":{"name":"PresenterAssignedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"timestamp":1695905530632},"core":{"header":{"name":"PresenterAssignedEvtMsg","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"body":{"presenterId":"w_zgrip4yqisad","presenterName":"User 8259790","assignedBy":"w_zgrip4yqisad"}}} -{"envelope":{"name":"UserJoinedMeetingEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_oyvspjajpxt3"},"timestamp":1695905532773},"core":{"header":{"name":"UserJoinedMeetingEvtMsg","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_oyvspjajpxt3"},"body":{"intId":"w_oyvspjajpxt3","extId":"w_oyvspjajpxt3","name":"User 8259790","role":"VIEWER","guest":false,"authed":true,"guestStatus":"ALLOW","emoji":"none","reactionEmoji":"none","raiseHand":false,"away":false,"pin":false,"presenter":false,"locked":true,"avatar":"","color":"#6a1b9a","clientType":"HTML5"}}} -{"envelope":{"name":"UserJoinedVoiceConfToClientEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"timestamp":1695905536374},"core":{"header":{"name":"UserJoinedVoiceConfToClientEvtMsg","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"body":{"voiceConf":"76996","intId":"w_zgrip4yqisad","voiceUserId":"2","callerName":"User+8259790","callerNum":"w_zgrip4yqisad_1-bbbID-User+8259790","color":"#4a148c","muted":false,"talking":false,"callingWith":"none","listenOnly":false}}} -{"envelope":{"name":"UserLeftVoiceConfToClientEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"timestamp":1695905538274},"core":{"header":{"name":"UserLeftVoiceConfToClientEvtMsg","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"body":{"voiceConf":"76996","intId":"w_zgrip4yqisad","voiceUserId":"w_zgrip4yqisad"}}} -{"envelope":{"name":"UserBroadcastCamStartedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"timestamp":1695905540890},"core":{"header":{"name":"UserBroadcastCamStartedEvtMsg","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"body":{"userId":"w_zgrip4yqisad","stream":"w_zgrip4yqisad_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}}} -{"envelope":{"name":"UserBroadcastCamStoppedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"timestamp":1695905542909},"core":{"header":{"name":"UserBroadcastCamStoppedEvtMsg","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"body":{"userId":"w_zgrip4yqisad","stream":"w_zgrip4yqisad_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}}} -{"envelope":{"name":"ScreenshareRtmpBroadcastStartedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"not-used"},"timestamp":1695905546798},"core":{"header":{"name":"ScreenshareRtmpBroadcastStartedEvtMsg","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"not-used"},"body":{"voiceConf":"76996","screenshareConf":"76996","stream":"2f4d8e8b-1db5-4477-92f4-515b965c20d3","vidWidth":0,"vidHeight":0,"timestamp":"1695905546796","hasAudio":false,"contentType":"screenshare"}}} -{"envelope":{"name":"ScreenshareRtmpBroadcastStoppedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"not-used"},"timestamp":1695905549650},"core":{"header":{"name":"ScreenshareRtmpBroadcastStoppedEvtMsg","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"not-used"},"body":{"voiceConf":"","screenshareConf":"","stream":"","vidWidth":0,"vidHeight":0,"timestamp":""}}} -{"envelope":{"name":"GroupChatMessageBroadcastEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"timestamp":1695905554379},"core":{"header":{"name":"GroupChatMessageBroadcastEvtMsg","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"body":{"chatId":"MAIN-PUBLIC-GROUP-CHAT","msg":{"id":"1695905554378-70x5gmn9","timestamp":1695905554378,"correlationId":"w_zgrip4yqisad-1695905554362","sender":{"id":"w_zgrip4yqisad","name":"User 8259790","role":"MODERATOR"},"chatEmphasizedText":true,"message":"Public chat test"}}}} -{"envelope":{"name":"UserReactionEmojiChangedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"timestamp":1695905560065},"core":{"header":{"name":"UserReactionEmojiChangedEvtMsg","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"body":{"userId":"w_zgrip4yqisad","reactionEmoji":"🙁"}}} -{"envelope":{"name":"PollStartedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"timestamp":1695905577266},"core":{"header":{"name":"PollStartedEvtMsg","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"body":{"userId":"w_zgrip4yqisad","pollId":"37d6334834d5f897b842bd70081cc4caff3a59be-1695905527932/1/1695905577266","pollType":"A-4","secretPoll":false,"question":"ABCD poll test (public), no custom input","poll":{"id":"37d6334834d5f897b842bd70081cc4caff3a59be-1695905527932/1/1695905577266","isMultipleResponse":false,"answers":[{"id":0,"key":"A"},{"id":1,"key":"B"},{"id":2,"key":"C"},{"id":3,"key":"D"}]}}}} -{"envelope":{"name":"UserRespondedToPollRespMsg","routing":{"msgType":"DIRECT","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"timestamp":1695905580723},"core":{"header":{"name":"UserRespondedToPollRespMsg","meetingId":"b47ddaaded996f5293398ba71a4170e2d1c9f724-1695905527931","userId":"w_zgrip4yqisad"},"body":{"pollId":"37d6334834d5f897b842bd70081cc4caff3a59be-1695905527932/1/1695905577266","userId":"w_oyvspjajpxt3","answerIds":[0]}}} +{"envelope":{"name":"MeetingCreatedEvtMsg","routing":{"sender":"bbb-apps-akka"},"timestamp":1695925641465},"core":{"header":{"name":"MeetingCreatedEvtMsg"},"body":{"props":{"meetingProp":{"name":"random-9301511","extId":"random-9301511","intId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","meetingCameraCap":50,"maxPinnedCameras":3,"isBreakout":false,"disabledFeatures":[],"notifyRecordingIsOn":false,"presentationUploadExternalDescription":"","presentationUploadExternalUrl":""},"breakoutProps":{"parentId":"bbb-none","sequence":0,"freeJoin":false,"breakoutRooms":[],"record":false,"privateChatEnabled":true,"captureNotes":false,"captureSlides":false,"captureNotesFilename":"%%CONFNAME%%","captureSlidesFilename":"%%CONFNAME%%"},"durationProps":{"duration":0,"createdTime":1695925641462,"createdDate":"Thu Sep 28 15:27:21 BRT 2023","meetingExpireIfNoUserJoinedInMinutes":5,"meetingExpireWhenLastUserLeftInMinutes":1,"userInactivityInspectTimerInMinutes":0,"userInactivityThresholdInMinutes":30,"userActivitySignResponseDelayInMinutes":5,"endWhenNoModerator":false,"endWhenNoModeratorDelayInMinutes":1},"password":{"moderatorPass":"mp","viewerPass":"ap","learningDashboardAccessToken":"8zgbn9qbpxgz"},"recordProp":{"record":false,"autoStartRecording":false,"allowStartStopRecording":true,"recordFullDurationMedia":false,"keepEvents":true},"welcomeProp":{"welcomeMsgTemplate":"
Welcome to %%CONFNAME%%!","welcomeMsg":"
Welcome to random-9301511!","modOnlyMessage":""},"voiceProp":{"telVoice":"74692","voiceConf":"74692","dialNumber":"613-555-1234","muteOnStart":false},"usersProp":{"maxUsers":0,"maxUserConcurrentAccesses":3,"webcamsOnlyForModerator":false,"userCameraCap":3,"guestPolicy":"ASK_MODERATOR","meetingLayout":"CUSTOM_LAYOUT","allowModsToUnmuteUsers":true,"allowModsToEjectCameras":true,"authenticatedGuest":false},"metadataProp":{"metadata":{}},"lockSettingsProps":{"disableCam":false,"disableMic":false,"disablePrivateChat":false,"disablePublicChat":false,"disableNotes":false,"hideUserList":false,"lockOnJoin":true,"lockOnJoinConfigurable":false,"hideViewersCursor":false,"hideViewersAnnotation":false},"systemProps":{"html5InstanceId":1},"groups":[]}}}} +{"envelope":{"name":"PresenterAssignedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"timestamp":1695925645381},"core":{"header":{"name":"PresenterAssignedEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"body":{"presenterId":"w_hc16wzmqcevt","presenterName":"User 9165940","assignedBy":"w_hc16wzmqcevt"}}} +{"envelope":{"name":"UserJoinedMeetingEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"timestamp":1695925645381},"core":{"header":{"name":"UserJoinedMeetingEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"body":{"intId":"w_hc16wzmqcevt","extId":"w_hc16wzmqcevt","name":"User 9165940","role":"MODERATOR","guest":false,"authed":true,"guestStatus":"ALLOW","emoji":"none","reactionEmoji":"none","raiseHand":false,"away":false,"pin":false,"presenter":false,"locked":true,"avatar":"","color":"#7b1fa2","clientType":"HTML5"}}} +{"envelope":{"name":"PresenterAssignedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"timestamp":1695925645401},"core":{"header":{"name":"PresenterAssignedEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"body":{"presenterId":"w_hc16wzmqcevt","presenterName":"User 9165940","assignedBy":"w_hc16wzmqcevt"}}} +{"envelope":{"name":"PresenterAssignedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"timestamp":1695925645407},"core":{"header":{"name":"PresenterAssignedEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"body":{"presenterId":"w_hc16wzmqcevt","presenterName":"User 9165940","assignedBy":"w_hc16wzmqcevt"}}} +{"envelope":{"name":"UserJoinedMeetingEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_s3vx2orgmloh"},"timestamp":1695925647055},"core":{"header":{"name":"UserJoinedMeetingEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_s3vx2orgmloh"},"body":{"intId":"w_s3vx2orgmloh","extId":"w_s3vx2orgmloh","name":"User 9165940","role":"VIEWER","guest":false,"authed":true,"guestStatus":"ALLOW","emoji":"none","reactionEmoji":"none","raiseHand":false,"away":false,"pin":false,"presenter":false,"locked":true,"avatar":"","color":"#6a1b9a","clientType":"HTML5"}}} +{"envelope":{"name":"UserJoinedVoiceConfToClientEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"timestamp":1695925651877},"core":{"header":{"name":"UserJoinedVoiceConfToClientEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"body":{"voiceConf":"74692","intId":"w_hc16wzmqcevt","voiceUserId":"4","callerName":"User+9165940","callerNum":"w_hc16wzmqcevt_1-bbbID-User+9165940","color":"#4a148c","muted":false,"talking":false,"callingWith":"none","listenOnly":false}}} +{"envelope":{"name":"UserLeftVoiceConfToClientEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"timestamp":1695925655431},"core":{"header":{"name":"UserLeftVoiceConfToClientEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"body":{"voiceConf":"74692","intId":"w_hc16wzmqcevt","voiceUserId":"w_hc16wzmqcevt"}}} +{"envelope":{"name":"UserBroadcastCamStartedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"timestamp":1695925659250},"core":{"header":{"name":"UserBroadcastCamStartedEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"body":{"userId":"w_hc16wzmqcevt","stream":"w_hc16wzmqcevt_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}}} +{"envelope":{"name":"UserBroadcastCamStoppedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"timestamp":1695925660677},"core":{"header":{"name":"UserBroadcastCamStoppedEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"body":{"userId":"w_hc16wzmqcevt","stream":"w_hc16wzmqcevt_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}}} +{"envelope":{"name":"ScreenshareRtmpBroadcastStartedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"not-used"},"timestamp":1695925664605},"core":{"header":{"name":"ScreenshareRtmpBroadcastStartedEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"not-used"},"body":{"voiceConf":"74692","screenshareConf":"74692","stream":"4a354271-f9cb-448a-97f1-4e7f841b02a5","vidWidth":0,"vidHeight":0,"timestamp":"1695925664605","hasAudio":false,"contentType":"screenshare"}}} +{"envelope":{"name":"ScreenshareRtmpBroadcastStoppedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"not-used"},"timestamp":1695925666665},"core":{"header":{"name":"ScreenshareRtmpBroadcastStoppedEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"not-used"},"body":{"voiceConf":"","screenshareConf":"","stream":"","vidWidth":0,"vidHeight":0,"timestamp":""}}} +{"envelope":{"name":"GroupChatMessageBroadcastEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"timestamp":1695925670472},"core":{"header":{"name":"GroupChatMessageBroadcastEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"body":{"chatId":"MAIN-PUBLIC-GROUP-CHAT","msg":{"id":"1695925670472-plwf615o","timestamp":1695925670472,"correlationId":"w_hc16wzmqcevt-1695925670453","sender":{"id":"w_hc16wzmqcevt","name":"User 9165940","role":"MODERATOR"},"chatEmphasizedText":true,"message":"Public chat test"}}}} +{"envelope":{"name":"UserReactionEmojiChangedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"timestamp":1695925675997},"core":{"header":{"name":"UserReactionEmojiChangedEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"body":{"userId":"w_hc16wzmqcevt","reactionEmoji":"🙁"}}} +{"envelope":{"name":"PollStartedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"timestamp":1695925693463},"core":{"header":{"name":"PollStartedEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"body":{"userId":"w_hc16wzmqcevt","pollId":"cbabaab6246e315678ba0a357009337966843a60-1695925641463/1/1695925693463","pollType":"A-4","secretPoll":false,"question":"ABCD Poll test (public)","poll":{"id":"cbabaab6246e315678ba0a357009337966843a60-1695925641463/1/1695925693463","isMultipleResponse":false,"answers":[{"id":0,"key":"A"},{"id":1,"key":"B"},{"id":2,"key":"C"},{"id":3,"key":"D"}]}}}} +{"envelope":{"name":"UserRespondedToPollRespMsg","routing":{"msgType":"DIRECT","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"timestamp":1695925696670},"core":{"header":{"name":"UserRespondedToPollRespMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"body":{"pollId":"cbabaab6246e315678ba0a357009337966843a60-1695925641463/1/1695925693463","userId":"w_s3vx2orgmloh","answerIds":[0]}}} +{"envelope":{"name":"UserLeftMeetingEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_s3vx2orgmloh"},"timestamp":1695925716476},"core":{"header":{"name":"UserLeftMeetingEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_s3vx2orgmloh"},"body":{"intId":"w_s3vx2orgmloh","eject":false,"ejectedBy":"","reason":"","reasonCode":""}}} +{"envelope":{"name":"MeetingDestroyedEvtMsg","routing":{"sender":"bbb-apps-akka"},"timestamp":1695925722656},"core":{"header":{"name":"MeetingDestroyedEvtMsg"},"body":{"meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462"}}} From 8774918bce82b6bba237cd7dbf5fb5f3b940f3db Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Thu, 28 Sep 2023 21:34:30 -0300 Subject: [PATCH 026/154] fix: add dedicated user-raise-hand-changed event and interop < 2.7 with 2.7+ --- example/events/mapped-events.json | 39 +++++++++++----------- example/events/raw-events.json | 39 +++++++++++----------- src/process/event.js | 54 ++++++++++++++++++++++++------- 3 files changed, 85 insertions(+), 47 deletions(-) diff --git a/example/events/mapped-events.json b/example/events/mapped-events.json index 9be7e0c..d63a3a3 100644 --- a/example/events/mapped-events.json +++ b/example/events/mapped-events.json @@ -1,18 +1,21 @@ -{"data":{"type":"event","id":"meeting-created","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","external-meeting-id":"random-9301511","name":"random-9301511","is-breakout":false,"parent-id":"bbb-none","duration":0,"create-time":1695925641462,"create-date":"Thu Sep 28 15:27:21 BRT 2023","moderator-pass":"mp","viewer-pass":"ap","record":false,"voice-conf":"74692","dial-number":"613-555-1234","max-users":0,"metadata":{}}},"event":{"ts":1695925641468}}} -{"data":{"type":"event","id":"user-presenter-assigned","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_hc16wzmqcevt","external-user-id":""}},"event":{"ts":1695925645387}}} -{"data":{"type":"event","id":"user-joined","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_hc16wzmqcevt","external-user-id":"w_hc16wzmqcevt","name":"User 9165940","role":"MODERATOR","presenter":"false"}},"event":{"ts":1695925645383}}} -{"data":{"type":"event","id":"user-presenter-assigned","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_hc16wzmqcevt","external-user-id":"w_hc16wzmqcevt"}},"event":{"ts":1695925645402}}} -{"data":{"type":"event","id":"user-presenter-assigned","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_hc16wzmqcevt","external-user-id":"w_hc16wzmqcevt"}},"event":{"ts":1695925645408}}} -{"data":{"type":"event","id":"user-joined","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_s3vx2orgmloh","external-user-id":"w_s3vx2orgmloh","name":"User 9165940","role":"VIEWER","presenter":"false"}},"event":{"ts":1695925647058}}} -{"data":{"type":"event","id":"user-audio-voice-enabled","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_hc16wzmqcevt","external-user-id":"w_hc16wzmqcevt","listening-only":false,"sharing-mic":true}},"event":{"ts":1695925651878}}} -{"data":{"type":"event","id":"user-audio-voice-disabled","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_hc16wzmqcevt","external-user-id":"w_hc16wzmqcevt","listening-only":false,"sharing-mic":false}},"event":{"ts":1695925655433}}} -{"data":{"type":"event","id":"user-cam-broadcast-start","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_hc16wzmqcevt","external-user-id":"w_hc16wzmqcevt","stream":"w_hc16wzmqcevt_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}},"event":{"ts":1695925659251}}} -{"data":{"type":"event","id":"user-cam-broadcast-end","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_hc16wzmqcevt","external-user-id":"w_hc16wzmqcevt","stream":"w_hc16wzmqcevt_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}},"event":{"ts":1695925660679}}} -{"data":{"type":"event","id":"meeting-screenshare-started","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","external-meeting-id":"random-9301511"}},"event":{"ts":1695925664606}}} -{"data":{"type":"event","id":"meeting-screenshare-stopped","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","external-meeting-id":"random-9301511"}},"event":{"ts":1695925666666}}} -{"data":{"type":"event","id":"chat-group-message-sent","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","external-meeting-id":"random-9301511"},"chat-message":{"id":"1695925670472-plwf615o","message":"Public chat test","sender":{"internal-user-id":"w_hc16wzmqcevt","name":"User 9165940","time":1695925670472}},"chat-id":"MAIN-PUBLIC-GROUP-CHAT"},"event":{"ts":1695925670474}}} -{"data":{"type":"event","id":"user-emoji-changed","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_hc16wzmqcevt","external-user-id":"w_hc16wzmqcevt","emoji":"🙁"}},"event":{"ts":1695925675998}}} -{"data":{"type":"event","id":"poll-started","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_hc16wzmqcevt","external-user-id":"w_hc16wzmqcevt"},"poll":{"question":"ABCD Poll test (public)","answers":[{"id":0,"key":"A"},{"id":1,"key":"B"},{"id":2,"key":"C"},{"id":3,"key":"D"}]}},"event":{"ts":1695925693465}}} -{"data":{"type":"event","id":"poll-responded","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_hc16wzmqcevt","external-user-id":"w_hc16wzmqcevt"},"poll":{"id":"cbabaab6246e315678ba0a357009337966843a60-1695925641463/1/1695925693463","answerIds":[0]}},"event":{"ts":1695925696673}}} -{"data":{"type":"event","id":"user-left","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_s3vx2orgmloh","external-user-id":"w_s3vx2orgmloh"}},"event":{"ts":1695925716477}}} -{"data":{"type":"event","id":"meeting-ended","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","external-meeting-id":"random-9301511"}},"event":{"ts":1695925722658}}} +{"data":{"type":"event","id":"meeting-created","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","external-meeting-id":"random-9301511","name":"random-9301511","is-breakout":false,"parent-id":"bbb-none","duration":0,"create-time":1695947505532,"create-date":"Thu Sep 28 21:31:45 BRT 2023","moderator-pass":"mp","viewer-pass":"ap","record":false,"voice-conf":"74692","dial-number":"613-555-1234","max-users":0,"metadata":{}}},"event":{"ts":1695947505562}}} +{"data":{"type":"event","id":"user-presenter-assigned","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_pjtppf6m5scn","external-user-id":""}},"event":{"ts":1695947510338}}} +{"data":{"type":"event","id":"user-joined","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_pjtppf6m5scn","external-user-id":"w_pjtppf6m5scn","name":"User 9165940","role":"MODERATOR","presenter":"false"}},"event":{"ts":1695947510336}}} +{"data":{"type":"event","id":"user-presenter-assigned","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_pjtppf6m5scn","external-user-id":"w_pjtppf6m5scn"}},"event":{"ts":1695947510358}}} +{"data":{"type":"event","id":"user-presenter-assigned","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_pjtppf6m5scn","external-user-id":"w_pjtppf6m5scn"}},"event":{"ts":1695947510363}}} +{"data":{"type":"event","id":"user-joined","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_l7zixn2bdi9n","external-user-id":"w_l7zixn2bdi9n","name":"User 9165940","role":"VIEWER","presenter":"false"}},"event":{"ts":1695947515826}}} +{"data":{"type":"event","id":"user-audio-voice-enabled","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_pjtppf6m5scn","external-user-id":"w_pjtppf6m5scn","listening-only":false,"sharing-mic":true}},"event":{"ts":1695947519365}}} +{"data":{"type":"event","id":"user-audio-voice-disabled","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_pjtppf6m5scn","external-user-id":"w_pjtppf6m5scn","listening-only":false,"sharing-mic":false}},"event":{"ts":1695947523039}}} +{"data":{"type":"event","id":"user-cam-broadcast-start","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_pjtppf6m5scn","external-user-id":"w_pjtppf6m5scn","stream":"w_pjtppf6m5scn_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}},"event":{"ts":1695947527033}}} +{"data":{"type":"event","id":"user-cam-broadcast-end","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_pjtppf6m5scn","external-user-id":"w_pjtppf6m5scn","stream":"w_pjtppf6m5scn_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}},"event":{"ts":1695947529391}}} +{"data":{"type":"event","id":"meeting-screenshare-started","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","external-meeting-id":"random-9301511"}},"event":{"ts":1695947532594}}} +{"data":{"type":"event","id":"meeting-screenshare-stopped","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","external-meeting-id":"random-9301511"}},"event":{"ts":1695947536147}}} +{"data":{"type":"event","id":"chat-group-message-sent","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","external-meeting-id":"random-9301511"},"chat-message":{"id":"1695947543829-zpiq308w","message":"Public chat test","sender":{"internal-user-id":"w_pjtppf6m5scn","name":"User 9165940","time":1695947543829}},"chat-id":"MAIN-PUBLIC-GROUP-CHAT"},"event":{"ts":1695947543832}}} +{"data":{"type":"event","id":"user-emoji-changed","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_pjtppf6m5scn","external-user-id":"w_pjtppf6m5scn","emoji":"😃"}},"event":{"ts":1695947549881}}} +{"data":{"type":"event","id":"user-emoji-changed","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_pjtppf6m5scn","external-user-id":"w_pjtppf6m5scn","emoji":"none"}},"event":{"ts":1695947552889}}} +{"data":{"type":"event","id":"user-raise-hand-changed","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_pjtppf6m5scn","external-user-id":"w_pjtppf6m5scn","raise-hand":true}},"event":{"ts":1695947556279}}} +{"data":{"type":"event","id":"user-raise-hand-changed","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_pjtppf6m5scn","external-user-id":"w_pjtppf6m5scn","raise-hand":false}},"event":{"ts":1695947560399}}} +{"data":{"type":"event","id":"poll-started","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_pjtppf6m5scn","external-user-id":"w_pjtppf6m5scn"},"poll":{"question":"ABCD Poll test (public)","answers":[{"id":0,"key":"A"},{"id":1,"key":"B"},{"id":2,"key":"C"},{"id":3,"key":"D"}]}},"event":{"ts":1695947575622}}} +{"data":{"type":"event","id":"poll-responded","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_pjtppf6m5scn","external-user-id":"w_pjtppf6m5scn"},"poll":{"id":"4b6fb9fdf792ba4bd31bf443919a874d60b5b94b-1695947505534/1/1695947575620","answerIds":[0]}},"event":{"ts":1695947581958}}} +{"data":{"type":"event","id":"user-left","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_l7zixn2bdi9n","external-user-id":"w_l7zixn2bdi9n"}},"event":{"ts":1695947600568}}} +{"data":{"type":"event","id":"meeting-ended","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","external-meeting-id":"random-9301511"}},"event":{"ts":1695947607778}}} diff --git a/example/events/raw-events.json b/example/events/raw-events.json index e7b74db..d859c92 100644 --- a/example/events/raw-events.json +++ b/example/events/raw-events.json @@ -1,18 +1,21 @@ -{"envelope":{"name":"MeetingCreatedEvtMsg","routing":{"sender":"bbb-apps-akka"},"timestamp":1695925641465},"core":{"header":{"name":"MeetingCreatedEvtMsg"},"body":{"props":{"meetingProp":{"name":"random-9301511","extId":"random-9301511","intId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","meetingCameraCap":50,"maxPinnedCameras":3,"isBreakout":false,"disabledFeatures":[],"notifyRecordingIsOn":false,"presentationUploadExternalDescription":"","presentationUploadExternalUrl":""},"breakoutProps":{"parentId":"bbb-none","sequence":0,"freeJoin":false,"breakoutRooms":[],"record":false,"privateChatEnabled":true,"captureNotes":false,"captureSlides":false,"captureNotesFilename":"%%CONFNAME%%","captureSlidesFilename":"%%CONFNAME%%"},"durationProps":{"duration":0,"createdTime":1695925641462,"createdDate":"Thu Sep 28 15:27:21 BRT 2023","meetingExpireIfNoUserJoinedInMinutes":5,"meetingExpireWhenLastUserLeftInMinutes":1,"userInactivityInspectTimerInMinutes":0,"userInactivityThresholdInMinutes":30,"userActivitySignResponseDelayInMinutes":5,"endWhenNoModerator":false,"endWhenNoModeratorDelayInMinutes":1},"password":{"moderatorPass":"mp","viewerPass":"ap","learningDashboardAccessToken":"8zgbn9qbpxgz"},"recordProp":{"record":false,"autoStartRecording":false,"allowStartStopRecording":true,"recordFullDurationMedia":false,"keepEvents":true},"welcomeProp":{"welcomeMsgTemplate":"
Welcome to %%CONFNAME%%!","welcomeMsg":"
Welcome to random-9301511!","modOnlyMessage":""},"voiceProp":{"telVoice":"74692","voiceConf":"74692","dialNumber":"613-555-1234","muteOnStart":false},"usersProp":{"maxUsers":0,"maxUserConcurrentAccesses":3,"webcamsOnlyForModerator":false,"userCameraCap":3,"guestPolicy":"ASK_MODERATOR","meetingLayout":"CUSTOM_LAYOUT","allowModsToUnmuteUsers":true,"allowModsToEjectCameras":true,"authenticatedGuest":false},"metadataProp":{"metadata":{}},"lockSettingsProps":{"disableCam":false,"disableMic":false,"disablePrivateChat":false,"disablePublicChat":false,"disableNotes":false,"hideUserList":false,"lockOnJoin":true,"lockOnJoinConfigurable":false,"hideViewersCursor":false,"hideViewersAnnotation":false},"systemProps":{"html5InstanceId":1},"groups":[]}}}} -{"envelope":{"name":"PresenterAssignedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"timestamp":1695925645381},"core":{"header":{"name":"PresenterAssignedEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"body":{"presenterId":"w_hc16wzmqcevt","presenterName":"User 9165940","assignedBy":"w_hc16wzmqcevt"}}} -{"envelope":{"name":"UserJoinedMeetingEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"timestamp":1695925645381},"core":{"header":{"name":"UserJoinedMeetingEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"body":{"intId":"w_hc16wzmqcevt","extId":"w_hc16wzmqcevt","name":"User 9165940","role":"MODERATOR","guest":false,"authed":true,"guestStatus":"ALLOW","emoji":"none","reactionEmoji":"none","raiseHand":false,"away":false,"pin":false,"presenter":false,"locked":true,"avatar":"","color":"#7b1fa2","clientType":"HTML5"}}} -{"envelope":{"name":"PresenterAssignedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"timestamp":1695925645401},"core":{"header":{"name":"PresenterAssignedEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"body":{"presenterId":"w_hc16wzmqcevt","presenterName":"User 9165940","assignedBy":"w_hc16wzmqcevt"}}} -{"envelope":{"name":"PresenterAssignedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"timestamp":1695925645407},"core":{"header":{"name":"PresenterAssignedEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"body":{"presenterId":"w_hc16wzmqcevt","presenterName":"User 9165940","assignedBy":"w_hc16wzmqcevt"}}} -{"envelope":{"name":"UserJoinedMeetingEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_s3vx2orgmloh"},"timestamp":1695925647055},"core":{"header":{"name":"UserJoinedMeetingEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_s3vx2orgmloh"},"body":{"intId":"w_s3vx2orgmloh","extId":"w_s3vx2orgmloh","name":"User 9165940","role":"VIEWER","guest":false,"authed":true,"guestStatus":"ALLOW","emoji":"none","reactionEmoji":"none","raiseHand":false,"away":false,"pin":false,"presenter":false,"locked":true,"avatar":"","color":"#6a1b9a","clientType":"HTML5"}}} -{"envelope":{"name":"UserJoinedVoiceConfToClientEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"timestamp":1695925651877},"core":{"header":{"name":"UserJoinedVoiceConfToClientEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"body":{"voiceConf":"74692","intId":"w_hc16wzmqcevt","voiceUserId":"4","callerName":"User+9165940","callerNum":"w_hc16wzmqcevt_1-bbbID-User+9165940","color":"#4a148c","muted":false,"talking":false,"callingWith":"none","listenOnly":false}}} -{"envelope":{"name":"UserLeftVoiceConfToClientEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"timestamp":1695925655431},"core":{"header":{"name":"UserLeftVoiceConfToClientEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"body":{"voiceConf":"74692","intId":"w_hc16wzmqcevt","voiceUserId":"w_hc16wzmqcevt"}}} -{"envelope":{"name":"UserBroadcastCamStartedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"timestamp":1695925659250},"core":{"header":{"name":"UserBroadcastCamStartedEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"body":{"userId":"w_hc16wzmqcevt","stream":"w_hc16wzmqcevt_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}}} -{"envelope":{"name":"UserBroadcastCamStoppedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"timestamp":1695925660677},"core":{"header":{"name":"UserBroadcastCamStoppedEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"body":{"userId":"w_hc16wzmqcevt","stream":"w_hc16wzmqcevt_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}}} -{"envelope":{"name":"ScreenshareRtmpBroadcastStartedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"not-used"},"timestamp":1695925664605},"core":{"header":{"name":"ScreenshareRtmpBroadcastStartedEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"not-used"},"body":{"voiceConf":"74692","screenshareConf":"74692","stream":"4a354271-f9cb-448a-97f1-4e7f841b02a5","vidWidth":0,"vidHeight":0,"timestamp":"1695925664605","hasAudio":false,"contentType":"screenshare"}}} -{"envelope":{"name":"ScreenshareRtmpBroadcastStoppedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"not-used"},"timestamp":1695925666665},"core":{"header":{"name":"ScreenshareRtmpBroadcastStoppedEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"not-used"},"body":{"voiceConf":"","screenshareConf":"","stream":"","vidWidth":0,"vidHeight":0,"timestamp":""}}} -{"envelope":{"name":"GroupChatMessageBroadcastEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"timestamp":1695925670472},"core":{"header":{"name":"GroupChatMessageBroadcastEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"body":{"chatId":"MAIN-PUBLIC-GROUP-CHAT","msg":{"id":"1695925670472-plwf615o","timestamp":1695925670472,"correlationId":"w_hc16wzmqcevt-1695925670453","sender":{"id":"w_hc16wzmqcevt","name":"User 9165940","role":"MODERATOR"},"chatEmphasizedText":true,"message":"Public chat test"}}}} -{"envelope":{"name":"UserReactionEmojiChangedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"timestamp":1695925675997},"core":{"header":{"name":"UserReactionEmojiChangedEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"body":{"userId":"w_hc16wzmqcevt","reactionEmoji":"🙁"}}} -{"envelope":{"name":"PollStartedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"timestamp":1695925693463},"core":{"header":{"name":"PollStartedEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"body":{"userId":"w_hc16wzmqcevt","pollId":"cbabaab6246e315678ba0a357009337966843a60-1695925641463/1/1695925693463","pollType":"A-4","secretPoll":false,"question":"ABCD Poll test (public)","poll":{"id":"cbabaab6246e315678ba0a357009337966843a60-1695925641463/1/1695925693463","isMultipleResponse":false,"answers":[{"id":0,"key":"A"},{"id":1,"key":"B"},{"id":2,"key":"C"},{"id":3,"key":"D"}]}}}} -{"envelope":{"name":"UserRespondedToPollRespMsg","routing":{"msgType":"DIRECT","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"timestamp":1695925696670},"core":{"header":{"name":"UserRespondedToPollRespMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_hc16wzmqcevt"},"body":{"pollId":"cbabaab6246e315678ba0a357009337966843a60-1695925641463/1/1695925693463","userId":"w_s3vx2orgmloh","answerIds":[0]}}} -{"envelope":{"name":"UserLeftMeetingEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_s3vx2orgmloh"},"timestamp":1695925716476},"core":{"header":{"name":"UserLeftMeetingEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462","userId":"w_s3vx2orgmloh"},"body":{"intId":"w_s3vx2orgmloh","eject":false,"ejectedBy":"","reason":"","reasonCode":""}}} -{"envelope":{"name":"MeetingDestroyedEvtMsg","routing":{"sender":"bbb-apps-akka"},"timestamp":1695925722656},"core":{"header":{"name":"MeetingDestroyedEvtMsg"},"body":{"meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695925641462"}}} +{"envelope":{"name":"MeetingCreatedEvtMsg","routing":{"sender":"bbb-apps-akka"},"timestamp":1695947505550},"core":{"header":{"name":"MeetingCreatedEvtMsg"},"body":{"props":{"meetingProp":{"name":"random-9301511","extId":"random-9301511","intId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","meetingCameraCap":50,"maxPinnedCameras":3,"isBreakout":false,"disabledFeatures":[],"notifyRecordingIsOn":false,"presentationUploadExternalDescription":"","presentationUploadExternalUrl":""},"breakoutProps":{"parentId":"bbb-none","sequence":0,"freeJoin":false,"breakoutRooms":[],"record":false,"privateChatEnabled":true,"captureNotes":false,"captureSlides":false,"captureNotesFilename":"%%CONFNAME%%","captureSlidesFilename":"%%CONFNAME%%"},"durationProps":{"duration":0,"createdTime":1695947505532,"createdDate":"Thu Sep 28 21:31:45 BRT 2023","meetingExpireIfNoUserJoinedInMinutes":5,"meetingExpireWhenLastUserLeftInMinutes":1,"userInactivityInspectTimerInMinutes":0,"userInactivityThresholdInMinutes":30,"userActivitySignResponseDelayInMinutes":5,"endWhenNoModerator":false,"endWhenNoModeratorDelayInMinutes":1},"password":{"moderatorPass":"mp","viewerPass":"ap","learningDashboardAccessToken":"nl2q4mdkmcxz"},"recordProp":{"record":false,"autoStartRecording":false,"allowStartStopRecording":true,"recordFullDurationMedia":false,"keepEvents":true},"welcomeProp":{"welcomeMsgTemplate":"
Welcome to %%CONFNAME%%!","welcomeMsg":"
Welcome to random-9301511!","modOnlyMessage":""},"voiceProp":{"telVoice":"74692","voiceConf":"74692","dialNumber":"613-555-1234","muteOnStart":false},"usersProp":{"maxUsers":0,"maxUserConcurrentAccesses":3,"webcamsOnlyForModerator":false,"userCameraCap":3,"guestPolicy":"ASK_MODERATOR","meetingLayout":"CUSTOM_LAYOUT","allowModsToUnmuteUsers":true,"allowModsToEjectCameras":true,"authenticatedGuest":false},"metadataProp":{"metadata":{}},"lockSettingsProps":{"disableCam":false,"disableMic":false,"disablePrivateChat":false,"disablePublicChat":false,"disableNotes":false,"hideUserList":false,"lockOnJoin":true,"lockOnJoinConfigurable":false,"hideViewersCursor":false,"hideViewersAnnotation":false},"systemProps":{"html5InstanceId":1},"groups":[]}}}} +{"envelope":{"name":"PresenterAssignedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"timestamp":1695947510332},"core":{"header":{"name":"PresenterAssignedEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"body":{"presenterId":"w_pjtppf6m5scn","presenterName":"User 9165940","assignedBy":"w_pjtppf6m5scn"}}} +{"envelope":{"name":"UserJoinedMeetingEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"timestamp":1695947510331},"core":{"header":{"name":"UserJoinedMeetingEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"body":{"intId":"w_pjtppf6m5scn","extId":"w_pjtppf6m5scn","name":"User 9165940","role":"MODERATOR","guest":false,"authed":true,"guestStatus":"ALLOW","emoji":"none","reactionEmoji":"none","raiseHand":false,"away":false,"pin":false,"presenter":false,"locked":true,"avatar":"","color":"#7b1fa2","clientType":"HTML5"}}} +{"envelope":{"name":"PresenterAssignedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"timestamp":1695947510357},"core":{"header":{"name":"PresenterAssignedEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"body":{"presenterId":"w_pjtppf6m5scn","presenterName":"User 9165940","assignedBy":"w_pjtppf6m5scn"}}} +{"envelope":{"name":"PresenterAssignedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"timestamp":1695947510362},"core":{"header":{"name":"PresenterAssignedEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"body":{"presenterId":"w_pjtppf6m5scn","presenterName":"User 9165940","assignedBy":"w_pjtppf6m5scn"}}} +{"envelope":{"name":"UserJoinedMeetingEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_l7zixn2bdi9n"},"timestamp":1695947515820},"core":{"header":{"name":"UserJoinedMeetingEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_l7zixn2bdi9n"},"body":{"intId":"w_l7zixn2bdi9n","extId":"w_l7zixn2bdi9n","name":"User 9165940","role":"VIEWER","guest":false,"authed":true,"guestStatus":"ALLOW","emoji":"none","reactionEmoji":"none","raiseHand":false,"away":false,"pin":false,"presenter":false,"locked":true,"avatar":"","color":"#6a1b9a","clientType":"HTML5"}}} +{"envelope":{"name":"UserJoinedVoiceConfToClientEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"timestamp":1695947519363},"core":{"header":{"name":"UserJoinedVoiceConfToClientEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"body":{"voiceConf":"74692","intId":"w_pjtppf6m5scn","voiceUserId":"6","callerName":"User+9165940","callerNum":"w_pjtppf6m5scn_1-bbbID-User+9165940","color":"#4a148c","muted":false,"talking":false,"callingWith":"none","listenOnly":false}}} +{"envelope":{"name":"UserLeftVoiceConfToClientEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"timestamp":1695947523038},"core":{"header":{"name":"UserLeftVoiceConfToClientEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"body":{"voiceConf":"74692","intId":"w_pjtppf6m5scn","voiceUserId":"w_pjtppf6m5scn"}}} +{"envelope":{"name":"UserBroadcastCamStartedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"timestamp":1695947527032},"core":{"header":{"name":"UserBroadcastCamStartedEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"body":{"userId":"w_pjtppf6m5scn","stream":"w_pjtppf6m5scn_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}}} +{"envelope":{"name":"UserBroadcastCamStoppedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"timestamp":1695947529390},"core":{"header":{"name":"UserBroadcastCamStoppedEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"body":{"userId":"w_pjtppf6m5scn","stream":"w_pjtppf6m5scn_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}}} +{"envelope":{"name":"ScreenshareRtmpBroadcastStartedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"not-used"},"timestamp":1695947532592},"core":{"header":{"name":"ScreenshareRtmpBroadcastStartedEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"not-used"},"body":{"voiceConf":"74692","screenshareConf":"74692","stream":"7f3fe80d-823a-430d-aea4-9b4ebce1be12","vidWidth":0,"vidHeight":0,"timestamp":"1695947532592","hasAudio":false,"contentType":"screenshare"}}} +{"envelope":{"name":"ScreenshareRtmpBroadcastStoppedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"not-used"},"timestamp":1695947536146},"core":{"header":{"name":"ScreenshareRtmpBroadcastStoppedEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"not-used"},"body":{"voiceConf":"","screenshareConf":"","stream":"","vidWidth":0,"vidHeight":0,"timestamp":""}}} +{"envelope":{"name":"GroupChatMessageBroadcastEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"timestamp":1695947543830},"core":{"header":{"name":"GroupChatMessageBroadcastEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"body":{"chatId":"MAIN-PUBLIC-GROUP-CHAT","msg":{"id":"1695947543829-zpiq308w","timestamp":1695947543829,"correlationId":"w_pjtppf6m5scn-1695947543794","sender":{"id":"w_pjtppf6m5scn","name":"User 9165940","role":"MODERATOR"},"chatEmphasizedText":true,"message":"Public chat test"}}}} +{"envelope":{"name":"UserReactionEmojiChangedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"timestamp":1695947549880},"core":{"header":{"name":"UserReactionEmojiChangedEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"body":{"userId":"w_pjtppf6m5scn","reactionEmoji":"😃"}}} +{"envelope":{"name":"UserReactionEmojiChangedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"timestamp":1695947552888},"core":{"header":{"name":"UserReactionEmojiChangedEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"body":{"userId":"w_pjtppf6m5scn","reactionEmoji":"none"}}} +{"envelope":{"name":"UserRaiseHandChangedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"timestamp":1695947556278},"core":{"header":{"name":"UserRaiseHandChangedEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"body":{"userId":"w_pjtppf6m5scn","raiseHand":true}}} +{"envelope":{"name":"UserRaiseHandChangedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"timestamp":1695947560397},"core":{"header":{"name":"UserRaiseHandChangedEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"body":{"userId":"w_pjtppf6m5scn","raiseHand":false}}} +{"envelope":{"name":"PollStartedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"timestamp":1695947575620},"core":{"header":{"name":"PollStartedEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"body":{"userId":"w_pjtppf6m5scn","pollId":"4b6fb9fdf792ba4bd31bf443919a874d60b5b94b-1695947505534/1/1695947575620","pollType":"A-4","secretPoll":false,"question":"ABCD Poll test (public)","poll":{"id":"4b6fb9fdf792ba4bd31bf443919a874d60b5b94b-1695947505534/1/1695947575620","isMultipleResponse":false,"answers":[{"id":0,"key":"A"},{"id":1,"key":"B"},{"id":2,"key":"C"},{"id":3,"key":"D"}]}}}} +{"envelope":{"name":"UserRespondedToPollRespMsg","routing":{"msgType":"DIRECT","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"timestamp":1695947581956},"core":{"header":{"name":"UserRespondedToPollRespMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"body":{"pollId":"4b6fb9fdf792ba4bd31bf443919a874d60b5b94b-1695947505534/1/1695947575620","userId":"w_l7zixn2bdi9n","answerIds":[0]}}} +{"envelope":{"name":"UserLeftMeetingEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_l7zixn2bdi9n"},"timestamp":1695947600566},"core":{"header":{"name":"UserLeftMeetingEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_l7zixn2bdi9n"},"body":{"intId":"w_l7zixn2bdi9n","eject":false,"ejectedBy":"","reason":"","reasonCode":""}}} +{"envelope":{"name":"MeetingDestroyedEvtMsg","routing":{"sender":"bbb-apps-akka"},"timestamp":1695947607776},"core":{"header":{"name":"MeetingDestroyedEvtMsg"},"body":{"meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532"}}} diff --git a/src/process/event.js b/src/process/event.js index b3f840e..27fe09f 100644 --- a/src/process/event.js +++ b/src/process/event.js @@ -23,6 +23,7 @@ export default class WebhooksEvent { "user-presenter-assigned", "user-presenter-unassigned", "user-emoji-changed", + "user-raise-hand-changed", "chat-group-message-sent", "rap-published", "rap-unpublished", @@ -67,6 +68,8 @@ export default class WebhooksEvent { "UserBroadcastCamStoppedEvtMsg", "UserEmojiChangedEvtMsg", "UserReactionEmojiChangedEvtMsg", + // 2.7+ + "UserRaiseHandChangedEvtMsg", ], CHAT_EVENTS: [ "GroupChatMessageBroadcastEvtMsg", @@ -215,6 +218,22 @@ export default class WebhooksEvent { } } + handleUserEmojiChanged(message) { + try { + // < 2.7 => UserEmojiChangedEvtMsg also bundles the raiseHand action as an emoji + // >= 2.7 => UserEmojiChangedEvtMsg and UserRaiseHandChangedEvtMsg are separate events + // and the raiseHand action is not bundled as an emoji anymore + const { body } = message.core; + const emoji = body.emoji || body.reactionEmoji; + + if (emoji && emoji === "raiseHand") return "user-raise-hand-changed"; + return "user-emoji-changed"; + } catch (error) { + logger.error('error handling user emoji changed', error); + return "user-emoji-changed"; + } + } + // Map internal to external message for user information userTemplate(messageObj) { const msgBody = messageObj.core.body; @@ -244,17 +263,30 @@ export default class WebhooksEvent { } } }; - if (this.outputEvent.data["id"] === "user-audio-voice-enabled") { - this.outputEvent.data["attributes"]["user"]["listening-only"] = msgBody.listenOnly; - this.outputEvent.data["attributes"]["user"]["sharing-mic"] = ! msgBody.listenOnly; - } else if (this.outputEvent.data["id"] === "user-audio-voice-disabled") { - this.outputEvent.data["attributes"]["user"]["listening-only"] = false; - this.outputEvent.data["attributes"]["user"]["sharing-mic"] = false; - } else if (this.outputEvent.data["id"] === "user-emoji-changed") { - const emoji = msgBody.emoji || msgBody.reactionEmoji; - if (emoji && emoji !== "none") { + + // Refactor the if-else chain down there to a switch case block + switch (this.outputEvent.data.id) { + case "user-audio-voice-enabled": + this.outputEvent.data["attributes"]["user"]["listening-only"] = msgBody.listenOnly; + this.outputEvent.data["attributes"]["user"]["sharing-mic"] = !msgBody.listenOnly; + break; + case "user-audio-voice-disabled": + this.outputEvent.data["attributes"]["user"]["listening-only"] = false; + this.outputEvent.data["attributes"]["user"]["sharing-mic"] = false; + break; + case "user-emoji-changed": { + const emoji = msgBody.emoji || msgBody.reactionEmoji || "none"; this.outputEvent.data["attributes"]["user"]["emoji"] = emoji; + break; + } + case "user-raise-hand-changed": { + const emoji = msgBody.emoji || msgBody.reactionEmoji; + const raiseHand = msgBody.raiseHand || emoji === "raiseHand"; + this.outputEvent.data["attributes"]["user"]["raise-hand"] = raiseHand; + break; } + default: + break; } } @@ -465,8 +497,8 @@ export default class WebhooksEvent { case "PresenterAssignedEvtMsg": return "user-presenter-assigned"; case "PresenterUnassignedEvtMsg": return "user-presenter-unassigned"; case "UserEmojiChangedEvtMsg": - case "UserReactionEmojiChangedEvtMsg": - return "user-emoji-changed"; + case "UserReactionEmojiChangedEvtMsg": return this.handleUserEmojiChanged(message); + case "UserRaiseHandChangedEvtMsg": return "user-raise-hand-changed"; case "GroupChatMessageBroadcastEvtMsg": return "chat-group-message-sent"; case "PublishedRecordingSysMsg": return "rap-published"; case "UnpublishedRecordingSysMsg": return "rap-unpublished"; From 97eeadf3289e60102883e05d57601abb76730922 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Thu, 28 Sep 2023 21:35:00 -0300 Subject: [PATCH 027/154] feat: add option for the interceptor util to keep retrying hook creation calls --- extra/interceptor.js | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/extra/interceptor.js b/extra/interceptor.js index b0097b4..a9f92c0 100644 --- a/extra/interceptor.js +++ b/extra/interceptor.js @@ -14,6 +14,7 @@ const port = process.env.PORT || 3006; const sharedSecret = process.env.SHARED_SECRET || eject("SHARED_SECRET not set"); const catcherDomain = process.env.CATCHER_DOMAIN || eject("CATCHER_DOMAIN not set"); const bbbDomain = process.env.BBB_DOMAIN || eject("BBB_DOMAIN not set"); +const FOREVER = (process.env.FOREVER && process.env.FOREVER === 'true') || false; let server = null; const encodeForUrl = (value) => { @@ -63,16 +64,25 @@ const requestOptions = { method: "GET" } console.log("Registering a hook with", fullUrl); -request(requestOptions, (error, response, body) => { - const statusCode = response?.statusCode; - // consider 401 as success, because the callback worked but was denied by the recipient - if (statusCode >= 200 && statusCode < 300) { - console.debug("Hook registed - response from hook/create:", body); - } else { - console.log("Hook registration failed - response from hook/create:", body); - shutdown(1); - } -}); +const registerHook = () => { + request(requestOptions, (error, response, body) => { + const statusCode = response?.statusCode; + // consider 401 as success, because the callback worked but was denied by the recipient + if (statusCode >= 200 && statusCode < 300) { + console.debug("Hook registered - response from hook/create:", body); + } else { + console.log("Hook registration failed - response from hook/create:", body); + // if FOREVER, then keep trying to register the hook + // every 3s until it works - else exit with code 1 + if (FOREVER) { + console.log("Trying again in 3s..."); + setTimeout(registerHook, 3000); + } else { + shutdown(1); + } + } + }); +}; process.on('SIGINT', () => shutdown(0)); process.on('SIGTERM', () => shutdown(0)); @@ -84,3 +94,5 @@ process.on('unhandledRejection', (reason, promise) => { console.error('unhandledRejection:', reason, promise); shutdown(1); }); + +registerHook(); From 9858c079b2603859fb6e604a7de63e6ff23bb61c Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Thu, 28 Sep 2023 21:52:04 -0300 Subject: [PATCH 028/154] fix: guarantee Redis prefixes are stringified --- src/db/redis/base-storage.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/db/redis/base-storage.js b/src/db/redis/base-storage.js index 00d9a37..d40ca9b 100644 --- a/src/db/redis/base-storage.js +++ b/src/db/redis/base-storage.js @@ -27,8 +27,9 @@ class StorageItem { ...appOptions }) { this.client = client; - this.prefix = prefix; - this.setId = setId; + // Prefix and setId must be strings - convert + this.prefix = typeof prefix !== 'string' ? prefix.toString() : prefix; + this.setId = typeof setId !== 'string' ? setId.toString() : setId; this.id = id; this.alias = alias; this.payload = payload; From f0ebaa114ecfabc43b9f396f49248d7b64a8a5c0 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Thu, 28 Sep 2023 23:56:20 -0300 Subject: [PATCH 029/154] fix: remove request dep, restore domain field and bearer auth - Replaces request (unmaintained) with node-fetch (maintained, slimmer and should provide us with a clean transition to Node.js fetch when that is stable) - Restore the domain field in hooks - Restore the bearer auth option - Add nodemon and a start-dev script to enable auto-reload of the application on code changes --- extra/interceptor.js | 48 +-- package-lock.json | 518 ++++++++++++--------------- package.json | 4 +- src/out/webhooks/callback-emitter.js | 140 ++++---- src/out/webhooks/web-hooks.js | 8 +- 5 files changed, 330 insertions(+), 388 deletions(-) diff --git a/extra/interceptor.js b/extra/interceptor.js index a9f92c0..ce909f0 100644 --- a/extra/interceptor.js +++ b/extra/interceptor.js @@ -4,7 +4,7 @@ // Uses the first meeting started after the application runs and will list all // events, but only the first time they happen. import express from "express"; -import request from "request"; +import fetch from "node-fetch"; import sha1 from "sha1"; import bodyParser from 'body-parser'; @@ -59,29 +59,35 @@ const params = "callbackURL=" + encodeForUrl(myUrl); const checksum = sha1("hooks/create" + params + sharedSecret); const fullUrl = "http://" + bbbDomain + "/bigbluebutton/api/hooks/create?" + params + "&checksum=" + checksum -const requestOptions = { - uri: fullUrl, - method: "GET" -} console.log("Registering a hook with", fullUrl); -const registerHook = () => { - request(requestOptions, (error, response, body) => { - const statusCode = response?.statusCode; - // consider 401 as success, because the callback worked but was denied by the recipient - if (statusCode >= 200 && statusCode < 300) { - console.debug("Hook registered - response from hook/create:", body); + +const registerHook = async () => { + const controller = new AbortController(); + const abortTimeout = setTimeout(() => { + controller.abort(); + }, 2500); + + try { + const response = await fetch(fullUrl, { signal: controller.signal }); + const text = await response.text(); + if (response.ok) { + console.debug("Hook registered - response from hook/create:", text); } else { - console.log("Hook registration failed - response from hook/create:", body); - // if FOREVER, then keep trying to register the hook - // every 3s until it works - else exit with code 1 - if (FOREVER) { - console.log("Trying again in 3s..."); - setTimeout(registerHook, 3000); - } else { - shutdown(1); - } + throw new Error(text); } - }); + } catch (error) { + console.error("Hook registration failed - response from hook/create:", error); + // if FOREVER, then keep trying to register the hook + // every 3s until it works - else exit with code 1 + if (FOREVER) { + console.log("Trying again in 3s..."); + setTimeout(registerHook, 3000); + } else { + shutdown(1); + } + } finally { + clearTimeout(abortTimeout); + } }; process.on('SIGINT', () => shutdown(0)); diff --git a/package-lock.json b/package-lock.json index 5e44971..308a958 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,8 +14,8 @@ "express": "^4.18.2", "js-yaml": "^4.1.0", "nock": "^13.2.4", + "node-fetch": "^3.3.2", "redis": "^4.6.8", - "request": "^2.88.2", "sha1": "^1.1.1", "uuid": "^9.0.1", "winston": "^3.10.0" @@ -24,6 +24,7 @@ "eslint": "^8.49.0", "eslint-plugin-import": "^2.28.1", "mocha": "^9.2.2", + "nodemon": "^3.0.1", "sinon": "^12.0.1", "supertest": "^3.4.2" }, @@ -74,9 +75,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.2.tgz", - "integrity": "sha512-0MGxAVt1m/ZK+LTJp/j0qF7Hz97D9O/FH9Ms3ltnyIdDD57cbb1ACIQTkbHvNXtWDv5TPq7w5Kq56+cNukbo7g==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.9.0.tgz", + "integrity": "sha512-zJmuCWj2VLBt4c25CfBIbMZLGLyhkvs7LznyVX5HfpzeocThgIj5XQK4L+g3U36mMcx8bPMhGyPpwCATamC4jQ==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" @@ -410,6 +411,12 @@ "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", "dev": true }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -447,6 +454,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -622,22 +630,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "dependencies": { - "safer-buffer": "~2.1.0" - } - }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "engines": { - "node": ">=0.8" - } - }, "node_modules/async": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", @@ -646,7 +638,8 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true }, "node_modules/available-typed-arrays": { "version": "1.0.5", @@ -660,32 +653,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", - "engines": { - "node": "*" - } - }, - "node_modules/aws4": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", - "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, - "node_modules/bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", - "dependencies": { - "tweetnacl": "^0.14.3" - } - }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -803,11 +775,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" - }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -951,6 +918,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -1058,15 +1026,12 @@ "node": "*" } }, - "node_modules/dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", - "dependencies": { - "assert-plus": "^1.0.0" - }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", "engines": { - "node": ">=0.10" + "node": ">= 12" } }, "node_modules/debug": { @@ -1130,6 +1095,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, "engines": { "node": ">=0.4.0" } @@ -1180,15 +1146,6 @@ "node": ">=6.0.0" } }, - "node_modules/ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", - "dependencies": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -1710,25 +1667,20 @@ "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "node_modules/extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", - "engines": [ - "node >=0.6.0" - ] + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true }, "node_modules/fast-levenshtein": { "version": "2.0.6", @@ -1750,6 +1702,28 @@ "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -1850,18 +1824,11 @@ "is-callable": "^1.1.3" } }, - "node_modules/forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", - "engines": { - "node": "*" - } - }, "node_modules/form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.6", @@ -1871,6 +1838,17 @@ "node": ">= 0.12" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/formidable": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.6.tgz", @@ -1995,14 +1973,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", - "dependencies": { - "assert-plus": "^1.0.0" - } - }, "node_modules/glob": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", @@ -2109,27 +2079,6 @@ "node": ">=4.x" } }, - "node_modules/har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", - "engines": { - "node": ">=4" - } - }, - "node_modules/har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "deprecated": "this library is no longer supported", - "dependencies": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -2232,20 +2181,6 @@ "node": ">= 0.8" } }, - "node_modules/http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", - "dependencies": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - }, - "engines": { - "node": ">=0.8", - "npm": ">=1.3.7" - } - }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -2266,6 +2201,12 @@ "node": ">= 4" } }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -2637,11 +2578,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" - }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -2678,11 +2614,6 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, - "node_modules/isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" - }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -2694,26 +2625,17 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" - }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, - "node_modules/json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" - }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -2737,20 +2659,6 @@ "node": ">=6" } }, - "node_modules/jsprim": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", - "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", - "dependencies": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/just-extend": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", @@ -3237,6 +3145,41 @@ "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==" }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/node-gyp-build-optional-packages": { "version": "5.0.7", "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.7.tgz", @@ -3248,23 +3191,94 @@ "node-gyp-build-optional-packages-test": "build-test.js" } }, - "node_modules/normalize-path": { + "node_modules/nodemon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.1.tgz", + "integrity": "sha512-g9AZ7HmkhQkqXkRc20w+ZfQ73cHLbE8hnPbtaFbFtCumZsjyMhKk9LajQ07U5Ux28lvFjZ5X7HvWR1xzU8jHVw==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^3.2.7", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/nodemon/node_modules/has-flag": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" } }, - "node_modules/oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "node_modules/nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "dev": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, "engines": { "node": "*" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", @@ -3478,11 +3492,6 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, - "node_modules/performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" - }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -3530,15 +3539,17 @@ "node": ">= 0.10" } }, - "node_modules/psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, "engines": { "node": ">=6" } @@ -3696,54 +3707,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", - "dependencies": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/request/node_modules/qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/request/node_modules/uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", - "bin": { - "uuid": "bin/uuid" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -4049,6 +4012,18 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/sinon": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/sinon/-/sinon-12.0.1.tgz", @@ -4067,30 +4042,6 @@ "url": "https://opencollective.com/sinon" } }, - "node_modules/sshpk": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", - "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", - "dependencies": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - }, - "bin": { - "sshpk-conv": "bin/sshpk-conv", - "sshpk-sign": "bin/sshpk-sign", - "sshpk-verify": "bin/sshpk-verify" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", @@ -4322,16 +4273,16 @@ "node": ">=0.6" } }, - "node_modules/tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "node_modules/touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, "dependencies": { - "psl": "^1.1.28", - "punycode": "^2.1.1" + "nopt": "~1.0.10" }, - "engines": { - "node": ">=0.8" + "bin": { + "nodetouch": "bin/nodetouch.js" } }, "node_modules/triple-beam": { @@ -4371,22 +4322,6 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -4512,6 +4447,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -4524,6 +4465,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, "dependencies": { "punycode": "^2.1.0" } @@ -4561,24 +4503,14 @@ "node": ">= 0.8" } }, - "node_modules/verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", - "engines": [ - "node >=0.6.0" - ], - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" + "node_modules/web-streams-polyfill": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", + "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", + "engines": { + "node": ">= 8" } }, - "node_modules/verror/node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 803dccd..ef68247 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "type": "module", "scripts": { "start": "node app.js", + "dev-start": "nodemon --watch src --ext js,json,yml,yaml --exec node app.js", "test": "ALLOW_CONFIG_MUTATIONS=true mocha", "lint": "./node_modules/.bin/eslint ./", "lint:file": "./node_modules/.bin/eslint" @@ -20,8 +21,8 @@ "express": "^4.18.2", "js-yaml": "^4.1.0", "nock": "^13.2.4", + "node-fetch": "^3.3.2", "redis": "^4.6.8", - "request": "^2.88.2", "sha1": "^1.1.1", "uuid": "^9.0.1", "winston": "^3.10.0" @@ -33,6 +34,7 @@ "eslint": "^8.49.0", "eslint-plugin-import": "^2.28.1", "mocha": "^9.2.2", + "nodemon": "^3.0.1", "sinon": "^12.0.1", "supertest": "^3.4.2" } diff --git a/src/out/webhooks/callback-emitter.js b/src/out/webhooks/callback-emitter.js index 5cb91ef..ed9fb7f 100644 --- a/src/out/webhooks/callback-emitter.js +++ b/src/out/webhooks/callback-emitter.js @@ -1,8 +1,8 @@ -import request from 'request'; import url from 'url'; import { EventEmitter } from 'node:events'; import { newLogger } from '../../common/logger.js'; import Utils from '../../common/utils.js'; +import fetch from 'node-fetch'; const Logger = newLogger('callback-emitter'); @@ -25,7 +25,7 @@ const simplifiedEvent = (event) => { // Emits "success" on success, "failure" on error and "stopped" when gave up trying // to perform the callback. export default class CallbackEmitter extends EventEmitter { - constructor(callbackURL, event, permanent, options = {}) { + constructor(callbackURL, event, permanent, domain, options = {}) { super(); this.callbackURL = callbackURL; this.event = event; @@ -33,11 +33,19 @@ export default class CallbackEmitter extends EventEmitter { this.nextInterval = 0; this.timestamp = 0; this.permanent = permanent; + this._serverDomain = domain; + + if (callbackURL == null + || event == null + || domain == null + || domain == null) { + throw new Error("missing parameters"); + } this._permanentIntervalReset = options.permanentIntervalReset || 8; - this._serverDomain = options.domain; this._secret = options.secret; this._bearerAuth = options.auth2_0; + if (this._bearerAuth && this._secret == null) throw new Error("missing secret"); this._requestTimeout = options.requestTimeout; this._retryIntervals = options.retryIntervals || [ 1000, @@ -55,38 +63,35 @@ export default class CallbackEmitter extends EventEmitter { } _scheduleNext(timeout) { - setTimeout( () => { - this._emitMessage((error, result) => { - if ((error == null) && result) { - this.emit("success"); + setTimeout(async () => { + try { + await this._emitMessage(); + this.emit("success"); + } catch (error) { + this.emit("failure", error); + // get the next interval we have to wait and schedule a new try + const interval = this._retryIntervals[this.nextInterval]; + + if (interval != null) { + Logger.warn(`trying the callback again in ${interval/1000.0} secs: ${this.callbackURL}`); + this.nextInterval++; + this._scheduleNext(interval); + // no intervals anymore, time to give up } else { - this.emit("failure", error); - - // get the next interval we have to wait and schedule a new try - const interval = this._retryIntervals[this.nextInterval]; - if (interval != null) { - Logger.warn(`trying the callback again in ${interval/1000.0} secs`); - this.nextInterval++; - this._scheduleNext(interval); + this.nextInterval = this._permanentIntervalReset; - // no intervals anymore, time to give up + if (this.permanent){ + this._scheduleNext(this.nextInterval); } else { - this.nextInterval = this._permanentIntervalReset; - if(this.permanent){ - this._scheduleNext(this.nextInterval); - } - else { - this.emit("stopped"); - } + this.emit("stopped"); } } - }); - } - , timeout); + } + }, timeout); } - _emitMessage(callback) { - let data, requestOptions; + async _emitMessage() { + let data, requestOptions, callbackURL; const serverDomain = this._serverDomain; const sharedSecret = this._secret; const bearerAuth = this._bearerAuth; @@ -94,68 +99,65 @@ export default class CallbackEmitter extends EventEmitter { // data to be sent // note: keep keys in alphabetical order - data = { + data = new URLSearchParams({ event: "[" + this.message + "]", timestamp: this.timestamp, domain: serverDomain + }); + requestOptions = { + method: "POST", + body: data, + redirect: 'follow', + follow: 10, + // FIXME review - compress should be on? + compress: false, + timeout, }; if (bearerAuth) { - const callbackURL = this.callbackURL; - - requestOptions = { - followRedirect: true, - maxRedirects: 10, - uri: callbackURL, - method: "POST", - form: data, - auth: { - bearer: sharedSecret - }, - timeout + callbackURL = this.callbackURL; + requestOptions.headers = { + Authorization: `Bearer ${sharedSecret}`, }; - } - else { - // calculate the checksum + } else { const checksum = Utils.checksum(`${this.callbackURL}${JSON.stringify(data)}${sharedSecret}`); - // get the final callback URL, including the checksum - - let callbackURL = this.callbackURL; + callbackURL = this.callbackURL; try { const urlObj = url.parse(this.callbackURL, true); callbackURL += Utils.isEmpty(urlObj.search) ? "?" : "&"; callbackURL += `checksum=${checksum}`; - } catch (e) { + } catch (error) { Logger.error(`error parsing callback URL: ${this.callbackURL}`); - callback(e, false); - return; + throw error; } - - requestOptions = { - followRedirect: true, - maxRedirects: 10, - uri: callbackURL, - method: "POST", - form: data, - timeout - }; } const responseFailed = (response) => { - var statusCode = (response != null ? response.statusCode : undefined) - // consider 401 as success, because the callback worked but was denied by the recipient - return !((statusCode >= 200 && statusCode < 300) || statusCode == 401) + // consider 401 as success, because the callback worked but was denied by the recipient + return !(response.ok || response.status == 401) }; - request(requestOptions, (error, response) => { - if ((error != null) || responseFailed(response)) { - Logger.warn(`error in the callback call to: [${requestOptions.uri}] for ${simplifiedEvent(data)} error: ${error} status: ${response != null ? response.statusCode : undefined}`); - callback(error, false); + const controller = new AbortController(); + const abortTimeout = setTimeout(() => { + controller.abort(); + }, timeout); + requestOptions.signal = controller.signal; + + try { + const response = await fetch(callbackURL, requestOptions); + + if (responseFailed(response)) { + Logger.warn(`error in the callback call to: [${callbackURL}] for ${simplifiedEvent(data)} status: ${response != null ? response.status: undefined}`); + throw new Error(response.statusText); } else { - Logger.info(`successful callback call to: [${requestOptions.uri}] for ${simplifiedEvent(data)}`); - callback(null, true); + Logger.info(`successful callback call to: [${callbackURL}] for ${simplifiedEvent(data)}`); } - }); + } catch (error) { + Logger.warn(`error in the callback call to: [${callbackURL}] for ${simplifiedEvent(data)}`, error); + throw error; + } finally { + clearTimeout(abortTimeout); + } } } diff --git a/src/out/webhooks/web-hooks.js b/src/out/webhooks/web-hooks.js index 7053a64..8ac4c0f 100644 --- a/src/out/webhooks/web-hooks.js +++ b/src/out/webhooks/web-hooks.js @@ -74,11 +74,11 @@ export default class WebHooks { const emitter = new CallbackEmitter( hook.payload.callbackURL, event, - hook.payload.permanent, { + hook.payload.permanent, + this.config.server.domain, { permanentIntervalReset: this.config.permanentIntervalReset, - domain: this.config.domain, - secret: this.config.secret, - auth2_0: this.config.auth2_0, + secret: this.config.server.secret, + auth2_0: this.config.server.auth2_0, requestTimeout: this.config.requestTimeout, retryIntervals: this.config.retryIntervals, } From 328cc2ce73b8f4de66d4284addcd998c393c870f Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Fri, 29 Sep 2023 00:02:40 -0300 Subject: [PATCH 030/154] chore: move nock and body-parser to devDependencies --- package-lock.json | 15 +++++++++++---- package.json | 4 ++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 308a958..49d80ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,12 +8,10 @@ "name": "bbb-webhooks", "version": "2.6.0", "dependencies": { - "body-parser": "^1.20.0", "bullmq": "^4.11.4", "config": "^3.3.7", "express": "^4.18.2", "js-yaml": "^4.1.0", - "nock": "^13.2.4", "node-fetch": "^3.3.2", "redis": "^4.6.8", "sha1": "^1.1.1", @@ -21,9 +19,11 @@ "winston": "^3.10.0" }, "devDependencies": { + "body-parser": "^1.20.2", "eslint": "^8.49.0", "eslint-plugin-import": "^2.28.1", "mocha": "^9.2.2", + "nock": "^13.3.3", "nodemon": "^3.0.1", "sinon": "^12.0.1", "supertest": "^3.4.2" @@ -671,6 +671,7 @@ "version": "1.20.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "dev": true, "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -2646,7 +2647,8 @@ "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true }, "node_modules/json5": { "version": "2.2.3", @@ -3109,6 +3111,7 @@ "version": "13.3.3", "resolved": "https://registry.npmjs.org/nock/-/nock-13.3.3.tgz", "integrity": "sha512-z+KUlILy9SK/RjpeXDiDUEAq4T94ADPHE3qaRkf66mpEhzc/ytOMm3Bwdrbq6k1tMWkbdujiKim3G2tfQARuJw==", + "dev": true, "dependencies": { "debug": "^4.1.0", "json-stringify-safe": "^5.0.1", @@ -3123,6 +3126,7 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -3138,7 +3142,8 @@ "node_modules/nock/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true }, "node_modules/node-abort-controller": { "version": "3.1.1", @@ -3523,6 +3528,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true, "engines": { "node": ">= 8" } @@ -3609,6 +3615,7 @@ "version": "2.5.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", diff --git a/package.json b/package.json index ef68247..f80768c 100644 --- a/package.json +++ b/package.json @@ -15,12 +15,10 @@ "webhooks" ], "dependencies": { - "body-parser": "^1.20.0", "bullmq": "^4.11.4", "config": "^3.3.7", "express": "^4.18.2", "js-yaml": "^4.1.0", - "nock": "^13.2.4", "node-fetch": "^3.3.2", "redis": "^4.6.8", "sha1": "^1.1.1", @@ -31,9 +29,11 @@ "node": ">=18" }, "devDependencies": { + "body-parser": "^1.20.2", "eslint": "^8.49.0", "eslint-plugin-import": "^2.28.1", "mocha": "^9.2.2", + "nock": "^13.3.3", "nodemon": "^3.0.1", "sinon": "^12.0.1", "supertest": "^3.4.2" From ca9cd08bf9b07769ff9ed19fa3ab11b8dc585e88 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Fri, 29 Sep 2023 12:54:23 -0300 Subject: [PATCH 031/154] fix: restore stale mapping cleanup function --- src/db/redis/base-storage.js | 1 + src/db/redis/id-mapping.js | 9 +++++++-- src/db/redis/user-mapping.js | 5 +++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/db/redis/base-storage.js b/src/db/redis/base-storage.js index d40ca9b..b66ffd5 100644 --- a/src/db/redis/base-storage.js +++ b/src/db/redis/base-storage.js @@ -182,6 +182,7 @@ class StorageCompartmentKV { return this.localStorage[internal] && this.localStorage[internal]?.payload[field] === value; }).map(internal => { let mapping = this.localStorage[internal]; + if (mapping.payload[field] === value) { return mapping.destroy() .then(() => { diff --git a/src/db/redis/id-mapping.js b/src/db/redis/id-mapping.js index 3d897ee..fbedb45 100644 --- a/src/db/redis/id-mapping.js +++ b/src/db/redis/id-mapping.js @@ -86,8 +86,13 @@ class IDMappingCompartment extends StorageCompartmentKV { if (toRemove && toRemove.length > 0) { this.logger.info(`expiring the mappings: ${toRemove.map(map => map.print())}`); toRemove.forEach(mapping => { - UserMapping.removeMappingMeetingId(mapping.internalMeetingID); - mapping.destroy() + UserMapping.get().removeMappingWithMeetingId(mapping.payload.internalMeetingID).catch((error) => { + this.logger.error(`error removing user mapping for ${mapping.payload.internalMeetingID}`, error); + }).finally(() => { + this.destroy(mapping.id).catch((error) => { + this.logger.error(`error removing mapping for ${mapping.id}`, error); + }); + }); }); } } diff --git a/src/db/redis/user-mapping.js b/src/db/redis/user-mapping.js index 617762a..5924bb2 100644 --- a/src/db/redis/user-mapping.js +++ b/src/db/redis/user-mapping.js @@ -31,6 +31,11 @@ class UserMappingCompartment extends StorageCompartmentKV { return result; } + async removeMappingWithMeetingId(meetingId) { + const result = await this.destroyWithField('meetingId', meetingId); + return result; + } + async getInternalMeetingID(externalMeetingID) { const mapping = await this.findByField('externalMeetingID', externalMeetingID); return (mapping != null ? mapping.payload?.internalMeetingID : undefined); From e6525146156feab6d212a2fed5137b4b5d1e06f9 Mon Sep 17 00:00:00 2001 From: miguel-mconf Date: Mon, 2 Oct 2023 10:19:09 -0300 Subject: [PATCH 032/154] Initial commit on xAPI module --- app.js | 0 package-lock.json | 1 + package.json | 1 + src/out/xapi/compartment.js | 39 ++++++++++++++ src/out/xapi/index.js | 40 ++++++++++++++- src/out/xapi/templates.js | 100 ++++++++++++++++++++++++++++++++++++ src/out/xapi/xapi.js | 84 ++++++++++++++++++++++++++++++ 7 files changed, 264 insertions(+), 1 deletion(-) mode change 100755 => 100644 app.js create mode 100644 src/out/xapi/compartment.js create mode 100644 src/out/xapi/templates.js create mode 100644 src/out/xapi/xapi.js diff --git a/app.js b/app.js old mode 100755 new mode 100644 diff --git a/package-lock.json b/package-lock.json index 49d80ed..6e42e85 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "config": "^3.3.7", "express": "^4.18.2", "js-yaml": "^4.1.0", + "luxon": "^3.4.3", "node-fetch": "^3.3.2", "redis": "^4.6.8", "sha1": "^1.1.1", diff --git a/package.json b/package.json index f80768c..93112c6 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "config": "^3.3.7", "express": "^4.18.2", "js-yaml": "^4.1.0", + "luxon": "^3.4.3", "node-fetch": "^3.3.2", "redis": "^4.6.8", "sha1": "^1.1.1", diff --git a/src/out/xapi/compartment.js b/src/out/xapi/compartment.js new file mode 100644 index 0000000..6adbcea --- /dev/null +++ b/src/out/xapi/compartment.js @@ -0,0 +1,39 @@ +import { StorageCompartmentKV } from '../../db/redis/base-storage.js'; + +export default class XAPICompartment extends StorageCompartmentKV { + constructor(client, prefix, setId, options = {}) { + super(client, prefix, setId, options); + } + + async addOrUpdateMeetingData(meeting_data) { + const {internal_meeting_id, context_registration, bbb_origin_server_name, + planned_duration, create_time, meeting_name} = meeting_data; + + const payload = { + internal_meeting_id, + context_registration, + bbb_origin_server_name, + planned_duration, + create_time, + meeting_name, + }; + + const mapping = await this.save(payload, { + alias: internal_meeting_id, + }); + this.logger.info(`added user mapping to the list ${internal_meeting_id}: ${mapping.print()}`); + + return mapping; + } + + async getMeetingData(internal_meeting_id) { + const meeting_data = this.findByField('internal_meeting_id', internal_meeting_id); + return (meeting_data != null ? meeting_data.payload : undefined); + } + + // Initializes global methods for this model. + initialize() { + // return this.resync(); + return; + } +} \ No newline at end of file diff --git a/src/out/xapi/index.js b/src/out/xapi/index.js index 4c7a507..dd0031c 100644 --- a/src/out/xapi/index.js +++ b/src/out/xapi/index.js @@ -1,3 +1,7 @@ +import XAPI from './xapi.js'; +import XAPICompartment from './compartment.js'; +import redis from 'redis'; +import config from 'config'; /* * [MODULE_TYPES.OUTPUT]: { * load: 'function', @@ -21,11 +25,45 @@ export default class OutXAPI { this.loaded = false; } + _validateConfig () { + if (this.config == null) { + throw new Error("config not set"); + } + + // TODO + + return true; + } + async load () { + if (this._validateConfig()) { + this.redisClient = redis.createClient({ + host: config.get('redis.host'), + port: config.get('redis.port'), + password: config.has('redis.password') ? config.get('redis.password') : undefined, + }); + + await this.redisClient.connect(); + + this.logger.debug('OutXAPI.onEvent:', this.config ); + + this.meetingStorage = new XAPICompartment( + this.redisClient, + this.config.redis.keys.meetingPrefix, + this.config.redis.keys.meetings + ); + + this.xAPI = new XAPI(this.context, this.config, this.meetingStorage); + } this.loaded = true; } async unload () { + if (this.redisClient != null) { + await this.redisClient.disconnect(); + this.redisClient = null; + } + this.setCollector(OutXAPI._defaultCollector); this.loaded = false; } @@ -42,6 +80,6 @@ export default class OutXAPI { throw new Error("OutXAPI not loaded"); } - this.logger.debug('OutXAPI.onEvent:', event); + return this.xAPI.onEvent(event, raw); } } diff --git a/src/out/xapi/templates.js b/src/out/xapi/templates.js new file mode 100644 index 0000000..b1fb3f1 --- /dev/null +++ b/src/out/xapi/templates.js @@ -0,0 +1,100 @@ +import { DateTime, Duration } from 'luxon'; + +export default function getXAPIStatement(event, meeting_data){ + const { bbb_origin_server_name, + object_id, + meeting_name, + context_registration, + session_id, + planned_duration, + create_time} = meeting_data; + + const planned_duration_ISO = Duration.fromObject({ minutes: planned_duration }).toISO(); + const create_time_ISO = DateTime.fromMillis(create_time).toUTC().toISO(); + + const event_ts = event.data.event.ts; + + if(event.data.id == 'meeting-created'){ + return { + "actor": { + "account": { + "name": "", + "homePage": `https://${bbb_origin_server_name}` + } + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/initialized" + }, + "object": { + "id": `https://${bbb_origin_server_name}/xapi/activities/${object_id}`, + "definition": { + "type": "https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom", + "name": { + "en": meeting_name + } + } + }, + "context": { + "registration": context_registration, + "contextActivities": { + "category": [ + { + "id": "https://w3id.org/xapi/virtual-classroom", + "definition": { + "type": "http://adlnet.gov/expapi/activities/profile" + } + } + ] + }, + "extensions": { + "http://id.tincanapi.com/extension/planned-duration": planned_duration_ISO, + "https://w3id.org/xapi/cmi5/context/extensions/sessionid": session_id + } + }, + "timestamp": create_time_ISO + } + } + else if(event.data.id == 'meeting-ended'){ + return { + "actor": { + "account": { + "name": "", + "homePage": `https://${bbb_origin_server_name}` + } + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/terminated" + }, + "object": { + "id": `https://${bbb_origin_server_name}/xapi/activities/${object_id}`, + "definition": { + "type": "https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom", + "name": { + "en": meeting_name + } + } + }, + "result": { + "duration": Duration.fromMillis(event_ts - create_time).toISO() + }, + "context": { + "registration": context_registration, + "contextActivities": { + "category": [ + { + "id": "https://w3id.org/xapi/virtual-classroom", + "definition": { + "type": "http://adlnet.gov/expapi/activities/profile" + } + } + ] + }, + "extensions": { + "http://id.tincanapi.com/extension/planned-duration": planned_duration_ISO, + "https://w3id.org/xapi/cmi5/context/extensions/sessionid": session_id + } + }, + "timestamp": DateTime.fromMillis(event_ts).toUTC().toISO() + } + } +} \ No newline at end of file diff --git a/src/out/xapi/xapi.js b/src/out/xapi/xapi.js new file mode 100644 index 0000000..7569304 --- /dev/null +++ b/src/out/xapi/xapi.js @@ -0,0 +1,84 @@ +import getXAPIStatement from './templates.js'; +import { v5 as uuidv5 } from 'uuid'; +import { DateTime } from 'luxon'; +import fetch from 'node-fetch'; + +// XAPI will listen for events on redis coming from BigBlueButton, +// generate xAPI statements and send to a LRS +export default class XAPI { + constructor(context, config, meetingStorage) { + this.context = context; + this.logger = context.getLogger(); + this.config = config; + this.meetingStorage = meetingStorage; + } + + async postToLRS(statement){ + const {url, username, password} = this.config.xapi.lrs; + + const headers = { + "Authorization": `Basic ${Buffer.from(username + ":" + password).toString('base64')}`, + "Content-Type": "application/json", + "X-Experience-API-Version": "1.0.0", + } + + const requestOptions = { + method: "POST", + body: JSON.stringify(statement), + headers, + }; + + const xAPIEndpoint = new URL("xAPI/statements", url); + + try { + const response = await fetch(xAPIEndpoint, requestOptions); + const { status } = response; + const data = await response.json(); + this.logger.debug('OutXAPI.res.status:', {status, data} ); + } catch (err) { + // handle error + this.logger.debug('OutXAPI.err:', err ); + } + } + + async onEvent(event, raw) { + const meeting_data = { + internal_meeting_id: event.data.attributes.meeting["internal-meeting-id"], + external_meeting_id: event.data.attributes.meeting["external-meeting-id"], + uuid_namespace: this.config.xapi.uuid_namespace + } + + meeting_data.session_id = uuidv5(meeting_data.internal_meeting_id, meeting_data.uuid_namespace); + meeting_data.object_id = uuidv5(meeting_data.external_meeting_id, meeting_data.uuid_namespace); + + // if meeting-created event, set parameters + if (event.data.id == 'meeting-created') { + meeting_data.bbb_origin_server_name = event.data.attributes.meeting.metadata["bbb-origin-server-name"]; + meeting_data.planned_duration = event.data.attributes.meeting.duration; + meeting_data.create_time = event.data.attributes.meeting["create-time"]; + meeting_data.meeting_name = event.data.attributes.meeting.name; + + const meeting_create_day = DateTime.fromMillis(meeting_data.create_time).toFormat('yyyyMMdd'); + const external_key = `${meeting_data.external_meeting_id}_${meeting_create_day}`; + + meeting_data.context_registration = uuidv5(external_key, meeting_data.uuid_namespace); + + await this.meetingStorage.addOrUpdateMeetingData(meeting_data); + const XAPIStatement = getXAPIStatement(event, meeting_data); + // const meeting_data_storage = await this.meetingStorage.getMeetingData(meeting_data.internal_meeting_id); + await this.postToLRS(XAPIStatement); + } + // for other events, read parameters from redis + else { + // this.logger.debug('OutXAPI.meeting_ended_data:', {int_meet_id: meeting_data.internal_meeting_id} ); + const meeting_data_storage = await this.meetingStorage.getMeetingData(meeting_data.internal_meeting_id); + Object.assign(meeting_data, meeting_data_storage); + + if (event.data.id == 'meeting-ended'){ + const XAPIStatement = getXAPIStatement(event, meeting_data); + await this.postToLRS(XAPIStatement); + } + } + + } +} \ No newline at end of file From 2d07e3f3c04f24f5665781d150ae7c3798eb0ccd Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Mon, 2 Oct 2023 15:39:03 -0300 Subject: [PATCH 033/154] chore: add metadata to event examples --- example/events/mapped-events.json | 42 +++++++++++++++---------------- example/events/raw-events.json | 42 +++++++++++++++---------------- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/example/events/mapped-events.json b/example/events/mapped-events.json index d63a3a3..fc2a7aa 100644 --- a/example/events/mapped-events.json +++ b/example/events/mapped-events.json @@ -1,21 +1,21 @@ -{"data":{"type":"event","id":"meeting-created","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","external-meeting-id":"random-9301511","name":"random-9301511","is-breakout":false,"parent-id":"bbb-none","duration":0,"create-time":1695947505532,"create-date":"Thu Sep 28 21:31:45 BRT 2023","moderator-pass":"mp","viewer-pass":"ap","record":false,"voice-conf":"74692","dial-number":"613-555-1234","max-users":0,"metadata":{}}},"event":{"ts":1695947505562}}} -{"data":{"type":"event","id":"user-presenter-assigned","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_pjtppf6m5scn","external-user-id":""}},"event":{"ts":1695947510338}}} -{"data":{"type":"event","id":"user-joined","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_pjtppf6m5scn","external-user-id":"w_pjtppf6m5scn","name":"User 9165940","role":"MODERATOR","presenter":"false"}},"event":{"ts":1695947510336}}} -{"data":{"type":"event","id":"user-presenter-assigned","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_pjtppf6m5scn","external-user-id":"w_pjtppf6m5scn"}},"event":{"ts":1695947510358}}} -{"data":{"type":"event","id":"user-presenter-assigned","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_pjtppf6m5scn","external-user-id":"w_pjtppf6m5scn"}},"event":{"ts":1695947510363}}} -{"data":{"type":"event","id":"user-joined","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_l7zixn2bdi9n","external-user-id":"w_l7zixn2bdi9n","name":"User 9165940","role":"VIEWER","presenter":"false"}},"event":{"ts":1695947515826}}} -{"data":{"type":"event","id":"user-audio-voice-enabled","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_pjtppf6m5scn","external-user-id":"w_pjtppf6m5scn","listening-only":false,"sharing-mic":true}},"event":{"ts":1695947519365}}} -{"data":{"type":"event","id":"user-audio-voice-disabled","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_pjtppf6m5scn","external-user-id":"w_pjtppf6m5scn","listening-only":false,"sharing-mic":false}},"event":{"ts":1695947523039}}} -{"data":{"type":"event","id":"user-cam-broadcast-start","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_pjtppf6m5scn","external-user-id":"w_pjtppf6m5scn","stream":"w_pjtppf6m5scn_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}},"event":{"ts":1695947527033}}} -{"data":{"type":"event","id":"user-cam-broadcast-end","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_pjtppf6m5scn","external-user-id":"w_pjtppf6m5scn","stream":"w_pjtppf6m5scn_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}},"event":{"ts":1695947529391}}} -{"data":{"type":"event","id":"meeting-screenshare-started","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","external-meeting-id":"random-9301511"}},"event":{"ts":1695947532594}}} -{"data":{"type":"event","id":"meeting-screenshare-stopped","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","external-meeting-id":"random-9301511"}},"event":{"ts":1695947536147}}} -{"data":{"type":"event","id":"chat-group-message-sent","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","external-meeting-id":"random-9301511"},"chat-message":{"id":"1695947543829-zpiq308w","message":"Public chat test","sender":{"internal-user-id":"w_pjtppf6m5scn","name":"User 9165940","time":1695947543829}},"chat-id":"MAIN-PUBLIC-GROUP-CHAT"},"event":{"ts":1695947543832}}} -{"data":{"type":"event","id":"user-emoji-changed","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_pjtppf6m5scn","external-user-id":"w_pjtppf6m5scn","emoji":"😃"}},"event":{"ts":1695947549881}}} -{"data":{"type":"event","id":"user-emoji-changed","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_pjtppf6m5scn","external-user-id":"w_pjtppf6m5scn","emoji":"none"}},"event":{"ts":1695947552889}}} -{"data":{"type":"event","id":"user-raise-hand-changed","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_pjtppf6m5scn","external-user-id":"w_pjtppf6m5scn","raise-hand":true}},"event":{"ts":1695947556279}}} -{"data":{"type":"event","id":"user-raise-hand-changed","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_pjtppf6m5scn","external-user-id":"w_pjtppf6m5scn","raise-hand":false}},"event":{"ts":1695947560399}}} -{"data":{"type":"event","id":"poll-started","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_pjtppf6m5scn","external-user-id":"w_pjtppf6m5scn"},"poll":{"question":"ABCD Poll test (public)","answers":[{"id":0,"key":"A"},{"id":1,"key":"B"},{"id":2,"key":"C"},{"id":3,"key":"D"}]}},"event":{"ts":1695947575622}}} -{"data":{"type":"event","id":"poll-responded","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_pjtppf6m5scn","external-user-id":"w_pjtppf6m5scn"},"poll":{"id":"4b6fb9fdf792ba4bd31bf443919a874d60b5b94b-1695947505534/1/1695947575620","answerIds":[0]}},"event":{"ts":1695947581958}}} -{"data":{"type":"event","id":"user-left","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","external-meeting-id":"random-9301511"},"user":{"internal-user-id":"w_l7zixn2bdi9n","external-user-id":"w_l7zixn2bdi9n"}},"event":{"ts":1695947600568}}} -{"data":{"type":"event","id":"meeting-ended","attributes":{"meeting":{"internal-meeting-id":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","external-meeting-id":"random-9301511"}},"event":{"ts":1695947607778}}} +{"data":{"type":"event","id":"meeting-created","attributes":{"meeting":{"internal-meeting-id":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","external-meeting-id":"random-6696736","name":"random-6696736","is-breakout":false,"parent-id":"bbb-none","duration":0,"create-time":1696271692885,"create-date":"Mon Oct 02 15:34:52 BRT 2023","moderator-pass":"mp","viewer-pass":"ap","record":false,"voice-conf":"79025","dial-number":"613-555-1234","max-users":0,"metadata":{"test-meta-param":"\"test-param\""}}},"event":{"ts":1696271692893}}} +{"data":{"type":"event","id":"user-presenter-assigned","attributes":{"meeting":{"internal-meeting-id":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","external-meeting-id":"random-6696736"},"user":{"internal-user-id":"w_gydstnfg3p5u","external-user-id":""}},"event":{"ts":1696271703903}}} +{"data":{"type":"event","id":"user-joined","attributes":{"meeting":{"internal-meeting-id":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","external-meeting-id":"random-6696736"},"user":{"internal-user-id":"w_gydstnfg3p5u","external-user-id":"w_gydstnfg3p5u","name":"User 2261564","role":"MODERATOR","presenter":"false"}},"event":{"ts":1696271703899}}} +{"data":{"type":"event","id":"user-presenter-assigned","attributes":{"meeting":{"internal-meeting-id":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","external-meeting-id":"random-6696736"},"user":{"internal-user-id":"w_gydstnfg3p5u","external-user-id":"w_gydstnfg3p5u"}},"event":{"ts":1696271703938}}} +{"data":{"type":"event","id":"user-presenter-assigned","attributes":{"meeting":{"internal-meeting-id":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","external-meeting-id":"random-6696736"},"user":{"internal-user-id":"w_gydstnfg3p5u","external-user-id":"w_gydstnfg3p5u"}},"event":{"ts":1696271703947}}} +{"data":{"type":"event","id":"user-joined","attributes":{"meeting":{"internal-meeting-id":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","external-meeting-id":"random-6696736"},"user":{"internal-user-id":"w_8lvidy41zal2","external-user-id":"w_8lvidy41zal2","name":"User 2261564","role":"VIEWER","presenter":"false"}},"event":{"ts":1696271711011}}} +{"data":{"type":"event","id":"user-audio-voice-enabled","attributes":{"meeting":{"internal-meeting-id":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885"},"user":{"internal-user-id":"w_gydstnfg3p5u","external-user-id":"","listening-only":false,"sharing-mic":true}},"event":{"ts":1696271733563}}} +{"data":{"type":"event","id":"user-audio-voice-disabled","attributes":{"meeting":{"internal-meeting-id":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885"},"user":{"internal-user-id":"w_gydstnfg3p5u","external-user-id":"","listening-only":false,"sharing-mic":false}},"event":{"ts":1696271736375}}} +{"data":{"type":"event","id":"user-cam-broadcast-start","attributes":{"meeting":{"internal-meeting-id":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885"},"user":{"internal-user-id":"w_gydstnfg3p5u","external-user-id":"","stream":"w_gydstnfg3p5u_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}},"event":{"ts":1696271740868}}} +{"data":{"type":"event","id":"user-cam-broadcast-end","attributes":{"meeting":{"internal-meeting-id":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885"},"user":{"internal-user-id":"w_gydstnfg3p5u","external-user-id":"","stream":"w_gydstnfg3p5u_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}},"event":{"ts":1696271742391}}} +{"data":{"type":"event","id":"meeting-screenshare-started","attributes":{"meeting":{"internal-meeting-id":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885"}},"event":{"ts":1696271746688}}} +{"data":{"type":"event","id":"meeting-screenshare-stopped","attributes":{"meeting":{"internal-meeting-id":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885"}},"event":{"ts":1696271749156}}} +{"data":{"type":"event","id":"chat-group-message-sent","attributes":{"meeting":{"internal-meeting-id":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885"},"chat-message":{"id":"1696271758851-0a25o7ou","message":"Public chat test","sender":{"internal-user-id":"w_gydstnfg3p5u","name":"User 2261564","time":1696271758851}},"chat-id":"MAIN-PUBLIC-GROUP-CHAT"},"event":{"ts":1696271759784}}} +{"data":{"type":"event","id":"user-emoji-changed","attributes":{"meeting":{"internal-meeting-id":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885"},"user":{"internal-user-id":"w_gydstnfg3p5u","external-user-id":"","emoji":"🙁"}},"event":{"ts":1696271766075}}} +{"data":{"type":"event","id":"user-emoji-changed","attributes":{"meeting":{"internal-meeting-id":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885"},"user":{"internal-user-id":"w_gydstnfg3p5u","external-user-id":"","emoji":"none"}},"event":{"ts":1696271771122}}} +{"data":{"type":"event","id":"user-raise-hand-changed","attributes":{"meeting":{"internal-meeting-id":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885"},"user":{"internal-user-id":"w_gydstnfg3p5u","external-user-id":"","raise-hand":true}},"event":{"ts":1696271773291}}} +{"data":{"type":"event","id":"user-raise-hand-changed","attributes":{"meeting":{"internal-meeting-id":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885"},"user":{"internal-user-id":"w_gydstnfg3p5u","external-user-id":"","raise-hand":false}},"event":{"ts":1696271777873}}} +{"data":{"type":"event","id":"poll-started","attributes":{"meeting":{"internal-meeting-id":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885"},"user":{"internal-user-id":"w_gydstnfg3p5u","external-user-id":""},"poll":{"question":"ABCD Poll test (public","answers":[{"id":0,"key":"A"},{"id":1,"key":"B"},{"id":2,"key":"C"},{"id":3,"key":"D"}]}},"event":{"ts":1696271789475}}} +{"data":{"type":"event","id":"poll-responded","attributes":{"meeting":{"internal-meeting-id":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885"},"user":{"internal-user-id":"w_gydstnfg3p5u","external-user-id":""},"poll":{"id":"2e8114441fe446507e61c6aa3de818f295584fcf-1696271692887/1/1696271789402","answerIds":[0]}},"event":{"ts":1696271792358}}} +{"data":{"type":"event","id":"user-left","attributes":{"meeting":{"internal-meeting-id":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885"},"user":{"internal-user-id":"w_8lvidy41zal2","external-user-id":""}},"event":{"ts":1696271817906}}} +{"data":{"type":"event","id":"meeting-ended","attributes":{"meeting":{"internal-meeting-id":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885"}},"event":{"ts":1696271836396}}} diff --git a/example/events/raw-events.json b/example/events/raw-events.json index d859c92..6124bfc 100644 --- a/example/events/raw-events.json +++ b/example/events/raw-events.json @@ -1,21 +1,21 @@ -{"envelope":{"name":"MeetingCreatedEvtMsg","routing":{"sender":"bbb-apps-akka"},"timestamp":1695947505550},"core":{"header":{"name":"MeetingCreatedEvtMsg"},"body":{"props":{"meetingProp":{"name":"random-9301511","extId":"random-9301511","intId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","meetingCameraCap":50,"maxPinnedCameras":3,"isBreakout":false,"disabledFeatures":[],"notifyRecordingIsOn":false,"presentationUploadExternalDescription":"","presentationUploadExternalUrl":""},"breakoutProps":{"parentId":"bbb-none","sequence":0,"freeJoin":false,"breakoutRooms":[],"record":false,"privateChatEnabled":true,"captureNotes":false,"captureSlides":false,"captureNotesFilename":"%%CONFNAME%%","captureSlidesFilename":"%%CONFNAME%%"},"durationProps":{"duration":0,"createdTime":1695947505532,"createdDate":"Thu Sep 28 21:31:45 BRT 2023","meetingExpireIfNoUserJoinedInMinutes":5,"meetingExpireWhenLastUserLeftInMinutes":1,"userInactivityInspectTimerInMinutes":0,"userInactivityThresholdInMinutes":30,"userActivitySignResponseDelayInMinutes":5,"endWhenNoModerator":false,"endWhenNoModeratorDelayInMinutes":1},"password":{"moderatorPass":"mp","viewerPass":"ap","learningDashboardAccessToken":"nl2q4mdkmcxz"},"recordProp":{"record":false,"autoStartRecording":false,"allowStartStopRecording":true,"recordFullDurationMedia":false,"keepEvents":true},"welcomeProp":{"welcomeMsgTemplate":"
Welcome to %%CONFNAME%%!","welcomeMsg":"
Welcome to random-9301511!","modOnlyMessage":""},"voiceProp":{"telVoice":"74692","voiceConf":"74692","dialNumber":"613-555-1234","muteOnStart":false},"usersProp":{"maxUsers":0,"maxUserConcurrentAccesses":3,"webcamsOnlyForModerator":false,"userCameraCap":3,"guestPolicy":"ASK_MODERATOR","meetingLayout":"CUSTOM_LAYOUT","allowModsToUnmuteUsers":true,"allowModsToEjectCameras":true,"authenticatedGuest":false},"metadataProp":{"metadata":{}},"lockSettingsProps":{"disableCam":false,"disableMic":false,"disablePrivateChat":false,"disablePublicChat":false,"disableNotes":false,"hideUserList":false,"lockOnJoin":true,"lockOnJoinConfigurable":false,"hideViewersCursor":false,"hideViewersAnnotation":false},"systemProps":{"html5InstanceId":1},"groups":[]}}}} -{"envelope":{"name":"PresenterAssignedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"timestamp":1695947510332},"core":{"header":{"name":"PresenterAssignedEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"body":{"presenterId":"w_pjtppf6m5scn","presenterName":"User 9165940","assignedBy":"w_pjtppf6m5scn"}}} -{"envelope":{"name":"UserJoinedMeetingEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"timestamp":1695947510331},"core":{"header":{"name":"UserJoinedMeetingEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"body":{"intId":"w_pjtppf6m5scn","extId":"w_pjtppf6m5scn","name":"User 9165940","role":"MODERATOR","guest":false,"authed":true,"guestStatus":"ALLOW","emoji":"none","reactionEmoji":"none","raiseHand":false,"away":false,"pin":false,"presenter":false,"locked":true,"avatar":"","color":"#7b1fa2","clientType":"HTML5"}}} -{"envelope":{"name":"PresenterAssignedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"timestamp":1695947510357},"core":{"header":{"name":"PresenterAssignedEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"body":{"presenterId":"w_pjtppf6m5scn","presenterName":"User 9165940","assignedBy":"w_pjtppf6m5scn"}}} -{"envelope":{"name":"PresenterAssignedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"timestamp":1695947510362},"core":{"header":{"name":"PresenterAssignedEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"body":{"presenterId":"w_pjtppf6m5scn","presenterName":"User 9165940","assignedBy":"w_pjtppf6m5scn"}}} -{"envelope":{"name":"UserJoinedMeetingEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_l7zixn2bdi9n"},"timestamp":1695947515820},"core":{"header":{"name":"UserJoinedMeetingEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_l7zixn2bdi9n"},"body":{"intId":"w_l7zixn2bdi9n","extId":"w_l7zixn2bdi9n","name":"User 9165940","role":"VIEWER","guest":false,"authed":true,"guestStatus":"ALLOW","emoji":"none","reactionEmoji":"none","raiseHand":false,"away":false,"pin":false,"presenter":false,"locked":true,"avatar":"","color":"#6a1b9a","clientType":"HTML5"}}} -{"envelope":{"name":"UserJoinedVoiceConfToClientEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"timestamp":1695947519363},"core":{"header":{"name":"UserJoinedVoiceConfToClientEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"body":{"voiceConf":"74692","intId":"w_pjtppf6m5scn","voiceUserId":"6","callerName":"User+9165940","callerNum":"w_pjtppf6m5scn_1-bbbID-User+9165940","color":"#4a148c","muted":false,"talking":false,"callingWith":"none","listenOnly":false}}} -{"envelope":{"name":"UserLeftVoiceConfToClientEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"timestamp":1695947523038},"core":{"header":{"name":"UserLeftVoiceConfToClientEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"body":{"voiceConf":"74692","intId":"w_pjtppf6m5scn","voiceUserId":"w_pjtppf6m5scn"}}} -{"envelope":{"name":"UserBroadcastCamStartedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"timestamp":1695947527032},"core":{"header":{"name":"UserBroadcastCamStartedEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"body":{"userId":"w_pjtppf6m5scn","stream":"w_pjtppf6m5scn_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}}} -{"envelope":{"name":"UserBroadcastCamStoppedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"timestamp":1695947529390},"core":{"header":{"name":"UserBroadcastCamStoppedEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"body":{"userId":"w_pjtppf6m5scn","stream":"w_pjtppf6m5scn_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}}} -{"envelope":{"name":"ScreenshareRtmpBroadcastStartedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"not-used"},"timestamp":1695947532592},"core":{"header":{"name":"ScreenshareRtmpBroadcastStartedEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"not-used"},"body":{"voiceConf":"74692","screenshareConf":"74692","stream":"7f3fe80d-823a-430d-aea4-9b4ebce1be12","vidWidth":0,"vidHeight":0,"timestamp":"1695947532592","hasAudio":false,"contentType":"screenshare"}}} -{"envelope":{"name":"ScreenshareRtmpBroadcastStoppedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"not-used"},"timestamp":1695947536146},"core":{"header":{"name":"ScreenshareRtmpBroadcastStoppedEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"not-used"},"body":{"voiceConf":"","screenshareConf":"","stream":"","vidWidth":0,"vidHeight":0,"timestamp":""}}} -{"envelope":{"name":"GroupChatMessageBroadcastEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"timestamp":1695947543830},"core":{"header":{"name":"GroupChatMessageBroadcastEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"body":{"chatId":"MAIN-PUBLIC-GROUP-CHAT","msg":{"id":"1695947543829-zpiq308w","timestamp":1695947543829,"correlationId":"w_pjtppf6m5scn-1695947543794","sender":{"id":"w_pjtppf6m5scn","name":"User 9165940","role":"MODERATOR"},"chatEmphasizedText":true,"message":"Public chat test"}}}} -{"envelope":{"name":"UserReactionEmojiChangedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"timestamp":1695947549880},"core":{"header":{"name":"UserReactionEmojiChangedEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"body":{"userId":"w_pjtppf6m5scn","reactionEmoji":"😃"}}} -{"envelope":{"name":"UserReactionEmojiChangedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"timestamp":1695947552888},"core":{"header":{"name":"UserReactionEmojiChangedEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"body":{"userId":"w_pjtppf6m5scn","reactionEmoji":"none"}}} -{"envelope":{"name":"UserRaiseHandChangedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"timestamp":1695947556278},"core":{"header":{"name":"UserRaiseHandChangedEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"body":{"userId":"w_pjtppf6m5scn","raiseHand":true}}} -{"envelope":{"name":"UserRaiseHandChangedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"timestamp":1695947560397},"core":{"header":{"name":"UserRaiseHandChangedEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"body":{"userId":"w_pjtppf6m5scn","raiseHand":false}}} -{"envelope":{"name":"PollStartedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"timestamp":1695947575620},"core":{"header":{"name":"PollStartedEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"body":{"userId":"w_pjtppf6m5scn","pollId":"4b6fb9fdf792ba4bd31bf443919a874d60b5b94b-1695947505534/1/1695947575620","pollType":"A-4","secretPoll":false,"question":"ABCD Poll test (public)","poll":{"id":"4b6fb9fdf792ba4bd31bf443919a874d60b5b94b-1695947505534/1/1695947575620","isMultipleResponse":false,"answers":[{"id":0,"key":"A"},{"id":1,"key":"B"},{"id":2,"key":"C"},{"id":3,"key":"D"}]}}}} -{"envelope":{"name":"UserRespondedToPollRespMsg","routing":{"msgType":"DIRECT","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"timestamp":1695947581956},"core":{"header":{"name":"UserRespondedToPollRespMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_pjtppf6m5scn"},"body":{"pollId":"4b6fb9fdf792ba4bd31bf443919a874d60b5b94b-1695947505534/1/1695947575620","userId":"w_l7zixn2bdi9n","answerIds":[0]}}} -{"envelope":{"name":"UserLeftMeetingEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_l7zixn2bdi9n"},"timestamp":1695947600566},"core":{"header":{"name":"UserLeftMeetingEvtMsg","meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532","userId":"w_l7zixn2bdi9n"},"body":{"intId":"w_l7zixn2bdi9n","eject":false,"ejectedBy":"","reason":"","reasonCode":""}}} -{"envelope":{"name":"MeetingDestroyedEvtMsg","routing":{"sender":"bbb-apps-akka"},"timestamp":1695947607776},"core":{"header":{"name":"MeetingDestroyedEvtMsg"},"body":{"meetingId":"0d654de7f4929ce679d129e3fd8edb3eb4fb3c20-1695947505532"}}} +{"envelope":{"name":"MeetingCreatedEvtMsg","routing":{"sender":"bbb-apps-akka"},"timestamp":1696271692890},"core":{"header":{"name":"MeetingCreatedEvtMsg"},"body":{"props":{"meetingProp":{"name":"random-6696736","extId":"random-6696736","intId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","meetingCameraCap":50,"maxPinnedCameras":3,"isBreakout":false,"disabledFeatures":[],"notifyRecordingIsOn":false,"presentationUploadExternalDescription":"","presentationUploadExternalUrl":""},"breakoutProps":{"parentId":"bbb-none","sequence":0,"freeJoin":false,"breakoutRooms":[],"record":false,"privateChatEnabled":true,"captureNotes":false,"captureSlides":false,"captureNotesFilename":"%%CONFNAME%%","captureSlidesFilename":"%%CONFNAME%%"},"durationProps":{"duration":0,"createdTime":1696271692885,"createdDate":"Mon Oct 02 15:34:52 BRT 2023","meetingExpireIfNoUserJoinedInMinutes":5,"meetingExpireWhenLastUserLeftInMinutes":1,"userInactivityInspectTimerInMinutes":0,"userInactivityThresholdInMinutes":30,"userActivitySignResponseDelayInMinutes":5,"endWhenNoModerator":false,"endWhenNoModeratorDelayInMinutes":1},"password":{"moderatorPass":"mp","viewerPass":"ap","learningDashboardAccessToken":"fhlxxpwiwwpx"},"recordProp":{"record":false,"autoStartRecording":false,"allowStartStopRecording":true,"recordFullDurationMedia":false,"keepEvents":true},"welcomeProp":{"welcomeMsgTemplate":"
Welcome to %%CONFNAME%%!","welcomeMsg":"
Welcome to random-6696736!","modOnlyMessage":""},"voiceProp":{"telVoice":"79025","voiceConf":"79025","dialNumber":"613-555-1234","muteOnStart":false},"usersProp":{"maxUsers":0,"maxUserConcurrentAccesses":3,"webcamsOnlyForModerator":false,"userCameraCap":3,"guestPolicy":"ASK_MODERATOR","meetingLayout":"CUSTOM_LAYOUT","allowModsToUnmuteUsers":true,"allowModsToEjectCameras":true,"authenticatedGuest":false},"metadataProp":{"metadata":{"test-meta-param":"\"test-param\""}},"lockSettingsProps":{"disableCam":false,"disableMic":false,"disablePrivateChat":false,"disablePublicChat":false,"disableNotes":false,"hideUserList":false,"lockOnJoin":true,"lockOnJoinConfigurable":false,"hideViewersCursor":false,"hideViewersAnnotation":false},"systemProps":{"html5InstanceId":1},"groups":[]}}}} +{"envelope":{"name":"PresenterAssignedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"timestamp":1696271703894},"core":{"header":{"name":"PresenterAssignedEvtMsg","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"body":{"presenterId":"w_gydstnfg3p5u","presenterName":"User 2261564","assignedBy":"w_gydstnfg3p5u"}}} +{"envelope":{"name":"UserJoinedMeetingEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"timestamp":1696271703894},"core":{"header":{"name":"UserJoinedMeetingEvtMsg","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"body":{"intId":"w_gydstnfg3p5u","extId":"w_gydstnfg3p5u","name":"User 2261564","role":"MODERATOR","guest":false,"authed":true,"guestStatus":"ALLOW","emoji":"none","reactionEmoji":"none","raiseHand":false,"away":false,"pin":false,"presenter":false,"locked":true,"avatar":"","color":"#7b1fa2","clientType":"HTML5"}}} +{"envelope":{"name":"PresenterAssignedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"timestamp":1696271703931},"core":{"header":{"name":"PresenterAssignedEvtMsg","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"body":{"presenterId":"w_gydstnfg3p5u","presenterName":"User 2261564","assignedBy":"w_gydstnfg3p5u"}}} +{"envelope":{"name":"PresenterAssignedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"timestamp":1696271703946},"core":{"header":{"name":"PresenterAssignedEvtMsg","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"body":{"presenterId":"w_gydstnfg3p5u","presenterName":"User 2261564","assignedBy":"w_gydstnfg3p5u"}}} +{"envelope":{"name":"UserJoinedMeetingEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_8lvidy41zal2"},"timestamp":1696271711009},"core":{"header":{"name":"UserJoinedMeetingEvtMsg","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_8lvidy41zal2"},"body":{"intId":"w_8lvidy41zal2","extId":"w_8lvidy41zal2","name":"User 2261564","role":"VIEWER","guest":false,"authed":true,"guestStatus":"ALLOW","emoji":"none","reactionEmoji":"none","raiseHand":false,"away":false,"pin":false,"presenter":false,"locked":true,"avatar":"","color":"#6a1b9a","clientType":"HTML5"}}} +{"envelope":{"name":"UserJoinedVoiceConfToClientEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"timestamp":1696271733551},"core":{"header":{"name":"UserJoinedVoiceConfToClientEvtMsg","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"body":{"voiceConf":"79025","intId":"w_gydstnfg3p5u","voiceUserId":"1","callerName":"User+2261564","callerNum":"w_gydstnfg3p5u_2-bbbID-User+2261564","color":"#4a148c","muted":false,"talking":false,"callingWith":"none","listenOnly":false}}} +{"envelope":{"name":"UserLeftVoiceConfToClientEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"timestamp":1696271736360},"core":{"header":{"name":"UserLeftVoiceConfToClientEvtMsg","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"body":{"voiceConf":"79025","intId":"w_gydstnfg3p5u","voiceUserId":"w_gydstnfg3p5u"}}} +{"envelope":{"name":"UserBroadcastCamStartedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"timestamp":1696271740854},"core":{"header":{"name":"UserBroadcastCamStartedEvtMsg","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"body":{"userId":"w_gydstnfg3p5u","stream":"w_gydstnfg3p5u_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}}} +{"envelope":{"name":"UserBroadcastCamStoppedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"timestamp":1696271742384},"core":{"header":{"name":"UserBroadcastCamStoppedEvtMsg","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"body":{"userId":"w_gydstnfg3p5u","stream":"w_gydstnfg3p5u_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}}} +{"envelope":{"name":"ScreenshareRtmpBroadcastStartedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"not-used"},"timestamp":1696271746676},"core":{"header":{"name":"ScreenshareRtmpBroadcastStartedEvtMsg","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"not-used"},"body":{"voiceConf":"79025","screenshareConf":"79025","stream":"0b133d70-1699-4337-a3ba-66824d8a9a8e","vidWidth":0,"vidHeight":0,"timestamp":"1696271746659","hasAudio":true,"contentType":"screenshare"}}} +{"envelope":{"name":"ScreenshareRtmpBroadcastStoppedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"not-used"},"timestamp":1696271749144},"core":{"header":{"name":"ScreenshareRtmpBroadcastStoppedEvtMsg","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"not-used"},"body":{"voiceConf":"","screenshareConf":"","stream":"","vidWidth":0,"vidHeight":0,"timestamp":""}}} +{"envelope":{"name":"GroupChatMessageBroadcastEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"timestamp":1696271759769},"core":{"header":{"name":"GroupChatMessageBroadcastEvtMsg","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"body":{"chatId":"MAIN-PUBLIC-GROUP-CHAT","msg":{"id":"1696271758851-0a25o7ou","timestamp":1696271758851,"correlationId":"w_gydstnfg3p5u-1696271758385","sender":{"id":"w_gydstnfg3p5u","name":"User 2261564","role":"MODERATOR"},"chatEmphasizedText":true,"message":"Public chat test"}}}} +{"envelope":{"name":"UserReactionEmojiChangedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"timestamp":1696271766069},"core":{"header":{"name":"UserReactionEmojiChangedEvtMsg","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"body":{"userId":"w_gydstnfg3p5u","reactionEmoji":"🙁"}}} +{"envelope":{"name":"UserReactionEmojiChangedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"timestamp":1696271771117},"core":{"header":{"name":"UserReactionEmojiChangedEvtMsg","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"body":{"userId":"w_gydstnfg3p5u","reactionEmoji":"none"}}} +{"envelope":{"name":"UserRaiseHandChangedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"timestamp":1696271773281},"core":{"header":{"name":"UserRaiseHandChangedEvtMsg","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"body":{"userId":"w_gydstnfg3p5u","raiseHand":true}}} +{"envelope":{"name":"UserRaiseHandChangedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"timestamp":1696271777871},"core":{"header":{"name":"UserRaiseHandChangedEvtMsg","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"body":{"userId":"w_gydstnfg3p5u","raiseHand":false}}} +{"envelope":{"name":"PollStartedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"timestamp":1696271789454},"core":{"header":{"name":"PollStartedEvtMsg","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"body":{"userId":"w_gydstnfg3p5u","pollId":"2e8114441fe446507e61c6aa3de818f295584fcf-1696271692887/1/1696271789402","pollType":"A-4","secretPoll":false,"question":"ABCD Poll test (public","poll":{"id":"2e8114441fe446507e61c6aa3de818f295584fcf-1696271692887/1/1696271789402","isMultipleResponse":false,"answers":[{"id":0,"key":"A"},{"id":1,"key":"B"},{"id":2,"key":"C"},{"id":3,"key":"D"}]}}}} +{"envelope":{"name":"UserRespondedToPollRespMsg","routing":{"msgType":"DIRECT","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"timestamp":1696271792333},"core":{"header":{"name":"UserRespondedToPollRespMsg","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"body":{"pollId":"2e8114441fe446507e61c6aa3de818f295584fcf-1696271692887/1/1696271789402","userId":"w_8lvidy41zal2","answerIds":[0]}}} +{"envelope":{"name":"UserLeftMeetingEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_8lvidy41zal2"},"timestamp":1696271817904},"core":{"header":{"name":"UserLeftMeetingEvtMsg","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_8lvidy41zal2"},"body":{"intId":"w_8lvidy41zal2","eject":false,"ejectedBy":"","reason":"","reasonCode":""}}} +{"envelope":{"name":"MeetingDestroyedEvtMsg","routing":{"sender":"bbb-apps-akka"},"timestamp":1696271836394},"core":{"header":{"name":"MeetingDestroyedEvtMsg"},"body":{"meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885"}}} From eb2111d8098db6f6fcf5954bc1234773479f8a2f Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Tue, 3 Oct 2023 21:36:06 -0300 Subject: [PATCH 034/154] feat: add initial Prometheus instrumentation New metrics: - prom-client default metrics - bbb_webhooks_event_process_failures (input event parsing errors) - bbb_webhooks_event_dispatch_failures (output event dispatch failures generated by output modules) Also started adding JSDoc annotations to new code --- config/custom-environment-variables.yml | 6 + config/default.example.yml | 6 + package-lock.json | 390 ++++++++++++++++++++++++ package.json | 3 + src/metrics/http-server.js | 87 ++++++ src/metrics/index.js | 105 +++++++ src/metrics/prometheus-agent.js | 214 +++++++++++++ src/modules/module-wrapper.js | 38 ++- src/process/event-processor.js | 34 ++- 9 files changed, 875 insertions(+), 8 deletions(-) create mode 100644 src/metrics/http-server.js create mode 100644 src/metrics/index.js create mode 100644 src/metrics/prometheus-agent.js diff --git a/config/custom-environment-variables.yml b/config/custom-environment-variables.yml index 2976c49..0fc541f 100644 --- a/config/custom-environment-variables.yml +++ b/config/custom-environment-variables.yml @@ -12,6 +12,12 @@ log: filename: LOG_FILE stdout: LOG_STDOUT +prometheus: + enabled: PROM_ENABLED + port: PROM_PORT + path: PROM_PATH + collectDefaultMetrics: PROM_COLLECT_DEFAULT_METRICS + mappings: timeout: MAPPINGS_TIMEOUT diff --git a/config/default.example.yml b/config/default.example.yml index 84f7c69..076d534 100644 --- a/config/default.example.yml +++ b/config/default.example.yml @@ -3,6 +3,12 @@ log: filename: /var/log/bigbluebutton/bbb-webhooks.log stdout: true +prometheus: + enabled: false + port: 3004 + path: /metrics + collectDefaultMetrics: false + # Shared secret of your BigBlueButton server. bbb: serverDomain: myserver.com diff --git a/package-lock.json b/package-lock.json index 49d80ed..25d0456 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "express": "^4.18.2", "js-yaml": "^4.1.0", "node-fetch": "^3.3.2", + "prom-client": "^14.2.0", "redis": "^4.6.8", "sha1": "^1.1.1", "uuid": "^9.0.1", @@ -22,6 +23,8 @@ "body-parser": "^1.20.2", "eslint": "^8.49.0", "eslint-plugin-import": "^2.28.1", + "eslint-plugin-jsdoc": "^46.8.2", + "jsdoc": "^4.0.2", "mocha": "^9.2.2", "nock": "^13.3.3", "nodemon": "^3.0.1", @@ -41,6 +44,18 @@ "node": ">=0.10.0" } }, + "node_modules/@babel/parser": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -59,6 +74,20 @@ "kuler": "^2.0.0" } }, + "node_modules/@es-joy/jsdoccomment": { + "version": "0.40.1", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.40.1.tgz", + "integrity": "sha512-YORCdZSusAlBrFpZ77pJjc5r1bQs5caPWtAu+WWmiSo+8XaUzseapVrfAtiRFbQWnrBxxLLEwF6f6ZG/UgCQCg==", + "dev": true, + "dependencies": { + "comment-parser": "1.4.0", + "esquery": "^1.5.0", + "jsdoc-type-pratt-parser": "~4.0.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -199,6 +228,18 @@ "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" }, + "node_modules/@jsdoc/salty": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.5.tgz", + "integrity": "sha512-TfRP53RqunNe2HBobVBJ0VLhK1HbfvBYeTC1ahnN64PWvyYyGebmMiPkuwvD9fpw2ZbkoPb8Q7mwy0aR8Z9rvw==", + "dev": true, + "dependencies": { + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v12.0.0" + } + }, "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.2.tgz", @@ -400,6 +441,28 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-pTjcqY9E4nOI55Wgpz7eiI8+LzdYnw3qxXCfHyBDdPbYvbyLgWLJGh8EdPvqawwMK1Uo1794AUkkR38Fr0g+2g==", + "dev": true + }, + "node_modules/@types/markdown-it": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", + "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", + "dev": true, + "dependencies": { + "@types/linkify-it": "*", + "@types/mdurl": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.3.tgz", + "integrity": "sha512-T5k6kTXak79gwmIOaDF2UUQXFbnBE0zBUzF20pz7wDYu0RQMzWg+Ml/Pz50214NsFHBITkoi5VtdjFZnJ2ijjA==", + "dev": true + }, "node_modules/@types/triple-beam": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.3.tgz", @@ -512,6 +575,15 @@ "node": ">= 8" } }, + "node_modules/are-docs-informative": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", + "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", + "dev": true, + "engines": { + "node": ">=14" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -667,6 +739,17 @@ "node": ">=8" } }, + "node_modules/bintrees": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", + "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==" + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, "node_modules/body-parser": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", @@ -719,6 +802,18 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bullmq": { "version": "4.11.4", "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-4.11.4.tgz", @@ -776,6 +871,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/catharsis": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", + "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", + "dev": true, + "dependencies": { + "lodash": "^4.17.15" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -927,6 +1034,15 @@ "node": ">= 0.8" } }, + "node_modules/comment-parser": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.0.tgz", + "integrity": "sha512-QLyTNiZ2KDOibvFPlZ6ZngVsZ/0gYnE6uTXi5aoDg8ed3AkJAz4sEje3Y8a29hQ1s6A99MZXe47fLAXQ1rTqaw==", + "dev": true, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", @@ -1171,6 +1287,15 @@ "node": ">= 0.8" } }, + "node_modules/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-abstract": { "version": "1.22.2", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.2.tgz", @@ -1469,6 +1594,52 @@ "semver": "bin/semver.js" } }, + "node_modules/eslint-plugin-jsdoc": { + "version": "46.8.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.8.2.tgz", + "integrity": "sha512-5TSnD018f3tUJNne4s4gDWQflbsgOycIKEUBoCLn6XtBMgNHxQFmV8vVxUtiPxAQq8lrX85OaSG/2gnctxw9uQ==", + "dev": true, + "dependencies": { + "@es-joy/jsdoccomment": "~0.40.1", + "are-docs-informative": "^0.0.2", + "comment-parser": "1.4.0", + "debug": "^4.3.4", + "escape-string-regexp": "^4.0.0", + "esquery": "^1.5.0", + "is-builtin-module": "^3.2.1", + "semver": "^7.5.4", + "spdx-expression-parse": "^3.0.1" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, "node_modules/eslint-scope": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", @@ -2065,6 +2236,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -2372,6 +2549,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-builtin-module": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "dev": true, + "dependencies": { + "builtin-modules": "^3.3.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -2626,6 +2818,62 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "dev": true, + "dependencies": { + "xmlcreate": "^2.0.4" + } + }, + "node_modules/jsdoc": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.2.tgz", + "integrity": "sha512-e8cIg2z62InH7azBBi3EsSEqrKx+nUtAS5bBcYTSpZFA+vhNPyhv8PTFZ0WsjOPDj04/dOLlm08EDcQJDqaGQg==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.15", + "@jsdoc/salty": "^0.2.1", + "@types/markdown-it": "^12.2.3", + "bluebird": "^3.7.2", + "catharsis": "^0.9.0", + "escape-string-regexp": "^2.0.0", + "js2xmlparser": "^4.0.2", + "klaw": "^3.0.0", + "markdown-it": "^12.3.2", + "markdown-it-anchor": "^8.4.1", + "marked": "^4.0.10", + "mkdirp": "^1.0.4", + "requizzle": "^0.2.3", + "strip-json-comments": "^3.1.0", + "underscore": "~1.13.2" + }, + "bin": { + "jsdoc": "jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/jsdoc-type-pratt-parser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.0.0.tgz", + "integrity": "sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/jsdoc/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -2676,6 +2924,15 @@ "json-buffer": "3.0.1" } }, + "node_modules/klaw": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", + "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.9" + } + }, "node_modules/kuler": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", @@ -2694,6 +2951,15 @@ "node": ">= 0.8.0" } }, + "node_modules/linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "dev": true, + "dependencies": { + "uc.micro": "^1.0.1" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2789,6 +3055,50 @@ "node": ">=12" } }, + "node_modules/markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/markdown-it-anchor": { + "version": "8.6.7", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz", + "integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==", + "dev": true, + "peerDependencies": { + "@types/markdown-it": "*", + "markdown-it": "*" + } + }, + "node_modules/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "dev": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "dev": true + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -2861,6 +3171,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/mocha": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz", @@ -3524,6 +3846,17 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, + "node_modules/prom-client": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-14.2.0.tgz", + "integrity": "sha512-sF308EhTenb/pDRPakm+WgiN+VdM/T1RaHj1x+MvAuT8UiQP8JmOEbxVqtkbfR4LrvOg5n7ic01kRBDGXjYikA==", + "dependencies": { + "tdigest": "^0.1.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/propagate": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", @@ -3723,6 +4056,15 @@ "node": ">=0.10.0" } }, + "node_modules/requizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", + "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", + "dev": true, + "dependencies": { + "lodash": "^4.17.21" + } + }, "node_modules/resolve": { "version": "1.22.6", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz", @@ -4049,6 +4391,28 @@ "url": "https://opencollective.com/sinon" } }, + "node_modules/spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.15.tgz", + "integrity": "sha512-lpT8hSQp9jAKp9mhtBU4Xjon8LPGBvLIuBiSVhMEtmLecTh2mO0tlqrAMp47tBXzMr13NJMQ2lf7RpQGLJ3HsQ==", + "dev": true + }, "node_modules/stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", @@ -4249,6 +4613,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tdigest": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", + "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==", + "dependencies": { + "bintrees": "1.0.2" + } + }, "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", @@ -4439,6 +4811,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "dev": true + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -4460,6 +4838,12 @@ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", "dev": true }, + "node_modules/underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", + "dev": true + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -4656,6 +5040,12 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", + "dev": true + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index f80768c..41299ec 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "express": "^4.18.2", "js-yaml": "^4.1.0", "node-fetch": "^3.3.2", + "prom-client": "^14.2.0", "redis": "^4.6.8", "sha1": "^1.1.1", "uuid": "^9.0.1", @@ -32,6 +33,8 @@ "body-parser": "^1.20.2", "eslint": "^8.49.0", "eslint-plugin-import": "^2.28.1", + "eslint-plugin-jsdoc": "^46.8.2", + "jsdoc": "^4.0.2", "mocha": "^9.2.2", "nock": "^13.3.3", "nodemon": "^3.0.1", diff --git a/src/metrics/http-server.js b/src/metrics/http-server.js new file mode 100644 index 0000000..c9ace6e --- /dev/null +++ b/src/metrics/http-server.js @@ -0,0 +1,87 @@ +import http from 'node:http'; + +/** + * HttpServer. + */ +class HttpServer { + /** + * constructor. + * @public + * @param {string} host - HTTP server host to bind to + * @param {number} port - HTTP server port to bind to + * @param {Function} callback - callback function to be called on each request + * @param {object} options - options object + * @param {object} options.logger - logger object + * @returns {HttpServer} - HttpServer object + */ + constructor(host, port, callback, { + logger = console, + } = {}) { + this.host = host; + this.port = port; + this.requestCallback = callback; + this.logger = logger; + } + + /** + * start - creates the HTTP server (without listening). + * @public + */ + start () { + this.server = http.createServer(this.requestCallback) + .on('error', this._handleError.bind(this)) + .on('clientError', this._handleError.bind(this)); + } + + /** + * close - closes the HTTP server. + * @public + * @param {Function} callback - callback function to be called on close + * @returns {http.Server} - server object + */ + close (callback) { + return this.server.close(callback); + } + + /** + * handleError - handles HTTP server 'error' and 'clientError' events. + * @private + * @param {Error} error - error object + */ + _handleError (error) { + if (error.code === 'EADDRINUSE') { + this.logger.warn("EADDRINUSE, won't spawn HTTP server", { + host: this.host, port: this.port, + }); + this.server.close(); + } else if (error.code === 'ECONNRESET') { + this.logger.warn("HTTPServer: ECONNRESET ", { errorMessage: error.message }); + } else { + this.logger.error("Returned error", error); + } + + throw error; + } + + /** + * getServerObject - returns the HTTP server object. + * @public + * @returns {http.Server} - server object + */ + getServerObject() { + return this.server; + } + + /** + * listen - starts listening on the HTTP server at the specified host and port. + * @public + * @param {Function} callback - callback function to be called on listen + * @returns {http.Server} - server object + */ + listen(callback) { + this.logger.info(`HTTPServer is listening: ${this.host}:${this.port}`); + return this.server.listen(this.port, this.host, callback); + } +} + +export default HttpServer; diff --git a/src/metrics/index.js b/src/metrics/index.js new file mode 100644 index 0000000..3d8b4f7 --- /dev/null +++ b/src/metrics/index.js @@ -0,0 +1,105 @@ +import config from 'config'; +import PrometheusAgent from './prometheus-agent.js'; +import { Counter } from 'prom-client'; +import { newLogger } from '../common/logger.js'; + +const logger = newLogger('prometheus'); + +const { + enabled: METRICS_ENABLED = false, + host: METRICS_HOST = 'localhost', + port: METRICS_PORT = '3004', + path: METRICS_PATH = '/metrics', + collectDefaultMetrics: COLLECT_DEFAULT_METRICS, +} = config.has('prometheus') ? config.get('prometheus') : { enabled: false }; + +let METRICS, AGENT; +const PREFIX = 'bbb_webhooks_'; +const METRIC_NAMES = { + OUTPUT_QUEUE_SIZE: `${PREFIX}output_queue_size`, + MODULE_STATUS: `${PREFIX}module_status`, + EVENT_PROCESS_FAILURES: `${PREFIX}event_process_failures`, + EVENT_DISPATCH_FAILURES: `${PREFIX}event_dispatch_failures`, +} + +/** + * injectMetrics - Inject a metrics dictionary into the Prometheus agent. + * @param {PrometheusAgent} agent - Prometheus agent + * @param {object} metricsDictionary - Metrics dictionary (key: metric name, value: prom-client metric object) + * @returns {boolean} - True if metrics were injected, false otherwise + * @public + */ +const injectMetrics = (agent, metricsDictionary) => { + agent.injectMetrics(metricsDictionary); + return true; +} + +/** + * buildDefaultMetrics - Build the default metrics dictionary. + * @returns {object} - Metrics dictionary (key: metric name, value: prom-client metric object) + * @public + */ +const buildDefaultMetrics = () => { + if (METRICS == null) { + METRICS = { + // TODO to be implemented + //[METRIC_NAMES.MODULE_STATUS]: new Gauge({ + // name: METRIC_NAMES.MODULE_STATUS, + // help: 'Status of each module', + // labelNames: ['module', 'moduleType'], + //}), + + //[METRIC_NAMES.OUTPUT_QUEUE_SIZE]: new Gauge({ + // name: METRIC_NAMES.OUTPUT_QUEUE_SIZE, + // help: 'Event queue size for each output module', + // labelNames: ['module'], + //}), + + [METRIC_NAMES.EVENT_PROCESS_FAILURES]: new Counter({ + name: METRIC_NAMES.EVENT_PROCESS_FAILURES, + help: 'Number of event processing failures', + }), + + [METRIC_NAMES.EVENT_DISPATCH_FAILURES]: new Counter({ + name: METRIC_NAMES.EVENT_DISPATCH_FAILURES, + help: 'Number of event dispatch failures', + labelNames: ['outputEventId', 'module'], + }), + } + } + + return METRICS; +}; + +/** + * getExporter - Start the Prometheus agent. + * @returns {PrometheusAgent} - Prometheus agent + * @public + */ +const getExporter = () => { + if (!METRICS_ENABLED) return null; + if (AGENT && AGENT.started) return AGENT; + + AGENT = new PrometheusAgent(METRICS_HOST, METRICS_PORT, { + path: METRICS_PATH, + prefix: PREFIX, + collectDefaultMetrics: COLLECT_DEFAULT_METRICS, + logger, + }); + + if (injectMetrics(AGENT, buildDefaultMetrics())) { + AGENT.start(); + return AGENT; + } + + return null; +} + +export default { + METRICS_ENABLED, + METRIC_NAMES, + METRICS, + injectMetrics, + getExporter, +}; + diff --git a/src/metrics/prometheus-agent.js b/src/metrics/prometheus-agent.js new file mode 100644 index 0000000..64c5374 --- /dev/null +++ b/src/metrics/prometheus-agent.js @@ -0,0 +1,214 @@ +import promclient from 'prom-client'; +import HttpServer from './http-server.js'; + +/** + * PrometheusScrapeAgent. + * @class + */ +class PrometheusScrapeAgent { + /** + * constructor. + * @param {string} host - Host to bind to for the metrics HTTP server + * @param {number} port - Port to bind to for the metrics HTTP server + * @param {object} options - Options object + * @param {string} options.path - Path to expose metrics on + * @param {string} options.prefix - Prefix to add to all metrics + * @param {number} options.collectionTimeout - Timeout for collecting metrics + * @param {boolean} options.collectDefaultMetrics - Whether to collect prom-client default metrics + * @param {object} options.logger - Logger object + * @returns {PrometheusScrapeAgent} - PrometheusScrapeAgent object + */ + constructor (host, port, options = {}) { + this.host = host; + this.port = port; + this.metrics = {}; + this.started = false; + + this.path = options.path || '/metrics'; + this.metricsPrefix = options.prefix || ''; + this.collectionTimeout = options.collectionTimeout || 10000; + this.collectDefaultMetrics = options.collectDefaultMetrics || false; + this.logger = options.logger || console; + } + + /** + * getMetric - Get a metric by name. + * @param {string} name - Name of the metric + * @returns {promclient.Metric} - Metric object + */ + getMetric (name) { + return this.metrics[name]; + } + + /** + * _collect - Collect metrics and expose them on the metrics HTTP server. + * @private + * @param {http.ServerResponse} response - HTTP server response to write metrics to + * @async + */ + async _collect (response) { + try { + const _response = await this.collect(response); + _response.writeHead(200, { 'Content-Type': promclient.register.contentType }); + const content = await promclient.register.metrics(); + _response.end(content); + } catch (error) { + response.writeHead(500) + response.end(error.message); + this.logger.error('Prometheus: error collecting metrics', + { errorCode: error.code, errorMessage: error.message }); + } + } + + /** + * collect - Override this method to add a custom collector. + * @param {http.ServerResponse} response - HTTP response + * @returns {Promise} - Promise object + * @async + * @abstract + */ + collect (response) { + return Promise.resolve(response); + } + + /** + * defaultMetricsHandler - Default request handler for metrics HTTP server. + * @param {http.IncomingMessage} request - HTTP request + * @param {http.ServerResponse} response - HTTP response + */ + defaultMetricsHandler (request, response) { + switch (request.method) { + case 'GET': + if (request.url === this.path) { + this._collect(response); + return; + } + response.writeHead(404).end(); + break; + default: + response.writeHead(501) + response.end(); + break; + } + } + + /** + * start - Start the metrics HTTP server. + * @param {Function} requestHandler - Request handler for metrics HTTP server + */ + start (requestHandler = this.defaultMetricsHandler.bind(this)) { + if (this.collectDefaultMetrics) promclient.collectDefaultMetrics({ + prefix: this.metricsPrefix, + timeout: this.collectionTimeout, + }); + + this.metricsServer = new HttpServer(this.host, this.port, requestHandler, { + logger: this.logger, + }); + this.metricsServer.start(); + this.metricsServer.listen(); + this.started = true; + } + + /** + * injectMetrics - Inject new metrics into the metrics dictionary. + * @param {object} metricsDictionary - Metrics dictionary + */ + injectMetrics (metricsDictionary) { + this.metrics = { ...this.metrics, ...metricsDictionary } + } + + /** + * increment - Increment a metric COUNTER + * @param {string} metricName - Name of the metric + * @param {object} labelsObject - An object containing labels and their values for the metric + */ + increment (metricName, labelsObject) { + if (!this.started) return; + + const metric = this.metrics[metricName]; + if (metric) { + metric.inc(labelsObject) + } + } + + /** + * decrement - Decrement a metric COUNTER + * @param {string} metricName - Name of the metric + * @param {object} labelsObject - An object containing labels and their values for the metric + */ + decrement (metricName, labelsObject) { + if (!this.started) return; + + const metric = this.metrics[metricName]; + if (metric) { + metric.dec(labelsObject) + } + } + + /** + * set - Set a metric GAUGE to a value + * @param {string} metricName - Name of the metric + * @param {number} value - Value to set the metric to + * @param {object} labelsObject - An object containing labels and their values for the metric + */ + set (metricName, value, labelsObject = {}) { + if (!this.started) return; + + const metric = this.metrics[metricName]; + if (metric) { + metric.set(labelsObject, value) + } + } + + /** + * setCollectorWithGenerator - Override a specific metric's collector with a generator function. + * @param {string} metricName - Name of the metric + * @param {Function} generator - Generator function to be called on each collect + */ + setCollectorWithGenerator (metricName, generator) { + const metric = this.getMetric(metricName); + if (metric) { + /** + * metric.collect. + */ + metric.collect = () => { + metric.set(generator()); + }; + } + } + + /** + * setCollector - Override a specific metric's collector with a custom collector. + * @param {string} metricName - Name of the metric + * @param {Function} collector - Custom collector function to be called on each collect + */ + setCollector (metricName, collector) { + const metric = this.getMetric(metricName); + + if (metric) { + metric.collect = collector.bind(metric); + } + } + + /** + * reset - Reset metrics values. Resets all metrics if no metric name is provided. + * @param {string[]} metrics - Array of metric names to reset + */ + reset (metrics = []) { + if (metrics == null || metrics.length === 0) { + promclient.register.resetMetrics(); + return; + } + + metrics.forEach(metricName => { + const metric = this.getMetric(metricName); + + if (metric) { + promclient.register.reset(metricName); + } + }); + } +} + +export default PrometheusScrapeAgent; diff --git a/src/modules/module-wrapper.js b/src/modules/module-wrapper.js index 0578392..9801073 100644 --- a/src/modules/module-wrapper.js +++ b/src/modules/module-wrapper.js @@ -1,8 +1,9 @@ 'use strict'; +import EventEmitter from 'events'; import { newLogger } from '../common/logger.js'; import { MODULE_TYPES, validateModuleDefinition } from './definitions.js'; -import { createQueue, getQueue } from './queue.js'; +import { createQueue, getQueue, deleteQueue } from './queue.js'; // [MODULE_TYPES.INPUT]: { // load: 'function', @@ -23,12 +24,13 @@ import { createQueue, getQueue } from './queue.js'; // delete: 'function', // }, -export default class ModuleWrapper { +export default class ModuleWrapper extends EventEmitter { static _defaultCollector () { throw new Error('Collector not set'); } constructor (name, type, context, config = {}) { + super(); this.name = name; this.type = type; this.id = `${name}-${type}`; @@ -38,6 +40,8 @@ export default class ModuleWrapper { this.logger.debug(`created module wrapper for ${name}`, { type, config }); this._module = null; + this._queue = null; + this._worker = null; } set config(config) { @@ -82,12 +86,16 @@ export default class ModuleWrapper { return this._module?.type || this._type; } + _getQueueId() { + return this.config.queue.id || `${this.name}-out-queue`; + } + _setupOutboundQueues() { if (this.type !== MODULE_TYPES.out) return; if (this.config.queue.enabled) { this.logger.debug(`setting up outbound queues for module ${this.name}`, this.config.queue); - const queueId = this.config.queue.id || `${this.name}-out-queue`; + const queueId = this._getQueueId(); const processor = async (job) => { if (job.name !== 'event') { this.logger.error(`job ${job.name}:${job.id} is not an event`); @@ -96,13 +104,26 @@ export default class ModuleWrapper { const { event, raw } = job.data; await this._onEvent(event, raw); }; + const { queue, worker } = createQueue(queueId, processor, { host: this.config.queue.host, port: this.config.queue.port, password: this.config.queue.password, concurrency: this.config.queue.concurrency || 1, + limiter: this.config.queue.limiter || undefined, }); + this._queue = queue; + this._worker = worker; + this._worker.on('failed', (job, error) => { + if (job.name !== 'event') { + this.logger.error(`job ${job.name}:${job.id} failed`, error); + this.emit('eventDispatchFailed', { event: job.data?.event, raw: job.data?.raw, error }); + } + }); + this._worker.on('error', (error) => { + this.logger.error(`worker for queue ${queueId} received error`, error); + }); this.logger.info(`created queue ${queueId} for module ${this.name}`, { queueId, queueConcurrency: this.config.queue.concurrency || 1, @@ -177,6 +198,11 @@ export default class ModuleWrapper { } unload() { + this.removeAllListeners(); + this._worker = null; + this._queue = null; + deleteQueue(this._getQueueId()); + if (this._module?.unload) { return this._module.unload(); } @@ -206,11 +232,11 @@ export default class ModuleWrapper { } if (this.config.queue.enabled) { - const queueId = this.config.queue.id || `${this.name}-out-queue`; + const queueId = this._getQueueId(); const { queue } = getQueue(queueId); - const job = await queue.add('event', { event, raw }); + await queue.add('event', { event, raw }); } else { - return this._onEvent(event, raw); + await this._onEvent(event, raw); } } } diff --git a/src/process/event-processor.js b/src/process/event-processor.js index 32812ef..26500f1 100644 --- a/src/process/event-processor.js +++ b/src/process/event-processor.js @@ -3,6 +3,7 @@ import { newLogger } from '../common/logger.js'; import WebhooksEvent from '../process/event.js'; import UserMapping from '../db/redis/user-mapping.js'; import Utils from '../common/utils.js'; +import Metrics from '../metrics/index.js'; const Logger = newLogger('event-processor'); @@ -17,6 +18,24 @@ export default class EventProcessor { ) { this.inputs = inputs; this.outputs = outputs; + + this._exporter = Metrics.getExporter(); + } + + _trackModuleEvents() { + this.outputs.forEach((output) => { + output.on('eventDispatchFailed', ({ event, raw, error }) => { + Logger.error('error notifying output module', { + error: error.stack, + event, + raw, + }); + this._exporter.increment(Metrics.METRIC_NAMES.EVENT_DISPATCH_FAILURES, { + outputEventId: event?.data?.id || 'unknown', + module: output.name, + }); + }); + }); } start() { @@ -110,10 +129,11 @@ export default class EventProcessor { } } } catch (error) { - Logger.error(`error processing event: ${error}`, { + Logger.error('error processing event', { error: error.stack, event, }); + this._exporter.increment(Metrics.METRIC_NAMES.EVENT_PROCESS_FAILURES); } } @@ -126,7 +146,17 @@ export default class EventProcessor { } this.outputs.forEach((output) => { - output.onEvent(message, raw); + output.onEvent(message, raw).catch((error) => { + Logger.error('error notifying output module', { + error: error.stack, + event: message, + raw, + }); + this._exporter.increment(Metrics.METRIC_NAMES.EVENT_DISPATCH_FAILURES, { + outputEventId: message?.data?.id || 'unknown', + module: output.name, + }); + }); }); } } From 4f7c8005e93237f80577c8158be549c9f677a632 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Tue, 3 Oct 2023 22:07:14 -0300 Subject: [PATCH 035/154] chore: add more jsdoc annotations --- .eslintrc.yml | 2 + application.js | 27 +++++++-- package.json | 3 +- src/common/logger.js | 102 +++++++++++++++++++++++++++++----- src/modules/context.js | 50 ++++++++++++++++- src/modules/module-wrapper.js | 81 ++++++++++++++++++++++++++- src/modules/queue.js | 35 +++++++++++- 7 files changed, 275 insertions(+), 25 deletions(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index 2be4362..f1d2ec7 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -4,6 +4,7 @@ env: extends: - eslint:recommended - plugin:import/recommended + - plugin:jsdoc/recommended parserOptions: sourceType: module ecmaVersion: 2022 @@ -15,3 +16,4 @@ rules: no-whitespace-before-property: "warn" no-multiple-empty-lines: ["warn", { max: 1 }] import/no-extraneous-dependencies: "error" + jsdoc/no-undefined-types: "off" diff --git a/application.js b/application.js index 3e55853..d8e9ba6 100644 --- a/application.js +++ b/application.js @@ -3,7 +3,19 @@ import Logger from './src/common/logger.js'; import ModuleManager from './src/modules/index.js'; import EventProcessor from './src/process/event-processor.js'; -export default class Application { +/** + * Application. + * @class + * @classdesc Wrapper class for the whole bbb-webhooks application. + * @property {ModuleManager} moduleManager - Module manager. + * @property {EventProcessor} eventProcessor - Event processor. + * @property {boolean} _initialized - Initialized. + */ +class Application { + /** + * constructor. + * @constructs Application + */ constructor() { this.moduleManager = new ModuleManager(config.get("modules")); this.eventProcessor = null; @@ -11,14 +23,17 @@ export default class Application { this._initialized = false; } + /** + * start. + * @returns {Promise} Promise. + * @async + * @public + */ async start() { if (this._initialized) return Promise.resolve(); const { inputModules, outputModules } = await this.moduleManager.load(); - this.eventProcessor = new EventProcessor( - inputModules, - outputModules, - ); + this.eventProcessor = new EventProcessor(inputModules, outputModules); await this.eventProcessor.start(); return Promise.all([ @@ -31,3 +46,5 @@ export default class Application { }); } } + +export default Application; diff --git a/package.json b/package.json index 41299ec..957171b 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "dev-start": "nodemon --watch src --ext js,json,yml,yaml --exec node app.js", "test": "ALLOW_CONFIG_MUTATIONS=true mocha", "lint": "./node_modules/.bin/eslint ./", - "lint:file": "./node_modules/.bin/eslint" + "lint:file": "./node_modules/.bin/eslint", + "jsdoc": "./node_modules/.bin/jsdoc app.js application.js src/ -r" }, "keywords": [ "bigbluebutton", diff --git a/src/common/logger.js b/src/common/logger.js index 5f3fda9..f056f7e 100644 --- a/src/common/logger.js +++ b/src/common/logger.js @@ -11,16 +11,6 @@ const { stdout: STDOUT = true } = LOG_CONFIG; const { combine, colorize, timestamp, json, printf, errors, splat } = format; - -addColors({ - error: 'red', - warn: 'yellow', - info: 'green', - verbose: 'cyan', - debug: 'magenta', - trace: 'gray' -}); - const LEVELS = { error: 0, warn: 1, @@ -29,18 +19,85 @@ const LEVELS = { debug: 4, trace: 5, }; +addColors({ + error: 'red', + warn: 'yellow', + info: 'green', + verbose: 'cyan', + debug: 'magenta', + trace: 'gray' +}); -const shimmerLoggerWithLabel = (logger, label) => { +/** + * The logging library used by this module. + * @name external:winston + * @external winston + * @private + */ + +/** + * The logging class exposed by this module. + * @name external:winston.Logger + * @memberof external:winston + * @external winston.Logger + * @class + */ + +/** + * Method to log a message at a specified level. + * @name external:winston.Logger#log + * @memberof external:winston.Logger + * @function + * @param {string} level - The log level to use. + * @param {string} message - The message to log. + */ + +/** + * @typedef {object} BbbWebhooksLogger + * @property {external:winston.Logger#log} error - log a message at the error level + * @property {external:winston.Logger#log} warn - log a message at the warn level + * @property {external:winston.Logger#log} info - log a message at the info level + * @property {external:winston.Logger#log} verbose - log a message at the verbose level + * @property {external:winston.Logger#log} debug - log a message at the debug level + * @property {external:winston.Logger#log} trace - log a message at the trace level + */ + +/** + * _shimmerLoggerWithLabel. + * @private + * @param {external:winston.Logger} logger - the logger to be shimmered + * @param {string} label - the label to be prepended to the message + * @returns {BbbWebhooksLogger} the shimmered logger + */ +const _shimmerLoggerWithLabel = (logger, label) => { const shimmeredLogger = Object.assign({}, logger); Object.keys(LEVELS).forEach((level) => { + /** + * shimmeredLogger[level]. + * @param {object} message - the message to be logged + * @param {string} meta - loggable object to be stringified and appended to the message (metadata) + */ shimmeredLogger[level] = (message, meta) => { - logger[level](`[${label}] ${message}`, meta); + logger.log(level, `[${label}] ${message}`, meta); } }); return shimmeredLogger; }; +/** + * @typedef {object} LoggerOptions + * @property {string} filename - the filename to log to + * @property {string} level - the maximum log level to use + * @property {boolean} stdout - whether to log to stdout + */ + +/** + * _newLogger. + * @private + * @param {LoggerOptions} options - the options to be used when creating the logger + * @returns {external:winston.Logger} a Winston logger instance + */ const _newLogger = ({ filename, level, @@ -102,10 +159,25 @@ const BASE_LOGGER = _newLogger({ stdout: STDOUT, }); -const logger = shimmerLoggerWithLabel(BASE_LOGGER, 'bbb-webhooks'); - +/** + * The default logger instance for bbb-webhooks (with label 'bbb-webhooks') + * @name logger + * @instance + * @public + * @type {BbbWebhooksLogger} + */ +const logger = _shimmerLoggerWithLabel(BASE_LOGGER, 'bbb-webhooks'); +/** + * Creates a new logger with the specified label prepended to all messages + * @name newLogger + * @instance + * @function + * @public + * @param {string} label - the label to be prepended to the message + * @returns {BbbWebhooksLogger} the new logger + */ const newLogger = (label) => { - return shimmerLoggerWithLabel(BASE_LOGGER, label); + return _shimmerLoggerWithLabel(BASE_LOGGER, label); } export default logger; diff --git a/src/modules/context.js b/src/modules/context.js index 7504b2c..1c4505a 100644 --- a/src/modules/context.js +++ b/src/modules/context.js @@ -2,13 +2,35 @@ import { newLogger } from '../common/logger.js'; import config from 'config'; import { StorageCompartmentKV, StorageItem } from '../db/redis/base-storage.js'; -export default class Context { +/** + * Context. + * @class + * @classdesc Context class containing utility functions and objects for submodules + * of the application. + * @property {string} name - Name of the context. + * @property {object} configuration - Configuration object. + * @property {Map} _loggers - Map of loggers. + */ +class Context { + /** + * constructor. + * @param {object} configuration - Context configuration data + * @param {string} configuration.name - Submodule name + * @param {object} configuration.logger - Logger object + * @param {object} configuration.config - Submodule-specific configuration + */ constructor(configuration) { this.name = configuration.name; this.configuration = config.util.cloneDeep(configuration); this._loggers = new Map(); } + /** + * getLogger - Get a new logger with the given label and append it to the + * context's logger map. + * @param {string} label - Label for the logger + * @returns {BbbWebhooksLogger} - Logger object + */ getLogger(label = this.name) { if (!this._loggers.has(label)) { this._loggers.set(label, newLogger(label)); @@ -17,18 +39,42 @@ export default class Context { return this._loggers.get(label); } + /** + * destroy - Destroy the context. + * @public + * @returns {void} + */ destroy () { this._loggers.clear(); } + /** + * keyValueCompartmentConstructor - Return a key-value compartment util class. + * @returns {StorageCompartmentKV} - StorageCompartmentKV class + * @public + */ keyValueCompartmentConstructor () { return StorageCompartmentKV; } + /** + * keyValueItemConstructor - Return a key-value item util class. + * @returns {StorageItem} - StorageItem class + * @public + */ keyValueItemConstructor () { return StorageItem; } + /** + * keyValueStorageFactory - Return a key-value storage instance + * @param {string} prefix - Prefix for the storage compartment (namespace) + * @param {string} setId - Redis SET ID for the storage compartment + * @param {object} options - Options object + * @param {StorageItem} options.itemClass - Storage item class + * @param {string} options.aliasField - Item field to use as index (alias) + * @returns {StorageCompartmentKV} - StorageCompartmentKV instance + */ keyValueStorageFactory (prefix, setId, { itemClass = StorageItem, aliasField, @@ -39,3 +85,5 @@ export default class Context { }); } } + +export default Context; diff --git a/src/modules/module-wrapper.js b/src/modules/module-wrapper.js index 9801073..2a1fecf 100644 --- a/src/modules/module-wrapper.js +++ b/src/modules/module-wrapper.js @@ -24,11 +24,32 @@ import { createQueue, getQueue, deleteQueue } from './queue.js'; // delete: 'function', // }, -export default class ModuleWrapper extends EventEmitter { +/** + * ModuleWrapper. + * @augments {EventEmitter} + * @class + */ +class ModuleWrapper extends EventEmitter { + /** + * _defaultCollector - Default event colletion function. + * @static + * @private + * @throws {Error} - Error thrown if the default collector is called + * @memberof ModuleWrapper + */ static _defaultCollector () { throw new Error('Collector not set'); } + /** + * constructor. + * @param {string} name - Module name + * @param {string} type - Module type + * @param {Context} context - Context object + * @param {object} config - Module-specific configuration + * @constructs ModuleWrapper + * @augments {EventEmitter} + */ constructor (name, type, context, config = {}) { super(); this.name = name; @@ -86,10 +107,22 @@ export default class ModuleWrapper extends EventEmitter { return this._module?.type || this._type; } + /** + * _getQueueId - Get the queue ID for the module. + * @private + * @returns {string} - Queue ID + * @memberof ModuleWrapper + */ _getQueueId() { return this.config.queue.id || `${this.name}-out-queue`; } + /** + * _setupOutboundQueues - Setup outbound queues for the module only if the + * module is an output module and the queue is enabled. + * @private + * @memberof ModuleWrapper + */ _setupOutboundQueues() { if (this.type !== MODULE_TYPES.out) return; @@ -131,6 +164,12 @@ export default class ModuleWrapper extends EventEmitter { } } + /** + * _bootstrap - Initialize the necessary data for the module's creation + * @private + * @returns {Promise} - Promise object + * @memberof ModuleWrapper + */ _bootstrap() { if (!this._module) { throw new Error(`module ${this.name} is not loaded`); @@ -153,6 +192,14 @@ export default class ModuleWrapper extends EventEmitter { } } + /** + * setCollector - Set the event collector function for input modules. + * This function MUST be called by the module when it wants + * to send an event to the event processor. + * @param {Function} collector - Event collector function + * @throws {Error} - Error thrown if the module does not support setCollector + * @memberof ModuleWrapper + */ setCollector(collector) { if (typeof collector !== 'function') { throw new Error(`collector must be a function`); @@ -165,6 +212,13 @@ export default class ModuleWrapper extends EventEmitter { } } + /** + * load - Load the module. + * @returns {Promise} - Promise object that resolves to the module wrapper + * @async + * @memberof ModuleWrapper + * @throws {Error} - Error thrown if the module cannot be loaded + */ async load() { // Dynamically import the module provided via this.name // and instantiate it with the context and config provided @@ -197,6 +251,12 @@ export default class ModuleWrapper extends EventEmitter { return this; } + /** + * unload - Unload the module. + * @returns {Promise} - Promise object + * @async + * @memberof ModuleWrapper + */ unload() { this.removeAllListeners(); this._worker = null; @@ -210,6 +270,13 @@ export default class ModuleWrapper extends EventEmitter { return Promise.resolve(); } + /** + * setContext - Set the context for the module. + * @param {Context} context - Context object + * @throws {Error} - Error thrown if the module does not support setContext + * @memberof ModuleWrapper + * @returns {Promise} - Promise object + */ setContext(context) { if (this._module?.setContext) { return this._module.setContext(context); @@ -226,6 +293,16 @@ export default class ModuleWrapper extends EventEmitter { throw new Error("Not implemented"); } + /** + * _onEvent - Event handler middleware for output modules. + * Catches events dispatched by the event processor and + * forwards them to the module's onEvent() method. + * @param {object} event - Event object in the format of a WebhooksEvent object + * @param {object} raw - Raw event object + * @memberof ModuleWrapper + * @returns {Promise} - Promise object + * @throws {Error} - Error thrown if the module does not support onEvent + */ async onEvent(event, raw) { if (this.type !== MODULE_TYPES.out) { throw new Error(`module ${this.name} is not an output module`); @@ -240,3 +317,5 @@ export default class ModuleWrapper extends EventEmitter { } } } + +export default ModuleWrapper; diff --git a/src/modules/queue.js b/src/modules/queue.js index 60612a0..0d2001a 100644 --- a/src/modules/queue.js +++ b/src/modules/queue.js @@ -2,7 +2,17 @@ import { Queue, Worker } from 'bullmq' const queues = new Map(); -// Create a new connection in every instance +/** + * createQueue - Create a new BullMQ queue and worker. + * @param {string} id - Queue ID + * @param {Function} processor - Job processor function + * @param {object} options - Queue options + * @param {string} options.host - Redis host + * @param {string} options.port - Redis port + * @param {string} options.password - Redis password + * @param {number} options.concurrency - Worker concurrency + * @returns {object} - a { queue, worker } object + */ const createQueue = (id, processor, { host, port, @@ -39,6 +49,12 @@ const createQueue = (id, processor, { }; }; +/** + * addJob - Fetch the queue for the given ID and add a job to it. + * @param {string} id - Queue ID + * @param {object} job - Job to add (BullMQ job) + * @returns {object} - a { queue, worker } object + */ const addJob = (id, job) => { const queue = queues.get(id); @@ -49,14 +65,28 @@ const addJob = (id, job) => { return queue; } +/** + * getQueue - Get the queue for the given ID. + * @param {string} id - Queue ID + * @returns {object} - a { queue, worker } object + */ const getQueue = (id) => { return queues.get(id); } +/** + * getQueues. + * @returns {Map} - a Map of { queue, worker } objects + */ const getQueues = () => { return queues; } +/** + * deleteQueue - Delete the queue for the given ID. + * @param {string} id - Queue ID + * @returns {boolean} - True if the queue was deleted, false otherwise + */ const deleteQueue = (id) => { const queue = queues.get(id); @@ -64,9 +94,10 @@ const deleteQueue = (id) => { queue.queue.close(); queue.worker.close(); queues.delete(id); + return true; } - return queue; + return false; } export { From 50c700716414240d1b885ba81758b707a93d9346 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Wed, 4 Oct 2023 10:54:07 -0300 Subject: [PATCH 036/154] chore: add utils to pretty-print sample event files, update event files - Pretty printing is useful for debuggingo - Update event files with proper external-user-id mappings --- example/events/mapped-events.json | 42 +++++++++++++++---------------- example/events/pretty-print.js | 33 ++++++++++++++++++++++++ example/events/raw-events.json | 42 +++++++++++++++---------------- 3 files changed, 75 insertions(+), 42 deletions(-) create mode 100644 example/events/pretty-print.js diff --git a/example/events/mapped-events.json b/example/events/mapped-events.json index fc2a7aa..8a20baf 100644 --- a/example/events/mapped-events.json +++ b/example/events/mapped-events.json @@ -1,21 +1,21 @@ -{"data":{"type":"event","id":"meeting-created","attributes":{"meeting":{"internal-meeting-id":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","external-meeting-id":"random-6696736","name":"random-6696736","is-breakout":false,"parent-id":"bbb-none","duration":0,"create-time":1696271692885,"create-date":"Mon Oct 02 15:34:52 BRT 2023","moderator-pass":"mp","viewer-pass":"ap","record":false,"voice-conf":"79025","dial-number":"613-555-1234","max-users":0,"metadata":{"test-meta-param":"\"test-param\""}}},"event":{"ts":1696271692893}}} -{"data":{"type":"event","id":"user-presenter-assigned","attributes":{"meeting":{"internal-meeting-id":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","external-meeting-id":"random-6696736"},"user":{"internal-user-id":"w_gydstnfg3p5u","external-user-id":""}},"event":{"ts":1696271703903}}} -{"data":{"type":"event","id":"user-joined","attributes":{"meeting":{"internal-meeting-id":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","external-meeting-id":"random-6696736"},"user":{"internal-user-id":"w_gydstnfg3p5u","external-user-id":"w_gydstnfg3p5u","name":"User 2261564","role":"MODERATOR","presenter":"false"}},"event":{"ts":1696271703899}}} -{"data":{"type":"event","id":"user-presenter-assigned","attributes":{"meeting":{"internal-meeting-id":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","external-meeting-id":"random-6696736"},"user":{"internal-user-id":"w_gydstnfg3p5u","external-user-id":"w_gydstnfg3p5u"}},"event":{"ts":1696271703938}}} -{"data":{"type":"event","id":"user-presenter-assigned","attributes":{"meeting":{"internal-meeting-id":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","external-meeting-id":"random-6696736"},"user":{"internal-user-id":"w_gydstnfg3p5u","external-user-id":"w_gydstnfg3p5u"}},"event":{"ts":1696271703947}}} -{"data":{"type":"event","id":"user-joined","attributes":{"meeting":{"internal-meeting-id":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","external-meeting-id":"random-6696736"},"user":{"internal-user-id":"w_8lvidy41zal2","external-user-id":"w_8lvidy41zal2","name":"User 2261564","role":"VIEWER","presenter":"false"}},"event":{"ts":1696271711011}}} -{"data":{"type":"event","id":"user-audio-voice-enabled","attributes":{"meeting":{"internal-meeting-id":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885"},"user":{"internal-user-id":"w_gydstnfg3p5u","external-user-id":"","listening-only":false,"sharing-mic":true}},"event":{"ts":1696271733563}}} -{"data":{"type":"event","id":"user-audio-voice-disabled","attributes":{"meeting":{"internal-meeting-id":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885"},"user":{"internal-user-id":"w_gydstnfg3p5u","external-user-id":"","listening-only":false,"sharing-mic":false}},"event":{"ts":1696271736375}}} -{"data":{"type":"event","id":"user-cam-broadcast-start","attributes":{"meeting":{"internal-meeting-id":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885"},"user":{"internal-user-id":"w_gydstnfg3p5u","external-user-id":"","stream":"w_gydstnfg3p5u_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}},"event":{"ts":1696271740868}}} -{"data":{"type":"event","id":"user-cam-broadcast-end","attributes":{"meeting":{"internal-meeting-id":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885"},"user":{"internal-user-id":"w_gydstnfg3p5u","external-user-id":"","stream":"w_gydstnfg3p5u_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}},"event":{"ts":1696271742391}}} -{"data":{"type":"event","id":"meeting-screenshare-started","attributes":{"meeting":{"internal-meeting-id":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885"}},"event":{"ts":1696271746688}}} -{"data":{"type":"event","id":"meeting-screenshare-stopped","attributes":{"meeting":{"internal-meeting-id":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885"}},"event":{"ts":1696271749156}}} -{"data":{"type":"event","id":"chat-group-message-sent","attributes":{"meeting":{"internal-meeting-id":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885"},"chat-message":{"id":"1696271758851-0a25o7ou","message":"Public chat test","sender":{"internal-user-id":"w_gydstnfg3p5u","name":"User 2261564","time":1696271758851}},"chat-id":"MAIN-PUBLIC-GROUP-CHAT"},"event":{"ts":1696271759784}}} -{"data":{"type":"event","id":"user-emoji-changed","attributes":{"meeting":{"internal-meeting-id":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885"},"user":{"internal-user-id":"w_gydstnfg3p5u","external-user-id":"","emoji":"🙁"}},"event":{"ts":1696271766075}}} -{"data":{"type":"event","id":"user-emoji-changed","attributes":{"meeting":{"internal-meeting-id":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885"},"user":{"internal-user-id":"w_gydstnfg3p5u","external-user-id":"","emoji":"none"}},"event":{"ts":1696271771122}}} -{"data":{"type":"event","id":"user-raise-hand-changed","attributes":{"meeting":{"internal-meeting-id":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885"},"user":{"internal-user-id":"w_gydstnfg3p5u","external-user-id":"","raise-hand":true}},"event":{"ts":1696271773291}}} -{"data":{"type":"event","id":"user-raise-hand-changed","attributes":{"meeting":{"internal-meeting-id":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885"},"user":{"internal-user-id":"w_gydstnfg3p5u","external-user-id":"","raise-hand":false}},"event":{"ts":1696271777873}}} -{"data":{"type":"event","id":"poll-started","attributes":{"meeting":{"internal-meeting-id":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885"},"user":{"internal-user-id":"w_gydstnfg3p5u","external-user-id":""},"poll":{"question":"ABCD Poll test (public","answers":[{"id":0,"key":"A"},{"id":1,"key":"B"},{"id":2,"key":"C"},{"id":3,"key":"D"}]}},"event":{"ts":1696271789475}}} -{"data":{"type":"event","id":"poll-responded","attributes":{"meeting":{"internal-meeting-id":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885"},"user":{"internal-user-id":"w_gydstnfg3p5u","external-user-id":""},"poll":{"id":"2e8114441fe446507e61c6aa3de818f295584fcf-1696271692887/1/1696271789402","answerIds":[0]}},"event":{"ts":1696271792358}}} -{"data":{"type":"event","id":"user-left","attributes":{"meeting":{"internal-meeting-id":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885"},"user":{"internal-user-id":"w_8lvidy41zal2","external-user-id":""}},"event":{"ts":1696271817906}}} -{"data":{"type":"event","id":"meeting-ended","attributes":{"meeting":{"internal-meeting-id":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885"}},"event":{"ts":1696271836396}}} +{"data":{"type":"event","id":"meeting-created","attributes":{"meeting":{"internal-meeting-id":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","external-meeting-id":"random-5935090","name":"random-5935090","is-breakout":false,"parent-id":"bbb-none","duration":0,"create-time":1696436185331,"create-date":"Wed Oct 04 13:16:25 BRT 2023","moderator-pass":"mp","viewer-pass":"ap","record":false,"voice-conf":"73065","dial-number":"613-555-1234","max-users":0,"metadata":{"test-meta-param":"test-param"}}},"event":{"ts":1696436185338}}} +{"data":{"type":"event","id":"user-presenter-assigned","attributes":{"meeting":{"internal-meeting-id":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","external-meeting-id":"random-5935090"},"user":{"internal-user-id":"w_nzuqejrku243","external-user-id":"w_nzuqejrku243"}},"event":{"ts":1696436195263}}} +{"data":{"type":"event","id":"user-joined","attributes":{"meeting":{"internal-meeting-id":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","external-meeting-id":"random-5935090"},"user":{"internal-user-id":"w_nzuqejrku243","external-user-id":"w_nzuqejrku243","name":"User 3426823","role":"MODERATOR","presenter":"false"}},"event":{"ts":1696436195260}}} +{"data":{"type":"event","id":"user-presenter-assigned","attributes":{"meeting":{"internal-meeting-id":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","external-meeting-id":"random-5935090"},"user":{"internal-user-id":"w_nzuqejrku243","external-user-id":"w_nzuqejrku243"}},"event":{"ts":1696436195285}}} +{"data":{"type":"event","id":"user-presenter-assigned","attributes":{"meeting":{"internal-meeting-id":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","external-meeting-id":"random-5935090"},"user":{"internal-user-id":"w_nzuqejrku243","external-user-id":"w_nzuqejrku243"}},"event":{"ts":1696436195301}}} +{"data":{"type":"event","id":"user-joined","attributes":{"meeting":{"internal-meeting-id":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","external-meeting-id":"random-5935090"},"user":{"internal-user-id":"w_ramw3yqd7kpj","external-user-id":"w_ramw3yqd7kpj","name":"User 3426823","role":"VIEWER","presenter":"false"}},"event":{"ts":1696436195866}}} +{"data":{"type":"event","id":"user-audio-voice-enabled","attributes":{"meeting":{"internal-meeting-id":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","external-meeting-id":"random-5935090"},"user":{"internal-user-id":"w_nzuqejrku243","external-user-id":"w_nzuqejrku243","listening-only":false,"sharing-mic":true}},"event":{"ts":1696436201282}}} +{"data":{"type":"event","id":"user-audio-voice-disabled","attributes":{"meeting":{"internal-meeting-id":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","external-meeting-id":"random-5935090"},"user":{"internal-user-id":"w_nzuqejrku243","external-user-id":"w_nzuqejrku243","listening-only":false,"sharing-mic":false}},"event":{"ts":1696436205375}}} +{"data":{"type":"event","id":"user-cam-broadcast-start","attributes":{"meeting":{"internal-meeting-id":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","external-meeting-id":"random-5935090"},"user":{"internal-user-id":"w_nzuqejrku243","external-user-id":"w_nzuqejrku243","stream":"w_nzuqejrku243_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}},"event":{"ts":1696436208365}}} +{"data":{"type":"event","id":"user-cam-broadcast-end","attributes":{"meeting":{"internal-meeting-id":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","external-meeting-id":"random-5935090"},"user":{"internal-user-id":"w_nzuqejrku243","external-user-id":"w_nzuqejrku243","stream":"w_nzuqejrku243_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}},"event":{"ts":1696436209601}}} +{"data":{"type":"event","id":"meeting-screenshare-started","attributes":{"meeting":{"internal-meeting-id":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","external-meeting-id":"random-5935090"}},"event":{"ts":1696436215452}}} +{"data":{"type":"event","id":"meeting-screenshare-stopped","attributes":{"meeting":{"internal-meeting-id":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","external-meeting-id":"random-5935090"}},"event":{"ts":1696436216557}}} +{"data":{"type":"event","id":"chat-group-message-sent","attributes":{"meeting":{"internal-meeting-id":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","external-meeting-id":"random-5935090"},"chat-message":{"id":"1696436220774-9ll3p48g","message":"Public chat test","sender":{"internal-user-id":"w_nzuqejrku243","name":"User 3426823","time":1696436220774}},"chat-id":"MAIN-PUBLIC-GROUP-CHAT"},"event":{"ts":1696436220791}}} +{"data":{"type":"event","id":"user-emoji-changed","attributes":{"meeting":{"internal-meeting-id":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","external-meeting-id":"random-5935090"},"user":{"internal-user-id":"w_nzuqejrku243","external-user-id":"w_nzuqejrku243","emoji":"🙁"}},"event":{"ts":1696436225296}}} +{"data":{"type":"event","id":"user-emoji-changed","attributes":{"meeting":{"internal-meeting-id":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","external-meeting-id":"random-5935090"},"user":{"internal-user-id":"w_nzuqejrku243","external-user-id":"w_nzuqejrku243","emoji":"none"}},"event":{"ts":1696436227191}}} +{"data":{"type":"event","id":"user-raise-hand-changed","attributes":{"meeting":{"internal-meeting-id":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","external-meeting-id":"random-5935090"},"user":{"internal-user-id":"w_nzuqejrku243","external-user-id":"w_nzuqejrku243","raise-hand":true}},"event":{"ts":1696436229156}}} +{"data":{"type":"event","id":"user-raise-hand-changed","attributes":{"meeting":{"internal-meeting-id":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","external-meeting-id":"random-5935090"},"user":{"internal-user-id":"w_nzuqejrku243","external-user-id":"w_nzuqejrku243","raise-hand":false}},"event":{"ts":1696436231122}}} +{"data":{"type":"event","id":"poll-started","attributes":{"meeting":{"internal-meeting-id":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","external-meeting-id":"random-5935090"},"user":{"internal-user-id":"w_nzuqejrku243","external-user-id":"w_nzuqejrku243"},"poll":{"question":"ABCD Poll test (public)","answers":[{"id":0,"key":"A"},{"id":1,"key":"B"},{"id":2,"key":"C"},{"id":3,"key":"D"}]}},"event":{"ts":1696436242125}}} +{"data":{"type":"event","id":"poll-responded","attributes":{"meeting":{"internal-meeting-id":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","external-meeting-id":"random-5935090"},"user":{"internal-user-id":"w_nzuqejrku243","external-user-id":"w_nzuqejrku243"},"poll":{"id":"2519ad945dc29da2da3132bc29f4450359635b1c-1696436185332/1/1696436242099","answerIds":[0]}},"event":{"ts":1696436245008}}} +{"data":{"type":"event","id":"user-left","attributes":{"meeting":{"internal-meeting-id":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","external-meeting-id":"random-5935090"},"user":{"internal-user-id":"w_ramw3yqd7kpj","external-user-id":"w_ramw3yqd7kpj"}},"event":{"ts":1696436270353}}} +{"data":{"type":"event","id":"meeting-ended","attributes":{"meeting":{"internal-meeting-id":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","external-meeting-id":"random-5935090"}},"event":{"ts":1696436281162}}} diff --git a/example/events/pretty-print.js b/example/events/pretty-print.js new file mode 100644 index 0000000..4380675 --- /dev/null +++ b/example/events/pretty-print.js @@ -0,0 +1,33 @@ +/* Read mapped-events.json and raw-events.json from the current directory and + * print them in a human-readable format (new files with a -pretty suffix). + * Overwrites existing files. + */ + +import { open, rm, writeFile } from 'node:fs/promises'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +(async () => { + await rm(join(__dirname, 'mapped-events-pretty.json'), { force: true }); + const mHandle = await open(join(__dirname, 'mapped-events.json'), 'r'); + for await (const line of mHandle.readLines()) { + // Parse JSON and write it pretty-printed (2 spaces) to the suffixed file + // Append a newline to the end of the file + await writeFile( + join(__dirname, 'mapped-events-pretty.json'), + JSON.stringify(JSON.parse(line), null, 2) + '\n', { flag: 'a' }, + ); + } + + await rm(join(__dirname, 'raw-events-pretty.json'), { force: true }); + const rHandle = await open(join(__dirname, 'raw-events.json'), 'r'); + for await (const line of rHandle.readLines()) { + await writeFile( + join(__dirname, 'raw-events-pretty.json'), + JSON.stringify(JSON.parse(line), null, 2) + '\n', { flag: 'a' }, + ); + } +})(); diff --git a/example/events/raw-events.json b/example/events/raw-events.json index 6124bfc..10f3784 100644 --- a/example/events/raw-events.json +++ b/example/events/raw-events.json @@ -1,21 +1,21 @@ -{"envelope":{"name":"MeetingCreatedEvtMsg","routing":{"sender":"bbb-apps-akka"},"timestamp":1696271692890},"core":{"header":{"name":"MeetingCreatedEvtMsg"},"body":{"props":{"meetingProp":{"name":"random-6696736","extId":"random-6696736","intId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","meetingCameraCap":50,"maxPinnedCameras":3,"isBreakout":false,"disabledFeatures":[],"notifyRecordingIsOn":false,"presentationUploadExternalDescription":"","presentationUploadExternalUrl":""},"breakoutProps":{"parentId":"bbb-none","sequence":0,"freeJoin":false,"breakoutRooms":[],"record":false,"privateChatEnabled":true,"captureNotes":false,"captureSlides":false,"captureNotesFilename":"%%CONFNAME%%","captureSlidesFilename":"%%CONFNAME%%"},"durationProps":{"duration":0,"createdTime":1696271692885,"createdDate":"Mon Oct 02 15:34:52 BRT 2023","meetingExpireIfNoUserJoinedInMinutes":5,"meetingExpireWhenLastUserLeftInMinutes":1,"userInactivityInspectTimerInMinutes":0,"userInactivityThresholdInMinutes":30,"userActivitySignResponseDelayInMinutes":5,"endWhenNoModerator":false,"endWhenNoModeratorDelayInMinutes":1},"password":{"moderatorPass":"mp","viewerPass":"ap","learningDashboardAccessToken":"fhlxxpwiwwpx"},"recordProp":{"record":false,"autoStartRecording":false,"allowStartStopRecording":true,"recordFullDurationMedia":false,"keepEvents":true},"welcomeProp":{"welcomeMsgTemplate":"
Welcome to %%CONFNAME%%!","welcomeMsg":"
Welcome to random-6696736!","modOnlyMessage":""},"voiceProp":{"telVoice":"79025","voiceConf":"79025","dialNumber":"613-555-1234","muteOnStart":false},"usersProp":{"maxUsers":0,"maxUserConcurrentAccesses":3,"webcamsOnlyForModerator":false,"userCameraCap":3,"guestPolicy":"ASK_MODERATOR","meetingLayout":"CUSTOM_LAYOUT","allowModsToUnmuteUsers":true,"allowModsToEjectCameras":true,"authenticatedGuest":false},"metadataProp":{"metadata":{"test-meta-param":"\"test-param\""}},"lockSettingsProps":{"disableCam":false,"disableMic":false,"disablePrivateChat":false,"disablePublicChat":false,"disableNotes":false,"hideUserList":false,"lockOnJoin":true,"lockOnJoinConfigurable":false,"hideViewersCursor":false,"hideViewersAnnotation":false},"systemProps":{"html5InstanceId":1},"groups":[]}}}} -{"envelope":{"name":"PresenterAssignedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"timestamp":1696271703894},"core":{"header":{"name":"PresenterAssignedEvtMsg","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"body":{"presenterId":"w_gydstnfg3p5u","presenterName":"User 2261564","assignedBy":"w_gydstnfg3p5u"}}} -{"envelope":{"name":"UserJoinedMeetingEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"timestamp":1696271703894},"core":{"header":{"name":"UserJoinedMeetingEvtMsg","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"body":{"intId":"w_gydstnfg3p5u","extId":"w_gydstnfg3p5u","name":"User 2261564","role":"MODERATOR","guest":false,"authed":true,"guestStatus":"ALLOW","emoji":"none","reactionEmoji":"none","raiseHand":false,"away":false,"pin":false,"presenter":false,"locked":true,"avatar":"","color":"#7b1fa2","clientType":"HTML5"}}} -{"envelope":{"name":"PresenterAssignedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"timestamp":1696271703931},"core":{"header":{"name":"PresenterAssignedEvtMsg","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"body":{"presenterId":"w_gydstnfg3p5u","presenterName":"User 2261564","assignedBy":"w_gydstnfg3p5u"}}} -{"envelope":{"name":"PresenterAssignedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"timestamp":1696271703946},"core":{"header":{"name":"PresenterAssignedEvtMsg","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"body":{"presenterId":"w_gydstnfg3p5u","presenterName":"User 2261564","assignedBy":"w_gydstnfg3p5u"}}} -{"envelope":{"name":"UserJoinedMeetingEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_8lvidy41zal2"},"timestamp":1696271711009},"core":{"header":{"name":"UserJoinedMeetingEvtMsg","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_8lvidy41zal2"},"body":{"intId":"w_8lvidy41zal2","extId":"w_8lvidy41zal2","name":"User 2261564","role":"VIEWER","guest":false,"authed":true,"guestStatus":"ALLOW","emoji":"none","reactionEmoji":"none","raiseHand":false,"away":false,"pin":false,"presenter":false,"locked":true,"avatar":"","color":"#6a1b9a","clientType":"HTML5"}}} -{"envelope":{"name":"UserJoinedVoiceConfToClientEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"timestamp":1696271733551},"core":{"header":{"name":"UserJoinedVoiceConfToClientEvtMsg","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"body":{"voiceConf":"79025","intId":"w_gydstnfg3p5u","voiceUserId":"1","callerName":"User+2261564","callerNum":"w_gydstnfg3p5u_2-bbbID-User+2261564","color":"#4a148c","muted":false,"talking":false,"callingWith":"none","listenOnly":false}}} -{"envelope":{"name":"UserLeftVoiceConfToClientEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"timestamp":1696271736360},"core":{"header":{"name":"UserLeftVoiceConfToClientEvtMsg","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"body":{"voiceConf":"79025","intId":"w_gydstnfg3p5u","voiceUserId":"w_gydstnfg3p5u"}}} -{"envelope":{"name":"UserBroadcastCamStartedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"timestamp":1696271740854},"core":{"header":{"name":"UserBroadcastCamStartedEvtMsg","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"body":{"userId":"w_gydstnfg3p5u","stream":"w_gydstnfg3p5u_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}}} -{"envelope":{"name":"UserBroadcastCamStoppedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"timestamp":1696271742384},"core":{"header":{"name":"UserBroadcastCamStoppedEvtMsg","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"body":{"userId":"w_gydstnfg3p5u","stream":"w_gydstnfg3p5u_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}}} -{"envelope":{"name":"ScreenshareRtmpBroadcastStartedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"not-used"},"timestamp":1696271746676},"core":{"header":{"name":"ScreenshareRtmpBroadcastStartedEvtMsg","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"not-used"},"body":{"voiceConf":"79025","screenshareConf":"79025","stream":"0b133d70-1699-4337-a3ba-66824d8a9a8e","vidWidth":0,"vidHeight":0,"timestamp":"1696271746659","hasAudio":true,"contentType":"screenshare"}}} -{"envelope":{"name":"ScreenshareRtmpBroadcastStoppedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"not-used"},"timestamp":1696271749144},"core":{"header":{"name":"ScreenshareRtmpBroadcastStoppedEvtMsg","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"not-used"},"body":{"voiceConf":"","screenshareConf":"","stream":"","vidWidth":0,"vidHeight":0,"timestamp":""}}} -{"envelope":{"name":"GroupChatMessageBroadcastEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"timestamp":1696271759769},"core":{"header":{"name":"GroupChatMessageBroadcastEvtMsg","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"body":{"chatId":"MAIN-PUBLIC-GROUP-CHAT","msg":{"id":"1696271758851-0a25o7ou","timestamp":1696271758851,"correlationId":"w_gydstnfg3p5u-1696271758385","sender":{"id":"w_gydstnfg3p5u","name":"User 2261564","role":"MODERATOR"},"chatEmphasizedText":true,"message":"Public chat test"}}}} -{"envelope":{"name":"UserReactionEmojiChangedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"timestamp":1696271766069},"core":{"header":{"name":"UserReactionEmojiChangedEvtMsg","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"body":{"userId":"w_gydstnfg3p5u","reactionEmoji":"🙁"}}} -{"envelope":{"name":"UserReactionEmojiChangedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"timestamp":1696271771117},"core":{"header":{"name":"UserReactionEmojiChangedEvtMsg","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"body":{"userId":"w_gydstnfg3p5u","reactionEmoji":"none"}}} -{"envelope":{"name":"UserRaiseHandChangedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"timestamp":1696271773281},"core":{"header":{"name":"UserRaiseHandChangedEvtMsg","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"body":{"userId":"w_gydstnfg3p5u","raiseHand":true}}} -{"envelope":{"name":"UserRaiseHandChangedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"timestamp":1696271777871},"core":{"header":{"name":"UserRaiseHandChangedEvtMsg","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"body":{"userId":"w_gydstnfg3p5u","raiseHand":false}}} -{"envelope":{"name":"PollStartedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"timestamp":1696271789454},"core":{"header":{"name":"PollStartedEvtMsg","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"body":{"userId":"w_gydstnfg3p5u","pollId":"2e8114441fe446507e61c6aa3de818f295584fcf-1696271692887/1/1696271789402","pollType":"A-4","secretPoll":false,"question":"ABCD Poll test (public","poll":{"id":"2e8114441fe446507e61c6aa3de818f295584fcf-1696271692887/1/1696271789402","isMultipleResponse":false,"answers":[{"id":0,"key":"A"},{"id":1,"key":"B"},{"id":2,"key":"C"},{"id":3,"key":"D"}]}}}} -{"envelope":{"name":"UserRespondedToPollRespMsg","routing":{"msgType":"DIRECT","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"timestamp":1696271792333},"core":{"header":{"name":"UserRespondedToPollRespMsg","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_gydstnfg3p5u"},"body":{"pollId":"2e8114441fe446507e61c6aa3de818f295584fcf-1696271692887/1/1696271789402","userId":"w_8lvidy41zal2","answerIds":[0]}}} -{"envelope":{"name":"UserLeftMeetingEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_8lvidy41zal2"},"timestamp":1696271817904},"core":{"header":{"name":"UserLeftMeetingEvtMsg","meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885","userId":"w_8lvidy41zal2"},"body":{"intId":"w_8lvidy41zal2","eject":false,"ejectedBy":"","reason":"","reasonCode":""}}} -{"envelope":{"name":"MeetingDestroyedEvtMsg","routing":{"sender":"bbb-apps-akka"},"timestamp":1696271836394},"core":{"header":{"name":"MeetingDestroyedEvtMsg"},"body":{"meetingId":"cfbf5ac5fc92eaa37719e2eee00fa373071a61a4-1696271692885"}}} +{"envelope":{"name":"MeetingCreatedEvtMsg","routing":{"sender":"bbb-apps-akka"},"timestamp":1696436185335},"core":{"header":{"name":"MeetingCreatedEvtMsg"},"body":{"props":{"meetingProp":{"name":"random-5935090","extId":"random-5935090","intId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","meetingCameraCap":50,"maxPinnedCameras":3,"isBreakout":false,"disabledFeatures":[],"notifyRecordingIsOn":false,"presentationUploadExternalDescription":"","presentationUploadExternalUrl":""},"breakoutProps":{"parentId":"bbb-none","sequence":0,"freeJoin":false,"breakoutRooms":[],"record":false,"privateChatEnabled":true,"captureNotes":false,"captureSlides":false,"captureNotesFilename":"%%CONFNAME%%","captureSlidesFilename":"%%CONFNAME%%"},"durationProps":{"duration":0,"createdTime":1696436185331,"createdDate":"Wed Oct 04 13:16:25 BRT 2023","meetingExpireIfNoUserJoinedInMinutes":5,"meetingExpireWhenLastUserLeftInMinutes":1,"userInactivityInspectTimerInMinutes":0,"userInactivityThresholdInMinutes":30,"userActivitySignResponseDelayInMinutes":5,"endWhenNoModerator":false,"endWhenNoModeratorDelayInMinutes":1},"password":{"moderatorPass":"mp","viewerPass":"ap","learningDashboardAccessToken":"kax7opjvcmem"},"recordProp":{"record":false,"autoStartRecording":false,"allowStartStopRecording":true,"recordFullDurationMedia":false,"keepEvents":true},"welcomeProp":{"welcomeMsgTemplate":"
Welcome to %%CONFNAME%%!","welcomeMsg":"
Welcome to random-5935090!","modOnlyMessage":""},"voiceProp":{"telVoice":"73065","voiceConf":"73065","dialNumber":"613-555-1234","muteOnStart":false},"usersProp":{"maxUsers":0,"maxUserConcurrentAccesses":3,"webcamsOnlyForModerator":false,"userCameraCap":3,"guestPolicy":"ASK_MODERATOR","meetingLayout":"CUSTOM_LAYOUT","allowModsToUnmuteUsers":true,"allowModsToEjectCameras":true,"authenticatedGuest":false},"metadataProp":{"metadata":{"test-meta-param":"test-param"}},"lockSettingsProps":{"disableCam":false,"disableMic":false,"disablePrivateChat":false,"disablePublicChat":false,"disableNotes":false,"hideUserList":false,"lockOnJoin":true,"lockOnJoinConfigurable":false,"hideViewersCursor":false,"hideViewersAnnotation":false},"systemProps":{"html5InstanceId":1},"groups":[]}}}} +{"envelope":{"name":"PresenterAssignedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"timestamp":1696436195257},"core":{"header":{"name":"PresenterAssignedEvtMsg","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"body":{"presenterId":"w_nzuqejrku243","presenterName":"User 3426823","assignedBy":"w_nzuqejrku243"}}} +{"envelope":{"name":"UserJoinedMeetingEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"timestamp":1696436195257},"core":{"header":{"name":"UserJoinedMeetingEvtMsg","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"body":{"intId":"w_nzuqejrku243","extId":"w_nzuqejrku243","name":"User 3426823","role":"MODERATOR","guest":false,"authed":true,"guestStatus":"ALLOW","emoji":"none","reactionEmoji":"none","raiseHand":false,"away":false,"pin":false,"presenter":false,"locked":true,"avatar":"","color":"#7b1fa2","clientType":"HTML5"}}} +{"envelope":{"name":"PresenterAssignedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"timestamp":1696436195282},"core":{"header":{"name":"PresenterAssignedEvtMsg","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"body":{"presenterId":"w_nzuqejrku243","presenterName":"User 3426823","assignedBy":"w_nzuqejrku243"}}} +{"envelope":{"name":"PresenterAssignedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"timestamp":1696436195297},"core":{"header":{"name":"PresenterAssignedEvtMsg","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"body":{"presenterId":"w_nzuqejrku243","presenterName":"User 3426823","assignedBy":"w_nzuqejrku243"}}} +{"envelope":{"name":"UserJoinedMeetingEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_ramw3yqd7kpj"},"timestamp":1696436195862},"core":{"header":{"name":"UserJoinedMeetingEvtMsg","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_ramw3yqd7kpj"},"body":{"intId":"w_ramw3yqd7kpj","extId":"w_ramw3yqd7kpj","name":"User 3426823","role":"VIEWER","guest":false,"authed":true,"guestStatus":"ALLOW","emoji":"none","reactionEmoji":"none","raiseHand":false,"away":false,"pin":false,"presenter":false,"locked":true,"avatar":"","color":"#6a1b9a","clientType":"HTML5"}}} +{"envelope":{"name":"UserJoinedVoiceConfToClientEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"timestamp":1696436201276},"core":{"header":{"name":"UserJoinedVoiceConfToClientEvtMsg","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"body":{"voiceConf":"73065","intId":"w_nzuqejrku243","voiceUserId":"2","callerName":"User+3426823","callerNum":"w_nzuqejrku243_1-bbbID-User+3426823","color":"#4a148c","muted":false,"talking":false,"callingWith":"none","listenOnly":false}}} +{"envelope":{"name":"UserLeftVoiceConfToClientEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"timestamp":1696436205370},"core":{"header":{"name":"UserLeftVoiceConfToClientEvtMsg","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"body":{"voiceConf":"73065","intId":"w_nzuqejrku243","voiceUserId":"w_nzuqejrku243"}}} +{"envelope":{"name":"UserBroadcastCamStartedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"timestamp":1696436208363},"core":{"header":{"name":"UserBroadcastCamStartedEvtMsg","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"body":{"userId":"w_nzuqejrku243","stream":"w_nzuqejrku243_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}}} +{"envelope":{"name":"UserBroadcastCamStoppedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"timestamp":1696436209599},"core":{"header":{"name":"UserBroadcastCamStoppedEvtMsg","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"body":{"userId":"w_nzuqejrku243","stream":"w_nzuqejrku243_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}}} +{"envelope":{"name":"ScreenshareRtmpBroadcastStartedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"not-used"},"timestamp":1696436215445},"core":{"header":{"name":"ScreenshareRtmpBroadcastStartedEvtMsg","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"not-used"},"body":{"voiceConf":"73065","screenshareConf":"73065","stream":"d9f27fb6-eed2-498d-8deb-b8870b9381b9","vidWidth":0,"vidHeight":0,"timestamp":"1696436215436","hasAudio":false,"contentType":"screenshare"}}} +{"envelope":{"name":"ScreenshareRtmpBroadcastStoppedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"not-used"},"timestamp":1696436216551},"core":{"header":{"name":"ScreenshareRtmpBroadcastStoppedEvtMsg","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"not-used"},"body":{"voiceConf":"","screenshareConf":"","stream":"","vidWidth":0,"vidHeight":0,"timestamp":""}}} +{"envelope":{"name":"GroupChatMessageBroadcastEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"timestamp":1696436220782},"core":{"header":{"name":"GroupChatMessageBroadcastEvtMsg","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"body":{"chatId":"MAIN-PUBLIC-GROUP-CHAT","msg":{"id":"1696436220774-9ll3p48g","timestamp":1696436220774,"correlationId":"w_nzuqejrku243-1696436220761","sender":{"id":"w_nzuqejrku243","name":"User 3426823","role":"MODERATOR"},"chatEmphasizedText":true,"message":"Public chat test"}}}} +{"envelope":{"name":"UserReactionEmojiChangedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"timestamp":1696436225290},"core":{"header":{"name":"UserReactionEmojiChangedEvtMsg","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"body":{"userId":"w_nzuqejrku243","reactionEmoji":"🙁"}}} +{"envelope":{"name":"UserReactionEmojiChangedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"timestamp":1696436227190},"core":{"header":{"name":"UserReactionEmojiChangedEvtMsg","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"body":{"userId":"w_nzuqejrku243","reactionEmoji":"none"}}} +{"envelope":{"name":"UserRaiseHandChangedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"timestamp":1696436229151},"core":{"header":{"name":"UserRaiseHandChangedEvtMsg","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"body":{"userId":"w_nzuqejrku243","raiseHand":true}}} +{"envelope":{"name":"UserRaiseHandChangedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"timestamp":1696436231120},"core":{"header":{"name":"UserRaiseHandChangedEvtMsg","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"body":{"userId":"w_nzuqejrku243","raiseHand":false}}} +{"envelope":{"name":"PollStartedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"timestamp":1696436242115},"core":{"header":{"name":"PollStartedEvtMsg","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"body":{"userId":"w_nzuqejrku243","pollId":"2519ad945dc29da2da3132bc29f4450359635b1c-1696436185332/1/1696436242099","pollType":"A-4","secretPoll":false,"question":"ABCD Poll test (public)","poll":{"id":"2519ad945dc29da2da3132bc29f4450359635b1c-1696436185332/1/1696436242099","isMultipleResponse":false,"answers":[{"id":0,"key":"A"},{"id":1,"key":"B"},{"id":2,"key":"C"},{"id":3,"key":"D"}]}}}} +{"envelope":{"name":"UserRespondedToPollRespMsg","routing":{"msgType":"DIRECT","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"timestamp":1696436244987},"core":{"header":{"name":"UserRespondedToPollRespMsg","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"body":{"pollId":"2519ad945dc29da2da3132bc29f4450359635b1c-1696436185332/1/1696436242099","userId":"w_ramw3yqd7kpj","answerIds":[0]}}} +{"envelope":{"name":"UserLeftMeetingEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_ramw3yqd7kpj"},"timestamp":1696436270350},"core":{"header":{"name":"UserLeftMeetingEvtMsg","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_ramw3yqd7kpj"},"body":{"intId":"w_ramw3yqd7kpj","eject":false,"ejectedBy":"","reason":"","reasonCode":""}}} +{"envelope":{"name":"MeetingDestroyedEvtMsg","routing":{"sender":"bbb-apps-akka"},"timestamp":1696436281160},"core":{"header":{"name":"MeetingDestroyedEvtMsg"},"body":{"meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331"}}} From 505eff085f69483ffd1816d4f7b9a6041f3fd9b8 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Wed, 4 Oct 2023 20:11:54 -0300 Subject: [PATCH 037/154] fix: accept mapped poll events as input events --- src/process/event.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/process/event.js b/src/process/event.js index 27fe09f..2230abe 100644 --- a/src/process/event.js +++ b/src/process/event.js @@ -46,6 +46,8 @@ export default class WebhooksEvent { "rap-deleted", "rap-post-publish-started", "rap-post-publish-ended", + "poll-started", + "poll-responded", ]; static RAW = { From b359fcb8c595fe81e7cff19729ff2751f6b95213 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Wed, 4 Oct 2023 23:07:11 -0300 Subject: [PATCH 038/154] fix: use URL instead of host/port/pass on Redis client creation Also added REDIS_PASSWORD env var mapping --- config/custom-environment-variables.yml | 3 +++ src/db/redis/index.js | 12 +++++------- src/in/redis/index.js | 11 +++++------ src/modules/index.js | 13 ++++++++++--- 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/config/custom-environment-variables.yml b/config/custom-environment-variables.yml index 0fc541f..178c747 100644 --- a/config/custom-environment-variables.yml +++ b/config/custom-environment-variables.yml @@ -6,6 +6,7 @@ bbb: redis: host: REDIS_HOST port: REDIS_PORT + password: REDIS_PASSWORD log: level: LOG_LEVEL @@ -26,11 +27,13 @@ modules: config: host: REDIS_HOST port: REDIS_PORT + password: REDIS_PASSWORD ../in/redis/index.js: config: redis: host: REDIS_HOST port: REDIS_PORT + password: REDIS_PASSWORD ../out/webhooks/index.js: config: api: diff --git a/src/db/redis/index.js b/src/db/redis/index.js index 35daa3d..3db13ca 100644 --- a/src/db/redis/index.js +++ b/src/db/redis/index.js @@ -1,4 +1,4 @@ -import redis from 'redis'; +import { createClient } from 'redis'; import config from 'config'; import HookCompartment from './hooks.js'; import IDMappingC from './id-mapping.js'; @@ -37,12 +37,10 @@ export default class RedisDB { } async load() { - const { host, port, password } = this.config; - - this._redisClient = redis.createClient({ - host, - port, - password, + const { password, host, port } = this.config; + const redisUrl = `redis://${password ? `:${password}@` : ''}${host}:${port}`; + this._redisClient = createClient({ + url: redisUrl, }); this._redisClient.on("error", this._onRedisError.bind(this)); await this._redisClient.connect(); diff --git a/src/in/redis/index.js b/src/in/redis/index.js index b36c267..f54591c 100644 --- a/src/in/redis/index.js +++ b/src/in/redis/index.js @@ -1,4 +1,4 @@ -import redis from 'redis'; +import { createClient } from 'redis'; import Utils from '../../common/utils.js'; /* @@ -80,12 +80,11 @@ export default class InRedis { async load () { if (this._validateConfig()) { - this.pubsub = redis.createClient({ - host: this.config.host, - port: this.config.port, - password: this.config.password, + const { password, host, port } = this.config.redis; + const redisUrl = `redis://${password ? `:${password}@` : ''}${host}:${port}`; + this.pubsub = createClient({ + url: redisUrl, }); - await this.pubsub.connect(); await this._subscribeToEvents(); } diff --git a/src/modules/index.js b/src/modules/index.js index b0b7bf9..914b20a 100644 --- a/src/modules/index.js +++ b/src/modules/index.js @@ -11,6 +11,12 @@ import { } from './definitions.js'; const UNEXPECTED_TERMINATION_SIGNALS = ['SIGABRT', 'SIGBUS', 'SIGSEGV', 'SIGILL']; +const REDIS_CONF = { + host: config.get('redis.host'), + port: config.get('redis.port'), + password: config.has('redis.password') ? config.get('redis.password') : undefined, +} +REDIS_CONF.REDIS_URL = `redis://${REDIS_CONF.password ? `:${REDIS_CONF.password}@` : ''}${REDIS_CONF.host}:${REDIS_CONF.port}`; const BASE_CONFIGURATION = { server: { domain: config.get('bbb.serverDomain'), @@ -18,9 +24,10 @@ const BASE_CONFIGURATION = { auth2_0: config.get('bbb.auth2_0'), }, redis: { - host: config.get('redis.host'), - port: config.get('redis.port'), - password: config.has('redis.password') ? config.get('redis.password') : undefined, + host: REDIS_CONF.host, + port: REDIS_CONF.port, + password: REDIS_CONF.password, + url: REDIS_CONF.REDIS_URL, }, } From 0a28d57f0bbe04ecab2f6bddc13c6ddbca517563 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Thu, 5 Oct 2023 10:15:27 -0300 Subject: [PATCH 039/154] fix: missing poll ID from poll-started --- example/events/mapped-events.json | 42 +++++++++++++++---------------- example/events/raw-events.json | 42 +++++++++++++++---------------- src/process/event.js | 4 ++- 3 files changed, 45 insertions(+), 43 deletions(-) diff --git a/example/events/mapped-events.json b/example/events/mapped-events.json index 8a20baf..003f4ea 100644 --- a/example/events/mapped-events.json +++ b/example/events/mapped-events.json @@ -1,21 +1,21 @@ -{"data":{"type":"event","id":"meeting-created","attributes":{"meeting":{"internal-meeting-id":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","external-meeting-id":"random-5935090","name":"random-5935090","is-breakout":false,"parent-id":"bbb-none","duration":0,"create-time":1696436185331,"create-date":"Wed Oct 04 13:16:25 BRT 2023","moderator-pass":"mp","viewer-pass":"ap","record":false,"voice-conf":"73065","dial-number":"613-555-1234","max-users":0,"metadata":{"test-meta-param":"test-param"}}},"event":{"ts":1696436185338}}} -{"data":{"type":"event","id":"user-presenter-assigned","attributes":{"meeting":{"internal-meeting-id":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","external-meeting-id":"random-5935090"},"user":{"internal-user-id":"w_nzuqejrku243","external-user-id":"w_nzuqejrku243"}},"event":{"ts":1696436195263}}} -{"data":{"type":"event","id":"user-joined","attributes":{"meeting":{"internal-meeting-id":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","external-meeting-id":"random-5935090"},"user":{"internal-user-id":"w_nzuqejrku243","external-user-id":"w_nzuqejrku243","name":"User 3426823","role":"MODERATOR","presenter":"false"}},"event":{"ts":1696436195260}}} -{"data":{"type":"event","id":"user-presenter-assigned","attributes":{"meeting":{"internal-meeting-id":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","external-meeting-id":"random-5935090"},"user":{"internal-user-id":"w_nzuqejrku243","external-user-id":"w_nzuqejrku243"}},"event":{"ts":1696436195285}}} -{"data":{"type":"event","id":"user-presenter-assigned","attributes":{"meeting":{"internal-meeting-id":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","external-meeting-id":"random-5935090"},"user":{"internal-user-id":"w_nzuqejrku243","external-user-id":"w_nzuqejrku243"}},"event":{"ts":1696436195301}}} -{"data":{"type":"event","id":"user-joined","attributes":{"meeting":{"internal-meeting-id":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","external-meeting-id":"random-5935090"},"user":{"internal-user-id":"w_ramw3yqd7kpj","external-user-id":"w_ramw3yqd7kpj","name":"User 3426823","role":"VIEWER","presenter":"false"}},"event":{"ts":1696436195866}}} -{"data":{"type":"event","id":"user-audio-voice-enabled","attributes":{"meeting":{"internal-meeting-id":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","external-meeting-id":"random-5935090"},"user":{"internal-user-id":"w_nzuqejrku243","external-user-id":"w_nzuqejrku243","listening-only":false,"sharing-mic":true}},"event":{"ts":1696436201282}}} -{"data":{"type":"event","id":"user-audio-voice-disabled","attributes":{"meeting":{"internal-meeting-id":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","external-meeting-id":"random-5935090"},"user":{"internal-user-id":"w_nzuqejrku243","external-user-id":"w_nzuqejrku243","listening-only":false,"sharing-mic":false}},"event":{"ts":1696436205375}}} -{"data":{"type":"event","id":"user-cam-broadcast-start","attributes":{"meeting":{"internal-meeting-id":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","external-meeting-id":"random-5935090"},"user":{"internal-user-id":"w_nzuqejrku243","external-user-id":"w_nzuqejrku243","stream":"w_nzuqejrku243_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}},"event":{"ts":1696436208365}}} -{"data":{"type":"event","id":"user-cam-broadcast-end","attributes":{"meeting":{"internal-meeting-id":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","external-meeting-id":"random-5935090"},"user":{"internal-user-id":"w_nzuqejrku243","external-user-id":"w_nzuqejrku243","stream":"w_nzuqejrku243_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}},"event":{"ts":1696436209601}}} -{"data":{"type":"event","id":"meeting-screenshare-started","attributes":{"meeting":{"internal-meeting-id":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","external-meeting-id":"random-5935090"}},"event":{"ts":1696436215452}}} -{"data":{"type":"event","id":"meeting-screenshare-stopped","attributes":{"meeting":{"internal-meeting-id":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","external-meeting-id":"random-5935090"}},"event":{"ts":1696436216557}}} -{"data":{"type":"event","id":"chat-group-message-sent","attributes":{"meeting":{"internal-meeting-id":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","external-meeting-id":"random-5935090"},"chat-message":{"id":"1696436220774-9ll3p48g","message":"Public chat test","sender":{"internal-user-id":"w_nzuqejrku243","name":"User 3426823","time":1696436220774}},"chat-id":"MAIN-PUBLIC-GROUP-CHAT"},"event":{"ts":1696436220791}}} -{"data":{"type":"event","id":"user-emoji-changed","attributes":{"meeting":{"internal-meeting-id":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","external-meeting-id":"random-5935090"},"user":{"internal-user-id":"w_nzuqejrku243","external-user-id":"w_nzuqejrku243","emoji":"🙁"}},"event":{"ts":1696436225296}}} -{"data":{"type":"event","id":"user-emoji-changed","attributes":{"meeting":{"internal-meeting-id":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","external-meeting-id":"random-5935090"},"user":{"internal-user-id":"w_nzuqejrku243","external-user-id":"w_nzuqejrku243","emoji":"none"}},"event":{"ts":1696436227191}}} -{"data":{"type":"event","id":"user-raise-hand-changed","attributes":{"meeting":{"internal-meeting-id":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","external-meeting-id":"random-5935090"},"user":{"internal-user-id":"w_nzuqejrku243","external-user-id":"w_nzuqejrku243","raise-hand":true}},"event":{"ts":1696436229156}}} -{"data":{"type":"event","id":"user-raise-hand-changed","attributes":{"meeting":{"internal-meeting-id":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","external-meeting-id":"random-5935090"},"user":{"internal-user-id":"w_nzuqejrku243","external-user-id":"w_nzuqejrku243","raise-hand":false}},"event":{"ts":1696436231122}}} -{"data":{"type":"event","id":"poll-started","attributes":{"meeting":{"internal-meeting-id":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","external-meeting-id":"random-5935090"},"user":{"internal-user-id":"w_nzuqejrku243","external-user-id":"w_nzuqejrku243"},"poll":{"question":"ABCD Poll test (public)","answers":[{"id":0,"key":"A"},{"id":1,"key":"B"},{"id":2,"key":"C"},{"id":3,"key":"D"}]}},"event":{"ts":1696436242125}}} -{"data":{"type":"event","id":"poll-responded","attributes":{"meeting":{"internal-meeting-id":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","external-meeting-id":"random-5935090"},"user":{"internal-user-id":"w_nzuqejrku243","external-user-id":"w_nzuqejrku243"},"poll":{"id":"2519ad945dc29da2da3132bc29f4450359635b1c-1696436185332/1/1696436242099","answerIds":[0]}},"event":{"ts":1696436245008}}} -{"data":{"type":"event","id":"user-left","attributes":{"meeting":{"internal-meeting-id":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","external-meeting-id":"random-5935090"},"user":{"internal-user-id":"w_ramw3yqd7kpj","external-user-id":"w_ramw3yqd7kpj"}},"event":{"ts":1696436270353}}} -{"data":{"type":"event","id":"meeting-ended","attributes":{"meeting":{"internal-meeting-id":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","external-meeting-id":"random-5935090"}},"event":{"ts":1696436281162}}} +{"data":{"type":"event","id":"meeting-created","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035","name":"random-3136035","is-breakout":false,"parent-id":"bbb-none","duration":0,"create-time":1696511528029,"create-date":"Thu Oct 05 10:12:08 BRT 2023","moderator-pass":"mp","viewer-pass":"ap","record":false,"voice-conf":"79052","dial-number":"613-555-1234","max-users":0,"metadata":{}}},"event":{"ts":1696511528509}}} +{"data":{"type":"event","id":"user-joined","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"user":{"internal-user-id":"w_1fewpbchnudl","external-user-id":"w_1fewpbchnudl","name":"User 1430416","role":"MODERATOR","presenter":"false"}},"event":{"ts":1696511534740}}} +{"data":{"type":"event","id":"user-presenter-assigned","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"user":{"internal-user-id":"w_1fewpbchnudl","external-user-id":"w_1fewpbchnudl"}},"event":{"ts":1696511534773}}} +{"data":{"type":"event","id":"user-presenter-assigned","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"user":{"internal-user-id":"w_1fewpbchnudl","external-user-id":"w_1fewpbchnudl"}},"event":{"ts":1696511534830}}} +{"data":{"type":"event","id":"user-presenter-assigned","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"user":{"internal-user-id":"w_1fewpbchnudl","external-user-id":"w_1fewpbchnudl"}},"event":{"ts":1696511534878}}} +{"data":{"type":"event","id":"user-joined","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"user":{"internal-user-id":"w_lxcl6yagcfbj","external-user-id":"w_lxcl6yagcfbj","name":"User 1430416","role":"VIEWER","presenter":"false"}},"event":{"ts":1696511539543}}} +{"data":{"type":"event","id":"user-audio-voice-enabled","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"user":{"internal-user-id":"w_1fewpbchnudl","external-user-id":"w_1fewpbchnudl","listening-only":false,"sharing-mic":true}},"event":{"ts":1696511543354}}} +{"data":{"type":"event","id":"user-audio-voice-disabled","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"user":{"internal-user-id":"w_1fewpbchnudl","external-user-id":"w_1fewpbchnudl","listening-only":false,"sharing-mic":false}},"event":{"ts":1696511545350}}} +{"data":{"type":"event","id":"user-cam-broadcast-start","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"user":{"internal-user-id":"w_1fewpbchnudl","external-user-id":"w_1fewpbchnudl","stream":"w_1fewpbchnudl_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}},"event":{"ts":1696511549091}}} +{"data":{"type":"event","id":"user-cam-broadcast-end","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"user":{"internal-user-id":"w_1fewpbchnudl","external-user-id":"w_1fewpbchnudl","stream":"w_1fewpbchnudl_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}},"event":{"ts":1696511550383}}} +{"data":{"type":"event","id":"meeting-screenshare-started","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"}},"event":{"ts":1696511553283}}} +{"data":{"type":"event","id":"meeting-screenshare-stopped","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"}},"event":{"ts":1696511556391}}} +{"data":{"type":"event","id":"chat-group-message-sent","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"chat-message":{"id":"1696511561799-rplr4p2r","message":"Public chat test","sender":{"internal-user-id":"w_1fewpbchnudl","name":"User 1430416","time":1696511561799}},"chat-id":"MAIN-PUBLIC-GROUP-CHAT"},"event":{"ts":1696511561816}}} +{"data":{"type":"event","id":"user-emoji-changed","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"user":{"internal-user-id":"w_1fewpbchnudl","external-user-id":"w_1fewpbchnudl","emoji":"🙁"}},"event":{"ts":1696511567694}}} +{"data":{"type":"event","id":"user-emoji-changed","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"user":{"internal-user-id":"w_1fewpbchnudl","external-user-id":"w_1fewpbchnudl","emoji":"none"}},"event":{"ts":1696511568939}}} +{"data":{"type":"event","id":"user-raise-hand-changed","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"user":{"internal-user-id":"w_1fewpbchnudl","external-user-id":"w_1fewpbchnudl","raise-hand":true}},"event":{"ts":1696511569968}}} +{"data":{"type":"event","id":"user-raise-hand-changed","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"user":{"internal-user-id":"w_1fewpbchnudl","external-user-id":"w_1fewpbchnudl","raise-hand":false}},"event":{"ts":1696511571012}}} +{"data":{"type":"event","id":"poll-started","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"user":{"internal-user-id":"w_1fewpbchnudl","external-user-id":"w_1fewpbchnudl"},"poll":{"id":"820cff23c27f85f7cccaffb1c6f2fd2d9adcede7-1696511528075/1/1696511581955","question":"ABCD Poll test (public)","answers":[{"id":0,"key":"A"},{"id":1,"key":"B"},{"id":2,"key":"C"},{"id":3,"key":"D"}]}},"event":{"ts":1696511581988}}} +{"data":{"type":"event","id":"poll-responded","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"user":{"internal-user-id":"w_1fewpbchnudl","external-user-id":"w_1fewpbchnudl"},"poll":{"id":"820cff23c27f85f7cccaffb1c6f2fd2d9adcede7-1696511528075/1/1696511581955","answerIds":[0]}},"event":{"ts":1696511584276}}} +{"data":{"type":"event","id":"user-left","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"user":{"internal-user-id":"w_lxcl6yagcfbj","external-user-id":"w_lxcl6yagcfbj"}},"event":{"ts":1696511603380}}} +{"data":{"type":"event","id":"meeting-ended","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"}},"event":{"ts":1696511606899}}} diff --git a/example/events/raw-events.json b/example/events/raw-events.json index 10f3784..cdfb9d6 100644 --- a/example/events/raw-events.json +++ b/example/events/raw-events.json @@ -1,21 +1,21 @@ -{"envelope":{"name":"MeetingCreatedEvtMsg","routing":{"sender":"bbb-apps-akka"},"timestamp":1696436185335},"core":{"header":{"name":"MeetingCreatedEvtMsg"},"body":{"props":{"meetingProp":{"name":"random-5935090","extId":"random-5935090","intId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","meetingCameraCap":50,"maxPinnedCameras":3,"isBreakout":false,"disabledFeatures":[],"notifyRecordingIsOn":false,"presentationUploadExternalDescription":"","presentationUploadExternalUrl":""},"breakoutProps":{"parentId":"bbb-none","sequence":0,"freeJoin":false,"breakoutRooms":[],"record":false,"privateChatEnabled":true,"captureNotes":false,"captureSlides":false,"captureNotesFilename":"%%CONFNAME%%","captureSlidesFilename":"%%CONFNAME%%"},"durationProps":{"duration":0,"createdTime":1696436185331,"createdDate":"Wed Oct 04 13:16:25 BRT 2023","meetingExpireIfNoUserJoinedInMinutes":5,"meetingExpireWhenLastUserLeftInMinutes":1,"userInactivityInspectTimerInMinutes":0,"userInactivityThresholdInMinutes":30,"userActivitySignResponseDelayInMinutes":5,"endWhenNoModerator":false,"endWhenNoModeratorDelayInMinutes":1},"password":{"moderatorPass":"mp","viewerPass":"ap","learningDashboardAccessToken":"kax7opjvcmem"},"recordProp":{"record":false,"autoStartRecording":false,"allowStartStopRecording":true,"recordFullDurationMedia":false,"keepEvents":true},"welcomeProp":{"welcomeMsgTemplate":"
Welcome to %%CONFNAME%%!","welcomeMsg":"
Welcome to random-5935090!","modOnlyMessage":""},"voiceProp":{"telVoice":"73065","voiceConf":"73065","dialNumber":"613-555-1234","muteOnStart":false},"usersProp":{"maxUsers":0,"maxUserConcurrentAccesses":3,"webcamsOnlyForModerator":false,"userCameraCap":3,"guestPolicy":"ASK_MODERATOR","meetingLayout":"CUSTOM_LAYOUT","allowModsToUnmuteUsers":true,"allowModsToEjectCameras":true,"authenticatedGuest":false},"metadataProp":{"metadata":{"test-meta-param":"test-param"}},"lockSettingsProps":{"disableCam":false,"disableMic":false,"disablePrivateChat":false,"disablePublicChat":false,"disableNotes":false,"hideUserList":false,"lockOnJoin":true,"lockOnJoinConfigurable":false,"hideViewersCursor":false,"hideViewersAnnotation":false},"systemProps":{"html5InstanceId":1},"groups":[]}}}} -{"envelope":{"name":"PresenterAssignedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"timestamp":1696436195257},"core":{"header":{"name":"PresenterAssignedEvtMsg","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"body":{"presenterId":"w_nzuqejrku243","presenterName":"User 3426823","assignedBy":"w_nzuqejrku243"}}} -{"envelope":{"name":"UserJoinedMeetingEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"timestamp":1696436195257},"core":{"header":{"name":"UserJoinedMeetingEvtMsg","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"body":{"intId":"w_nzuqejrku243","extId":"w_nzuqejrku243","name":"User 3426823","role":"MODERATOR","guest":false,"authed":true,"guestStatus":"ALLOW","emoji":"none","reactionEmoji":"none","raiseHand":false,"away":false,"pin":false,"presenter":false,"locked":true,"avatar":"","color":"#7b1fa2","clientType":"HTML5"}}} -{"envelope":{"name":"PresenterAssignedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"timestamp":1696436195282},"core":{"header":{"name":"PresenterAssignedEvtMsg","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"body":{"presenterId":"w_nzuqejrku243","presenterName":"User 3426823","assignedBy":"w_nzuqejrku243"}}} -{"envelope":{"name":"PresenterAssignedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"timestamp":1696436195297},"core":{"header":{"name":"PresenterAssignedEvtMsg","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"body":{"presenterId":"w_nzuqejrku243","presenterName":"User 3426823","assignedBy":"w_nzuqejrku243"}}} -{"envelope":{"name":"UserJoinedMeetingEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_ramw3yqd7kpj"},"timestamp":1696436195862},"core":{"header":{"name":"UserJoinedMeetingEvtMsg","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_ramw3yqd7kpj"},"body":{"intId":"w_ramw3yqd7kpj","extId":"w_ramw3yqd7kpj","name":"User 3426823","role":"VIEWER","guest":false,"authed":true,"guestStatus":"ALLOW","emoji":"none","reactionEmoji":"none","raiseHand":false,"away":false,"pin":false,"presenter":false,"locked":true,"avatar":"","color":"#6a1b9a","clientType":"HTML5"}}} -{"envelope":{"name":"UserJoinedVoiceConfToClientEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"timestamp":1696436201276},"core":{"header":{"name":"UserJoinedVoiceConfToClientEvtMsg","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"body":{"voiceConf":"73065","intId":"w_nzuqejrku243","voiceUserId":"2","callerName":"User+3426823","callerNum":"w_nzuqejrku243_1-bbbID-User+3426823","color":"#4a148c","muted":false,"talking":false,"callingWith":"none","listenOnly":false}}} -{"envelope":{"name":"UserLeftVoiceConfToClientEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"timestamp":1696436205370},"core":{"header":{"name":"UserLeftVoiceConfToClientEvtMsg","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"body":{"voiceConf":"73065","intId":"w_nzuqejrku243","voiceUserId":"w_nzuqejrku243"}}} -{"envelope":{"name":"UserBroadcastCamStartedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"timestamp":1696436208363},"core":{"header":{"name":"UserBroadcastCamStartedEvtMsg","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"body":{"userId":"w_nzuqejrku243","stream":"w_nzuqejrku243_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}}} -{"envelope":{"name":"UserBroadcastCamStoppedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"timestamp":1696436209599},"core":{"header":{"name":"UserBroadcastCamStoppedEvtMsg","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"body":{"userId":"w_nzuqejrku243","stream":"w_nzuqejrku243_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}}} -{"envelope":{"name":"ScreenshareRtmpBroadcastStartedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"not-used"},"timestamp":1696436215445},"core":{"header":{"name":"ScreenshareRtmpBroadcastStartedEvtMsg","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"not-used"},"body":{"voiceConf":"73065","screenshareConf":"73065","stream":"d9f27fb6-eed2-498d-8deb-b8870b9381b9","vidWidth":0,"vidHeight":0,"timestamp":"1696436215436","hasAudio":false,"contentType":"screenshare"}}} -{"envelope":{"name":"ScreenshareRtmpBroadcastStoppedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"not-used"},"timestamp":1696436216551},"core":{"header":{"name":"ScreenshareRtmpBroadcastStoppedEvtMsg","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"not-used"},"body":{"voiceConf":"","screenshareConf":"","stream":"","vidWidth":0,"vidHeight":0,"timestamp":""}}} -{"envelope":{"name":"GroupChatMessageBroadcastEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"timestamp":1696436220782},"core":{"header":{"name":"GroupChatMessageBroadcastEvtMsg","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"body":{"chatId":"MAIN-PUBLIC-GROUP-CHAT","msg":{"id":"1696436220774-9ll3p48g","timestamp":1696436220774,"correlationId":"w_nzuqejrku243-1696436220761","sender":{"id":"w_nzuqejrku243","name":"User 3426823","role":"MODERATOR"},"chatEmphasizedText":true,"message":"Public chat test"}}}} -{"envelope":{"name":"UserReactionEmojiChangedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"timestamp":1696436225290},"core":{"header":{"name":"UserReactionEmojiChangedEvtMsg","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"body":{"userId":"w_nzuqejrku243","reactionEmoji":"🙁"}}} -{"envelope":{"name":"UserReactionEmojiChangedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"timestamp":1696436227190},"core":{"header":{"name":"UserReactionEmojiChangedEvtMsg","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"body":{"userId":"w_nzuqejrku243","reactionEmoji":"none"}}} -{"envelope":{"name":"UserRaiseHandChangedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"timestamp":1696436229151},"core":{"header":{"name":"UserRaiseHandChangedEvtMsg","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"body":{"userId":"w_nzuqejrku243","raiseHand":true}}} -{"envelope":{"name":"UserRaiseHandChangedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"timestamp":1696436231120},"core":{"header":{"name":"UserRaiseHandChangedEvtMsg","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"body":{"userId":"w_nzuqejrku243","raiseHand":false}}} -{"envelope":{"name":"PollStartedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"timestamp":1696436242115},"core":{"header":{"name":"PollStartedEvtMsg","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"body":{"userId":"w_nzuqejrku243","pollId":"2519ad945dc29da2da3132bc29f4450359635b1c-1696436185332/1/1696436242099","pollType":"A-4","secretPoll":false,"question":"ABCD Poll test (public)","poll":{"id":"2519ad945dc29da2da3132bc29f4450359635b1c-1696436185332/1/1696436242099","isMultipleResponse":false,"answers":[{"id":0,"key":"A"},{"id":1,"key":"B"},{"id":2,"key":"C"},{"id":3,"key":"D"}]}}}} -{"envelope":{"name":"UserRespondedToPollRespMsg","routing":{"msgType":"DIRECT","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"timestamp":1696436244987},"core":{"header":{"name":"UserRespondedToPollRespMsg","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_nzuqejrku243"},"body":{"pollId":"2519ad945dc29da2da3132bc29f4450359635b1c-1696436185332/1/1696436242099","userId":"w_ramw3yqd7kpj","answerIds":[0]}}} -{"envelope":{"name":"UserLeftMeetingEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_ramw3yqd7kpj"},"timestamp":1696436270350},"core":{"header":{"name":"UserLeftMeetingEvtMsg","meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331","userId":"w_ramw3yqd7kpj"},"body":{"intId":"w_ramw3yqd7kpj","eject":false,"ejectedBy":"","reason":"","reasonCode":""}}} -{"envelope":{"name":"MeetingDestroyedEvtMsg","routing":{"sender":"bbb-apps-akka"},"timestamp":1696436281160},"core":{"header":{"name":"MeetingDestroyedEvtMsg"},"body":{"meetingId":"2d482ccee818e9fe178efbd070a590412cbb5680-1696436185331"}}} +{"envelope":{"name":"MeetingCreatedEvtMsg","routing":{"sender":"bbb-apps-akka"},"timestamp":1696511528407},"core":{"header":{"name":"MeetingCreatedEvtMsg"},"body":{"props":{"meetingProp":{"name":"random-3136035","extId":"random-3136035","intId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","meetingCameraCap":50,"maxPinnedCameras":3,"isBreakout":false,"disabledFeatures":[],"notifyRecordingIsOn":false,"presentationUploadExternalDescription":"","presentationUploadExternalUrl":""},"breakoutProps":{"parentId":"bbb-none","sequence":0,"freeJoin":false,"breakoutRooms":[],"record":false,"privateChatEnabled":true,"captureNotes":false,"captureSlides":false,"captureNotesFilename":"%%CONFNAME%%","captureSlidesFilename":"%%CONFNAME%%"},"durationProps":{"duration":0,"createdTime":1696511528029,"createdDate":"Thu Oct 05 10:12:08 BRT 2023","meetingExpireIfNoUserJoinedInMinutes":5,"meetingExpireWhenLastUserLeftInMinutes":1,"userInactivityInspectTimerInMinutes":0,"userInactivityThresholdInMinutes":30,"userActivitySignResponseDelayInMinutes":5,"endWhenNoModerator":false,"endWhenNoModeratorDelayInMinutes":1},"password":{"moderatorPass":"mp","viewerPass":"ap","learningDashboardAccessToken":"qhuyxxcosvom"},"recordProp":{"record":false,"autoStartRecording":false,"allowStartStopRecording":true,"recordFullDurationMedia":false,"keepEvents":true},"welcomeProp":{"welcomeMsgTemplate":"
Welcome to %%CONFNAME%%!","welcomeMsg":"
Welcome to random-3136035!","modOnlyMessage":""},"voiceProp":{"telVoice":"79052","voiceConf":"79052","dialNumber":"613-555-1234","muteOnStart":false},"usersProp":{"maxUsers":0,"maxUserConcurrentAccesses":3,"webcamsOnlyForModerator":false,"userCameraCap":3,"guestPolicy":"ASK_MODERATOR","meetingLayout":"CUSTOM_LAYOUT","allowModsToUnmuteUsers":true,"allowModsToEjectCameras":true,"authenticatedGuest":false},"metadataProp":{"metadata":{}},"lockSettingsProps":{"disableCam":false,"disableMic":false,"disablePrivateChat":false,"disablePublicChat":false,"disableNotes":false,"hideUserList":false,"lockOnJoin":true,"lockOnJoinConfigurable":false,"hideViewersCursor":false,"hideViewersAnnotation":false},"systemProps":{"html5InstanceId":1},"groups":[]}}}} +{"envelope":{"name":"UserJoinedMeetingEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"timestamp":1696511534721},"core":{"header":{"name":"UserJoinedMeetingEvtMsg","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"body":{"intId":"w_1fewpbchnudl","extId":"w_1fewpbchnudl","name":"User 1430416","role":"MODERATOR","guest":false,"authed":true,"guestStatus":"ALLOW","emoji":"none","reactionEmoji":"none","raiseHand":false,"away":false,"pin":false,"presenter":false,"locked":true,"avatar":"","color":"#7b1fa2","clientType":"HTML5"}}} +{"envelope":{"name":"PresenterAssignedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"timestamp":1696511534736},"core":{"header":{"name":"PresenterAssignedEvtMsg","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"body":{"presenterId":"w_1fewpbchnudl","presenterName":"User 1430416","assignedBy":"w_1fewpbchnudl"}}} +{"envelope":{"name":"PresenterAssignedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"timestamp":1696511534827},"core":{"header":{"name":"PresenterAssignedEvtMsg","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"body":{"presenterId":"w_1fewpbchnudl","presenterName":"User 1430416","assignedBy":"w_1fewpbchnudl"}}} +{"envelope":{"name":"PresenterAssignedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"timestamp":1696511534875},"core":{"header":{"name":"PresenterAssignedEvtMsg","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"body":{"presenterId":"w_1fewpbchnudl","presenterName":"User 1430416","assignedBy":"w_1fewpbchnudl"}}} +{"envelope":{"name":"UserJoinedMeetingEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_lxcl6yagcfbj"},"timestamp":1696511539538},"core":{"header":{"name":"UserJoinedMeetingEvtMsg","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_lxcl6yagcfbj"},"body":{"intId":"w_lxcl6yagcfbj","extId":"w_lxcl6yagcfbj","name":"User 1430416","role":"VIEWER","guest":false,"authed":true,"guestStatus":"ALLOW","emoji":"none","reactionEmoji":"none","raiseHand":false,"away":false,"pin":false,"presenter":false,"locked":true,"avatar":"","color":"#6a1b9a","clientType":"HTML5"}}} +{"envelope":{"name":"UserJoinedVoiceConfToClientEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"timestamp":1696511543338},"core":{"header":{"name":"UserJoinedVoiceConfToClientEvtMsg","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"body":{"voiceConf":"79052","intId":"w_1fewpbchnudl","voiceUserId":"1","callerName":"User+1430416","callerNum":"w_1fewpbchnudl_1-bbbID-User+1430416","color":"#4a148c","muted":false,"talking":false,"callingWith":"none","listenOnly":false}}} +{"envelope":{"name":"UserLeftVoiceConfToClientEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"timestamp":1696511545342},"core":{"header":{"name":"UserLeftVoiceConfToClientEvtMsg","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"body":{"voiceConf":"79052","intId":"w_1fewpbchnudl","voiceUserId":"w_1fewpbchnudl"}}} +{"envelope":{"name":"UserBroadcastCamStartedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"timestamp":1696511549084},"core":{"header":{"name":"UserBroadcastCamStartedEvtMsg","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"body":{"userId":"w_1fewpbchnudl","stream":"w_1fewpbchnudl_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}}} +{"envelope":{"name":"UserBroadcastCamStoppedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"timestamp":1696511550373},"core":{"header":{"name":"UserBroadcastCamStoppedEvtMsg","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"body":{"userId":"w_1fewpbchnudl","stream":"w_1fewpbchnudl_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}}} +{"envelope":{"name":"ScreenshareRtmpBroadcastStartedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"not-used"},"timestamp":1696511553277},"core":{"header":{"name":"ScreenshareRtmpBroadcastStartedEvtMsg","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"not-used"},"body":{"voiceConf":"79052","screenshareConf":"79052","stream":"d20caa7c-3329-4c46-8f5f-a18b6c95a6fe","vidWidth":0,"vidHeight":0,"timestamp":"1696511553261","hasAudio":true,"contentType":"screenshare"}}} +{"envelope":{"name":"ScreenshareRtmpBroadcastStoppedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"not-used"},"timestamp":1696511556378},"core":{"header":{"name":"ScreenshareRtmpBroadcastStoppedEvtMsg","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"not-used"},"body":{"voiceConf":"","screenshareConf":"","stream":"","vidWidth":0,"vidHeight":0,"timestamp":""}}} +{"envelope":{"name":"GroupChatMessageBroadcastEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"timestamp":1696511561804},"core":{"header":{"name":"GroupChatMessageBroadcastEvtMsg","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"body":{"chatId":"MAIN-PUBLIC-GROUP-CHAT","msg":{"id":"1696511561799-rplr4p2r","timestamp":1696511561799,"correlationId":"w_1fewpbchnudl-1696511561780","sender":{"id":"w_1fewpbchnudl","name":"User 1430416","role":"MODERATOR"},"chatEmphasizedText":true,"message":"Public chat test"}}}} +{"envelope":{"name":"UserReactionEmojiChangedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"timestamp":1696511567688},"core":{"header":{"name":"UserReactionEmojiChangedEvtMsg","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"body":{"userId":"w_1fewpbchnudl","reactionEmoji":"🙁"}}} +{"envelope":{"name":"UserReactionEmojiChangedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"timestamp":1696511568937},"core":{"header":{"name":"UserReactionEmojiChangedEvtMsg","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"body":{"userId":"w_1fewpbchnudl","reactionEmoji":"none"}}} +{"envelope":{"name":"UserRaiseHandChangedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"timestamp":1696511569963},"core":{"header":{"name":"UserRaiseHandChangedEvtMsg","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"body":{"userId":"w_1fewpbchnudl","raiseHand":true}}} +{"envelope":{"name":"UserRaiseHandChangedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"timestamp":1696511571010},"core":{"header":{"name":"UserRaiseHandChangedEvtMsg","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"body":{"userId":"w_1fewpbchnudl","raiseHand":false}}} +{"envelope":{"name":"PollStartedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"timestamp":1696511581965},"core":{"header":{"name":"PollStartedEvtMsg","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"body":{"userId":"w_1fewpbchnudl","pollId":"820cff23c27f85f7cccaffb1c6f2fd2d9adcede7-1696511528075/1/1696511581955","pollType":"A-4","secretPoll":false,"question":"ABCD Poll test (public)","poll":{"id":"820cff23c27f85f7cccaffb1c6f2fd2d9adcede7-1696511528075/1/1696511581955","isMultipleResponse":false,"answers":[{"id":0,"key":"A"},{"id":1,"key":"B"},{"id":2,"key":"C"},{"id":3,"key":"D"}]}}}} +{"envelope":{"name":"UserRespondedToPollRespMsg","routing":{"msgType":"DIRECT","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"timestamp":1696511584252},"core":{"header":{"name":"UserRespondedToPollRespMsg","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"body":{"pollId":"820cff23c27f85f7cccaffb1c6f2fd2d9adcede7-1696511528075/1/1696511581955","userId":"w_lxcl6yagcfbj","answerIds":[0]}}} +{"envelope":{"name":"UserLeftMeetingEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_lxcl6yagcfbj"},"timestamp":1696511603366},"core":{"header":{"name":"UserLeftMeetingEvtMsg","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_lxcl6yagcfbj"},"body":{"intId":"w_lxcl6yagcfbj","eject":false,"ejectedBy":"","reason":"","reasonCode":""}}} +{"envelope":{"name":"MeetingDestroyedEvtMsg","routing":{"sender":"bbb-apps-akka"},"timestamp":1696511606884},"core":{"header":{"name":"MeetingDestroyedEvtMsg"},"body":{"meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029"}}} diff --git a/src/process/event.js b/src/process/event.js index 2230abe..c4d6a60 100644 --- a/src/process/event.js +++ b/src/process/event.js @@ -446,6 +446,7 @@ export default class WebhooksEvent { header, } = messageObj.core; const extId = UserMapping.get().getExternalUserID(header.userId) || body.extId || ""; + const pollId = body.pollId || body.poll?.id; this.outputEvent = { data: { @@ -461,7 +462,7 @@ export default class WebhooksEvent { "external-user-id": extId, }, poll: { - "id": body.pollId + "id": pollId, } }, event: { @@ -472,6 +473,7 @@ export default class WebhooksEvent { if (this.outputEvent.data.id === "poll-started") { this.outputEvent.data.attributes.poll = { + ...this.outputEvent.data.attributes.poll, question: body.question, answers: body.poll.answers, }; From 755f445b76ff4df9819579cced0129c382c392ad Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Thu, 5 Oct 2023 15:52:07 -0300 Subject: [PATCH 040/154] fix: deep clone input payload in StorageItem Avoids mutating the original argument --- src/db/redis/base-storage.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/db/redis/base-storage.js b/src/db/redis/base-storage.js index b66ffd5..439228c 100644 --- a/src/db/redis/base-storage.js +++ b/src/db/redis/base-storage.js @@ -1,5 +1,6 @@ import { newLogger } from '../../common/logger.js'; import { v4 as uuidv4 } from 'uuid'; +import config from 'config'; const stringifyValues = (o) => { Object.keys(o).forEach(k => { @@ -32,7 +33,7 @@ class StorageItem { this.setId = typeof setId !== 'string' ? setId.toString() : setId; this.id = id; this.alias = alias; - this.payload = payload; + this.payload = config.util.cloneDeep(payload); if (typeof serializer === 'function') this.serialize = serializer.bind(this); if (typeof deserializer === 'function') this.deserialize = deserializer.bind(this); this.appOptions = appOptions; @@ -130,7 +131,6 @@ class StorageCompartmentKV { return data; } - async save(payload, { id = uuidv4(), alias, From b2a38f0dc506ea29158219101f6e2873670c86f5 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Thu, 5 Oct 2023 16:00:07 -0300 Subject: [PATCH 041/154] chore: guarantee that the stringification util also does not mutate input --- src/db/redis/base-storage.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/db/redis/base-storage.js b/src/db/redis/base-storage.js index 439228c..0a28e4b 100644 --- a/src/db/redis/base-storage.js +++ b/src/db/redis/base-storage.js @@ -2,19 +2,21 @@ import { newLogger } from '../../common/logger.js'; import { v4 as uuidv4 } from 'uuid'; import config from 'config'; -const stringifyValues = (o) => { - Object.keys(o).forEach(k => { +const stringifyValues = (obj) => { + // Deep clone the object so we don't modify the original. + const cObj = config.util.cloneDeep(obj); + Object.keys(cObj).forEach(k => { // Make all values strings, but ignore nullish/undefined values. - if (o[k] == null) { - delete o[k]; - } else if (typeof o[k] === 'object') { - o[k] = JSON.stringify(stringifyValues(o[k])); + if (cObj[k] == null) { + delete cObj[k]; + } else if (typeof cObj[k] === 'object') { + cObj[k] = JSON.stringify(stringifyValues(cObj[k])); } else { - o[k] = '' + o[k]; + cObj[k] = '' + cObj[k]; } }); - return o; + return cObj; } class StorageItem { From ddd4fa06e3611841bdfbfc3153119982856005c9 Mon Sep 17 00:00:00 2001 From: miguel-mconf Date: Thu, 5 Oct 2023 18:35:52 -0300 Subject: [PATCH 042/154] All xAPI statements are now implemented --- src/out/xapi/compartment.js | 86 +++++++++++- src/out/xapi/index.js | 18 ++- src/out/xapi/templates.js | 272 ++++++++++++++++++++++++------------ src/out/xapi/xapi.js | 131 ++++++++++++----- 4 files changed, 374 insertions(+), 133 deletions(-) diff --git a/src/out/xapi/compartment.js b/src/out/xapi/compartment.js index 6adbcea..8cc5c72 100644 --- a/src/out/xapi/compartment.js +++ b/src/out/xapi/compartment.js @@ -1,6 +1,6 @@ import { StorageCompartmentKV } from '../../db/redis/base-storage.js'; -export default class XAPICompartment extends StorageCompartmentKV { +export class meetingCompartment extends StorageCompartmentKV { constructor(client, prefix, setId, options = {}) { super(client, prefix, setId, options); } @@ -21,7 +21,23 @@ export default class XAPICompartment extends StorageCompartmentKV { const mapping = await this.save(payload, { alias: internal_meeting_id, }); - this.logger.info(`added user mapping to the list ${internal_meeting_id}: ${mapping.print()}`); + this.logger.info(`added meeting data to the list ${internal_meeting_id}: ${mapping.print()}`); + + return mapping; + } + + async addOrUpdateUserData(user_data) { + const {internal_user_id, user_name} = user_data; + + const payload = { + internal_user_id, + user_name, + }; + + const mapping = await this.save(payload, { + alias: internal_user_id, + }); + this.logger.info(`added user data to the list ${internal_user_id}: ${mapping.print()}`); return mapping; } @@ -33,7 +49,71 @@ export default class XAPICompartment extends StorageCompartmentKV { // Initializes global methods for this model. initialize() { - // return this.resync(); + return; + } +} + +export class userCompartment extends StorageCompartmentKV { + constructor(client, prefix, setId, options = {}) { + super(client, prefix, setId, options); + } + + async addOrUpdateUserData(user_data) { + const {internal_user_id, user_name} = user_data; + + const payload = { + internal_user_id, + user_name, + }; + + const mapping = await this.save(payload, { + alias: internal_user_id, + }); + this.logger.info(`added poll data to the list ${internal_user_id}: ${mapping.print()}`); + + return mapping; + } + + async getUserData(internal_user_id) { + const user_data = this.findByField('internal_user_id', internal_user_id); + return (user_data != null ? user_data.payload : undefined); + } + + // Initializes global methods for this model. + initialize() { + return; + } +} + +export class pollCompartment extends StorageCompartmentKV { + constructor(client, prefix, setId, options = {}) { + super(client, prefix, setId, options); + } + + async addOrUpdatePollData(poll_data) { + const {object_id, question, choices} = poll_data; + + const payload = { + object_id, + question, + choices, + }; + + const mapping = await this.save(payload, { + alias: object_id, + }); + this.logger.info(`added user data to the list ${object_id}: ${mapping.print()}`); + + return mapping; + } + + async getPollData(object_id) { + const poll_data = this.findByField('object_id', object_id); + return (poll_data != null ? poll_data.payload : undefined); + } + + // Initializes global methods for this model. + initialize() { return; } } \ No newline at end of file diff --git a/src/out/xapi/index.js b/src/out/xapi/index.js index dd0031c..058459e 100644 --- a/src/out/xapi/index.js +++ b/src/out/xapi/index.js @@ -1,5 +1,5 @@ import XAPI from './xapi.js'; -import XAPICompartment from './compartment.js'; +import {meetingCompartment, userCompartment, pollCompartment} from './compartment.js'; import redis from 'redis'; import config from 'config'; /* @@ -47,13 +47,25 @@ export default class OutXAPI { this.logger.debug('OutXAPI.onEvent:', this.config ); - this.meetingStorage = new XAPICompartment( + this.meetingStorage = new meetingCompartment( this.redisClient, this.config.redis.keys.meetingPrefix, this.config.redis.keys.meetings ); - this.xAPI = new XAPI(this.context, this.config, this.meetingStorage); + this.userStorage = new userCompartment( + this.redisClient, + this.config.redis.keys.userPrefix, + this.config.redis.keys.users + ); + + this.pollStorage = new pollCompartment( + this.redisClient, + this.config.redis.keys.pollPrefix, + this.config.redis.keys.polls + ); + + this.xAPI = new XAPI(this.context, this.config, this.meetingStorage, this.userStorage, this.pollStorage); } this.loaded = true; } diff --git a/src/out/xapi/templates.js b/src/out/xapi/templates.js index b1fb3f1..6ff4d61 100644 --- a/src/out/xapi/templates.js +++ b/src/out/xapi/templates.js @@ -1,100 +1,188 @@ import { DateTime, Duration } from 'luxon'; -export default function getXAPIStatement(event, meeting_data){ - const { bbb_origin_server_name, - object_id, - meeting_name, - context_registration, - session_id, - planned_duration, - create_time} = meeting_data; - - const planned_duration_ISO = Duration.fromObject({ minutes: planned_duration }).toISO(); - const create_time_ISO = DateTime.fromMillis(create_time).toUTC().toISO(); - - const event_ts = event.data.event.ts; - - if(event.data.id == 'meeting-created'){ - return { - "actor": { - "account": { - "name": "", - "homePage": `https://${bbb_origin_server_name}` - } - }, - "verb": { - "id": "http://adlnet.gov/expapi/verbs/initialized" - }, - "object": { - "id": `https://${bbb_origin_server_name}/xapi/activities/${object_id}`, - "definition": { - "type": "https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom", - "name": { - "en": meeting_name - } - } - }, - "context": { - "registration": context_registration, - "contextActivities": { - "category": [ - { - "id": "https://w3id.org/xapi/virtual-classroom", - "definition": { - "type": "http://adlnet.gov/expapi/activities/profile" - } - } - ] - }, - "extensions": { - "http://id.tincanapi.com/extension/planned-duration": planned_duration_ISO, - "https://w3id.org/xapi/cmi5/context/extensions/sessionid": session_id - } - }, - "timestamp": create_time_ISO - } +export default function getXAPIStatement(event, meeting_data, user_data = null, poll_data = null) { + const { bbb_origin_server_name, + object_id, + meeting_name, + context_registration, + session_id, + planned_duration, + create_time } = meeting_data; + + const planned_duration_ISO = Duration.fromObject({ minutes: planned_duration }).toISO(); + const create_time_ISO = DateTime.fromMillis(create_time).toUTC().toISO(); + + const event_ts = event.data.event.ts; + + if ( event.data.id == 'meeting-created' + || event.data.id == 'meeting-ended' + || event.data.id == 'user-joined' + || event.data.id == 'user-left' + || event.data.id == 'user-audio-voice-enabled' + || event.data.id == 'user-audio-voice-disabled' + || event.data.id == 'user-cam-broadcast-start' + || event.data.id == 'user-cam-broadcast-end' + || event.data.id == 'meeting-screenshare-started' + || event.data.id == 'meeting-screenshare-stopped' + || event.data.id == 'chat-group-message-sent' + || event.data.id == 'poll-started' + || event.data.id == 'poll-responded') { + const verbMappings = { + 'meeting-created': 'http://adlnet.gov/expapi/verbs/initialized', + 'meeting-ended': 'http://adlnet.gov/expapi/verbs/terminated', + 'user-joined': 'http://activitystrea.ms/join', + 'user-left': 'http://activitystrea.ms/leave', + 'user-audio-voice-enabled': 'http://adlnet.gov/expapi/verbs/interacted', + 'user-audio-voice-disabled': 'http://adlnet.gov/expapi/verbs/interacted', + 'user-cam-broadcast-start': 'http://adlnet.gov/expapi/verbs/interacted', + 'user-cam-broadcast-end': 'http://adlnet.gov/expapi/verbs/interacted', + 'meeting-screenshare-started': 'http://adlnet.gov/expapi/verbs/interacted', + 'meeting-screenshare-stopped': 'http://adlnet.gov/expapi/verbs/interacted', + 'chat-group-message-sent': 'https://w3id.org/xapi/acrossx/verbs/posted', + 'poll-started': 'http://adlnet.gov/expapi/verbs/asked', + 'poll-responded': 'http://adlnet.gov/expapi/verbs/answered', } - else if(event.data.id == 'meeting-ended'){ - return { - "actor": { - "account": { - "name": "", - "homePage": `https://${bbb_origin_server_name}` - } - }, - "verb": { - "id": "http://adlnet.gov/expapi/verbs/terminated" - }, - "object": { - "id": `https://${bbb_origin_server_name}/xapi/activities/${object_id}`, + + // TODO check for data integrity + const statement = { + "actor": { + "account": { + "name": user_data?.user_name || "", + "homePage": `https://${bbb_origin_server_name}` + } + }, + "verb": { + "id": verbMappings.hasOwnProperty(event.data.id) ? verbMappings[event.data.id] : null + }, + "object": { + "id": `https://${bbb_origin_server_name}/xapi/activities/${object_id}`, + "definition": { + "type": "https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom", + "name": { + "en": meeting_name + } + }, + }, + "context": { + "registration": context_registration, + "contextActivities": { + "category": [ + { + "id": "https://w3id.org/xapi/virtual-classroom", "definition": { - "type": "https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom", - "name": { - "en": meeting_name - } + "type": "http://adlnet.gov/expapi/activities/profile" } - }, - "result": { - "duration": Duration.fromMillis(event_ts - create_time).toISO() - }, - "context": { - "registration": context_registration, - "contextActivities": { - "category": [ - { - "id": "https://w3id.org/xapi/virtual-classroom", - "definition": { - "type": "http://adlnet.gov/expapi/activities/profile" - } - } - ] - }, - "extensions": { - "http://id.tincanapi.com/extension/planned-duration": planned_duration_ISO, - "https://w3id.org/xapi/cmi5/context/extensions/sessionid": session_id - } - }, - "timestamp": DateTime.fromMillis(event_ts).toUTC().toISO() + } + ] + }, + "extensions": { + "https://w3id.org/xapi/cmi5/context/extensions/sessionid": session_id } + }, + "timestamp": DateTime.fromMillis(event_ts).toUTC().toISO() } + + // Custom 'meeting-created' attributes + if (event.data.id == 'meeting-created'){ + statement.context.extensions["http://id.tincanapi.com/extension/planned-duration"] = planned_duration_ISO + statement.timestamp = create_time_ISO; + } + + // Custom 'meeting-ended' attributes + else if(event.data.id == 'meeting-ended'){ + statement.context.extensions["http://id.tincanapi.com/extension/planned-duration"] = planned_duration_ISO + statement.result = { + "duration": Duration.fromMillis(event_ts - create_time).toISO() + } + } + + // Custom attributes for multiple interactions + else if (event.data.id == 'user-audio-voice-enabled' + || event.data.id == 'user-audio-voice-disabled' + || event.data.id == 'user-cam-broadcast-start' + || event.data.id == 'user-cam-broadcast-end' + || event.data.id == 'meeting-screenshare-started' + || event.data.id == 'meeting-screenshare-stopped') { + + const extension = { + "user-audio-voice-enabled": "micro-activated", + "user-audio-voice-disabled": "micro-activated", + "user-cam-broadcast-start": "camera-activated", + "user-cam-broadcast-end": "camera-activated", + "meeting-screenshare-started": "screen-shared", + "meeting-screenshare-stopped": "screen-shared", + }[event.data.id] + + const extension_uri = `https://w3id.org/xapi/virtual-classroom/extensions/${extension}`; + + const extension_enabled = { + "user-audio-voice-enabled": "true", + "user-audio-voice-disabled": "false", + "user-cam-broadcast-start": "true", + "user-cam-broadcast-end": "false", + "meeting-screenshare-started": "true", + "meeting-screenshare-stopped": "false", + }[event.data.id] + + statement.context.extensions[extension_uri] = extension_enabled; + } + + // Custom 'user-raise-hand-changed' attributes + else if (event.data.id == 'user-raise-hand-changed'){ + const extension_uri = 'https://w3id.org/xapi/virtual-classroom/extensions/hand-raised'; + const extension_enabled = event.data.attributes.user["raise-hand"]; + statement.context.extensions[extension_uri] = extension_enabled; + } + + // Custom 'chat-group-message-sent' attributes + else if(event.data.id == 'chat-group-message-sent'){ + statement.object = { + "id": `https://${bbb_origin_server_name}/xapi/activities/${user_data?.msg_object_id}`, + "definition": { + "type": "https://w3id.org/xapi/acrossx/activities/message" + } + } + + statement.context.contextActivities.parent = [ + { + "id": `https://${bbb_origin_server_name}/xapi/activities/${object_id}`, + "definition": { + "type": "https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom" + } + } + ] + statement.timestamp = user_data?.time; + } + + // Custom 'poll-started' and 'poll-responded' attributes + else if(event.data.id == 'poll-started' || event.data.id == 'poll-responded'){ + statement.object = { + "id": `https://${bbb_origin_server_name}/xapi/activities/${poll_data?.object_id}`, + "definition":{ + "description": { + "en": poll_data?.question, + }, + "type": "http://adlnet.gov/expapi/activities/cmi.interaction", + "interactionType": "choice", + "choices": poll_data?.choices, + } + } + + statement.context.contextActivities.parent = [ + { + "id": `https://${bbb_origin_server_name}/xapi/activities/${object_id}`, + "definition": { + "type": "https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom" + } + } + ] + if(event.data.id == 'poll-responded'){ + statement.result = { + "response": event.data.attributes.poll.answerIds.join(','), + } + } + } + + return statement + } } \ No newline at end of file diff --git a/src/out/xapi/xapi.js b/src/out/xapi/xapi.js index 7569304..261ac1f 100644 --- a/src/out/xapi/xapi.js +++ b/src/out/xapi/xapi.js @@ -6,79 +6,140 @@ import fetch from 'node-fetch'; // XAPI will listen for events on redis coming from BigBlueButton, // generate xAPI statements and send to a LRS export default class XAPI { - constructor(context, config, meetingStorage) { - this.context = context; - this.logger = context.getLogger(); - this.config = config; - this.meetingStorage = meetingStorage; + constructor(context, config, meetingStorage, userStorage, pollStorage) { + this.context = context; + this.logger = context.getLogger(); + this.config = config; + this.meetingStorage = meetingStorage; + this.userStorage = userStorage; + this.pollStorage = pollStorage; } - async postToLRS(statement){ - const {url, username, password} = this.config.xapi.lrs; + async postToLRS(statement) { + const { lrs_endpoint, lrs_username, lrs_password } = this.config.lrs; const headers = { - "Authorization": `Basic ${Buffer.from(username + ":" + password).toString('base64')}`, - "Content-Type": "application/json", - "X-Experience-API-Version": "1.0.0", + 'Authorization': `Basic ${Buffer.from(lrs_username + ':' + lrs_password).toString('base64')}`, + 'Content-Type': 'application/json', + 'X-Experience-API-Version': '1.0.0', } const requestOptions = { - method: "POST", + method: 'POST', body: JSON.stringify(statement), headers, }; - - const xAPIEndpoint = new URL("xAPI/statements", url); + + const xAPIEndpoint = new URL('xAPI/statements', lrs_endpoint); try { const response = await fetch(xAPIEndpoint, requestOptions); - const { status } = response; + const { status } = response; const data = await response.json(); - this.logger.debug('OutXAPI.res.status:', {status, data} ); + this.logger.debug('OutXAPI.res.status:', { status, data }); } catch (err) { - // handle error - this.logger.debug('OutXAPI.err:', err ); + this.logger.debug('OutXAPI.err:', err); } } async onEvent(event, raw) { + // TODO: return promise earlier to avoid holding the queue const meeting_data = { - internal_meeting_id: event.data.attributes.meeting["internal-meeting-id"], - external_meeting_id: event.data.attributes.meeting["external-meeting-id"], - uuid_namespace: this.config.xapi.uuid_namespace + internal_meeting_id: event.data.attributes.meeting['internal-meeting-id'], + external_meeting_id: event.data.attributes.meeting['external-meeting-id'], } - meeting_data.session_id = uuidv5(meeting_data.internal_meeting_id, meeting_data.uuid_namespace); - meeting_data.object_id = uuidv5(meeting_data.external_meeting_id, meeting_data.uuid_namespace); + const uuid_namespace = this.config.uuid_namespace; + + meeting_data.session_id = uuidv5(meeting_data.internal_meeting_id, uuid_namespace); + meeting_data.object_id = uuidv5(meeting_data.external_meeting_id, uuid_namespace); + + let XAPIStatement; - // if meeting-created event, set parameters + // if meeting-created event, set meeting_data on redis if (event.data.id == 'meeting-created') { - meeting_data.bbb_origin_server_name = event.data.attributes.meeting.metadata["bbb-origin-server-name"]; + meeting_data.bbb_origin_server_name = event.data.attributes.meeting.metadata['bbb-origin-server-name']; meeting_data.planned_duration = event.data.attributes.meeting.duration; - meeting_data.create_time = event.data.attributes.meeting["create-time"]; + meeting_data.create_time = event.data.attributes.meeting['create-time']; meeting_data.meeting_name = event.data.attributes.meeting.name; const meeting_create_day = DateTime.fromMillis(meeting_data.create_time).toFormat('yyyyMMdd'); const external_key = `${meeting_data.external_meeting_id}_${meeting_create_day}`; - meeting_data.context_registration = uuidv5(external_key, meeting_data.uuid_namespace); + meeting_data.context_registration = uuidv5(external_key, uuid_namespace); await this.meetingStorage.addOrUpdateMeetingData(meeting_data); - const XAPIStatement = getXAPIStatement(event, meeting_data); - // const meeting_data_storage = await this.meetingStorage.getMeetingData(meeting_data.internal_meeting_id); - await this.postToLRS(XAPIStatement); + XAPIStatement = getXAPIStatement(event, meeting_data); } - // for other events, read parameters from redis + // if not meeting-created event, read meeting_data from redis else { - // this.logger.debug('OutXAPI.meeting_ended_data:', {int_meet_id: meeting_data.internal_meeting_id} ); const meeting_data_storage = await this.meetingStorage.getMeetingData(meeting_data.internal_meeting_id); Object.assign(meeting_data, meeting_data_storage); - if (event.data.id == 'meeting-ended'){ - const XAPIStatement = getXAPIStatement(event, meeting_data); - await this.postToLRS(XAPIStatement); + if (event.data.id == 'meeting-ended') { + XAPIStatement = getXAPIStatement(event, meeting_data); } - } + // if user-joined event, set user_data on redis + else if (event.data.id == 'user-joined') { + const user_data = { + internal_user_id: event.data.attributes.user['internal-user-id'], + user_name: event.data.attributes.user.name, + } + await this.userStorage.addOrUpdateUserData(user_data); + XAPIStatement = getXAPIStatement(event, meeting_data, user_data); + } + // if not user-joined user event, read user_data on redis + else if ( + event.data.id == 'user-left' + || event.data.id == 'user-audio-voice-enabled' + || event.data.id == 'user-audio-voice-disabled' + || event.data.id == 'user-cam-broadcast-start' + || event.data.id == 'user-cam-broadcast-end' + || event.data.id == 'meeting-screenshare-started' + || event.data.id == 'meeting-screenshare-stopped' + || event.data.id == 'user-raise-hand-changed') { + const internal_user_id = event.data.attributes.user?.['internal-user-id']; + const user_data = internal_user_id ? await this.userStorage.getUserData(internal_user_id) : null; + XAPIStatement = getXAPIStatement(event, meeting_data, user_data); + } + else if (event.data.id == 'chat-group-message-sent') { + const user_data = event.data.attributes['chat-message']?.sender; + const msg_key = `${user_data?.internal_user_id}_${user_data?.time}`; + user_data.msg_object_id = uuidv5(msg_key, uuid_namespace); + XAPIStatement = getXAPIStatement(event, meeting_data, user_data); + } + else if (event.data.id == 'poll-started' || event.data.id == 'poll-responded') { + const internal_user_id = event.data.attributes.user?.['internal-user-id']; + const user_data = internal_user_id ? await this.userStorage.getUserData(internal_user_id) : null; + const object_id = uuidv5(event.data.attributes.poll.id, uuid_namespace); + let poll_data; + + if (event.data.id == 'poll-started') { + var choices = (event.data.attributes.poll.answers.map(a => { + return { id: a.id.toString(), "description": { "en": a.key } } + })); + poll_data = { + object_id, + question: event.data.attributes.poll.question, + choices, + } + await this.pollStorage.addOrUpdatePollData(poll_data); + } + else if (event.data.id == 'poll-responded') { + poll_data = object_id ? await this.pollStorage.getPollData(object_id) : null; + poll_data.choices = poll_data.choices.map(item => { + const parsedItem = JSON.parse(item); + const description = JSON.parse(parsedItem.description); + return { + id: JSON.parse(item).id, + description: { en: description.en } + }; + }); + } + XAPIStatement = getXAPIStatement(event, meeting_data, user_data, poll_data); + } + } + await this.postToLRS(XAPIStatement); } } \ No newline at end of file From 98937cdbbdc03cbeccfc194c43e71ab94e8764b7 Mon Sep 17 00:00:00 2001 From: miguel-mconf Date: Thu, 5 Oct 2023 19:15:17 -0300 Subject: [PATCH 043/154] fixed server domain --- src/out/xapi/xapi.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/out/xapi/xapi.js b/src/out/xapi/xapi.js index 261ac1f..18750b7 100644 --- a/src/out/xapi/xapi.js +++ b/src/out/xapi/xapi.js @@ -58,7 +58,8 @@ export default class XAPI { // if meeting-created event, set meeting_data on redis if (event.data.id == 'meeting-created') { - meeting_data.bbb_origin_server_name = event.data.attributes.meeting.metadata['bbb-origin-server-name']; + const serverDomain = this.config.server.domain; + meeting_data.bbb_origin_server_name = serverDomain; meeting_data.planned_duration = event.data.attributes.meeting.duration; meeting_data.create_time = event.data.attributes.meeting['create-time']; meeting_data.meeting_name = event.data.attributes.meeting.name; From 582d1449dc49799231da3e5e522b044212b9cdfc Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Fri, 6 Oct 2023 13:57:26 -0300 Subject: [PATCH 044/154] fix(webhooks): clear callback emitters on failure Also added some more JsDoc annotations to the webhooks module --- src/out/webhooks/callback-emitter.js | 122 +++++++++++++++++---------- src/out/webhooks/index.js | 54 +++++++++++- src/out/webhooks/web-hooks.js | 83 ++++++++++++++---- 3 files changed, 196 insertions(+), 63 deletions(-) diff --git a/src/out/webhooks/callback-emitter.js b/src/out/webhooks/callback-emitter.js index ed9fb7f..1419a94 100644 --- a/src/out/webhooks/callback-emitter.js +++ b/src/out/webhooks/callback-emitter.js @@ -1,20 +1,16 @@ import url from 'url'; import { EventEmitter } from 'node:events'; -import { newLogger } from '../../common/logger.js'; import Utils from '../../common/utils.js'; import fetch from 'node-fetch'; -const Logger = newLogger('callback-emitter'); - // A simple string that identifies the event -const simplifiedEvent = (event) => { - if (event.event != null) { - event = event.event - } +const simplifiedEvent = (_event) => { + let event = _event.event ? _event.event : _event; + try { - const eventJs = JSON.parse(event); - return `event: { name: ${(eventJs.data != null ? eventJs.data.id : undefined)}, timestamp: ${(eventJs.data.event != null ? eventJs.data.event.ts : undefined)} }`; - } catch (e) { + const parsedEvent = JSON.parse(event); + return `event: { name: ${parsedEvent?.data?.id}, timestamp: ${(parsedEvent?.data?.event?.ts)} }`; + } catch (error) { return `event: ${event}`; } }; @@ -25,7 +21,28 @@ const simplifiedEvent = (event) => { // Emits "success" on success, "failure" on error and "stopped" when gave up trying // to perform the callback. export default class CallbackEmitter extends EventEmitter { - constructor(callbackURL, event, permanent, domain, options = {}) { + static EVENTS = { + // The callback was successfully emitted + SUCCESS: "success", + // The callback could not be emitted + FAILURE: "failure", + // The callback could not be emitted and we gave up trying + STOPPED: "stopped", + }; + + constructor( + callbackURL, + event, + permanent, + domain, { + secret, + auth2_0, + requestTimeout, + retryIntervals, + permanentIntervalReset, + logger = console, + } = {}, + ) { super(); this.callbackURL = callbackURL; this.event = event; @@ -33,7 +50,7 @@ export default class CallbackEmitter extends EventEmitter { this.nextInterval = 0; this.timestamp = 0; this.permanent = permanent; - this._serverDomain = domain; + this.logger = logger; if (callbackURL == null || event == null @@ -42,12 +59,14 @@ export default class CallbackEmitter extends EventEmitter { throw new Error("missing parameters"); } - this._permanentIntervalReset = options.permanentIntervalReset || 8; - this._secret = options.secret; - this._bearerAuth = options.auth2_0; + this._dispatched = false; + this._permanentIntervalReset = permanentIntervalReset || 8; + this._serverDomain = domain; + this._secret = secret; + this._bearerAuth = auth2_0; if (this._bearerAuth && this._secret == null) throw new Error("missing secret"); - this._requestTimeout = options.requestTimeout; - this._retryIntervals = options.retryIntervals || [ + this._requestTimeout = requestTimeout; + this._retryIntervals = retryIntervals || [ 1000, 2000, 5000, @@ -56,24 +75,20 @@ export default class CallbackEmitter extends EventEmitter { ]; } - start() { - this.timestamp = new Date().getTime(); - this.nextInterval = 0; - this._scheduleNext(0); - } - _scheduleNext(timeout) { - setTimeout(async () => { + this._clearDispatcher(); + this._dispatcher = setTimeout(async () => { try { - await this._emitMessage(); - this.emit("success"); + await this._dispatch(); + this._dispatched = true; + this.emit(CallbackEmitter.EVENTS.SUCCESS); } catch (error) { - this.emit("failure", error); + this.emit(CallbackEmitter.EVENTS.FAILURE, error); // get the next interval we have to wait and schedule a new try const interval = this._retryIntervals[this.nextInterval]; if (interval != null) { - Logger.warn(`trying the callback again in ${interval/1000.0} secs: ${this.callbackURL}`); + this.logger.warn(`trying the callback again in ${interval/1000.0} secs: ${this.callbackURL}`); this.nextInterval++; this._scheduleNext(interval); // no intervals anymore, time to give up @@ -83,28 +98,27 @@ export default class CallbackEmitter extends EventEmitter { if (this.permanent){ this._scheduleNext(this.nextInterval); } else { - this.emit("stopped"); + this.emit(CallbackEmitter.EVENTS.STOPPED); } } } }, timeout); } - async _emitMessage() { - let data, requestOptions, callbackURL; + async _dispatch() { + let callbackURL; const serverDomain = this._serverDomain; const sharedSecret = this._secret; const bearerAuth = this._bearerAuth; const timeout = this._requestTimeout; - // data to be sent // note: keep keys in alphabetical order - data = new URLSearchParams({ + const data = new URLSearchParams({ + domain: serverDomain, event: "[" + this.message + "]", timestamp: this.timestamp, - domain: serverDomain }); - requestOptions = { + const requestOptions = { method: "POST", body: data, redirect: 'follow', @@ -128,36 +142,54 @@ export default class CallbackEmitter extends EventEmitter { callbackURL += Utils.isEmpty(urlObj.search) ? "?" : "&"; callbackURL += `checksum=${checksum}`; } catch (error) { - Logger.error(`error parsing callback URL: ${this.callbackURL}`); + this.logger.error(`error parsing callback URL: ${this.callbackURL}`); throw error; } } - const responseFailed = (response) => { - // consider 401 as success, because the callback worked but was denied by the recipient - return !(response.ok || response.status == 401) - }; - + // consider 401 as success, because the callback worked but was denied by the recipient + const responseFailed = (response) => !(response.ok || response.status == 401); const controller = new AbortController(); const abortTimeout = setTimeout(() => { controller.abort(); }, timeout); requestOptions.signal = controller.signal; + const stringifiedEvent = simplifiedEvent(data); try { const response = await fetch(callbackURL, requestOptions); if (responseFailed(response)) { - Logger.warn(`error in the callback call to: [${callbackURL}] for ${simplifiedEvent(data)} status: ${response != null ? response.status: undefined}`); + this.logger.warn(`error in the callback call to: [${callbackURL}] for ${stringifiedEvent} status: ${response != null ? response.status: undefined}`); throw new Error(response.statusText); - } else { - Logger.info(`successful callback call to: [${callbackURL}] for ${simplifiedEvent(data)}`); } + + this.logger.info(`successful callback call to: [${callbackURL}] for ${stringifiedEvent}`); } catch (error) { - Logger.warn(`error in the callback call to: [${callbackURL}] for ${simplifiedEvent(data)}`, error); + this.logger.warn(`error in the callback call to: [${callbackURL}] for ${stringifiedEvent}`, error); throw error; } finally { clearTimeout(abortTimeout); } } + + _clearDispatcher() { + if (this._dispatcher != null) { + clearTimeout(this._dispatcher); + this._dispatcher = null; + } + } + + start() { + this.timestamp = new Date().getTime(); + this.nextInterval = 0; + this._scheduleNext(0); + } + + stop() { + this._clearDispatcher(); + if (!this._dispatched) this.emit(CallbackEmitter.EVENTS.STOPPED); + + this.removeAllListeners(); + } } diff --git a/src/out/webhooks/index.js b/src/out/webhooks/index.js index 0fdab25..9a0a4cd 100644 --- a/src/out/webhooks/index.js +++ b/src/out/webhooks/index.js @@ -11,13 +11,36 @@ import HookCompartment from '../../db/redis/hooks.js'; * }, */ -export default class OutWebHooks { +/** + * OutWebHooks - Entrypoint for the Webhooks output module. + * @class + * @classdesc Entrypoint for the Webhooks output module - relays incoming + * events to the internal Webhooks dispatcher, exposes + * the module API implementation for the main application to use + * as well as manages the Webhooks API server. + * @property {Context} context - This module's context as provided by the main application. + * @property {BbbWebhooksLogger} logger - The logger. + * @property {object} config - This module's configuration object. + * @property {API} api - The Webhooks API server. + * @property {WebHooks} webHooks - The Webhooks dispatcher. + * @property {boolean} loaded - Whether the module is loaded or not. + */ +class OutWebHooks { + /** + * @type {string} - The module's API implementation. + * @static + */ static type = "out"; static _defaultCollector () { throw new Error('Collector not set'); } + /** + * constructor. + * @param {Context} context - The main application's context. + * @param {object} config - The module's configuration object. + */ constructor (context, config = {}) { this.type = OutWebHooks.type; this.config = config; @@ -31,14 +54,25 @@ export default class OutWebHooks { this.loaded = false; } + /** + * load - Loads the out-webhooks module by starting the API server, + * creating permanent hooks and resyncing the hooks DB. + * @async + * @returns {Promise} + */ async load () { - await this.webHooks.start(), await this.api.start(this.config.api.port, this.config.api.bind); await this.api.createPermanents(); this.loaded = true; } + /** + * unload - Unloads the out-webhooks module by stopping the API server, + * and re-setting the collector to the default one. + * @async + * @returns {Promise} + */ async unload () { if (this.webHooks) { this.webHooks = null; @@ -48,6 +82,12 @@ export default class OutWebHooks { this.loaded = false; } + /** + * setContext - Sets the applicatino context for this module. + * and assigns a logger instance to it. + * @param {Context} context - The main application's context. + * @returns {Context} The assigned context. + */ setContext (context) { this.context = context; this.logger = context.getLogger(); @@ -55,6 +95,14 @@ export default class OutWebHooks { return context; } + /** + * onEvent - Relays an incoming event to the Webhooks dispatcher. + * @see {@link WebHooks#onEvent} + * @async + * @param {object} event - The mapped event object. + * @param {object } raw - The raw event object. + * @returns {Promise} + */ async onEvent (event, raw) { if (!this.loaded || !this.webHooks) { throw new Error("OutWebHooks not loaded"); @@ -63,3 +111,5 @@ export default class OutWebHooks { return this.webHooks.onEvent(event, raw); } } + +export default OutWebHooks; diff --git a/src/out/webhooks/web-hooks.js b/src/out/webhooks/web-hooks.js index 8ac4c0f..6d2c7a3 100644 --- a/src/out/webhooks/web-hooks.js +++ b/src/out/webhooks/web-hooks.js @@ -1,19 +1,32 @@ import CallbackEmitter from './callback-emitter.js'; import HookCompartment from '../../db/redis/hooks.js'; -// Web hooks will listen for events on redis coming from BigBlueButton and -// perform HTTP calls with them to all registered hooks. -export default class WebHooks { +/** + * WebHooks. + * @class + * @classdesc Relays incoming events to registered webhook URLs. + * @property {Context} context - This module's context as provided by the main application. + * @property {BbbWebhooksLogger} logger - The logger. + * @property {object} config - This module's configuration object. + */ +class WebHooks { + /** + * constructor. + * @param {Context} context - This module's context as provided by the main application. + * @param {object} config - This module's configuration object. + */ constructor(context, config) { this.context = context; this.logger = context.getLogger(); this.config = config; } - start() { - return Promise.resolve(); - } - + /** + * _processRaw - Dispatch raw events to hooks that expect raw data. + * @param {object} event - A raw event to be dispatched. + * @returns {Promise} - A promise that resolves when all hooks have been notified. + * @private + */ _processRaw(event) { let meetingID; let hooks = HookCompartment.get().allGlobalSync(); @@ -43,6 +56,12 @@ export default class WebHooks { return Promise.resolve(); } + /** + * _extractIntMeetingID - Extract the internal meeting ID from mapped or raw events. + * @param {object} message - A mapped or raw event object. + * @returns {string} - The internal meeting ID. + * @private + */ _extractIntMeetingID(message) { // Webhooks events return message?.data?.attributes?.meeting["internal-meeting-id"] @@ -53,22 +72,39 @@ export default class WebHooks { || message?.core?.body?.meetingId; } + /** + * _extractExternalMeetingID - Extract the external meeting ID from a mapped event. + * @param {object} message - A mapped event object. + * @returns {string} - The external meeting ID. + * @private + */ _extractExternalMeetingID(message) { return message?.data?.attributes?.meeting["external-meeting-id"]; } + /** + * dispatch - Dispatch an event to the target hook. + * @param {object} event - The event to be dispatched (raw or mapped) + * @param {StorageItem} hook - The hook to which the event should be dispatched + * (as a StorageItem object). + * The event will *not* be dispatched if the hook is invalid, + * or if the event is not in the list of events to be sent + * for that hook. + * @returns {Promise} - A promise that resolves when the event has been dispatched. + * @public + * @async + */ dispatch(event, hook) { return new Promise((resolve, reject) => { - if (event == null) return; - + // Check for an invalid event - skip if that's the case + if (event == null) return; + // CHeck if the event is in the list of events to be sent (if list was specified) if (hook.payload.eventID != null - && (event == null - || event.data == null - || event.data.id == null + && (event?.data?.id == null || (!hook.payload.eventID.some((ev) => ev == event.data.id.toLowerCase()))) ) { - this.logger.info(`${hook.payload.callbackURL} skipping event because not in event list for hook: ${JSON.stringify(event)}`); - return ; + this.logger.info(`${hook.payload.callbackURL} skipping event because not in event list`, { eventID: event.data.id }); + return; } const emitter = new CallbackEmitter( @@ -85,15 +121,29 @@ export default class WebHooks { ); emitter.start(); - emitter.on("success", resolve); - emitter.once("stopped", () => { + emitter.on(CallbackEmitter.EVENTS.SUCCESS, () => { + this.logger.info(`successfully dispatched to ${hook.payload.callbackURL}`); + emitter.stop(); + return resolve(); + }); + emitter.once(CallbackEmitter.EVENTS.STOPPED, () => { this.logger.warn(`too many failed attempts to perform a callback call, removing the hook for: ${hook.payload.callbackURL}`); + emitter.stop(); // TODO just disable return hook.destroy().then(resolve).catch(reject); }); }); } + /** + * onEvent - Handles incoming events received by the main application (relayed + * from this module's entrypoint, OutWebHooks). + * @param {object} event - A mapped webhook event object. + * @param {object} raw - A raw webhook event object. + * @returns {Promise} - A promise that resolves when all hooks have been notified. + * @public + * @async + */ onEvent(event, raw) { let hooks = HookCompartment.get().allGlobalSync(); // filter the hooks that need to receive this event @@ -127,3 +177,4 @@ export default class WebHooks { }); } } +export default WebHooks; From 5ebd661c43e32d35065c5999a713539f1f55b775 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Fri, 6 Oct 2023 14:09:42 -0300 Subject: [PATCH 045/154] feat: add file logging --- src/common/logger.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/common/logger.js b/src/common/logger.js index f056f7e..554bd85 100644 --- a/src/common/logger.js +++ b/src/common/logger.js @@ -2,6 +2,7 @@ import { addColors, format, createLogger, transports } from 'winston'; import config from 'config'; +// jsonStringify is an extraneous dependency from Winston import jsonStringify from 'safe-stable-stringify'; const LOG_CONFIG = config.get('log') || {}; @@ -105,6 +106,18 @@ const _newLogger = ({ }) => { const loggingTransports = []; + if (filename) { + loggingTransports.push(new transports.File({ + filename, + format: combine( + timestamp(), + splat(), + errors({ stack: true }), + json(), + ) + })); + } + if (stdout) { if (process.env.NODE_ENV !== 'production') { // Development logging - fancier, more human readable stuff From 88c48a54047a38fe8e3aa7ef2869ff4c21b1c17e Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Fri, 6 Oct 2023 14:11:24 -0300 Subject: [PATCH 046/154] chore: expose prometheus.host/PROM_HOST --- config/custom-environment-variables.yml | 1 + config/default.example.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/config/custom-environment-variables.yml b/config/custom-environment-variables.yml index 178c747..83d905f 100644 --- a/config/custom-environment-variables.yml +++ b/config/custom-environment-variables.yml @@ -15,6 +15,7 @@ log: prometheus: enabled: PROM_ENABLED + host: PROM_HOST port: PROM_PORT path: PROM_PATH collectDefaultMetrics: PROM_COLLECT_DEFAULT_METRICS diff --git a/config/default.example.yml b/config/default.example.yml index 076d534..a58696e 100644 --- a/config/default.example.yml +++ b/config/default.example.yml @@ -5,6 +5,7 @@ log: prometheus: enabled: false + host: 127.0.0.1 port: 3004 path: /metrics collectDefaultMetrics: false From 7907916ca249fda93977544fc7fea1acb82b6ead Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Fri, 6 Oct 2023 14:27:33 -0300 Subject: [PATCH 047/154] fix(prometheus): shim agent even metrics are disabled Prevents exceptions from being thrown without having to check whether an agent is enabled wherever we use it --- src/metrics/index.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/metrics/index.js b/src/metrics/index.js index 3d8b4f7..f9b6f88 100644 --- a/src/metrics/index.js +++ b/src/metrics/index.js @@ -7,7 +7,7 @@ const logger = newLogger('prometheus'); const { enabled: METRICS_ENABLED = false, - host: METRICS_HOST = 'localhost', + host: METRICS_HOST = '127.0.0.1', port: METRICS_PORT = '3004', path: METRICS_PATH = '/metrics', collectDefaultMetrics: COLLECT_DEFAULT_METRICS, @@ -77,22 +77,22 @@ const buildDefaultMetrics = () => { * @public */ const getExporter = () => { - if (!METRICS_ENABLED) return null; if (AGENT && AGENT.started) return AGENT; - AGENT = new PrometheusAgent(METRICS_HOST, METRICS_PORT, { - path: METRICS_PATH, - prefix: PREFIX, - collectDefaultMetrics: COLLECT_DEFAULT_METRICS, - logger, - }); + if (AGENT == null) { + AGENT = new PrometheusAgent(METRICS_HOST, METRICS_PORT, { + path: METRICS_PATH, + prefix: PREFIX, + collectDefaultMetrics: COLLECT_DEFAULT_METRICS, + logger, + }); + } - if (injectMetrics(AGENT, buildDefaultMetrics())) { + if (METRICS_ENABLED && injectMetrics(AGENT, buildDefaultMetrics())) { AGENT.start(); - return AGENT; } - return null; + return AGENT; } export default { From c07c45fc557c7bca23304f5773396fa311d7b29e Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Fri, 6 Oct 2023 15:34:17 -0300 Subject: [PATCH 048/154] chore: add indent and keyword-spacing linter rules --- .eslintrc.yml | 2 ++ src/metrics/index.js | 20 ++++++++++++++++++-- src/modules/index.js | 9 +++++++-- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index f1d2ec7..a0fb41c 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -17,3 +17,5 @@ rules: no-multiple-empty-lines: ["warn", { max: 1 }] import/no-extraneous-dependencies: "error" jsdoc/no-undefined-types: "off" + keyword-spacing: ["warn", { before: true, after: true }] + indent: ["warn", 2, { "SwitchCase": 1 }] diff --git a/src/metrics/index.js b/src/metrics/index.js index f9b6f88..1a8031c 100644 --- a/src/metrics/index.js +++ b/src/metrics/index.js @@ -13,7 +13,6 @@ const { collectDefaultMetrics: COLLECT_DEFAULT_METRICS, } = config.has('prometheus') ? config.get('prometheus') : { enabled: false }; -let METRICS, AGENT; const PREFIX = 'bbb_webhooks_'; const METRIC_NAMES = { OUTPUT_QUEUE_SIZE: `${PREFIX}output_queue_size`, @@ -22,12 +21,16 @@ const METRIC_NAMES = { EVENT_DISPATCH_FAILURES: `${PREFIX}event_dispatch_failures`, } +let METRICS = {} +let AGENT; + /** * injectMetrics - Inject a metrics dictionary into the Prometheus agent. * @param {PrometheusAgent} agent - Prometheus agent * @param {object} metricsDictionary - Metrics dictionary (key: metric name, value: prom-client metric object) * @returns {boolean} - True if metrics were injected, false otherwise * @public + * @memberof module:exporter */ const injectMetrics = (agent, metricsDictionary) => { agent.injectMetrics(metricsDictionary); @@ -37,7 +40,8 @@ const injectMetrics = (agent, metricsDictionary) => { /** * buildDefaultMetrics - Build the default metrics dictionary. * @returns {object} - Metrics dictionary (key: metric name, value: prom-client metric object) - * @public + * @private + * @memberof module:exporter */ const buildDefaultMetrics = () => { if (METRICS == null) { @@ -75,6 +79,7 @@ const buildDefaultMetrics = () => { * getExporter - Start the Prometheus agent. * @returns {PrometheusAgent} - Prometheus agent * @public + * @memberof module:exporter */ const getExporter = () => { if (AGENT && AGENT.started) return AGENT; @@ -95,6 +100,17 @@ const getExporter = () => { return AGENT; } +/** + * Exporter module for bbb-webhooks. + * @module exporter + * @public + * @type {object} + * @property {boolean} METRICS_ENABLED - Whether metrics are enabled or not. + * @property {object} METRIC_NAMES - Metric names. + * @property {object} METRICS - Metrics dictionary (key: metric name, value: prom-client metric object) + * @property {function} injectMetrics - Inject a metrics dictionary into the Prometheus agent. + * @property {Function} getExporter - Start the Prometheus agent. + */ export default { METRICS_ENABLED, METRIC_NAMES, diff --git a/src/modules/index.js b/src/modules/index.js index 914b20a..a24e015 100644 --- a/src/modules/index.js +++ b/src/modules/index.js @@ -9,6 +9,7 @@ import { validateModulesConf, validateModuleConf } from './definitions.js'; +import Exporter from '../metrics/index.js'; const UNEXPECTED_TERMINATION_SIGNALS = ['SIGABRT', 'SIGBUS', 'SIGSEGV', 'SIGILL']; const REDIS_CONF = { @@ -55,9 +56,13 @@ export default class ModuleManager { this.logger = newLogger('module-manager'); } - _buildContext(configuration) { + _buildContext(configuration) { configuration.config = { ...BASE_CONFIGURATION, ...configuration.config }; - return new Context(configuration); + const utils = { + exporter: Exporter, + }; + + return new Context(configuration, utils); } getModulesByType(type) { From 7b7eaa3f62080969cf9ba464cc7d9bb968183473 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Fri, 6 Oct 2023 15:58:33 -0300 Subject: [PATCH 049/154] feat: add utils object to module Context Currently carries only a metrics exporter singleton util --- src/metrics/index.js | 20 +++++++++++++------- src/modules/context.js | 5 ++++- src/modules/module-wrapper.js | 1 + 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/metrics/index.js b/src/metrics/index.js index 1a8031c..ef8363b 100644 --- a/src/metrics/index.js +++ b/src/metrics/index.js @@ -101,15 +101,21 @@ const getExporter = () => { } /** - * Exporter module for bbb-webhooks. * @module exporter + * @typedef {object} MetricsExporter + * @property {boolean} METRICS_ENABLED - Whether the exporter is enabled or not + * @property {object} METRIC_NAMES - Indexed metric names + * @property {object} METRICS - Active metrics dictionary (key: metric name, value: prom-client metric object) + * @property {Function} injectMetrics - Inject a new metrics dictionary into the Prometheus agent + * Merges with the existing dictionary + * @property {Function} getExporter - Get a Prometheus agent instance to use for updating metrics + */ + +/** + * Metrics exporter util singleton object. + * @type {MetricsExporter} * @public - * @type {object} - * @property {boolean} METRICS_ENABLED - Whether metrics are enabled or not. - * @property {object} METRIC_NAMES - Metric names. - * @property {object} METRICS - Metrics dictionary (key: metric name, value: prom-client metric object) - * @property {function} injectMetrics - Inject a metrics dictionary into the Prometheus agent. - * @property {Function} getExporter - Start the Prometheus agent. + * @memberof module:exporter */ export default { METRICS_ENABLED, diff --git a/src/modules/context.js b/src/modules/context.js index 1c4505a..72896ee 100644 --- a/src/modules/context.js +++ b/src/modules/context.js @@ -18,10 +18,13 @@ class Context { * @param {string} configuration.name - Submodule name * @param {object} configuration.logger - Logger object * @param {object} configuration.config - Submodule-specific configuration + * @param {object} utils - Utility functions and objects + * @param {MetricsExporter} utils.exporter - Metrics exporter */ - constructor(configuration) { + constructor(configuration, utils = {}) { this.name = configuration.name; this.configuration = config.util.cloneDeep(configuration); + this.utils = utils; this._loggers = new Map(); } diff --git a/src/modules/module-wrapper.js b/src/modules/module-wrapper.js index 2a1fecf..b98d6f1 100644 --- a/src/modules/module-wrapper.js +++ b/src/modules/module-wrapper.js @@ -58,6 +58,7 @@ class ModuleWrapper extends EventEmitter { this.context = context; this.config = config; this.logger = newLogger('module-wrapper'); + // FIXME This is logging sensitive data, so we need to redact it this.logger.debug(`created module wrapper for ${name}`, { type, config }); this._module = null; From 9ae1c4064816a9556851e228ff2ae4c9c081b368 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Fri, 6 Oct 2023 16:22:49 -0300 Subject: [PATCH 050/154] feat(prometheus): implement module_status metric --- src/metrics/index.js | 16 ++++++++-------- src/modules/index.js | 11 +++++++++++ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/metrics/index.js b/src/metrics/index.js index ef8363b..621cf6d 100644 --- a/src/metrics/index.js +++ b/src/metrics/index.js @@ -1,6 +1,6 @@ import config from 'config'; import PrometheusAgent from './prometheus-agent.js'; -import { Counter } from 'prom-client'; +import { Counter, Gauge } from 'prom-client'; import { newLogger } from '../common/logger.js'; const logger = newLogger('prometheus'); @@ -44,15 +44,15 @@ const injectMetrics = (agent, metricsDictionary) => { * @memberof module:exporter */ const buildDefaultMetrics = () => { - if (METRICS == null) { + if (METRICS == null || Object.keys(METRICS).length === 0) { METRICS = { - // TODO to be implemented - //[METRIC_NAMES.MODULE_STATUS]: new Gauge({ - // name: METRIC_NAMES.MODULE_STATUS, - // help: 'Status of each module', - // labelNames: ['module', 'moduleType'], - //}), + [METRIC_NAMES.MODULE_STATUS]: new Gauge({ + name: METRIC_NAMES.MODULE_STATUS, + help: 'Status of each module', + labelNames: ['module', 'moduleType'], + }), + // TODO to be implemented //[METRIC_NAMES.OUTPUT_QUEUE_SIZE]: new Gauge({ // name: METRIC_NAMES.OUTPUT_QUEUE_SIZE, // help: 'Event queue size for each output module', diff --git a/src/modules/index.js b/src/modules/index.js index a24e015..90763bf 100644 --- a/src/modules/index.js +++ b/src/modules/index.js @@ -111,8 +111,19 @@ export default class ModuleManager { await module.load() this.modules[module.id] = module; this.logger.info(`module ${name} loaded`); + + Exporter.getExporter().set( + Exporter.METRIC_NAMES.MODULE_STATUS, + 1, + { module: name, moduleType: description.type }, + ); } catch (error) { this.logger.error(`failed to load module ${name}`, error); + Exporter.getExporter().set( + Exporter.METRIC_NAMES.MODULE_STATUS, + 0, + { module: name, moduleType: description.type }, + ); } } From 828f44ce5cfdf90a4e6d24b99df3e287ab0d50a8 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Mon, 9 Oct 2023 11:50:07 -0300 Subject: [PATCH 051/154] feat: add user-audio-muted and user-audio-unmuted events They track the UserMutedVoiceEvtMsg raw event. Initial mute state MUST be tracked via the user-audio-voice-enabled event. Subsequent mute state switches should be tracked via user-audio-muted/user-audio-unmuted events. --- src/process/event-processor.js | 3 ++- src/process/event.js | 28 ++++++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/process/event-processor.js b/src/process/event-processor.js index 26500f1..82b127f 100644 --- a/src/process/event-processor.js +++ b/src/process/event-processor.js @@ -1,3 +1,4 @@ +import config from 'config'; import IDMapping from '../db/redis/id-mapping.js'; import { newLogger } from '../common/logger.js'; import WebhooksEvent from '../process/event.js'; @@ -104,7 +105,7 @@ export default class EventProcessor { break; case "user-left": UserMapping.get().removeMapping( - outputEvent.data.attributes.user["internal-user-id"] + outputEvent.data.attributes.user["internal-user-id"] ).catch((error) => { Logger.error(`error removing user mapping: ${error}`, { error: error.stack, diff --git a/src/process/event.js b/src/process/event.js index c4d6a60..177c435 100644 --- a/src/process/event.js +++ b/src/process/event.js @@ -18,6 +18,9 @@ export default class WebhooksEvent { "user-left", "user-audio-voice-enabled", "user-audio-voice-disabled", + "user-audio-muted", + "user-audio-unmuted", + "user-audio-unhandled", "user-cam-broadcast-start", "user-cam-broadcast-end", "user-presenter-assigned", @@ -62,6 +65,7 @@ export default class WebhooksEvent { USER_EVENTS: [ "UserJoinedMeetingEvtMsg", "UserLeftMeetingEvtMsg", + "UserMutedVoiceEvtMsg", "UserJoinedVoiceConfToClientEvtMsg", "UserLeftVoiceConfToClientEvtMsg", "PresenterAssignedEvtMsg", @@ -117,7 +121,7 @@ export default class WebhooksEvent { // Map internal message based on it's type map() { if (this.inputEvent) { - if (this.mappedEvent(this.inputEvent, WebhooksEvent.RAW.MEETING_EVENTS)) { + if (this.mappedEvent(this.inputEvent, WebhooksEvent.RAW.MEETING_EVENTS)) { this.meetingTemplate(this.inputEvent); } else if (this.mappedEvent(this.inputEvent, WebhooksEvent.RAW.USER_EVENTS)) { this.userTemplate(this.inputEvent); @@ -236,6 +240,20 @@ export default class WebhooksEvent { } } + handleUserMutedVoice(message) { + try { + const { body } = message.core; + const muted = body.muted; + + if (muted === true) return "user-audio-muted"; + if (muted === false) return "user-audio-unmuted"; + return "user-audio-unhandled"; + } catch (error) { + logger.error('error handling user muted voice', error); + return "user-audio-unhandled"; + } + } + // Map internal to external message for user information userTemplate(messageObj) { const msgBody = messageObj.core.body; @@ -266,15 +284,20 @@ export default class WebhooksEvent { } }; - // Refactor the if-else chain down there to a switch case block switch (this.outputEvent.data.id) { case "user-audio-voice-enabled": this.outputEvent.data["attributes"]["user"]["listening-only"] = msgBody.listenOnly; this.outputEvent.data["attributes"]["user"]["sharing-mic"] = !msgBody.listenOnly; + this.outputEvent.data["attributes"]["user"]["muted"] = msgBody.muted; break; case "user-audio-voice-disabled": this.outputEvent.data["attributes"]["user"]["listening-only"] = false; this.outputEvent.data["attributes"]["user"]["sharing-mic"] = false; + this.outputEvent.data["attributes"]["user"]["muted"] = true; + break; + case "user-audio-muted": + case "user-audio-unmuted": + this.outputEvent.data["attributes"]["user"]["muted"] = msgBody.muted; break; case "user-emoji-changed": { const emoji = msgBody.emoji || msgBody.reactionEmoji || "none"; @@ -496,6 +519,7 @@ export default class WebhooksEvent { case "UserLeftMeetingEvtMsg": return "user-left"; case "UserJoinedVoiceConfToClientEvtMsg": return "user-audio-voice-enabled"; case "UserLeftVoiceConfToClientEvtMsg": return "user-audio-voice-disabled"; + case "UserMutedVoiceEvtMsg": return this.handleUserMutedVoice(message); case "UserBroadcastCamStartedEvtMsg": return "user-cam-broadcast-start"; case "UserBroadcastCamStoppedEvtMsg": return "user-cam-broadcast-end"; case "PresenterAssignedEvtMsg": return "user-presenter-assigned"; From fb8d7efda48bbfa15af8082cfcae7143269ffa2b Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Mon, 9 Oct 2023 14:02:22 -0300 Subject: [PATCH 052/154] fix(db): filter invalid items in getAll --- src/db/redis/base-storage.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/db/redis/base-storage.js b/src/db/redis/base-storage.js index 0a28e4b..c923136 100644 --- a/src/db/redis/base-storage.js +++ b/src/db/redis/base-storage.js @@ -237,6 +237,8 @@ class StorageCompartmentKV { getAll() { const allWithAliases = Object.keys(this.localStorage).reduce((arr, id) => { + if (this.localStorage[id] == null) return arr; + arr.push(this.localStorage[id]); return arr; }, []); From 810d3b444201fc6db34a5cfff7b3dba0b0fda732 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Mon, 9 Oct 2023 15:26:48 -0300 Subject: [PATCH 053/154] refactor(webhooks): simplify raw event handling, prevent duplicate POST requests --- extra/interceptor.js | 10 +++++-- src/db/redis/hooks.js | 2 +- src/out/webhooks/api/api.js | 4 ++- src/out/webhooks/index.js | 3 ++- src/out/webhooks/web-hooks.js | 51 +++++++++++------------------------ test/test.js | 32 +++++++++++----------- 6 files changed, 45 insertions(+), 57 deletions(-) diff --git a/extra/interceptor.js b/extra/interceptor.js index ce909f0..335957e 100644 --- a/extra/interceptor.js +++ b/extra/interceptor.js @@ -15,6 +15,10 @@ const sharedSecret = process.env.SHARED_SECRET || eject("SHARED_SECRET not set") const catcherDomain = process.env.CATCHER_DOMAIN || eject("CATCHER_DOMAIN not set"); const bbbDomain = process.env.BBB_DOMAIN || eject("BBB_DOMAIN not set"); const FOREVER = (process.env.FOREVER && process.env.FOREVER === 'true') || false; +const GET_RAW = (process.env.GET_RAW && process.env.GET_RAW === 'true') || false; +const EVENT_ID = process.env.EVENT_ID || ''; +const MEETING_ID = process.env.MEETING_ID || ''; + let server = null; const encodeForUrl = (value) => { @@ -53,9 +57,11 @@ app.post("/callback", (req, res) => { }); console.log("Server listening on port", port); -// registers a global hook on the webhooks app +// registers a hook on the webhooks app const myUrl = "http://" + catcherDomain + ":" + port + "/callback"; -const params = "callbackURL=" + encodeForUrl(myUrl); +let params = "callbackURL=" + encodeForUrl(myUrl) + "&getRaw=" + GET_RAW; +if (EVENT_ID) params += "&eventID=" + EVENT_ID; +if (MEETING_ID) params += "&meetingID=" + MEETING_ID; const checksum = sha1("hooks/create" + params + sharedSecret); const fullUrl = "http://" + bbbDomain + "/bigbluebutton/api/hooks/create?" + params + "&checksum=" + checksum diff --git a/src/db/redis/hooks.js b/src/db/redis/hooks.js index bb4d139..1cbd370 100644 --- a/src/db/redis/hooks.js +++ b/src/db/redis/hooks.js @@ -147,7 +147,7 @@ class HookCompartment extends StorageCompartmentKV { return null; } - allGlobalSync() { + getAllGlobalHooks() { return this.getAll().filter(hook => this.isGlobal(hook)); } diff --git a/src/out/webhooks/api/api.js b/src/out/webhooks/api/api.js index fbb66ec..d35b60a 100644 --- a/src/out/webhooks/api/api.js +++ b/src/out/webhooks/api/api.js @@ -18,6 +18,7 @@ const fromMonit = req => (req.headers["user-agent"] != null) && req.headers["use // Web server that listens for API calls and process them. export default class API { static logger = newLogger('api'); + static setStorage (storage) { API.storage = storage; } @@ -34,6 +35,7 @@ export default class API { this._permanentURLs = options.permanentURLs || []; this._secret = options.secret; + this._exporter = options.exporter; this._validateChecksum = this._validateChecksum.bind(this); this._registerRoutes(); @@ -177,7 +179,7 @@ export default class API { if (meetingID != null) { // all the hooks that receive events from this meeting - hooks = API.storage.get().allGlobalSync(); + hooks = API.storage.get().getAllGlobalHooks(); hooks = hooks.concat(API.storage.get().findByExternalMeetingID(meetingID)); hooks = Utils.sortBy(hooks, hook => hook.id); } else { diff --git a/src/out/webhooks/index.js b/src/out/webhooks/index.js index 9a0a4cd..849ced7 100644 --- a/src/out/webhooks/index.js +++ b/src/out/webhooks/index.js @@ -45,12 +45,13 @@ class OutWebHooks { this.type = OutWebHooks.type; this.config = config; this.setContext(context); + this.webHooks = new WebHooks(this.context, this.config); this.api = new API({ permanentURLs: this.config.permanentURLs, secret: this.config.server.secret, + exporter: this.context.exporter, }); API.setStorage(HookCompartment); - this.webHooks = new WebHooks(this.context, this.config); this.loaded = false; } diff --git a/src/out/webhooks/web-hooks.js b/src/out/webhooks/web-hooks.js index 6d2c7a3..5734d98 100644 --- a/src/out/webhooks/web-hooks.js +++ b/src/out/webhooks/web-hooks.js @@ -23,37 +23,19 @@ class WebHooks { /** * _processRaw - Dispatch raw events to hooks that expect raw data. - * @param {object} event - A raw event to be dispatched. + * @param {object} hook - The hook to which the event should be dispatched + * @param {object} rawEvent - A raw event to be dispatched. * @returns {Promise} - A promise that resolves when all hooks have been notified. * @private */ - _processRaw(event) { - let meetingID; - let hooks = HookCompartment.get().allGlobalSync(); + _processRaw(hook, rawEvent) { + if (hook == null || !hook?.payload?.getRaw || !this.config.getRaw) return Promise.resolve(); - // Add hooks for the specific meeting that expect raw data - // Get meetingId for a raw message that was previously mapped by another webhook application or if it's straight from redis - meetingID = this._extractIntMeetingID(event); + this.logger.info('dispatching raw event to hook', { callbackURL: hook.payload.callbackURL }); - if (meetingID != null) { - const eMeetingID = this._extractExternalMeetingID(event); - hooks = hooks.concat(HookCompartment.get().findByExternalMeetingID(eMeetingID)); - // Notify the hooks that expect raw data - return Promise.all(hooks.map((hook) => { - if (hook == null) return Promise.resolve(); - - if (hook.payload.getRaw) { - this.logger.info('dispatching raw event to hook', { callbackURL: hook.payload.callbackURL }); - return this.dispatch(event, hook).catch((error) => { - this.logger.error('failed to enqueue', { calbackURL: hook.payload.callbackURL, error: error.stack }); - }); - } - - return Promise.resolve(); - })); - } - - return Promise.resolve(); + return this.dispatch(rawEvent, hook).catch((error) => { + this.logger.error('failed to enqueue', { calbackURL: hook.payload.callbackURL, error: error.stack }); + }); } /** @@ -145,10 +127,11 @@ class WebHooks { * @async */ onEvent(event, raw) { - let hooks = HookCompartment.get().allGlobalSync(); + const meetingID = this._extractIntMeetingID(event); + let hooks = HookCompartment.get().getAllGlobalHooks(); + // filter the hooks that need to receive this event // add hooks that are registered for this specific meeting - const meetingID = this._extractIntMeetingID(event); if (meetingID != null) { const eMeetingID = this._extractExternalMeetingID(event); hooks = hooks.concat(HookCompartment.get().findByExternalMeetingID(eMeetingID)); @@ -161,20 +144,16 @@ class WebHooks { return Promise.all(hooks.map((hook) => { if (hook == null) return Promise.resolve(); + if (!hook.payload.getRaw) { this.logger.info('dispatching event to hook', { callbackURL: hook.payload.callbackURL }); return this.dispatch(event, hook).catch((error) => { this.logger.error('failed to enqueue', { calbackURL: hook.payload.callbackURL, error: error.stack }); }); + } else { + return this._processRaw(hook, raw); } - - return Promise.resolve(); - })).then(() => { - const sendRaw = hooks.some(hook => hook && hook.payload.getRaw); - if (sendRaw && this.config.getRaw) return this._processRaw(raw); - - return Promise.resolve(); - }); + })); } } export default WebHooks; diff --git a/test/test.js b/test/test.js index 66d9ee5..b6e7116 100644 --- a/test/test.js +++ b/test/test.js @@ -32,7 +32,7 @@ describe('bbb-webhooks tests', () => { .catch(done); }); beforeEach((done) => { - const hooks = Hook.get().allGlobalSync(); + const hooks = Hook.get().getAllGlobalHooks(); Helpers.flushall(redisClient); hooks.forEach((hook) => { Helpers.flushredis(hook); @@ -41,7 +41,7 @@ describe('bbb-webhooks tests', () => { done(); }) after(() => { - const hooks = Hook.get().allGlobalSync(); + const hooks = Hook.get().getAllGlobalHooks(); Helpers.flushall(redisClient); hooks.forEach((hook) => { Helpers.flushredis(hook); @@ -57,7 +57,7 @@ describe('bbb-webhooks tests', () => { .get(getUrl) .expect('Content-Type', /text\/xml/) .expect(200, () => { - const hooks = Hook.get().allGlobalSync(); + const hooks = Hook.get().getAllGlobalHooks(); if (hooks && hooks.some(hook => hook.payload.permanent)) { done(); } else { @@ -76,7 +76,7 @@ describe('bbb-webhooks tests', () => { }).then(() => { done(); }).catch(done); }); it('should destroy a hook', (done) => { - const hooks = Hook.get().allGlobalSync(); + const hooks = Hook.get().getAllGlobalHooks(); const hook = hooks[hooks.length-1].id; let getUrl = utils.checksumAPI(Helpers.url + Helpers.destroyUrl(hook), SHARED_SECRET); getUrl = Helpers.destroyUrl(hook) + '&checksum=' + getUrl @@ -85,7 +85,7 @@ describe('bbb-webhooks tests', () => { .get(getUrl) .expect('Content-Type', /text\/xml/) .expect(200, () => { - const hooks = Hook.get().allGlobalSync(); + const hooks = Hook.get().getAllGlobalHooks(); if (hooks && hooks.every(hook => hook.payload.callbackURL != Helpers.callback)) done(); }) }) @@ -99,7 +99,7 @@ describe('bbb-webhooks tests', () => { .get(getUrl) .expect('Content-Type', /text\/xml/) .expect(200, () => { - const hooks = Hook.get().allGlobalSync(); + const hooks = Hook.get().getAllGlobalHooks(); if (hooks && hooks[0].payload.callbackURL == WH_CONFIG.permanentURLs[0].url) { done(); } @@ -112,7 +112,7 @@ describe('bbb-webhooks tests', () => { describe('GET /hooks/create getRaw hook', () => { after( (done) => { - const hooks = Hook.get().allGlobalSync(); + const hooks = Hook.get().getAllGlobalHooks(); Hook.get().removeSubscription(hooks[hooks.length-1].id) .then(() => { done(); }) .catch(done); @@ -125,7 +125,7 @@ describe('bbb-webhooks tests', () => { .get(getUrl) .expect('Content-Type', /text\/xml/) .expect(200, () => { - const hooks = Hook.get().allGlobalSync(); + const hooks = Hook.get().getAllGlobalHooks(); if (hooks && hooks.some((hook) => { return hook.payload.getRaw })) { done(); } @@ -138,18 +138,18 @@ describe('bbb-webhooks tests', () => { describe('/POST mapped message', () => { before((done) => { - const hooks = Hook.get().allGlobalSync(); + const hooks = Hook.get().getAllGlobalHooks(); const hook = hooks[0]; Helpers.flushredis(hook); done(); }); after(() => { - const hooks = Hook.get().allGlobalSync(); + const hooks = Hook.get().getAllGlobalHooks(); const hook = hooks[0]; Helpers.flushredis(hook); }) it('should post mapped message ', (done) => { - const hooks = Hook.get().allGlobalSync(); + const hooks = Hook.get().getAllGlobalHooks(); const hook = hooks[0]; const getpost = nock(WH_CONFIG.permanentURLs[0].url) .filteringRequestBody((body) => { @@ -165,20 +165,20 @@ describe('bbb-webhooks tests', () => { }); describe('/POST raw message', () => { before((done) => { - const hooks = Hook.get().allGlobalSync(); + const hooks = Hook.get().getAllGlobalHooks(); const hook = hooks[0]; Helpers.flushredis(hook); done(); }); after((done) => { - const hooks = Hook.get().allGlobalSync(); + const hooks = Hook.get().getAllGlobalHooks(); Hook.get().removeSubscription(hooks[hooks.length-1].id) .then(() => { done(); }) .catch(done); Helpers.flushredis(hooks[hooks.length-1]); }); it('should post raw message ', (done) => { - const hooks = Hook.get().allGlobalSync(); + const hooks = Hook.get().getAllGlobalHooks(); const hook = hooks[0]; const getpost = nock(Helpers.callback) @@ -201,13 +201,13 @@ describe('bbb-webhooks tests', () => { describe('/POST multi message', () => { before( () =>{ - const hooks = Hook.get().allGlobalSync(); + const hooks = Hook.get().getAllGlobalHooks(); const hook = hooks[0]; Helpers.flushredis(hook); hook.queue = ["multiMessage1"]; }); it('should post multi message ', (done) => { - const hooks = Hook.get().allGlobalSync(); + const hooks = Hook.get().getAllGlobalHooks(); const hook = hooks[0]; hook.enqueue("multiMessage2") const getpost = nock(WH_CONFIG.permanentURLs[0].url) From 13a0d85d3ef65098ecb123799949288812803bd9 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Tue, 10 Oct 2023 16:54:57 -0300 Subject: [PATCH 054/154] feat(webhooks): add metrics for the webhooks output module - bbb_webhooks_out_hooks_api_requests - bbb_webhooks_out_hooks_registered_hooks - bbb_webhooks_out_hooks_hook_failures - bbb_webhooks_out_hooks_processed_events --- src/metrics/index.js | 60 ++++---- src/metrics/prometheus-agent.js | 2 +- src/modules/index.js | 4 +- src/out/webhooks/api/api.js | 227 ++++++++++++++++-------------- src/out/webhooks/api/responses.js | 47 +++++-- src/out/webhooks/index.js | 67 +++++++-- src/out/webhooks/metrics.js | 45 ++++++ src/out/webhooks/web-hooks.js | 71 +++++++++- src/process/event-processor.js | 3 +- 9 files changed, 370 insertions(+), 156 deletions(-) create mode 100644 src/out/webhooks/metrics.js diff --git a/src/metrics/index.js b/src/metrics/index.js index 621cf6d..cc1d64d 100644 --- a/src/metrics/index.js +++ b/src/metrics/index.js @@ -1,6 +1,12 @@ import config from 'config'; import PrometheusAgent from './prometheus-agent.js'; -import { Counter, Gauge } from 'prom-client'; +import { + Counter, + Gauge, + Histogram, + Summary, + register, +} from 'prom-client'; import { newLogger } from '../common/logger.js'; const logger = newLogger('prometheus'); @@ -26,13 +32,17 @@ let AGENT; /** * injectMetrics - Inject a metrics dictionary into the Prometheus agent. - * @param {PrometheusAgent} agent - Prometheus agent * @param {object} metricsDictionary - Metrics dictionary (key: metric name, value: prom-client metric object) + * @param {object} options - Options object + * @param {PrometheusAgent} options.agent - Prometheus agent to inject the metrics into + * If not specified, the default agent will be used * @returns {boolean} - True if metrics were injected, false otherwise * @public * @memberof module:exporter */ -const injectMetrics = (agent, metricsDictionary) => { +const injectMetrics = (metricsDictionary, { + agent = AGENT, +} = {}) => { agent.injectMetrics(metricsDictionary); return true; } @@ -75,29 +85,15 @@ const buildDefaultMetrics = () => { return METRICS; }; -/** - * getExporter - Start the Prometheus agent. - * @returns {PrometheusAgent} - Prometheus agent - * @public - * @memberof module:exporter - */ -const getExporter = () => { - if (AGENT && AGENT.started) return AGENT; - - if (AGENT == null) { - AGENT = new PrometheusAgent(METRICS_HOST, METRICS_PORT, { - path: METRICS_PATH, - prefix: PREFIX, - collectDefaultMetrics: COLLECT_DEFAULT_METRICS, - logger, - }); - } - - if (METRICS_ENABLED && injectMetrics(AGENT, buildDefaultMetrics())) { - AGENT.start(); - } +AGENT = new PrometheusAgent(METRICS_HOST, METRICS_PORT, { + path: METRICS_PATH, + prefix: PREFIX, + collectDefaultMetrics: COLLECT_DEFAULT_METRICS, + logger, +}); - return AGENT; +if (METRICS_ENABLED && injectMetrics(buildDefaultMetrics())) { + AGENT.start(); } /** @@ -106,9 +102,14 @@ const getExporter = () => { * @property {boolean} METRICS_ENABLED - Whether the exporter is enabled or not * @property {object} METRIC_NAMES - Indexed metric names * @property {object} METRICS - Active metrics dictionary (key: metric name, value: prom-client metric object) + * @property {Function} Counter - prom-client Counter class + * @property {Function} Gauge - prom-client Gauge class + * @property {Function} Histogram - prom-client Histogram class + * @property {Function} Summary - prom-client Summary class + * @property {Function} register - Register a new metric with the Prometheus agent * @property {Function} injectMetrics - Inject a new metrics dictionary into the Prometheus agent * Merges with the existing dictionary - * @property {Function} getExporter - Get a Prometheus agent instance to use for updating metrics + * @property {PrometheusAgent} agent - Prometheus agent */ /** @@ -121,7 +122,12 @@ export default { METRICS_ENABLED, METRIC_NAMES, METRICS, + Counter, + Gauge, + Histogram, + Summary, + register, injectMetrics, - getExporter, + agent: AGENT, }; diff --git a/src/metrics/prometheus-agent.js b/src/metrics/prometheus-agent.js index 64c5374..30c23fc 100644 --- a/src/metrics/prometheus-agent.js +++ b/src/metrics/prometheus-agent.js @@ -205,7 +205,7 @@ class PrometheusScrapeAgent { const metric = this.getMetric(metricName); if (metric) { - promclient.register.reset(metricName); + metric.reset(metricName); } }); } diff --git a/src/modules/index.js b/src/modules/index.js index 90763bf..8f50b69 100644 --- a/src/modules/index.js +++ b/src/modules/index.js @@ -112,14 +112,14 @@ export default class ModuleManager { this.modules[module.id] = module; this.logger.info(`module ${name} loaded`); - Exporter.getExporter().set( + Exporter.agent.set( Exporter.METRIC_NAMES.MODULE_STATUS, 1, { module: name, moduleType: description.type }, ); } catch (error) { this.logger.error(`failed to load module ${name}`, error); - Exporter.getExporter().set( + Exporter.agent.set( Exporter.METRIC_NAMES.MODULE_STATUS, 0, { module: name, moduleType: description.type }, diff --git a/src/out/webhooks/api/api.js b/src/out/webhooks/api/api.js index d35b60a..80f6f62 100644 --- a/src/out/webhooks/api/api.js +++ b/src/out/webhooks/api/api.js @@ -3,6 +3,7 @@ import url from 'url'; import { newLogger } from '../../../common/logger.js'; import Utils from '../../../common/utils.js'; import responses from './responses.js'; +import { METRIC_NAMES } from '../metrics.js'; // Returns a simple string with a description of the client that made // the request. It includes the IP address and the user agent. @@ -10,11 +11,6 @@ const clientDataSimple = req => `ip ${Utils.ipFromRequest(req)}, using ${req.hea // Cleans up a string with an XML in it removing spaces and new lines from between the tags. const cleanupXML = string => string.trim().replace(/>\s*/g, '>'); - -// Was this request made by monit? -// TODO remove/review -const fromMonit = req => (req.headers["user-agent"] != null) && req.headers["user-agent"].match(/^monit/); - // Web server that listens for API calls and process them. export default class API { static logger = newLogger('api'); @@ -36,38 +32,28 @@ export default class API { this._permanentURLs = options.permanentURLs || []; this._secret = options.secret; this._exporter = options.exporter; + this._validateChecksum = this._validateChecksum.bind(this); this._registerRoutes(); } - start(port, bind) { - return new Promise((resolve, reject) => { - this.server = this.app.listen(port, bind, () => { - if (this.server.address() == null) { - API.logger.error(`aborting, could not bind to port ${port}`); - return reject(new Error(`API failed to start, EARADDRINUSE`)); - } - API.logger.info(`listening on port ${port} in ${this.app.settings.env.toUpperCase()} mode`); - return resolve(); - }); - }); - } - _registerRoutes() { - // Request logger - this.app.all("*", (req, res, next) => { - if (!fromMonit(req)) { - API.logger.info(`${req.method} request to ${req.url} from: ${clientDataSimple(req)}`); - } + this.app.use((req, res, next) => { + const { method, url, baseUrl, path } = req; + + API.logger.info(`Received: ${method} request to ${baseUrl + path}`, { + clientData: clientDataSimple(req), + url, + }); next(); }); this.app.get("/bigbluebutton/api/hooks/create", this._validateChecksum, this._create.bind(this)); - this.app.get("/bigbluebutton/api/hooks/destroy", this._validateChecksum, this._destroy); - this.app.get("/bigbluebutton/api/hooks/list", this._validateChecksum, this._list); + this.app.get("/bigbluebutton/api/hooks/destroy", this._validateChecksum, this._destroy.bind(this)); + this.app.get("/bigbluebutton/api/hooks/list", this._validateChecksum, this._list.bind(this)); this.app.get("/bigbluebutton/api/hooks/ping", (req, res) => { - res.write("bbb-webhooks up!"); + res.write("bbb-webhooks API up!"); res.end(); }); } @@ -84,6 +70,8 @@ export default class API { const meetingID = urlObj.query["meetingID"]; const eventID = urlObj.query["eventID"]; let getRaw = urlObj.query["getRaw"]; + let returncode = responses.RETURN_CODES.SUCCESS; + let messageKey; if (getRaw) { getRaw = JSON.parse(getRaw.toLowerCase()); @@ -93,121 +81,138 @@ export default class API { if (callbackURL == null) { API.respondWithXML(res, responses.missingParamCallbackURL); - return; - } - - try { - const { hook, duplicated } = await API.storage.get().addSubscription({ - callbackURL, - meetingID, - eventID, - permanent: this._isHookPermanent(callbackURL), - getRaw, - }); - - let msg; - - if (duplicated) { - msg = responses.createDuplicated(hook.id); - } else if (hook != null) { - const { permanent, getRaw } = hook.payload; - msg = responses.createSuccess(hook.id, permanent, getRaw); - } else { - msg = responses.createFailure; - } - - API.respondWithXML(res, msg); - } catch (error) { - API.logger.error(`error creating hook ${error}`); - API.respondWithXML(res, responses.createFailure); - } - } - - // Create a permanent hook. Permanent hooks can't be deleted via API and will try to emit a message until it succeed - async createPermanents() { - for (let i = 0; i < this._permanentURLs.length; i++) { + returncode = responses.RETURN_CODES.FAILED; + messageKey = responses.MESSAGE_KEYS.missingParamCallbackURL; + } else { try { - const { url: callbackURL, getRaw } = this._permanentURLs[i]; const { hook, duplicated } = await API.storage.get().addSubscription({ callbackURL, + meetingID, + eventID, permanent: this._isHookPermanent(callbackURL), getRaw, }); + let msg; + if (duplicated) { - API.logger.info(`permanent hook already set ${hook.id}`, { hook: hook.payload }); + msg = responses.createDuplicated(hook.id); + messageKey = responses.MESSAGE_KEYS.duplicateWarning; } else if (hook != null) { - API.logger.info('permanent hook created successfully'); + const { permanent, getRaw } = hook.payload; + msg = responses.createSuccess(hook.id, permanent, getRaw); } else { - API.logger.error('error creating permanent hook'); + msg = responses.createFailure; + returncode = responses.RETURN_CODES.FAILED; + messageKey = responses.MESSAGE_KEYS.createHookError; } + + API.respondWithXML(res, msg); } catch (error) { - API.logger.error(`error creating permanent hook ${error}`); + API.logger.error('error creating hook', error); + API.respondWithXML(res, responses.createFailure); + returncode = responses.RETURN_CODES.FAILED; + messageKey = responses.MESSAGE_KEYS.createHookError; } } + + this._exporter.agent.increment(METRIC_NAMES.API_REQUESTS, { + method: req.method, + path: urlObj.pathname, + returncode, + messageKey, + }); } + // Create a permanent hook. Permanent hooks can't be deleted via API and will try to emit a message until it succeed async _destroy(req, res, next) { const urlObj = url.parse(req.url, true); const hookID = urlObj.query["hookID"]; + let returncode = responses.RETURN_CODES.SUCCESS; + let messageKey; if (hookID == null) { + returncode = responses.RETURN_CODES.FAILED; + messageKey = responses.MESSAGE_KEYS.missingParamHookID; API.respondWithXML(res, responses.missingParamHookID); } else { - let removed, failed; try { - removed = await API.storage.get().removeSubscription(hookID); - } catch (error) { - API.logger.error('error removing hook', error); - failed = true; - } finally { + const removed = await API.storage.get().removeSubscription(hookID); if (removed) { API.respondWithXML(res, responses.destroySuccess); - } else if (failed) { - API.respondWithXML(res, responses.destroyFailure); } else { + returncode = responses.RETURN_CODES.FAILED; + messageKey = responses.MESSAGE_KEYS.destroyMissingHook; API.respondWithXML(res, responses.destroyNoHook); } + } catch (error) { + API.logger.error('error removing hook', error); + returncode = responses.RETURN_CODES.FAILED; + messageKey = responses.MESSAGE_KEYS.destroyHookError; + API.respondWithXML(res, responses.destroyFailure); } } + + this._exporter.agent.increment(METRIC_NAMES.API_REQUESTS, { + method: req.method, + path: urlObj.pathname, + returncode, + messageKey, + }); } _list(req, res, next) { let hooks; const urlObj = url.parse(req.url, true); const meetingID = urlObj.query["meetingID"]; + let returncode = responses.RETURN_CODES.SUCCESS; + let messageKey; - if (meetingID != null) { - // all the hooks that receive events from this meeting - hooks = API.storage.get().getAllGlobalHooks(); - hooks = hooks.concat(API.storage.get().findByExternalMeetingID(meetingID)); - hooks = Utils.sortBy(hooks, hook => hook.id); - } else { - // no meetingID, return all hooks - hooks = API.storage.get().getAll(); + try { + if (meetingID != null) { + // all the hooks that receive events from this meeting + hooks = API.storage.get().getAllGlobalHooks(); + hooks = hooks.concat(API.storage.get().findByExternalMeetingID(meetingID)); + hooks = Utils.sortBy(hooks, hook => hook.id); + } else { + // no meetingID, return all hooks + hooks = API.storage.get().getAll(); + } + + let msg = "SUCCESS"; + hooks.forEach((hook) => { + const { + eventID, + externalMeetingID, + callbackURL, + permanent, + getRaw, + } = hook.payload; + msg += ""; + msg += `${hook.id}`; + msg += ``; + if (!API.storage.get().isGlobal(hook)) { msg += ``; } + if (eventID != null) { msg += `${eventID}`; } + msg += `${permanent}`; + msg += `${getRaw}`; + msg += ""; + }); + msg += ""; + + API.respondWithXML(res, msg); + } catch (error) { + API.logger.error('error listing hooks', error); + returncode = responses.RETURN_CODES.FAILED; + messageKey = responses.MESSAGE_KEYS.listHookError; + API.respondWithXML(res, responses.listFailure); } - let msg = "SUCCESS"; - hooks.forEach((hook) => { - const { - eventID, - externalMeetingID, - callbackURL, - permanent, - getRaw, - } = hook.payload; - msg += ""; - msg += `${hook.id}`; - msg += ``; - if (!API.storage.get().isGlobal(hook)) { msg += ``; } - if (eventID != null) { msg += `${eventID}`; } - msg += `${permanent}`; - msg += `${getRaw}`; - msg += ""; + this._exporter.agent.increment(METRIC_NAMES.API_REQUESTS, { + method: req.method, + path: urlObj.pathname, + returncode, + messageKey, }); - msg += ""; - - API.respondWithXML(res, msg); } // Validates the checksum in the request `req`. @@ -221,8 +226,26 @@ export default class API { next(); } else { API.logger.info('checksum check failed, sending a checksumError response', responses.checksumError); - res.setHeader("Content-Type", "text/xml"); - res.send(cleanupXML(responses.checksumError)); + API.respondWithXML(res, responses.checksumError); + this._exporter.agent.increment(METRIC_NAMES.API_REQUEST_FAILURES_XML, { + method: req.method, + path: urlObj.pathname, + returncode: responses.RETURN_CODES.FAILED, + messageKey: responses.MESSAGE_KEYS.checksumError, + }); } } + + start(port, bind) { + return new Promise((resolve, reject) => { + this.server = this.app.listen(port, bind, () => { + if (this.server.address() == null) { + API.logger.error(`aborting, could not bind to port ${port}`); + return reject(new Error(`API failed to start, EARADDRINUSE`)); + } + API.logger.info(`listening on port ${port} in ${this.app.settings.env.toUpperCase()} mode`); + return resolve(); + }); + }); + } } diff --git a/src/out/webhooks/api/responses.js b/src/out/webhooks/api/responses.js index 8849c76..90b962c 100644 --- a/src/out/webhooks/api/responses.js +++ b/src/out/webhooks/api/responses.js @@ -1,60 +1,84 @@ +const RETURN_CODES = { + SUCCESS: "SUCCESS", + FAILED: "FAILED", +}; +const MESSAGE_KEYS = { + checksumError: "checksumError", + createHookError: "createHookError", + duplicateWarning: "duplicateWarning", + destroyHookError: "destroyHookError", + listHookError: "listHookError", + destroyMissingHook: "destroyMissingHook", + missingParamCallbackURL: "missingParamCallbackURL", + missingParamHookID: "missingParamHookID", +}; + const failure = (key, msg) => ` \ - FAILED \ + ${RETURN_CODES.FAILED} \ ${key} \ ${msg} \ `; + const checksumError = failure( - "checksumError", + MESSAGE_KEYS.checksumError, "You did not pass the checksum security check.", ); + const createSuccess = (id, permanent, getRaw) => ` \ - SUCCESS \ + ${RETURN_CODES.SUCCESS} \ ${id} \ ${permanent} \ ${getRaw} \ `; const createFailure = failure( - "createHookError", + MESSAGE_KEYS.createHookError, "An error happened while creating your hook. Check the logs." ); const createDuplicated = (id) => ` \ - SUCCESS \ + ${RETURN_CODES.SUCCESS} \ ${id} \ - duplicateWarning \ + ${MESSAGE_KEYS.duplicateWarning} \ There is already a hook for this callback URL. \ `; const destroySuccess = ` \ - SUCCESS \ + ${RETURN_CODES.SUCCESS} \ true \ `; const destroyFailure = failure( - "destroyHookError", + MESSAGE_KEYS.destroyHookError, "An error happened while removing your hook. Check the logs." ); const destroyNoHook = failure( - "destroyMissingHook", + MESSAGE_KEYS.destroyMissingHook, "The hook informed was not found." ); const missingParamCallbackURL = failure( - "missingParamCallbackURL", + MESSAGE_KEYS.missingParamCallbackURL, "You must specify a callbackURL in the parameters." ); const missingParamHookID = failure( - "missingParamHookID", + MESSAGE_KEYS.missingParamHookID, "You must specify a hookID in the parameters." ); +const listFailure = failure( + MESSAGE_KEYS.listHookError, + "An error happened while listing registered hooks. Check the logs." +); + export default { + RETURN_CODES, + MESSAGE_KEYS, checksumError, createSuccess, createFailure, @@ -62,6 +86,7 @@ export default { destroySuccess, destroyFailure, destroyNoHook, + listFailure, missingParamCallbackURL, missingParamHookID, }; diff --git a/src/out/webhooks/index.js b/src/out/webhooks/index.js index 849ced7..7775069 100644 --- a/src/out/webhooks/index.js +++ b/src/out/webhooks/index.js @@ -1,6 +1,7 @@ import WebHooks from './web-hooks.js'; import API from './api/api.js'; import HookCompartment from '../../db/redis/hooks.js'; +import { buildMetrics, METRIC_NAMES } from './metrics.js'; /* * [MODULE_TYPES.OUTPUT]: { @@ -22,7 +23,7 @@ import HookCompartment from '../../db/redis/hooks.js'; * @property {BbbWebhooksLogger} logger - The logger. * @property {object} config - This module's configuration object. * @property {API} api - The Webhooks API server. - * @property {WebHooks} webHooks - The Webhooks dispatcher. + * @property {WebHooks} webhooks - The Webhooks dispatcher. * @property {boolean} loaded - Whether the module is loaded or not. */ class OutWebHooks { @@ -45,14 +46,60 @@ class OutWebHooks { this.type = OutWebHooks.type; this.config = config; this.setContext(context); - this.webHooks = new WebHooks(this.context, this.config); + this.loaded = false; + this._exporter = this.context.utils.exporter; + + this._bootstrapExporter(); + this.webhooks = new WebHooks( + this.context, + this.config, { + exporter: this._exporter, + permanentURLs: this.config.permanentURLs, + }, + ); this.api = new API({ - permanentURLs: this.config.permanentURLs, secret: this.config.server.secret, - exporter: this.context.exporter, + exporter: this._exporter, + permanentURLs: this.config.permanentURLs, }); API.setStorage(HookCompartment); - this.loaded = false; + } + + /** + * _collectRegisteredHooks - Collects registered hooks data for the Prometheus + * exporter. + * @private + */ + _collectRegisteredHooks () { + try { + const hooks = HookCompartment.get().getAll(); + this._exporter.agent.reset([METRIC_NAMES.REGISTERED_HOOKS]); + hooks.forEach(hook => { + this._exporter.agent.set(METRIC_NAMES.REGISTERED_HOOKS, 1, { + callbackURL: hook.payload.callbackURL, + permanent: hook.payload.permanent, + getRaw: hook.payload.getRaw, + // FIXME enabled is hardecoded until enabled/disabled logic is implemented + enabled: true, + }); + }); + } catch (error) { + this.logger.error('Prometheus failed to collect registered hooks', { error: error.stack }); + } + } + + /** + * _bootstrapExporter - Injects the module's metrics into the Prometheus + * exporter. + * This method is called in the constructor. + * @private + */ + _bootstrapExporter () { + this._exporter.injectMetrics(buildMetrics(this._exporter)); + this._exporter.agent.setCollector( + METRIC_NAMES.REGISTERED_HOOKS, + this._collectRegisteredHooks.bind(this) + ); } /** @@ -63,7 +110,7 @@ class OutWebHooks { */ async load () { await this.api.start(this.config.api.port, this.config.api.bind); - await this.api.createPermanents(); + await this.webhooks.createPermanentHooks(); this.loaded = true; } @@ -75,8 +122,8 @@ class OutWebHooks { * @returns {Promise} */ async unload () { - if (this.webHooks) { - this.webHooks = null; + if (this.webhooks) { + this.webhooks = null; } this.setCollector(OutWebHooks._defaultCollector); @@ -105,11 +152,11 @@ class OutWebHooks { * @returns {Promise} */ async onEvent (event, raw) { - if (!this.loaded || !this.webHooks) { + if (!this.loaded || !this.webhooks) { throw new Error("OutWebHooks not loaded"); } - return this.webHooks.onEvent(event, raw); + return this.webhooks.onEvent(event, raw); } } diff --git a/src/out/webhooks/metrics.js b/src/out/webhooks/metrics.js new file mode 100644 index 0000000..dd6d4cd --- /dev/null +++ b/src/out/webhooks/metrics.js @@ -0,0 +1,45 @@ +const METRICS_PREFIX = 'bbb_webhooks_out_hooks'; +const METRIC_NAMES = { + API_REQUESTS: `${METRICS_PREFIX}_api_requests`, + REGISTERED_HOOKS: `${METRICS_PREFIX}_registered_hooks`, + HOOK_FAILURES: `${METRICS_PREFIX}_hook_failures`, + PROCESSED_EVENTS: `${METRICS_PREFIX}_processed_events`, +} + +/** + * buildMetrics - Builds out-webhooks metrics for the Prometheus exporter. + * @param {MetricsExporter} exporter - The Prometheus exporter. + * @returns {object} - Metrics dictionary (key: metric name, value: prom-client metric object) + */ +const buildMetrics = ({ Counter, Gauge }) => { + return { + [METRIC_NAMES.API_REQUESTS]: new Counter({ + name: METRIC_NAMES.API_REQUESTS, + help: 'Webhooks API requests', + labelNames: ['method', 'path', 'statusCode', 'returncode', 'messageKey'], + }), + + [METRIC_NAMES.REGISTERED_HOOKS]: new Gauge({ + name: METRIC_NAMES.REGISTERED_HOOKS, + help: 'Registered hooks', + labelNames: ['callbackURL', 'permanent', 'getRaw', 'enabled'], + }), + + [METRIC_NAMES.HOOK_FAILURES]: new Counter({ + name: METRIC_NAMES.HOOK_FAILURES, + help: 'Hook failures', + labelNames: ['callbackURL', 'reason', 'eventId'], + }), + + [METRIC_NAMES.PROCESSED_EVENTS]: new Counter({ + name: METRIC_NAMES.PROCESSED_EVENTS, + help: 'Processed events', + labelNames: ['eventId', 'callbackURL'], + }), + } +}; + +export { + METRIC_NAMES, + buildMetrics, +}; diff --git a/src/out/webhooks/web-hooks.js b/src/out/webhooks/web-hooks.js index 5734d98..a0ef41c 100644 --- a/src/out/webhooks/web-hooks.js +++ b/src/out/webhooks/web-hooks.js @@ -1,5 +1,6 @@ import CallbackEmitter from './callback-emitter.js'; import HookCompartment from '../../db/redis/hooks.js'; +import { METRIC_NAMES } from './metrics.js'; /** * WebHooks. @@ -14,11 +15,20 @@ class WebHooks { * constructor. * @param {Context} context - This module's context as provided by the main application. * @param {object} config - This module's configuration object. + * @param {object} options - Options. + * @param {MetricsExporter} options.exporter - The exporter. + * @param {Array} options.permanentURLs - An array of permanent webhook URLs to be registered. */ - constructor(context, config) { + constructor(context, config, { + exporter = {}, + permanentURLs = [], + } = {}) { this.context = context; this.logger = context.getLogger(); this.config = config; + + this._exporter = exporter; + this._permanentURLs = permanentURLs; } /** @@ -64,6 +74,47 @@ class WebHooks { return message?.data?.attributes?.meeting["external-meeting-id"]; } + /** + * _isHookPermanent - Check if a hook is permanent. + * @param {string} callbackURL - The callback URL of the hook. + * @returns {boolean} - Whether the hook is permanent or not. + * @private + */ + _isHookPermanent(callbackURL) { + return this._permanentURLs.some(obj => { + return obj.url === callbackURL + }); + } + + /** + * createPermanentHooks - Create permanent hooks. + * @returns {Promise} - A promise that resolves when all permanent hooks have been created. + * @public + * @async + */ + async createPermanentHooks() { + for (let i = 0; i < this._permanentURLs.length; i++) { + try { + const { url: callbackURL, getRaw } = this._permanentURLs[i]; + const { hook, duplicated } = await HookCompartment.get().addSubscription({ + callbackURL, + permanent: this._isHookPermanent(callbackURL), + getRaw, + }); + + if (duplicated) { + this.logger.info(`permanent hook already set ${hook.id}`, { hook: hook.payload }); + } else if (hook != null) { + this.logger.info('permanent hook created successfully'); + } else { + this.logger.error('error creating permanent hook'); + } + } catch (error) { + this.logger.error(`error creating permanent hook ${error}`); + } + } + } + /** * dispatch - Dispatch an event to the target hook. * @param {object} event - The event to be dispatched (raw or mapped) @@ -106,11 +157,29 @@ class WebHooks { emitter.on(CallbackEmitter.EVENTS.SUCCESS, () => { this.logger.info(`successfully dispatched to ${hook.payload.callbackURL}`); emitter.stop(); + this._exporter.agent.increment(METRIC_NAMES.PROCESSED_EVENTS, { + callbackURL: hook.payload.callbackURL, + eventId: event.data.id, + }); return resolve(); }); + + emitter.on(CallbackEmitter.EVENTS.FAILED, (error) => { + this._exporter.agent.increment(METRIC_NAMES.HOOK_FAILURES, { + callbackURL: hook.payload.callbackURL, + reason: error.message, + eventId: event.data.id, + }); + }); + emitter.once(CallbackEmitter.EVENTS.STOPPED, () => { this.logger.warn(`too many failed attempts to perform a callback call, removing the hook for: ${hook.payload.callbackURL}`); emitter.stop(); + this._exporter.agent.increment(METRIC_NAMES.HOOK_FAILURES, { + callbackURL: hook.payload.callbackURL, + reason: 'too many failed attempts', + eventId: event.data.id, + }); // TODO just disable return hook.destroy().then(resolve).catch(reject); }); diff --git a/src/process/event-processor.js b/src/process/event-processor.js index 82b127f..4e88754 100644 --- a/src/process/event-processor.js +++ b/src/process/event-processor.js @@ -1,4 +1,3 @@ -import config from 'config'; import IDMapping from '../db/redis/id-mapping.js'; import { newLogger } from '../common/logger.js'; import WebhooksEvent from '../process/event.js'; @@ -20,7 +19,7 @@ export default class EventProcessor { this.inputs = inputs; this.outputs = outputs; - this._exporter = Metrics.getExporter(); + this._exporter = Metrics.agent; } _trackModuleEvents() { From e44c90c3682e5454b3c4b63b9cbbc9d6ed7d6bf7 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Wed, 11 Oct 2023 15:58:16 -0300 Subject: [PATCH 055/154] feat(webhooks): add support for SHA256|384|512, + - Add support for SHA256|384|512 when validating API calls checksums * See `modules."../out/webhooks/index.js".config.api.supportedChecksumAlgorithms` - Add support for specifying which hashing algorithm should be used when generating the checksum for event callbacks (POST, hooks) * See `modules."../out/webhooks/index.js".config.hookChecksumAlgorithm` - fix(webhooks): exception when generating processed event metrics - refactor: move specific util methods from the main application to the webhhooks module --- config/custom-environment-variables.yml | 2 + config/default.example.yml | 12 ++ src/common/utils.js | 92 ------------- src/out/webhooks/api/api.js | 9 +- src/out/webhooks/callback-emitter.js | 11 +- src/out/webhooks/index.js | 1 + src/out/webhooks/utils.js | 171 ++++++++++++++++++++++++ src/out/webhooks/web-hooks.js | 18 ++- test/test.js | 26 +++- 9 files changed, 231 insertions(+), 111 deletions(-) create mode 100644 src/out/webhooks/utils.js diff --git a/config/custom-environment-variables.yml b/config/custom-environment-variables.yml index 83d905f..584d409 100644 --- a/config/custom-environment-variables.yml +++ b/config/custom-environment-variables.yml @@ -40,6 +40,8 @@ modules: api: bind: SERVER_BIND_IP port: SERVER_PORT + supportedChecksumAlgorithms: SUPPORTED_CHECKSUM_ALGORITHMS + hookChecksumAlgorithm: HOOK_CHECKSUM_ALGORITHM queue: enabled: ENABLE_WH_QUEUE permanentURLs: diff --git a/config/default.example.yml b/config/default.example.yml index a58696e..f9a1db8 100644 --- a/config/default.example.yml +++ b/config/default.example.yml @@ -68,10 +68,22 @@ modules: api: bind: 127.0.0.1 port: 3005 + # Supported checksum generation algorithms for BBB API calls + # This should mirror the supportedChecksumAlgorithms configuration + # in bbb-web.properties + supportedChecksumAlgorithms: + - sha1 + - sha256 + - sha384 + - sha512 queue: enabled: false maxSize: 10000 concurrency: 1 + # Defines the algorithm to be used when generating checksums for + # callback POST requests. One of: sha1, sha256, sha384 or sha512 + # Default: sha1 + hookChecksumAlgorithm: sha1 # IP where permanent hook will post data (more than 1 URL means more than 1 permanent hook) permanentURLs: [] # How many messages will be enqueued to be processed at the same time diff --git a/src/common/utils.js b/src/common/utils.js index 7de4afa..63df371 100644 --- a/src/common/utils.js +++ b/src/common/utils.js @@ -1,98 +1,6 @@ -import sha1 from "sha1"; -import url from "url"; -import config from "config"; - -// Calculates the checksum given a url `fullUrl` and a `salt`, as calculate by bbb-web. -const checksumAPI = function(fullUrl, salt) { - const query = queryFromUrl(fullUrl); - const method = methodFromUrl(fullUrl); - return checksum(method + query + salt); -}; - -// Calculates the checksum for a string. -// Just a wrapper for the method that actually does it. -const checksum = string => sha1(string); - -// Get the query of an API call from the url object (from url.parse()) -// Example: -// -// * `fullUrl` = `http://bigbluebutton.org/bigbluebutton/api/create?name=Demo+Meeting&meetingID=Demo` -// * returns: `name=Demo+Meeting&meetingID=Demo` -const queryFromUrl = function(fullUrl) { - - // Returns the query without the checksum. - // We can't use url.parse() because it would change the encoding - // and the checksum wouldn't match. We need the url exactly as - // the client sent us. - let query = fullUrl.replace(/&checksum=[^&]*/, ''); - query = query.replace(/checksum=[^&]*&/, ''); - query = query.replace(/checksum=[^&]*$/, ''); - const matched = query.match(/\?(.*)/); - if (matched != null) { - return matched[1]; - } else { - return ''; - } -}; - -// Get the method name of an API call from the url object (from url.parse()) -// Example: -// -// * `fullUrl` = `http://mconf.org/bigbluebutton/api/create?name=Demo+Meeting&meetingID=Demo` -// * returns: `create` -const methodFromUrl = function(fullUrl) { - const urlObj = url.parse(fullUrl, true); - return urlObj.pathname.substr((config.get("bbb.apiPath") + "/").length); -}; - -// Returns the IP address of the client that made a request `req`. -// If can not determine the IP, returns `127.0.0.1`. -const ipFromRequest = function(req) { - - // the first ip in the list if the ip of the client - // the others are proxys between him and us - let ipAddress; - if ((req.headers != null ? req.headers["x-forwarded-for"] : undefined) != null) { - let ips = req.headers["x-forwarded-for"].split(","); - ipAddress = ips[0] != null ? ips[0].trim() : undefined; - } - - // fallbacks - if (!ipAddress) { ipAddress = req.headers != null ? req.headers["x-real-ip"] : undefined; } // when behind nginx - if (!ipAddress) { ipAddress = req.connection != null ? req.connection.remoteAddress : undefined; } - if (!ipAddress) { ipAddress = "127.0.0.1"; } - return ipAddress; -}; - const isEmpty = (obj) => [Object, Array].includes((obj || {}).constructor) && !Object.entries((obj || {})).length; -const sortBy = (key) => (a, b) => { - if (a[key] > b[key]) return 1; - if (a[key] < b[key]) return -1; - return 0; -}; - -const stringify = (obj) => { - if (obj == null) return obj; - - switch (typeof obj) { - case "string": - return obj; - case "object": - return JSON.stringify(obj); - default: - return obj.toString(); - } -} - export default { - checksumAPI, - checksum, - queryFromUrl, - methodFromUrl, - ipFromRequest, isEmpty, - sortBy, - stringify, }; diff --git a/src/out/webhooks/api/api.js b/src/out/webhooks/api/api.js index 80f6f62..0682d2b 100644 --- a/src/out/webhooks/api/api.js +++ b/src/out/webhooks/api/api.js @@ -1,7 +1,7 @@ import express from 'express'; import url from 'url'; import { newLogger } from '../../../common/logger.js'; -import Utils from '../../../common/utils.js'; +import Utils from '../utils.js'; import responses from './responses.js'; import { METRIC_NAMES } from '../metrics.js'; @@ -32,6 +32,7 @@ export default class API { this._permanentURLs = options.permanentURLs || []; this._secret = options.secret; this._exporter = options.exporter; + this._supportedChecksumAlgorithms = options.supportedChecksumAlgorithms; this._validateChecksum = this._validateChecksum.bind(this); @@ -219,12 +220,10 @@ export default class API { // If it doesn't match BigBlueButton's shared secret, will send an XML response // with an error code just like BBB does. _validateChecksum(req, res, next) { - const urlObj = url.parse(req.url, true); - const checksum = urlObj.query["checksum"]; - - if (checksum === Utils.checksumAPI(req.url, this._secret)) { + if (Utils.isUrlChecksumValid(req.url, this._secret, this._supportedChecksumAlgorithms)) { next(); } else { + const urlObj = url.parse(req.url, true); API.logger.info('checksum check failed, sending a checksumError response', responses.checksumError); API.respondWithXML(res, responses.checksumError); this._exporter.agent.increment(METRIC_NAMES.API_REQUEST_FAILURES_XML, { diff --git a/src/out/webhooks/callback-emitter.js b/src/out/webhooks/callback-emitter.js index 1419a94..186320f 100644 --- a/src/out/webhooks/callback-emitter.js +++ b/src/out/webhooks/callback-emitter.js @@ -1,6 +1,6 @@ import url from 'url'; import { EventEmitter } from 'node:events'; -import Utils from '../../common/utils.js'; +import Utils from './utils.js'; import fetch from 'node-fetch'; // A simple string that identifies the event @@ -41,6 +41,7 @@ export default class CallbackEmitter extends EventEmitter { retryIntervals, permanentIntervalReset, logger = console, + checksumAlgorithm, } = {}, ) { super(); @@ -73,6 +74,7 @@ export default class CallbackEmitter extends EventEmitter { 10000, 30000, ]; + this._checksumAlgorithm = checksumAlgorithm; } _scheduleNext(timeout) { @@ -88,7 +90,7 @@ export default class CallbackEmitter extends EventEmitter { const interval = this._retryIntervals[this.nextInterval]; if (interval != null) { - this.logger.warn(`trying the callback again in ${interval/1000.0} secs: ${this.callbackURL}`); + this.logger.warn(`trying the callback again in ${interval/1000.0} secs: ${this.callbackURL}`, error); this.nextInterval++; this._scheduleNext(interval); // no intervals anymore, time to give up @@ -134,7 +136,10 @@ export default class CallbackEmitter extends EventEmitter { Authorization: `Bearer ${sharedSecret}`, }; } else { - const checksum = Utils.checksum(`${this.callbackURL}${JSON.stringify(data)}${sharedSecret}`); + const checksum = Utils.shaHex( + `${this.callbackURL}${JSON.stringify(data)}${sharedSecret}`, + this._checksumAlgorithm, + ); // get the final callback URL, including the checksum callbackURL = this.callbackURL; try { diff --git a/src/out/webhooks/index.js b/src/out/webhooks/index.js index 7775069..4cc7208 100644 --- a/src/out/webhooks/index.js +++ b/src/out/webhooks/index.js @@ -61,6 +61,7 @@ class OutWebHooks { secret: this.config.server.secret, exporter: this._exporter, permanentURLs: this.config.permanentURLs, + supportedChecksumAlgorithms: this.config.api.supportedChecksumAlgorithms, }); API.setStorage(HookCompartment); } diff --git a/src/out/webhooks/utils.js b/src/out/webhooks/utils.js new file mode 100644 index 0000000..5d058ed --- /dev/null +++ b/src/out/webhooks/utils.js @@ -0,0 +1,171 @@ +import url from "url"; +import config from "config"; +import crypto from "crypto"; + +/** + * queryFromUrl - Returns the query string from a URL string while preserving + * encoding. + * @param {string} fullUrl - The URL to extract the query string from. + * @returns {string} - The query string. + * @private + */ +const queryFromUrl = (fullUrl) => { + let query = fullUrl.replace(/&checksum=[^&]*/, ''); + query = query.replace(/checksum=[^&]*&/, ''); + query = query.replace(/checksum=[^&]*$/, ''); + const matched = query.match(/\?(.*)/); + if (matched != null) { + return matched[1]; + } else { + return ''; + } +}; + +/** + * methodFromUrl - Returns the API method string from a URL. + * @param {string} fullUrl - The URL to extract the API method string from. + * @returns {string} - The API method string. + * @private + */ +const methodFromUrl = (fullUrl) => { + const urlObj = url.parse(fullUrl, true); + return urlObj.pathname.substr((config.get("bbb.apiPath") + "/").length); +}; + +/** + * isChecksumAlgorithmSupported - Checks if a checksum algorithm is supported. + * @param {string} algorithm - The algorithm to check. + * @param {string} supported - The list of supported algorithms. + * @returns {boolean} - Whether the algorithm is supported or not. + * @private + */ +const isChecksumAlgorithmSupported = (algorithm, supported) => { + if (supported == null || supported.length === 0) return false; + return supported.indexOf(algorithm) !== -1; +}; + +/** + * getChecksumAlgorithmFromLength - Returns the checksum algorithm that matches a checksum length. + * @param {number} length - The length of the checksum. + * @returns {string} - The checksum algorithm (one of sha1, sha256, sha384, sha512). + * @private + * @throws {Error} - If no algorithm could be found that matches the provided checksum length. + */ +const getChecksumAlgorithmFromLength = (length) => { + switch (length) { + case 40: + return "sha1"; + case 64: + return "sha256"; + case 96: + return "sha384"; + case 128: + return "sha512"; + default: + throw new Error(`No algorithm could be found that matches the provided checksum length: ${length}`); + } +}; + +/* + * Public + */ + +/** + * ipFromRequest - Returns the IP address of the client that made a request `req`. + * If can not determine the IP, returns `127.0.0.1`. + * @param {object} req - The request object. + * @returns {string} - The IP address of the client. + * @public + */ +const ipFromRequest = (req) => { + // the first ip in the list if the ip of the client + // the others are proxys between him and us + let ipAddress; + if ((req.headers != null ? req.headers["x-forwarded-for"] : undefined) != null) { + let ips = req.headers["x-forwarded-for"].split(","); + ipAddress = ips[0] != null ? ips[0].trim() : undefined; + } + + // fallbacks + if (!ipAddress) { ipAddress = req.headers != null ? req.headers["x-real-ip"] : undefined; } // when behind nginx + if (!ipAddress) { ipAddress = req.connection != null ? req.connection.remoteAddress : undefined; } + if (!ipAddress) { ipAddress = "127.0.0.1"; } + return ipAddress; +}; + +/** + * shaHex - Calculates the SHA hash of a string. + * @param {string} data - The string to calculate the hash for. + * @param {string} algorithm - Hashing algorithm to use (sha1, sha256, sha384, sha512). + * @returns {string} - The hash of the string. + * @public + */ +const shaHex = (data, algorithm) => { + return crypto.createHash(algorithm).update(data).digest("hex"); +}; + +/** + * checksumAPI - Calculates the checksum of a URL using a secret and a hashing algorithm. + * @param {string} fullUrl - The URL to calculate the checksum for. + * @param {string} salt - The secret to use for the checksum. + * @param {string} algorithm - The hashing algorithm to use (sha1, sha256, sha384, sha512). + * @returns {string} - The checksum of the URL. + * @public + */ +const checksumAPI = (fullUrl, salt, algorithm) => { + const query = queryFromUrl(fullUrl); + const method = methodFromUrl(fullUrl); + + return shaHex(method + query + salt, algorithm); +}; + +/** + * isUrlChecksumValid - Checks if the checksum of a URL is valid against a secret + * and a hashing algorithm. + * @param {string} urlStr - The URL to check. + * @param {string} secret - The secret to use for the checksum. + * @param {Array} supportedAlgorithms - The list of supported algorithms. + * @returns {boolean} - Whether the checksum is valid or not. + * @public + */ +const isUrlChecksumValid = (urlStr, secret, supportedAlgorithms) => { + const urlObj = url.parse(urlStr, true); + const checksum = urlObj.query["checksum"]; + const algorithm = getChecksumAlgorithmFromLength(checksum.length); + + if (!isChecksumAlgorithmSupported(algorithm, supportedAlgorithms)) { + return false; + } + + return checksum === checksumAPI(urlStr, secret, algorithm, supportedAlgorithms); +}; + +/** + * isEmpty - Checks if an arbitrary value classifies as an empty array or object. + * @param {*} obj - The value to check. + * @returns {boolean} - Whether the value is empty or not. + * @public + */ +const isEmpty = (obj) => [Object, Array].includes((obj || {}).constructor) + && !Object.entries((obj || {})).length; + +/** + * sortBy - Sorts an array of objects by a key. + * @param {string|number} key - The key to sort by. + * @returns {function} - A function that can be used to sort an array of objects by the given key. + * @public + */ +const sortBy = (key) => (a, b) => { + if (a[key] > b[key]) return 1; + if (a[key] < b[key]) return -1; + return 0; +}; + +export default { + ipFromRequest, + shaHex, + checksumAPI, + isUrlChecksumValid, + isEmpty, + sortBy, +}; diff --git a/src/out/webhooks/web-hooks.js b/src/out/webhooks/web-hooks.js index a0ef41c..c8b13d3 100644 --- a/src/out/webhooks/web-hooks.js +++ b/src/out/webhooks/web-hooks.js @@ -131,12 +131,17 @@ class WebHooks { return new Promise((resolve, reject) => { // Check for an invalid event - skip if that's the case if (event == null) return; + const mappedEventId = event?.data?.id; + const eventId = mappedEventId + || event?.envelope?.name + || 'unknownEvent'; + // CHeck if the event is in the list of events to be sent (if list was specified) if (hook.payload.eventID != null - && (event?.data?.id == null - || (!hook.payload.eventID.some((ev) => ev == event.data.id.toLowerCase()))) + && (mappedEventId == null + || (!hook.payload.eventID.some((ev) => ev == mappedEventId.toLowerCase()))) ) { - this.logger.info(`${hook.payload.callbackURL} skipping event because not in event list`, { eventID: event.data.id }); + this.logger.info(`${hook.payload.callbackURL} skipping event because not in event list`, { eventID: eventId }); return; } @@ -150,6 +155,7 @@ class WebHooks { auth2_0: this.config.server.auth2_0, requestTimeout: this.config.requestTimeout, retryIntervals: this.config.retryIntervals, + checksumAlgorithm: this.config.hookChecksumAlgorithm, } ); @@ -159,7 +165,7 @@ class WebHooks { emitter.stop(); this._exporter.agent.increment(METRIC_NAMES.PROCESSED_EVENTS, { callbackURL: hook.payload.callbackURL, - eventId: event.data.id, + eventId, }); return resolve(); }); @@ -168,7 +174,7 @@ class WebHooks { this._exporter.agent.increment(METRIC_NAMES.HOOK_FAILURES, { callbackURL: hook.payload.callbackURL, reason: error.message, - eventId: event.data.id, + eventId, }); }); @@ -178,7 +184,7 @@ class WebHooks { this._exporter.agent.increment(METRIC_NAMES.HOOK_FAILURES, { callbackURL: hook.payload.callbackURL, reason: 'too many failed attempts', - eventId: event.data.id, + eventId, }); // TODO just disable return hook.destroy().then(resolve).catch(reject); diff --git a/test/test.js b/test/test.js index b6e7116..7b9167c 100644 --- a/test/test.js +++ b/test/test.js @@ -1,7 +1,7 @@ import { describe, it, before, after, beforeEach } from 'mocha'; import request from 'supertest'; import nock from "nock"; -import utils from '../src/common/utils.js'; +import Utils from '../src/out/webhooks/utils.js'; import config from 'config'; import Hook from '../src/db/redis/hooks.js'; import Helpers from './helpers.js' @@ -13,6 +13,7 @@ const SHARED_SECRET = process.env.SHARED_SECRET || function () { throw new Error const MODULES = config.get('modules'); const WH_CONFIG = MODULES['../out/webhooks/index.js'].config; const IN_REDIS_CONFIG = MODULES['../in/redis/index.js'].config.redis; +const CHECKSUM_ALGORITHM = 'sha1'; describe('bbb-webhooks tests', () => { const application = new Application(); @@ -50,7 +51,10 @@ describe('bbb-webhooks tests', () => { describe('GET /hooks/list permanent', () => { it('should list permanent hook', (done) => { - let getUrl = utils.checksumAPI(Helpers.url + Helpers.listUrl, SHARED_SECRET); + let getUrl = Utils.checksumAPI( + Helpers.url + Helpers.listUrl, + SHARED_SECRET, CHECKSUM_ALGORITHM + ); getUrl = Helpers.listUrl + '?checksum=' + getUrl request(Helpers.url) @@ -78,7 +82,11 @@ describe('bbb-webhooks tests', () => { it('should destroy a hook', (done) => { const hooks = Hook.get().getAllGlobalHooks(); const hook = hooks[hooks.length-1].id; - let getUrl = utils.checksumAPI(Helpers.url + Helpers.destroyUrl(hook), SHARED_SECRET); + let getUrl = Utils.checksumAPI( + Helpers.url + Helpers.destroyUrl(hook), + SHARED_SECRET, + CHECKSUM_ALGORITHM, + ); getUrl = Helpers.destroyUrl(hook) + '&checksum=' + getUrl request(Helpers.url) @@ -93,7 +101,11 @@ describe('bbb-webhooks tests', () => { describe('GET /hooks/destroy permanent hook', () => { it('should not destroy the permanent hook', (done) => { - let getUrl = utils.checksumAPI(Helpers.url + Helpers.destroyPermanent, SHARED_SECRET); + let getUrl = Utils.checksumAPI( + Helpers.url + Helpers.destroyPermanent, + SHARED_SECRET, + CHECKSUM_ALGORITHM, + ); getUrl = Helpers.destroyPermanent + '&checksum=' + getUrl request(Helpers.url) .get(getUrl) @@ -118,7 +130,11 @@ describe('bbb-webhooks tests', () => { .catch(done); }); it('should create a hook with getRaw=true', (done) => { - let getUrl = utils.checksumAPI(Helpers.url + Helpers.createUrl + Helpers.createRaw, SHARED_SECRET); + let getUrl = Utils.checksumAPI( + Helpers.url + Helpers.createUrl + Helpers.createRaw, + SHARED_SECRET, + CHECKSUM_ALGORITHM, + ); getUrl = Helpers.createUrl + '&checksum=' + getUrl + Helpers.createRaw request(Helpers.url) From 68caf5dfaba987d86270753d551fb75aabb42726 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Wed, 11 Oct 2023 16:06:13 -0300 Subject: [PATCH 056/154] refactor: remove sha1 as a dependency Use native crypto module instead --- extra/interceptor.js | 4 ++-- package-lock.json | 29 ----------------------------- package.json | 1 - 3 files changed, 2 insertions(+), 32 deletions(-) diff --git a/extra/interceptor.js b/extra/interceptor.js index 335957e..8575f3b 100644 --- a/extra/interceptor.js +++ b/extra/interceptor.js @@ -5,7 +5,7 @@ // events, but only the first time they happen. import express from "express"; import fetch from "node-fetch"; -import sha1 from "sha1"; +import crypto from "crypto"; import bodyParser from 'body-parser'; // server configs @@ -62,7 +62,7 @@ const myUrl = "http://" + catcherDomain + ":" + port + "/callback"; let params = "callbackURL=" + encodeForUrl(myUrl) + "&getRaw=" + GET_RAW; if (EVENT_ID) params += "&eventID=" + EVENT_ID; if (MEETING_ID) params += "&meetingID=" + MEETING_ID; -const checksum = sha1("hooks/create" + params + sharedSecret); +const checksum = crypto.createHash('sha1').update("hooks/create" + params + sharedSecret).digest('hex'); const fullUrl = "http://" + bbbDomain + "/bigbluebutton/api/hooks/create?" + params + "&checksum=" + checksum console.log("Registering a hook with", fullUrl); diff --git a/package-lock.json b/package-lock.json index 3258454..7c773e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,6 @@ "node-fetch": "^3.3.2", "prom-client": "^14.2.0", "redis": "^4.6.8", - "sha1": "^1.1.1", "uuid": "^9.0.1", "winston": "^3.10.0" }, @@ -900,14 +899,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/charenc": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", - "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", - "engines": { - "node": "*" - } - }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -1136,14 +1127,6 @@ "node": ">= 8" } }, - "node_modules/crypt": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", - "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", - "engines": { - "node": "*" - } - }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -4308,18 +4291,6 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, - "node_modules/sha1": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/sha1/-/sha1-1.1.1.tgz", - "integrity": "sha512-dZBS6OrMjtgVkopB1Gmo4RQCDKiZsqcpAQpkV/aaj+FCrCg8r4I4qMkDPQjBgLIxlmu9k4nUbWq6ohXahOneYA==", - "dependencies": { - "charenc": ">= 0.0.1", - "crypt": ">= 0.0.1" - }, - "engines": { - "node": "*" - } - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/package.json b/package.json index b7fd0a1..fef56df 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,6 @@ "node-fetch": "^3.3.2", "prom-client": "^14.2.0", "redis": "^4.6.8", - "sha1": "^1.1.1", "uuid": "^9.0.1", "winston": "^3.10.0" }, From 0bb41d4dc8e4c66c9761a3a7760aabfe779bc25c Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Mon, 16 Oct 2023 13:24:15 -0300 Subject: [PATCH 057/154] fix: handle hookID as hook index when resyncing Backwards-compatibility with hooks registered in bbb-webhooks <= 2.x --- src/db/redis/hooks.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/db/redis/hooks.js b/src/db/redis/hooks.js index 1cbd370..0913fb9 100644 --- a/src/db/redis/hooks.js +++ b/src/db/redis/hooks.js @@ -22,7 +22,9 @@ import { StorageCompartmentKV } from './base-storage.js'; // and send up to 10 events in every post class HookCompartment extends StorageCompartmentKV { static itemDeserializer(data) { - const { id, ...payload } = data; + // hookID destructuring is here for backwards compatibility with previous + // versions of the hooks app. + const { id: rID, hookID, ...payload } = data; const parsedPayload = { callbackURL: payload.callbackURL, externalMeetingID: payload.externalMeetingID, @@ -35,7 +37,7 @@ class HookCompartment extends StorageCompartmentKV { }; return { - id, + id: rID || hookID, ...parsedPayload, }; } From 3d714d27cf4c4b1b32c043b9d4ca666fa18c35af Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Mon, 16 Oct 2023 13:24:57 -0300 Subject: [PATCH 058/154] chore: add cmd-line util to list hooks via API --- extra/list-hooks.js | 66 +++++++++++++++++++++++++++++++++++++++++++++ package-lock.json | 29 ++++++++++++++++++++ package.json | 1 + 3 files changed, 96 insertions(+) create mode 100644 extra/list-hooks.js diff --git a/extra/list-hooks.js b/extra/list-hooks.js new file mode 100644 index 0000000..70ac887 --- /dev/null +++ b/extra/list-hooks.js @@ -0,0 +1,66 @@ +/* eslint no-console: "off" */ + +import fetch from "node-fetch"; +import crypto from "crypto"; +import { XMLParser } from "fast-xml-parser"; + +const eject = (reason) => { throw new Error(reason); } +const sharedSecret = process.env.SHARED_SECRET || eject("SHARED_SECRET not set"); +const bbbDomain = process.env.BBB_DOMAIN || eject("BBB_DOMAIN not set"); +const MEETING_ID = process.env.MEETING_ID || ''; +const parser = new XMLParser(); + +const shutdown = (code) => { + console.log(`Shutting down, code ${code}`); + process.exit(code); +} + +// registers a hook on the webhooks app +let params = ""; +if (MEETING_ID) params += "&meetingID=" + MEETING_ID; +const checksum = crypto.createHash('sha1').update("hooks/list" + params + sharedSecret).digest('hex'); +const fullUrl = "http://" + bbbDomain + "/bigbluebutton/api/hooks/list?" + + params + "&checksum=" + checksum +console.log("Registering a hook with", fullUrl); + +const listHooks = async () => { + const controller = new AbortController(); + const abortTimeout = setTimeout(() => { + controller.abort(); + }, 2500); + + try { + const response = await fetch(fullUrl, { signal: controller.signal }); + const text = await response.text(); + if (response.ok) { + const hooksObj = parser.parse(text); + const hooks = hooksObj.response?.hooks?.hook || []; + const length = !Array.isArray(hooks) ? (Object.keys(hooks).length > 0 | 0): hooks.length; + + console.debug("Registered hooks (hooks/list):", { + hooks, + returncode: hooksObj.response.returncode, + numberOfHooks: length, + }); + } else { + throw new Error(text); + } + } catch (error) { + console.error("Hooks/list failed", error); + } finally { + clearTimeout(abortTimeout); + } +}; + +process.on('SIGINT', () => shutdown(0)); +process.on('SIGTERM', () => shutdown(0)); +process.on('uncaughtException', (error) => { + console.error('uncaughtException:', error); + shutdown(1); +}); +process.on('unhandledRejection', (reason, promise) => { + console.error('unhandledRejection:', reason, promise); + shutdown(1); +}); + +listHooks(); diff --git a/package-lock.json b/package-lock.json index 7c773e1..4030d81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "eslint": "^8.49.0", "eslint-plugin-import": "^2.28.1", "eslint-plugin-jsdoc": "^46.8.2", + "fast-xml-parser": "^4.3.2", "jsdoc": "^4.0.2", "mocha": "^9.2.2", "nock": "^13.3.3", @@ -1844,6 +1845,28 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-xml-parser": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.3.2.tgz", + "integrity": "sha512-rmrXUXwbJedoXkStenj1kkljNF7ugn5ZjR9FJcwmCfcCbtOMDghPajbc+Tck6vE6F5XsDmx+Pr2le9fw8+pXBg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastq": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", @@ -4511,6 +4534,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", + "dev": true + }, "node_modules/superagent": { "version": "3.8.3", "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", diff --git a/package.json b/package.json index fef56df..c50f71c 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "eslint": "^8.49.0", "eslint-plugin-import": "^2.28.1", "eslint-plugin-jsdoc": "^46.8.2", + "fast-xml-parser": "^4.3.2", "jsdoc": "^4.0.2", "mocha": "^9.2.2", "nock": "^13.3.3", From 26e02f9bbef0a710aedfd9c25efe5779aff34591 Mon Sep 17 00:00:00 2001 From: miguel-mconf Date: Mon, 16 Oct 2023 16:09:24 -0300 Subject: [PATCH 059/154] Fix mic not enable check in xAPI module --- src/out/xapi/templates.js | 12 +- src/out/xapi/xapi.js | 308 +++++++++++++++++++++----------------- 2 files changed, 182 insertions(+), 138 deletions(-) diff --git a/src/out/xapi/templates.js b/src/out/xapi/templates.js index 6ff4d61..8127246 100644 --- a/src/out/xapi/templates.js +++ b/src/out/xapi/templates.js @@ -20,6 +20,8 @@ export default function getXAPIStatement(event, meeting_data, user_data = null, || event.data.id == 'user-left' || event.data.id == 'user-audio-voice-enabled' || event.data.id == 'user-audio-voice-disabled' + || event.data.id == "user-audio-muted" + || event.data.id == "user-audio-unmuted" || event.data.id == 'user-cam-broadcast-start' || event.data.id == 'user-cam-broadcast-end' || event.data.id == 'meeting-screenshare-started' @@ -34,6 +36,8 @@ export default function getXAPIStatement(event, meeting_data, user_data = null, 'user-left': 'http://activitystrea.ms/leave', 'user-audio-voice-enabled': 'http://adlnet.gov/expapi/verbs/interacted', 'user-audio-voice-disabled': 'http://adlnet.gov/expapi/verbs/interacted', + 'user-audio-muted': 'http://adlnet.gov/expapi/verbs/interacted', + 'user-audio-unmuted': 'http://adlnet.gov/expapi/verbs/interacted', 'user-cam-broadcast-start': 'http://adlnet.gov/expapi/verbs/interacted', 'user-cam-broadcast-end': 'http://adlnet.gov/expapi/verbs/interacted', 'meeting-screenshare-started': 'http://adlnet.gov/expapi/verbs/interacted', @@ -99,6 +103,8 @@ export default function getXAPIStatement(event, meeting_data, user_data = null, // Custom attributes for multiple interactions else if (event.data.id == 'user-audio-voice-enabled' || event.data.id == 'user-audio-voice-disabled' + || event.data.id == "user-audio-muted" + || event.data.id == "user-audio-unmuted" || event.data.id == 'user-cam-broadcast-start' || event.data.id == 'user-cam-broadcast-end' || event.data.id == 'meeting-screenshare-started' @@ -107,6 +113,8 @@ export default function getXAPIStatement(event, meeting_data, user_data = null, const extension = { "user-audio-voice-enabled": "micro-activated", "user-audio-voice-disabled": "micro-activated", + "user-audio-muted": "micro-activated", + "user-audio-unmuted": "micro-activated", "user-cam-broadcast-start": "camera-activated", "user-cam-broadcast-end": "camera-activated", "meeting-screenshare-started": "screen-shared", @@ -118,6 +126,8 @@ export default function getXAPIStatement(event, meeting_data, user_data = null, const extension_enabled = { "user-audio-voice-enabled": "true", "user-audio-voice-disabled": "false", + "user-audio-muted": "false", + "user-audio-unmuted": "true", "user-cam-broadcast-start": "true", "user-cam-broadcast-end": "false", "meeting-screenshare-started": "true", @@ -185,4 +195,4 @@ export default function getXAPIStatement(event, meeting_data, user_data = null, return statement } -} \ No newline at end of file +} diff --git a/src/out/xapi/xapi.js b/src/out/xapi/xapi.js index 18750b7..c00d514 100644 --- a/src/out/xapi/xapi.js +++ b/src/out/xapi/xapi.js @@ -1,146 +1,180 @@ -import getXAPIStatement from './templates.js'; -import { v5 as uuidv5 } from 'uuid'; -import { DateTime } from 'luxon'; -import fetch from 'node-fetch'; +import getXAPIStatement from "./templates.js"; +import { v5 as uuidv5 } from "uuid"; +import { DateTime } from "luxon"; +import fetch from "node-fetch"; // XAPI will listen for events on redis coming from BigBlueButton, // generate xAPI statements and send to a LRS export default class XAPI { - constructor(context, config, meetingStorage, userStorage, pollStorage) { - this.context = context; - this.logger = context.getLogger(); - this.config = config; - this.meetingStorage = meetingStorage; - this.userStorage = userStorage; - this.pollStorage = pollStorage; + constructor(context, config, meetingStorage, userStorage, pollStorage) { + this.context = context; + this.logger = context.getLogger(); + this.config = config; + this.meetingStorage = meetingStorage; + this.userStorage = userStorage; + this.pollStorage = pollStorage; + } + + async postToLRS(statement) { + const { lrs_endpoint, lrs_username, lrs_password } = this.config.lrs; + + const headers = { + Authorization: `Basic ${Buffer.from( + lrs_username + ":" + lrs_password + ).toString("base64")}`, + "Content-Type": "application/json", + "X-Experience-API-Version": "1.0.0", + }; + + const requestOptions = { + method: "POST", + body: JSON.stringify(statement), + headers, + }; + + const xAPIEndpoint = new URL("xAPI/statements", lrs_endpoint); + + try { + const response = await fetch(xAPIEndpoint, requestOptions); + const { status } = response; + const data = await response.json(); + this.logger.debug("OutXAPI.res.status:", { status, data }); + } catch (err) { + this.logger.debug("OutXAPI.err:", err); } - - async postToLRS(statement) { - const { lrs_endpoint, lrs_username, lrs_password } = this.config.lrs; - - const headers = { - 'Authorization': `Basic ${Buffer.from(lrs_username + ':' + lrs_password).toString('base64')}`, - 'Content-Type': 'application/json', - 'X-Experience-API-Version': '1.0.0', - } - - const requestOptions = { - method: 'POST', - body: JSON.stringify(statement), - headers, - }; - - const xAPIEndpoint = new URL('xAPI/statements', lrs_endpoint); - - try { - const response = await fetch(xAPIEndpoint, requestOptions); - const { status } = response; - const data = await response.json(); - this.logger.debug('OutXAPI.res.status:', { status, data }); - } catch (err) { - this.logger.debug('OutXAPI.err:', err); - } + } + + async onEvent(event, raw) { + // TODO: return promise earlier to avoid holding the queue + const meeting_data = { + internal_meeting_id: event.data.attributes.meeting["internal-meeting-id"], + external_meeting_id: event.data.attributes.meeting["external-meeting-id"], + }; + + const uuid_namespace = this.config.uuid_namespace; + + meeting_data.session_id = uuidv5( + meeting_data.internal_meeting_id, + uuid_namespace + ); + meeting_data.object_id = uuidv5( + meeting_data.external_meeting_id, + uuid_namespace + ); + + let XAPIStatement; + + // if meeting-created event, set meeting_data on redis + if (event.data.id == "meeting-created") { + const serverDomain = this.config.server.domain; + meeting_data.bbb_origin_server_name = serverDomain; + meeting_data.planned_duration = event.data.attributes.meeting.duration; + meeting_data.create_time = event.data.attributes.meeting["create-time"]; + meeting_data.meeting_name = event.data.attributes.meeting.name; + + const meeting_create_day = DateTime.fromMillis( + meeting_data.create_time + ).toFormat("yyyyMMdd"); + const external_key = `${meeting_data.external_meeting_id}_${meeting_create_day}`; + + meeting_data.context_registration = uuidv5(external_key, uuid_namespace); + + await this.meetingStorage.addOrUpdateMeetingData(meeting_data); + XAPIStatement = getXAPIStatement(event, meeting_data); } - - async onEvent(event, raw) { - // TODO: return promise earlier to avoid holding the queue - const meeting_data = { - internal_meeting_id: event.data.attributes.meeting['internal-meeting-id'], - external_meeting_id: event.data.attributes.meeting['external-meeting-id'], - } - - const uuid_namespace = this.config.uuid_namespace; - - meeting_data.session_id = uuidv5(meeting_data.internal_meeting_id, uuid_namespace); - meeting_data.object_id = uuidv5(meeting_data.external_meeting_id, uuid_namespace); - - let XAPIStatement; - - // if meeting-created event, set meeting_data on redis - if (event.data.id == 'meeting-created') { - const serverDomain = this.config.server.domain; - meeting_data.bbb_origin_server_name = serverDomain; - meeting_data.planned_duration = event.data.attributes.meeting.duration; - meeting_data.create_time = event.data.attributes.meeting['create-time']; - meeting_data.meeting_name = event.data.attributes.meeting.name; - - const meeting_create_day = DateTime.fromMillis(meeting_data.create_time).toFormat('yyyyMMdd'); - const external_key = `${meeting_data.external_meeting_id}_${meeting_create_day}`; - - meeting_data.context_registration = uuidv5(external_key, uuid_namespace); - - await this.meetingStorage.addOrUpdateMeetingData(meeting_data); - XAPIStatement = getXAPIStatement(event, meeting_data); + // if not meeting-created event, read meeting_data from redis + else { + const meeting_data_storage = await this.meetingStorage.getMeetingData( + meeting_data.internal_meeting_id + ); + Object.assign(meeting_data, meeting_data_storage); + + if (event.data.id == "meeting-ended") { + XAPIStatement = getXAPIStatement(event, meeting_data); + } + // if user-joined event, set user_data on redis + else if (event.data.id == "user-joined") { + const user_data = { + internal_user_id: event.data.attributes.user["internal-user-id"], + user_name: event.data.attributes.user.name, + }; + await this.userStorage.addOrUpdateUserData(user_data); + XAPIStatement = getXAPIStatement(event, meeting_data, user_data); + } + // if not user-joined user event, read user_data on redis + else if ( + event.data.id == "user-left" || + event.data.id == "user-audio-voice-enabled" || + event.data.id == "user-audio-voice-disabled" || + event.data.id == "user-audio-muted" || + event.data.id == "user-audio-unmuted" || + event.data.id == "user-cam-broadcast-start" || + event.data.id == "user-cam-broadcast-end" || + event.data.id == "meeting-screenshare-started" || + event.data.id == "meeting-screenshare-stopped" || + event.data.id == "user-raise-hand-changed" + ) { + // If mic is not enabled in "user-audio-voice-enabled" event, do not send statement + if(event.data.id == "user-audio-voice-enabled" && + (event.data.attributes.user["listening-only"] == true || + event.data.attributes.user.muted == true || + event.data.attributes.user["sharing-mic"] == false) ){ + return; } - // if not meeting-created event, read meeting_data from redis - else { - const meeting_data_storage = await this.meetingStorage.getMeetingData(meeting_data.internal_meeting_id); - Object.assign(meeting_data, meeting_data_storage); - - if (event.data.id == 'meeting-ended') { - XAPIStatement = getXAPIStatement(event, meeting_data); - } - // if user-joined event, set user_data on redis - else if (event.data.id == 'user-joined') { - const user_data = { - internal_user_id: event.data.attributes.user['internal-user-id'], - user_name: event.data.attributes.user.name, - } - await this.userStorage.addOrUpdateUserData(user_data); - XAPIStatement = getXAPIStatement(event, meeting_data, user_data); - } - // if not user-joined user event, read user_data on redis - else if ( - event.data.id == 'user-left' - || event.data.id == 'user-audio-voice-enabled' - || event.data.id == 'user-audio-voice-disabled' - || event.data.id == 'user-cam-broadcast-start' - || event.data.id == 'user-cam-broadcast-end' - || event.data.id == 'meeting-screenshare-started' - || event.data.id == 'meeting-screenshare-stopped' - || event.data.id == 'user-raise-hand-changed') { - const internal_user_id = event.data.attributes.user?.['internal-user-id']; - const user_data = internal_user_id ? await this.userStorage.getUserData(internal_user_id) : null; - XAPIStatement = getXAPIStatement(event, meeting_data, user_data); - } - else if (event.data.id == 'chat-group-message-sent') { - const user_data = event.data.attributes['chat-message']?.sender; - const msg_key = `${user_data?.internal_user_id}_${user_data?.time}`; - user_data.msg_object_id = uuidv5(msg_key, uuid_namespace); - XAPIStatement = getXAPIStatement(event, meeting_data, user_data); - } - else if (event.data.id == 'poll-started' || event.data.id == 'poll-responded') { - const internal_user_id = event.data.attributes.user?.['internal-user-id']; - const user_data = internal_user_id ? await this.userStorage.getUserData(internal_user_id) : null; - const object_id = uuidv5(event.data.attributes.poll.id, uuid_namespace); - let poll_data; - - if (event.data.id == 'poll-started') { - var choices = (event.data.attributes.poll.answers.map(a => { - return { id: a.id.toString(), "description": { "en": a.key } } - })); - poll_data = { - object_id, - question: event.data.attributes.poll.question, - choices, - } - await this.pollStorage.addOrUpdatePollData(poll_data); - - } - else if (event.data.id == 'poll-responded') { - poll_data = object_id ? await this.pollStorage.getPollData(object_id) : null; - poll_data.choices = poll_data.choices.map(item => { - const parsedItem = JSON.parse(item); - const description = JSON.parse(parsedItem.description); - return { - id: JSON.parse(item).id, - description: { en: description.en } - }; - }); - } - XAPIStatement = getXAPIStatement(event, meeting_data, user_data, poll_data); - } + const internal_user_id = + event.data.attributes.user?.["internal-user-id"]; + const user_data = internal_user_id + ? await this.userStorage.getUserData(internal_user_id) + : null; + XAPIStatement = getXAPIStatement(event, meeting_data, user_data); + } else if (event.data.id == "chat-group-message-sent") { + const user_data = event.data.attributes["chat-message"]?.sender; + const msg_key = `${user_data?.internal_user_id}_${user_data?.time}`; + user_data.msg_object_id = uuidv5(msg_key, uuid_namespace); + XAPIStatement = getXAPIStatement(event, meeting_data, user_data); + } else if ( + event.data.id == "poll-started" || + event.data.id == "poll-responded" + ) { + const internal_user_id = + event.data.attributes.user?.["internal-user-id"]; + const user_data = internal_user_id + ? await this.userStorage.getUserData(internal_user_id) + : null; + const object_id = uuidv5(event.data.attributes.poll.id, uuid_namespace); + let poll_data; + + if (event.data.id == "poll-started") { + var choices = event.data.attributes.poll.answers.map((a) => { + return { id: a.id.toString(), description: { en: a.key } }; + }); + poll_data = { + object_id, + question: event.data.attributes.poll.question, + choices, + }; + await this.pollStorage.addOrUpdatePollData(poll_data); + } else if (event.data.id == "poll-responded") { + poll_data = object_id + ? await this.pollStorage.getPollData(object_id) + : null; + poll_data.choices = poll_data.choices.map((item) => { + const parsedItem = JSON.parse(item); + const description = JSON.parse(parsedItem.description); + return { + id: JSON.parse(item).id, + description: { en: description.en }, + }; + }); } - await this.postToLRS(XAPIStatement); + XAPIStatement = getXAPIStatement( + event, + meeting_data, + user_data, + poll_data + ); + } } -} \ No newline at end of file + await this.postToLRS(XAPIStatement); + } +} From 63e134498810ff28da952487bdec0930c4434c47 Mon Sep 17 00:00:00 2001 From: miguel-mconf Date: Mon, 16 Oct 2023 16:24:11 -0300 Subject: [PATCH 060/154] xAPI - Do not send invalid statements to the LRS --- src/out/xapi/xapi.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/out/xapi/xapi.js b/src/out/xapi/xapi.js index c00d514..09ccc39 100644 --- a/src/out/xapi/xapi.js +++ b/src/out/xapi/xapi.js @@ -62,7 +62,7 @@ export default class XAPI { uuid_namespace ); - let XAPIStatement; + let XAPIStatement = null; // if meeting-created event, set meeting_data on redis if (event.data.id == "meeting-created") { @@ -175,6 +175,8 @@ export default class XAPI { ); } } - await this.postToLRS(XAPIStatement); + if(XAPIStatement !== null){ + await this.postToLRS(XAPIStatement); + } } } From 627f753ad0a108d5498dbe437af031797ed1e310 Mon Sep 17 00:00:00 2001 From: miguel-mconf Date: Mon, 16 Oct 2023 17:10:45 -0300 Subject: [PATCH 061/154] xAPI - check for availability of Redis storage before proceding --- src/out/xapi/compartment.js | 12 +++---- src/out/xapi/index.js | 18 +++++------ src/out/xapi/templates.js | 62 ++++++++++++++++++------------------- src/out/xapi/xapi.js | 24 ++++++++++---- 4 files changed, 64 insertions(+), 52 deletions(-) diff --git a/src/out/xapi/compartment.js b/src/out/xapi/compartment.js index 8cc5c72..6899586 100644 --- a/src/out/xapi/compartment.js +++ b/src/out/xapi/compartment.js @@ -6,8 +6,8 @@ export class meetingCompartment extends StorageCompartmentKV { } async addOrUpdateMeetingData(meeting_data) { - const {internal_meeting_id, context_registration, bbb_origin_server_name, - planned_duration, create_time, meeting_name} = meeting_data; + const { internal_meeting_id, context_registration, bbb_origin_server_name, + planned_duration, create_time, meeting_name } = meeting_data; const payload = { internal_meeting_id, @@ -27,7 +27,7 @@ export class meetingCompartment extends StorageCompartmentKV { } async addOrUpdateUserData(user_data) { - const {internal_user_id, user_name} = user_data; + const { internal_user_id, user_name } = user_data; const payload = { internal_user_id, @@ -59,7 +59,7 @@ export class userCompartment extends StorageCompartmentKV { } async addOrUpdateUserData(user_data) { - const {internal_user_id, user_name} = user_data; + const { internal_user_id, user_name } = user_data; const payload = { internal_user_id, @@ -91,7 +91,7 @@ export class pollCompartment extends StorageCompartmentKV { } async addOrUpdatePollData(poll_data) { - const {object_id, question, choices} = poll_data; + const { object_id, question, choices } = poll_data; const payload = { object_id, @@ -116,4 +116,4 @@ export class pollCompartment extends StorageCompartmentKV { initialize() { return; } -} \ No newline at end of file +} diff --git a/src/out/xapi/index.js b/src/out/xapi/index.js index 058459e..9646e55 100644 --- a/src/out/xapi/index.js +++ b/src/out/xapi/index.js @@ -1,5 +1,5 @@ import XAPI from './xapi.js'; -import {meetingCompartment, userCompartment, pollCompartment} from './compartment.js'; +import { meetingCompartment, userCompartment, pollCompartment } from './compartment.js'; import redis from 'redis'; import config from 'config'; /* @@ -14,18 +14,18 @@ import config from 'config'; export default class OutXAPI { static type = "out"; - static _defaultCollector () { + static _defaultCollector() { throw new Error('Collector not set'); } - constructor (context, config = {}) { + constructor(context, config = {}) { this.type = OutXAPI.type; this.config = config; this.setContext(context); this.loaded = false; } - _validateConfig () { + _validateConfig() { if (this.config == null) { throw new Error("config not set"); } @@ -35,7 +35,7 @@ export default class OutXAPI { return true; } - async load () { + async load() { if (this._validateConfig()) { this.redisClient = redis.createClient({ host: config.get('redis.host'), @@ -45,7 +45,7 @@ export default class OutXAPI { await this.redisClient.connect(); - this.logger.debug('OutXAPI.onEvent:', this.config ); + this.logger.debug('OutXAPI.onEvent:', this.config); this.meetingStorage = new meetingCompartment( this.redisClient, @@ -70,7 +70,7 @@ export default class OutXAPI { this.loaded = true; } - async unload () { + async unload() { if (this.redisClient != null) { await this.redisClient.disconnect(); this.redisClient = null; @@ -80,14 +80,14 @@ export default class OutXAPI { this.loaded = false; } - setContext (context) { + setContext(context) { this.context = context; this.logger = context.getLogger(); return context; } - async onEvent (event, raw) { + async onEvent(event, raw) { if (!this.loaded) { throw new Error("OutXAPI not loaded"); } diff --git a/src/out/xapi/templates.js b/src/out/xapi/templates.js index 8127246..01d9645 100644 --- a/src/out/xapi/templates.js +++ b/src/out/xapi/templates.js @@ -14,21 +14,21 @@ export default function getXAPIStatement(event, meeting_data, user_data = null, const event_ts = event.data.event.ts; - if ( event.data.id == 'meeting-created' - || event.data.id == 'meeting-ended' - || event.data.id == 'user-joined' - || event.data.id == 'user-left' - || event.data.id == 'user-audio-voice-enabled' - || event.data.id == 'user-audio-voice-disabled' - || event.data.id == "user-audio-muted" - || event.data.id == "user-audio-unmuted" - || event.data.id == 'user-cam-broadcast-start' - || event.data.id == 'user-cam-broadcast-end' - || event.data.id == 'meeting-screenshare-started' - || event.data.id == 'meeting-screenshare-stopped' - || event.data.id == 'chat-group-message-sent' - || event.data.id == 'poll-started' - || event.data.id == 'poll-responded') { + if (event.data.id == 'meeting-created' + || event.data.id == 'meeting-ended' + || event.data.id == 'user-joined' + || event.data.id == 'user-left' + || event.data.id == 'user-audio-voice-enabled' + || event.data.id == 'user-audio-voice-disabled' + || event.data.id == "user-audio-muted" + || event.data.id == "user-audio-unmuted" + || event.data.id == 'user-cam-broadcast-start' + || event.data.id == 'user-cam-broadcast-end' + || event.data.id == 'meeting-screenshare-started' + || event.data.id == 'meeting-screenshare-stopped' + || event.data.id == 'chat-group-message-sent' + || event.data.id == 'poll-started' + || event.data.id == 'poll-responded') { const verbMappings = { 'meeting-created': 'http://adlnet.gov/expapi/verbs/initialized', 'meeting-ended': 'http://adlnet.gov/expapi/verbs/terminated', @@ -87,13 +87,13 @@ export default function getXAPIStatement(event, meeting_data, user_data = null, } // Custom 'meeting-created' attributes - if (event.data.id == 'meeting-created'){ + if (event.data.id == 'meeting-created') { statement.context.extensions["http://id.tincanapi.com/extension/planned-duration"] = planned_duration_ISO statement.timestamp = create_time_ISO; } // Custom 'meeting-ended' attributes - else if(event.data.id == 'meeting-ended'){ + else if (event.data.id == 'meeting-ended') { statement.context.extensions["http://id.tincanapi.com/extension/planned-duration"] = planned_duration_ISO statement.result = { "duration": Duration.fromMillis(event_ts - create_time).toISO() @@ -102,13 +102,13 @@ export default function getXAPIStatement(event, meeting_data, user_data = null, // Custom attributes for multiple interactions else if (event.data.id == 'user-audio-voice-enabled' - || event.data.id == 'user-audio-voice-disabled' - || event.data.id == "user-audio-muted" - || event.data.id == "user-audio-unmuted" - || event.data.id == 'user-cam-broadcast-start' - || event.data.id == 'user-cam-broadcast-end' - || event.data.id == 'meeting-screenshare-started' - || event.data.id == 'meeting-screenshare-stopped') { + || event.data.id == 'user-audio-voice-disabled' + || event.data.id == "user-audio-muted" + || event.data.id == "user-audio-unmuted" + || event.data.id == 'user-cam-broadcast-start' + || event.data.id == 'user-cam-broadcast-end' + || event.data.id == 'meeting-screenshare-started' + || event.data.id == 'meeting-screenshare-stopped') { const extension = { "user-audio-voice-enabled": "micro-activated", @@ -138,14 +138,14 @@ export default function getXAPIStatement(event, meeting_data, user_data = null, } // Custom 'user-raise-hand-changed' attributes - else if (event.data.id == 'user-raise-hand-changed'){ + else if (event.data.id == 'user-raise-hand-changed') { const extension_uri = 'https://w3id.org/xapi/virtual-classroom/extensions/hand-raised'; const extension_enabled = event.data.attributes.user["raise-hand"]; statement.context.extensions[extension_uri] = extension_enabled; } // Custom 'chat-group-message-sent' attributes - else if(event.data.id == 'chat-group-message-sent'){ + else if (event.data.id == 'chat-group-message-sent') { statement.object = { "id": `https://${bbb_origin_server_name}/xapi/activities/${user_data?.msg_object_id}`, "definition": { @@ -157,7 +157,7 @@ export default function getXAPIStatement(event, meeting_data, user_data = null, { "id": `https://${bbb_origin_server_name}/xapi/activities/${object_id}`, "definition": { - "type": "https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom" + "type": "https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom" } } ] @@ -165,10 +165,10 @@ export default function getXAPIStatement(event, meeting_data, user_data = null, } // Custom 'poll-started' and 'poll-responded' attributes - else if(event.data.id == 'poll-started' || event.data.id == 'poll-responded'){ + else if (event.data.id == 'poll-started' || event.data.id == 'poll-responded') { statement.object = { "id": `https://${bbb_origin_server_name}/xapi/activities/${poll_data?.object_id}`, - "definition":{ + "definition": { "description": { "en": poll_data?.question, }, @@ -182,11 +182,11 @@ export default function getXAPIStatement(event, meeting_data, user_data = null, { "id": `https://${bbb_origin_server_name}/xapi/activities/${object_id}`, "definition": { - "type": "https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom" + "type": "https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom" } } ] - if(event.data.id == 'poll-responded'){ + if (event.data.id == 'poll-responded') { statement.result = { "response": event.data.attributes.poll.answerIds.join(','), } diff --git a/src/out/xapi/xapi.js b/src/out/xapi/xapi.js index 09ccc39..b72a339 100644 --- a/src/out/xapi/xapi.js +++ b/src/out/xapi/xapi.js @@ -87,6 +87,10 @@ export default class XAPI { const meeting_data_storage = await this.meetingStorage.getMeetingData( meeting_data.internal_meeting_id ); + // Do not proceed if meeting_data is not found on the storage + if (meeting_data_storage === undefined) { + return; + } Object.assign(meeting_data, meeting_data_storage); if (event.data.id == "meeting-ended") { @@ -115,10 +119,10 @@ export default class XAPI { event.data.id == "user-raise-hand-changed" ) { // If mic is not enabled in "user-audio-voice-enabled" event, do not send statement - if(event.data.id == "user-audio-voice-enabled" && - (event.data.attributes.user["listening-only"] == true || - event.data.attributes.user.muted == true || - event.data.attributes.user["sharing-mic"] == false) ){ + if (event.data.id == "user-audio-voice-enabled" && + (event.data.attributes.user["listening-only"] == true || + event.data.attributes.user.muted == true || + event.data.attributes.user["sharing-mic"] == false)) { return; } const internal_user_id = @@ -126,6 +130,10 @@ export default class XAPI { const user_data = internal_user_id ? await this.userStorage.getUserData(internal_user_id) : null; + // Do not proceed if user_data is requested but not found on the storage + if (user_data === undefined) { + return; + } XAPIStatement = getXAPIStatement(event, meeting_data, user_data); } else if (event.data.id == "chat-group-message-sent") { const user_data = event.data.attributes["chat-message"]?.sender; @@ -158,6 +166,10 @@ export default class XAPI { poll_data = object_id ? await this.pollStorage.getPollData(object_id) : null; + // Do not proceed if poll_data is requested but not found on the storage + if (poll_data === undefined) { + return; + } poll_data.choices = poll_data.choices.map((item) => { const parsedItem = JSON.parse(item); const description = JSON.parse(parsedItem.description); @@ -175,8 +187,8 @@ export default class XAPI { ); } } - if(XAPIStatement !== null){ - await this.postToLRS(XAPIStatement); + if (XAPIStatement !== null) { + await this.postToLRS(XAPIStatement); } } } From e1df244fa2068d4058e2930db63ca7d8b002b52d Mon Sep 17 00:00:00 2001 From: miguel-mconf Date: Mon, 16 Oct 2023 18:24:38 -0300 Subject: [PATCH 062/154] xAPI - returning Promise on onEvent method to avoid locking the queue --- src/out/xapi/xapi.js | 255 +++++++++++++++++++++++-------------------- 1 file changed, 139 insertions(+), 116 deletions(-) diff --git a/src/out/xapi/xapi.js b/src/out/xapi/xapi.js index b72a339..88a7e6c 100644 --- a/src/out/xapi/xapi.js +++ b/src/out/xapi/xapi.js @@ -45,7 +45,6 @@ export default class XAPI { } async onEvent(event, raw) { - // TODO: return promise earlier to avoid holding the queue const meeting_data = { internal_meeting_id: event.data.attributes.meeting["internal-meeting-id"], external_meeting_id: event.data.attributes.meeting["external-meeting-id"], @@ -64,131 +63,155 @@ export default class XAPI { let XAPIStatement = null; - // if meeting-created event, set meeting_data on redis - if (event.data.id == "meeting-created") { - const serverDomain = this.config.server.domain; - meeting_data.bbb_origin_server_name = serverDomain; - meeting_data.planned_duration = event.data.attributes.meeting.duration; - meeting_data.create_time = event.data.attributes.meeting["create-time"]; - meeting_data.meeting_name = event.data.attributes.meeting.name; - - const meeting_create_day = DateTime.fromMillis( - meeting_data.create_time - ).toFormat("yyyyMMdd"); - const external_key = `${meeting_data.external_meeting_id}_${meeting_create_day}`; - - meeting_data.context_registration = uuidv5(external_key, uuid_namespace); - - await this.meetingStorage.addOrUpdateMeetingData(meeting_data); - XAPIStatement = getXAPIStatement(event, meeting_data); - } - // if not meeting-created event, read meeting_data from redis - else { - const meeting_data_storage = await this.meetingStorage.getMeetingData( - meeting_data.internal_meeting_id - ); - // Do not proceed if meeting_data is not found on the storage - if (meeting_data_storage === undefined) { - return; - } - Object.assign(meeting_data, meeting_data_storage); - - if (event.data.id == "meeting-ended") { + return new Promise(async (resolve, reject) => { + // if meeting-created event, set meeting_data on redis + if (event.data.id == "meeting-created") { + const serverDomain = this.config.server.domain; + meeting_data.bbb_origin_server_name = serverDomain; + meeting_data.planned_duration = event.data.attributes.meeting.duration; + meeting_data.create_time = event.data.attributes.meeting["create-time"]; + meeting_data.meeting_name = event.data.attributes.meeting.name; + + const meeting_create_day = DateTime.fromMillis( + meeting_data.create_time + ).toFormat("yyyyMMdd"); + const external_key = `${meeting_data.external_meeting_id}_${meeting_create_day}`; + + meeting_data.context_registration = uuidv5(external_key, uuid_namespace); + try { + await this.meetingStorage.addOrUpdateMeetingData(meeting_data); + resolve(); + } catch (error) { + return reject(error); + } XAPIStatement = getXAPIStatement(event, meeting_data); } - // if user-joined event, set user_data on redis - else if (event.data.id == "user-joined") { - const user_data = { - internal_user_id: event.data.attributes.user["internal-user-id"], - user_name: event.data.attributes.user.name, - }; - await this.userStorage.addOrUpdateUserData(user_data); - XAPIStatement = getXAPIStatement(event, meeting_data, user_data); - } - // if not user-joined user event, read user_data on redis - else if ( - event.data.id == "user-left" || - event.data.id == "user-audio-voice-enabled" || - event.data.id == "user-audio-voice-disabled" || - event.data.id == "user-audio-muted" || - event.data.id == "user-audio-unmuted" || - event.data.id == "user-cam-broadcast-start" || - event.data.id == "user-cam-broadcast-end" || - event.data.id == "meeting-screenshare-started" || - event.data.id == "meeting-screenshare-stopped" || - event.data.id == "user-raise-hand-changed" - ) { - // If mic is not enabled in "user-audio-voice-enabled" event, do not send statement - if (event.data.id == "user-audio-voice-enabled" && - (event.data.attributes.user["listening-only"] == true || - event.data.attributes.user.muted == true || - event.data.attributes.user["sharing-mic"] == false)) { - return; - } - const internal_user_id = - event.data.attributes.user?.["internal-user-id"]; - const user_data = internal_user_id - ? await this.userStorage.getUserData(internal_user_id) - : null; - // Do not proceed if user_data is requested but not found on the storage - if (user_data === undefined) { - return; + // if not meeting-created event, read meeting_data from redis + else { + const meeting_data_storage = await this.meetingStorage.getMeetingData( + meeting_data.internal_meeting_id + ); + // Do not proceed if meeting_data is not found on the storage + if (meeting_data_storage === undefined) { + return reject(); } - XAPIStatement = getXAPIStatement(event, meeting_data, user_data); - } else if (event.data.id == "chat-group-message-sent") { - const user_data = event.data.attributes["chat-message"]?.sender; - const msg_key = `${user_data?.internal_user_id}_${user_data?.time}`; - user_data.msg_object_id = uuidv5(msg_key, uuid_namespace); - XAPIStatement = getXAPIStatement(event, meeting_data, user_data); - } else if ( - event.data.id == "poll-started" || - event.data.id == "poll-responded" - ) { - const internal_user_id = - event.data.attributes.user?.["internal-user-id"]; - const user_data = internal_user_id - ? await this.userStorage.getUserData(internal_user_id) - : null; - const object_id = uuidv5(event.data.attributes.poll.id, uuid_namespace); - let poll_data; + Object.assign(meeting_data, meeting_data_storage); - if (event.data.id == "poll-started") { - var choices = event.data.attributes.poll.answers.map((a) => { - return { id: a.id.toString(), description: { en: a.key } }; - }); - poll_data = { - object_id, - question: event.data.attributes.poll.question, - choices, + if (event.data.id == "meeting-ended") { + resolve(); + XAPIStatement = getXAPIStatement(event, meeting_data); + } + // if user-joined event, set user_data on redis + else if (event.data.id == "user-joined") { + const user_data = { + internal_user_id: event.data.attributes.user["internal-user-id"], + user_name: event.data.attributes.user.name, }; - await this.pollStorage.addOrUpdatePollData(poll_data); - } else if (event.data.id == "poll-responded") { - poll_data = object_id - ? await this.pollStorage.getPollData(object_id) + try { + await this.userStorage.addOrUpdateUserData(user_data); + resolve(); + } catch (error) { + return reject(error); + } + XAPIStatement = getXAPIStatement(event, meeting_data, user_data); + } + // if not user-joined user event, read user_data on redis + else if ( + event.data.id == "user-left" || + event.data.id == "user-audio-voice-enabled" || + event.data.id == "user-audio-voice-disabled" || + event.data.id == "user-audio-muted" || + event.data.id == "user-audio-unmuted" || + event.data.id == "user-cam-broadcast-start" || + event.data.id == "user-cam-broadcast-end" || + event.data.id == "meeting-screenshare-started" || + event.data.id == "meeting-screenshare-stopped" || + event.data.id == "user-raise-hand-changed" + ) { + resolve(); + // If mic is not enabled in "user-audio-voice-enabled" event, do not send statement + if (event.data.id == "user-audio-voice-enabled" && + (event.data.attributes.user["listening-only"] == true || + event.data.attributes.user.muted == true || + event.data.attributes.user["sharing-mic"] == false)) { + return; + } + const internal_user_id = + event.data.attributes.user?.["internal-user-id"]; + const user_data = internal_user_id + ? await this.userStorage.getUserData(internal_user_id) : null; - // Do not proceed if poll_data is requested but not found on the storage - if (poll_data === undefined) { + // Do not proceed if user_data is requested but not found on the storage + if (user_data === undefined) { return; } - poll_data.choices = poll_data.choices.map((item) => { - const parsedItem = JSON.parse(item); - const description = JSON.parse(parsedItem.description); - return { - id: JSON.parse(item).id, - description: { en: description.en }, + XAPIStatement = getXAPIStatement(event, meeting_data, user_data); + // Chat message + } else if (event.data.id == "chat-group-message-sent") { + resolve(); + const user_data = event.data.attributes["chat-message"]?.sender; + const msg_key = `${user_data?.internal_user_id}_${user_data?.time}`; + user_data.msg_object_id = uuidv5(msg_key, uuid_namespace); + XAPIStatement = getXAPIStatement(event, meeting_data, user_data); + // Poll events + } else if ( + event.data.id == "poll-started" || + event.data.id == "poll-responded" + ) { + if (event.data.id == "poll-responded") { + resolve(); + } + const internal_user_id = + event.data.attributes.user?.["internal-user-id"]; + const user_data = internal_user_id + ? await this.userStorage.getUserData(internal_user_id) + : null; + const object_id = uuidv5(event.data.attributes.poll.id, uuid_namespace); + let poll_data; + + if (event.data.id == "poll-started") { + var choices = event.data.attributes.poll.answers.map((a) => { + return { id: a.id.toString(), description: { en: a.key } }; + }); + poll_data = { + object_id, + question: event.data.attributes.poll.question, + choices, }; - }); + try { + await this.pollStorage.addOrUpdatePollData(poll_data); + resolve(); + } catch (error) { + return reject(error); + } + } else if (event.data.id == "poll-responded") { + poll_data = object_id + ? await this.pollStorage.getPollData(object_id) + : null; + // Do not proceed if poll_data is requested but not found on the storage + if (poll_data === undefined) { + return; + } + poll_data.choices = poll_data.choices.map((item) => { + const parsedItem = JSON.parse(item); + const description = JSON.parse(parsedItem.description); + return { + id: JSON.parse(item).id, + description: { en: description.en }, + }; + }); + } + XAPIStatement = getXAPIStatement( + event, + meeting_data, + user_data, + poll_data + ); } - XAPIStatement = getXAPIStatement( - event, - meeting_data, - user_data, - poll_data - ); } - } - if (XAPIStatement !== null) { - await this.postToLRS(XAPIStatement); - } + if (XAPIStatement !== null) { + await this.postToLRS(XAPIStatement); + } + }); } } From a4ca7a7a7d1fbf4c4cccad9dff188c337a305637 Mon Sep 17 00:00:00 2001 From: miguel-mconf Date: Tue, 17 Oct 2023 18:16:29 -0300 Subject: [PATCH 063/154] Added meta_xapi-enabled metadata support --- src/out/xapi/compartment.js | 23 ++++------------------- src/out/xapi/xapi.js | 16 +++++++++++++++- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/out/xapi/compartment.js b/src/out/xapi/compartment.js index 6899586..a255593 100644 --- a/src/out/xapi/compartment.js +++ b/src/out/xapi/compartment.js @@ -7,7 +7,7 @@ export class meetingCompartment extends StorageCompartmentKV { async addOrUpdateMeetingData(meeting_data) { const { internal_meeting_id, context_registration, bbb_origin_server_name, - planned_duration, create_time, meeting_name } = meeting_data; + planned_duration, create_time, meeting_name, xapi_enabled } = meeting_data; const payload = { internal_meeting_id, @@ -16,6 +16,7 @@ export class meetingCompartment extends StorageCompartmentKV { planned_duration, create_time, meeting_name, + xapi_enabled, }; const mapping = await this.save(payload, { @@ -26,22 +27,6 @@ export class meetingCompartment extends StorageCompartmentKV { return mapping; } - async addOrUpdateUserData(user_data) { - const { internal_user_id, user_name } = user_data; - - const payload = { - internal_user_id, - user_name, - }; - - const mapping = await this.save(payload, { - alias: internal_user_id, - }); - this.logger.info(`added user data to the list ${internal_user_id}: ${mapping.print()}`); - - return mapping; - } - async getMeetingData(internal_meeting_id) { const meeting_data = this.findByField('internal_meeting_id', internal_meeting_id); return (meeting_data != null ? meeting_data.payload : undefined); @@ -69,7 +54,7 @@ export class userCompartment extends StorageCompartmentKV { const mapping = await this.save(payload, { alias: internal_user_id, }); - this.logger.info(`added poll data to the list ${internal_user_id}: ${mapping.print()}`); + this.logger.info(`added user data to the list ${internal_user_id}: ${mapping.print()}`); return mapping; } @@ -102,7 +87,7 @@ export class pollCompartment extends StorageCompartmentKV { const mapping = await this.save(payload, { alias: object_id, }); - this.logger.info(`added user data to the list ${object_id}: ${mapping.print()}`); + this.logger.info(`added poll data to the list ${object_id}: ${mapping.print()}`); return mapping; } diff --git a/src/out/xapi/xapi.js b/src/out/xapi/xapi.js index 88a7e6c..467d27b 100644 --- a/src/out/xapi/xapi.js +++ b/src/out/xapi/xapi.js @@ -71,6 +71,7 @@ export default class XAPI { meeting_data.planned_duration = event.data.attributes.meeting.duration; meeting_data.create_time = event.data.attributes.meeting["create-time"]; meeting_data.meeting_name = event.data.attributes.meeting.name; + meeting_data.xapi_enabled = event.data.attributes.meeting.metadata?.["meta_xapi-enabled"] !== 'false' ? 'true' : 'false'; const meeting_create_day = DateTime.fromMillis( meeting_data.create_time @@ -78,12 +79,19 @@ export default class XAPI { const external_key = `${meeting_data.external_meeting_id}_${meeting_create_day}`; meeting_data.context_registration = uuidv5(external_key, uuid_namespace); + //set meeting_data on redis try { await this.meetingStorage.addOrUpdateMeetingData(meeting_data); resolve(); } catch (error) { return reject(error); } + + // Do not proceed if xapi_enabled === 'false' was passed in the metadata + if (meeting_data.xapi_enabled === 'false') { + return reject(); + } + XAPIStatement = getXAPIStatement(event, meeting_data); } // if not meeting-created event, read meeting_data from redis @@ -97,6 +105,11 @@ export default class XAPI { } Object.assign(meeting_data, meeting_data_storage); + // Do not proceed if xapi_enabled === 'false' was passed in the metadata + if (meeting_data.xapi_enabled === 'false') { + return reject(); + } + if (event.data.id == "meeting-ended") { resolve(); XAPIStatement = getXAPIStatement(event, meeting_data); @@ -178,6 +191,7 @@ export default class XAPI { question: event.data.attributes.poll.question, choices, }; + //set poll_data on redis try { await this.pollStorage.addOrUpdatePollData(poll_data); resolve(); @@ -209,7 +223,7 @@ export default class XAPI { ); } } - if (XAPIStatement !== null) { + if (XAPIStatement !== null && meeting_data.xapi_enabled === 'true') { await this.postToLRS(XAPIStatement); } }); From 6f165836f2b3d9866f98e154f9f957fa56622a29 Mon Sep 17 00:00:00 2001 From: miguel-mconf Date: Tue, 17 Oct 2023 18:34:38 -0300 Subject: [PATCH 064/154] Fixed xapi-enabled metadata name --- src/out/xapi/xapi.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/out/xapi/xapi.js b/src/out/xapi/xapi.js index 467d27b..585197c 100644 --- a/src/out/xapi/xapi.js +++ b/src/out/xapi/xapi.js @@ -71,7 +71,7 @@ export default class XAPI { meeting_data.planned_duration = event.data.attributes.meeting.duration; meeting_data.create_time = event.data.attributes.meeting["create-time"]; meeting_data.meeting_name = event.data.attributes.meeting.name; - meeting_data.xapi_enabled = event.data.attributes.meeting.metadata?.["meta_xapi-enabled"] !== 'false' ? 'true' : 'false'; + meeting_data.xapi_enabled = event.data.attributes.meeting.metadata?.["xapi-enabled"] !== 'false' ? 'true' : 'false'; const meeting_create_day = DateTime.fromMillis( meeting_data.create_time From 2d58ff1b402e9dfe85f4e46fd09c80193ec2cffc Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Wed, 18 Oct 2023 10:48:47 -0300 Subject: [PATCH 065/154] fix(webhooks): set logger in callback-emitter --- src/out/webhooks/web-hooks.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/out/webhooks/web-hooks.js b/src/out/webhooks/web-hooks.js index c8b13d3..4d196ef 100644 --- a/src/out/webhooks/web-hooks.js +++ b/src/out/webhooks/web-hooks.js @@ -156,6 +156,7 @@ class WebHooks { requestTimeout: this.config.requestTimeout, retryIntervals: this.config.retryIntervals, checksumAlgorithm: this.config.hookChecksumAlgorithm, + logger: this.logger, } ); From 754f3bc25812fa91744d5e69b45284a5253e56d6 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Wed, 18 Oct 2023 11:43:24 -0300 Subject: [PATCH 066/154] fix(webhooks): prevent loop when handling permanent hook failures, + Fix a loop scenario where permanent hooks were fire nonstop after resetting their failure interval Improve hook dispatch failure logging and metric reporting --- config/default.example.yml | 4 +-- src/out/webhooks/callback-emitter.js | 48 ++++++++++++++++++---------- src/out/webhooks/web-hooks.js | 8 +++-- 3 files changed, 39 insertions(+), 21 deletions(-) diff --git a/config/default.example.yml b/config/default.example.yml index f9a1db8..3cc8e24 100644 --- a/config/default.example.yml +++ b/config/default.example.yml @@ -109,8 +109,8 @@ modules: - 60000 - 60000 - 60000 - # Reset permanent interval when exceeding maximum attemps - permanentIntervalReset: 8 + # Reset permanent interval when exceeding maximum attemps (ms) + permanentIntervalReset: 60000 # Hook's request module timeout for socket conn establishment and/or responses (ms) requestTimeout: 5000 retry: diff --git a/src/out/webhooks/callback-emitter.js b/src/out/webhooks/callback-emitter.js index 186320f..8598842 100644 --- a/src/out/webhooks/callback-emitter.js +++ b/src/out/webhooks/callback-emitter.js @@ -5,13 +5,17 @@ import fetch from 'node-fetch'; // A simple string that identifies the event const simplifiedEvent = (_event) => { - let event = _event.event ? _event.event : _event; try { - const parsedEvent = JSON.parse(event); - return `event: { name: ${parsedEvent?.data?.id}, timestamp: ${(parsedEvent?.data?.event?.ts)} }`; + const event = typeof _event === 'string' + ? JSON.parse(_event) + : _event.event + ? _event.event + : _event; + + return `event: { name: ${event?.data?.id}, timestamp: ${(event?.data?.event?.ts)} }`; } catch (error) { - return `event: ${event}`; + return `event: ${_event}`; } }; @@ -47,7 +51,7 @@ export default class CallbackEmitter extends EventEmitter { super(); this.callbackURL = callbackURL; this.event = event; - this.message = JSON.stringify(event); + this.eventStr = JSON.stringify(event); this.nextInterval = 0; this.timestamp = 0; this.permanent = permanent; @@ -61,7 +65,6 @@ export default class CallbackEmitter extends EventEmitter { } this._dispatched = false; - this._permanentIntervalReset = permanentIntervalReset || 8; this._serverDomain = domain; this._secret = secret; this._bearerAuth = auth2_0; @@ -74,6 +77,7 @@ export default class CallbackEmitter extends EventEmitter { 10000, 30000, ]; + this._permanentIntervalReset = permanentIntervalReset || 60000; this._checksumAlgorithm = checksumAlgorithm; } @@ -90,15 +94,14 @@ export default class CallbackEmitter extends EventEmitter { const interval = this._retryIntervals[this.nextInterval]; if (interval != null) { - this.logger.warn(`trying the callback again in ${interval/1000.0} secs: ${this.callbackURL}`, error); + this.logger.warn(`trying the callback again in ${interval/1000.0} secs: ${this.callbackURL}`); this.nextInterval++; this._scheduleNext(interval); // no intervals anymore, time to give up } else { - this.nextInterval = this._permanentIntervalReset; - if (this.permanent){ - this._scheduleNext(this.nextInterval); + this.logger.warn(`callback retries expired, but it's permanent - try the callback again in ${this._permanentIntervalReset/1000.0} secs: ${this.callbackURL}`); + this._scheduleNext(this._permanentIntervalReset); } else { this.emit(CallbackEmitter.EVENTS.STOPPED); } @@ -117,7 +120,7 @@ export default class CallbackEmitter extends EventEmitter { // note: keep keys in alphabetical order const data = new URLSearchParams({ domain: serverDomain, - event: "[" + this.message + "]", + event: "[" + this.eventStr + "]", timestamp: this.timestamp, }); const requestOptions = { @@ -159,19 +162,32 @@ export default class CallbackEmitter extends EventEmitter { controller.abort(); }, timeout); requestOptions.signal = controller.signal; - const stringifiedEvent = simplifiedEvent(data); + const stringifiedEvent = simplifiedEvent(this.event); try { const response = await fetch(callbackURL, requestOptions); if (responseFailed(response)) { - this.logger.warn(`error in the callback call to: [${callbackURL}] for ${stringifiedEvent} status: ${response != null ? response.status: undefined}`); - throw new Error(response.statusText); + const failedResponseError = new Error( + `Invalid response: ${response?.status || 'unknown'}` || 'unknown error', + ); + failedResponseError.code = response?.status; } - this.logger.info(`successful callback call to: [${callbackURL}] for ${stringifiedEvent}`); + this.logger.info(`successful callback call to: [${callbackURL}]`, { + event: stringifiedEvent, + status: response?.status, + statusText: response?.statusText, + }); } catch (error) { - this.logger.warn(`error in the callback call to: [${callbackURL}] for ${stringifiedEvent}`, error); + if (error.code == null) error.code = 'unknown'; + this.logger.warn(`error in the callback call to: [${callbackURL}]`, { + event: stringifiedEvent, + errorMessage: error?.message, + errorName: error?.name, + errorCode: error.code, + stack: error?.stack, + }); throw error; } finally { clearTimeout(abortTimeout); diff --git a/src/out/webhooks/web-hooks.js b/src/out/webhooks/web-hooks.js index 4d196ef..e2e381b 100644 --- a/src/out/webhooks/web-hooks.js +++ b/src/out/webhooks/web-hooks.js @@ -171,10 +171,10 @@ class WebHooks { return resolve(); }); - emitter.on(CallbackEmitter.EVENTS.FAILED, (error) => { + emitter.on(CallbackEmitter.EVENTS.FAILURE, (error) => { this._exporter.agent.increment(METRIC_NAMES.HOOK_FAILURES, { callbackURL: hook.payload.callbackURL, - reason: error.message, + reason: error.code || error.name || 'unknown', eventId, }); }); @@ -224,7 +224,9 @@ class WebHooks { if (!hook.payload.getRaw) { this.logger.info('dispatching event to hook', { callbackURL: hook.payload.callbackURL }); return this.dispatch(event, hook).catch((error) => { - this.logger.error('failed to enqueue', { calbackURL: hook.payload.callbackURL, error: error.stack }); + this.logger.error('failed to enqueue', { + calbackURL: hook.payload.callbackURL, error: error.stack + }); }); } else { return this._processRaw(hook, raw); From 40c6ea90056cec07048610ee7fe9d286b5247bf3 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Wed, 18 Oct 2023 13:07:36 -0300 Subject: [PATCH 067/154] fix: deep clone and merge base config with module configs --- src/modules/index.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/modules/index.js b/src/modules/index.js index 8f50b69..64a6c05 100644 --- a/src/modules/index.js +++ b/src/modules/index.js @@ -18,7 +18,7 @@ const REDIS_CONF = { password: config.has('redis.password') ? config.get('redis.password') : undefined, } REDIS_CONF.REDIS_URL = `redis://${REDIS_CONF.password ? `:${REDIS_CONF.password}@` : ''}${REDIS_CONF.host}:${REDIS_CONF.port}`; -const BASE_CONFIGURATION = { +const BASE_CONFIGURATION = Object.freeze({ server: { domain: config.get('bbb.serverDomain'), secret: config.get('bbb.sharedSecret'), @@ -30,7 +30,7 @@ const BASE_CONFIGURATION = { password: REDIS_CONF.password, url: REDIS_CONF.REDIS_URL, }, -} +}); export default class ModuleManager { static moduleTypes = MODULE_TYPES; @@ -57,7 +57,9 @@ export default class ModuleManager { } _buildContext(configuration) { - configuration.config = { ...BASE_CONFIGURATION, ...configuration.config }; + const base = config.util.cloneDeep(BASE_CONFIGURATION); + const extended = config.util.extendDeep(base, configuration.config, 8); + configuration.config = extended; const utils = { exporter: Exporter, }; From 3e1bbcfb32f3a1506e020c83961c158398c427f1 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Wed, 18 Oct 2023 13:08:49 -0300 Subject: [PATCH 068/154] fix(xapi): use redisUrl in client creation from module configs host/port/password pattern is not used in node-redis@4, prefer redisUrl instead Fetch redis config params from context configuration rather than via node-config directly --- src/out/xapi/index.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/out/xapi/index.js b/src/out/xapi/index.js index 9646e55..7f52911 100644 --- a/src/out/xapi/index.js +++ b/src/out/xapi/index.js @@ -1,7 +1,6 @@ import XAPI from './xapi.js'; import { meetingCompartment, userCompartment, pollCompartment } from './compartment.js'; -import redis from 'redis'; -import config from 'config'; +import { createClient } from 'redis'; /* * [MODULE_TYPES.OUTPUT]: { * load: 'function', @@ -37,10 +36,10 @@ export default class OutXAPI { async load() { if (this._validateConfig()) { - this.redisClient = redis.createClient({ - host: config.get('redis.host'), - port: config.get('redis.port'), - password: config.has('redis.password') ? config.get('redis.password') : undefined, + const { url, password, host, port } = this.config.redis || this.config; + const redisUrl = url || `redis://${password ? `:${password}@` : ''}${host}:${port}`; + this.redisClient = createClient({ + url: redisUrl, }); await this.redisClient.connect(); From 6c15f0ef30fbab42056c2262841baa72e7803476 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Wed, 18 Oct 2023 15:53:40 -0300 Subject: [PATCH 069/154] fix(hook): prevent hook duplication on boot resync For hooks with the same callbackURL, but different ID - always picks the first hit in the set --- src/db/redis/hooks.js | 41 +++++++++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/src/db/redis/hooks.js b/src/db/redis/hooks.js index 0913fb9..ca1c309 100644 --- a/src/db/redis/hooks.js +++ b/src/db/redis/hooks.js @@ -86,6 +86,7 @@ class HookCompartment extends StorageCompartmentKV { } async addSubscription({ + id, callbackURL, meetingID, eventID, @@ -110,9 +111,9 @@ class HookCompartment extends StorageCompartmentKV { } this.logger.info(`adding a hook with callback URL: [${callbackURL}]`, { payload }); - const id = permanent ? uuidv5(callbackURL, uuidv5.URL) : uuidv4(); + const finalID = id || (permanent ? uuidv5(callbackURL, uuidv5.URL) : uuidv4()); hook = await this.save(payload, { - id, + id: finalID, alias: callbackURL, }); @@ -143,16 +144,40 @@ class HookCompartment extends StorageCompartmentKV { return this.find(id); } - firstSync() { - const keys = Object.keys(this.localStorage); - if (keys.length > 0) return this.localStorage[keys[0]]; - return null; - } - getAllGlobalHooks() { return this.getAll().filter(hook => this.isGlobal(hook)); } + // Gets all mappings from redis to populate the local database. + async resync() { + try { + const mappings = await this.redisClient.sMembers(this.setId); + + this.logger.info(`starting hooks resync, mappings registered: [${mappings}]`); + if (mappings != null && mappings.length > 0) { + for (let id of mappings) { + try { + const data = await this.redisClient.hGetAll(this.prefix + ":" + id); + const payloadWithID = this.deserialize(data); + + if (payloadWithID && Object.keys(payloadWithID).length > 0) { + await this.addSubscription(payloadWithID); + } + } catch (error) { + this.logger.error('error getting information for a mapping from redis', error); + } + } + + const stringifiedMappings = this.getAll().map(m => m.print()); + this.logger.info(`finished resync, mappings registered: [${stringifiedMappings}]`); + } else { + this.logger.info(`finished resync, no mappings registered`); + } + } catch (error) { + this.logger.error('error getting list of mappings from redis', error); + } + } + // Initializes global methods for this model. initialize() { return this.resync(); From 91b114d2878b2db53dc7c7b1bee385c280138417 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Wed, 18 Oct 2023 15:54:21 -0300 Subject: [PATCH 070/154] chore: new redis prefixes for out-webhooks compartment and processor compartments Decoupling from older v2 mappings for the sake of cleanliness --- config/default.example.yml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/config/default.example.yml b/config/default.example.yml index 3cc8e24..02d50d9 100644 --- a/config/default.example.yml +++ b/config/default.example.yml @@ -28,13 +28,12 @@ redis: host: 127.0.0.1 port: 6379 keys: - hookPrefix: bigbluebutton:webhooks:hook - hooks: bigbluebutton:webhooks:hooks - mappings: bigbluebutton:webhooks:mappings - mappingPrefix: bigbluebutton:webhooks:mapping - eventsPrefix: bigbluebutton:webhooks:events - userMaps: bigbluebutton:webhooks:userMaps - userMapPrefix: bigbluebutton:webhooks:userMap + hookPrefix: bigbluebutton:webhooks:out:hook + hooks: bigbluebutton:webhooks:out:hooks + mappings: bigbluebutton:webhooks:proc:mappings + mappingPrefix: bigbluebutton:webhooks:proc:mapping + userMaps: bigbluebutton:webhooks:proc:userMaps + userMapPrefix: bigbluebutton:webhooks:proc:userMap # Basic module config entry template: # key: , From 513235d3100dd3a3f7f07fe20b9898ec00c4b96a Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Wed, 18 Oct 2023 15:59:20 -0300 Subject: [PATCH 071/154] fix(webhooks): increment registered hooks gauge rather than set it to 1 Provides a more reliable indicator of duplicated hook entries (which has to be considered a bug) --- src/out/webhooks/index.js | 2 +- src/out/webhooks/utils.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/out/webhooks/index.js b/src/out/webhooks/index.js index 4cc7208..48f294a 100644 --- a/src/out/webhooks/index.js +++ b/src/out/webhooks/index.js @@ -76,7 +76,7 @@ class OutWebHooks { const hooks = HookCompartment.get().getAll(); this._exporter.agent.reset([METRIC_NAMES.REGISTERED_HOOKS]); hooks.forEach(hook => { - this._exporter.agent.set(METRIC_NAMES.REGISTERED_HOOKS, 1, { + this._exporter.agent.increment(METRIC_NAMES.REGISTERED_HOOKS, { callbackURL: hook.payload.callbackURL, permanent: hook.payload.permanent, getRaw: hook.payload.getRaw, diff --git a/src/out/webhooks/utils.js b/src/out/webhooks/utils.js index 5d058ed..1fc7c16 100644 --- a/src/out/webhooks/utils.js +++ b/src/out/webhooks/utils.js @@ -152,7 +152,7 @@ const isEmpty = (obj) => [Object, Array].includes((obj || {}).constructor) /** * sortBy - Sorts an array of objects by a key. * @param {string|number} key - The key to sort by. - * @returns {function} - A function that can be used to sort an array of objects by the given key. + * @returns {Function} - A function that can be used to sort an array of objects by the given key. * @public */ const sortBy = (key) => (a, b) => { From 6db9c3d7e4b634749b4ec3552c8065635eff0e89 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Thu, 19 Oct 2023 11:45:19 -0300 Subject: [PATCH 072/154] fix: handle file transport creation failures --- src/common/logger.js | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/common/logger.js b/src/common/logger.js index 554bd85..f86c7a5 100644 --- a/src/common/logger.js +++ b/src/common/logger.js @@ -107,15 +107,20 @@ const _newLogger = ({ const loggingTransports = []; if (filename) { - loggingTransports.push(new transports.File({ - filename, - format: combine( - timestamp(), - splat(), - errors({ stack: true }), - json(), - ) - })); + try { + loggingTransports.push(new transports.File({ + filename, + format: combine( + timestamp(), + splat(), + errors({ stack: true }), + json(), + ) + })); + } catch (error) { + // eslint-disable-next-line no-console + console.error("Failed to create file transport, won't log to file", error); + } } if (stdout) { From 76739e0b3c18638cdfd16df7a4aeaa3a56d941f9 Mon Sep 17 00:00:00 2001 From: mp Date: Thu, 19 Oct 2023 15:22:54 -0300 Subject: [PATCH 073/154] Avoid handling invalid events for xAPI and new xAPI templates partially implemented --- src/out/xapi/compartment.js | 4 +- src/out/xapi/templates.js | 149 ++++++++++++++++++++---------------- src/out/xapi/xapi.js | 65 ++++++++++------ 3 files changed, 126 insertions(+), 92 deletions(-) diff --git a/src/out/xapi/compartment.js b/src/out/xapi/compartment.js index a255593..a2b6126 100644 --- a/src/out/xapi/compartment.js +++ b/src/out/xapi/compartment.js @@ -6,13 +6,13 @@ export class meetingCompartment extends StorageCompartmentKV { } async addOrUpdateMeetingData(meeting_data) { - const { internal_meeting_id, context_registration, bbb_origin_server_name, + const { internal_meeting_id, context_registration, server_domain, planned_duration, create_time, meeting_name, xapi_enabled } = meeting_data; const payload = { internal_meeting_id, context_registration, - bbb_origin_server_name, + server_domain, planned_duration, create_time, meeting_name, diff --git a/src/out/xapi/templates.js b/src/out/xapi/templates.js index 01d9645..008018d 100644 --- a/src/out/xapi/templates.js +++ b/src/out/xapi/templates.js @@ -1,7 +1,14 @@ import { DateTime, Duration } from 'luxon'; +/** + * + * @param event + * @param meeting_data + * @param user_data + * @param poll_data + */ export default function getXAPIStatement(event, meeting_data, user_data = null, poll_data = null) { - const { bbb_origin_server_name, + const { server_domain, object_id, meeting_name, context_registration, @@ -12,36 +19,47 @@ export default function getXAPIStatement(event, meeting_data, user_data = null, const planned_duration_ISO = Duration.fromObject({ minutes: planned_duration }).toISO(); const create_time_ISO = DateTime.fromMillis(create_time).toUTC().toISO(); - const event_ts = event.data.event.ts; - - if (event.data.id == 'meeting-created' - || event.data.id == 'meeting-ended' - || event.data.id == 'user-joined' - || event.data.id == 'user-left' - || event.data.id == 'user-audio-voice-enabled' - || event.data.id == 'user-audio-voice-disabled' - || event.data.id == "user-audio-muted" - || event.data.id == "user-audio-unmuted" - || event.data.id == 'user-cam-broadcast-start' - || event.data.id == 'user-cam-broadcast-end' - || event.data.id == 'meeting-screenshare-started' - || event.data.id == 'meeting-screenshare-stopped' - || event.data.id == 'chat-group-message-sent' - || event.data.id == 'poll-started' - || event.data.id == 'poll-responded') { + const eventId = event.data.id; + const eventTs = event.data.event.ts; + + const session_parent = [ + { + "id": `https://${server_domain}/xapi/activities/${object_id}`, + "definition": { + "type": "https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom" + } + } + ] + + if (eventId == 'meeting-created' + || eventId == 'meeting-ended' + || eventId == 'user-joined' + || eventId == 'user-left' + || eventId == 'user-audio-voice-enabled' + || eventId == 'user-audio-voice-disabled' + || eventId == "user-audio-muted" + || eventId == "user-audio-unmuted" + || eventId == 'user-cam-broadcast-start' + || eventId == 'user-cam-broadcast-end' + || eventId == 'meeting-screenshare-started' + || eventId == 'meeting-screenshare-stopped' + || eventId == 'chat-group-message-sent' + || eventId == 'poll-started' + || eventId == 'poll-responded' + || eventId == 'user-raise-hand-changed') { const verbMappings = { 'meeting-created': 'http://adlnet.gov/expapi/verbs/initialized', 'meeting-ended': 'http://adlnet.gov/expapi/verbs/terminated', 'user-joined': 'http://activitystrea.ms/join', 'user-left': 'http://activitystrea.ms/leave', - 'user-audio-voice-enabled': 'http://adlnet.gov/expapi/verbs/interacted', - 'user-audio-voice-disabled': 'http://adlnet.gov/expapi/verbs/interacted', - 'user-audio-muted': 'http://adlnet.gov/expapi/verbs/interacted', - 'user-audio-unmuted': 'http://adlnet.gov/expapi/verbs/interacted', - 'user-cam-broadcast-start': 'http://adlnet.gov/expapi/verbs/interacted', - 'user-cam-broadcast-end': 'http://adlnet.gov/expapi/verbs/interacted', - 'meeting-screenshare-started': 'http://adlnet.gov/expapi/verbs/interacted', - 'meeting-screenshare-stopped': 'http://adlnet.gov/expapi/verbs/interacted', + 'user-audio-voice-enabled': 'http://activitystrea.ms/start', + 'user-audio-voice-disabled': 'https://w3id.org/xapi/virtual-classroom/verbs/stopped', + 'user-audio-muted': 'https://w3id.org/xapi/virtual-classroom/verbs/stopped', + 'user-audio-unmuted': 'http://activitystrea.ms/start', + 'user-cam-broadcast-start': 'http://activitystrea.ms/start', + 'user-cam-broadcast-end': 'https://w3id.org/xapi/virtual-classroom/verbs/stopped', + 'meeting-screenshare-started': 'http://activitystrea.ms/share', + 'meeting-screenshare-stopped': 'http://activitystrea.ms/unshare', 'chat-group-message-sent': 'https://w3id.org/xapi/acrossx/verbs/posted', 'poll-started': 'http://adlnet.gov/expapi/verbs/asked', 'poll-responded': 'http://adlnet.gov/expapi/verbs/answered', @@ -52,14 +70,14 @@ export default function getXAPIStatement(event, meeting_data, user_data = null, "actor": { "account": { "name": user_data?.user_name || "", - "homePage": `https://${bbb_origin_server_name}` + "homePage": `https://${server_domain}` } }, "verb": { - "id": verbMappings.hasOwnProperty(event.data.id) ? verbMappings[event.data.id] : null + "id": Object.prototype.hasOwnProperty.call(verbMappings, eventId) ? verbMappings[eventId] : null }, "object": { - "id": `https://${bbb_origin_server_name}/xapi/activities/${object_id}`, + "id": `https://${server_domain}/xapi/activities/${object_id}`, "definition": { "type": "https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom", "name": { @@ -83,32 +101,32 @@ export default function getXAPIStatement(event, meeting_data, user_data = null, "https://w3id.org/xapi/cmi5/context/extensions/sessionid": session_id } }, - "timestamp": DateTime.fromMillis(event_ts).toUTC().toISO() + "timestamp": DateTime.fromMillis(eventTs).toUTC().toISO() } // Custom 'meeting-created' attributes - if (event.data.id == 'meeting-created') { + if (eventId == 'meeting-created') { statement.context.extensions["http://id.tincanapi.com/extension/planned-duration"] = planned_duration_ISO statement.timestamp = create_time_ISO; } // Custom 'meeting-ended' attributes - else if (event.data.id == 'meeting-ended') { + else if (eventId == 'meeting-ended') { statement.context.extensions["http://id.tincanapi.com/extension/planned-duration"] = planned_duration_ISO statement.result = { - "duration": Duration.fromMillis(event_ts - create_time).toISO() + "duration": Duration.fromMillis(eventTs - create_time).toISO() } } // Custom attributes for multiple interactions - else if (event.data.id == 'user-audio-voice-enabled' - || event.data.id == 'user-audio-voice-disabled' - || event.data.id == "user-audio-muted" - || event.data.id == "user-audio-unmuted" - || event.data.id == 'user-cam-broadcast-start' - || event.data.id == 'user-cam-broadcast-end' - || event.data.id == 'meeting-screenshare-started' - || event.data.id == 'meeting-screenshare-stopped') { + else if (eventId == 'user-audio-voice-enabled' + || eventId == 'user-audio-voice-disabled' + || eventId == "user-audio-muted" + || eventId == "user-audio-unmuted" + || eventId == 'user-cam-broadcast-start' + || eventId == 'user-cam-broadcast-end' + || eventId == 'meeting-screenshare-started' + || eventId == 'meeting-screenshare-stopped') { const extension = { "user-audio-voice-enabled": "micro-activated", @@ -119,7 +137,7 @@ export default function getXAPIStatement(event, meeting_data, user_data = null, "user-cam-broadcast-end": "camera-activated", "meeting-screenshare-started": "screen-shared", "meeting-screenshare-stopped": "screen-shared", - }[event.data.id] + }[eventId] const extension_uri = `https://w3id.org/xapi/virtual-classroom/extensions/${extension}`; @@ -132,42 +150,43 @@ export default function getXAPIStatement(event, meeting_data, user_data = null, "user-cam-broadcast-end": "false", "meeting-screenshare-started": "true", "meeting-screenshare-stopped": "false", - }[event.data.id] + }[eventId] statement.context.extensions[extension_uri] = extension_enabled; + // TODO: implement new format for multimedia statements + // statement.context.contextActivities.parent = session_parent; } // Custom 'user-raise-hand-changed' attributes - else if (event.data.id == 'user-raise-hand-changed') { - const extension_uri = 'https://w3id.org/xapi/virtual-classroom/extensions/hand-raised'; - const extension_enabled = event.data.attributes.user["raise-hand"]; - statement.context.extensions[extension_uri] = extension_enabled; + else if (eventId == 'user-raise-hand-changed') { + const raisedHandVerb = "https://w3id.org/xapi/virtual-classroom/verbs/reacted"; + const loweredHandVerb = "https://w3id.org/xapi/virtual-classroom/verbs/unreacted"; + const isRaiseHand = event.data.attributes.user["raise-hand"]; + statement.verb = isRaiseHand ? raisedHandVerb : loweredHandVerb; + statement.result = { + "extensions": { + "https://w3id.org/xapi/virtual-classroom/extensions/emoji": "U+1F590" + } + } } // Custom 'chat-group-message-sent' attributes - else if (event.data.id == 'chat-group-message-sent') { + else if (eventId == 'chat-group-message-sent') { statement.object = { - "id": `https://${bbb_origin_server_name}/xapi/activities/${user_data?.msg_object_id}`, + "id": `https://${server_domain}/xapi/activities/${user_data?.msg_object_id}`, "definition": { "type": "https://w3id.org/xapi/acrossx/activities/message" } } - statement.context.contextActivities.parent = [ - { - "id": `https://${bbb_origin_server_name}/xapi/activities/${object_id}`, - "definition": { - "type": "https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom" - } - } - ] + statement.context.contextActivities.parent = session_parent; statement.timestamp = user_data?.time; } // Custom 'poll-started' and 'poll-responded' attributes - else if (event.data.id == 'poll-started' || event.data.id == 'poll-responded') { + else if (eventId == 'poll-started' || eventId == 'poll-responded') { statement.object = { - "id": `https://${bbb_origin_server_name}/xapi/activities/${poll_data?.object_id}`, + "id": `https://${server_domain}/xapi/activities/${poll_data?.object_id}`, "definition": { "description": { "en": poll_data?.question, @@ -178,15 +197,8 @@ export default function getXAPIStatement(event, meeting_data, user_data = null, } } - statement.context.contextActivities.parent = [ - { - "id": `https://${bbb_origin_server_name}/xapi/activities/${object_id}`, - "definition": { - "type": "https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom" - } - } - ] - if (event.data.id == 'poll-responded') { + statement.context.contextActivities.parent = session_parent; + if (eventId == 'poll-responded') { statement.result = { "response": event.data.attributes.poll.answerIds.join(','), } @@ -195,4 +207,5 @@ export default function getXAPIStatement(event, meeting_data, user_data = null, return statement } + else return null; } diff --git a/src/out/xapi/xapi.js b/src/out/xapi/xapi.js index 585197c..898461f 100644 --- a/src/out/xapi/xapi.js +++ b/src/out/xapi/xapi.js @@ -13,6 +13,24 @@ export default class XAPI { this.meetingStorage = meetingStorage; this.userStorage = userStorage; this.pollStorage = pollStorage; + this.validEvents = [ + 'chat-group-message-sent', + 'meeting-created', + 'meeting-ended', + 'meeting-screenshare-started', + 'meeting-screenshare-stopped', + 'poll-started', + 'poll-responded', + 'user-audio-muted', + 'user-audio-unmuted', + 'user-audio-voice-disabled', + 'user-audio-voice-enabled', + 'user-joined', + 'user-left', + 'user-cam-broadcast-end', + 'user-cam-broadcast-start', + 'user-raise-hand-changed' + ] } async postToLRS(statement) { @@ -45,6 +63,10 @@ export default class XAPI { } async onEvent(event, raw) { + const eventId = event.data.id; + + if (this.validEvents.indexOf(eventId) <= -1) return Promise.resolve + const meeting_data = { internal_meeting_id: event.data.attributes.meeting["internal-meeting-id"], external_meeting_id: event.data.attributes.meeting["external-meeting-id"], @@ -65,9 +87,8 @@ export default class XAPI { return new Promise(async (resolve, reject) => { // if meeting-created event, set meeting_data on redis - if (event.data.id == "meeting-created") { - const serverDomain = this.config.server.domain; - meeting_data.bbb_origin_server_name = serverDomain; + if (eventId == "meeting-created") { + meeting_data.server_domain = this.config.server.domain; meeting_data.planned_duration = event.data.attributes.meeting.duration; meeting_data.create_time = event.data.attributes.meeting["create-time"]; meeting_data.meeting_name = event.data.attributes.meeting.name; @@ -110,12 +131,12 @@ export default class XAPI { return reject(); } - if (event.data.id == "meeting-ended") { + if (eventId == "meeting-ended") { resolve(); XAPIStatement = getXAPIStatement(event, meeting_data); } // if user-joined event, set user_data on redis - else if (event.data.id == "user-joined") { + else if (eventId == "user-joined") { const user_data = { internal_user_id: event.data.attributes.user["internal-user-id"], user_name: event.data.attributes.user.name, @@ -130,20 +151,20 @@ export default class XAPI { } // if not user-joined user event, read user_data on redis else if ( - event.data.id == "user-left" || - event.data.id == "user-audio-voice-enabled" || - event.data.id == "user-audio-voice-disabled" || - event.data.id == "user-audio-muted" || - event.data.id == "user-audio-unmuted" || - event.data.id == "user-cam-broadcast-start" || - event.data.id == "user-cam-broadcast-end" || - event.data.id == "meeting-screenshare-started" || - event.data.id == "meeting-screenshare-stopped" || - event.data.id == "user-raise-hand-changed" + eventId == "user-left" || + eventId == "user-audio-voice-enabled" || + eventId == "user-audio-voice-disabled" || + eventId == "user-audio-muted" || + eventId == "user-audio-unmuted" || + eventId == "user-cam-broadcast-start" || + eventId == "user-cam-broadcast-end" || + eventId == "meeting-screenshare-started" || + eventId == "meeting-screenshare-stopped" || + eventId == "user-raise-hand-changed" ) { resolve(); // If mic is not enabled in "user-audio-voice-enabled" event, do not send statement - if (event.data.id == "user-audio-voice-enabled" && + if (eventId == "user-audio-voice-enabled" && (event.data.attributes.user["listening-only"] == true || event.data.attributes.user.muted == true || event.data.attributes.user["sharing-mic"] == false)) { @@ -160,7 +181,7 @@ export default class XAPI { } XAPIStatement = getXAPIStatement(event, meeting_data, user_data); // Chat message - } else if (event.data.id == "chat-group-message-sent") { + } else if (eventId == "chat-group-message-sent") { resolve(); const user_data = event.data.attributes["chat-message"]?.sender; const msg_key = `${user_data?.internal_user_id}_${user_data?.time}`; @@ -168,10 +189,10 @@ export default class XAPI { XAPIStatement = getXAPIStatement(event, meeting_data, user_data); // Poll events } else if ( - event.data.id == "poll-started" || - event.data.id == "poll-responded" + eventId == "poll-started" || + eventId == "poll-responded" ) { - if (event.data.id == "poll-responded") { + if (eventId == "poll-responded") { resolve(); } const internal_user_id = @@ -182,7 +203,7 @@ export default class XAPI { const object_id = uuidv5(event.data.attributes.poll.id, uuid_namespace); let poll_data; - if (event.data.id == "poll-started") { + if (eventId == "poll-started") { var choices = event.data.attributes.poll.answers.map((a) => { return { id: a.id.toString(), description: { en: a.key } }; }); @@ -198,7 +219,7 @@ export default class XAPI { } catch (error) { return reject(error); } - } else if (event.data.id == "poll-responded") { + } else if (eventId == "poll-responded") { poll_data = object_id ? await this.pollStorage.getPollData(object_id) : null; From 266b92740a0cbb960ea4d614b2d9aab56ca6ee20 Mon Sep 17 00:00:00 2001 From: mp Date: Thu, 19 Oct 2023 17:35:56 -0300 Subject: [PATCH 074/154] Implemented new media xAPI templates --- src/out/xapi/compartment.js | 192 ++++++++++++++++++------------------ src/out/xapi/templates.js | 44 ++++----- src/out/xapi/xapi.js | 28 +++--- 3 files changed, 131 insertions(+), 133 deletions(-) diff --git a/src/out/xapi/compartment.js b/src/out/xapi/compartment.js index a2b6126..490ccd1 100644 --- a/src/out/xapi/compartment.js +++ b/src/out/xapi/compartment.js @@ -1,104 +1,108 @@ import { StorageCompartmentKV } from '../../db/redis/base-storage.js'; export class meetingCompartment extends StorageCompartmentKV { - constructor(client, prefix, setId, options = {}) { - super(client, prefix, setId, options); - } - - async addOrUpdateMeetingData(meeting_data) { - const { internal_meeting_id, context_registration, server_domain, - planned_duration, create_time, meeting_name, xapi_enabled } = meeting_data; - - const payload = { - internal_meeting_id, - context_registration, - server_domain, - planned_duration, - create_time, - meeting_name, - xapi_enabled, - }; - - const mapping = await this.save(payload, { - alias: internal_meeting_id, - }); - this.logger.info(`added meeting data to the list ${internal_meeting_id}: ${mapping.print()}`); - - return mapping; - } - - async getMeetingData(internal_meeting_id) { - const meeting_data = this.findByField('internal_meeting_id', internal_meeting_id); - return (meeting_data != null ? meeting_data.payload : undefined); - } - - // Initializes global methods for this model. - initialize() { - return; - } + constructor(client, prefix, setId, options = {}) { + super(client, prefix, setId, options); + } + + async addOrUpdateMeetingData(meeting_data) { + const { internal_meeting_id, context_registration, server_domain, + planned_duration, create_time, meeting_name, xapi_enabled } = meeting_data; + + const payload = { + internal_meeting_id, + context_registration, + server_domain, + planned_duration, + create_time, + meeting_name, + xapi_enabled, + }; + + const mapping = await this.save(payload, { + alias: internal_meeting_id, + }); + this.logger.info(`added meeting data to the list ${internal_meeting_id}: ${mapping.print()}`); + + return mapping; + } + + async getMeetingData(internal_meeting_id) { + const meeting_data = this.findByField('internal_meeting_id', internal_meeting_id); + return (meeting_data != null ? meeting_data.payload : undefined); + } + + // Initializes global methods for this model. + initialize() { + return; + } } export class userCompartment extends StorageCompartmentKV { - constructor(client, prefix, setId, options = {}) { - super(client, prefix, setId, options); - } - - async addOrUpdateUserData(user_data) { - const { internal_user_id, user_name } = user_data; - - const payload = { - internal_user_id, - user_name, - }; - - const mapping = await this.save(payload, { - alias: internal_user_id, - }); - this.logger.info(`added user data to the list ${internal_user_id}: ${mapping.print()}`); - - return mapping; - } - - async getUserData(internal_user_id) { - const user_data = this.findByField('internal_user_id', internal_user_id); - return (user_data != null ? user_data.payload : undefined); - } - - // Initializes global methods for this model. - initialize() { - return; - } + constructor(client, prefix, setId, options = {}) { + super(client, prefix, setId, options); + } + + async addOrUpdateUserData(user_data) { + const { internal_user_id, user_name, user_micro_object_id, + user_camera_object_id, user_screen_object_id } = user_data; + + const payload = { + internal_user_id, + user_name, + user_micro_object_id, + user_camera_object_id, + user_screen_object_id, + }; + + const mapping = await this.save(payload, { + alias: internal_user_id, + }); + this.logger.info(`added user data to the list ${internal_user_id}: ${mapping.print()}`); + + return mapping; + } + + async getUserData(internal_user_id) { + const user_data = this.findByField('internal_user_id', internal_user_id); + return (user_data != null ? user_data.payload : undefined); + } + + // Initializes global methods for this model. + initialize() { + return; + } } export class pollCompartment extends StorageCompartmentKV { - constructor(client, prefix, setId, options = {}) { - super(client, prefix, setId, options); - } - - async addOrUpdatePollData(poll_data) { - const { object_id, question, choices } = poll_data; - - const payload = { - object_id, - question, - choices, - }; - - const mapping = await this.save(payload, { - alias: object_id, - }); - this.logger.info(`added poll data to the list ${object_id}: ${mapping.print()}`); - - return mapping; - } - - async getPollData(object_id) { - const poll_data = this.findByField('object_id', object_id); - return (poll_data != null ? poll_data.payload : undefined); - } - - // Initializes global methods for this model. - initialize() { - return; - } + constructor(client, prefix, setId, options = {}) { + super(client, prefix, setId, options); + } + + async addOrUpdatePollData(poll_data) { + const { object_id, question, choices } = poll_data; + + const payload = { + object_id, + question, + choices, + }; + + const mapping = await this.save(payload, { + alias: object_id, + }); + this.logger.info(`added poll data to the list ${object_id}: ${mapping.print()}`); + + return mapping; + } + + async getPollData(object_id) { + const poll_data = this.findByField('object_id', object_id); + return (poll_data != null ? poll_data.payload : undefined); + } + + // Initializes global methods for this model. + initialize() { + return; + } } diff --git a/src/out/xapi/templates.js b/src/out/xapi/templates.js index 008018d..37d739f 100644 --- a/src/out/xapi/templates.js +++ b/src/out/xapi/templates.js @@ -128,33 +128,27 @@ export default function getXAPIStatement(event, meeting_data, user_data = null, || eventId == 'meeting-screenshare-started' || eventId == 'meeting-screenshare-stopped') { - const extension = { - "user-audio-voice-enabled": "micro-activated", - "user-audio-voice-disabled": "micro-activated", - "user-audio-muted": "micro-activated", - "user-audio-unmuted": "micro-activated", - "user-cam-broadcast-start": "camera-activated", - "user-cam-broadcast-end": "camera-activated", - "meeting-screenshare-started": "screen-shared", - "meeting-screenshare-stopped": "screen-shared", + const media = { + "user-audio-voice-enabled": "micro", + "user-audio-voice-disabled": "micro", + "user-audio-muted": "micro", + "user-audio-unmuted": "micro", + "user-cam-broadcast-start": "camera", + "user-cam-broadcast-end": "camera", + "meeting-screenshare-started": "screen", + "meeting-screenshare-stopped": "screen", }[eventId] - const extension_uri = `https://w3id.org/xapi/virtual-classroom/extensions/${extension}`; - - const extension_enabled = { - "user-audio-voice-enabled": "true", - "user-audio-voice-disabled": "false", - "user-audio-muted": "false", - "user-audio-unmuted": "true", - "user-cam-broadcast-start": "true", - "user-cam-broadcast-end": "false", - "meeting-screenshare-started": "true", - "meeting-screenshare-stopped": "false", - }[eventId] - - statement.context.extensions[extension_uri] = extension_enabled; - // TODO: implement new format for multimedia statements - // statement.context.contextActivities.parent = session_parent; + statement.object = { + "id": user_data?.[`user_${media}_object_id`], + "definition": { + "type": `https://w3id.org/xapi/virtual-classroom/activity-types/${media}`, + "name": { + "en": `${user_data?.user_name}'s ${media}` + } + } + }; + statement.context.contextActivities.parent = session_parent; } // Custom 'user-raise-hand-changed' attributes diff --git a/src/out/xapi/xapi.js b/src/out/xapi/xapi.js index 898461f..8faa63b 100644 --- a/src/out/xapi/xapi.js +++ b/src/out/xapi/xapi.js @@ -33,6 +33,10 @@ export default class XAPI { ] } + _uuid(payload) { + return uuidv5( payload, this.config.uuid_namespace ); + } + async postToLRS(statement) { const { lrs_endpoint, lrs_username, lrs_password } = this.config.lrs; @@ -72,16 +76,8 @@ export default class XAPI { external_meeting_id: event.data.attributes.meeting["external-meeting-id"], }; - const uuid_namespace = this.config.uuid_namespace; - - meeting_data.session_id = uuidv5( - meeting_data.internal_meeting_id, - uuid_namespace - ); - meeting_data.object_id = uuidv5( - meeting_data.external_meeting_id, - uuid_namespace - ); + meeting_data.session_id = this._uuid(meeting_data.internal_meeting_id); + meeting_data.object_id = this._uuid(meeting_data.external_meeting_id); let XAPIStatement = null; @@ -99,7 +95,7 @@ export default class XAPI { ).toFormat("yyyyMMdd"); const external_key = `${meeting_data.external_meeting_id}_${meeting_create_day}`; - meeting_data.context_registration = uuidv5(external_key, uuid_namespace); + meeting_data.context_registration = this._uuid(external_key); //set meeting_data on redis try { await this.meetingStorage.addOrUpdateMeetingData(meeting_data); @@ -137,9 +133,13 @@ export default class XAPI { } // if user-joined event, set user_data on redis else if (eventId == "user-joined") { + const internal_user_id = event.data.attributes.user["internal-user-id"]; const user_data = { - internal_user_id: event.data.attributes.user["internal-user-id"], + internal_user_id, user_name: event.data.attributes.user.name, + user_micro_object_id: this._uuid(internal_user_id + "_micro"), + user_camera_object_id: this._uuid(internal_user_id + "_camera"), + user_screen_object_id: this._uuid(internal_user_id + "_screen"), }; try { await this.userStorage.addOrUpdateUserData(user_data); @@ -185,7 +185,7 @@ export default class XAPI { resolve(); const user_data = event.data.attributes["chat-message"]?.sender; const msg_key = `${user_data?.internal_user_id}_${user_data?.time}`; - user_data.msg_object_id = uuidv5(msg_key, uuid_namespace); + user_data.msg_object_id = this._uuid(msg_key); XAPIStatement = getXAPIStatement(event, meeting_data, user_data); // Poll events } else if ( @@ -200,7 +200,7 @@ export default class XAPI { const user_data = internal_user_id ? await this.userStorage.getUserData(internal_user_id) : null; - const object_id = uuidv5(event.data.attributes.poll.id, uuid_namespace); + const object_id = this._uuid(event.data.attributes.poll.id); let poll_data; if (eventId == "poll-started") { From 5925659e83e389a703ec54dc557bc2dd3a079ffb Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Thu, 19 Oct 2023 22:59:41 -0300 Subject: [PATCH 075/154] fix(webhooks): properly interpret invalid response codes as failures --- src/out/webhooks/callback-emitter.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/out/webhooks/callback-emitter.js b/src/out/webhooks/callback-emitter.js index 8598842..9b36e98 100644 --- a/src/out/webhooks/callback-emitter.js +++ b/src/out/webhooks/callback-emitter.js @@ -156,7 +156,7 @@ export default class CallbackEmitter extends EventEmitter { } // consider 401 as success, because the callback worked but was denied by the recipient - const responseFailed = (response) => !(response.ok || response.status == 401); + const responseOk = (response) => response.ok || response.status === 401; const controller = new AbortController(); const abortTimeout = setTimeout(() => { controller.abort(); @@ -167,11 +167,12 @@ export default class CallbackEmitter extends EventEmitter { try { const response = await fetch(callbackURL, requestOptions); - if (responseFailed(response)) { + if (!responseOk(response)) { const failedResponseError = new Error( `Invalid response: ${response?.status || 'unknown'}` || 'unknown error', ); failedResponseError.code = response?.status; + throw failedResponseError; } this.logger.info(`successful callback call to: [${callbackURL}]`, { From b821bd1fa3cd03369997c4e98824fde705e8f593 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Thu, 19 Oct 2023 23:02:06 -0300 Subject: [PATCH 076/154] fix: send user-left events for trailing users when a meeting ends BBB meeting end events imply user left events for any trailing users. This is not a thing for output modules here (eg xapi), which expect user event chains to be completely described (ie an user join must have a compatible user-left). This commit spoofs user left events for remaining users in a meeting whenever a meeting-ended event is generated for that session. There's some extra data present in this spoofed event when compared to the original one, but it doesn't affect functionality at all - if anything, the original event will be extended with that extra info later on. This also fixes an issue with user DB mappings not being cleaned up on meeting end. --- src/db/redis/base-storage.js | 10 ++++++ src/db/redis/user-mapping.js | 6 ++++ src/process/event-processor.js | 66 ++++++++++++++++++++++++++++++---- 3 files changed, 75 insertions(+), 7 deletions(-) diff --git a/src/db/redis/base-storage.js b/src/db/redis/base-storage.js index c923136..2d8f3de 100644 --- a/src/db/redis/base-storage.js +++ b/src/db/redis/base-storage.js @@ -208,6 +208,16 @@ class StorageCompartmentKV { ); } + findAllWithField(field, value) { + const dupe = Object.keys(this.localStorage).filter(internal => { + return this.localStorage[internal] && this.localStorage[internal]?.payload[field] === value; + }).map(internal => { + return this.localStorage[internal]; + }); + + return [...new Set(dupe)]; + } + findByField(field, value) { if (field != null && value != null) { for (let internal in this.localStorage) { diff --git a/src/db/redis/user-mapping.js b/src/db/redis/user-mapping.js index 5924bb2..f57e303 100644 --- a/src/db/redis/user-mapping.js +++ b/src/db/redis/user-mapping.js @@ -41,6 +41,12 @@ class UserMappingCompartment extends StorageCompartmentKV { return (mapping != null ? mapping.payload?.internalMeetingID : undefined); } + async getUsersFromMeeting(internalMeetingID) { + const mappings = await this.findAllWithField('meetingId', internalMeetingID); + + return mappings != null ? mappings.map((mapping) => mapping.payload) : []; + } + getUser(internalUserID) { const mapping = this.findByField('internalUserID', internalUserID); return (mapping != null ? mapping.payload?.user : undefined); diff --git a/src/process/event-processor.js b/src/process/event-processor.js index 4e88754..7b658c5 100644 --- a/src/process/event-processor.js +++ b/src/process/event-processor.js @@ -60,6 +60,61 @@ export default class EventProcessor { return parsedEvent; } + // TODO move this to an event factory + // Spoofs a user left event for a given user when a meeting ends (so that the user + // is removed from the user mapping AND the user left event is sent to output modules). + // This is necessary because the user left event is not sent by BBB when a meeting ends. + _spoofUserLeftEvent(internalMeetingID, externalMeetingID, userData) { + if (userData == null || userData.user == null) { + Logger.warn('cannot spoof user left event, user is null'); + return; + } + + const spoofedUserLeft = { + data: { + "type": "event", + "id": "user-left", + "attributes":{ + "meeting":{ + "internal-meeting-id": internalMeetingID, + "external-meeting-id": externalMeetingID, + }, + "user": userData.user, + }, + "event":{ + "ts": Date.now() + } + } + }; + + this.processInputEvent(spoofedUserLeft); + } + + async _handleMeetingEndedEvent(event) { + const internalMeetingId = event.data.attributes.meeting["internal-meeting-id"]; + const externalMeetingId = event.data.attributes.meeting["external-meeting-id"]; + + try { + await IDMapping.get().removeMapping(internalMeetingId) + } catch (error) { + Logger.error(`error removing meeting mapping: ${error}`, { + error: error.stack, + event, + }); + } + + try { + const users = await UserMapping.get().getUsersFromMeeting(internalMeetingId); + users.forEach(user => this._spoofUserLeftEvent(internalMeetingId, externalMeetingId, user)); + await UserMapping.get().removeMappingWithMeetingId(internalMeetingId); + } catch (error) { + Logger.error(`error removing user mappings: ${error}`, { + error: error.stack, + event, + }); + } + } + processInputEvent(event) { try { const rawEvent = this._parseEvent(event); @@ -115,12 +170,7 @@ export default class EventProcessor { }); break; case "meeting-ended": - IDMapping.get().removeMapping(internalMeetingId).catch((error) => { - Logger.error(`error removing meeting mapping: ${error}`, { - error: error.stack, - event, - }); - }).finally(() => { + this._handleMeetingEndedEvent(outputEvent).finally(() => { this._notifyOutputModules(outputEvent, rawEvent); }); break; @@ -148,7 +198,9 @@ export default class EventProcessor { this.outputs.forEach((output) => { output.onEvent(message, raw).catch((error) => { Logger.error('error notifying output module', { - error: error.stack, + module: output.name, + error: error?.stack, + errorMessage: error?.message, event: message, raw, }); From 7cbb0b74147e098b77c05f5daeaab71243915794 Mon Sep 17 00:00:00 2001 From: mp Date: Fri, 20 Oct 2023 01:07:58 -0300 Subject: [PATCH 077/154] Added capability to extract base64 encoded lrs_payload from metadata --- src/out/xapi/compartment.js | 6 ++++-- src/out/xapi/xapi.js | 29 +++++++++++++++++++++++++---- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/out/xapi/compartment.js b/src/out/xapi/compartment.js index 490ccd1..bd6d3a9 100644 --- a/src/out/xapi/compartment.js +++ b/src/out/xapi/compartment.js @@ -6,8 +6,8 @@ export class meetingCompartment extends StorageCompartmentKV { } async addOrUpdateMeetingData(meeting_data) { - const { internal_meeting_id, context_registration, server_domain, - planned_duration, create_time, meeting_name, xapi_enabled } = meeting_data; + const { internal_meeting_id, context_registration, server_domain, planned_duration, + create_time, meeting_name, xapi_enabled, lrs_endpoint, lrs_token } = meeting_data; const payload = { internal_meeting_id, @@ -17,6 +17,8 @@ export class meetingCompartment extends StorageCompartmentKV { create_time, meeting_name, xapi_enabled, + lrs_endpoint, + lrs_token, }; const mapping = await this.save(payload, { diff --git a/src/out/xapi/xapi.js b/src/out/xapi/xapi.js index 8faa63b..9dc6040 100644 --- a/src/out/xapi/xapi.js +++ b/src/out/xapi/xapi.js @@ -37,9 +37,12 @@ export default class XAPI { return uuidv5( payload, this.config.uuid_namespace ); } - async postToLRS(statement) { - const { lrs_endpoint, lrs_username, lrs_password } = this.config.lrs; - + async postToLRS(statement, meeting_data) { + let { lrs_endpoint, lrs_username, lrs_password } = this.config.lrs; + if (meeting_data.lrs_endpoint !== ''){ + lrs_endpoint = meeting_data.lrs_endpoint; + } + const lrs_token = meeting_data.lrs_token; const headers = { Authorization: `Basic ${Buffer.from( lrs_username + ":" + lrs_password @@ -48,6 +51,10 @@ export default class XAPI { "X-Experience-API-Version": "1.0.0", }; + if (lrs_token !== ''){ + headers.Authorization = `Bearer ${lrs_token}` + } + const requestOptions = { method: "POST", body: JSON.stringify(statement), @@ -90,6 +97,20 @@ export default class XAPI { meeting_data.meeting_name = event.data.attributes.meeting.name; meeting_data.xapi_enabled = event.data.attributes.meeting.metadata?.["xapi-enabled"] !== 'false' ? 'true' : 'false'; + const lrs_payload = event.data.attributes.meeting.metadata?.["secret_lrs-payload"]; + let lrs_endpoint = ''; + let lrs_token = ''; + + // if lrs_payload exists, extracts lrs_endpoint and lrs_token from it + if (lrs_payload !== undefined){ + const payload_buffer = new Buffer.from(lrs_payload, 'base64'); + const payload_text = payload_buffer.toString('ascii'); + ({lrs_endpoint, lrs_token} = JSON.parse(payload_text)); + } + + meeting_data.lrs_endpoint = lrs_endpoint; + meeting_data.lrs_token = lrs_token; + const meeting_create_day = DateTime.fromMillis( meeting_data.create_time ).toFormat("yyyyMMdd"); @@ -245,7 +266,7 @@ export default class XAPI { } } if (XAPIStatement !== null && meeting_data.xapi_enabled === 'true') { - await this.postToLRS(XAPIStatement); + await this.postToLRS(XAPIStatement, meeting_data); } }); } From dcd5609f58ad057977058374c527ab874ad507f9 Mon Sep 17 00:00:00 2001 From: mp Date: Fri, 20 Oct 2023 09:12:31 -0300 Subject: [PATCH 078/154] updated secret-lrs-payload metadata name --- src/out/xapi/xapi.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/out/xapi/xapi.js b/src/out/xapi/xapi.js index 9dc6040..2d47794 100644 --- a/src/out/xapi/xapi.js +++ b/src/out/xapi/xapi.js @@ -97,7 +97,7 @@ export default class XAPI { meeting_data.meeting_name = event.data.attributes.meeting.name; meeting_data.xapi_enabled = event.data.attributes.meeting.metadata?.["xapi-enabled"] !== 'false' ? 'true' : 'false'; - const lrs_payload = event.data.attributes.meeting.metadata?.["secret_lrs-payload"]; + const lrs_payload = event.data.attributes.meeting.metadata?.["secret-lrs-payload"]; let lrs_endpoint = ''; let lrs_token = ''; From b04bcb7920af132a23ede792eb228307c66f2865 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Fri, 20 Oct 2023 09:12:38 -0300 Subject: [PATCH 079/154] fix(xapi): resolve onEvent promise when event is invalid --- src/out/xapi/xapi.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/out/xapi/xapi.js b/src/out/xapi/xapi.js index 2d47794..3ad29f1 100644 --- a/src/out/xapi/xapi.js +++ b/src/out/xapi/xapi.js @@ -76,7 +76,7 @@ export default class XAPI { async onEvent(event, raw) { const eventId = event.data.id; - if (this.validEvents.indexOf(eventId) <= -1) return Promise.resolve + if (this.validEvents.indexOf(eventId) <= -1) return Promise.resolve(); const meeting_data = { internal_meeting_id: event.data.attributes.meeting["internal-meeting-id"], From d14df54afdfe121116103eaa10959aaf12f18238 Mon Sep 17 00:00:00 2001 From: mp Date: Fri, 20 Oct 2023 10:30:02 -0300 Subject: [PATCH 080/154] (xapi) remove retrievable data from redis --- src/out/xapi/compartment.js | 9 ++------- src/out/xapi/xapi.js | 26 ++++++++++++++++++++------ 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/out/xapi/compartment.js b/src/out/xapi/compartment.js index bd6d3a9..a0ae51a 100644 --- a/src/out/xapi/compartment.js +++ b/src/out/xapi/compartment.js @@ -6,13 +6,12 @@ export class meetingCompartment extends StorageCompartmentKV { } async addOrUpdateMeetingData(meeting_data) { - const { internal_meeting_id, context_registration, server_domain, planned_duration, + const { internal_meeting_id, context_registration, planned_duration, create_time, meeting_name, xapi_enabled, lrs_endpoint, lrs_token } = meeting_data; const payload = { internal_meeting_id, context_registration, - server_domain, planned_duration, create_time, meeting_name, @@ -46,15 +45,11 @@ export class userCompartment extends StorageCompartmentKV { } async addOrUpdateUserData(user_data) { - const { internal_user_id, user_name, user_micro_object_id, - user_camera_object_id, user_screen_object_id } = user_data; + const { internal_user_id, user_name } = user_data; const payload = { internal_user_id, user_name, - user_micro_object_id, - user_camera_object_id, - user_screen_object_id, }; const mapping = await this.save(payload, { diff --git a/src/out/xapi/xapi.js b/src/out/xapi/xapi.js index 2d47794..092bc88 100644 --- a/src/out/xapi/xapi.js +++ b/src/out/xapi/xapi.js @@ -81,6 +81,7 @@ export default class XAPI { const meeting_data = { internal_meeting_id: event.data.attributes.meeting["internal-meeting-id"], external_meeting_id: event.data.attributes.meeting["external-meeting-id"], + server_domain: this.config.server.domain, }; meeting_data.session_id = this._uuid(meeting_data.internal_meeting_id); @@ -91,7 +92,6 @@ export default class XAPI { return new Promise(async (resolve, reject) => { // if meeting-created event, set meeting_data on redis if (eventId == "meeting-created") { - meeting_data.server_domain = this.config.server.domain; meeting_data.planned_duration = event.data.attributes.meeting.duration; meeting_data.create_time = event.data.attributes.meeting["create-time"]; meeting_data.meeting_name = event.data.attributes.meeting.name; @@ -158,9 +158,6 @@ export default class XAPI { const user_data = { internal_user_id, user_name: event.data.attributes.user.name, - user_micro_object_id: this._uuid(internal_user_id + "_micro"), - user_camera_object_id: this._uuid(internal_user_id + "_camera"), - user_screen_object_id: this._uuid(internal_user_id + "_screen"), }; try { await this.userStorage.addOrUpdateUserData(user_data); @@ -191,8 +188,8 @@ export default class XAPI { event.data.attributes.user["sharing-mic"] == false)) { return; } - const internal_user_id = - event.data.attributes.user?.["internal-user-id"]; + const internal_user_id = event.data.attributes.user?.["internal-user-id"]; + const user_data = internal_user_id ? await this.userStorage.getUserData(internal_user_id) : null; @@ -200,6 +197,23 @@ export default class XAPI { if (user_data === undefined) { return; } + const media = { + "user-audio-voice-enabled": "micro", + "user-audio-voice-disabled": "micro", + "user-audio-muted": "micro", + "user-audio-unmuted": "micro", + "user-cam-broadcast-start": "camera", + "user-cam-broadcast-end": "camera", + "meeting-screenshare-started": "screen", + "meeting-screenshare-stopped": "screen", + }[eventId] + + if (media !== undefined){ + user_data[`user_${media}_object_id`] = this._uuid(`${internal_user_id}_${media}`); + } + + user_data.user_camera_object_id = this._uuid(internal_user_id + "_camera"); + user_data.user_screen_object_id = this._uuid(internal_user_id + "_screen"); XAPIStatement = getXAPIStatement(event, meeting_data, user_data); // Chat message } else if (eventId == "chat-group-message-sent") { From db727815d0282ad2da82bc6a80cd6d4740b05e0b Mon Sep 17 00:00:00 2001 From: mp Date: Fri, 20 Oct 2023 10:34:11 -0300 Subject: [PATCH 081/154] (xapi) quick-fix: removed unecessary uuid calculation --- src/out/xapi/xapi.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/out/xapi/xapi.js b/src/out/xapi/xapi.js index 9d27a4d..40d6ad6 100644 --- a/src/out/xapi/xapi.js +++ b/src/out/xapi/xapi.js @@ -212,8 +212,6 @@ export default class XAPI { user_data[`user_${media}_object_id`] = this._uuid(`${internal_user_id}_${media}`); } - user_data.user_camera_object_id = this._uuid(internal_user_id + "_camera"); - user_data.user_screen_object_id = this._uuid(internal_user_id + "_screen"); XAPIStatement = getXAPIStatement(event, meeting_data, user_data); // Chat message } else if (eventId == "chat-group-message-sent") { From d0bcba25de5e026e23c8a547b087a8b3794956b1 Mon Sep 17 00:00:00 2001 From: mp Date: Fri, 20 Oct 2023 11:03:56 -0300 Subject: [PATCH 082/154] Added statement log when POST request fails --- src/out/xapi/xapi.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/out/xapi/xapi.js b/src/out/xapi/xapi.js index 40d6ad6..9eef93d 100644 --- a/src/out/xapi/xapi.js +++ b/src/out/xapi/xapi.js @@ -68,8 +68,11 @@ export default class XAPI { const { status } = response; const data = await response.json(); this.logger.debug("OutXAPI.res.status:", { status, data }); + if (status < 200 || status >= 400){ + this.logger.debug("OutXAPI.res.post_fail:", { statement }); + } } catch (err) { - this.logger.debug("OutXAPI.err:", err); + this.logger.debug("OutXAPI.res.err:", err); } } From abf907b9096f6ac1dce10f6ce4ea53e03b38e04c Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Fri, 20 Oct 2023 12:04:36 -0300 Subject: [PATCH 083/154] chore: change default file log location To a more agnostic location --- config/default.example.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/default.example.yml b/config/default.example.yml index 02d50d9..cf988c2 100644 --- a/config/default.example.yml +++ b/config/default.example.yml @@ -1,6 +1,6 @@ log: level: debug - filename: /var/log/bigbluebutton/bbb-webhooks.log + filename: /var/log/bbb-webhooks.log stdout: true prometheus: From 945a019ae1cb8a0eaf989b1dac6e32d600be1ab7 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Fri, 20 Oct 2023 13:05:00 -0300 Subject: [PATCH 084/154] chore: add bash support to Docker image --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index 5b2fdf6..b9a7dd1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,7 @@ FROM node:20-alpine +RUN apk add --no-cache bash + WORKDIR /app COPY package.json package-lock.json ./ From 07f84c89a8ec8c83d66d045e1cf729b693176993 Mon Sep 17 00:00:00 2001 From: mp Date: Fri, 20 Oct 2023 13:45:32 -0300 Subject: [PATCH 085/154] fix(xapi): object.id for media statements is now valid --- src/out/xapi/templates.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/out/xapi/templates.js b/src/out/xapi/templates.js index 37d739f..04fafbb 100644 --- a/src/out/xapi/templates.js +++ b/src/out/xapi/templates.js @@ -140,7 +140,7 @@ export default function getXAPIStatement(event, meeting_data, user_data = null, }[eventId] statement.object = { - "id": user_data?.[`user_${media}_object_id`], + "id": `https://${server_domain}/xapi/activities/${user_data?.[`user_${media}_object_id`]}`, "definition": { "type": `https://w3id.org/xapi/virtual-classroom/activity-types/${media}`, "name": { From 10b58fc627048a7c9648cd306d8cdb54dba291af Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Fri, 20 Oct 2023 14:13:41 -0300 Subject: [PATCH 086/154] feat(docker): add sample docker-compose for running locally built images --- docker-compose.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..9f44d5f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,17 @@ +version: '3' +services: + webhooks: + build: + context: ./ + dockerfile: Dockerfile + container_name: bbb-webhooks + restart: always + environment: + - NODE_CONFIG_DIR=/etc/bigbluebutton/bbb-webhooks.yml:/app/config + ports: + - "3005:3005" + - "3004:3004" + volumes: + - /etc/bigbluebutton/:/etc/bigbluebutton/ + - /var/log/bbb-webhooks/:/var/log/bbb-webhooks/ + network_mode: bridge From 628deb62933b279538f13ae52058b14900e2b06e Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Fri, 20 Oct 2023 14:20:53 -0300 Subject: [PATCH 087/154] docs: add dependency table --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index de98697..51d59c6 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,16 @@ This is a node.js application that listens for all events on BigBlueButton and s You can read the full documentation at: https://docs.bigbluebutton.org/development/webhooks +## Dependencies and pre-requisites + +There are a few dependencies that need to be installed before you can run this application. +Their minimum versions are listed below. + +| Dependency | Minimum version | +|-----------------------|:---------------------------:| +| Node.js | >= v18.x | +| npm | >= v7.x | +| Redis | >= v5.0 | ## Development @@ -49,7 +59,6 @@ If you are editing these permanent urls after they have already been committed t - `redis-cli flushall` - **_IMPORTANT:_** Running the above command clears the redis database entirely. This will result in all meetings, processing or not, to be cleared from the database, and may result in broken meetings currently processing. - ## Production Follow the commands below starting within the `bigbluebutton/bbb-webhooks` directory. From 0a37ed95f874c84983b9c14f2e616d7b768a4252 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Fri, 20 Oct 2023 15:42:35 -0300 Subject: [PATCH 088/154] fix(xapi): provide rejection reason on all scenarios +: tune error log to logger.error --- src/out/xapi/xapi.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/out/xapi/xapi.js b/src/out/xapi/xapi.js index 9eef93d..9280162 100644 --- a/src/out/xapi/xapi.js +++ b/src/out/xapi/xapi.js @@ -72,11 +72,11 @@ export default class XAPI { this.logger.debug("OutXAPI.res.post_fail:", { statement }); } } catch (err) { - this.logger.debug("OutXAPI.res.err:", err); + this.logger.error("OutXAPI.res.err:", err); } } - async onEvent(event, raw) { + async onEvent(event) { const eventId = event.data.id; if (this.validEvents.indexOf(eventId) <= -1) return Promise.resolve(); @@ -130,7 +130,7 @@ export default class XAPI { // Do not proceed if xapi_enabled === 'false' was passed in the metadata if (meeting_data.xapi_enabled === 'false') { - return reject(); + return reject(new Error('xapi is disabled for this meeting')); } XAPIStatement = getXAPIStatement(event, meeting_data); @@ -142,13 +142,13 @@ export default class XAPI { ); // Do not proceed if meeting_data is not found on the storage if (meeting_data_storage === undefined) { - return reject(); + return reject(new Error('meeting data not found')); } Object.assign(meeting_data, meeting_data_storage); // Do not proceed if xapi_enabled === 'false' was passed in the metadata if (meeting_data.xapi_enabled === 'false') { - return reject(); + return reject(new Error('xapi is disabled for this meeting')); } if (eventId == "meeting-ended") { From 13029f2e6964adb59d9cb00557e8a05cd7bd888f Mon Sep 17 00:00:00 2001 From: mp Date: Fri, 20 Oct 2023 15:47:32 -0300 Subject: [PATCH 089/154] fix(xapi) - correct error handlingwhen user_data or poll_data are not available --- src/out/xapi/xapi.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/out/xapi/xapi.js b/src/out/xapi/xapi.js index 9eef93d..15a56fc 100644 --- a/src/out/xapi/xapi.js +++ b/src/out/xapi/xapi.js @@ -195,7 +195,7 @@ export default class XAPI { const user_data = internal_user_id ? await this.userStorage.getUserData(internal_user_id) - : null; + : undefined; // Do not proceed if user_data is requested but not found on the storage if (user_data === undefined) { return; @@ -258,7 +258,7 @@ export default class XAPI { } else if (eventId == "poll-responded") { poll_data = object_id ? await this.pollStorage.getPollData(object_id) - : null; + : undefined; // Do not proceed if poll_data is requested but not found on the storage if (poll_data === undefined) { return; From f094c6c62678b9cb02fcedb921a3d1859e0369c6 Mon Sep 17 00:00:00 2001 From: mp Date: Fri, 20 Oct 2023 15:51:07 -0300 Subject: [PATCH 090/154] (xapi) - adopted 'name' instead of 'user_name' to keep user_data consistent with chat_msg events --- src/out/xapi/compartment.js | 4 ++-- src/out/xapi/templates.js | 4 ++-- src/out/xapi/xapi.js | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/out/xapi/compartment.js b/src/out/xapi/compartment.js index a0ae51a..665ec41 100644 --- a/src/out/xapi/compartment.js +++ b/src/out/xapi/compartment.js @@ -45,11 +45,11 @@ export class userCompartment extends StorageCompartmentKV { } async addOrUpdateUserData(user_data) { - const { internal_user_id, user_name } = user_data; + const { internal_user_id, name } = user_data; const payload = { internal_user_id, - user_name, + name, }; const mapping = await this.save(payload, { diff --git a/src/out/xapi/templates.js b/src/out/xapi/templates.js index 04fafbb..0cade8b 100644 --- a/src/out/xapi/templates.js +++ b/src/out/xapi/templates.js @@ -69,7 +69,7 @@ export default function getXAPIStatement(event, meeting_data, user_data = null, const statement = { "actor": { "account": { - "name": user_data?.user_name || "", + "name": user_data?.name || "", "homePage": `https://${server_domain}` } }, @@ -144,7 +144,7 @@ export default function getXAPIStatement(event, meeting_data, user_data = null, "definition": { "type": `https://w3id.org/xapi/virtual-classroom/activity-types/${media}`, "name": { - "en": `${user_data?.user_name}'s ${media}` + "en": `${user_data?.name}'s ${media}` } } }; diff --git a/src/out/xapi/xapi.js b/src/out/xapi/xapi.js index 8d9b732..0618e3e 100644 --- a/src/out/xapi/xapi.js +++ b/src/out/xapi/xapi.js @@ -160,7 +160,7 @@ export default class XAPI { const internal_user_id = event.data.attributes.user["internal-user-id"]; const user_data = { internal_user_id, - user_name: event.data.attributes.user.name, + name: event.data.attributes.user.name, }; try { await this.userStorage.addOrUpdateUserData(user_data); From 469028222e6173c8abf3a69be595438fab74ac95 Mon Sep 17 00:00:00 2001 From: mp Date: Fri, 20 Oct 2023 16:00:40 -0300 Subject: [PATCH 091/154] (xapi) when xapi_enabled==false the Promise is now resolved instead of rejected --- src/out/xapi/xapi.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/out/xapi/xapi.js b/src/out/xapi/xapi.js index 0618e3e..2a07fa6 100644 --- a/src/out/xapi/xapi.js +++ b/src/out/xapi/xapi.js @@ -130,7 +130,7 @@ export default class XAPI { // Do not proceed if xapi_enabled === 'false' was passed in the metadata if (meeting_data.xapi_enabled === 'false') { - return reject(new Error('xapi is disabled for this meeting')); + return resolve(); } XAPIStatement = getXAPIStatement(event, meeting_data); From 3e5a711d86713dc71dd596e97e573be63ad2e989 Mon Sep 17 00:00:00 2001 From: mp Date: Fri, 20 Oct 2023 16:14:57 -0300 Subject: [PATCH 092/154] (xapi) when xapi_enabled==false the Promise is now resolved instead of rejected (again) --- src/out/xapi/xapi.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/out/xapi/xapi.js b/src/out/xapi/xapi.js index 2a07fa6..0891ece 100644 --- a/src/out/xapi/xapi.js +++ b/src/out/xapi/xapi.js @@ -148,7 +148,7 @@ export default class XAPI { // Do not proceed if xapi_enabled === 'false' was passed in the metadata if (meeting_data.xapi_enabled === 'false') { - return reject(new Error('xapi is disabled for this meeting')); + return resolve(); } if (eventId == "meeting-ended") { From 8044891968773e4957470e6f8809aee8cfef1e4e Mon Sep 17 00:00:00 2001 From: mp Date: Fri, 20 Oct 2023 19:04:12 -0300 Subject: [PATCH 093/154] Added xAPI module README.md --- src/out/xapi/README.md | 76 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 src/out/xapi/README.md diff --git a/src/out/xapi/README.md b/src/out/xapi/README.md new file mode 100644 index 0000000..89fb573 --- /dev/null +++ b/src/out/xapi/README.md @@ -0,0 +1,76 @@ +# xAPI output module +This is a bbb-webhooks output module responsible for exporting events from BigBlueButton (BBB) to a Learning Record Store (LRS) using the xAPI pattern. + +# YAML configuration +This module is set and configured in the `default.yml` file using the following YAML structure: + +```yml +modules: + ../out/xapi/index.js: + type: out + config: + lrs: + lrs_endpoint: https://your_lrs.endpoint + lrs_username: user + lrs_password: pass + uuid_namespace: 01234567-89ab-cdef-0123-456789abcdef + redis: + keys: + meetings: bigbluebutton:webhooks:xapi:meetings + meetingPrefix: bigbluebutton:webhooks:xapi:meeting + users: bigbluebutton:webhooks:xapi:users + userPrefix: bigbluebutton:webhooks:xapi:user + polls: bigbluebutton:webhooks:xapi:polls + pollPrefix: bigbluebutton:webhooks:xapi:poll +``` +## LRS Configuration +The LRS configuration section specifies the details required to connect to the Learning Record Store (LRS) where the xAPI statements will be sent. Here are the configuration parameters: + +- **lrs_endpoint**: The URL of the LRS where xAPI statements will be sent. +- **lrs_username**: The username or API key used to authenticate with the LRS. +- **lrs_password**: The password or API key secret used for authentication with the LRS. + +This is the standalone strategy to connect to the LRS, because it does not support multi-tenancy. Connection to the LRS with multi-tenancy could be achieved by sending relevant metadata (explained below), and if that method is used, these parameters are ignored, and thus are not required. + +## UUID Namespace +The **uuid_namespace** parameter is used to define a unique identifier namespace for generating UUIDs. This namespace helps ensure uniqueness when generating identifiers for xAPI statements, and should be kept safe. + +## Redis Configuration +The Redis configuration section defines the keys and key prefixes used for storing and retrieving data related to BBB events. These keys and prefixes are associated with different types of events within the application: + +- **meetings**: Key for storing information about meetings. +- **meetingPrefix**: Prefix for keys related to specific meeting events. +- **users**: Key for storing information about users. +- **userPrefix**: Prefix for keys related to specific user events. +- **polls**: Key for storing information about polls or surveys. +- **pollPrefix**: Prefix for keys related to specific poll events. + +# BBB event metadata +You have the option to set relevant metadata when creating a meeting in Big Blue Button (BBB). This metadata allows you to control the generation and sending of xAPI events to the Learning Record Store (LRS). + +## Supported Metadata Parameters +### meta_xapi-enabled +- **Description**: This parameter controls whether xAPI events are generated and sent to the LRS for a specific meeting. +- **Values**: true or false +- **Default Value**: true (xAPI events are enabled by default) + +If you set `meta_xapi-enabled` to false, no xAPI events will be generated or sent to the LRS for that particular meeting. This provides the flexibility to choose which meetings should be tracked using xAPI. + +### meta_secret-lrs-payload +- **Description**: This parameter allows you to specify the credentials and endpoint of the Learning Record Store (LRS) where the xAPI events will be sent. The payload is a Base64-encoded string representing a JSON object. +- **Value Format**: Base64-encoded JSON object **(for now)** +- **JSON Payload Structure**: +```json +{ + "lrs_endpoint": "https://lrs1.example.com", + "lrs_token": "AAF32423SDF5345" +} +``` +- **Example**: +``` +meta_secret-lrs-payload: YmFzZTY0IGVuY29kaW5nIHNjaGVtZXMgYXJlIGNvbW1vbmx5IHVzZWQgd2hlbiB0aGVtZSBkYXRhIG5lZWRzIHRvIGJlIHNlcnZlciB3aXRob3V0IG1vZGlmaWNhdGlvbiBkdXJpbmcgdHJhY2tlci4gVGhpcyBlbmNvZGluZyBwYXJ0ZW50IHNlcnZlciB3aWxsIGJlIHN0b3JlZCBhbmQgdHJhbnNmZXJyZWQgb3ZlciBtZWRpYSB0aGF0IGFyZSBkZXNpZ25lZCB0byBkZWFsIHdpdGggdGV4dC4gVGhpcyBwYXJ0... +``` + +The `meta_secret-lrs-payload` parameter allows you to securely define the LRS endpoint and authentication token for each meeting. It ensures that xAPI events generated during the meeting are sent to the correct LRS. + +This strategy to connect to the LRS supports multi-tenancy, as each meeting could point to a different LRS endpoint. If this parameter is present in the metadata, the endpoint and credentials contained in the YML file (lrs_* parameters) are ignored and thus not required. From af8b60bc6eb14fdf074c4ce97060c0c660491ce2 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Tue, 24 Oct 2023 13:25:36 -0300 Subject: [PATCH 094/154] chore: replace logger, Winston -> Pino Winston@3 is too problematic for a logging library, experiment with Pino for a while to see how it goes --- package-lock.json | 581 +++++++++++++++++++++++++++---------------- package.json | 5 +- src/common/logger.js | 199 +++++---------- 3 files changed, 427 insertions(+), 358 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4030d81..4f56386 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,10 +14,10 @@ "js-yaml": "^4.1.0", "luxon": "^3.4.3", "node-fetch": "^3.3.2", + "pino": "^8.16.1", "prom-client": "^14.2.0", "redis": "^4.6.8", - "uuid": "^9.0.1", - "winston": "^3.10.0" + "uuid": "^9.0.1" }, "devDependencies": { "body-parser": "^1.20.2", @@ -29,6 +29,7 @@ "mocha": "^9.2.2", "nock": "^13.3.3", "nodemon": "^3.0.1", + "pino-pretty": "^10.2.3", "sinon": "^12.0.1", "supertest": "^3.4.2" }, @@ -57,24 +58,6 @@ "node": ">=6.0.0" } }, - "node_modules/@colors/colors": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", - "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/@dabh/diagnostics": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", - "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", - "dependencies": { - "colorspace": "1.1.x", - "enabled": "2.0.x", - "kuler": "^2.0.0" - } - }, "node_modules/@es-joy/jsdoccomment": { "version": "0.40.1", "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.40.1.tgz", @@ -464,11 +447,6 @@ "integrity": "sha512-T5k6kTXak79gwmIOaDF2UUQXFbnBE0zBUzF20pz7wDYu0RQMzWg+Ml/Pz50214NsFHBITkoi5VtdjFZnJ2ijjA==", "dev": true }, - "node_modules/@types/triple-beam": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.3.tgz", - "integrity": "sha512-6tOUG+nVHn0cJbVp25JFayS5UE6+xlbcNF9Lo9mU7U0zk3zeUShZied4YEQZjy1JBF043FSkdXw8YkUJuVtB5g==" - }, "node_modules/@ungap/promise-all-settled": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", @@ -481,6 +459,17 @@ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "dev": true }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -703,17 +692,20 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/async": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", - "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -731,6 +723,25 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -803,6 +814,29 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/builtin-modules": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", @@ -958,15 +992,6 @@ "node": ">=0.10.0" } }, - "node_modules/color": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", - "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", - "dependencies": { - "color-convert": "^1.9.3", - "color-string": "^1.6.0" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -982,38 +1007,14 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "node_modules/color/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, - "node_modules/colorspace": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", - "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", - "dependencies": { - "color": "^3.1.3", - "text-hex": "1.0.x" - } + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true }, "node_modules/combined-stream": { "version": "1.0.8", @@ -1136,6 +1137,15 @@ "node": ">= 12" } }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -1259,11 +1269,6 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, - "node_modules/enabled": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", - "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" - }, "node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -1272,6 +1277,15 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/entities": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", @@ -1743,6 +1757,22 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/express": { "version": "4.18.2", "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", @@ -1827,6 +1857,12 @@ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "dev": true }, + "node_modules/fast-copy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.1.tgz", + "integrity": "sha512-Knr7NOtK3HWRYGtHoJrjkaWepqT8thIVGAwt0p0aUs1zqkAzXZV4vo9fFNwyb5fcqK1GKYFYxldQdIDVKhUAfA==", + "dev": true + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -1845,6 +1881,20 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-redact": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.3.0.tgz", + "integrity": "sha512-6T5V1QK1u4oF+ATxs1lWUmlEk6P2T9HqJG3e2DnHOdVgZy2rFJBoEnrIedcTXlkAHU/zKC+7KETJ+KGGKwxgMQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true + }, "node_modules/fast-xml-parser": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.3.2.tgz", @@ -1876,11 +1926,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fecha": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", - "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" - }, "node_modules/fetch-blob": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", @@ -1989,11 +2034,6 @@ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, - "node_modules/fn.name": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", - "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" - }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -2351,6 +2391,30 @@ "he": "bin/he" } }, + "node_modules/help-me": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-4.2.0.tgz", + "integrity": "sha512-TAOnTB8Tz5Dw8penUuzHVrKNKlCIbwwbHnXraNJxPwf8LRtE2HlM84RYuezMFcwOJmoYOCWVDyJ8TQGxn9PgxA==", + "dev": true, + "dependencies": { + "glob": "^8.0.0", + "readable-stream": "^3.6.0" + } + }, + "node_modules/help-me/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -2377,6 +2441,25 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -2511,11 +2594,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" - }, "node_modules/is-bigint": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", @@ -2722,17 +2800,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-string": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", @@ -2814,6 +2881,15 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -2940,11 +3016,6 @@ "graceful-fs": "^4.1.9" } }, - "node_modules/kuler": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", - "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" - }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -3025,24 +3096,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/logform": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.5.1.tgz", - "integrity": "sha512-9FyqAm9o9NKKfiAKfZoYo9bGXXuwMkxQiQttkT4YjjVtQVIQtK6LmVtlxmCaFswo6N4AfEkHqZTV0taDtPotNg==", - "dependencies": { - "@colors/colors": "1.5.0", - "@types/triple-beam": "^1.3.2", - "fecha": "^4.2.0", - "ms": "^2.1.1", - "safe-stable-stringify": "^2.3.1", - "triple-beam": "^1.3.0" - } - }, - "node_modules/logform/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -3694,6 +3747,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -3713,14 +3774,6 @@ "wrappy": "1" } }, - "node_modules/one-time": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", - "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", - "dependencies": { - "fn.name": "1.x.x" - } - }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -3838,6 +3891,114 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pino": { + "version": "8.16.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.16.1.tgz", + "integrity": "sha512-3bKsVhBmgPjGV9pyn4fO/8RtoVDR8ssW1ev819FsRXlRNgW8gR/9Kx+gCK4UPWd4JjrRDLWpzd/pb1AyWm3MGA==", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "v1.1.0", + "pino-std-serializers": "^6.0.0", + "process-warning": "^2.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^3.7.0", + "thread-stream": "^2.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.1.0.tgz", + "integrity": "sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==", + "dependencies": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + } + }, + "node_modules/pino-abstract-transport/node_modules/readable-stream": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz", + "integrity": "sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/pino-abstract-transport/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/pino-pretty": { + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-10.2.3.tgz", + "integrity": "sha512-4jfIUc8TC1GPUfDyMSlW1STeORqkoxec71yhxIpLDQapUu8WOuoz2TTCoidrIssyz78LZC69whBMPIKCMbi3cw==", + "dev": true, + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^3.0.0", + "fast-safe-stringify": "^2.1.1", + "help-me": "^4.0.1", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^1.0.0", + "pump": "^3.0.0", + "readable-stream": "^4.0.0", + "secure-json-parse": "^2.4.0", + "sonic-boom": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-pretty/node_modules/readable-stream": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz", + "integrity": "sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==", + "dev": true, + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/pino-pretty/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", + "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -3847,12 +4008,25 @@ "node": ">= 0.8.0" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, + "node_modules/process-warning": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.2.0.tgz", + "integrity": "sha512-/1WZ8+VQjR6avWOgHeEPd7SDQmFQ1B5mC1eRXsCm5TarlNmx/wCsa5GEaxGm05BORRtyG/Ex/3xq3TuRvq57qg==" + }, "node_modules/prom-client": { "version": "14.2.0", "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-14.2.0.tgz", @@ -3891,6 +4065,16 @@ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", "dev": true }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", @@ -3934,6 +4118,11 @@ } ] }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -4005,6 +4194,14 @@ "node": ">=8.10.0" } }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/redis": { "version": "4.6.10", "resolved": "https://registry.npmjs.org/redis/-/redis-4.6.10.tgz", @@ -4230,6 +4427,12 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", + "dev": true + }, "node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -4348,14 +4551,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -4386,6 +4581,14 @@ "url": "https://opencollective.com/sinon" } }, + "node_modules/sonic-boom": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.7.0.tgz", + "integrity": "sha512-IudtNvSqA/ObjN97tfgNmOKyDOs4dNcg4cUUsHDebqsgb8wGBBwb31LIgShNO8fye0dFI52X1+tFoKKI6Rq1Gg==", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, "node_modules/spdx-exceptions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", @@ -4408,12 +4611,12 @@ "integrity": "sha512-lpT8hSQp9jAKp9mhtBU4Xjon8LPGBvLIuBiSVhMEtmLecTh2mO0tlqrAMp47tBXzMr13NJMQ2lf7RpQGLJ3HsQ==", "dev": true }, - "node_modules/stack-trace": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", "engines": { - "node": "*" + "node": ">= 10.x" } }, "node_modules/standard-as-callback": { @@ -4433,6 +4636,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, "dependencies": { "safe-buffer": "~5.1.0" } @@ -4440,7 +4644,8 @@ "node_modules/string_decoder/node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true }, "node_modules/string-width": { "version": "4.2.3", @@ -4622,17 +4827,20 @@ "bintrees": "1.0.2" } }, - "node_modules/text-hex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", - "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" - }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/thread-stream": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.4.1.tgz", + "integrity": "sha512-d/Ex2iWd1whipbT681JmTINKw0ZwOUBZm7+Gjs64DHuX34mmw8vJL2bFAaNacaW72zYiTJxSHi5abUuOi5nsfg==", + "dependencies": { + "real-require": "^0.2.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -4665,14 +4873,6 @@ "nodetouch": "bin/nodetouch.js" } }, - "node_modules/triple-beam": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", - "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", - "engines": { - "node": ">= 14.0.0" - } - }, "node_modules/tsconfig-paths": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", @@ -4865,7 +5065,8 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true }, "node_modules/utils-merge": { "version": "1.0.1", @@ -4953,66 +5154,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/winston": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.10.0.tgz", - "integrity": "sha512-nT6SIDaE9B7ZRO0u3UvdrimG0HkB7dSTAgInQnNR2SOPJ4bvq5q79+pXLftKmP52lJGW15+H5MCK0nM9D3KB/g==", - "dependencies": { - "@colors/colors": "1.5.0", - "@dabh/diagnostics": "^2.0.2", - "async": "^3.2.3", - "is-stream": "^2.0.0", - "logform": "^2.4.0", - "one-time": "^1.0.0", - "readable-stream": "^3.4.0", - "safe-stable-stringify": "^2.3.1", - "stack-trace": "0.0.x", - "triple-beam": "^1.3.0", - "winston-transport": "^4.5.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/winston-transport": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz", - "integrity": "sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==", - "dependencies": { - "logform": "^2.3.2", - "readable-stream": "^3.6.0", - "triple-beam": "^1.3.0" - }, - "engines": { - "node": ">= 6.4.0" - } - }, - "node_modules/winston-transport/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/winston/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/workerpool": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", diff --git a/package.json b/package.json index c50f71c..5234275 100644 --- a/package.json +++ b/package.json @@ -22,10 +22,10 @@ "js-yaml": "^4.1.0", "luxon": "^3.4.3", "node-fetch": "^3.3.2", + "pino": "^8.16.1", "prom-client": "^14.2.0", "redis": "^4.6.8", - "uuid": "^9.0.1", - "winston": "^3.10.0" + "uuid": "^9.0.1" }, "engines": { "node": ">=18" @@ -40,6 +40,7 @@ "mocha": "^9.2.2", "nock": "^13.3.3", "nodemon": "^3.0.1", + "pino-pretty": "^10.2.3", "sinon": "^12.0.1", "supertest": "^3.4.2" } diff --git a/src/common/logger.js b/src/common/logger.js index f86c7a5..98068e4 100644 --- a/src/common/logger.js +++ b/src/common/logger.js @@ -1,90 +1,11 @@ -'use strict'; - -import { addColors, format, createLogger, transports } from 'winston'; +import pino from 'pino'; import config from 'config'; -// jsonStringify is an extraneous dependency from Winston -import jsonStringify from 'safe-stable-stringify'; - const LOG_CONFIG = config.get('log') || {}; const { level: DEFAULT_LEVEL, filename: DEFAULT_FILENAME, - stdout: STDOUT = true + stdout: STDOUT = true, } = LOG_CONFIG; -const { combine, colorize, timestamp, json, printf, errors, splat } = format; -const LEVELS = { - error: 0, - warn: 1, - info: 2, - verbose: 3, - debug: 4, - trace: 5, -}; -addColors({ - error: 'red', - warn: 'yellow', - info: 'green', - verbose: 'cyan', - debug: 'magenta', - trace: 'gray' -}); - -/** - * The logging library used by this module. - * @name external:winston - * @external winston - * @private - */ - -/** - * The logging class exposed by this module. - * @name external:winston.Logger - * @memberof external:winston - * @external winston.Logger - * @class - */ - -/** - * Method to log a message at a specified level. - * @name external:winston.Logger#log - * @memberof external:winston.Logger - * @function - * @param {string} level - The log level to use. - * @param {string} message - The message to log. - */ - -/** - * @typedef {object} BbbWebhooksLogger - * @property {external:winston.Logger#log} error - log a message at the error level - * @property {external:winston.Logger#log} warn - log a message at the warn level - * @property {external:winston.Logger#log} info - log a message at the info level - * @property {external:winston.Logger#log} verbose - log a message at the verbose level - * @property {external:winston.Logger#log} debug - log a message at the debug level - * @property {external:winston.Logger#log} trace - log a message at the trace level - */ - -/** - * _shimmerLoggerWithLabel. - * @private - * @param {external:winston.Logger} logger - the logger to be shimmered - * @param {string} label - the label to be prepended to the message - * @returns {BbbWebhooksLogger} the shimmered logger - */ -const _shimmerLoggerWithLabel = (logger, label) => { - const shimmeredLogger = Object.assign({}, logger); - Object.keys(LEVELS).forEach((level) => { - /** - * shimmeredLogger[level]. - * @param {object} message - the message to be logged - * @param {string} meta - loggable object to be stringified and appended to the message (metadata) - */ - shimmeredLogger[level] = (message, meta) => { - logger.log(level, `[${label}] ${message}`, meta); - } - }); - - return shimmeredLogger; -}; /** * @typedef {object} LoggerOptions @@ -97,7 +18,7 @@ const _shimmerLoggerWithLabel = (logger, label) => { * _newLogger. * @private * @param {LoggerOptions} options - the options to be used when creating the logger - * @returns {external:winston.Logger} a Winston logger instance + * @returns {external:pino.Logger} a Pino logger instance */ const _newLogger = ({ filename, @@ -108,15 +29,14 @@ const _newLogger = ({ if (filename) { try { - loggingTransports.push(new transports.File({ - filename, - format: combine( - timestamp(), - splat(), - errors({ stack: true }), - json(), - ) - })); + loggingTransports.push({ + level, + target: 'pino/file', + options: { + destination: filename, + mkdir: true, + } + }); } catch (error) { // eslint-disable-next-line no-console console.error("Failed to create file transport, won't log to file", error); @@ -126,47 +46,53 @@ const _newLogger = ({ if (stdout) { if (process.env.NODE_ENV !== 'production') { // Development logging - fancier, more human readable stuff - loggingTransports.push(new transports.Console({ - format: combine( - colorize(), - timestamp(), - errors({ stack: true }), - printf(({ level, message, timestamp, ...meta}) => { - const stringifiedRest = jsonStringify(Object.assign({}, meta, { - splat: undefined - })); - - if (stringifiedRest !== '{}') { - return `${timestamp} - ${level}: ${message} ${stringifiedRest}`; - } else { - return `${timestamp} - ${level}: ${message}`; - } - }), - ) - })); + loggingTransports.push({ + level, + target: 'pino-pretty', + colorize: true, + }); } else { - loggingTransports.push(new transports.Console({ - format: combine( - timestamp(), - splat(), - json(), - errors({ stack: true }), - ) - })); + // Production logging - regular stdout, no colors, no fancy stuff + loggingTransports.push({ + level, + target: 'pino/file', + options: { + destination: 1, + }, + }); } } - const logger = createLogger({ - levels: LEVELS, - level, - transports: loggingTransports, - exitOnError: false, - }); + const hooks = { + // Reverse the order of arguments for the log method - message comes first, + // then the rest of the arguments (object params, errors, ...) + logMethod (inputArgs, method) { + if (inputArgs.length >= 2) { + const arg1 = inputArgs.shift() + const arg2 = inputArgs.shift() + return method.apply(this, [arg2, arg1, ...inputArgs]) + } + return method.apply(this, inputArgs) + } + } + + const targets = pino.transport({ targets: loggingTransports }) ; - logger.on('error', (error) => { + targets.on('error', error => { // eslint-disable-next-line no-console - console.error("Logger failure", error); - }); + console.error('CRITICAL logger failure: ', error.toString()); + + if (filename && error.toString().includes('ENOENT') || error.toString().includes('EACCES')) { + // eslint-disable-next-line no-console + console.error(`CRITICAL: failed to get log file ${filename}`); + process.exit(1); + } + }) + + const logger = pino({ + level, + hooks, + }, targets); return logger; } @@ -177,14 +103,6 @@ const BASE_LOGGER = _newLogger({ stdout: STDOUT, }); -/** - * The default logger instance for bbb-webhooks (with label 'bbb-webhooks') - * @name logger - * @instance - * @public - * @type {BbbWebhooksLogger} - */ -const logger = _shimmerLoggerWithLabel(BASE_LOGGER, 'bbb-webhooks'); /** * Creates a new logger with the specified label prepended to all messages * @name newLogger @@ -195,8 +113,17 @@ const logger = _shimmerLoggerWithLabel(BASE_LOGGER, 'bbb-webhooks'); * @returns {BbbWebhooksLogger} the new logger */ const newLogger = (label) => { - return _shimmerLoggerWithLabel(BASE_LOGGER, label); -} + return BASE_LOGGER.child({ mod: label }); +}; + +/** + * The default logger instance for bbb-webhooks (with label 'bbb-webhooks') + * @name logger + * @instance + * @public + * @type {BbbWebhooksLogger} + */ +const logger = newLogger('bbb-webhooks'); export default logger; From e2ac3cd10c4e6c0e9082e24b1b5f07ef1c30c86c Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Tue, 24 Oct 2023 13:47:13 -0300 Subject: [PATCH 095/154] fix: redact secret in logs --- src/common/logger.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/common/logger.js b/src/common/logger.js index 98068e4..61ccce5 100644 --- a/src/common/logger.js +++ b/src/common/logger.js @@ -92,6 +92,7 @@ const _newLogger = ({ const logger = pino({ level, hooks, + redact: ['config.server.secret'], }, targets); return logger; From 6ff81b1d8740c14282ebac674f83feaf06050ce1 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Tue, 24 Oct 2023 13:50:08 -0300 Subject: [PATCH 096/154] chore(xapi): remove spurious log --- src/out/xapi/index.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/out/xapi/index.js b/src/out/xapi/index.js index 7f52911..af36e1e 100644 --- a/src/out/xapi/index.js +++ b/src/out/xapi/index.js @@ -44,8 +44,6 @@ export default class OutXAPI { await this.redisClient.connect(); - this.logger.debug('OutXAPI.onEvent:', this.config); - this.meetingStorage = new meetingCompartment( this.redisClient, this.config.redis.keys.meetingPrefix, From dd67596576bdf3775655b36aa192beb5c88ef2a7 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Tue, 24 Oct 2023 15:28:46 -0300 Subject: [PATCH 097/154] fix(db): handle nested payload field rw --- src/db/redis/base-storage.js | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/db/redis/base-storage.js b/src/db/redis/base-storage.js index 2d8f3de..f565769 100644 --- a/src/db/redis/base-storage.js +++ b/src/db/redis/base-storage.js @@ -43,6 +43,10 @@ class StorageItem { this.logger = newLogger(`db:${this.prefix}|${this.setId}`); } + _accessNested (obj, path) { + return path.split('.').reduce((o, i) => o[i], obj); + } + async save() { try { await this.redisClient.hSet(this.prefix + ":" + this.id, this.serialize(this)); @@ -119,6 +123,10 @@ class StorageCompartmentKV { this.logger = newLogger(`db:${this.prefix}|${this.setId}`); } + _accessNested (obj, path) { + return path.split('.').reduce((o, i) => o[i], obj); + } + serialize(data) { const r = { id: data.id, @@ -181,11 +189,12 @@ class StorageCompartmentKV { async destroyWithField(field, value) { return Promise.all( Object.keys(this.localStorage).filter(internal => { - return this.localStorage[internal] && this.localStorage[internal]?.payload[field] === value; + return this.localStorage[internal]?.payload + && this._accessNested(this.localStorage[internal].payload, field) === value; }).map(internal => { let mapping = this.localStorage[internal]; - if (mapping.payload[field] === value) { + if (this._accessNested(mapping.payload, field) === value) { return mapping.destroy() .then(() => { return mapping; @@ -210,7 +219,8 @@ class StorageCompartmentKV { findAllWithField(field, value) { const dupe = Object.keys(this.localStorage).filter(internal => { - return this.localStorage[internal] && this.localStorage[internal]?.payload[field] === value; + return this.localStorage[internal]?.payload + && this._accessNested(this.localStorage[internal].payload, field) === value; }).map(internal => { return this.localStorage[internal]; }); @@ -222,7 +232,7 @@ class StorageCompartmentKV { if (field != null && value != null) { for (let internal in this.localStorage) { const mapping = this.localStorage[internal]; - if (mapping != null && mapping.payload[field] === value) { + if (mapping != null && this._accessNested(mapping.payload, field) === value) { return mapping; } } @@ -235,8 +245,8 @@ class StorageCompartmentKV { if (field != null && value != null) { for (let internal in this.localStorage) { const mapping = this.localStorage[internal]; - if (mapping != null && mapping.payload[field] === value) { - mapping.payload = { ...mapping.payload, ...payload }; + if (mapping != null && this._accessNested(mapping.payload, field) === value) { + mapping.payload = config.util.extendDeep({}, mapping.payload, payload, 5); return mapping.save(); } } From 05c0a809a40f8352ecff4e2977c41e13d0e4458a Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Tue, 24 Oct 2023 15:31:55 -0300 Subject: [PATCH 098/154] feat: add internal/external-user-id to meeting-screenshare-started/stopped events --- src/db/redis/user-mapping.js | 45 +++++++++++++++--- src/process/event-processor.js | 58 ++++++++++++++++++++++- src/process/event.js | 87 ++++++++++++++++++++++------------ 3 files changed, 153 insertions(+), 37 deletions(-) diff --git a/src/db/redis/user-mapping.js b/src/db/redis/user-mapping.js index f57e303..88170d2 100644 --- a/src/db/redis/user-mapping.js +++ b/src/db/redis/user-mapping.js @@ -1,12 +1,25 @@ import config from 'config'; -import Logger from '../../common/logger.js'; -import { v4 as uuidv4 } from 'uuid'; -import { StorageItem, StorageCompartmentKV } from './base-storage.js'; +import { StorageCompartmentKV } from './base-storage.js'; class UserMappingCompartment extends StorageCompartmentKV { - constructor(client, prefix, setId) { + static itemDeserializer(data) { + const { id, ...payload } = data; + const { user, ...rest } = payload; + const parsedPayload = { + ...rest, + user: JSON.parse(user), + }; + + return { + id, + ...parsedPayload, + }; + } + + constructor(client, prefix, setId, options = {}) { super(client, prefix, setId, { aliasField: 'internalUserID', + ...options, }); } @@ -41,12 +54,28 @@ class UserMappingCompartment extends StorageCompartmentKV { return (mapping != null ? mapping.payload?.internalMeetingID : undefined); } - async getUsersFromMeeting(internalMeetingID) { - const mappings = await this.findAllWithField('meetingId', internalMeetingID); + getUsersFromMeeting(internalMeetingID) { + const mappings = this.findAllWithField('meetingId', internalMeetingID); return mappings != null ? mappings.map((mapping) => mapping.payload) : []; } + getMeetingPresenter (internalMeetingID) { + const mappings = this.getUsersFromMeeting(internalMeetingID); + + return mappings.find((mapping) => + mapping?.user?.presenter === true || mapping?.user?.presenter === 'true' + ); + } + + getMeetingScreenShareOwner (internalMeetingID) { + const mappings = this.getUsersFromMeeting(internalMeetingID); + + return mappings.find((mapping) => + mapping?.user?.screenshare === true || mapping?.user?.screenshare === 'true' + ); + } + getUser(internalUserID) { const mapping = this.findByField('internalUserID', internalUserID); return (mapping != null ? mapping.payload?.user : undefined); @@ -70,7 +99,9 @@ const init = (redisClient) => { UserMapping = new UserMappingCompartment( redisClient, config.get('redis.keys.userMapPrefix'), - config.get('redis.keys.userMaps') + config.get('redis.keys.userMaps'), { + deserializer: UserMappingCompartment.itemDeserializer, + } ); return UserMapping.initialize(); } diff --git a/src/process/event-processor.js b/src/process/event-processor.js index 7b658c5..f9d47b7 100644 --- a/src/process/event-processor.js +++ b/src/process/event-processor.js @@ -122,7 +122,7 @@ export default class EventProcessor { const outputEvent = eventInstance.outputEvent; if (!Utils.isEmpty(outputEvent)) { - Logger.debug('raw event succesfully parsed', { rawEvent }); + Logger.trace('raw event succesfully parsed', { rawEvent }); const internalMeetingId = outputEvent.data.attributes.meeting["internal-meeting-id"]; IDMapping.get().reportActivity(internalMeetingId); @@ -169,6 +169,62 @@ export default class EventProcessor { this._notifyOutputModules(outputEvent, rawEvent); }); break; + case "user-presenter-assigned": + UserMapping.get().updateWithField( + 'internalUserID', + outputEvent.data.attributes.user["internal-user-id"], { + user: { + presenter: true, + }, + } + ).catch((error) => { + Logger.error('error updating user mapping', error); + }).finally(() => { + this._notifyOutputModules(outputEvent, rawEvent); + }); + break; + case "user-presenter-unassigned": + UserMapping.get().updateWithField( + 'internalUserID', + outputEvent.data.attributes.user["internal-user-id"], { + user: { + presenter: false, + }, + } + ).catch((error) => { + Logger.error('error updating user mapping', error); + }).finally(() => { + this._notifyOutputModules(outputEvent, rawEvent); + }); + break; + case "meeting-screenshare-started": + UserMapping.get().updateWithField( + 'internalUserID', + outputEvent.data.attributes.user["internal-user-id"], { + user: { + screenshare: true, + }, + } + ).catch((error) => { + Logger.error('error updating user mapping', error); + }).finally(() => { + this._notifyOutputModules(outputEvent, rawEvent); + }); + break; + case "meeting-screenshare-stopped": + UserMapping.get().updateWithField( + 'internalUserID', + outputEvent.data.attributes.user["internal-user-id"], { + user: { + screenshare: false, + }, + } + ).catch((error) => { + Logger.error('error updating user mapping', error); + }).finally(() => { + this._notifyOutputModules(outputEvent, rawEvent); + }); + break; case "meeting-ended": this._handleMeetingEndedEvent(outputEvent).finally(() => { this._notifyOutputModules(outputEvent, rawEvent); diff --git a/src/process/event.js b/src/process/event.js index 177c435..e97f802 100644 --- a/src/process/event.js +++ b/src/process/event.js @@ -192,35 +192,64 @@ export default class WebhooksEvent { } } - if (messageObj.envelope.name === "MeetingCreatedEvtMsg") { - this.outputEvent.data.attributes = { - "meeting":{ - "internal-meeting-id": props.meetingProp.intId, - "external-meeting-id": props.meetingProp.extId, - "name": props.meetingProp.name, - "is-breakout": props.meetingProp.isBreakout, - "parent-id": props.breakoutProps.parentId, - "duration": props.durationProps.duration, - "create-time": props.durationProps.createdTime, - "create-date": props.durationProps.createdDate, - "moderator-pass": props.password.moderatorPass, - "viewer-pass": props.password.viewerPass, - "record": props.recordProp.record, - "voice-conf": props.voiceProp.voiceConf, - "dial-number": props.voiceProp.dialNumber, - "max-users": props.usersProp.maxUsers, - "metadata": props.metadataProp.metadata - } - }; - } - if (messageObj.envelope.name === "SetCurrentPresentationEvtMsg") { - this.outputEvent.data.attributes = { - "meeting":{ - "internal-meeting-id": meetingId, - "external-meeting-id": IDMapping.get().getExternalMeetingID(meetingId), - "presentation-id": messageObj.core.body.presentationId - } - }; + switch (messageObj.envelope.name) { + case "MeetingCreatedEvtMsg": + this.outputEvent.data.attributes = { + "meeting":{ + "internal-meeting-id": props.meetingProp.intId, + "external-meeting-id": props.meetingProp.extId, + "name": props.meetingProp.name, + "is-breakout": props.meetingProp.isBreakout, + "parent-id": props.breakoutProps.parentId, + "duration": props.durationProps.duration, + "create-time": props.durationProps.createdTime, + "create-date": props.durationProps.createdDate, + "moderator-pass": props.password.moderatorPass, + "viewer-pass": props.password.viewerPass, + "record": props.recordProp.record, + "voice-conf": props.voiceProp.voiceConf, + "dial-number": props.voiceProp.dialNumber, + "max-users": props.usersProp.maxUsers, + "metadata": props.metadataProp.metadata + } + }; + break; + + case "SetCurrentPresentationEvtMsg": + this.outputEvent.data.attributes = { + "meeting":{ + "internal-meeting-id": meetingId, + "external-meeting-id": IDMapping.get().getExternalMeetingID(meetingId), + "presentation-id": messageObj.core.body.presentationId + } + }; + break; + + case "ScreenshareRtmpBroadcastStartedEvtMsg": { + const presenter = UserMapping.get().getMeetingPresenter(meetingId); + this.outputEvent.data.attributes = { + ...this.outputEvent.data.attributes, + user:{ + "internal-user-id": presenter.internalUserID, + "external-user-id": presenter.externalUserID, + } + }; + break; + } + case "ScreenshareRtmpBroadcastStoppedEvtMsg": { + const owner = UserMapping.get().getMeetingScreenShareOwner(meetingId); + this.outputEvent.data.attributes = { + ...this.outputEvent.data.attributes, + user:{ + "internal-user-id": owner.internalUserID, + "external-user-id": owner.externalUserID, + } + }; + + break; + } + + default: return; } } From b00d65abf54308d13015f9785b709774163fdc80 Mon Sep 17 00:00:00 2001 From: mp Date: Tue, 24 Oct 2023 16:39:11 -0300 Subject: [PATCH 099/154] (xAPI) - Discarded unnecessary JSON parsing of poll_data in poll-answered events --- src/out/xapi/xapi.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/out/xapi/xapi.js b/src/out/xapi/xapi.js index 0891ece..ea858cd 100644 --- a/src/out/xapi/xapi.js +++ b/src/out/xapi/xapi.js @@ -263,14 +263,6 @@ export default class XAPI { if (poll_data === undefined) { return; } - poll_data.choices = poll_data.choices.map((item) => { - const parsedItem = JSON.parse(item); - const description = JSON.parse(parsedItem.description); - return { - id: JSON.parse(item).id, - description: { en: description.en }, - }; - }); } XAPIStatement = getXAPIStatement( event, From 7cbf19a19523b9cc9c805a764008cbf7a21c493d Mon Sep 17 00:00:00 2001 From: mp Date: Tue, 24 Oct 2023 16:51:48 -0300 Subject: [PATCH 100/154] (xAPI) - setting correct verb at user-raise-hand-changed event --- src/out/xapi/templates.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/out/xapi/templates.js b/src/out/xapi/templates.js index 0cade8b..2762dad 100644 --- a/src/out/xapi/templates.js +++ b/src/out/xapi/templates.js @@ -156,7 +156,7 @@ export default function getXAPIStatement(event, meeting_data, user_data = null, const raisedHandVerb = "https://w3id.org/xapi/virtual-classroom/verbs/reacted"; const loweredHandVerb = "https://w3id.org/xapi/virtual-classroom/verbs/unreacted"; const isRaiseHand = event.data.attributes.user["raise-hand"]; - statement.verb = isRaiseHand ? raisedHandVerb : loweredHandVerb; + statement.verb.id = isRaiseHand ? raisedHandVerb : loweredHandVerb; statement.result = { "extensions": { "https://w3id.org/xapi/virtual-classroom/extensions/emoji": "U+1F590" From 4e5f9f64e3764aa6fa710f44edc8ec41a25e7380 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Wed, 25 Oct 2023 10:21:47 -0300 Subject: [PATCH 101/154] chore: update mapped-events.json with screenshare user data --- example/events/mapped-events.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example/events/mapped-events.json b/example/events/mapped-events.json index 003f4ea..b36efd3 100644 --- a/example/events/mapped-events.json +++ b/example/events/mapped-events.json @@ -8,8 +8,8 @@ {"data":{"type":"event","id":"user-audio-voice-disabled","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"user":{"internal-user-id":"w_1fewpbchnudl","external-user-id":"w_1fewpbchnudl","listening-only":false,"sharing-mic":false}},"event":{"ts":1696511545350}}} {"data":{"type":"event","id":"user-cam-broadcast-start","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"user":{"internal-user-id":"w_1fewpbchnudl","external-user-id":"w_1fewpbchnudl","stream":"w_1fewpbchnudl_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}},"event":{"ts":1696511549091}}} {"data":{"type":"event","id":"user-cam-broadcast-end","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"user":{"internal-user-id":"w_1fewpbchnudl","external-user-id":"w_1fewpbchnudl","stream":"w_1fewpbchnudl_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}},"event":{"ts":1696511550383}}} -{"data":{"type":"event","id":"meeting-screenshare-started","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"}},"event":{"ts":1696511553283}}} -{"data":{"type":"event","id":"meeting-screenshare-stopped","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"}},"event":{"ts":1696511556391}}} +{"data":{"type":"event","id":"meeting-screenshare-started","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"user":{"internal-user-id":"w_1fewpbchnudl","external-user-id":"w_1fewpbchnudl"}},"event":{"ts":1696511553283}}} +{"data":{"type":"event","id":"meeting-screenshare-stopped","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"user":{"internal-user-id":"w_1fewpbchnudl","external-user-id":"w_1fewpbchnudl"}},"event":{"ts":1696511556391}}} {"data":{"type":"event","id":"chat-group-message-sent","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"chat-message":{"id":"1696511561799-rplr4p2r","message":"Public chat test","sender":{"internal-user-id":"w_1fewpbchnudl","name":"User 1430416","time":1696511561799}},"chat-id":"MAIN-PUBLIC-GROUP-CHAT"},"event":{"ts":1696511561816}}} {"data":{"type":"event","id":"user-emoji-changed","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"user":{"internal-user-id":"w_1fewpbchnudl","external-user-id":"w_1fewpbchnudl","emoji":"🙁"}},"event":{"ts":1696511567694}}} {"data":{"type":"event","id":"user-emoji-changed","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"user":{"internal-user-id":"w_1fewpbchnudl","external-user-id":"w_1fewpbchnudl","emoji":"none"}},"event":{"ts":1696511568939}}} From 7dbbf305787c857b587583b9d17d97fc51d1b95b Mon Sep 17 00:00:00 2001 From: mp Date: Thu, 26 Oct 2023 11:27:15 -0300 Subject: [PATCH 102/154] Added lrs credentials descryption capability --- src/out/xapi/decrypt.js | 29 +++++++++++++++++++++++++++++ src/out/xapi/xapi.js | 6 +++--- 2 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 src/out/xapi/decrypt.js diff --git a/src/out/xapi/decrypt.js b/src/out/xapi/decrypt.js new file mode 100644 index 0000000..fc043d5 --- /dev/null +++ b/src/out/xapi/decrypt.js @@ -0,0 +1,29 @@ +import crypto from 'crypto' + +/** + * + * @param encryptedObj + * @param secret + */ +export default function decryptStr(encryptedObj, secret) { + // Decode the base64-encoded text + const encryptedText = Buffer.from(encryptedObj, 'base64'); + + // Extract salt (first 8 bytes) and ciphertext (the rest) + const salt = encryptedText.subarray(8, 16); + const ciphertext = encryptedText.subarray(16); + + // Derive the key and IV using PBKDF2 + const keyIVBuffer = crypto.pbkdf2Sync(secret, salt, 10000, 48, 'sha256'); + const key = keyIVBuffer.subarray(0, 32); + const iv = keyIVBuffer.subarray(32); + + // Create a decipher object with IV + const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv); + + // Update the decipher with the ciphertext + let decrypted = decipher.update(ciphertext, 'binary', 'utf8'); + decrypted += decipher.final('utf8'); + + return decrypted; +} diff --git a/src/out/xapi/xapi.js b/src/out/xapi/xapi.js index ea858cd..478b325 100644 --- a/src/out/xapi/xapi.js +++ b/src/out/xapi/xapi.js @@ -1,4 +1,5 @@ import getXAPIStatement from "./templates.js"; +import decryptStr from "./decrypt.js" import { v5 as uuidv5 } from "uuid"; import { DateTime } from "luxon"; import fetch from "node-fetch"; @@ -104,10 +105,9 @@ export default class XAPI { let lrs_endpoint = ''; let lrs_token = ''; - // if lrs_payload exists, extracts lrs_endpoint and lrs_token from it + // if lrs_payload exists, decrypts with the server secret and extracts lrs_endpoint and lrs_token from it if (lrs_payload !== undefined){ - const payload_buffer = new Buffer.from(lrs_payload, 'base64'); - const payload_text = payload_buffer.toString('ascii'); + const payload_text = decryptStr(lrs_payload, this.config.server.secret); ({lrs_endpoint, lrs_token} = JSON.parse(payload_text)); } From 0b874e6653bb76daa23cb488d6ced93b612615bb Mon Sep 17 00:00:00 2001 From: mp Date: Thu, 26 Oct 2023 13:05:51 -0300 Subject: [PATCH 103/154] (xapi) - Changed references to internal_user_id to external_user_id --- src/out/xapi/compartment.js | 14 +++++++------- src/out/xapi/xapi.js | 20 ++++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/out/xapi/compartment.js b/src/out/xapi/compartment.js index 665ec41..0ffd921 100644 --- a/src/out/xapi/compartment.js +++ b/src/out/xapi/compartment.js @@ -6,7 +6,7 @@ export class meetingCompartment extends StorageCompartmentKV { } async addOrUpdateMeetingData(meeting_data) { - const { internal_meeting_id, context_registration, planned_duration, + const { _meeting_id, context_registration, planned_duration, create_time, meeting_name, xapi_enabled, lrs_endpoint, lrs_token } = meeting_data; const payload = { @@ -45,23 +45,23 @@ export class userCompartment extends StorageCompartmentKV { } async addOrUpdateUserData(user_data) { - const { internal_user_id, name } = user_data; + const { external_user_id, name } = user_data; const payload = { - internal_user_id, + external_user_id, name, }; const mapping = await this.save(payload, { - alias: internal_user_id, + alias: external_user_id, }); - this.logger.info(`added user data to the list ${internal_user_id}: ${mapping.print()}`); + this.logger.info(`added user data to the list ${external_user_id}: ${mapping.print()}`); return mapping; } - async getUserData(internal_user_id) { - const user_data = this.findByField('internal_user_id', internal_user_id); + async getUserData(external_user_id) { + const user_data = this.findByField('external_user_id', external_user_id); return (user_data != null ? user_data.payload : undefined); } diff --git a/src/out/xapi/xapi.js b/src/out/xapi/xapi.js index 478b325..8450ad6 100644 --- a/src/out/xapi/xapi.js +++ b/src/out/xapi/xapi.js @@ -157,9 +157,9 @@ export default class XAPI { } // if user-joined event, set user_data on redis else if (eventId == "user-joined") { - const internal_user_id = event.data.attributes.user["internal-user-id"]; + const external_user_id = event.data.attributes.user["internal-user-id"]; const user_data = { - internal_user_id, + external_user_id, name: event.data.attributes.user.name, }; try { @@ -191,10 +191,10 @@ export default class XAPI { event.data.attributes.user["sharing-mic"] == false)) { return; } - const internal_user_id = event.data.attributes.user?.["internal-user-id"]; + const external_user_id = event.data.attributes.user?.["internal-user-id"]; - const user_data = internal_user_id - ? await this.userStorage.getUserData(internal_user_id) + const user_data = external_user_id + ? await this.userStorage.getUserData(external_user_id) : undefined; // Do not proceed if user_data is requested but not found on the storage if (user_data === undefined) { @@ -212,7 +212,7 @@ export default class XAPI { }[eventId] if (media !== undefined){ - user_data[`user_${media}_object_id`] = this._uuid(`${internal_user_id}_${media}`); + user_data[`user_${media}_object_id`] = this._uuid(`${external_user_id}_${media}`); } XAPIStatement = getXAPIStatement(event, meeting_data, user_data); @@ -220,7 +220,7 @@ export default class XAPI { } else if (eventId == "chat-group-message-sent") { resolve(); const user_data = event.data.attributes["chat-message"]?.sender; - const msg_key = `${user_data?.internal_user_id}_${user_data?.time}`; + const msg_key = `${user_data?.external_user_id}_${user_data?.time}`; user_data.msg_object_id = this._uuid(msg_key); XAPIStatement = getXAPIStatement(event, meeting_data, user_data); // Poll events @@ -231,10 +231,10 @@ export default class XAPI { if (eventId == "poll-responded") { resolve(); } - const internal_user_id = + const external_user_id = event.data.attributes.user?.["internal-user-id"]; - const user_data = internal_user_id - ? await this.userStorage.getUserData(internal_user_id) + const user_data = external_user_id + ? await this.userStorage.getUserData(external_user_id) : null; const object_id = this._uuid(event.data.attributes.poll.id); let poll_data; From e42c4dcfebd5b1cfc51bb1ce739e9cad5e2fdefb Mon Sep 17 00:00:00 2001 From: mp Date: Thu, 26 Oct 2023 13:09:19 -0300 Subject: [PATCH 104/154] fix(xapi) - undo typo in compartment.js --- src/out/xapi/compartment.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/out/xapi/compartment.js b/src/out/xapi/compartment.js index 0ffd921..39175d6 100644 --- a/src/out/xapi/compartment.js +++ b/src/out/xapi/compartment.js @@ -6,7 +6,7 @@ export class meetingCompartment extends StorageCompartmentKV { } async addOrUpdateMeetingData(meeting_data) { - const { _meeting_id, context_registration, planned_duration, + const { internal_meeting_id, context_registration, planned_duration, create_time, meeting_name, xapi_enabled, lrs_endpoint, lrs_token } = meeting_data; const payload = { From 89aa051ba79f8385446b63102b18be46be94ae76 Mon Sep 17 00:00:00 2001 From: mp Date: Thu, 26 Oct 2023 13:28:56 -0300 Subject: [PATCH 105/154] (xapi) - Updated README --- src/out/xapi/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/out/xapi/README.md b/src/out/xapi/README.md index 89fb573..83b66fd 100644 --- a/src/out/xapi/README.md +++ b/src/out/xapi/README.md @@ -57,8 +57,8 @@ You have the option to set relevant metadata when creating a meeting in Big Blue If you set `meta_xapi-enabled` to false, no xAPI events will be generated or sent to the LRS for that particular meeting. This provides the flexibility to choose which meetings should be tracked using xAPI. ### meta_secret-lrs-payload -- **Description**: This parameter allows you to specify the credentials and endpoint of the Learning Record Store (LRS) where the xAPI events will be sent. The payload is a Base64-encoded string representing a JSON object. -- **Value Format**: Base64-encoded JSON object **(for now)** +- **Description**: This parameter allows you to specify the credentials and endpoint of the Learning Record Store (LRS) where the xAPI events will be sent. The payload is a Base64-encoded string representing a JSON object encrypted (AES 256/PBKDF2) using the **server secret** as the **passphrase**. +- **Value Format**: Base64-encoded JSON object encrypted with AES 256/PBKDF2 encryption - **JSON Payload Structure**: ```json { @@ -68,7 +68,7 @@ If you set `meta_xapi-enabled` to false, no xAPI events will be generated or sen ``` - **Example**: ``` -meta_secret-lrs-payload: YmFzZTY0IGVuY29kaW5nIHNjaGVtZXMgYXJlIGNvbW1vbmx5IHVzZWQgd2hlbiB0aGVtZSBkYXRhIG5lZWRzIHRvIGJlIHNlcnZlciB3aXRob3V0IG1vZGlmaWNhdGlvbiBkdXJpbmcgdHJhY2tlci4gVGhpcyBlbmNvZGluZyBwYXJ0ZW50IHNlcnZlciB3aWxsIGJlIHN0b3JlZCBhbmQgdHJhbnNmZXJyZWQgb3ZlciBtZWRpYSB0aGF0IGFyZSBkZXNpZ25lZCB0byBkZWFsIHdpdGggdGV4dC4gVGhpcyBwYXJ0... +meta_secret-lrs-payload: U2FsdGVkX1+9SPjkogUTf8sDxUf7Hu/llOglOkEBlO+7crvt8uedZ8CuEPl/64kNjCmqT71zIxNidELYEYJtUt/RXUiyz2mAvPCeVA3OLvdUX0z2lZOu6kRwwdqEekg2YqicUi5/HO/6AnXegSRXeQH0WReYtjbcxpPUpX/XxfU2yGxQqDgkMG2D2IVyBsJnxVdrOUBf75MFSe02JO++46YJJmsy/... ``` The `meta_secret-lrs-payload` parameter allows you to securely define the LRS endpoint and authentication token for each meeting. It ensures that xAPI events generated during the meeting are sent to the correct LRS. From 6e9c71fd9407c1909d29eec7d14d565464839e34 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Sun, 29 Oct 2023 22:48:46 -0300 Subject: [PATCH 106/154] feat: add enabled flag to module spec Allows controlling whether a module should be used without having to strip the whole module config Also add XAPI module template config to default.example.yml --- config/default.example.yml | 21 +++++++++++++++++++++ src/modules/index.js | 10 +++++++++- src/out/xapi/README.md | 1 + 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/config/default.example.yml b/config/default.example.yml index cf988c2..0a8ccba 100644 --- a/config/default.example.yml +++ b/config/default.example.yml @@ -37,16 +37,19 @@ redis: # Basic module config entry template: # key: , +# enabled: true|false (optional, default: true) # type: in|out|db, # config: (optional) # modules: ../db/redis/index.js: + enabled: true type: db config: host: 127.0.0.1 port: 6379 ../in/redis/index.js: + enabled: true type: in config: redis: @@ -62,6 +65,7 @@ modules: - bigbluebutton:from-bbb-apps:users - bigbluebutton:from-rap ../out/webhooks/index.js: + enabled: true type: out config: api: @@ -116,3 +120,20 @@ modules: attempts: 12 initialInterval: 1 increaseFactor: 2 + ../out/xapi/index.js: + enabled: false + type: out + config: + lrs: + lrs_endpoint: https://your_lrs.endpoint + lrs_username: user + lrs_password: pass + uuid_namespace: 01234567-89ab-cdef-0123-456789abcdef + redis: + keys: + meetings: bigbluebutton:webhooks:xapi:meetings + meetingPrefix: bigbluebutton:webhooks:xapi:meeting + users: bigbluebutton:webhooks:xapi:users + userPrefix: bigbluebutton:webhooks:xapi:user + polls: bigbluebutton:webhooks:xapi:polls + pollPrefix: bigbluebutton:webhooks:xapi:poll diff --git a/src/modules/index.js b/src/modules/index.js index 64a6c05..2785639 100644 --- a/src/modules/index.js +++ b/src/modules/index.js @@ -107,6 +107,12 @@ export default class ModuleManager { for (const [name, description] of sortedModules) { try { const fullConfiguration = { name, ...description }; + + if (description.enabled === false) { + this.logger.warn(`module ${name} disabled, skipping`); + continue; + } + validateModuleConf(fullConfiguration); const context = this._buildContext(fullConfiguration); const module = new ModuleWrapper(name, description.type, context, context.configuration.config); @@ -148,7 +154,9 @@ export default class ModuleManager { // Added this listener to identify unhandled promises, but we should start making // sense of those as we find them process.on('unhandledRejection', (reason) => { - this.logger.error("CRITICAL: Unhandled promise rejection", { reason: reason.toString(), stack: reason.stack }); + this.logger.error("CRITICAL: Unhandled promise rejection", { + reason: reason.toString(), stack: reason.stack, + }); if (process.env.NODE_ENV !== 'production') process.exit(1); }); diff --git a/src/out/xapi/README.md b/src/out/xapi/README.md index 83b66fd..6bf57a3 100644 --- a/src/out/xapi/README.md +++ b/src/out/xapi/README.md @@ -7,6 +7,7 @@ This module is set and configured in the `default.yml` file using the following ```yml modules: ../out/xapi/index.js: + enabled: true type: out config: lrs: From eaf2ef2d3bfaba82d07c6b7fa4ec0bc182557abc Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Sun, 29 Oct 2023 22:58:50 -0300 Subject: [PATCH 107/154] chore: add separate flag to control logging to file Disabled by default - prefer using externals to pipe stdout to something else --- config/custom-environment-variables.yml | 5 +++-- config/default.example.yml | 7 +++++-- src/common/logger.js | 3 ++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/config/custom-environment-variables.yml b/config/custom-environment-variables.yml index 584d409..d264750 100644 --- a/config/custom-environment-variables.yml +++ b/config/custom-environment-variables.yml @@ -10,8 +10,9 @@ redis: log: level: LOG_LEVEL - filename: LOG_FILE - stdout: LOG_STDOUT + stdout: LOG_TO_STDOUT + file: LOG_TO_FILE + filename: LOG_FILENAME prometheus: enabled: PROM_ENABLED diff --git a/config/default.example.yml b/config/default.example.yml index 0a8ccba..cbf7877 100644 --- a/config/default.example.yml +++ b/config/default.example.yml @@ -1,7 +1,10 @@ log: - level: debug - filename: /var/log/bbb-webhooks.log + level: info + # Wether to log to stdout or not stdout: true + # Wether to log to log.filename or not + file: false + filename: /var/log/bbb-webhooks.log prometheus: enabled: false diff --git a/src/common/logger.js b/src/common/logger.js index 61ccce5..5fb7680 100644 --- a/src/common/logger.js +++ b/src/common/logger.js @@ -3,6 +3,7 @@ import config from 'config'; const LOG_CONFIG = config.get('log') || {}; const { level: DEFAULT_LEVEL, + file: DEFAULT_USE_FILE, filename: DEFAULT_FILENAME, stdout: STDOUT = true, } = LOG_CONFIG; @@ -27,7 +28,7 @@ const _newLogger = ({ }) => { const loggingTransports = []; - if (filename) { + if (DEFAULT_USE_FILE && filename) { try { loggingTransports.push({ level, From 01e0c74e377c95340e35c88497f55be872d89b7e Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Mon, 30 Oct 2023 00:44:13 -0300 Subject: [PATCH 108/154] chore: add instructions on how to run via Docker/docker-compose --- Dockerfile | 2 +- README.md | 51 +++++++++++++++++++++++++++++++++++++++++++++- app.js | 4 ++++ docker-compose.yml | 10 ++++----- src/common/env.js | 5 +++++ 5 files changed, 64 insertions(+), 8 deletions(-) create mode 100644 src/common/env.js diff --git a/Dockerfile b/Dockerfile index b9a7dd1..ec17206 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ RUN npm ci --omit=dev COPY . . -RUN cp config/default.example.yml config/local.yml +RUN cp config/default.example.yml config/default.yml EXPOSE 3005 diff --git a/README.md b/README.md index 51d59c6..2e2602c 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ If you are editing these permanent urls after they have already been committed t - `redis-cli flushall` - **_IMPORTANT:_** Running the above command clears the redis database entirely. This will result in all meetings, processing or not, to be cleared from the database, and may result in broken meetings currently processing. -## Production +## Manually installing the application on a BBB server Follow the commands below starting within the `bigbluebutton/bbb-webhooks` directory. @@ -79,3 +79,52 @@ Follow the commands below starting within the `bigbluebutton/bbb-webhooks` direc 9. Start the bbb-webhooks service: - `sudo systemctl bbb-webhooks restart` + +## Running via Docker + +A sample docker-compose for convenience that should get the application image +built and running with as little effort as possible. See the [Dockerfile](Dockerfile) +and [docker-compose.yml](docker-compose.yml) for more details. + +To build and run the application image, run the following command from within +the root directory of the project: + +``` +docker-compose up -d +``` + +To stop the application, run the following command from within the root directory +of the project: + +``` +docker-compose down +``` + +The container runs with the default Node container user, `node`. To override it, +feel free to set the `user` property in the `docker-compose.yml` file (e.g.: `user: ${UID}:${GID}`). + +### Configuring the application when running via Docker + +The application configuration can be modified by creating and editing an override +file in `/etc/bigbluebutton/bbb-webhooks/production.yml`. The file will be mounted +into the container and its contents will be *merged* with the default configuration. +The only exception to this are array attributes, which are *replaced*. + +The default configuration file used by the container can be found at +[config/default.example.yml](config/default.example.yml). + +As an example, suppose you want to override the `bbb.serverDomain` and +`bbb.sharedSecret` values as well as enable the `out/xapi` module. You would +create the following override file: + +```yaml +bbb: + serverDomain: 'bbb.example.com' + sharedSecret: 'secret' +modules: + ../out/xapi/index.js: + enabled: true +``` + +The rest of the configurable attributes can be found in the default configuration +file at [config/default.example.yml](config/default.example.yml). diff --git a/app.js b/app.js index 4edc02e..543dcfe 100644 --- a/app.js +++ b/app.js @@ -1,3 +1,7 @@ +/* eslint-disable-next-line no-unused-vars */ +import { NODE_CONFIG_DIR, SUPPRESS_NO_CONFIG_WARNING } from "./src/common/env.js"; +// eslint-disable-next-line no-unused-vars +import config from 'config'; import Application from './application.js'; const application = new Application(); diff --git a/docker-compose.yml b/docker-compose.yml index 9f44d5f..374b22a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,11 +7,9 @@ services: container_name: bbb-webhooks restart: always environment: - - NODE_CONFIG_DIR=/etc/bigbluebutton/bbb-webhooks.yml:/app/config - ports: - - "3005:3005" - - "3004:3004" + - NODE_ENV=production + - NODE_CONFIG_DIR=/etc/bigbluebutton/bbb-webhooks/:/app/config/ volumes: - - /etc/bigbluebutton/:/etc/bigbluebutton/ + - /etc/bigbluebutton/bbb-webhooks/:/etc/bigbluebutton/bbb-webhooks/ - /var/log/bbb-webhooks/:/var/log/bbb-webhooks/ - network_mode: bridge + network_mode: "host" diff --git a/src/common/env.js b/src/common/env.js new file mode 100644 index 0000000..4808ddb --- /dev/null +++ b/src/common/env.js @@ -0,0 +1,5 @@ +process.env.NODE_CONFIG_DIR = `/etc/bigbluebutton/bbb-webhooks/:${process.cwd()}/config/`; +process.env.SUPPRESS_NO_CONFIG_WARNING = "true"; + +export const NODE_CONFIG_DIR = process.env.NODE_CONFIG_DIR; +export const SUPPRESS_NO_CONFIG_WARNING = process.env.SUPPRESS_NO_CONFIG_WARNING; From 76c3123da4e13f2b2eeb6db1fc6320d61e631e3a Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Mon, 30 Oct 2023 00:46:56 -0300 Subject: [PATCH 109/154] 3.0.0-alpha.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4f56386..67a45a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "bbb-webhooks", - "version": "2.6.0", + "version": "3.0.0-alpha.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bbb-webhooks", - "version": "2.6.0", + "version": "3.0.0-alpha.0", "dependencies": { "bullmq": "^4.11.4", "config": "^3.3.7", diff --git a/package.json b/package.json index 5234275..972955e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bbb-webhooks", - "version": "2.6.0", + "version": "3.0.0-alpha.0", "description": "A BigBlueButton mudule for events WebHooks", "type": "module", "scripts": { From 3400dcb224ac585f6db9c84dfaf38f02c01d3b30 Mon Sep 17 00:00:00 2001 From: mp Date: Mon, 30 Oct 2023 12:44:41 -0300 Subject: [PATCH 110/154] xapi - added lrs credentials encrypt/decrypt instructions --- src/out/xapi/README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/out/xapi/README.md b/src/out/xapi/README.md index 83b66fd..72ee61a 100644 --- a/src/out/xapi/README.md +++ b/src/out/xapi/README.md @@ -66,9 +66,17 @@ If you set `meta_xapi-enabled` to false, no xAPI events will be generated or sen "lrs_token": "AAF32423SDF5345" } ``` +- **Encrypting the Payload**: The Payload should be encrypted with the server secret using the following bash command (provided the lrs credential are in the `lrs.conf` file and server secret is `bab3fd92bcd7d464`): +```bash +cat ./lrs.conf | openssl aes-256-cbc -pass "pass:bab3fd92bcd7d464" -pbkdf2 -a -A +``` +- **Decrypting the Payload**: The Payload can be decrypted with the server secret using the following bash command: +```bash +echo -n U2FsdGVkX18fLg33ChrHbHyIvbcdDwU6+4yX2yTb4gbDKOKSG3hhsd2+TS0ZK15fZlo4G1SQqaxm1OGo1fIsoji82T4SD4y5p1G2g9E9gAKzZC2Z5R454rw7/xGvX7uYGd/fbJcZraMYmafX1Zg3qA== | openssl aes-256-cbc -d -pass "pass:bab3fd92bcd7d464" -pbkdf2 -a -A +`````` - **Example**: ``` -meta_secret-lrs-payload: U2FsdGVkX1+9SPjkogUTf8sDxUf7Hu/llOglOkEBlO+7crvt8uedZ8CuEPl/64kNjCmqT71zIxNidELYEYJtUt/RXUiyz2mAvPCeVA3OLvdUX0z2lZOu6kRwwdqEekg2YqicUi5/HO/6AnXegSRXeQH0WReYtjbcxpPUpX/XxfU2yGxQqDgkMG2D2IVyBsJnxVdrOUBf75MFSe02JO++46YJJmsy/... +meta_secret-lrs-payload: U2FsdGVkX18fLg33ChrHbHyIvbcdDwU6+4yX2yTb4gbDKOKSG3hhsd2+TS0ZK15fZlo4G1SQqaxm1OGo1fIsoji82T4SD4y5p1G2g9E9gAKzZC2Z5R454rw7/xGvX7uYGd/fbJcZraMYmafX1Zg3qA== ``` The `meta_secret-lrs-payload` parameter allows you to securely define the LRS endpoint and authentication token for each meeting. It ensures that xAPI events generated during the meeting are sent to the correct LRS. From c239c0f97419f5568f277e74a2d1d29a9460fd60 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Tue, 31 Oct 2023 13:57:33 -0300 Subject: [PATCH 111/154] chore: update example event files - Different user names for mod and viewero - user-left for all users - audio-muted/unmuted events --- example/events/mapped-events.json | 45 ++++++++++++++++--------------- example/events/raw-events.json | 45 ++++++++++++++++--------------- 2 files changed, 48 insertions(+), 42 deletions(-) diff --git a/example/events/mapped-events.json b/example/events/mapped-events.json index b36efd3..cb6e8df 100644 --- a/example/events/mapped-events.json +++ b/example/events/mapped-events.json @@ -1,21 +1,24 @@ -{"data":{"type":"event","id":"meeting-created","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035","name":"random-3136035","is-breakout":false,"parent-id":"bbb-none","duration":0,"create-time":1696511528029,"create-date":"Thu Oct 05 10:12:08 BRT 2023","moderator-pass":"mp","viewer-pass":"ap","record":false,"voice-conf":"79052","dial-number":"613-555-1234","max-users":0,"metadata":{}}},"event":{"ts":1696511528509}}} -{"data":{"type":"event","id":"user-joined","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"user":{"internal-user-id":"w_1fewpbchnudl","external-user-id":"w_1fewpbchnudl","name":"User 1430416","role":"MODERATOR","presenter":"false"}},"event":{"ts":1696511534740}}} -{"data":{"type":"event","id":"user-presenter-assigned","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"user":{"internal-user-id":"w_1fewpbchnudl","external-user-id":"w_1fewpbchnudl"}},"event":{"ts":1696511534773}}} -{"data":{"type":"event","id":"user-presenter-assigned","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"user":{"internal-user-id":"w_1fewpbchnudl","external-user-id":"w_1fewpbchnudl"}},"event":{"ts":1696511534830}}} -{"data":{"type":"event","id":"user-presenter-assigned","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"user":{"internal-user-id":"w_1fewpbchnudl","external-user-id":"w_1fewpbchnudl"}},"event":{"ts":1696511534878}}} -{"data":{"type":"event","id":"user-joined","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"user":{"internal-user-id":"w_lxcl6yagcfbj","external-user-id":"w_lxcl6yagcfbj","name":"User 1430416","role":"VIEWER","presenter":"false"}},"event":{"ts":1696511539543}}} -{"data":{"type":"event","id":"user-audio-voice-enabled","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"user":{"internal-user-id":"w_1fewpbchnudl","external-user-id":"w_1fewpbchnudl","listening-only":false,"sharing-mic":true}},"event":{"ts":1696511543354}}} -{"data":{"type":"event","id":"user-audio-voice-disabled","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"user":{"internal-user-id":"w_1fewpbchnudl","external-user-id":"w_1fewpbchnudl","listening-only":false,"sharing-mic":false}},"event":{"ts":1696511545350}}} -{"data":{"type":"event","id":"user-cam-broadcast-start","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"user":{"internal-user-id":"w_1fewpbchnudl","external-user-id":"w_1fewpbchnudl","stream":"w_1fewpbchnudl_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}},"event":{"ts":1696511549091}}} -{"data":{"type":"event","id":"user-cam-broadcast-end","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"user":{"internal-user-id":"w_1fewpbchnudl","external-user-id":"w_1fewpbchnudl","stream":"w_1fewpbchnudl_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}},"event":{"ts":1696511550383}}} -{"data":{"type":"event","id":"meeting-screenshare-started","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"user":{"internal-user-id":"w_1fewpbchnudl","external-user-id":"w_1fewpbchnudl"}},"event":{"ts":1696511553283}}} -{"data":{"type":"event","id":"meeting-screenshare-stopped","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"user":{"internal-user-id":"w_1fewpbchnudl","external-user-id":"w_1fewpbchnudl"}},"event":{"ts":1696511556391}}} -{"data":{"type":"event","id":"chat-group-message-sent","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"chat-message":{"id":"1696511561799-rplr4p2r","message":"Public chat test","sender":{"internal-user-id":"w_1fewpbchnudl","name":"User 1430416","time":1696511561799}},"chat-id":"MAIN-PUBLIC-GROUP-CHAT"},"event":{"ts":1696511561816}}} -{"data":{"type":"event","id":"user-emoji-changed","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"user":{"internal-user-id":"w_1fewpbchnudl","external-user-id":"w_1fewpbchnudl","emoji":"🙁"}},"event":{"ts":1696511567694}}} -{"data":{"type":"event","id":"user-emoji-changed","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"user":{"internal-user-id":"w_1fewpbchnudl","external-user-id":"w_1fewpbchnudl","emoji":"none"}},"event":{"ts":1696511568939}}} -{"data":{"type":"event","id":"user-raise-hand-changed","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"user":{"internal-user-id":"w_1fewpbchnudl","external-user-id":"w_1fewpbchnudl","raise-hand":true}},"event":{"ts":1696511569968}}} -{"data":{"type":"event","id":"user-raise-hand-changed","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"user":{"internal-user-id":"w_1fewpbchnudl","external-user-id":"w_1fewpbchnudl","raise-hand":false}},"event":{"ts":1696511571012}}} -{"data":{"type":"event","id":"poll-started","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"user":{"internal-user-id":"w_1fewpbchnudl","external-user-id":"w_1fewpbchnudl"},"poll":{"id":"820cff23c27f85f7cccaffb1c6f2fd2d9adcede7-1696511528075/1/1696511581955","question":"ABCD Poll test (public)","answers":[{"id":0,"key":"A"},{"id":1,"key":"B"},{"id":2,"key":"C"},{"id":3,"key":"D"}]}},"event":{"ts":1696511581988}}} -{"data":{"type":"event","id":"poll-responded","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"user":{"internal-user-id":"w_1fewpbchnudl","external-user-id":"w_1fewpbchnudl"},"poll":{"id":"820cff23c27f85f7cccaffb1c6f2fd2d9adcede7-1696511528075/1/1696511581955","answerIds":[0]}},"event":{"ts":1696511584276}}} -{"data":{"type":"event","id":"user-left","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"},"user":{"internal-user-id":"w_lxcl6yagcfbj","external-user-id":"w_lxcl6yagcfbj"}},"event":{"ts":1696511603380}}} -{"data":{"type":"event","id":"meeting-ended","attributes":{"meeting":{"internal-meeting-id":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","external-meeting-id":"random-3136035"}},"event":{"ts":1696511606899}}} +{"data":{"type":"event","id":"meeting-created","attributes":{"meeting":{"internal-meeting-id":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","external-meeting-id":"random-9019868","name":"random-9019868","is-breakout":false,"parent-id":"bbb-none","duration":0,"create-time":1698771157700,"create-date":"Tue Oct 31 13:52:37 BRT 2023","moderator-pass":"mp","viewer-pass":"ap","record":false,"voice-conf":"71347","dial-number":"613-555-1234","max-users":0,"metadata":{}}},"event":{"ts":1698771158138}}} +{"data":{"type":"event","id":"user-joined","attributes":{"meeting":{"internal-meeting-id":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","external-meeting-id":"random-9019868"},"user":{"internal-user-id":"w_xfsb9gxtlfom","external-user-id":"w_xfsb9gxtlfom","name":"John Doe","role":"MODERATOR","presenter":false}},"event":{"ts":1698771164651}}} +{"data":{"type":"event","id":"user-presenter-assigned","attributes":{"meeting":{"internal-meeting-id":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","external-meeting-id":"random-9019868"},"user":{"internal-user-id":"w_xfsb9gxtlfom","external-user-id":"w_xfsb9gxtlfom"}},"event":{"ts":1698771164695}}} +{"data":{"type":"event","id":"user-presenter-assigned","attributes":{"meeting":{"internal-meeting-id":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","external-meeting-id":"random-9019868"},"user":{"internal-user-id":"w_xfsb9gxtlfom","external-user-id":"w_xfsb9gxtlfom"}},"event":{"ts":1698771164762}}} +{"data":{"type":"event","id":"user-presenter-assigned","attributes":{"meeting":{"internal-meeting-id":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","external-meeting-id":"random-9019868"},"user":{"internal-user-id":"w_xfsb9gxtlfom","external-user-id":"w_xfsb9gxtlfom"}},"event":{"ts":1698771164807}}} +{"data":{"type":"event","id":"user-joined","attributes":{"meeting":{"internal-meeting-id":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","external-meeting-id":"random-9019868"},"user":{"internal-user-id":"w_uxw0dzhtvk0l","external-user-id":"w_uxw0dzhtvk0l","name":"Mary Sue","role":"VIEWER","presenter":false}},"event":{"ts":1698771173896}}} +{"data":{"type":"event","id":"user-audio-voice-enabled","attributes":{"meeting":{"internal-meeting-id":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","external-meeting-id":"random-9019868"},"user":{"internal-user-id":"w_xfsb9gxtlfom","external-user-id":"w_xfsb9gxtlfom","listening-only":false,"sharing-mic":true,"muted":false}},"event":{"ts":1698771180561}}} +{"data":{"type":"event","id":"user-audio-muted","attributes":{"meeting":{"internal-meeting-id":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","external-meeting-id":"random-9019868"},"user":{"internal-user-id":"w_xfsb9gxtlfom","external-user-id":"w_xfsb9gxtlfom","muted":true}},"event":{"ts":1698771185945}}} +{"data":{"type":"event","id":"user-audio-unmuted","attributes":{"meeting":{"internal-meeting-id":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","external-meeting-id":"random-9019868"},"user":{"internal-user-id":"w_xfsb9gxtlfom","external-user-id":"w_xfsb9gxtlfom","muted":false}},"event":{"ts":1698771197509}}} +{"data":{"type":"event","id":"user-audio-voice-disabled","attributes":{"meeting":{"internal-meeting-id":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","external-meeting-id":"random-9019868"},"user":{"internal-user-id":"w_xfsb9gxtlfom","external-user-id":"w_xfsb9gxtlfom","listening-only":false,"sharing-mic":false,"muted":true}},"event":{"ts":1698771202728}}} +{"data":{"type":"event","id":"user-cam-broadcast-start","attributes":{"meeting":{"internal-meeting-id":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","external-meeting-id":"random-9019868"},"user":{"internal-user-id":"w_xfsb9gxtlfom","external-user-id":"w_xfsb9gxtlfom","stream":"w_xfsb9gxtlfom_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}},"event":{"ts":1698771212439}}} +{"data":{"type":"event","id":"user-cam-broadcast-end","attributes":{"meeting":{"internal-meeting-id":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","external-meeting-id":"random-9019868"},"user":{"internal-user-id":"w_xfsb9gxtlfom","external-user-id":"w_xfsb9gxtlfom","stream":"w_xfsb9gxtlfom_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}},"event":{"ts":1698771219314}}} +{"data":{"type":"event","id":"meeting-screenshare-started","attributes":{"meeting":{"internal-meeting-id":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","external-meeting-id":"random-9019868"},"user":{"internal-user-id":"w_xfsb9gxtlfom","external-user-id":"w_xfsb9gxtlfom"}},"event":{"ts":1698771224459}}} +{"data":{"type":"event","id":"meeting-screenshare-stopped","attributes":{"meeting":{"internal-meeting-id":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","external-meeting-id":"random-9019868"},"user":{"internal-user-id":"w_xfsb9gxtlfom","external-user-id":"w_xfsb9gxtlfom"}},"event":{"ts":1698771228671}}} +{"data":{"type":"event","id":"chat-group-message-sent","attributes":{"meeting":{"internal-meeting-id":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","external-meeting-id":"random-9019868"},"chat-message":{"id":"1698771232706-9idm9pxf","message":"Public chat test","sender":{"internal-user-id":"w_xfsb9gxtlfom","name":"John Doe","time":1698771232706}},"chat-id":"MAIN-PUBLIC-GROUP-CHAT"},"event":{"ts":1698771232718}}} +{"data":{"type":"event","id":"user-emoji-changed","attributes":{"meeting":{"internal-meeting-id":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","external-meeting-id":"random-9019868"},"user":{"internal-user-id":"w_xfsb9gxtlfom","external-user-id":"w_xfsb9gxtlfom","emoji":"🙁"}},"event":{"ts":1698771238519}}} +{"data":{"type":"event","id":"user-emoji-changed","attributes":{"meeting":{"internal-meeting-id":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","external-meeting-id":"random-9019868"},"user":{"internal-user-id":"w_xfsb9gxtlfom","external-user-id":"w_xfsb9gxtlfom","emoji":"none"}},"event":{"ts":1698771241768}}} +{"data":{"type":"event","id":"user-raise-hand-changed","attributes":{"meeting":{"internal-meeting-id":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","external-meeting-id":"random-9019868"},"user":{"internal-user-id":"w_xfsb9gxtlfom","external-user-id":"w_xfsb9gxtlfom","raise-hand":true}},"event":{"ts":1698771246543}}} +{"data":{"type":"event","id":"user-raise-hand-changed","attributes":{"meeting":{"internal-meeting-id":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","external-meeting-id":"random-9019868"},"user":{"internal-user-id":"w_xfsb9gxtlfom","external-user-id":"w_xfsb9gxtlfom","raise-hand":false}},"event":{"ts":1698771251456}}} +{"data":{"type":"event","id":"poll-started","attributes":{"meeting":{"internal-meeting-id":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","external-meeting-id":"random-9019868"},"user":{"internal-user-id":"w_xfsb9gxtlfom","external-user-id":"w_xfsb9gxtlfom"},"poll":{"id":"2be5820127b5f8efc050fa003e83ef53fa62c356-1698771157729/1/1698771266437","question":"ABCD Poll Test","answers":[{"id":0,"key":"A"},{"id":1,"key":"B"},{"id":2,"key":"C"},{"id":3,"key":"D"}]}},"event":{"ts":1698771266455}}} +{"data":{"type":"event","id":"poll-responded","attributes":{"meeting":{"internal-meeting-id":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","external-meeting-id":"random-9019868"},"user":{"internal-user-id":"w_xfsb9gxtlfom","external-user-id":"w_xfsb9gxtlfom"},"poll":{"id":"2be5820127b5f8efc050fa003e83ef53fa62c356-1698771157729/1/1698771266437","answerIds":[0]}},"event":{"ts":1698771271189}}} +{"data":{"type":"event","id":"user-left","attributes":{"meeting":{"internal-meeting-id":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","external-meeting-id":"random-9019868"},"user":{"internal-user-id":"w_uxw0dzhtvk0l","external-user-id":"w_uxw0dzhtvk0l"}},"event":{"ts":1698771293007}}} +{"data":{"type":"event","id":"user-left","attributes":{"meeting":{"internal-meeting-id":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","external-meeting-id":"random-9019868"},"user":{"internal-user-id":"w_xfsb9gxtlfom","external-user-id":"w_xfsb9gxtlfom"}},"event":{"ts":1698771303012}}} +{"data":{"type":"event","id":"meeting-ended","attributes":{"meeting":{"internal-meeting-id":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","external-meeting-id":"random-9019868"}},"event":{"ts":1698771310228}}} diff --git a/example/events/raw-events.json b/example/events/raw-events.json index cdfb9d6..82df278 100644 --- a/example/events/raw-events.json +++ b/example/events/raw-events.json @@ -1,21 +1,24 @@ -{"envelope":{"name":"MeetingCreatedEvtMsg","routing":{"sender":"bbb-apps-akka"},"timestamp":1696511528407},"core":{"header":{"name":"MeetingCreatedEvtMsg"},"body":{"props":{"meetingProp":{"name":"random-3136035","extId":"random-3136035","intId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","meetingCameraCap":50,"maxPinnedCameras":3,"isBreakout":false,"disabledFeatures":[],"notifyRecordingIsOn":false,"presentationUploadExternalDescription":"","presentationUploadExternalUrl":""},"breakoutProps":{"parentId":"bbb-none","sequence":0,"freeJoin":false,"breakoutRooms":[],"record":false,"privateChatEnabled":true,"captureNotes":false,"captureSlides":false,"captureNotesFilename":"%%CONFNAME%%","captureSlidesFilename":"%%CONFNAME%%"},"durationProps":{"duration":0,"createdTime":1696511528029,"createdDate":"Thu Oct 05 10:12:08 BRT 2023","meetingExpireIfNoUserJoinedInMinutes":5,"meetingExpireWhenLastUserLeftInMinutes":1,"userInactivityInspectTimerInMinutes":0,"userInactivityThresholdInMinutes":30,"userActivitySignResponseDelayInMinutes":5,"endWhenNoModerator":false,"endWhenNoModeratorDelayInMinutes":1},"password":{"moderatorPass":"mp","viewerPass":"ap","learningDashboardAccessToken":"qhuyxxcosvom"},"recordProp":{"record":false,"autoStartRecording":false,"allowStartStopRecording":true,"recordFullDurationMedia":false,"keepEvents":true},"welcomeProp":{"welcomeMsgTemplate":"
Welcome to %%CONFNAME%%!","welcomeMsg":"
Welcome to random-3136035!","modOnlyMessage":""},"voiceProp":{"telVoice":"79052","voiceConf":"79052","dialNumber":"613-555-1234","muteOnStart":false},"usersProp":{"maxUsers":0,"maxUserConcurrentAccesses":3,"webcamsOnlyForModerator":false,"userCameraCap":3,"guestPolicy":"ASK_MODERATOR","meetingLayout":"CUSTOM_LAYOUT","allowModsToUnmuteUsers":true,"allowModsToEjectCameras":true,"authenticatedGuest":false},"metadataProp":{"metadata":{}},"lockSettingsProps":{"disableCam":false,"disableMic":false,"disablePrivateChat":false,"disablePublicChat":false,"disableNotes":false,"hideUserList":false,"lockOnJoin":true,"lockOnJoinConfigurable":false,"hideViewersCursor":false,"hideViewersAnnotation":false},"systemProps":{"html5InstanceId":1},"groups":[]}}}} -{"envelope":{"name":"UserJoinedMeetingEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"timestamp":1696511534721},"core":{"header":{"name":"UserJoinedMeetingEvtMsg","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"body":{"intId":"w_1fewpbchnudl","extId":"w_1fewpbchnudl","name":"User 1430416","role":"MODERATOR","guest":false,"authed":true,"guestStatus":"ALLOW","emoji":"none","reactionEmoji":"none","raiseHand":false,"away":false,"pin":false,"presenter":false,"locked":true,"avatar":"","color":"#7b1fa2","clientType":"HTML5"}}} -{"envelope":{"name":"PresenterAssignedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"timestamp":1696511534736},"core":{"header":{"name":"PresenterAssignedEvtMsg","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"body":{"presenterId":"w_1fewpbchnudl","presenterName":"User 1430416","assignedBy":"w_1fewpbchnudl"}}} -{"envelope":{"name":"PresenterAssignedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"timestamp":1696511534827},"core":{"header":{"name":"PresenterAssignedEvtMsg","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"body":{"presenterId":"w_1fewpbchnudl","presenterName":"User 1430416","assignedBy":"w_1fewpbchnudl"}}} -{"envelope":{"name":"PresenterAssignedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"timestamp":1696511534875},"core":{"header":{"name":"PresenterAssignedEvtMsg","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"body":{"presenterId":"w_1fewpbchnudl","presenterName":"User 1430416","assignedBy":"w_1fewpbchnudl"}}} -{"envelope":{"name":"UserJoinedMeetingEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_lxcl6yagcfbj"},"timestamp":1696511539538},"core":{"header":{"name":"UserJoinedMeetingEvtMsg","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_lxcl6yagcfbj"},"body":{"intId":"w_lxcl6yagcfbj","extId":"w_lxcl6yagcfbj","name":"User 1430416","role":"VIEWER","guest":false,"authed":true,"guestStatus":"ALLOW","emoji":"none","reactionEmoji":"none","raiseHand":false,"away":false,"pin":false,"presenter":false,"locked":true,"avatar":"","color":"#6a1b9a","clientType":"HTML5"}}} -{"envelope":{"name":"UserJoinedVoiceConfToClientEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"timestamp":1696511543338},"core":{"header":{"name":"UserJoinedVoiceConfToClientEvtMsg","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"body":{"voiceConf":"79052","intId":"w_1fewpbchnudl","voiceUserId":"1","callerName":"User+1430416","callerNum":"w_1fewpbchnudl_1-bbbID-User+1430416","color":"#4a148c","muted":false,"talking":false,"callingWith":"none","listenOnly":false}}} -{"envelope":{"name":"UserLeftVoiceConfToClientEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"timestamp":1696511545342},"core":{"header":{"name":"UserLeftVoiceConfToClientEvtMsg","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"body":{"voiceConf":"79052","intId":"w_1fewpbchnudl","voiceUserId":"w_1fewpbchnudl"}}} -{"envelope":{"name":"UserBroadcastCamStartedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"timestamp":1696511549084},"core":{"header":{"name":"UserBroadcastCamStartedEvtMsg","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"body":{"userId":"w_1fewpbchnudl","stream":"w_1fewpbchnudl_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}}} -{"envelope":{"name":"UserBroadcastCamStoppedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"timestamp":1696511550373},"core":{"header":{"name":"UserBroadcastCamStoppedEvtMsg","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"body":{"userId":"w_1fewpbchnudl","stream":"w_1fewpbchnudl_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}}} -{"envelope":{"name":"ScreenshareRtmpBroadcastStartedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"not-used"},"timestamp":1696511553277},"core":{"header":{"name":"ScreenshareRtmpBroadcastStartedEvtMsg","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"not-used"},"body":{"voiceConf":"79052","screenshareConf":"79052","stream":"d20caa7c-3329-4c46-8f5f-a18b6c95a6fe","vidWidth":0,"vidHeight":0,"timestamp":"1696511553261","hasAudio":true,"contentType":"screenshare"}}} -{"envelope":{"name":"ScreenshareRtmpBroadcastStoppedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"not-used"},"timestamp":1696511556378},"core":{"header":{"name":"ScreenshareRtmpBroadcastStoppedEvtMsg","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"not-used"},"body":{"voiceConf":"","screenshareConf":"","stream":"","vidWidth":0,"vidHeight":0,"timestamp":""}}} -{"envelope":{"name":"GroupChatMessageBroadcastEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"timestamp":1696511561804},"core":{"header":{"name":"GroupChatMessageBroadcastEvtMsg","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"body":{"chatId":"MAIN-PUBLIC-GROUP-CHAT","msg":{"id":"1696511561799-rplr4p2r","timestamp":1696511561799,"correlationId":"w_1fewpbchnudl-1696511561780","sender":{"id":"w_1fewpbchnudl","name":"User 1430416","role":"MODERATOR"},"chatEmphasizedText":true,"message":"Public chat test"}}}} -{"envelope":{"name":"UserReactionEmojiChangedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"timestamp":1696511567688},"core":{"header":{"name":"UserReactionEmojiChangedEvtMsg","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"body":{"userId":"w_1fewpbchnudl","reactionEmoji":"🙁"}}} -{"envelope":{"name":"UserReactionEmojiChangedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"timestamp":1696511568937},"core":{"header":{"name":"UserReactionEmojiChangedEvtMsg","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"body":{"userId":"w_1fewpbchnudl","reactionEmoji":"none"}}} -{"envelope":{"name":"UserRaiseHandChangedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"timestamp":1696511569963},"core":{"header":{"name":"UserRaiseHandChangedEvtMsg","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"body":{"userId":"w_1fewpbchnudl","raiseHand":true}}} -{"envelope":{"name":"UserRaiseHandChangedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"timestamp":1696511571010},"core":{"header":{"name":"UserRaiseHandChangedEvtMsg","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"body":{"userId":"w_1fewpbchnudl","raiseHand":false}}} -{"envelope":{"name":"PollStartedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"timestamp":1696511581965},"core":{"header":{"name":"PollStartedEvtMsg","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"body":{"userId":"w_1fewpbchnudl","pollId":"820cff23c27f85f7cccaffb1c6f2fd2d9adcede7-1696511528075/1/1696511581955","pollType":"A-4","secretPoll":false,"question":"ABCD Poll test (public)","poll":{"id":"820cff23c27f85f7cccaffb1c6f2fd2d9adcede7-1696511528075/1/1696511581955","isMultipleResponse":false,"answers":[{"id":0,"key":"A"},{"id":1,"key":"B"},{"id":2,"key":"C"},{"id":3,"key":"D"}]}}}} -{"envelope":{"name":"UserRespondedToPollRespMsg","routing":{"msgType":"DIRECT","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"timestamp":1696511584252},"core":{"header":{"name":"UserRespondedToPollRespMsg","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_1fewpbchnudl"},"body":{"pollId":"820cff23c27f85f7cccaffb1c6f2fd2d9adcede7-1696511528075/1/1696511581955","userId":"w_lxcl6yagcfbj","answerIds":[0]}}} -{"envelope":{"name":"UserLeftMeetingEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_lxcl6yagcfbj"},"timestamp":1696511603366},"core":{"header":{"name":"UserLeftMeetingEvtMsg","meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029","userId":"w_lxcl6yagcfbj"},"body":{"intId":"w_lxcl6yagcfbj","eject":false,"ejectedBy":"","reason":"","reasonCode":""}}} -{"envelope":{"name":"MeetingDestroyedEvtMsg","routing":{"sender":"bbb-apps-akka"},"timestamp":1696511606884},"core":{"header":{"name":"MeetingDestroyedEvtMsg"},"body":{"meetingId":"038b74cf04c0ee371f939c9e54de060753899589-1696511528029"}}} +{"envelope":{"name":"MeetingCreatedEvtMsg","routing":{"sender":"bbb-apps-akka"},"timestamp":1698771158093},"core":{"header":{"name":"MeetingCreatedEvtMsg"},"body":{"props":{"meetingProp":{"name":"random-9019868","extId":"random-9019868","intId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","meetingCameraCap":50,"maxPinnedCameras":3,"isBreakout":false,"disabledFeatures":[],"notifyRecordingIsOn":false,"presentationUploadExternalDescription":"","presentationUploadExternalUrl":""},"breakoutProps":{"parentId":"bbb-none","sequence":0,"freeJoin":false,"breakoutRooms":[],"record":false,"privateChatEnabled":true,"captureNotes":false,"captureSlides":false,"captureNotesFilename":"%%CONFNAME%%","captureSlidesFilename":"%%CONFNAME%%"},"durationProps":{"duration":0,"createdTime":1698771157700,"createdDate":"Tue Oct 31 13:52:37 BRT 2023","meetingExpireIfNoUserJoinedInMinutes":5,"meetingExpireWhenLastUserLeftInMinutes":1,"userInactivityInspectTimerInMinutes":0,"userInactivityThresholdInMinutes":30,"userActivitySignResponseDelayInMinutes":5,"endWhenNoModerator":false,"endWhenNoModeratorDelayInMinutes":1},"password":{"moderatorPass":"mp","viewerPass":"ap","learningDashboardAccessToken":"igucwyjkab6i"},"recordProp":{"record":false,"autoStartRecording":false,"allowStartStopRecording":true,"recordFullDurationMedia":false,"keepEvents":true},"welcomeProp":{"welcomeMsgTemplate":"
Welcome to %%CONFNAME%%!","welcomeMsg":"
Welcome to random-9019868!","modOnlyMessage":""},"voiceProp":{"telVoice":"71347","voiceConf":"71347","dialNumber":"613-555-1234","muteOnStart":false},"usersProp":{"maxUsers":0,"maxUserConcurrentAccesses":3,"webcamsOnlyForModerator":false,"userCameraCap":3,"guestPolicy":"ASK_MODERATOR","meetingLayout":"CUSTOM_LAYOUT","allowModsToUnmuteUsers":true,"allowModsToEjectCameras":true,"authenticatedGuest":false},"metadataProp":{"metadata":{}},"lockSettingsProps":{"disableCam":false,"disableMic":false,"disablePrivateChat":false,"disablePublicChat":false,"disableNotes":false,"hideUserList":false,"lockOnJoin":true,"lockOnJoinConfigurable":false,"hideViewersCursor":false,"hideViewersAnnotation":false},"systemProps":{"html5InstanceId":1},"groups":[]}}}} +{"envelope":{"name":"UserJoinedMeetingEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"w_xfsb9gxtlfom"},"timestamp":1698771164638},"core":{"header":{"name":"UserJoinedMeetingEvtMsg","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"w_xfsb9gxtlfom"},"body":{"intId":"w_xfsb9gxtlfom","extId":"w_xfsb9gxtlfom","name":"John Doe","role":"MODERATOR","guest":false,"authed":true,"guestStatus":"ALLOW","emoji":"none","reactionEmoji":"none","raiseHand":false,"away":false,"pin":false,"presenter":false,"locked":true,"avatar":"","color":"#7b1fa2","clientType":"HTML5"}}} +{"envelope":{"name":"PresenterAssignedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"w_xfsb9gxtlfom"},"timestamp":1698771164647},"core":{"header":{"name":"PresenterAssignedEvtMsg","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"w_xfsb9gxtlfom"},"body":{"presenterId":"w_xfsb9gxtlfom","presenterName":"John Doe","assignedBy":"w_xfsb9gxtlfom"}}} +{"envelope":{"name":"PresenterAssignedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"w_xfsb9gxtlfom"},"timestamp":1698771164757},"core":{"header":{"name":"PresenterAssignedEvtMsg","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"w_xfsb9gxtlfom"},"body":{"presenterId":"w_xfsb9gxtlfom","presenterName":"John Doe","assignedBy":"w_xfsb9gxtlfom"}}} +{"envelope":{"name":"PresenterAssignedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"w_xfsb9gxtlfom"},"timestamp":1698771164805},"core":{"header":{"name":"PresenterAssignedEvtMsg","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"w_xfsb9gxtlfom"},"body":{"presenterId":"w_xfsb9gxtlfom","presenterName":"John Doe","assignedBy":"w_xfsb9gxtlfom"}}} +{"envelope":{"name":"UserJoinedMeetingEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"w_uxw0dzhtvk0l"},"timestamp":1698771173894},"core":{"header":{"name":"UserJoinedMeetingEvtMsg","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"w_uxw0dzhtvk0l"},"body":{"intId":"w_uxw0dzhtvk0l","extId":"w_uxw0dzhtvk0l","name":"Mary Sue","role":"VIEWER","guest":false,"authed":true,"guestStatus":"ALLOW","emoji":"none","reactionEmoji":"none","raiseHand":false,"away":false,"pin":false,"presenter":false,"locked":true,"avatar":"","color":"#6a1b9a","clientType":"HTML5"}}} +{"envelope":{"name":"UserJoinedVoiceConfToClientEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"w_xfsb9gxtlfom"},"timestamp":1698771180548},"core":{"header":{"name":"UserJoinedVoiceConfToClientEvtMsg","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"w_xfsb9gxtlfom"},"body":{"voiceConf":"71347","intId":"w_xfsb9gxtlfom","voiceUserId":"1","callerName":"John+Doe","callerNum":"w_xfsb9gxtlfom_1-bbbID-John+Doe","color":"#4a148c","muted":false,"talking":false,"callingWith":"none","listenOnly":false}}} +{"envelope":{"name":"UserMutedVoiceEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"w_xfsb9gxtlfom"},"timestamp":1698771185939},"core":{"header":{"name":"UserMutedVoiceEvtMsg","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"w_xfsb9gxtlfom"},"body":{"voiceConf":"71347","intId":"w_xfsb9gxtlfom","voiceUserId":"w_xfsb9gxtlfom","muted":true}}} +{"envelope":{"name":"UserMutedVoiceEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"w_xfsb9gxtlfom"},"timestamp":1698771197507},"core":{"header":{"name":"UserMutedVoiceEvtMsg","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"w_xfsb9gxtlfom"},"body":{"voiceConf":"71347","intId":"w_xfsb9gxtlfom","voiceUserId":"w_xfsb9gxtlfom","muted":false}}} +{"envelope":{"name":"UserLeftVoiceConfToClientEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"w_xfsb9gxtlfom"},"timestamp":1698771202720},"core":{"header":{"name":"UserLeftVoiceConfToClientEvtMsg","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"w_xfsb9gxtlfom"},"body":{"voiceConf":"71347","intId":"w_xfsb9gxtlfom","voiceUserId":"w_xfsb9gxtlfom"}}} +{"envelope":{"name":"UserBroadcastCamStartedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"w_xfsb9gxtlfom"},"timestamp":1698771212433},"core":{"header":{"name":"UserBroadcastCamStartedEvtMsg","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"w_xfsb9gxtlfom"},"body":{"userId":"w_xfsb9gxtlfom","stream":"w_xfsb9gxtlfom_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}}} +{"envelope":{"name":"UserBroadcastCamStoppedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"w_xfsb9gxtlfom"},"timestamp":1698771219309},"core":{"header":{"name":"UserBroadcastCamStoppedEvtMsg","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"w_xfsb9gxtlfom"},"body":{"userId":"w_xfsb9gxtlfom","stream":"w_xfsb9gxtlfom_0e2313d7c9d39860e908e20cd196adc3a6b8bf5c16110fdadc63741f48193c19"}}} +{"envelope":{"name":"ScreenshareRtmpBroadcastStartedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"not-used"},"timestamp":1698771224452},"core":{"header":{"name":"ScreenshareRtmpBroadcastStartedEvtMsg","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"not-used"},"body":{"voiceConf":"71347","screenshareConf":"71347","stream":"182c133e-5512-4695-be20-8cc760dc1595","vidWidth":0,"vidHeight":0,"timestamp":"1698771224444","hasAudio":false,"contentType":"screenshare"}}} +{"envelope":{"name":"ScreenshareRtmpBroadcastStoppedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"not-used"},"timestamp":1698771228664},"core":{"header":{"name":"ScreenshareRtmpBroadcastStoppedEvtMsg","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"not-used"},"body":{"voiceConf":"","screenshareConf":"","stream":"","vidWidth":0,"vidHeight":0,"timestamp":""}}} +{"envelope":{"name":"GroupChatMessageBroadcastEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"w_xfsb9gxtlfom"},"timestamp":1698771232712},"core":{"header":{"name":"GroupChatMessageBroadcastEvtMsg","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"w_xfsb9gxtlfom"},"body":{"chatId":"MAIN-PUBLIC-GROUP-CHAT","msg":{"id":"1698771232706-9idm9pxf","timestamp":1698771232706,"correlationId":"w_xfsb9gxtlfom-1698771232678","sender":{"id":"w_xfsb9gxtlfom","name":"John Doe","role":"MODERATOR"},"chatEmphasizedText":true,"message":"Public chat test"}}}} +{"envelope":{"name":"UserReactionEmojiChangedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"w_xfsb9gxtlfom"},"timestamp":1698771238514},"core":{"header":{"name":"UserReactionEmojiChangedEvtMsg","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"w_xfsb9gxtlfom"},"body":{"userId":"w_xfsb9gxtlfom","reactionEmoji":"🙁"}}} +{"envelope":{"name":"UserReactionEmojiChangedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"w_xfsb9gxtlfom"},"timestamp":1698771241767},"core":{"header":{"name":"UserReactionEmojiChangedEvtMsg","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"w_xfsb9gxtlfom"},"body":{"userId":"w_xfsb9gxtlfom","reactionEmoji":"none"}}} +{"envelope":{"name":"UserRaiseHandChangedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"w_xfsb9gxtlfom"},"timestamp":1698771246539},"core":{"header":{"name":"UserRaiseHandChangedEvtMsg","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"w_xfsb9gxtlfom"},"body":{"userId":"w_xfsb9gxtlfom","raiseHand":true}}} +{"envelope":{"name":"UserRaiseHandChangedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"w_xfsb9gxtlfom"},"timestamp":1698771251455},"core":{"header":{"name":"UserRaiseHandChangedEvtMsg","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"w_xfsb9gxtlfom"},"body":{"userId":"w_xfsb9gxtlfom","raiseHand":false}}} +{"envelope":{"name":"PollStartedEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"w_xfsb9gxtlfom"},"timestamp":1698771266447},"core":{"header":{"name":"PollStartedEvtMsg","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"w_xfsb9gxtlfom"},"body":{"userId":"w_xfsb9gxtlfom","pollId":"2be5820127b5f8efc050fa003e83ef53fa62c356-1698771157729/1/1698771266437","pollType":"A-4","secretPoll":false,"question":"ABCD Poll Test","poll":{"id":"2be5820127b5f8efc050fa003e83ef53fa62c356-1698771157729/1/1698771266437","isMultipleResponse":false,"answers":[{"id":0,"key":"A"},{"id":1,"key":"B"},{"id":2,"key":"C"},{"id":3,"key":"D"}]}}}} +{"envelope":{"name":"UserRespondedToPollRespMsg","routing":{"msgType":"DIRECT","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"w_xfsb9gxtlfom"},"timestamp":1698771271169},"core":{"header":{"name":"UserRespondedToPollRespMsg","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"w_xfsb9gxtlfom"},"body":{"pollId":"2be5820127b5f8efc050fa003e83ef53fa62c356-1698771157729/1/1698771266437","userId":"w_uxw0dzhtvk0l","answerIds":[0]}}} +{"envelope":{"name":"UserLeftMeetingEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"w_uxw0dzhtvk0l"},"timestamp":1698771293000},"core":{"header":{"name":"UserLeftMeetingEvtMsg","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"w_uxw0dzhtvk0l"},"body":{"intId":"w_uxw0dzhtvk0l","eject":false,"ejectedBy":"","reason":"","reasonCode":""}}} +{"envelope":{"name":"UserLeftMeetingEvtMsg","routing":{"msgType":"BROADCAST_TO_MEETING","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"w_xfsb9gxtlfom"},"timestamp":1698771303008},"core":{"header":{"name":"UserLeftMeetingEvtMsg","meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700","userId":"w_xfsb9gxtlfom"},"body":{"intId":"w_xfsb9gxtlfom","eject":false,"ejectedBy":"","reason":"","reasonCode":""}}} +{"envelope":{"name":"MeetingDestroyedEvtMsg","routing":{"sender":"bbb-apps-akka"},"timestamp":1698771310209},"core":{"header":{"name":"MeetingDestroyedEvtMsg"},"body":{"meetingId":"8043c8452ae9830aac14c517adff3839dbd9228f-1698771157700"}}} From 9ac45f218a08dcfe2b80e88bc9c7a52ac0d6e8a2 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Tue, 31 Oct 2023 15:11:38 -0300 Subject: [PATCH 112/154] test: restore remaining out/webhooks tests, remove nock --- package-lock.json | 54 -------------- package.json | 3 +- test/helpers.js | 28 ++++---- test/hooks-post-catcher.js | 109 ++++++++++++++++++++++++++++ test/test.js | 143 ++++++++++++++++++------------------- 5 files changed, 193 insertions(+), 144 deletions(-) create mode 100644 test/hooks-post-catcher.js diff --git a/package-lock.json b/package-lock.json index 67a45a1..59a5415 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,6 @@ "fast-xml-parser": "^4.3.2", "jsdoc": "^4.0.2", "mocha": "^9.2.2", - "nock": "^13.3.3", "nodemon": "^3.0.1", "pino-pretty": "^10.2.3", "sinon": "^12.0.1", @@ -2975,12 +2974,6 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true - }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -3489,44 +3482,6 @@ "isarray": "0.0.1" } }, - "node_modules/nock": { - "version": "13.3.3", - "resolved": "https://registry.npmjs.org/nock/-/nock-13.3.3.tgz", - "integrity": "sha512-z+KUlILy9SK/RjpeXDiDUEAq4T94ADPHE3qaRkf66mpEhzc/ytOMm3Bwdrbq6k1tMWkbdujiKim3G2tfQARuJw==", - "dev": true, - "dependencies": { - "debug": "^4.1.0", - "json-stringify-safe": "^5.0.1", - "lodash": "^4.17.21", - "propagate": "^2.0.0" - }, - "engines": { - "node": ">= 10.13" - } - }, - "node_modules/nock/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/nock/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "node_modules/node-abort-controller": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", @@ -4038,15 +3993,6 @@ "node": ">=10" } }, - "node_modules/propagate": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", - "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", diff --git a/package.json b/package.json index 972955e..ffdfdc0 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "start": "node app.js", "dev-start": "nodemon --watch src --ext js,json,yml,yaml --exec node app.js", - "test": "ALLOW_CONFIG_MUTATIONS=true mocha", + "test": "LOG_LEVEL=silent ALLOW_CONFIG_MUTATIONS=true mocha", "lint": "./node_modules/.bin/eslint ./", "lint:file": "./node_modules/.bin/eslint", "jsdoc": "./node_modules/.bin/jsdoc app.js application.js src/ -r" @@ -38,7 +38,6 @@ "fast-xml-parser": "^4.3.2", "jsdoc": "^4.0.2", "mocha": "^9.2.2", - "nock": "^13.3.3", "nodemon": "^3.0.1", "pino-pretty": "^10.2.3", "sinon": "^12.0.1", diff --git a/test/helpers.js b/test/helpers.js index f8d6e5c..7d2c953 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -2,7 +2,9 @@ const helpers = {}; helpers.url = 'http://127.0.0.1'; helpers.port = ':3005'; -helpers.callback = 'http://we2bh.requestcatcher.com' +helpers.callback = 'http://127.0.0.1:3008/callback'; +helpers.rawCatcherURL = 'http://127.0.0.1:3006/callback'; +helpers.mappedCatcherURL = 'http://127.0.0.1:3007/callback'; helpers.callbackURL = '?callbackURL=' + helpers.callback helpers.apiPath = '/bigbluebutton/api/hooks/' helpers.createUrl = helpers.port + helpers.apiPath + 'create/' + helpers.callbackURL @@ -14,23 +16,23 @@ helpers.listUrl = 'list/' helpers.rawMessage = { envelope: { name: 'PresenterAssignedEvtMsg', - routing: { - msgType: 'BROADCAST_TO_MEETING', - meetingId: 'a674bb9c6ff92bfa6d5a0a1e530fabb56023932e-1509387833678', - userId: 'w_ysgy0erqgayc' - } + routing: { + msgType: 'BROADCAST_TO_MEETING', + meetingId: 'a674bb9c6ff92bfa6d5a0a1e530fabb56023932e-1509387833678', + userId: 'w_ysgy0erqgayc' + } }, core: { header: { - name: 'PresenterAssignedEvtMsg', - meetingId: 'a674bb9c6ff92bfa6d5a0a1e530fabb56023932e-1509387833678', - userId: 'w_ysgy0erqgayc' + name: 'PresenterAssignedEvtMsg', + meetingId: 'a674bb9c6ff92bfa6d5a0a1e530fabb56023932e-1509387833678', + userId: 'w_ysgy0erqgayc' }, body: { - presenterId: 'w_ysgy0erqgayc', - presenterName: 'User 4125097', - assignedBy: 'w_vlnwu1wkhena' - } + presenterId: 'w_ysgy0erqgayc', + presenterName: 'User 4125097', + assignedBy: 'w_vlnwu1wkhena' + } } }; diff --git a/test/hooks-post-catcher.js b/test/hooks-post-catcher.js new file mode 100644 index 0000000..f738599 --- /dev/null +++ b/test/hooks-post-catcher.js @@ -0,0 +1,109 @@ +/* eslint no-console: "off" */ +import express from "express"; +import fetch from "node-fetch"; +import crypto from "crypto"; +import bodyParser from 'body-parser'; +import EventEmitter from 'events'; + +const encodeForUrl = (value) => { + return encodeURIComponent(value) + .replace(/%20/g, '+') + .replace(/[!'()]/g, escape) + .replace(/\*/g, "%2A") +}; + +class HooksPostCatcher extends EventEmitter { + static encodeForUrl = encodeForUrl; + + constructor (url, { useLogger = false } = {}) { + super(); + this.url = url; + this.started = false; + this._parsedUrl = new URL(url); + this.port = this._parsedUrl.port; + this.logger = useLogger ? this.logger : { log: () => {}, error: () => {} }; + if (!this.port) throw new Error("Port not specified in URL"); + } + + start () { + return new Promise((resolve, reject) => { + this.app = express(); + this.app.use(bodyParser.json()); + this.app.use(bodyParser.urlencoded({ + extended: true + })); + this.app.post("/callback", (req, res) => { + try { + this.logger.log("-------------------------------------"); + this.logger.log("* Received:", req.url); + this.logger.log("* Body:", req.body); + this.logger.log("-------------------------------------\n"); + res.statusCode = 200; + res.send(); + this.emit("callback", req.body); + } catch (error) { + this.logger.error("Error processing callback:", error); + res.statusCode = 500; + res.send(); + this.emit("error", error); + } + }); + + this.server = this.app.listen(this.port, (error) => { + if (error) { + this.logger.error("Error starting server:", error); + reject(error); + return; + } + + this.logger.log("Server listening on", this.url); + this.started = true; + resolve(); + }); + }); + } + + stop () { + this.server.close(); + this.started = false; + this.removeAllListeners(); + } + + async createHook (bbbDomain, sharedSecret, { + getRaw = false, + eventId = null, + meetingId = null, + } = {}) { + if (!this.started) this.start(); + const myUrl = this.url; + let params = `callbackURL=${HooksPostCatcher.encodeForUrl(myUrl)}&getRaw=${getRaw}`; + if (eventId) params += "&eventID=" + eventId; + if (meetingId) params += "&meetingID=" + meetingId; + const checksum = crypto + .createHash('sha1') + .update("hooks/create" + params + sharedSecret).digest('hex'); + const fullUrl = `http://${bbbDomain}/bigbluebutton/api/hooks/create?` + + params + "&checksum=" + checksum + this.logger.log("Registering a hook with", fullUrl); + + const controller = new AbortController(); + const abortTimeout = setTimeout(controller.abort, 2500); + + try { + const response = await fetch(fullUrl, { signal: controller.signal }); + const text = await response.text(); + if (response.ok) { + this.logger.debug("Hook registered - response from hook/create:", text); + } else { + throw new Error(text); + } + } catch (error) { + this.logger.error("Hook registration failed - response from hook/create:", error); + throw error; + } finally { + clearTimeout(abortTimeout); + } + } +} + +export default HooksPostCatcher; diff --git a/test/test.js b/test/test.js index 7b9167c..c36c0fd 100644 --- a/test/test.js +++ b/test/test.js @@ -1,15 +1,17 @@ import { describe, it, before, after, beforeEach } from 'mocha'; import request from 'supertest'; -import nock from "nock"; -import Utils from '../src/out/webhooks/utils.js'; +import redis from 'redis'; import config from 'config'; +import Utils from '../src/out/webhooks/utils.js'; import Hook from '../src/db/redis/hooks.js'; import Helpers from './helpers.js' import Application from '../application.js'; -import redis from 'redis'; +import HooksPostCatcher from './hooks-post-catcher.js'; const TEST_CHANNEL = 'test-channel'; -const SHARED_SECRET = process.env.SHARED_SECRET || function () { throw new Error('SHARED_SECRET not set'); }(); +const SHARED_SECRET = process.env.SHARED_SECRET + || config.has('bbb.sharedSecret') ? config.get('bbb.sharedSecret') : false + || function () { throw new Error('SHARED_SECRET not set'); }(); const MODULES = config.get('modules'); const WH_CONFIG = MODULES['../out/webhooks/index.js'].config; const IN_REDIS_CONFIG = MODULES['../in/redis/index.js'].config.redis; @@ -25,13 +27,17 @@ describe('bbb-webhooks tests', () => { before((done) => { WH_CONFIG.queueSize = 10; - WH_CONFIG.permanentURLs = [ { url: "https://wh.requestcatcher.com", getRaw: true } ]; + WH_CONFIG.permanentURLs = [ + { url: Helpers.rawCatcherURL, getRaw: true }, + { url: Helpers.mappedCatcherURL, getRaw: false }, + ]; IN_REDIS_CONFIG.inboundChannels = [...IN_REDIS_CONFIG.inboundChannels, TEST_CHANNEL]; application.start() .then(redisClient.connect()) .then(() => { done(); }) .catch(done); }); + beforeEach((done) => { const hooks = Hook.get().getAllGlobalHooks(); Helpers.flushall(redisClient); @@ -41,12 +47,14 @@ describe('bbb-webhooks tests', () => { done(); }) - after(() => { + + after((done) => { const hooks = Hook.get().getAllGlobalHooks(); Helpers.flushall(redisClient); hooks.forEach((hook) => { Helpers.flushredis(hook); }); + done(); }); describe('GET /hooks/list permanent', () => { @@ -79,6 +87,7 @@ describe('bbb-webhooks tests', () => { getRaw: false, }).then(() => { done(); }).catch(done); }); + it('should destroy a hook', (done) => { const hooks = Hook.get().getAllGlobalHooks(); const hook = hooks[hooks.length-1].id; @@ -105,8 +114,7 @@ describe('bbb-webhooks tests', () => { Helpers.url + Helpers.destroyPermanent, SHARED_SECRET, CHECKSUM_ALGORITHM, - ); - getUrl = Helpers.destroyPermanent + '&checksum=' + getUrl + ); getUrl = Helpers.destroyPermanent + '&checksum=' + getUrl request(Helpers.url) .get(getUrl) .expect('Content-Type', /text\/xml/) @@ -114,8 +122,7 @@ describe('bbb-webhooks tests', () => { const hooks = Hook.get().getAllGlobalHooks(); if (hooks && hooks[0].payload.callbackURL == WH_CONFIG.permanentURLs[0].url) { done(); - } - else { + } else { done(new Error("should not delete permanent")); } }) @@ -129,6 +136,7 @@ describe('bbb-webhooks tests', () => { .then(() => { done(); }) .catch(done); }); + it('should create a hook with getRaw=true', (done) => { let getUrl = Utils.checksumAPI( Helpers.url + Helpers.createUrl + Helpers.createRaw, @@ -144,8 +152,7 @@ describe('bbb-webhooks tests', () => { const hooks = Hook.get().getAllGlobalHooks(); if (hooks && hooks.some((hook) => { return hook.payload.getRaw })) { done(); - } - else { + } else { done(new Error("getRaw hook was not created")) } }) @@ -153,95 +160,81 @@ describe('bbb-webhooks tests', () => { }); describe('/POST mapped message', () => { + let catcher; + before((done) => { + catcher = new HooksPostCatcher(WH_CONFIG.permanentURLs[1].url); const hooks = Hook.get().getAllGlobalHooks(); const hook = hooks[0]; Helpers.flushredis(hook); - done(); + catcher.start().then(() => { + done(); + }); }); - after(() => { + + after((done) => { const hooks = Hook.get().getAllGlobalHooks(); const hook = hooks[0]; Helpers.flushredis(hook); + catcher.stop(); + done(); }) + it('should post mapped message ', (done) => { - const hooks = Hook.get().getAllGlobalHooks(); - const hook = hooks[0]; - const getpost = nock(WH_CONFIG.permanentURLs[0].url) - .filteringRequestBody((body) => { - let parsed = JSON.parse(body) - return parsed[0].data.id ? "mapped" : "not mapped"; - }) - .post("/", "mapped") - .reply(200, (res) => { - done(); - }); + catcher.once('callback', (body) => { + try { + let parsed = JSON.parse(body?.event); + if (parsed[0].data?.id) { + done(); + } else { + done(new Error("unmapped message")); + } + } catch (error) { + done(error); + } + }); + redisClient.publish(TEST_CHANNEL, JSON.stringify(Helpers.rawMessage)); }) }); + describe('/POST raw message', () => { + let catcher; + before((done) => { - const hooks = Hook.get().getAllGlobalHooks(); - const hook = hooks[0]; - Helpers.flushredis(hook); - done(); + catcher = new HooksPostCatcher(WH_CONFIG.permanentURLs[0].url); + const hooks = Hook.get().getAllGlobalHooks(); + const hook = hooks[0]; + Helpers.flushredis(hook); + catcher.start().then(() => { + done(); + }); }); + after((done) => { + catcher.stop(); const hooks = Hook.get().getAllGlobalHooks(); Hook.get().removeSubscription(hooks[hooks.length-1].id) .then(() => { done(); }) .catch(done); Helpers.flushredis(hooks[hooks.length-1]); }); - it('should post raw message ', (done) => { - const hooks = Hook.get().getAllGlobalHooks(); - const hook = hooks[0]; - const getpost = nock(Helpers.callback) - .filteringRequestBody( (body) => { - if (body.indexOf("PresenterAssignedEvtMsg")) { - return "raw message"; + it('should post raw message ', (done) => { + catcher.once('callback', (body) => { + try { + let parsed = JSON.parse(body?.event); + if (parsed[0]?.envelope?.name == Helpers.rawMessage.envelope.name) { + done(); + } else { + done(new Error("message is not raw")); } - else { return "not raw"; } - }) - .post("/", "raw message") - .reply(200, () => { - done(); - }); - const permanent = nock(WH_CONFIG.permanentURLs[0].url) - .post("/") - .reply(200) - redisClient.publish(TEST_CHANNEL, JSON.stringify(Helpers.rawMessage)); - }) - }); + } catch (error) { + done(error); + } + }); - describe('/POST multi message', () => { - before( () =>{ - const hooks = Hook.get().getAllGlobalHooks(); - const hook = hooks[0]; - Helpers.flushredis(hook); - hook.queue = ["multiMessage1"]; - }); - it('should post multi message ', (done) => { - const hooks = Hook.get().getAllGlobalHooks(); - const hook = hooks[0]; - hook.enqueue("multiMessage2") - const getpost = nock(WH_CONFIG.permanentURLs[0].url) - .filteringPath( (path) => { - return path.split('?')[0]; - }) - .filteringRequestBody( (body) => { - if (body.indexOf("multiMessage1") != -1 && body.indexOf("multiMessage2") != -1) { - return "multiMess" - } - else { - return "not multi" - } - }) - .post("/", "multiMess") - .reply(200, (res) => { - done(); - }); + redisClient.publish(TEST_CHANNEL, JSON.stringify(Helpers.rawMessage)); }) }); }); From ce740b36d6009635724cc7b312d3c95138ec15fe Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Wed, 1 Nov 2023 00:00:05 -0300 Subject: [PATCH 113/154] test: split test suites, add XAPI test suite template --- package.json | 4 +- test/hooks-post-catcher.js | 109 ---------------------------- test/index.js | 70 ++++++++++++++++++ test/utils/post-catcher.js | 75 +++++++++++++++++++ test/{ => webhooks}/helpers.js | 0 test/webhooks/hooks-post-catcher.js | 49 +++++++++++++ test/{test.js => webhooks/index.js} | 70 +++++++++--------- test/xapi/events.js | 38 ++++++++++ test/xapi/index.js | 93 ++++++++++++++++++++++++ 9 files changed, 363 insertions(+), 145 deletions(-) delete mode 100644 test/hooks-post-catcher.js create mode 100644 test/index.js create mode 100644 test/utils/post-catcher.js rename test/{ => webhooks}/helpers.js (100%) create mode 100644 test/webhooks/hooks-post-catcher.js rename test/{test.js => webhooks/index.js} (79%) create mode 100644 test/xapi/events.js create mode 100644 test/xapi/index.js diff --git a/package.json b/package.json index ffdfdc0..b61447f 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "scripts": { "start": "node app.js", "dev-start": "nodemon --watch src --ext js,json,yml,yaml --exec node app.js", - "test": "LOG_LEVEL=silent ALLOW_CONFIG_MUTATIONS=true mocha", + "test": "LOG_LEVEL=silent ALL_TESTS=true ALLOW_CONFIG_MUTATIONS=true mocha", + "test:webhooks": "LOG_LEVEL=silent ALL_TESTS=false ALLOW_CONFIG_MUTATIONS=true WEBHOOKS_SUITE=true mocha", + "test:xapi": "LOG_LEVEL=silent ALL_TESTS=false ALLOW_CONFIG_MUTATIONS=true XAPI_SUITE=true mocha", "lint": "./node_modules/.bin/eslint ./", "lint:file": "./node_modules/.bin/eslint", "jsdoc": "./node_modules/.bin/jsdoc app.js application.js src/ -r" diff --git a/test/hooks-post-catcher.js b/test/hooks-post-catcher.js deleted file mode 100644 index f738599..0000000 --- a/test/hooks-post-catcher.js +++ /dev/null @@ -1,109 +0,0 @@ -/* eslint no-console: "off" */ -import express from "express"; -import fetch from "node-fetch"; -import crypto from "crypto"; -import bodyParser from 'body-parser'; -import EventEmitter from 'events'; - -const encodeForUrl = (value) => { - return encodeURIComponent(value) - .replace(/%20/g, '+') - .replace(/[!'()]/g, escape) - .replace(/\*/g, "%2A") -}; - -class HooksPostCatcher extends EventEmitter { - static encodeForUrl = encodeForUrl; - - constructor (url, { useLogger = false } = {}) { - super(); - this.url = url; - this.started = false; - this._parsedUrl = new URL(url); - this.port = this._parsedUrl.port; - this.logger = useLogger ? this.logger : { log: () => {}, error: () => {} }; - if (!this.port) throw new Error("Port not specified in URL"); - } - - start () { - return new Promise((resolve, reject) => { - this.app = express(); - this.app.use(bodyParser.json()); - this.app.use(bodyParser.urlencoded({ - extended: true - })); - this.app.post("/callback", (req, res) => { - try { - this.logger.log("-------------------------------------"); - this.logger.log("* Received:", req.url); - this.logger.log("* Body:", req.body); - this.logger.log("-------------------------------------\n"); - res.statusCode = 200; - res.send(); - this.emit("callback", req.body); - } catch (error) { - this.logger.error("Error processing callback:", error); - res.statusCode = 500; - res.send(); - this.emit("error", error); - } - }); - - this.server = this.app.listen(this.port, (error) => { - if (error) { - this.logger.error("Error starting server:", error); - reject(error); - return; - } - - this.logger.log("Server listening on", this.url); - this.started = true; - resolve(); - }); - }); - } - - stop () { - this.server.close(); - this.started = false; - this.removeAllListeners(); - } - - async createHook (bbbDomain, sharedSecret, { - getRaw = false, - eventId = null, - meetingId = null, - } = {}) { - if (!this.started) this.start(); - const myUrl = this.url; - let params = `callbackURL=${HooksPostCatcher.encodeForUrl(myUrl)}&getRaw=${getRaw}`; - if (eventId) params += "&eventID=" + eventId; - if (meetingId) params += "&meetingID=" + meetingId; - const checksum = crypto - .createHash('sha1') - .update("hooks/create" + params + sharedSecret).digest('hex'); - const fullUrl = `http://${bbbDomain}/bigbluebutton/api/hooks/create?` + - params + "&checksum=" + checksum - this.logger.log("Registering a hook with", fullUrl); - - const controller = new AbortController(); - const abortTimeout = setTimeout(controller.abort, 2500); - - try { - const response = await fetch(fullUrl, { signal: controller.signal }); - const text = await response.text(); - if (response.ok) { - this.logger.debug("Hook registered - response from hook/create:", text); - } else { - throw new Error(text); - } - } catch (error) { - this.logger.error("Hook registration failed - response from hook/create:", error); - throw error; - } finally { - clearTimeout(abortTimeout); - } - } -} - -export default HooksPostCatcher; diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..1896b83 --- /dev/null +++ b/test/index.js @@ -0,0 +1,70 @@ +import { describe, before, after, beforeEach } from 'mocha'; +import redis from 'redis'; +import config from 'config'; +import Application from '../application.js'; +import WebhooksSuite, { MOD_CONFIG as WH_CONFIG } from './webhooks/index.js'; +import XAPISuite, { MOD_CONFIG as XAPI_CONFIG } from './xapi/index.js'; + +let MODULES = config.get('modules'); +MODULES = config.util.extendDeep(MODULES, XAPI_CONFIG); +MODULES = config.util.extendDeep(MODULES, WH_CONFIG); + +const IN_REDIS_CONFIG = MODULES['../in/redis/index.js'].config.redis; +const SHARED_SECRET = process.env.SHARED_SECRET + || config.has('bbb.sharedSecret') ? config.get('bbb.sharedSecret') : false + || function () { throw new Error('SHARED_SECRET not set'); }(); +const ALL_TESTS = process.env.ALL_TESTS ? process.env.ALL_TESTS === 'true' : true; +const TEST_CHANNEL = 'test-channel'; + +describe('bbb-webhooks test suite', () => { + const application = new Application(); + const redisClient = redis.createClient({ + host: config.get('redis.host'), + port: config.get('redis.port'), + password: config.has('redis.password') ? config.get('redis.password') : undefined, + }); + + before((done) => { + IN_REDIS_CONFIG.inboundChannels = [...IN_REDIS_CONFIG.inboundChannels, TEST_CHANNEL]; + application.start() + .then(redisClient.connect()) + .then(() => { done(); }) + .catch(done); + }); + + beforeEach((done) => { + redisClient.flushDb(); + done(); + }) + + after((done) => { + redisClient.flushDb(); + done(); + }); + + // Add tests for each module here + + describe('out/webhooks tests', () => { + const context = { + application, + redisClient, + sharedSecret: SHARED_SECRET, + testChannel: TEST_CHANNEL, + force: ALL_TESTS, + }; + + WebhooksSuite(context); + }); + + describe('out/xapi tests', () => { + const context = { + application, + redisClient, + sharedSecret: SHARED_SECRET, + testChannel: TEST_CHANNEL, + force: ALL_TESTS, + }; + + XAPISuite(context); + }); +}); diff --git a/test/utils/post-catcher.js b/test/utils/post-catcher.js new file mode 100644 index 0000000..5b72cf8 --- /dev/null +++ b/test/utils/post-catcher.js @@ -0,0 +1,75 @@ +/* eslint no-console: "off" */ +import express from "express"; +import bodyParser from 'body-parser'; +import EventEmitter from 'events'; + +const encodeForUrl = (value) => { + return encodeURIComponent(value) + .replace(/%20/g, '+') + .replace(/[!'()]/g, escape) + .replace(/\*/g, "%2A") +}; + +class PostCatcher extends EventEmitter { + static encodeForUrl = encodeForUrl; + + constructor (url, { + useLogger = false, + path = "/callback", + } = {}) { + super(); + this.url = url; + this.started = false; + this.path = path; + this._parsedUrl = new URL(url); + this.port = this._parsedUrl.port; + this.logger = useLogger ? console : { log: () => {}, error: () => {} }; + if (!this.port) throw new Error("Port not specified in URL"); + } + + start () { + return new Promise((resolve, reject) => { + this.app = express(); + this.app.use(bodyParser.json()); + this.app.use(bodyParser.urlencoded({ + extended: true + })); + this.app.post(this.path, (req, res) => { + try { + this.logger.log("-------------------------------------"); + this.logger.log("* Received:", req.url); + this.logger.log("* Body:", req.body); + this.logger.log("-------------------------------------\n"); + res.statusCode = 200; + res.send(JSON.stringify({ status: "OK" })); + this.emit("callback", req.body); + } catch (error) { + this.logger.error("Error processing callback:", error); + res.statusCode = 500; + res.send(); + this.emit("error", error); + } + }); + + this.server = this.app.listen(this.port, (error) => { + if (error) { + this.logger.error("Error starting server:", error); + reject(error); + return; + } + + this.logger.log("Server listening on", this.url); + this.started = true; + resolve(); + }); + }); + } + + stop () { + this.server.close(); + this.started = false; + this.removeAllListeners(); + } +} + +export default PostCatcher; diff --git a/test/helpers.js b/test/webhooks/helpers.js similarity index 100% rename from test/helpers.js rename to test/webhooks/helpers.js diff --git a/test/webhooks/hooks-post-catcher.js b/test/webhooks/hooks-post-catcher.js new file mode 100644 index 0000000..4b63096 --- /dev/null +++ b/test/webhooks/hooks-post-catcher.js @@ -0,0 +1,49 @@ +/* eslint no-console: "off" */ +import fetch from "node-fetch"; +import crypto from "crypto"; +import PostCatcher from '../utils/post-catcher.js'; + +class HooksPostCatcher extends PostCatcher { + constructor (url, options) { + super(url, options); + } + + async createHook (bbbDomain, sharedSecret, { + getRaw = false, + eventId = null, + meetingId = null, + } = {}) { + if (!this.started) this.start(); + let params = `callbackURL=${HooksPostCatcher.encodeForUrl(this.url)}&getRaw=${getRaw}`; + if (eventId) params += "&eventID=" + eventId; + if (meetingId) params += "&meetingID=" + meetingId; + const checksum = crypto + .createHash('sha1') + .update("hooks/create" + params + sharedSecret).digest('hex'); + const fullUrl = `http://${bbbDomain}/bigbluebutton/api/hooks/create?` + + params + + "&checksum=" + + checksum; + this.logger.log("Registering a hook with", fullUrl); + + const controller = new AbortController(); + const abortTimeout = setTimeout(controller.abort, 2500); + + try { + const response = await fetch(fullUrl, { signal: controller.signal }); + const text = await response.text(); + if (response.ok) { + this.logger.debug("Hook registered - response from hook/create:", text); + } else { + throw new Error(text); + } + } catch (error) { + this.logger.error("Hook registration failed - response from hook/create:", error); + throw error; + } finally { + clearTimeout(abortTimeout); + } + } +} + +export default HooksPostCatcher; diff --git a/test/test.js b/test/webhooks/index.js similarity index 79% rename from test/test.js rename to test/webhooks/index.js index c36c0fd..3526002 100644 --- a/test/test.js +++ b/test/webhooks/index.js @@ -1,46 +1,31 @@ import { describe, it, before, after, beforeEach } from 'mocha'; import request from 'supertest'; -import redis from 'redis'; import config from 'config'; -import Utils from '../src/out/webhooks/utils.js'; -import Hook from '../src/db/redis/hooks.js'; +import Utils from '../../src/out/webhooks/utils.js'; +import Hook from '../../src/db/redis/hooks.js'; import Helpers from './helpers.js' -import Application from '../application.js'; import HooksPostCatcher from './hooks-post-catcher.js'; -const TEST_CHANNEL = 'test-channel'; -const SHARED_SECRET = process.env.SHARED_SECRET - || config.has('bbb.sharedSecret') ? config.get('bbb.sharedSecret') : false - || function () { throw new Error('SHARED_SECRET not set'); }(); const MODULES = config.get('modules'); const WH_CONFIG = MODULES['../out/webhooks/index.js'].config; -const IN_REDIS_CONFIG = MODULES['../in/redis/index.js'].config.redis; const CHECKSUM_ALGORITHM = 'sha1'; +const WEBHOOKS_SUITE = process.env.WEBHOOKS_SUITE ? process.env.WEBHOOKS_SUITE === 'true' : false; -describe('bbb-webhooks tests', () => { - const application = new Application(); - const redisClient = redis.createClient({ - host: config.get('redis.host'), - port: config.get('redis.port'), - password: config.has('redis.password') ? config.get('redis.password') : undefined, - }); +export default function suite({ + redisClient, + sharedSecret, + testChannel, + force, +}) { + if (!WEBHOOKS_SUITE && !force) return; before((done) => { - WH_CONFIG.queueSize = 10; - WH_CONFIG.permanentURLs = [ - { url: Helpers.rawCatcherURL, getRaw: true }, - { url: Helpers.mappedCatcherURL, getRaw: false }, - ]; - IN_REDIS_CONFIG.inboundChannels = [...IN_REDIS_CONFIG.inboundChannels, TEST_CHANNEL]; - application.start() - .then(redisClient.connect()) - .then(() => { done(); }) - .catch(done); + done(); }); beforeEach((done) => { const hooks = Hook.get().getAllGlobalHooks(); - Helpers.flushall(redisClient); + hooks.forEach((hook) => { Helpers.flushredis(hook); }); @@ -50,10 +35,11 @@ describe('bbb-webhooks tests', () => { after((done) => { const hooks = Hook.get().getAllGlobalHooks(); - Helpers.flushall(redisClient); + hooks.forEach((hook) => { Helpers.flushredis(hook); }); + done(); }); @@ -61,7 +47,7 @@ describe('bbb-webhooks tests', () => { it('should list permanent hook', (done) => { let getUrl = Utils.checksumAPI( Helpers.url + Helpers.listUrl, - SHARED_SECRET, CHECKSUM_ALGORITHM + sharedSecret, CHECKSUM_ALGORITHM ); getUrl = Helpers.listUrl + '?checksum=' + getUrl @@ -93,7 +79,7 @@ describe('bbb-webhooks tests', () => { const hook = hooks[hooks.length-1].id; let getUrl = Utils.checksumAPI( Helpers.url + Helpers.destroyUrl(hook), - SHARED_SECRET, + sharedSecret, CHECKSUM_ALGORITHM, ); getUrl = Helpers.destroyUrl(hook) + '&checksum=' + getUrl @@ -112,7 +98,7 @@ describe('bbb-webhooks tests', () => { it('should not destroy the permanent hook', (done) => { let getUrl = Utils.checksumAPI( Helpers.url + Helpers.destroyPermanent, - SHARED_SECRET, + sharedSecret, CHECKSUM_ALGORITHM, ); getUrl = Helpers.destroyPermanent + '&checksum=' + getUrl request(Helpers.url) @@ -140,7 +126,7 @@ describe('bbb-webhooks tests', () => { it('should create a hook with getRaw=true', (done) => { let getUrl = Utils.checksumAPI( Helpers.url + Helpers.createUrl + Helpers.createRaw, - SHARED_SECRET, + sharedSecret, CHECKSUM_ALGORITHM, ); getUrl = Helpers.createUrl + '&checksum=' + getUrl + Helpers.createRaw @@ -194,7 +180,7 @@ describe('bbb-webhooks tests', () => { } }); - redisClient.publish(TEST_CHANNEL, JSON.stringify(Helpers.rawMessage)); + redisClient.publish(testChannel, JSON.stringify(Helpers.rawMessage)); }) }); @@ -234,7 +220,21 @@ describe('bbb-webhooks tests', () => { } }); - redisClient.publish(TEST_CHANNEL, JSON.stringify(Helpers.rawMessage)); + redisClient.publish(testChannel, JSON.stringify(Helpers.rawMessage)); }) }); -}); +} + +export const MOD_CONFIG = { + '../out/webhooks/index.js': { + enabled: WEBHOOKS_SUITE, + config: { + queueSize: 10, + permanentURLs: [ + { url: Helpers.rawCatcherURL, getRaw: true }, + { url: Helpers.mappedCatcherURL, getRaw: false }, + ], + }, + }, +}; + diff --git a/test/xapi/events.js b/test/xapi/events.js new file mode 100644 index 0000000..fc18583 --- /dev/null +++ b/test/xapi/events.js @@ -0,0 +1,38 @@ +import { open } from 'node:fs/promises'; +import { fileURLToPath } from 'url'; +const MAPPED_EVENTS_PATH = fileURLToPath( + new URL('../../example/events/mapped-events.json', import.meta.url) +); + +const mapSamplesToEvents = async () => { + const eventList = []; + const mHandle = await open(MAPPED_EVENTS_PATH, 'r'); + + for await (const line of mHandle.readLines()) { + eventList.push(JSON.parse(line)); + } + + await mHandle.close(); + + return eventList; +} + +const validators = { + 'http://adlnet.gov/expapi/verbs/initialized': (statement) => { + return statement.verb.id === 'http://adlnet.gov/expapi/verbs/initialized'; + } +} + +const validate = (statement) => { + const validator = validators[statement.verb.id]; + + if (!validator) throw new Error(`No validator for ${statement.verb.id}`); + + return validator(statement); +} + +export { + mapSamplesToEvents, + validators, + validate, +}; diff --git a/test/xapi/index.js b/test/xapi/index.js new file mode 100644 index 0000000..62f3f83 --- /dev/null +++ b/test/xapi/index.js @@ -0,0 +1,93 @@ +import { describe, it, before, after } from 'mocha'; +import { mapSamplesToEvents, validate } from './events.js'; +import PostCatcher from '../utils/post-catcher.js'; + +const XAPI_SUITE = process.env.XAPI_SUITE ? process.env.XAPI_SUITE === 'true' : false; +const MOCK_LRS_URL = 'http://127.0.0.1:9009'; + +const generateTestCase = (event, redisClient, channel) => { + const eventId = event.data.id; + + return () => { + let lrsCatcher = new PostCatcher(MOCK_LRS_URL, { + useLogger: false, + path: '/xAPI/statements', + }); + + before((done) => { + lrsCatcher.start().then(() => { + done(); + }).catch((err) => { + done(err); + }); + }); + + after((done) => { + lrsCatcher.stop(); + done(); + }); + + it(`should validate ${eventId}`, (done) => { + lrsCatcher.once('callback', (statement) => { + try { + // Uncomment to debug + //console.debug("Statement received", statement); + const valid = validate(statement); + if (!valid) { + done(new Error(`Event ${eventId} is not valid`)); + } else { + done(); + } + } catch (error) { + done(error); + } + }); + + redisClient.publish(channel, JSON.stringify(event)); + }); + }; +}; + +export default function suite({ + redisClient, + testChannel, + force, +}) { + if (!XAPI_SUITE && !force) return; + let events = []; + + describe('xapi tests', () => { + before((done) => { + mapSamplesToEvents().then((mappedEvents) => { + events = mappedEvents; + done(); + }).catch((err) => { + done(err); + }); + }); + + after((done) => { + done(); + }); + + it('should generate tests', () => { + events.forEach((event) => { + describe(`xapi: ${event.data.id}`, generateTestCase(event, redisClient, testChannel)); + }); + }); + }); +} + +export const MOD_CONFIG = { + '../out/xapi/index.js': { + enabled: XAPI_SUITE, + config: { + lrs: { + lrs_endpoint: MOCK_LRS_URL, + lrs_username: 'admin', + lrs_password: 'admin', + }, + uuid_namespace: '22946e5b-1860-4436-a025-cb133ca4c1d3', + } + } +}; From 437d363376f08a8f99577b19a4971aaaf2d85cda Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Wed, 1 Nov 2023 15:14:49 -0300 Subject: [PATCH 114/154] test: treat webhooks module config as optional --- test/webhooks/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/webhooks/index.js b/test/webhooks/index.js index 3526002..9362a73 100644 --- a/test/webhooks/index.js +++ b/test/webhooks/index.js @@ -7,7 +7,7 @@ import Helpers from './helpers.js' import HooksPostCatcher from './hooks-post-catcher.js'; const MODULES = config.get('modules'); -const WH_CONFIG = MODULES['../out/webhooks/index.js'].config; +const WH_CONFIG = MODULES['../out/webhooks/index.js']?.config; const CHECKSUM_ALGORITHM = 'sha1'; const WEBHOOKS_SUITE = process.env.WEBHOOKS_SUITE ? process.env.WEBHOOKS_SUITE === 'true' : false; From 0cb401a6bf8019f32d929c10f513d1d2960e5535 Mon Sep 17 00:00:00 2001 From: mp Date: Mon, 6 Nov 2023 11:37:07 -0300 Subject: [PATCH 115/154] (xapi) - added verb validation on the test suit for every valid event/statement --- test/xapi/events.js | 81 ++++++++++++++++++++++++++++++++++++++++++--- test/xapi/index.js | 2 +- 2 files changed, 77 insertions(+), 6 deletions(-) diff --git a/test/xapi/events.js b/test/xapi/events.js index fc18583..f412af0 100644 --- a/test/xapi/events.js +++ b/test/xapi/events.js @@ -4,12 +4,34 @@ const MAPPED_EVENTS_PATH = fileURLToPath( new URL('../../example/events/mapped-events.json', import.meta.url) ); +const validEvents = [ + 'chat-group-message-sent', + 'meeting-created', + 'meeting-ended', + 'meeting-screenshare-started', + 'meeting-screenshare-stopped', + 'poll-started', + 'poll-responded', + 'user-audio-muted', + 'user-audio-unmuted', + 'user-audio-voice-disabled', + 'user-audio-voice-enabled', + 'user-joined', + 'user-left', + 'user-cam-broadcast-end', + 'user-cam-broadcast-start', + 'user-raise-hand-changed' +] + const mapSamplesToEvents = async () => { const eventList = []; const mHandle = await open(MAPPED_EVENTS_PATH, 'r'); for await (const line of mHandle.readLines()) { - eventList.push(JSON.parse(line)); + const event = JSON.parse(line) + if (validEvents.includes(event.data.id)){ + eventList.push(event); + } } await mHandle.close(); @@ -18,17 +40,66 @@ const mapSamplesToEvents = async () => { } const validators = { - 'http://adlnet.gov/expapi/verbs/initialized': (statement) => { + 'meeting-created': (event, statement) => { return statement.verb.id === 'http://adlnet.gov/expapi/verbs/initialized'; + }, + 'meeting-ended': (event, statement) => { + return statement.verb.id === 'http://adlnet.gov/expapi/verbs/terminated'; + }, + 'user-joined': (event, statement) => { + return statement.verb.id === 'http://activitystrea.ms/join'; + }, + 'user-left': (event, statement) => { + return statement.verb.id === 'http://activitystrea.ms/leave'; + }, + 'user-audio-voice-enabled': (event, statement) => { + return statement.verb.id === 'http://activitystrea.ms/start'; + }, + 'user-audio-voice-disabled': (event, statement) => { + return statement.verb.id === 'https://w3id.org/xapi/virtual-classroom/verbs/stopped'; + }, + 'user-audio-muted': (event, statement) => { + return statement.verb.id === 'https://w3id.org/xapi/virtual-classroom/verbs/stopped'; + }, + 'user-audio-unmuted': (event, statement) => { + return statement.verb.id === 'http://activitystrea.ms/start'; + }, + 'user-cam-broadcast-start': (event, statement) => { + return statement.verb.id === 'http://activitystrea.ms/start'; + }, + 'user-cam-broadcast-end': (event, statement) => { + return statement.verb.id === 'https://w3id.org/xapi/virtual-classroom/verbs/stopped'; + }, + 'meeting-screenshare-started': (event, statement) => { + return statement.verb.id === 'http://activitystrea.ms/share'; + }, + 'meeting-screenshare-stopped': (event, statement) => { + return statement.verb.id === 'http://activitystrea.ms/unshare'; + }, + 'chat-group-message-sent': (event, statement) => { + return statement.verb.id === 'https://w3id.org/xapi/acrossx/verbs/posted'; + }, + 'poll-started': (event, statement) => { + return statement.verb.id === 'http://adlnet.gov/expapi/verbs/asked'; + }, + 'poll-responded': (event, statement) => { + return statement.verb.id === 'http://adlnet.gov/expapi/verbs/answered'; + }, + 'user-raise-hand-changed': (event, statement) => { + const raisedHandVerb = "https://w3id.org/xapi/virtual-classroom/verbs/reacted"; + const loweredHandVerb = "https://w3id.org/xapi/virtual-classroom/verbs/unreacted"; + const isRaiseHand = event.data.attributes.user["raise-hand"]; + return statement.verb.id === isRaiseHand ? raisedHandVerb : loweredHandVerb; } } -const validate = (statement) => { - const validator = validators[statement.verb.id]; +const validate = (event, statement) => { + const eventId = event.data.id; + const validator = validators[eventId]; if (!validator) throw new Error(`No validator for ${statement.verb.id}`); - return validator(statement); + return validator(event, statement); } export { diff --git a/test/xapi/index.js b/test/xapi/index.js index 62f3f83..61b80fa 100644 --- a/test/xapi/index.js +++ b/test/xapi/index.js @@ -32,7 +32,7 @@ const generateTestCase = (event, redisClient, channel) => { try { // Uncomment to debug //console.debug("Statement received", statement); - const valid = validate(statement); + const valid = validate(event, statement); if (!valid) { done(new Error(`Event ${eventId} is not valid`)); } else { From b9b22d96407266c1bd95d36f33ff23f5cec19435 Mon Sep 17 00:00:00 2001 From: mp Date: Mon, 6 Nov 2023 12:09:15 -0300 Subject: [PATCH 116/154] (xapi) - Added Statement information on test error --- test/xapi/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/xapi/index.js b/test/xapi/index.js index 61b80fa..d341f8b 100644 --- a/test/xapi/index.js +++ b/test/xapi/index.js @@ -34,7 +34,7 @@ const generateTestCase = (event, redisClient, channel) => { //console.debug("Statement received", statement); const valid = validate(event, statement); if (!valid) { - done(new Error(`Event ${eventId} is not valid`)); + done(new Error(`Event ${eventId} is not valid.\nStatement: ${JSON.stringify(statement)}`)); } else { done(); } From 188b5659a3a3468e86a632d54e60ecdb6503c4d3 Mon Sep 17 00:00:00 2001 From: mp Date: Mon, 6 Nov 2023 20:20:41 -0300 Subject: [PATCH 117/154] (xapi) - Added extra tests for xAPI events --- test/xapi/events.js | 90 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 74 insertions(+), 16 deletions(-) diff --git a/test/xapi/events.js b/test/xapi/events.js index f412af0..88f5e47 100644 --- a/test/xapi/events.js +++ b/test/xapi/events.js @@ -1,5 +1,7 @@ import { open } from 'node:fs/promises'; import { fileURLToPath } from 'url'; +import { validate as validateUUID } from "uuid"; +import { DateTime } from "luxon"; const MAPPED_EVENTS_PATH = fileURLToPath( new URL('../../example/events/mapped-events.json', import.meta.url) ); @@ -39,57 +41,113 @@ const mapSamplesToEvents = async () => { return eventList; } +const isValidISODate = (dateString) => DateTime.fromISO(dateString, { zone: "utc", setZone: true }).isValid; + +const validateCommonProperties = statement => + statement.context.contextActivities.category[0].id == "https://w3id.org/xapi/virtual-classroom" +&& statement.context.contextActivities.category[0].definition.type == "http://adlnet.gov/expapi/activities/profile" +&& validateUUID(statement.context.registration) +&& validateUUID(statement.context.extensions['https://w3id.org/xapi/cmi5/context/extensions/sessionid']) +&& Object.prototype.hasOwnProperty.call(statement, 'actor') +&& isValidISODate(statement.timestamp); + const validators = { 'meeting-created': (event, statement) => { - return statement.verb.id === 'http://adlnet.gov/expapi/verbs/initialized'; + return statement.verb.id === 'http://adlnet.gov/expapi/verbs/initialized' + && statement.object.definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom' + && validateCommonProperties(statement); }, 'meeting-ended': (event, statement) => { - return statement.verb.id === 'http://adlnet.gov/expapi/verbs/terminated'; + return statement.verb.id === 'http://adlnet.gov/expapi/verbs/terminated' + && statement.object.definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom' + && validateCommonProperties(statement); }, 'user-joined': (event, statement) => { - return statement.verb.id === 'http://activitystrea.ms/join'; + return statement.verb.id === 'http://activitystrea.ms/join' + && statement.object.definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom' + && validateCommonProperties(statement); }, 'user-left': (event, statement) => { - return statement.verb.id === 'http://activitystrea.ms/leave'; + return statement.verb.id === 'http://activitystrea.ms/leave' + && statement.object.definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom' + && validateCommonProperties(statement); }, 'user-audio-voice-enabled': (event, statement) => { - return statement.verb.id === 'http://activitystrea.ms/start'; + return statement.verb.id === 'http://activitystrea.ms/start' + && statement.object.definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/micro' + && validateCommonProperties(statement) + && statement.context.contextActivities.parent[0].definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom'; }, 'user-audio-voice-disabled': (event, statement) => { - return statement.verb.id === 'https://w3id.org/xapi/virtual-classroom/verbs/stopped'; + return statement.verb.id === 'https://w3id.org/xapi/virtual-classroom/verbs/stopped' + && statement.object.definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/micro' + && validateCommonProperties(statement) + && statement.context.contextActivities.parent[0].definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom'; }, 'user-audio-muted': (event, statement) => { - return statement.verb.id === 'https://w3id.org/xapi/virtual-classroom/verbs/stopped'; + return statement.verb.id === 'https://w3id.org/xapi/virtual-classroom/verbs/stopped' + && statement.object.definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/micro' + && validateCommonProperties(statement) + && statement.context.contextActivities.parent[0].definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom'; }, 'user-audio-unmuted': (event, statement) => { - return statement.verb.id === 'http://activitystrea.ms/start'; + return statement.verb.id === 'http://activitystrea.ms/start' + && statement.object.definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/micro' + && validateCommonProperties(statement) + && statement.context.contextActivities.parent[0].definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom'; }, 'user-cam-broadcast-start': (event, statement) => { - return statement.verb.id === 'http://activitystrea.ms/start'; + return statement.verb.id === 'http://activitystrea.ms/start' + && statement.object.definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/camera' + && validateCommonProperties(statement) + && statement.context.contextActivities.parent[0].definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom'; }, 'user-cam-broadcast-end': (event, statement) => { - return statement.verb.id === 'https://w3id.org/xapi/virtual-classroom/verbs/stopped'; + return statement.verb.id === 'https://w3id.org/xapi/virtual-classroom/verbs/stopped' + && statement.object.definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/camera' + && validateCommonProperties(statement) + && statement.context.contextActivities.parent[0].definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom'; }, 'meeting-screenshare-started': (event, statement) => { - return statement.verb.id === 'http://activitystrea.ms/share'; + return statement.verb.id === 'http://activitystrea.ms/share' + && statement.object.definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/screen' + && validateCommonProperties(statement) + && statement.context.contextActivities.parent[0].definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom'; }, 'meeting-screenshare-stopped': (event, statement) => { - return statement.verb.id === 'http://activitystrea.ms/unshare'; + return statement.verb.id === 'http://activitystrea.ms/unshare' + && statement.object.definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/screen' + && validateCommonProperties(statement) + && statement.context.contextActivities.parent[0].definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom'; }, 'chat-group-message-sent': (event, statement) => { - return statement.verb.id === 'https://w3id.org/xapi/acrossx/verbs/posted'; + return statement.verb.id === 'https://w3id.org/xapi/acrossx/verbs/posted' + && statement.object.definition.type === 'https://w3id.org/xapi/acrossx/activities/message' + && validateCommonProperties(statement) + && statement.context.contextActivities.parent[0].definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom'; }, 'poll-started': (event, statement) => { - return statement.verb.id === 'http://adlnet.gov/expapi/verbs/asked'; + return statement.verb.id === 'http://adlnet.gov/expapi/verbs/asked' + && statement.object.definition.type === 'http://adlnet.gov/expapi/activities/cmi.interaction' + && statement.object.definition.interactionType === 'choice' + && validateCommonProperties(statement) + && statement.context.contextActivities.parent[0].definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom'; }, 'poll-responded': (event, statement) => { - return statement.verb.id === 'http://adlnet.gov/expapi/verbs/answered'; + return statement.verb.id === 'http://adlnet.gov/expapi/verbs/answered' + && statement.object.definition.type === 'http://adlnet.gov/expapi/activities/cmi.interaction' + && statement.object.definition.interactionType === 'choice' + && validateCommonProperties(statement) + && statement.context.contextActivities.parent[0].definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom'; }, 'user-raise-hand-changed': (event, statement) => { const raisedHandVerb = "https://w3id.org/xapi/virtual-classroom/verbs/reacted"; const loweredHandVerb = "https://w3id.org/xapi/virtual-classroom/verbs/unreacted"; const isRaiseHand = event.data.attributes.user["raise-hand"]; - return statement.verb.id === isRaiseHand ? raisedHandVerb : loweredHandVerb; + return statement.verb.id === isRaiseHand ? raisedHandVerb : loweredHandVerb + && statement.object.definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom' + && statement.result.extensions["https://w3id.org/xapi/virtual-classroom/extensions/emoji"] === 'U+1F590' + && validateCommonProperties(statement); } } From 9cc21ff153170d24b612fabb378021fd61519679 Mon Sep 17 00:00:00 2001 From: mp Date: Mon, 6 Nov 2023 20:32:54 -0300 Subject: [PATCH 118/154] (xapi) - Set chat message statements timestamp to ISO format --- src/out/xapi/templates.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/out/xapi/templates.js b/src/out/xapi/templates.js index 2762dad..ee96579 100644 --- a/src/out/xapi/templates.js +++ b/src/out/xapi/templates.js @@ -174,7 +174,7 @@ export default function getXAPIStatement(event, meeting_data, user_data = null, } statement.context.contextActivities.parent = session_parent; - statement.timestamp = user_data?.time; + if (user_data?.time !== undefined) statement.timestamp = DateTime.fromMillis(user_data.time).toUTC().toISO(); } // Custom 'poll-started' and 'poll-responded' attributes From f7b5f2265e7ffb248164c4e9b5f6aad24f368a26 Mon Sep 17 00:00:00 2001 From: mp Date: Wed, 8 Nov 2023 19:13:58 -0300 Subject: [PATCH 119/154] (xapi) - implemented all the tests for the statements --- test/xapi/events.js | 114 +++++++++++++++++++++++++------------------- 1 file changed, 66 insertions(+), 48 deletions(-) diff --git a/test/xapi/events.js b/test/xapi/events.js index 88f5e47..fbb99c8 100644 --- a/test/xapi/events.js +++ b/test/xapi/events.js @@ -1,7 +1,7 @@ import { open } from 'node:fs/promises'; import { fileURLToPath } from 'url'; import { validate as validateUUID } from "uuid"; -import { DateTime } from "luxon"; +import { DateTime, Duration } from "luxon"; const MAPPED_EVENTS_PATH = fileURLToPath( new URL('../../example/events/mapped-events.json', import.meta.url) ); @@ -42,112 +42,130 @@ const mapSamplesToEvents = async () => { } const isValidISODate = (dateString) => DateTime.fromISO(dateString, { zone: "utc", setZone: true }).isValid; +const isValidISODuration = (durationString) => Duration.fromISO(durationString).isValid; + +const validateVerb = (statement, verbId) => statement.verb.id === verbId; + +const validateDefinitionType = (statement, definitionType) => statement.object.definition.type === definitionType; const validateCommonProperties = statement => statement.context.contextActivities.category[0].id == "https://w3id.org/xapi/virtual-classroom" && statement.context.contextActivities.category[0].definition.type == "http://adlnet.gov/expapi/activities/profile" +&& validateUUID(statement.object.id.substring(statement.object.id.length - 36)) && validateUUID(statement.context.registration) && validateUUID(statement.context.extensions['https://w3id.org/xapi/cmi5/context/extensions/sessionid']) && Object.prototype.hasOwnProperty.call(statement, 'actor') && isValidISODate(statement.timestamp); +const validateVirtualClassroomParent = statement => + statement.context.contextActivities.parent[0].definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom' +&& validateUUID(statement.context.contextActivities.parent[0].id.substring(statement.context.contextActivities.parent[0].id.length - 36)) + +const validatePoll = statement => + statement.object.definition.interactionType === 'choice' + && Array.isArray(statement.object.definition.choices) + const validators = { 'meeting-created': (event, statement) => { - return statement.verb.id === 'http://adlnet.gov/expapi/verbs/initialized' - && statement.object.definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom' + return validateVerb(statement, 'http://adlnet.gov/expapi/verbs/initialized') + && validateDefinitionType(statement, 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom') + && isValidISODuration(statement.context.extensions['http://id.tincanapi.com/extension/planned-duration']) && validateCommonProperties(statement); }, 'meeting-ended': (event, statement) => { - return statement.verb.id === 'http://adlnet.gov/expapi/verbs/terminated' - && statement.object.definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom' + return validateVerb(statement, 'http://adlnet.gov/expapi/verbs/terminated') + && validateDefinitionType(statement, 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom') + && isValidISODuration(statement.result.duration) + && isValidISODuration(statement.context.extensions['http://id.tincanapi.com/extension/planned-duration']) && validateCommonProperties(statement); }, 'user-joined': (event, statement) => { - return statement.verb.id === 'http://activitystrea.ms/join' - && statement.object.definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom' + return validateVerb(statement, 'http://activitystrea.ms/join') + && validateDefinitionType(statement, 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom') && validateCommonProperties(statement); }, 'user-left': (event, statement) => { - return statement.verb.id === 'http://activitystrea.ms/leave' - && statement.object.definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom' + return validateVerb(statement, 'http://activitystrea.ms/leave') + && validateDefinitionType(statement, 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom') && validateCommonProperties(statement); }, 'user-audio-voice-enabled': (event, statement) => { - return statement.verb.id === 'http://activitystrea.ms/start' - && statement.object.definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/micro' + return validateVerb(statement, 'http://activitystrea.ms/start') + && validateDefinitionType(statement, 'https://w3id.org/xapi/virtual-classroom/activity-types/micro') && validateCommonProperties(statement) - && statement.context.contextActivities.parent[0].definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom'; + && validateVirtualClassroomParent(statement); }, 'user-audio-voice-disabled': (event, statement) => { - return statement.verb.id === 'https://w3id.org/xapi/virtual-classroom/verbs/stopped' - && statement.object.definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/micro' + return validateVerb(statement, 'https://w3id.org/xapi/virtual-classroom/verbs/stopped') + && validateDefinitionType(statement, 'https://w3id.org/xapi/virtual-classroom/activity-types/micro') && validateCommonProperties(statement) - && statement.context.contextActivities.parent[0].definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom'; + && validateVirtualClassroomParent(statement); }, 'user-audio-muted': (event, statement) => { - return statement.verb.id === 'https://w3id.org/xapi/virtual-classroom/verbs/stopped' - && statement.object.definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/micro' + return validateVerb(statement, 'https://w3id.org/xapi/virtual-classroom/verbs/stopped') + && validateDefinitionType(statement, 'https://w3id.org/xapi/virtual-classroom/activity-types/micro') && validateCommonProperties(statement) - && statement.context.contextActivities.parent[0].definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom'; + && validateVirtualClassroomParent(statement); }, 'user-audio-unmuted': (event, statement) => { - return statement.verb.id === 'http://activitystrea.ms/start' - && statement.object.definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/micro' + return validateVerb(statement, 'http://activitystrea.ms/start') + && validateDefinitionType(statement, 'https://w3id.org/xapi/virtual-classroom/activity-types/micro') && validateCommonProperties(statement) - && statement.context.contextActivities.parent[0].definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom'; + && validateVirtualClassroomParent(statement); }, 'user-cam-broadcast-start': (event, statement) => { - return statement.verb.id === 'http://activitystrea.ms/start' - && statement.object.definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/camera' + return validateVerb(statement, 'http://activitystrea.ms/start') + && validateDefinitionType(statement, 'https://w3id.org/xapi/virtual-classroom/activity-types/camera') && validateCommonProperties(statement) - && statement.context.contextActivities.parent[0].definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom'; + && validateVirtualClassroomParent(statement); }, 'user-cam-broadcast-end': (event, statement) => { - return statement.verb.id === 'https://w3id.org/xapi/virtual-classroom/verbs/stopped' - && statement.object.definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/camera' + return validateVerb(statement, 'https://w3id.org/xapi/virtual-classroom/verbs/stopped') + && validateDefinitionType(statement, 'https://w3id.org/xapi/virtual-classroom/activity-types/camera') && validateCommonProperties(statement) - && statement.context.contextActivities.parent[0].definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom'; + && validateVirtualClassroomParent(statement); }, 'meeting-screenshare-started': (event, statement) => { - return statement.verb.id === 'http://activitystrea.ms/share' - && statement.object.definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/screen' + return validateVerb(statement, 'http://activitystrea.ms/share') + && validateDefinitionType(statement, 'https://w3id.org/xapi/virtual-classroom/activity-types/screen') && validateCommonProperties(statement) - && statement.context.contextActivities.parent[0].definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom'; + && validateVirtualClassroomParent(statement); }, 'meeting-screenshare-stopped': (event, statement) => { - return statement.verb.id === 'http://activitystrea.ms/unshare' - && statement.object.definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/screen' + return validateVerb(statement, 'http://activitystrea.ms/unshare') + && validateDefinitionType(statement, 'https://w3id.org/xapi/virtual-classroom/activity-types/screen') && validateCommonProperties(statement) - && statement.context.contextActivities.parent[0].definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom'; + && validateVirtualClassroomParent(statement); }, 'chat-group-message-sent': (event, statement) => { - return statement.verb.id === 'https://w3id.org/xapi/acrossx/verbs/posted' - && statement.object.definition.type === 'https://w3id.org/xapi/acrossx/activities/message' + return validateVerb(statement, 'https://w3id.org/xapi/acrossx/verbs/posted') + && validateDefinitionType(statement, 'https://w3id.org/xapi/acrossx/activities/message') && validateCommonProperties(statement) - && statement.context.contextActivities.parent[0].definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom'; + && validateVirtualClassroomParent(statement); }, 'poll-started': (event, statement) => { - return statement.verb.id === 'http://adlnet.gov/expapi/verbs/asked' - && statement.object.definition.type === 'http://adlnet.gov/expapi/activities/cmi.interaction' - && statement.object.definition.interactionType === 'choice' + return validateVerb(statement, 'http://adlnet.gov/expapi/verbs/asked') + && validateDefinitionType(statement, 'http://adlnet.gov/expapi/activities/cmi.interaction') + && validatePoll(statement) && validateCommonProperties(statement) - && statement.context.contextActivities.parent[0].definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom'; + && validateVirtualClassroomParent(statement); }, 'poll-responded': (event, statement) => { - return statement.verb.id === 'http://adlnet.gov/expapi/verbs/answered' - && statement.object.definition.type === 'http://adlnet.gov/expapi/activities/cmi.interaction' - && statement.object.definition.interactionType === 'choice' + return validateVerb(statement, 'http://adlnet.gov/expapi/verbs/answered') + && validateDefinitionType(statement, 'http://adlnet.gov/expapi/activities/cmi.interaction') + && validatePoll(statement) && validateCommonProperties(statement) - && statement.context.contextActivities.parent[0].definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom'; + && validateVirtualClassroomParent(statement) + && Object.prototype.hasOwnProperty.call(statement.result, 'response'); }, 'user-raise-hand-changed': (event, statement) => { const raisedHandVerb = "https://w3id.org/xapi/virtual-classroom/verbs/reacted"; const loweredHandVerb = "https://w3id.org/xapi/virtual-classroom/verbs/unreacted"; const isRaiseHand = event.data.attributes.user["raise-hand"]; - return statement.verb.id === isRaiseHand ? raisedHandVerb : loweredHandVerb - && statement.object.definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom' - && statement.result.extensions["https://w3id.org/xapi/virtual-classroom/extensions/emoji"] === 'U+1F590' - && validateCommonProperties(statement); + return validateVerb(statement, isRaiseHand ? raisedHandVerb : loweredHandVerb) + && validateDefinitionType(statement, 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom') + && validateCommonProperties(statement) + && statement.result.extensions["https://w3id.org/xapi/virtual-classroom/extensions/emoji"] === 'U+1F590'; } } From e519eb792b5ca6c06f2a34545f69293ccf8996e6 Mon Sep 17 00:00:00 2001 From: mp Date: Wed, 8 Nov 2023 19:18:56 -0300 Subject: [PATCH 120/154] (xapi) - fixed error message for the absence of a validator --- test/xapi/events.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/xapi/events.js b/test/xapi/events.js index fbb99c8..29c52aa 100644 --- a/test/xapi/events.js +++ b/test/xapi/events.js @@ -66,12 +66,12 @@ const validatePoll = statement => && Array.isArray(statement.object.definition.choices) const validators = { - 'meeting-created': (event, statement) => { - return validateVerb(statement, 'http://adlnet.gov/expapi/verbs/initialized') - && validateDefinitionType(statement, 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom') - && isValidISODuration(statement.context.extensions['http://id.tincanapi.com/extension/planned-duration']) - && validateCommonProperties(statement); - }, + // 'meeting-created': (event, statement) => { + // return validateVerb(statement, 'http://adlnet.gov/expapi/verbs/initialized') + // && validateDefinitionType(statement, 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom') + // && isValidISODuration(statement.context.extensions['http://id.tincanapi.com/extension/planned-duration']) + // && validateCommonProperties(statement); + // }, 'meeting-ended': (event, statement) => { return validateVerb(statement, 'http://adlnet.gov/expapi/verbs/terminated') && validateDefinitionType(statement, 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom') @@ -173,7 +173,7 @@ const validate = (event, statement) => { const eventId = event.data.id; const validator = validators[eventId]; - if (!validator) throw new Error(`No validator for ${statement.verb.id}`); + if (!validator) throw new Error(`No validator for eventId "${eventId}"`); return validator(event, statement); } From 51de17ebbaa5987ce2a1506f3195863138e7341f Mon Sep 17 00:00:00 2001 From: mp Date: Thu, 9 Nov 2023 09:55:49 -0300 Subject: [PATCH 121/154] (xapi) - uncommented validator commented by mistake --- test/xapi/events.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/xapi/events.js b/test/xapi/events.js index 29c52aa..94d67bb 100644 --- a/test/xapi/events.js +++ b/test/xapi/events.js @@ -66,12 +66,12 @@ const validatePoll = statement => && Array.isArray(statement.object.definition.choices) const validators = { - // 'meeting-created': (event, statement) => { - // return validateVerb(statement, 'http://adlnet.gov/expapi/verbs/initialized') - // && validateDefinitionType(statement, 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom') - // && isValidISODuration(statement.context.extensions['http://id.tincanapi.com/extension/planned-duration']) - // && validateCommonProperties(statement); - // }, + 'meeting-created': (event, statement) => { + return validateVerb(statement, 'http://adlnet.gov/expapi/verbs/initialized') + && validateDefinitionType(statement, 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom') + && isValidISODuration(statement.context.extensions['http://id.tincanapi.com/extension/planned-duration']) + && validateCommonProperties(statement); + }, 'meeting-ended': (event, statement) => { return validateVerb(statement, 'http://adlnet.gov/expapi/verbs/terminated') && validateDefinitionType(statement, 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom') From 220059fd69daa7671f5dcc1bc15e28a8c483f5d0 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Thu, 9 Nov 2023 15:16:54 -0300 Subject: [PATCH 122/154] fix: 2.6 compat for user-raise-hand-changed/emoji-changed event --- src/process/event-processor.js | 68 +++++++++++++++++++++++++++++++++- src/process/event.js | 26 ++----------- 2 files changed, 71 insertions(+), 23 deletions(-) diff --git a/src/process/event-processor.js b/src/process/event-processor.js index f9d47b7..e8d08b9 100644 --- a/src/process/event-processor.js +++ b/src/process/event-processor.js @@ -4,6 +4,7 @@ import WebhooksEvent from '../process/event.js'; import UserMapping from '../db/redis/user-mapping.js'; import Utils from '../common/utils.js'; import Metrics from '../metrics/index.js'; +import config from 'config'; const Logger = newLogger('event-processor'); @@ -115,6 +116,63 @@ export default class EventProcessor { } } + _handleUserEmojiChangedEvent(outputEvent, rawEvent) { + const internalUserId = outputEvent.data.attributes.user["internal-user-id"]; + const emoji = outputEvent.data.attributes.user.emoji; + + this._notifyOutputModules(outputEvent, rawEvent); + + // If the emoji changed to raiseHand, we're dealing with BBB < 2.7 + // where the events weren't separated yet. In this case, we'll + // spoof a raiseHand event and store the state so we can + // spoof a raiseHand: false event when user-emoji-changed is + // called again with a different emoji. + if (emoji === 'raiseHand') { + const spoofedEvent = config.util.cloneDeep(outputEvent); + spoofedEvent.data.id = 'user-raise-hand-changed'; + delete spoofedEvent.data.attributes.user.emoji; + spoofedEvent.data.attributes.user.raiseHand = true; + + return UserMapping.get().updateWithField( + 'internalUserID', + internalUserId, { + user: { + raiseHand: true, + }, + } + ).catch((error) => { + Logger.error('error updating user mapping', error);} + ).finally(() => { + this._notifyOutputModules(spoofedEvent, rawEvent); + }); + } + + const userInfo = UserMapping.get().getUser(internalUserId); + + // Emoji changed and raiseHand was true, so we'll spoof a raiseHand: false + if (userInfo != null && userInfo.raiseHand === true) { + const spoofedEvent = config.util.cloneDeep(outputEvent); + spoofedEvent.data.id = 'user-raise-hand-changed'; + delete spoofedEvent.data.attributes.user.emoji; + spoofedEvent.data.attributes.user.raiseHand = false; + + return UserMapping.get().updateWithField( + 'internalUserID', + internalUserId, { + user: { + raiseHand: false, + }, + } + ).catch((error) => { + Logger.error('error updating user mapping', error); + }).finally(() => { + this._notifyOutputModules(spoofedEvent, rawEvent); + }); + } + + return Promise.resolve(); + } + processInputEvent(event) { try { const rawEvent = this._parseEvent(event); @@ -126,7 +184,8 @@ export default class EventProcessor { const internalMeetingId = outputEvent.data.attributes.meeting["internal-meeting-id"]; IDMapping.get().reportActivity(internalMeetingId); - // First treat meeting events to add/remove ID mappings + // Any kind of retrocompatibility logic, output event post-processing, + // data storage et al. should be done here. switch (outputEvent.data.id) { case "meeting-created": IDMapping.get().addOrUpdateMapping(internalMeetingId, @@ -225,6 +284,9 @@ export default class EventProcessor { this._notifyOutputModules(outputEvent, rawEvent); }); break; + case "user-emoji-changed": + this._handleUserEmojiChangedEvent(outputEvent, rawEvent); + break; case "meeting-ended": this._handleMeetingEndedEvent(outputEvent).finally(() => { this._notifyOutputModules(outputEvent, rawEvent); @@ -251,6 +313,10 @@ export default class EventProcessor { return; } + Logger.info('notifying output modules', { + event: message, + }); + this.outputs.forEach((output) => { output.onEvent(message, raw).catch((error) => { Logger.error('error notifying output module', { diff --git a/src/process/event.js b/src/process/event.js index e97f802..7349457 100644 --- a/src/process/event.js +++ b/src/process/event.js @@ -143,13 +143,13 @@ export default class WebhooksEvent { } if (this.outputEvent) { - logger.info('output event mapped', this.outputEvent); + logger.debug('output event mapped', { event: this.outputEvent }); } return this.outputEvent; } - logger.warn('invalid input event', this.inputEvent); + logger.warn('invalid input event', { event: this.inputEvent }); return null; } @@ -253,22 +253,6 @@ export default class WebhooksEvent { } } - handleUserEmojiChanged(message) { - try { - // < 2.7 => UserEmojiChangedEvtMsg also bundles the raiseHand action as an emoji - // >= 2.7 => UserEmojiChangedEvtMsg and UserRaiseHandChangedEvtMsg are separate events - // and the raiseHand action is not bundled as an emoji anymore - const { body } = message.core; - const emoji = body.emoji || body.reactionEmoji; - - if (emoji && emoji === "raiseHand") return "user-raise-hand-changed"; - return "user-emoji-changed"; - } catch (error) { - logger.error('error handling user emoji changed', error); - return "user-emoji-changed"; - } - } - handleUserMutedVoice(message) { try { const { body } = message.core; @@ -334,9 +318,7 @@ export default class WebhooksEvent { break; } case "user-raise-hand-changed": { - const emoji = msgBody.emoji || msgBody.reactionEmoji; - const raiseHand = msgBody.raiseHand || emoji === "raiseHand"; - this.outputEvent.data["attributes"]["user"]["raise-hand"] = raiseHand; + this.outputEvent.data["attributes"]["user"]["raise-hand"] = msgBody.raiseHand; break; } default: @@ -554,7 +536,7 @@ export default class WebhooksEvent { case "PresenterAssignedEvtMsg": return "user-presenter-assigned"; case "PresenterUnassignedEvtMsg": return "user-presenter-unassigned"; case "UserEmojiChangedEvtMsg": - case "UserReactionEmojiChangedEvtMsg": return this.handleUserEmojiChanged(message); + case "UserReactionEmojiChangedEvtMsg": return 'user-emoji-changed'; case "UserRaiseHandChangedEvtMsg": return "user-raise-hand-changed"; case "GroupChatMessageBroadcastEvtMsg": return "chat-group-message-sent"; case "PublishedRecordingSysMsg": return "rap-published"; From ff586c5aecb6ef70ec9598d74a3c3430fa12c45a Mon Sep 17 00:00:00 2001 From: mp Date: Thu, 9 Nov 2023 20:57:22 -0300 Subject: [PATCH 123/154] (xapi) - implemented fail reason on tests --- test/xapi/events.js | 47 +++++------------- test/xapi/index.js | 3 +- test/xapi/validateFunctions.js | 89 ++++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 36 deletions(-) create mode 100644 test/xapi/validateFunctions.js diff --git a/test/xapi/events.js b/test/xapi/events.js index 94d67bb..21c1b35 100644 --- a/test/xapi/events.js +++ b/test/xapi/events.js @@ -1,7 +1,8 @@ import { open } from 'node:fs/promises'; import { fileURLToPath } from 'url'; -import { validate as validateUUID } from "uuid"; -import { DateTime, Duration } from "luxon"; +import { validateVerb, validateDefinitionType, validateCommonProperties, + validatePlannedDuration, validateResultDuration, validateVirtualClassroomParent, + validatePoll, validatePollResponse, validateRaiseHandEmoji } from './validateFunctions.js'; const MAPPED_EVENTS_PATH = fileURLToPath( new URL('../../example/events/mapped-events.json', import.meta.url) ); @@ -41,42 +42,18 @@ const mapSamplesToEvents = async () => { return eventList; } -const isValidISODate = (dateString) => DateTime.fromISO(dateString, { zone: "utc", setZone: true }).isValid; -const isValidISODuration = (durationString) => Duration.fromISO(durationString).isValid; - -const validateVerb = (statement, verbId) => statement.verb.id === verbId; - -const validateDefinitionType = (statement, definitionType) => statement.object.definition.type === definitionType; - -const validateCommonProperties = statement => - statement.context.contextActivities.category[0].id == "https://w3id.org/xapi/virtual-classroom" -&& statement.context.contextActivities.category[0].definition.type == "http://adlnet.gov/expapi/activities/profile" -&& validateUUID(statement.object.id.substring(statement.object.id.length - 36)) -&& validateUUID(statement.context.registration) -&& validateUUID(statement.context.extensions['https://w3id.org/xapi/cmi5/context/extensions/sessionid']) -&& Object.prototype.hasOwnProperty.call(statement, 'actor') -&& isValidISODate(statement.timestamp); - -const validateVirtualClassroomParent = statement => - statement.context.contextActivities.parent[0].definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom' -&& validateUUID(statement.context.contextActivities.parent[0].id.substring(statement.context.contextActivities.parent[0].id.length - 36)) - -const validatePoll = statement => - statement.object.definition.interactionType === 'choice' - && Array.isArray(statement.object.definition.choices) - const validators = { 'meeting-created': (event, statement) => { return validateVerb(statement, 'http://adlnet.gov/expapi/verbs/initialized') && validateDefinitionType(statement, 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom') - && isValidISODuration(statement.context.extensions['http://id.tincanapi.com/extension/planned-duration']) + && validatePlannedDuration(statement) && validateCommonProperties(statement); }, 'meeting-ended': (event, statement) => { return validateVerb(statement, 'http://adlnet.gov/expapi/verbs/terminated') && validateDefinitionType(statement, 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom') - && isValidISODuration(statement.result.duration) - && isValidISODuration(statement.context.extensions['http://id.tincanapi.com/extension/planned-duration']) + && validateResultDuration(statement) + && validatePlannedDuration(statement) && validateCommonProperties(statement); }, 'user-joined': (event, statement) => { @@ -156,16 +133,16 @@ const validators = { && validatePoll(statement) && validateCommonProperties(statement) && validateVirtualClassroomParent(statement) - && Object.prototype.hasOwnProperty.call(statement.result, 'response'); + && validatePollResponse(statement); }, 'user-raise-hand-changed': (event, statement) => { - const raisedHandVerb = "https://w3id.org/xapi/virtual-classroom/verbs/reacted"; - const loweredHandVerb = "https://w3id.org/xapi/virtual-classroom/verbs/unreacted"; - const isRaiseHand = event.data.attributes.user["raise-hand"]; + const raisedHandVerb = 'https://w3id.org/xapi/virtual-classroom/verbs/reacted'; + const loweredHandVerb = 'https://w3id.org/xapi/virtual-classroom/verbs/unreacted'; + const isRaiseHand = event.data.attributes.user['raise-hand']; return validateVerb(statement, isRaiseHand ? raisedHandVerb : loweredHandVerb) && validateDefinitionType(statement, 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom') && validateCommonProperties(statement) - && statement.result.extensions["https://w3id.org/xapi/virtual-classroom/extensions/emoji"] === 'U+1F590'; + && validateRaiseHandEmoji(statement); } } @@ -173,7 +150,7 @@ const validate = (event, statement) => { const eventId = event.data.id; const validator = validators[eventId]; - if (!validator) throw new Error(`No validator for eventId "${eventId}"`); + if (!validator) throw new Error(`No validator for eventId '${eventId}'`); return validator(event, statement); } diff --git a/test/xapi/index.js b/test/xapi/index.js index d341f8b..9abc4a0 100644 --- a/test/xapi/index.js +++ b/test/xapi/index.js @@ -34,11 +34,12 @@ const generateTestCase = (event, redisClient, channel) => { //console.debug("Statement received", statement); const valid = validate(event, statement); if (!valid) { - done(new Error(`Event ${eventId} is not valid.\nStatement: ${JSON.stringify(statement)}`)); + done(new Error(`Event ${eventId} is not valid.\n\nStatement: ${JSON.stringify(statement)}`)); } else { done(); } } catch (error) { + error.message += `\n\nStatement: ${JSON.stringify(statement)}`; done(error); } }); diff --git a/test/xapi/validateFunctions.js b/test/xapi/validateFunctions.js new file mode 100644 index 0000000..478c018 --- /dev/null +++ b/test/xapi/validateFunctions.js @@ -0,0 +1,89 @@ +import { validate as validateUUID } from 'uuid'; +import { DateTime, Duration } from 'luxon'; + +const isValidISODate = (dateString) => DateTime.fromISO(dateString, { zone: 'utc', setZone: true }).isValid; +const isValidISODuration = (durationString) => Duration.fromISO(durationString).isValid; + +const checkCondition = (condition, errorMsg) => { + if (condition === true) return true + else throw new Error(errorMsg) +}; + +const validateVerb = (statement, verbId) => checkCondition( + statement.verb.id === verbId, + `verb.id '${statement.verb.id}' should be '${verbId}'` +); + +const validateDefinitionType = (statement, definitionType) => checkCondition( + statement.object.definition.type === definitionType, + `object.definition.type '${statement.object.definition.type}' should be '${definitionType}'` +); + +const validateCommonProperties = statement => checkCondition( + statement.context.contextActivities.category[0].id == 'https://w3id.org/xapi/virtual-classroom', + `context.contextActivities.category[0].id '${statement.context.contextActivities.category[0].id}' \ +should be 'https://w3id.org/xapi/virtual-classroom'` +) && checkCondition(statement.context.contextActivities.category[0].definition.type == 'http://adlnet.gov/expapi/activities/profile', + `context.contextActivities.category[0].definition.type '${statement.context.contextActivities.category[0].definition.type}' \ +should be 'http://adlnet.gov/expapi/activities/profile'` +) && checkCondition(validateUUID(statement.object.id.substring(statement.object.id.length - 36)), + `object.id '${statement.object.id}' should end in an UUID` +) +&& checkCondition(validateUUID(statement.context.registration), + `context.registration '${statement.context.registration}' should be an UUID`) +&& checkCondition(validateUUID(statement.context.extensions['https://w3id.org/xapi/cmi5/context/extensions/sessionid']), + `context.extensions['https://w3id.org/xapi/cmi5/context/extensions/sessionid' \ +'${statement.context.extensions['https://w3id.org/xapi/cmi5/context/extensions/sessionid']}' should be an UUID`) +&& checkCondition(Object.prototype.hasOwnProperty.call(statement, 'actor'), `actor should be present`) +&& checkCondition(isValidISODate(statement.timestamp), 'timestamp should be a valid ISO date'); + +const validatePlannedDuration = statement => checkCondition( + isValidISODuration(statement.context.extensions['http://id.tincanapi.com/extension/planned-duration']), + `context.extensions['http://id.tincanapi.com/extension/planned-duration'] \ +'${statement.context.extensions['http://id.tincanapi.com/extension/planned-duration']}' \ +should be a valid ISO duration` +); + +const validateResultDuration = statement => checkCondition( + isValidISODuration(statement.result.duration), + `result.duration '${statement.result.duration}' should be a valid ISO duration` +); + +const validateVirtualClassroomParent = statement => checkCondition( + statement.context.contextActivities.parent[0].definition.type === 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom', + `context.contextActivities.parent[0].definition.type '${statement.context.contextActivities.parent[0].definition.type}' \ +should be 'https://w3id.org/xapi/virtual-classroom/activity-types/virtual-classroom'`) +&& checkCondition( + validateUUID(statement.context.contextActivities.parent[0].id.substring(statement.context.contextActivities.parent[0].id.length - 36)), + `context.contextActivities.parent[0].id '${statement.context.contextActivities.parent[0].id}' should end in an UUID`) + +const validatePoll = statement => checkCondition(statement.object.definition.interactionType === 'choice', + `object.definition.interactionType '${statement.object.definition.interactionType}' should be 'choice'`) + && checkCondition(Array.isArray(statement.object.definition.choices), + `object.definition.choices '${statement.object.definition.choices}' should be an Array`) + +const validatePollResponse = statement => checkCondition( + Object.prototype.hasOwnProperty.call(statement, 'result'), + `statement should have a 'result' property` +) && checkCondition( + Object.prototype.hasOwnProperty.call(statement.result, 'response'), + `result should have a 'response'` +) + +const validateRaiseHandEmoji = statement => checkCondition( + statement.result.extensions['https://w3id.org/xapi/virtual-classroom/extensions/emoji'] === 'U+1F590', + `result.extensions['https://w3id.org/xapi/virtual-classroom/extensions/emoji'] \ +'${statement.result.extensions['https://w3id.org/xapi/virtual-classroom/extensions/emoji']}' should be 'U+1F590'` +); + +export { + validateVerb, + validateDefinitionType, + validateCommonProperties, + validatePlannedDuration, + validateResultDuration, + validateVirtualClassroomParent, + validatePoll, + validatePollResponse, + validateRaiseHandEmoji, +}; From ef03eb20451c7eb75789b75b071bf4dcd7ce2633 Mon Sep 17 00:00:00 2001 From: mp Date: Thu, 9 Nov 2023 21:15:52 -0300 Subject: [PATCH 124/154] (xapi) - clarified lrs credentials decryption comments --- src/out/xapi/decrypt.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/out/xapi/decrypt.js b/src/out/xapi/decrypt.js index fc043d5..91d3f87 100644 --- a/src/out/xapi/decrypt.js +++ b/src/out/xapi/decrypt.js @@ -9,7 +9,8 @@ export default function decryptStr(encryptedObj, secret) { // Decode the base64-encoded text const encryptedText = Buffer.from(encryptedObj, 'base64'); - // Extract salt (first 8 bytes) and ciphertext (the rest) + // Extract salt (bytes 8 to 15) and ciphertext (the rest) + // the first 8 bytes are reserved for OpenSSL's 'Salted__' magic prefix const salt = encryptedText.subarray(8, 16); const ciphertext = encryptedText.subarray(16); From 38966c5454afa229c982671b5efd7c7bb4d953eb Mon Sep 17 00:00:00 2001 From: mp Date: Thu, 9 Nov 2023 21:41:49 -0300 Subject: [PATCH 125/154] (xapi) - described the test suite on the xAPI readme file --- src/out/xapi/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/out/xapi/README.md b/src/out/xapi/README.md index 006f2a3..0a176e1 100644 --- a/src/out/xapi/README.md +++ b/src/out/xapi/README.md @@ -83,3 +83,11 @@ meta_secret-lrs-payload: U2FsdGVkX18fLg33ChrHbHyIvbcdDwU6+4yX2yTb4gbDKOKSG3hhsd2 The `meta_secret-lrs-payload` parameter allows you to securely define the LRS endpoint and authentication token for each meeting. It ensures that xAPI events generated during the meeting are sent to the correct LRS. This strategy to connect to the LRS supports multi-tenancy, as each meeting could point to a different LRS endpoint. If this parameter is present in the metadata, the endpoint and credentials contained in the YML file (lrs_* parameters) are ignored and thus not required. + +# Testing xAPI statements +A test suite for the xAPI statements is provided and can be run using the following command: +``` +npm run test:xapi +``` + +The test suite uses the mapped events file `(example/mapped-events)` and test each event separately to ensure the xAPI module will generate correct statements for each event. From a092454836109ec229857282d49a62e284dd7082 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Thu, 16 Nov 2023 12:06:04 -0300 Subject: [PATCH 126/154] fix: add Redis disconnection handling Things changed a bit in node-redis v3->v4 migration --- src/db/redis/index.js | 5 +++-- src/in/redis/index.js | 10 ++++++++-- src/out/xapi/index.js | 6 ++++++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/db/redis/index.js b/src/db/redis/index.js index 3db13ca..5142606 100644 --- a/src/db/redis/index.js +++ b/src/db/redis/index.js @@ -33,7 +33,7 @@ export default class RedisDB { } _onRedisError(error) { - this.logger.error("Redis error: ", { error }); + this.logger.error("Redis client failure", error); } async load() { @@ -42,7 +42,8 @@ export default class RedisDB { this._redisClient = createClient({ url: redisUrl, }); - this._redisClient.on("error", this._onRedisError.bind(this)); + this._redisClient.on('error', this._onRedisError.bind(this)); + this._redisClient.on('ready', () => this.logger.info('Redis client is ready')); await this._redisClient.connect(); await IDMappingC.init(this._redisClient); await UserMappingC.init(this._redisClient); diff --git a/src/in/redis/index.js b/src/in/redis/index.js index f54591c..3af4918 100644 --- a/src/in/redis/index.js +++ b/src/in/redis/index.js @@ -72,12 +72,16 @@ export default class InRedis { return Promise.all( this.config.redis.inboundChannels.map((channel) => { return this.pubsub.subscribe(channel, this._onPubsubEvent.bind(this)) - .then(() => this.logger.info(`subscribed to: ${channel}`)) - .catch((error) => this.logger.error(`error subscribing to: ${channel}: ${error}`)); + .then(() => this.logger.info(`subscribed to: ${channel}`)) + .catch((error) => this.logger.error(`error subscribing to: ${channel}: ${error}`)); }) ); } + _onRedisError(error) { + this.logger.error("Redis client failure", error); + } + async load () { if (this._validateConfig()) { const { password, host, port } = this.config.redis; @@ -85,6 +89,8 @@ export default class InRedis { this.pubsub = createClient({ url: redisUrl, }); + this.pubsub.on('error', this._onRedisError.bind(this)); + this.pubsub.on('ready', () => this.logger.info('Redis client is ready')); await this.pubsub.connect(); await this._subscribeToEvents(); } diff --git a/src/out/xapi/index.js b/src/out/xapi/index.js index af36e1e..7a683b3 100644 --- a/src/out/xapi/index.js +++ b/src/out/xapi/index.js @@ -34,6 +34,10 @@ export default class OutXAPI { return true; } + _onRedisError(error) { + this.logger.error("Redis client failure", error); + } + async load() { if (this._validateConfig()) { const { url, password, host, port } = this.config.redis || this.config; @@ -41,6 +45,8 @@ export default class OutXAPI { this.redisClient = createClient({ url: redisUrl, }); + this.redisClient.on('error', this._onRedisError.bind(this)); + this.redisClient.on('ready', () => this.logger.info('Redis client is ready')); await this.redisClient.connect(); From a32f641d9b4a799b9d6e4288a56b522ded41103b Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Mon, 20 Nov 2023 09:29:40 -0300 Subject: [PATCH 127/154] chore(test): exit on termination, restore test script for all modules --- package.json | 6 +++--- test/index.js | 3 +-- test/webhooks/index.js | 3 ++- test/xapi/index.js | 15 ++++++++------- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index b61447f..56a91b3 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,9 @@ "scripts": { "start": "node app.js", "dev-start": "nodemon --watch src --ext js,json,yml,yaml --exec node app.js", - "test": "LOG_LEVEL=silent ALL_TESTS=true ALLOW_CONFIG_MUTATIONS=true mocha", - "test:webhooks": "LOG_LEVEL=silent ALL_TESTS=false ALLOW_CONFIG_MUTATIONS=true WEBHOOKS_SUITE=true mocha", - "test:xapi": "LOG_LEVEL=silent ALL_TESTS=false ALLOW_CONFIG_MUTATIONS=true XAPI_SUITE=true mocha", + "test": "LOG_LEVEL=silent ALL_TESTS=true ALLOW_CONFIG_MUTATIONS=true mocha --exit", + "test:webhooks": "LOG_LEVEL=silent ALL_TESTS=false ALLOW_CONFIG_MUTATIONS=true WEBHOOKS_SUITE=true mocha --exit", + "test:xapi": "LOG_LEVEL=silent ALL_TESTS=false ALLOW_CONFIG_MUTATIONS=true XAPI_SUITE=true mocha --exit", "lint": "./node_modules/.bin/eslint ./", "lint:file": "./node_modules/.bin/eslint", "jsdoc": "./node_modules/.bin/jsdoc app.js application.js src/ -r" diff --git a/test/index.js b/test/index.js index 1896b83..da7ca56 100644 --- a/test/index.js +++ b/test/index.js @@ -6,8 +6,7 @@ import WebhooksSuite, { MOD_CONFIG as WH_CONFIG } from './webhooks/index.js'; import XAPISuite, { MOD_CONFIG as XAPI_CONFIG } from './xapi/index.js'; let MODULES = config.get('modules'); -MODULES = config.util.extendDeep(MODULES, XAPI_CONFIG); -MODULES = config.util.extendDeep(MODULES, WH_CONFIG); +MODULES = config.util.extendDeep(MODULES, WH_CONFIG, XAPI_CONFIG); const IN_REDIS_CONFIG = MODULES['../in/redis/index.js'].config.redis; const SHARED_SECRET = process.env.SHARED_SECRET diff --git a/test/webhooks/index.js b/test/webhooks/index.js index 9362a73..e1aaa13 100644 --- a/test/webhooks/index.js +++ b/test/webhooks/index.js @@ -10,6 +10,7 @@ const MODULES = config.get('modules'); const WH_CONFIG = MODULES['../out/webhooks/index.js']?.config; const CHECKSUM_ALGORITHM = 'sha1'; const WEBHOOKS_SUITE = process.env.WEBHOOKS_SUITE ? process.env.WEBHOOKS_SUITE === 'true' : false; +const ALL_TESTS = process.env.ALL_TESTS ? process.env.ALL_TESTS === 'true' : true; export default function suite({ redisClient, @@ -227,7 +228,7 @@ export default function suite({ export const MOD_CONFIG = { '../out/webhooks/index.js': { - enabled: WEBHOOKS_SUITE, + enabled: WEBHOOKS_SUITE || ALL_TESTS, config: { queueSize: 10, permanentURLs: [ diff --git a/test/xapi/index.js b/test/xapi/index.js index 9abc4a0..40e145f 100644 --- a/test/xapi/index.js +++ b/test/xapi/index.js @@ -3,6 +3,7 @@ import { mapSamplesToEvents, validate } from './events.js'; import PostCatcher from '../utils/post-catcher.js'; const XAPI_SUITE = process.env.XAPI_SUITE ? process.env.XAPI_SUITE === 'true' : false; +const ALL_TESTS = process.env.ALL_TESTS ? process.env.ALL_TESTS === 'true' : true; const MOCK_LRS_URL = 'http://127.0.0.1:9009'; const generateTestCase = (event, redisClient, channel) => { @@ -57,10 +58,14 @@ export default function suite({ if (!XAPI_SUITE && !force) return; let events = []; - describe('xapi tests', () => { + describe('xapi test generation', () => { before((done) => { mapSamplesToEvents().then((mappedEvents) => { events = mappedEvents; + events.forEach((event) => { + describe(`xapi: ${event.data.id}`, generateTestCase(event, redisClient, testChannel)); + }); + done(); }).catch((err) => { done(err); @@ -71,17 +76,13 @@ export default function suite({ done(); }); - it('should generate tests', () => { - events.forEach((event) => { - describe(`xapi: ${event.data.id}`, generateTestCase(event, redisClient, testChannel)); - }); - }); + it('should generate xAPI tests', () => {}); }); } export const MOD_CONFIG = { '../out/xapi/index.js': { - enabled: XAPI_SUITE, + enabled: XAPI_SUITE || ALL_TESTS, config: { lrs: { lrs_endpoint: MOCK_LRS_URL, From f7655d81219fb9a9007c73430241d5745c9a8254 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Mon, 20 Nov 2023 09:49:04 -0300 Subject: [PATCH 128/154] fix: properly call module unload --- src/modules/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/index.js b/src/modules/index.js index 2785639..8ffac0d 100644 --- a/src/modules/index.js +++ b/src/modules/index.js @@ -190,7 +190,7 @@ export default class ModuleManager { for (var proc in this.modules) { if (Object.prototype.hasOwnProperty.call(this.modules, proc)) { let procObj = this.modules[proc]; - if (typeof procObj.stop === 'function') procObj.stop() + if (typeof procObj.unload === 'function') procObj.unload(); } } From 328e46b121e2d28df86a9bc1c7811dfd75b83bd9 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Mon, 20 Nov 2023 09:49:55 -0300 Subject: [PATCH 129/154] chore: add CHANGELOG.md file --- CHANGELOG.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a2c7fe0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,38 @@ +# CHANGELOG + +All notable changes to this project will be documented in this file. + +### v3.0.0-beta.1 + +* feat(test): add support for modular test suites +* feat(test): add xAPI test suite +* refactor(test): remove nock as a dependency +* fix(test): restore remaining out/webhooks tests +* fix(xapi): set chat message statements timestamp to ISO format +* fix: add Redis disconnection handling + +### v3.0.0-alpha.1 + +* !refactor: application rewritten to use a modular input/processing/ouput system +* !refactor: modernize codebase (ES6 imports, Node.js >= 18 etc.) +* !refactor(webhooks): the webhooks functionality was rewritten into an output module +* !refactor(webhooks): hook IDs are now UUIDs instead of integers +* !refactor: new logging system (using Pino) +* !refactor: migrate node-redis from v3 to v4 +* !refactor: new queue system (using Bullmq) +* refactor(webhooks): replace request with node-fetch +* refactor: replace sha1 dependency with native code +* feat: new xAPI output module with support for multitenancy + - Implements https://github.com/gaia-x-dases/xapi-virtual-classroom + - For more information: (README.md)[src/out/xapi/README.md] +* feat(events): add support for poll events +* feat(events): add support for raise-hand events +* feat(events): add support for emoji events +* feat(events): add user info to screenshare events +* feat(events): add support for audio muted/unmuted events +* feat: add Prometheus instrumentation +* feat: add JSDoc annotations to most of the codebase +* feat: log to file +* feat: add support for multiple checksum algorithms (SHA1,...,SHA512) +* fix(events): user-left events are now emitted for trailing users on meeting-ended events +* build: add docker-compose and updated Dockerfile examples From 64fd175de6965d7202b70f9f35ee8cee43caba02 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Mon, 20 Nov 2023 09:50:13 -0300 Subject: [PATCH 130/154] 3.0.0-beta.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 59a5415..f5681e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "bbb-webhooks", - "version": "3.0.0-alpha.0", + "version": "3.0.0-beta.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bbb-webhooks", - "version": "3.0.0-alpha.0", + "version": "3.0.0-beta.0", "dependencies": { "bullmq": "^4.11.4", "config": "^3.3.7", diff --git a/package.json b/package.json index 56a91b3..f406b0e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bbb-webhooks", - "version": "3.0.0-alpha.0", + "version": "3.0.0-beta.0", "description": "A BigBlueButton mudule for events WebHooks", "type": "module", "scripts": { From a9c6690a2c2b064bf9595a14c188dd265b0e12bc Mon Sep 17 00:00:00 2001 From: mp Date: Tue, 28 Nov 2023 10:02:37 -0300 Subject: [PATCH 131/154] (xAPI) - ensure the correct lrs_endpoint is used --- src/out/xapi/xapi.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/out/xapi/xapi.js b/src/out/xapi/xapi.js index 8450ad6..f717a0d 100644 --- a/src/out/xapi/xapi.js +++ b/src/out/xapi/xapi.js @@ -62,7 +62,9 @@ export default class XAPI { headers, }; - const xAPIEndpoint = new URL("xAPI/statements", lrs_endpoint); + // Remove /xapi(/)(statements)(/) from the end of the lrs_endpoint to ensure the correct endpoint is used + const fixed_lrs_endpoint = lrs_endpoint.replace(/\/xapi\/?(statements)?\/?$/i, ''); + const xAPIEndpoint = new URL("xAPI/statements", fixed_lrs_endpoint); try { const response = await fetch(xAPIEndpoint, requestOptions); From 6e5f87a6d188a3939aa80d8bdb3ac253a7e7a567 Mon Sep 17 00:00:00 2001 From: mp Date: Fri, 15 Dec 2023 16:54:44 -0300 Subject: [PATCH 132/154] xAPI: add support for meta_xapi-create-end-actor-name --- src/out/xapi/README.md | 5 +++++ src/out/xapi/compartment.js | 4 +++- src/out/xapi/templates.js | 5 ++++- src/out/xapi/xapi.js | 1 + 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/out/xapi/README.md b/src/out/xapi/README.md index 0a176e1..6572c67 100644 --- a/src/out/xapi/README.md +++ b/src/out/xapi/README.md @@ -57,6 +57,11 @@ You have the option to set relevant metadata when creating a meeting in Big Blue If you set `meta_xapi-enabled` to false, no xAPI events will be generated or sent to the LRS for that particular meeting. This provides the flexibility to choose which meetings should be tracked using xAPI. +### meta_xapi-create-end-actor-name +- **Description**: This parameter specifies the actor name to be used in the meeting-created/ended (Initialized/Terminated) statements +- **Value Format**: string +- **Default Value**: `` + ### meta_secret-lrs-payload - **Description**: This parameter allows you to specify the credentials and endpoint of the Learning Record Store (LRS) where the xAPI events will be sent. The payload is a Base64-encoded string representing a JSON object encrypted (AES 256/PBKDF2) using the **server secret** as the **passphrase**. - **Value Format**: Base64-encoded JSON object encrypted with AES 256/PBKDF2 encryption diff --git a/src/out/xapi/compartment.js b/src/out/xapi/compartment.js index 39175d6..0aa4186 100644 --- a/src/out/xapi/compartment.js +++ b/src/out/xapi/compartment.js @@ -7,7 +7,8 @@ export class meetingCompartment extends StorageCompartmentKV { async addOrUpdateMeetingData(meeting_data) { const { internal_meeting_id, context_registration, planned_duration, - create_time, meeting_name, xapi_enabled, lrs_endpoint, lrs_token } = meeting_data; + create_time, meeting_name, xapi_enabled, create_end_actor_name, + lrs_endpoint, lrs_token } = meeting_data; const payload = { internal_meeting_id, @@ -16,6 +17,7 @@ export class meetingCompartment extends StorageCompartmentKV { create_time, meeting_name, xapi_enabled, + create_end_actor_name, lrs_endpoint, lrs_token, }; diff --git a/src/out/xapi/templates.js b/src/out/xapi/templates.js index ee96579..9666570 100644 --- a/src/out/xapi/templates.js +++ b/src/out/xapi/templates.js @@ -14,7 +14,8 @@ export default function getXAPIStatement(event, meeting_data, user_data = null, context_registration, session_id, planned_duration, - create_time } = meeting_data; + create_time, + create_end_actor_name } = meeting_data; const planned_duration_ISO = Duration.fromObject({ minutes: planned_duration }).toISO(); const create_time_ISO = DateTime.fromMillis(create_time).toUTC().toISO(); @@ -106,12 +107,14 @@ export default function getXAPIStatement(event, meeting_data, user_data = null, // Custom 'meeting-created' attributes if (eventId == 'meeting-created') { + statement.actor.account.name = create_end_actor_name; statement.context.extensions["http://id.tincanapi.com/extension/planned-duration"] = planned_duration_ISO statement.timestamp = create_time_ISO; } // Custom 'meeting-ended' attributes else if (eventId == 'meeting-ended') { + statement.actor.account.name = create_end_actor_name; statement.context.extensions["http://id.tincanapi.com/extension/planned-duration"] = planned_duration_ISO statement.result = { "duration": Duration.fromMillis(eventTs - create_time).toISO() diff --git a/src/out/xapi/xapi.js b/src/out/xapi/xapi.js index f717a0d..ee161d0 100644 --- a/src/out/xapi/xapi.js +++ b/src/out/xapi/xapi.js @@ -102,6 +102,7 @@ export default class XAPI { meeting_data.create_time = event.data.attributes.meeting["create-time"]; meeting_data.meeting_name = event.data.attributes.meeting.name; meeting_data.xapi_enabled = event.data.attributes.meeting.metadata?.["xapi-enabled"] !== 'false' ? 'true' : 'false'; + meeting_data.create_end_actor_name = event.data.attributes.meeting.metadata?.["xapi-create-end-actor-name"] || ""; const lrs_payload = event.data.attributes.meeting.metadata?.["secret-lrs-payload"]; let lrs_endpoint = ''; From 36bfb92ce3f1ed6acf35533673237ac4c03e5291 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Tue, 19 Dec 2023 11:22:36 -0300 Subject: [PATCH 133/154] chore: update CHANGELOG.md for v3.0.0-beta.1 --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2c7fe0..b824c6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. ### v3.0.0-beta.1 +* fix(xapi): ensure the correct lrs_endpoint is used +* feat(xapi): add suport for meta_xapi-create-end-actor-name + +### v3.0.0-beta.0 + * feat(test): add support for modular test suites * feat(test): add xAPI test suite * refactor(test): remove nock as a dependency From 3be95cfc9ab411b11689d0b90e25bc6a50364413 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Tue, 19 Dec 2023 11:22:51 -0300 Subject: [PATCH 134/154] 3.0.0-beta.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index f5681e0..bbd852d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "bbb-webhooks", - "version": "3.0.0-beta.0", + "version": "3.0.0-beta.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bbb-webhooks", - "version": "3.0.0-beta.0", + "version": "3.0.0-beta.1", "dependencies": { "bullmq": "^4.11.4", "config": "^3.3.7", diff --git a/package.json b/package.json index f406b0e..bad7532 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bbb-webhooks", - "version": "3.0.0-beta.0", + "version": "3.0.0-beta.1", "description": "A BigBlueButton mudule for events WebHooks", "type": "module", "scripts": { From f472869387637d9e9d3f1fc4edb21bd102e4c1b9 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Fri, 22 Dec 2023 20:29:56 -0300 Subject: [PATCH 135/154] refactor(webhooks): tidy API logs --- src/out/webhooks/api/api.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/out/webhooks/api/api.js b/src/out/webhooks/api/api.js index 0682d2b..4c95b24 100644 --- a/src/out/webhooks/api/api.js +++ b/src/out/webhooks/api/api.js @@ -43,7 +43,7 @@ export default class API { this.app.use((req, res, next) => { const { method, url, baseUrl, path } = req; - API.logger.info(`Received: ${method} request to ${baseUrl + path}`, { + API.logger.info(`received: ${method} request to ${baseUrl + path}`, { clientData: clientDataSimple(req), url, }); @@ -224,7 +224,7 @@ export default class API { next(); } else { const urlObj = url.parse(req.url, true); - API.logger.info('checksum check failed, sending a checksumError response', responses.checksumError); + API.logger.warn('invalid checksum', { response: responses.checksumError }); API.respondWithXML(res, responses.checksumError); this._exporter.agent.increment(METRIC_NAMES.API_REQUEST_FAILURES_XML, { method: req.method, From 9b6ee2a1389c78ee994e60061266d1b9e0ad2968 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Fri, 22 Dec 2023 20:32:30 -0300 Subject: [PATCH 136/154] fix(webhooks): re-implement includeEvents/excludeEvents includeEvents/excludeEvents were feature flags present in the previous version (v2). Those flags provided filter in/out lists for events generated by the webhooks module and weren't ported to the rewritten bbb-webhooks. --- config/custom-environment-variables.yml | 6 ++++++ config/default.example.yml | 5 +++++ src/out/webhooks/web-hooks.js | 28 +++++++++++++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/config/custom-environment-variables.yml b/config/custom-environment-variables.yml index d264750..18582e2 100644 --- a/config/custom-environment-variables.yml +++ b/config/custom-environment-variables.yml @@ -43,6 +43,12 @@ modules: port: SERVER_PORT supportedChecksumAlgorithms: SUPPORTED_CHECKSUM_ALGORITHMS hookChecksumAlgorithm: HOOK_CHECKSUM_ALGORITHM + includeEvents: + __name: INCLUDE_EVENTS + __format: json + excludeEvents: + __name: EXCLUDE_EVENTS + __format: json queue: enabled: ENABLE_WH_QUEUE permanentURLs: diff --git a/config/default.example.yml b/config/default.example.yml index cbf7877..f18d92a 100644 --- a/config/default.example.yml +++ b/config/default.example.yml @@ -90,6 +90,11 @@ modules: # callback POST requests. One of: sha1, sha256, sha384 or sha512 # Default: sha1 hookChecksumAlgorithm: sha1 + # Events to be included on the callback POST request. If not set, + # all events will that aren't excluded will be included. + includeEvents: [] + # Events to be excluded on the callback POST request. + excludeEvents: [] # IP where permanent hook will post data (more than 1 URL means more than 1 permanent hook) permanentURLs: [] # How many messages will be enqueued to be processed at the same time diff --git a/src/out/webhooks/web-hooks.js b/src/out/webhooks/web-hooks.js index e2e381b..41a8041 100644 --- a/src/out/webhooks/web-hooks.js +++ b/src/out/webhooks/web-hooks.js @@ -86,6 +86,31 @@ class WebHooks { }); } + /** + * _shouldIgnoreEvent - Check if an event should be ignored according to + * the includeEvents/excludeEvents configurations. + * @param {object} event - The event to be checked. + * @returns {boolean} - Whether the event should be ignored or not. + * @private + */ + _shouldIgnoreEvent(event) { + const eventId = event?.data?.id; + const filterIn = this.config.includeEvents || []; + const filterOut = this.config.excludeEvents || []; + + if (filterIn.length > 0 && !filterIn.includes(eventId)) { + this.logger.debug('event not included in the list of events to be sent', { eventId }); + return true; + } + + if (filterOut.length > 0 && filterOut.includes(eventId)) { + this.logger.debug('event included in the list of events to be ignored', { eventId }); + return true; + } + + return false; + } + /** * createPermanentHooks - Create permanent hooks. * @returns {Promise} - A promise that resolves when all permanent hooks have been created. @@ -193,6 +218,7 @@ class WebHooks { }); } + /** * onEvent - Handles incoming events received by the main application (relayed * from this module's entrypoint, OutWebHooks). @@ -203,6 +229,8 @@ class WebHooks { * @async */ onEvent(event, raw) { + if (this._shouldIgnoreEvent(event)) return Promise.resolve(); + const meetingID = this._extractIntMeetingID(event); let hooks = HookCompartment.get().getAllGlobalHooks(); From d7eff6822010875827464df98f135497421b3747 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Fri, 22 Dec 2023 20:34:27 -0300 Subject: [PATCH 137/154] chore: update CHANGELOG.md for v3.0.0-beta.2 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b824c6f..fe5efb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to this project will be documented in this file. +### v3.0.0-beta.2 + +fix(webhooks): re-implement includeEvents/excludeEvents + ### v3.0.0-beta.1 * fix(xapi): ensure the correct lrs_endpoint is used From f42b5e53e001eee1ce611051ce40b7e2ea7cf5ef Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Fri, 22 Dec 2023 20:34:46 -0300 Subject: [PATCH 138/154] 3.0.0-beta.2 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index bbd852d..5acdb34 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "bbb-webhooks", - "version": "3.0.0-beta.1", + "version": "3.0.0-beta.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bbb-webhooks", - "version": "3.0.0-beta.1", + "version": "3.0.0-beta.2", "dependencies": { "bullmq": "^4.11.4", "config": "^3.3.7", diff --git a/package.json b/package.json index bad7532..870943a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bbb-webhooks", - "version": "3.0.0-beta.1", + "version": "3.0.0-beta.2", "description": "A BigBlueButton mudule for events WebHooks", "type": "module", "scripts": { From c843efd61d2f7de31504cca0f35aea9deb28b91a Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Tue, 9 Jan 2024 13:36:59 +0000 Subject: [PATCH 139/154] build: bullmq@4.17.0, bump transitive deps --- CHANGELOG.md | 6 +- package-lock.json | 655 ++++++++++++++++++++++------------------------ package.json | 2 +- 3 files changed, 326 insertions(+), 337 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe5efb2..1b2c98b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,13 @@ All notable changes to this project will be documented in this file. +### v3.0.0-beta.3 + +* build: bullmq@4.17.0, bump transitive deps + ### v3.0.0-beta.2 -fix(webhooks): re-implement includeEvents/excludeEvents +* fix(webhooks): re-implement includeEvents/excludeEvents ### v3.0.0-beta.1 diff --git a/package-lock.json b/package-lock.json index 5acdb34..0f704dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "bbb-webhooks", "version": "3.0.0-beta.2", "dependencies": { - "bullmq": "^4.11.4", + "bullmq": "4.17.0", "config": "^3.3.7", "express": "^4.18.2", "js-yaml": "^4.1.0", @@ -46,9 +46,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", - "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", + "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -58,12 +58,12 @@ } }, "node_modules/@es-joy/jsdoccomment": { - "version": "0.40.1", - "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.40.1.tgz", - "integrity": "sha512-YORCdZSusAlBrFpZ77pJjc5r1bQs5caPWtAu+WWmiSo+8XaUzseapVrfAtiRFbQWnrBxxLLEwF6f6ZG/UgCQCg==", + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.41.0.tgz", + "integrity": "sha512-aKUhyn1QI5Ksbqcr3fFJj16p99QdjUxXAEuFst1Z47DRyoiMwivIH9MV/ARcJOCXVjPfjITciej8ZD2O/6qUmw==", "dev": true, "dependencies": { - "comment-parser": "1.4.0", + "comment-parser": "1.4.1", "esquery": "^1.5.0", "jsdoc-type-pratt-parser": "~4.0.0" }, @@ -87,18 +87,18 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.9.0.tgz", - "integrity": "sha512-zJmuCWj2VLBt4c25CfBIbMZLGLyhkvs7LznyVX5HfpzeocThgIj5XQK4L+g3U36mMcx8bPMhGyPpwCATamC4jQ==", + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, "node_modules/@eslint/eslintrc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", - "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", @@ -142,21 +142,21 @@ "dev": true }, "node_modules/@eslint/js": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.50.0.tgz", - "integrity": "sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", + "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.11", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", - "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", + "@humanwhocodes/object-schema": "^2.0.1", "debug": "^4.1.1", "minimatch": "^3.0.5" }, @@ -201,9 +201,9 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, "node_modules/@ioredis/commands": { @@ -212,9 +212,9 @@ "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" }, "node_modules/@jsdoc/salty": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.5.tgz", - "integrity": "sha512-TfRP53RqunNe2HBobVBJ0VLhK1HbfvBYeTC1ahnN64PWvyYyGebmMiPkuwvD9fpw2ZbkoPb8Q7mwy0aR8Z9rvw==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.7.tgz", + "integrity": "sha512-mh8LbS9d4Jq84KLw8pzho7XC2q2/IJGiJss3xwRoLD1A+EE16SjN4PfaG4jRCzKegTFLlN0Zd8SdUPE6XdoPFg==", "dev": true, "dependencies": { "lodash": "^4.17.21" @@ -339,9 +339,9 @@ } }, "node_modules/@redis/client": { - "version": "1.5.11", - "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.11.tgz", - "integrity": "sha512-cV7yHcOAtNQ5x/yQl7Yw1xf53kO0FNDTdDU6bFIMbW6ljB7U7ns0YRM+QIkpoqTAt6zK5k9Fq0QWlUbLcq9AvA==", + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.13.tgz", + "integrity": "sha512-epkUM9D0Sdmt93/8Ozk43PNjLi36RZzG+d/T1Gdu5AI8jvghonTeLYV69WVWdilvFo+PYxbP0TZ0saMvr6nscQ==", "dependencies": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", @@ -352,9 +352,9 @@ } }, "node_modules/@redis/graph": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.0.tgz", - "integrity": "sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz", + "integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==", "peerDependencies": { "@redis/client": "^1.0.0" } @@ -368,9 +368,9 @@ } }, "node_modules/@redis/search": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.5.tgz", - "integrity": "sha512-hPP8w7GfGsbtYEJdn4n7nXa6xt6hVZnnDktKW4ArMaFQ/m/aR7eFvsLQmG/mn1Upq99btPJk+F27IQ2dYpCoUg==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.6.tgz", + "integrity": "sha512-mZXCxbTYKBQ3M2lZnEddwEAks0Kc7nauire8q20oA0oA/LoA+E/b5Y5KZn232ztPb1FkIGqo12vh3Lf+Vw5iTw==", "peerDependencies": { "@redis/client": "^1.0.0" } @@ -425,9 +425,9 @@ "dev": true }, "node_modules/@types/linkify-it": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.3.tgz", - "integrity": "sha512-pTjcqY9E4nOI55Wgpz7eiI8+LzdYnw3qxXCfHyBDdPbYvbyLgWLJGh8EdPvqawwMK1Uo1794AUkkR38Fr0g+2g==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.5.tgz", + "integrity": "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==", "dev": true }, "node_modules/@types/markdown-it": { @@ -441,9 +441,9 @@ } }, "node_modules/@types/mdurl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.3.tgz", - "integrity": "sha512-T5k6kTXak79gwmIOaDF2UUQXFbnBE0zBUzF20pz7wDYu0RQMzWg+Ml/Pz50214NsFHBITkoi5VtdjFZnJ2ijjA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.5.tgz", + "integrity": "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==", "dev": true }, "node_modules/@ungap/promise-all-settled": { @@ -452,6 +452,12 @@ "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", "dev": true }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -482,9 +488,9 @@ } }, "node_modules/acorn": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -849,9 +855,9 @@ } }, "node_modules/bullmq": { - "version": "4.11.4", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-4.11.4.tgz", - "integrity": "sha512-LuCR3ILngYa3CLC5jyf8DU4Yokj9T12MWwBogP3S4IiJUtbJsQ9GTGFxho3imRxXfcd9DUfrABT/pSoqVigXiQ==", + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-4.17.0.tgz", + "integrity": "sha512-URnHgB01rlCP8RTpmW3kFnvv3vdd2aI1OcBMYQwnqODxGiJUlz9MibDVXE83mq7ee1eS1IvD9lMQqGszX6E5Pw==", "dependencies": { "cron-parser": "^4.6.0", "glob": "^8.0.3", @@ -873,12 +879,13 @@ } }, "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1028,19 +1035,22 @@ } }, "node_modules/comment-parser": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.0.tgz", - "integrity": "sha512-QLyTNiZ2KDOibvFPlZ6ZngVsZ/0gYnE6uTXi5aoDg8ed3AkJAz4sEje3Y8a29hQ1s6A99MZXe47fLAXQ1rTqaw==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz", + "integrity": "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==", "dev": true, "engines": { "node": ">= 12.0.0" } }, "node_modules/component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/concat-map": { "version": "0.0.1", @@ -1172,10 +1182,9 @@ "dev": true }, "node_modules/define-data-property": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.0.tgz", - "integrity": "sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g==", - "dev": true, + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", "dependencies": { "get-intrinsic": "^1.2.1", "gopd": "^1.0.1", @@ -1295,26 +1304,26 @@ } }, "node_modules/es-abstract": { - "version": "1.22.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.2.tgz", - "integrity": "sha512-YoxfFcDmhjOgWPWsV13+2RNjq1F6UQnfs+8TftwNqtzlmFzEXvlUwdrNrYeaizfjQzRMxkZ6ElWMOJIFKdVqwA==", + "version": "1.22.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", + "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", "dev": true, "dependencies": { "array-buffer-byte-length": "^1.0.0", "arraybuffer.prototype.slice": "^1.0.2", "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", + "call-bind": "^1.0.5", "es-set-tostringtag": "^2.0.1", "es-to-primitive": "^1.2.1", "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.1", + "get-intrinsic": "^1.2.2", "get-symbol-description": "^1.0.0", "globalthis": "^1.0.3", "gopd": "^1.0.1", - "has": "^1.0.3", "has-property-descriptors": "^1.0.0", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", + "hasown": "^2.0.0", "internal-slot": "^1.0.5", "is-array-buffer": "^3.0.2", "is-callable": "^1.2.7", @@ -1324,7 +1333,7 @@ "is-string": "^1.0.7", "is-typed-array": "^1.1.12", "is-weakref": "^1.0.2", - "object-inspect": "^1.12.3", + "object-inspect": "^1.13.1", "object-keys": "^1.1.1", "object.assign": "^4.1.4", "regexp.prototype.flags": "^1.5.1", @@ -1338,7 +1347,7 @@ "typed-array-byte-offset": "^1.0.0", "typed-array-length": "^1.0.4", "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.11" + "which-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" @@ -1348,26 +1357,26 @@ } }, "node_modules/es-set-tostringtag": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", - "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", + "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", "dev": true, "dependencies": { - "get-intrinsic": "^1.1.3", - "has": "^1.0.3", - "has-tostringtag": "^1.0.0" + "get-intrinsic": "^1.2.2", + "has-tostringtag": "^1.0.0", + "hasown": "^2.0.0" }, "engines": { "node": ">= 0.4" } }, "node_modules/es-shim-unscopables": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", - "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", "dev": true, "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.0" } }, "node_modules/es-to-primitive": { @@ -1414,18 +1423,19 @@ } }, "node_modules/eslint": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.50.0.tgz", - "integrity": "sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", + "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.50.0", - "@humanwhocodes/config-array": "^0.11.11", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.56.0", + "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -1526,28 +1536,28 @@ "dev": true }, "node_modules/eslint-plugin-import": { - "version": "2.28.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.28.1.tgz", - "integrity": "sha512-9I9hFlITvOV55alzoKBI+K9q74kv0iKMeY6av5+umsNwayt59fz692daGyjR+oStBQgx6nwR9rXldDev3Clw+A==", + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", + "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", "dev": true, "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.findlastindex": "^1.2.2", - "array.prototype.flat": "^1.3.1", - "array.prototype.flatmap": "^1.3.1", + "array-includes": "^3.1.7", + "array.prototype.findlastindex": "^1.2.3", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", "debug": "^3.2.7", "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.7", + "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.8.0", - "has": "^1.0.3", - "is-core-module": "^2.13.0", + "hasown": "^2.0.0", + "is-core-module": "^2.13.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", - "object.fromentries": "^2.0.6", - "object.groupby": "^1.0.0", - "object.values": "^1.1.6", + "object.fromentries": "^2.0.7", + "object.groupby": "^1.0.1", + "object.values": "^1.1.7", "semver": "^6.3.1", - "tsconfig-paths": "^3.14.2" + "tsconfig-paths": "^3.15.0" }, "engines": { "node": ">=4" @@ -1593,26 +1603,26 @@ } }, "node_modules/eslint-plugin-jsdoc": { - "version": "46.8.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.8.2.tgz", - "integrity": "sha512-5TSnD018f3tUJNne4s4gDWQflbsgOycIKEUBoCLn6XtBMgNHxQFmV8vVxUtiPxAQq8lrX85OaSG/2gnctxw9uQ==", + "version": "46.10.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.10.1.tgz", + "integrity": "sha512-x8wxIpv00Y50NyweDUpa+58ffgSAI5sqe+zcZh33xphD0AVh+1kqr1ombaTRb7Fhpove1zfUuujlX9DWWBP5ag==", "dev": true, "dependencies": { - "@es-joy/jsdoccomment": "~0.40.1", + "@es-joy/jsdoccomment": "~0.41.0", "are-docs-informative": "^0.0.2", - "comment-parser": "1.4.0", + "comment-parser": "1.4.1", "debug": "^4.3.4", "escape-string-regexp": "^4.0.0", "esquery": "^1.5.0", "is-builtin-module": "^3.2.1", "semver": "^7.5.4", - "spdx-expression-parse": "^3.0.1" + "spdx-expression-parse": "^4.0.0" }, "engines": { "node": ">=16" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" } }, "node_modules/eslint-plugin-jsdoc/node_modules/debug": { @@ -1917,9 +1927,9 @@ } }, "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz", + "integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==", "dev": true, "dependencies": { "reusify": "^1.0.4" @@ -2014,17 +2024,17 @@ } }, "node_modules/flat-cache": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz", - "integrity": "sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, "dependencies": { - "flatted": "^3.2.7", + "flatted": "^3.2.9", "keyv": "^4.5.3", "rimraf": "^3.0.2" }, "engines": { - "node": ">=12.0.0" + "node": "^10.12.0 || >=12.0.0" } }, "node_modules/flatted": { @@ -2113,9 +2123,12 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/function.prototype.name": { "version": "1.1.6", @@ -2162,14 +2175,14 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", + "function-bind": "^1.1.2", "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2241,9 +2254,9 @@ } }, "node_modules/globals": { - "version": "13.22.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.22.0.tgz", - "integrity": "sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -2274,7 +2287,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, "dependencies": { "get-intrinsic": "^1.1.3" }, @@ -2303,17 +2315,6 @@ "node": ">=4.x" } }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", @@ -2333,12 +2334,11 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", - "dev": true, + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", "dependencies": { - "get-intrinsic": "^1.1.1" + "get-intrinsic": "^1.2.2" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2381,6 +2381,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -2391,28 +2402,10 @@ } }, "node_modules/help-me": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/help-me/-/help-me-4.2.0.tgz", - "integrity": "sha512-TAOnTB8Tz5Dw8penUuzHVrKNKlCIbwwbHnXraNJxPwf8LRtE2HlM84RYuezMFcwOJmoYOCWVDyJ8TQGxn9PgxA==", - "dev": true, - "dependencies": { - "glob": "^8.0.0", - "readable-stream": "^3.6.0" - } - }, - "node_modules/help-me/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "dev": true }, "node_modules/http-errors": { "version": "2.0.0", @@ -2460,9 +2453,9 @@ ] }, "node_modules/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", + "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", "dev": true, "engines": { "node": ">= 4" @@ -2514,13 +2507,13 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/internal-slot": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", - "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", + "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.0", - "has": "^1.0.3", + "get-intrinsic": "^1.2.2", + "hasown": "^2.0.0", "side-channel": "^1.0.4" }, "engines": { @@ -2661,12 +2654,12 @@ } }, "node_modules/is-core-module": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", - "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", "dev": true, "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2992,9 +2985,9 @@ "dev": true }, "node_modules/keyv": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", - "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "dependencies": { "json-buffer": "3.0.1" @@ -3101,9 +3094,9 @@ } }, "node_modules/luxon": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.3.tgz", - "integrity": "sha512-tFWBiv3h7z+T/tDaoxA8rqTxy1CHV6gHS//QdaH4pulbq/JuBSGgQspQQqcgnwdAx6pNI7cmvz5Sv/addzHmUg==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", "engines": { "node": ">=12" } @@ -3373,9 +3366,9 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/msgpackr": { - "version": "1.9.9", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.9.9.tgz", - "integrity": "sha512-sbn6mioS2w0lq1O6PpGtsv6Gy8roWM+o3o4Sqjd6DudrL/nOugY+KyJUimoWzHnf9OkO0T6broHFnYE/R05t9A==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.10.1.tgz", + "integrity": "sha512-r5VRLv9qouXuLiIBrLpl2d5ZvPt8svdQTl5/vMvE4nzDMyEX4sgW5yWhuBBj5UmgwOTWj8CIdSXn5sAfsHAWIQ==", "optionalDependencies": { "msgpackr-extract": "^3.0.2" } @@ -3428,9 +3421,9 @@ } }, "node_modules/nise": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.4.tgz", - "integrity": "sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.5.tgz", + "integrity": "sha512-VJuPIfUFaXNRzETTQEEItTOP8Y171ijr+JLq42wHes3DiryR8vT+1TXQW/Rx8JNUhyYYWyIvjXTU6dOhJcs9Nw==", "dev": true, "dependencies": { "@sinonjs/commons": "^2.0.0", @@ -3534,13 +3527,13 @@ } }, "node_modules/nodemon": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.1.tgz", - "integrity": "sha512-g9AZ7HmkhQkqXkRc20w+ZfQ73cHLbE8hnPbtaFbFtCumZsjyMhKk9LajQ07U5Ux28lvFjZ5X7HvWR1xzU8jHVw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.2.tgz", + "integrity": "sha512-9qIN2LNTrEzpOPBaWHTm4Asy1LxXLSickZStAQ4IZe7zsoIpD/A7LWxhZV3t4Zu352uBcqVnRsDXSMR2Sc3lTA==", "dev": true, "dependencies": { "chokidar": "^3.5.2", - "debug": "^3.2.7", + "debug": "^4", "ignore-by-default": "^1.0.1", "minimatch": "^3.1.2", "pstree.remy": "^1.1.8", @@ -3562,12 +3555,20 @@ } }, "node_modules/nodemon/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "dependencies": { - "ms": "^2.1.1" + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, "node_modules/nodemon/node_modules/has-flag": { @@ -3580,9 +3581,9 @@ } }, "node_modules/nodemon/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, "node_modules/nodemon/node_modules/supports-color": { @@ -3622,9 +3623,9 @@ } }, "node_modules/object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3639,13 +3640,13 @@ } }, "node_modules/object.assign": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", "has-symbols": "^1.0.3", "object-keys": "^1.1.1" }, @@ -3847,16 +3848,16 @@ } }, "node_modules/pino": { - "version": "8.16.1", - "resolved": "https://registry.npmjs.org/pino/-/pino-8.16.1.tgz", - "integrity": "sha512-3bKsVhBmgPjGV9pyn4fO/8RtoVDR8ssW1ev819FsRXlRNgW8gR/9Kx+gCK4UPWd4JjrRDLWpzd/pb1AyWm3MGA==", + "version": "8.17.2", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.17.2.tgz", + "integrity": "sha512-LA6qKgeDMLr2ux2y/YiUt47EfgQ+S9LznBWOJdN3q1dx2sv0ziDLUBeVpyVv17TEcGCBuWf0zNtg3M5m1NhhWQ==", "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.1.1", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "v1.1.0", "pino-std-serializers": "^6.0.0", - "process-warning": "^2.0.0", + "process-warning": "^3.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", @@ -3876,40 +3877,17 @@ "split2": "^4.0.0" } }, - "node_modules/pino-abstract-transport/node_modules/readable-stream": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz", - "integrity": "sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==", - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/pino-abstract-transport/node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, "node_modules/pino-pretty": { - "version": "10.2.3", - "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-10.2.3.tgz", - "integrity": "sha512-4jfIUc8TC1GPUfDyMSlW1STeORqkoxec71yhxIpLDQapUu8WOuoz2TTCoidrIssyz78LZC69whBMPIKCMbi3cw==", + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-10.3.1.tgz", + "integrity": "sha512-az8JbIYeN/1iLj2t0jR9DV48/LQ3RC6hZPpapKPkb84Q+yTidMCpgWxIT3N0flnBDilyBQ1luWNpOeJptjdp/g==", "dev": true, "dependencies": { "colorette": "^2.0.7", "dateformat": "^4.6.3", "fast-copy": "^3.0.0", "fast-safe-stringify": "^2.1.1", - "help-me": "^4.0.1", + "help-me": "^5.0.0", "joycon": "^3.1.1", "minimist": "^1.2.6", "on-exit-leak-free": "^2.1.0", @@ -3924,31 +3902,6 @@ "pino-pretty": "bin.js" } }, - "node_modules/pino-pretty/node_modules/readable-stream": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz", - "integrity": "sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==", - "dev": true, - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/pino-pretty/node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, "node_modules/pino-std-serializers": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", @@ -3978,9 +3931,9 @@ "dev": true }, "node_modules/process-warning": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.2.0.tgz", - "integrity": "sha512-/1WZ8+VQjR6avWOgHeEPd7SDQmFQ1B5mC1eRXsCm5TarlNmx/wCsa5GEaxGm05BORRtyG/Ex/3xq3TuRvq57qg==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==" }, "node_modules/prom-client": { "version": "14.2.0", @@ -4022,9 +3975,9 @@ } }, "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "engines": { "node": ">=6" @@ -4102,32 +4055,20 @@ } }, "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/readable-stream/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true - }, - "node_modules/readable-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -4149,15 +4090,15 @@ } }, "node_modules/redis": { - "version": "4.6.10", - "resolved": "https://registry.npmjs.org/redis/-/redis-4.6.10.tgz", - "integrity": "sha512-mmbyhuKgDiJ5TWUhiKhBssz+mjsuSI/lSZNPI9QvZOYzWvYGejtb+W3RlDDf8LD6Bdl5/mZeG8O1feUGhXTxEg==", + "version": "4.6.12", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.6.12.tgz", + "integrity": "sha512-41Xuuko6P4uH4VPe5nE3BqXHB7a9lkFL0J29AlxKaIfD6eWO8VO/5PDF9ad2oS+mswMsfFxaM5DlE3tnXT+P8Q==", "dependencies": { "@redis/bloom": "1.2.0", - "@redis/client": "1.5.11", - "@redis/graph": "1.1.0", + "@redis/client": "1.5.13", + "@redis/graph": "1.1.1", "@redis/json": "1.0.6", - "@redis/search": "1.1.5", + "@redis/search": "1.1.6", "@redis/time-series": "1.0.5" } }, @@ -4216,9 +4157,9 @@ } }, "node_modules/resolve": { - "version": "1.22.6", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz", - "integrity": "sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==", + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dev": true, "dependencies": { "is-core-module": "^2.13.0", @@ -4444,6 +4385,20 @@ "node": ">= 0.8.0" } }, + "node_modules/set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "dependencies": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/set-function-name": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", @@ -4513,6 +4468,7 @@ "version": "12.0.1", "resolved": "https://registry.npmjs.org/sinon/-/sinon-12.0.1.tgz", "integrity": "sha512-iGu29Xhym33ydkAT+aNQFBINakjq69kKO6ByPvTsm3yyIACfyQttRTP03aBP/I8GfhFmLzrnKwNNkr0ORb1udg==", + "deprecated": "16.1.1", "dev": true, "dependencies": { "@sinonjs/commons": "^1.8.3", @@ -4528,9 +4484,9 @@ } }, "node_modules/sonic-boom": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.7.0.tgz", - "integrity": "sha512-IudtNvSqA/ObjN97tfgNmOKyDOs4dNcg4cUUsHDebqsgb8wGBBwb31LIgShNO8fye0dFI52X1+tFoKKI6Rq1Gg==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.8.0.tgz", + "integrity": "sha512-ybz6OYOUjoQQCQ/i4LU8kaToD8ACtYP+Cj5qd2AO36bwbdewxWJ3ArmJ2cr6AvxlL2o0PqnCcPGUgkILbfkaCA==", "dependencies": { "atomic-sleep": "^1.0.0" } @@ -4542,9 +4498,9 @@ "dev": true }, "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", "dev": true, "dependencies": { "spdx-exceptions": "^2.1.0", @@ -4552,9 +4508,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.15.tgz", - "integrity": "sha512-lpT8hSQp9jAKp9mhtBU4Xjon8LPGBvLIuBiSVhMEtmLecTh2mO0tlqrAMp47tBXzMr13NJMQ2lf7RpQGLJ3HsQ==", + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz", + "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==", "dev": true }, "node_modules/split2": { @@ -4579,20 +4535,13 @@ } }, "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "dependencies": { - "safe-buffer": "~5.1.0" + "safe-buffer": "~5.2.0" } }, - "node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -4722,12 +4671,48 @@ "ms": "^2.1.1" } }, + "node_modules/superagent/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, "node_modules/superagent/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, + "node_modules/superagent/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/superagent/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/superagent/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/supertest": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/supertest/-/supertest-3.4.2.tgz", @@ -4820,9 +4805,9 @@ } }, "node_modules/tsconfig-paths": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", - "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", "dev": true, "dependencies": { "@types/json5": "^0.0.29", @@ -5043,9 +5028,9 @@ } }, "node_modules/web-streams-polyfill": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", - "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.2.tgz", + "integrity": "sha512-3pRGuxRF5gpuZc0W+EpwQRmCD7gRqcDOMt688KmdlDAgAyaB1XlN0zq2njfDNm44XVdIouE7pZ6GzbdyH47uIQ==", "engines": { "node": ">= 8" } @@ -5082,13 +5067,13 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", - "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", + "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", "dev": true, "dependencies": { "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", + "call-bind": "^1.0.4", "for-each": "^0.3.3", "gopd": "^1.0.1", "has-tostringtag": "^1.0.0" diff --git a/package.json b/package.json index 870943a..0b1d393 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "webhooks" ], "dependencies": { - "bullmq": "^4.11.4", + "bullmq": "4.17.0", "config": "^3.3.7", "express": "^4.18.2", "js-yaml": "^4.1.0", From fea98d5969050cec792b2a5890f58fd91ce57872 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Tue, 9 Jan 2024 13:42:37 +0000 Subject: [PATCH 140/154] 3.0.0-beta.3 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0f704dd..77706db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "bbb-webhooks", - "version": "3.0.0-beta.2", + "version": "3.0.0-beta.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bbb-webhooks", - "version": "3.0.0-beta.2", + "version": "3.0.0-beta.3", "dependencies": { "bullmq": "4.17.0", "config": "^3.3.7", diff --git a/package.json b/package.json index 0b1d393..214764e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bbb-webhooks", - "version": "3.0.0-beta.2", + "version": "3.0.0-beta.3", "description": "A BigBlueButton mudule for events WebHooks", "type": "module", "scripts": { From 89d36eac107a78f4af5ff97468084c59bf96640d Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Tue, 9 Jan 2024 22:24:50 +0000 Subject: [PATCH 141/154] build: set .nvmrc to lts/iron (Node.js 20) --- .nvmrc | 2 +- CHANGELOG.md | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.nvmrc b/.nvmrc index a77793e..9de2256 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -lts/hydrogen +lts/iron diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b2c98b..72f9581 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to this project will be documented in this file. +### v3.0.0-beta.4 (UNRELEASED) + +* build: set .nvmrc to lts/iron (Node.js 20) + ### v3.0.0-beta.3 * build: bullmq@4.17.0, bump transitive deps From 6d3c8f0014c30054a719c4a23cef047db396aeec Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Tue, 9 Jan 2024 20:01:39 -0300 Subject: [PATCH 142/154] fix: pick up mocha configs via new .mocharc.yml file --- CHANGELOG.md | 1 + package.json | 6 +++--- test/.mocharc.yml | 1 + test/mocha.opts | 1 - 4 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 test/.mocharc.yml delete mode 100644 test/mocha.opts diff --git a/CHANGELOG.md b/CHANGELOG.md index 72f9581..67cdc2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file. ### v3.0.0-beta.4 (UNRELEASED) +* fix: pick up mocha configs via new .mocharc.yml file * build: set .nvmrc to lts/iron (Node.js 20) ### v3.0.0-beta.3 diff --git a/package.json b/package.json index 214764e..b13a0ab 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,9 @@ "scripts": { "start": "node app.js", "dev-start": "nodemon --watch src --ext js,json,yml,yaml --exec node app.js", - "test": "LOG_LEVEL=silent ALL_TESTS=true ALLOW_CONFIG_MUTATIONS=true mocha --exit", - "test:webhooks": "LOG_LEVEL=silent ALL_TESTS=false ALLOW_CONFIG_MUTATIONS=true WEBHOOKS_SUITE=true mocha --exit", - "test:xapi": "LOG_LEVEL=silent ALL_TESTS=false ALLOW_CONFIG_MUTATIONS=true XAPI_SUITE=true mocha --exit", + "test": "LOG_LEVEL=silent ALL_TESTS=true ALLOW_CONFIG_MUTATIONS=true mocha --config=test/.mocharc.yml --exit", + "test:webhooks": "LOG_LEVEL=silent ALL_TESTS=false ALLOW_CONFIG_MUTATIONS=true WEBHOOKS_SUITE=true mocha --config=test/.mocharc.yml --exit", + "test:xapi": "LOG_LEVEL=silent ALL_TESTS=false ALLOW_CONFIG_MUTATIONS=true XAPI_SUITE=true mocha --config=test/.mocharc.yml --exit", "lint": "./node_modules/.bin/eslint ./", "lint:file": "./node_modules/.bin/eslint", "jsdoc": "./node_modules/.bin/jsdoc app.js application.js src/ -r" diff --git a/test/.mocharc.yml b/test/.mocharc.yml new file mode 100644 index 0000000..c82ed8e --- /dev/null +++ b/test/.mocharc.yml @@ -0,0 +1 @@ +timeout: '5000' diff --git a/test/mocha.opts b/test/mocha.opts deleted file mode 100644 index cf80ee7..0000000 --- a/test/mocha.opts +++ /dev/null @@ -1 +0,0 @@ ---timeout 5000 From 08b862c31228a494dffb052e7ba564aff3ec1022 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Tue, 9 Jan 2024 20:48:01 -0300 Subject: [PATCH 143/154] fix(test): use redisUrl for node-redis client configuration --- CHANGELOG.md | 3 ++- test/index.js | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67cdc2f..0fd5e59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,8 @@ All notable changes to this project will be documented in this file. ### v3.0.0-beta.4 (UNRELEASED) -* fix: pick up mocha configs via new .mocharc.yml file +* fix(test): use redisUrl for node-redis client configuration +* fix(test): pick up mocha configs via new .mocharc.yml file * build: set .nvmrc to lts/iron (Node.js 20) ### v3.0.0-beta.3 diff --git a/test/index.js b/test/index.js index da7ca56..1f5e5e5 100644 --- a/test/index.js +++ b/test/index.js @@ -17,10 +17,10 @@ const TEST_CHANNEL = 'test-channel'; describe('bbb-webhooks test suite', () => { const application = new Application(); + const { host, port, password } = config.get('redis'); + const redisUrl = `redis://${password ? `:${password}@` : ''}${host}:${port}`; const redisClient = redis.createClient({ - host: config.get('redis.host'), - port: config.get('redis.port'), - password: config.has('redis.password') ? config.get('redis.password') : undefined, + url: redisUrl, }); before((done) => { From 82e80a1c9dad67f7cc7cfe43320b38cbb0dd5530 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Wed, 10 Jan 2024 14:01:43 +0000 Subject: [PATCH 144/154] !fix(webhooks): remove general getRaw configuration There's an unused global getRaw configuration in the webhooks module that conflicted with the hook-specific getRaw parameter. It superseded the specific configuration, which caused inconsistencies depending on base configuration being used. Remove the general getRaw configuration since it's unused and doesn't really make sense - and fix the inconsistency as a side effect. --- CHANGELOG.md | 1 + config/default.example.yml | 2 -- src/out/webhooks/web-hooks.js | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fd5e59..4bb0b3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file. ### v3.0.0-beta.4 (UNRELEASED) +* !fix(webhooks): remove general getRaw configuration * fix(test): use redisUrl for node-redis client configuration * fix(test): pick up mocha configs via new .mocharc.yml file * build: set .nvmrc to lts/iron (Node.js 20) diff --git a/config/default.example.yml b/config/default.example.yml index f18d92a..50f5b33 100644 --- a/config/default.example.yml +++ b/config/default.example.yml @@ -99,8 +99,6 @@ modules: permanentURLs: [] # How many messages will be enqueued to be processed at the same time queueSize: 10000 - # Allow permanent hooks to receive raw message, which is the message straight from BBB - getRaw: true # If set to higher than 1, will send events on the format: # "event=[{event1},{event2}],timestamp=000" or "[{event1},{event2}]" (based on using auth2_0 or not) # when there are more than 1 event on the queue at the moment of processing the queue. diff --git a/src/out/webhooks/web-hooks.js b/src/out/webhooks/web-hooks.js index 41a8041..1ef53fa 100644 --- a/src/out/webhooks/web-hooks.js +++ b/src/out/webhooks/web-hooks.js @@ -39,7 +39,7 @@ class WebHooks { * @private */ _processRaw(hook, rawEvent) { - if (hook == null || !hook?.payload?.getRaw || !this.config.getRaw) return Promise.resolve(); + if (hook == null || !hook?.payload?.getRaw) return Promise.resolve(); this.logger.info('dispatching raw event to hook', { callbackURL: hook.payload.callbackURL }); From 4045c0e33468d4aaf47c6f32da73eff8f50327e4 Mon Sep 17 00:00:00 2001 From: Felipe Cecagno Date: Tue, 9 Jan 2024 19:05:44 -0300 Subject: [PATCH 145/154] chore: support internal_meeting_id != record_id on rap events --- src/process/event.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/process/event.js b/src/process/event.js index 7349457..897e595 100644 --- a/src/process/event.js +++ b/src/process/event.js @@ -367,9 +367,10 @@ export default class WebhooksEvent { "id": this.mapInternalMessage(messageObj), "attributes": { "meeting": { - "internal-meeting-id": data.recordId, + "internal-meeting-id": data.internalMeetingId, "external-meeting-id": IDMapping.get().getExternalMeetingID(data.recordId) - } + }, + "record-id": data.recordId }, "event": { "ts": Date.now() From 2f0c4a6b73e7bce62fa4b6b82bdf9e1d7c3834a7 Mon Sep 17 00:00:00 2001 From: Felipe Cecagno Date: Tue, 9 Jan 2024 19:05:57 -0300 Subject: [PATCH 146/154] chore: remove unused events --- src/process/event.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/process/event.js b/src/process/event.js index 897e595..24b9155 100644 --- a/src/process/event.js +++ b/src/process/event.js @@ -44,9 +44,6 @@ export default class WebhooksEvent { "rap-post-process-ended", "rap-publish-started", "rap-publish-ended", - "rap-published", - "rap-unpublished", - "rap-deleted", "rap-post-publish-started", "rap-post-publish-ended", "poll-started", From d698aa6efe219f527bd36c19de17a8bbcb5f9fc6 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Wed, 10 Jan 2024 14:10:54 +0000 Subject: [PATCH 147/154] chore: update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bb0b3f..9514b83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file. ### v3.0.0-beta.4 (UNRELEASED) +* chore: remove unused events + * `rap-published`, `rap-unpublished`, `rap-deleted` +* chore: support internal_meeting_id != record_id on rap events * !fix(webhooks): remove general getRaw configuration * fix(test): use redisUrl for node-redis client configuration * fix(test): pick up mocha configs via new .mocharc.yml file From 53ae3a46c645e16d891092555a956ab724d3a62f Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Tue, 16 Jan 2024 14:41:53 -0300 Subject: [PATCH 148/154] fix: use ISO timestamps in production logs --- CHANGELOG.md | 1 + src/common/logger.js | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9514b83..985b985 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file. ### v3.0.0-beta.4 (UNRELEASED) +* fix: use ISO timestamps in production logs * chore: remove unused events * `rap-published`, `rap-unpublished`, `rap-deleted` * chore: support internal_meeting_id != record_id on rap events diff --git a/src/common/logger.js b/src/common/logger.js index 5fb7680..5ed009d 100644 --- a/src/common/logger.js +++ b/src/common/logger.js @@ -94,6 +94,7 @@ const _newLogger = ({ level, hooks, redact: ['config.server.secret'], + timestamp: pino.stdTimeFunctions.isoTime, }, targets); return logger; From ff0cbf56eee07f0e1569194ecbcc7ec33fba8a04 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Thu, 18 Jan 2024 17:41:54 -0300 Subject: [PATCH 149/154] 3.0.0-beta.4 --- CHANGELOG.md | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 985b985..3fb566c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to this project will be documented in this file. -### v3.0.0-beta.4 (UNRELEASED) +### v3.0.0-beta.4 * fix: use ISO timestamps in production logs * chore: remove unused events diff --git a/package-lock.json b/package-lock.json index 77706db..8dc88d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "bbb-webhooks", - "version": "3.0.0-beta.3", + "version": "3.0.0-beta.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bbb-webhooks", - "version": "3.0.0-beta.3", + "version": "3.0.0-beta.4", "dependencies": { "bullmq": "4.17.0", "config": "^3.3.7", diff --git a/package.json b/package.json index b13a0ab..62504e6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bbb-webhooks", - "version": "3.0.0-beta.3", + "version": "3.0.0-beta.4", "description": "A BigBlueButton mudule for events WebHooks", "type": "module", "scripts": { From 842b78ee4c9c2914c20891146740caa53459b77c Mon Sep 17 00:00:00 2001 From: Felipe Cecagno Date: Tue, 9 Jan 2024 21:04:25 -0300 Subject: [PATCH 150/154] feat: pipelines with GitHub Actions chore: remove unneeded bash installation from container feature: add pipeline to run the tests feature: add hadolint to build pipeline fix: copy configs for testing chore: split jobs chore: remove build cache chore: update build-push-action to v5 fix: unauthorized to push image fix: syntax feature: trivy pipeline fix: provide write access to trivy to upload the vulnerability report chore: add cache to build chore: update permissions and severity to trivy report fix: adjust cache chore: add trivy scanner to docker image as well fix: set trivy credentials fix: trivy format chore: activate cache chore: try to use github action cache for the image refactor: .dockerignore refactor: updates to build cache, image labels and tags, and conditions when the workflows run fix: make workflows reusable fix: yaml syntax fix: step name key chore: only build and push after hadolint and tests fix: add permission to write comments on issues/pr fix: adjust permissions chore: do not run workflows on release, only on tag fix: do not try to add comment to pr if not pr chore: add docs for the workflows refactor: change action to add pr comment which doesn't duplicate the same content Revert "refactor: change action to add pr comment which doesn't duplicate the same content" This reverts commit 40424533041ba2ac4b4b52dade51dc49f22c835f. chore: update comment message --- .dockerignore | 17 +++- .github/workflows/README.md | 13 +++ .github/workflows/docker-image.yml | 100 ++++++++++++++++++++++++ .github/workflows/docker-lint.yml | 19 +++++ .github/workflows/docker-scan.yml | 30 +++++++ .github/workflows/docker-tests.yml | 52 ++++++++++++ Dockerfile | 2 - config/custom-environment-variables.yml | 2 + 8 files changed, 229 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/README.md create mode 100644 .github/workflows/docker-image.yml create mode 100644 .github/workflows/docker-lint.yml create mode 100644 .github/workflows/docker-scan.yml create mode 100644 .github/workflows/docker-tests.yml diff --git a/.dockerignore b/.dockerignore index 5eeabec..c6e6657 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,8 +1,17 @@ +.dockerignore +.env .git/ -node_modules/ +.github/ +.gitignore +.nvmrc +*~ +*log.* *swn *swo *swp -*~ -*log.* -.env +docker-compose.yaml +Dockerfile +example/ +extra/ +node_modules/ +test/ diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 0000000..6b402ff --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,13 @@ +Add the following secrets to the GitHub repo: +``` +REGISTRY_USERNAME +REGISTRY_TOKEN +``` +They are the credentials to be used to push the image to the docker images registry. + +Add the following variables to the GitHub repo: +``` +REGISTRY_URI +REGISTRY_ORGANIZATION +``` +Considering the image `bigbluebutton/bbb-webhooks:v3.0.0`, the value for `REGISTRY_URI` would be `docker.io` (URI for DockerHub) and `REGISTRY_ORGANIZATION` would be `bigbluebutton`. The image name `bbb-webhooks` isn't configurable, and the tag will be the GitHub tag OR `pr-`. diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 0000000..26b73e7 --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -0,0 +1,100 @@ +name: Build and push image to registry +on: + pull_request: + types: + - synchronize + push: + tags: + - '*' +permissions: + contents: read +jobs: + hadolint: + uses: ./.github/workflows/docker-lint.yml + + tests: + uses: ./.github/workflows/docker-tests.yml + + build: + permissions: + contents: read # for actions/checkout to fetch code + security-events: write # for github/codeql-action/upload-sarif to upload SARIF results + actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status + pull-requests: write + name: Build and push + runs-on: ubuntu-22.04 + needs: + - hadolint + - tests + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + registry: ${{ vars.REGISTRY_URI }} + username: ${{ secrets.REGISTRY_USERNAME }} + password: ${{ secrets.REGISTRY_TOKEN }} + + - uses: rlespinasse/github-slug-action@v4 + + - name: Calculate tag + id: tag + run: | + if [ "$GITHUB_EVENT_NAME" == "pull_request" ]; then + TAG="pr-${{ github.event.number }}" + else + TAG=${{ github.ref_name }} + fi + echo "IMAGE=${{ vars.REGISTRY_URI }}/${{ vars.REGISTRY_ORGANIZATION }}/bbb-webhooks:$TAG" >> $GITHUB_OUTPUT + + - name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + images: ${{ steps.tag.outputs.IMAGE }} + + - name: Build and push image + uses: docker/build-push-action@v5 + with: + push: true + tags: ${{ steps.tag.outputs.IMAGE }} + context: . + platforms: linux/amd64 + cache-from: type=registry,ref=${{ steps.tag.outputs.IMAGE }} + cache-to: type=registry,ref=${{ steps.tag.outputs.IMAGE }},image-manifest=true,oci-mediatypes=true,mode=max + labels: | + ${{ steps.meta.outputs.labels }} + + - name: Add comment to pr + if: ${{ github.event_name == 'pull_request' }} + uses: actions/github-script@v7 + with: + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: "Updated Docker image pushed to `${{ steps.tag.outputs.IMAGE }}`" + }) + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + image-ref: ${{ steps.tag.outputs.IMAGE }} + format: 'sarif' + output: 'trivy-results.sarif' + severity: 'CRITICAL,HIGH' + env: + TRIVY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} + TRIVY_PASSWORD: ${{ secrets.REGISTRY_TOKEN }} + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: 'trivy-results.sarif' diff --git a/.github/workflows/docker-lint.yml b/.github/workflows/docker-lint.yml new file mode 100644 index 0000000..eb58884 --- /dev/null +++ b/.github/workflows/docker-lint.yml @@ -0,0 +1,19 @@ +name: Run hadolint +on: + workflow_dispatch: + workflow_call: +permissions: + contents: read +jobs: + hadolint: + name: Run hadolint check + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v3 + + # TODO add hadolint output as comment on PR + # https://github.com/hadolint/hadolint-action#output + - uses: hadolint/hadolint-action@v3.1.0 + with: + dockerfile: Dockerfile diff --git a/.github/workflows/docker-scan.yml b/.github/workflows/docker-scan.yml new file mode 100644 index 0000000..e06e4b1 --- /dev/null +++ b/.github/workflows/docker-scan.yml @@ -0,0 +1,30 @@ +name: Run trivy on filesystem +on: + workflow_dispatch: +permissions: + contents: read +jobs: + trivy: + permissions: + contents: read # for actions/checkout to fetch code + security-events: write # for github/codeql-action/upload-sarif to upload SARIF results + actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status + name: Run trivy check + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v3 + + - name: Run Trivy vulnerability scanner in repo mode + uses: aquasecurity/trivy-action@master + with: + scan-type: 'fs' + ignore-unfixed: true + format: 'sarif' + output: 'trivy-results.sarif' + severity: 'CRITICAL,HIGH' + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: 'trivy-results.sarif' diff --git a/.github/workflows/docker-tests.yml b/.github/workflows/docker-tests.yml new file mode 100644 index 0000000..b9ccf10 --- /dev/null +++ b/.github/workflows/docker-tests.yml @@ -0,0 +1,52 @@ +name: Run tests +on: + workflow_dispatch: + workflow_call: +permissions: + contents: read +jobs: + tests: + name: Run tests + # https://docs.github.com/en/actions/using-containerized-services/creating-redis-service-containers#running-jobs-in-containers + # Containers must run in Linux based operating systems + runs-on: ubuntu-22.04 + # Docker Hub image that `container-job` executes in + container: node:20-alpine + + # Service containers to run with `container-job` + services: + # Label used to access the service container + redis: + # Docker Hub image + image: redis + # Set health checks to wait until redis has started + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + # Downloads a copy of the code in your repository before running CI tests + - name: Check out repository code + uses: actions/checkout@v4 + + # Performs a clean installation of all dependencies in the `package.json` file + # For more information, see https://docs.npmjs.com/cli/ci.html + - name: Install dependencies + run: npm ci + + - name: Copy config + run: cp config/default.example.yml config/default.yml + + - name: Run tests + # Runs a script that creates a Redis client, populates + # the client with data, and retrieves data + run: npm run test + # Environment variable used by the `client.js` script to create a new Redis client. + env: + # The hostname used to communicate with the Redis service container + REDIS_HOST: redis + # The default Redis port + REDIS_PORT: 6379 + XAPI_ENABLED: true diff --git a/Dockerfile b/Dockerfile index ec17206..c9d88b0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,5 @@ FROM node:20-alpine -RUN apk add --no-cache bash - WORKDIR /app COPY package.json package-lock.json ./ diff --git a/config/custom-environment-variables.yml b/config/custom-environment-variables.yml index 18582e2..78b4ed7 100644 --- a/config/custom-environment-variables.yml +++ b/config/custom-environment-variables.yml @@ -57,3 +57,5 @@ modules: requestTimeout: __name: REQUEST_TIMEOUT __format: json + ../out/xapi/index.js: + enabled: XAPI_ENABLED From 1c37608867ea179e7632a87b3b446de375e37385 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Thu, 25 Jan 2024 20:12:21 -0300 Subject: [PATCH 151/154] 3.0.0-beta.5 --- CHANGELOG.md | 4 ++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fb566c..57e282e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to this project will be documented in this file. +### v3.0.0-beta.5 + +* feat: pipelines with GitHub Actions + ### v3.0.0-beta.4 * fix: use ISO timestamps in production logs diff --git a/package-lock.json b/package-lock.json index 8dc88d6..6405206 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "bbb-webhooks", - "version": "3.0.0-beta.4", + "version": "3.0.0-beta.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bbb-webhooks", - "version": "3.0.0-beta.4", + "version": "3.0.0-beta.5", "dependencies": { "bullmq": "4.17.0", "config": "^3.3.7", diff --git a/package.json b/package.json index 62504e6..fc81474 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bbb-webhooks", - "version": "3.0.0-beta.4", + "version": "3.0.0-beta.5", "description": "A BigBlueButton mudule for events WebHooks", "type": "module", "scripts": { From b82ee9ad54c79de5822b78ddbe26364642af741a Mon Sep 17 00:00:00 2001 From: Felipe Cecagno Date: Fri, 26 Jan 2024 12:17:32 -0300 Subject: [PATCH 152/154] chore: update github-slug-action to latest version --- .github/workflows/docker-image.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 26b73e7..3525493 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -41,7 +41,7 @@ jobs: username: ${{ secrets.REGISTRY_USERNAME }} password: ${{ secrets.REGISTRY_TOKEN }} - - uses: rlespinasse/github-slug-action@v4 + - uses: rlespinasse/github-slug-action@v4.4.1 - name: Calculate tag id: tag From dc416c878eed8a6d9fcab7dcf7e585dc52a22087 Mon Sep 17 00:00:00 2001 From: Felipe Cecagno Date: Fri, 26 Jan 2024 12:33:11 -0300 Subject: [PATCH 153/154] fix: adjust action triggers for pr --- .github/workflows/docker-image.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 26b73e7..2df2420 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -2,6 +2,8 @@ name: Build and push image to registry on: pull_request: types: + - opened + - reopened - synchronize push: tags: From 4ac79f2176f13191d5c7049c9fed4c60455f0e9d Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Mon, 29 Jan 2024 11:25:26 -0300 Subject: [PATCH 154/154] 3.0.0 --- CHANGELOG.md | 50 +++++++++++++++++++++++++++++++++++++++++++++-- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 51 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57e282e..845bc36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,52 @@ All notable changes to this project will be documented in this file. +### v3.0.0 + +#### Changelog since v3.0.0-beta.5 + +* chore: update github-slug-actions to v4.4.1 +* fix: adjust actions triggers for pr (+ opened, reopened) + +#### Changelog since v2.6.1 + +* feat: new xAPI output module with support for multitenancy + - Implements https://github.com/gaia-x-dases/xapi-virtual-classroom + - For more information: (README.md)[src/out/xapi/README.md] +* feat(xapi): add suport for meta_xapi-create-end-actor-name +* feat(webhooks): implement includeEvents/excludeEvents +* feat(events): add support for poll events +* feat(events): add support for raise-hand events +* feat(events): add support for emoji events +* feat(events): add user info to screenshare events +* feat(events): add support for audio muted/unmuted events +* feat: support internal_meeting_id != record_id on rap events +* feat: add Prometheus instrumentation +* feat: add JSDoc annotations to most of the codebase +* feat: log to file +* feat: add support for multiple checksum algorithms (SHA1,...,SHA512) +* feat(test): add support for modular test suites +* feat(test): add xAPI test suite +* feat: pipelines with GitHub Actions +* !refactor: application rewritten to use a modular input/processing/ouput system +* !refactor: modernize codebase (ES6 imports, Node.js >= 18 etc.) +* !refactor(webhooks): the webhooks functionality was rewritten into an output module +* !refactor(webhooks): hook IDs are now UUIDs instead of integers +* !refactor: new logging system (using Pino) +* !refactor: migrate node-redis from v3 to v4 +* !refactor: new queue system (using Bullmq) +* refactor(test): remove nock as a dependency +* refactor(webhooks): replace request with node-fetch +* refactor: replace sha1 dependency with native code +* refactor: remove unused events + * `rap-published`, `rap-unpublished`, `rap-deleted` +* !fix(webhooks): remove general getRaw configuration +* fix(events): user-left events are now emitted for trailing users on meeting-ended events +* fix(test): restore remaining out/webhooks tests +* fix: add Redis disconnection handling +* build: add docker-compose and updated Dockerfile examples +* build: set .nvmrc to lts/iron (Node.js 20) + ### v3.0.0-beta.5 * feat: pipelines with GitHub Actions @@ -9,9 +55,9 @@ All notable changes to this project will be documented in this file. ### v3.0.0-beta.4 * fix: use ISO timestamps in production logs -* chore: remove unused events +* refactor: remove unused events * `rap-published`, `rap-unpublished`, `rap-deleted` -* chore: support internal_meeting_id != record_id on rap events +* feat: support internal_meeting_id != record_id on rap events * !fix(webhooks): remove general getRaw configuration * fix(test): use redisUrl for node-redis client configuration * fix(test): pick up mocha configs via new .mocharc.yml file diff --git a/package-lock.json b/package-lock.json index 6405206..f34a730 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "bbb-webhooks", - "version": "3.0.0-beta.5", + "version": "3.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bbb-webhooks", - "version": "3.0.0-beta.5", + "version": "3.0.0", "dependencies": { "bullmq": "4.17.0", "config": "^3.3.7", diff --git a/package.json b/package.json index fc81474..c4895bc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bbb-webhooks", - "version": "3.0.0-beta.5", + "version": "3.0.0", "description": "A BigBlueButton mudule for events WebHooks", "type": "module", "scripts": {