import lodash from 'lodash'; import { HTTP_VERBS, DESTINATION_TYPE, WBXAPPAPI_SERVICE, DEFAULT_MEETING_INFO_REQUEST_BODY, } from '../constants'; import Metrics from '../metrics'; import BEHAVIORAL_METRICS from '../metrics/constants'; import MeetingInfoUtil from './utilv2'; const PASSWORD_ERROR_DEFAULT_MESSAGE = 'Password required. Call fetchMeetingInfo() with password argument'; const CAPTCHA_ERROR_DEFAULT_MESSAGE = 'Captcha required. Call fetchMeetingInfo() with captchaInfo argument'; const ADHOC_MEETING_DEFAULT_ERROR = 'Failed starting the adhoc meeting, Please contact support team '; const MEETING_IS_IN_PROGRESS_MESSAGE = 'Meeting is in progress'; const STATIC_MEETING_LINK_ALREADY_EXISTS_MESSAGE = 'Static meeting link already exists'; const FETCH_STATIC_MEETING_LINK = 'Meeting link does not exists for conversation'; const CAPTCHA_ERROR_REQUIRES_PASSWORD_CODES = [423005, 423006]; const CAPTCHA_ERROR_REQUIRES_REGISTRATION_ID_CODES = [423007]; const POLICY_ERROR_CODES = [403049, 403104, 403103, 403048, 403102, 403101]; const JOIN_FORBIDDEN_CODES = [403003]; /** * 403021 - Meeting registration is required * 403022 - Meeting registration is still pending * 403024 - Meeting registration have been rejected * 403137 - Registration ID verified failure * 423007 - Registration ID input too many time,please input captcha code * 403026 - Need to join meeting via webcast * 403037 - Meeting join required registration ID * 403137 - Registration ID verified failure * */ const JOIN_WEBINAR_ERROR_CODES = [403021, 403022, 403024, 403137, 423007, 403026, 403037, 403137]; /** * Error to indicate that wbxappapi requires a password */ export class MeetingInfoV2PasswordError extends Error { meetingInfo: any; sdkMessage: any; wbxAppApiCode: any; body: any; /** * * @constructor * @param {Number} [wbxAppApiErrorCode] * @param {Object} [meetingInfo] * @param {String} [message] */ constructor( wbxAppApiErrorCode?: number, meetingInfo?: object, message: string = PASSWORD_ERROR_DEFAULT_MESSAGE ) { super(`${message}, code=${wbxAppApiErrorCode}`); this.name = 'MeetingInfoV2PasswordError'; this.sdkMessage = message; this.stack = new Error().stack; this.wbxAppApiCode = wbxAppApiErrorCode; this.meetingInfo = meetingInfo; } } /** * Error generating a adhoc space meeting */ export class MeetingInfoV2AdhocMeetingError extends Error { sdkMessage: any; wbxAppApiCode: any; /** * * @constructor * @param {Number} [wbxAppApiErrorCode] * @param {String} [message] */ constructor(wbxAppApiErrorCode?: number, message: string = ADHOC_MEETING_DEFAULT_ERROR) { super(`${message}, code=${wbxAppApiErrorCode}`); this.name = 'MeetingInfoV2AdhocMeetingError'; this.sdkMessage = message; this.stack = new Error().stack; this.wbxAppApiCode = wbxAppApiErrorCode; } } /** * Error preventing join because of a meeting policy */ export class MeetingInfoV2PolicyError extends Error { meetingInfo: object; sdkMessage: string; wbxAppApiCode: number; /** * * @constructor * @param {Number} [wbxAppApiErrorCode] * @param {Object} [meetingInfo] * @param {String} [message] */ constructor(wbxAppApiErrorCode?: number, meetingInfo?: object, message?: string) { super(`${message}, code=${wbxAppApiErrorCode}`); this.name = 'MeetingInfoV2AdhocMeetingError'; this.sdkMessage = message; this.stack = new Error().stack; this.wbxAppApiCode = wbxAppApiErrorCode; this.meetingInfo = meetingInfo; } } /** * Error to indicate that preferred webex site not present to start adhoc meeting */ export class MeetingInfoV2CaptchaError extends Error { captchaInfo: any; isPasswordRequired: any; isRegistrationIdRequired: any; sdkMessage: any; wbxAppApiCode: any; body: any; /** * * @constructor * @param {Number} [wbxAppApiErrorCode] * @param {Object} [captchaInfo] * @param {String} [message] */ constructor( wbxAppApiErrorCode?: number, captchaInfo?: object, message: string = CAPTCHA_ERROR_DEFAULT_MESSAGE ) { super(`${message}, code=${wbxAppApiErrorCode}`); this.name = 'MeetingInfoV2PasswordError'; this.sdkMessage = message; this.stack = new Error().stack; this.wbxAppApiCode = wbxAppApiErrorCode; this.isPasswordRequired = CAPTCHA_ERROR_REQUIRES_PASSWORD_CODES.includes(wbxAppApiErrorCode); this.isRegistrationIdRequired = CAPTCHA_ERROR_REQUIRES_REGISTRATION_ID_CODES.includes(wbxAppApiErrorCode); this.captchaInfo = captchaInfo; } } /** * Error preventing join because of a webinar have some error */ export class MeetingInfoV2JoinWebinarError extends Error { meetingInfo: any; sdkMessage: any; wbxAppApiCode: any; body: any; /** * * @constructor * @param {Number} [wbxAppApiErrorCode] * @param {Object} [meetingInfo] * @param {String} [message] */ constructor(wbxAppApiErrorCode?: number, meetingInfo?: object, message?: string) { super(`${message}, code=${wbxAppApiErrorCode}`); this.name = 'MeetingInfoV2JoinWebinarError'; this.sdkMessage = message; this.stack = new Error().stack; this.wbxAppApiCode = wbxAppApiErrorCode; this.meetingInfo = meetingInfo; } } /** * Error preventing join because of a forbidden error */ export class MeetingInfoV2JoinForbiddenError extends Error { meetingInfo: any; sdkMessage: any; wbxAppApiCode: any; body: any; /** * * @constructor * @param {Number} [wbxAppApiErrorCode] * @param {Object} [meetingInfo] * @param {String} [message] */ constructor(wbxAppApiErrorCode?: number, meetingInfo?: object, message?: string) { super(`${message}, code=${wbxAppApiErrorCode}`); this.name = 'MeetingInfoV2JoinForbiddenError'; this.sdkMessage = message; this.stack = new Error().stack; this.wbxAppApiCode = wbxAppApiErrorCode; this.meetingInfo = meetingInfo; } } /** * Error fetching static link for a conversation when it does not exist */ export class MeetingInfoV2StaticLinkDoesNotExistError extends Error { sdkMessage: any; wbxAppApiCode: any; body: any; /** * * @constructor * @param {Number} [wbxAppApiErrorCode] * @param {String} [message] */ constructor(wbxAppApiErrorCode?: number, message: string = FETCH_STATIC_MEETING_LINK) { super(`${message}, code=${wbxAppApiErrorCode}`); this.name = 'MeetingInfoV2StaticLinkDoesNotExistError'; this.sdkMessage = message; this.stack = new Error().stack; this.wbxAppApiCode = wbxAppApiErrorCode; } } /** * Error enabling/disabling static meeting link */ export class MeetingInfoV2MeetingIsInProgressError extends Error { sdkMessage: any; wbxAppApiCode: any; body: any; /** * * @constructor * @param {Number} [wbxAppApiErrorCode] * @param {String} [message] * @param {Boolean} [enable] */ constructor( wbxAppApiErrorCode?: number, message = MEETING_IS_IN_PROGRESS_MESSAGE, enable = false ) { super(`${message}, code=${wbxAppApiErrorCode}, enable=${enable}`); this.name = 'MeetingInfoV2MeetingIsInProgressError'; this.sdkMessage = message; this.stack = new Error().stack; this.wbxAppApiCode = wbxAppApiErrorCode; } } /** * Error enabling/disabling static meeting link */ export class MeetingInfoV2StaticMeetingLinkAlreadyExists extends Error { sdkMessage: any; wbxAppApiCode: any; body: any; /** * * @constructor * @param {Number} [wbxAppApiErrorCode] * @param {String} [message] */ constructor(wbxAppApiErrorCode?: number, message = STATIC_MEETING_LINK_ALREADY_EXISTS_MESSAGE) { super(`${message}, code=${wbxAppApiErrorCode}`); this.name = 'MeetingInfoV2StaticMeetingLinkAlreadyExists'; this.sdkMessage = message; this.stack = new Error().stack; this.wbxAppApiCode = wbxAppApiErrorCode; } } /** * @class MeetingInfo */ export default class MeetingInfoV2 { webex: any; /** * * @param {WebexSDK} webex */ constructor(webex) { this.webex = webex; } /** * converts hydra id into conversation url and persons Id * @param {String} destination one of many different types of destinations to look up info for * @param {String} [type] to match up with the destination value * @returns {Promise} destination and type * @public * @memberof MeetingInfo */ fetchInfoOptions(destination: string, type: string = null) { return MeetingInfoUtil.getDestinationType({ destination, type, webex: this.webex, }); } /** * Raises a MeetingInfoV2PolicyError for policy error codes * @param {any} err the error from the request * @returns {void} */ handlePolicyError = (err) => { if (!err.body) { return; } if (POLICY_ERROR_CODES.includes(err.body?.code)) { Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MEETING_INFO_POLICY_ERROR, { code: err.body?.code, }); throw new MeetingInfoV2PolicyError( err.body?.code, err.body?.data?.meetingInfo, err.body?.message ); } }; /** * Raises a handleJoinWebinarError for join webinar error codes * @param {any} err the error from the request * @returns {void} */ handleJoinWebinarError = (err) => { if (!err.body) { return; } if (JOIN_WEBINAR_ERROR_CODES.includes(err.body?.code)) { Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.JOIN_WEBINAR_ERROR, { code: err.body?.code, }); throw new MeetingInfoV2JoinWebinarError( err.body?.code, err.body?.data?.meetingInfo, err.body?.message ); } }; /** * Raises a handleForbiddenError for join meeting forbidden error * @param {any} err the error from the request * @returns {void} */ handleForbiddenError = (err) => { if (!err.body) { return; } if (JOIN_FORBIDDEN_CODES.includes(err.body?.code)) { Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.JOIN_FORBIDDEN_ERROR, { code: err.body?.code, }); throw new MeetingInfoV2JoinForbiddenError( err.body?.code, err.body?.data?.meetingInfo, err.body?.message ); } }; /** * helper function to either create an adhoc space meeting or enable static meeting link * @param {String} conversationUrl conversationUrl to start adhoc meeting on * @param {String} installedOrgID org ID of user's machine * @param {Boolean} enableStaticMeetingLink whether or not to enable static meeting link * @param {String} classificationId need it to start adhoc meeting if space support classification * @returns {Promise} returns a meeting info object * @public * @memberof MeetingInfo */ async createAdhocSpaceMeetingOrEnableStaticMeetingLink( conversationUrl: string, installedOrgID?: string, // setting this to true enables static meeting link enableStaticMeetingLink = false, classificationId = undefined ) { const getInvitees = (particpants = []) => { const invitees = []; if (particpants) { particpants.forEach((participant) => { invitees.push({ email: participant.emailAddress, ciUserUuid: participant.entryUUID, }); }); } return invitees; }; return this.webex .request({uri: conversationUrl, qs: {includeParticipants: true}, disableTransform: true}) .then(({body: conversation}) => { const body = { title: conversation.displayName, spaceUrl: conversation.url, keyUrl: conversation.encryptionKeyUrl, kroUrl: conversation.kmsResourceObjectUrl, invitees: getInvitees(conversation.participants?.items), installedOrgID, schedule: enableStaticMeetingLink, classificationId, }; if (installedOrgID) { body.installedOrgID = installedOrgID; } const uri = this.webex.meetings.preferredWebexSite ? `https://${this.webex.meetings.preferredWebexSite}/wbxappapi/v2/meetings/spaceInstant` : ''; return this.webex.request({ method: HTTP_VERBS.POST, uri, body, }); }); } /** * Creates adhoc space meetings for a space by fetching the conversation infomation * @param {String} conversationUrl conversationUrl to start adhoc meeting on * @param {String} installedOrgID org ID of user's machine * @param {String} classificationId if space is support classification, it needs provide it during start instant meeting * @returns {Promise} returns a meeting info object * @public * @memberof MeetingInfo */ async createAdhocSpaceMeeting( conversationUrl: string, installedOrgID?: string, classificationId?: string ) { if (!this.webex.meetings.preferredWebexSite) { throw Error('No preferred webex site found'); } return this.createAdhocSpaceMeetingOrEnableStaticMeetingLink( conversationUrl, installedOrgID, false, classificationId ) .then((requestResult) => { Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADHOC_MEETING_SUCCESS); return requestResult; }) .catch((err) => { this.handlePolicyError(err); this.handleJoinWebinarError(err); this.handleForbiddenError(err); Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADHOC_MEETING_FAILURE, { reason: err.message, stack: err.stack, }); throw new MeetingInfoV2AdhocMeetingError(err.body?.code, err.body?.message); }); } /** * Fetches details for static meeting link * @param {String} conversationUrl conversationUrl that's required to find static meeting link if it exists * @returns {Promise} returns a Promise * @public * @memberof MeetingInfo */ async fetchStaticMeetingLink(conversationUrl: string) { if (!this.webex.meetings.preferredWebexSite) { throw Error('No preferred webex site found'); } const body = { spaceUrl: conversationUrl, }; const uri = this.webex.meetings.preferredWebexSite ? `https://${this.webex.meetings.preferredWebexSite}/wbxappapi/v2/meetings/spaceInstant/query` : ''; return this.webex .request({ method: HTTP_VERBS.POST, uri, body, }) .then((requestResult) => { Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.FETCH_STATIC_MEETING_LINK_SUCCESS); return requestResult; }) .catch((err) => { if (err?.statusCode === 403) { Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MEETING_LINK_DOES_NOT_EXIST_ERROR, { reason: err.message, stack: err.stack, }); throw new MeetingInfoV2StaticLinkDoesNotExistError(err.body?.code, err.body?.message); } Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.FETCH_STATIC_MEETING_LINK_FAILURE, { reason: err.message, stack: err.stack, }); throw err; }); } /** * Enables static meeting link * @param {String} conversationUrl conversationUrl that's required to enable static meeting link * @returns {Promise} returns a Promise * @public * @memberof MeetingInfo */ async enableStaticMeetingLink(conversationUrl: string) { if (!this.webex.meetings.preferredWebexSite) { throw Error('No preferred webex site found'); } return this.createAdhocSpaceMeetingOrEnableStaticMeetingLink(conversationUrl, undefined, true) .then((requestResult) => { Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ENABLE_STATIC_METTING_LINK_SUCCESS); return requestResult; }) .catch((err) => { if (err?.statusCode === 403) { Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MEETING_IS_IN_PROGRESS_ERROR, { reason: err.message, stack: err.stack, }); throw new MeetingInfoV2MeetingIsInProgressError(err.body?.code, err.body?.message, true); } if (err?.statusCode === 409) { Metrics.sendBehavioralMetric( BEHAVIORAL_METRICS.STATIC_MEETING_LINK_ALREADY_EXISTS_ERROR, { reason: err.message, stack: err.stack, } ); throw new MeetingInfoV2StaticMeetingLinkAlreadyExists(err.body?.code, err.body?.message); } Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ENABLE_STATIC_METTING_LINK_FAILURE, { reason: err.message, stack: err.stack, }); throw err; }); } /** * Disables static meeting link for given conversation url * @param {String} conversationUrl conversationUrl that's required to disable static meeting link if it exists * @returns {Promise} returns a Promise * @public * @memberof MeetingInfo */ async disableStaticMeetingLink(conversationUrl: string) { if (!this.webex.meetings.preferredWebexSite) { throw Error('No preferred webex site found'); } const body = { spaceUrl: conversationUrl, }; const uri = this.webex.meetings.preferredWebexSite ? `https://${this.webex.meetings.preferredWebexSite}/wbxappapi/v2/meetings/spaceInstant/deletePersistentMeeting` : ''; return this.webex .request({ method: HTTP_VERBS.POST, uri, body, }) .then((requestResult) => { Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.DISABLE_STATIC_MEETING_LINK_SUCCESS); return requestResult; }) .catch((err) => { if (err?.statusCode === 403) { Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MEETING_IS_IN_PROGRESS_ERROR, { reason: err.message, stack: err.stack, }); throw new MeetingInfoV2MeetingIsInProgressError(err.body?.code, err.body?.message); } Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.DISABLE_STATIC_MEETING_LINK_FAILURE, { reason: err.message, stack: err.stack, }); throw err; }); } /** * Fetches meeting info from the server * @param {String} destination one of many different types of destinations to look up info for * @param {DESTINATION_TYPE} [type] to match up with the destination value * @param {String} password * @param {Object} captchaInfo * @param {String} captchaInfo.code * @param {String} captchaInfo.id * @param {String} installedOrgID org ID of user's machine * @param {String} locusId * @param {Object} extraParams * @param {Object} options * @param {String} registrationId * @param {String} fullSiteUrl * @param {String} classificationId * @returns {Promise} returns a meeting info object * @public * @memberof MeetingInfo */ async fetchMeetingInfo( destination: string, type: DESTINATION_TYPE = null, password: string = null, captchaInfo: { code: string; id: string; } = null, installedOrgID = null, locusId = null, extraParams: object = {}, options: {meetingId?: string; sendCAevents?: boolean} = {}, registrationId: string = null, fullSiteUrl: string = null, classificationId: string = null ) { const {meetingId, sendCAevents} = options; const destinationType = await MeetingInfoUtil.getDestinationType({ destination, type, webex: this.webex, }); if ( destinationType.type === DESTINATION_TYPE.CONVERSATION_URL && this.webex.config.meetings.experimental.enableAdhocMeetings && this.webex.meetings.preferredWebexSite ) { return this.createAdhocSpaceMeeting( destinationType.destination, installedOrgID, classificationId ); } const body = await MeetingInfoUtil.getRequestBody({ ...destinationType, password, captchaInfo, installedOrgID, locusId, extraParams, registrationId, disableWebRedirect: true, }); // If the body only contains the default properties, we don't have enough to // fetch the meeting info so don't bother trying. if ( !lodash.difference(Object.keys(body), Object.keys(DEFAULT_MEETING_INFO_REQUEST_BODY)).length ) { const err = new Error('Not enough information to fetch meeting info'); Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.FETCH_MEETING_INFO_V1_FAILURE, { reason: err.message, destinationType: destinationType?.type, webExMeetingId: destinationType?.info?.webExMeetingId, sipUri: destinationType?.info?.sipUri, }); throw err; } const requestOptions: any = { method: HTTP_VERBS.POST, body, }; const directURI = await MeetingInfoUtil.getDirectMeetingInfoURI(destinationType); if (fullSiteUrl) { requestOptions.uri = `https://${fullSiteUrl}/wbxappapi/v1/meetingInfo`; } else if (directURI) { requestOptions.uri = directURI; } else { requestOptions.service = WBXAPPAPI_SERVICE; requestOptions.resource = 'meetingInfo'; } if (meetingId && sendCAevents) { this.webex.internal.newMetrics.submitInternalEvent({ name: 'internal.client.meetinginfo.request', }); this.webex.internal.newMetrics.submitClientEvent({ name: 'client.meetinginfo.request', options: { meetingId, }, }); } return this.webex .request(requestOptions) .then((response) => { if (meetingId && sendCAevents) { this.webex.internal.newMetrics.submitInternalEvent({ name: 'internal.client.meetinginfo.response', }); this.webex.internal.newMetrics.submitClientEvent({ name: 'client.meetinginfo.response', payload: { identifiers: { meetingLookupUrl: response?.url, }, }, options: { meetingId, webexConferenceIdStr: response?.body?.confIdStr || response?.body?.confID, globalMeetingId: response?.body?.meetingId, }, }); } Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.FETCH_MEETING_INFO_V1_SUCCESS); return response; }) .catch((err) => { if (meetingId && sendCAevents) { this.webex.internal.newMetrics.submitInternalEvent({ name: 'internal.client.meetinginfo.response', }); this.webex.internal.newMetrics.submitClientEvent({ name: 'client.meetinginfo.response', payload: { identifiers: { meetingLookupUrl: err?.url, }, }, options: { meetingId, rawError: err, }, }); } if (err?.statusCode === 403) { this.handlePolicyError(err); this.handleJoinWebinarError(err); this.handleForbiddenError(err); Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.VERIFY_PASSWORD_ERROR, { reason: err.message, stack: err.stack, }); throw new MeetingInfoV2PasswordError(err.body?.code, err.body?.data?.meetingInfo); } if (err?.statusCode === 423) { Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.VERIFY_CAPTCHA_ERROR, { reason: err.message, stack: err.stack, }); throw new MeetingInfoV2CaptchaError(err.body?.code, { captchaId: err.body.captchaID, verificationImageURL: err.body.verificationImageURL, verificationAudioURL: err.body.verificationAudioURL, refreshURL: err.body.refreshURL, }); } Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.FETCH_MEETING_INFO_V1_FAILURE, { reason: err.message, stack: err.stack, }); throw err; }); } }