UNPKG

fc-nexmo-client1

Version:
433 lines (432 loc) 16.6 kB
'use strict'; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); /* * Nexmo Client SDK * Utility functions * * Copyright (c) Nexmo Inc. */ const uuid_1 = __importDefault(require("uuid")); const socket_io_client_1 = __importDefault(require("socket.io-client")); const application_1 = __importDefault(require("./application")); const MEDIA_CONNECTIVITY_TIMEOUT = 40000; // 40s is the default timeout for ice candidates gathering const WS_CONNECTIVITY_TIMEOUT = 20000; // 20s is the default timeout for ws connection /** * Utilities class for the SDK. * * @class Utils * @private */ class Utils { /** * Get the Member from the username of a conversation * * @param {string} username the username of the member to get * @param {Conversation} conversation the Conversation to search in * @returns {Member} the requested Member * @static */ static getMemberFromNameOrNull(conversation, username) { if (!conversation || !username) return null; for (let member of conversation.members.values()) { if (member.user.name === username) { return member; } } return null; } /** * Get the Member's number or uri from the event's channel field * * @param {object} channel the event's channel field * @returns {string} the requested Member number or uri * @static */ static getMemberNumberFromEventOrNull(channel) { const from = channel && channel.from; if (from && (from.number || from.uri)) { return from.number || from.uri; } return null; } /** * Perform a network request to the given url * * @param {object} reqObject the object that has all the information for the request * @param {string} url the request url * @param {string} type=GET|POST|PUT|DELETE the types of the network request * @param {object} [data] the data that are going to be sent * @param {string} [responseType] the response type of the request * @param {string} token the jwt token for the network request * @returns {Promise<NetworkRequestResponse>} the NetworkRequestResponse * @static */ static networkRequest(reqObject) { return new Promise((resolve, reject) => { if (!reqObject.token && !reqObject.url.includes('logging') && !reqObject.url.includes('ping')) { // eslint-disable-next-line prefer-promise-reject-errors reject({ response: { type: 'error:user:token', description: 'network error on request. Please create a new session.' } }); } const xhr = new XMLHttpRequest(); let data; xhr.open(reqObject.type, reqObject.url, true); if (reqObject.token) { xhr.setRequestHeader('Authorization', 'Bearer ' + reqObject.token); } if (reqObject && reqObject.url.includes('image')) { xhr.responseType = ''; data = reqObject.data; xhr.onloadstart = () => { resolve(xhr); }; } else { xhr.responseType = reqObject.responseType || 'json'; data = JSON.stringify(reqObject.data) || null; xhr.setRequestHeader('Content-type', 'application/json; charset=utf-8'); } xhr.onload = () => { if (xhr.status === 200 || xhr.status === 201 || xhr.status === 204) { resolve(xhr); } else { reject(xhr); } }; xhr.onerror = (error) => { reject(error); }; xhr.send(data); }); } /** * Perform a GET network request for fetching paginated conversations and events * * @param {string} url the request url * @param {object} [params] network request params * @param {string} [params.cursor] cursor parameter to access the next or previous page of a data set * @param {number} [params.page_size] the number of resources returned in a single request list * @param {string} [params.order] 'asc' or 'desc' ordering of resources (usually based on creation time) * @param {string} [params.event_type] the type of event used to filter event requests ('member:joined', 'audio:dtmf', etc) * @param {string} token the jwt token for the network request * @param {string} [version=Application.CONVERSATION_API_VERSION.v1] version of conversation service that is used for the request (one of v1 and v3) * * @returns {Promise<XMLHttpRequest.response>} the XMLHttpRequest * @static * @example <caption>Sending a nexmo GET request</caption> * paginationRequest(url, params).then((response) => { * response.items: {}, * response.cursor: { * prev: '', * next: '', * self: '' * }, * response.page_size: 10, * response.order: 'asc', * }); */ static async paginationRequest(url, params, token, version = application_1.default.CONVERSATION_API_VERSION.v1) { try { const xhr = await Utils.networkRequest({ type: 'GET', url: Utils.addUrlSearchParams(url, params), token }); const { page_size, _embedded, _links } = xhr.response; const resource = url.split('/').pop().trim(); return { items: (version === application_1.default.CONVERSATION_API_VERSION.v1) ? _embedded.data[resource] : _embedded[resource], cursor: { prev: _links.prev ? new URLSearchParams(_links.prev.href).get('cursor') : '', next: _links.next ? new URLSearchParams(_links.next.href).get('cursor') : '', self: _links.self ? new URLSearchParams(_links.self.href).get('cursor') : '' }, page_size: page_size, order: params.order || 'asc', event_type: params.event_type || null }; } catch ({ response }) { const parsed_error = response ? response : { type: 'error:network:get-request', description: 'network error on nexmo get request' }; if (parsed_error.validation) { parsed_error.description = parsed_error.validation[Object.keys(parsed_error.validation)[0]]; } throw parsed_error; } } /** * Update the Search Params of a url * @returns {string} the appended url * @static */ static addUrlSearchParams(url, params = {}) { let appended_url = new URL(url); Object.keys(params).forEach((key) => { if (params[key] && !(typeof params[key] === 'string' && params[key].length < 1) && params[key] !== null) { appended_url.searchParams.set(key, params[key]); } }); return appended_url.href; } /** * Deep merges two objects * @returns {Object} the new merged object * @static */ static deepMergeObj(obj1, obj2) { const mergedObj = JSON.parse(JSON.stringify(obj1)); // Merge the object into the new mergedObject for (let prop in obj2) { // If the property is an object then merge properties if (Object.prototype.toString.call(obj2[prop]) === '[object Object]') { mergedObj[prop] = Utils.deepMergeObj(mergedObj[prop], obj2[prop]); } else { mergedObj[prop] = obj2[prop]; } } return mergedObj; } /** * Inject a script into the document * * @param {string} s script being executed * @param {requestCallback} c the callback fired after script executed * @static */ static injectScript(u, c) { if (typeof document !== 'undefined') { let h = document.getElementsByTagName('head')[0]; let s = document.createElement('script'); s.async = true; s.src = u; s.onload = s.onreadystatechange = () => { if (!s.readyState || /loaded|complete/.test(s.readyState)) { s.onload = s.onreadystatechange = null; s = null; if (c) { c(); } } }; h.insertBefore(s, h.firstChild); } } static allocateUUID() { return uuid_1.default.v4(); } /** * Validate dtmf digit * @static */ static validateDTMF(digit) { return typeof digit === 'string' ? /^[\da-dA-D#*pP]{1,45}$$/.test(digit) : false; } /** * Get the nexmo bugsnag api key * @private */ static _getBugsnagKey() { return '76498fc1ca8d9b0a173a44e2b873d7ed'; } /** * Update the member legs array with the new one received in the event * * @param {Array} legs the member legs array * @param {NXMEvent} event the member event holding the new legs array * @static */ static updateMemberLegs(legs, event) { if (legs) { // find the leg in the legs array if exists const leg = legs.find((leg) => leg.leg_id === event.body.leg_id); if (!leg) { legs.push({ leg_id: event.body.leg_id, status: event.body.status }); } else if (leg.status !== event.body.status) { // if the status of the leg is different from the event status // update the leg object with the new leg status let index = legs.indexOf(leg); legs.fill(leg.status = event.body.status, index, index++); } } else { legs = [{ leg_id: event.body.leg_id, status: event.body.status }]; } return legs; } /** * Check if the event is referenced to a call or simple conversation * @private */ static _isCallEvent(event) { const { channel, media } = event.body; // in case we have a transfer we should fetch the conversation // including the new membership if (event.type === "rtc:transfer") return true; // this check differentiates the call flow with the non call // IP-PSTN (member:joined) should have an knocking_id inside the channel // PSTN-IP and IP-IP (member:invited) should have audio_settings.enabled = true if (channel && ((media && media.audio_settings && media.audio_settings.enabled) || (media && media.audio && media.audio.enabled) || channel.knocking_id)) { return true; } return false; } /** * Fetch an image from Media Service * @private */ static async _fetchImage(url, token) { const { response } = await Utils.networkRequest({ type: 'GET', url, responseType: 'arraybuffer', token }); const responseArray = new Uint8Array(response); // Convert the int array to a binary String // We have to use apply() as we are converting an *array* // and String.fromCharCode() takes one or more single values, not // an array. // support large image files (Chunking) let res = ''; const chunk = 8 * 1024; let i; for (i = 0; i < responseArray.length / chunk; i++) { res += String.fromCharCode.apply(null, responseArray.subarray(i * chunk, (i + 1) * chunk)); } res += String.fromCharCode.apply(null, responseArray.subarray(i * chunk)); return 'data:image/jpeg;base64,' + btoa(res); } /** * Check if HTTP URL is reachable * @private */ static async _checkHttpConnectivity(url) { const timeBeforeConnecting = Date.now(); try { await Utils.networkRequest({ type: 'GET', url }); const connectionTime = Date.now() - timeBeforeConnecting; return { url, canConnect: true, connectionTime }; } catch (error) { return { url, canConnect: false, error }; } } /** * Check if websocket URL is reachable * @private */ static _checkWsConnectivity(ws_url, path, config) { return new Promise((resolve, reject) => { const socket_io_config = Object.assign({ path }, config); const timeBeforeConnecting = Date.now(); const connection = socket_io_client_1.default.connect(ws_url, socket_io_config); const timeout = setTimeout(() => resolve({ url: ws_url, canConnect: false }), WS_CONNECTIVITY_TIMEOUT); connection.on('connect', () => { const connectionTime = Date.now() - timeBeforeConnecting; connection.disconnect(); clearTimeout(timeout); resolve({ url: ws_url, canConnect: true, connectionTime }); }); connection.on('error', (error) => { connection.disconnect(); clearTimeout(timeout); resolve({ url: ws_url, canConnect: false, error }); }); }); } /** * Return a list with the connection health of the Media Servers * @private */ static async _checkMediaServers(token, nexmo_api_url, datacenter) { try { const { response } = await Utils.networkRequest({ type: 'GET', url: `${nexmo_api_url}/v0.3/discovery/media/${datacenter}`, token }); const reqList = response.map((host) => Utils._checkMediaConnectivity(host.ip, host.port)); return await Promise.all(reqList); } catch (error) { return []; } } /** * Check if we can establish a peer connection with a specific Media Server * @private */ static async _checkMediaConnectivity(ip, port) { return new Promise(async (resolve, reject) => { const configuration = { iceServers: [{ urls: `stun:${ip}:${port}` }] }; const pc = new RTCPeerConnection(configuration); const timeBeforeConnecting = Date.now(); const offer = await pc.createOffer({ offerToReceiveAudio: true }); pc.setLocalDescription(offer); const timeout = setTimeout(() => { pc.close(); resolve({ ip, canConnect: false }); }, MEDIA_CONNECTIVITY_TIMEOUT); pc.onicecandidate = ({ candidate }) => { if ((candidate === null || candidate === void 0 ? void 0 : candidate.type) === "srflx") { const connectionTime = Date.now() - timeBeforeConnecting; // Connection established successfully clearTimeout(timeout); pc.close(); resolve({ ip, canConnect: true, connectionTime }); } }; pc.onicecandidateerror = (event) => { if (event.errorCode) { pc.close(); clearTimeout(timeout); resolve({ ip, canConnect: false, error: event }); } }; }); } /** * Check if the user is re invited to an existing conversation * @private */ static _checkIfUserIsReInvited(conversations, event) { var _a; if (!conversations.has(event.cid)) return false; if (!(event.type === 'member:invited' || event.type === 'member:joined')) return false; const me = (_a = conversations.get(event.cid)) === null || _a === void 0 ? void 0 : _a.me; if (!me) return false; if (me.user.name === event.body.user.name && me.state === 'LEFT') return true; return false; } } exports.default = Utils; module.exports = Utils;