diff --git a/bigbluebutton-html5/imports/api/1.1/chat/server/methods.js b/bigbluebutton-html5/imports/api/1.1/chat/server/methods.js index 940de17c0cbb3626ba986cad9b6066b6987d45c0..6ce8893022abca29a65e3c1d79edb2c6aaac9f6b 100644 --- a/bigbluebutton-html5/imports/api/1.1/chat/server/methods.js +++ b/bigbluebutton-html5/imports/api/1.1/chat/server/methods.js @@ -1,7 +1,3 @@ -import mapToAcl from '/imports/startup/mapToAcl'; import { Meteor } from 'meteor/meteor'; -import sendChat from './methods/sendChat'; -Meteor.methods(mapToAcl(['methods.sendChat'], { - sendChat, -})); +Meteor.methods({}); diff --git a/bigbluebutton-html5/imports/api/2.0/chat/index.js b/bigbluebutton-html5/imports/api/2.0/chat/index.js new file mode 100755 index 0000000000000000000000000000000000000000..19531e9c47309b698db8be80cbc09714157f3ccd --- /dev/null +++ b/bigbluebutton-html5/imports/api/2.0/chat/index.js @@ -0,0 +1 @@ +export default new Mongo.Collection('chat2x'); diff --git a/bigbluebutton-html5/imports/api/2.0/chat/server/eventHandlers.js b/bigbluebutton-html5/imports/api/2.0/chat/server/eventHandlers.js new file mode 100644 index 0000000000000000000000000000000000000000..4e66deea9e0177ca2457a6ba90cef6a6d0f9a3e9 --- /dev/null +++ b/bigbluebutton-html5/imports/api/2.0/chat/server/eventHandlers.js @@ -0,0 +1,7 @@ +import RedisPubSub from '/imports/startup/server/redis2x'; +import handleChatMessage from './handlers/chatMessage'; +import handleChatHistory from './handlers/chatHistory'; + +RedisPubSub.on('GetChatHistoryRespMsg', handleChatHistory); +RedisPubSub.on('SendPublicMessageEvtMsg', handleChatMessage); +RedisPubSub.on('SendPrivateMessageEvtMsg', handleChatMessage); diff --git a/bigbluebutton-html5/imports/api/2.0/chat/server/handlers/chatHistory.js b/bigbluebutton-html5/imports/api/2.0/chat/server/handlers/chatHistory.js new file mode 100644 index 0000000000000000000000000000000000000000..40293145a550463438d868f4b49047f9a551934e --- /dev/null +++ b/bigbluebutton-html5/imports/api/2.0/chat/server/handlers/chatHistory.js @@ -0,0 +1,17 @@ +import { check } from 'meteor/check'; +import addChat from '../modifiers/addChat'; + +export default function handleChatHistory({ body }, meetingId) { + const { history } = body; + + check(meetingId, String); + check(history, Array); + + const chatsAdded = []; + + history.forEach((message) => { + chatsAdded.push(addChat(meetingId, message)); + }); + + return chatsAdded; +} diff --git a/bigbluebutton-html5/imports/api/2.0/chat/server/handlers/chatMessage.js b/bigbluebutton-html5/imports/api/2.0/chat/server/handlers/chatMessage.js new file mode 100644 index 0000000000000000000000000000000000000000..4ee789871cce0ef06cb75b9e0bf8fd4e9f00db14 --- /dev/null +++ b/bigbluebutton-html5/imports/api/2.0/chat/server/handlers/chatMessage.js @@ -0,0 +1,11 @@ +import { check } from 'meteor/check'; +import addChat from '../modifiers/addChat'; + +export default function handleChatMessage({ body }, meetingId) { + const { message } = body; + + check(meetingId, String); + check(message, Object); + + return addChat(meetingId, message); +} diff --git a/bigbluebutton-html5/imports/api/2.0/chat/server/index.js b/bigbluebutton-html5/imports/api/2.0/chat/server/index.js new file mode 100644 index 0000000000000000000000000000000000000000..92451ac76bf27410726e8f3cd2eebac46cd7b83e --- /dev/null +++ b/bigbluebutton-html5/imports/api/2.0/chat/server/index.js @@ -0,0 +1,3 @@ +import './eventHandlers'; +import './methods'; +import './publishers'; diff --git a/bigbluebutton-html5/imports/api/2.0/chat/server/methods.js b/bigbluebutton-html5/imports/api/2.0/chat/server/methods.js new file mode 100644 index 0000000000000000000000000000000000000000..940de17c0cbb3626ba986cad9b6066b6987d45c0 --- /dev/null +++ b/bigbluebutton-html5/imports/api/2.0/chat/server/methods.js @@ -0,0 +1,7 @@ +import mapToAcl from '/imports/startup/mapToAcl'; +import { Meteor } from 'meteor/meteor'; +import sendChat from './methods/sendChat'; + +Meteor.methods(mapToAcl(['methods.sendChat'], { + sendChat, +})); diff --git a/bigbluebutton-html5/imports/api/2.0/chat/server/methods/sendChat.js b/bigbluebutton-html5/imports/api/2.0/chat/server/methods/sendChat.js new file mode 100755 index 0000000000000000000000000000000000000000..f1afbf068a0f343d827c11d610251dfb945e6911 --- /dev/null +++ b/bigbluebutton-html5/imports/api/2.0/chat/server/methods/sendChat.js @@ -0,0 +1,59 @@ +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; +import RedisPubSub from '/imports/startup/server/redis'; +import RegexWebUrl from '/imports/utils/regex-weburl'; + +const HTML_SAFE_MAP = { + '<': '<', + '>': '>', + '"': '"', + "'": ''', +}; + +const parseMessage = (message) => { + let parsedMessage = message || ''; + parsedMessage = parsedMessage.trim(); + + // Replace <br/> with \n\r + parsedMessage = parsedMessage.replace(/<br\s*[\\/]?>/gi, '\n\r'); + + // Sanitize. See: http://shebang.brandonmintern.com/foolproof-html-escaping-in-javascript/ + parsedMessage = parsedMessage.replace(/[<>'"]/g, c => HTML_SAFE_MAP[c]); + + // Replace flash links to flash valid ones + parsedMessage = parsedMessage.replace(RegexWebUrl, "<a href='event:$&'><u>$&</u></a>"); + + return parsedMessage; +}; + +export default function sendChat(credentials, message) { + const REDIS_CONFIG = Meteor.settings.redis; + const CHANNEL = REDIS_CONFIG.channels.toBBBApps.chat; + + const CHAT_CONFIG = Meteor.settings.public.chat; + const PUBLIC_CHAT_TYPE = CHAT_CONFIG.type_public; + + const { meetingId, requesterUserId, requesterToken } = credentials; + + check(meetingId, String); + check(requesterUserId, String); + check(requesterToken, String); + check(message, Object); + + let eventName = 'SendPrivateMessagePubMsg'; + const parsedMessage = message; + + parsedMessage.message = parseMessage(message.message); + + if (message.chat_type === PUBLIC_CHAT_TYPE) { + eventName = 'SendPublicMessagePubMsg'; + } + + const payload = { + parsedMessage, + meetingId, + requesterId: message.fromUserid, + }; + + return RedisPubSub.publish(CHANNEL, eventName, payload); +} diff --git a/bigbluebutton-html5/imports/api/2.0/chat/server/modifiers/addChat.js b/bigbluebutton-html5/imports/api/2.0/chat/server/modifiers/addChat.js new file mode 100755 index 0000000000000000000000000000000000000000..38b4aa3d9679f69607763906ab89b73b7592845f --- /dev/null +++ b/bigbluebutton-html5/imports/api/2.0/chat/server/modifiers/addChat.js @@ -0,0 +1,62 @@ +import Chat from '/imports/api/2.0/chat'; +import Logger from '/imports/startup/server/logger'; +import { check } from 'meteor/check'; +import { BREAK_LINE } from '/imports/utils/lineEndings'; + +const parseMessage = (message) => { + let parsedMessage = message || ''; + + // Replace \r and \n to <br/> + parsedMessage = parsedMessage.replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, `$1${BREAK_LINE}$2`); + + // Replace flash links to html valid ones + parsedMessage = parsedMessage.split('<a href=\'event:').join('<a target="_blank" href=\''); + parsedMessage = parsedMessage.split('<a href="event:').join('<a target="_blank" href="'); + + return parsedMessage; +}; + +export default function addChat(meetingId, message) { + const parsedMessage = parseMessage(message.message); + + const fromUserId = message.fromUserId; + const toUserId = message.toUserId; + + check(fromUserId, String); + check(toUserId, String); + + const selector = { + meetingId, + 'message.fromTime': message.fromTime, + 'message.fromUserId': message.fromUserId, + 'message.toUserId': message.toUserId, + }; + + const modifier = { + $set: { + meetingId, + message: { + message: parsedMessage, + toUsername: message.toUsername, + fromTimezoneOffset: message.fromTimezoneOffset, + fromColor: message.fromColor, + toUserId: message.toUserId, + fromUserId: message.fromUserId, + fromTime: message.fromTime, + fromUsername: message.fromUsername, + }, + }, + }; + + const cb = (err, numChanged) => { + if (err) { + return Logger.error(`Adding chat to collection: ${err}`); + } + + const { insertedId } = numChanged; + const to = message.toUsername || 'PUBLIC'; + return Logger.info(`Added chat id=${insertedId} from=${message.fromUsername} to=${to}`); + }; + + return Chat.upsert(selector, modifier, cb); +} diff --git a/bigbluebutton-html5/imports/api/2.0/chat/server/modifiers/clearChats.js b/bigbluebutton-html5/imports/api/2.0/chat/server/modifiers/clearChats.js new file mode 100755 index 0000000000000000000000000000000000000000..b2ecab2d591acbf6cf2e7f494d2e0126a6959402 --- /dev/null +++ b/bigbluebutton-html5/imports/api/2.0/chat/server/modifiers/clearChats.js @@ -0,0 +1,10 @@ +import Chat from '/imports/api/2.0/chat'; +import Logger from '/imports/startup/server/logger'; + +export default function clearChats(meetingId) { + if (meetingId) { + return Chat.remove({ meetingId }, Logger.info(`Cleared Chats (${meetingId})`)); + } + + return Chat.remove({}, Logger.info('Cleared Chats (all)')); +} diff --git a/bigbluebutton-html5/imports/api/2.0/chat/server/modifiers/clearUserSystemMessages.js b/bigbluebutton-html5/imports/api/2.0/chat/server/modifiers/clearUserSystemMessages.js new file mode 100644 index 0000000000000000000000000000000000000000..32053f04bda6f419731de405b93770ff90f62c63 --- /dev/null +++ b/bigbluebutton-html5/imports/api/2.0/chat/server/modifiers/clearUserSystemMessages.js @@ -0,0 +1,24 @@ +import Chat from '/imports/api/2.0/chat'; +import Logger from '/imports/startup/server/logger'; +import { check } from 'meteor/check'; + +/** + * Remove any system message from the user with userId. + * + * @param {string} meetingId + * @param {string} userId + */ +export default function clearUserSystemMessages(meetingId, userId) { + check(meetingId, String); + check(userId, String); + + const CHAT_CONFIG = Meteor.settings.public.chat; + + const selector = { + meetingId, + 'message.fromUserid': CHAT_CONFIG.type_system, + 'message.toUserid': userId, + }; + + return Chat.remove(selector, Logger.info(`Removing system messages from: (${userId})`)); +} diff --git a/bigbluebutton-html5/imports/api/2.0/chat/server/publishers.js b/bigbluebutton-html5/imports/api/2.0/chat/server/publishers.js new file mode 100644 index 0000000000000000000000000000000000000000..b005e9e845726df4ccbe224074340e714b8652aa --- /dev/null +++ b/bigbluebutton-html5/imports/api/2.0/chat/server/publishers.js @@ -0,0 +1,41 @@ +import Chat from '/imports/api/2.0/chat'; +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; +import Logger from '/imports/startup/server/logger'; + +import mapToAcl from '/imports/startup/mapToAcl'; + +function chat(credentials) { + const CHAT_CONFIG = Meteor.settings.public.chat; + const PUBLIC_CHAT_TYPE = CHAT_CONFIG.type_public; + + const { meetingId, requesterUserId, requesterToken } = credentials; + + check(meetingId, String); + check(requesterUserId, String); + check(requesterToken, String); + + Logger.info(`Publishing chat for ${meetingId} ${requesterUserId} ${requesterToken}`); + + return Chat.find({ + $or: [ + { + 'message.chatType': PUBLIC_CHAT_TYPE, + meetingId, + }, { + 'message.fromUserid': requesterUserId, + meetingId, + }, { + 'message.toUserid': requesterUserId, + meetingId, + }, + ], + }); +} + +function publish(...args) { + const boundChat = chat.bind(this); + return mapToAcl('subscriptions.chat', boundChat)(args); +} + +Meteor.publish('chat2x', publish); diff --git a/bigbluebutton-html5/imports/startup/client/base.jsx b/bigbluebutton-html5/imports/startup/client/base.jsx index 8fe54898f4bcebf4040ade8b219490e12920262c..12a4d3060b4f572815e0dd753845488292dfa5d8 100644 --- a/bigbluebutton-html5/imports/startup/client/base.jsx +++ b/bigbluebutton-html5/imports/startup/client/base.jsx @@ -84,7 +84,7 @@ Base.propTypes = propTypes; Base.defaultProps = defaultProps; const SUBSCRIPTIONS_NAME = [ - 'users2x', 'users', 'chat', 'cursor', 'cursor2x', 'deskshare', 'meetings', 'meetings2x', + 'users2x', 'users', 'chat', 'chat2x', 'cursor', 'cursor2x', 'deskshare', 'meetings', 'meetings2x', 'polls', 'presentations', 'presentations2x', 'shapes', 'shapes2x', 'slides', 'slides2x', 'captions', 'breakouts', ]; diff --git a/bigbluebutton-html5/imports/startup/server/redis2x.js b/bigbluebutton-html5/imports/startup/server/redis2x.js index a3b38b95e7ff25904fa9322dce63d9b8e7b3639d..bbb64c04434147a991a2e059ca938318c5065aca 100644 --- a/bigbluebutton-html5/imports/startup/server/redis2x.js +++ b/bigbluebutton-html5/imports/startup/server/redis2x.js @@ -105,7 +105,7 @@ class RedisPubSub2x { Logger.info(`2.0 QUEUE | PROGRESS ${this.queue.progress()}% | LENGTH ${this.queue.length()}} ${eventName} | CHANNEL ${channel}`); // We should only handle messages from this two channels, else, we simple ignore them. - if (channel !== fromAkkaApps || channel !== toHTML5) { + if (channel !== fromAkkaApps && channel !== toHTML5) { Logger.warn(`The following message was ignored: CHANNEL ${channel} MESSAGE ${message}`); return; } diff --git a/bigbluebutton-html5/server/main.js b/bigbluebutton-html5/server/main.js index 30fa511fcf34757bcd3b0f9c191957aaf2779605..98f14fdb955f3ca7d3b9c782933db11a3b34102b 100755 --- a/bigbluebutton-html5/server/main.js +++ b/bigbluebutton-html5/server/main.js @@ -18,6 +18,7 @@ import '/imports/api/2.0/shapes/server'; import '/imports/api/2.0/cursor/server'; import '/imports/api/2.0/presentations/server'; import '/imports/api/2.0/slides/server'; +import '/imports/api/2.0/chat/server'; // Commons import '/imports/api/log-client/server';