diff --git a/bbb-webhooks/application.js b/bbb-webhooks/application.js index 6b1e641e26443064677e3981f7e7fbc10d581767..32bf0bcfe006f4d8221ff6516230714f541c23ae 100644 --- a/bbb-webhooks/application.js +++ b/bbb-webhooks/application.js @@ -4,6 +4,7 @@ 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"); // Class that defines the application. Listens for events on redis and starts the // process to perform the callback calls. @@ -19,10 +20,12 @@ module.exports = class Application { start() { Hook.initialize(() => { - IDMapping.initialize(() => { - this.webServer.start(config.server.port); - this.webServer.createPermanent(); - this.webHooks.start(); + UserMapping.initialize(() => { + IDMapping.initialize(()=> { + this.webServer.start(config.server.port); + this.webServer.createPermanent(); + this.webHooks.start(); + }); }); }); } diff --git a/bbb-webhooks/config.js b/bbb-webhooks/config.js index 21de8e01ed5d574354cb93cd85633e76264e0137..e97a25bca48a50622642d33282e34ca43260dc03 100644 --- a/bbb-webhooks/config.js +++ b/bbb-webhooks/config.js @@ -44,6 +44,8 @@ config.redis.keys.hooks = "bigbluebutton:webhooks:hooks"; config.redis.keys.mappings = "bigbluebutton:webhooks:mappings"; config.redis.keys.mapping = id => `bigbluebutton:webhooks:mapping:${id}`; config.redis.keys.events = id => `bigbluebutton:webhooks:events:${id}`; +config.redis.keys.userMaps = "bigbluebutton:webhooks:userMaps"; +config.redis.keys.userMap = id => `bigbluebutton:webhooks:userMap:${id}`; config.api = {}; config.api.responses = {}; diff --git a/bbb-webhooks/id_mapping.js b/bbb-webhooks/id_mapping.js index 2c2ecb603952a514de2250dc4fffcbf0d389da59..f16d5fc18ce7a95cb0768083d145929bae11406c 100644 --- a/bbb-webhooks/id_mapping.js +++ b/bbb-webhooks/id_mapping.js @@ -4,6 +4,7 @@ const redis = require("redis"); const config = require("./config.js"); 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. @@ -163,7 +164,10 @@ module.exports = class IDMapping { const toRemove = _.filter(all, mapping => mapping.lastActivity < (now - config.mappings.timeout)); if (!_.isEmpty(toRemove)) { Logger.info("[IDMapping] expiring the mappings:", _.map(toRemove, map => map.print())); - toRemove.forEach(mapping => mapping.destroy()); + toRemove.forEach(mapping => { + UserMapping.removeMappingMeetingId(mapping.internalMeetingID); + mapping.destroy() + }); } } diff --git a/bbb-webhooks/userMapping.js b/bbb-webhooks/userMapping.js new file mode 100644 index 0000000000000000000000000000000000000000..6770b0ae6b530e1409661aac895fb88aa0834bca --- /dev/null +++ b/bbb-webhooks/userMapping.js @@ -0,0 +1,190 @@ +const _ = require("lodash"); +const async = require("async"); +const redis = require("redis"); + +const config = require("./config.js"); +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:<id>" 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.redisClient = config.redis.client; + } + + save(callback) { + this.redisClient.hmset(config.redis.keys.userMap(this.id), this.toRedis(), (error, reply) => { + if (error != null) { Logger.error("[UserMapping] error saving mapping to redis:", error, reply); } + this.redisClient.sadd(config.redis.keys.userMaps, this.id, (error, reply) => { + if (error != null) { Logger.error("[UserMapping] error saving mapping ID to the list of mappings:", error, reply); } + + db[this.internalUserID] = this; + (typeof callback === 'function' ? callback(error, db[this.internalUserID]) : undefined); + }); + }); + } + + destroy(callback) { + this.redisClient.srem(config.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.redis.keys.userMap(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 + }; + return r; + } + + fromRedis(redisData) { + this.id = parseInt(redisData.id); + this.externalUserID = redisData.externalUserID; + this.internalUserID = redisData.internalUserID; + this.meetingId = redisData.meetingId; + } + + print() { + return JSON.stringify(this.toRedis()); + } + + static addMapping(internalUserID, externalUserID, meetingId, callback) { + let mapping = new UserMapping(); + mapping.id = nextID++; + mapping.internalUserID = internalUserID; + mapping.externalUserID = externalUserID; + mapping.meetingId = meetingId; + 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()); + return (typeof callback === 'function' ? callback(error, result) : undefined); + })); + } else { + result.push(undefined); + } + } + callback(); + })(); + } + + 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 = config.redis.client; + let tasks = []; + + return client.smembers(config.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.redis.keys.userMap(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/bbb-webhooks/web_hooks.js b/bbb-webhooks/web_hooks.js index 8461e23c59e9a8745e9432b05f9cd0fcee0accdf..2744de96d228ca106c87e953d87f4a3c9e01540a 100644 --- a/bbb-webhooks/web_hooks.js +++ b/bbb-webhooks/web_hooks.js @@ -7,6 +7,7 @@ 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. @@ -38,27 +39,31 @@ module.exports = class WebHooks { messageMapped.mapMessage(JSON.parse(message)); message = messageMapped.mappedObject; if (!_.isEmpty(message) && !config.hooks.getRaw) { - const id = message.data.attributes.meeting["internal-meeting-id"]; - IDMapping.reportActivity(id); + const intId = message.data.attributes.meeting["internal-meeting-id"]; + IDMapping.reportActivity(intId); // First treat meeting events to add/remove ID mappings - if ((message.data != null ? message.data.id : undefined) === "meeting-created") { - Logger.info(`[WebHooks] got create message on meetings channel [${channel}]:`, message); - IDMapping.addOrUpdateMapping(message.data.attributes.meeting["internal-meeting-id"], message.data.attributes.meeting["external-meeting-id"], (error, result) => + 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() - ); - - // TODO: Temporarily commented because we still need the mapping for recording events, - // after the meeting ended. - // else if message.header?.name is "meeting_destroyed_event" - // Logger.info "[WebHooks] got destroy message on meetings channel [#{channel}]", message - // IDMapping.removeMapping message.payload?.meeting_id, (error, result) -> - // processMessage() - - } else { - processMessage(); + processMessage(); + }); + break; + case "user-joined": + UserMapping.addMapping(message.data.attributes.user["internal-user-id"],message.data.attributes.user["external-user-id"], intId, () => { + 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(); } } else { this._processRaw(raw);