feat(groupchat-polls-permissions): Backend implementation.

Adding UI option which is usable only with allowners module.
This commit is contained in:
damencho
2025-03-27 13:29:26 -05:00
committed by Дамян Минков
parent dd8f2f53f3
commit 0f5412715a
13 changed files with 215 additions and 7 deletions

View File

@@ -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.",

View File

@@ -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;

View File

@@ -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);
}
}
}

View File

@@ -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
});
}
};
}

View File

@@ -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) {

View File

@@ -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>
);
}

View File

@@ -252,6 +252,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
return {
...newProps,
chatWithPermissionsEnabled: tabState?.chatWithPermissionsEnabled,
followMeEnabled: tabState?.followMeEnabled,
followMeRecorderEnabled: tabState?.followMeRecorderEnabled,
startAudioMuted: tabState?.startAudioMuted,

View File

@@ -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.

View File

@@ -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),

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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