mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-09-10 01:58:40 +00:00
Compare commits
40 Commits
8719
...
join-muc-t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1d52c7f54 | ||
|
|
9714e99c4c | ||
|
|
b8408665f7 | ||
|
|
1acdf1c384 | ||
|
|
890597b2e7 | ||
|
|
100b5f8163 | ||
|
|
788a1c55ed | ||
|
|
b2898455e0 | ||
|
|
c05277c1d6 | ||
|
|
be12cdeeff | ||
|
|
171768faa6 | ||
|
|
b9e8981ecc | ||
|
|
31c17cec2d | ||
|
|
0465a9fdda | ||
|
|
b0491d7d2b | ||
|
|
1659b978a9 | ||
|
|
59dc791362 | ||
|
|
863cbab6b0 | ||
|
|
b050e5f5e8 | ||
|
|
bf8d83953b | ||
|
|
f16bf466eb | ||
|
|
29ea811527 | ||
|
|
435d034fdb | ||
|
|
419baa7ab7 | ||
|
|
9eb7b7bb01 | ||
|
|
19ee989cda | ||
|
|
ab1dcc5375 | ||
|
|
3047b4c8c4 | ||
|
|
2afce3d151 | ||
|
|
1cea9b1786 | ||
|
|
2b7299ae05 | ||
|
|
4b50f13e96 | ||
|
|
c639acebcf | ||
|
|
1a34ed9a2d | ||
|
|
0939e207eb | ||
|
|
8c3ea05ae6 | ||
|
|
daf8a929b1 | ||
|
|
2f3df2c66f | ||
|
|
d8d1f8331e | ||
|
|
0e69336f94 |
@@ -49,6 +49,10 @@ public class JitsiInitializer implements Initializer<Boolean> {
|
||||
// Register activity lifecycle handler for the orientation locker module.
|
||||
((Application) context).registerActivityLifecycleCallbacks(OrientationActivityLifecycle.getInstance());
|
||||
|
||||
// Initialize ReactInstanceManager during application startup
|
||||
// This ensures it's ready before any Activity onCreate is called
|
||||
ReactInstanceManagerHolder.initReactInstanceManager((Application) context);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -102,6 +102,10 @@ public class JitsiMeetActivity extends AppCompatActivity
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// ReactInstanceManager is now initialized by JitsiInitializer during application startup
|
||||
// Just call onHostResume since the manager is already ready
|
||||
JitsiMeetActivityDelegate.onHostResume(this);
|
||||
|
||||
setContentView(R.layout.activity_jitsi_meet);
|
||||
this.jitsiView = findViewById(R.id.jitsiView);
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.jitsi.meet.sdk;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.util.AttributeSet;
|
||||
@@ -196,8 +197,6 @@ public class JitsiMeetView extends FrameLayout {
|
||||
}
|
||||
|
||||
setBackgroundColor(BACKGROUND_COLOR);
|
||||
|
||||
ReactInstanceManagerHolder.initReactInstanceManager((Activity) context);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,6 +18,7 @@ package org.jitsi.meet.sdk;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.Application;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
@@ -207,9 +208,9 @@ class ReactInstanceManagerHolder {
|
||||
* time. All {@code ReactRootView} instances will be tied to the one and
|
||||
* only {@code ReactInstanceManager}.
|
||||
*
|
||||
* @param activity {@code Activity} current running Activity.
|
||||
* @param app {@code Application}
|
||||
*/
|
||||
static void initReactInstanceManager(Activity activity) {
|
||||
static void initReactInstanceManager(Application app) {
|
||||
if (reactInstanceManager != null) {
|
||||
return;
|
||||
}
|
||||
@@ -231,14 +232,14 @@ class ReactInstanceManagerHolder {
|
||||
|
||||
reactInstanceManager
|
||||
= ReactInstanceManager.builder()
|
||||
.setApplication(activity.getApplication())
|
||||
.setCurrentActivity(activity)
|
||||
.setApplication(app)
|
||||
.setCurrentActivity(null)
|
||||
.setBundleAssetName("index.android.bundle")
|
||||
.setJSMainModulePath("index.android")
|
||||
.setJavaScriptExecutorFactory(new HermesExecutorFactory())
|
||||
.addPackages(getReactNativePackages())
|
||||
.setUseDeveloperSupport(BuildConfig.DEBUG)
|
||||
.setInitialLifecycleState(LifecycleState.RESUMED)
|
||||
.setInitialLifecycleState(LifecycleState.BEFORE_CREATE)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,8 @@ import {
|
||||
commonUserJoinedHandling,
|
||||
commonUserLeftHandling,
|
||||
getConferenceOptions,
|
||||
sendLocalParticipant
|
||||
sendLocalParticipant,
|
||||
updateTrackMuteState
|
||||
} from './react/features/base/conference/functions';
|
||||
import { getReplaceParticipant, getSsrcRewritingFeatureFlag } from './react/features/base/config/functions';
|
||||
import { connect } from './react/features/base/connection/actions.web';
|
||||
@@ -1663,8 +1664,12 @@ export default {
|
||||
room.on(
|
||||
JitsiConferenceEvents.START_MUTED_POLICY_CHANGED,
|
||||
({ audio, video }) => {
|
||||
APP.store.dispatch(
|
||||
onStartMutedPolicyChanged(audio, video));
|
||||
APP.store.dispatch(onStartMutedPolicyChanged(audio, video));
|
||||
|
||||
const state = APP.store.getState();
|
||||
|
||||
updateTrackMuteState(state, APP.store.dispatch, true);
|
||||
updateTrackMuteState(state, APP.store.dispatch, false);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -300,6 +300,12 @@
|
||||
"alreadySharedVideoTitle": "Only one shared video is allowed at a time",
|
||||
"applicationWindow": "Application window",
|
||||
"authenticationRequired": "Authentication required",
|
||||
"cameraCaptureDialog": {
|
||||
"description": "Take and send a picture using your mobile camera",
|
||||
"ok": "Open camera",
|
||||
"reject": "Not now",
|
||||
"title": "Take a picture"
|
||||
},
|
||||
"cameraConstraintFailedError": "Your camera does not satisfy some of the required constraints.",
|
||||
"cameraNotFoundError": "Camera was not found.",
|
||||
"cameraNotSendingData": "We are unable to access your camera. Please check if another application is using this device, select another device from the settings menu or try to reload the application.",
|
||||
@@ -375,6 +381,7 @@
|
||||
"micTimeoutError": "Could not start audio source. Timeout occurred!",
|
||||
"micUnknownError": "Cannot use microphone for an unknown reason.",
|
||||
"moderationAudioLabel": "Allow attendees to unmute themselves",
|
||||
"moderationDesktopLabel": "Allow non-moderators to share their screen",
|
||||
"moderationVideoLabel": "Allow non-moderators to start their video",
|
||||
"muteEveryoneDialog": "The participants can unmute themselves at any time.",
|
||||
"muteEveryoneDialogModerationOn": "The participants can send a request to speak at any time.",
|
||||
@@ -387,6 +394,9 @@
|
||||
"muteEveryoneSelf": "yourself",
|
||||
"muteEveryoneStartMuted": "Everyone starts muted from now on",
|
||||
"muteEveryoneTitle": "Mute everyone?",
|
||||
"muteEveryonesDesktopDialog": "The participants can share their screen at any time.",
|
||||
"muteEveryonesDesktopDialogModerationOn": "The participants can send a request to share their screen at any time.",
|
||||
"muteEveryonesDesktopTitle": "Stop everyone's screen share?",
|
||||
"muteEveryonesVideoDialog": "The participants can turn on their video at any time.",
|
||||
"muteEveryonesVideoDialogModerationOn": "The participants can send a request to turn on their video at any time.",
|
||||
"muteEveryonesVideoDialogOk": "Disable",
|
||||
|
||||
@@ -30,6 +30,7 @@ import { overwriteConfig } from '../../react/features/base/config/actions';
|
||||
import { getWhitelistedJSON } from '../../react/features/base/config/functions.any';
|
||||
import { toggleDialog } from '../../react/features/base/dialog/actions';
|
||||
import { isSupportedBrowser } from '../../react/features/base/environment/environment';
|
||||
import { isMobileBrowser } from '../../react/features/base/environment/utils';
|
||||
import { parseJWTFromURLParams } from '../../react/features/base/jwt/functions';
|
||||
import JitsiMeetJS, { JitsiRecordingConstants } from '../../react/features/base/lib-jitsi-meet';
|
||||
import { MEDIA_TYPE, VIDEO_TYPE } from '../../react/features/base/media/constants';
|
||||
@@ -113,7 +114,10 @@ import { RECORDING_METADATA_ID, RECORDING_TYPES } from '../../react/features/rec
|
||||
import { getActiveSession, supportsLocalRecording } from '../../react/features/recording/functions';
|
||||
import { startAudioScreenShareFlow, startScreenShareFlow } from '../../react/features/screen-share/actions';
|
||||
import { isScreenAudioSupported } from '../../react/features/screen-share/functions';
|
||||
import { toggleScreenshotCaptureSummary } from '../../react/features/screenshot-capture/actions';
|
||||
import {
|
||||
openCameraCaptureDialog,
|
||||
toggleScreenshotCaptureSummary
|
||||
} from '../../react/features/screenshot-capture/actions';
|
||||
import { isScreenshotCaptureEnabled } from '../../react/features/screenshot-capture/functions';
|
||||
import SettingsDialog from '../../react/features/settings/components/web/SettingsDialog';
|
||||
import { SETTINGS_TABS } from '../../react/features/settings/constants';
|
||||
@@ -940,6 +944,20 @@ function initCommands() {
|
||||
});
|
||||
});
|
||||
break;
|
||||
case 'capture-camera-picture' : {
|
||||
const { cameraFacingMode, descriptionText, titleText } = request;
|
||||
|
||||
if (!isMobileBrowser()) {
|
||||
logger.error('This feature is only supported on mobile');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
APP.store.dispatch(openCameraCaptureDialog(callback, { cameraFacingMode,
|
||||
descriptionText,
|
||||
titleText }));
|
||||
break;
|
||||
}
|
||||
case 'deployment-info':
|
||||
callback(APP.store.getState()['features/base/config'].deploymentInfo);
|
||||
break;
|
||||
|
||||
21
modules/API/external/external_api.js
vendored
21
modules/API/external/external_api.js
vendored
@@ -820,6 +820,27 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures a picture through OS camera.
|
||||
*
|
||||
* @param {string} cameraFacingMode - The OS camera facing mode (environment/user).
|
||||
* @param {string} descriptionText - The OS camera facing mode (environment/user).
|
||||
* @param {string} titleText - The OS camera facing mode (environment/user).
|
||||
* @returns {Promise<string>} - Resolves with a base64 encoded image data of the screenshot.
|
||||
*/
|
||||
captureCameraPicture(
|
||||
cameraFacingMode,
|
||||
descriptionText,
|
||||
titleText
|
||||
) {
|
||||
return this._transport.sendRequest({
|
||||
name: 'capture-camera-picture',
|
||||
cameraFacingMode,
|
||||
descriptionText,
|
||||
titleText
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the listeners and removes the Jitsi Meet frame.
|
||||
*
|
||||
|
||||
@@ -83,7 +83,8 @@ import {
|
||||
getConferenceState,
|
||||
getCurrentConference,
|
||||
getVisitorOptions,
|
||||
sendLocalParticipant
|
||||
sendLocalParticipant,
|
||||
updateTrackMuteState
|
||||
} from './functions';
|
||||
import logger from './logger';
|
||||
import { IConferenceMetadata, IJitsiConference } from './reducer';
|
||||
@@ -186,6 +187,15 @@ function _addConferenceListeners(conference: IJitsiConference, dispatch: IStore[
|
||||
(disableVideoMuteChange: boolean) => {
|
||||
dispatch(setVideoUnmutePermissions(disableVideoMuteChange));
|
||||
});
|
||||
conference.on(
|
||||
JitsiConferenceEvents.START_MUTED_POLICY_CHANGED,
|
||||
({ audio, video }: { audio: boolean; video: boolean; }) => {
|
||||
dispatch(onStartMutedPolicyChanged(audio, video));
|
||||
|
||||
updateTrackMuteState(state, dispatch, true);
|
||||
updateTrackMuteState(state, dispatch, false);
|
||||
}
|
||||
);
|
||||
|
||||
// Dispatches into features/base/tracks follow:
|
||||
|
||||
@@ -1013,6 +1023,8 @@ export function setStartMutedPolicy(
|
||||
audio: startAudioMuted,
|
||||
video: startVideoMuted
|
||||
});
|
||||
|
||||
dispatch(onStartMutedPolicyChanged(startAudioMuted, startVideoMuted));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -40,3 +40,8 @@ export const CONFERENCE_LEAVE_REASONS = {
|
||||
SWITCH_ROOM: 'switch_room',
|
||||
UNRECOVERABLE_ERROR: 'unrecoverable_error'
|
||||
};
|
||||
|
||||
/**
|
||||
* The ID of the notification that is shown when the user is muted by focus.
|
||||
*/
|
||||
export const START_MUTED_NOTIFICATION_ID = 'start-muted';
|
||||
|
||||
@@ -3,9 +3,13 @@ import { upperFirst, words } from 'lodash-es';
|
||||
|
||||
import { getName } from '../../app/functions';
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { showNotification } from '../../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
|
||||
import { determineTranscriptionLanguage } from '../../transcribing/functions';
|
||||
import { IStateful } from '../app/types';
|
||||
import { JitsiTrackErrors } from '../lib-jitsi-meet';
|
||||
import { setAudioMuted, setVideoMuted } from '../media/actions';
|
||||
import { VIDEO_MUTISM_AUTHORITY } from '../media/constants';
|
||||
import {
|
||||
participantJoined,
|
||||
participantLeft
|
||||
@@ -22,7 +26,8 @@ import { setObfuscatedRoom } from './actions';
|
||||
import {
|
||||
AVATAR_URL_COMMAND,
|
||||
EMAIL_COMMAND,
|
||||
JITSI_CONFERENCE_URL_KEY
|
||||
JITSI_CONFERENCE_URL_KEY,
|
||||
START_MUTED_NOTIFICATION_ID
|
||||
} from './constants';
|
||||
import logger from './logger';
|
||||
import { IJitsiConference } from './reducer';
|
||||
@@ -574,3 +579,42 @@ function safeStartCase(s = '') {
|
||||
(result, word, index) => result + (index ? ' ' : '') + upperFirst(word)
|
||||
, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the mute state of the track based on the start muted policy.
|
||||
*
|
||||
* @param {Object|Function} stateful - Either the whole Redux state object or the Redux store's {@code getState} method.
|
||||
* @param {Function} dispatch - Redux dispatch function.
|
||||
* @param {boolean} isAudio - Whether the track is audio or video.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function updateTrackMuteState(stateful: IStateful, dispatch: IStore['dispatch'], isAudio: boolean) {
|
||||
const state = toState(stateful);
|
||||
const mutedPolicyKey = isAudio ? 'startAudioMutedPolicy' : 'startVideoMutedPolicy';
|
||||
const mutedPolicyValue = state['features/base/conference'][mutedPolicyKey];
|
||||
|
||||
// Currently, the policy only supports force muting others, not unmuting them.
|
||||
if (!mutedPolicyValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
let muteStateUpdated = false;
|
||||
const { muted } = isAudio ? state['features/base/media'].audio : state['features/base/media'].video;
|
||||
|
||||
if (isAudio && !Boolean(muted)) {
|
||||
dispatch(setAudioMuted(mutedPolicyValue, true));
|
||||
muteStateUpdated = true;
|
||||
} else if (!isAudio && !Boolean(muted)) {
|
||||
// TODO: Add a new authority for video mutism for the moderator case.
|
||||
dispatch(setVideoMuted(mutedPolicyValue, VIDEO_MUTISM_AUTHORITY.USER, true));
|
||||
muteStateUpdated = true;
|
||||
}
|
||||
|
||||
if (muteStateUpdated) {
|
||||
dispatch(showNotification({
|
||||
titleKey: 'notify.mutedTitle',
|
||||
descriptionKey: 'notify.muted',
|
||||
uid: START_MUTED_NOTIFICATION_ID // use the same id, to make sure we show one notification
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.SHORT));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,6 @@ import {
|
||||
} from './functions';
|
||||
import logger from './logger';
|
||||
import { IConferenceMetadata } from './reducer';
|
||||
import './subscriber';
|
||||
|
||||
/**
|
||||
* Handler for before unload event.
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import { IStore } from '../../app/types';
|
||||
import { showNotification } from '../../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
|
||||
import { setAudioMuted, setVideoMuted } from '../media/actions';
|
||||
import { VIDEO_MUTISM_AUTHORITY } from '../media/constants';
|
||||
import StateListenerRegistry from '../redux/StateListenerRegistry';
|
||||
|
||||
let hasShownNotification = false;
|
||||
|
||||
/**
|
||||
* Handles changes in the start muted policy for audio and video tracks in the meta data set for the conference.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => state['features/base/conference'].startAudioMutedPolicy,
|
||||
/* listener */ (startAudioMutedPolicy, store) => {
|
||||
_updateTrackMuteState(store, true);
|
||||
});
|
||||
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => state['features/base/conference'].startVideoMutedPolicy,
|
||||
/* listener */(startVideoMutedPolicy, store) => {
|
||||
_updateTrackMuteState(store, false);
|
||||
});
|
||||
|
||||
/**
|
||||
* Updates the mute state of the track based on the start muted policy.
|
||||
*
|
||||
* @param {IStore} store - The redux store.
|
||||
* @param {boolean} isAudio - Whether the track is audio or video.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _updateTrackMuteState(store: IStore, isAudio: boolean) {
|
||||
const { dispatch, getState } = store;
|
||||
const mutedPolicyKey = isAudio ? 'startAudioMutedPolicy' : 'startVideoMutedPolicy';
|
||||
const mutedPolicyValue = getState()['features/base/conference'][mutedPolicyKey];
|
||||
|
||||
// Currently, the policy only supports force muting others, not unmuting them.
|
||||
if (!mutedPolicyValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
let muteStateUpdated = false;
|
||||
const { muted } = isAudio ? getState()['features/base/media'].audio : getState()['features/base/media'].video;
|
||||
|
||||
if (isAudio && !Boolean(muted)) {
|
||||
dispatch(setAudioMuted(mutedPolicyValue, true));
|
||||
muteStateUpdated = true;
|
||||
} else if (!isAudio && !Boolean(muted)) {
|
||||
// TODO: Add a new authority for video mutism for the moderator case.
|
||||
dispatch(setVideoMuted(mutedPolicyValue, VIDEO_MUTISM_AUTHORITY.USER, true));
|
||||
muteStateUpdated = true;
|
||||
}
|
||||
|
||||
if (!hasShownNotification && muteStateUpdated) {
|
||||
hasShownNotification = true;
|
||||
dispatch(showNotification({
|
||||
titleKey: 'notify.mutedTitle',
|
||||
descriptionKey: 'notify.muted'
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.SHORT));
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* @enum {string}
|
||||
*/
|
||||
export const CAMERA_FACING_MODE = {
|
||||
export const CAMERA_FACING_MODE: Record<string, string> = {
|
||||
ENVIRONMENT: 'environment',
|
||||
USER: 'user'
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { FixedSizeList } from 'react-window';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import Icon from '../../../base/icons/components/Icon';
|
||||
import { IconArrowDown, IconArrowUp } from '../../../base/icons/svg';
|
||||
import { withPixelLineHeight } from '../../../base/styles/functions.web';
|
||||
@@ -78,6 +79,7 @@ export default function CurrentVisitorsList({ searchString }: IProps) {
|
||||
const visitors = useSelector(getVisitorsList);
|
||||
const featureEnabled = useSelector(isVisitorsListEnabled);
|
||||
const shouldDisplayList = useSelector(shouldDisplayCurrentVisitorsList);
|
||||
const { defaultRemoteDisplayName } = useSelector((state: IReduxState) => state['features/base/config']);
|
||||
const { t } = useTranslation();
|
||||
const { classes } = useStyles();
|
||||
const dispatch = useDispatch();
|
||||
@@ -109,9 +111,11 @@ export default function CurrentVisitorsList({ searchString }: IProps) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const filtered = visitors.filter(v =>
|
||||
normalizeAccents(v.name).toLowerCase().includes(normalizeAccents(searchString).toLowerCase())
|
||||
);
|
||||
const filtered = visitors.filter(v => {
|
||||
const displayName = v.name || defaultRemoteDisplayName || 'Fellow Jitster';
|
||||
|
||||
return normalizeAccents(displayName).toLowerCase().includes(normalizeAccents(searchString).toLowerCase());
|
||||
});
|
||||
|
||||
// ListItem height is 56px including padding so the item size
|
||||
// for virtualization needs to match it exactly to avoid clipping.
|
||||
@@ -125,7 +129,7 @@ export default function CurrentVisitorsList({ searchString }: IProps) {
|
||||
<ParticipantItem
|
||||
actionsTrigger = { ACTION_TRIGGER.HOVER }
|
||||
audioMediaState = { MEDIA_STATE.NONE }
|
||||
displayName = { v.name }
|
||||
displayName = { v.name || defaultRemoteDisplayName || 'Fellow Jitster' }
|
||||
participantID = { v.id }
|
||||
videoMediaState = { MEDIA_STATE.NONE } />
|
||||
</div>
|
||||
|
||||
3
react/features/polls/logger.ts
Normal file
3
react/features/polls/logger.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { getLogger } from '../base/logging/functions';
|
||||
|
||||
export default getLogger('features/polls');
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
COMMAND_NEW_POLL,
|
||||
COMMAND_OLD_POLLS
|
||||
} from './constants';
|
||||
import logger from './logger';
|
||||
import { IAnswer, IPoll, IPollData } from './types';
|
||||
|
||||
/**
|
||||
@@ -43,7 +44,16 @@ const parsePollData = (pollData: Partial<IPollData>): IPoll | null => {
|
||||
const { id, senderId, question, answers } = pollData;
|
||||
|
||||
if (typeof id !== 'string' || typeof senderId !== 'string'
|
||||
|| typeof question !== 'string' || !(answers instanceof Array)) {
|
||||
|| typeof question !== 'string' || !(answers instanceof Array)) {
|
||||
logger.error('Malformed poll data received:', pollData);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Validate answers.
|
||||
if (answers.some(answer => typeof answer !== 'string')) {
|
||||
logger.error('Malformed answers data received:', answers);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -173,7 +183,7 @@ function _handleReceivePollsMessage(data: any, dispatch: IStore['dispatch'], get
|
||||
const receivedAnswer: IAnswer = {
|
||||
voterId,
|
||||
pollId,
|
||||
answers: answers.slice(0, MAX_ANSWERS)
|
||||
answers: answers.slice(0, MAX_ANSWERS).map(Boolean)
|
||||
};
|
||||
|
||||
dispatch(receiveAnswer(pollId, receivedAnswer));
|
||||
@@ -188,7 +198,7 @@ function _handleReceivePollsMessage(data: any, dispatch: IStore['dispatch'], get
|
||||
const poll = parsePollData(pollData);
|
||||
|
||||
if (poll === null) {
|
||||
console.warn('[features/polls] Invalid old poll data');
|
||||
logger.warn('Malformed old poll data', pollData);
|
||||
} else {
|
||||
dispatch(receivePoll(pollData.id, poll, false));
|
||||
}
|
||||
|
||||
154
react/features/screenshot-capture/CameraCaptureDialog.tsx
Normal file
154
react/features/screenshot-capture/CameraCaptureDialog.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
import { Theme } from '@mui/material';
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect, useDispatch } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { hideDialog } from '../base/dialog/actions';
|
||||
import { translate } from '../base/i18n/functions';
|
||||
import Label from '../base/label/components/web/Label';
|
||||
import { CAMERA_FACING_MODE } from '../base/media/constants';
|
||||
import Button from '../base/ui/components/web/Button';
|
||||
import Dialog from '../base/ui/components/web/Dialog';
|
||||
import { BUTTON_TYPES } from '../base/ui/constants.any';
|
||||
|
||||
import { ICameraCapturePayload } from './actionTypes';
|
||||
|
||||
const useStyles = makeStyles()((theme: Theme) => ({
|
||||
container: {
|
||||
display: 'flex',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(3),
|
||||
textAlign: 'center'
|
||||
},
|
||||
buttonsContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(3),
|
||||
width: '100%',
|
||||
maxWidth: '100%'
|
||||
},
|
||||
|
||||
hidden: {
|
||||
display: 'none'
|
||||
},
|
||||
label: {
|
||||
background: 'transparent',
|
||||
margin: `${theme.spacing(3)} 0 ${theme.spacing(6)}`,
|
||||
},
|
||||
button: {
|
||||
width: '100%',
|
||||
height: '48px',
|
||||
maxWidth: '400px'
|
||||
}
|
||||
}));
|
||||
|
||||
/**
|
||||
* The type of {@link CameraCaptureDialog}'s React {@code Component} props.
|
||||
*/
|
||||
interface IProps extends WithTranslation {
|
||||
/**
|
||||
* Callback function on file input changed.
|
||||
*/
|
||||
callback: ({ error, dataURL }: { dataURL?: string; error?: string; }) => void;
|
||||
|
||||
/**
|
||||
* The camera capture payload.
|
||||
*/
|
||||
componentProps: ICameraCapturePayload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the Camera capture dialog.
|
||||
*
|
||||
* @param {Object} props - The props of the component.
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
const CameraCaptureDialog = ({
|
||||
callback,
|
||||
componentProps,
|
||||
t,
|
||||
}: IProps) => {
|
||||
const { cameraFacingMode = CAMERA_FACING_MODE.ENVIRONMENT,
|
||||
descriptionText,
|
||||
titleText } = componentProps;
|
||||
const dispatch = useDispatch();
|
||||
const { classes } = useStyles();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const onCancel = useCallback(() => {
|
||||
callback({
|
||||
error: 'User canceled!'
|
||||
});
|
||||
dispatch(hideDialog());
|
||||
}, []);
|
||||
|
||||
const onSubmit = useCallback(() => {
|
||||
inputRef.current?.click();
|
||||
}, []);
|
||||
|
||||
const onInputChange = useCallback(event => {
|
||||
const reader = new FileReader();
|
||||
const files = event.target.files;
|
||||
|
||||
if (!files?.[0]) {
|
||||
callback({
|
||||
error: 'No picture selected!'
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
reader.onload = () => {
|
||||
callback({ dataURL: reader.result as string });
|
||||
dispatch(hideDialog());
|
||||
};
|
||||
reader.onerror = () => {
|
||||
callback({ error: 'Failed generating base64 URL!' });
|
||||
dispatch(hideDialog());
|
||||
};
|
||||
|
||||
reader.readAsDataURL(files[0]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
cancel = {{ hidden: true }}
|
||||
disableAutoHideOnSubmit = { true }
|
||||
ok = {{ hidden: true }}
|
||||
onCancel = { onCancel }
|
||||
titleKey = { titleText || t('dialog.cameraCaptureDialog.title') }>
|
||||
<div className = { classes.container }>
|
||||
<Label
|
||||
aria-label = { descriptionText || t('dialog.cameraCaptureDialog.description') }
|
||||
className = { classes.label }
|
||||
text = { descriptionText || t('dialog.cameraCaptureDialog.description') } />
|
||||
<div className = { classes.buttonsContainer } >
|
||||
<Button
|
||||
accessibilityLabel = { t('dialog.cameraCaptureDialog.ok') }
|
||||
className = { classes.button }
|
||||
labelKey = { 'dialog.cameraCaptureDialog.ok' }
|
||||
onClick = { onSubmit } />
|
||||
<Button
|
||||
accessibilityLabel = { t('dialog.cameraCaptureDialog.reject') }
|
||||
className = { classes.button }
|
||||
labelKey = { 'dialog.cameraCaptureDialog.reject' }
|
||||
onClick = { onCancel }
|
||||
type = { BUTTON_TYPES.TERTIARY } />
|
||||
<input
|
||||
accept = 'image/*'
|
||||
capture = { cameraFacingMode }
|
||||
className = { classes.hidden }
|
||||
onChange = { onInputChange }
|
||||
ref = { inputRef }
|
||||
type = 'file' />
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default translate(connect()(CameraCaptureDialog));
|
||||
@@ -7,3 +7,23 @@
|
||||
*/
|
||||
|
||||
export const SET_SCREENSHOT_CAPTURE = 'SET_SCREENSHOT_CAPTURE';
|
||||
|
||||
/**
|
||||
* The camera capture payload.
|
||||
*/
|
||||
export interface ICameraCapturePayload {
|
||||
/**
|
||||
* Selected camera on open.
|
||||
*/
|
||||
cameraFacingMode?: string;
|
||||
|
||||
/**
|
||||
* Custom explanatory text to show.
|
||||
*/
|
||||
descriptionText?: string,
|
||||
|
||||
/**
|
||||
* Custom dialog title text.
|
||||
*/
|
||||
titleText?: string
|
||||
};
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { IStore } from '../app/types';
|
||||
import { openDialog } from '../base/dialog/actions';
|
||||
import { isMobileBrowser } from '../base/environment/utils';
|
||||
import { getLocalJitsiDesktopTrack } from '../base/tracks/functions';
|
||||
|
||||
import { SET_SCREENSHOT_CAPTURE } from './actionTypes';
|
||||
import CameraCaptureDialog from './CameraCaptureDialog';
|
||||
import { ICameraCapturePayload, SET_SCREENSHOT_CAPTURE } from './actionTypes';
|
||||
import { createScreenshotCaptureSummary } from './functions';
|
||||
import logger from './logger';
|
||||
|
||||
@@ -61,3 +64,23 @@ export function toggleScreenshotCaptureSummary(enabled: boolean) {
|
||||
return Promise.resolve();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens {@code CameraCaptureDialog}.
|
||||
*
|
||||
* @param {Function} callback - The callback to execute on picture taken.
|
||||
* @param {ICameraCapturePayload} componentProps - The camera capture payload.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function openCameraCaptureDialog(callback: Function, componentProps: ICameraCapturePayload) {
|
||||
return (dispatch: IStore['dispatch']) => {
|
||||
if (!isMobileBrowser()) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(openDialog(CameraCaptureDialog, {
|
||||
callback,
|
||||
componentProps
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -91,6 +91,23 @@ export function getNotificationsMap(stateful: IStateful): { [key: string]: boole
|
||||
}, {});
|
||||
}
|
||||
|
||||
function normalizeCurrentLanguage(language: string) {
|
||||
if (!language) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [ country, lang ] = language.split('-');
|
||||
const jitsiNormalized = `${country}${lang ?? ''}`;
|
||||
|
||||
if (LANGUAGES.includes(jitsiNormalized)) {
|
||||
return jitsiNormalized;
|
||||
}
|
||||
|
||||
if (LANGUAGES.includes(country)) {
|
||||
return country;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the properties for the "More" tab from settings dialog from Redux
|
||||
* state.
|
||||
@@ -102,7 +119,7 @@ export function getNotificationsMap(stateful: IStateful): { [key: string]: boole
|
||||
export function getMoreTabProps(stateful: IStateful) {
|
||||
const state = toState(stateful);
|
||||
const stageFilmstripEnabled = isStageFilmstripEnabled(state);
|
||||
const language = i18next.language || DEFAULT_LANGUAGE;
|
||||
const language = normalizeCurrentLanguage(i18next.language) || DEFAULT_LANGUAGE;
|
||||
const configuredTabs: string[] = interfaceConfig.SETTINGS_SECTIONS || [];
|
||||
|
||||
// when self view is controlled by the config we hide the settings
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Image, View, ViewStyle } from 'react-native';
|
||||
import { SvgCssUri } from 'react-native-svg';
|
||||
import { SvgCssUri } from 'react-native-svg/css';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Client } from '@stomp/stompjs';
|
||||
import { Client, StompSubscription } from '@stomp/stompjs';
|
||||
|
||||
import logger from './logger';
|
||||
import { WebsocketClient } from './websocket-client';
|
||||
@@ -10,6 +10,9 @@ import { WebsocketClient } from './websocket-client';
|
||||
export class VisitorsListWebsocketClient extends WebsocketClient {
|
||||
private static client: VisitorsListWebsocketClient;
|
||||
|
||||
private _topicSubscription: StompSubscription | undefined;
|
||||
private _queueSubscription: StompSubscription | undefined;
|
||||
|
||||
/**
|
||||
* Creates a new instance of the VisitorsListWebsocketClient.
|
||||
*
|
||||
@@ -87,7 +90,7 @@ export class VisitorsListWebsocketClient extends WebsocketClient {
|
||||
const cachedDeltas: Array<{ n: string; r: string; s: string; }> = [];
|
||||
|
||||
// Subscribe first for deltas so we don't miss any while waiting for the initial list
|
||||
this.stompClient.subscribe(topicEndpoint, deltaMessage => {
|
||||
this._topicSubscription = this.stompClient.subscribe(topicEndpoint, deltaMessage => {
|
||||
try {
|
||||
const updates: Array<{ n: string; r: string; s: string; }> = JSON.parse(deltaMessage.body);
|
||||
|
||||
@@ -102,7 +105,7 @@ export class VisitorsListWebsocketClient extends WebsocketClient {
|
||||
});
|
||||
|
||||
// Subscribe for the initial list after topic subscription is active
|
||||
const queueSubscription = this.stompClient.subscribe(queueEndpoint, message => {
|
||||
this._queueSubscription = this.stompClient.subscribe(queueEndpoint, message => {
|
||||
try {
|
||||
const visitors: Array<{ n: string; r: string; }> = JSON.parse(message.body);
|
||||
|
||||
@@ -110,7 +113,11 @@ export class VisitorsListWebsocketClient extends WebsocketClient {
|
||||
initialReceived = true;
|
||||
initialCallback(visitors);
|
||||
|
||||
queueSubscription.unsubscribe();
|
||||
// Unsubscribe from queue after receiving initial list
|
||||
if (this._queueSubscription) {
|
||||
this._queueSubscription.unsubscribe();
|
||||
this._queueSubscription = undefined;
|
||||
}
|
||||
|
||||
if (cachedDeltas.length) {
|
||||
deltaCallback(cachedDeltas);
|
||||
@@ -124,4 +131,45 @@ export class VisitorsListWebsocketClient extends WebsocketClient {
|
||||
|
||||
this.stompClient.activate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribes from both topic and queue subscriptions.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
override unsubscribe(): void {
|
||||
if (this._topicSubscription) {
|
||||
this._topicSubscription.unsubscribe();
|
||||
logger.debug('Unsubscribed from visitors list topic');
|
||||
this._topicSubscription = undefined;
|
||||
}
|
||||
|
||||
if (this._queueSubscription) {
|
||||
this._queueSubscription.unsubscribe();
|
||||
logger.debug('Unsubscribed from visitors list queue');
|
||||
this._queueSubscription = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects the current stomp client instance and clears it.
|
||||
* Unsubscribes from any active subscriptions first.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
override disconnect(): Promise<any> {
|
||||
if (!this.stompClient) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const url = this.stompClient.brokerURL;
|
||||
|
||||
// Unsubscribe first (synchronous), then disconnect
|
||||
this.unsubscribe();
|
||||
|
||||
return this.stompClient.deactivate().then(() => {
|
||||
logger.debug(`disconnected from: ${url}`);
|
||||
this.stompClient = undefined;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
ENDPOINT_MESSAGE_RECEIVED,
|
||||
UPDATE_CONFERENCE_METADATA
|
||||
} from '../base/conference/actionTypes';
|
||||
import { IConferenceMetadata } from '../base/conference/reducer';
|
||||
import { SET_CONFIG } from '../base/config/actionTypes';
|
||||
import { CONNECTION_FAILED } from '../base/connection/actionTypes';
|
||||
import { connect, setPreferVisitor } from '../base/connection/actions';
|
||||
@@ -176,7 +177,11 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
|
||||
// let's subscribe for visitor waiting queue
|
||||
const { room } = getState()['features/base/conference'];
|
||||
const { disableBeforeUnloadHandlers = false } = getState()['features/base/config'];
|
||||
const conferenceJid = `${room}@${hosts?.muc}`;
|
||||
const beforeUnloadHandler = () => {
|
||||
WebsocketClient.getInstance().disconnect();
|
||||
};
|
||||
|
||||
WebsocketClient.getInstance()
|
||||
.connect(`wss://${visitorsConfig?.queueService}/visitor/websocket`,
|
||||
@@ -185,8 +190,12 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
if ('status' in msg && msg.status === 'live') {
|
||||
logger.info('The conference is now live!');
|
||||
|
||||
|
||||
WebsocketClient.getInstance().disconnect()
|
||||
.then(() => {
|
||||
window.removeEventListener(
|
||||
disableBeforeUnloadHandlers ? 'unload' : 'beforeunload',
|
||||
beforeUnloadHandler);
|
||||
let delay = 0;
|
||||
|
||||
// now let's connect to meeting
|
||||
@@ -213,20 +222,18 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
dispatch(setInVisitorsQueue(true));
|
||||
});
|
||||
|
||||
/**
|
||||
* Disconnecting the WebSocket client when the user closes the page.
|
||||
*/
|
||||
window.addEventListener(disableBeforeUnloadHandlers ? 'unload' : 'beforeunload', beforeUnloadHandler);
|
||||
|
||||
|
||||
break;
|
||||
}
|
||||
case PARTICIPANT_UPDATED: {
|
||||
const { visitors: visitorsConfig } = toState(getState)['features/base/config'];
|
||||
const { metadata } = getState()['features/base/conference'];
|
||||
|
||||
if (visitorsConfig?.queueService && isLocalParticipantModerator(getState)) {
|
||||
const { metadata } = getState()['features/base/conference'];
|
||||
|
||||
if (metadata?.visitors?.live === false && !WebsocketClient.getInstance().isActive()) {
|
||||
// when go live is available and false, we should subscribe
|
||||
// to the service if available to listen for waiting visitors
|
||||
_subscribeQueueStats(getState(), dispatch);
|
||||
}
|
||||
}
|
||||
_handleQueueAndNotification(dispatch, getState, metadata);
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -242,26 +249,8 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
}
|
||||
case UPDATE_CONFERENCE_METADATA: {
|
||||
const { metadata } = action;
|
||||
const { visitors: visitorsConfig } = toState(getState)['features/base/config'];
|
||||
|
||||
if (!visitorsConfig?.queueService) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (isLocalParticipantModerator(getState)) {
|
||||
if (metadata?.visitors?.live === false) {
|
||||
if (!WebsocketClient.getInstance().isActive()) {
|
||||
// if metadata go live changes to goLive false and local is moderator
|
||||
// we should subscribe to the service if available to listen for waiting visitors
|
||||
_subscribeQueueStats(getState(), dispatch);
|
||||
}
|
||||
|
||||
_showNotLiveNotification(dispatch, getVisitorsInQueueCount(getState));
|
||||
} else if (metadata?.visitors?.live) {
|
||||
dispatch(hideNotification(VISITORS_NOT_LIVE_NOTIFICATION_ID));
|
||||
WebsocketClient.getInstance().disconnect();
|
||||
}
|
||||
}
|
||||
_handleQueueAndNotification(dispatch, getState, metadata);
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -275,6 +264,38 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
return next(action);
|
||||
});
|
||||
|
||||
/**
|
||||
* Handles the queue connection and notification for visitors if needed.
|
||||
*
|
||||
* @param {IStore.dispatch} dispatch - The Redux dispatch function.
|
||||
* @param {IStore.getState} getState - The Redux getState function.
|
||||
* @param {IConferenceMetadata} metadata - The conference metadata.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _handleQueueAndNotification(
|
||||
dispatch: IStore['dispatch'],
|
||||
getState: IStore['getState'],
|
||||
metadata: IConferenceMetadata | undefined): void {
|
||||
const { visitors: visitorsConfig } = toState(getState)['features/base/config'];
|
||||
|
||||
if (!(visitorsConfig?.queueService && isLocalParticipantModerator(getState))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (metadata?.visitors?.live === false) {
|
||||
if (!WebsocketClient.getInstance().isActive()) {
|
||||
// if metadata go live changes to goLive false and local is moderator
|
||||
// we should subscribe to the service if available to listen for waiting visitors
|
||||
_subscribeQueueStats(getState(), dispatch);
|
||||
}
|
||||
|
||||
_showNotLiveNotification(dispatch, getVisitorsInQueueCount(getState));
|
||||
} else if (metadata?.visitors?.live) {
|
||||
dispatch(hideNotification(VISITORS_NOT_LIVE_NOTIFICATION_ID));
|
||||
WebsocketClient.getInstance().disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a notification that the meeting is not live.
|
||||
*
|
||||
|
||||
@@ -63,7 +63,7 @@ ReducerRegistry.register<IVisitorsState>('features/visitors', (state = DEFAULT_S
|
||||
};
|
||||
}
|
||||
case UPDATE_VISITORS_IN_QUEUE_COUNT: {
|
||||
if (state.count === action.count) {
|
||||
if (state.inQueueCount === action.count) {
|
||||
return state;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import { Client } from '@stomp/stompjs';
|
||||
import { Client, StompSubscription } from '@stomp/stompjs';
|
||||
|
||||
import logger from './logger';
|
||||
|
||||
@@ -28,6 +28,8 @@ export class WebsocketClient {
|
||||
|
||||
private _connectCount = 0;
|
||||
|
||||
private _subscription: StompSubscription | undefined;
|
||||
|
||||
/**
|
||||
* WebsocketClient getInstance.
|
||||
*
|
||||
@@ -100,7 +102,7 @@ export class WebsocketClient {
|
||||
this._connectCount++;
|
||||
connectCallback?.();
|
||||
|
||||
this.stompClient.subscribe(endpoint, message => {
|
||||
this._subscription = this.stompClient.subscribe(endpoint, message => {
|
||||
try {
|
||||
callback(JSON.parse(message.body));
|
||||
} catch (e) {
|
||||
@@ -113,7 +115,21 @@ export class WebsocketClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects the current stomp client instance and clears it.
|
||||
* Unsubscribes from the current subscription.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
unsubscribe(): void {
|
||||
if (this._subscription) {
|
||||
this._subscription.unsubscribe();
|
||||
logger.debug('Unsubscribed from WebSocket topic');
|
||||
this._subscription = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects the current stomp client instance and clears it.
|
||||
* Unsubscribes from any active subscriptions first if available.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
@@ -124,8 +140,11 @@ export class WebsocketClient {
|
||||
|
||||
const url = this.stompClient.brokerURL;
|
||||
|
||||
// Unsubscribe first (synchronous), then disconnect
|
||||
this.unsubscribe();
|
||||
|
||||
return this.stompClient.deactivate().then(() => {
|
||||
logger.info(`disconnected from: ${url}`);
|
||||
logger.debug(`disconnected from: ${url}`);
|
||||
this.stompClient = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -157,7 +157,14 @@ function start_av_moderation(room, mediaType, occupant)
|
||||
room.av_moderation = {};
|
||||
room.av_moderation_actors = {};
|
||||
end
|
||||
room.av_moderation[mediaType] = array{};
|
||||
room.av_moderation[mediaType] = array();
|
||||
|
||||
-- add all current moderators to the new whitelist
|
||||
for _, room_occupant in room:each_occupant() do
|
||||
if room_occupant.role == 'moderator' and not ends_with(room_occupant.nick, '/focus') then
|
||||
room.av_moderation[mediaType]:push(internal_room_jid_match_rewrite(room_occupant.nick));
|
||||
end
|
||||
end
|
||||
|
||||
-- We want to set startMuted policy in metadata, in case of new participants are joining to respect
|
||||
-- it, that will be enforced by jicofo
|
||||
@@ -166,7 +173,7 @@ function start_av_moderation(room, mediaType, occupant)
|
||||
-- We want to keep the previous value of startMuted for this mediaType if av moderation is disabled
|
||||
-- to be able to restore
|
||||
local av_moderation_startMuted_restore = room.av_moderation_startMuted_restore or {};
|
||||
av_moderation_startMuted_restore = startMutedMetadata[mediaType];
|
||||
av_moderation_startMuted_restore[mediaType] = startMutedMetadata[mediaType];
|
||||
room.av_moderation_startMuted_restore = av_moderation_startMuted_restore;
|
||||
|
||||
startMutedMetadata[mediaType] = true;
|
||||
@@ -262,6 +269,12 @@ function on_message(event)
|
||||
|
||||
-- send message to all occupants
|
||||
notify_occupants_enable(nil, enabled, room, occupant.nick, mediaType);
|
||||
|
||||
if enabled then
|
||||
-- inform all moderators for the newly created whitelist
|
||||
notify_whitelist_change(nil, true, room, mediaType);
|
||||
end
|
||||
|
||||
return true;
|
||||
elseif moderation_command.attr.jidToWhitelist then
|
||||
local occupant_jid = moderation_command.attr.jidToWhitelist;
|
||||
@@ -357,9 +370,13 @@ function occupant_joined(event)
|
||||
-- NOTE for some reason event.occupant.role is not reflecting the actual occupant role (when changed
|
||||
-- from allowners module) but iterating over room occupants returns the correct role
|
||||
for _, room_occupant in room:each_occupant() do
|
||||
-- if moderator send the whitelist
|
||||
if room_occupant.nick == occupant.nick and room_occupant.role == 'moderator' then
|
||||
notify_whitelist_change(room_occupant.jid, false, room);
|
||||
-- if it is a moderator, send the whitelist to every moderator
|
||||
if room_occupant.nick == occupant.nick and room_occupant.role == 'moderator' then
|
||||
for _,mediaType in pairs({'audio', 'video', 'desktop'}) do
|
||||
if room.av_moderation[mediaType] then
|
||||
notify_whitelist_change(nil, true, room, mediaType);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -367,14 +384,30 @@ end
|
||||
|
||||
-- when a occupant was granted moderator we need to update him with the whitelist
|
||||
function occupant_affiliation_changed(event)
|
||||
local room = event.room;
|
||||
if not room.av_moderation or is_healthcheck_room(room.jid) or is_admin(event.jid)
|
||||
or event.affiliation ~= 'owner' then
|
||||
return;
|
||||
end
|
||||
|
||||
-- in any enabled media type add the new moderator to the whitelist
|
||||
for _, room_occupant in room:each_occupant() do
|
||||
if room_occupant.bare_jid == event.jid then
|
||||
for _,mediaType in pairs({'audio', 'video', 'desktop'}) do
|
||||
if room.av_moderation[mediaType] then
|
||||
room.av_moderation[mediaType]:push(internal_room_jid_match_rewrite(room_occupant.nick));
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- the actor can be nil if is coming from allowners or similar module we want to skip it here
|
||||
-- as we will handle it in occupant_joined
|
||||
if event.actor and event.affiliation == 'owner' and event.room.av_moderation then
|
||||
local room = event.room;
|
||||
-- event.jid is the bare jid of participant
|
||||
for _, occupant in room:each_occupant() do
|
||||
if occupant.bare_jid == event.jid then
|
||||
notify_whitelist_change(occupant.jid, false, room);
|
||||
if event.actor and event.affiliation == 'owner' then
|
||||
-- notify all moderators for the new grant moderator and the change in whitelists
|
||||
for _,mediaType in pairs({'audio', 'video', 'desktop'}) do
|
||||
if room.av_moderation[mediaType] then
|
||||
notify_whitelist_change(nil, true, room, mediaType);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
--- This module removes identity information from presence stanzas when the
|
||||
--- hideDisplayNameForAll or hideDisplayNameForGuests options are enabled
|
||||
--- for a room.
|
||||
|
||||
--- To be enabled under the main muc component
|
||||
local filters = require 'util.filters';
|
||||
local st = require 'util.stanza';
|
||||
|
||||
local util = module:require 'util';
|
||||
local filter_identity_from_presence = util.filter_identity_from_presence;
|
||||
local get_room_by_name_and_subdomain = util.get_room_by_name_and_subdomain;
|
||||
local is_admin = util.is_admin;
|
||||
local ends_with = util.ends_with;
|
||||
local internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite;
|
||||
|
||||
local NICK_NS = 'http://jabber.org/protocol/nick';
|
||||
|
||||
-- we need to get the shared resource for joining moderators, as participants are marked as moderators
|
||||
-- after joining which is after the filter for stanza/out, but we need to know will this participant be a moderator
|
||||
local joining_moderator_participants = module:shared('moderators/joining_moderator_participants');
|
||||
|
||||
--- Filter presence sent to non-moderator members of a room when the hideDisplayNameForGuests option is set.
|
||||
function filter_stanza_out(stanza, session)
|
||||
if stanza.name ~= 'presence' or stanza.attr.type == 'error'
|
||||
or stanza.attr.type == 'unavailable' or ends_with(stanza.attr.from, '/focus') then
|
||||
@@ -21,48 +25,26 @@ function filter_stanza_out(stanza, session)
|
||||
end
|
||||
|
||||
local room = get_room_by_name_and_subdomain(session.jitsi_web_query_room, session.jitsi_web_query_prefix);
|
||||
local shouldFilter = false;
|
||||
|
||||
if not room or room._data.hideDisplayNameForGuests ~= true then
|
||||
return stanza;
|
||||
end
|
||||
|
||||
local occupant = room:get_occupant_by_real_jid(stanza.attr.to);
|
||||
if occupant then
|
||||
if stanza.attr.from == internal_room_jid_match_rewrite(occupant.nick) then
|
||||
-- we ignore self-presences, in this case and role will not be correct
|
||||
return stanza;
|
||||
end
|
||||
|
||||
if occupant.role ~= 'moderator' and not joining_moderator_participants[occupant.bare_jid] then
|
||||
local st_clone = st.clone(stanza);
|
||||
st_clone:remove_children('nick', NICK_NS);
|
||||
return st_clone;
|
||||
if room and (room._data.hideDisplayNameForGuests == true or room._data.hideDisplayNameForAll == true) then
|
||||
local occupant = room:get_occupant_by_real_jid(stanza.attr.to);
|
||||
-- don't touch self-presence
|
||||
if occupant and stanza.attr.from ~= internal_room_jid_match_rewrite(occupant.nick) then
|
||||
local isModerator = (occupant.role == 'moderator' or joining_moderator_participants[occupant.bare_jid]);
|
||||
shouldFilter = room._data.hideDisplayNameForAll or not isModerator;
|
||||
end
|
||||
end
|
||||
|
||||
return stanza;
|
||||
end
|
||||
|
||||
function filter_stanza_in(stanza, session)
|
||||
if stanza.name ~= 'presence' or stanza.attr.type == 'error' or stanza.attr.type == 'unavailable' then
|
||||
if shouldFilter then
|
||||
return filter_identity_from_presence(stanza);
|
||||
else
|
||||
return stanza;
|
||||
end
|
||||
|
||||
local room = get_room_by_name_and_subdomain(session.jitsi_web_query_room, session.jitsi_web_query_prefix);
|
||||
|
||||
-- if hideDisplayNameForAll we want to drop any display name from the presence stanza
|
||||
if not room or room._data.hideDisplayNameForAll ~= true then
|
||||
return stanza;
|
||||
end
|
||||
|
||||
stanza:remove_children('nick', NICK_NS);
|
||||
|
||||
return stanza;
|
||||
end
|
||||
|
||||
function filter_session(session)
|
||||
filters.add_filter(session, 'stanzas/out', filter_stanza_out, -100);
|
||||
filters.add_filter(session, 'stanzas/in', filter_stanza_in, -100);
|
||||
end
|
||||
|
||||
function module.load()
|
||||
|
||||
@@ -19,6 +19,7 @@ local internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite;
|
||||
local process_host_module = util.process_host_module;
|
||||
local table_shallow_copy = util.table_shallow_copy;
|
||||
local table_add = util.table_add;
|
||||
local table_equals = util.table_equals;
|
||||
|
||||
local MUC_NS = 'http://jabber.org/protocol/muc';
|
||||
local COMPONENT_IDENTITY_TYPE = 'room_metadata';
|
||||
@@ -188,12 +189,15 @@ function on_message(event)
|
||||
jsonData.data = res;
|
||||
end
|
||||
|
||||
room.jitsiMetadata[jsonData.key] = jsonData.data;
|
||||
local old_value = room.jitsiMetadata[jsonData.key];
|
||||
if not table_equals(old_value, jsonData.data) then
|
||||
room.jitsiMetadata[jsonData.key] = jsonData.data;
|
||||
|
||||
broadcastMetadata(room);
|
||||
broadcastMetadata(room);
|
||||
|
||||
-- fire and event for the change
|
||||
main_muc_module:fire_event('jitsi-metadata-updated', { room = room; actor = occupant; key = jsonData.key; });
|
||||
-- fire and event for the change
|
||||
main_muc_module:fire_event('jitsi-metadata-updated', { room = room; actor = occupant; key = jsonData.key; });
|
||||
end
|
||||
|
||||
return true;
|
||||
end
|
||||
@@ -238,12 +242,24 @@ function process_main_muc_loaded(main_muc, host_module)
|
||||
|
||||
local startMutedMetadata = room.jitsiMetadata.startMuted or {};
|
||||
|
||||
startMutedMetadata.audio = startMuted.attr.audio == 'true';
|
||||
startMutedMetadata.video = startMuted.attr.video == 'true';
|
||||
local audioNewValue = startMuted.attr.audio == 'true';
|
||||
local videoNewValue = startMuted.attr.video == 'true';
|
||||
local send_update = false;
|
||||
|
||||
room.jitsiMetadata.startMuted = startMutedMetadata;
|
||||
if startMutedMetadata.audio ~= audioNewValue then
|
||||
startMutedMetadata.audio = audioNewValue;
|
||||
send_update = true;
|
||||
end
|
||||
if startMutedMetadata.video ~= videoNewValue then
|
||||
startMutedMetadata.video = videoNewValue;
|
||||
send_update = true;
|
||||
end
|
||||
|
||||
host_module:fire_event('room-metadata-changed', { room = room; });
|
||||
if send_update then
|
||||
room.jitsiMetadata.startMuted = startMutedMetadata;
|
||||
|
||||
host_module:fire_event('room-metadata-changed', { room = room; });
|
||||
end
|
||||
end);
|
||||
end
|
||||
|
||||
|
||||
@@ -12,12 +12,16 @@ local st = require 'util.stanza';
|
||||
local jid = require 'util.jid';
|
||||
local new_id = require 'util.id'.medium;
|
||||
local util = module:require 'util';
|
||||
local filter_identity_from_presence = util.filter_identity_from_presence;
|
||||
local is_admin = util.is_admin;
|
||||
local presence_check_status = util.presence_check_status;
|
||||
local process_host_module = util.process_host_module;
|
||||
local is_transcriber_jigasi = util.is_transcriber_jigasi;
|
||||
local json = require 'cjson.safe';
|
||||
|
||||
-- Debug flag
|
||||
local DEBUG = false;
|
||||
|
||||
local MUC_NS = 'http://jabber.org/protocol/muc';
|
||||
|
||||
-- required parameter for custom muc component prefix, defaults to 'conference'
|
||||
@@ -97,6 +101,25 @@ local function send_visitors_iq(conference_service, room, type)
|
||||
module:send(visitors_iq);
|
||||
end
|
||||
|
||||
-- Filter out identity information (nick name, email, etc) from a presence stanza,
|
||||
-- if the hideDisplayNameForGuests option for the room is set (note that the
|
||||
-- hideDisplayNameForAll option is implemented in a diffrent way and does not
|
||||
-- require filtering here)
|
||||
-- This is applied to presence of main room participants before it is sent out to
|
||||
-- vnodes.
|
||||
local function filter_stanza_nick_if_needed(stanza, room)
|
||||
if not stanza or stanza.name ~= 'presence' or stanza.attr.type == 'error' or stanza.attr.type == 'unavailable' then
|
||||
return stanza;
|
||||
end
|
||||
|
||||
-- if hideDisplayNameForGuests we want to drop any display name from the presence stanza
|
||||
if room and (room._data.hideDisplayNameForGuests or room._data.hideDisplayNameForAll) then
|
||||
return filter_identity_from_presence(stanza);
|
||||
end
|
||||
|
||||
return stanza;
|
||||
end
|
||||
|
||||
-- an event received from visitors component, which receives iqs from jicofo
|
||||
local function connect_vnode(event)
|
||||
local room, vnode = event.room, event.vnode;
|
||||
@@ -123,7 +146,7 @@ local function connect_vnode(event)
|
||||
|
||||
for _, o in room:each_occupant() do
|
||||
if not is_admin(o.bare_jid) then
|
||||
local fmuc_pr = st.clone(o:get_presence());
|
||||
local fmuc_pr = filter_stanza_nick_if_needed(st.clone(o:get_presence()), room);
|
||||
local user, _, res = jid.split(o.nick);
|
||||
fmuc_pr.attr.to = jid.join(user, conference_service , res);
|
||||
fmuc_pr.attr.from = o.jid;
|
||||
@@ -206,7 +229,8 @@ end, 900);
|
||||
process_host_module(main_muc_component_config, function(host_module, host)
|
||||
-- detects presence change in a main participant and propagate it to the used visitor nodes
|
||||
host_module:hook('muc-occupant-pre-change', function (event)
|
||||
local room, stanza, occupant = event.room, event.stanza, event.dest_occupant;
|
||||
local room, stanzaEv, occupant = event.room, event.stanza, event.dest_occupant;
|
||||
local stanza = filter_stanza_nick_if_needed(stanzaEv, room);
|
||||
|
||||
-- filter focus and configured domains (used for jibri and transcribers)
|
||||
if is_admin(stanza.attr.from) or visitors_nodes[room.jid] == nil
|
||||
@@ -227,7 +251,8 @@ process_host_module(main_muc_component_config, function(host_module, host)
|
||||
|
||||
-- when a main participant leaves inform the visitor nodes
|
||||
host_module:hook('muc-occupant-left', function (event)
|
||||
local room, stanza, occupant = event.room, event.stanza, event.occupant;
|
||||
local room, stanzaEv, occupant = event.room, event.stanza, event.occupant;
|
||||
local stanza = filter_stanza_nick_if_needed(stanzaEv, room);
|
||||
|
||||
-- ignore configured domains (jibri and transcribers)
|
||||
if is_admin(occupant.bare_jid) or visitors_nodes[room.jid] == nil or visitors_nodes[room.jid].nodes == nil
|
||||
@@ -270,7 +295,8 @@ process_host_module(main_muc_component_config, function(host_module, host)
|
||||
|
||||
-- detects new participants joining main room and sending them to the visitor nodes
|
||||
host_module:hook('muc-occupant-joined', function (event)
|
||||
local room, stanza, occupant = event.room, event.stanza, event.occupant;
|
||||
local room, stanzaEv, occupant = event.room, event.stanza, event.occupant;
|
||||
local stanza = filter_stanza_nick_if_needed(stanzaEv, room);
|
||||
|
||||
-- filter focus, ignore configured domains (jibri and transcribers)
|
||||
if is_admin(stanza.attr.from) or visitors_nodes[room.jid] == nil
|
||||
@@ -294,7 +320,8 @@ process_host_module(main_muc_component_config, function(host_module, host)
|
||||
end);
|
||||
-- forwards messages from main participants to vnodes
|
||||
host_module:hook('muc-occupant-groupchat', function(event)
|
||||
local room, stanza, occupant = event.room, event.stanza, event.occupant;
|
||||
local room, stanzaEv, occupant = event.room, event.stanza, event.occupant;
|
||||
local stanza = filter_stanza_nick_if_needed(stanzaEv, room);
|
||||
|
||||
-- filter sending messages from transcribers/jibris to visitors
|
||||
if not visitors_nodes[room.jid] then
|
||||
@@ -314,7 +341,8 @@ process_host_module(main_muc_component_config, function(host_module, host)
|
||||
-- receiving messages from visitor nodes and forward them to local main participants
|
||||
-- and forward them to the rest of visitor nodes
|
||||
host_module:hook('muc-occupant-groupchat', function(event)
|
||||
local occupant, room, stanza = event.occupant, event.room, event.stanza;
|
||||
local occupant, room, stanzaEv = event.occupant, event.room, event.stanza;
|
||||
local stanza = filter_stanza_nick_if_needed(stanzaEv, room);
|
||||
local to = stanza.attr.to;
|
||||
local from = stanza.attr.from;
|
||||
local from_vnode = jid.host(from);
|
||||
|
||||
@@ -620,11 +620,14 @@ end
|
||||
local function table_compare(old_table, new_table)
|
||||
local removed = {}
|
||||
local added = {}
|
||||
local modified = {}
|
||||
|
||||
-- Find removed items (in old but not in new)
|
||||
for id, _ in pairs(old_table) do
|
||||
for id, value in pairs(old_table) do
|
||||
if new_table[id] == nil then
|
||||
table.insert(removed, id)
|
||||
elseif new_table[id] ~= value then
|
||||
table.insert(modified, id)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -635,7 +638,20 @@ local function table_compare(old_table, new_table)
|
||||
end
|
||||
end
|
||||
|
||||
return removed, added
|
||||
return removed, added, modified
|
||||
end
|
||||
|
||||
local function table_equals(t1, t2)
|
||||
if t1 == nil then
|
||||
return t2 == nil;
|
||||
end
|
||||
if t2 == nil then
|
||||
return t1 == nil;
|
||||
end
|
||||
|
||||
local removed, added, modified = table_compare(t1, t2);
|
||||
|
||||
return next(removed) == nil and next(added) == nil and next(modified) == nil
|
||||
end
|
||||
|
||||
-- Splits a string using delimiter
|
||||
@@ -685,11 +701,35 @@ local function is_admin(_jid)
|
||||
return false;
|
||||
end
|
||||
|
||||
-- Filter out identity information (nick name, email, etc) from a presence stanza.
|
||||
local function filter_identity_from_presence(orig_stanza)
|
||||
local stanza = st.clone(orig_stanza);
|
||||
|
||||
stanza:remove_children('nick', 'http://jabber.org/protocol/nick');
|
||||
stanza:remove_children('email');
|
||||
stanza:remove_children('stats-id');
|
||||
local identity = stanza:get_child('identity');
|
||||
if identity then
|
||||
local user = identity:get_child('user');
|
||||
local name = identity:get_child('name');
|
||||
if user then
|
||||
user:remove_children('email');
|
||||
user:remove_children('name');
|
||||
end
|
||||
if name then
|
||||
name:remove_children('name'); -- Remove name with no namespace
|
||||
end
|
||||
end
|
||||
|
||||
return stanza;
|
||||
end
|
||||
|
||||
return {
|
||||
OUTBOUND_SIP_JIBRI_PREFIXES = OUTBOUND_SIP_JIBRI_PREFIXES;
|
||||
INBOUND_SIP_JIBRI_PREFIXES = INBOUND_SIP_JIBRI_PREFIXES;
|
||||
RECORDER_PREFIXES = RECORDER_PREFIXES;
|
||||
extract_subdomain = extract_subdomain;
|
||||
filter_identity_from_presence = filter_identity_from_presence;
|
||||
is_admin = is_admin;
|
||||
is_feature_allowed = is_feature_allowed;
|
||||
is_jibri = is_jibri;
|
||||
@@ -722,4 +762,5 @@ return {
|
||||
table_compare = table_compare;
|
||||
table_shallow_copy = table_shallow_copy;
|
||||
table_find = table_find;
|
||||
table_equals = table_equals;
|
||||
};
|
||||
|
||||
@@ -1,47 +1,13 @@
|
||||
# Ignore certificate errors (self-signed certificates)
|
||||
#ALLOW_INSECURE_CERTS=true
|
||||
|
||||
# The base url that will be used for the test (default will be using "https://alpha.jitsi.net")
|
||||
# If there is a tenant in the URL it must end with a slash (e.g. "https://alpha.jitsi.net/sometenant/")
|
||||
#BASE_URL=
|
||||
|
||||
# Room name suffix to use when creating new room names
|
||||
#ROOM_NAME_SUFFIX=
|
||||
|
||||
# Room name prefix to use when creating new room names
|
||||
#ROOM_NAME_PREFIX=
|
||||
|
||||
# To be able to match a domain to a specific address
|
||||
# The format is "MAP example.com 1.2.3.4"
|
||||
#RESOLVER_RULES=
|
||||
|
||||
# Ignore certificate errors (self-signed certificates)
|
||||
#ALLOW_INSECURE_CERTS=true
|
||||
|
||||
# Whether to run the browser in headless mode
|
||||
#HEADLESS=false
|
||||
|
||||
# The path to the browser video capture file
|
||||
#VIDEO_CAPTURE_FILE=tests/resources/FourPeople_1280x720_30.y4m
|
||||
|
||||
# The tenant used when executing the iframeAPI tests, will override any tenant from BASE_URL if any
|
||||
#IFRAME_TENANT=
|
||||
|
||||
# The grid host url (https://mygrid.com/wd/hub)
|
||||
#GRID_HOST_URL=
|
||||
|
||||
# The path to the private key used for generating JWT token (.pk)
|
||||
#JWT_PRIVATE_KEY_PATH=
|
||||
# The kid to use in the token
|
||||
#JWT_KID=
|
||||
|
||||
# An access token to use to create meetings (used for the first participant)
|
||||
#JWT_ACCESS_TOKEN=
|
||||
|
||||
# The count of workers that execute the tests in parallel
|
||||
# MAX_INSTANCES=1
|
||||
|
||||
# The address of the webhooks proxy used to test the webhooks feature (e.g. wss://your.service/?tenant=sometenant)
|
||||
#WEBHOOKS_PROXY_URL=
|
||||
# A shared secret to authenticate the webhook proxy connection
|
||||
#WEBHOOKS_PROXY_SHARED_SECRET=
|
||||
# Whether to use beta for the first participants
|
||||
#BROWSER_CHROME_BETA=false
|
||||
#BROWSER_FF_BETA=false
|
||||
|
||||
# A rest URL to be used by dial-in tests to invite jigasi to the conference
|
||||
#DIAL_IN_REST_URL=
|
||||
@@ -49,12 +15,60 @@
|
||||
# A destination number to dialout, that auto answers and sends media
|
||||
#DIAL_OUT_URL=
|
||||
|
||||
# The grid host url (https://mygrid.com/wd/hub)
|
||||
#GRID_HOST_URL=
|
||||
|
||||
# Whether to run the browser in headless mode
|
||||
#HEADLESS=false
|
||||
|
||||
# The tenant used when executing the iframeAPI tests, will override any tenant from BASE_URL if any
|
||||
#IFRAME_TENANT=
|
||||
|
||||
# Configure properties for jaas-specific tests (specs/jaas). Note that some of the iFrame tests can also
|
||||
# be used to test JaaS, they are configured separately via IFRAME_TENANT, JWT_KID, JWT_PRIVATE_KEY_PATH.
|
||||
# Domain for the JaaS environment, e.g. stage.8x8.vc
|
||||
JAAS_DOMAIN=
|
||||
# The key ID
|
||||
JAAS_KID=
|
||||
# The path to the private key used for generating JWT token (.pk) for jaas-specific tests
|
||||
JAAS_PRIVATE_KEY_PATH=
|
||||
# The JaaS tenant, e.g. vpaas-magic-cookie-abcdabcd1234567890
|
||||
JAAS_TENANT=
|
||||
|
||||
# An access token to use to create meetings (used for the first participant)
|
||||
#JWT_ACCESS_TOKEN=
|
||||
|
||||
# The kid to use in the token for non-jaas-specific tests (though it could be a jaas key).
|
||||
#JWT_KID=
|
||||
|
||||
# The path to the private key used for generating JWT token (.pk) for non-jaas-specific tests (though it could be a
|
||||
# jaas key).
|
||||
#JWT_PRIVATE_KEY_PATH=
|
||||
|
||||
# The count of workers that execute the tests in parallel
|
||||
# MAX_INSTANCES=1
|
||||
|
||||
# To be able to match a domain to a specific address
|
||||
# The format is "MAP example.com 1.2.3.4"
|
||||
#RESOLVER_RULES=
|
||||
|
||||
# Room name prefix to use when creating new room names
|
||||
#ROOM_NAME_PREFIX=
|
||||
|
||||
# Room name suffix to use when creating new room names
|
||||
#ROOM_NAME_SUFFIX=
|
||||
|
||||
# A destination number to dialout, that auto answer and sends media audio and video
|
||||
#SIP_JIBRI_DIAL_OUT_URL=
|
||||
|
||||
# Whether to use beta for the first participants
|
||||
#BROWSER_CHROME_BETA=false
|
||||
#BROWSER_FF_BETA=false
|
||||
# The path to the browser video capture file
|
||||
#VIDEO_CAPTURE_FILE=tests/resources/FourPeople_1280x720_30.y4m
|
||||
|
||||
# A shared secret to authenticate the webhook proxy connection
|
||||
#WEBHOOKS_PROXY_SHARED_SECRET=
|
||||
|
||||
# The address of the webhooks proxy used to test the webhooks feature (e.g. wss://your.service/?tenant=sometenant)
|
||||
#WEBHOOKS_PROXY_URL=
|
||||
|
||||
# A stream key abd broadcast ID that can be used by the tests to stream to YouTube
|
||||
#YTUBE_TEST_STREAM_KEY=
|
||||
|
||||
@@ -24,6 +24,7 @@ import VideoQualityDialog from '../pageobjects/VideoQualityDialog';
|
||||
import Visitors from '../pageobjects/Visitors';
|
||||
|
||||
import { LOG_PREFIX, logInfo } from './browserLogger';
|
||||
import { IToken } from './token';
|
||||
import { IContext, IJoinOptions } from './types';
|
||||
|
||||
export const P1 = 'p1';
|
||||
@@ -49,7 +50,10 @@ export class Participant {
|
||||
*/
|
||||
private _name: string;
|
||||
private _endpointId: string;
|
||||
private _jwt?: string;
|
||||
/**
|
||||
* The token that this participant was initialized with.
|
||||
*/
|
||||
private _token?: IToken;
|
||||
|
||||
/**
|
||||
* The default config to use when joining.
|
||||
@@ -110,11 +114,11 @@ export class Participant {
|
||||
* Creates a participant with given name.
|
||||
*
|
||||
* @param {string} name - The name of the participant.
|
||||
* @param {string }jwt - The jwt if any.
|
||||
* @param {string} token - The token if any.
|
||||
*/
|
||||
constructor(name: string, jwt?: string) {
|
||||
constructor(name: string, token?: IToken) {
|
||||
this._name = name;
|
||||
this._jwt = jwt;
|
||||
this._token = token;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -200,13 +204,13 @@ export class Participant {
|
||||
};
|
||||
}
|
||||
|
||||
if (ctx.iframeAPI) {
|
||||
if (ctx.testProperties.useIFrameApi) {
|
||||
config.room = 'iframeAPITest.html';
|
||||
}
|
||||
|
||||
let url = urlObjectToString(config) || '';
|
||||
|
||||
if (ctx.iframeAPI) {
|
||||
if (ctx.testProperties.useIFrameApi) {
|
||||
const baseUrl = new URL(this.driver.options.baseUrl || '');
|
||||
|
||||
// @ts-ignore
|
||||
@@ -219,8 +223,8 @@ export class Participant {
|
||||
url = `${url}&tenant="${baseUrl.pathname.substring(1)}"`;
|
||||
}
|
||||
}
|
||||
if (this._jwt) {
|
||||
url = `${url}&jwt="${this._jwt}"`;
|
||||
if (this._token?.jwt) {
|
||||
url = `${url}&jwt="${this._token.jwt}"`;
|
||||
}
|
||||
|
||||
if (options.baseUrl) {
|
||||
@@ -231,7 +235,8 @@ export class Participant {
|
||||
|
||||
let urlToLoad = url.startsWith('/') ? url.substring(1) : url;
|
||||
|
||||
if (options.preferGenerateToken && !ctx.iframeAPI && ctx.isJaasAvailable() && process.env.IFRAME_TENANT) {
|
||||
if (options.preferGenerateToken && !ctx.testProperties.useIFrameApi
|
||||
&& process.env.JWT_KID?.startsWith('vpaas-magic-cookie-') && process.env.IFRAME_TENANT) {
|
||||
// This to enables tests like invite, which can force using the jaas auth instead of the provided token
|
||||
urlToLoad = `/${process.env.IFRAME_TENANT}/${urlToLoad}`;
|
||||
}
|
||||
@@ -241,7 +246,7 @@ export class Participant {
|
||||
|
||||
await this.waitForPageToLoad();
|
||||
|
||||
if (ctx.iframeAPI) {
|
||||
if (ctx.testProperties.useIFrameApi) {
|
||||
const mainFrame = this.driver.$('iframe');
|
||||
|
||||
await this.driver.switchFrame(mainFrame);
|
||||
@@ -338,6 +343,10 @@ export class Participant {
|
||||
&& APP.store?.getState()['features/base/participants']?.local?.role === 'moderator');
|
||||
}
|
||||
|
||||
async isVisitor() {
|
||||
return await this.execute(() => APP?.store?.getState()['features/visitors']?.iAmVisitor || false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the meeting supports breakout rooms.
|
||||
*/
|
||||
@@ -447,7 +456,7 @@ export class Participant {
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for number of participants.
|
||||
* Waits until the number of participants is exactly the given number.
|
||||
*
|
||||
* @param {number} number - The number of participant to wait for.
|
||||
* @param {string} msg - A custom message to use.
|
||||
@@ -891,4 +900,11 @@ export class Participant {
|
||||
return this.driver.$(`//span[@id="participant_${endpointId}" and contains(@class, "dominant-speaker")]`)
|
||||
.waitForDisplayed({ timeout: 5_000 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the token that this participant was initialized with.
|
||||
*/
|
||||
getToken(): IToken | undefined {
|
||||
return this._token;
|
||||
}
|
||||
}
|
||||
|
||||
43
tests/helpers/TestProperties.ts
Normal file
43
tests/helpers/TestProperties.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* An interface that tests can export (as a TEST_PROPERTIES property) to define what they require.
|
||||
*/
|
||||
export type ITestProperties = {
|
||||
/** The test uses the iFrame API. */
|
||||
useIFrameApi: boolean;
|
||||
/** The test requires jaas, it should be skipped when the jaas configuration is not enabled. */
|
||||
useJaas: boolean;
|
||||
/** The test requires the webhook proxy. */
|
||||
useWebhookProxy: boolean;
|
||||
};
|
||||
|
||||
const defaultProperties: ITestProperties = {
|
||||
useIFrameApi: false,
|
||||
useWebhookProxy: false,
|
||||
useJaas: false
|
||||
};
|
||||
|
||||
const testProperties: Record<string, ITestProperties> = {};
|
||||
|
||||
/**
|
||||
* Set properties for a test file. This was needed because I couldn't find a hook that executes with describe() before
|
||||
* the code in wdio.conf.ts's before() hook. The intention is for tests to execute this directly. The properties don't
|
||||
* change dynamically.
|
||||
*
|
||||
* @param filename the absolute path to the test file
|
||||
* @param properties the properties to set for the test file, defaults will be applied for missing properties
|
||||
*/
|
||||
export function setTestProperties(filename: string, properties: Partial<ITestProperties>): void {
|
||||
if (testProperties[filename]) {
|
||||
console.warn(`Test properties for ${filename} are already set. Overwriting.`);
|
||||
}
|
||||
|
||||
testProperties[filename] = { ...defaultProperties, ...properties };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param testFilePath - The absolute path to the test file
|
||||
* @returns Promise<ITestProperties> - The test properties with defaults applied
|
||||
*/
|
||||
export async function getTestProperties(testFilePath: string): Promise<ITestProperties> {
|
||||
return testProperties[testFilePath] || { ...defaultProperties };
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
import fs from 'fs';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import process from 'node:process';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { P1, P2, P3, P4, Participant } from './Participant';
|
||||
import { IToken, generateToken } from './token';
|
||||
import { IContext, IJoinOptions } from './types';
|
||||
|
||||
const SUBJECT_XPATH = '//div[starts-with(@class, "subject-text")]';
|
||||
@@ -171,7 +169,7 @@ async function _joinParticipant( // eslint-disable-line max-params
|
||||
const p = ctx[name] as Participant;
|
||||
|
||||
if (p) {
|
||||
if (ctx.iframeAPI) {
|
||||
if (ctx.testProperties.useIFrameApi) {
|
||||
await p.switchInPage();
|
||||
}
|
||||
|
||||
@@ -179,7 +177,7 @@ async function _joinParticipant( // eslint-disable-line max-params
|
||||
return;
|
||||
}
|
||||
|
||||
if (ctx.iframeAPI) {
|
||||
if (ctx.testProperties.useIFrameApi) {
|
||||
// when loading url make sure we are on the top page context or strange errors may occur
|
||||
await p.switchToAPI();
|
||||
}
|
||||
@@ -190,25 +188,33 @@ async function _joinParticipant( // eslint-disable-line max-params
|
||||
// we want the participant instance re-recreated so we clear any kept state, like endpoint ID
|
||||
}
|
||||
|
||||
let jwtToken;
|
||||
let token: IToken = { jwt: '' };
|
||||
|
||||
if (name === P1) {
|
||||
if (!options?.skipFirstModerator) {
|
||||
// we prioritize the access token when iframe is not used and private key is set,
|
||||
// otherwise if private key is not specified we use the access token if set
|
||||
if (process.env.JWT_ACCESS_TOKEN
|
||||
&& ((ctx.jwtPrivateKeyPath && !ctx.iframeAPI && !options?.preferGenerateToken)
|
||||
&& ((ctx.jwtPrivateKeyPath && !ctx.testProperties.useIFrameApi && !options?.preferGenerateToken)
|
||||
|| !ctx.jwtPrivateKeyPath)) {
|
||||
jwtToken = process.env.JWT_ACCESS_TOKEN;
|
||||
token = { jwt: process.env.JWT_ACCESS_TOKEN };
|
||||
} else if (ctx.jwtPrivateKeyPath) {
|
||||
jwtToken = getToken(ctx, name, options);
|
||||
token = generateToken({
|
||||
...options?.tokenOptions,
|
||||
displayName: name,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (name === P2) {
|
||||
jwtToken = options?.preferGenerateToken ? getToken(ctx, P2, options) : undefined;
|
||||
if (options?.preferGenerateToken) {
|
||||
token = generateToken({
|
||||
...options?.tokenOptions,
|
||||
displayName: name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const newParticipant = new Participant(name, jwtToken);
|
||||
const newParticipant = new Participant(name, token);
|
||||
|
||||
// set the new participant instance
|
||||
// @ts-ignore
|
||||
@@ -283,64 +289,6 @@ export async function muteVideoAndCheck(testee: Participant, observer: Participa
|
||||
await observer.getParticipantsPane().assertVideoMuteIconIsDisplayed(testee);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a JWT token for a moderator.
|
||||
*/
|
||||
function getToken(ctx: IContext, displayName: string, options?: IJoinOptions) {
|
||||
const keyid = process.env.JWT_KID;
|
||||
const headers = {
|
||||
algorithm: 'RS256',
|
||||
noTimestamp: true,
|
||||
expiresIn: '24h',
|
||||
keyid
|
||||
};
|
||||
|
||||
if (!keyid) {
|
||||
console.error('JWT_KID is not set');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const key = fs.readFileSync(ctx.jwtPrivateKeyPath);
|
||||
|
||||
const payload = {
|
||||
'aud': 'jitsi',
|
||||
'iss': 'chat',
|
||||
'sub': keyid.substring(0, keyid.indexOf('/')),
|
||||
'context': {
|
||||
'user': {
|
||||
'name': displayName,
|
||||
'id': uuidv4(),
|
||||
'avatar': 'https://avatars0.githubusercontent.com/u/3671647',
|
||||
'email': 'john.doe@jitsi.org'
|
||||
},
|
||||
'group': uuidv4(),
|
||||
'features': {
|
||||
'outbound-call': 'true',
|
||||
'transcription': 'true',
|
||||
'recording': 'true',
|
||||
'sip-outbound-call': true,
|
||||
'livestreaming': true
|
||||
},
|
||||
},
|
||||
'room': '*'
|
||||
};
|
||||
|
||||
// if the moderator is set, or options are missing, we assume moderator
|
||||
if (options?.moderator || !options) {
|
||||
// @ts-ignore
|
||||
payload.context.user.moderator = true;
|
||||
} else if (options.visitor) {
|
||||
// @ts-ignore
|
||||
payload.context.user.role = 'visitor';
|
||||
}
|
||||
|
||||
ctx.data[`${displayName}-jwt-payload`] = payload;
|
||||
|
||||
// @ts-ignore
|
||||
return jwt.sign(payload, key, headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a JID string.
|
||||
* @param str the string to parse.
|
||||
|
||||
132
tests/helpers/token.ts
Normal file
132
tests/helpers/token.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import fs from 'fs';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export type ITokenOptions = {
|
||||
displayName?: string;
|
||||
/**
|
||||
* The duration for which the token is valid, e.g. "1h" for one hour.
|
||||
*/
|
||||
exp?: string;
|
||||
/**
|
||||
* The key ID to use for the token.
|
||||
* If not provided, JWT_KID will be used from the environment variables.
|
||||
*/
|
||||
keyId?: string;
|
||||
/**
|
||||
* The path to the private key file used to sign the token.
|
||||
* If not provided, JWT_PRIVATE_KEY_PATH will be used from the environment variables.
|
||||
*/
|
||||
keyPath?: string;
|
||||
/**
|
||||
* Whether to set the 'moderator' flag.
|
||||
*/
|
||||
moderator?: boolean;
|
||||
/**
|
||||
* The room for which the token is valid, or '*'. Defaults to '*'.
|
||||
*/
|
||||
room?: string;
|
||||
sub?: string;
|
||||
/**
|
||||
* Whether to set the 'visitor' flag.
|
||||
*/
|
||||
visitor?: boolean;
|
||||
};
|
||||
|
||||
export type IToken = {
|
||||
/**
|
||||
* The JWT headers, for easy reference.
|
||||
*/
|
||||
headers?: any;
|
||||
/**
|
||||
* The signed JWT.
|
||||
*/
|
||||
jwt: string;
|
||||
/**
|
||||
* The options used to generate the token.
|
||||
*/
|
||||
options?: ITokenOptions;
|
||||
/**
|
||||
* The token's payload, for easy reference.
|
||||
*/
|
||||
payload?: any;
|
||||
};
|
||||
|
||||
export function generatePayload(options: ITokenOptions): any {
|
||||
const payload = {
|
||||
'aud': 'jitsi',
|
||||
'iss': 'chat',
|
||||
'sub': options?.sub || '',
|
||||
'context': {
|
||||
'user': {
|
||||
'name': options.displayName,
|
||||
'id': uuidv4(),
|
||||
'avatar': 'https://avatars0.githubusercontent.com/u/3671647',
|
||||
'email': 'john.doe@jitsi.org'
|
||||
},
|
||||
'group': uuidv4(),
|
||||
'features': {
|
||||
'outbound-call': 'true',
|
||||
'transcription': 'true',
|
||||
'recording': 'true',
|
||||
'sip-outbound-call': true,
|
||||
'livestreaming': true
|
||||
},
|
||||
},
|
||||
'room': options.room || '*'
|
||||
};
|
||||
|
||||
if (options.moderator) {
|
||||
// @ts-ignore
|
||||
payload.context.user.moderator = true;
|
||||
} else if (options.visitor) {
|
||||
// @ts-ignore
|
||||
payload.context.user.role = 'visitor';
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a signed token.
|
||||
*/
|
||||
export function generateToken(options: ITokenOptions): IToken {
|
||||
const keyId = options.keyId || process.env.JWT_KID;
|
||||
const keyPath = options.keyPath || process.env.JWT_PRIVATE_KEY_PATH;
|
||||
const headers = {
|
||||
algorithm: 'RS256',
|
||||
noTimestamp: true,
|
||||
expiresIn: options.exp || '24h',
|
||||
keyid: keyId,
|
||||
};
|
||||
|
||||
if (!keyId) {
|
||||
throw new Error('JWT_KID is not set');
|
||||
}
|
||||
|
||||
if (!keyPath) {
|
||||
throw new Error('JWT_PRIVATE_KEY_PATH is not set');
|
||||
}
|
||||
|
||||
const key = fs.readFileSync(keyPath);
|
||||
const payload = generatePayload({
|
||||
...options,
|
||||
displayName: options?.displayName || '',
|
||||
sub: keyId.substring(0, keyId.indexOf('/'))
|
||||
});
|
||||
|
||||
return {
|
||||
headers,
|
||||
// @ts-ignore
|
||||
jwt: jwt.sign(payload, key, headers),
|
||||
options,
|
||||
payload
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generated a signed token and return just the JWT string.
|
||||
*/
|
||||
export function generateJwt(options: ITokenOptions): string {
|
||||
return generateToken(options).jwt;
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
import { IConfig } from '../../react/features/base/config/configType';
|
||||
|
||||
import type { Participant } from './Participant';
|
||||
import { ITestProperties } from './TestProperties';
|
||||
import type WebhookProxy from './WebhookProxy';
|
||||
import { ITokenOptions } from './token';
|
||||
|
||||
export type IContext = {
|
||||
data: any;
|
||||
iframeAPI: boolean;
|
||||
isJaasAvailable: () => boolean;
|
||||
/**
|
||||
* Whether the configuration specifies a JaaS account for the iFrame API tests.
|
||||
*/
|
||||
iFrameUsesJaas: boolean;
|
||||
jwtKid: string;
|
||||
jwtPrivateKeyPath: string;
|
||||
keepAlive: Array<any>;
|
||||
@@ -16,6 +20,7 @@ export type IContext = {
|
||||
p4: Participant;
|
||||
roomName: string;
|
||||
skipSuiteTests: boolean;
|
||||
testProperties: ITestProperties;
|
||||
times: any;
|
||||
webhooksProxy: WebhookProxy;
|
||||
};
|
||||
@@ -37,11 +42,6 @@ export type IJoinOptions = {
|
||||
*/
|
||||
displayName?: string;
|
||||
|
||||
/**
|
||||
* Whether to create a moderator token for joining.
|
||||
*/
|
||||
moderator?: boolean;
|
||||
|
||||
/**
|
||||
* When joining the first participant and jwt singing material is available and a provided token
|
||||
* is available, prefer generating a new token for the first participant.
|
||||
@@ -70,7 +70,7 @@ export type IJoinOptions = {
|
||||
skipWaitToJoin?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to create a visitor token for joining.
|
||||
* Options used when generating a token.
|
||||
*/
|
||||
visitor?: boolean;
|
||||
tokenOptions?: ITokenOptions;
|
||||
};
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { expect } from '@wdio/globals';
|
||||
|
||||
import type { Participant } from '../../helpers/Participant';
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { ensureTwoParticipants } from '../../helpers/participants';
|
||||
import { fetchJson } from '../../helpers/utils';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
useIFrameApi: true,
|
||||
useWebhookProxy: true
|
||||
});
|
||||
|
||||
describe('Chat', () => {
|
||||
it('joining the meeting', async () => {
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
||||
import { P1, P2, Participant } from '../../helpers/Participant';
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { ensureTwoParticipants, parseJid } from '../../helpers/participants';
|
||||
import { IContext } from '../../helpers/types';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
useIFrameApi: true,
|
||||
useWebhookProxy: true
|
||||
});
|
||||
|
||||
/**
|
||||
* Tests PARTICIPANT_LEFT webhook.
|
||||
*/
|
||||
async function checkParticipantLeftHook(ctx: IContext, p: Participant, reason: string, checkId = false) {
|
||||
async function checkParticipantLeftHook(ctx: IContext, p: Participant, reason: string, checkId = false, conferenceJid: string) {
|
||||
const { webhooksProxy } = ctx;
|
||||
|
||||
if (webhooksProxy) {
|
||||
@@ -28,14 +34,14 @@ async function checkParticipantLeftHook(ctx: IContext, p: Participant, reason: s
|
||||
} = await webhooksProxy.waitForEvent('PARTICIPANT_LEFT');
|
||||
|
||||
expect('PARTICIPANT_LEFT').toBe(event.eventType);
|
||||
expect(event.data.conference).toBe(ctx.data.conferenceJid);
|
||||
expect(event.data.conference).toBe(conferenceJid);
|
||||
expect(event.data.disconnectReason).toBe(reason);
|
||||
expect(event.data.isBreakout).toBe(false);
|
||||
expect(event.data.participantId).toBe(await p.getEndpointId());
|
||||
expect(event.data.name).toBe(p.name);
|
||||
|
||||
if (checkId) {
|
||||
const jwtPayload = ctx.data[`${p.name}-jwt-payload`];
|
||||
const jwtPayload = p.getToken()?.payload;
|
||||
|
||||
expect(event.data.id).toBe(jwtPayload?.context?.user?.id);
|
||||
expect(event.data.group).toBe(jwtPayload?.context?.group);
|
||||
@@ -45,6 +51,8 @@ async function checkParticipantLeftHook(ctx: IContext, p: Participant, reason: s
|
||||
}
|
||||
|
||||
describe('Participants presence', () => {
|
||||
let conferenceJid: string = '';
|
||||
|
||||
it('joining the meeting', async () => {
|
||||
// ensure 2 participants one moderator and one guest, we will load both with iframeAPI
|
||||
await ensureTwoParticipants(ctx);
|
||||
@@ -113,7 +121,7 @@ describe('Participants presence', () => {
|
||||
|
||||
const { node, resource } = parseJid(roomsInfo.jid);
|
||||
|
||||
ctx.data.conferenceJid = roomsInfo.jid.substring(0, roomsInfo.jid.indexOf('/'));
|
||||
conferenceJid = roomsInfo.jid.substring(0, roomsInfo.jid.indexOf('/'));
|
||||
|
||||
const p1EpId = await p1.getEndpointId();
|
||||
|
||||
@@ -135,7 +143,7 @@ describe('Participants presence', () => {
|
||||
} = await webhooksProxy.waitForEvent('ROOM_CREATED');
|
||||
|
||||
expect('ROOM_CREATED').toBe(event.eventType);
|
||||
expect(event.data.conference).toBe(ctx.data.conferenceJid);
|
||||
expect(event.data.conference).toBe(conferenceJid);
|
||||
expect(event.data.isBreakout).toBe(false);
|
||||
}
|
||||
}
|
||||
@@ -231,7 +239,7 @@ describe('Participants presence', () => {
|
||||
|
||||
const roomsInfo = (await p1.getIframeAPI().getRoomsInfo()).rooms[0];
|
||||
|
||||
ctx.data.conferenceJid = roomsInfo.jid.substring(0, roomsInfo.jid.indexOf('/'));
|
||||
conferenceJid = roomsInfo.jid.substring(0, roomsInfo.jid.indexOf('/'));
|
||||
|
||||
await p1.getIframeAPI().addEventListener('participantKickedOut');
|
||||
await p2.getIframeAPI().addEventListener('participantKickedOut');
|
||||
@@ -248,7 +256,7 @@ describe('Participants presence', () => {
|
||||
timeoutMsg: 'participantKickedOut event not received on p2 side'
|
||||
});
|
||||
|
||||
await checkParticipantLeftHook(ctx, p2, 'kicked', true);
|
||||
await checkParticipantLeftHook(ctx, p2, 'kicked', true, conferenceJid);
|
||||
|
||||
expect(eventP1).toBeDefined();
|
||||
expect(eventP2).toBeDefined();
|
||||
@@ -315,7 +323,7 @@ describe('Participants presence', () => {
|
||||
} = await webhooksProxy.waitForEvent('PARTICIPANT_JOINED');
|
||||
|
||||
expect('PARTICIPANT_JOINED').toBe(event.eventType);
|
||||
expect(event.data.conference).toBe(ctx.data.conferenceJid);
|
||||
expect(event.data.conference).toBe(conferenceJid);
|
||||
expect(event.data.isBreakout).toBe(false);
|
||||
expect(event.data.moderator).toBe(false);
|
||||
expect(event.data.name).toBe(await p2.getLocalDisplayName());
|
||||
@@ -385,7 +393,7 @@ describe('Participants presence', () => {
|
||||
expect(eventConferenceLeftP2).toBeDefined();
|
||||
expect(eventConferenceLeftP2.roomName).toBe(roomName);
|
||||
|
||||
await checkParticipantLeftHook(ctx, p2, 'left');
|
||||
await checkParticipantLeftHook(ctx, p2, 'left', false, conferenceJid);
|
||||
|
||||
const eventReadyToCloseP2 = await p2.driver.waitUntil(() => p2.getIframeAPI().getEventResult('readyToClose'), {
|
||||
timeout: 2000,
|
||||
@@ -396,7 +404,7 @@ describe('Participants presence', () => {
|
||||
});
|
||||
|
||||
it('dispose conference', async () => {
|
||||
const { data: { conferenceJid }, p1, roomName, webhooksProxy } = ctx;
|
||||
const { p1, roomName, webhooksProxy } = ctx;
|
||||
|
||||
await p1.switchToAPI();
|
||||
|
||||
@@ -414,7 +422,7 @@ describe('Participants presence', () => {
|
||||
expect(eventConferenceLeft).toBeDefined();
|
||||
expect(eventConferenceLeft.roomName).toBe(roomName);
|
||||
|
||||
await checkParticipantLeftHook(ctx, p1, 'left', true);
|
||||
await checkParticipantLeftHook(ctx, p1, 'left', true, conferenceJid);
|
||||
if (webhooksProxy) {
|
||||
// ROOM_DESTROYED webhook
|
||||
// @ts-ignore
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { expect } from '@wdio/globals';
|
||||
|
||||
import type { Participant } from '../../helpers/Participant';
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import type WebhookProxy from '../../helpers/WebhookProxy';
|
||||
import { ensureOneParticipant, ensureTwoParticipants } from '../../helpers/participants';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
useIFrameApi: true,
|
||||
useWebhookProxy: true
|
||||
});
|
||||
|
||||
describe('Transcriptions', () => {
|
||||
it('joining the meeting', async () => {
|
||||
await ensureOneParticipant(ctx);
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { ensureOneParticipant, ensureTwoParticipants } from '../../helpers/participants';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
useIFrameApi: true,
|
||||
useWebhookProxy: true
|
||||
});
|
||||
|
||||
describe('Visitors', () => {
|
||||
it('joining the meeting', async () => {
|
||||
const { webhooksProxy } = ctx;
|
||||
@@ -33,7 +39,7 @@ describe('Visitors', () => {
|
||||
it('visitor joins', async () => {
|
||||
await ensureTwoParticipants(ctx, {
|
||||
preferGenerateToken: true,
|
||||
visitor: true,
|
||||
tokenOptions: { visitor: true },
|
||||
skipInMeetingChecks: true
|
||||
});
|
||||
|
||||
@@ -72,7 +78,7 @@ describe('Visitors', () => {
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('PARTICIPANT_JOINED');
|
||||
|
||||
const jwtPayload = ctx.data[`${p2.name}-jwt-payload`];
|
||||
const jwtPayload = p2.getToken()?.payload;
|
||||
|
||||
expect('PARTICIPANT_JOINED').toBe(event.eventType);
|
||||
expect(event.data.avatar).toBe(jwtPayload.context.user.avatar);
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { expect } from '@wdio/globals';
|
||||
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { ensureOneParticipant, ensureTwoParticipants } from '../../helpers/participants';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
useIFrameApi: true,
|
||||
useWebhookProxy: true
|
||||
});
|
||||
|
||||
describe('Visitors', () => {
|
||||
it('joining the meeting', async () => {
|
||||
const { webhooksProxy } = ctx;
|
||||
@@ -37,7 +43,7 @@ describe('Visitors', () => {
|
||||
it('go live', async () => {
|
||||
await ensureTwoParticipants(ctx, {
|
||||
preferGenerateToken: true,
|
||||
visitor: true,
|
||||
tokenOptions: { visitor: true },
|
||||
skipWaitToJoin: true,
|
||||
skipInMeetingChecks: true
|
||||
});
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { ensureOneParticipant, ensureTwoParticipants, joinSecondParticipant } from '../../helpers/participants';
|
||||
import type SecurityDialog from '../../pageobjects/SecurityDialog';
|
||||
|
||||
let roomKey: string;
|
||||
|
||||
/**
|
||||
* 1. Lock the room (make sure the image changes to locked)
|
||||
* 2. Join with a second browser/tab
|
||||
@@ -29,14 +31,14 @@ describe('Lock Room', () => {
|
||||
const p2PasswordDialog = p2.getPasswordDialog();
|
||||
|
||||
await p2PasswordDialog.waitForDialog();
|
||||
await p2PasswordDialog.submitPassword(`${ctx.data.roomKey}1234`);
|
||||
await p2PasswordDialog.submitPassword(`${roomKey}1234`);
|
||||
|
||||
// give sometime to the password prompt to disappear and send the password
|
||||
await p2.driver.pause(500);
|
||||
|
||||
// wait for password prompt
|
||||
await p2PasswordDialog.waitForDialog();
|
||||
await p2PasswordDialog.submitPassword(ctx.data.roomKey);
|
||||
await p2PasswordDialog.submitPassword(roomKey);
|
||||
|
||||
await p2.waitToJoinMUC();
|
||||
|
||||
@@ -106,7 +108,7 @@ describe('Lock Room', () => {
|
||||
const p2PasswordDialog = p2.getPasswordDialog();
|
||||
|
||||
await p2PasswordDialog.waitForDialog();
|
||||
await p2PasswordDialog.submitPassword(`${ctx.data.roomKey}1234`);
|
||||
await p2PasswordDialog.submitPassword(`${roomKey}1234`);
|
||||
|
||||
// give sometime to the password prompt to disappear and send the password
|
||||
await p2.driver.pause(500);
|
||||
@@ -132,7 +134,7 @@ describe('Lock Room', () => {
|
||||
* Participant1 locks the room.
|
||||
*/
|
||||
async function participant1LockRoom() {
|
||||
ctx.data.roomKey = `${Math.trunc(Math.random() * 1_000_000)}`;
|
||||
roomKey = `${Math.trunc(Math.random() * 1_000_000)}`;
|
||||
|
||||
const { p1 } = ctx;
|
||||
const p1SecurityDialog = p1.getSecurityDialog();
|
||||
@@ -142,7 +144,7 @@ async function participant1LockRoom() {
|
||||
|
||||
await waitForRoomLockState(p1SecurityDialog, false);
|
||||
|
||||
await p1SecurityDialog.addPassword(ctx.data.roomKey);
|
||||
await p1SecurityDialog.addPassword(roomKey);
|
||||
|
||||
await p1SecurityDialog.clickCloseButton();
|
||||
|
||||
|
||||
@@ -77,8 +77,8 @@ describe('AVModeration', () => {
|
||||
});
|
||||
|
||||
it('hangup and change moderator', async () => {
|
||||
// no moderator switching if jaas is available
|
||||
if (ctx.isJaasAvailable()) {
|
||||
// no moderator switching if jaas is available.
|
||||
if (ctx.iFrameUsesJaas) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -195,8 +195,8 @@ describe('Lobby', () => {
|
||||
});
|
||||
|
||||
it('change of moderators in lobby', async () => {
|
||||
// no moderator switching if jaas is available
|
||||
if (ctx.isJaasAvailable()) {
|
||||
// no moderator switching if jaas is available.
|
||||
if (ctx.iFrameUsesJaas) {
|
||||
return;
|
||||
}
|
||||
await hangupAllParticipants();
|
||||
@@ -287,8 +287,8 @@ describe('Lobby', () => {
|
||||
});
|
||||
|
||||
it('moderator leaves while lobby enabled', async () => {
|
||||
// no moderator switching if jaas is available
|
||||
if (ctx.isJaasAvailable()) {
|
||||
// no moderator switching if jaas is available.
|
||||
if (ctx.iFrameUsesJaas) {
|
||||
return;
|
||||
}
|
||||
const { p1, p2, p3 } = ctx;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Participant } from '../../helpers/Participant';
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { ensureOneParticipant } from '../../helpers/participants';
|
||||
import {
|
||||
cleanup,
|
||||
@@ -8,6 +9,11 @@ import {
|
||||
waitForAudioFromDialInParticipant
|
||||
} from '../helpers/DialIn';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
useIFrameApi: true,
|
||||
useWebhookProxy: true
|
||||
});
|
||||
|
||||
describe('Invite iframeAPI', () => {
|
||||
it('join participant', async () => {
|
||||
await ensureOneParticipant(ctx);
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { ensureOneParticipant } from '../../helpers/participants';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
useIFrameApi: true,
|
||||
useWebhookProxy: true
|
||||
});
|
||||
|
||||
describe('Recording', () => {
|
||||
it('join participant', async () => {
|
||||
await ensureOneParticipant(ctx);
|
||||
@@ -180,7 +186,7 @@ async function testRecordingStopped(command: boolean) {
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('RECORDING_UPLOADED', 20000);
|
||||
|
||||
const jwtPayload = ctx.data[`${p1.name}-jwt-payload`];
|
||||
const jwtPayload = p1.getToken()?.payload;
|
||||
|
||||
expect(recordingUploadedEvent.data.initiatorId).toBe(jwtPayload?.context?.user?.id);
|
||||
expect(recordingUploadedEvent.data.participants.some(
|
||||
|
||||
51
tests/specs/helpers/jaas.ts
Normal file
51
tests/specs/helpers/jaas.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Participant } from '../../helpers/Participant';
|
||||
import { IToken, ITokenOptions, generateToken } from '../../helpers/token';
|
||||
|
||||
/**
|
||||
* Creates a new Participant and joins the MUC with the given name. The jaas-specific properties must be set as
|
||||
* environment variables: JAAS_DOMAIN and JAAS_TENANT, JAAS_KID, JAAS_PRIVATE_KEY_PATH.
|
||||
*
|
||||
* @param roomName The name of the room to join, without the tenant.
|
||||
* @param instanceId This is the "name" passed to the Participant, I think it's used to match against one of the
|
||||
* pre-configured browser instances in wdio? It must be one of 'p1', 'p2', 'p3', or 'p4'. TODO: figure out how this
|
||||
* should be used.
|
||||
* @param token the token to use, if any.
|
||||
*/
|
||||
export async function joinMuc(roomName: string, instanceId: 'p1' | 'p2' | 'p3' | 'p4', token?: IToken) {
|
||||
if (!process.env.JAAS_DOMAIN || !process.env.JAAS_TENANT) {
|
||||
throw new Error('JAAS_DOMAIN and JAAS_TENANT environment variables must be set');
|
||||
}
|
||||
|
||||
// TODO: this should re-use code from Participant (e.g. setting config).
|
||||
let url = `https://${process.env.JAAS_DOMAIN}/${process.env.JAAS_TENANT}/${roomName}`;
|
||||
|
||||
if (token) {
|
||||
url += `?jwt=${token.jwt}`;
|
||||
}
|
||||
url += '#config.prejoinConfig.enabled=false';
|
||||
|
||||
const newParticipant = new Participant(instanceId, token);
|
||||
|
||||
try {
|
||||
await newParticipant.driver.setTimeout({ 'pageLoad': 30000 });
|
||||
await newParticipant.driver.url(url);
|
||||
await newParticipant.waitForPageToLoad();
|
||||
await newParticipant.waitToJoinMUC();
|
||||
} catch (error) {
|
||||
}
|
||||
|
||||
return newParticipant;
|
||||
}
|
||||
|
||||
export function generateJaasToken(options: ITokenOptions): IToken {
|
||||
if (!process.env.JAAS_PRIVATE_KEY_PATH || !process.env.JAAS_KID) {
|
||||
throw new Error('JAAS_PRIVATE_KEY_PATH and JAAS_KID environment variables must be set');
|
||||
}
|
||||
|
||||
// Don't override the keyId and keyPath if they are already set in options, allow tests to set them.
|
||||
return generateToken({
|
||||
...options,
|
||||
keyId: options.keyId || process.env.JAAS_KID,
|
||||
keyPath: options.keyPath || process.env.JAAS_PRIVATE_KEY_PATH,
|
||||
});
|
||||
}
|
||||
63
tests/specs/jaas/joinMuc.spec.ts
Normal file
63
tests/specs/jaas/joinMuc.spec.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { joinMuc, generateJaasToken as t } from '../helpers/jaas';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
useJaas: true
|
||||
});
|
||||
|
||||
describe('XMPP login and MUC join test', () => {
|
||||
it('with a valid token (wildcard room)', async () => {
|
||||
const p = await joinMuc(ctx.roomName, 'p1', t({ room: '*' }));
|
||||
|
||||
expect(await p.isInMuc()).toBe(true);
|
||||
expect(await p.isModerator()).toBe(false);
|
||||
});
|
||||
|
||||
it('with a valid token (specific room)', async () => {
|
||||
const p = await joinMuc(ctx.roomName, 'p1', t({ room: ctx.roomName }));
|
||||
|
||||
expect(await p.isInMuc()).toBe(true);
|
||||
expect(await p.isModerator()).toBe(false);
|
||||
});
|
||||
|
||||
it('with a token with bad signature', async () => {
|
||||
const token = t({ room: ctx.roomName });
|
||||
|
||||
token.jwt = token.jwt + 'badSignature';
|
||||
|
||||
const p = await joinMuc(ctx.roomName, 'p1', token);
|
||||
|
||||
expect(Boolean(await p.isInMuc())).toBe(false);
|
||||
});
|
||||
|
||||
it('with an expired token', async () => {
|
||||
const p = await joinMuc(ctx.roomName, 'p1', t({ exp: '-1m' }));
|
||||
|
||||
expect(Boolean(await p.isInMuc())).toBe(false);
|
||||
});
|
||||
|
||||
it('with a token using the wrong key ID', async () => {
|
||||
const p = await joinMuc(ctx.roomName, 'p1', t({ keyId: 'invalid-key-id' }));
|
||||
|
||||
expect(Boolean(await p.isInMuc())).toBe(false);
|
||||
});
|
||||
|
||||
it('with a token for a different room', async () => {
|
||||
const p = await joinMuc(ctx.roomName, 'p1', t({ room: ctx.roomName + 'different' }));
|
||||
|
||||
expect(Boolean(await p.isInMuc())).toBe(false);
|
||||
});
|
||||
|
||||
it('with a moderator token', async () => {
|
||||
const p = await joinMuc(ctx.roomName, 'p1', t({ moderator: true }));
|
||||
|
||||
expect(await p.isInMuc()).toBe(true);
|
||||
expect(await p.isModerator()).toBe(true);
|
||||
});
|
||||
|
||||
// it('without sending a conference-request', async () => {
|
||||
// console.log('Joining a MUC without sending a conference-request');
|
||||
// // TODO verify failure
|
||||
// //expect(await joinMuc(ctx.roomName, 'p1', token)).toBe(true);
|
||||
// });
|
||||
});
|
||||
30
tests/specs/jaas/maxOccupants.spec.ts
Normal file
30
tests/specs/jaas/maxOccupants.spec.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { joinMuc, generateJaasToken as t } from '../helpers/jaas';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
useJaas: true,
|
||||
useWebhookProxy: true
|
||||
});
|
||||
|
||||
describe('MaxOccupants limit enforcement', () => {
|
||||
it('test maxOccupants limit', async () => {
|
||||
ctx.webhooksProxy.defaultMeetingSettings = {
|
||||
maxOccupants: 2
|
||||
};
|
||||
|
||||
const p1 = await joinMuc(ctx.roomName, 'p1', t({ room: ctx.roomName }));
|
||||
const p2 = await joinMuc(ctx.roomName, 'p2', t({ room: ctx.roomName }));
|
||||
|
||||
expect(await p1.isInMuc()).toBe(true);
|
||||
expect(await p2.isInMuc()).toBe(true);
|
||||
|
||||
// Third participant should be rejected (exceeding maxOccupants), even if it's a moderator
|
||||
let p3 = await joinMuc(ctx.roomName, 'p3', t({ room: ctx.roomName, moderator: true }));
|
||||
|
||||
expect(Boolean(await p3.isInMuc())).toBe(false);
|
||||
|
||||
await p1.hangup();
|
||||
p3 = await joinMuc(ctx.roomName, 'p3', t({ room: ctx.roomName }));
|
||||
expect(await p3.isInMuc()).toBe(true);
|
||||
});
|
||||
});
|
||||
52
tests/specs/jaas/visitors/participantsSoftLimit.spec.ts
Normal file
52
tests/specs/jaas/visitors/participantsSoftLimit.spec.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { setTestProperties } from '../../../helpers/TestProperties';
|
||||
import { joinMuc, generateJaasToken as t } from '../../helpers/jaas';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
useJaas: true,
|
||||
useWebhookProxy: true
|
||||
});
|
||||
|
||||
describe('Visitors triggered by reaching participantsSoftLimit', () => {
|
||||
it('test participantsSoftLimit', async () => {
|
||||
ctx.webhooksProxy.defaultMeetingSettings = {
|
||||
participantsSoftLimit: 2,
|
||||
visitorsEnabled: true
|
||||
};
|
||||
|
||||
/// XXX the "name" of the participant MUST match one of the "capabilities" defined in wdio. It's not a "participant", it's an instance configuration!
|
||||
const m = await joinMuc(
|
||||
ctx.roomName,
|
||||
'p1',
|
||||
t({ room: ctx.roomName, displayName: 'Mo de Rator', moderator: true })
|
||||
);
|
||||
|
||||
expect(await m.isInMuc()).toBe(true);
|
||||
expect(await m.isModerator()).toBe(true);
|
||||
expect(await m.isVisitor()).toBe(false);
|
||||
console.log('Moderator joined');
|
||||
|
||||
// Joining with a participant token before participantSoftLimit has been reached
|
||||
const p = await joinMuc(
|
||||
ctx.roomName,
|
||||
'p2',
|
||||
t({ room: ctx.roomName, displayName: 'Parti Cipant' })
|
||||
);
|
||||
|
||||
expect(await p.isInMuc()).toBe(true);
|
||||
expect(await p.isModerator()).toBe(false);
|
||||
expect(await p.isVisitor()).toBe(false);
|
||||
console.log('Participant joined');
|
||||
|
||||
// Joining with a participant token after participantSoftLimit has been reached
|
||||
const v = await joinMuc(
|
||||
ctx.roomName,
|
||||
'p3',
|
||||
t({ room: ctx.roomName, displayName: 'Visi Tor' })
|
||||
);
|
||||
|
||||
expect(await v.isInMuc()).toBe(true);
|
||||
expect(await v.isModerator()).toBe(false);
|
||||
expect(await v.isVisitor()).toBe(true);
|
||||
console.log('Visitor joined');
|
||||
});
|
||||
});
|
||||
61
tests/specs/jaas/visitors/visitorTokens.spec.ts
Normal file
61
tests/specs/jaas/visitors/visitorTokens.spec.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { setTestProperties } from '../../../helpers/TestProperties';
|
||||
import { joinMuc, generateJaasToken as t } from '../../helpers/jaas';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
useJaas: true,
|
||||
useWebhookProxy: true
|
||||
});
|
||||
|
||||
describe('Visitors triggered by visitor tokens', () => {
|
||||
it('test visitor tokens', async () => {
|
||||
ctx.webhooksProxy.defaultMeetingSettings = {
|
||||
visitorsEnabled: true
|
||||
};
|
||||
|
||||
const m = await joinMuc(
|
||||
ctx.roomName,
|
||||
'p1',
|
||||
t({ room: ctx.roomName, displayName: 'Mo de Rator', moderator: true })
|
||||
);
|
||||
|
||||
expect(await m.isInMuc()).toBe(true);
|
||||
expect(await m.isModerator()).toBe(true);
|
||||
expect(await m.isVisitor()).toBe(false);
|
||||
console.log('Moderator joined');
|
||||
|
||||
// Joining with a participant token before any visitors
|
||||
const p = await joinMuc(
|
||||
ctx.roomName,
|
||||
'p2',
|
||||
t({ room: ctx.roomName, displayName: 'Parti Cipant' })
|
||||
);
|
||||
|
||||
expect(await p.isInMuc()).toBe(true);
|
||||
expect(await p.isModerator()).toBe(false);
|
||||
expect(await p.isVisitor()).toBe(false);
|
||||
console.log('Participant joined');
|
||||
|
||||
// Joining with a visitor token
|
||||
const v = await joinMuc(
|
||||
ctx.roomName,
|
||||
'p3',
|
||||
t({ room: ctx.roomName, displayName: 'Visi Tor', visitor: true })
|
||||
);
|
||||
|
||||
expect(await v.isInMuc()).toBe(true);
|
||||
expect(await v.isModerator()).toBe(false);
|
||||
expect(await v.isVisitor()).toBe(true);
|
||||
console.log('Visitor joined');
|
||||
|
||||
// Joining with a participant token after visitors...:mindblown:
|
||||
const v2 = await joinMuc(
|
||||
ctx.roomName,
|
||||
'p4',
|
||||
t({ room: ctx.roomName, displayName: 'Visi Tor 2' }));
|
||||
|
||||
expect(await v2.isInMuc()).toBe(true);
|
||||
expect(await v2.isModerator()).toBe(false);
|
||||
expect(await v2.isVisitor()).toBe(true);
|
||||
console.log('Visitor2 joined');
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,7 @@ import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import pretty from 'pretty';
|
||||
|
||||
import { getTestProperties } from './helpers/TestProperties';
|
||||
import WebhookProxy from './helpers/WebhookProxy';
|
||||
import { getLogs, initLogger, logInfo } from './helpers/browserLogger';
|
||||
import { IContext } from './helpers/types';
|
||||
@@ -180,7 +181,11 @@ export const config: WebdriverIO.MultiremoteConfig = {
|
||||
console.warn('We expect to run a single suite, but got more than one');
|
||||
}
|
||||
|
||||
const testName = path.basename(specs[0]).replace('.spec.ts', '');
|
||||
const testFilePath = specs[0].replace(/^file:\/\//, '');
|
||||
const testName = path.relative('tests/specs', testFilePath)
|
||||
.replace(/.spec.ts$/, '')
|
||||
.replace(/\//g, '-');
|
||||
const testProperties = await getTestProperties(testFilePath);
|
||||
|
||||
console.log(`Running test: ${testName} via worker: ${cid}`);
|
||||
|
||||
@@ -191,6 +196,7 @@ export const config: WebdriverIO.MultiremoteConfig = {
|
||||
times: {}
|
||||
} as IContext;
|
||||
globalAny.ctx.keepAlive = [];
|
||||
globalAny.ctx.testProperties = testProperties;
|
||||
|
||||
await Promise.all(multiremotebrowser.instances.map(async (instance: string) => {
|
||||
const bInstance = multiremotebrowser.getInstance(instance);
|
||||
@@ -224,13 +230,12 @@ export const config: WebdriverIO.MultiremoteConfig = {
|
||||
globalAny.ctx.roomName = globalAny.ctx.roomName.toLowerCase();
|
||||
globalAny.ctx.jwtPrivateKeyPath = process.env.JWT_PRIVATE_KEY_PATH;
|
||||
globalAny.ctx.jwtKid = process.env.JWT_KID;
|
||||
globalAny.ctx.isJaasAvailable = () => globalAny.ctx.jwtKid?.startsWith('vpaas-magic-cookie-');
|
||||
globalAny.ctx.iFrameUsesJaas = process.env.JWT_PRIVATE_KEY_PATH
|
||||
&& process.env.JWT_KID?.startsWith('vpaas-magic-cookie-');
|
||||
|
||||
// If we are running the iFrameApi tests, we need to mark it as such and if needed to create the proxy
|
||||
// and connect to it.
|
||||
if (testName.startsWith('iFrameApi')) {
|
||||
globalAny.ctx.iframeAPI = true;
|
||||
|
||||
if (testProperties.useWebhookProxy) {
|
||||
if (!globalAny.ctx.webhooksProxy
|
||||
&& process.env.WEBHOOKS_PROXY_URL && process.env.WEBHOOKS_PROXY_SHARED_SECRET) {
|
||||
globalAny.ctx.webhooksProxy = new WebhookProxy(
|
||||
@@ -240,6 +245,20 @@ export const config: WebdriverIO.MultiremoteConfig = {
|
||||
globalAny.ctx.webhooksProxy.connect();
|
||||
}
|
||||
}
|
||||
|
||||
if (testProperties.useWebhookProxy && !globalAny.ctx.webhooksProxy) {
|
||||
console.warn(`WebhookProxy is not available, skipping ${testName}`);
|
||||
globalAny.ctx.skipSuiteTests = true;
|
||||
}
|
||||
|
||||
const isJaasConfigured = process.env.JAAS_DOMAIN && process.env.JAAS_TENANT
|
||||
&& process.env.JAAS_PRIVATE_KEY_PATH && process.env.JAAS_KID;
|
||||
|
||||
if (testProperties.useJaas && !isJaasConfigured) {
|
||||
console.warn(`JaaS is not configured, skipping ${testName}. `
|
||||
+ 'Set JAAS_DOMAIN, JAAS_TENANT, JAAS_KID, and JAAS_PRIVATE_KEY_PATH to enable.');
|
||||
globalAny.ctx.skipSuiteTests = true;
|
||||
}
|
||||
},
|
||||
|
||||
after() {
|
||||
|
||||
Reference in New Issue
Block a user