diff --git a/app/src/RoomClient.js b/app/src/RoomClient.js index 0d75ddb..762c0cf 100644 --- a/app/src/RoomClient.js +++ b/app/src/RoomClient.js @@ -417,7 +417,7 @@ export default class RoomClient login() { - const url = `/auth/login?id=${this._peerId}`; + const url = `/auth/login?peerId=${this._peerId}&roomId=${this._roomId}`; window.open(url, 'loginWindow'); } @@ -433,16 +433,8 @@ export default class RoomClient const { displayName, picture } = data; - if (store.getState().room.state === 'connected') - { - this.changeDisplayName(displayName); - this.changePicture(picture); - } - else - { - store.dispatch(settingsActions.setDisplayName(displayName)); - store.dispatch(meActions.setPicture(picture)); - } + store.dispatch(settingsActions.setDisplayName(displayName)); + store.dispatch(meActions.setPicture(picture)); store.dispatch(meActions.loggedIn(true)); @@ -1155,6 +1147,86 @@ export default class RoomClient 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 // mute: true/false async modifyPeerConsumer(peerId, type, mute) @@ -1914,10 +1986,10 @@ export default class RoomClient case 'newPeer': { - const { id, displayName, picture } = notification.data; + const { id, displayName, picture, roles } = notification.data; store.dispatch( - peerActions.addPeer({ id, displayName, picture, consumers: [] })); + peerActions.addPeer({ id, displayName, picture, roles, consumers: [] })); store.dispatch(requestActions.notify( { @@ -2016,6 +2088,96 @@ export default class RoomClient 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: { @@ -2195,7 +2357,7 @@ export default class RoomClient canShareFiles : this._torrentSupport })); - const { peers } = await this.sendRequest( + const { roles, peers } = await this.sendRequest( 'join', { displayName : displayName, @@ -2203,7 +2365,25 @@ export default class RoomClient 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) { diff --git a/app/src/actions/meActions.js b/app/src/actions/meActions.js index 0162ee8..0afb9f7 100644 --- a/app/src/actions/meActions.js +++ b/app/src/actions/meActions.js @@ -15,6 +15,18 @@ export const loggedIn = (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) => ({ type : 'SET_PICTURE', diff --git a/app/src/actions/peerActions.js b/app/src/actions/peerActions.js index 4caca32..1a87151 100644 --- a/app/src/actions/peerActions.js +++ b/app/src/actions/peerActions.js @@ -45,3 +45,22 @@ export const setPeerPicture = (peerId, picture) => type : 'SET_PEER_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 } + }); diff --git a/app/src/actions/roomActions.js b/app/src/actions/roomActions.js index 560c77d..6003b9e 100644 --- a/app/src/actions/roomActions.js +++ b/app/src/actions/roomActions.js @@ -109,4 +109,22 @@ export const toggleConsumerFullscreen = (consumerId) => ({ type : 'TOGGLE_FULLSCREEN_CONSUMER', 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 } }); \ No newline at end of file diff --git a/app/src/components/ChooseRoom.js b/app/src/components/ChooseRoom.js index ea6b097..31ff420 100644 --- a/app/src/components/ChooseRoom.js +++ b/app/src/components/ChooseRoom.js @@ -1,21 +1,15 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; import { Link } from 'react-router-dom'; -import { connect } from 'react-redux'; import { withStyles } from '@material-ui/core/styles'; -import { withRoomContext } from '../RoomContext'; import isElectron from 'is-electron'; import PropTypes from 'prop-types'; import { useIntl, FormattedMessage } from 'react-intl'; import randomString from 'random-string'; import Dialog from '@material-ui/core/Dialog'; 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 Button from '@material-ui/core/Button'; import TextField from '@material-ui/core/TextField'; -import Tooltip from '@material-ui/core/Tooltip'; import CookieConsent from 'react-cookie-consent'; import MuiDialogTitle from '@material-ui/core/DialogTitle'; import MuiDialogContent from '@material-ui/core/DialogContent'; @@ -88,63 +82,12 @@ const styles = (theme) => const DialogTitle = withStyles(styles)((props) => { - const [ open, setOpen ] = useState(false); - - 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); - }; + const { children, classes, ...other } = props; return ( { window.config && window.config.logo && Logo } {children} - { window.config && window.config.loginEnabled && - - - { myPicture ? - - : - - } - - - } ); }); @@ -165,9 +108,6 @@ const DialogActions = withStyles((theme) => ({ }))(MuiDialogActions); const ChooseRoom = ({ - roomClient, - loggedIn, - myPicture, classes }) => { @@ -184,13 +124,7 @@ const ChooseRoom = ({ paper : classes.dialogPaper }} > - - { - loggedIn ? roomClient.logout() : roomClient.login(); - }} - > + { window.config && window.config.title ? window.config.title : 'Multiparty meeting' }
@@ -258,34 +192,7 @@ const ChooseRoom = ({ 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) => -{ - 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))); \ No newline at end of file +export default withStyles(styles)(ChooseRoom); \ No newline at end of file diff --git a/app/src/components/MeetingDrawer/ParticipantList/ListModerator.js b/app/src/components/MeetingDrawer/ParticipantList/ListModerator.js new file mode 100644 index 0000000..1c711a1 --- /dev/null +++ b/app/src/components/MeetingDrawer/ParticipantList/ListModerator.js @@ -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 ( +
+ +
+ +
+ +
+ ); +}; + +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))); \ No newline at end of file diff --git a/app/src/components/MeetingDrawer/ParticipantList/ListPeer.js b/app/src/components/MeetingDrawer/ParticipantList/ListPeer.js index 9f74e65..26a8ab3 100644 --- a/app/src/components/MeetingDrawer/ParticipantList/ListPeer.js +++ b/app/src/components/MeetingDrawer/ParticipantList/ListPeer.js @@ -12,6 +12,7 @@ import MicIcon from '@material-ui/icons/Mic'; import MicOffIcon from '@material-ui/icons/MicOff'; import ScreenIcon from '@material-ui/icons/ScreenShare'; import ScreenOffIcon from '@material-ui/icons/StopScreenShare'; +import ExitIcon from '@material-ui/icons/ExitToApp'; import EmptyAvatar from '../../../images/avatar-empty.jpeg'; import HandIcon from '../../../images/icon-hand-white.svg'; @@ -91,40 +92,6 @@ const styles = (theme) => flexDirection : 'row', justifyContent : 'flex-start', 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 { roomClient, + isModerator, peer, micConsumer, screenConsumer, @@ -185,9 +153,8 @@ const ListPeer = (props) => })} color={ screenVisible ? 'primary' : 'secondary'} disabled={ peer.peerScreenInProgress } - onClick={(e) => + onClick={() => { - e.stopPropagation(); screenVisible ? roomClient.modifyPeerConsumer(peer.id, 'screen', true) : roomClient.modifyPeerConsumer(peer.id, 'screen', false); @@ -207,9 +174,8 @@ const ListPeer = (props) => })} color={ micEnabled ? 'primary' : 'secondary'} disabled={ peer.peerAudioInProgress } - onClick={(e) => + onClick={() => { - e.stopPropagation(); micEnabled ? roomClient.modifyPeerConsumer(peer.id, 'mic', true) : roomClient.modifyPeerConsumer(peer.id, 'mic', false); @@ -221,6 +187,21 @@ const ListPeer = (props) => } + { isModerator && + + { + roomClient.kickPeer(peer.id); + }} + > + + + }
); @@ -230,6 +211,7 @@ ListPeer.propTypes = { roomClient : PropTypes.any.isRequired, advancedMode : PropTypes.bool, + isModerator : PropTypes.bool, peer : appPropTypes.Peer.isRequired, micConsumer : appPropTypes.Consumer, webcamConsumer : appPropTypes.Consumer, diff --git a/app/src/components/MeetingDrawer/ParticipantList/ParticipantList.js b/app/src/components/MeetingDrawer/ParticipantList/ParticipantList.js index 3313d2b..dbf5491 100644 --- a/app/src/components/MeetingDrawer/ParticipantList/ParticipantList.js +++ b/app/src/components/MeetingDrawer/ParticipantList/ParticipantList.js @@ -11,7 +11,9 @@ import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; import ListPeer from './ListPeer'; import ListMe from './ListMe'; +import ListModerator from './ListModerator'; import Volume from '../../Containers/Volume'; +import * as userRoles from '../../../reducers/userRoles'; const styles = (theme) => ({ @@ -76,6 +78,7 @@ class ParticipantList extends React.PureComponent const { roomClient, advancedMode, + isModerator, passivePeers, selectedPeerId, spotlightPeers, @@ -84,6 +87,17 @@ class ParticipantList extends React.PureComponent return (
{ this.node = node; }}> + { isModerator && + + } @@ -142,6 +160,7 @@ ParticipantList.propTypes = { roomClient : PropTypes.any.isRequired, advancedMode : PropTypes.bool, + isModerator : PropTypes.bool, passivePeers : PropTypes.array, selectedPeerId : PropTypes.string, spotlightPeers : PropTypes.array, @@ -151,6 +170,8 @@ ParticipantList.propTypes = const mapStateToProps = (state) => { return { + isModerator : state.me.roles.includes(userRoles.MODERATOR) || + state.me.roles.includes(userRoles.ADMIN), passivePeers : passivePeersSelector(state), selectedPeerId : state.room.selectedPeerId, spotlightPeers : spotlightPeersSelector(state) @@ -165,6 +186,7 @@ const ParticipantListContainer = withRoomContext(connect( areStatesEqual : (next, prev) => { return ( + prev.me.roles === next.me.roles && prev.peers === next.peers && prev.room.spotlights === next.room.spotlights && prev.room.selectedPeerId === next.room.selectedPeerId diff --git a/app/src/reducers/me.js b/app/src/reducers/me.js index ca97e32..e065c7b 100644 --- a/app/src/reducers/me.js +++ b/app/src/reducers/me.js @@ -1,8 +1,11 @@ +import * as userRoles from './userRoles'; + const initialState = { id : null, picture : null, isMobile : false, + roles : [ userRoles.ALL ], canSendMic : false, canSendWebcam : false, canShareScreen : false, @@ -49,6 +52,24 @@ const me = (state = initialState, action) => 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': return { ...state, picture: action.payload.picture }; diff --git a/app/src/reducers/peers.js b/app/src/reducers/peers.js index ddd104f..e3d207d 100644 --- a/app/src/reducers/peers.js +++ b/app/src/reducers/peers.js @@ -16,6 +16,9 @@ const peer = (state = {}, action) => case 'SET_PEER_SCREEN_IN_PROGRESS': return { ...state, peerScreenInProgress: action.payload.flag }; + + case 'SET_PEER_KICK_IN_PROGRESS': + return { ...state, peerKickInProgress: action.payload.flag }; case 'SET_PEER_RAISE_HAND_STATE': return { ...state, raiseHandState: action.payload.raiseHandState }; @@ -40,6 +43,21 @@ const peer = (state = {}, action) => 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: return state; } @@ -71,6 +89,8 @@ const peers = (state = {}, action) => case 'SET_PEER_RAISE_HAND_STATE': case 'SET_PEER_PICTURE': case 'ADD_CONSUMER': + case 'ADD_PEER_ROLE': + case 'REMOVE_PEER_ROLE': { const oldPeer = state[action.payload.peerId]; @@ -82,6 +102,7 @@ const peers = (state = {}, action) => return { ...state, [oldPeer.id]: peer(oldPeer, action) }; } + case 'SET_PEER_KICK_IN_PROGRESS': case 'REMOVE_CONSUMER': { const oldPeer = state[action.payload.peerId]; diff --git a/app/src/reducers/room.js b/app/src/reducers/room.js index 747d316..d963a0c 100644 --- a/app/src/reducers/room.js +++ b/app/src/reducers/room.js @@ -1,24 +1,27 @@ const initialState = { - name : '', - state : 'new', // new/connecting/connected/disconnected/closed, - locked : false, - inLobby : false, - signInRequired : false, - accessCode : '', // access code to the room if locked and joinByAccessCode == true - joinByAccessCode : true, // if true: accessCode is a possibility to open the room - activeSpeakerId : null, - torrentSupport : false, - showSettings : false, - fullScreenConsumer : null, // ConsumerID - windowConsumer : null, // ConsumerID - toolbarsVisible : true, - mode : 'democratic', - selectedPeerId : null, - spotlights : [], - settingsOpen : false, - lockDialogOpen : false, - joined : false + name : '', + state : 'new', // new/connecting/connected/disconnected/closed, + locked : false, + inLobby : false, + signInRequired : false, + accessCode : '', // access code to the room if locked and joinByAccessCode == true + joinByAccessCode : true, // if true: accessCode is a possibility to open the room + activeSpeakerId : null, + torrentSupport : false, + showSettings : false, + fullScreenConsumer : null, // ConsumerID + windowConsumer : null, // ConsumerID + toolbarsVisible : true, + mode : 'democratic', + selectedPeerId : null, + spotlights : [], + settingsOpen : false, + lockDialogOpen : false, + joined : false, + muteAllInProgress : false, + stopAllVideoInProgress : false, + closeMeetingInProgress : false }; const room = (state = initialState, action) => @@ -110,7 +113,7 @@ const room = (state = initialState, action) => case 'TOGGLE_JOINED': { - const joined = !state.joined; + const joined = true; return { ...state, joined }; } @@ -163,6 +166,15 @@ const room = (state = initialState, action) => 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: return state; } diff --git a/app/src/reducers/userRoles.js b/app/src/reducers/userRoles.js new file mode 100644 index 0000000..217a760 --- /dev/null +++ b/app/src/reducers/userRoles.js @@ -0,0 +1,4 @@ +export const ADMIN = 'admin'; +export const MODERATOR = 'moderator'; +export const AUTHENTICATED = 'authenticated'; +export const ALL = 'normal'; \ No newline at end of file diff --git a/app/src/translations/cn.json b/app/src/translations/cn.json index 3b4cc2a..cb439e5 100644 --- a/app/src/translations/cn.json +++ b/app/src/translations/cn.json @@ -49,6 +49,9 @@ "room.spotlights": "Spotlight中的参与者", "room.passive": "被动参与者", "room.videoPaused": "该视频已暂停", + "room.muteAll": null, + "room.stopAllVideo": null, + "room.closeMeeting": null, "tooltip.login": "登录", "tooltip.logout": "注销", @@ -60,6 +63,7 @@ "tooltip.lobby": "显示大厅", "tooltip.settings": "显示设置", "tooltip.participants": "显示参加者", + "tooltip.kickParticipant": null, "label.roomName": "房间名称", "label.chooseRoomButton": "继续", diff --git a/app/src/translations/de.json b/app/src/translations/de.json index 80969f5..d78e5bd 100644 --- a/app/src/translations/de.json +++ b/app/src/translations/de.json @@ -4,7 +4,7 @@ "socket.reconnected": "Verbindung wieder herges|tellt", "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.consentUnderstand": "I understand", "room.joined": "Konferenzraum betreten", @@ -49,6 +49,9 @@ "room.spotlights": "Aktive Teinehmer", "room.passive": "Passive Teilnehmer", "room.videoPaused": "Video gestoppt", + "room.muteAll": null, + "room.stopAllVideo": null, + "room.closeMeeting": null, "tooltip.login": "Anmelden", "tooltip.logout": "Abmelden", @@ -60,9 +63,10 @@ "tooltip.lobby": "Warteraum", "tooltip.settings": "Einstellungen", "tooltip.participants": "Teilnehmer", + "tooltip.kickParticipant": null, - "label.roomName": "Room name", - "label.chooseRoomButton": "Continue", + "label.roomName": null, + "label.chooseRoomButton": null, "label.yourName": "Dein Name", "label.newWindow": "In separatem Fenster öffnen", "label.fullscreen": "Vollbild", diff --git a/app/src/translations/dk.json b/app/src/translations/dk.json index e07eb6e..87be182 100644 --- a/app/src/translations/dk.json +++ b/app/src/translations/dk.json @@ -49,6 +49,9 @@ "room.spotlights": "Deltagere i fokus", "room.passive": "Passive deltagere", "room.videoPaused": "Denne video er sat på pause", + "room.muteAll": null, + "room.stopAllVideo": null, + "room.closeMeeting": null, "tooltip.login": "Log ind", "tooltip.logout": "Log ud", @@ -60,6 +63,7 @@ "tooltip.lobby": "Vis lobby", "tooltip.settings": "Vis indstillinger", "tooltip.participants": "Vis deltagere", + "tooltip.kickParticipant": null, "label.roomName": "Værelsesnavn", "label.chooseRoomButton": "Fortsæt", @@ -106,7 +110,7 @@ "filesharing.finished": "Filen er færdig med at downloade", "filesharing.save": "Gem", "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.", "device.devicesChanged": "Detekteret ndringer i dine enheder, konfigurer dine enheder i indstillingsdialogen", diff --git a/app/src/translations/el.json b/app/src/translations/el.json index 4acc799..1c028df 100644 --- a/app/src/translations/el.json +++ b/app/src/translations/el.json @@ -49,6 +49,9 @@ "room.spotlights": "Συμμετέχοντες στο Spotlight", "room.passive": "Παθητικοί συμμετέχοντες", "room.videoPaused": "Το βίντεο έχει σταματήσει", + "room.muteAll": null, + "room.stopAllVideo": null, + "room.closeMeeting": null, "tooltip.login": "Σύνδεση", "tooltip.logout": "Αποσύνδεση", @@ -60,6 +63,7 @@ "tooltip.lobby": "Εμφάνιση λόμπι", "tooltip.settings": "Εμφάνιση ρυθμίσεων", "tooltip.participants": "Εμφάνιση συμμετεχόντων", + "tooltip.kickParticipant": null, "label.roomName": "Όνομα δωματίου", "label.chooseRoomButton": "Συνέχεια", @@ -75,8 +79,8 @@ "label.shareFile": "Διαμοιραστείτε ένα αρχείο", "label.fileSharingUnsupported": "Ο διαμοιρασμός αρχείων δεν υποστηρίζεται", "label.unknown": "Άγνωστο", - "label.democratic": "Democratic view", - "label.filmstrip": "Filmstrip view", + "label.democratic": null, + "label.filmstrip": null, "label.low": "Χαμηλή", "label.medium": "Μέτρια", "label.high": "Υψηλή (HD)", diff --git a/app/src/translations/en.json b/app/src/translations/en.json index 9f5be9c..b7248f5 100644 --- a/app/src/translations/en.json +++ b/app/src/translations/en.json @@ -49,6 +49,9 @@ "room.spotlights": "Participants in Spotlight", "room.passive": "Passive Participants", "room.videoPaused": "This video is paused", + "room.muteAll": "Mute all", + "room.stopAllVideo": "Stop all video", + "room.closeMeeting": "Close meeting", "tooltip.login": "Log in", "tooltip.logout": "Log out", @@ -60,6 +63,7 @@ "tooltip.lobby": "Show lobby", "tooltip.settings": "Show settings", "tooltip.participants": "Show participants", + "tooltip.kickParticipant": "Kick out participant", "label.roomName": "Room name", "label.chooseRoomButton": "Continue", diff --git a/app/src/translations/es.json b/app/src/translations/es.json index 0d8e07a..4325509 100644 --- a/app/src/translations/es.json +++ b/app/src/translations/es.json @@ -1,140 +1,144 @@ { - "socket.disconnected": "Desconectado", - "socket.reconnecting": "Desconectado, intentando reconectar", - "socket.reconnected": "Reconectado", - "socket.requestError": "Error en la petición al servidor", + "socket.disconnected": "Desconectado", + "socket.reconnecting": "Desconectado, intentando reconectar", + "socket.reconnected": "Reconectado", + "socket.requestError": "Error en la petición al servidor", - "room.chooseRoom": "Indique el nombre de la sala a la que le gustaría unirse", - "room.cookieConsent": "Esta web utiliza cookies para mejorar la experiencia de usuario", - "room.consentUnderstand": "I understand", - "room.joined": "Se ha unido a la sala", - "room.cantJoin": "No ha sido posible unirse a la sala", - "room.youLocked": "Ha cerrado la sala", - "room.cantLock": "No ha sido posible cerrar la sala", - "room.youUnLocked": "Ha abierto la sala", - "room.cantUnLock": "No ha sido posible abrir la sala", - "room.locked": "La sala ahora es privada", - "room.unlocked": "La sala ahora es pública", - "room.newLobbyPeer": "Nuevo participante en la sala de espera", - "room.lobbyPeerLeft": "Un participante en espera ha salido", - "room.lobbyPeerChangedDisplayName": "Participante en espera cambió su nombre a {displayName}", - "room.lobbyPeerChangedPicture": "Participante en espera cambió su foto", - "room.setAccessCode": "Código de acceso de la sala actualizado", - "room.accessCodeOn": "Código de acceso de la sala activado", - "room.accessCodeOff": "Código de acceso de la sala desactivado", - "room.peerChangedDisplayName": "{oldDisplayName} es ahora {displayName}", - "room.newPeer": "{displayName} se unió a la sala", - "room.newFile": "Nuevo fichero disponible", - "room.toggleAdvancedMode": "Cambiado a modo avanzado", - "room.setDemocraticView": "Cambiado a modo democrático", - "room.setFilmStripView": "Cambiado a modo viñeta", - "room.loggedIn": "Ha iniciado sesión", - "room.loggedOut": "Ha cerrado su sesión", - "room.changedDisplayName": "Ha cambiado su nombre a {displayName}", - "room.changeDisplayNameError": "Hubo un error al intentar cambiar su nombre", - "room.chatError": "No ha sido posible enviar su mensaje", - "room.aboutToJoin": "Está a punto de unirse a una reunión", - "room.roomId": "ID de la sala: {roomName}", - "room.setYourName": "Indique el nombre con el que quiere participar y cómo quiere unirse:", - "room.audioOnly": "Solo sonido", - "room.audioVideo": "Sonido y vídeo", - "room.youAreReady": "Ok, está preparado", - "room.emptyRequireLogin": "¡La sala está vacía! Puede iniciar sesión para comenzar la reunión o esperar hasta que el anfitrión se una", - "room.locketWait": "La sala es privada - espere hasta que alguien le invite ...", - "room.lobbyAdministration": "Administración de la sala de espera", - "room.peersInLobby": "Participantes en la sala de espera", - "room.lobbyEmpty": "La sala de espera está vacía", - "room.hiddenPeers": "{hiddenPeersCount, plural, one {participante} other {participantes}}", - "room.me": "Yo", - "room.spotlights": "Participantes destacados", - "room.passive": "Participantes pasivos", - "room.videoPaused": "El vídeo está pausado", + "room.chooseRoom": "Indique el nombre de la sala a la que le gustaría unirse", + "room.cookieConsent": "Esta web utiliza cookies para mejorar la experiencia de usuario", + "room.consentUnderstand": "I understand", + "room.joined": "Se ha unido a la sala", + "room.cantJoin": "No ha sido posible unirse a la sala", + "room.youLocked": "Ha cerrado la sala", + "room.cantLock": "No ha sido posible cerrar la sala", + "room.youUnLocked": "Ha abierto la sala", + "room.cantUnLock": "No ha sido posible abrir la sala", + "room.locked": "La sala ahora es privada", + "room.unlocked": "La sala ahora es pública", + "room.newLobbyPeer": "Nuevo participante en la sala de espera", + "room.lobbyPeerLeft": "Un participante en espera ha salido", + "room.lobbyPeerChangedDisplayName": "Participante en espera cambió su nombre a {displayName}", + "room.lobbyPeerChangedPicture": "Participante en espera cambió su foto", + "room.setAccessCode": "Código de acceso de la sala actualizado", + "room.accessCodeOn": "Código de acceso de la sala activado", + "room.accessCodeOff": "Código de acceso de la sala desactivado", + "room.peerChangedDisplayName": "{oldDisplayName} es ahora {displayName}", + "room.newPeer": "{displayName} se unió a la sala", + "room.newFile": "Nuevo fichero disponible", + "room.toggleAdvancedMode": "Cambiado a modo avanzado", + "room.setDemocraticView": "Cambiado a modo democrático", + "room.setFilmStripView": "Cambiado a modo viñeta", + "room.loggedIn": "Ha iniciado sesión", + "room.loggedOut": "Ha cerrado su sesión", + "room.changedDisplayName": "Ha cambiado su nombre a {displayName}", + "room.changeDisplayNameError": "Hubo un error al intentar cambiar su nombre", + "room.chatError": "No ha sido posible enviar su mensaje", + "room.aboutToJoin": "Está a punto de unirse a una reunión", + "room.roomId": "ID de la sala: {roomName}", + "room.setYourName": "Indique el nombre con el que quiere participar y cómo quiere unirse:", + "room.audioOnly": "Solo sonido", + "room.audioVideo": "Sonido y vídeo", + "room.youAreReady": "Ok, está preparado", + "room.emptyRequireLogin": "¡La sala está vacía! Puede iniciar sesión para comenzar la reunión o esperar hasta que el anfitrión se una", + "room.locketWait": "La sala es privada - espere hasta que alguien le invite ...", + "room.lobbyAdministration": "Administración de la sala de espera", + "room.peersInLobby": "Participantes en la sala de espera", + "room.lobbyEmpty": "La sala de espera está vacía", + "room.hiddenPeers": "{hiddenPeersCount, plural, one {participante} other {participantes}}", + "room.me": "Yo", + "room.spotlights": "Participantes destacados", + "room.passive": "Participantes pasivos", + "room.videoPaused": "El vídeo está pausado", + "room.muteAll": null, + "room.stopAllVideo": null, + "room.closeMeeting": null, - "tooltip.login": "Entrar", - "tooltip.logout": "Salir", - "tooltip.admitFromLobby": "Admitir desde la sala de espera", - "tooltip.lockRoom": "Configurar sala como privada", - "tooltip.unLockRoom": "Configurar sala como pública", - "tooltip.enterFullscreen": "Presentar en pantalla completa", - "tooltip.leaveFullscreen": "Salir de la pantalla completa", - "tooltip.lobby": "Mostrar sala de espera", - "tooltip.settings": "Mostrar ajustes", - "tooltip.participants": "Mostrar participantes", + "tooltip.login": "Entrar", + "tooltip.logout": "Salir", + "tooltip.admitFromLobby": "Admitir desde la sala de espera", + "tooltip.lockRoom": "Configurar sala como privada", + "tooltip.unLockRoom": "Configurar sala como pública", + "tooltip.enterFullscreen": "Presentar en pantalla completa", + "tooltip.leaveFullscreen": "Salir de la pantalla completa", + "tooltip.lobby": "Mostrar sala de espera", + "tooltip.settings": "Mostrar ajustes", + "tooltip.participants": "Mostrar participantes", + "tooltip.kickParticipant": null, - "label.roomName": "Nombre de la sala", - "label.chooseRoomButton": "Continuar", - "label.yourName": "Su nombre", - "label.newWindow": "Nueva ventana", - "label.fullscreen": "Pantalla completa", - "label.openDrawer": "Abrir panel", - "label.leave": "Salir", - "label.chatInput": "Escriba su mensaje...", - "label.chat": "Chat", - "label.filesharing": "Compartir ficheros", - "label.participants": "Participantes", - "label.shareFile": "Compartir fichero", - "label.fileSharingUnsupported": "Compartir ficheros no está disponible", - "label.unknown": "Desconocido", - "label.democratic": "Vista democrática", - "label.filmstrip": "Vista en viñeta", - "label.low": "Baja", - "label.medium": "Media", - "label.high": "Alta (HD)", - "label.veryHigh": "Muy alta (FHD)", - "label.ultra": "Ultra (UHD)", - "label.close": "Cerrar", + "label.roomName": "Nombre de la sala", + "label.chooseRoomButton": "Continuar", + "label.yourName": "Su nombre", + "label.newWindow": "Nueva ventana", + "label.fullscreen": "Pantalla completa", + "label.openDrawer": "Abrir panel", + "label.leave": "Salir", + "label.chatInput": "Escriba su mensaje...", + "label.chat": "Chat", + "label.filesharing": "Compartir ficheros", + "label.participants": "Participantes", + "label.shareFile": "Compartir fichero", + "label.fileSharingUnsupported": "Compartir ficheros no está disponible", + "label.unknown": "Desconocido", + "label.democratic": "Vista democrática", + "label.filmstrip": "Vista en viñeta", + "label.low": "Baja", + "label.medium": "Media", + "label.high": "Alta (HD)", + "label.veryHigh": "Muy alta (FHD)", + "label.ultra": "Ultra (UHD)", + "label.close": "Cerrar", - "settings.settings": "Ajustes", - "settings.camera": "Cámara", - "settings.selectCamera": "Seleccionar dispositivo de vídeo", - "settings.cantSelectCamera": "No ha sido posible seleccionar el dispositivo de vídeo", - "settings.audio": "Dispositivo de sonido", - "settings.selectAudio": "Seleccione dispositivo de sonido", - "settings.cantSelectAudio": "No ha sido posible seleccionar el dispositivo de sonido", - "settings.resolution": "Seleccione su resolución de imagen", - "settings.layout": "Disposición de la sala", - "settings.selectRoomLayout": "Seleccione la disposición de la sala", - "settings.advancedMode": "Modo avanzado", - "settings.permanentTopBar": "Barra superior permanente", - "settings.lastn": "Cantidad de videos visibles", + "settings.settings": "Ajustes", + "settings.camera": "Cámara", + "settings.selectCamera": "Seleccionar dispositivo de vídeo", + "settings.cantSelectCamera": "No ha sido posible seleccionar el dispositivo de vídeo", + "settings.audio": "Dispositivo de sonido", + "settings.selectAudio": "Seleccione dispositivo de sonido", + "settings.cantSelectAudio": "No ha sido posible seleccionar el dispositivo de sonido", + "settings.resolution": "Seleccione su resolución de imagen", + "settings.layout": "Disposición de la sala", + "settings.selectRoomLayout": "Seleccione la disposición de la sala", + "settings.advancedMode": "Modo avanzado", + "settings.permanentTopBar": "Barra superior permanente", + "settings.lastn": "Cantidad de videos visibles", - "filesharing.saveFileError": "No ha sido posible guardar el fichero", - "filesharing.startingFileShare": "Intentando compartir el fichero", - "filesharing.successfulFileShare": "El fichero se compartió con éxito", - "filesharing.unableToShare": "No ha sido posible compartir el fichero", - "filesharing.error": "Hubo un error al compartir el fichero", - "filesharing.finished": "Descarga del fichero finalizada", - "filesharing.save": "Guardar", - "filesharing.sharedFile": "{displayName} compartió un fichero", - "filesharing.download": "Descargar", - "filesharing.missingSeeds": "Si este proceso demora en exceso, puede ocurrir que no haya nadie compartiendo el fichero. Pruebe a pedirle a alguien que vuelva a subir el fichero que busca.", + "filesharing.saveFileError": "No ha sido posible guardar el fichero", + "filesharing.startingFileShare": "Intentando compartir el fichero", + "filesharing.successfulFileShare": "El fichero se compartió con éxito", + "filesharing.unableToShare": "No ha sido posible compartir el fichero", + "filesharing.error": "Hubo un error al compartir el fichero", + "filesharing.finished": "Descarga del fichero finalizada", + "filesharing.save": "Guardar", + "filesharing.sharedFile": "{displayName} compartió un fichero", + "filesharing.download": "Descargar", + "filesharing.missingSeeds": "Si este proceso demora en exceso, puede ocurrir que no haya nadie compartiendo el fichero. Pruebe a pedirle a alguien que vuelva a subir el fichero que busca.", - "devices.devicesChanged": "Sus dispositivos han cambiado, vuelva a configurarlos en la ventana de ajustes", + "devices.devicesChanged": "Sus dispositivos han cambiado, vuelva a configurarlos en la ventana de ajustes", - "device.audioUnsupported": "Sonido no disponible", - "device.activateAudio": "Activar sonido", - "device.muteAudio": "Silenciar sonido", - "device.unMuteAudio": "Reactivar sonido", + "device.audioUnsupported": "Sonido no disponible", + "device.activateAudio": "Activar sonido", + "device.muteAudio": "Silenciar sonido", + "device.unMuteAudio": "Reactivar sonido", - "device.videoUnsupported": "Vídeo no disponible", - "device.startVideo": "Iniciar vídeo", - "device.stopVideo": "Detener vídeo", + "device.videoUnsupported": "Vídeo no disponible", + "device.startVideo": "Iniciar vídeo", + "device.stopVideo": "Detener vídeo", - "device.screenSharingUnsupported": "Compartir pantalla no disponible", - "device.startScreenSharing": "Iniciar compartir pantalla", - "device.stopScreenSharing": "Detener compartir pantalla", + "device.screenSharingUnsupported": "Compartir pantalla no disponible", + "device.startScreenSharing": "Iniciar compartir pantalla", + "device.stopScreenSharing": "Detener compartir pantalla", - "devices.microphoneDisconnected": "Micrófono desconectado", - "devices.microphoneError": "Hubo un error al acceder a su micrófono", - "devices.microPhoneMute": "Desactivar micrófono", - "devices.micophoneUnMute": "Activar micrófono", - "devices.microphoneEnable": "Micrófono activado", - "devices.microphoneMuteError": "No ha sido posible desactivar su micrófono", - "devices.microphoneUnMuteError": "No ha sido posible activar su micrófono", + "devices.microphoneDisconnected": "Micrófono desconectado", + "devices.microphoneError": "Hubo un error al acceder a su micrófono", + "devices.microPhoneMute": "Desactivar micrófono", + "devices.micophoneUnMute": "Activar micrófono", + "devices.microphoneEnable": "Micrófono activado", + "devices.microphoneMuteError": "No ha sido posible desactivar su micrófono", + "devices.microphoneUnMuteError": "No ha sido posible activar su micrófono", - "devices.screenSharingDisconnected": "Pantalla compartida desconectada", - "devices.screenSharingError": "Hubo un error al acceder a su pantalla", + "devices.screenSharingDisconnected": "Pantalla compartida desconectada", + "devices.screenSharingError": "Hubo un error al acceder a su pantalla", - "devices.cameraDisconnected": "Cámara desconectada", - "devices.cameraError": "Hubo un error al acceder a su cámara" + "devices.cameraDisconnected": "Cámara desconectada", + "devices.cameraError": "Hubo un error al acceder a su cámara" } diff --git a/app/src/translations/fr.json b/app/src/translations/fr.json index ce2953b..8e889b4 100644 --- a/app/src/translations/fr.json +++ b/app/src/translations/fr.json @@ -1,139 +1,143 @@ { - "socket.disconnected" : " Vous avez été déconnecté", - "socket.reconnecting" : " Vous avez été déconnecté, reconnexion en cours", - "socket.reconnected" : " Vous êtes reconnecté", - "socket.requestError" : " Erreur sur une requête serveur", + "socket.disconnected": "Vous avez été déconnecté", + "socket.reconnecting": "Vous avez été déconnecté, reconnexion en cours", + "socket.reconnected": "Vous êtes reconnecté", + "socket.requestError": "Erreur sur une requête serveur", - "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.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.consentUnderstand": "I understand", - "room.joined" : " Vous avez rejoint la salle", - "room.cantJoin" : " Impossible de rejoindre la salle", - "room.youLocked" : " Vous avez privatisé la salle", - "room.cantLock" : " Impossible de privatiser la salle", - "room.youUnLocked" : " Vous avez dé-privatiser la salle", - "room.cantUnLock" : " Impossible de dé-privatiser la réunion", - "room.locked" : " La réunion est privée", - "room.unlocked" : " La réunion est publique", - "room.newLobbyPeer" : " Un nouveau participant est dans la salle d’attente", - "room.lobbyPeerLeft" : " Un participant de la salle d’attente vient de partir", - "room.lobbyPeerChangedDisplayName" : " Un participant dans la salle d’attente a changé de nom pour {displayName}", - "room.lobbyPeerChangedPicture" : " Un participant dans le hall à changer de photo", - "room.setAccessCode" : " Code d’accès à la réunion mis à jour", - "room.accessCodeOn" : " Code d’accès à la réunion activée", - "room.accessCodeOff" : " Code d’accès à la réunion désactivée", - "room.peerChangedDisplayName" : " {oldDisplayName} est maintenant {displayName}", - "room.newPeer" : " {displayName} a rejoint la salle", - "room.newFile" : " Nouveau fichier disponible", - "room.toggleAdvancedMode" : " Basculer en mode avancé", - "room.setDemocraticView" : " Passer en vue démocratique", - "room.setFilmStripView" : " Passer en vue vignette", - "room.loggedIn" : " Vous êtes connecté", - "room.loggedOut" : " Vous êtes déconnecté", - "room.changedDisplayName" : " Votre nom à changer pour {displayname}", - "room.changeDisplayNameError" : " Une erreur s’est produite pour votre changement de nom", - "room.chatError" : " Impossible d’envoyer un message", - "room.aboutToJoin" : " Vous allez rejoindre une réunion", - "room.roomId" : " Salle ID: {roomName}", - "room.setYourName" : " Choisissez votre nom de participant puis comment vous connecter :", - "room.audioOnly" : " Audio uniquement", - "room.audioVideo" : " Audio et Vidéo", - "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.locketWait" : " La réunion est privatisée - attendez que quelqu’un vous laisse entrer", - "room.lobbyAdministration" : " Administration de la salle d’attente", - "room.peersInLobby" : " Participants 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.me" : " Moi", - "room.spotlights" : " Participants actifs", - "room.passive" : " Participants passifs", - "room.videoPaused" : " La vidéo est en pause", + "room.joined": "Vous avez rejoint la salle", + "room.cantJoin": "Impossible de rejoindre la salle", + "room.youLocked": "Vous avez privatisé la salle", + "room.cantLock": "Impossible de privatiser la salle", + "room.youUnLocked": "Vous avez dé-privatiser la salle", + "room.cantUnLock": "Impossible de dé-privatiser la réunion", + "room.locked": "La réunion est privée", + "room.unlocked": "La réunion est publique", + "room.newLobbyPeer": "Un nouveau participant est dans la salle d’attente", + "room.lobbyPeerLeft": "Un participant de la salle d’attente vient de partir", + "room.lobbyPeerChangedDisplayName": "Un participant dans la salle d’attente a changé de nom pour {displayName}", + "room.lobbyPeerChangedPicture": "Un participant dans le hall à changer de photo", + "room.setAccessCode": "Code d’accès à la réunion mis à jour", + "room.accessCodeOn": "Code d’accès à la réunion activée", + "room.accessCodeOff": "Code d’accès à la réunion désactivée", + "room.peerChangedDisplayName": "{oldDisplayName} est maintenant {displayName}", + "room.newPeer": "{displayName} a rejoint la salle", + "room.newFile": "Nouveau fichier disponible", + "room.toggleAdvancedMode": "Basculer en mode avancé", + "room.setDemocraticView": "Passer en vue démocratique", + "room.setFilmStripView": "Passer en vue vignette", + "room.loggedIn": "Vous êtes connecté", + "room.loggedOut": "Vous êtes déconnecté", + "room.changedDisplayName": "Votre nom à changer pour {displayname}", + "room.changeDisplayNameError": "Une erreur s’est produite pour votre changement de nom", + "room.chatError": "Impossible d’envoyer un message", + "room.aboutToJoin": "Vous allez rejoindre une réunion", + "room.roomId": "Salle ID: {roomName}", + "room.setYourName": "Choisissez votre nom de participant puis comment vous connecter:", + "room.audioOnly": "Audio uniquement", + "room.audioVideo": "Audio et Vidéo", + "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.locketWait": "La réunion est privatisée - attendez que quelqu’un vous laisse entrer", + "room.lobbyAdministration": "Administration de la salle d’attente", + "room.peersInLobby": "Participants 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.me": "Moi", + "room.spotlights": "Participants actifs", + "room.passive": "Participants passifs", + "room.videoPaused": "La vidéo est en pause", + "room.muteAll": null, + "room.stopAllVideo": null, + "room.closeMeeting": null, - "tooltip.login" : " Connexion", - "tooltip.logout" : " Déconnexion", - "tooltip.admitFromLobby" : " Autorisé depuis la salle d'attente", - "tooltip.lockRoom" : " Privatisation de la salle", - "tooltip.unLockRoom" : " Dé-privatisation de la salle", - "tooltip.enterFullscreen" : " Afficher en plein écran", - "tooltip.leaveFullscreen" : " Quitter le plein écran", - "tooltip.lobby" : " Afficher la salle d'attente", - "tooltip.settings" : " Afficher les paramètres", + "tooltip.login": "Connexion", + "tooltip.logout": "Déconnexion", + "tooltip.admitFromLobby": "Autorisé depuis la salle d'attente", + "tooltip.lockRoom": "Privatisation de la salle", + "tooltip.unLockRoom": "Dé-privatisation de la salle", + "tooltip.enterFullscreen": "Afficher en plein écran", + "tooltip.leaveFullscreen": "Quitter le plein écran", + "tooltip.lobby": "Afficher la salle d'attente", + "tooltip.settings": "Afficher les paramètres", "tooltip.participants": "Afficher les participants", + "tooltip.kickParticipant": null, - "label.roomName" : " Nom de la salle", - "label.chooseRoomButton" : " Continuer", - "label.yourName" : " Votre nom", - "label.newWindow" : " Nouvelle fenêtre", - "label.fullscreen" : " Plein écran", - "label.openDrawer" : " Ouvrir Drawer", - "label.leave" : " Quiter", - "label.chatInput" : " Entrer un message", - "label.chat" : " Chat", - "label.filesharing" : " Partage de fichier", - "label.participants" : " Participants", - "label.shareFile" : " Partager un fichier", - "label.fileSharingUnsupported" : " Partage de fichier non supporté", - "label.unknown" : " Inconnu", - "label.democratic" : " Vue démocratique", - "label.filmstrip" : " Vue avec miniature", - "label.low" : " Basse définition", - "label.medium" : " Définition normale", - "label.high" : " Haute Définition (HD)", - "label.veryHigh" : " Très Haute Définition (FHD)", - "label.ultra" : " Ultra Haute Définition", - "label.close" : " Fermer", + "label.roomName": "Nom de la salle", + "label.chooseRoomButton": "Continuer", + "label.yourName": "Votre nom", + "label.newWindow": "Nouvelle fenêtre", + "label.fullscreen": "Plein écran", + "label.openDrawer": "Ouvrir Drawer", + "label.leave": "Quiter", + "label.chatInput": "Entrer un message", + "label.chat": "Chat", + "label.filesharing": "Partage de fichier", + "label.participants": "Participants", + "label.shareFile": "Partager un fichier", + "label.fileSharingUnsupported": "Partage de fichier non supporté", + "label.unknown": "Inconnu", + "label.democratic": "Vue démocratique", + "label.filmstrip": "Vue avec miniature", + "label.low": "Basse définition", + "label.medium": "Définition normale", + "label.high": "Haute Définition (HD)", + "label.veryHigh": "Très Haute Définition (FHD)", + "label.ultra": "Ultra Haute Définition", + "label.close": "Fermer", - "settings.settings" : " Paramètres", - "settings.camera" : " Caméra", - "settings.selectCamera" : " Sélectionner votre caméra", - "settings.cantSelectCamera" : " Impossible de sélectionner votre caméra", - "settings.audio" : " Microphone", - "settings.selectAudio" : " Sélectionner votre microphone", - "settings.cantSelectAudio" : " Impossible de sélectionner votre la caméra", - "settings.resolution" : " Sélection votre résolution", - "settings.layout" : " Mode d'affichage de la salle", - "settings.selectRoomLayout" : " Sélectionner l'affiche de la salle", - "settings.advancedMode" : " Mode avancé", + "settings.settings": "Paramètres", + "settings.camera": "Caméra", + "settings.selectCamera": "Sélectionner votre caméra", + "settings.cantSelectCamera": "Impossible de sélectionner votre caméra", + "settings.audio": "Microphone", + "settings.selectAudio": "Sélectionner votre microphone", + "settings.cantSelectAudio": "Impossible de sélectionner votre la caméra", + "settings.resolution": "Sélection votre résolution", + "settings.layout": "Mode d'affichage de la salle", + "settings.selectRoomLayout": "Sélectionner l'affiche de la salle", + "settings.advancedMode": "Mode avancé", "settings.permanentTopBar": "Barre supérieure permanente", "settings.lastn": "Nombre de vidéos visibles", - "filesharing.saveFileError" : " Impossible d'enregistrer le fichier", - "filesharing.startingFileShare" : " Début du transfert de fichier", - "filesharing.successfulFileShare" : " Fichier transféré", - "filesharing.unableToShare" : " Impossible de transférer le fichier", - "filesharing.error" : " Erreur lors du transfert de fichier", - "filesharing.finished" : " Fin du transfert de fichier", - "filesharing.save" : " Sauver", - "filesharing.sharedFile" : " {displayName} a partagé un fichier", - "filesharing.download" : " Télécharger", - "filesharing.missingSeeds" : " Si le téléchargement prend trop de temps c’est qu’il n’y a peut-être plus personne qui partage ce torrent. Demander à quelqu’un de repartager le document.", - "devices.devicesChanged" : " Vos périphériques ont changé, reconfigurer vos périphériques avec le menu paramètre", + "filesharing.saveFileError": "Impossible d'enregistrer le fichier", + "filesharing.startingFileShare": "Début du transfert de fichier", + "filesharing.successfulFileShare": "Fichier transféré", + "filesharing.unableToShare": "Impossible de transférer le fichier", + "filesharing.error": "Erreur lors du transfert de fichier", + "filesharing.finished": "Fin du transfert de fichier", + "filesharing.save": "Sauver", + "filesharing.sharedFile": "{displayName} a partagé un fichier", + "filesharing.download": "Télécharger", + "filesharing.missingSeeds": "Si le téléchargement prend trop de temps c’est qu’il n’y a peut-être plus personne qui partage ce torrent. Demander à quelqu’un de repartager le document.", + "devices.devicesChanged": "Vos périphériques ont changé, reconfigurer vos périphériques avec le menu paramètre", - "device.audioUnsupported" : " Microphone non supporté", - "device.activateAudio" : " Activer l'audio", - "device.muteAudio" : " Désactiver l'audio", - "device.unMuteAudio" : " Réactiver l'audio", + "device.audioUnsupported": "Microphone non supporté", + "device.activateAudio": "Activer l'audio", + "device.muteAudio": "Désactiver l'audio", + "device.unMuteAudio": "Réactiver l'audio", - "device.videoUnsupported" : " Vidéo non supporté", - "device.startVideo" : " Démarrer la vidéo", - "device.stopVideo" : " Arrêter la vidéo", + "device.videoUnsupported": "Vidéo non supporté", + "device.startVideo": "Démarrer la vidéo", + "device.stopVideo": "Arrêter la vidéo", - "device.screenSharingUnsupported" : " Partage d'écran non supporté", - "device.startScreenSharing" : " Démarrer le partage d 'écran'", - "device.stopScreenSharing" : " Arrêter le partage d'écran", + "device.screenSharingUnsupported": "Partage d'écran non supporté", + "device.startScreenSharing": "Démarrer le partage d 'écran'", + "device.stopScreenSharing": "Arrêter le partage d'écran", - "devices.microphoneDisconnected" : " Microphone déconnecté", - "devices.microphoneError" : " Une erreur est apparue lors de l'accès à votre microphone", - "devices.microPhoneMute" : " Désactiver le microphone", - "devices.micophoneUnMute" : " Réactiver le microphone", - "devices.microphoneEnable" : " Activer le microphone", - "devices.microphoneMuteError" : " Impossible de désactiver le microphone", - "devices.microphoneUnMuteError" : " Impossible de réactiver le microphone", + "devices.microphoneDisconnected": "Microphone déconnecté", + "devices.microphoneError": "Une erreur est apparue lors de l'accès à votre microphone", + "devices.microPhoneMute": "Désactiver le microphone", + "devices.micophoneUnMute": "Réactiver le microphone", + "devices.microphoneEnable": "Activer le microphone", + "devices.microphoneMuteError": "Impossible de désactiver le microphone", + "devices.microphoneUnMuteError": "Impossible de réactiver le microphone", - "devices.screenSharingDisconnected" : " Partage d'écran déconnecté", - "devices.screenSharingError" : " Une erreur est apparue lors de l'accès à votre partage d'écran", + "devices.screenSharingDisconnected": "Partage d'écran déconnecté", + "devices.screenSharingError": "Une erreur est apparue lors de l'accès à votre partage d'écran", - "devices.cameraDisconnected" : " Caméra déconnectée", - "devices.cameraError" : " Une erreur est apparue lors de l'accès à votre caméra" + "devices.cameraDisconnected": "Caméra déconnectée", + "devices.cameraError": "Une erreur est apparue lors de l'accès à votre caméra" } diff --git a/app/src/translations/hr.json b/app/src/translations/hr.json index ff01d7b..5e97b0c 100644 --- a/app/src/translations/hr.json +++ b/app/src/translations/hr.json @@ -49,6 +49,9 @@ "room.spotlights": "Učesnici u fokusu", "room.passive": "Pasivni učesnici", "room.videoPaused": "Video pauziran", + "room.muteAll": null, + "room.stopAllVideo": null, + "room.closeMeeting": null, "tooltip.login": "Prijava", "tooltip.logout": "Odjava", @@ -60,6 +63,7 @@ "tooltip.lobby": "Prikaži predvorje", "tooltip.settings": "Prikaži postavke", "tooltip.participants": "Pokažite sudionike", + "tooltip.kickParticipant": null, "label.roomName": "Naziv sobe", "label.chooseRoomButton": "Nastavi", diff --git a/app/src/translations/hu.json b/app/src/translations/hu.json index 6d63a7a..9c4399a 100644 --- a/app/src/translations/hu.json +++ b/app/src/translations/hu.json @@ -4,7 +4,7 @@ "socket.reconnected": "Sikeres újarkapcsolódá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.consentUnderstand": "I understand", "room.joined": "Csatlakozátál a konferenciához", @@ -49,6 +49,9 @@ "room.spotlights": "Látható résztvevők", "room.passive": "Passzív résztvevők", "room.videoPaused": "Ez a videóstream szünetel", + "room.muteAll": null, + "room.stopAllVideo": null, + "room.closeMeeting": null, "tooltip.login": "Belépés", "tooltip.logout": "Kilépés", @@ -60,6 +63,7 @@ "tooltip.lobby": "Az előszobában várakozók listája", "tooltip.settings": "Beállítások", "tooltip.participants": "Résztvevők", + "tooltip.kickParticipant": null, "label.roomName": "Konferencia", "label.chooseRoomButton": "Tovább", diff --git a/app/src/translations/nb.json b/app/src/translations/nb.json index 3f3dc32..6c81dea 100644 --- a/app/src/translations/nb.json +++ b/app/src/translations/nb.json @@ -49,6 +49,9 @@ "room.spotlights": "Deltakere i fokus", "room.passive": "Passive deltakere", "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.logout": "Logg ut", @@ -60,6 +63,7 @@ "tooltip.lobby": "Vis lobby", "tooltip.settings": "Vis innstillinger", "tooltip.participants": "Vis deltakere", + "tooltip.kickParticipant": "Spark ut deltaker", "label.roomName": "Møtenavn", "label.chooseRoomButton": "Fortsett", diff --git a/app/src/translations/pl.json b/app/src/translations/pl.json index d5dfe03..662f878 100644 --- a/app/src/translations/pl.json +++ b/app/src/translations/pl.json @@ -49,6 +49,9 @@ "room.spotlights": "Aktywni uczestnicy", "room.passive": "Pasywni uczestnicy", "room.videoPaused": "To wideo jest wstrzymane.", + "room.muteAll": null, + "room.stopAllVideo": null, + "room.closeMeeting": null, "tooltip.login": "Zaloguj", "tooltip.logout": "Wyloguj", @@ -60,6 +63,7 @@ "tooltip.lobby": "Pokaż poczekalnię", "tooltip.settings": "Pokaż ustawienia", "tooltip.participants": "Pokaż uczestników", + "tooltip.kickParticipant": null, "label.roomName": "Nazwa konferencji", "label.chooseRoomButton": "Kontynuuj", diff --git a/app/src/translations/pt.json b/app/src/translations/pt.json index 9fefbfe..5f034e9 100644 --- a/app/src/translations/pt.json +++ b/app/src/translations/pt.json @@ -49,6 +49,9 @@ "room.spotlights": "Participantes em foco", "room.passive": "Participantes passivos", "room.videoPaused": "Este vídeo está em pausa", + "room.muteAll": null, + "room.stopAllVideo": null, + "room.closeMeeting": null, "tooltip.login": "Entrar", "tooltip.logout": "Sair", @@ -60,6 +63,7 @@ "tooltip.lobby": "Apresentar sala de espera", "tooltip.settings": "Apresentar definições", "tooltip.participants": "Apresentar participantes", + "tooltip.kickParticipant": null, "label.roomName": "Nome da sala", "label.chooseRoomButton": "Continuar", diff --git a/app/src/translations/ro.json b/app/src/translations/ro.json index 23bd970..a187665 100644 --- a/app/src/translations/ro.json +++ b/app/src/translations/ro.json @@ -49,6 +49,9 @@ "room.spotlights": "Participanți în Spotlight", "room.passive": "Participanți pasivi", "room.videoPaused": "Acest video este pus pe pauză", + "room.muteAll": null, + "room.stopAllVideo": null, + "room.closeMeeting": null, "tooltip.login": "Intră în cont", "tooltip.logout": "Deconectare", @@ -58,7 +61,9 @@ "tooltip.enterFullscreen": "Modul ecran complet", "tooltip.leaveFullscreen": "Ieșire din modul ecran complet", "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.chooseRoomButton": "Continuare", diff --git a/server/config/config.example.js b/server/config/config.example.js index e0b78e4..53b1f05 100644 --- a/server/config/config.example.js +++ b/server/config/config.example.js @@ -1,4 +1,5 @@ const os = require('os'); +const userRoles = require('../userRoles'); module.exports = { @@ -63,18 +64,109 @@ module.exports = // listeningRedirectPort disabled // use case: loadbalancer backend httpOnly : false, - // If this is set to true, only signed-in users will be able - // to join a room directly. Non-signed-in users (guests) will - // always be put in the lobby regardless of room lock status. - // If false, there is no difference between guests and signed-in - // users when joining. - requireSignInToAccess : true, - // This flag has no effect when requireSignInToAccess is false - // When truthy, the room will be open to all users when the first - // authenticated user has already joined the room. - activateOnHostJoin : true, + // This function will be called on successful login through oidc. + // Use this function to map your oidc userinfo to the Peer object. + // The roomId is equal to the room name. + // See examples below. + // Examples: + /* + // All authenicated users will be MODERATOR and AUTHENTICATED + userMapping : async ({ peer, roomId, userinfo }) => + { + 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, // Mediasoup settings - mediasoup : + mediasoup : { numWorkers : Object.keys(os.cpus()).length, // mediasoup Worker settings. diff --git a/server/lib/Lobby.js b/server/lib/Lobby.js index 75f4bf9..75cae49 100644 --- a/server/lib/Lobby.js +++ b/server/lib/Lobby.js @@ -14,7 +14,7 @@ class Lobby extends EventEmitter // Closed flag. this._closed = false; - this._peers = new Map(); + this._peers = {}; } close() @@ -23,27 +23,28 @@ class Lobby extends EventEmitter this._closed = true; - this._peers.forEach((peer) => + // Close the peers. + for (const peer in this._peers) { if (!peer.closed) peer.close(); - }); + } - this._peers.clear(); + this._peers = null; } checkEmpty() { logger.info('checkEmpty()'); - return this._peers.size === 0; + return Object.keys(this._peers).length === 0; } peerList() { logger.info('peerList()'); - return Array.from(this._peers.values()).map((peer) => + return Object.values(this._peers).map((peer) => ({ peerId : peer.id, displayName : peer.displayName @@ -52,38 +53,42 @@ class Lobby extends EventEmitter hasPeer(peerId) { - return this._peers.has(peerId); + return this._peers[peerId] != null; } promoteAllPeers() { logger.info('promoteAllPeers()'); - this._peers.forEach((peer) => + for (const peer in this._peers) { if (peer.socket) this.promotePeer(peer.id); - }); + } } promotePeer(peerId) { logger.info('promotePeer() [peer:"%s"]', peerId); - const peer = this._peers.get(peerId); + const peer = this._peers[peerId]; if (peer) { 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.socketRequestHandler = null; - peer.authenticationHandler = null; + peer.gotRoleHandler = null; + peer.displayNameChangeHandler = null; + peer.pictureChangeHandler = null; peer.closeHandler = null; 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('changeDisplayName', peer); - this.emit('changePicture', peer); - this.emit('peerAuthenticated', peer); - } + this.emit('peerRolesChanged', peer); + }; + + peer.displayNameChangeHandler = () => + { + logger.info('parkPeer() | displayNameChange [peer:"%s"]', peer.id); + + this.emit('changeDisplayName', peer); + }; + + peer.pictureChangeHandler = () => + { + logger.info('parkPeer() | pictureChange [peer:"%s"]', peer.id); + + this.emit('changePicture', peer); }; peer.closeHandler = () => @@ -133,7 +147,7 @@ class Lobby extends EventEmitter this.emit('peerClosed', peer); - this._peers.delete(peer.id); + delete this._peers[peer.id]; if (this.checkEmpty()) this.emit('lobbyEmpty'); @@ -141,9 +155,11 @@ class Lobby extends EventEmitter 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); @@ -169,8 +185,6 @@ class Lobby extends EventEmitter peer.displayName = displayName; - this.emit('changeDisplayName', peer); - cb(); break; @@ -181,8 +195,6 @@ class Lobby extends EventEmitter peer.picture = picture; - this.emit('changePicture', peer); - cb(); break; diff --git a/server/lib/Peer.js b/server/lib/Peer.js index cce62aa..1878a61 100644 --- a/server/lib/Peer.js +++ b/server/lib/Peer.js @@ -1,17 +1,20 @@ const EventEmitter = require('events').EventEmitter; +const userRoles = require('../userRoles'); const Logger = require('./Logger'); const logger = new Logger('Peer'); 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(); this._id = id; + this._roomId = roomId; + this._authId = null; this._socket = socket; @@ -22,7 +25,7 @@ class Peer extends EventEmitter this._inLobby = false; - this._authenticated = false; + this._roles = [ userRoles.ALL ]; this._displayName = false; @@ -40,8 +43,6 @@ class Peer extends EventEmitter this._consumers = new Map(); - this._checkAuthentication(); - this._handlePeer(); } @@ -58,56 +59,25 @@ class Peer extends EventEmitter transport.close(); }); - if (this._socket) - this._socket.disconnect(true); + if (this.socket) + this.socket.disconnect(true); this.emit('close'); } _handlePeer() { - this.socket.use((packet, next) => + if (this.socket) { - this._checkAuthentication(); - - return next(); - }); - - this.socket.on('disconnect', () => - { - if (this.closed) - return; - - logger.debug('"disconnect" event [id:%s]', this.id); - - 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; + this.socket.on('disconnect', () => + { + if (this.closed) + return; + + logger.debug('"disconnect" event [id:%s]', this.id); + + this.close(); + }); } } @@ -121,6 +91,16 @@ class Peer extends EventEmitter this._id = id; } + get roomId() + { + return this._roomId; + } + + set roomId(roomId) + { + this._roomId = roomId; + } + get authId() { return this._authId; @@ -166,21 +146,9 @@ class Peer extends EventEmitter this._inLobby = inLobby; } - get authenticated() + get roles() { - return this._authenticated; - } - - set authenticated(authenticated) - { - if (authenticated !== this._authenticated) - { - const oldAuthenticated = this._authenticated; - - this._authenticated = authenticated; - - this.emit('authenticationChanged', { oldAuthenticated }); - } + return this._roles; } get displayName() @@ -262,6 +230,35 @@ class Peer extends EventEmitter 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) { this.transports.set(id, transport); @@ -319,7 +316,8 @@ class Peer extends EventEmitter { id : this.id, displayName : this.displayName, - picture : this.picture + picture : this.picture, + roles : this.roles }; return peerInfo; diff --git a/server/lib/Room.js b/server/lib/Room.js index 5d86f11..f0ceb66 100644 --- a/server/lib/Room.js +++ b/server/lib/Room.js @@ -2,6 +2,7 @@ const EventEmitter = require('events').EventEmitter; const axios = require('axios'); const Logger = require('./Logger'); const Lobby = require('./Lobby'); +const userRoles = require('../userRoles'); const config = require('../config/config'); const logger = new Logger('Room'); @@ -54,6 +55,12 @@ class Room extends EventEmitter // Locked flag. 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 this._joinByAccesCode = true; @@ -100,11 +107,8 @@ class Room extends EventEmitter // Close the peers. for (const peer in this._peers) { - if (Object.prototype.hasOwnProperty.call(this._peers, peer)) - { - if (!peer.closed) - peer.close(); - } + if (!peer.closed) + peer.close(); } this._peers = null; @@ -118,26 +122,27 @@ class Room extends EventEmitter 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]) { logger.warn( 'handleConnection() | there is already a peer with same peerId [peer:"%s"]', 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) - { this._parkPeer(peer); - } 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._handleGuest(peer); } @@ -145,21 +150,12 @@ class Room extends EventEmitter _handleGuest(peer) { - if (config.requireSignInToAccess) - { - if (config.activateOnHostJoin && !this.checkEmpty()) - { - this._peerJoining(peer); - } - else - { - this._parkPeer(peer); - this._notification(peer.socket, 'signInRequired'); - } - } + if (config.activateOnHostJoin && !this.checkEmpty()) + this._peerJoining(peer); else { - this._peerJoining(peer); + this._parkPeer(peer); + this._notification(peer.socket, 'signInRequired'); } } @@ -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) => @@ -304,7 +317,6 @@ class Room extends EventEmitter }, 10000); } - // checks both room and lobby checkEmpty() { return Object.keys(this._peers).length === 0; @@ -324,12 +336,8 @@ class Room extends EventEmitter { peer.socket.join(this._roomId); - const index = this._lastN.indexOf(peer.id); - - if (index === -1) // We don't have this peer, add to end - { - this._lastN.push(peer.id); - } + // If we don't have this peer, add to end + !this._lastN.includes(peer.id) && this._lastN.push(peer.id); this._peers[peer.id] = peer; @@ -402,25 +410,17 @@ class Room extends EventEmitter // If the Peer was joined, notify all Peers. if (peer.joined) - { this._notification(peer.socket, 'peerClosed', { peerId: peer.id }, true); - } - const index = this._lastN.indexOf(peer.id); - - if (index > -1) // We have this peer in the list, remove - { - this._lastN.splice(index, 1); - } + // Remove from lastN + this._lastN = this._lastN.filter((id) => id !== peer.id); delete this._peers[peer.id]; // If this is the last Peer in the room and // lobby is empty, close the room after a while. if (this.checkEmpty() && this._lobby.checkEmpty()) - { this.selfDestructCountdown(); - } }); peer.on('displayNameChanged', ({ oldDisplayName }) => @@ -449,6 +449,32 @@ class Room extends EventEmitter picture : peer.picture }, 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) @@ -464,27 +490,6 @@ class Room extends EventEmitter 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. if (peer.joined) throw new Error('Peer already joined'); @@ -512,7 +517,10 @@ class Room extends EventEmitter .filter((joinedPeer) => joinedPeer.id !== peer.id) .map((joinedPeer) => (joinedPeer.peerInfo)); - cb(null, { peers: peerInfos }); + cb(null, { + roles : peer.roles, + peers : peerInfos + }); // Mark the new Peer as joined. peer.joined = true; @@ -540,7 +548,8 @@ class Room extends EventEmitter { id : peer.id, displayName : displayName, - picture : picture + picture : picture, + roles : peer.roles } ); } @@ -1106,6 +1115,92 @@ class Room extends EventEmitter 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: { 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) { socket.broadcast.to(this._roomId).emit( 'notification', { method, data } ); + + if (includeSender) + socket.emit('notification', { method, data }); } else { diff --git a/server/server.js b/server/server.js index 8faa837..ee29495 100755 --- a/server/server.js +++ b/server/server.js @@ -18,6 +18,7 @@ const Peer = require('./lib/Peer'); const base64 = require('base-64'); const helmet = require('helmet'); +const userRoles = require('./userRoles'); const { loginHelper, logoutHelper @@ -190,7 +191,7 @@ function setupLTI(ltiConfig) } 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 @@ -241,51 +242,6 @@ function setupOIDC(oidcIssuer) _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); } ); @@ -324,7 +280,8 @@ async function setupAuth() { passport.authenticate('oidc', { state : base64.encode(JSON.stringify({ - id : req.query.id + peerId : req.query.peerId, + roomId : req.query.roomId })) })(req, res, next); }); @@ -341,6 +298,19 @@ async function setupAuth() // logout 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(); res.send(logoutHelper()); }); @@ -349,35 +319,35 @@ async function setupAuth() app.get( '/auth/callback', passport.authenticate('oidc', { failureRedirect: '/auth/login' }), - (req, res) => + async (req, res) => { const state = JSON.parse(base64.decode(req.query.state)); - let displayName; - let picture; + const { peerId, roomId } = state; - 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) - displayName = req.user.displayName; - else - displayName = ''; - - if (req.user.picture != null) - picture = req.user.picture; - else - picture = '/static/media/buddy.403cb9f6.svg'; + await config.userMapping({ + peer, + roomId, + userinfo : req.user._userinfo + }); } - const peer = peers.get(state.id); - - peer && (peer.displayName = displayName); - peer && (peer.picture = picture); - peer && (peer.authenticated = true); - res.send(loginHelper({ - displayName, - picture + displayName : peer.displayName, + picture : peer.picture })); } ); @@ -495,12 +465,36 @@ async function runWebSocketServer() queue.push(async () => { const room = await getOrCreateRoom({ roomId }); - const peer = new Peer({ id: peerId, socket }); + const peer = new Peer({ id: peerId, roomId, socket }); peers.set(peerId, peer); 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); }) .catch((error) => diff --git a/server/userRoles.js b/server/userRoles.js new file mode 100644 index 0000000..c8cf886 --- /dev/null +++ b/server/userRoles.js @@ -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' +}; \ No newline at end of file