Implement basic GroupChat messages. Closes #4987

This commit is contained in:
Oswaldo Acauan 2018-03-09 15:42:14 -03:00
parent 4a6b5e5e02
commit 8b9cc79c3d
27 changed files with 537 additions and 5 deletions

View File

@ -0,0 +1,19 @@
import { Meteor } from 'meteor/meteor';
const GroupChat = new Mongo.Collection('group-chat-msg');
if (Meteor.isServer) {
GroupChat._ensureIndex({
meetingId: 1, chatId: 1, access: 1, users: 1,
});
}
export default GroupChat;
export const CHAT_ACCESS = {
PUBLIC: 'PUBLIC_ACCESS',
PRIVATE: 'PRIVATE_ACCESS',
};
export const CHAT_ACCESS_PUBLIC = CHAT_ACCESS.PUBLIC;
export const CHAT_ACCESS_PRIVATE = CHAT_ACCESS.PRIVATE;

View File

@ -0,0 +1,6 @@
import RedisPubSub from '/imports/startup/server/redis';
import handleGroupChatsMsgs from './handlers/groupChatsMsgs';
import handleGroupChatMsgBroadcast from './handlers/groupChatMsgBroadcast';
RedisPubSub.on('GetGroupChatMsgsRespMsg', handleGroupChatsMsgs);
RedisPubSub.on('GroupChatMessageBroadcastEvtMsg', handleGroupChatMsgBroadcast);

View File

@ -0,0 +1,12 @@
import { check } from 'meteor/check';
import addGroupChatMsg from '../modifiers/addGroupChatMsg';
export default function handleGroupChatMsgBroadcast({ body }, meetingId) {
const { chatId, msg } = body;
check(meetingId, String);
check(chatId, String);
check(msg, Object);
return addGroupChatMsg(meetingId, chatId, msg);
}

View File

@ -0,0 +1,19 @@
import { Match, check } from 'meteor/check';
import addGroupChatMsg from '../modifiers/addGroupChatMsg';
export default function handleGroupChatsMsgs({ body }, meetingId) {
const { chatId, msgs, msg } = body;
check(meetingId, String);
check(chatId, String);
check(msgs, Match.Maybe(Array));
check(msg, Match.Maybe(Array));
const msgsAdded = [];
(msgs || msg).forEach((m) => {
msgsAdded.push(addGroupChatMsg(meetingId, chatId, m));
});
return msgsAdded;
}

View File

@ -0,0 +1,3 @@
import './eventHandlers';
import './methods';
import './publishers';

View File

@ -0,0 +1,7 @@
import { Meteor } from 'meteor/meteor';
import mapToAcl from '/imports/startup/mapToAcl';
import sendGroupChatMsg from './methods/sendGroupChatMsg';
Meteor.methods(mapToAcl(['methods.sendGroupChatMsg'], {
sendGroupChatMsg,
}));

View File

@ -0,0 +1,55 @@
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 = {
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
};
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 sendGroupChatMsg(credentials, chatId, message) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const { meetingId, requesterUserId, requesterToken } = credentials;
check(meetingId, String);
check(requesterUserId, String);
check(requesterToken, String);
check(message, Object);
const eventName = 'SendGroupChatMessageMsg';
const parsedMessage = parseMessage(message);
const payload = {
chatId,
// correlationId: `${Date.now()}`,
sender: {
id: requesterUserId,
name: '',
},
// color: '1',
message: parsedMessage,
};
return RedisPubSub.publishUserMessage(CHANNEL, eventName, meetingId, requesterUserId, payload);
}

View File

@ -0,0 +1,65 @@
import flat from 'flat';
import { Match, check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import GroupChatMsg from '/imports/api/group-chat-msg';
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 addGroupChatMsg(meetingId, chatId, msg) {
check(meetingId, String);
check(chatId, String);
check(msg, {
id: String,
timestamp: Number,
sender: Object,
color: String,
message: String,
correlationId: Match.Maybe(String),
});
const msgDocument = {
...msg,
meetingId,
chatId,
message: parseMessage(msg.message),
sender: msg.sender.id,
};
const selector = {
meetingId,
chatId,
id: msg.id,
};
const modifier = {
$set: flat(msgDocument, { safe: true }),
};
const cb = (err, numChanged) => {
if (err) {
return Logger.error(`Adding group-chat-msg to collection: ${err}`);
}
const { insertedId } = numChanged;
if (insertedId) {
return Logger.info(`Added group-chat-msg msgId=${msg.id} chatId=${chatId} meetingId=${meetingId}`);
}
return Logger.info(`Upserted group-chat-msg msgId=${msg.id} chatId=${chatId} meetingId=${meetingId}`);
};
return GroupChatMsg.upsert(selector, modifier, cb);
}

View File

@ -0,0 +1,14 @@
import GroupChatMsg from '/imports/api/group-chat-msg';
import Logger from '/imports/startup/server/logger';
export default function clearGroupChatMsg(meetingId, chatId) {
if (meetingId) {
return GroupChatMsg.remove({ meetingId }, Logger.info(`Cleared GroupChat (${meetingId})`));
}
if (chatId) {
return GroupChatMsg.remove({ meetingId, chatId }, Logger.info(`Cleared GroupChat (${meetingId}, ${chatId})`));
}
return GroupChatMsg.remove({}, Logger.info('Cleared GroupChat (all)'));
}

View File

@ -0,0 +1,27 @@
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import GroupChatMsg from '/imports/api/group-chat-msg';
export default function removeGroupChat(meetingId, chatId) {
check(meetingId, String);
check(chatId, String);
const selector = {
chatId,
meetingId,
};
const cb = (err, numChanged) => {
if (err) {
Logger.error(`Removing group-chat-msg from collection: ${err}`);
return;
}
if (numChanged) {
// TODO: Clear group-chat-msg-messages
Logger.info(`Removed group-chat-msg id=${chatId} meeting=${meetingId}`);
}
};
return GroupChatMsg.remove(selector, cb);
}

View File

@ -0,0 +1,36 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import mapToAcl from '/imports/startup/mapToAcl';
import { GroupChat, CHAT_ACCESS_PUBLIC } from '/imports/api/group-chat-msg';
function groupChatMsg(credentials) {
const { meetingId, requesterUserId, requesterToken } = credentials;
check(meetingId, String);
check(requesterUserId, String);
check(requesterToken, String);
Logger.info(`Publishing group-chat-msg for ${meetingId} ${requesterUserId} ${requesterToken}`);
return GroupChat.find({
$or: [
{
access: CHAT_ACCESS_PUBLIC,
meetingId,
}, {
users: { $in: [requesterUserId] },
meetingId,
},
],
});
}
function publish(...args) {
const boundGroupChat = groupChatMsg.bind(this);
return mapToAcl('subscriptions.group-chat-msg', boundGroupChat)(args);
}
Meteor.publish('group-chat-msg', publish);

View File

@ -0,0 +1,19 @@
import { Meteor } from 'meteor/meteor';
const GroupChat = new Mongo.Collection('group-chat');
if (Meteor.isServer) {
GroupChat._ensureIndex({
meetingId: 1, chatId: 1, access: 1, users: 1,
});
}
export default GroupChat;
export const CHAT_ACCESS = {
PUBLIC: 'PUBLIC_ACCESS',
PRIVATE: 'PRIVATE_ACCESS',
};
export const CHAT_ACCESS_PUBLIC = CHAT_ACCESS.PUBLIC;
export const CHAT_ACCESS_PRIVATE = CHAT_ACCESS.PRIVATE;

View File

@ -0,0 +1,8 @@
import RedisPubSub from '/imports/startup/server/redis';
import handleGroupChats from './handlers/groupChats';
import handleGroupChatCreated from './handlers/groupChatCreated';
import handleGroupChatDestroyed from './handlers/groupChatDestroyed';
RedisPubSub.on('GetGroupChatsRespMsg', handleGroupChats);
RedisPubSub.on('GroupChatCreatedEvtMsg', handleGroupChatCreated);
RedisPubSub.on('GroupChatDestroyedEvtMsg', handleGroupChatDestroyed);

View File

@ -0,0 +1,9 @@
import { check } from 'meteor/check';
import addGroupChat from '../modifiers/addGroupChat';
export default function handleGroupChatCreated({ body }, meetingId) {
check(meetingId, String);
check(body, Object);
return addGroupChat(meetingId, body);
}

View File

@ -0,0 +1,9 @@
import { check } from 'meteor/check';
import addGroupChat from '../modifiers/addGroupChat';
export default function handleGroupChatDestroyed({ body }, meetingId) {
check(meetingId, String);
check(body, Object);
return addGroupChat(meetingId, body);
}

View File

@ -0,0 +1,17 @@
import { check } from 'meteor/check';
import addGroupChat from '../modifiers/addGroupChat';
export default function handleGroupChats({ body }, meetingId) {
const { chats } = body;
check(meetingId, String);
check(chats, Array);
const chatsAdded = [];
chats.forEach((chat) => {
chatsAdded.push(addGroupChat(meetingId, chat));
});
return chatsAdded;
}

View File

@ -0,0 +1,4 @@
import '/imports/api/group-chat-msg/server';
import './eventHandlers';
import './methods';
import './publishers';

View File

@ -0,0 +1,9 @@
import { Meteor } from 'meteor/meteor';
import mapToAcl from '/imports/startup/mapToAcl';
import createGroupChat from './methods/createGroupChat';
import destroyGroupChat from './methods/destroyGroupChat';
Meteor.methods(mapToAcl(['methods.createGroupChat', 'methods.destroyGroupChat'], {
createGroupChat,
destroyGroupChat,
}));

View File

@ -0,0 +1,31 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import RedisPubSub from '/imports/startup/server/redis';
export default function createGroupChat(credentials) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const { meetingId, requesterUserId, requesterToken } = credentials;
check(meetingId, String);
check(requesterUserId, String);
check(requesterToken, String);
const eventName = 'CreateGroupChatReqMsg';
const payload = {
// TODO: Implement this together with #4988
// correlationId: String,
// name: String,
// access: String,
// users: Vector[String],
// msg: Vector[{
// correlationId: String,
// sender: GroupChatUser,
// color: String,
// message: String
// }],
};
return RedisPubSub.publishUserMessage(CHANNEL, eventName, meetingId, requesterUserId, payload);
}

View File

@ -0,0 +1,22 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import RedisPubSub from '/imports/startup/server/redis';
export default function createGroupChat(credentials) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const { meetingId, requesterUserId, requesterToken } = credentials;
check(meetingId, String);
check(requesterUserId, String);
check(requesterToken, String);
const eventName = 'DestroyGroupChatReqMsg';
const payload = {
// TODO: Implement this together with #4988
// chats: Array[String],
};
return RedisPubSub.publishUserMessage(CHANNEL, eventName, meetingId, requesterUserId, payload);
}

View File

@ -0,0 +1,52 @@
import flat from 'flat';
import { Match, check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import GroupChat from '/imports/api/group-chat';
export default function addGroupChat(meetingId, chat) {
check(meetingId, String);
check(chat, {
id: Match.Maybe(String),
chatId: Match.Maybe(String),
correlationId: Match.Maybe(String),
name: String,
access: String,
createdBy: Object,
users: Array,
msg: Match.Maybe(Array),
});
const chatDocument = {
meetingId,
chatId: chat.chatId || chat.id,
name: chat.name,
access: chat.access,
users: chat.users.map(u => u.id),
createdBy: chat.createdBy.id,
};
const selector = {
chatId: chatDocument.chatId,
meetingId,
};
const modifier = {
$set: flat(chatDocument, { safe: true }),
};
const cb = (err, numChanged) => {
if (err) {
return Logger.error(`Adding group-chat to collection: ${err}`);
}
const { insertedId } = numChanged;
if (insertedId) {
return Logger.info(`Added group-chat name=${chat.name} meetingId=${meetingId}`);
}
return Logger.info(`Upserted group-chat name=${chat.name} meetingId=${meetingId}`);
};
return GroupChat.upsert(selector, modifier, cb);
}

View File

@ -0,0 +1,13 @@
import GroupChat from '/imports/api/group-chat';
import Logger from '/imports/startup/server/logger';
import clearGroupChatMsg from 'imports/api/group-chat-msg/server/modifiers/clearGroupChatMsg';
export default function clearGroupChat(meetingId) {
if (meetingId) {
clearGroupChatMsg(meetingId);
return GroupChat.remove({ meetingId }, Logger.info(`Cleared GroupChat (${meetingId})`));
}
clearGroupChatMsg();
return GroupChat.remove({}, Logger.info('Cleared GroupChat (all)'));
}

View File

@ -0,0 +1,29 @@
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import GroupChat from '/imports/api/group-chat';
import clearGroupChatMsg from 'imports/api/group-chat-msg/server/modifiers/clearGroupChatMsg';
export default function removeGroupChat(meetingId, chatId) {
check(meetingId, String);
check(chatId, String);
const selector = {
chatId,
meetingId,
};
const cb = (err, numChanged) => {
if (err) {
Logger.error(`Removing group-chat from collection: ${err}`);
return;
}
if (numChanged) {
// TODO: Clear group-chat-messages
Logger.info(`Removed group-chat id=${chatId} meeting=${meetingId}`);
clearGroupChatMsg(meetingId, chatId);
}
};
return GroupChat.remove(selector, cb);
}

View File

@ -0,0 +1,36 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import mapToAcl from '/imports/startup/mapToAcl';
import { GroupChat, CHAT_ACCESS_PUBLIC } from '/imports/api/group-chat';
function groupChat(credentials) {
const { meetingId, requesterUserId, requesterToken } = credentials;
check(meetingId, String);
check(requesterUserId, String);
check(requesterToken, String);
Logger.info(`Publishing group-chat for ${meetingId} ${requesterUserId} ${requesterToken}`);
return GroupChat.find({
$or: [
{
access: CHAT_ACCESS_PUBLIC,
meetingId,
}, {
users: { $in: [requesterUserId] },
meetingId,
},
],
});
}
function publish(...args) {
const boundGroupChat = groupChat.bind(this);
return mapToAcl('subscriptions.group-chat', boundGroupChat)(args);
}
Meteor.publish('group-chat', publish);

View File

@ -74,7 +74,9 @@
"captions", "captions",
"breakouts", "breakouts",
"voiceUsers", "voiceUsers",
"whiteboard-multi-user" "whiteboard-multi-user",
"group-chat",
"group-chat-msg"
], ],
"methods": [ "methods": [
"listenOnlyToggle", "listenOnlyToggle",
@ -82,7 +84,10 @@
"setEmojiStatus", "setEmojiStatus",
"toggleSelfVoice", "toggleSelfVoice",
"publishVote", "publishVote",
"sendChat" "sendChat",
"createGroupChat",
"destroyGroupChat",
"sendGroupChatMsg"
] ]
}, },
"moderator": { "moderator": {

View File

@ -74,7 +74,9 @@
"captions", "captions",
"breakouts", "breakouts",
"voiceUsers", "voiceUsers",
"whiteboard-multi-user" "whiteboard-multi-user",
"group-chat",
"group-chat-msg"
], ],
"methods": [ "methods": [
"listenOnlyToggle", "listenOnlyToggle",
@ -82,7 +84,10 @@
"setEmojiStatus", "setEmojiStatus",
"toggleSelfVoice", "toggleSelfVoice",
"publishVote", "publishVote",
"sendChat" "sendChat",
"createGroupChat",
"destroyGroupChat",
"sendGroupChatMsg"
] ]
}, },
"moderator": { "moderator": {

View File

@ -11,6 +11,7 @@ import '/imports/api/presentations/server';
import '/imports/api/slides/server'; import '/imports/api/slides/server';
import '/imports/api/breakouts/server'; import '/imports/api/breakouts/server';
import '/imports/api/chat/server'; import '/imports/api/chat/server';
import '/imports/api/group-chat/server';
import '/imports/api/screenshare/server'; import '/imports/api/screenshare/server';
import '/imports/api/voice-users/server'; import '/imports/api/voice-users/server';
import '/imports/api/whiteboard-multi-user/server'; import '/imports/api/whiteboard-multi-user/server';