Add support for moderating rooms. Kick user, mute all users, stop all videos.

auto_join_3.3
Håvar Aambø Fosstveit 2020-03-22 00:43:47 +01:00
parent c70740f5c7
commit 7f2f27b858
15 changed files with 515 additions and 57 deletions

View File

@ -1126,6 +1126,66 @@ export default class RoomClient
lobbyPeerActions.setLobbyPeerPromotionInProgress(peerId, false)); lobbyPeerActions.setLobbyPeerPromotionInProgress(peerId, false));
} }
async kickPeer(peerId)
{
logger.debug('kickPeer() [peerId:"%s"]', peerId);
store.dispatch(
peerActions.setPeerKickInProgress(peerId, true));
try
{
await this.sendRequest('moderator:kickPeer', { peerId });
}
catch (error)
{
logger.error('kickPeer() failed: %o', error);
}
store.dispatch(
peerActions.setPeerKickInProgress(peerId, false));
}
async muteAllPeers()
{
logger.debug('muteAllPeers()');
store.dispatch(
roomActions.setMuteAllInProgress(true));
try
{
await this.sendRequest('moderator:muteAll');
}
catch (error)
{
logger.error('muteAllPeers() failed: %o', error);
}
store.dispatch(
roomActions.setMuteAllInProgress(false));
}
async stopAllPeerVideo()
{
logger.debug('stopAllPeerVideo()');
store.dispatch(
roomActions.setStopAllVideoInProgress(true));
try
{
await this.sendRequest('moderator:stopAllVideo');
}
catch (error)
{
logger.error('stopAllPeerVideo() failed: %o', error);
}
store.dispatch(
roomActions.setStopAllVideoInProgress(false));
}
// type: mic/webcam/screen // type: mic/webcam/screen
// mute: true/false // mute: true/false
async modifyPeerConsumer(peerId, type, mute) async modifyPeerConsumer(peerId, type, mute)
@ -1902,10 +1962,10 @@ export default class RoomClient
case 'newPeer': case 'newPeer':
{ {
const { id, displayName, picture } = notification.data; const { id, displayName, picture, roles } = notification.data;
store.dispatch( store.dispatch(
peerActions.addPeer({ id, displayName, picture, consumers: [] })); peerActions.addPeer({ id, displayName, picture, roles, consumers: [] }));
store.dispatch(requestActions.notify( store.dispatch(requestActions.notify(
{ {
@ -2004,6 +2064,96 @@ export default class RoomClient
break; break;
} }
case 'moderator:mute':
{
// const { peerId } = notification.data;
if (this._micProducer && !this._micProducer.paused)
{
this.muteMic();
store.dispatch(requestActions.notify(
{
text : intl.formatMessage({
id : 'moderator.mute',
defaultMessage : 'Moderator muted your microphone'
})
}));
}
break;
}
case 'moderator:stopVideo':
{
// const { peerId } = notification.data;
this.disableWebcam();
this.disableScreenSharing();
store.dispatch(requestActions.notify(
{
text : intl.formatMessage({
id : 'moderator.mute',
defaultMessage : 'Moderator stopped your video'
})
}));
break;
}
case 'moderator:kick':
{
// Need some feedback
this.close();
break;
}
case 'gotRole':
{
const { peerId, role } = notification.data;
if (peerId === this._peerId)
{
store.dispatch(meActions.addRole({ role }));
store.dispatch(requestActions.notify(
{
text : intl.formatMessage({
id : 'roles.gotRole',
defaultMessage : `You got the role: ${role}`
})
}));
}
else
store.dispatch(peerActions.addPeerRole({ peerId, role }));
break;
}
case 'lostRole':
{
const { peerId, role } = notification.data;
if (peerId === this._peerId)
{
store.dispatch(meActions.removeRole({ role }));
store.dispatch(requestActions.notify(
{
text : intl.formatMessage({
id : 'roles.lostRole',
defaultMessage : `You lost the role: ${role}`
})
}));
}
else
store.dispatch(peerActions.removePeerRole({ peerId, role }));
break;
}
default: default:
{ {
@ -2158,7 +2308,7 @@ export default class RoomClient
canShareFiles : this._torrentSupport canShareFiles : this._torrentSupport
})); }));
const { peers } = await this.sendRequest( const { roles, peers } = await this.sendRequest(
'join', 'join',
{ {
displayName : displayName, displayName : displayName,
@ -2166,7 +2316,25 @@ export default class RoomClient
rtpCapabilities : this._mediasoupDevice.rtpCapabilities rtpCapabilities : this._mediasoupDevice.rtpCapabilities
}); });
logger.debug('_joinRoom() joined, got peers [peers:"%o"]', peers); logger.debug('_joinRoom() joined [peers:"%o", roles:"%o"]', peers, roles);
const myRoles = store.getState().me.roles;
for (const role of roles)
{
if (!myRoles.includes(role))
{
store.dispatch(meActions.addRole({ role }));
store.dispatch(requestActions.notify(
{
text : intl.formatMessage({
id : 'roles.gotRole',
defaultMessage : `You got the role: ${role}`
})
}));
}
}
for (const peer of peers) for (const peer of peers)
{ {

View File

@ -10,6 +10,18 @@ export const loggedIn = (flag) =>
payload : { flag } payload : { flag }
}); });
export const addRole = ({ role }) =>
({
type : 'ADD_ROLE',
payload : { role }
});
export const removeRole = (role) =>
({
type : 'REMOVE_ROLE',
payload : { role }
});
export const setPicture = (picture) => export const setPicture = (picture) =>
({ ({
type : 'SET_PICTURE', type : 'SET_PICTURE',

View File

@ -45,3 +45,22 @@ export const setPeerPicture = (peerId, picture) =>
type : 'SET_PEER_PICTURE', type : 'SET_PEER_PICTURE',
payload : { peerId, picture } payload : { peerId, picture }
}); });
export const addPeerRole = (peerId, role) =>
({
type : 'ADD_PEER_ROLE',
payload : { peerId, role }
});
export const removePeerRole = (peerId, role) =>
({
type : 'REMOVE_PEER_ROLE',
payload : { peerId, role }
});
export const setPeerKickInProgress = (peerId, flag) =>
({
type : 'SET_PEER_KICK_IN_PROGRESS',
payload : { peerId, flag }
});

View File

@ -109,4 +109,16 @@ export const toggleConsumerFullscreen = (consumerId) =>
({ ({
type : 'TOGGLE_FULLSCREEN_CONSUMER', type : 'TOGGLE_FULLSCREEN_CONSUMER',
payload : { consumerId } payload : { consumerId }
});
export const setMuteAllInProgress = (flag) =>
({
type : 'MUTE_ALL_IN_PROGRESS',
payload : { flag }
});
export const setStopAllVideoInProgress = (flag) =>
({
type : 'STOP_ALL_VIDEO_IN_PROGRESS',
payload : { flag }
}); });

View File

@ -0,0 +1,101 @@
import React from 'react';
import { connect } from 'react-redux';
import { withStyles } from '@material-ui/core/styles';
import PropTypes from 'prop-types';
import { withRoomContext } from '../../../RoomContext';
import { useIntl, FormattedMessage } from 'react-intl';
import Button from '@material-ui/core/Button';
const styles = (theme) =>
({
root :
{
padding : theme.spacing(1),
width : '100%',
overflow : 'hidden',
cursor : 'auto',
display : 'flex'
},
actionButtons :
{
display : 'flex'
},
divider :
{
marginLeft : theme.spacing(2)
}
});
const ListModerator = (props) =>
{
const intl = useIntl();
const {
roomClient,
room,
classes
} = props;
return (
<div className={classes.root}>
<Button
aria-label={intl.formatMessage({
id : 'room.muteAll',
defaultMessage : 'Mute all'
})}
className={classes.actionButton}
variant='contained'
color='secondary'
disabled={room.muteAllInProgress}
onClick={() => roomClient.muteAllPeers()}
>
<FormattedMessage
id='room.muteAll'
defaultMessage='Mute all'
/>
</Button>
<div className={classes.divider} />
<Button
aria-label={intl.formatMessage({
id : 'room.stopAllVideo',
defaultMessage : 'Stop all video'
})}
className={classes.actionButton}
variant='contained'
color='secondary'
disabled={room.stopAllVideoInProgress}
onClick={() => roomClient.stopAllPeerVideo()}
>
<FormattedMessage
id='room.stopAllVideo'
defaultMessage='Stop all video'
/>
</Button>
</div>
);
};
ListModerator.propTypes =
{
roomClient : PropTypes.any.isRequired,
room : PropTypes.object.isRequired,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) => ({
room : state.room
});
export default withRoomContext(connect(
mapStateToProps,
null,
null,
{
areStatesEqual : (next, prev) =>
{
return (
prev.room === next.room
);
}
}
)(withStyles(styles)(ListModerator)));

View File

@ -12,6 +12,7 @@ import MicIcon from '@material-ui/icons/Mic';
import MicOffIcon from '@material-ui/icons/MicOff'; import MicOffIcon from '@material-ui/icons/MicOff';
import ScreenIcon from '@material-ui/icons/ScreenShare'; import ScreenIcon from '@material-ui/icons/ScreenShare';
import ScreenOffIcon from '@material-ui/icons/StopScreenShare'; import ScreenOffIcon from '@material-ui/icons/StopScreenShare';
import ExitIcon from '@material-ui/icons/ExitToApp';
import EmptyAvatar from '../../../images/avatar-empty.jpeg'; import EmptyAvatar from '../../../images/avatar-empty.jpeg';
import HandIcon from '../../../images/icon-hand-white.svg'; import HandIcon from '../../../images/icon-hand-white.svg';
@ -91,40 +92,6 @@ const styles = (theme) =>
flexDirection : 'row', flexDirection : 'row',
justifyContent : 'flex-start', justifyContent : 'flex-start',
alignItems : 'center' alignItems : 'center'
},
button :
{
flex : '0 0 auto',
margin : '0.3rem',
borderRadius : 2,
backgroundColor : 'rgba(0, 0, 0, 0.5)',
cursor : 'pointer',
transitionProperty : 'opacity, background-color',
transitionDuration : '0.15s',
width : 'var(--media-control-button-size)',
height : 'var(--media-control-button-size)',
opacity : 0.85,
'&:hover' :
{
opacity : 1
},
'&.unsupported' :
{
pointerEvents : 'none'
},
'&.disabled' :
{
pointerEvents : 'none',
backgroundColor : 'var(--media-control-botton-disabled)'
},
'&.on' :
{
backgroundColor : 'var(--media-control-botton-on)'
},
'&.off' :
{
backgroundColor : 'var(--media-control-botton-off)'
}
} }
}); });
@ -134,6 +101,7 @@ const ListPeer = (props) =>
const { const {
roomClient, roomClient,
isModerator,
peer, peer,
micConsumer, micConsumer,
screenConsumer, screenConsumer,
@ -185,9 +153,8 @@ const ListPeer = (props) =>
})} })}
color={ screenVisible ? 'primary' : 'secondary'} color={ screenVisible ? 'primary' : 'secondary'}
disabled={ peer.peerScreenInProgress } disabled={ peer.peerScreenInProgress }
onClick={(e) => onClick={() =>
{ {
e.stopPropagation();
screenVisible ? screenVisible ?
roomClient.modifyPeerConsumer(peer.id, 'screen', true) : roomClient.modifyPeerConsumer(peer.id, 'screen', true) :
roomClient.modifyPeerConsumer(peer.id, 'screen', false); roomClient.modifyPeerConsumer(peer.id, 'screen', false);
@ -207,9 +174,8 @@ const ListPeer = (props) =>
})} })}
color={ micEnabled ? 'primary' : 'secondary'} color={ micEnabled ? 'primary' : 'secondary'}
disabled={ peer.peerAudioInProgress } disabled={ peer.peerAudioInProgress }
onClick={(e) => onClick={() =>
{ {
e.stopPropagation();
micEnabled ? micEnabled ?
roomClient.modifyPeerConsumer(peer.id, 'mic', true) : roomClient.modifyPeerConsumer(peer.id, 'mic', true) :
roomClient.modifyPeerConsumer(peer.id, 'mic', false); roomClient.modifyPeerConsumer(peer.id, 'mic', false);
@ -221,6 +187,21 @@ const ListPeer = (props) =>
<MicOffIcon /> <MicOffIcon />
} }
</IconButton> </IconButton>
{ isModerator &&
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.kickParticipant',
defaultMessage : 'Kick out participant'
})}
disabled={ peer.peerKickInProgress }
onClick={() =>
{
roomClient.kickPeer(peer.id);
}}
>
<ExitIcon />
</IconButton>
}
</div> </div>
</div> </div>
); );
@ -230,6 +211,7 @@ ListPeer.propTypes =
{ {
roomClient : PropTypes.any.isRequired, roomClient : PropTypes.any.isRequired,
advancedMode : PropTypes.bool, advancedMode : PropTypes.bool,
isModerator : PropTypes.bool,
peer : appPropTypes.Peer.isRequired, peer : appPropTypes.Peer.isRequired,
micConsumer : appPropTypes.Consumer, micConsumer : appPropTypes.Consumer,
webcamConsumer : appPropTypes.Consumer, webcamConsumer : appPropTypes.Consumer,

View File

@ -11,7 +11,9 @@ import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import ListPeer from './ListPeer'; import ListPeer from './ListPeer';
import ListMe from './ListMe'; import ListMe from './ListMe';
import ListModerator from './ListModerator';
import Volume from '../../Containers/Volume'; import Volume from '../../Containers/Volume';
import * as userRoles from '../../../reducers/userRoles';
const styles = (theme) => const styles = (theme) =>
({ ({
@ -76,6 +78,7 @@ class ParticipantList extends React.PureComponent
const { const {
roomClient, roomClient,
advancedMode, advancedMode,
isModerator,
passivePeers, passivePeers,
selectedPeerId, selectedPeerId,
spotlightPeers, spotlightPeers,
@ -84,6 +87,17 @@ class ParticipantList extends React.PureComponent
return ( return (
<div className={classes.root} ref={(node) => { this.node = node; }}> <div className={classes.root} ref={(node) => { this.node = node; }}>
{ isModerator &&
<ul className={classes.list}>
<li className={classes.listheader}>
<FormattedMessage
id='room.moderatoractions'
defaultMessage='Moderator actions'
/>
</li>
<ListModerator />
</ul>
}
<ul className={classes.list}> <ul className={classes.list}>
<li className={classes.listheader}> <li className={classes.listheader}>
<FormattedMessage <FormattedMessage
@ -108,7 +122,7 @@ class ParticipantList extends React.PureComponent
})} })}
onClick={() => roomClient.setSelectedPeer(peerId)} onClick={() => roomClient.setSelectedPeer(peerId)}
> >
<ListPeer id={peerId} advancedMode={advancedMode}> <ListPeer id={peerId} advancedMode={advancedMode} isModerator={isModerator}>
<Volume small id={peerId} /> <Volume small id={peerId} />
</ListPeer> </ListPeer>
</li> </li>
@ -129,7 +143,7 @@ class ParticipantList extends React.PureComponent
})} })}
onClick={() => roomClient.setSelectedPeer(peerId)} onClick={() => roomClient.setSelectedPeer(peerId)}
> >
<ListPeer id={peerId} advancedMode={advancedMode} /> <ListPeer id={peerId} advancedMode={advancedMode} isModerator={isModerator} />
</li> </li>
))} ))}
</ul> </ul>
@ -142,6 +156,7 @@ ParticipantList.propTypes =
{ {
roomClient : PropTypes.any.isRequired, roomClient : PropTypes.any.isRequired,
advancedMode : PropTypes.bool, advancedMode : PropTypes.bool,
isModerator : PropTypes.bool,
passivePeers : PropTypes.array, passivePeers : PropTypes.array,
selectedPeerId : PropTypes.string, selectedPeerId : PropTypes.string,
spotlightPeers : PropTypes.array, spotlightPeers : PropTypes.array,
@ -150,7 +165,12 @@ ParticipantList.propTypes =
const mapStateToProps = (state) => const mapStateToProps = (state) =>
{ {
const isModerator =
state.me.roles.includes(userRoles.MODERATOR) ||
state.me.roles.includes(userRoles.ADMIN);
return { return {
isModerator,
passivePeers : passivePeersSelector(state), passivePeers : passivePeersSelector(state),
selectedPeerId : state.room.selectedPeerId, selectedPeerId : state.room.selectedPeerId,
spotlightPeers : spotlightPeersSelector(state) spotlightPeers : spotlightPeersSelector(state)
@ -165,6 +185,7 @@ const ParticipantListContainer = withRoomContext(connect(
areStatesEqual : (next, prev) => areStatesEqual : (next, prev) =>
{ {
return ( return (
prev.me.roles === next.me.roles &&
prev.peers === next.peers && prev.peers === next.peers &&
prev.room.spotlights === next.room.spotlights && prev.room.spotlights === next.room.spotlights &&
prev.room.selectedPeerId === next.room.selectedPeerId prev.room.selectedPeerId === next.room.selectedPeerId

View File

@ -1,7 +1,10 @@
import * as userRoles from './userRoles';
const initialState = const initialState =
{ {
id : null, id : null,
picture : null, picture : null,
roles : [ userRoles.ALL ],
canSendMic : false, canSendMic : false,
canSendWebcam : false, canSendWebcam : false,
canShareScreen : false, canShareScreen : false,
@ -43,6 +46,21 @@ const me = (state = initialState, action) =>
return { ...state, loggedIn: flag }; return { ...state, loggedIn: flag };
} }
case 'ADD_ROLE':
{
const roles = [ ...state.roles, action.payload.role ];
return { ...state, roles };
}
case 'REMOVE_ROLE':
{
const roles = state.roles.filter((role) =>
role !== action.payload.role);
return { ...state, roles };
}
case 'SET_PICTURE': case 'SET_PICTURE':
return { ...state, picture: action.payload.picture }; return { ...state, picture: action.payload.picture };

View File

@ -16,6 +16,9 @@ const peer = (state = {}, action) =>
case 'SET_PEER_SCREEN_IN_PROGRESS': case 'SET_PEER_SCREEN_IN_PROGRESS':
return { ...state, peerScreenInProgress: action.payload.flag }; return { ...state, peerScreenInProgress: action.payload.flag };
case 'SET_PEER_KICK_IN_PROGRESS':
return { ...state, peerKickInProgress: action.payload.flag };
case 'SET_PEER_RAISE_HAND_STATE': case 'SET_PEER_RAISE_HAND_STATE':
return { ...state, raiseHandState: action.payload.raiseHandState }; return { ...state, raiseHandState: action.payload.raiseHandState };
@ -40,6 +43,21 @@ const peer = (state = {}, action) =>
return { ...state, picture: action.payload.picture }; return { ...state, picture: action.payload.picture };
} }
case 'ADD_PEER_ROLE':
{
const roles = [ ...state.roles, action.payload.role ];
return { ...state, roles };
}
case 'REMOVE_PEER_ROLE':
{
const roles = state.roles.filter((role) =>
role !== action.payload.role);
return { ...state, roles };
}
default: default:
return state; return state;
} }
@ -71,6 +89,8 @@ const peers = (state = {}, action) =>
case 'SET_PEER_RAISE_HAND_STATE': case 'SET_PEER_RAISE_HAND_STATE':
case 'SET_PEER_PICTURE': case 'SET_PEER_PICTURE':
case 'ADD_CONSUMER': case 'ADD_CONSUMER':
case 'ADD_PEER_ROLE':
case 'REMOVE_PEER_ROLE':
{ {
const oldPeer = state[action.payload.peerId]; const oldPeer = state[action.payload.peerId];
@ -82,6 +102,7 @@ const peers = (state = {}, action) =>
return { ...state, [oldPeer.id]: peer(oldPeer, action) }; return { ...state, [oldPeer.id]: peer(oldPeer, action) };
} }
case 'SET_PEER_KICK_IN_PROGRESS':
case 'REMOVE_CONSUMER': case 'REMOVE_CONSUMER':
{ {
const oldPeer = state[action.payload.peerId]; const oldPeer = state[action.payload.peerId];

View File

@ -163,6 +163,12 @@ const room = (state = initialState, action) =>
return { ...state, spotlights }; return { ...state, spotlights };
} }
case 'MUTE_ALL_IN_PROGRESS':
return { ...state, muteAllInProgress: action.payload.flag };
case 'STOP_ALL_VIDEO_IN_PROGRESS':
return { ...state, stopAllVideoInProgress: action.payload.flag };
default: default:
return state; return state;
} }

View File

@ -0,0 +1,4 @@
export const ADMIN = 'admin';
export const MODERATOR = 'moderator';
export const AUTHENTICATED = 'authenticated';
export const ALL = 'normal';

View File

@ -75,13 +75,13 @@ class Lobby extends EventEmitter
if (peer) if (peer)
{ {
peer.socket.removeListener('request', peer.socketRequestHandler); peer.socket.removeListener('request', peer.socketRequestHandler);
peer.removeListener('rolesChange', peer.roleChangeHandler); peer.removeListener('gotRole', peer.gotRoleHandler);
peer.removeListener('displayNameChanged', peer.displayNameChangeHandler); peer.removeListener('displayNameChanged', peer.displayNameChangeHandler);
peer.removeListener('pictureChanged', peer.pictureChangeHandler); peer.removeListener('pictureChanged', peer.pictureChangeHandler);
peer.removeListener('close', peer.closeHandler); peer.removeListener('close', peer.closeHandler);
peer.socketRequestHandler = null; peer.socketRequestHandler = null;
peer.roleChangeHandler = null; peer.gotRoleHandler = null;
peer.displayNameChangeHandler = null; peer.displayNameChangeHandler = null;
peer.pictureChangeHandler = null; peer.pictureChangeHandler = null;
peer.closeHandler = null; peer.closeHandler = null;
@ -116,7 +116,7 @@ class Lobby extends EventEmitter
}); });
}; };
peer.roleChangeHandler = () => peer.gotRoleHandler = () =>
{ {
logger.info('parkPeer() | rolesChange [peer:"%s"]', peer.id); logger.info('parkPeer() | rolesChange [peer:"%s"]', peer.id);
@ -156,7 +156,7 @@ class Lobby extends EventEmitter
this._peers.set(peer.id, peer); this._peers.set(peer.id, peer);
peer.on('rolesChange', peer.roleChangeHandler); peer.on('gotRole', peer.gotRoleHandler);
peer.on('displayNameChanged', peer.displayNameChangeHandler); peer.on('displayNameChanged', peer.displayNameChangeHandler);
peer.on('pictureChanged', peer.pictureChangeHandler); peer.on('pictureChanged', peer.pictureChangeHandler);

View File

@ -224,7 +224,7 @@ class Peer extends EventEmitter
logger.info('addRole() | [newRole:"%s]"', newRole); logger.info('addRole() | [newRole:"%s]"', newRole);
this.emit('rolesChange', { newRole }); this.emit('gotRole', { newRole });
} }
} }
@ -236,7 +236,7 @@ class Peer extends EventEmitter
logger.info('removeRole() | [oldRole:"%s]"', oldRole); logger.info('removeRole() | [oldRole:"%s]"', oldRole);
this.emit('rolesChange', { oldRole }); this.emit('lostRole', { oldRole });
} }
} }
@ -302,7 +302,8 @@ class Peer extends EventEmitter
{ {
id : this.id, id : this.id,
displayName : this.displayName, displayName : this.displayName,
picture : this.picture picture : this.picture,
roles : this.roles
}; };
return peerInfo; return peerInfo;

View File

@ -420,6 +420,32 @@ class Room extends EventEmitter
picture : peer.picture picture : peer.picture
}, true); }, true);
}); });
peer.on('gotRole', ({ newRole }) =>
{
// Ensure the Peer is joined.
if (!peer.joined)
return;
// Spread to others
this._notification(peer.socket, 'gotRole', {
peerId : peer.id,
role : newRole
}, true);
});
peer.on('lostRole', ({ oldRole }) =>
{
// Ensure the Peer is joined.
if (!peer.joined)
return;
// Spread to others
this._notification(peer.socket, 'lostRole', {
peerId : peer.id,
role : oldRole
}, true);
});
} }
async _handleSocketRequest(peer, request, cb) async _handleSocketRequest(peer, request, cb)
@ -483,7 +509,10 @@ class Room extends EventEmitter
.filter((joinedPeer) => joinedPeer.id !== peer.id) .filter((joinedPeer) => joinedPeer.id !== peer.id)
.map((joinedPeer) => (joinedPeer.peerInfo)); .map((joinedPeer) => (joinedPeer.peerInfo));
cb(null, { peers: peerInfos }); cb(null, {
roles : peer.roles,
peers : peerInfos
});
// Mark the new Peer as joined. // Mark the new Peer as joined.
peer.joined = true; peer.joined = true;
@ -511,7 +540,8 @@ class Room extends EventEmitter
{ {
id : peer.id, id : peer.id,
displayName : displayName, displayName : displayName,
picture : picture picture : picture,
roles : peer.roles
} }
); );
} }
@ -1077,6 +1107,69 @@ class Room extends EventEmitter
break; break;
} }
case 'moderator:muteAll':
{
if (
!peer.hasRole(userRoles.MODERATOR) &&
!peer.hasRole(userRoles.ADMIN)
)
throw new Error('peer does not have moderator priveleges');
// Spread to others
this._notification(peer.socket, 'moderator:mute', {
peerId : peer.id
}, true);
cb();
break;
}
case 'moderator:stopAllVideo':
{
if (
!peer.hasRole(userRoles.MODERATOR) &&
!peer.hasRole(userRoles.ADMIN)
)
throw new Error('peer does not have moderator priveleges');
// Spread to others
this._notification(peer.socket, 'moderator:stopVideo', {
peerId : peer.id
}, true);
cb();
break;
}
case 'moderator:kickPeer':
{
if (
!peer.hasRole(userRoles.MODERATOR) &&
!peer.hasRole(userRoles.ADMIN)
)
throw new Error('peer does not have moderator priveleges');
const { peerId } = request.data;
const kickPeer = this._peers[peerId];
if (!kickPeer)
throw new Error(`peer with id "${peerId}" not found`);
this._notification(
kickPeer.socket,
'moderator:kick'
);
kickPeer.close();
cb();
break;
}
default: default:
{ {
logger.error('unknown request.method "%s"', request.method); logger.error('unknown request.method "%s"', request.method);

View File

@ -1,12 +1,12 @@
module.exports = { module.exports = {
// Allowed to enter locked rooms + all other priveleges // Allowed to enter locked rooms + all other priveleges
ADMIN : 0, ADMIN : 'admin',
// Allowed to enter restricted rooms if configured. // Allowed to enter restricted rooms if configured.
// Allowed to moderate users in a room (mute all, // Allowed to moderate users in a room (mute all,
// spotlight video, kick users) // spotlight video, kick users)
MODERATOR : 1, MODERATOR : 'moderator',
// Same as MODERATOR, but can't moderate users // Same as MODERATOR, but can't moderate users
AUTHENTICATED : 2, AUTHENTICATED : 'authenticated',
// No priveleges // No priveleges
ALL : 3 ALL : 'normal'
}; };