Merge pull request #3684 from oswaldoacauan/refactor-api-users

[HTML5] Refactor API Users
This commit is contained in:
Anton Georgiev 2017-03-02 11:32:02 -05:00 committed by GitHub
commit d83c391997
52 changed files with 1018 additions and 1136 deletions

View File

@ -2,7 +2,7 @@ import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import Meetings from '/imports/api/meetings';
import { Meteor } from 'meteor/meteor';
import handleLockingMic from '/imports/api/users/server/modifiers/handleLockingMic';
import lockAllViewersMic from '/imports/api/users/server/modifiers/lockAllViewersMic';
export default function handlePermissionSettingsChange({ payload }) {
const meetingId = payload.meeting_id;
@ -41,7 +41,7 @@ export default function handlePermissionSettingsChange({ payload }) {
}
if (permissions.disableMic) {
handleLockingMic(meetingId, permissions);
lockAllViewersMic(meetingId);
}
if (numChanged) {

View File

@ -2,7 +2,7 @@ import Meetings from '/imports/api/chat';
import Logger from '/imports/startup/server/logger';
import removeMeeting from './removeMeeting';
import { clearUsersCollection } from '/imports/api/users/server/modifiers/clearUsersCollection';
import clearUsers from '/imports/api/users/server/modifiers/clearUsers';
import clearChats from '/imports/api/chat/server/modifiers/clearChats';
import clearBreakouts from '/imports/api/breakouts/server/modifiers/clearBreakouts';
import clearShapes from '/imports/api/shapes/server/modifiers/clearShapes';
@ -22,7 +22,7 @@ export default function clearMeetings() {
clearPolls();
clearShapes();
clearSlides();
clearUsersCollection();
clearUsers();
return Logger.info('Cleared Meetings (all)');
});

View File

@ -2,7 +2,7 @@ import { check } from 'meteor/check';
import Meetings from '/imports/api/meetings';
import Logger from '/imports/startup/server/logger';
import { clearUsersCollection } from '/imports/api/users/server/modifiers/clearUsersCollection';
import clearUsers from '/imports/api/users/server/modifiers/clearUsers';
import clearChats from '/imports/api/chat/server/modifiers/clearChats';
import clearShapes from '/imports/api/shapes/server/modifiers/clearShapes';
import clearSlides from '/imports/api/slides/server/modifiers/clearSlides';
@ -31,7 +31,7 @@ export default function removeMeeting(meetingId) {
clearPolls(meetingId);
clearShapes(meetingId);
clearSlides(meetingId);
clearUsersCollection(meetingId);
clearUsers(meetingId);
return Logger.info(`Removed meeting id=${meetingId}`);
}

View File

@ -1,3 +1,4 @@
// TODO: This file should be a `service.js` somewhere in the /ui folder
import Users from '/imports/api/users';
import Meetings from '/imports/api/meetings';
import Auth from '/imports/ui/services/auth';

View File

@ -1,21 +0,0 @@
import { publish } from '/imports/api/common/server/helpers';
import { isAllowedTo } from '/imports/startup/server/userPermissions';
import { appendMessageHeader } from '/imports/api/common/server/helpers';
import { logger } from '/imports/startup/server/logger';
import Meetings from '/imports/api/meetings';
export default function getStun(credentials) {
const REDIS_CONFIG = Meteor.settings.redis;
const { meetingId, requesterUserId } = credentials;
const eventName = 'send_stun_turn_info_request_message';
let message = {
payload: {
meeting_id: meetingId,
requester_id: requesterUserId,
},
};
message = appendMessageHeader(eventName, message);
return publish(REDIS_CONFIG.channels.fromBBBUsers, message);
};

View File

@ -1,41 +0,0 @@
import { eventEmitter } from '/imports/startup/server';
import { updateVoiceUser } from '/imports/api/users/server/modifiers/updateVoiceUser';
eventEmitter.on('user_left_voice_message', function (arg) {
handleVoiceEvent(arg);
});
eventEmitter.on('user_joined_voice_message', function (arg) {
handleVoiceEvent(arg);
});
eventEmitter.on('user_voice_talking_message', function (arg) {
handleVoiceEvent(arg);
});
eventEmitter.on('user_voice_muted_message', function (arg) {
handleVoiceEvent(arg);
});
eventEmitter.on('user_listening_only', function (arg) {
const voiceUserObj = {
web_userid: arg.payload.userid,
listen_only: arg.payload.listen_only,
};
const meetingId = arg.payload.meeting_id;
return updateVoiceUser(meetingId, voiceUserObj, arg.callback);
});
const handleVoiceEvent = function (arg) {
const meetingId = arg.payload.meeting_id;
const voiceUser = arg.payload.user.voiceUser;
const voiceUserObj = {
web_userid: voiceUser.web_userid,
listen_only: arg.payload.listen_only,
talking: voiceUser.talking,
joined: voiceUser.joined,
locked: voiceUser.locked,
muted: voiceUser.muted,
};
return updateVoiceUser(meetingId, voiceUserObj, arg.callback);
};

View File

@ -0,0 +1,26 @@
import RedisPubSub from '/imports/startup/server/redis';
import handleGetUsers from './handlers/getUsers';
import handleRemoveUser from './handlers/removeUser';
import handlePresenterAssigned from './handlers/presenterAssigned';
import handleEmojiStatus from './handlers/emojiStatus';
import handleLockedStatusChange from './handlers/lockedStatusChange';
import handleUserJoined from './handlers/userJoined';
import handleValidateAuthToken from './handlers/validateAuthToken';
import handleVoiceUpdate from './handlers/voiceUpdate';
import handleListeningOnly from './handlers/listeningOnly';
RedisPubSub.on('validate_auth_token_reply', handleValidateAuthToken);
RedisPubSub.on('get_users_reply', handleGetUsers);
RedisPubSub.on('user_joined_message', handleUserJoined);
RedisPubSub.on('user_eject_from_meeting', handleRemoveUser);
RedisPubSub.on('disconnect_user_message', handleRemoveUser);
RedisPubSub.on('user_left_message', handleRemoveUser);
RedisPubSub.on('presenter_assigned_message', handlePresenterAssigned);
RedisPubSub.on('user_emoji_status_message', handleEmojiStatus);
RedisPubSub.on('user_locked_message', handleLockedStatusChange);
RedisPubSub.on('user_unlocked_message', handleLockedStatusChange);
RedisPubSub.on('user_left_voice_message', handleVoiceUpdate);
RedisPubSub.on('user_joined_voice_message', handleVoiceUpdate);
RedisPubSub.on('user_voice_talking_message', handleVoiceUpdate);
RedisPubSub.on('user_voice_muted_message', handleVoiceUpdate);
RedisPubSub.on('user_listening_only', handleListeningOnly);

View File

@ -0,0 +1,37 @@
import Logger from '/imports/startup/server/logger';
import { check } from 'meteor/check';
import Users from '/imports/api/users';
export default function handleEmojiStatus({ payload }) {
const meetingId = payload.meeting_id;
const userId = payload.userid;
const status = payload.emoji_status;
check(meetingId, String);
check(userId, String);
check(status, String);
const selector = {
meetingId,
userId,
};
const modifier = {
$set: {
'user.set_emoji_time': (new Date()).getTime(),
'user.emoji_status': status,
},
};
const cb = (err, numChanged) => {
if (err) {
return Logger.error(`Assigning user emoji status: ${err}`);
}
if (numChanged) {
return Logger.info(`Assigned user emoji status '${status}' id=${userId} meeting=${meetingId}`);
}
};
return Users.update(selector, modifier, cb);
};

View File

@ -0,0 +1,35 @@
import { check } from 'meteor/check';
import { inReplyToHTML5Client } from '/imports/api/common/server/helpers';
import Logger from '/imports/startup/server/logger';
import Users from '/imports/api/users';
import addUser from '../modifiers/addUser';
import removeUser from '../modifiers/removeUser';
export default function handleGetUsers({ payload }) {
if (!inReplyToHTML5Client({ payload })) {
return;
}
const meetingId = payload.meeting_id;
const users = payload.users;
check(meetingId, String);
check(users, Array);
const usersIds = users.map(m => m.userid);
const usersToRemove = Users.find({
meetingId,
userId: { $nin: usersIds },
}).fetch();
usersToRemove.forEach(user => removeUser(meetingId, user.userId));
let usersAdded = [];
users.forEach(user => {
usersAdded.push(addUser(meetingId, user));
});
return usersAdded;
};

View File

@ -0,0 +1,36 @@
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import Users from '/imports/api/users';
export default function handleListeningOnly({ payload }) {
const meetingId = payload.meeting_id;
const userId = payload.userid;
const listenOnly = payload.listen_only;
check(meetingId, String);
check(userId, String);
check(listenOnly, Boolean);
const selector = {
meetingId,
userId,
};
const modifier = {
$set: {
'user.listenOnly': listenOnly,
},
};
const cb = (err, numChanged) => {
if (err) {
return Logger.error(`Assigning user listen only status: ${err}`);
}
if (numChanged) {
return Logger.info(`Assigned listen only status '${listenOnly}' user=${userId} meeting=${meetingId}`);
}
};
return Users.update(selector, modifier, cb);
};

View File

@ -0,0 +1,58 @@
import Logger from '/imports/startup/server/logger';
import { check } from 'meteor/check';
import Users from '/imports/api/users';
import muteToggle from '../methods/muteToggle';
export default function handleLockedStatusChange({ payload }) {
const meetingId = payload.meeting_id;
const userId = payload.userid;
const isLocked = arg.payload.locked;
check(meetingId, String);
check(userId, String);
check(isLocked, String);
const selector = {
meetingId,
userId,
};
const User = Users.findOne(selector);
if (!User) {
throw new Meteor.Error(
'user-not-found', `You need a valid user to be able to set presenter`);
}
const modifier = {
$set: {
'user.locked': isLocked,
},
};
const cb = (err, numChanged) => {
if (err) {
return Logger.error(`Assigning user locked status: ${err}`);
}
if (numChanged) {
if (User.user.role === 'VIEWER'
&& !User.user.listenOnly
&& !User.user.voiceUser.muted
&& User.user.voiceUser.joined
&& isLocked) {
const credentials = {
meetingId,
requesterUserId: userId,
};
muteToggle(credentials, userId, true);
}
return Logger.info(`Assigned locked status '${isLocked ? 'locked' : 'unlocked'}' id=${newPresenterId} meeting=${meetingId}`);
}
};
return Users.update(selector, modifier, cb);
};

View File

@ -0,0 +1,61 @@
import Logger from '/imports/startup/server/logger';
import { check } from 'meteor/check';
import Users from '/imports/api/users';
export default function handlePresenterAssigned({ payload }) {
const meetingId = payload.meeting_id;
const newPresenterId = payload.new_presenter_id;
check(meetingId, String);
check(newPresenterId, String);
const selector = {
meetingId,
userId: newPresenterId,
};
const modifier = {
$set: {
'user.presenter': true,
},
};
const cb = (err, numChanged) => {
if (err) {
return Logger.error(`Assigning user as presenter: ${err}`);
}
if (numChanged) {
unassignCurrentPresenter(meetingId, newPresenterId);
return Logger.info(`Assigned user as presenter id=${newPresenterId} meeting=${meetingId}`);
}
};
return Users.update(selector, modifier, cb);
};
const unassignCurrentPresenter = (meetingId, newPresenterId) => {
const selector = {
meetingId,
userId: { $ne: newPresenterId },
'user.presenter': true,
};
const modifier = {
$set: {
'user.presenter': false,
},
};
const cb = (err, numChanged) => {
if (err) {
return Logger.error(`Unassigning current presenter from collection: ${err}`);
}
if (numChanged) {
return Logger.info(`Unassign current presenter meeting=${meetingId}`);
}
};
return Users.update(selector, modifier, cb);
};

View File

@ -0,0 +1,14 @@
import Logger from '/imports/startup/server/logger';
import { check } from 'meteor/check';
import removeUser from '../modifiers/removeUser';
export default function handleRemoveUser({ payload }) {
const meetingId = payload.meeting_id;
const userId = payload.userid || payload.user.userid;
check(meetingId, String);
check(userId, String);
return removeUser(meetingId, userId);
};

View File

@ -0,0 +1,14 @@
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import addUser from '../modifiers/addUser';
export default function handleUserJoined({ payload }) {
const meetingId = payload.meeting_id;
const user = payload.user;
check(meetingId, String);
check(user, Object);
return addUser(meetingId, user);
};

View File

@ -0,0 +1,66 @@
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import Meetings from '/imports/api/meetings';
import Users from '/imports/api/users';
import addChat from '/imports/api/chat/server/modifiers/addChat';
export default function handleValidateAuthToken({ payload }) {
const meetingId = payload.meeting_id;
const userId = payload.userid;
const validStatus = payload.valid;
check(meetingId, String);
check(userId, String);
check(validStatus, String);
const selector = {
meetingId,
userId,
};
const modifier = {
$set: {
validated: validStatus,
},
};
const cb = (err, numChanged) => {
if (err) {
return Logger.error(`Validating auth token: ${err}`);
}
if (numChanged) {
if (validStatus) {
addWelcomeChatMessage(meetingId, userId);
}
return Logger.info(`Validated auth token as '${validStatus}' user=${userId} meeting=${meetingId}`);
}
};
return Users.update(selector, modifier, cb);
};
const addWelcomeChatMessage = (meetingId, userId) => {
const APP_CONFIG = Meteor.settings.public.app;
const CHAT_CONFIG = Meteor.settings.public.chat;
const Meeting = Meetings.findOne({ meetingId });
let welcomeMessage = APP_CONFIG.defaultWelcomeMessage
.concat(APP_CONFIG.defaultWelcomeMessageFooter)
.replace(/%%CONFNAME%%/, Meeting.meetingName);
const message = {
chat_type: CHAT_CONFIG.type_system,
message: welcomeMessage,
from_color: '0x3399FF',
to_userid: userId,
from_userid: CHAT_CONFIG.type_system,
from_username: '',
from_time: (new Date()).getTime(),
};
return addChat(meetingId, message);
};

View File

@ -0,0 +1,20 @@
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import updateVoiceUser from '../modifiers/updateVoiceUser';
export default function handleVoiceUpdate({ payload }) {
const meetingId = payload.meeting_id;
const user = payload.user;
check(meetingId, String);
check(user, Object);
const voiceUser = user.voiceUser;
check(voiceUser, Object);
const userId = voiceUser.web_userid;
check(userId, String);
return updateVoiceUser(meetingId, userId, voiceUser);
};

View File

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

View File

@ -0,0 +1,17 @@
import { Meteor } from 'meteor/meteor';
import kickUser from './methods/kickUser';
import listenOnlyToggle from './methods/listenOnlyToggle';
import userLogout from './methods/userLogout';
import assignPresenter from './methods/assignPresenter';
import muteToggle from './methods/muteToggle';
import setEmojiStatus from './methods/setEmojiStatus';
Meteor.methods({
kickUser,
listenOnlyToggle,
userLogout,
assignPresenter,
setEmojiStatus,
muteUser: (...args) => muteToggle(...args, true),
unmuteUser: (...args) => muteToggle(...args, false),
});

View File

@ -0,0 +1,42 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import RedisPubSub from '/imports/startup/server/redis';
import Logger from '/imports/startup/server/logger';
import { isAllowedTo } from '/imports/startup/server/userPermissions';
import Users from '/imports/api/users';
export default function assignPresenter(credentials, userId) {
const REDIS_CONFIG = Meteor.settings.redis;
const CHANNEL = REDIS_CONFIG.channels.toBBBApps.users;
const EVENT_NAME = 'assign_presenter_request_message';
const { meetingId, requesterUserId } = credentials;
check(meetingId, String);
check(requesterUserId, String);
check(userId, String);
if (!isAllowedTo('setPresenter', credentials)) {
throw new Meteor.Error('not-allowed', `You are not allowed to setPresenter`);
}
const User = Users.findOne({
meetingId,
userId,
});
if (!User) {
throw new Meteor.Error(
'user-not-found', `You need a valid user to be able to set presenter`);
}
let payload = {
new_presenter_id: userId,
new_presenter_name: User.user.name,
meeting_id: meetingId,
assigned_by: requesterUserId,
};
Logger.verbose(`User '${userId}' setted as presenter by '${requesterUserId}' from meeting '${meetingId}'`);
return RedisPubSub.publish(CHANNEL, EVENT_NAME, payload);
};

View File

@ -1,25 +1,31 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import RedisPubSub from '/imports/startup/server/redis';
import Logger from '/imports/startup/server/logger';
import { isAllowedTo } from '/imports/startup/server/userPermissions';
import { appendMessageHeader, publish } from '/imports/api/common/server/helpers';
Meteor.methods({
//meetingId: the meeting where the user is
//toKickUserId: the userid of the user to kick
//requesterUserId: the userid of the user that wants to kick
//authToken: the authToken of the user that wants to kick
kickUser(credentials, toKickUserId) {
const REDIS_CONFIG = Meteor.settings.redis;
const { meetingId, requesterUserId, requesterToken } = credentials;
let message;
if (isAllowedTo('kickUser', credentials)) {
message = {
payload: {
userid: toKickUserId,
ejected_by: requesterUserId,
meeting_id: meetingId,
},
};
message = appendMessageHeader('eject_user_from_meeting_request_message', message);
return publish(REDIS_CONFIG.channels.toBBBApps.users, message);
}
},
});
export default function kickUser(credentials, userId) {
const REDIS_CONFIG = Meteor.settings.redis;
const CHANNEL = REDIS_CONFIG.channels.toBBBApps.users;
const EVENT_NAME = 'eject_user_from_meeting_request_message';
const { meetingId, requesterUserId } = credentials;
check(meetingId, String);
check(requesterUserId, String);
check(userId, String);
if (!isAllowedTo('kickUser', credentials)) {
throw new Meteor.Error('not-allowed', `You are not allowed to kickUser`);
}
let payload = {
userid: userId,
ejected_by: requesterUserId,
meeting_id: meetingId,
};
Logger.verbose(`User '${userId}' was kicked by '${requesterUserId}' from meeting '${meetingId}'`);
return RedisPubSub.publish(CHANNEL, EVENT_NAME, payload);
};

View File

@ -1,70 +0,0 @@
import { isAllowedTo } from '/imports/startup/server/userPermissions';
import { appendMessageHeader, publish } from '/imports/api/common/server/helpers';
import Meetings from '/imports/api/meetings';
import Users from '/imports/api/users';
import { logger } from '/imports/startup/server/logger';
Meteor.methods({
// meetingId: the meetingId of the meeting the user is in
// toSetUserId: the userId of the user joining
// requesterUserId: the userId of the requester
// requesterToken: the authToken of the requester
listenOnlyRequestToggle(credentials, isJoining) {
const REDIS_CONFIG = Meteor.settings.redis;
let username;
let voiceConf;
const { meetingId, requesterUserId, requesterToken } = credentials;
const meetingObject = Meetings.findOne({
meetingId: meetingId,
});
const userObject = Users.findOne({
meetingId: meetingId,
userId: requesterUserId,
});
if (meetingObject != null) {
voiceConf = meetingObject.voiceConf;
}
if (userObject != null) {
username = userObject.user.name;
}
if (isJoining) {
if (isAllowedTo('joinListenOnly', credentials)) {
let message = {
payload: {
userid: requesterUserId,
meeting_id: meetingId,
voice_conf: voiceConf,
name: username,
},
};
message = appendMessageHeader('user_connected_to_global_audio', message);
logger.info(
`publishing a user listenOnly toggleRequest ${isJoining} ` +
`request for ${requesterUserId}`
);
publish(REDIS_CONFIG.channels.toBBBApps.meeting, message);
}
} else {
if (isAllowedTo('leaveListenOnly', credentials)) {
let message = {
payload: {
userid: requesterUserId,
meeting_id: meetingId,
voice_conf: voiceConf,
name: username,
},
};
message = appendMessageHeader('user_disconnected_from_global_audio', message);
logger.info(
`publishing a user listenOnly toggleRequest ${isJoining} ` +
`request for ${requesterUserId}`
);
publish(REDIS_CONFIG.channels.toBBBApps.meeting, message);
}
}
},
});

View File

@ -0,0 +1,60 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import RedisPubSub from '/imports/startup/server/redis';
import Logger from '/imports/startup/server/logger';
import { isAllowedTo } from '/imports/startup/server/userPermissions';
import Meetings from '/imports/api/meetings';
import Users from '/imports/api/users';
export default function listenOnlyToggle(credentials, isJoining = true) {
const REDIS_CONFIG = Meteor.settings.redis;
const CHANNEL = REDIS_CONFIG.channels.toBBBApps.meeting;
const { meetingId, requesterUserId } = credentials;
check(meetingId, String);
check(requesterUserId, String);
check(isJoining, Boolean);
if (isJoining) {
let EVENT_NAME = 'user_connected_to_global_audio';
if (!isAllowedTo('joinListenOnly', credentials)) {
throw new Meteor.Error('not-allowed', `You are not allowed to joinListenOnly`);
}
} else {
let EVENT_NAME = 'user_disconnected_from_global_audio';
if (!isAllowedTo('leaveListenOnly', credentials)) {
throw new Meteor.Error('not-allowed', `You are not allowed to leaveListenOnly`);
}
}
const Metting = Meetings.findOne({ meetingId });
if (!Metting) {
throw new Meteor.Error(
'metting-not-found', `You need a valid meeting to be able to toggle audio`);
}
check(Metting.voiceConf, String);
const User = Users.findOne({
meetingId,
userId: requesterUserId,
});
if (!User) {
throw new Meteor.Error(
'user-not-found', `You need a valid user to be able to toggle audio`);
}
check(User.user.name, String);
let payload = {
userid: requesterUserId,
meeting_id: meetingId,
voice_conf: Metting.voiceConf,
name: User.user.name,
};
Logger.verbose(`User '${requesterUserId}' ${isJoining ? 'joined' : 'left'} global audio from meeting '${meetingId}'`);
return RedisPubSub.publish(CHANNEL, EVENT_NAME, payload);
};

View File

@ -0,0 +1,38 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import RedisPubSub from '/imports/startup/server/redis';
import Logger from '/imports/startup/server/logger';
import { isAllowedTo } from '/imports/startup/server/userPermissions';
export default function muteToggle(credentials, userId, isMuted = true) {
const REDIS_CONFIG = Meteor.settings.redis;
const CHANNEL = REDIS_CONFIG.channels.toBBBApps.users;
const EVENT_NAME = 'mute_user_request_message';
const { meetingId, requesterUserId } = credentials;
check(meetingId, String);
check(requesterUserId, String);
check(userId, String);
let action = userId === requesterUserId ? 'muteSelf' : 'muteOther';
if (!isMuted) {
action = `un${action}`;
}
if (!isAllowedTo(action, credentials)) {
throw new Meteor.Error('not-allowed', `You are not allowed to ${action}`);
}
let payload = {
user_id: userId,
meeting_id: meetingId,
mute: isMuted,
requester_id: requesterUserId,
};
Logger.verbose(`User '${userId}' was ${!isMuted ? 'un' : ''}muted by '${requesterUserId}' from meeting '${meetingId}'`);
return RedisPubSub.publish(CHANNEL, EVENT_NAME, payload);
};

View File

@ -1,42 +0,0 @@
import { publish } from '/imports/api/common/server/helpers';
import { isAllowedTo } from '/imports/startup/server/userPermissions';
import { appendMessageHeader } from '/imports/api/common/server/helpers';
import { updateVoiceUser } from '/imports/api/users/server/modifiers/updateVoiceUser';
import { logger } from '/imports/startup/server/logger';
Meteor.methods({
// meetingId: the meetingId of the meeting the user[s] is in
// toMuteUserId: the userId of the user to be muted
// requesterUserId: the userId of the requester
// requesterToken: the authToken of the requester
muteUser(credentials, toMuteUserId) {
const REDIS_CONFIG = Meteor.settings.redis;
const { meetingId, requesterUserId, requesterToken } = credentials;
const action = function () {
if (toMuteUserId === requesterUserId) {
return 'muteSelf';
} else {
return 'muteOther';
}
};
if (isAllowedTo(action(), credentials)) {
let message = {
payload: {
user_id: toMuteUserId,
meeting_id: meetingId,
mute: true,
requester_id: requesterUserId,
},
};
message = appendMessageHeader('mute_user_request_message', message);
logger.info(`publishing a user mute request for ${toMuteUserId}`);
publish(REDIS_CONFIG.channels.toBBBApps.users, message);
updateVoiceUser(meetingId, {
web_userid: toMuteUserId,
talking: false,
muted: true,
});
}
},
});

View File

@ -0,0 +1,22 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import RedisPubSub from '/imports/startup/server/redis';
import Logger from '/imports/startup/server/logger';
export default function requestStunTurn(meetingId, requesterUserId) {
const REDIS_CONFIG = Meteor.settings.redis;
const CHANNEL = REDIS_CONFIG.channels.fromBBBUsers;
const EVENT_NAME = 'send_stun_turn_info_request_message';
check(meetingId, String);
check(requesterUserId, String);
let payload = {
meeting_id: meetingId,
requester_id: requesterUserId,
};
Logger.verbose(`User '${requesterUserId}' requested stun/turn from meeting '${meetingId}'`);
return RedisPubSub.publish(CHANNEL, EVENT_NAME, payload);
};

View File

@ -0,0 +1,31 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import RedisPubSub from '/imports/startup/server/redis';
import Logger from '/imports/startup/server/logger';
import { isAllowedTo } from '/imports/startup/server/userPermissions';
export default function setEmojiStatus(credentials, userId, status) {
const REDIS_CONFIG = Meteor.settings.redis;
const CHANNEL = REDIS_CONFIG.channels.toBBBApps.users;
const EVENT_NAME = 'user_emoji_status_message';
const { meetingId, requesterUserId } = credentials;
check(meetingId, String);
check(requesterUserId, String);
check(userId, String);
if (!isAllowedTo('setEmojiStatus', credentials)) {
throw new Meteor.Error('not-allowed', `You are not allowed to setEmojiStatus`);
}
let payload = {
emoji_status: status,
userid: userId,
meeting_id: meetingId,
};
Logger.verbose(`User '${userId}' emoji status updated to '${status}' by '${requesterUserId}' from meeting '${meetingId}'`);
return RedisPubSub.publish(CHANNEL, EVENT_NAME, payload);
};

View File

@ -1,32 +0,0 @@
import { publish } from '/imports/api/common/server/helpers';
import { isAllowedTo } from '/imports/startup/server/userPermissions';
import { appendMessageHeader } from '/imports/api/common/server/helpers';
Meteor.methods({
//meetingId: the meeting where the user is
//newPresenterId: the userid of the new presenter
//requesterUserId: the userid of the user that wants to change the presenter
//newPresenterName: user name of the new presenter
//authToken: the authToken of the user that wants to kick
setUserPresenter(
credentials,
newPresenterId,
newPresenterName) {
const REDIS_CONFIG = Meteor.settings.redis;
const { meetingId, requesterUserId } = credentials;
let message;
if (isAllowedTo('setPresenter', credentials)) {
message = {
payload: {
new_presenter_id: newPresenterId,
new_presenter_name: newPresenterName,
meeting_id: meetingId,
assigned_by: requesterUserId,
},
};
message = appendMessageHeader('assign_presenter_request_message', message);
return publish(REDIS_CONFIG.channels.toBBBApps.users, message);
}
},
});

View File

@ -1,42 +0,0 @@
import { publish } from '/imports/api/common/server/helpers';
import { isAllowedTo } from '/imports/startup/server/userPermissions';
import { appendMessageHeader } from '/imports/api/common/server/helpers';
import { updateVoiceUser } from '/imports/api/users/server/modifiers/updateVoiceUser';
import { logger } from '/imports/startup/server/logger';
Meteor.methods({
// meetingId: the meetingId of the meeting the user[s] is in
// toMuteUserId: the userId of the user to be unmuted
// requesterUserId: the userId of the requester
// requesterToken: the authToken of the requester
unmuteUser(credentials, toMuteUserId) {
const REDIS_CONFIG = Meteor.settings.redis;
const { meetingId, requesterUserId, requesterToken } = credentials;
const action = function () {
if (toMuteUserId === requesterUserId) {
return 'unmuteSelf';
} else {
return 'unmuteOther';
}
};
if (isAllowedTo(action(), credentials)) {
let message = {
payload: {
user_id: toMuteUserId,
meeting_id: meetingId,
mute: false,
requester_id: requesterUserId,
},
};
message = appendMessageHeader('mute_user_request_message', message);
logger.info(`publishing a user unmute request for ${toMuteUserId}`);
publish(REDIS_CONFIG.channels.toBBBApps.users, message);
updateVoiceUser(meetingId, {
web_userid: toMuteUserId,
talking: false,
muted: false,
});
}
},
});

View File

@ -0,0 +1,48 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import RedisPubSub from '/imports/startup/server/redis';
import Logger from '/imports/startup/server/logger';
import { isAllowedTo } from '/imports/startup/server/userPermissions';
import Users from '/imports/api/users';
import setConnectionStatus from '../modifiers/setConnectionStatus';
import listenOnlyToggle from './listenOnlyToggle';
const OFFLINE_CONNECTION_STATUS = 'offline';
export default function userLeaving(credentials, userId) {
const REDIS_CONFIG = Meteor.settings.redis;
const CHANNEL = REDIS_CONFIG.channels.toBBBApps.users;
const EVENT_NAME = 'user_leaving_request';
const { meetingId, requesterUserId } = credentials;
check(meetingId, String);
check(requesterUserId, String);
check(userId, String);
const User = Users.findOne({
meetingId,
userId,
});
if (!User) {
throw new Meteor.Error(
'user-not-found', `You need a valid user to be able to toggle audio`);
}
if (User.user.connection_status === OFFLINE_CONNECTION_STATUS) {
return;
}
if (User.user.listenOnly) {
listenOnlyToggle(credentials, false);
}
let payload = {
meeting_id: meetingId,
userid: userId,
};
Logger.verbose(`User '${requesterUserId}' left meeting '${meetingId}'`);
return RedisPubSub.publish(CHANNEL, EVENT_NAME, payload);
};

View File

@ -1,13 +1,14 @@
import { Meteor } from 'meteor/meteor';
import { isAllowedTo } from '/imports/startup/server/userPermissions';
import { requestUserLeaving } from '/imports/api/users/server/modifiers/requestUserLeaving';
import { logger } from '/imports/startup/server/logger';
Meteor.methods({
userLogout(credentials) {
if (isAllowedTo('logoutSelf', credentials)) {
const { meetingId, requesterUserId, requesterToken } = credentials;
logger.info(`a user is logging out from ${meetingId}:${requesterUserId}`);
return requestUserLeaving(meetingId, requesterUserId);
}
},
});
import userLeaving from './userLeaving';
export default function userLogout(credentials) {
if (!isAllowedTo('logoutSelf', credentials)) {
throw new Meteor.Error('not-allowed', `You are not allowed to logoutSelf`);
}
const { requesterUserId } = credentials;
return userLeaving(credentials, requesterUserId);
};

View File

@ -1,25 +0,0 @@
import { publish } from '/imports/api/common/server/helpers';
import { isAllowedTo } from '/imports/startup/server/userPermissions';
import { appendMessageHeader } from '/imports/api/common/server/helpers';
Meteor.methods({
userSetEmoji(credentials, toRaiseUserId, status) {
const REDIS_CONFIG = Meteor.settings.redis;
const { meetingId, requesterUserId, requesterToken } = credentials;
let message;
if (isAllowedTo('setEmojiStatus', credentials)) {
message = {
payload: {
emoji_status: status,
userid: toRaiseUserId,
meeting_id: meetingId,
},
};
message = appendMessageHeader('user_emoji_status_message', message);
// publish to pubsub
publish(REDIS_CONFIG.channels.toBBBApps.users, message);
}
},
});

View File

@ -1,34 +1,48 @@
import { logger } from '/imports/startup/server/logger';
import { createDummyUser } from '/imports/api/users/server/modifiers/createDummyUser';
import { publish } from '/imports/api/common/server/helpers';
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import RedisPubSub from '/imports/startup/server/redis';
import Logger from '/imports/startup/server/logger';
import { isAllowedTo } from '/imports/startup/server/userPermissions';
import Users from '/imports/api/users';
Meteor.methods({
// Construct and send a message to bbb-web to validate the user
validateAuthToken(credentials) {
const REDIS_CONFIG = Meteor.settings.redis;
const { meetingId, requesterUserId, requesterToken } = credentials;
logger.info('sending a validate_auth_token with', {
userid: requesterUserId,
authToken: requesterToken,
meetingid: meetingId,
});
let message = {
payload: {
auth_token: requesterToken,
userid: requesterUserId,
meeting_id: meetingId,
},
header: {
timestamp: new Date().getTime(),
reply_to: `${meetingId}/${requesterUserId}`,
name: 'validate_auth_token',
},
};
if ((requesterToken != null) && (requesterUserId != null) && (meetingId != null)) {
createDummyUser(meetingId, requesterUserId, requesterToken);
return publish(REDIS_CONFIG.channels.toBBBApps.meeting, message);
} else {
return logger.info('did not have enough information to send a validate_auth_token message');
}
},
});
import createDummyUser from '../modifiers/createDummyUser';
import setConnectionStatus from '../modifiers/setConnectionStatus';
const ONLINE_CONNECTION_STATUS = 'online';
export default function validateAuthToken(credentials) {
const REDIS_CONFIG = Meteor.settings.redis;
const CHANNEL = REDIS_CONFIG.channels.toBBBApps.meeting;
const EVENT_NAME = 'validate_auth_token';
const { meetingId, requesterUserId, requesterToken } = credentials;
check(meetingId, String);
check(requesterUserId, String);
check(requesterToken, String);
const User = Users.findOne({
meetingId,
userId: requesterUserId,
});
if (!User) {
createDummyUser(meetingId, requesterUserId, requesterToken);
} else if (User.validated) {
setConnectionStatus(meetingId, requesterUserId, ONLINE_CONNECTION_STATUS);
}
let payload = {
auth_token: requesterToken,
userid: requesterUserId,
meeting_id: meetingId,
};
const header = {
reply_to: `${meetingId}/${requesterUserId}`,
};
Logger.verbose(`User '${requesterUserId}' is trying to validate auth token for meeting '${meetingId}'`);
return RedisPubSub.publish(CHANNEL, EVENT_NAME, payload, header);
};

View File

@ -0,0 +1,71 @@
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import Meetings from '/imports/api/meetings';
import Users from '/imports/api/users';
import requestStunTurn from '../methods/requestStunTurn';
export default function addUser(meetingId, user) {
check(user, Object);
check(meetingId, String);
const userId = user.userid;
check(userId, String);
const selector = {
meetingId,
userId,
};
const modifier = {
$set: {
meetingId,
userId,
'user.connection_status': 'online',
'user.userid': userId,
'user.extern_userid': user.extern_userid,
'user.role': user.role,
'user.name': user.name,
'user._sort_name': user.name.trim().toLowerCase(),
'user.avatarURL': user.avatarURL,
'user.set_emoji_time': user.set_emoji_time || (new Date()).getTime(),
'user.time_of_joining': (new Date()).getTime(),
'user.emoji_status': user.emoji_status,
'user.webcam_stream': user.webcam_stream,
'user.presenter': user.presenter,
'user.locked': user.locked,
'user.phone_user': user.phone_user,
'user.listenOnly': user.listenOnly,
'user.has_stream': user.has_stream,
'user.voiceUser.web_userid': user.voiceUser.web_userid,
'user.voiceUser.callernum': user.voiceUser.callernum,
'user.voiceUser.userid': user.voiceUser.userid,
'user.voiceUser.talking': user.voiceUser.talking,
'user.voiceUser.joined': user.voiceUser.joined,
'user.voiceUser.callername': user.voiceUser.callername,
'user.voiceUser.locked': user.voiceUser.locked,
'user.voiceUser.muted': user.voiceUser.muted,
},
};
const cb = (err, numChanged) => {
if (err) {
return Logger.error(`Adding user to collection: ${err}`);
}
// TODO: Do we really need to request the stun/turn everytime?
requestStunTurn(meetingId, userId);
const { insertedId } = numChanged;
if (insertedId) {
return Logger.info(`Added user id=${userId} meeting=${meetingId}`);
}
if (numChanged) {
return Logger.info(`Upserted user id=${userId} meeting=${meetingId}`);
}
};
return Users.upsert(selector, modifier, cb);
};

View File

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

View File

@ -1,26 +0,0 @@
import Users from '/imports/api/users';
import { logger } from '/imports/startup/server/logger';
// called on server start and on meeting end
export function clearUsersCollection() {
const meetingId = arguments[0];
if (meetingId != null) {
return Users.remove({
meetingId: meetingId,
}, err => {
if (err != null) {
return logger.error(`_error ${JSON.stringify(err)} while removing users from ${meetingId}`);
} else {
return logger.info(`_cleared Users Collection (meetingId: ${meetingId})!`);
}
});
} else {
return Users.remove({}, err => {
if (err != null) {
return logger.error(`_error ${JSON.stringify(err)} while removing users from all meetings`);
} else {
return logger.info('_cleared Users Collection (all meetings)!');
}
});
}
};

View File

@ -1,24 +1,33 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import Users from '/imports/api/users';
import { logger } from '/imports/startup/server/logger';
export function createDummyUser(meetingId, userId, authToken) {
if (Users.findOne({
userId: userId,
meetingId: meetingId,
authToken: authToken,
}) != null) {
const msg = `html5 user userId:[${userId}] from [${meetingId}] tried to revalidate token`;
return logger.info(msg);
} else {
return Users.insert({
meetingId: meetingId,
userId: userId,
authToken: authToken,
clientType: 'HTML5',
validated: false, //will be validated on validate_auth_token_reply
}, (err, id) => {
const res = Users.find({ meetingId: meetingId, }).count();
return logger.info(`_added a dummy html5 user userId=[${userId}] Users.size is now ${res}`);
});
export default function createDummyUser(meetingId, userId, authToken) {
check(meetingId, String);
check(userId, String);
check(authToken, String);
const User = Users.findOne({ meetingId, userId });
if (User) {
throw new Meteor.Error('existing-user', `Tried to create a dummy user for an existing user`);
}
const doc = {
meetingId,
userId,
authToken,
clientType: 'HTML5',
validated: false,
};
const cb = (err, numChanged) => {
if (err) {
return Logger.error(`Creating dummy user to collection: ${err}`);
}
return Logger.info(`Created dummy user id=${userId} token=${authToken} meeting=${meetingId}`);
};
return Users.insert(doc, cb);
};

View File

@ -1,212 +0,0 @@
import { logger } from '/imports/startup/server/logger';
import { eventEmitter } from '/imports/startup/server';
import { userJoined } from './userJoined';
import { setUserLockedStatus } from './setUserLockedStatus';
import { markUserOffline } from './markUserOffline';
import { inReplyToHTML5Client } from '/imports/api/common/server/helpers';
import Users from '../..';
eventEmitter.on('user_eject_from_meeting', function (arg) {
handleRemoveUserEvent(arg);
});
eventEmitter.on('disconnect_user_message', function (arg) {
handleRemoveUserEvent(arg);
});
eventEmitter.on('user_left_message', function (arg) {
handleRemoveUserEvent(arg);
});
eventEmitter.on('validate_auth_token_reply', function (arg) {
const meetingId = arg.payload.meeting_id;
const userId = arg.payload.userid;
const user = Users.findOne({
userId: userId,
meetingId: meetingId,
});
const validStatus = arg.payload.valid;
// if the user already exists in the db
if (user != null && user.clientType === 'HTML5') {
//if the html5 client user was validated successfully, add a flag
return Users.update({
userId: userId,
meetingId: meetingId,
}, {
$set: {
validated: validStatus,
},
}, (err, numChanged) => {
if (numChanged.insertedId != null) {
let funct = function (cbk) {
let user = Users.findOne({
userId: userId,
meetingId: meetingId,
});
let val;
if (user != null) {
val = user.validated;
}
logger.info(`user.validated for ${userId} in meeting ${meetingId} just became ${val}`);
return cbk();
};
return funct(arg.callback);
} else {
return arg.callback();
}
});
} else {
logger.info('a non-html5 user got validate_auth_token_reply.');
return arg.callback();
}
});
eventEmitter.on('user_joined_message', function (arg) {
const meetingId = arg.payload.meeting_id;
const payload = arg.payload;
let userObj = payload.user;
let dbUser = Users.findOne({
userId: userObj.userid,
meetingId: meetingId,
});
// On attempting reconnection of Flash clients (in voiceBridge) we receive
// an extra user_joined_message. Ignore it as it will add an extra user
// in the user list, creating discrepancy with the list in the Flash client
if (dbUser != null && dbUser.user != null && dbUser.user.connection_status === 'offline') {
if (payload.user != null && payload.user.phone_user) {
logger.error('offline AND phone user');
return arg.callback(); //return without joining the user
}
} else {
if (dbUser != null && dbUser.clientType === 'HTML5') {
// typically html5 users will be in
// the db [as a dummy user] before the joining message
let status = dbUser.validated;
logger.info(`in user_joined_message the validStatus of the user was ${status}`);
userObj.timeOfJoining = arg.header.current_time;
}
return userJoined(meetingId, userObj, arg.callback);
}
return arg.callback();
});
eventEmitter.on('get_users_reply', function (arg) {
if (inReplyToHTML5Client(arg)) {
let users = arg.payload.users;
const meetingId = arg.payload.meeting_id;
//TODO make the serialization be split per meeting. This will allow us to
// use N threads vs 1 and we'll take advantage of Mongo's concurrency tricks
// Processing the users recursively with a callback to notify us,
// ensuring that we update the users collection serially
let processUser = function () {
let user = users.pop();
if (user != null) {
user.timeOfJoining = arg.header.current_time;
if (user.emoji_status !== 'none' && typeof user.emoji_status === 'string') {
user.set_emoji_time = new Date();
return userJoined(meetingId, user, processUser);
} else {
return userJoined(meetingId, user, processUser);
}
} else {
return arg.callback(); // all meeting arrays (if any) have been processed
}
};
return processUser();
} else {
arg.callback();
}
});
eventEmitter.on('user_emoji_status_message', function (arg) {
const userId = arg.payload.userid;
const meetingId = arg.payload.meeting_id;
const emojiStatus = arg.payload.emoji_status;
if (userId != null && meetingId != null) {
const setEmojiTime = new Date();
Users.update({
'user.userid': userId,
}, {
$set: {
'user.set_emoji_time': setEmojiTime,
'user.emoji_status': emojiStatus,
},
}, (err, numUpdated) => logger.info(`Updating emoji numUpdated=${numUpdated}, err=${err}`));
}
return arg.callback();
});
eventEmitter.on('user_locked_message', function (arg) {
handleLockEvent(arg);
});
eventEmitter.on('user_unlocked_message', function (arg) {
handleLockEvent(arg);
});
eventEmitter.on('presenter_assigned_message', function (arg) {
const meetingId = arg.payload.meeting_id;
const newPresenterId = arg.payload.new_presenter_id;
if (newPresenterId != null) {
// reset the previous presenter
Users.update({
'user.presenter': true,
meetingId: meetingId,
}, {
$set: {
'user.presenter': false,
},
}, (err, numUpdated) => logger.info(
` Updating old presenter numUpdated=${numUpdated}, ` +
` err=${err}`
)
);
// set the new presenter
Users.update({
'user.userid': newPresenterId,
meetingId: meetingId,
}, {
$set: {
'user.presenter': true,
},
}, (err, numUpdated) => logger.info(
` Updating new presenter numUpdated=${numUpdated}, ` +
`err=${err}`
)
);
}
return arg.callback();
});
const handleRemoveUserEvent = function (arg) {
const { payload, callback } = arg;
if ((payload.userid || payload.user.userid) &&
payload.meeting_id) {
const meetingId = payload.meeting_id;
const userId = payload.userid || payload.user.userid;
return markUserOffline(meetingId, userId, callback);
} else {
logger.info('could not perform handleRemoveUserEvent');
return callback();
}
};
const handleLockEvent = function (arg) {
const userId = arg.payload.userid;
const isLocked = arg.payload.locked;
const meetingId = arg.payload.meeting_id;
setUserLockedStatus(meetingId, userId, isLocked);
return arg.callback();
};

View File

@ -1,26 +0,0 @@
import Users from '/imports/api/users';
// when new lock settings including disableMic are set,
// all viewers that are in the audio bridge with a mic should be muted and locked
export default function handleLockingMic(meetingId, newSettings) {
// send mute requests for the viewer users joined with mic
let userObject;
let results = [];
const userObjects = Users.find({
meetingId: meetingId,
'user.role': 'VIEWER',
'user.listenOnly': false,
'user.locked': true,
'user.voiceUser.joined': true,
'user.voiceUser.muted': false,
}).fetch();
const _userObjectsLength = userObjects.length;
for (let i = 0; i < _userObjectsLength; i++) {
userObject = userObjects[i];
results.push(Meteor.call('muteUser', meetingId, userObject.userId, userObject.userId,
userObject.authToken, true)); //true for muted
}
return results;
};

View File

@ -0,0 +1,24 @@
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import Users from '/imports/api/users';
export default function lockAllViewersMic(meetingId) {
const selector = {
meetingId,
'user.role': 'VIEWER',
'user.listenOnly': false,
'user.locked': true,
'user.voiceUser.joined': true,
'user.voiceUser.muted': false,
};
const usersToMute = Users.find(selector).fetch();
usersToMute.forEach(user =>
muteToggle({
meetingId,
requesterUserId: user.userId,
}, userId, true)
);
};

View File

@ -1,75 +0,0 @@
import Users from '/imports/api/users';
import { logger } from '/imports/startup/server/logger';
// Only callable from server
// Received information from BBB-Apps that a user left
// Need to update the collection
// params: meetingid, userid as defined in BBB-Apps
// callback
export function markUserOffline(meetingId, userId, callback) {
// mark the user as offline. remove from the collection on meeting_end #TODO
let user = Users.findOne({
meetingId: meetingId,
userId: userId,
});
if (user != null && user.clientType === 'HTML5') {
logger.info(`marking html5 user [${userId}] as offline in meeting[${meetingId}]`);
return Users.update({
meetingId: meetingId,
userId: userId,
}, {
$set: {
'user.connection_status': 'offline',
'user.voiceUser.talking': false,
'user.voiceUser.joined': false,
'user.voiceUser.muted': false,
'user.time_of_joining': 0,
'user.listenOnly': false, //TODO make this user: {}
},
}, (err, numChanged) => {
let funct;
if (err != null) {
logger.error(
`_unsucc update (mark as offline) of user ${user.user.name} ` +
`${userId} err=${JSON.stringify(err)}`
);
return callback();
} else {
funct = function (cbk) {
logger.info(
`_marking as offline html5 user ${user.user.name} ` +
`${userId} numChanged=${numChanged}`
);
return cbk();
};
return funct(callback);
}
});
} else {
return Users.remove({
meetingId: meetingId,
userId: userId,
}, (err, numDeletions) => {
let funct;
if (err != null) {
logger.error(
`_unsucc deletion of user ${user != null ? user.user.name : void 0} ` +
`${userId} err=${JSON.stringify(err)}`
);
return callback();
} else {
funct = function (cbk) {
logger.info(
`_deleting info for user ${user != null ? user.user.name : void 0} ` +
`${userId} numDeletions=${numDeletions}`
);
return cbk();
};
return funct(callback);
}
});
}
};

View File

@ -0,0 +1,56 @@
import { check } from 'meteor/check';
import Users from '/imports/api/slides';
import Logger from '/imports/startup/server/logger';
import setConnectionStatus from './setConnectionStatus';
const CLIENT_TYPE_HTML = 'HTML5';
export default function removeUser(meetingId, userId) {
check(meetingId, String);
check(userId, String);
const selector = {
meetingId,
userId,
};
const User = Users.findOne(selector);
if (User && User.clientType !== CLIENT_TYPE_HTML) {
const cb = (err, numChanged) => {
if (err) {
return Logger.error(`Removing user from collection: ${err}`);
}
if (numChanged) {
return Logger.info(`Removed user id=${userId} meeting=${meetingId}`);
}
};
return Users.remove(selector, cb);
}
const modifier = {
$set: {
'user.connection_status': 'offline',
'user.voiceUser.talking': false,
'user.voiceUser.joined': false,
'user.voiceUser.muted': false,
'user.time_of_joining': 0,
'user.listenOnly': false,
},
};
const cb = (err, numChanged) => {
if (err) {
return Logger.error(`Removing user from collection: ${err}`);
}
if (numChanged) {
return Logger.info(`Removed ${CLIENT_TYPE_HTML} user id=${userId} meeting=${meetingId}`);
}
};
return Users.update(selector, modifier, cb);
};

View File

@ -1,63 +0,0 @@
import { publish } from '/imports/api/common/server/helpers';
import Meetings from '/imports/api/meetings';
import Users from '/imports/api/users';
import { logger } from '/imports/startup/server/logger';
// Corresponds to a valid action on the HTML clientside
// After authorization, publish a user_leaving_request in redis
// params: meetingid, userid as defined in BBB-App
export function requestUserLeaving(meetingId, userId) {
const REDIS_CONFIG = Meteor.settings.redis;
let voiceConf;
const userObject = Users.findOne({
meetingId: meetingId,
userId: userId,
});
const meetingObject = Meetings.findOne({
meetingId: meetingId,
});
if (meetingObject != null) {
voiceConf = meetingObject.voiceConf;
}
if ((userObject != null) && (voiceConf != null) && (userId != null) && (meetingId != null)) {
let lOnly = false;
if (userObject.hasOwnProperty('user') && userObject.user.hasOwnProperty('listenOnly')) {
lOnly = userObject.user.listenOnly;
}
// end listenOnly audio for the departing user
if (lOnly) {
const listenOnlyMessage = {
payload: {
userid: userId,
meeting_id: meetingId,
voice_conf: voiceConf,
name: userObject.user.name,
},
header: {
timestamp: new Date().getTime(),
name: 'user_disconnected_from_global_audio',
},
};
publish(REDIS_CONFIG.channels.toBBBApps.meeting, listenOnlyMessage);
}
// remove user from meeting
const message = {
payload: {
meeting_id: meetingId,
userid: userId,
},
header: {
timestamp: new Date().getTime(),
name: 'user_leaving_request',
},
};
logger.info(`sending a user_leaving_request for ${meetingId}:${userId}`);
return publish(REDIS_CONFIG.channels.toBBBApps.users, message);
} else {
return logger.info('did not have enough information to send a user_leaving_request');
}
};

View File

@ -0,0 +1,40 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import Users from '/imports/api/users';
import Logger from '/imports/startup/server/logger';
const VALID_CONNECTION_STATUS = ['online', 'offline'];
export default function setConnectionStatus(meetingId, userId, status = 'online') {
check(meetingId, String);
check(userId, String);
check(status, String);
if (!VALID_CONNECTION_STATUS.includes(status)) {
throw new Meteor.Error('invalid-connection-status',
`Invalid connection status, received ${status} expecting ${VALID_CONNECTION_STATUS.join()}`);
}
const selector = {
meetingId,
userId,
};
const modifier = {
$set: {
'user.connection_status': status,
},
};
const cb = (err, numChanged) => {
if (err) {
return Logger.error(`Updating connection status user=${userId}: ${err}`);
}
if (numChanged) {
return Logger.info(`Updated connection status user=${userId} status=${status} meeting=${meetingId}`);
}
};
return Users.update(selector, modifier, cb);
};

View File

@ -1,41 +0,0 @@
import Users from '/imports/api/users';
import { logger } from '/imports/startup/server/logger';
// change the locked status of a user (lock settings)
export function setUserLockedStatus(meetingId, userId, isLocked) {
let userObject;
userObject = Users.findOne({
meetingId: meetingId,
userId: userId,
});
if (userObject != null) {
Users.update({
userId: userId,
meetingId: meetingId,
}, {
$set: {
'user.locked': isLocked,
},
}, (err, numChanged) => {
if (err != null) {
return logger.error(`_error ${err} while updating user ${userId} with lock settings`);
} else {
return logger.info(
`_setting user locked status for:[${userId}] ` +
`from [${meetingId}] locked=${isLocked}`
);
}
});
// if the user is sharing audio, he should be muted upon locking involving disableMic
if (userObject.user.role === 'VIEWER' && !userObject.user.listenOnly &&
userObject.user.voiceUser.joined && !userObject.user.voiceUser.muted && isLocked) {
// TODO why are we doing Meteor.call here?! Anton
return Meteor.call('muteUser', meetingId, userObject.userId, userObject.userId,
userObject.authToken, true); //true for muted
}
} else {
let tempMsg = '(unsuccessful-no such user) setting user locked status for userid:';
return logger.error(`${tempMsg}[${userId}] from [${meetingId}] locked=${isLocked}`);
}
};

View File

@ -1,47 +1,35 @@
import Users from '/imports/api/users';
import { check } from 'meteor/check';
import Users from '/imports/api/slides';
import Logger from '/imports/startup/server/logger';
//update a voiceUser - a helper method
export function updateVoiceUser(meetingId, voiceUserObject, callback = () => {}) {
export default function updateVoiceUser(meetingId, userId, voiceUser) {
check(meetingId, String);
check(voiceUserObject, Object);
const webUserId = voiceUserObject.web_userid;
check(webUserId, String);
let userObject = Users.findOne({
userId: webUserId,
});
Logger.debug(`user ${userObject.userId} vu=${JSON.stringify(voiceUserObject)}`);
check(userObject, Object);
check(userId, String);
check(voiceUser, Object);
const selector = {
meetingId,
userId: webUserId,
userId,
};
const modifier = {
$set: {
'user.voiceUser.talking': voiceUserObject.talking || false,
'user.voiceUser.joined': voiceUserObject.joined || false,
'user.voiceUser.locked': voiceUserObject.locked || false,
'user.voiceUser.muted': voiceUserObject.muted || false,
'user.listenOnly': voiceUserObject.listen_only || false,
'user.voiceUser.talking': voiceUser.talking,
'user.voiceUser.joined': voiceUser.joined,
'user.voiceUser.locked': voiceUser.locked,
'user.voiceUser.muted': voiceUser.muted,
},
};
const cb = (err, numChanged) => {
if (err) {
Logger.error(
`failed to update voiceUser ${JSON.stringify(voiceUserObject)} err=${JSON.stringify(err)}`
);
return Logger.error(`Updating voice user=${userId}: ${err}`);
}
return callback();
if (numChanged) {
return Logger.verbose(`Updated voice user=${userId} meeting=${meetingId}`);
}
};
Users.update(selector, modifier, cb);
return Users.update(selector, modifier, cb);
};

View File

@ -1,189 +0,0 @@
import Chat from '/imports/api/chat';
import Users from '/imports/api/users';
import Meetings from '/imports/api/meetings';
import { logger } from '/imports/startup/server/logger';
import { BREAK_LINE } from '/imports/utils/lineEndings.js';
import getStun from '/imports/api/phone/server/getStun';
const parseMessage = (message) => {
message = message || '';
// Replace \r and \n to <br/>
message = message.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="`);
return message;
};
export function userJoined(meetingId, user, callback) {
const APP_CONFIG = Meteor.settings.public.app;
let welcomeMessage;
const userId = user.userid;
const userObject = Users.findOne({
userId: user.userid,
meetingId: meetingId,
});
// the collection already contains an entry for this user
// because the user is reconnecting OR
// in the case of an html5 client user we added a dummy user on
// register_user_message (to save authToken)
if (userObject != null && userObject.authToken != null) {
getStun({
meetingId: meetingId,
requesterUserId: userId,
});
Users.update({
userId: user.userid,
meetingId: meetingId,
}, {
$set: {
user: {
userid: user.userid,
presenter: user.presenter,
name: user.name,
_sort_name: user.name.toLowerCase(),
phone_user: user.phone_user,
set_emoji_time: user.set_emoji_time,
emoji_status: user.emoji_status,
has_stream: user.has_stream,
role: user.role,
listenOnly: user.listenOnly,
extern_userid: user.extern_userid,
locked: user.locked,
time_of_joining: user.timeOfJoining,
connection_status: 'online', // TODO consider other default value
voiceUser: {
web_userid: user.voiceUser.web_userid,
callernum: user.voiceUser.callernum,
userid: user.voiceUser.userid,
talking: user.voiceUser.talking,
joined: user.voiceUser.joined,
callername: user.voiceUser.callername,
locked: user.voiceUser.locked,
muted: user.voiceUser.muted,
},
webcam_stream: user.webcam_stream,
},
},
}, err => {
let funct;
if (err != null) {
logger.error(`_error ${err} when trying to insert user ${userId}`);
return callback();
} else {
funct = function (cbk) {
logger.info(
`_(case1) UPDATING USER ${user.userid}, ` +
`authToken= ${userObject.authToken}, ` +
`locked=${user.locked}, username=${user.name}`
);
return cbk();
};
return funct(callback);
}
});
const meetingObject = Meetings.findOne({
meetingId: meetingId,
});
if (meetingObject != null) {
welcomeMessage = APP_CONFIG.defaultWelcomeMessage.replace(/%%CONFNAME%%/,
meetingObject.meetingName);
}
welcomeMessage = welcomeMessage + APP_CONFIG.defaultWelcomeMessageFooter;
// add the welcome message if it's not there already OR update time_of_joining
return Chat.upsert({
meetingId: meetingId,
userId: userId,
'message.chat_type': 'SYSTEM_MESSAGE',
'message.to_userid': userId,
}, {
meetingId: meetingId,
userId: userId,
message: {
chat_type: 'SYSTEM_MESSAGE',
message: parseMessage(welcomeMessage),
from_color: '0x3399FF',
to_userid: userId,
from_userid: 'SYSTEM_MESSAGE',
from_username: '',
from_time: (user != null && user.timeOfJoining != null) ? +(user.timeOfJoining) : 0,
},
}, err => {
if (err != null) {
return logger.error(`_error ${err} when trying to insert welcome message for ${userId}`);
} else {
return logger.info(`_added/updated a system message in chat for user ${userId}`);
}
// note that we already called callback() when updating the user. Adding
// the welcome message in the chat is not as vital and we can afford to
// complete it when possible, without blocking the serial event messages processing
});
} else {
// logger.info "NOTE: got user_joined_message #{user.name} #{user.userid}"
getStun({
meetingId: meetingId,
requesterUserId: userId,
});
return Users.upsert({
meetingId: meetingId,
userId: userId,
}, {
meetingId: meetingId,
userId: userId,
user: {
userid: user.userid,
presenter: user.presenter,
name: user.name,
_sort_name: user.name.toLowerCase(),
phone_user: user.phone_user,
emoji_status: user.emoji_status,
set_emoji_time: user.set_emoji_time,
has_stream: user.has_stream,
role: user.role,
listenOnly: user.listenOnly,
extern_userid: user.extern_userid,
locked: user.locked,
time_of_joining: user.timeOfJoining,
connection_status: '',
voiceUser: {
web_userid: user.voiceUser.web_userid,
callernum: user.voiceUser.callernum,
userid: user.voiceUser.userid,
talking: user.voiceUser.talking,
joined: user.voiceUser.joined,
callername: user.voiceUser.callername,
locked: user.voiceUser.locked,
muted: user.voiceUser.muted,
},
webcam_stream: user.webcam_stream,
},
}, (err, numChanged) => {
let funct;
if (numChanged.insertedId != null) {
funct = function (cbk) {
logger.info(
`_joining user (case2) userid=[${userId}]:${user.name}. Users.size is now ` +
`${Users.find({
meetingId: meetingId,
}).count()}`
);
return cbk();
};
return funct(callback);
} else {
return callback();
}
});
}
};

View File

@ -1,77 +0,0 @@
import Users from '/imports/api/users';
import { isAllowedTo } from '/imports/startup/server/userPermissions';
import { logger } from '/imports/startup/server/logger';
import { requestUserLeaving } from '/imports/api/users/server/modifiers/requestUserLeaving';
// Publish only the online users that are in the particular meetingId
// Also contains reconnection and connection_status info
Meteor.publish('users', function (credentials) {
const meetingId = credentials.meetingId;
const userid = credentials.requesterUserId;
const authToken = credentials.requesterToken;
logger.info(`attempt publishing users for ${meetingId}, ${userid}, ${authToken}`);
const userObject = Users.findOne({
userId: userid,
meetingId: meetingId,
});
if (!!userObject && !!userObject.user) {
let username = 'UNKNOWN';
if (isAllowedTo('subscribeUsers', credentials)) {
logger.info(`${userid} was allowed to subscribe to 'users'`);
username = userObject.user.name;
// offline -> online
if (userObject.user.connection_status !== 'online') {
Meteor.call('validateAuthToken', credentials);
setConnectionStatus(meetingId, userid, 'online');
}
this._session.socket.on('close', Meteor.bindEnvironment((function (_this) {
return function () {
logger.info(`a user lost connection: session.id=${_this._session.id}` +
` userId = ${userid}, username=${username}, meeting=${meetingId}`);
setConnectionStatus(meetingId, userid, 'offline');
return requestUserLeaving(meetingId, userid);
};
})(this)));
return getUsers(meetingId);
} else {
logger.warn('was not authorized to subscribe to users');
return this.error(new Meteor.Error(402, 'User was not authorized to subscribe to users'));
}
} else { //subscribing before the user was added to the collection
Meteor.call('validateAuthToken', credentials);
logger.info(`Sending validateAuthTokenthere for user ${userid} in ${meetingId}.`);
return getUsers(meetingId);
}
});
const getUsers = function (meetingId) {
//publish the users which are not offline
return Users.find({
meetingId: meetingId,
'user.connection_status': {
$in: ['online', ''],
},
}, {
fields: {
authToken: false,
},
});
};
const setConnectionStatus = function (meetingId, userId, statusStr) {
Users.update({
meetingId: meetingId,
userId: userId,
}, {
$set: {
'user.connection_status': statusStr,
},
}, (err, numChanged) => {
logger.info(`User ${userId} in ${meetingId} goes ${statusStr}`);
});
};

View File

@ -0,0 +1,44 @@
import Users from '/imports/api/users';
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import { isAllowedTo } from '/imports/startup/server/userPermissions';
import userLeaving from './methods/userLeaving';
import validateAuthToken from './methods/validateAuthToken';
Meteor.publish('users', function (credentials) {
const { meetingId, requesterUserId, requesterToken } = credentials;
check(meetingId, String);
check(requesterUserId, String);
check(requesterToken, String);
validateAuthToken(credentials);
// TODO(auth): We need to fix the Authentication flow to enable ACL
// if (!isAllowedTo('subscribeUsers', credentials)) {
// this.error(new Meteor.Error(402, "The user was not authorized to subscribe for 'Users'"));
// }
this.onStop(() => {
userLeaving(credentials, requesterUserId);
});
const selector = {
meetingId,
'user.connection_status': {
$in: ['online', ''],
},
};
const options = {
fields: {
authToken: false,
},
};
Logger.info(`Publishing Users for ${meetingId} ${requesterUserId} ${requesterToken}`);
return Users.find(selector, options);
});

View File

@ -8,6 +8,8 @@ const presenter = {
//poll
subscribePoll: true,
subscribeAnswers: true,
setPresenter: true,
};
// holds the values for whether the moderator user is allowed to perform an action (true)

View File

@ -22,7 +22,7 @@ let getEmojiData = () => {
// Below doesn't even need to receieve credentials
const setEmoji = (toRaiseUserId, status) => {
callServer('userSetEmoji', toRaiseUserId, status);
callServer('setEmojiStatus', toRaiseUserId, status);
};
export default {

View File

@ -219,12 +219,12 @@ const userActions = {
},
clearStatus: {
label: 'Clear Status',
handler: user => callServer('userSetEmoji', user.id, 'none'),
handler: user => callServer('setEmojiStatus', user.id, 'none'),
icon: 'clear-status',
},
setPresenter: {
label: 'Make Presenter',
handler: user => callServer('setUserPresenter', user.id, user.name),
handler: user => callServer('assignPresenter', user.id),
icon: 'presentation',
},
kick: {

View File

@ -1,49 +1,19 @@
import '/imports/startup/server';
import '/imports/api/chat/server';
import '/imports/api/cursor/server';
import '/imports/api/deskshare/server/publications';
import '/imports/api/deskshare/server/modifiers/clearDeskshareCollection';
import '/imports/api/deskshare/server/modifiers/handleDeskShareChange';
import '/imports/api/deskshare/server/modifiers/handleIncomingDeskshareMessage';
import '/imports/api/deskshare/server/modifiers/eventHandlers';
import '/imports/api/meetings/server';
import '/imports/api/phone/server/modifiers/eventHandlers';
import '/imports/api/polls/server';
import '/imports/api/breakouts/server';
import '/imports/api/presentations/server';
import '/imports/api/shapes/server';
import '/imports/api/slides/server';
import '/imports/api/captions/server';
import '/imports/api/users/server/publications';
import '/imports/api/users/server/methods/kickUser';
import '/imports/api/users/server/methods/listenOnlyRequestToggle';
import '/imports/api/users/server/methods/muteUser';
import '/imports/api/users/server/methods/setUserPresenter';
import '/imports/api/users/server/methods/unmuteUser';
import '/imports/api/users/server/methods/userLogout';
import '/imports/api/users/server/methods/userSetEmoji';
import '/imports/api/users/server/methods/validateAuthToken';
import '/imports/api/users/server/modifiers/clearUsersCollection';
import '/imports/api/users/server/modifiers/createDummyUser';
import '/imports/api/users/server/modifiers/handleLockingMic';
import '/imports/api/users/server/modifiers/markUserOffline';
import '/imports/api/users/server/modifiers/requestUserLeaving';
import '/imports/api/users/server/modifiers/setUserLockedStatus';
import '/imports/api/users/server/modifiers/updateVoiceUser';
import '/imports/api/users/server/modifiers/userJoined';
import '/imports/api/users/server/modifiers/eventHandlers';
import '/imports/api/users/server';
import '/imports/api/common/server/helpers';
import '/imports/startup/server/logger';
import '/imports/startup/server/userPermissions';