diff --git a/bigbluebutton-html5/.gitignore b/bigbluebutton-html5/.gitignore index 2df8a85420..535d3f9662 100755 --- a/bigbluebutton-html5/.gitignore +++ b/bigbluebutton-html5/.gitignore @@ -5,6 +5,7 @@ node_modules/ .meteor/dev_bundle private/config/server/* +!private/config/server/app.yaml !private/config/server/redis.yaml !private/config/server/shell.yaml diff --git a/bigbluebutton-html5/imports/api/captions/server/eventHandlers.js b/bigbluebutton-html5/imports/api/captions/server/eventHandlers.js new file mode 100644 index 0000000000..297645f310 --- /dev/null +++ b/bigbluebutton-html5/imports/api/captions/server/eventHandlers.js @@ -0,0 +1,8 @@ +import RedisPubSub from '/imports/startup/server/redis'; +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); diff --git a/bigbluebutton-html5/imports/api/captions/server/handlers/captionHistory.js b/bigbluebutton-html5/imports/api/captions/server/handlers/captionHistory.js new file mode 100644 index 0000000000..8a78677584 --- /dev/null +++ b/bigbluebutton-html5/imports/api/captions/server/handlers/captionHistory.js @@ -0,0 +1,64 @@ +import _ from 'underscore'; +import Captions from '/imports/api/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); + + let captionsAdded = []; + _.each(captionHistory, (caption, locale) => { + let ownerId = caption[0]; + let captions = caption[1].slice(0); + let 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) => { + let captionHistoryObject = { + locale, + ownerId, + captions, + index, + next: (index < chunks.length - 1) ? index + 1 : undefined, + }; + + captionsAdded.push(addCaption(meetingId, locale, captionHistoryObject)); + }); + + }); + + return captionsAdded; +}; diff --git a/bigbluebutton-html5/imports/api/captions/server/handlers/captionOwnerUpdate.js b/bigbluebutton-html5/imports/api/captions/server/handlers/captionOwnerUpdate.js new file mode 100644 index 0000000000..eff8e43427 --- /dev/null +++ b/bigbluebutton-html5/imports/api/captions/server/handlers/captionOwnerUpdate.js @@ -0,0 +1,50 @@ +import Captions from '/imports/api/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, + }; + + let 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); +}; diff --git a/bigbluebutton-html5/imports/api/captions/server/modifiers/updateCaptionsCollection.js b/bigbluebutton-html5/imports/api/captions/server/handlers/captionUpdate.js old mode 100755 new mode 100644 similarity index 83% rename from bigbluebutton-html5/imports/api/captions/server/modifiers/updateCaptionsCollection.js rename to bigbluebutton-html5/imports/api/captions/server/handlers/captionUpdate.js index db8c4ed28f..a655575264 --- a/bigbluebutton-html5/imports/api/captions/server/modifiers/updateCaptionsCollection.js +++ b/bigbluebutton-html5/imports/api/captions/server/handlers/captionUpdate.js @@ -1,11 +1,21 @@ import Captions from '/imports/api/captions'; -import { logger } from '/imports/startup/server/logger'; +import Logger from '/imports/startup/server/logger'; +import { check } from 'meteor/check'; +import addCaption from '../modifiers/addCaption'; -export function updateCaptionsCollection(meetingId, locale, payload) { +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); let captionsObjects = Captions.find({ - meetingId: meetingId, - locale: locale, + meetingId, + locale, }, { sort: { locale: 1, @@ -93,18 +103,18 @@ export function updateCaptionsCollection(meetingId, locale, payload) { //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 > 1000) { + 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 let _nextIndex = objectsToUpdate[i].captionHistory.next; if (_nextIndex != null && - captionsObjects[_nextIndex].captionHistory.captions.length < 1000) { + captionsObjects[_nextIndex].captionHistory.captions.length < CAPTION_CHUNK_LENGTH) { - let extraString = objectsToUpdate[i].captionHistory.captions.slice(1000); + let 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, 1000); + _captions = _captions.slice(0, CAPTION_CHUNK_LENGTH); objectsToUpdate[i].captionHistory.captions = _captions; //check to see if the next object was added to objectsToUpdate array @@ -125,8 +135,8 @@ export function updateCaptionsCollection(meetingId, locale, payload) { //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 let tempObj = objectsToUpdate.splice(i, 1); - let extraString = tempObj[0].captionHistory.captions.slice(1000); - tempObj[0].captionHistory.captions = tempObj[0].captionHistory.captions.slice(0, 1000); + 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; let tempIndex = tempObj[0].captionHistory.next; @@ -139,13 +149,13 @@ export function updateCaptionsCollection(meetingId, locale, payload) { captionHistory: { locale: locale, ownerId: tempObj[0].captionHistory.ownerId, - captions: extraString.slice(0, 1000), + captions: extraString.slice(0, CAPTION_CHUNK_LENGTH), index: maxIndex, next: null, }, }; maxIndex += 1; - extraString = extraString.slice(1000); + extraString = extraString.slice(CAPTION_CHUNK_LENGTH); if (extraString.length > 0) { entry.captionHistory.next = maxIndex; } else { @@ -161,25 +171,11 @@ export function updateCaptionsCollection(meetingId, locale, payload) { } } - //updating the database - for (i = 0; i < objectsToUpdate.length; i++) { - Captions.upsert( - { - _id: objectsToUpdate[i]._id, - meetingId: objectsToUpdate[i].meetingId, - locale: objectsToUpdate[i].locale, - }, - { - $set: { - meetingId: meetingId, - locale: locale, - 'captionHistory.locale': locale, - 'captionHistory.ownerId': objectsToUpdate[i].captionHistory.ownerId, - 'captionHistory.captions': objectsToUpdate[i].captionHistory.captions, - 'captionHistory.next': objectsToUpdate[i].captionHistory.next, - 'captionHistory.index': objectsToUpdate[i].captionHistory.index, - }, - } - ); - } + let captionsAdded = []; + objectsToUpdate.forEach(entry => { + const { _id, meetingId, locale, captionHistory } = entry; + captionsAdded.push(addCaption(meetingId, locale, captionHistory, _id)); + }); + + return captionsAdded; }; diff --git a/bigbluebutton-html5/imports/api/captions/server/index.js b/bigbluebutton-html5/imports/api/captions/server/index.js new file mode 100644 index 0000000000..92451ac76b --- /dev/null +++ b/bigbluebutton-html5/imports/api/captions/server/index.js @@ -0,0 +1,3 @@ +import './eventHandlers'; +import './methods'; +import './publishers'; diff --git a/bigbluebutton-html5/imports/api/captions/server/methods.js b/bigbluebutton-html5/imports/api/captions/server/methods.js new file mode 100644 index 0000000000..1ce65c3698 --- /dev/null +++ b/bigbluebutton-html5/imports/api/captions/server/methods.js @@ -0,0 +1,4 @@ +import { Meteor } from 'meteor/meteor'; + +Meteor.methods({ +}); diff --git a/bigbluebutton-html5/imports/api/captions/server/modifiers/addCaption.js b/bigbluebutton-html5/imports/api/captions/server/modifiers/addCaption.js new file mode 100644 index 0000000000..b848e779ca --- /dev/null +++ b/bigbluebutton-html5/imports/api/captions/server/modifiers/addCaption.js @@ -0,0 +1,49 @@ +import { check } from 'meteor/check'; +import Captions from '/imports/api/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); + + const selector = { + meetingId, + locale, + }; + + if (id) { + selector._id = id; + } else { + selector['captionHistory.index'] = captionHistory.index; + } + + let modifier = { + $set: { + meetingId, + locale, + 'captionHistory.locale': locale, + 'captionHistory.ownerId': captionHistory.ownerId, + 'captionHistory.captions':captionHistory.captions, + 'captionHistory.next': captionHistory.next, + 'captionHistory.index': captionHistory.index, + }, + }; + + const cb = (err, numChanged) => { + if (err) { + return Logger.error(`Adding caption to collection: ${err}`); + } + + const { insertedId } = numChanged; + if (insertedId) { + return Logger.verbose(`Added caption locale=${locale} meeting=${meetingId}`); + } + + if (numChanged) { + return Logger.verbose(`Upserted caption locale=${locale} meeting=${meetingId}`); + } + }; + + return Captions.upsert(selector, modifier, cb); +}; diff --git a/bigbluebutton-html5/imports/api/captions/server/modifiers/addCaptionsToCollection.js b/bigbluebutton-html5/imports/api/captions/server/modifiers/addCaptionsToCollection.js deleted file mode 100755 index 75664d398f..0000000000 --- a/bigbluebutton-html5/imports/api/captions/server/modifiers/addCaptionsToCollection.js +++ /dev/null @@ -1,46 +0,0 @@ -import Captions from '/imports/api/captions'; -import { logger } from '/imports/startup/server/logger'; - -export function addCaptionsToCollection(meetingId, locale, captionHistory) { - let captionsString = captionHistory[1].slice(0); - let captions = []; - - if (captionsString.length > 0) { - while (captionsString.length > 0) { - if (captionsString.length > 1000) { - captions.push(captionsString.slice(0, 1000)); - captionsString = captionsString.slice(1000); - } else { - captions.push(captionsString); - captionsString = captionsString.slice(captionsString.length); - } - } - } else { - captions.push(''); - } - - let i; - let next; - for (i = 0; i < captions.length; i++) { - if (i < captions.length - 1) { - next = i + 1; - } else { - next = undefined; - } - - const entry = { - meetingId: meetingId, - locale: locale, - captionHistory: { - locale: locale, - ownerId: captionHistory[0], - captions: captions[i], - index: i, - next: next, - }, - }; - Captions.insert(entry); - } - - logger.info(`added captions locale=[${locale}]:meetingId=[${meetingId}].`); -}; diff --git a/bigbluebutton-html5/imports/api/captions/server/modifiers/clearCaptions.js b/bigbluebutton-html5/imports/api/captions/server/modifiers/clearCaptions.js new file mode 100644 index 0000000000..5427179324 --- /dev/null +++ b/bigbluebutton-html5/imports/api/captions/server/modifiers/clearCaptions.js @@ -0,0 +1,10 @@ +import Captions from '/imports/api/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({}, Logger.info('Cleared Captions (all)')); +}; diff --git a/bigbluebutton-html5/imports/api/captions/server/modifiers/clearCaptionsCollection.js b/bigbluebutton-html5/imports/api/captions/server/modifiers/clearCaptionsCollection.js deleted file mode 100644 index 1beeeb6c1d..0000000000 --- a/bigbluebutton-html5/imports/api/captions/server/modifiers/clearCaptionsCollection.js +++ /dev/null @@ -1,14 +0,0 @@ -import Captions from '/imports/api/captions'; -import { logger } from '/imports/startup/server/logger'; - -// called on server start and meeting end -export function clearCaptionsCollection() { - const meetingId = arguments[0]; - if (meetingId != null) { - return Captions.remove({ - meetingId: meetingId, - }, logger.info(`cleared Captions Collection (meetingId: ${meetingId}!`)); - } else { - return Captions.remove({}, logger.info('cleared Captions Collection (all meetings)!')); - } -}; diff --git a/bigbluebutton-html5/imports/api/captions/server/modifiers/eventHandlers.js b/bigbluebutton-html5/imports/api/captions/server/modifiers/eventHandlers.js deleted file mode 100755 index ce9282fea5..0000000000 --- a/bigbluebutton-html5/imports/api/captions/server/modifiers/eventHandlers.js +++ /dev/null @@ -1,79 +0,0 @@ -import { eventEmitter } from '/imports/startup/server'; -import { inReplyToHTML5Client } from '/imports/api/common/server/helpers'; -import { addCaptionsToCollection } from './addCaptionsToCollection'; -import { updateCaptionsCollection } from './updateCaptionsCollection'; -import Meetings from '/imports/api/meetings'; -import Captions from '/imports/api/captions'; -import Logger from '/imports/startup/server/logger'; - -eventEmitter.on('send_caption_history_reply_message', function (arg) { - Logger.debug('message', JSON.stringify(arg)); - if (inReplyToHTML5Client(arg)) { - const meetingId = arg.payload.meeting_id; - if (Captions.findOne({ - meetingId: meetingId, - }) == null) { - const captionHistory = arg.payload.caption_history; - for (let locale in captionHistory) { - addCaptionsToCollection(meetingId, locale, captionHistory[locale]); - } - } - } - - return arg.callback(); -}); - -eventEmitter.on('update_caption_owner_message', function (arg) { - Logger.debug(JSON.stringify(arg)); - const meetingId = arg.payload.meeting_id; - let payload = arg.payload; - - if (Captions.findOne({ - meetingId: meetingId, - locale: payload.locale, - }) != null) { - Captions.update( - { - meetingId: meetingId, - locale: payload.locale, - }, - { - $set: { - 'captionHistory.ownerId': payload.owner_id, - }, - }, - { - multi: true, - } - ); - } else { - const entry = { - meetingId: meetingId, - locale: payload.locale, - captionHistory: { - locale: payload.locale, - ownerId: payload.owner_id, - captions: '', - index: 0, - length: 0, - next: null, - }, - }; - Captions.insert(entry); - } - - return arg.callback(); -}); - -eventEmitter.on('edit_caption_history_message', function (arg) { - Logger.debug('edit_caption_history_message ' + JSON.stringify(arg)); - let payload = arg.payload; - let meetingId = payload.meeting_id; - let locale = payload.locale; - - if (meetingId != null) { - updateCaptionsCollection(meetingId, locale, payload); - } - - return arg.callback(); -}); diff --git a/bigbluebutton-html5/imports/api/captions/server/publications.js b/bigbluebutton-html5/imports/api/captions/server/publications.js deleted file mode 100644 index f8f5e3b5f5..0000000000 --- a/bigbluebutton-html5/imports/api/captions/server/publications.js +++ /dev/null @@ -1,16 +0,0 @@ -import Captions from '/imports/api/captions'; -import { isAllowedTo } from '/imports/startup/server/userPermissions'; -import { logger } from '/imports/startup/server/logger'; - -Meteor.publish('captions', function (credentials) { - if (isAllowedTo('subscribeCaptions', credentials)) { - const { meetingId, requesterUserId, requesterToken } = credentials; - logger.info(`publishing captions for ${meetingId} ${requesterUserId} ${requesterToken}`); - - return Captions.find({ - meetingId: meetingId, - }); - } else { - this.error(new Meteor.Error(402, "The user was not authorized to subscribe for 'captions'")); - } -}); diff --git a/bigbluebutton-html5/imports/api/captions/server/publishers.js b/bigbluebutton-html5/imports/api/captions/server/publishers.js new file mode 100644 index 0000000000..901fb3e4f9 --- /dev/null +++ b/bigbluebutton-html5/imports/api/captions/server/publishers.js @@ -0,0 +1,22 @@ +import Captions from '/imports/api/captions'; +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; +import Logger from '/imports/startup/server/logger'; +import { isAllowedTo } from '/imports/startup/server/userPermissions'; + +Meteor.publish('captions', function (credentials) { + // TODO: Some publishers have ACL and others dont + // if (isAllowedTo('subscribeCaptions', credentials)) { + // this.error(new Meteor.Error(402, "The user was not authorized to subscribe for 'captions'")); + // } + + const { meetingId, requesterUserId, requesterToken } = credentials; + + check(meetingId, String); + check(requesterUserId, String); + check(requesterToken, String); + + Logger.verbose(`Publishing Captions for ${meetingId} ${requesterUserId} ${requesterToken}`); + + return Captions.find({ meetingId }); +}); diff --git a/bigbluebutton-html5/imports/api/meetings/server/modifiers/clearMeetings.js b/bigbluebutton-html5/imports/api/meetings/server/modifiers/clearMeetings.js index ec4e875abb..fc450369a7 100755 --- a/bigbluebutton-html5/imports/api/meetings/server/modifiers/clearMeetings.js +++ b/bigbluebutton-html5/imports/api/meetings/server/modifiers/clearMeetings.js @@ -9,13 +9,12 @@ import clearShapes from '/imports/api/shapes/server/modifiers/clearShapes'; import clearSlides from '/imports/api/slides/server/modifiers/clearSlides'; import clearPolls from '/imports/api/polls/server/modifiers/clearPolls'; import clearCursor from '/imports/api/cursor/server/modifiers/clearCursor'; -import { clearCaptionsCollection } - from '/imports/api/captions/server/modifiers/clearCaptionsCollection'; +import clearCaptions from '/imports/api/captions/server/modifiers/clearCaptions'; import clearPresentations from '/imports/api/presentations/server/modifiers/clearPresentations'; export default function clearMeetings() { return Meetings.remove({}, (err) => { - clearCaptionsCollection(); + clearCaptions(); clearChats(); clearCursor(); clearPresentations(); diff --git a/bigbluebutton-html5/imports/api/meetings/server/modifiers/removeMeeting.js b/bigbluebutton-html5/imports/api/meetings/server/modifiers/removeMeeting.js index 36ddf26005..7db9212fce 100755 --- a/bigbluebutton-html5/imports/api/meetings/server/modifiers/removeMeeting.js +++ b/bigbluebutton-html5/imports/api/meetings/server/modifiers/removeMeeting.js @@ -8,8 +8,7 @@ import clearShapes from '/imports/api/shapes/server/modifiers/clearShapes'; import clearSlides from '/imports/api/slides/server/modifiers/clearSlides'; import clearPolls from '/imports/api/polls/server/modifiers/clearPolls'; import clearCursor from '/imports/api/cursor/server/modifiers/clearCursor'; -import { clearCaptionsCollection } - from '/imports/api/captions/server/modifiers/clearCaptionsCollection'; +import clearCaptions from '/imports/api/captions/server/modifiers/clearCaptions'; import clearPresentations from '/imports/api/presentations/server/modifiers/clearPresentations'; export default function removeMeeting(meetingId) { @@ -25,7 +24,7 @@ export default function removeMeeting(meetingId) { } if (numChanged) { - clearCaptionsCollection(meetingId); + clearCaptions(meetingId); clearChats(meetingId); clearCursor(meetingId); clearPresentations(meetingId); diff --git a/bigbluebutton-html5/imports/ui/components/app/component.jsx b/bigbluebutton-html5/imports/ui/components/app/component.jsx index 1a8d49d3ec..756a25b9cb 100755 --- a/bigbluebutton-html5/imports/ui/components/app/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/app/component.jsx @@ -209,10 +209,10 @@ export default class App extends Component {
{this.renderNavBar()} {this.renderMedia()} - {this.renderClosedCaptions()} {this.renderActionsBar()}
{this.renderSidebar()} + {this.renderClosedCaptions()} {this.renderAudioElement()} {this.renderModal()} diff --git a/bigbluebutton-html5/private/config/server/app.yaml b/bigbluebutton-html5/private/config/server/app.yaml new file mode 100644 index 0000000000..83d4aaaec7 --- /dev/null +++ b/bigbluebutton-html5/private/config/server/app.yaml @@ -0,0 +1,2 @@ +app: + captionsChunkLength: 1000 diff --git a/bigbluebutton-html5/server/main.js b/bigbluebutton-html5/server/main.js index ea6ded8183..dc6b1b5095 100755 --- a/bigbluebutton-html5/server/main.js +++ b/bigbluebutton-html5/server/main.js @@ -23,9 +23,7 @@ import '/imports/api/shapes/server'; import '/imports/api/slides/server'; -import '/imports/api/captions/server/publications'; -import '/imports/api/captions/server/modifiers/clearCaptionsCollection'; -import '/imports/api/captions/server/modifiers/eventHandlers'; +import '/imports/api/captions/server'; import '/imports/api/users/server/publications'; import '/imports/api/users/server/methods/kickUser';