Merge branch 'feat-user-roles' into develop

auto_join_3.3
Håvar Aambø Fosstveit 2020-03-26 21:31:57 +01:00
commit 5071282d40
31 changed files with 1244 additions and 669 deletions

View File

@ -417,7 +417,7 @@ export default class RoomClient
login() login()
{ {
const url = `/auth/login?id=${this._peerId}`; const url = `/auth/login?peerId=${this._peerId}&roomId=${this._roomId}`;
window.open(url, 'loginWindow'); window.open(url, 'loginWindow');
} }
@ -433,16 +433,8 @@ export default class RoomClient
const { displayName, picture } = data; const { displayName, picture } = data;
if (store.getState().room.state === 'connected')
{
this.changeDisplayName(displayName);
this.changePicture(picture);
}
else
{
store.dispatch(settingsActions.setDisplayName(displayName)); store.dispatch(settingsActions.setDisplayName(displayName));
store.dispatch(meActions.setPicture(picture)); store.dispatch(meActions.setPicture(picture));
}
store.dispatch(meActions.loggedIn(true)); store.dispatch(meActions.loggedIn(true));
@ -1155,6 +1147,86 @@ 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));
}
async closeMeeting()
{
logger.debug('closeMeeting()');
store.dispatch(
roomActions.setCloseMeetingInProgress(true));
try
{
await this.sendRequest('moderator:closeMeeting');
}
catch (error)
{
logger.error('closeMeeting() failed: %o', error);
}
store.dispatch(
roomActions.setCloseMeetingInProgress(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)
@ -1914,10 +1986,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(
{ {
@ -2017,6 +2089,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:
{ {
logger.error( logger.error(
@ -2195,7 +2357,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,
@ -2203,7 +2365,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

@ -15,6 +15,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

@ -110,3 +110,21 @@ 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 }
});
export const setCloseMeetingInProgress = (flag) =>
({
type : 'CLOSE_MEETING_IN_PROGRESS',
payload : { flag }
});

View File

@ -1,21 +1,15 @@
import React, { useState, useEffect } from 'react'; import React, { useState } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../RoomContext';
import isElectron from 'is-electron'; import isElectron from 'is-electron';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useIntl, FormattedMessage } from 'react-intl'; import { useIntl, FormattedMessage } from 'react-intl';
import randomString from 'random-string'; import randomString from 'random-string';
import Dialog from '@material-ui/core/Dialog'; import Dialog from '@material-ui/core/Dialog';
import DialogContentText from '@material-ui/core/DialogContentText'; import DialogContentText from '@material-ui/core/DialogContentText';
import IconButton from '@material-ui/core/IconButton';
import AccountCircle from '@material-ui/icons/AccountCircle';
import Avatar from '@material-ui/core/Avatar';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import TextField from '@material-ui/core/TextField'; import TextField from '@material-ui/core/TextField';
import Tooltip from '@material-ui/core/Tooltip';
import CookieConsent from 'react-cookie-consent'; import CookieConsent from 'react-cookie-consent';
import MuiDialogTitle from '@material-ui/core/DialogTitle'; import MuiDialogTitle from '@material-ui/core/DialogTitle';
import MuiDialogContent from '@material-ui/core/DialogContent'; import MuiDialogContent from '@material-ui/core/DialogContent';
@ -88,63 +82,12 @@ const styles = (theme) =>
const DialogTitle = withStyles(styles)((props) => const DialogTitle = withStyles(styles)((props) =>
{ {
const [ open, setOpen ] = useState(false); const { children, classes, ...other } = props;
const intl = useIntl();
useEffect(() =>
{
const openTimer = setTimeout(() => setOpen(true), 1000);
const closeTimer = setTimeout(() => setOpen(false), 4000);
return () =>
{
clearTimeout(openTimer);
clearTimeout(closeTimer);
};
}, []);
const { children, classes, myPicture, onLogin, ...other } = props;
const handleTooltipClose = () =>
{
setOpen(false);
};
const handleTooltipOpen = () =>
{
setOpen(true);
};
return ( return (
<MuiDialogTitle disableTypography className={classes.dialogTitle} {...other}> <MuiDialogTitle disableTypography className={classes.dialogTitle} {...other}>
{ window.config && window.config.logo && <img alt='Logo' className={classes.logo} src={window.config.logo} /> } { window.config && window.config.logo && <img alt='Logo' className={classes.logo} src={window.config.logo} /> }
<Typography variant='h5'>{children}</Typography> <Typography variant='h5'>{children}</Typography>
{ window.config && window.config.loginEnabled &&
<Tooltip
onClose={handleTooltipClose}
onOpen={handleTooltipOpen}
open={open}
title={intl.formatMessage({
id : 'tooltip.login',
defaultMessage : 'Click to log in'
})}
placement='left'
>
<IconButton
aria-label='Account'
className={classes.loginButton}
color='inherit'
onClick={onLogin}
>
{ myPicture ?
<Avatar src={myPicture} className={classes.largeAvatar} />
:
<AccountCircle className={classes.largeIcon} />
}
</IconButton>
</Tooltip>
}
</MuiDialogTitle> </MuiDialogTitle>
); );
}); });
@ -165,9 +108,6 @@ const DialogActions = withStyles((theme) => ({
}))(MuiDialogActions); }))(MuiDialogActions);
const ChooseRoom = ({ const ChooseRoom = ({
roomClient,
loggedIn,
myPicture,
classes classes
}) => }) =>
{ {
@ -184,13 +124,7 @@ const ChooseRoom = ({
paper : classes.dialogPaper paper : classes.dialogPaper
}} }}
> >
<DialogTitle <DialogTitle>
myPicture={myPicture}
onLogin={() =>
{
loggedIn ? roomClient.logout() : roomClient.login();
}}
>
{ window.config && window.config.title ? window.config.title : 'Multiparty meeting' } { window.config && window.config.title ? window.config.title : 'Multiparty meeting' }
<hr /> <hr />
</DialogTitle> </DialogTitle>
@ -258,34 +192,7 @@ const ChooseRoom = ({
ChooseRoom.propTypes = ChooseRoom.propTypes =
{ {
roomClient : PropTypes.any.isRequired,
loginEnabled : PropTypes.bool.isRequired,
loggedIn : PropTypes.bool.isRequired,
myPicture : PropTypes.string,
classes : PropTypes.object.isRequired classes : PropTypes.object.isRequired
}; };
const mapStateToProps = (state) => export default withStyles(styles)(ChooseRoom);
{
return {
loginEnabled : state.me.loginEnabled,
loggedIn : state.me.loggedIn,
myPicture : state.me.picture
};
};
export default withRoomContext(connect(
mapStateToProps,
null,
null,
{
areStatesEqual : (next, prev) =>
{
return (
prev.me.loginEnabled === next.me.loginEnabled &&
prev.me.loggedIn === next.me.loggedIn &&
prev.me.picture === next.me.picture
);
}
}
)(withStyles(styles)(ChooseRoom)));

View File

@ -0,0 +1,118 @@
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 className={classes.divider} />
<Button
aria-label={intl.formatMessage({
id : 'room.closeMeeting',
defaultMessage : 'Close meeting'
})}
className={classes.actionButton}
variant='contained'
color='secondary'
disabled={room.closeMeetingInProgress}
onClick={() => roomClient.closeMeeting()}
>
<FormattedMessage
id='room.closeMeeting'
defaultMessage='Close meeting'
/>
</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,11 @@ 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 +160,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,
@ -151,6 +170,8 @@ ParticipantList.propTypes =
const mapStateToProps = (state) => const mapStateToProps = (state) =>
{ {
return { return {
isModerator : state.me.roles.includes(userRoles.MODERATOR) ||
state.me.roles.includes(userRoles.ADMIN),
passivePeers : passivePeersSelector(state), passivePeers : passivePeersSelector(state),
selectedPeerId : state.room.selectedPeerId, selectedPeerId : state.room.selectedPeerId,
spotlightPeers : spotlightPeersSelector(state) spotlightPeers : spotlightPeersSelector(state)
@ -165,6 +186,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,8 +1,11 @@
import * as userRoles from './userRoles';
const initialState = const initialState =
{ {
id : null, id : null,
picture : null, picture : null,
isMobile : false, isMobile : false,
roles : [ userRoles.ALL ],
canSendMic : false, canSendMic : false,
canSendWebcam : false, canSendWebcam : false,
canShareScreen : false, canShareScreen : false,
@ -49,6 +52,24 @@ const me = (state = initialState, action) =>
return { ...state, loggedIn: flag }; return { ...state, loggedIn: flag };
} }
case 'ADD_ROLE':
{
if (state.roles.includes(action.payload.role))
return state;
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

@ -17,6 +17,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

@ -18,7 +18,10 @@ const initialState =
spotlights : [], spotlights : [],
settingsOpen : false, settingsOpen : false,
lockDialogOpen : false, lockDialogOpen : false,
joined : false joined : false,
muteAllInProgress : false,
stopAllVideoInProgress : false,
closeMeetingInProgress : false
}; };
const room = (state = initialState, action) => const room = (state = initialState, action) =>
@ -110,7 +113,7 @@ const room = (state = initialState, action) =>
case 'TOGGLE_JOINED': case 'TOGGLE_JOINED':
{ {
const joined = !state.joined; const joined = true;
return { ...state, joined }; return { ...state, joined };
} }
@ -163,6 +166,15 @@ 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 };
case 'CLOSE_MEETING_IN_PROGRESS':
return { ...state, closeMeetingInProgress: 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

@ -49,6 +49,9 @@
"room.spotlights": "Spotlight中的参与者", "room.spotlights": "Spotlight中的参与者",
"room.passive": "被动参与者", "room.passive": "被动参与者",
"room.videoPaused": "该视频已暂停", "room.videoPaused": "该视频已暂停",
"room.muteAll": null,
"room.stopAllVideo": null,
"room.closeMeeting": null,
"tooltip.login": "登录", "tooltip.login": "登录",
"tooltip.logout": "注销", "tooltip.logout": "注销",
@ -60,6 +63,7 @@
"tooltip.lobby": "显示大厅", "tooltip.lobby": "显示大厅",
"tooltip.settings": "显示设置", "tooltip.settings": "显示设置",
"tooltip.participants": "显示参加者", "tooltip.participants": "显示参加者",
"tooltip.kickParticipant": null,
"label.roomName": "房间名称", "label.roomName": "房间名称",
"label.chooseRoomButton": "继续", "label.chooseRoomButton": "继续",

View File

@ -4,7 +4,7 @@
"socket.reconnected": "Verbindung wieder herges|tellt", "socket.reconnected": "Verbindung wieder herges|tellt",
"socket.requestError": "Fehler bei Serveranfrage", "socket.requestError": "Fehler bei Serveranfrage",
"room.chooseRoom": "Choose the name of the room you would like to join", "room.chooseRoom": null,
"room.cookieConsent": "Diese Seite verwendet Cookies, um die Benutzerfreundlichkeit zu erhöhen", "room.cookieConsent": "Diese Seite verwendet Cookies, um die Benutzerfreundlichkeit zu erhöhen",
"room.consentUnderstand": "I understand", "room.consentUnderstand": "I understand",
"room.joined": "Konferenzraum betreten", "room.joined": "Konferenzraum betreten",
@ -49,6 +49,9 @@
"room.spotlights": "Aktive Teinehmer", "room.spotlights": "Aktive Teinehmer",
"room.passive": "Passive Teilnehmer", "room.passive": "Passive Teilnehmer",
"room.videoPaused": "Video gestoppt", "room.videoPaused": "Video gestoppt",
"room.muteAll": null,
"room.stopAllVideo": null,
"room.closeMeeting": null,
"tooltip.login": "Anmelden", "tooltip.login": "Anmelden",
"tooltip.logout": "Abmelden", "tooltip.logout": "Abmelden",
@ -60,9 +63,10 @@
"tooltip.lobby": "Warteraum", "tooltip.lobby": "Warteraum",
"tooltip.settings": "Einstellungen", "tooltip.settings": "Einstellungen",
"tooltip.participants": "Teilnehmer", "tooltip.participants": "Teilnehmer",
"tooltip.kickParticipant": null,
"label.roomName": "Room name", "label.roomName": null,
"label.chooseRoomButton": "Continue", "label.chooseRoomButton": null,
"label.yourName": "Dein Name", "label.yourName": "Dein Name",
"label.newWindow": "In separatem Fenster öffnen", "label.newWindow": "In separatem Fenster öffnen",
"label.fullscreen": "Vollbild", "label.fullscreen": "Vollbild",

View File

@ -49,6 +49,9 @@
"room.spotlights": "Deltagere i fokus", "room.spotlights": "Deltagere i fokus",
"room.passive": "Passive deltagere", "room.passive": "Passive deltagere",
"room.videoPaused": "Denne video er sat på pause", "room.videoPaused": "Denne video er sat på pause",
"room.muteAll": null,
"room.stopAllVideo": null,
"room.closeMeeting": null,
"tooltip.login": "Log ind", "tooltip.login": "Log ind",
"tooltip.logout": "Log ud", "tooltip.logout": "Log ud",
@ -60,6 +63,7 @@
"tooltip.lobby": "Vis lobby", "tooltip.lobby": "Vis lobby",
"tooltip.settings": "Vis indstillinger", "tooltip.settings": "Vis indstillinger",
"tooltip.participants": "Vis deltagere", "tooltip.participants": "Vis deltagere",
"tooltip.kickParticipant": null,
"label.roomName": "Værelsesnavn", "label.roomName": "Værelsesnavn",
"label.chooseRoomButton": "Fortsæt", "label.chooseRoomButton": "Fortsæt",
@ -106,7 +110,7 @@
"filesharing.finished": "Filen er færdig med at downloade", "filesharing.finished": "Filen er færdig med at downloade",
"filesharing.save": "Gem", "filesharing.save": "Gem",
"filesharing.sharedFile": "{displayName} delte en fil", "filesharing.sharedFile": "{displayName} delte en fil",
"filesharing.download": "Download", "filesharing.download": null,
"filesharing.missingSeeds": "Hvis denne proces tager lang tid, er der muligvis ikke nogen, der seedede denne torrent. Prøv at bede nogen om at uploade den fil, du ønsker at hente.", "filesharing.missingSeeds": "Hvis denne proces tager lang tid, er der muligvis ikke nogen, der seedede denne torrent. Prøv at bede nogen om at uploade den fil, du ønsker at hente.",
"device.devicesChanged": "Detekteret ndringer i dine enheder, konfigurer dine enheder i indstillingsdialogen", "device.devicesChanged": "Detekteret ndringer i dine enheder, konfigurer dine enheder i indstillingsdialogen",

View File

@ -49,6 +49,9 @@
"room.spotlights": "Συμμετέχοντες στο Spotlight", "room.spotlights": "Συμμετέχοντες στο Spotlight",
"room.passive": "Παθητικοί συμμετέχοντες", "room.passive": "Παθητικοί συμμετέχοντες",
"room.videoPaused": "Το βίντεο έχει σταματήσει", "room.videoPaused": "Το βίντεο έχει σταματήσει",
"room.muteAll": null,
"room.stopAllVideo": null,
"room.closeMeeting": null,
"tooltip.login": "Σύνδεση", "tooltip.login": "Σύνδεση",
"tooltip.logout": "Αποσύνδεση", "tooltip.logout": "Αποσύνδεση",
@ -60,6 +63,7 @@
"tooltip.lobby": "Εμφάνιση λόμπι", "tooltip.lobby": "Εμφάνιση λόμπι",
"tooltip.settings": "Εμφάνιση ρυθμίσεων", "tooltip.settings": "Εμφάνιση ρυθμίσεων",
"tooltip.participants": "Εμφάνιση συμμετεχόντων", "tooltip.participants": "Εμφάνιση συμμετεχόντων",
"tooltip.kickParticipant": null,
"label.roomName": "Όνομα δωματίου", "label.roomName": "Όνομα δωματίου",
"label.chooseRoomButton": "Συνέχεια", "label.chooseRoomButton": "Συνέχεια",
@ -75,8 +79,8 @@
"label.shareFile": "Διαμοιραστείτε ένα αρχείο", "label.shareFile": "Διαμοιραστείτε ένα αρχείο",
"label.fileSharingUnsupported": "Ο διαμοιρασμός αρχείων δεν υποστηρίζεται", "label.fileSharingUnsupported": "Ο διαμοιρασμός αρχείων δεν υποστηρίζεται",
"label.unknown": "Άγνωστο", "label.unknown": "Άγνωστο",
"label.democratic": "Democratic view", "label.democratic": null,
"label.filmstrip": "Filmstrip view", "label.filmstrip": null,
"label.low": "Χαμηλή", "label.low": "Χαμηλή",
"label.medium": "Μέτρια", "label.medium": "Μέτρια",
"label.high": "Υψηλή (HD)", "label.high": "Υψηλή (HD)",

View File

@ -49,6 +49,9 @@
"room.spotlights": "Participants in Spotlight", "room.spotlights": "Participants in Spotlight",
"room.passive": "Passive Participants", "room.passive": "Passive Participants",
"room.videoPaused": "This video is paused", "room.videoPaused": "This video is paused",
"room.muteAll": "Mute all",
"room.stopAllVideo": "Stop all video",
"room.closeMeeting": "Close meeting",
"tooltip.login": "Log in", "tooltip.login": "Log in",
"tooltip.logout": "Log out", "tooltip.logout": "Log out",
@ -60,6 +63,7 @@
"tooltip.lobby": "Show lobby", "tooltip.lobby": "Show lobby",
"tooltip.settings": "Show settings", "tooltip.settings": "Show settings",
"tooltip.participants": "Show participants", "tooltip.participants": "Show participants",
"tooltip.kickParticipant": "Kick out participant",
"label.roomName": "Room name", "label.roomName": "Room name",
"label.chooseRoomButton": "Continue", "label.chooseRoomButton": "Continue",

View File

@ -49,6 +49,9 @@
"room.spotlights": "Participantes destacados", "room.spotlights": "Participantes destacados",
"room.passive": "Participantes pasivos", "room.passive": "Participantes pasivos",
"room.videoPaused": "El vídeo está pausado", "room.videoPaused": "El vídeo está pausado",
"room.muteAll": null,
"room.stopAllVideo": null,
"room.closeMeeting": null,
"tooltip.login": "Entrar", "tooltip.login": "Entrar",
"tooltip.logout": "Salir", "tooltip.logout": "Salir",
@ -60,6 +63,7 @@
"tooltip.lobby": "Mostrar sala de espera", "tooltip.lobby": "Mostrar sala de espera",
"tooltip.settings": "Mostrar ajustes", "tooltip.settings": "Mostrar ajustes",
"tooltip.participants": "Mostrar participantes", "tooltip.participants": "Mostrar participantes",
"tooltip.kickParticipant": null,
"label.roomName": "Nombre de la sala", "label.roomName": "Nombre de la sala",
"label.chooseRoomButton": "Continuar", "label.chooseRoomButton": "Continuar",

View File

@ -1,139 +1,143 @@
{ {
"socket.disconnected" : " Vous avez été déconnecté", "socket.disconnected": "Vous avez été déconnecté",
"socket.reconnecting" : " Vous avez été déconnecté, reconnexion en cours", "socket.reconnecting": "Vous avez été déconnecté, reconnexion en cours",
"socket.reconnected" : " Vous êtes reconnecté", "socket.reconnected": "Vous êtes reconnecté",
"socket.requestError" : " Erreur sur une requête serveur", "socket.requestError": "Erreur sur une requête serveur",
"room.chooseRoom" : " Choisissez le nom de la réunion que vous souhaitez rejoindre", "room.chooseRoom": "Choisissez le nom de la réunion que vous souhaitez rejoindre",
"room.cookieConsent" : " Ce site utilise les cookies pour améliorer votre expérience utilisateur", "room.cookieConsent": "Ce site utilise les cookies pour améliorer votre expérience utilisateur",
"room.consentUnderstand": "I understand", "room.consentUnderstand": "I understand",
"room.joined" : " Vous avez rejoint la salle", "room.joined": "Vous avez rejoint la salle",
"room.cantJoin" : " Impossible de rejoindre la salle", "room.cantJoin": "Impossible de rejoindre la salle",
"room.youLocked" : " Vous avez privatisé la salle", "room.youLocked": "Vous avez privatisé la salle",
"room.cantLock" : " Impossible de privatiser la salle", "room.cantLock": "Impossible de privatiser la salle",
"room.youUnLocked" : " Vous avez dé-privatiser la salle", "room.youUnLocked": "Vous avez dé-privatiser la salle",
"room.cantUnLock" : " Impossible de dé-privatiser la réunion", "room.cantUnLock": "Impossible de dé-privatiser la réunion",
"room.locked" : " La réunion est privée", "room.locked": "La réunion est privée",
"room.unlocked" : " La réunion est publique", "room.unlocked": "La réunion est publique",
"room.newLobbyPeer" : " Un nouveau participant est dans la salle dattente", "room.newLobbyPeer": "Un nouveau participant est dans la salle dattente",
"room.lobbyPeerLeft" : " Un participant de la salle dattente vient de partir", "room.lobbyPeerLeft": "Un participant de la salle dattente vient de partir",
"room.lobbyPeerChangedDisplayName" : " Un participant dans la salle dattente a changé de nom pour {displayName}", "room.lobbyPeerChangedDisplayName": "Un participant dans la salle dattente a changé de nom pour {displayName}",
"room.lobbyPeerChangedPicture" : " Un participant dans le hall à changer de photo", "room.lobbyPeerChangedPicture": "Un participant dans le hall à changer de photo",
"room.setAccessCode" : " Code daccès à la réunion mis à jour", "room.setAccessCode": "Code daccès à la réunion mis à jour",
"room.accessCodeOn" : " Code daccès à la réunion activée", "room.accessCodeOn": "Code daccès à la réunion activée",
"room.accessCodeOff" : " Code daccès à la réunion désactivée", "room.accessCodeOff": "Code daccès à la réunion désactivée",
"room.peerChangedDisplayName" : " {oldDisplayName} est maintenant {displayName}", "room.peerChangedDisplayName": "{oldDisplayName} est maintenant {displayName}",
"room.newPeer" : " {displayName} a rejoint la salle", "room.newPeer": "{displayName} a rejoint la salle",
"room.newFile" : " Nouveau fichier disponible", "room.newFile": "Nouveau fichier disponible",
"room.toggleAdvancedMode" : " Basculer en mode avancé", "room.toggleAdvancedMode": "Basculer en mode avancé",
"room.setDemocraticView" : " Passer en vue démocratique", "room.setDemocraticView": "Passer en vue démocratique",
"room.setFilmStripView" : " Passer en vue vignette", "room.setFilmStripView": "Passer en vue vignette",
"room.loggedIn" : " Vous êtes connecté", "room.loggedIn": "Vous êtes connecté",
"room.loggedOut" : " Vous êtes déconnecté", "room.loggedOut": "Vous êtes déconnecté",
"room.changedDisplayName" : " Votre nom à changer pour {displayname}", "room.changedDisplayName": "Votre nom à changer pour {displayname}",
"room.changeDisplayNameError" : " Une erreur sest produite pour votre changement de nom", "room.changeDisplayNameError": "Une erreur sest produite pour votre changement de nom",
"room.chatError" : " Impossible denvoyer un message", "room.chatError": "Impossible denvoyer un message",
"room.aboutToJoin" : " Vous allez rejoindre une réunion", "room.aboutToJoin": "Vous allez rejoindre une réunion",
"room.roomId" : " Salle ID: {roomName}", "room.roomId": "Salle ID: {roomName}",
"room.setYourName" : " Choisissez votre nom de participant puis comment vous connecter :", "room.setYourName": "Choisissez votre nom de participant puis comment vous connecter:",
"room.audioOnly" : " Audio uniquement", "room.audioOnly": "Audio uniquement",
"room.audioVideo" : " Audio et Vidéo", "room.audioVideo": "Audio et Vidéo",
"room.youAreReady" : " Ok, vous êtes prêt", "room.youAreReady": "Ok, vous êtes prêt",
"room.emptyRequireLogin" : " La réunion est vide ! Vous pouvez vous connecter pour commencer la réunion ou attendre qu'un hôte se connecte", "room.emptyRequireLogin": "La réunion est vide ! Vous pouvez vous connecter pour commencer la réunion ou attendre qu'un hôte se connecte",
"room.locketWait" : " La réunion est privatisée - attendez que quelquun vous laisse entrer", "room.locketWait": "La réunion est privatisée - attendez que quelquun vous laisse entrer",
"room.lobbyAdministration" : " Administration de la salle dattente", "room.lobbyAdministration": "Administration de la salle dattente",
"room.peersInLobby" : " Participants dans la salle dattente", "room.peersInLobby": "Participants dans la salle dattente",
"room.lobbyEmpty" : " Il n'y a actuellement aucun participant dans la salle d'attente", "room.lobbyEmpty": "Il n'y a actuellement aucun participant dans la salle d'attente",
"room.hiddenPeers" : " {hiddenPeersCount, plural, one {participant} other {participants}}", "room.hiddenPeers": "{hiddenPeersCount, plural, one {participant} other {participants}}",
"room.me" : " Moi", "room.me": "Moi",
"room.spotlights" : " Participants actifs", "room.spotlights": "Participants actifs",
"room.passive" : " Participants passifs", "room.passive": "Participants passifs",
"room.videoPaused" : " La vidéo est en pause", "room.videoPaused": "La vidéo est en pause",
"room.muteAll": null,
"room.stopAllVideo": null,
"room.closeMeeting": null,
"tooltip.login" : " Connexion", "tooltip.login": "Connexion",
"tooltip.logout" : " Déconnexion", "tooltip.logout": "Déconnexion",
"tooltip.admitFromLobby" : " Autorisé depuis la salle d'attente", "tooltip.admitFromLobby": "Autorisé depuis la salle d'attente",
"tooltip.lockRoom" : " Privatisation de la salle", "tooltip.lockRoom": "Privatisation de la salle",
"tooltip.unLockRoom" : " Dé-privatisation de la salle", "tooltip.unLockRoom": "Dé-privatisation de la salle",
"tooltip.enterFullscreen" : " Afficher en plein écran", "tooltip.enterFullscreen": "Afficher en plein écran",
"tooltip.leaveFullscreen" : " Quitter le plein écran", "tooltip.leaveFullscreen": "Quitter le plein écran",
"tooltip.lobby" : " Afficher la salle d'attente", "tooltip.lobby": "Afficher la salle d'attente",
"tooltip.settings" : " Afficher les paramètres", "tooltip.settings": "Afficher les paramètres",
"tooltip.participants": "Afficher les participants", "tooltip.participants": "Afficher les participants",
"tooltip.kickParticipant": null,
"label.roomName" : " Nom de la salle", "label.roomName": "Nom de la salle",
"label.chooseRoomButton" : " Continuer", "label.chooseRoomButton": "Continuer",
"label.yourName" : " Votre nom", "label.yourName": "Votre nom",
"label.newWindow" : " Nouvelle fenêtre", "label.newWindow": "Nouvelle fenêtre",
"label.fullscreen" : " Plein écran", "label.fullscreen": "Plein écran",
"label.openDrawer" : " Ouvrir Drawer", "label.openDrawer": "Ouvrir Drawer",
"label.leave" : " Quiter", "label.leave": "Quiter",
"label.chatInput" : " Entrer un message", "label.chatInput": "Entrer un message",
"label.chat" : " Chat", "label.chat": "Chat",
"label.filesharing" : " Partage de fichier", "label.filesharing": "Partage de fichier",
"label.participants" : " Participants", "label.participants": "Participants",
"label.shareFile" : " Partager un fichier", "label.shareFile": "Partager un fichier",
"label.fileSharingUnsupported" : " Partage de fichier non supporté", "label.fileSharingUnsupported": "Partage de fichier non supporté",
"label.unknown" : " Inconnu", "label.unknown": "Inconnu",
"label.democratic" : " Vue démocratique", "label.democratic": "Vue démocratique",
"label.filmstrip" : " Vue avec miniature", "label.filmstrip": "Vue avec miniature",
"label.low" : " Basse définition", "label.low": "Basse définition",
"label.medium" : " Définition normale", "label.medium": "Définition normale",
"label.high" : " Haute Définition (HD)", "label.high": "Haute Définition (HD)",
"label.veryHigh" : " Très Haute Définition (FHD)", "label.veryHigh": "Très Haute Définition (FHD)",
"label.ultra" : " Ultra Haute Définition", "label.ultra": "Ultra Haute Définition",
"label.close" : " Fermer", "label.close": "Fermer",
"settings.settings" : " Paramètres", "settings.settings": "Paramètres",
"settings.camera" : " Caméra", "settings.camera": "Caméra",
"settings.selectCamera" : " Sélectionner votre caméra", "settings.selectCamera": "Sélectionner votre caméra",
"settings.cantSelectCamera" : " Impossible de sélectionner votre caméra", "settings.cantSelectCamera": "Impossible de sélectionner votre caméra",
"settings.audio" : " Microphone", "settings.audio": "Microphone",
"settings.selectAudio" : " Sélectionner votre microphone", "settings.selectAudio": "Sélectionner votre microphone",
"settings.cantSelectAudio" : " Impossible de sélectionner votre la caméra", "settings.cantSelectAudio": "Impossible de sélectionner votre la caméra",
"settings.resolution" : " Sélection votre résolution", "settings.resolution": "Sélection votre résolution",
"settings.layout" : " Mode d'affichage de la salle", "settings.layout": "Mode d'affichage de la salle",
"settings.selectRoomLayout" : " Sélectionner l'affiche de la salle", "settings.selectRoomLayout": "Sélectionner l'affiche de la salle",
"settings.advancedMode" : " Mode avancé", "settings.advancedMode": "Mode avancé",
"settings.permanentTopBar": "Barre supérieure permanente", "settings.permanentTopBar": "Barre supérieure permanente",
"settings.lastn": "Nombre de vidéos visibles", "settings.lastn": "Nombre de vidéos visibles",
"filesharing.saveFileError" : " Impossible d'enregistrer le fichier", "filesharing.saveFileError": "Impossible d'enregistrer le fichier",
"filesharing.startingFileShare" : " Début du transfert de fichier", "filesharing.startingFileShare": "Début du transfert de fichier",
"filesharing.successfulFileShare" : " Fichier transféré", "filesharing.successfulFileShare": "Fichier transféré",
"filesharing.unableToShare" : " Impossible de transférer le fichier", "filesharing.unableToShare": "Impossible de transférer le fichier",
"filesharing.error" : " Erreur lors du transfert de fichier", "filesharing.error": "Erreur lors du transfert de fichier",
"filesharing.finished" : " Fin du transfert de fichier", "filesharing.finished": "Fin du transfert de fichier",
"filesharing.save" : " Sauver", "filesharing.save": "Sauver",
"filesharing.sharedFile" : " {displayName} a partagé un fichier", "filesharing.sharedFile": "{displayName} a partagé un fichier",
"filesharing.download" : " Télécharger", "filesharing.download": "Télécharger",
"filesharing.missingSeeds" : " Si le téléchargement prend trop de temps cest quil ny a peut-être plus personne qui partage ce torrent. Demander à quelquun de repartager le document.", "filesharing.missingSeeds": "Si le téléchargement prend trop de temps cest quil ny a peut-être plus personne qui partage ce torrent. Demander à quelquun de repartager le document.",
"devices.devicesChanged" : " Vos périphériques ont changé, reconfigurer vos périphériques avec le menu paramètre", "devices.devicesChanged": "Vos périphériques ont changé, reconfigurer vos périphériques avec le menu paramètre",
"device.audioUnsupported" : " Microphone non supporté", "device.audioUnsupported": "Microphone non supporté",
"device.activateAudio" : " Activer l'audio", "device.activateAudio": "Activer l'audio",
"device.muteAudio" : " Désactiver l'audio", "device.muteAudio": "Désactiver l'audio",
"device.unMuteAudio" : " Réactiver l'audio", "device.unMuteAudio": "Réactiver l'audio",
"device.videoUnsupported" : " Vidéo non supporté", "device.videoUnsupported": "Vidéo non supporté",
"device.startVideo" : " Démarrer la vidéo", "device.startVideo": "Démarrer la vidéo",
"device.stopVideo" : " Arrêter la vidéo", "device.stopVideo": "Arrêter la vidéo",
"device.screenSharingUnsupported" : " Partage d'écran non supporté", "device.screenSharingUnsupported": "Partage d'écran non supporté",
"device.startScreenSharing" : " Démarrer le partage d 'écran'", "device.startScreenSharing": "Démarrer le partage d 'écran'",
"device.stopScreenSharing" : " Arrêter le partage d'écran", "device.stopScreenSharing": "Arrêter le partage d'écran",
"devices.microphoneDisconnected" : " Microphone déconnecté", "devices.microphoneDisconnected": "Microphone déconnecté",
"devices.microphoneError" : " Une erreur est apparue lors de l'accès à votre microphone", "devices.microphoneError": "Une erreur est apparue lors de l'accès à votre microphone",
"devices.microPhoneMute" : " Désactiver le microphone", "devices.microPhoneMute": "Désactiver le microphone",
"devices.micophoneUnMute" : " Réactiver le microphone", "devices.micophoneUnMute": "Réactiver le microphone",
"devices.microphoneEnable" : " Activer le microphone", "devices.microphoneEnable": "Activer le microphone",
"devices.microphoneMuteError" : " Impossible de désactiver le microphone", "devices.microphoneMuteError": "Impossible de désactiver le microphone",
"devices.microphoneUnMuteError" : " Impossible de réactiver le microphone", "devices.microphoneUnMuteError": "Impossible de réactiver le microphone",
"devices.screenSharingDisconnected" : " Partage d'écran déconnecté", "devices.screenSharingDisconnected": "Partage d'écran déconnecté",
"devices.screenSharingError" : " Une erreur est apparue lors de l'accès à votre partage d'écran", "devices.screenSharingError": "Une erreur est apparue lors de l'accès à votre partage d'écran",
"devices.cameraDisconnected" : " Caméra déconnectée", "devices.cameraDisconnected": "Caméra déconnectée",
"devices.cameraError" : " Une erreur est apparue lors de l'accès à votre caméra" "devices.cameraError": "Une erreur est apparue lors de l'accès à votre caméra"
} }

View File

@ -49,6 +49,9 @@
"room.spotlights": "Učesnici u fokusu", "room.spotlights": "Učesnici u fokusu",
"room.passive": "Pasivni učesnici", "room.passive": "Pasivni učesnici",
"room.videoPaused": "Video pauziran", "room.videoPaused": "Video pauziran",
"room.muteAll": null,
"room.stopAllVideo": null,
"room.closeMeeting": null,
"tooltip.login": "Prijava", "tooltip.login": "Prijava",
"tooltip.logout": "Odjava", "tooltip.logout": "Odjava",
@ -60,6 +63,7 @@
"tooltip.lobby": "Prikaži predvorje", "tooltip.lobby": "Prikaži predvorje",
"tooltip.settings": "Prikaži postavke", "tooltip.settings": "Prikaži postavke",
"tooltip.participants": "Pokažite sudionike", "tooltip.participants": "Pokažite sudionike",
"tooltip.kickParticipant": null,
"label.roomName": "Naziv sobe", "label.roomName": "Naziv sobe",
"label.chooseRoomButton": "Nastavi", "label.chooseRoomButton": "Nastavi",

View File

@ -4,7 +4,7 @@
"socket.reconnected": "Sikeres újarkapcsolódás", "socket.reconnected": "Sikeres újarkapcsolódás",
"socket.requestError": "Sikertelen szerver lekérés", "socket.requestError": "Sikertelen szerver lekérés",
"room.chooseRoom": "Choose the name of the room you would like to join", "room.chooseRoom": null,
"room.cookieConsent": "Ez a weblap a felhasználói élmény fokozása miatt sütiket használ", "room.cookieConsent": "Ez a weblap a felhasználói élmény fokozása miatt sütiket használ",
"room.consentUnderstand": "I understand", "room.consentUnderstand": "I understand",
"room.joined": "Csatlakozátál a konferenciához", "room.joined": "Csatlakozátál a konferenciához",
@ -49,6 +49,9 @@
"room.spotlights": "Látható résztvevők", "room.spotlights": "Látható résztvevők",
"room.passive": "Passzív résztvevők", "room.passive": "Passzív résztvevők",
"room.videoPaused": "Ez a videóstream szünetel", "room.videoPaused": "Ez a videóstream szünetel",
"room.muteAll": null,
"room.stopAllVideo": null,
"room.closeMeeting": null,
"tooltip.login": "Belépés", "tooltip.login": "Belépés",
"tooltip.logout": "Kilépés", "tooltip.logout": "Kilépés",
@ -60,6 +63,7 @@
"tooltip.lobby": "Az előszobában várakozók listája", "tooltip.lobby": "Az előszobában várakozók listája",
"tooltip.settings": "Beállítások", "tooltip.settings": "Beállítások",
"tooltip.participants": "Résztvevők", "tooltip.participants": "Résztvevők",
"tooltip.kickParticipant": null,
"label.roomName": "Konferencia", "label.roomName": "Konferencia",
"label.chooseRoomButton": "Tovább", "label.chooseRoomButton": "Tovább",

View File

@ -49,6 +49,9 @@
"room.spotlights": "Deltakere i fokus", "room.spotlights": "Deltakere i fokus",
"room.passive": "Passive deltakere", "room.passive": "Passive deltakere",
"room.videoPaused": "Denne videoen er inaktiv", "room.videoPaused": "Denne videoen er inaktiv",
"room.muteAll": "Demp alle",
"room.stopAllVideo": "Stopp all video",
"room.closeMeeting": "Avslutt møte",
"tooltip.login": "Logg in", "tooltip.login": "Logg in",
"tooltip.logout": "Logg ut", "tooltip.logout": "Logg ut",
@ -60,6 +63,7 @@
"tooltip.lobby": "Vis lobby", "tooltip.lobby": "Vis lobby",
"tooltip.settings": "Vis innstillinger", "tooltip.settings": "Vis innstillinger",
"tooltip.participants": "Vis deltakere", "tooltip.participants": "Vis deltakere",
"tooltip.kickParticipant": "Spark ut deltaker",
"label.roomName": "Møtenavn", "label.roomName": "Møtenavn",
"label.chooseRoomButton": "Fortsett", "label.chooseRoomButton": "Fortsett",

View File

@ -49,6 +49,9 @@
"room.spotlights": "Aktywni uczestnicy", "room.spotlights": "Aktywni uczestnicy",
"room.passive": "Pasywni uczestnicy", "room.passive": "Pasywni uczestnicy",
"room.videoPaused": "To wideo jest wstrzymane.", "room.videoPaused": "To wideo jest wstrzymane.",
"room.muteAll": null,
"room.stopAllVideo": null,
"room.closeMeeting": null,
"tooltip.login": "Zaloguj", "tooltip.login": "Zaloguj",
"tooltip.logout": "Wyloguj", "tooltip.logout": "Wyloguj",
@ -60,6 +63,7 @@
"tooltip.lobby": "Pokaż poczekalnię", "tooltip.lobby": "Pokaż poczekalnię",
"tooltip.settings": "Pokaż ustawienia", "tooltip.settings": "Pokaż ustawienia",
"tooltip.participants": "Pokaż uczestników", "tooltip.participants": "Pokaż uczestników",
"tooltip.kickParticipant": null,
"label.roomName": "Nazwa konferencji", "label.roomName": "Nazwa konferencji",
"label.chooseRoomButton": "Kontynuuj", "label.chooseRoomButton": "Kontynuuj",

View File

@ -49,6 +49,9 @@
"room.spotlights": "Participantes em foco", "room.spotlights": "Participantes em foco",
"room.passive": "Participantes passivos", "room.passive": "Participantes passivos",
"room.videoPaused": "Este vídeo está em pausa", "room.videoPaused": "Este vídeo está em pausa",
"room.muteAll": null,
"room.stopAllVideo": null,
"room.closeMeeting": null,
"tooltip.login": "Entrar", "tooltip.login": "Entrar",
"tooltip.logout": "Sair", "tooltip.logout": "Sair",
@ -60,6 +63,7 @@
"tooltip.lobby": "Apresentar sala de espera", "tooltip.lobby": "Apresentar sala de espera",
"tooltip.settings": "Apresentar definições", "tooltip.settings": "Apresentar definições",
"tooltip.participants": "Apresentar participantes", "tooltip.participants": "Apresentar participantes",
"tooltip.kickParticipant": null,
"label.roomName": "Nome da sala", "label.roomName": "Nome da sala",
"label.chooseRoomButton": "Continuar", "label.chooseRoomButton": "Continuar",

View File

@ -49,6 +49,9 @@
"room.spotlights": "Participanți în Spotlight", "room.spotlights": "Participanți în Spotlight",
"room.passive": "Participanți pasivi", "room.passive": "Participanți pasivi",
"room.videoPaused": "Acest video este pus pe pauză", "room.videoPaused": "Acest video este pus pe pauză",
"room.muteAll": null,
"room.stopAllVideo": null,
"room.closeMeeting": null,
"tooltip.login": "Intră în cont", "tooltip.login": "Intră în cont",
"tooltip.logout": "Deconectare", "tooltip.logout": "Deconectare",
@ -58,7 +61,9 @@
"tooltip.enterFullscreen": "Modul ecran complet", "tooltip.enterFullscreen": "Modul ecran complet",
"tooltip.leaveFullscreen": "Ieșire din modul ecran complet", "tooltip.leaveFullscreen": "Ieșire din modul ecran complet",
"tooltip.lobby": "Arată holul", "tooltip.lobby": "Arată holul",
"tooltip.settings": "Arată participanții", "tooltip.settings": "Arată setăile",
"tooltip.participants": null,
"tooltip.kickParticipant": null,
"label.roomName": "Numele camerei", "label.roomName": "Numele camerei",
"label.chooseRoomButton": "Continuare", "label.chooseRoomButton": "Continuare",

View File

@ -1,4 +1,5 @@
const os = require('os'); const os = require('os');
const userRoles = require('../userRoles');
module.exports = module.exports =
{ {
@ -63,15 +64,106 @@ module.exports =
// listeningRedirectPort disabled // listeningRedirectPort disabled
// use case: loadbalancer backend // use case: loadbalancer backend
httpOnly : false, httpOnly : false,
// If this is set to true, only signed-in users will be able // This function will be called on successful login through oidc.
// to join a room directly. Non-signed-in users (guests) will // Use this function to map your oidc userinfo to the Peer object.
// always be put in the lobby regardless of room lock status. // The roomId is equal to the room name.
// If false, there is no difference between guests and signed-in // See examples below.
// users when joining. // Examples:
requireSignInToAccess : true, /*
// This flag has no effect when requireSignInToAccess is false // All authenicated users will be MODERATOR and AUTHENTICATED
// When truthy, the room will be open to all users when the first userMapping : async ({ peer, roomId, userinfo }) =>
// authenticated user has already joined the room. {
peer.addRole(userRoles.MODERATOR);
peer.addRole(userRoles.AUTHENTICATED);
},
// All authenicated users will be AUTHENTICATED,
// and those with the moderator role set in the userinfo
// will also be MODERATOR
userMapping : async ({ peer, roomId, userinfo }) =>
{
if (
Array.isArray(userinfo.meet_roles) &&
userinfo.meet_roles.includes('moderator')
)
{
peer.addRole(userRoles.MODERATOR);
}
if (
Array.isArray(userinfo.meet_roles) &&
userinfo.meet_roles.includes('meetingadmin')
)
{
peer.addRole(userRoles.ADMIN);
}
peer.addRole(userRoles.AUTHENTICATED);
},
// All authenicated users will be AUTHENTICATED,
// and those with email ending with @example.com
// will also be MODERATOR
userMapping : async ({ peer, roomId, userinfo }) =>
{
if (userinfo.email && userinfo.email.endsWith('@example.com'))
{
peer.addRole(userRoles.MODERATOR);
}
peer.addRole(userRoles.AUTHENTICATED);
}
// All authenicated users will be AUTHENTICATED,
// and those with email ending with @example.com
// will also be MODERATOR
userMapping : async ({ peer, roomId, userinfo }) =>
{
if (userinfo.email && userinfo.email.endsWith('@example.com'))
{
peer.addRole(userRoles.MODERATOR);
}
peer.addRole(userRoles.AUTHENTICATED);
},
*/
userMapping : async ({ peer, roomId, userinfo }) =>
{
if (userinfo.picture != null)
{
if (!userinfo.picture.match(/^http/g))
{
peer.picture = `data:image/jpeg;base64, ${userinfo.picture}`;
}
else
{
peer.picture = userinfo.picture;
}
}
if (userinfo.nickname != null)
{
peer.displayName = userinfo.nickname;
}
if (userinfo.name != null)
{
peer.displayName = userinfo.name;
}
if (userinfo.email != null)
{
peer.email = userinfo.email;
}
},
// Required roles for Access. All users have the role "ALL" by default.
// Other roles need to be added in the "userMapping" function. This
// is an Array of roles. userRoles.ADMIN have all priveleges and access
// always.
//
// Example:
// [ userRoles.MODERATOR, userRoles.AUTHENTICATED ]
// This will allow all MODERATOR and AUTHENTICATED users access.
requiredRolesForAccess : [ userRoles.ALL ],
// When truthy, the room will be open to all users when as long as there
// are allready users in the room
activateOnHostJoin : true, activateOnHostJoin : true,
// Mediasoup settings // Mediasoup settings
mediasoup : mediasoup :

View File

@ -14,7 +14,7 @@ class Lobby extends EventEmitter
// Closed flag. // Closed flag.
this._closed = false; this._closed = false;
this._peers = new Map(); this._peers = {};
} }
close() close()
@ -23,27 +23,28 @@ class Lobby extends EventEmitter
this._closed = true; this._closed = true;
this._peers.forEach((peer) => // Close the peers.
for (const peer in this._peers)
{ {
if (!peer.closed) if (!peer.closed)
peer.close(); peer.close();
}); }
this._peers.clear(); this._peers = null;
} }
checkEmpty() checkEmpty()
{ {
logger.info('checkEmpty()'); logger.info('checkEmpty()');
return this._peers.size === 0; return Object.keys(this._peers).length === 0;
} }
peerList() peerList()
{ {
logger.info('peerList()'); logger.info('peerList()');
return Array.from(this._peers.values()).map((peer) => return Object.values(this._peers).map((peer) =>
({ ({
peerId : peer.id, peerId : peer.id,
displayName : peer.displayName displayName : peer.displayName
@ -52,38 +53,42 @@ class Lobby extends EventEmitter
hasPeer(peerId) hasPeer(peerId)
{ {
return this._peers.has(peerId); return this._peers[peerId] != null;
} }
promoteAllPeers() promoteAllPeers()
{ {
logger.info('promoteAllPeers()'); logger.info('promoteAllPeers()');
this._peers.forEach((peer) => for (const peer in this._peers)
{ {
if (peer.socket) if (peer.socket)
this.promotePeer(peer.id); this.promotePeer(peer.id);
}); }
} }
promotePeer(peerId) promotePeer(peerId)
{ {
logger.info('promotePeer() [peer:"%s"]', peerId); logger.info('promotePeer() [peer:"%s"]', peerId);
const peer = this._peers.get(peerId); const peer = this._peers[peerId];
if (peer) if (peer)
{ {
peer.socket.removeListener('request', peer.socketRequestHandler); peer.socket.removeListener('request', peer.socketRequestHandler);
peer.removeListener('authenticationChanged', peer.authenticationHandler); peer.removeListener('gotRole', peer.gotRoleHandler);
peer.removeListener('displayNameChanged', peer.displayNameChangeHandler);
peer.removeListener('pictureChanged', peer.pictureChangeHandler);
peer.removeListener('close', peer.closeHandler); peer.removeListener('close', peer.closeHandler);
peer.socketRequestHandler = null; peer.socketRequestHandler = null;
peer.authenticationHandler = null; peer.gotRoleHandler = null;
peer.displayNameChangeHandler = null;
peer.pictureChangeHandler = null;
peer.closeHandler = null; peer.closeHandler = null;
this.emit('promotePeer', peer); this.emit('promotePeer', peer);
this._peers.delete(peerId); delete this._peers[peerId];
} }
} }
@ -112,16 +117,25 @@ class Lobby extends EventEmitter
}); });
}; };
peer.authenticationHandler = () => peer.gotRoleHandler = () =>
{ {
logger.info('parkPeer() | authenticationChange [peer:"%s"]', peer.id); logger.info('parkPeer() | rolesChange [peer:"%s"]', peer.id);
if (peer.authenticated) this.emit('peerRolesChanged', peer);
};
peer.displayNameChangeHandler = () =>
{ {
logger.info('parkPeer() | displayNameChange [peer:"%s"]', peer.id);
this.emit('changeDisplayName', peer); this.emit('changeDisplayName', peer);
};
peer.pictureChangeHandler = () =>
{
logger.info('parkPeer() | pictureChange [peer:"%s"]', peer.id);
this.emit('changePicture', peer); this.emit('changePicture', peer);
this.emit('peerAuthenticated', peer);
}
}; };
peer.closeHandler = () => peer.closeHandler = () =>
@ -133,7 +147,7 @@ class Lobby extends EventEmitter
this.emit('peerClosed', peer); this.emit('peerClosed', peer);
this._peers.delete(peer.id); delete this._peers[peer.id];
if (this.checkEmpty()) if (this.checkEmpty())
this.emit('lobbyEmpty'); this.emit('lobbyEmpty');
@ -141,9 +155,11 @@ class Lobby extends EventEmitter
this._notification(peer.socket, 'enteredLobby'); this._notification(peer.socket, 'enteredLobby');
this._peers.set(peer.id, peer); this._peers[peer.id] = peer;
peer.on('authenticationChanged', peer.authenticationHandler); peer.on('gotRole', peer.gotRoleHandler);
peer.on('displayNameChanged', peer.displayNameChangeHandler);
peer.on('pictureChanged', peer.pictureChangeHandler);
peer.socket.on('request', peer.socketRequestHandler); peer.socket.on('request', peer.socketRequestHandler);
@ -169,8 +185,6 @@ class Lobby extends EventEmitter
peer.displayName = displayName; peer.displayName = displayName;
this.emit('changeDisplayName', peer);
cb(); cb();
break; break;
@ -181,8 +195,6 @@ class Lobby extends EventEmitter
peer.picture = picture; peer.picture = picture;
this.emit('changePicture', peer);
cb(); cb();
break; break;

View File

@ -1,17 +1,20 @@
const EventEmitter = require('events').EventEmitter; const EventEmitter = require('events').EventEmitter;
const userRoles = require('../userRoles');
const Logger = require('./Logger'); const Logger = require('./Logger');
const logger = new Logger('Peer'); const logger = new Logger('Peer');
class Peer extends EventEmitter class Peer extends EventEmitter
{ {
constructor({ id, socket }) constructor({ id, roomId, socket })
{ {
logger.info('constructor() [id:"%s", socket:"%s"]', id, socket.id); logger.info('constructor() [id:"%s"]', id);
super(); super();
this._id = id; this._id = id;
this._roomId = roomId;
this._authId = null; this._authId = null;
this._socket = socket; this._socket = socket;
@ -22,7 +25,7 @@ class Peer extends EventEmitter
this._inLobby = false; this._inLobby = false;
this._authenticated = false; this._roles = [ userRoles.ALL ];
this._displayName = false; this._displayName = false;
@ -40,8 +43,6 @@ class Peer extends EventEmitter
this._consumers = new Map(); this._consumers = new Map();
this._checkAuthentication();
this._handlePeer(); this._handlePeer();
} }
@ -58,21 +59,16 @@ class Peer extends EventEmitter
transport.close(); transport.close();
}); });
if (this._socket) if (this.socket)
this._socket.disconnect(true); this.socket.disconnect(true);
this.emit('close'); this.emit('close');
} }
_handlePeer() _handlePeer()
{ {
this.socket.use((packet, next) => if (this.socket)
{ {
this._checkAuthentication();
return next();
});
this.socket.on('disconnect', () => this.socket.on('disconnect', () =>
{ {
if (this.closed) if (this.closed)
@ -83,32 +79,6 @@ class Peer extends EventEmitter
this.close(); this.close();
}); });
} }
_checkAuthentication()
{
if (
Boolean(this.socket.handshake.session.passport) &&
Boolean(this.socket.handshake.session.passport.user)
)
{
const {
id,
displayName,
picture,
email
} = this.socket.handshake.session.passport.user;
id && (this.authId = id);
displayName && (this.displayName = displayName);
picture && (this.picture = picture);
email && (this.email = email);
this.authenticated = true;
}
else
{
this.authenticated = false;
}
} }
get id() get id()
@ -121,6 +91,16 @@ class Peer extends EventEmitter
this._id = id; this._id = id;
} }
get roomId()
{
return this._roomId;
}
set roomId(roomId)
{
this._roomId = roomId;
}
get authId() get authId()
{ {
return this._authId; return this._authId;
@ -166,21 +146,9 @@ class Peer extends EventEmitter
this._inLobby = inLobby; this._inLobby = inLobby;
} }
get authenticated() get roles()
{ {
return this._authenticated; return this._roles;
}
set authenticated(authenticated)
{
if (authenticated !== this._authenticated)
{
const oldAuthenticated = this._authenticated;
this._authenticated = authenticated;
this.emit('authenticationChanged', { oldAuthenticated });
}
} }
get displayName() get displayName()
@ -262,6 +230,35 @@ class Peer extends EventEmitter
return this._consumers; return this._consumers;
} }
addRole(newRole)
{
if (!this._roles.includes(newRole))
{
this._roles.push(newRole);
logger.info('addRole() | [newRole:"%s]"', newRole);
this.emit('gotRole', { newRole });
}
}
removeRole(oldRole)
{
if (this._roles.includes(oldRole))
{
this._roles = this._roles.filter((role) => role !== oldRole);
logger.info('removeRole() | [oldRole:"%s]"', oldRole);
this.emit('lostRole', { oldRole });
}
}
hasRole(role)
{
return this._roles.includes(role);
}
addTransport(id, transport) addTransport(id, transport)
{ {
this.transports.set(id, transport); this.transports.set(id, transport);
@ -319,7 +316,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

@ -2,6 +2,7 @@ const EventEmitter = require('events').EventEmitter;
const axios = require('axios'); const axios = require('axios');
const Logger = require('./Logger'); const Logger = require('./Logger');
const Lobby = require('./Lobby'); const Lobby = require('./Lobby');
const userRoles = require('../userRoles');
const config = require('../config/config'); const config = require('../config/config');
const logger = new Logger('Room'); const logger = new Logger('Room');
@ -54,6 +55,12 @@ class Room extends EventEmitter
// Locked flag. // Locked flag.
this._locked = false; this._locked = false;
// Required roles to access
this._requiredRoles = [ userRoles.ALL ];
if ('requiredRolesForAccess' in config)
this._requiredRoles = config.requiredRolesForAccess;
// if true: accessCode is a possibility to open the room // if true: accessCode is a possibility to open the room
this._joinByAccesCode = true; this._joinByAccesCode = true;
@ -99,13 +106,10 @@ class Room extends EventEmitter
// Close the peers. // Close the peers.
for (const peer in this._peers) for (const peer in this._peers)
{
if (Object.prototype.hasOwnProperty.call(this._peers, peer))
{ {
if (!peer.closed) if (!peer.closed)
peer.close(); peer.close();
} }
}
this._peers = null; this._peers = null;
@ -118,50 +122,42 @@ class Room extends EventEmitter
handlePeer(peer) handlePeer(peer)
{ {
logger.info('handlePeer() [peer:"%s"]', peer.id); logger.info('handlePeer() [peer:"%s", roles:"%s"]', peer.id, peer.roles);
// This will allow reconnects to join despite lock // Allow reconnections, remove old peer
if (this._peers[peer.id]) if (this._peers[peer.id])
{ {
logger.warn( logger.warn(
'handleConnection() | there is already a peer with same peerId [peer:"%s"]', 'handleConnection() | there is already a peer with same peerId [peer:"%s"]',
peer.id); peer.id);
peer.close(); this._peers[peer.id].close();
}
return; // Always let ADMIN in, even if locked
} if (peer.roles.includes(userRoles.ADMIN))
this._peerJoining(peer);
else if (this._locked) else if (this._locked)
{
this._parkPeer(peer); this._parkPeer(peer);
}
else else
{ {
peer.authenticated ? // If the user has a role in config.requiredRolesForAccess, let them in
peer.roles.some((role) => this._requiredRoles.includes(role)) ?
this._peerJoining(peer) : this._peerJoining(peer) :
this._handleGuest(peer); this._handleGuest(peer);
} }
} }
_handleGuest(peer) _handleGuest(peer)
{
if (config.requireSignInToAccess)
{ {
if (config.activateOnHostJoin && !this.checkEmpty()) if (config.activateOnHostJoin && !this.checkEmpty())
{
this._peerJoining(peer); this._peerJoining(peer);
}
else else
{ {
this._parkPeer(peer); this._parkPeer(peer);
this._notification(peer.socket, 'signInRequired'); this._notification(peer.socket, 'signInRequired');
} }
} }
else
{
this._peerJoining(peer);
}
}
_handleLobby() _handleLobby()
{ {
@ -179,9 +175,26 @@ class Room extends EventEmitter
} }
}); });
this._lobby.on('peerAuthenticated', (peer) => this._lobby.on('peerRolesChanged', (peer) =>
{ {
!this._locked && this._lobby.promotePeer(peer.id); // Always let admin in, even if locked
if (peer.roles.includes(userRoles.ADMIN))
{
this._lobby.promotePeer(peer.id);
return;
}
// If the user has a role in config.requiredRolesForAccess, let them in
if (
!this._locked &&
peer.roles.some((role) => this._requiredRoles.includes(role))
)
{
this._lobby.promotePeer(peer.id);
return;
}
}); });
this._lobby.on('changeDisplayName', (changedPeer) => this._lobby.on('changeDisplayName', (changedPeer) =>
@ -304,7 +317,6 @@ class Room extends EventEmitter
}, 10000); }, 10000);
} }
// checks both room and lobby
checkEmpty() checkEmpty()
{ {
return Object.keys(this._peers).length === 0; return Object.keys(this._peers).length === 0;
@ -324,12 +336,8 @@ class Room extends EventEmitter
{ {
peer.socket.join(this._roomId); peer.socket.join(this._roomId);
const index = this._lastN.indexOf(peer.id); // If we don't have this peer, add to end
!this._lastN.includes(peer.id) && this._lastN.push(peer.id);
if (index === -1) // We don't have this peer, add to end
{
this._lastN.push(peer.id);
}
this._peers[peer.id] = peer; this._peers[peer.id] = peer;
@ -402,25 +410,17 @@ class Room extends EventEmitter
// If the Peer was joined, notify all Peers. // If the Peer was joined, notify all Peers.
if (peer.joined) if (peer.joined)
{
this._notification(peer.socket, 'peerClosed', { peerId: peer.id }, true); this._notification(peer.socket, 'peerClosed', { peerId: peer.id }, true);
}
const index = this._lastN.indexOf(peer.id); // Remove from lastN
this._lastN = this._lastN.filter((id) => id !== peer.id);
if (index > -1) // We have this peer in the list, remove
{
this._lastN.splice(index, 1);
}
delete this._peers[peer.id]; delete this._peers[peer.id];
// If this is the last Peer in the room and // If this is the last Peer in the room and
// lobby is empty, close the room after a while. // lobby is empty, close the room after a while.
if (this.checkEmpty() && this._lobby.checkEmpty()) if (this.checkEmpty() && this._lobby.checkEmpty())
{
this.selfDestructCountdown(); this.selfDestructCountdown();
}
}); });
peer.on('displayNameChanged', ({ oldDisplayName }) => peer.on('displayNameChanged', ({ oldDisplayName }) =>
@ -449,6 +449,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, 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, true);
});
} }
async _handleSocketRequest(peer, request, cb) async _handleSocketRequest(peer, request, cb)
@ -464,27 +490,6 @@ class Room extends EventEmitter
case 'join': case 'join':
{ {
try
{
if (peer.socket.handshake.session.passport.user.displayName)
{
this._notification(
peer.socket,
'changeDisplayname',
{
peerId : peer.id,
displayName : peer.socket.handshake.session.passport.user.displayName,
oldDisplayName : ''
},
true
);
}
}
catch (error)
{
logger.error(error);
}
// Ensure the Peer is not already joined. // Ensure the Peer is not already joined.
if (peer.joined) if (peer.joined)
throw new Error('Peer already joined'); throw new Error('Peer already joined');
@ -512,7 +517,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;
@ -540,7 +548,8 @@ class Room extends EventEmitter
{ {
id : peer.id, id : peer.id,
displayName : displayName, displayName : displayName,
picture : picture picture : picture,
roles : peer.roles
} }
); );
} }
@ -1106,6 +1115,92 @@ 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:closeMeeting':
{
if (
!peer.hasRole(userRoles.MODERATOR) &&
!peer.hasRole(userRoles.ADMIN)
)
throw new Error('peer does not have moderator priveleges');
this._notification(
peer.socket,
'moderator:kick',
null,
true
);
cb();
// Close the room
this.close();
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);
@ -1319,13 +1414,16 @@ class Room extends EventEmitter
}); });
} }
_notification(socket, method, data = {}, broadcast = false) _notification(socket, method, data = {}, broadcast = false, includeSender = false)
{ {
if (broadcast) if (broadcast)
{ {
socket.broadcast.to(this._roomId).emit( socket.broadcast.to(this._roomId).emit(
'notification', { method, data } 'notification', { method, data }
); );
if (includeSender)
socket.emit('notification', { method, data });
} }
else else
{ {

View File

@ -18,6 +18,7 @@ const Peer = require('./lib/Peer');
const base64 = require('base-64'); const base64 = require('base-64');
const helmet = require('helmet'); const helmet = require('helmet');
const userRoles = require('./userRoles');
const { const {
loginHelper, loginHelper,
logoutHelper logoutHelper
@ -190,7 +191,7 @@ function setupLTI(ltiConfig)
} }
if (lti.lis_person_name_full) if (lti.lis_person_name_full)
{ {
user.displayName=lti.lis_person_name_full; user.displayName = lti.lis_person_name_full;
} }
// Perform local authentication if necessary // Perform local authentication if necessary
@ -241,51 +242,6 @@ function setupOIDC(oidcIssuer)
_claims : tokenset.claims _claims : tokenset.claims
}; };
if (userinfo.picture != null)
{
if (!userinfo.picture.match(/^http/g))
{
user.picture = `data:image/jpeg;base64, ${userinfo.picture}`;
}
else
{
user.picture = userinfo.picture;
}
}
if (userinfo.nickname != null)
{
user.displayName = userinfo.nickname;
}
if (userinfo.name != null)
{
user.displayName = userinfo.name;
}
if (userinfo.email != null)
{
user.email = userinfo.email;
}
if (userinfo.given_name != null)
{
user.name={};
user.name.givenName = userinfo.given_name;
}
if (userinfo.family_name != null)
{
if (user.name == null) user.name={};
user.name.familyName = userinfo.family_name;
}
if (userinfo.middle_name != null)
{
if (user.name == null) user.name={};
user.name.middleName = userinfo.middle_name;
}
return done(null, user); return done(null, user);
} }
); );
@ -324,7 +280,8 @@ async function setupAuth()
{ {
passport.authenticate('oidc', { passport.authenticate('oidc', {
state : base64.encode(JSON.stringify({ state : base64.encode(JSON.stringify({
id : req.query.id peerId : req.query.peerId,
roomId : req.query.roomId
})) }))
})(req, res, next); })(req, res, next);
}); });
@ -341,6 +298,19 @@ async function setupAuth()
// logout // logout
app.get('/auth/logout', (req, res) => app.get('/auth/logout', (req, res) =>
{ {
const { peerId } = req.session;
const peer = peers.get(peerId);
if (peer)
{
for (const role of peer.roles)
{
if (role !== userRoles.ALL)
peer.removeRole(role);
}
}
req.logout(); req.logout();
res.send(logoutHelper()); res.send(logoutHelper());
}); });
@ -349,35 +319,35 @@ async function setupAuth()
app.get( app.get(
'/auth/callback', '/auth/callback',
passport.authenticate('oidc', { failureRedirect: '/auth/login' }), passport.authenticate('oidc', { failureRedirect: '/auth/login' }),
(req, res) => async (req, res) =>
{ {
const state = JSON.parse(base64.decode(req.query.state)); const state = JSON.parse(base64.decode(req.query.state));
let displayName; const { peerId, roomId } = state;
let picture;
if (req.user != null) req.session.peerId = peerId;
req.session.roomId = roomId;
let peer = peers.get(peerId);
if (!peer) // User has no socket session yet, make temporary
peer = new Peer({ id: peerId, roomId });
if (peer && peer.roomId !== roomId) // The peer is mischievous
throw new Error('peer authenticated with wrong room');
if (peer && typeof config.userMapping === 'function')
{ {
if (req.user.displayName != null) await config.userMapping({
displayName = req.user.displayName; peer,
else roomId,
displayName = ''; userinfo : req.user._userinfo
});
if (req.user.picture != null)
picture = req.user.picture;
else
picture = '/static/media/buddy.403cb9f6.svg';
} }
const peer = peers.get(state.id);
peer && (peer.displayName = displayName);
peer && (peer.picture = picture);
peer && (peer.authenticated = true);
res.send(loginHelper({ res.send(loginHelper({
displayName, displayName : peer.displayName,
picture picture : peer.picture
})); }));
} }
); );
@ -495,12 +465,36 @@ async function runWebSocketServer()
queue.push(async () => queue.push(async () =>
{ {
const room = await getOrCreateRoom({ roomId }); const room = await getOrCreateRoom({ roomId });
const peer = new Peer({ id: peerId, socket }); const peer = new Peer({ id: peerId, roomId, socket });
peers.set(peerId, peer); peers.set(peerId, peer);
peer.on('close', () => peers.delete(peerId)); peer.on('close', () => peers.delete(peerId));
if (
Boolean(socket.handshake.session.passport) &&
Boolean(socket.handshake.session.passport.user)
)
{
const {
id,
displayName,
picture,
email,
_userinfo
} = socket.handshake.session.passport.user;
peer.authId= id;
peer.displayName = displayName;
peer.picture = picture;
peer.email = email;
if (typeof config.userMapping === 'function')
{
await config.userMapping({ peer, roomId, userinfo: _userinfo });
}
}
room.handlePeer(peer); room.handlePeer(peer);
}) })
.catch((error) => .catch((error) =>

View File

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