Compare commits

...

18 Commits

Author SHA1 Message Date
Andrei Gavrilescu
5314d779ed feat(rtcstats): attach LogCollector storage to rtcstats lifecycle
* attach LogCollector to rtcstats lifecycle

* remove legacy param
2025-04-02 09:07:39 -05:00
Mihaela Dumitru
cf7c39d3e5 fix(recording) disable option to dismiss consent dialog (#15876) 2025-04-02 15:54:27 +03:00
Calin-Teodor
4e4f91f184 fix(base/ui): fallback to og value if not found in allTokens 2025-04-02 15:47:28 +03:00
damencho
972a13f3fe chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1955.0.0+31b27f96...v1963.0.0+403ceed2
2025-04-02 06:17:09 -05:00
Joshua Irmer
2ec3cfcb9e fix(audio-devices): fix index as pos in set
Signed-off-by: Joshua Irmer <irmer@gonicus.de>
2025-04-02 08:14:41 +02:00
damencho
a3d89ea66f fix(visitors): Fixes using table for moderators.
Using other types breaks memory serialization of the room.
2025-04-01 13:35:28 -05:00
damencho
487fd5de1f fix(visitors): Fixes configure script. 2025-04-01 13:35:28 -05:00
damencho
bed94e6698 feat(visitors): Respects moderatorId and option to auto promote all moderators. 2025-03-31 11:49:26 -05:00
damencho
939fdf6c8e feat(visitors): Drops propagating moderator_id to visitor nodes.
It is not needed to propagate it as based on userId and the request of forcePromote we can make decision in main prosody.
2025-03-31 11:49:26 -05:00
damencho
0f5412715a feat(groupchat-polls-permissions): Backend implementation.
Adding UI option which is usable only with allowners module.
2025-03-31 11:49:13 -05:00
damencho
dd8f2f53f3 feat(allowners): Adds a field to metadata which can be checked later by clients. 2025-03-31 11:49:13 -05:00
damencho
92df4bfbbb feat: Backend reports default permissions.
When any of the backend is used 'anonymous', 'jitsi-anonymous', 'internal_hashed', 'internal_plain', 'cyrus' and a participant becomes a moderator, because of external module or because set from jicofo we send to client with the self-presence about becoming moderator a default set of permissions which can be controlled via prosody config.
If using 'token' authentication the above applies only if there is a token and the token does not contain context.features.
2025-03-31 11:49:13 -05:00
damencho
b97798e1ca fix(allowners): Fixes presence optimization when using tenant. 2025-03-31 11:49:13 -05:00
damencho
f488ef1e5c feat(groupchat-polls-permissions): Native implementation. 2025-03-31 11:49:13 -05:00
damencho
c4fcfb8666 feat: Switches by default to using websockets. 2025-03-31 11:48:15 -05:00
damencho
0ccb088c05 feat(prosody): Drops support for 0.11. 2025-03-31 11:48:15 -05:00
damencho
a7f4d9cadd chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1948.0.0+102907eb...v1955.0.0+31b27f96
2025-03-28 13:12:41 -05:00
Calin-Teodor
716c6cc567 fix(mobile/external-api): check types for recording error, initiator, terminator 2025-03-28 17:37:14 +02:00
61 changed files with 591 additions and 1153 deletions

1
_unlock Normal file
View File

@@ -0,0 +1 @@
OK

View File

@@ -1888,6 +1888,16 @@ export default {
}, timeout);
}
);
room.on(JitsiConferenceEvents.PERMISSIONS_RECEIVED, p => {
const localParticipant = getLocalParticipant(APP.store.getState());
APP.store.dispatch(participantUpdated({
id: localParticipant.id,
local: true,
features: p
}));
});
},
/**

View File

@@ -49,7 +49,8 @@ var config = {
bosh: 'https://jitsi-meet.example.com/' + subdir + 'http-bind',
// Websocket URL (XMPP)
// websocket: 'wss://jitsi-meet.example.com/' + subdir + 'xmpp-websocket',
websocket: 'wss://jitsi-meet.example.com/' + subdir + 'xmpp-websocket',
websocketKeepAliveUrl: 'https://jitsi-meet.example.com/' + subdir + '_unlock',
// Whether BOSH should be preferred over WebSocket if both are configured.
// preferBosh: false,

4
debian/control vendored
View File

@@ -34,7 +34,7 @@ Description: Configuration for web serving of Jitsi Meet
Package: jitsi-meet-prosody
Architecture: all
Depends: openssl, prosody (>= 0.11.7) | prosody-trunk | prosody-0.12 | prosody-0.11, lua-sec, lua-basexx, lua-luaossl, lua-cjson, lua-inspect
Depends: openssl, prosody (>= 0.12.0) | prosody-trunk | prosody-0.12 | prosody-13.0, lua-sec, lua-basexx, lua-luaossl, lua-cjson, lua-inspect
Replaces: jitsi-meet-tokens
Description: Prosody configuration for Jitsi Meet
Jitsi Meet is a WebRTC JavaScript application that uses Jitsi
@@ -48,7 +48,7 @@ Description: Prosody configuration for Jitsi Meet
Package: jitsi-meet-tokens
Architecture: all
Depends: ${misc:Depends}, prosody-trunk | prosody-0.11 | prosody-0.12 | prosody (>= 0.11.7), jitsi-meet-prosody
Depends: ${misc:Depends}, prosody-trunk | prosody-0.12 | prosody-13.0 | prosody (>= 0.12.0), jitsi-meet-prosody
Description: Prosody token authentication plugin for Jitsi Meet
Package: jitsi-meet-turnserver

View File

@@ -38,6 +38,11 @@ unlimited_jids = {
"jvb@auth.jitmeet.example.com"
}
-- https://prosody.im/doc/modules/mod_smacks
smacks_max_unacked_stanzas = 5;
smacks_hibernation_time = 60;
smacks_max_old_sessions = 1;
VirtualHost "jitmeet.example.com"
authentication = "jitsi-anonymous" -- do not delete me
-- Properties below are modified by jitsi-meet-tokens package config
@@ -58,6 +63,7 @@ VirtualHost "jitmeet.example.com"
-- we need bosh
modules_enabled = {
"bosh";
"smacks";
"ping"; -- Enable mod_ping
"speakerstats";
"external_services";
@@ -127,6 +133,13 @@ VirtualHost "auth.jitmeet.example.com"
authentication = "internal_hashed"
smacks_hibernation_time = 15;
VirtualHost "recorder.jitmeet.example.com"
modules_enabled = {
"smacks";
}
authentication = "internal_hashed"
smacks_max_old_sessions = 2000;
-- Proxy to jicofo's user JID, so that it doesn't have to register as a component.
Component "focus.jitmeet.example.com" "client_proxy"
target_address = "focusUser@auth.jitmeet.example.com"

View File

@@ -150,6 +150,12 @@ server {
# alias /usr/share/jitsi-meet/load-test/libs/$1;
#}
location = /_unlock {
add_header 'Access-Control-Allow-Origin' '*';
add_header Strict-Transport-Security 'max-age=63072000; includeSubDomains';
add_header "Cache-Control" "no-cache, no-store";
}
location ~ ^/conference-request/v1(\/.*)?$ {
proxy_pass http://127.0.0.1:8888/conference-request/v1$1;
add_header "Cache-Control" "no-cache, no-store";

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

10
package-lock.json generated
View File

@@ -62,7 +62,7 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1948.0.0+102907eb/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1963.0.0+403ceed2/lib-jitsi-meet.tgz",
"lodash-es": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@@ -16891,8 +16891,8 @@
},
"node_modules/lib-jitsi-meet": {
"version": "0.0.0",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1948.0.0+102907eb/lib-jitsi-meet.tgz",
"integrity": "sha512-WJDlU62YbpIfV4x9bOYapy5sUGiz9eKurf/yhm7xK85E8zxuY3W8tQrr2TUS2UohXwkVJS11zHcYJnlHeBEBhQ==",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1963.0.0+403ceed2/lib-jitsi-meet.tgz",
"integrity": "sha512-TJkaGbQ5FTHpAsmDqJhjOeyL2n/Ww7YNEt83m29LsIUfHnoOYhg3y/eLgxUY/put1veDcWE0+w8LkAyf56qvFA==",
"license": "Apache-2.0",
"dependencies": {
"@jitsi/js-utils": "2.2.1",
@@ -37377,8 +37377,8 @@
}
},
"lib-jitsi-meet": {
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1948.0.0+102907eb/lib-jitsi-meet.tgz",
"integrity": "sha512-WJDlU62YbpIfV4x9bOYapy5sUGiz9eKurf/yhm7xK85E8zxuY3W8tQrr2TUS2UohXwkVJS11zHcYJnlHeBEBhQ==",
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1963.0.0+403ceed2/lib-jitsi-meet.tgz",
"integrity": "sha512-TJkaGbQ5FTHpAsmDqJhjOeyL2n/Ww7YNEt83m29LsIUfHnoOYhg3y/eLgxUY/put1veDcWE0+w8LkAyf56qvFA==",
"requires": {
"@jitsi/js-utils": "2.2.1",
"@jitsi/logger": "2.0.2",

View File

@@ -68,7 +68,7 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1948.0.0+102907eb/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1963.0.0+403ceed2/lib-jitsi-meet.tgz",
"lodash-es": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",

View File

@@ -190,10 +190,10 @@ export interface IConfig {
obfuscateRoomName?: boolean;
rtcstatsEnabled?: boolean;
rtcstatsEndpoint?: string;
rtcstatsLogFlushSizeBytes?: number;
rtcstatsPollInterval?: number;
rtcstatsSendSdp?: boolean;
rtcstatsStoreLogs?: boolean;
rtcstatsUseLegacy?: boolean;
scriptURLs?: Array<string>;
watchRTCEnabled?: boolean;
whiteListedEvents?: string[];

View File

@@ -24,7 +24,6 @@ export function _cleanupConfig(config: IConfig) {
delete config.analytics?.rtcstatsEndpoint;
delete config.analytics?.rtcstatsPollInterval;
delete config.analytics?.rtcstatsSendSdp;
delete config.analytics?.rtcstatsUseLegacy;
delete config.analytics?.obfuscateRoomName;
delete config.analytics?.watchRTCEnabled;
delete config.watchRTCConfigParams;

View File

@@ -45,14 +45,12 @@ export function getJwtName(state: IReduxState) {
*
* @param {IReduxState} state - The app state.
* @param {string} feature - The feature we want to check.
* @param {boolean} ifNoToken - Default value if there is no token.
* @param {boolean} ifNotInFeatures - Default value if features prop exists but does not have the {@code feature}.
* @returns {boolean}
*/
export function isJwtFeatureEnabled(
state: IReduxState,
feature: ParticipantFeaturesKey,
ifNoToken: boolean,
ifNotInFeatures: boolean
) {
const { jwt } = state['features/base/jwt'];
@@ -67,14 +65,12 @@ export function isJwtFeatureEnabled(
jwt,
localParticipantFeatures: features,
feature,
ifNoToken,
ifNotInFeatures
});
}
interface IIsJwtFeatureEnabledStatelessParams {
feature: ParticipantFeaturesKey;
ifNoToken: boolean;
ifNotInFeatures: boolean;
jwt?: string;
localParticipantFeatures?: IParticipantFeatures;
@@ -86,7 +82,6 @@ interface IIsJwtFeatureEnabledStatelessParams {
* @param {string | undefined} jwt - The jwt token.
* @param {ILocalParticipant} localParticipantFeatures - The features of the local participant.
* @param {string} feature - The feature we want to check.
* @param {boolean} ifNoToken - Default value if there is no token.
* @param {boolean} ifNotInFeatures - Default value if features is missing
* or prop exists but does not have the {@code feature}.
* @returns {boolean}
@@ -95,18 +90,9 @@ export function isJwtFeatureEnabledStateless({
jwt,
localParticipantFeatures: features,
feature,
ifNoToken,
ifNotInFeatures
}: IIsJwtFeatureEnabledStatelessParams) {
if (!jwt) {
return ifNoToken;
}
if (typeof features === 'undefined') {
return ifNoToken;
}
if (typeof features[feature] === 'undefined') {
if (!jwt || typeof features?.[feature] === 'undefined') {
return ifNotInFeatures;
}

View File

@@ -1,12 +1,13 @@
import { IStore } from '../../app/types';
import JitsiMeetJS from '../../base/lib-jitsi-meet';
import RTCStats from '../../rtcstats/RTCStats';
import { isRTCStatsEnabled } from '../../rtcstats/functions';
/**
* Implements log storage interface from the @jitsi/logger lib.
* Implements log storage interface from the @jitsi/logger lib, as it stands
* now it only sends logs to the rtcstats server in case it is enabled.
*/
export default class JitsiMeetLogStorage {
counter: number;
getState: IStore['getState'];
/**
@@ -15,12 +16,6 @@ export default class JitsiMeetLogStorage {
* @param {Function} getState - The Redux store's {@code getState} method.
*/
constructor(getState: IStore['getState']) {
/**
* Counts each log entry, increases on every batch log entry stored.
*
* @type {number}
*/
this.counter = 1;
/**
* The Redux store's {@code getState} method.
@@ -31,18 +26,14 @@ export default class JitsiMeetLogStorage {
}
/**
* The JitsiMeetLogStorage is ready when the conference has been joined.
* A conference is considered joined when the 'conference' field is defined
* in the base/conference state.
* The JitsiMeetLogStorage is ready we can use the rtcstats trace to send logs
* to the rtcstats server.
*
* @returns {boolean} <tt>true</tt> when this storage is ready or
* <tt>false</tt> otherwise.
*/
isReady() {
const { conference, error: conferenceError } = this.getState()['features/base/conference'];
const { error: connectionError } = this.getState()['features/base/connection'];
return Boolean(conference || conferenceError || connectionError);
return JitsiMeetJS.rtcstats.isTraceAvailable();
}
/**
@@ -55,9 +46,9 @@ export default class JitsiMeetLogStorage {
const config = this.getState()['features/base/config'];
// Saving the logs in RTCStats is a new feature and so there is no prior behavior that needs to be maintained.
// That said, this is still experimental and needs to be rolled out gradually so we want this to be off by
// default.
// RTCStats can run without sending app logs to the server.
// Be mindful that there exists another LogStorage instance withing lib-jitsi-meet,
// that is used to send logs generated there.
return config?.analytics?.rtcstatsStoreLogs && isRTCStatsEnabled(this.getState());
}

View File

@@ -107,9 +107,6 @@ function _conferenceJoined({ getState }: IStore, next: Function, action: AnyActi
const { logCollector } = getState()['features/base/logging'];
if (logCollector && conference === getCurrentConference(getState())) {
// Start the LogCollector's periodic "store logs" task
logCollector.start();
// Make an attempt to flush in case a lot of logs have been cached,
// before the collector was started.
logCollector.flush();
@@ -150,12 +147,21 @@ function _initLogging({ dispatch, getState }: IStore,
// Create the LogCollector and register it as the global log transport. It
// is done early to capture as much logs as possible. Captured logs will be
// cached, before the JitsiMeetLogStorage gets ready (statistics module is
// initialized).
// cached, before the JitsiMeetLogStorage gets ready (RTCStats trace is
// available).
if (!logCollector && !loggingConfig.disableLogCollector) {
const _logCollector = new Logger.LogCollector(new JitsiMeetLogStorage(getState));
const { apiLogLevels, analytics: { rtcstatsLogFlushSizeBytes } = {} } = getState()['features/base/config'];
const { apiLogLevels } = getState()['features/base/config'];
// The smaller the flush size the smaller the chance of losing logs, but
// the more often the logs will be sent to the server, by default the LogCollector
// will set once the logs reach 10KB or 30 seconds have passed since the last flush,
// this means if something happens between that interval and the logs don't get flushed
// they will be lost, for instance the meeting tab is closed, the browser crashes,
// an uncaught exception happens, etc.
// If undefined is passed the default values will be used,
const _logCollector = new Logger.LogCollector(new JitsiMeetLogStorage(getState), {
maxEntryLength: rtcstatsLogFlushSizeBytes
});
if (apiLogLevels && Array.isArray(apiLogLevels) && typeof APP === 'object') {
const transport = buildExternalApiLogTransport(apiLogLevels);
@@ -165,6 +171,9 @@ function _initLogging({ dispatch, getState }: IStore,
}
Logger.addGlobalTransport(_logCollector);
_logCollector.start();
dispatch(setLogCollector(_logCollector));
// The JitsiMeetInMemoryLogStorage can not be accessed on mobile through

View File

@@ -14,7 +14,7 @@ export function createColorTokens(colorMap: Object): any {
return Object.entries(colorMap)
.reduce((result, [ token, value ]: [any, string]) => {
const color = allTokens[value as keyof typeof allTokens];
const color = allTokens[value as keyof typeof allTokens] || value;
return Object.assign(result, { [token]: color });
}, {});

View File

@@ -12,10 +12,20 @@ import Input from '../../../base/ui/components/native/Input';
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
import styles from './styles';
import { isSendGroupChatDisabled } from '../../functions';
interface IProps extends WithTranslation {
/**
* Whether sending group chat messages is disabled.
*/
_isSendGroupChatDisabled: boolean;
/**
* The id of the message recipient, if any.
*/
_privateMessageRecipientId?: string;
/**
* Application's aspect ratio.
*/
@@ -102,7 +112,8 @@ class ChatInputBar extends Component<IProps, IState> {
returnKeyType = 'send'
value = { this.state.message } />
<IconButton
disabled = { !this.state.message }
disabled = { !this.state.message
|| (this.props._isSendGroupChatDisabled && !this.props._privateMessageRecipientId) }
id = { this.props.t('chat.sendButton') }
onPress = { this._onSubmit }
src = { IconSend }
@@ -144,9 +155,19 @@ class ChatInputBar extends Component<IProps, IState> {
* @returns {void}
*/
_onSubmit() {
const {
_isSendGroupChatDisabled,
_privateMessageRecipientId,
onSend
} = this.props;
if (_isSendGroupChatDisabled && !_privateMessageRecipientId) {
return;
}
const message = this.state.message.trim();
message && this.props.onSend(message);
message && onSend(message);
this.setState({
message: '',
showSend: false
@@ -163,8 +184,12 @@ class ChatInputBar extends Component<IProps, IState> {
*/
function _mapStateToProps(state: IReduxState) {
const { aspectRatio } = state['features/base/responsive-ui'];
const { privateMessageRecipient } = state['features/chat'];
const isGroupChatDisabled = isSendGroupChatDisabled(state);
return {
_isSendGroupChatDisabled: isGroupChatDisabled,
_privateMessageRecipientId: privateMessageRecipient?.id,
aspectRatio
};
}

View File

@@ -22,6 +22,9 @@ interface IProps extends WithTranslation {
*/
_areSmileysDisabled: boolean;
/**
* Whether sending group chat messages is disabled.
*/
_isSendGroupChatDisabled: boolean;
/**

View File

@@ -7,6 +7,7 @@ import emojiAsciiAliases from 'react-emoji-render/data/asciiAliases';
import { IReduxState } from '../app/types';
import { getLocalizedDateFormatter } from '../base/i18n/dateUtil';
import i18next from '../base/i18n/i18next';
import { MEET_FEATURES } from '../base/jwt/constants';
import { isJwtFeatureEnabled } from '../base/jwt/functions';
import { getParticipantById } from '../base/participants/functions';
import { escapeRegexp } from '../base/util/helpers';
@@ -206,5 +207,5 @@ export function isSendGroupChatDisabled(state: IReduxState) {
return false;
}
return !isJwtFeatureEnabled(state, 'send-groupchat', false, false);
return !isJwtFeatureEnabled(state, MEET_FEATURES.SEND_GROUPCHAT, false);
}

View File

@@ -256,6 +256,7 @@ MiddlewareRegistry.register(store => next => action => {
lobbyChat: false
}, false, true);
}
break;
}
}

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

@@ -7,9 +7,10 @@ import { getRoomName } from '../base/conference/functions';
import { getInviteURL } from '../base/connection/functions';
import { isIosMobileBrowser } from '../base/environment/utils';
import i18next from '../base/i18n/i18next';
import { MEET_FEATURES } from '../base/jwt/constants';
import { isJwtFeatureEnabled } from '../base/jwt/functions';
import { JitsiRecordingConstants } from '../base/lib-jitsi-meet';
import { getLocalParticipant, isLocalParticipantModerator } from '../base/participants/functions';
import { getLocalParticipant } from '../base/participants/functions';
import { toState } from '../base/redux/functions';
import { doGetJSON } from '../base/util/httpUtils';
import { parseURLParams } from '../base/util/parseURLParams';
@@ -491,10 +492,8 @@ export function isAddPeopleEnabled(state: IReduxState): boolean {
*/
export function isDialOutEnabled(state: IReduxState): boolean {
const { conference } = state['features/base/conference'];
const isModerator = isLocalParticipantModerator(state);
return isJwtFeatureEnabled(state, 'outbound-call', isModerator, false)
&& conference?.isSIPCallingSupported();
return isJwtFeatureEnabled(state, MEET_FEATURES.OUTBOUND_CALL, false) && conference?.isSIPCallingSupported();
}
/**
@@ -505,10 +504,8 @@ export function isDialOutEnabled(state: IReduxState): boolean {
*/
export function isSipInviteEnabled(state: IReduxState): boolean {
const { sipInviteUrl } = state['features/base/config'];
const isModerator = isLocalParticipantModerator(state);
return isJwtFeatureEnabled(state, 'sip-outbound-call', isModerator, false)
&& Boolean(sipInviteUrl);
return isJwtFeatureEnabled(state, MEET_FEATURES.SIP_OUTBOUND_CALL, false) && Boolean(sipInviteUrl);
}
/**

View File

@@ -298,17 +298,20 @@ externalAPIEnabled && MiddlewareRegistry.register(store => next => action => {
timestamp
} = action.sessionData;
const getId = (obj: any) => typeof obj === 'object' ? obj.getId() : obj;
const getError = (err: any) => typeof err === 'object' ? String(err) : err;
sendEvent(
store,
RECORDING_STATUS_CHANGED,
/* data */ {
error,
error: getError(error),
id,
initiator,
initiator: getId(initiator),
liveStreamViewURL,
mode,
status,
terminator,
terminator: getId(terminator),
timestamp
});
break;

View File

@@ -20,7 +20,7 @@ import PollsList from './PollsList';
import { pollsStyles } from './styles';
const PollsPane = (props: AbstractProps) => {
const { createMode, onCreate, setCreateMode, t } = props;
const { createMode, isCreatePollsDisabled, onCreate, setCreateMode, t } = props;
const navigation = useNavigation();
const { isPollsTabFocused } = useSelector((state: IReduxState) => state['features/chat']);
const { nbUnreadPolls } = useSelector((state: IReduxState) => state['features/polls']);
@@ -57,6 +57,7 @@ const PollsPane = (props: AbstractProps) => {
<PollsList setCreateMode = { setCreateMode } />
<Button
accessibilityLabel = 'polls.create.create'
disabled = { isCreatePollsDisabled }
id = { t('polls.create.create') }
labelKey = 'polls.create.create'
onClick = { onCreate }

View File

@@ -1,4 +1,5 @@
import { IReduxState } from '../app/types';
import { MEET_FEATURES } from '../base/jwt/constants';
import { isJwtFeatureEnabled } from '../base/jwt/functions';
import { IAnswerData } from './types';
@@ -77,5 +78,5 @@ export function isCreatePollDisabled(state: IReduxState) {
return false;
}
return !isJwtFeatureEnabled(state, 'create-polls', false, false);
return !isJwtFeatureEnabled(state, MEET_FEATURES.CREATE_POLLS, false);
}

View File

@@ -1,12 +1,9 @@
import { IStore } from '../app/types';
import { getMeetingRegion, getRecordingSharingUrl } from '../base/config/functions';
import { MEET_FEATURES } from '../base/jwt/constants';
import { isJwtFeatureEnabled } from '../base/jwt/functions';
import JitsiMeetJS, { JitsiRecordingConstants } from '../base/lib-jitsi-meet';
import {
getLocalParticipant,
getParticipantDisplayName,
isLocalParticipantModerator
} from '../base/participants/functions';
import { getLocalParticipant, getParticipantDisplayName } from '../base/participants/functions';
import { BUTTON_TYPES } from '../base/ui/constants.any';
import { copyText } from '../base/util/copyText';
import { getVpaasTenant, isVpaasMeeting } from '../jaas/functions';
@@ -435,10 +432,9 @@ export function showStartRecordingNotificationWithCallback(openRecordingDialog:
customActionNameKey: [ 'notify.suggestRecordingAction' ],
customActionHandler: [ () => {
state = getState();
const isModerator = isLocalParticipantModerator(state);
const { recordingService } = state['features/base/config'];
const canBypassDialog = recordingService?.enabled
&& isJwtFeatureEnabled(state, 'recording', isModerator, false);
&& isJwtFeatureEnabled(state, MEET_FEATURES.RECORDING, false);
if (canBypassDialog) {
const options = {

View File

@@ -2,7 +2,6 @@ import { IReduxState } from '../../../app/types';
import { IconSites } from '../../../base/icons/svg';
import { MEET_FEATURES } from '../../../base/jwt/constants';
import { isJwtFeatureEnabled } from '../../../base/jwt/functions';
import { isLocalParticipantModerator } from '../../../base/participants/functions';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { isInBreakoutRoom } from '../../../breakout-rooms/functions';
import { maybeShowPremiumFeatureDialog } from '../../../jaas/actions';
@@ -129,11 +128,10 @@ export function _mapStateToProps(state: IReduxState, ownProps: IProps) {
// If the containing component provides the visible prop, that is one
// above all, but if not, the button should be autonomous and decide on
// its own to be visible or not.
const isModerator = isLocalParticipantModerator(state);
const liveStreaming = getLiveStreaming(state);
visible = isLiveStreamingButtonVisible({
liveStreamingAllowed: isJwtFeatureEnabled(state, 'livestreaming', isModerator, false),
liveStreamingAllowed: isJwtFeatureEnabled(state, MEET_FEATURES.LIVESTREAMING, false),
liveStreamingEnabled: liveStreaming?.enabled,
isInBreakoutRoom: isInBreakoutRoom(state)
});

View File

@@ -6,8 +6,8 @@ import { sendAnalytics } from '../../../analytics/functions';
import { IReduxState, IStore } from '../../../app/types';
import ColorSchemeRegistry from '../../../base/color-scheme/ColorSchemeRegistry';
import { _abstractMapStateToProps } from '../../../base/dialog/functions';
import { MEET_FEATURES } from '../../../base/jwt/constants';
import { isJwtFeatureEnabled } from '../../../base/jwt/functions';
import { isLocalParticipantModerator } from '../../../base/participants/functions';
import { authorizeDropbox, updateDropboxToken } from '../../../dropbox/actions';
import { isVpaasMeeting } from '../../../jaas/functions';
import { canAddTranscriber } from '../../../transcribing/functions';
@@ -414,14 +414,13 @@ class AbstractStartRecordingDialogContent extends Component<IProps, IState> {
export function mapStateToProps(state: IReduxState) {
const { localRecording, recordingService } = state['features/base/config'];
const _localRecordingAvailable = !localRecording?.disable && supportsLocalRecording();
const isModerator = isLocalParticipantModerator(state);
return {
..._abstractMapStateToProps(state),
isVpaas: isVpaasMeeting(state),
_canStartTranscribing: canAddTranscriber(state),
_hideStorageWarning: Boolean(recordingService?.hideStorageWarning),
_renderRecording: isJwtFeatureEnabled(state, 'recording', isModerator, false),
_renderRecording: isJwtFeatureEnabled(state, MEET_FEATURES.RECORDING, false),
_localRecordingAvailable,
_localRecordingEnabled: !localRecording?.disable,
_localRecordingSelfEnabled: !localRecording?.disableSelfRecording,

View File

@@ -23,6 +23,8 @@ export default function RecordingConsentDialog() {
<Dialog
cancel = {{ hidden: true }}
disableBackdropClose = { true }
disableEscape = { true }
hideCloseButton = { true }
ok = {{ translationKey: 'dialog.Understand' }}
onSubmit = { consent }
titleKey = 'dialog.recordingInProgressTitle'>

View File

@@ -2,14 +2,11 @@ import i18next from 'i18next';
import { IReduxState, IStore } from '../app/types';
import { isMobileBrowser } from '../base/environment/utils';
import { MEET_FEATURES } from '../base/jwt/constants';
import { isJwtFeatureEnabled } from '../base/jwt/functions';
import { JitsiRecordingConstants, browser } from '../base/lib-jitsi-meet';
import { getSoundFileSrc } from '../base/media/functions';
import {
getLocalParticipant,
getRemoteParticipants,
isLocalParticipantModerator
} from '../base/participants/functions';
import { getLocalParticipant, getRemoteParticipants } from '../base/participants/functions';
import { registerSound, unregisterSound } from '../base/sounds/actions';
import { isInBreakoutRoom as isInBreakoutRoomF } from '../breakout-rooms/functions';
import { isEnabled as isDropboxEnabled } from '../dropbox/functions';
@@ -203,9 +200,7 @@ export function canStopRecording(state: IReduxState) {
}
if (isCloudRecordingRunning(state) || isRecorderTranscriptionsRunning(state)) {
const isModerator = isLocalParticipantModerator(state);
return isJwtFeatureEnabled(state, 'recording', isModerator, false);
return isJwtFeatureEnabled(state, MEET_FEATURES.RECORDING, false);
}
return false;
@@ -257,7 +252,6 @@ export function getRecordButtonProps(state: IReduxState) {
// If the containing component provides the visible prop, that is one
// above all, but if not, the button should be autonomus and decide on
// its own to be visible or not.
const isModerator = isLocalParticipantModerator(state);
const {
recordingService,
localRecording
@@ -269,7 +263,7 @@ export function getRecordButtonProps(state: IReduxState) {
if (localRecordingEnabled) {
visible = true;
} else if (isJwtFeatureEnabled(state, 'recording', isModerator, false)) {
} else if (isJwtFeatureEnabled(state, MEET_FEATURES.RECORDING, false)) {
visible = recordingEnabled;
}

View File

@@ -1,8 +1,8 @@
import { useSelector } from 'react-redux';
import { IReduxState } from '../app/types';
import { MEET_FEATURES } from '../base/jwt/constants';
import { isJwtFeatureEnabled } from '../base/jwt/functions';
import { isLocalParticipantModerator } from '../base/participants/functions';
import { isInBreakoutRoom } from '../breakout-rooms/functions';
import { getLiveStreaming } from './components/LiveStream/functions';
@@ -45,10 +45,9 @@ export function useRecordingButton() {
*/
export function useLiveStreamingButton() {
const toolbarButtons = useSelector((state: IReduxState) => state['features/toolbox'].toolbarButtons);
const localParticipantIsModerator = useSelector(isLocalParticipantModerator);
const liveStreaming = useSelector(getLiveStreaming);
const liveStreamingAllowed = useSelector((state: IReduxState) =>
isJwtFeatureEnabled(state, 'livestreaming', localParticipantIsModerator, false));
isJwtFeatureEnabled(state, MEET_FEATURES.LIVESTREAMING, false));
const _isInBreakoutRoom = useSelector(isInBreakoutRoom);
if (toolbarButtons?.includes('recording')

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

@@ -29,9 +29,10 @@ interface IProps {
hasError?: boolean;
/**
* Flag indicating if there is a problem with the device.
* Index of the device item used to generate this entry.
* Indexes are 0 based.
*/
index?: number;
index: number;
/**
* Flag indicating the selection state.
@@ -190,7 +191,7 @@ const MicrophoneEntry = ({
return (
<li
aria-checked = { isSelected }
aria-posinset = { index }
aria-posinset = { index + 1 } // Add one to offset the 0 based index.
aria-setsize = { length }
className = { classes.container }
onClick = { onClick }

View File

@@ -25,7 +25,8 @@ interface IProps {
deviceId: string;
/**
* Flag controlling the selection state of the entry.
* Index of the device item used to generate this entry.
* Indexes are 0 based.
*/
index: number;
@@ -139,7 +140,7 @@ const SpeakerEntry = (props: IProps) => {
return (
<li
aria-checked = { isSelected }
aria-posinset = { index }
aria-posinset = { index + 1 } // Add one to offset the 0 based index.
aria-setsize = { length }
className = { classes.container }
onClick = { _onClick }

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

@@ -2,9 +2,9 @@ import { AnyAction } from 'redux';
import { IStore } from '../app/types';
import { ENDPOINT_MESSAGE_RECEIVED } from '../base/conference/actionTypes';
import { MEET_FEATURES } from '../base/jwt/constants';
import { isJwtFeatureEnabled } from '../base/jwt/functions';
import JitsiMeetJS from '../base/lib-jitsi-meet';
import { isLocalParticipantModerator } from '../base/participants/functions';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import { TRANSCRIBER_JOINED } from '../transcribing/actionTypes';
@@ -293,8 +293,7 @@ function _requestingSubtitlesChange(
enabled);
if (enabled && conference?.getTranscriptionStatus() === JitsiMeetJS.constants.transcriptionStatus.OFF) {
const isModerator = isLocalParticipantModerator(state);
const featureAllowed = isJwtFeatureEnabled(getState(), 'transcription', isModerator, false);
const featureAllowed = isJwtFeatureEnabled(getState(), MEET_FEATURES.TRANSCRIPTION, false);
if (featureAllowed) {
conference?.dial(TRANSCRIBER_DIAL_NUMBER)

View File

@@ -94,7 +94,7 @@ export default function Toolbox({
const transcribing = useSelector(isTranscribing);
// Do not convert to selector, it returns new array and will cause re-rendering of toolbox on every action.
const jwtDisabledButtons = getJwtDisabledButtons(transcribing, isModerator, jwt, localParticipant?.features);
const jwtDisabledButtons = getJwtDisabledButtons(transcribing, jwt, localParticipant?.features);
const reactionsButtonEnabled = useSelector(isReactionsButtonEnabled);
const _shouldDisplayReactionsButtons = useSelector(shouldDisplayReactionsButtons);

View File

@@ -27,14 +27,12 @@ export function isAudioMuteButtonDisabled(state: IReduxState) {
* This function is stateless as it returns a new array and may cause re-rendering.
*
* @param {boolean} isTranscribing - Whether there is currently a transcriber in the meeting.
* @param {boolean} isModerator - Whether local participant is moderator.
* @param {string | undefined} jwt - The jwt token.
* @param {ILocalParticipant} localParticipantFeatures - The features of the local participant.
* @returns {string[]} - The disabled by jwt buttons array.
*/
export function getJwtDisabledButtons(
isTranscribing: boolean,
isModerator: boolean,
jwt: string | undefined,
localParticipantFeatures?: IParticipantFeatures) {
const acc = [];
@@ -43,7 +41,6 @@ export function getJwtDisabledButtons(
jwt,
localParticipantFeatures,
feature: 'livestreaming',
ifNoToken: isModerator,
ifNotInFeatures: false
})) {
acc.push('livestreaming');
@@ -53,7 +50,6 @@ export function getJwtDisabledButtons(
jwt,
localParticipantFeatures,
feature: 'transcription',
ifNoToken: isModerator,
ifNotInFeatures: false
})) {
acc.push('closedcaptions');

View File

@@ -73,7 +73,7 @@ export function isAudioSettingsButtonDisabled(state: IReduxState) {
export function isDesktopShareButtonDisabled(state: IReduxState) {
const { muted, unmuteBlocked } = state['features/base/media'].video;
const videoOrShareInProgress = !muted || isScreenMediaShared(state);
const enabledInJwt = isJwtFeatureEnabled(state, MEET_FEATURES.SCREEN_SHARING, true, true);
const enabledInJwt = isJwtFeatureEnabled(state, MEET_FEATURES.SCREEN_SHARING, true);
return !enabledInJwt || (unmuteBlocked && !videoOrShareInProgress);
}

View File

@@ -2,8 +2,8 @@ import i18next from 'i18next';
import { IReduxState } from '../app/types';
import { IConfig } from '../base/config/configType';
import { MEET_FEATURES } from '../base/jwt/constants';
import { isJwtFeatureEnabled } from '../base/jwt/functions';
import { isLocalParticipantModerator } from '../base/participants/functions';
import JITSI_TO_BCP47_MAP from './jitsi-bcp47-map.json';
import logger from './logger';
@@ -77,8 +77,7 @@ export function isRecorderTranscriptionsRunning(state: IReduxState) {
*/
export function canAddTranscriber(state: IReduxState) {
const { transcription } = state['features/base/config'];
const isModerator = isLocalParticipantModerator(state);
const isTranscribingAllowed = isJwtFeatureEnabled(state, 'transcription', isModerator, false);
const isTranscribingAllowed = isJwtFeatureEnabled(state, MEET_FEATURES.TRANSCRIPTION, false);
return Boolean(transcription?.enabled) && isTranscribingAllowed;
}

View File

@@ -14,7 +14,7 @@ echo "Will configure $NUMBER_OF_INSTANCES number of visitor prosodies"
set -e
set -x
. /etc/jitsi/jicofo/config
JICOFO_HOSTNAME=$(echo get jitsi-videobridge/jvb-hostname | sudo debconf-communicate jicofo | cut -d' ' -f2-)
# Configure prosody instances
for (( i=1 ; i<=${NUMBER_OF_INSTANCES} ; i++ ));

View File

@@ -1,232 +0,0 @@
local dt = require "util.datetime";
local base64 = require "util.encodings".base64;
local hashes = require "util.hashes";
local st = require "util.stanza";
local jid = require "util.jid";
local array = require "util.array";
local set = require "util.set";
local default_host = module:get_option_string("external_service_host", module.host);
local default_port = module:get_option_number("external_service_port");
local default_secret = module:get_option_string("external_service_secret");
local default_ttl = module:get_option_number("external_service_ttl", 86400);
local configured_services = module:get_option_array("external_services", {});
local access = module:get_option_set("external_service_access", {});
-- https://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
local function behave_turn_rest_credentials(srv, item, secret)
local ttl = default_ttl;
if type(item.ttl) == "number" then
ttl = item.ttl;
end
local expires = srv.expires or os.time() + ttl;
local username;
if type(item.username) == "string" then
username = string.format("%d:%s", expires, item.username);
else
username = string.format("%d", expires);
end
srv.username = username;
srv.password = base64.encode(hashes.hmac_sha1(secret, srv.username));
end
local algorithms = {
turn = behave_turn_rest_credentials;
}
-- filter config into well-defined service records
local function prepare(item)
if type(item) ~= "table" then
module:log("error", "Service definition is not a table: %q", item);
return nil;
end
local srv = {
type = nil;
transport = nil;
host = default_host;
port = default_port;
username = nil;
password = nil;
restricted = nil;
expires = nil;
};
if type(item.type) == "string" then
srv.type = item.type;
else
module:log("error", "Service missing mandatory 'type' field: %q", item);
return nil;
end
if type(item.transport) == "string" then
srv.transport = item.transport;
end
if type(item.host) == "string" then
srv.host = item.host;
end
if type(item.port) == "number" then
srv.port = item.port;
end
if type(item.username) == "string" then
srv.username = item.username;
end
if type(item.password) == "string" then
srv.password = item.password;
srv.restricted = true;
end
if item.restricted == true then
srv.restricted = true;
end
if type(item.expires) == "number" then
srv.expires = item.expires;
elseif type(item.ttl) == "number" then
srv.expires = os.time() + item.ttl;
end
if (item.secret == true and default_secret) or type(item.secret) == "string" then
local secret_cb = item.credentials_cb or algorithms[item.algorithm] or algorithms[srv.type];
local secret = item.secret;
if secret == true then
secret = default_secret;
end
if secret_cb then
secret_cb(srv, item, secret);
srv.restricted = true;
end
end
return srv;
end
function module.load()
-- Trigger errors on startup
local services = configured_services / prepare;
if #services == 0 then
module:log("warn", "No services configured or all had errors");
end
end
-- Ensure only valid items are added in events
local services_mt = {
__index = getmetatable(array()).__index;
__newindex = function (self, i, v)
rawset(self, i, assert(prepare(v), "Invalid service entry added"));
end;
}
function get_services()
local extras = module:get_host_items("external_service");
local services = ( configured_services + extras ) / prepare;
setmetatable(services, services_mt);
return services;
end
function services_xml(services, name, namespace)
local reply = st.stanza(name or "services", { xmlns = namespace or "urn:xmpp:extdisco:2" });
for _, srv in ipairs(services) do
reply:tag("service", {
type = srv.type;
transport = srv.transport;
host = srv.host;
port = srv.port and string.format("%d", srv.port) or nil;
username = srv.username;
password = srv.password;
expires = srv.expires and dt.datetime(srv.expires) or nil;
restricted = srv.restricted and "1" or nil;
}):up();
end
return reply;
end
local function handle_services(event)
local origin, stanza = event.origin, event.stanza;
local action = stanza.tags[1];
local user_bare = jid.bare(stanza.attr.from);
local user_host = jid.host(user_bare);
if not ((access:empty() and origin.type == "c2s") or access:contains(user_bare) or access:contains(user_host)) then
origin.send(st.error_reply(stanza, "auth", "forbidden"));
return true;
end
local services = get_services();
local requested_type = action.attr.type;
if requested_type then
services:filter(function(item)
return item.type == requested_type;
end);
end
module:fire_event("external_service/services", {
origin = origin;
stanza = stanza;
requested_type = requested_type;
services = services;
});
local reply = st.reply(stanza):add_child(services_xml(services, action.name, action.attr.xmlns));
origin.send(reply);
return true;
end
local function handle_credentials(event)
local origin, stanza = event.origin, event.stanza;
local action = stanza.tags[1];
if origin.type ~= "c2s" then
origin.send(st.error_reply(stanza, "auth", "forbidden", "The 'port' and 'type' attributes are required."));
return true;
end
local services = get_services();
services:filter(function (item)
return item.restricted;
end)
local requested_credentials = set.new();
for service in action:childtags("service") do
if not service.attr.type or not service.attr.host then
origin.send(st.error_reply(stanza, "modify", "bad-request"));
return true;
end
requested_credentials:add(string.format("%s:%s:%d", service.attr.type, service.attr.host,
tonumber(service.attr.port) or 0));
end
module:fire_event("external_service/credentials", {
origin = origin;
stanza = stanza;
requested_credentials = requested_credentials;
services = services;
});
services:filter(function (srv)
local port_key = string.format("%s:%s:%d", srv.type, srv.host, srv.port or 0);
local portless_key = string.format("%s:%s:%d", srv.type, srv.host, 0);
return requested_credentials:contains(port_key) or requested_credentials:contains(portless_key);
end);
local reply = st.reply(stanza):add_child(services_xml(services, action.name, action.attr.xmlns));
origin.send(reply);
return true;
end
-- XEP-0215 v0.7
module:add_feature("urn:xmpp:extdisco:2");
module:hook("iq-get/host/urn:xmpp:extdisco:2:services", handle_services);
module:hook("iq-get/host/urn:xmpp:extdisco:2:credentials", handle_credentials);
-- COMPAT XEP-0215 v0.6
-- Those still on the old version gets to deal with undefined attributes until they upgrade.
module:add_feature("urn:xmpp:extdisco:1");
module:hook("iq-get/host/urn:xmpp:extdisco:1:services", handle_services);
module:hook("iq-get/host/urn:xmpp:extdisco:1:credentials", handle_credentials);

View File

@@ -10,7 +10,6 @@ local room_jid_match_rewrite = util.room_jid_match_rewrite;
local is_feature_allowed = util.is_feature_allowed;
local is_sip_jigasi = util.is_sip_jigasi;
local get_room_from_jid = util.get_room_from_jid;
local is_healthcheck_room = util.is_healthcheck_room;
local process_host_module = util.process_host_module;
local jid_bare = require "util.jid".bare;
@@ -218,49 +217,10 @@ end
module:hook_global('config-reloaded', load_config);
function process_set_affiliation(event)
local actor, affiliation, jid, previous_affiliation, room
= event.actor, event.affiliation, event.jid, event.previous_affiliation, event.room;
local actor_session = sessions[actor];
if is_admin(jid) or is_healthcheck_room(room.jid) or not actor or not previous_affiliation
or not actor_session or not actor_session.jitsi_meet_context_features then
return;
end
local occupant;
for _, o in room:each_occupant() do
if o.bare_jid == jid then
occupant = o;
end
end
if not occupant then
return;
end
local occupant_session = sessions[occupant.jid];
if not occupant_session then
return;
end
if previous_affiliation == 'none' and affiliation == 'owner' then
occupant_session.granted_jitsi_meet_context_features = actor_session.jitsi_meet_context_features;
occupant_session.granted_jitsi_meet_context_user_id = actor_session.jitsi_meet_context_user["id"];
occupant_session.granted_jitsi_meet_context_group_id = actor_session.jitsi_meet_context_group;
elseif previous_affiliation == 'owner' and ( affiliation == 'member' or affiliation == 'none' ) then
occupant_session.granted_jitsi_meet_context_features = nil;
occupant_session.granted_jitsi_meet_context_user_id = nil;
occupant_session.granted_jitsi_meet_context_group_id = nil;
end
end
function process_main_muc_loaded(main_muc, host_module)
module:log('debug', 'Main muc loaded');
main_muc_service = main_muc;
module:log("info", "Hook to muc events on %s", main_muc_component_host);
host_module:hook("muc-pre-set-affiliation", process_set_affiliation);
end
process_host_module(main_muc_component_host, function(host_module, host)

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

@@ -323,6 +323,7 @@ module:hook('muc-broadcast-presence', function (event)
-- a promotion detected let's send it to main prosody
if raiseHand then
local user_id;
local group_id;
local is_moderator;
local session = sessions[occupant.jid];
local identity = session and session.jitsi_meet_context_user;
@@ -338,12 +339,9 @@ module:hook('muc-broadcast-presence', function (event)
-- so we can be auto promoted
if identity and identity.id then
user_id = session.jitsi_meet_context_user.id;
group_id = session.jitsi_meet_context_group;
if room._data.moderator_id then
if room._data.moderator_id == user_id then
is_moderator = true;
end
elseif session.auth_token and auto_promoted_with_token then
if session.auth_token and auto_promoted_with_token then
if not session.jitsi_meet_tenant_mismatch or session.jitsi_web_query_prefix == '' then
-- non-vpaas and having a token is considered a moderator, and if it is not in '/' tenant
-- the tenant from url and token should match
@@ -367,6 +365,7 @@ module:hook('muc-broadcast-presence', function (event)
jid = occupant.jid,
time = raiseHand,
userId = user_id,
groupId = group_id,
forcePromote = is_moderator and 'true' or 'false';
}):up();
@@ -642,7 +641,6 @@ local function iq_from_main_handler(event)
-- if this is update it will either set or remove the password
room:set_password(node.attr.password);
room._data.meetingId = node.attr.meetingId;
room._data.moderator_id = node.attr.moderatorId;
local createdTimestamp = node.attr.createdTimestamp;
room.created_timestamp = createdTimestamp and tonumber(createdTimestamp) or nil;

View File

@@ -0,0 +1,182 @@
-- this is auto loaded by meeting_id
local filters = require 'util.filters';
local jid = require 'util.jid';
local modulemanager = require "prosody.core.modulemanager";
local util = module:require 'util';
local is_admin = util.is_admin;
local get_room_from_jid = util.get_room_from_jid;
local is_healthcheck_room = util.is_healthcheck_room;
local room_jid_match_rewrite = util.room_jid_match_rewrite;
local ends_with = util.ends_with;
local presence_check_status = util.presence_check_status;
local MUC_NS = 'http://jabber.org/protocol/muc';
local muc_domain_prefix = module:get_option_string('muc_mapper_domain_prefix', 'conference');
local muc_domain_base = module:get_option_string('muc_mapper_domain_base');
if not muc_domain_base then
module:log('warn', 'No "muc_domain_base" option set, disabling kick check endpoint.');
return ;
end
-- only the visitor prosody has main_domain setting
local is_visitor_prosody = module:get_option_string('main_domain') ~= nil;
-- load it only on the main muc component as it is loaded by muc_meeting_id which is loaded and for the breakout room muc
if muc_domain_prefix..'.'..muc_domain_base ~= module.host or is_visitor_prosody then
return;
end
local sessions = prosody.full_sessions;
local default_permissions;
local function load_config()
default_permissions = module:get_option('jitsi_default_permissions', {
livestreaming = true;
recording = true;
transcription = true;
['outbound-call'] = true;
['create-polls'] = true;
['send-groupchat'] = true;
flip = true;
});
end
load_config();
function process_set_affiliation(event)
local actor, affiliation, jid, previous_affiliation, room
= event.actor, event.affiliation, event.jid, event.previous_affiliation, event.room;
local actor_session = sessions[actor];
if is_admin(jid) or is_healthcheck_room(room.jid) or not actor or not previous_affiliation
or not actor_session or not actor_session.jitsi_meet_context_features then
return;
end
local occupant;
for _, o in room:each_occupant() do
if o.bare_jid == jid then
occupant = o;
end
end
if not occupant then
return;
end
local occupant_session = sessions[occupant.jid];
if not occupant_session then
return;
end
if previous_affiliation == 'none' and affiliation == 'owner' then
occupant_session.granted_jitsi_meet_context_features = actor_session.jitsi_meet_context_features;
occupant_session.granted_jitsi_meet_context_user_id = actor_session.jitsi_meet_context_user['id'];
occupant_session.granted_jitsi_meet_context_group_id = actor_session.jitsi_meet_context_group;
elseif previous_affiliation == 'owner' and ( affiliation == 'member' or affiliation == 'none' ) then
occupant_session.granted_jitsi_meet_context_features = nil;
occupant_session.granted_jitsi_meet_context_user_id = nil;
occupant_session.granted_jitsi_meet_context_group_id = nil;
-- on revoke
if not session.auth_token then
occupant_session.jitsi_meet_context_features = nil;
end
end
end
-- Detects when sending self-presence because of role change
-- we can end up here because of the following cases:
-- 1. user joins the room and is granted moderator by another moderator or jicofo
-- 2. Some module changes the role of the user by using set_affiliation method
-- In cases where authentication is 'anonymous', 'jitsi-anonymous', 'internal_hashed', 'internal_plain', 'cyrus' we
-- want to send default permissions all to indicate UI that everything is allowed (to not relay on the UI to check
-- is participant moderator or not), to allow finer control over the permissions.
-- In case the authentication is 'token' based we want to send permissions only if the token of the user does not include
-- features in the user.context.
-- In case of allowners we want to send the permissions, no matter of the authentication method.
-- 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)
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') then
return stanza;
end
local bare_to = jid.bare(stanza.attr.to);
if is_admin(bare_to) then
return stanza;
end
local muc_x = stanza:get_child('x', MUC_NS..'#user');
if not muc_x then
return stanza;
end
local room = get_room_from_jid(room_jid_match_rewrite(jid.bare(stanza.attr.from)));
if not room or not room.send_default_permissions_to or is_healthcheck_room(room.jid) then
return stanza;
end
if session.auth_token and session.jitsi_meet_context_features then -- token and features are set so skip
room.send_default_permissions_to[bare_to] = nil;
return stanza;
end
-- we are sending permissions only when becoming a member
local is_moderator = false;
for item in muc_x:childtags('item') do
if item.attr.role == 'moderator' then
is_moderator = true;
break;
end
end
if not is_moderator or not room.send_default_permissions_to[bare_to]
or not presence_check_status(muc_x, '110') then
return stanza;
end
local permissions_to_send = session.granted_jitsi_meet_context_features or default_permissions;
room.send_default_permissions_to[bare_to] = nil;
if not session.granted_jitsi_meet_context_features and not session.jitsi_meet_context_features then
session.jitsi_meet_context_features = {};
end
stanza:tag('permissions', { xmlns='http://jitsi.org/jitmeet' });
for k, v in pairs(permissions_to_send) do
local val = tostring(v);
stanza:tag('p', { name = k, val = val }):up();
if session.jitsi_meet_context_features then
session.jitsi_meet_context_features[k] = val;
end
end
stanza:up();
return stanza;
end
-- we need to indicate that we will send permissions if we need to
module:hook('muc-pre-set-affiliation', function(event)
local jid, room = event.jid, event.room;
if not room.send_default_permissions_to then
room.send_default_permissions_to = {};
end
room.send_default_permissions_to[jid] = true;
end);
module:hook('muc-set-affiliation', process_set_affiliation);
function filter_session(session)
-- domain mapper is filtering on default priority 0
-- allowners is -1 and we need it after that
filters.add_filter(session, 'stanzas/out', filter_stanza, -2);
end
-- enable filtering presences
filters.add_filter_hook(filter_session);

View File

@@ -1,3 +1,4 @@
--- activate under the main muc component
local filters = require 'util.filters';
local jid = require "util.jid";
local jid_bare = require "util.jid".bare;
@@ -8,6 +9,7 @@ local is_admin = util.is_admin;
local is_healthcheck_room = util.is_healthcheck_room;
local is_moderated = util.is_moderated;
local get_room_from_jid = util.get_room_from_jid;
local room_jid_match_rewrite = util.room_jid_match_rewrite;
local presence_check_status = util.presence_check_status;
local MUC_NS = 'http://jabber.org/protocol/muc';
@@ -23,6 +25,14 @@ load_config();
-- have length of 1
local joining_moderator_participants = {};
module:hook("muc-room-created", function(event)
local room = event.room;
if room.jitsiMetadata then
room.jitsiMetadata.allownersEnabled = true;
end
end, -2); -- room_metadata should run before this module on -1
module:hook("muc-occupant-pre-join", function (event)
local room, occupant = event.room, event.occupant;
@@ -83,7 +93,7 @@ function filter_stanza(stanza)
end
-- we want to filter presences only on this host for allowners and skip anything like lobby etc.
local host_from = jid_host(stanza.attr.from);
local host_from = jid_host(room_jid_match_rewrite(stanza.attr.from));
if host_from ~= module.host then
return stanza;
end

View File

@@ -12,6 +12,8 @@ local presence_check_status = main_util.presence_check_status;
local QUEUE_MAX_SIZE = 500;
module:depends("jitsi_permissions");
-- Common module for all logic that can be loaded under the conference muc component.
--
-- This module:

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

@@ -1,683 +0,0 @@
-- XEP-0198: Stream Management for Prosody IM
--
-- Copyright (C) 2010-2015 Matthew Wild
-- Copyright (C) 2010 Waqas Hussain
-- Copyright (C) 2012-2021 Kim Alvefur
-- Copyright (C) 2012 Thijs Alkemade
-- Copyright (C) 2014 Florian Zeitz
-- Copyright (C) 2016-2020 Thilo Molitor
--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
local st = require "util.stanza";
local dep = require "util.dependencies";
local cache = dep.softreq("util.cache"); -- only available in prosody 0.10+
local uuid_generate = require "util.uuid".generate;
local jid = require "util.jid";
local t_remove = table.remove;
local math_min = math.min;
local math_max = math.max;
local os_time = os.time;
local tonumber, tostring = tonumber, tostring;
local add_filter = require "util.filters".add_filter;
local timer = require "util.timer";
local datetime = require "util.datetime";
local xmlns_mam2 = "urn:xmpp:mam:2";
local xmlns_sm2 = "urn:xmpp:sm:2";
local xmlns_sm3 = "urn:xmpp:sm:3";
local xmlns_errors = "urn:ietf:params:xml:ns:xmpp-stanzas";
local xmlns_delay = "urn:xmpp:delay";
local sm2_attr = { xmlns = xmlns_sm2 };
local sm3_attr = { xmlns = xmlns_sm3 };
local resume_timeout = module:get_option_number("smacks_hibernation_time", 600);
local s2s_smacks = module:get_option_boolean("smacks_enabled_s2s", true);
local s2s_resend = module:get_option_boolean("smacks_s2s_resend", false);
local max_unacked_stanzas = module:get_option_number("smacks_max_unacked_stanzas", 0);
local max_inactive_unacked_stanzas = module:get_option_number("smacks_max_inactive_unacked_stanzas", 256);
local delayed_ack_timeout = module:get_option_number("smacks_max_ack_delay", 30);
local max_hibernated_sessions = module:get_option_number("smacks_max_hibernated_sessions", 10);
local max_old_sessions = module:get_option_number("smacks_max_old_sessions", 10);
local core_process_stanza = prosody.core_process_stanza;
local sessionmanager = require"core.sessionmanager";
assert(max_hibernated_sessions > 0, "smacks_max_hibernated_sessions must be greater than 0");
assert(max_old_sessions > 0, "smacks_max_old_sessions must be greater than 0");
local c2s_sessions = module:shared("/*/c2s/sessions");
local function init_session_cache(max_entries, evict_callback)
-- old prosody version < 0.10 (no limiting at all!)
if not cache then
local store = {};
return {
get = function(user, key)
if not user then return nil; end
if not key then return nil; end
return store[key];
end;
set = function(user, key, value)
if not user then return nil; end
if not key then return nil; end
store[key] = value;
end;
};
end
-- use per user limited cache for prosody >= 0.10
local stores = {};
return {
get = function(user, key)
if not user then return nil; end
if not key then return nil; end
if not stores[user] then
stores[user] = cache.new(max_entries, evict_callback);
end
return stores[user]:get(key);
end;
set = function(user, key, value)
if not user then return nil; end
if not key then return nil; end
if not stores[user] then stores[user] = cache.new(max_entries, evict_callback); end
stores[user]:set(key, value);
-- remove empty caches completely
if not stores[user]:count() then stores[user] = nil; end
end;
};
end
local old_session_registry = init_session_cache(max_old_sessions, nil);
local session_registry = init_session_cache(max_hibernated_sessions, function(resumption_token, session)
if session.destroyed then return true; end -- destroyed session can always be removed from cache
session.log("warn", "User has too much hibernated sessions, removing oldest session (token: %s)", resumption_token);
-- store old session's h values on force delete
-- save only actual h value and username/host (for security)
old_session_registry.set(session.username, resumption_token, {
h = session.handled_stanza_count,
username = session.username,
host = session.host
});
return true; -- allow session to be removed from full cache to make room for new one
end);
local function stoppable_timer(delay, callback)
local stopped = false;
local timer = module:add_timer(delay, function (t)
if stopped then return; end
return callback(t);
end);
if timer and timer.stop then return timer; end -- new prosody api includes stop() function
return {
stop = function(self) stopped = true end;
timer;
};
end
local function delayed_ack_function(session, stanza)
-- fire event only if configured to do so and our session is not already hibernated or destroyed
if delayed_ack_timeout > 0 and session.awaiting_ack
and not session.hibernating and not session.destroyed then
session.log("debug", "Firing event 'smacks-ack-delayed', queue = %d",
session.outgoing_stanza_queue and #session.outgoing_stanza_queue or 0);
module:fire_event("smacks-ack-delayed", {origin = session, queue = session.outgoing_stanza_queue, stanza = stanza});
end
session.delayed_ack_timer = nil;
end
local function can_do_smacks(session, advertise_only)
if session.smacks then return false, "unexpected-request", "Stream management is already enabled"; end
local session_type = session.type;
if session.username then
if not(advertise_only) and not(session.resource) then -- Fail unless we're only advertising sm
return false, "unexpected-request", "Client must bind a resource before enabling stream management";
end
return true;
elseif s2s_smacks and (session_type == "s2sin" or session_type == "s2sout") then
return true;
end
return false, "service-unavailable", "Stream management is not available for this stream";
end
module:hook("stream-features",
function (event)
if can_do_smacks(event.origin, true) then
event.features:tag("sm", sm2_attr):tag("optional"):up():up();
event.features:tag("sm", sm3_attr):tag("optional"):up():up();
end
end);
module:hook("s2s-stream-features",
function (event)
if can_do_smacks(event.origin, true) then
event.features:tag("sm", sm2_attr):tag("optional"):up():up();
event.features:tag("sm", sm3_attr):tag("optional"):up():up();
end
end);
local function request_ack_if_needed(session, force, reason, stanza)
local queue = session.outgoing_stanza_queue;
local expected_h = session.last_acknowledged_stanza + #queue;
-- session.log("debug", "*** SMACKS(1) ***: awaiting_ack=%s, hibernating=%s", tostring(session.awaiting_ack), tostring(session.hibernating));
if session.awaiting_ack == nil and not session.hibernating then
local max_unacked = max_unacked_stanzas;
if session.state == "inactive" then
max_unacked = max_inactive_unacked_stanzas;
end
-- this check of last_requested_h prevents ack-loops if missbehaving clients report wrong
-- stanza counts. it is set when an <r> is really sent (e.g. inside timer), preventing any
-- further requests until a higher h-value would be expected.
-- session.log("debug", "*** SMACKS(2) ***: #queue=%s, max_unacked_stanzas=%s, expected_h=%s, last_requested_h=%s", tostring(#queue), tostring(max_unacked_stanzas), tostring(expected_h), tostring(session.last_requested_h));
if (#queue > max_unacked and expected_h ~= session.last_requested_h) or force then
session.log("debug", "Queuing <r> (in a moment) from %s - #queue=%d", reason, #queue);
session.awaiting_ack = false;
session.awaiting_ack_timer = stoppable_timer(1e-06, function ()
-- session.log("debug", "*** SMACKS(3) ***: awaiting_ack=%s, hibernating=%s", tostring(session.awaiting_ack), tostring(session.hibernating));
-- only request ack if needed and our session is not already hibernated or destroyed
if not session.awaiting_ack and not session.hibernating and not session.destroyed then
session.log("debug", "Sending <r> (inside timer, before send) from %s - #queue=%d", reason, #queue);
(session.sends2s or session.send)(st.stanza("r", { xmlns = session.smacks }))
if session.destroyed then return end -- sending something can trigger destruction
session.awaiting_ack = true;
-- expected_h could be lower than this expression e.g. more stanzas added to the queue meanwhile)
session.last_requested_h = session.last_acknowledged_stanza + #queue;
session.log("debug", "Sending <r> (inside timer, after send) from %s - #queue=%d", reason, #queue);
if not session.delayed_ack_timer then
session.delayed_ack_timer = stoppable_timer(delayed_ack_timeout, function()
delayed_ack_function(session, nil); -- we don't know if this is the only new stanza in the queue
end);
end
end
end);
end
end
-- Trigger "smacks-ack-delayed"-event if we added new (ackable) stanzas to the outgoing queue
-- and there isn't already a timer for this event running.
-- If we wouldn't do this, stanzas added to the queue after the first "smacks-ack-delayed"-event
-- would not trigger this event (again).
if #queue > max_unacked_stanzas and session.awaiting_ack and session.delayed_ack_timer == nil then
session.log("debug", "Calling delayed_ack_function directly (still waiting for ack)");
delayed_ack_function(session, stanza); -- this is the only new stanza in the queue --> provide it to other modules
end
end
local function outgoing_stanza_filter(stanza, session)
-- XXX: Normally you wouldn't have to check the xmlns for a stanza as it's
-- supposed to be nil.
-- However, when using mod_smacks with mod_websocket, then mod_websocket's
-- stanzas/out filter can get called before this one and adds the xmlns.
local is_stanza = stanza.attr and
(not stanza.attr.xmlns or stanza.attr.xmlns == 'jabber:client')
and not stanza.name:find":";
if is_stanza and not stanza._cached then
local queue = session.outgoing_stanza_queue;
local cached_stanza = st.clone(stanza);
cached_stanza._cached = true;
if cached_stanza and cached_stanza.name ~= "iq" and cached_stanza:get_child("delay", xmlns_delay) == nil then
cached_stanza = cached_stanza:tag("delay", {
xmlns = xmlns_delay,
from = jid.bare(session.full_jid or session.host),
stamp = datetime.datetime()
});
end
queue[#queue+1] = cached_stanza;
if session.hibernating then
session.log("debug", "hibernating since %s, stanza queued", datetime.datetime(session.hibernating));
module:fire_event("smacks-hibernation-stanza-queued", {origin = session, queue = queue, stanza = cached_stanza});
return nil;
end
request_ack_if_needed(session, false, "outgoing_stanza_filter", stanza);
end
return stanza;
end
local function count_incoming_stanzas(stanza, session)
if not stanza.attr.xmlns then
session.handled_stanza_count = session.handled_stanza_count + 1;
session.log("debug", "Handled %d incoming stanzas", session.handled_stanza_count);
end
return stanza;
end
local function wrap_session_out(session, resume)
if not resume then
session.outgoing_stanza_queue = {};
session.last_acknowledged_stanza = 0;
end
add_filter(session, "stanzas/out", outgoing_stanza_filter, -999);
local session_close = session.close;
function session.close(...)
if session.resumption_token then
session_registry.set(session.username, session.resumption_token, nil);
old_session_registry.set(session.username, session.resumption_token, nil);
session.resumption_token = nil;
end
-- send out last ack as per revision 1.5.2 of XEP-0198
if session.smacks and session.conn then
(session.sends2s or session.send)(st.stanza("a", { xmlns = session.smacks, h = string.format("%d", session.handled_stanza_count) }));
end
return session_close(...);
end
return session;
end
local function wrap_session_in(session, resume)
if not resume then
session.handled_stanza_count = 0;
end
add_filter(session, "stanzas/in", count_incoming_stanzas, 999);
return session;
end
local function wrap_session(session, resume)
wrap_session_out(session, resume);
wrap_session_in(session, resume);
return session;
end
function handle_enable(session, stanza, xmlns_sm)
local ok, err, err_text = can_do_smacks(session);
if not ok then
session.log("warn", "Failed to enable smacks: %s", err_text); -- TODO: XEP doesn't say we can send error text, should it?
(session.sends2s or session.send)(st.stanza("failed", { xmlns = xmlns_sm }):tag(err, { xmlns = xmlns_errors}));
return true;
end
module:log("debug", "Enabling stream management");
session.smacks = xmlns_sm;
wrap_session(session, false);
local resume_token;
local resume = stanza.attr.resume;
if resume == "true" or resume == "1" then
resume_token = uuid_generate();
session_registry.set(session.username, resume_token, session);
session.resumption_token = resume_token;
end
(session.sends2s or session.send)(st.stanza("enabled", { xmlns = xmlns_sm, id = resume_token, resume = resume, max = tostring(resume_timeout) }));
return true;
end
module:hook_stanza(xmlns_sm2, "enable", function (session, stanza) return handle_enable(session, stanza, xmlns_sm2); end, 100);
module:hook_stanza(xmlns_sm3, "enable", function (session, stanza) return handle_enable(session, stanza, xmlns_sm3); end, 100);
module:hook_stanza("http://etherx.jabber.org/streams", "features",
function (session, stanza)
stoppable_timer(1e-6, function ()
if can_do_smacks(session) then
if stanza:get_child("sm", xmlns_sm3) then
session.sends2s(st.stanza("enable", sm3_attr));
session.smacks = xmlns_sm3;
elseif stanza:get_child("sm", xmlns_sm2) then
session.sends2s(st.stanza("enable", sm2_attr));
session.smacks = xmlns_sm2;
else
return;
end
wrap_session_out(session, false);
end
end);
end);
function handle_enabled(session, stanza, xmlns_sm)
module:log("debug", "Enabling stream management");
session.smacks = xmlns_sm;
wrap_session_in(session, false);
-- FIXME Resume?
return true;
end
module:hook_stanza(xmlns_sm2, "enabled", function (session, stanza) return handle_enabled(session, stanza, xmlns_sm2); end, 100);
module:hook_stanza(xmlns_sm3, "enabled", function (session, stanza) return handle_enabled(session, stanza, xmlns_sm3); end, 100);
function handle_r(origin, stanza, xmlns_sm)
if not origin.smacks then
module:log("debug", "Received ack request from non-smack-enabled session");
return;
end
module:log("debug", "Received ack request, acking for %d", origin.handled_stanza_count);
-- Reply with <a>
(origin.sends2s or origin.send)(st.stanza("a", { xmlns = xmlns_sm, h = string.format("%d", origin.handled_stanza_count) }));
-- piggyback our own ack request if needed (see request_ack_if_needed() for explanation of last_requested_h)
local expected_h = origin.last_acknowledged_stanza + #origin.outgoing_stanza_queue;
if #origin.outgoing_stanza_queue > 0 and expected_h ~= origin.last_requested_h then
request_ack_if_needed(origin, true, "piggybacked by handle_r", nil);
end
return true;
end
module:hook_stanza(xmlns_sm2, "r", function (origin, stanza) return handle_r(origin, stanza, xmlns_sm2); end);
module:hook_stanza(xmlns_sm3, "r", function (origin, stanza) return handle_r(origin, stanza, xmlns_sm3); end);
function handle_a(origin, stanza)
if not origin.smacks then return; end
origin.awaiting_ack = nil;
if origin.awaiting_ack_timer then
origin.awaiting_ack_timer:stop();
end
if origin.delayed_ack_timer then
origin.delayed_ack_timer:stop();
origin.delayed_ack_timer = nil;
end
-- Remove handled stanzas from outgoing_stanza_queue
-- origin.log("debug", "ACK: h=%s, last=%s", stanza.attr.h or "", origin.last_acknowledged_stanza or "");
local h = tonumber(stanza.attr.h);
if not h then
origin:close{ condition = "invalid-xml"; text = "Missing or invalid 'h' attribute"; };
return;
end
local handled_stanza_count = h-origin.last_acknowledged_stanza;
local queue = origin.outgoing_stanza_queue;
if handled_stanza_count > #queue then
origin.log("warn", "The client says it handled %d new stanzas, but we only sent %d :)",
handled_stanza_count, #queue);
origin.log("debug", "Client h: %d, our h: %d", tonumber(stanza.attr.h), origin.last_acknowledged_stanza);
for i=1,#queue do
origin.log("debug", "Q item %d: %s", i, tostring(queue[i]));
end
end
for i=1,math_min(handled_stanza_count,#queue) do
local handled_stanza = t_remove(origin.outgoing_stanza_queue, 1);
module:fire_event("delivery/success", { session = origin, stanza = handled_stanza });
end
origin.log("debug", "#queue = %d", #queue);
origin.last_acknowledged_stanza = origin.last_acknowledged_stanza + handled_stanza_count;
request_ack_if_needed(origin, false, "handle_a", nil)
return true;
end
module:hook_stanza(xmlns_sm2, "a", handle_a);
module:hook_stanza(xmlns_sm3, "a", handle_a);
--TODO: Optimise... incoming stanzas should be handled by a per-session
-- function that has a counter as an upvalue (no table indexing for increments,
-- and won't slow non-198 sessions). We can also then remove the .handled flag
-- on stanzas
local function handle_unacked_stanzas(session)
local queue = session.outgoing_stanza_queue;
local error_attr = { type = "cancel" };
if #queue > 0 then
session.outgoing_stanza_queue = {};
for i=1,#queue do
if not module:fire_event("delivery/failure", { session = session, stanza = queue[i] }) then
if queue[i].attr.type ~= "error" then
local reply = st.reply(queue[i]);
if reply.attr.to ~= session.full_jid then
reply.attr.type = "error";
reply:tag("error", error_attr)
:tag("recipient-unavailable", {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"});
core_process_stanza(session, reply);
end
end
end
end
end
end
-- don't send delivery errors for messages which will be delivered by mam later on
-- check if stanza was archived --> this will allow us to send back errors for stanzas not archived
-- because the user configured the server to do so ("no-archive"-setting for one special contact for example)
local function get_stanza_id(stanza, by_jid)
for tag in stanza:childtags("stanza-id", "urn:xmpp:sid:0") do
if tag.attr.by == by_jid then
return tag.attr.id;
end
end
return nil;
end
module:hook("delivery/failure", function(event)
local session, stanza = event.session, event.stanza;
-- Only deal with authenticated (c2s) sessions
if session.username then
if stanza.name == "message" and stanza.attr.xmlns == nil and
( stanza.attr.type == "chat" or ( stanza.attr.type or "normal" ) == "normal" ) then
-- don't store messages in offline store if they are mam results
local mam_result = stanza:get_child("result", xmlns_mam2);
if mam_result ~= nil then
return true; -- stanza already "handled", don't send an error and don't add it to offline storage
end
-- do nothing here for normal messages and don't send out "message delivery errors",
-- because messages are already in MAM at this point (no need to frighten users)
local stanza_id = get_stanza_id(stanza, jid.bare(session.full_jid));
if session.mam_requested and stanza_id ~= nil then
session.log("debug", "mod_smacks delivery/failure returning true for mam-handled stanza: mam-archive-id=%s", tostring(stanza_id));
return true; -- stanza handled, don't send an error
end
-- store message in offline store, if this client does not use mam *and* was the last client online
local sessions = prosody.hosts[module.host].sessions[session.username] and
prosody.hosts[module.host].sessions[session.username].sessions or nil;
if sessions and next(sessions) == session.resource and next(sessions, session.resource) == nil then
local ok = module:fire_event("message/offline/handle", { origin = session, stanza = stanza } );
session.log("debug", "mod_smacks delivery/failuere returning %s for offline-handled stanza", tostring(ok));
return ok; -- if stanza was handled, don't send an error
end
end
end
end);
module:hook("pre-resource-unbind", function (event)
local session, err = event.session, event.error;
if session.smacks then
if not session.resumption_token then
local queue = session.outgoing_stanza_queue;
if #queue > 0 then
session.log("debug", "Destroying session with %d unacked stanzas", #queue);
handle_unacked_stanzas(session);
end
else
session.log("debug", "mod_smacks hibernating session for up to %d seconds", resume_timeout);
local hibernate_time = os_time(); -- Track the time we went into hibernation
session.hibernating = hibernate_time;
local resumption_token = session.resumption_token;
module:fire_event("smacks-hibernation-start", {origin = session, queue = session.outgoing_stanza_queue});
timer.add_task(resume_timeout, function ()
session.log("debug", "mod_smacks hibernation timeout reached...");
-- We need to check the current resumption token for this resource
-- matches the smacks session this timer is for in case it changed
-- (for example, the client may have bound a new resource and
-- started a new smacks session, or not be using smacks)
local curr_session = full_sessions[session.full_jid];
if session.destroyed then
session.log("debug", "The session has already been destroyed");
elseif curr_session and curr_session.resumption_token == resumption_token
-- Check the hibernate time still matches what we think it is,
-- otherwise the session resumed and re-hibernated.
and session.hibernating == hibernate_time then
-- wait longer if the timeout isn't reached because push was enabled for this session
-- session.first_hibernated_push is the starting point for hibernation timeouts of those push enabled clients
-- wait for an additional resume_timeout seconds if no push occurred since hibernation at all
local current_time = os_time();
local timeout_start = math_max(session.hibernating, session.first_hibernated_push or session.hibernating);
if session.push_identifier ~= nil and not session.first_hibernated_push then
session.log("debug", "No push happened since hibernation started, hibernating session for up to %d extra seconds", resume_timeout);
return resume_timeout;
end
if session.push_identifier ~= nil and current_time-timeout_start < resume_timeout then
session.log("debug", "A push happened since hibernation started, hibernating session for up to %d extra seconds", resume_timeout-(current_time-timeout_start));
return resume_timeout-(current_time-timeout_start); -- time left to wait
end
session.log("debug", "Destroying session for hibernating too long");
session_registry.set(session.username, session.resumption_token, nil);
-- save only actual h value and username/host (for security)
old_session_registry.set(session.username, session.resumption_token, {
h = session.handled_stanza_count,
username = session.username,
host = session.host
});
session.resumption_token = nil;
sessionmanager.destroy_session(session);
else
session.log("debug", "Session resumed before hibernation timeout, all is well")
end
end);
return true; -- Postpone destruction for now
end
end
end);
local function handle_s2s_destroyed(event)
local session = event.session;
local queue = session.outgoing_stanza_queue;
if queue and #queue > 0 then
session.log("warn", "Destroying session with %d unacked stanzas", #queue);
if s2s_resend then
for i = 1, #queue do
module:send(queue[i]);
end
session.outgoing_stanza_queue = nil;
else
handle_unacked_stanzas(session);
end
end
end
module:hook("s2sout-destroyed", handle_s2s_destroyed);
module:hook("s2sin-destroyed", handle_s2s_destroyed);
local function get_session_id(session)
return session.id or (tostring(session):match("[a-f0-9]+$"));
end
function handle_resume(session, stanza, xmlns_sm)
if session.full_jid then
session.log("warn", "Tried to resume after resource binding");
session.send(st.stanza("failed", { xmlns = xmlns_sm })
:tag("unexpected-request", { xmlns = xmlns_errors })
);
return true;
end
local id = stanza.attr.previd;
local original_session = session_registry.get(session.username, id);
if not original_session then
session.log("debug", "Tried to resume non-existent session with id %s", id);
local old_session = old_session_registry.get(session.username, id);
if old_session and session.username == old_session.username
and session.host == old_session.host
and old_session.h then
session.send(st.stanza("failed", { xmlns = xmlns_sm, h = string.format("%d", old_session.h) })
:tag("item-not-found", { xmlns = xmlns_errors })
);
else
session.send(st.stanza("failed", { xmlns = xmlns_sm })
:tag("item-not-found", { xmlns = xmlns_errors })
);
end;
elseif session.username == original_session.username
and session.host == original_session.host then
session.log("debug", "mod_smacks resuming existing session %s...", get_session_id(original_session));
original_session.log("debug", "mod_smacks session resumed from %s...", get_session_id(session));
-- TODO: All this should move to sessionmanager (e.g. session:replace(new_session))
if original_session.conn then
original_session.log("debug", "mod_smacks closing an old connection for this session");
local conn = original_session.conn;
c2s_sessions[conn] = nil;
conn:close();
end
local migrated_session_log = session.log;
original_session.ip = session.ip;
original_session.conn = session.conn;
original_session.send = session.send;
original_session.close = session.close;
original_session.filter = session.filter;
original_session.filter.session = original_session;
original_session.filters = session.filters;
original_session.stream = session.stream;
original_session.secure = session.secure;
original_session.hibernating = nil;
session.log = original_session.log;
session.type = original_session.type;
wrap_session(original_session, true);
-- Inform xmppstream of the new session (passed to its callbacks)
original_session.stream:set_session(original_session);
-- Similar for connlisteners
c2s_sessions[session.conn] = original_session;
original_session.send(st.stanza("resumed", { xmlns = xmlns_sm,
h = string.format("%d", original_session.handled_stanza_count), previd = id }));
-- Fake an <a> with the h of the <resume/> from the client
original_session:dispatch_stanza(st.stanza("a", { xmlns = xmlns_sm,
h = stanza.attr.h }));
-- Ok, we need to re-send any stanzas that the client didn't see
-- ...they are what is now left in the outgoing stanza queue
-- We have to use the send of "session" because we don't want to add our resent stanzas
-- to the outgoing queue again
local queue = original_session.outgoing_stanza_queue;
session.log("debug", "resending all unacked stanzas that are still queued after resume, #queue = %d", #queue);
for i=1,#queue do
session.send(queue[i]);
end
session.log("debug", "all stanzas resent, now disabling send() in this migrated session, #queue = %d", #queue);
function session.send(stanza)
migrated_session_log("error", "Tried to send stanza on old session migrated by smacks resume (maybe there is a bug?): %s", tostring(stanza));
return false;
end
module:fire_event("smacks-hibernation-end", {origin = session, resumed = original_session, queue = queue});
request_ack_if_needed(original_session, true, "handle_resume", nil);
else
module:log("warn", "Client %s@%s[%s] tried to resume stream for %s@%s[%s]",
session.username or "?", session.host or "?", session.type,
original_session.username or "?", original_session.host or "?", original_session.type);
session.send(st.stanza("failed", { xmlns = xmlns_sm })
:tag("not-authorized", { xmlns = xmlns_errors }));
end
return true;
end
module:hook_stanza(xmlns_sm2, "resume", function (session, stanza) return handle_resume(session, stanza, xmlns_sm2); end);
module:hook_stanza(xmlns_sm3, "resume", function (session, stanza) return handle_resume(session, stanza, xmlns_sm3); end);
module:hook("csi-client-active", function (event)
if event.origin.smacks then
request_ack_if_needed(event.origin, true, "csi-active", nil);
end
end);
module:hook("csi-flushing", function (event)
if event.session.smacks then
request_ack_if_needed(event.session, true, "csi-active", nil);
end
end);
local function handle_read_timeout(event)
local session = event.session;
if session.smacks then
if session.awaiting_ack then
if session.awaiting_ack_timer then
session.awaiting_ack_timer:stop();
end
if session.delayed_ack_timer then
session.delayed_ack_timer:stop();
session.delayed_ack_timer = nil;
end
return false; -- Kick the session
end
session.log("debug", "Sending <r> (read timeout)");
(session.sends2s or session.send)(st.stanza("r", { xmlns = session.smacks }));
session.awaiting_ack = true;
if not session.delayed_ack_timer then
session.delayed_ack_timer = stoppable_timer(delayed_ack_timeout, function()
delayed_ack_function(session, nil);
end);
end
return true;
end
end
module:hook("s2s-read-timeout", handle_read_timeout);
module:hook("c2s-read-timeout", handle_read_timeout);

View File

@@ -65,7 +65,6 @@ local function send_visitors_iq(conference_service, room, type)
password = type ~= 'disconnect' and room:get_password() or '',
lobby = room._data.lobbyroom and 'true' or 'false',
meetingId = room._data.meetingId,
moderatorId = room._data.moderator_id, -- can be used from external modules to set single moderator for meetings
createdTimestamp = room.created_timestamp and tostring(room.created_timestamp) or nil
});
@@ -314,6 +313,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

View File

@@ -13,6 +13,7 @@ local get_room_from_jid = util.get_room_from_jid;
local get_focus_occupant = util.get_focus_occupant;
local get_room_by_name_and_subdomain = util.get_room_by_name_and_subdomain;
local internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite;
local table_find = util.table_find;
local is_vpaas = util.is_vpaas;
local is_sip_jibri_join = util.is_sip_jibri_join;
local process_host_module = util.process_host_module;
@@ -71,13 +72,31 @@ function send_json_message(to_jid, json_message)
module:send(stanza);
end
local function request_promotion_received(room, from_jid, from_vnode, nick, time, user_id, force_promote)
local function request_promotion_received(room, from_jid, from_vnode, nick, time, user_id, group_id, force_promote_requested)
-- if visitors is enabled for the room
if visitors_promotion_map[room.jid] then
local force_promote = auto_allow_promotion;
if not force_promote and force_promote_requested == 'true' then
-- Let's do the force_promote checks if requested
-- _data.moderator_id can be used from external modules to set single moderator for a meeting
-- or a whole group of moderators
if room._data.moderator_id == user_id or room._data.moderator_id == group_id
-- all moderators are allowed to auto promote, the fact that user_id and force_promote_requested are set
-- means that the user has token and is moderator on visitor node side
or room._data.allModerators
-- can be used by external modules to set multiple moderator ids (table of values)
or table_find(room._data.moderators, user_id)
then
force_promote = true;
end
end
-- only for raise hand, ignore lowering the hand
if time and time > 0 and (
auto_allow_promotion
or force_promote == 'true') then
if time and time > 0 and force_promote then
-- we are in auto-allow mode, let's reply with accept
-- we store where the request is coming from so we can send back the response
local username = new_id():lower();
@@ -280,6 +299,7 @@ local function stanza_handler(event)
display_name,
tonumber(request_promotion.attr.time),
request_promotion.attr.userId,
request_promotion.attr.groupId,
request_promotion.attr.forcePromote
);
end

View File

@@ -1,19 +0,0 @@
# HG changeset patch
# User Matthew Wild <mwild1@gmail.com>
# Date 1579882890 0
# Node ID 37936c72846d77bb4b23c4987ccc9dc8805fe67c
# Parent b9a054ad38e72c0480534c06a7b4397c048d122a
mod_websocket: Fire event on session creation (thanks Aaron van Meerten)
diff -r b9a054ad38e7 -r 37936c72846d plugins/mod_websocket.lua
--- a/plugins/mod_websocket.lua Thu Jan 23 21:59:13 2020 +0000
+++ b/plugins/mod_websocket.lua Fri Jan 24 16:21:30 2020 +0000
@@ -305,6 +305,8 @@
response.headers.sec_webSocket_accept = base64(sha1(request.headers.sec_websocket_key .. "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"));
response.headers.sec_webSocket_protocol = "xmpp";
+ module:fire_event("websocket-session", { session = session, request = request });
+
session.log("debug", "Sending WebSocket handshake");
return "";

View File

@@ -1,21 +0,0 @@
--- muc.lib.lua 2016-10-26 18:26:53.432377291 +0000
+++ muc.lib.lua 2016-10-26 18:41:40.754426072 +0000
@@ -1256,15 +1256,16 @@
if actor == true then
actor = nil -- So we can pass it safely to 'publicise_occupant_status' below
else
+ local actor_affiliation = self:get_affiliation(actor);
+
-- Can't do anything to other owners or admins
local occupant_affiliation = self:get_affiliation(occupant.bare_jid);
- if occupant_affiliation == "owner" or occupant_affiliation == "admin" then
+ if (occupant_affiliation == "owner" and actor_affiliation ~= "owner") or (occupant_affiliation == "admin" and actor_affiliation ~= "admin" and actor_affiliation ~= "owner") then
return nil, "cancel", "not-allowed";
end
-- If you are trying to give or take moderator role you need to be an owner or admin
if occupant.role == "moderator" or role == "moderator" then
- local actor_affiliation = self:get_affiliation(actor);
if actor_affiliation ~= "owner" and actor_affiliation ~= "admin" then
return nil, "cancel", "not-allowed";
end

View File

@@ -1,11 +0,0 @@
diff -r 214a679823e8 core/features.lua
--- a/core/features.lua Mon May 01 15:10:32 2023 +0200
+++ b/core/features.lua Wed May 24 11:53:34 2023 -0500
@@ -4,5 +4,7 @@
available = set.new{
-- mod_bookmarks bundled
"mod_bookmarks";
+
+ "s2sout-pre-connect-event";
};
};

View File

@@ -1,14 +0,0 @@
diff -r 214a679823e8 plugins/mod_s2s.lua
--- a/mod_s2s.lua Mon May 01 15:10:32 2023 +0200
+++ b/mod_s2s.lua Wed May 24 11:53:34 2023 -0500
@@ -230,6 +230,10 @@
resolver;
});
end
+
+ local pre_event = { session = host_session; resolver = resolver };
+ module:context(from_host):fire_event("s2sout-pre-connect", pre_event);
+ resolver = pre_event.resolver;
connect(resolver, listener, nil, { session = host_session });
m_initiated_connections:with_labels(from_host):add(1)
return true;

View File

@@ -132,11 +132,7 @@ function get_room_from_jid(room_jid)
local component = hosts[host];
if component then
local muc = component.modules.muc
if muc and rawget(muc,"rooms") then
-- We're running 0.9.x or 0.10 (old MUC API)
return muc.rooms[room_jid];
elseif muc and rawget(muc,"get_room_from_jid") then
-- We're running >0.10 (new MUC API)
if muc then
return muc.get_room_from_jid(room_jid);
else
return
@@ -595,6 +591,19 @@ function table_shallow_copy(t)
return t2
end
local function table_find(tab, val)
if not tab then
return nil
end
for i, v in ipairs(tab) do
if v == val then
return i
end
end
return nil
end
-- Splits a string using delimiter
function split_string(str, delimiter)
str = str .. delimiter;
@@ -676,4 +685,5 @@ return {
starts_with = starts_with;
starts_with_one_of = starts_with_one_of;
table_shallow_copy = table_shallow_copy;
table_find = table_find;
};