mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2025-12-30 03:12:29 +00:00
feat(groupchat-polls-permissions): Backend implementation.
Adding UI option which is usable only with allowners module.
This commit is contained in:
@@ -1108,6 +1108,7 @@
|
||||
"signedIn": "Currently accessing calendar events for {{email}}. Click the Disconnect button below to stop accessing calendar events.",
|
||||
"title": "Calendar"
|
||||
},
|
||||
"chatWithPermissions": "Chat requires permission",
|
||||
"desktopShareFramerate": "Desktop sharing frame rate",
|
||||
"desktopShareHighFpsWarning": "A higher frame rate for desktop sharing might affect your bandwidth. You need to restart the screen share for the new settings to take effect.",
|
||||
"desktopShareWarning": "You need to restart the screen share for the new settings to take effect.",
|
||||
|
||||
@@ -15,8 +15,10 @@ import {
|
||||
SET_PRIVATE_MESSAGE_RECIPIENT
|
||||
} from './actionTypes';
|
||||
import { IMessage } from './types';
|
||||
import { UPDATE_CONFERENCE_METADATA } from '../base/conference/actionTypes';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
groupChatWithPermissions: false,
|
||||
isOpen: false,
|
||||
isPollsTabFocused: false,
|
||||
lastReadMessage: undefined,
|
||||
@@ -29,6 +31,7 @@ const DEFAULT_STATE = {
|
||||
};
|
||||
|
||||
export interface IChatState {
|
||||
groupChatWithPermissions: boolean;
|
||||
isLobbyChatActive: boolean;
|
||||
isOpen: boolean;
|
||||
isPollsTabFocused: boolean;
|
||||
@@ -203,6 +206,16 @@ ReducerRegistry.register<IChatState>('features/chat', (state = DEFAULT_STATE, ac
|
||||
isLobbyChatActive: false,
|
||||
lobbyMessageRecipient: undefined
|
||||
};
|
||||
case UPDATE_CONFERENCE_METADATA: {
|
||||
const { metadata } = action;
|
||||
|
||||
if (metadata?.permissions) {
|
||||
return {
|
||||
...state,
|
||||
groupChatWithPermissions: Boolean(metadata.permissions.groupChatRestricted)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
|
||||
import { SET_DYNAMIC_BRANDING_DATA } from './actionTypes';
|
||||
import { SET_DYNAMIC_BRANDING_DATA, SET_DYNAMIC_BRANDING_READY } from './actionTypes';
|
||||
import { fetchCustomIcons } from './functions.any';
|
||||
import logger from './logger';
|
||||
import { getCurrentConference } from '../base/conference/functions';
|
||||
import { getLocalParticipant, isLocalParticipantModerator } from '../base/participants/functions';
|
||||
import { PARTICIPANT_ROLE_CHANGED } from '../base/participants/actionTypes';
|
||||
import { PARTICIPANT_ROLE } from '../base/participants/constants';
|
||||
import { IStateful } from '../base/app/types';
|
||||
import { toState } from '../base/redux/functions';
|
||||
|
||||
MiddlewareRegistry.register(() => next => action => {
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
case SET_DYNAMIC_BRANDING_DATA: {
|
||||
const { customIcons } = action.value;
|
||||
@@ -23,7 +29,75 @@ MiddlewareRegistry.register(() => next => action => {
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case PARTICIPANT_ROLE_CHANGED: {
|
||||
const state = store.getState();
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
|
||||
if (localParticipant?.id !== action.participant.id
|
||||
&& action.participant.role !== PARTICIPANT_ROLE.MODERATOR) {
|
||||
break;
|
||||
}
|
||||
|
||||
maybeUpdatePermissions(state);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case SET_DYNAMIC_BRANDING_READY: {
|
||||
const state = store.getState();
|
||||
|
||||
if (!isLocalParticipantModerator(state)) {
|
||||
break;
|
||||
}
|
||||
|
||||
maybeUpdatePermissions(state);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
|
||||
/**
|
||||
* Updates the permissions metadata for the current conference if the local participant is a moderator.
|
||||
*
|
||||
* @param {Object|Function} stateful - Object or function that can be resolved
|
||||
* to the Redux state.
|
||||
* @returns {void}
|
||||
*/
|
||||
function maybeUpdatePermissions(stateful: IStateful): void {
|
||||
const {
|
||||
groupChatRequiresPermission,
|
||||
pollCreationRequiresPermission
|
||||
} = toState(stateful)['features/dynamic-branding'];
|
||||
|
||||
if (groupChatRequiresPermission || pollCreationRequiresPermission) {
|
||||
const conference = getCurrentConference(stateful);
|
||||
|
||||
if (!conference) {
|
||||
return;
|
||||
}
|
||||
|
||||
const permissions: {
|
||||
groupChatRestricted?: boolean;
|
||||
pollCreationRestricted?: boolean;
|
||||
} = conference.getMetadataHandler().getMetadata().permissions || {};
|
||||
let sendUpdate = false;
|
||||
|
||||
if (groupChatRequiresPermission && !permissions.groupChatRestricted) {
|
||||
permissions.groupChatRestricted = true;
|
||||
sendUpdate = true;
|
||||
}
|
||||
|
||||
if (pollCreationRequiresPermission && !permissions.pollCreationRestricted) {
|
||||
permissions.pollCreationRestricted = true;
|
||||
sendUpdate = true;
|
||||
}
|
||||
|
||||
if (sendUpdate) {
|
||||
conference.getMetadataHandler().setMetadata('permissions', permissions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,6 +189,17 @@ export function submitModeratorTab(newState: any) {
|
||||
dispatch(setStartMutedPolicy(
|
||||
newState.startAudioMuted, newState.startVideoMuted));
|
||||
}
|
||||
|
||||
if (newState.chatWithPermissionsEnabled !== currentState.chatWithPermissionsEnabled) {
|
||||
const { conference } = getState()['features/base/conference'];
|
||||
|
||||
const currentPermissions = conference?.getMetadataHandler().getMetadata().permissions || {};
|
||||
|
||||
conference?.getMetadataHandler().setMetadata('permissions', {
|
||||
...currentPermissions,
|
||||
groupChatRestricted: newState.chatWithPermissionsEnabled
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import FormSection from './FormSection';
|
||||
const ModeratorSection = () => {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
chatWithPermissionsEnabled,
|
||||
followMeActive,
|
||||
followMeEnabled,
|
||||
followMeRecorderActive,
|
||||
@@ -52,6 +53,16 @@ const ModeratorSection = () => {
|
||||
dispatch(updateSettings({ soundsReactions: enabled }));
|
||||
}, [ dispatch, updateSettings, setStartReactionsMuted ]);
|
||||
|
||||
const { conference } = useSelector((state: IReduxState) => state['features/base/conference']);
|
||||
const onChatWithPermissionsToggled = useCallback((enabled?: boolean) => {
|
||||
const currentPermissions = conference?.getMetadataHandler().getMetadata().permissions || {};
|
||||
|
||||
conference?.getMetadataHandler().setMetadata('permissions', {
|
||||
...currentPermissions,
|
||||
groupChatRestricted: enabled
|
||||
});
|
||||
}, [ dispatch, conference ]);
|
||||
|
||||
const followMeRecorderChecked = followMeRecorderEnabled && !followMeRecorderActive;
|
||||
|
||||
const moderationSettings = useMemo(() => {
|
||||
@@ -85,7 +96,12 @@ const ModeratorSection = () => {
|
||||
label: 'settings.startReactionsMuted',
|
||||
state: startReactionsMuted,
|
||||
onChange: onStartReactionsMutedToggled
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'settings.chatWithPermissions',
|
||||
state: chatWithPermissionsEnabled,
|
||||
onChange: onChatWithPermissionsToggled
|
||||
},
|
||||
];
|
||||
|
||||
if (disableReactionsModeration) {
|
||||
|
||||
@@ -14,11 +14,21 @@ import Checkbox from '../../../base/ui/components/web/Checkbox';
|
||||
*/
|
||||
export interface IProps extends AbstractDialogTabProps, WithTranslation {
|
||||
|
||||
/**
|
||||
* Whether the user has selected the chat with permissions feature to be enabled.
|
||||
*/
|
||||
chatWithPermissionsEnabled: boolean;
|
||||
|
||||
/**
|
||||
* CSS classes object.
|
||||
*/
|
||||
classes?: Partial<Record<keyof ReturnType<typeof styles>, string>>;
|
||||
|
||||
/**
|
||||
* Whether to hide chat with permissions.
|
||||
*/
|
||||
disableChatWithPermissions: boolean;
|
||||
|
||||
/**
|
||||
* If set hides the reactions moderation setting.
|
||||
*/
|
||||
@@ -103,6 +113,7 @@ class ModeratorTab extends AbstractDialogTab<IProps, any> {
|
||||
this._onStartReactionsMutedChanged = this._onStartReactionsMutedChanged.bind(this);
|
||||
this._onFollowMeEnabledChanged = this._onFollowMeEnabledChanged.bind(this);
|
||||
this._onFollowMeRecorderEnabledChanged = this._onFollowMeRecorderEnabledChanged.bind(this);
|
||||
this._onChatWithPermissionsChanged = this._onChatWithPermissionsChanged.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -170,6 +181,17 @@ class ModeratorTab extends AbstractDialogTab<IProps, any> {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to select if chat with permissions should be activated.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onChatWithPermissionsChanged({ target: { checked } }: React.ChangeEvent<HTMLInputElement>) {
|
||||
super._onChange({ chatWithPermissionsEnabled: checked });
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
@@ -178,6 +200,8 @@ class ModeratorTab extends AbstractDialogTab<IProps, any> {
|
||||
*/
|
||||
override render() {
|
||||
const {
|
||||
chatWithPermissionsEnabled,
|
||||
disableChatWithPermissions,
|
||||
disableReactionsModeration,
|
||||
followMeActive,
|
||||
followMeEnabled,
|
||||
@@ -232,6 +256,13 @@ class ModeratorTab extends AbstractDialogTab<IProps, any> {
|
||||
label = { t('settings.startReactionsMuted') }
|
||||
name = 'start-reactions-muted'
|
||||
onChange = { this._onStartReactionsMutedChanged } /> }
|
||||
{ !disableChatWithPermissions
|
||||
&& <Checkbox
|
||||
checked = { chatWithPermissionsEnabled }
|
||||
className = { classes.checkbox }
|
||||
label = { t('settings.chatWithPermissions') }
|
||||
name = 'chat-with-permissions'
|
||||
onChange = { this._onChatWithPermissionsChanged } /> }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -252,6 +252,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
|
||||
|
||||
return {
|
||||
...newProps,
|
||||
chatWithPermissionsEnabled: tabState?.chatWithPermissionsEnabled,
|
||||
followMeEnabled: tabState?.followMeEnabled,
|
||||
followMeRecorderEnabled: tabState?.followMeRecorderEnabled,
|
||||
startAudioMuted: tabState?.startAudioMuted,
|
||||
|
||||
@@ -47,7 +47,7 @@ const styles = () => {
|
||||
*/
|
||||
class VirtualBackgroundTab extends AbstractDialogTab<IProps, any> {
|
||||
/**
|
||||
* Initializes a new {@code ModeratorTab} instance.
|
||||
* Initializes a new {@code VirtualBackgroundTab} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
|
||||
@@ -138,14 +138,18 @@ export function getModeratorTabProps(stateful: IStateful) {
|
||||
startVideoMutedPolicy,
|
||||
startReactionsMuted
|
||||
} = state['features/base/conference'];
|
||||
const { groupChatWithPermissions } = state['features/chat'];
|
||||
const { disableReactionsModeration } = state['features/base/config'];
|
||||
const followMeActive = isFollowMeActive(state);
|
||||
const followMeRecorderActive = isFollowMeRecorderActive(state);
|
||||
const showModeratorSettings = shouldShowModeratorSettings(state);
|
||||
const disableChatWithPermissions = !conference?.getMetadataHandler().getMetadata().allownersEnabled;
|
||||
|
||||
// The settings sections to display.
|
||||
return {
|
||||
chatWithPermissionsEnabled: Boolean(groupChatWithPermissions),
|
||||
showModeratorSettings: Boolean(conference && showModeratorSettings),
|
||||
disableChatWithPermissions: Boolean(disableChatWithPermissions),
|
||||
disableReactionsModeration: Boolean(disableReactionsModeration),
|
||||
followMeActive: Boolean(conference && followMeActive),
|
||||
followMeEnabled: Boolean(conference && followMeEnabled),
|
||||
|
||||
42
resources/prosody-plugins/mod_filter_messages.lua
Normal file
42
resources/prosody-plugins/mod_filter_messages.lua
Normal file
@@ -0,0 +1,42 @@
|
||||
-- enable under the main muc module
|
||||
-- a module that will filter group messages based on features (jitsi_meet_context_features)
|
||||
-- when requested via metadata (permissions.groupChatRestricted)
|
||||
local util = module:require 'util';
|
||||
local get_room_from_jid = util.get_room_from_jid;
|
||||
local st = require 'util.stanza';
|
||||
|
||||
local function on_message(event)
|
||||
local stanza = event.stanza;
|
||||
local body = stanza:get_child('body');
|
||||
local session = event.origin;
|
||||
|
||||
if not body or not session then
|
||||
-- we ignore messages without body - lobby, polls ...
|
||||
return;
|
||||
end
|
||||
|
||||
-- get room name with tenant and find room.
|
||||
-- this should already been through domain mapper and this should be the real room jid [tenant]name format
|
||||
local room = get_room_from_jid(stanza.attr.to);
|
||||
if not room then
|
||||
module:log('warn', 'No room found found for %s', stanza.attr.to);
|
||||
return;
|
||||
end
|
||||
|
||||
if room.jitsiMetadata and room.jitsiMetadata.permissions
|
||||
and room.jitsiMetadata.permissions.groupChatRestricted
|
||||
and not is_feature_allowed('send-groupchat', session.jitsi_meet_context_features) then
|
||||
|
||||
local reply = st.error_reply(stanza, 'cancel', 'not-allowed', 'Sending group messages not allowed');
|
||||
if session.type == 's2sin' or session.type == 's2sout' then
|
||||
reply.skipMapping = true;
|
||||
end
|
||||
module:send(reply);
|
||||
|
||||
-- let's filter this message
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
module:hook('message/bare', on_message); -- room messages
|
||||
module:hook('jitsi-visitor-groupchat-pre-route', on_message); -- visitors messages
|
||||
@@ -100,10 +100,13 @@ end
|
||||
-- In case permissions were granted we want to send the granted permissions in all cases except when the user is
|
||||
-- using token that has features pre-defined (authentication is 'token').
|
||||
function filter_stanza(stanza, session)
|
||||
local bare_to = jid.bare(stanza.attr.to);
|
||||
if not stanza.attr or not stanza.attr.to or stanza.name ~= 'presence'
|
||||
or stanza.attr.type == 'unavailable'
|
||||
or ends_with(stanza.attr.from, '/focus') or is_admin(bare_to) then
|
||||
or stanza.attr.type == 'unavailable' or ends_with(stanza.attr.from, '/focus') then
|
||||
return stanza;
|
||||
end
|
||||
|
||||
local bare_to = jid.bare(stanza.attr.to);
|
||||
if is_admin(bare_to) then
|
||||
return stanza;
|
||||
end
|
||||
|
||||
|
||||
@@ -95,6 +95,13 @@ module:hook('jitsi-endpoint-message-received', function(event)
|
||||
return true;
|
||||
end
|
||||
|
||||
if room.jitsiMetadata and room.jitsiMetadata.permissions
|
||||
and room.jitsiMetadata.permissions.pollCreationRestricted
|
||||
and not is_feature_allowed('create-polls', origin.jitsi_meet_context_features) then
|
||||
origin.send(st.error_reply(stanza, 'cancel', 'not-allowed', 'Creation of polls not allowed for user'));
|
||||
return true;
|
||||
end
|
||||
|
||||
local answers = {}
|
||||
local compact_answers = {}
|
||||
for i, name in ipairs(data.answers) do
|
||||
|
||||
@@ -314,6 +314,11 @@ process_host_module(main_muc_component_config, function(host_module, host)
|
||||
return;
|
||||
end
|
||||
|
||||
if host_module:fire_event('jitsi-visitor-groupchat-pre-route', event) then
|
||||
-- message filtered
|
||||
return;
|
||||
end
|
||||
|
||||
-- a message from visitor occupant of known visitor node
|
||||
stanza.attr.from = to;
|
||||
for _, o in room:each_occupant() do
|
||||
|
||||
Reference in New Issue
Block a user