Merged 2.0 and 1.1 breakouts, chat, and captions

This commit is contained in:
Oleksandr Zhurbenko 2017-10-11 14:25:18 -07:00
parent aa39361360
commit 47d9b53cc2
61 changed files with 233 additions and 1464 deletions

View File

@ -1 +1,14 @@
export default new Mongo.Collection('breakouts');
import { Meteor } from 'meteor/meteor';
const Breakouts = new Mongo.Collection('breakouts2x');
if (Meteor.isServer) {
// types of queries for the breakouts:
// 1. breakoutId ( handleJoinUrl, roomStarted, clearBreakouts )
// 2. parentMeetingId ( updateTimeRemaining )
Breakouts._ensureIndex({ breakoutId: 1 });
Breakouts._ensureIndex({ parentMeetingId: 1 });
}
export default Breakouts;

View File

@ -1,13 +1,10 @@
import RedisPubSub from '/imports/startup/server/redis';
import handleCreateBreakout from './handlers/createBreakout';
import handleBreakoutStarted from './handlers/breakoutStarted';
import RedisPubSub from '/imports/startup/server/redis2x';
import handleBreakoutJoinURL from './handlers/breakoutJoinURL';
import handleBreakoutStarted from './handlers/breakoutStarted';
import handleUpdateTimeRemaining from './handlers/updateTimeRemaining';
import handleBreakoutClosed from './handlers/breakoutClosed';
RedisPubSub.on('CreateBreakoutRoomRequest', handleCreateBreakout);
RedisPubSub.on('BreakoutRoomStarted', handleBreakoutStarted);
RedisPubSub.on('BreakoutRoomJoinURL', handleBreakoutJoinURL);
RedisPubSub.on('BreakoutRoomsTimeRemainingUpdate', handleUpdateTimeRemaining);
RedisPubSub.on('BreakoutRoomClosed', handleBreakoutClosed);
RedisPubSub.on('BreakoutRoomStartedEvtMsg', handleBreakoutStarted);
RedisPubSub.on('BreakoutRoomJoinURLEvtMsg', handleBreakoutJoinURL);
RedisPubSub.on('BreakoutRoomsTimeRemainingUpdateEvtMsg', handleUpdateTimeRemaining);
RedisPubSub.on('BreakoutRoomEndedEvtMsg', handleBreakoutClosed);

View File

@ -1,10 +1,10 @@
import clearBreakouts from '../modifiers/clearBreakouts';
import { check } from 'meteor/check';
import clearBreakouts from '../modifiers/clearBreakouts';
export default function handleBreakoutClosed({ payload }) {
const meetingId = payload.meetingId;
export default function handleBreakoutClosed({ body }) {
const { breakoutId } = body;
check(meetingId, String);
check(breakoutId, String);
return clearBreakouts(meetingId);
return clearBreakouts(breakoutId);
}

View File

@ -1,9 +1,9 @@
import Breakouts from '/imports/api/1.1/breakouts';
import Logger from '/imports/startup/server/logger';
import { check } from 'meteor/check';
import xml2js from 'xml2js';
import url from 'url';
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import Breakouts from '/imports/api/2.0/breakouts';
const xmlParser = new xml2js.Parser();
const getUrlParams = (urlToParse) => {
@ -12,48 +12,48 @@ const getUrlParams = (urlToParse) => {
return parsedUrl.query;
};
export default function handleBreakoutJoinURL({ payload }) {
const REDIS_CONFIG = Meteor.settings.redis;
const CLIENT_HTML = 'HTML5';
export default function handleBreakoutJoinURL({ body }) {
const {
noRedirectJoinURL,
} = payload;
userId,
breakoutId,
} = body;
check(noRedirectJoinURL, String);
const urlParams = getUrlParams(noRedirectJoinURL);
const selector = {
externalMeetingId: urlParams.meetingID,
breakoutId,
};
let breakout = Breakouts.findOne(selector);
const res = Meteor.http.call('get', noRedirectJoinURL);
xmlParser.parseString(res.content, (err, parsedXML) => {
if (err) {
return Logger.error(`An Error occured when parsing xml response for: ${noRedirectJoinURL}`);
}
breakout = Breakouts.findOne(selector);
const breakout = Breakouts.findOne(selector);
const { response } = parsedXML;
const users = breakout.users;
const user = {
userId: payload.userId,
userId,
urlParams: {
meetingId: response.meeting_id[0],
userId: response.user_id[0],
authToken: response.auth_token[0],
sessionToken: response.session_token[0],
},
};
const userExists = users.find(u => user.userId === u.userId);
if (userExists) {
return;
return null;
}
const modifier = {
@ -62,9 +62,9 @@ export default function handleBreakoutJoinURL({ payload }) {
},
};
const cb = (err, numChanged) => {
if (err) {
return Logger.error(`Adding breakout to collection: ${err}`);
const cb = (cbErr, numChanged) => {
if (cbErr) {
return Logger.error(`Adding breakout to collection: ${cbErr}`);
}
const {

View File

@ -1,38 +1,41 @@
import Breakouts from '/imports/api/1.1/breakouts';
import Breakouts from '/imports/api/2.0/breakouts';
import Logger from '/imports/startup/server/logger';
import { check } from 'meteor/check';
import flat from 'flat';
export default function handleBreakoutRoomStarted({ payload }) {
export default function handleBreakoutRoomStarted({ body }, meetingId) {
const {
meetingId,
timeRemaining,
externalMeetingId,
} = payload;
parentMeetingId,
breakout,
} = body;
const { breakoutId } = breakout;
const timeRemaining = 15;
check(meetingId, String);
const selector = {
breakoutMeetingId: meetingId,
breakoutId,
};
modifier = {
$set: {
users: [],
timeRemaining: Number(timeRemaining),
externalMeetingId,
},
const modifier = {
$set: Object.assign(
{ users: [] },
{ timeRemaining: Number(timeRemaining) },
{ parentMeetingId },
flat(breakout),
),
};
const cb = (err, numChanged) => {
const cb = (err) => {
if (err) {
return Logger.error(`updating breakout: ${err}`);
}
if (numChanged) {
return Logger.info('Updated timeRemaining and externalMeetingId ' +
`for breakout id=${meetingId}`);
}
return Logger.info('Updated timeRemaining and externalMeetingId ' +
`for breakout id=${breakoutId}`);
};
return Breakouts.update(selector, modifier, cb);
return Breakouts.upsert(selector, modifier, cb);
}

View File

@ -1,12 +0,0 @@
import Breakouts from '/imports/api/1.1/breakouts';
import Logger from '/imports/startup/server/logger';
import { check } from 'meteor/check';
import addBreakout from '../modifiers/addBreakout';
export default function handleCreateBreakout({ payload }) {
const { breakoutMeetingId } = payload;
check(breakoutMeetingId, String);
return addBreakout(payload);
}

View File

@ -1,12 +1,11 @@
import Breakouts from '/imports/api/1.1/breakouts';
import Logger from '/imports/startup/server/logger';
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import Breakouts from '/imports/api/2.0/breakouts';
export default function handleUpdateTimeRemaining({ payload }) {
export default function handleUpdateTimeRemaining({ body }, meetingId) {
const {
meetingId,
timeRemaining,
} = payload;
} = body;
check(meetingId, String);
check(timeRemaining, Number);
@ -25,15 +24,13 @@ export default function handleUpdateTimeRemaining({ payload }) {
multi: true,
};
const cb = (err, numChanged) => {
const cb = (err) => {
if (err) {
return Logger.error(`Updating breakouts: ${err}`);
}
if (numChanged) {
return Logger.info('Updated breakout time remaining for breakouts ' +
`where parentMeetingId=${meetingId}`);
}
return Logger.info('Updated breakout time remaining for breakouts ' +
`where parentMeetingId=${meetingId}`);
};
return Breakouts.update(selector, modifier, options, cb);

View File

@ -1,42 +0,0 @@
import Breakouts from '/imports/api/1.1/breakouts';
import Logger from '/imports/startup/server/logger';
import { check } from 'meteor/check';
export default function addBreakout(payload) {
const {
breakoutMeetingId,
parentMeetingId,
name,
} = payload;
check(breakoutMeetingId, String);
check(parentMeetingId, String);
check(name, String);
const selector = { breakoutMeetingId };
const modifier = {
$set: {
breakoutMeetingId,
parentMeetingId,
name,
},
};
const cb = (err, numChanged) => {
if (err) {
return Logger.error(`Adding breakout to collection: ${err}`);
}
const {
insertedId,
} = numChanged;
if (insertedId) {
return Logger.info(`Added breakout id=${breakoutMeetingId}`);
}
return Logger.info(`Upserted breakout id=${breakoutMeetingId}`);
};
return Breakouts.upsert(selector, modifier, cb);
}

View File

@ -1,19 +1,13 @@
import Breakouts from '/imports/api/1.1/breakouts';
import Logger from '/imports/startup/server/logger';
import removeMeeting from '/imports/api/1.1/meetings/server/modifiers/removeMeeting';
import Breakouts from '/imports/api/2.0/breakouts';
export default function clearBreakouts(meetingId) {
if (meetingId) {
export default function clearBreakouts(breakoutId) {
if (breakoutId) {
const selector = {
breakoutMeetingId: meetingId,
breakoutId,
};
const cb = () => {
Logger.info(`Cleared Breakouts (${meetingId})`);
removeMeeting(meetingId);
};
return Breakouts.remove(selector, cb);
return Breakouts.remove(selector);
}
return Breakouts.remove({}, Logger.info('Cleared Breakouts (all)'));

View File

@ -1,6 +1,7 @@
import Breakouts from '/imports/api/1.1/breakouts';
import { Meteor } from 'meteor/meteor';
import mapToAcl from '/imports/startup/mapToAcl';
import Breakouts from '/imports/api/2.0/breakouts';
import Logger from '/imports/startup/server/logger';
function breakouts(credentials) {
const {
@ -8,9 +9,12 @@ function breakouts(credentials) {
requesterUserId,
} = credentials;
Logger.info(`Publishing Breakouts2x for ${meetingId} ${requesterUserId}`);
return Breakouts.find({
$or: [
{ breakoutMeetingId: meetingId },
{ breakoutId: meetingId },
{ meetingId },
{
users: {
$elemMatch: { userId: requesterUserId },
@ -25,4 +29,4 @@ function publish(...args) {
return mapToAcl('subscriptions.breakouts', boundBreakouts)(args);
}
Meteor.publish('breakouts', publish);
Meteor.publish('breakouts2x', publish);

View File

@ -1 +1,14 @@
export default new Mongo.Collection('captions');
import { Meteor } from 'meteor/meteor';
const Captions = new Mongo.Collection('captions2x');
if (Meteor.isServer) {
// types of queries for the captions:
// 1. meetingId, locale, 'captionHistory.index' (History)
// 2. meetingId, locale (Owner update, Caption update, addCaption)
// 3. meetingId ( clear Captions)
Captions._ensureIndex({ meetingId: 1, locale: 1 });
}
export default Captions;

View File

@ -1,8 +1,9 @@
import RedisPubSub from '/imports/startup/server/redis';
import RedisPubSub from '/imports/startup/server/redis2x';
import handleCaptionHistory from './handlers/captionHistory';
import handleCaptionUpdate from './handlers/captionUpdate';
import handleCaptionOwnerUpdate from './handlers/captionOwnerUpdate';
RedisPubSub.on('send_caption_history_reply_message', handleCaptionHistory);
RedisPubSub.on('edit_caption_history_message', handleCaptionUpdate);
RedisPubSub.on('update_caption_owner_message', handleCaptionOwnerUpdate);
// TODO
RedisPubSub.on('SendCaptionHistoryRespMsg', handleCaptionHistory);
RedisPubSub.on('EditCaptionHistoryEvtMsg', handleCaptionUpdate);
RedisPubSub.on('UpdateCaptionOwnerEvtMsg', handleCaptionOwnerUpdate);

View File

@ -1,65 +0,0 @@
import _ from 'lodash';
import Captions from '/imports/api/1.1/captions';
import Logger from '/imports/startup/server/logger';
import { check } from 'meteor/check';
import { inReplyToHTML5Client } from '/imports/api/common/server/helpers';
import addCaption from '../modifiers/addCaption';
export default function handleCaptionHistory({ payload }) {
if (!inReplyToHTML5Client({ payload })) {
return;
}
const SERVER_CONFIG = Meteor.settings.app;
const CAPTION_CHUNK_LENGTH = SERVER_CONFIG.captionsChunkLength || 1000;
const meetingId = payload.meeting_id;
const locale = payload.locale;
const captionHistory = payload.caption_history;
check(meetingId, String);
check(captionHistory, Object);
const captionsAdded = [];
_.each(captionHistory, (caption, locale) => {
const ownerId = caption[0];
let captions = caption[1].slice(0);
const chunks = [];
if (captions.length === 0) {
chunks.push('');
} else {
while (captions.length > 0) {
if (captions.length > CAPTION_CHUNK_LENGTH) {
chunks.push(captions.slice(0, CAPTION_CHUNK_LENGTH));
captions = captions.slice(CAPTION_CHUNK_LENGTH);
} else {
chunks.push(captions);
captions = captions.slice(captions.length);
}
}
}
const selectorToRemove = {
meetingId,
locale,
'captionHistory.index': { $gt: (chunks.length - 1) },
};
Captions.remove(selectorToRemove);
chunks.forEach((captions, index) => {
const captionHistoryObject = {
locale,
ownerId,
captions,
index,
next: (index < chunks.length - 1) ? index + 1 : undefined,
};
captionsAdded.push(addCaption(meetingId, locale, captionHistoryObject));
});
});
return captionsAdded;
}

View File

@ -1,50 +0,0 @@
import Captions from '/imports/api/1.1/captions';
import Logger from '/imports/startup/server/logger';
import { check } from 'meteor/check';
import addCaption from '../modifiers/addCaption';
export default function handleCaptionOwnerUpdate({ payload }) {
const meetingId = payload.meeting_id;
const locale = payload.locale;
const ownerId = payload.owner_id;
check(meetingId, String);
check(locale, String);
check(ownerId, String);
const selector = {
meetingId,
locale,
};
const modifier = {
$set: {
'captionHistory.ownerId': ownerId,
},
};
const Caption = Captions.findOne(selector);
if (!Caption) {
const captionHistory = {
ownerId,
captions: '',
index: 0,
next: null,
};
return addCaption(meetingId, locale, captionHistory);
}
const cb = (err, numChanged) => {
if (err) {
return Logger.error(`Updating captions owner: ${err}`);
}
if (numChanged) {
return Logger.verbose(`Update caption owner locale=${locale} meeting=${meetingId}`);
}
};
return Captions.update(selector, modifier, { multi: true }, cb);
}

View File

@ -1,180 +0,0 @@
import Captions from '/imports/api/1.1/captions';
import Logger from '/imports/startup/server/logger';
import { check } from 'meteor/check';
import addCaption from '../modifiers/addCaption';
export default function handleCaptionUpdate({ payload }) {
const SERVER_CONFIG = Meteor.settings.app;
const CAPTION_CHUNK_LENGTH = SERVER_CONFIG.captionsChunkLength || 1000;
const meetingId = payload.meeting_id;
const locale = payload.locale;
check(meetingId, String);
check(locale, String);
const captionsObjects = Captions.find({
meetingId,
locale,
}, {
sort: {
locale: 1,
'captionHistory.index': 1,
},
}).fetch();
const objectsToUpdate = [];
if (captionsObjects != null) {
let startIndex;
let endIndex;
let length = 0;
let current = captionsObjects[0];
// looking for a start index and end index
// (end index only for the case when they are in the same block)
while (current != null) {
length += current.captionHistory.captions.length;
// if length is bigger than start index - we found our start index
if (length >= payload.start_index && startIndex == undefined) {
// check if it's a new character somewhere in the middle of captions text
if (length - 1 >= payload.start_index) {
startIndex = payload.start_index - (length - current.captionHistory.captions.length);
// check to see if the end_index is in the same object as start_index
if (length - 1 >= payload.end_index) {
endIndex = payload.end_index - (length - current.captionHistory.captions.length);
const _captions = current.captionHistory.captions;
current.captionHistory.captions = _captions.slice(0, startIndex) +
payload.text +
_captions.slice(endIndex);
objectsToUpdate.push(current);
break;
// end index is not in the same object as start_index, we will find it later
} else {
current.captionHistory.captions = current.captionHistory.captions.slice(0, startIndex) +
payload.text;
objectsToUpdate.push(current);
break;
}
// separate case for appending new characters to the very end of the string
} else if (current.captionHistory.next == null &&
length == payload.start_index &&
length == payload.start_index) {
startIndex = 1;
endIndex = 1;
current.captionHistory.captions += payload.text;
objectsToUpdate.push(current);
}
}
current = captionsObjects[current.captionHistory.next];
}
// looking for end index here if it wasn't in the same object as start index
if (startIndex != undefined && endIndex == undefined) {
current = captionsObjects[current.captionHistory.next];
while (current != null) {
length += current.captionHistory.captions.length;
// check to see if the end_index is in the current object
if (length - 1 >= payload.end_index) {
endIndex = payload.end_index - (length - current.captionHistory.captions.length);
current.captionHistory.captions = current.captionHistory.captions.slice(endIndex);
objectsToUpdate.push(current);
break;
// if end_index wasn't in the current object, that means this whole object was deleted
// initializing string to ''
} else {
current.captionHistory.captions = '';
objectsToUpdate.push(current);
}
current = captionsObjects[current.captionHistory.next];
}
}
// looking for the strings which exceed the limit and split them into multiple objects
let maxIndex = captionsObjects.length - 1;
for (i = 0; i < objectsToUpdate.length; i++) {
if (objectsToUpdate[i].captionHistory.captions.length > CAPTION_CHUNK_LENGTH) {
// string is too large. Check if the next object exists and if it can
// accomodate the part of the string that exceeds the limits
const _nextIndex = objectsToUpdate[i].captionHistory.next;
if (_nextIndex != null &&
captionsObjects[_nextIndex].captionHistory.captions.length < CAPTION_CHUNK_LENGTH) {
const extraString = objectsToUpdate[i].captionHistory.captions.slice(CAPTION_CHUNK_LENGTH);
// could assign it directly, but our linter complained
let _captions = objectsToUpdate[i].captionHistory.captions;
_captions = _captions.slice(0, CAPTION_CHUNK_LENGTH);
objectsToUpdate[i].captionHistory.captions = _captions;
// check to see if the next object was added to objectsToUpdate array
if (objectsToUpdate[i + 1] != null &&
objectsToUpdate[i].captionHistory.next == objectsToUpdate[i + 1].captionHistory.index) {
objectsToUpdate[i + 1].captionHistory.captions = extraString +
objectsToUpdate[i + 1].captionHistory.captions;
// next object wasn't added to objectsToUpdate array, adding it from captionsObjects array.
} else {
const nextObj = captionsObjects[objectsToUpdate[i].captionHistory.next];
nextObj.captionHistory.captions = extraString + nextObj.captionHistory.captions;
objectsToUpdate.push(nextObj);
}
// next object was full already, so we create another and insert it in between them
} else {
// need to take a current object out of the objectsToUpdate and add it back after
// every other object, so that Captions collection could be updated in a proper order
const tempObj = objectsToUpdate.splice(i, 1);
let extraString = tempObj[0].captionHistory.captions.slice(CAPTION_CHUNK_LENGTH);
tempObj[0].captionHistory.captions =
tempObj[0].captionHistory.captions.slice(0, CAPTION_CHUNK_LENGTH);
maxIndex += 1;
const tempIndex = tempObj[0].captionHistory.next;
tempObj[0].captionHistory.next = maxIndex;
while (extraString.length != 0) {
const entry = {
meetingId,
locale,
captionHistory: {
locale,
ownerId: tempObj[0].captionHistory.ownerId,
captions: extraString.slice(0, CAPTION_CHUNK_LENGTH),
index: maxIndex,
next: null,
},
};
maxIndex += 1;
extraString = extraString.slice(CAPTION_CHUNK_LENGTH);
if (extraString.length > 0) {
entry.captionHistory.next = maxIndex;
} else {
entry.captionHistory.next = tempIndex;
}
objectsToUpdate.push(entry);
}
objectsToUpdate.push(tempObj[0]);
}
}
}
}
const captionsAdded = [];
objectsToUpdate.forEach((entry) => {
const { _id, meetingId, locale, captionHistory } = entry;
captionsAdded.push(addCaption(meetingId, locale, captionHistory, _id));
});
return captionsAdded;
}

View File

@ -1,17 +1,26 @@
import { check } from 'meteor/check';
import Captions from '/imports/api/1.1/captions';
import { Match, check } from 'meteor/check';
import Captions from '/imports/api/2.0/captions';
import Logger from '/imports/startup/server/logger';
export default function addCaption(meetingId, locale, captionHistory, id = false) {
check(meetingId, String);
check(locale, String);
check(captionHistory, Object);
check(captionHistory, {
ownerId: String,
index: Number,
captions: String,
locale: Match.Maybe(String),
localeCode: Match.Maybe(String),
next: Match.OneOf(Number, undefined, null),
});
const selector = {
meetingId,
locale,
};
if (id) {
selector._id = id;
} else {
@ -22,27 +31,21 @@ export default function addCaption(meetingId, locale, captionHistory, id = false
$set: {
meetingId,
locale,
'captionHistory.locale': locale,
'captionHistory.ownerId': captionHistory.ownerId,
'captionHistory.captions': captionHistory.captions,
'captionHistory.next': captionHistory.next,
'captionHistory.index': captionHistory.index,
captionHistory,
},
};
const cb = (err, numChanged) => {
if (err) {
return Logger.error(`Adding caption to collection: ${err}`);
return Logger.error(`Adding caption2x to collection: ${err}`);
}
const { insertedId } = numChanged;
if (insertedId) {
return Logger.verbose(`Added caption locale=${locale} meeting=${meetingId}`);
return Logger.verbose(`Added caption2x locale=${locale} meeting=${meetingId}`);
}
if (numChanged) {
return Logger.verbose(`Upserted caption locale=${locale} meeting=${meetingId}`);
}
return Logger.verbose(`Upserted caption2x locale=${locale} meeting=${meetingId}`);
};
return Captions.upsert(selector, modifier, cb);

View File

@ -1,10 +1,10 @@
import Captions from '/imports/api/1.1/captions';
import Captions from '/imports/api/2.0/captions';
import Logger from '/imports/startup/server/logger';
export default function clearCaptions(meetingId) {
if (meetingId) {
return Captions.remove({ meetingId }, Logger.info(`Cleared Captions (${meetingId})`));
return Captions.remove({ meetingId }, Logger.info(`Cleared Captions2x (${meetingId})`));
}
return Captions.remove({}, Logger.info('Cleared Captions (all)'));
return Captions.remove({}, Logger.info('Cleared Captions2x (all)'));
}

View File

@ -1,4 +1,4 @@
import Captions from '/imports/api/1.1/captions';
import Captions from '/imports/api/2.0/captions';
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
@ -11,7 +11,7 @@ function captions(credentials) {
check(requesterUserId, String);
check(requesterToken, String);
Logger.verbose(`Publishing Captions for ${meetingId} ${requesterUserId} ${requesterToken}`);
Logger.verbose(`Publishing Captions2x for ${meetingId} ${requesterUserId} ${requesterToken}`);
return Captions.find({ meetingId });
}
@ -21,4 +21,4 @@ function publish(...args) {
return mapToAcl('subscriptions.captions', boundCaptions)(args);
}
Meteor.publish('captions', publish);
Meteor.publish('captions2x', publish);

View File

@ -1 +1,19 @@
export default new Mongo.Collection('chat');
import { Meteor } from 'meteor/meteor';
const Chat = new Mongo.Collection('chat2x');
if (Meteor.isServer) {
// types of queries for the chat:
// 1. meetingId, toUsername (publishers)
// 2. meetingId, fromUserId (publishers)
// 3. meetingId, toUserId (publishers)
// 4. meetingId, fromTime, fromUserId, toUserId (addChat)
// 5. meetingId (clearChat)
// 6. meetingId, fromUserId, toUserId (clearSystemMessages)
Chat._ensureIndex({ meetingId: 1, toUsername: 1 });
Chat._ensureIndex({ meetingId: 1, fromUserId: 1 });
Chat._ensureIndex({ meetingId: 1, toUserId: 1 });
}
export default Chat;

View File

@ -1,7 +1,9 @@
import RedisPubSub from '/imports/startup/server/redis';
import RedisPubSub from '/imports/startup/server/redis2x';
import handleChatMessage from './handlers/chatMessage';
import handleChatHistory from './handlers/chatHistory';
import handleChatPublicHistoryClear from './handlers/chatPublicHistoryClear';
RedisPubSub.on('get_chat_history_reply', handleChatHistory);
RedisPubSub.on('send_public_chat_message', handleChatMessage);
RedisPubSub.on('send_private_chat_message', handleChatMessage);
RedisPubSub.on('GetChatHistoryRespMsg', handleChatHistory);
RedisPubSub.on('SendPublicMessageEvtMsg', handleChatMessage);
RedisPubSub.on('SendPrivateMessageEvtMsg', handleChatMessage);
RedisPubSub.on('ClearPublicChatHistoryEvtMsg', handleChatPublicHistoryClear);

View File

@ -1,22 +1,15 @@
import Logger from '/imports/startup/server/logger';
import { check } from 'meteor/check';
import { inReplyToHTML5Client } from '/imports/api/common/server/helpers';
import addChat from '../modifiers/addChat';
export default function handleChatHistory({ payload }) {
if (!inReplyToHTML5Client({ payload })) {
return;
}
const meetingId = payload.meeting_id;
const chatHistory = payload.chat_history || [];
export default function handleChatHistory({ body }, meetingId) {
const { history } = body;
check(meetingId, String);
check(chatHistory, Array);
check(history, Array);
const chatsAdded = [];
chatHistory.forEach((message) => {
history.forEach((message) => {
chatsAdded.push(addChat(meetingId, message));
});

View File

@ -1,17 +1,11 @@
import Logger from '/imports/startup/server/logger';
import { check } from 'meteor/check';
import addChat from '../modifiers/addChat';
export default function handleChatMessage({ payload, header }) {
const message = payload.message;
const meetingId = payload.meeting_id;
export default function handleChatMessage({ body }, meetingId) {
const { message } = body;
check(meetingId, String);
check(message, Object);
// use current_time instead of message.from_time so that the
// chats from Flash and HTML5 have uniform times
message.from_time = +(header.current_time);
return addChat(meetingId, message);
}

View File

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

View File

@ -1,7 +1,6 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import RedisPubSub from '/imports/startup/server/redis';
import RedisPubSub from '/imports/startup/server/redis2x';
import RegexWebUrl from '/imports/utils/regex-weburl';
const HTML_SAFE_MAP = {
@ -11,30 +10,28 @@ const HTML_SAFE_MAP = {
"'": '&#39;',
};
const PUBLIC_CHAT_TYPE = 'PUBLIC_CHAT';
const parseMessage = (message) => {
message = message || '';
message = message.trim();
let parsedMessage = message || '';
parsedMessage = parsedMessage.trim();
// Replace <br/> with \n\r
message = message.replace(/<br\s*[\/]?>/gi, '\n\r');
parsedMessage = parsedMessage.replace(/<br\s*[\\/]?>/gi, '\n\r');
// Sanitize. See: http://shebang.brandonmintern.com/foolproof-html-escaping-in-javascript/
message = message.replace(/[<>'"]/g, c => HTML_SAFE_MAP[c]);
parsedMessage = parsedMessage.replace(/[<>'"]/g, c => HTML_SAFE_MAP[c]);
// Replace flash links to flash valid ones
message = message.replace(RegexWebUrl, "<a href='event:$&'><u>$&</u></a>");
parsedMessage = parsedMessage.replace(RegexWebUrl, "<a href='event:$&'><u>$&</u></a>");
return message;
return parsedMessage;
};
export default function sendChat(credentials, message) {
const REDIS_CONFIG = Meteor.settings.redis;
const CHANNEL = REDIS_CONFIG.channels.toBBBApps.chat;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const CHAT_CONFIG = Meteor.settings.public.chat;
const PUBLIC_CHAT_TYPE = CHAT_CONFIG.type_public;
const TO_PUBLIC_CHAT = CHAT_CONFIG.public_username;
const { meetingId, requesterUserId, requesterToken } = credentials;
@ -43,21 +40,15 @@ export default function sendChat(credentials, message) {
check(requesterToken, String);
check(message, Object);
let actionName = message.to_userid === requesterUserId ? 'chatSelf' : 'chatPrivate';
let eventName = 'send_private_chat_message';
let eventName = 'SendPrivateMessagePubMsg';
message.message = parseMessage(message.message);
const parsedMessage = message;
if (message.chat_type === PUBLIC_CHAT_TYPE) {
eventName = 'send_public_chat_message';
actionName = 'chatPublic';
parsedMessage.message = parseMessage(message.message);
if (message.toUsername === TO_PUBLIC_CHAT) {
eventName = 'SendPublicMessagePubMsg';
}
const payload = {
message,
meeting_id: meetingId,
requester_id: message.from_userid,
};
return RedisPubSub.publish(CHANNEL, eventName, payload);
return RedisPubSub.publishUserMessage(CHANNEL, eventName, meetingId, requesterUserId, { message: parsedMessage });
}

View File

@ -1,56 +1,61 @@
import Chat from '/imports/api/1.1/chat';
import flat from 'flat';
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.js';
import { Match, check } from 'meteor/check';
import { BREAK_LINE } from '/imports/utils/lineEndings';
const parseMessage = (message) => {
message = message || '';
let parsedMessage = message || '';
// Replace \r and \n to <br/>
message = message.replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, `$1${BREAK_LINE}$2`);
parsedMessage = parsedMessage.replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, `$1${BREAK_LINE}$2`);
// Replace flash links to html valid ones
message = message.split('<a href=\'event:').join('<a target="_blank" href=\'');
message = message.split('<a href="event:').join('<a target="_blank" href="');
parsedMessage = parsedMessage.split('<a href=\'event:').join('<a target="_blank" href=\'');
parsedMessage = parsedMessage.split('<a href="event:').join('<a target="_blank" href="');
return message;
return parsedMessage;
};
export default function addChat(meetingId, message) {
// manually convert time from 1.408645053653E12 to 1408645053653 if necessary
// (this is the time_from that the Flash client outputs)
message.from_time = +(message.from_time.toString().split('.').join('').split('E')[0]);
message.message = parseMessage(message.message);
const chatType = (userName) => {
const CHAT_CONFIG = Meteor.settings.public.chat;
const fromUserId = message.from_userid;
const toUserId = message.to_userid;
const typeByUser = {
[CHAT_CONFIG.system_username]: CHAT_CONFIG.type_system,
[CHAT_CONFIG.public_username]: CHAT_CONFIG.type_public,
};
check(fromUserId, String);
check(toUserId, String);
return userName in typeByUser ? typeByUser[userName] : CHAT_CONFIG.type_private;
};
export default function addChat(meetingId, chat) {
check(chat, {
message: String,
fromColor: String,
toUserId: String,
toUsername: String,
fromUserId: String,
fromUsername: Match.Maybe(String),
fromTime: Number,
fromTimezoneOffset: Match.Maybe(Number),
});
const selector = {
meetingId,
'message.from_time': message.from_time,
'message.from_userid': message.from_userid,
'message.to_userid': message.to_userid,
fromTime: chat.fromTime,
fromUserId: chat.fromUserId,
toUserId: chat.toUserId,
};
const modifier = {
$set: {
meetingId,
message: {
chat_type: message.chat_type,
message: message.message,
to_username: message.to_username,
from_tz_offset: message.from_tz_offset,
from_color: message.from_color,
to_userid: message.to_userid,
from_userid: message.from_userid,
from_time: message.from_time,
from_username: message.from_username,
from_lang: message.from_lang,
$set: Object.assign(
flat(chat, { safe: true }),
{
meetingId,
message: parseMessage(chat.message),
type: chatType(chat.toUsername),
},
},
),
};
const cb = (err, numChanged) => {
@ -59,11 +64,13 @@ export default function addChat(meetingId, message) {
}
const { insertedId } = numChanged;
const to = chat.toUsername || 'PUBLIC';
if (insertedId) {
const to = message.to_username || 'PUBLIC';
return Logger.info(`Added chat id=${insertedId} from=${message.from_username} to=${to}`);
return Logger.info(`Added chat from=${chat.fromUsername} to=${to} time=${chat.fromTime}`);
}
return Logger.info(`Upserted chat from=${chat.fromUsername} to=${to} time=${chat.fromTime}`);
};
return Chat.upsert(selector, modifier, cb);

View File

@ -1,4 +1,4 @@
import Chat from '/imports/api/1.1/chat';
import Chat from '/imports/api/2.0/chat';
import Logger from '/imports/startup/server/logger';
export default function clearChats(meetingId) {

View File

@ -1,7 +1,6 @@
import Chat from '/imports/api/1.1/chat';
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.js';
/**
* Remove any system message from the user with userId.
@ -17,8 +16,8 @@ export default function clearUserSystemMessages(meetingId, userId) {
const selector = {
meetingId,
'message.from_userid': CHAT_CONFIG.type_system,
'message.to_userid': userId,
fromUserId: CHAT_CONFIG.type_system,
toUserId: userId,
};
return Chat.remove(selector, Logger.info(`Removing system messages from: (${userId})`));

View File

@ -1,32 +1,31 @@
import Chat from '/imports/api/1.1/chat';
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);
const CHAT_CONFIG = Meteor.settings.public.chat;
const PUBLIC_CHAT_USERNAME = CHAT_CONFIG.public_username;
Logger.info(`Publishing chat for ${meetingId} ${requesterUserId} ${requesterToken}`);
return Chat.find({
$or: [
{
'message.chat_type': PUBLIC_CHAT_TYPE,
toUsername: PUBLIC_CHAT_USERNAME,
meetingId,
}, {
'message.from_userid': requesterUserId,
fromUserId: requesterUserId,
meetingId,
}, {
'message.to_userid': requesterUserId,
toUserId: requesterUserId,
meetingId,
},
],
@ -38,4 +37,4 @@ function publish(...args) {
return mapToAcl('subscriptions.chat', boundChat)(args);
}
Meteor.publish('chat', publish);
Meteor.publish('chat2x', publish);

View File

@ -1,14 +0,0 @@
import { Meteor } from 'meteor/meteor';
const Breakouts = new Mongo.Collection('breakouts2x');
if (Meteor.isServer) {
// types of queries for the breakouts:
// 1. breakoutId ( handleJoinUrl, roomStarted, clearBreakouts )
// 2. parentMeetingId ( updateTimeRemaining )
Breakouts._ensureIndex({ breakoutId: 1 });
Breakouts._ensureIndex({ parentMeetingId: 1 });
}
export default Breakouts;

View File

@ -1,10 +0,0 @@
import RedisPubSub from '/imports/startup/server/redis2x';
import handleBreakoutJoinURL from './handlers/breakoutJoinURL';
import handleBreakoutStarted from './handlers/breakoutStarted';
import handleUpdateTimeRemaining from './handlers/updateTimeRemaining';
import handleBreakoutClosed from './handlers/breakoutClosed';
RedisPubSub.on('BreakoutRoomStartedEvtMsg', handleBreakoutStarted);
RedisPubSub.on('BreakoutRoomJoinURLEvtMsg', handleBreakoutJoinURL);
RedisPubSub.on('BreakoutRoomsTimeRemainingUpdateEvtMsg', handleUpdateTimeRemaining);
RedisPubSub.on('BreakoutRoomEndedEvtMsg', handleBreakoutClosed);

View File

@ -1,10 +0,0 @@
import { check } from 'meteor/check';
import clearBreakouts from '../modifiers/clearBreakouts';
export default function handleBreakoutClosed({ body }) {
const { breakoutId } = body;
check(breakoutId, String);
return clearBreakouts(breakoutId);
}

View File

@ -1,82 +0,0 @@
import xml2js from 'xml2js';
import url from 'url';
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import Breakouts from '/imports/api/2.0/breakouts';
const xmlParser = new xml2js.Parser();
const getUrlParams = (urlToParse) => {
const options = { parseQueryString: true };
const parsedUrl = url.parse(urlToParse, options);
return parsedUrl.query;
};
export default function handleBreakoutJoinURL({ body }) {
const {
noRedirectJoinURL,
userId,
breakoutId,
} = body;
check(noRedirectJoinURL, String);
const urlParams = getUrlParams(noRedirectJoinURL);
const selector = {
breakoutId,
};
const res = Meteor.http.call('get', noRedirectJoinURL);
xmlParser.parseString(res.content, (err, parsedXML) => {
if (err) {
return Logger.error(`An Error occured when parsing xml response for: ${noRedirectJoinURL}`);
}
const breakout = Breakouts.findOne(selector);
const { response } = parsedXML;
const users = breakout.users;
const user = {
userId,
urlParams: {
meetingId: response.meeting_id[0],
userId: response.user_id[0],
authToken: response.auth_token[0],
sessionToken: response.session_token[0],
},
};
const userExists = users.find(u => user.userId === u.userId);
if (userExists) {
return null;
}
const modifier = {
$push: {
users: user,
},
};
const cb = (cbErr, numChanged) => {
if (cbErr) {
return Logger.error(`Adding breakout to collection: ${cbErr}`);
}
const {
insertedId,
} = numChanged;
if (insertedId) {
return Logger.info(`Added breakout id=${urlParams.meetingID}`);
}
return Logger.info(`Upserted breakout id=${urlParams.meetingID}`);
};
return Breakouts.upsert(selector, modifier, cb);
});
}

View File

@ -1,41 +0,0 @@
import Breakouts from '/imports/api/2.0/breakouts';
import Logger from '/imports/startup/server/logger';
import { check } from 'meteor/check';
import flat from 'flat';
export default function handleBreakoutRoomStarted({ body }, meetingId) {
const {
parentMeetingId,
breakout,
} = body;
const { breakoutId } = breakout;
const timeRemaining = 15;
check(meetingId, String);
const selector = {
breakoutId,
};
const modifier = {
$set: Object.assign(
{ users: [] },
{ timeRemaining: Number(timeRemaining) },
{ parentMeetingId },
flat(breakout),
),
};
const cb = (err) => {
if (err) {
return Logger.error(`updating breakout: ${err}`);
}
return Logger.info('Updated timeRemaining and externalMeetingId ' +
`for breakout id=${breakoutId}`);
};
return Breakouts.upsert(selector, modifier, cb);
}

View File

@ -1,37 +0,0 @@
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import Breakouts from '/imports/api/2.0/breakouts';
export default function handleUpdateTimeRemaining({ body }, meetingId) {
const {
timeRemaining,
} = body;
check(meetingId, String);
check(timeRemaining, Number);
const selector = {
parentMeetingId: meetingId,
};
const modifier = {
$set: {
timeRemaining,
},
};
const options = {
multi: true,
};
const cb = (err) => {
if (err) {
return Logger.error(`Updating breakouts: ${err}`);
}
return Logger.info('Updated breakout time remaining for breakouts ' +
`where parentMeetingId=${meetingId}`);
};
return Breakouts.update(selector, modifier, options, cb);
}

View File

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

View File

@ -1,3 +0,0 @@
import { Meteor } from 'meteor/meteor';
Meteor.methods({});

View File

@ -1,14 +0,0 @@
import Logger from '/imports/startup/server/logger';
import Breakouts from '/imports/api/2.0/breakouts';
export default function clearBreakouts(breakoutId) {
if (breakoutId) {
const selector = {
breakoutId,
};
return Breakouts.remove(selector);
}
return Breakouts.remove({}, Logger.info('Cleared Breakouts (all)'));
}

View File

@ -1,32 +0,0 @@
import { Meteor } from 'meteor/meteor';
import mapToAcl from '/imports/startup/mapToAcl';
import Breakouts from '/imports/api/2.0/breakouts';
import Logger from '/imports/startup/server/logger';
function breakouts(credentials) {
const {
meetingId,
requesterUserId,
} = credentials;
Logger.info(`Publishing Breakouts2x for ${meetingId} ${requesterUserId}`);
return Breakouts.find({
$or: [
{ breakoutId: meetingId },
{ meetingId },
{
users: {
$elemMatch: { userId: requesterUserId },
},
},
],
});
}
function publish(...args) {
const boundBreakouts = breakouts.bind(this);
return mapToAcl('subscriptions.breakouts', boundBreakouts)(args);
}
Meteor.publish('breakouts2x', publish);

View File

@ -1,14 +0,0 @@
import { Meteor } from 'meteor/meteor';
const Captions = new Mongo.Collection('captions2x');
if (Meteor.isServer) {
// types of queries for the captions:
// 1. meetingId, locale, 'captionHistory.index' (History)
// 2. meetingId, locale (Owner update, Caption update, addCaption)
// 3. meetingId ( clear Captions)
Captions._ensureIndex({ meetingId: 1, locale: 1 });
}
export default Captions;

View File

@ -1,9 +0,0 @@
import RedisPubSub from '/imports/startup/server/redis2x';
import handleCaptionHistory from './handlers/captionHistory';
import handleCaptionUpdate from './handlers/captionUpdate';
import handleCaptionOwnerUpdate from './handlers/captionOwnerUpdate';
// TODO
RedisPubSub.on('SendCaptionHistoryRespMsg', handleCaptionHistory);
RedisPubSub.on('EditCaptionHistoryEvtMsg', handleCaptionUpdate);
RedisPubSub.on('UpdateCaptionOwnerEvtMsg', handleCaptionOwnerUpdate);

View File

@ -1,57 +0,0 @@
import _ from 'lodash';
import Captions from '/imports/api/2.0/captions';
import { check } from 'meteor/check';
import addCaption from '../modifiers/addCaption';
export default function handleCaptionHistory({ body }, meetingId) {
const SERVER_CONFIG = Meteor.settings.app;
const CAPTION_CHUNK_LENGTH = SERVER_CONFIG.captionsChunkLength || 1000;
const captionHistory = body.history;
check(meetingId, String);
check(captionHistory, Object);
const captionsAdded = [];
_.each(captionHistory, (caption, locale) => {
const ownerId = caption[0];
let captions = caption[1].slice(0);
const chunks = [];
if (captions.length === 0) {
chunks.push('');
} else {
while (captions.length > 0) {
if (captions.length > CAPTION_CHUNK_LENGTH) {
chunks.push(captions.slice(0, CAPTION_CHUNK_LENGTH));
captions = captions.slice(CAPTION_CHUNK_LENGTH);
} else {
chunks.push(captions);
captions = captions.slice(captions.length);
}
}
}
const selectorToRemove = {
meetingId,
locale,
'captionHistory.index': { $gt: (chunks.length - 1) },
};
Captions.remove(selectorToRemove);
chunks.forEach((chunkCaptions, index) => {
const captionHistoryObject = {
locale,
ownerId,
chunkCaptions,
index,
next: (index < chunks.length - 1) ? index + 1 : undefined,
};
captionsAdded.push(addCaption(meetingId, locale, captionHistoryObject));
});
});
return captionsAdded;
}

View File

@ -1,46 +0,0 @@
import Captions from '/imports/api/2.0/captions';
import Logger from '/imports/startup/server/logger';
import { check } from 'meteor/check';
import addCaption from '../modifiers/addCaption';
export default function handleCaptionOwnerUpdate({ body }, meetingId) {
const { ownerId, locale } = body;
check(meetingId, String);
check(locale, String);
check(ownerId, String);
const selector = {
meetingId,
locale,
};
const modifier = {
$set: {
'captionHistory.ownerId': ownerId,
},
};
const Caption = Captions.findOne(selector);
if (!Caption) {
const captionHistory = {
ownerId,
captions: '',
index: 0,
next: null,
};
return addCaption(meetingId, locale, captionHistory);
}
const cb = (err) => {
if (err) {
return Logger.error(`Updating captions owner: ${err}`);
}
return Logger.verbose(`Update caption owner locale=${locale} meeting=${meetingId}`);
};
return Captions.update(selector, modifier, { multi: true }, cb);
}

View File

@ -1,178 +0,0 @@
import Captions from '/imports/api/2.0/captions';
import { check } from 'meteor/check';
import addCaption from '../modifiers/addCaption';
export default function handleCaptionUpdate({ body }, meetingId) {
const SERVER_CONFIG = Meteor.settings.app;
const CAPTION_CHUNK_LENGTH = SERVER_CONFIG.captionsChunkLength || 1000;
const { locale } = body;
check(meetingId, String);
check(locale, String);
const captionsObjects = Captions.find({
meetingId,
locale,
}, {
sort: {
locale: 1,
'captionHistory.index': 1,
},
}).fetch();
const objectsToUpdate = [];
if (captionsObjects != null) {
let startIndex;
let endIndex;
let length = 0;
let current = captionsObjects[0];
// looking for a start index and end index
// (end index only for the case when they are in the same block)
while (current != null) {
length += current.captionHistory.captions.length;
// if length is bigger than start index - we found our start index
if (length >= body.startIndex && startIndex == undefined) {
// check if it's a new character somewhere in the middle of captions text
if (length - 1 >= body.startIndex) {
startIndex = body.startIndex - (length - current.captionHistory.captions.length);
// check to see if the endIndex is in the same object as startIndex
if (length - 1 >= body.endIndex) {
endIndex = body.endIndex - (length - current.captionHistory.captions.length);
const _captions = current.captionHistory.captions;
current.captionHistory.captions = _captions.slice(0, startIndex) +
body.text +
_captions.slice(endIndex);
objectsToUpdate.push(current);
break;
// end index is not in the same object as startIndex, we will find it later
} else {
current.captionHistory.captions = current.captionHistory.captions.slice(0, startIndex) +
body.text;
objectsToUpdate.push(current);
break;
}
// separate case for appending new characters to the very end of the string
} else if (current.captionHistory.next == null &&
length == body.startIndex &&
length == body.startIndex) {
startIndex = 1;
endIndex = 1;
current.captionHistory.captions += body.text;
objectsToUpdate.push(current);
}
}
current = captionsObjects[current.captionHistory.next];
}
// looking for end index here if it wasn't in the same object as start index
if (startIndex != undefined && endIndex == undefined) {
current = captionsObjects[current.captionHistory.next];
while (current != null) {
length += current.captionHistory.captions.length;
// check to see if the endIndex is in the current object
if (length - 1 >= body.endIndex) {
endIndex = body.endIndex - (length - current.captionHistory.captions.length);
current.captionHistory.captions = current.captionHistory.captions.slice(endIndex);
objectsToUpdate.push(current);
break;
// if endIndex wasn't in the current object, that means this whole object was deleted
// initializing string to ''
} else {
current.captionHistory.captions = '';
objectsToUpdate.push(current);
}
current = captionsObjects[current.captionHistory.next];
}
}
// looking for the strings which exceed the limit and split them into multiple objects
let maxIndex = captionsObjects.length - 1;
for (let i = 0; i < objectsToUpdate.length; i++) {
if (objectsToUpdate[i].captionHistory.captions.length > CAPTION_CHUNK_LENGTH) {
// string is too large. Check if the next object exists and if it can
// accomodate the part of the string that exceeds the limits
const _nextIndex = objectsToUpdate[i].captionHistory.next;
if (_nextIndex != null &&
captionsObjects[_nextIndex].captionHistory.captions.length < CAPTION_CHUNK_LENGTH) {
const extraString = objectsToUpdate[i].captionHistory.captions.slice(CAPTION_CHUNK_LENGTH);
// could assign it directly, but our linter complained
let _captions = objectsToUpdate[i].captionHistory.captions;
_captions = _captions.slice(0, CAPTION_CHUNK_LENGTH);
objectsToUpdate[i].captionHistory.captions = _captions;
// check to see if the next object was added to objectsToUpdate array
if (objectsToUpdate[i + 1] != null &&
objectsToUpdate[i].captionHistory.next == objectsToUpdate[i + 1].captionHistory.index) {
objectsToUpdate[i + 1].captionHistory.captions = extraString +
objectsToUpdate[i + 1].captionHistory.captions;
// next object wasn't added to objectsToUpdate array, adding it from captionsObjects array.
} else {
const nextObj = captionsObjects[objectsToUpdate[i].captionHistory.next];
nextObj.captionHistory.captions = extraString + nextObj.captionHistory.captions;
objectsToUpdate.push(nextObj);
}
// next object was full already, so we create another and insert it in between them
} else {
// need to take a current object out of the objectsToUpdate and add it back after
// every other object, so that Captions collection could be updated in a proper order
const tempObj = objectsToUpdate.splice(i, 1);
let extraString = tempObj[0].captionHistory.captions.slice(CAPTION_CHUNK_LENGTH);
tempObj[0].captionHistory.captions =
tempObj[0].captionHistory.captions.slice(0, CAPTION_CHUNK_LENGTH);
maxIndex += 1;
const tempIndex = tempObj[0].captionHistory.next;
tempObj[0].captionHistory.next = maxIndex;
while (extraString.length != 0) {
const entry = {
meetingId,
locale,
captionHistory: {
locale,
ownerId: tempObj[0].captionHistory.ownerId,
captions: extraString.slice(0, CAPTION_CHUNK_LENGTH),
index: maxIndex,
next: null,
},
};
maxIndex += 1;
extraString = extraString.slice(CAPTION_CHUNK_LENGTH);
if (extraString.length > 0) {
entry.captionHistory.next = maxIndex;
} else {
entry.captionHistory.next = tempIndex;
}
objectsToUpdate.push(entry);
}
objectsToUpdate.push(tempObj[0]);
}
}
}
}
const captionsAdded = [];
objectsToUpdate.forEach((entry) => {
const { _id, meetingId, locale, captionHistory } = entry;
captionsAdded.push(addCaption(meetingId, locale, captionHistory, _id));
});
return captionsAdded;
}

View File

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

View File

@ -1,4 +0,0 @@
import { Meteor } from 'meteor/meteor';
Meteor.methods({
});

View File

@ -1,52 +0,0 @@
import { Match, check } from 'meteor/check';
import Captions from '/imports/api/2.0/captions';
import Logger from '/imports/startup/server/logger';
export default function addCaption(meetingId, locale, captionHistory, id = false) {
check(meetingId, String);
check(locale, String);
check(captionHistory, {
ownerId: String,
index: Number,
captions: String,
locale: Match.Maybe(String),
localeCode: Match.Maybe(String),
next: Match.OneOf(Number, undefined, null),
});
const selector = {
meetingId,
locale,
};
if (id) {
selector._id = id;
} else {
selector['captionHistory.index'] = captionHistory.index;
}
const modifier = {
$set: {
meetingId,
locale,
captionHistory,
},
};
const cb = (err, numChanged) => {
if (err) {
return Logger.error(`Adding caption2x to collection: ${err}`);
}
const { insertedId } = numChanged;
if (insertedId) {
return Logger.verbose(`Added caption2x locale=${locale} meeting=${meetingId}`);
}
return Logger.verbose(`Upserted caption2x locale=${locale} meeting=${meetingId}`);
};
return Captions.upsert(selector, modifier, cb);
}

View File

@ -1,10 +0,0 @@
import Captions from '/imports/api/2.0/captions';
import Logger from '/imports/startup/server/logger';
export default function clearCaptions(meetingId) {
if (meetingId) {
return Captions.remove({ meetingId }, Logger.info(`Cleared Captions2x (${meetingId})`));
}
return Captions.remove({}, Logger.info('Cleared Captions2x (all)'));
}

View File

@ -1,24 +0,0 @@
import Captions from '/imports/api/2.0/captions';
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import mapToAcl from '/imports/startup/mapToAcl';
function captions(credentials) {
const { meetingId, requesterUserId, requesterToken } = credentials;
check(meetingId, String);
check(requesterUserId, String);
check(requesterToken, String);
Logger.verbose(`Publishing Captions2x for ${meetingId} ${requesterUserId} ${requesterToken}`);
return Captions.find({ meetingId });
}
function publish(...args) {
const boundCaptions = captions.bind(this);
return mapToAcl('subscriptions.captions', boundCaptions)(args);
}
Meteor.publish('captions2x', publish);

View File

@ -1,19 +0,0 @@
import { Meteor } from 'meteor/meteor';
const Chat = new Mongo.Collection('chat2x');
if (Meteor.isServer) {
// types of queries for the chat:
// 1. meetingId, toUsername (publishers)
// 2. meetingId, fromUserId (publishers)
// 3. meetingId, toUserId (publishers)
// 4. meetingId, fromTime, fromUserId, toUserId (addChat)
// 5. meetingId (clearChat)
// 6. meetingId, fromUserId, toUserId (clearSystemMessages)
Chat._ensureIndex({ meetingId: 1, toUsername: 1 });
Chat._ensureIndex({ meetingId: 1, fromUserId: 1 });
Chat._ensureIndex({ meetingId: 1, toUserId: 1 });
}
export default Chat;

View File

@ -1,9 +0,0 @@
import RedisPubSub from '/imports/startup/server/redis2x';
import handleChatMessage from './handlers/chatMessage';
import handleChatHistory from './handlers/chatHistory';
import handleChatPublicHistoryClear from './handlers/chatPublicHistoryClear';
RedisPubSub.on('GetChatHistoryRespMsg', handleChatHistory);
RedisPubSub.on('SendPublicMessageEvtMsg', handleChatMessage);
RedisPubSub.on('SendPrivateMessageEvtMsg', handleChatMessage);
RedisPubSub.on('ClearPublicChatHistoryEvtMsg', handleChatPublicHistoryClear);

View File

@ -1,17 +0,0 @@
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;
}

View File

@ -1,11 +0,0 @@
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);
}

View File

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

View File

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

View File

@ -1,54 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import RedisPubSub from '/imports/startup/server/redis2x';
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 sendChat(credentials, message) {
const REDIS_CONFIG = Meteor.settings.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const CHAT_CONFIG = Meteor.settings.public.chat;
const TO_PUBLIC_CHAT = CHAT_CONFIG.public_username;
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.toUsername === TO_PUBLIC_CHAT) {
eventName = 'SendPublicMessagePubMsg';
}
return RedisPubSub.publishUserMessage(CHANNEL, eventName, meetingId, requesterUserId, { message: parsedMessage });
}

View File

@ -1,77 +0,0 @@
import flat from 'flat';
import Chat from '/imports/api/2.0/chat';
import Logger from '/imports/startup/server/logger';
import { Match, 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;
};
const chatType = (userName) => {
const CHAT_CONFIG = Meteor.settings.public.chat;
const typeByUser = {
[CHAT_CONFIG.system_username]: CHAT_CONFIG.type_system,
[CHAT_CONFIG.public_username]: CHAT_CONFIG.type_public,
};
return userName in typeByUser ? typeByUser[userName] : CHAT_CONFIG.type_private;
};
export default function addChat(meetingId, chat) {
check(chat, {
message: String,
fromColor: String,
toUserId: String,
toUsername: String,
fromUserId: String,
fromUsername: Match.Maybe(String),
fromTime: Number,
fromTimezoneOffset: Match.Maybe(Number),
});
const selector = {
meetingId,
fromTime: chat.fromTime,
fromUserId: chat.fromUserId,
toUserId: chat.toUserId,
};
const modifier = {
$set: Object.assign(
flat(chat, { safe: true }),
{
meetingId,
message: parseMessage(chat.message),
type: chatType(chat.toUsername),
},
),
};
const cb = (err, numChanged) => {
if (err) {
return Logger.error(`Adding chat to collection: ${err}`);
}
const { insertedId } = numChanged;
const to = chat.toUsername || 'PUBLIC';
if (insertedId) {
return Logger.info(`Added chat from=${chat.fromUsername} to=${to} time=${chat.fromTime}`);
}
return Logger.info(`Upserted chat from=${chat.fromUsername} to=${to} time=${chat.fromTime}`);
};
return Chat.upsert(selector, modifier, cb);
}

View File

@ -1,10 +0,0 @@
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)'));
}

View File

@ -1,24 +0,0 @@
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,
fromUserId: CHAT_CONFIG.type_system,
toUserId: userId,
};
return Chat.remove(selector, Logger.info(`Removing system messages from: (${userId})`));
}

View File

@ -1,40 +0,0 @@
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 { meetingId, requesterUserId, requesterToken } = credentials;
check(meetingId, String);
check(requesterUserId, String);
check(requesterToken, String);
const CHAT_CONFIG = Meteor.settings.public.chat;
const PUBLIC_CHAT_USERNAME = CHAT_CONFIG.public_username;
Logger.info(`Publishing chat for ${meetingId} ${requesterUserId} ${requesterToken}`);
return Chat.find({
$or: [
{
toUsername: PUBLIC_CHAT_USERNAME,
meetingId,
}, {
fromUserId: requesterUserId,
meetingId,
}, {
toUserId: requesterUserId,
meetingId,
},
],
});
}
function publish(...args) {
const boundChat = chat.bind(this);
return mapToAcl('subscriptions.chat', boundChat)(args);
}
Meteor.publish('chat2x', publish);