diff --git a/conference.js b/conference.js index fd8726cb05..2e2f6b6b51 100644 --- a/conference.js +++ b/conference.js @@ -484,8 +484,7 @@ export default { /** * Indicates if the desktop sharing functionality has been enabled. - * It takes into consideration {@link isDesktopSharingDisabledByConfig} - * as well as the status returned by + * It takes into consideration the status returned by * {@link JitsiMeetJS.isDesktopSharingEnabled()}. The latter can be false * either if the desktop sharing is not supported by the current browser * or if it was disabled through lib-jitsi-meet specific options (check @@ -493,19 +492,6 @@ export default { */ isDesktopSharingEnabled: false, - /** - * Set to true if the desktop sharing functionality has been - * explicitly disabled in the config. - */ - isDesktopSharingDisabledByConfig: false, - - /** - * The text displayed when the desktop sharing button is disabled through - * the config. The value is set through - * {@link interfaceConfig.DESKTOP_SHARING_BUTTON_DISABLED_TOOLTIP}. - */ - desktopSharingDisabledTooltip: null, - /** * The local audio track (if any). * FIXME tracks from redux store should be the single source of truth @@ -720,13 +706,8 @@ export default { APP.connection = connection = con; // Desktop sharing related stuff: - this.isDesktopSharingDisabledByConfig - = config.disableDesktopSharing; this.isDesktopSharingEnabled - = !this.isDesktopSharingDisabledByConfig - && JitsiMeetJS.isDesktopSharingEnabled(); - this.desktopSharingDisabledTooltip - = interfaceConfig.DESKTOP_SHARING_BUTTON_DISABLED_TOOLTIP; + = JitsiMeetJS.isDesktopSharingEnabled(); eventEmitter.emit( JitsiMeetConferenceEvents.DESKTOP_SHARING_ENABLED_CHANGED, this.isDesktopSharingEnabled); @@ -1909,6 +1890,14 @@ export default { JitsiConferenceEvents.PARTICIPANT_PROPERTY_CHANGED, (participant, name, oldValue, newValue) => { switch (name) { + case 'features_screen-sharing': { + APP.store.dispatch(participantUpdated({ + conference: room, + id: participant.getId(), + features: { 'screen-sharing': true } + })); + break; + } case 'raisedHand': APP.store.dispatch(participantUpdated({ conference: room, diff --git a/config.js b/config.js index 06148611e8..d6f5243731 100644 --- a/config.js +++ b/config.js @@ -142,9 +142,6 @@ var config = { // Desktop sharing - // Enable / disable desktop sharing - // disableDesktopSharing: false, - // The ID of the jidesha extension for Chrome. desktopSharingChromeExtId: null, @@ -248,6 +245,9 @@ var config = { // edit their profile. enableUserRolesBasedOnToken: false, + // Whether or not some features are checked based on token. + // enableFeaturesBasedOnToken: false, + // Message to show the users. Example: 'The service will be down for // maintenance at 01:00 AM GMT, // noticeMessage: '', diff --git a/css/_toolbars.scss b/css/_toolbars.scss index 4167e3d0c3..a8042edb75 100644 --- a/css/_toolbars.scss +++ b/css/_toolbars.scss @@ -95,7 +95,13 @@ } i.disabled { - cursor: initial + cursor: initial; + color: #3b475c; + } + + .disabled i { + cursor: initial; + color: #3b475c; } i.disabled:hover { @@ -135,6 +141,10 @@ &.unclickable:hover { background: inherit; } + &.disabled { + cursor: initial; + color: #3b475c; + } } .beta-tag { diff --git a/css/modals/invite/_add-people.scss b/css/modals/invite/_add-people.scss index 3d61d0bfbb..9b8762f79b 100644 --- a/css/modals/invite/_add-people.scss +++ b/css/modals/invite/_add-people.scss @@ -23,6 +23,21 @@ margin: auto; } } + + .footer-text-wrap { + display: flex; + } + + .footer-telephone-icon { + display: flex; + transform: scaleX(-1); + padding-left: 10px; + + i { + line-height: 20px; + margin: auto; + } + } } } diff --git a/interface_config.js b/interface_config.js index 5cc8772854..b0b8e78bd6 100644 --- a/interface_config.js +++ b/interface_config.js @@ -5,12 +5,6 @@ var interfaceConfig = { // methods allowing to use variables both in css and js. DEFAULT_BACKGROUND: '#474747', - /** - * In case the desktop sharing is disabled through the config the button - * will not be hidden, but displayed as disabled with this text us as - * a tooltip. - */ - DESKTOP_SHARING_BUTTON_DISABLED_TOOLTIP: null, INITIAL_TOOLBAR_TIMEOUT: 20000, TOOLBAR_TIMEOUT: 4000, TOOLBAR_ALWAYS_VISIBLE: false, diff --git a/lang/main.json b/lang/main.json index e0bf3b83ac..3adb24898e 100644 --- a/lang/main.json +++ b/lang/main.json @@ -361,6 +361,10 @@ "muteParticipantTitle": "Mute this member?", "muteParticipantBody": "You won't be able to unmute them, but they can unmute themselves at any time.", "muteParticipantButton": "Mute", + "liveStreamingDisabledTooltip": "Start live stream disabled.", + "liveStreamingDisabledForGuestTooltip": "Guests can't start live streaming.", + "recordingDisabledTooltip": "Start recording disabled.", + "recordingDisabledForGuestTooltip": "Guests can't start recordings.", "remoteControlTitle": "Remote desktop control", "remoteControlRequestMessage": "Will you allow __user__ to remotely control your desktop?", "remoteControlShareScreenWarning": "Note that if you press \"Allow\" you will share your screen!", @@ -371,6 +375,8 @@ "remoteControlStopMessage": "The remote control session ended!", "close": "Close", "shareYourScreen": "Share your screen", + "shareYourScreenDisabled": "Screen sharing disabled.", + "shareYourScreenDisabledForGuest": "Guests can't screen share.", "yourEntireScreen": "Your entire screen", "applicationWindow": "Application window" }, @@ -521,6 +527,7 @@ "countryNotSupported": "We do not support this destination yet.", "countryReminder": "Calling outside the US? Please make sure you start with the country code!", "disabled": "You can't invite people.", + "footerText": "Dialing out is disabled.", "invite": "Invite", "loading": "Searching for people and phone numbers", "loadingNumber": "Validating phone number", diff --git a/react/features/base/conference/functions.js b/react/features/base/conference/functions.js index 90a5d22bce..28034d73e8 100644 --- a/react/features/base/conference/functions.js +++ b/react/features/base/conference/functions.js @@ -188,8 +188,17 @@ function _reportError(msg, err) { */ export function sendLocalParticipant( stateful: Function | Object, - conference: { sendCommand: Function, setDisplayName: Function }) { - const { avatarID, avatarURL, email, name } = getLocalParticipant(stateful); + conference: { + sendCommand: Function, + setDisplayName: Function, + setLocalParticipantProperty: Function }) { + const { + avatarID, + avatarURL, + email, + features, + name + } = getLocalParticipant(stateful); avatarID && conference.sendCommand(AVATAR_ID_COMMAND, { value: avatarID @@ -200,5 +209,10 @@ export function sendLocalParticipant( email && conference.sendCommand(EMAIL_COMMAND, { value: email }); + + if (features && features['screen-sharing'] === 'true') { + conference.setLocalParticipantProperty('features_screen-sharing', true); + } + conference.setDisplayName(name); } diff --git a/react/features/base/config/functions.js b/react/features/base/config/functions.js index 46142c6526..e73e622d87 100644 --- a/react/features/base/config/functions.js +++ b/react/features/base/config/functions.js @@ -89,8 +89,6 @@ const WHITELISTED_KEYS = [ 'disableAGC', 'disableAP', 'disableAudioLevels', - 'disableDesktopSharing', - 'disableDesktopSharing', 'disableH264', 'disableHPF', 'disableNS', @@ -106,7 +104,6 @@ const WHITELISTED_KEYS = [ 'enableStatsID', 'enableTalkWhileMuted', 'enableTcc', - 'enableUserRolesBasedOnToken', 'etherpad_base', 'failICE', 'fileRecordingsEnabled', diff --git a/react/features/base/jwt/middleware.js b/react/features/base/jwt/middleware.js index ee10677419..31db0f112f 100644 --- a/react/features/base/jwt/middleware.js +++ b/react/features/base/jwt/middleware.js @@ -138,7 +138,7 @@ function _maybeSetCalleeInfoVisible({ dispatch, getState }, next, action) { */ function _overwriteLocalParticipant( { dispatch, getState }, - { avatarURL, email, name }) { + { avatarURL, email, name, features }) { let localParticipant; if ((avatarURL || email || name) @@ -157,6 +157,9 @@ function _overwriteLocalParticipant( if (name) { newProperties.name = name; } + if (features) { + newProperties.features = features; + } dispatch(participantUpdated(newProperties)); } } @@ -229,7 +232,9 @@ function _setJWT(store, next, action) { action.server = context.server; action.user = user; - user && _overwriteLocalParticipant(store, user); + user && _overwriteLocalParticipant( + store, { ...user, + features: context.features }); } } } else if (typeof APP === 'undefined') { @@ -281,6 +286,8 @@ function _undoOverwriteLocalParticipant( if (name === localParticipant.name) { newProperties.name = undefined; } + newProperties.features = undefined; + dispatch(participantUpdated(newProperties)); } } diff --git a/react/features/base/react/components/web/MultiSelectAutocomplete.js b/react/features/base/react/components/web/MultiSelectAutocomplete.js index 9f3f0b8da9..2370dea8d4 100644 --- a/react/features/base/react/components/web/MultiSelectAutocomplete.js +++ b/react/features/base/react/components/web/MultiSelectAutocomplete.js @@ -24,6 +24,12 @@ class MultiSelectAutocomplete extends Component { */ defaultValue: PropTypes.array, + /** + * Optional footer to show as a last element in the results. + * Should be of type {content: } + */ + footer: PropTypes.object, + /** * Indicates if the component is disabled. */ @@ -151,6 +157,7 @@ class MultiSelectAutocomplete extends Component {
{ */ _dialOutAuthUrl: PropTypes.string, + /** + * Whether to show a footer text after the search results + * as a last element. + */ + _footerTextEnabled: PropTypes.bool, + /** * The JWT token. */ @@ -168,11 +175,15 @@ class AddPeopleDialog extends Component<*, *> { * @returns {ReactElement} */ render() { - const { addPeopleEnabled, dialOutEnabled, t } = this.props; + const { _footerTextEnabled, + addPeopleEnabled, + dialOutEnabled, + t } = this.props; let isMultiSelectDisabled = this.state.addToCallInProgress || false; let placeholder; let loadingMessage; let noMatches; + let footerText; if (addPeopleEnabled && dialOutEnabled) { loadingMessage = 'addPeople.loading'; @@ -192,6 +203,19 @@ class AddPeopleDialog extends Component<*, *> { placeholder = 'addPeople.disabled'; } + if (_footerTextEnabled) { + footerText = { + content:
+
+ + + +
+ { translateToHTML(t, 'addPeople.footerText') } +
+ }; + } + return ( {
{ this._renderErrorMessage() } { function _mapStateToProps(state) { const { dialOutAuthUrl, + enableFeaturesBasedOnToken, peopleSearchQueryTypes, peopleSearchUrl } = state['features/base/config']; + let footerTextEnabled = false; + + if (enableFeaturesBasedOnToken) { + const { features = {} } = getLocalParticipant(state); + + if (String(features['outbound-call']) !== 'true') { + footerTextEnabled = true; + } + } return { _dialOutAuthUrl: dialOutAuthUrl, + _footerTextEnabled: footerTextEnabled, _jwt: state['features/base/jwt'].jwt, _peopleSearchQueryTypes: peopleSearchQueryTypes, _peopleSearchUrl: peopleSearchUrl diff --git a/react/features/toolbox/components/web/OverflowMenuItem.js b/react/features/toolbox/components/web/OverflowMenuItem.js index e767f3f16d..ef7af8ca0f 100644 --- a/react/features/toolbox/components/web/OverflowMenuItem.js +++ b/react/features/toolbox/components/web/OverflowMenuItem.js @@ -1,3 +1,4 @@ +import Tooltip from '@atlaskit/tooltip'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; @@ -8,6 +9,16 @@ import React, { Component } from 'react'; * @extends Component */ class OverflowMenuItem extends Component { + /** + * Default values for {@code OverflowMenuItem} component's properties. + * + * @static + */ + static defaultProps = { + tooltipPosition: 'left', + disabled: false + }; + /** * {@code OverflowMenuItem} component's property types. * @@ -20,6 +31,11 @@ class OverflowMenuItem extends Component { */ accessibilityLabel: PropTypes.string, + /** + * Whether menu item is disabled or not. + */ + disabled: PropTypes.bool, + /** * The icon class to use for displaying an icon before the link text. */ @@ -33,7 +49,18 @@ class OverflowMenuItem extends Component { /** * The text to display in the {@code OverflowMenuItem}. */ - text: PropTypes.string + text: PropTypes.string, + + /** + * The text to display in the tooltip. + */ + tooltip: PropTypes.string, + + /** + * From which direction the tooltip should appear, relative to the + * button. + */ + tooltipPosition: PropTypes.string }; /** @@ -43,15 +70,25 @@ class OverflowMenuItem extends Component { * @returns {ReactElement} */ render() { + let className = 'overflow-menu-item'; + + className += this.props.disabled ? ' disabled' : ''; + return (
  • + className = { className } + onClick = { this.props.disabled ? null : this.props.onClick }> - { this.props.text } + { this.props.tooltip + ? + { this.props.text } + + : this.props.text }
  • ); } diff --git a/react/features/toolbox/components/web/OverflowMenuLiveStreamingItem.js b/react/features/toolbox/components/web/OverflowMenuLiveStreamingItem.js index 78b018de84..92a0e8f8ba 100644 --- a/react/features/toolbox/components/web/OverflowMenuLiveStreamingItem.js +++ b/react/features/toolbox/components/web/OverflowMenuLiveStreamingItem.js @@ -1,39 +1,66 @@ -// @flow - +import Tooltip from '@atlaskit/tooltip'; +import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { translate } from '../../../base/i18n'; -/** - * The type of the React {@code Component} props of - * {@link OverflowMenuLiveStreamingItem}. - */ -type Props = { - - /** - * The callback to invoke when {@code OverflowMenuLiveStreamingItem} is - * clicked. - */ - onClick: Function, - - /** - * The current live streaming session, if any. - */ - session: ?Object, - - /** - * Invoked to obtain translated strings. - */ - t: Function -}; - /** * React {@code Component} for starting or stopping a live streaming of the * current conference and displaying the current state of live streaming. * * @extends Component */ -class OverflowMenuLiveStreamingItem extends Component { +class OverflowMenuLiveStreamingItem extends Component { + /** + * Default values for {@code OverflowMenuLiveStreamingItem} + * component's properties. + * + * @static + */ + static defaultProps = { + tooltipPosition: 'left', + disabled: false + }; + + /** + * The type of the React {@code Component} props of + * {@link OverflowMenuLiveStreamingItem}. + */ + static propTypes = { + + /** + * Whether menu item is disabled or not. + */ + disabled: PropTypes.bool, + + /** + * The callback to invoke when {@code OverflowMenuLiveStreamingItem} is + * clicked. + */ + onClick: Function, + + /** + * The current live streaming session, if any. + */ + session: PropTypes.object, + + /** + * Invoked to obtain translated strings. + */ + t: Function, + + /** + * The text to display in the tooltip. + */ + tooltip: PropTypes.string, + + /** + * From which direction the tooltip should appear, relative to the + * button. + */ + tooltipPosition: PropTypes.string + }; + /** * Implements React's {@link Component#render()}. * @@ -41,26 +68,43 @@ class OverflowMenuLiveStreamingItem extends Component { * @returns {ReactElement} */ render() { - const { onClick, session, t } = this.props; + const { disabled, onClick, session, t, tooltip, tooltipPosition } + = this.props; const translationKey = session ? 'dialog.stopLiveStreaming' : 'dialog.startLiveStreaming'; - return ( -
  • - - - + let className = 'overflow-menu-item'; + + className += disabled ? ' disabled' : ''; + + const button = (// eslint-disable-line no-extra-parens + { t(translationKey) } { t('recording.beta') } + + ); + + return ( +
  • + + + + { tooltip + ? + { button } + + : button }
  • ); } diff --git a/react/features/toolbox/components/web/Toolbox.js b/react/features/toolbox/components/web/Toolbox.js index 3b6b097229..ed20bea522 100644 --- a/react/features/toolbox/components/web/Toolbox.js +++ b/react/features/toolbox/components/web/Toolbox.js @@ -13,8 +13,9 @@ import { openDialog } from '../../../base/dialog'; import { translate } from '../../../base/i18n'; import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet'; import { - PARTICIPANT_ROLE, getLocalParticipant, + getParticipants, + isLocalParticipantModerator, participantUpdated } from '../../../base/participants'; import { getLocalVideoTrack, toggleScreensharing } from '../../../base/tracks'; @@ -74,9 +75,10 @@ type Props = { _conference: Object, /** - * Whether or not desktopsharing was explicitly configured to be disabled. + * The tooltip key to use when screensharing is disabled. Or undefined + * if non to be shown and the button to be hidden. */ - _desktopSharingDisabledByConfig: boolean, + _desktopSharingDisabledTooltipKey: boolean, /** * Whether or not screensharing is initialized. @@ -103,6 +105,12 @@ type Props = { */ _feedbackConfigured: boolean, + /** + * The tooltip key to use when file recording is disabled. Or undefined + * if non to be shown and the button to be hidden. + */ + _fileRecordingsDisabledTooltipKey: boolean, + /** * Whether or not the file recording feature is enabled for use. */ @@ -129,6 +137,12 @@ type Props = { */ _isGuest: boolean, + /** + * The tooltip key to use when live streaming is disabled. Or undefined + * if non to be shown and the button to be hidden. + */ + _liveStreamingDisabledTooltipKey: boolean, + /** * Whether or not the live streaming feature is enabled for use. */ @@ -925,17 +939,14 @@ class Toolbox extends Component { */ _renderDesktopSharingButton() { const { - _desktopSharingDisabledByConfig, _desktopSharingEnabled, + _desktopSharingDisabledTooltipKey, _screensharing, t } = this.props; - const disabledTooltipText - = interfaceConfig.DESKTOP_SHARING_BUTTON_DISABLED_TOOLTIP; - const showDisabledTooltip - = disabledTooltipText && _desktopSharingDisabledByConfig; - const visible = _desktopSharingEnabled || showDisabledTooltip; + const visible + = _desktopSharingEnabled || _desktopSharingDisabledTooltipKey; if (!visible) { return null; @@ -944,9 +955,9 @@ class Toolbox extends Component { const classNames = `icon-share-desktop ${ _screensharing ? 'toggled' : ''} ${ _desktopSharingEnabled ? '' : 'disabled'}`; - const tooltip = showDisabledTooltip - ? disabledTooltipText - : t('dialog.shareYourScreen'); + const tooltip = t( + _desktopSharingEnabled + ? 'dialog.shareYourScreen' : _desktopSharingDisabledTooltipKey); return ( { _editingDocument, _etherpadInitialized, _feedbackConfigured, + _fileRecordingsDisabledTooltipKey, _fileRecordingsEnabled, _fullScreen, _isGuest, + _liveStreamingDisabledTooltipKey, _liveStreamingEnabled, _liveStreamingSession, _sharingVideo, @@ -1000,13 +1013,15 @@ class Toolbox extends Component { text = { _fullScreen ? t('toolbar.exitFullScreen') : t('toolbar.enterFullScreen') } />, - _liveStreamingEnabled + (_liveStreamingEnabled || _liveStreamingDisabledTooltipKey) && this._shouldShowButton('livestreaming') && , - _fileRecordingsEnabled + session = { _liveStreamingSession } + tooltip = { t(_liveStreamingDisabledTooltipKey) } />, + (_fileRecordingsEnabled || _fileRecordingsDisabledTooltipKey) && this._shouldShowButton('recording') && this._renderRecordingButton(), this._shouldShowButton('sharedvideo') @@ -1070,7 +1085,11 @@ class Toolbox extends Component { * @returns {ReactElement|null} */ _renderRecordingButton() { - const { _fileRecordingSession, t } = this.props; + const { + _fileRecordingSession, + _fileRecordingsDisabledTooltipKey, + _fileRecordingsEnabled, + t } = this.props; const translationKey = _fileRecordingSession ? 'dialog.stopRecording' @@ -1080,10 +1099,12 @@ class Toolbox extends Component { + text = { t(translationKey) } + tooltip = { t(_fileRecordingsDisabledTooltipKey) } /> ); } @@ -1111,15 +1132,14 @@ class Toolbox extends Component { * @returns {{}} */ function _mapStateToProps(state) { - const { - conference, - desktopSharingEnabled - } = state['features/base/conference']; + const { conference } = state['features/base/conference']; + let { desktopSharingEnabled } = state['features/base/conference']; const { callStatsID, - disableDesktopSharing, + iAmRecorder + } = state['features/base/config']; + let { fileRecordingsEnabled, - iAmRecorder, liveStreamingEnabled } = state['features/base/config']; const sharedVideoStatus = state['features/shared-video'].status; @@ -1133,15 +1153,77 @@ function _mapStateToProps(state) { } = state['features/toolbox']; const localParticipant = getLocalParticipant(state); const localVideo = getLocalVideoTrack(state['features/base/tracks']); - const isModerator = localParticipant.role === PARTICIPANT_ROLE.MODERATOR; const addPeopleEnabled = isAddPeopleEnabled(state); const dialOutEnabled = isDialOutEnabled(state); + let desktopSharingDisabledTooltipKey; + let fileRecordingsDisabledTooltipKey; + let liveStreamingDisabledTooltipKey; + + fileRecordingsEnabled + = isLocalParticipantModerator(state) && fileRecordingsEnabled; + liveStreamingEnabled + = isLocalParticipantModerator(state) && liveStreamingEnabled; + + if (state['features/base/config'].enableFeaturesBasedOnToken) { + // we enable desktop sharing if any participant already have this + // feature enabled + desktopSharingEnabled = getParticipants(state) + .find(({ features = {} }) => + String(features['screen-sharing']) === 'true') !== undefined; + + // we want to show button and tooltip + if (state['features/base/jwt'].isGuest) { + desktopSharingDisabledTooltipKey + = 'dialog.shareYourScreenDisabledForGuest'; + } else { + desktopSharingDisabledTooltipKey + = 'dialog.shareYourScreenDisabled'; + } + + // we enable recording if the local participant have this + // feature enabled + const { features = {} } = localParticipant; + const { isGuest } = state['features/base/jwt']; + + fileRecordingsEnabled + = fileRecordingsEnabled && String(features.recording) === 'true'; + + // if the feature is disabled on purpose, do no show it, no tooltip + if (!fileRecordingsEnabled + && String(features.recording) !== 'disabled') { + // button and tooltip + if (isGuest) { + fileRecordingsDisabledTooltipKey + = 'dialog.recordingDisabledForGuestTooltip'; + } else { + fileRecordingsDisabledTooltipKey + = 'dialog.recordingDisabledTooltip'; + } + } + + liveStreamingEnabled + = liveStreamingEnabled && String(features.livestreaming) === 'true'; + + // if the feature is disabled on purpose, do no show it, no tooltip + if (!liveStreamingEnabled + && String(features.livestreaming) !== 'disabled') { + // button and tooltip + if (isGuest) { + liveStreamingDisabledTooltipKey + = 'dialog.liveStreamingDisabledForGuestTooltip'; + } else { + liveStreamingDisabledTooltipKey + = 'dialog.liveStreamingDisabledTooltip'; + } + } + } + return { _chatOpen: current === 'chat_container', _conference: conference, _desktopSharingEnabled: desktopSharingEnabled, - _desktopSharingDisabledByConfig: disableDesktopSharing, + _desktopSharingDisabledTooltipKey: desktopSharingDisabledTooltipKey, _dialog: Boolean(state['features/base/dialog'].component), _editingDocument: Boolean(state['features/etherpad'].editing), _etherpadInitialized: Boolean(state['features/etherpad'].initialized), @@ -1149,11 +1231,13 @@ function _mapStateToProps(state) { _hideInviteButton: iAmRecorder || (!addPeopleEnabled && !dialOutEnabled), _isGuest: state['features/base/jwt'].isGuest, - _fileRecordingsEnabled: isModerator && fileRecordingsEnabled, + _fileRecordingsDisabledTooltipKey: fileRecordingsDisabledTooltipKey, + _fileRecordingsEnabled: fileRecordingsEnabled, _fileRecordingSession: getActiveSession(state, JitsiRecordingConstants.mode.FILE), _fullScreen: fullScreen, - _liveStreamingEnabled: isModerator && liveStreamingEnabled, + _liveStreamingDisabledTooltipKey: liveStreamingDisabledTooltipKey, + _liveStreamingEnabled: liveStreamingEnabled, _liveStreamingSession: getActiveSession(state, JitsiRecordingConstants.mode.STREAM), _localParticipantID: localParticipant.id, diff --git a/resources/prosody-plugins/mod_filter_iq_jibri.lua b/resources/prosody-plugins/mod_filter_iq_jibri.lua new file mode 100644 index 0000000000..c211de6d30 --- /dev/null +++ b/resources/prosody-plugins/mod_filter_iq_jibri.lua @@ -0,0 +1,26 @@ +local st = require "util.stanza"; +local is_feature_allowed = module:require "util".is_feature_allowed; + +-- filters jibri iq in case of requested from jwt authenticated session that +-- has features in the user context, but without feature for recording +module:hook("pre-iq/full", function(event) + local stanza = event.stanza; + if stanza.name == "iq" then + local jibri = stanza:get_child('jibri', 'http://jitsi.org/protocol/jibri'); + if jibri then + local session = event.origin; + local token = session.auth_token; + + if jibri.attr.action == 'start' + and (token == nil + or not is_feature_allowed(session, + (jibri.attr.recording_mode == 'file' and 'recording' or 'livestreaming')) + ) then + module:log("info", + "Filtering jibri start recording, stanza:%s", tostring(stanza)); + session.send(st.error_reply(stanza, "auth", "forbidden")); + return true; + end + end + end +end); diff --git a/resources/prosody-plugins/mod_filter_iq_rayo.lua b/resources/prosody-plugins/mod_filter_iq_rayo.lua index 7073c29deb..6b69c90c41 100644 --- a/resources/prosody-plugins/mod_filter_iq_rayo.lua +++ b/resources/prosody-plugins/mod_filter_iq_rayo.lua @@ -2,6 +2,7 @@ local st = require "util.stanza"; local token_util = module:require "token/util".new(module); local room_jid_match_rewrite = module:require "util".room_jid_match_rewrite; +local is_feature_allowed = module:require "util".is_feature_allowed; -- no token configuration but required if token_util == nil then @@ -10,6 +11,8 @@ if token_util == nil then end -- filters rayo iq in case of requested from not jwt authenticated sessions +-- or if the session has features in user context and it doesn't mention +-- feature "outbound-call" to be enabled module:hook("pre-iq/full", function(event) local stanza = event.stanza; if stanza.name == "iq" then @@ -31,8 +34,11 @@ module:hook("pre-iq/full", function(event) if token == nil or roomName == nil - or not token_util:verify_room( - session, room_jid_match_rewrite(roomName)) then + or not token_util:verify_room(session, room_jid_match_rewrite(roomName)) + or not is_feature_allowed(session, + (dial.attr.to == 'jitsi_meet_transcribe' and 'transcription' + or 'outbound-call')) + then module:log("info", "Filtering stanza dial, stanza:%s", tostring(stanza)); session.send(st.error_reply(stanza, "auth", "forbidden")); diff --git a/resources/prosody-plugins/token/util.lib.lua b/resources/prosody-plugins/token/util.lib.lua index cf5a0b3d01..f9577687a5 100644 --- a/resources/prosody-plugins/token/util.lib.lua +++ b/resources/prosody-plugins/token/util.lib.lua @@ -235,6 +235,7 @@ end -- session.jitsi_meet_domain - the domain name value from the token -- session.jitsi_meet_context_user - the user details from the token -- session.jitsi_meet_context_group - the group value from the token +-- session.jitsi_meet_context_features - the features value from the token -- @param session the current session -- @return false and error function Util:process_and_verify_token(session) @@ -285,6 +286,11 @@ function Util:process_and_verify_token(session) -- Binds any group details to the session session.jitsi_meet_context_group = claims["context"]["group"]; end + + if claims["context"]["features"] ~= nil then + -- Binds any features details to the session + session.jitsi_meet_context_features = claims["context"]["features"]; + end end return true; else diff --git a/resources/prosody-plugins/util.lib.lua b/resources/prosody-plugins/util.lib.lua index 52600e7a39..9556364379 100644 --- a/resources/prosody-plugins/util.lib.lua +++ b/resources/prosody-plugins/util.lib.lua @@ -133,7 +133,22 @@ function update_presence_identity( "Presence with identity inserted %s", tostring(stanza)) end +-- Utility function to check whether feature is present and enabled. Allow +-- a feature if there are features present in the session(coming from +-- the token) and the value of the feature is true. +-- If features is not present in the token we skip feature detection and allow +-- everything. +function is_feature_allowed(session, feature) + if (session.jitsi_meet_context_features == nil + or session.jitsi_meet_context_features[feature] == "true") then + return true; + else + return false; + end +end + return { + is_feature_allowed = is_feature_allowed; get_room_from_jid = get_room_from_jid; wrap_async_run = wrap_async_run; room_jid_match_rewrite = room_jid_match_rewrite;