Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(meetings): add ability to reclaim host role with hostKey #3291

Merged
merged 7 commits into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions docs/samples/browser-plugin-meetings/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -1795,6 +1795,25 @@ function transferHostToMember(transferButton) {
}
}

function reclaimHost(reclaimHostBtn) {
const hostKey = reclaimHostBtn.previousElementSibling.value;
const meeting = getCurrentMeeting();
const selfId = meeting.members.selfId;
const role = {
type: 'MODERATOR',
hasRole: true,
hostKey,
};

meeting.members.assignRoles(selfId, [role])
.then(() => {
console.log('Host role reclaimed');
})
.catch((error) => {
console.log('Error reclaiming host role', error);
});
}

function viewParticipants() {
function createLabel(id, value = '') {
const label = document.createElement('label');
Expand Down Expand Up @@ -1923,6 +1942,19 @@ function viewParticipants() {
inviteDiv.appendChild(inviteBtn);

participantButtons.appendChild(inviteDiv);

const reclaimHostDiv = document.createElement('div');
const reclaimHostInput = document.createElement('input');
const reclaimHostBtn = createButton('Reclaim Host', reclaimHost);

reclaimHostDiv.style.display = 'flex';
reclaimHostInput.type = 'text';
reclaimHostInput.placeholder = 'Host Key';

reclaimHostDiv.appendChild(reclaimHostInput);
reclaimHostDiv.appendChild(reclaimHostBtn);

participantButtons.appendChild(reclaimHostDiv);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import * as MEETINGCONSTANTS from '../../constants';

/**
* Extended Error object for reclaim host role empty or wrong key
*/
export class ReclaimHostEmptyWrongKeyError extends Error {
sdkMessage: string;
error: null;
code: number;

/**
*
* @constructor
* @param {String} [message]
* @param {Object} [error]
*/
constructor(
message: string = MEETINGCONSTANTS.ERROR_DICTIONARY.RECLAIM_HOST_ROLE_EMPTY_OR_WRONG_KEY
.MESSAGE,
error: any = null
) {
super(message);

if (Error.captureStackTrace) {
Error.captureStackTrace(this, ReclaimHostEmptyWrongKeyError);
}

this.name = MEETINGCONSTANTS.ERROR_DICTIONARY.RECLAIM_HOST_ROLE_EMPTY_OR_WRONG_KEY.NAME;
this.sdkMessage =
message || MEETINGCONSTANTS.ERROR_DICTIONARY.RECLAIM_HOST_ROLE_EMPTY_OR_WRONG_KEY.MESSAGE;
this.error = error;

this.code = MEETINGCONSTANTS.ERROR_DICTIONARY.RECLAIM_HOST_ROLE_EMPTY_OR_WRONG_KEY.CODE;
}
}

/**
* Extended Error object for reclaim host role not supported
*/
export class ReclaimHostNotSupportedError extends Error {
sdkMessage: string;
error: null;
code: number;

/**
*
* @constructor
* @param {String} [message]
* @param {Object} [error]
*/
constructor(
message: string = MEETINGCONSTANTS.ERROR_DICTIONARY.RECLAIM_HOST_ROLE_NOT_SUPPORTED.MESSAGE,
error: any = null
) {
super(message);

if (Error.captureStackTrace) {
Error.captureStackTrace(this, ReclaimHostNotSupportedError);
}

this.name = MEETINGCONSTANTS.ERROR_DICTIONARY.RECLAIM_HOST_ROLE_NOT_SUPPORTED.NAME;
this.sdkMessage =
message || MEETINGCONSTANTS.ERROR_DICTIONARY.RECLAIM_HOST_ROLE_NOT_SUPPORTED.MESSAGE;
this.error = error;

this.code = MEETINGCONSTANTS.ERROR_DICTIONARY.RECLAIM_HOST_ROLE_NOT_SUPPORTED.CODE;
}
}

/**
* Extended Error object for reclaim host role not allowed for other participants
*/
export class ReclaimHostNotAllowedError extends Error {
sdkMessage: string;
error: null;
code: number;

/**
*
* @constructor
* @param {String} [message]
* @param {Object} [error]
*/
constructor(
message: string = MEETINGCONSTANTS.ERROR_DICTIONARY.RECLAIM_HOST_ROLE_NOT_ALLOWED.MESSAGE,
error: any = null
) {
super(message);

if (Error.captureStackTrace) {
Error.captureStackTrace(this, ReclaimHostNotAllowedError);
}

this.name = MEETINGCONSTANTS.ERROR_DICTIONARY.RECLAIM_HOST_ROLE_NOT_ALLOWED.NAME;
this.sdkMessage =
message || MEETINGCONSTANTS.ERROR_DICTIONARY.RECLAIM_HOST_ROLE_NOT_ALLOWED.MESSAGE;
this.error = error;

this.code = MEETINGCONSTANTS.ERROR_DICTIONARY.RECLAIM_HOST_ROLE_NOT_ALLOWED.CODE;
}
}

/**
* Extended Error object for reclaim host role when user is host already
*/
export class ReclaimHostIsHostAlreadyError extends Error {
sdkMessage: string;
error: null;
code: number;

/**
*
* @constructor
* @param {String} [message]
* @param {Object} [error]
*/
constructor(
message: string = MEETINGCONSTANTS.ERROR_DICTIONARY.RECLAIM_HOST_ROLE_IS_ALREADY_HOST.MESSAGE,
error: any = null
) {
super(message);

if (Error.captureStackTrace) {
Error.captureStackTrace(this, ReclaimHostIsHostAlreadyError);
}

this.name = MEETINGCONSTANTS.ERROR_DICTIONARY.RECLAIM_HOST_ROLE_IS_ALREADY_HOST.NAME;
this.sdkMessage =
message || MEETINGCONSTANTS.ERROR_DICTIONARY.RECLAIM_HOST_ROLE_IS_ALREADY_HOST.MESSAGE;
this.error = error;

this.code = MEETINGCONSTANTS.ERROR_DICTIONARY.RECLAIM_HOST_ROLE_IS_ALREADY_HOST.CODE;
}
}
31 changes: 31 additions & 0 deletions packages/@webex/plugin-meetings/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,13 @@ export const CALENDAR_EVENTS = {
DELETE: 'event:calendar.meeting.delete',
};

export const ASSIGN_ROLES_ERROR_CODES = {
ReclaimHostNotSupportedErrorCode: 2400127,
ReclaimHostNotAllowedErrorCode: 2403135,
ReclaimHostEmptyWrongKeyErrorCode: 2403136,
ReclaimHostIsHostAlreadyErrorCode: 2409150,
};

export const DEFAULT_GET_STATS_FILTER = {
types: [
'track',
Expand Down Expand Up @@ -449,6 +456,30 @@ export const ERROR_DICTIONARY = {
MESSAGE: 'Captcha is required.',
CODE: 8,
},
RECLAIM_HOST_ROLE_NOT_SUPPORTED: {
NAME: 'ReclaimHostRoleNotSupported',
MESSAGE:
'Non converged meetings, PSTN or SIP users in converged meetings are not supported currently.',
Shreyas281299 marked this conversation as resolved.
Show resolved Hide resolved
CODE: 9,
},
RECLAIM_HOST_ROLE_NOT_ALLOWED: {
NAME: 'ReclaimHostRoleNotAllowed',
MESSAGE:
'Reclaim Host Role Not Allowed For Other Participants. Participants cannot claim host role in PMR meeting, space instant meeting or escalated instant meeting. However, the original host still can reclaim host role when it manually makes another participant to be the host.',
CODE: 10,
},
RECLAIM_HOST_ROLE_EMPTY_OR_WRONG_KEY: {
NAME: 'ReclaimHostRoleEmptyOrWrongKey',
MESSAGE:
'Host Key Not Specified Or Matched. The original host can reclaim the host role without entering the host key. However, any other person who claims the host role must enter the host key to get it.',
CODE: 11,
},
RECLAIM_HOST_ROLE_IS_ALREADY_HOST: {
NAME: 'ReclaimHostRoleIsAlreadyHost',
MESSAGE:
'Participant Having Host Role Already. Participant who sends request to reclaim host role has already a host role.',
Shreyas281299 marked this conversation as resolved.
Show resolved Hide resolved
CODE: 12,
},
};

export const FLOOR_ACTION = {
Expand Down
9 changes: 9 additions & 0 deletions packages/@webex/plugin-meetings/src/member/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import MemberUtil from './util';
*/
export default class Member {
associatedUser: any;
canReclaimHost: boolean;
id: any;
isAudioMuted: any;
isContentSharing: any;
Expand Down Expand Up @@ -57,6 +58,13 @@ export default class Member {
}
| any = {}
) {
/**
* @instance
* @type {Boolean}
* @public
* @memberof Member
*/
this.canReclaimHost = false;
/**
* The server participant object
* @instance
Expand Down Expand Up @@ -250,6 +258,7 @@ export default class Member {
private processParticipant(participant: object) {
this.participant = participant;
if (participant) {
this.canReclaimHost = MemberUtil.canReclaimHost(participant);
this.id = MemberUtil.extractId(participant);
this.name = MemberUtil.extractName(participant);
this.isAudioMuted = MemberUtil.isAudioMuted(participant);
Expand Down
14 changes: 14 additions & 0 deletions packages/@webex/plugin-meetings/src/member/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,20 @@ import {IMediaStatus} from './member.types';

const MemberUtil: any = {};

/**
* @param {Object} participant the locus participant
* @returns {Boolean}
*/
MemberUtil.canReclaimHost = (participant) => {
if (!participant) {
throw new ParameterError(
'canReclaimHostRole could not be processed, participant is undefined.'
);
}

return participant.canReclaimHostRole || false;
};

/**
* @param {Object} participant the locus participant
* @returns {Boolean}
Expand Down
56 changes: 55 additions & 1 deletion packages/@webex/plugin-meetings/src/members/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@ import {isEmpty} from 'lodash';
// @ts-ignore
import {StatelessWebexPlugin} from '@webex/webex-core';

import {MEETINGS, EVENT_TRIGGERS, FLOOR_ACTION, CONTENT, WHITEBOARD} from '../constants';
import {
MEETINGS,
EVENT_TRIGGERS,
FLOOR_ACTION,
CONTENT,
WHITEBOARD,
ASSIGN_ROLES_ERROR_CODES,
} from '../constants';
import Trigger from '../common/events/trigger-proxy';
import Member from '../member';
import LoggerProxy from '../common/logs/logger-proxy';
Expand All @@ -14,6 +21,13 @@ import ParameterError from '../common/errors/parameter';
import MembersCollection from './collection';
import MembersRequest from './request';
import MembersUtil from './util';
import {ServerRoleShape} from './types';
import {
ReclaimHostEmptyWrongKeyError,
ReclaimHostIsHostAlreadyError,
ReclaimHostNotAllowedError,
ReclaimHostNotSupportedError,
} from '../common/errors/reclaim-host-role-error';

/**
* Members Update Event
Expand Down Expand Up @@ -686,6 +700,46 @@ export default class Members extends StatelessWebexPlugin {
return this.membersRequest.addMembers(options);
}

/**
* Assign role(s) to a member in the meeting
* @param {String} memberId
* @param {[ServerRoleShape]} roles - to assign an array of roles
* @returns {Promise}
* @public
* @memberof Members
*/
public assignRoles(memberId: string, roles: Array<ServerRoleShape>) {
if (!this.locusUrl) {
return Promise.reject(
new ParameterError(
'The associated locus url for this meetings members object must be defined.'
)
);
}
if (!memberId) {
return Promise.reject(
new ParameterError('The member id must be defined to assign the roles to a member.')
);
}
const options = MembersUtil.generateRoleAssignmentMemberOptions(memberId, roles, this.locusUrl);

return this.membersRequest.assignRolesMember(options).catch((error: any) => {
const errorCode = error.body?.errorCode;
switch (errorCode) {
case ASSIGN_ROLES_ERROR_CODES.ReclaimHostNotSupportedErrorCode:
return Promise.reject(new ReclaimHostNotSupportedError());
case ASSIGN_ROLES_ERROR_CODES.ReclaimHostNotAllowedErrorCode:
return Promise.reject(new ReclaimHostNotAllowedError());
case ASSIGN_ROLES_ERROR_CODES.ReclaimHostEmptyWrongKeyErrorCode:
return Promise.reject(new ReclaimHostEmptyWrongKeyError());
case ASSIGN_ROLES_ERROR_CODES.ReclaimHostIsHostAlreadyErrorCode:
return Promise.reject(new ReclaimHostIsHostAlreadyError());
default:
return Promise.reject(error);
}
});
}

/**
* Cancels an outgoing PSTN call to the associated meeting
* @param {String} invitee
Expand Down
20 changes: 20 additions & 0 deletions packages/@webex/plugin-meetings/src/members/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,26 @@ export default class MembersRequest extends StatelessWebexPlugin {
return this.request(requestParams);
}

/**
* Sends a request to assign roles to a member
* @param {Object} options
* @param {String} options.locusUrl
* @param {String} options.memberId ID of PSTN user
* @returns {Promise}
*/
assignRolesMember(options: any) {
if (!options || !options.locusUrl || !options.memberId) {
throw new ParameterError(
'memberId must be defined, and the associated locus url for this meeting object must be defined.'
);
}

const requestParams = MembersUtil.getRoleAssignmentMemberRequestParams(options);

// @ts-ignore
return this.request(requestParams);
}

removeMember(options) {
if (!options || !options.locusUrl || !options.memberId) {
throw new ParameterError(
Expand Down
Loading