'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
* Application Object Model
*
* Copyright (c) Nexmo Inc.
*/
const WildEmitter = require('wildemitter');
const loglevel_1 = require("loglevel");
const nexmoClientError_1 = require("./nexmoClientError");
const user_1 = __importDefault(require("./user"));
const conversation_1 = __importDefault(require("./conversation"));
const nxmCall_1 = __importDefault(require("./modules/nxmCall"));
const sip_events_1 = __importDefault(require("./handlers/sip_events"));
const rtc_events_1 = __importDefault(require("./handlers/rtc_events"));
const application_events_1 = __importDefault(require("./handlers/application_events"));
const utils_1 = __importDefault(require("./utils"));
const page_config_1 = __importDefault(require("./pages/page_config"));
const conversations_page_1 = __importDefault(require("./pages/conversations_page"));
const user_sessions_page_1 = __importDefault(require("./pages/user_sessions_page"));
const events_queue_1 = require("./handlers/events_queue");
const member_1 = __importDefault(require("./member"));
let sipEventHandler = null;
let rtcEventHandler = null;
let applicationEventsHandler = null;
/**
* Core application class for the SDK.
* Application is the parent object holding the list of conversations, the session object.
* Provides methods to create conversations and retrieve a list of the user's conversations, while it holds the listeners for
* user's invitations
* @class Application
* @param {NexmoClient} SDK session Object
* @param {object} params
* @example <caption>Accessing the list of conversations</caption>
* rtc.createSession(token).then((application) => {
* console.log(application.conversations);
* console.log(application.me.name, application.me.id);
* }).catch((error) => {
* console.error(error);
* });
* @emits Application#member:invited
* @emits Application#member:joined
* @emits Application#NXM-errors
* @emits Application#rtcstats:analytics
*/
class Application {
constructor(session, params) {
this.log = loglevel_1.getLogger(this.constructor.name);
this.session = session;
this.conversations = new Map();
this.synced_conversations_count = 0;
this.start_sync_time = 0;
this.stop_sync_time = 0;
// conversation_id, nxmCall
this.calls = new Map();
// knocking_id, nxmCall
this._call_draft_list = new Map();
this.pageConfig = new page_config_1.default((session.config || {}).conversations_page_config);
this.conversations_page_last = null;
this.activeStreams = [];
sipEventHandler = new sip_events_1.default(this);
rtcEventHandler = new rtc_events_1.default(this);
applicationEventsHandler = new application_events_1.default(this);
this.me = null;
Object.assign(this, params);
WildEmitter.mixin(Application);
}
/**
* Update Conversation instance or create a new one.
*
* Pre-created conversation exist from getConversations
* like initialised templates. When we explicitly ask to
* getConversation(), we receive members and other details
*
* @param {object} payload Conversation payload
* @private
*/
updateOrCreateConversation(payload) {
const conversation = this.conversations.get(payload.id);
if (conversation) {
conversation._updateObjectInstance(payload);
this.conversations.set(payload.id, conversation);
}
else {
this.conversations.set(payload.id, new conversation_1.default(this, payload));
}
return this.conversations.get(payload.id);
}
/**
* Application listening for member invited events.
*
* @event Application#member:invited
*
* @property {Member} member - The invited member
* @property {NXMEvent} event - The invitation event
*
* @example <caption>listen for member invited events on Application level</caption>
* application.on("member:invited",(member, event) => {
* console.log("Invited to the conversation: " + event.conversation.display_name || event.conversation.name);
* // identify the sender.
* console.log("Invited by: " + member.invited_by);
* //accept an invitation.
* application.conversations.get(event.conversation.id).join();
* //decline the invitation.
* application.conversations.get(event.conversation.id).leave();
* });
*/
/**
* Application listening for member joined events.
*
* @event Application#member:joined
*
* @property {Member} member - the member that joined the conversation
* @property {NXMEvent} event - the join event
*
* @example <caption>listen for member joined events on Application level</caption>
* application.on("member:joined",(member, event) => {
* console.log("JOINED", "Joined conversation: " + event.conversation.display_name || event.conversation.name);
* });
*/
/**
* Entry point for queing events in Application level
* @private
*/
async _enqueueEvent(response) {
if (this.session.config.enableEventsQueue) {
if (!this.eventsQueue) {
this.eventsQueue = new events_queue_1.EventsQueue((event) => this._handleEvent(event));
}
this.eventsQueue.enqueue(response, this);
}
else {
this._handleEvent(response);
}
}
/**
* Entry point for events in Application level
* @private
*/
async _handleEvent(event) {
var _a, _b, _c, _d, _e, _f, _g;
const isEventFromMe = ((_a = event._embedded) === null || _a === void 0 ? void 0 : _a.from_user) ? ((_c = (_b = event._embedded) === null || _b === void 0 ? void 0 : _b.from_user) === null || _c === void 0 ? void 0 : _c.id) === ((_d = this.me) === null || _d === void 0 ? void 0 : _d.id)
: ((_f = (_e = event.body) === null || _e === void 0 ? void 0 : _e.user) === null || _f === void 0 ? void 0 : _f.user_id) === ((_g = this.me) === null || _g === void 0 ? void 0 : _g.id);
// check if user is already part of the conversation and if it has a member on a valid
// state (INVITED, JOINED) otherwise user is being re-invited and we need to fetch the
// conversation and members info again
const isUserReInvited = utils_1.default._checkIfUserIsReInvited(this.conversations, event);
if (event.type.startsWith('sip')) {
sipEventHandler._handleSipCallEvent(event);
return event;
}
if (this.conversations.has(event.cid) && event.type !== "rtc:transfer" && !isUserReInvited) {
if (event.type.startsWith('rtc')) {
rtcEventHandler._handleRtcEvent(event);
}
this.conversations.get(event.cid)._handleEvent(event);
if ((event.type === 'member:joined' || event.type === 'member:invited') && isEventFromMe) {
this._handleApplicationEvent(event);
}
return event;
}
else {
// if event has cid get the conversation you don't know about (case: joined by another user)
if (event.cid) {
try {
if (isUserReInvited)
this.conversations.delete(event.cid);
let conversation;
if (utils_1.default._isCallEvent(event)) {
conversation = await this.getConversation(event.cid, Application.CONVERSATION_API_VERSION.v1);
}
else {
conversation = await this.getConversation(event.cid, Application.CONVERSATION_API_VERSION.v3);
}
this.conversations.set(event.cid, conversation);
await conversation._handleEvent(event);
await this._handleApplicationEvent(event);
if (event.type.startsWith("rtc")) {
rtcEventHandler._handleRtcEvent(event);
}
return Promise.resolve(event);
}
catch (error) {
this.log.error(error);
return Promise.reject(error);
}
}
}
}
/**
* Update user's token that was generated when they were first authenticated.
* @param {string} token - the new token
* @returns {Promise}
* @example <caption>listen for expired-token error events and then update the token on Application level</caption>
* application.on('system:error:expired-token', 'NXM-errors', (error) => {
* console.log('token expired');
* application.updateToken(token);
* });
*/
async updateToken(token) {
// SDK can be disconnected because of expired token
// this lets us update token for next reconnection attempt
if (this.session.connection && this.session.connection.disconnected) {
this.session.config.token = token;
this.session.connection.io.opts.query.token = token;
return Promise.resolve();
}
const reqObj = {
url: `${this.session.config.nexmo_api_url}/v0.2/sessions/${this.session.session_id}`,
type: 'PUT',
token
};
try {
await utils_1.default.networkRequest(reqObj);
if (this.me) {
this.session.config.token = token;
this.session.connection.io.opts.query.token = token;
}
}
catch (error) {
throw (new nexmoClientError_1.NexmoApiError(error));
}
}
/**
* Update the event to map local generated events
* in case we need a more specific event to pass in the application listener
* or f/w the event as it comes
* @private
*/
async _handleApplicationEvent(event) {
try {
this.log.debug("_handleApplicationEvent: ", { event });
const processed_event = applicationEventsHandler.handleEvent(event);
const conversation = this.conversations.get(event.cid);
let member;
if (conversation.members.has((processed_event || {}).from)) {
member = conversation.members.get(processed_event.from);
}
else if (event.type === 'member:joined' || event.type === 'member:invited') {
const params = { ...event.body, ...(event.from && { member_id: event.from }) };
member = new member_1.default(conversation, params);
}
else {
try {
member = await conversation.getMember(processed_event.from);
}
catch (error) {
this.log.warn(`There is an error getting the member ${error}`);
}
}
this.emit(processed_event.type, member, processed_event);
return event;
}
catch (e) {
this.log.error("_handleApplicationEvent: ", e);
throw (e);
}
}
/**
* Creates a call to specified user/s.
* @classdesc creates a call between the defined users
* @param {string[]} usernames - the user names for those we want to call
* @returns {Promise<NXMCall>} a NXMCall object with all the call properties
* @example <caption>Create a call with users</caption>
* application.on("call:status:changed", (nxmCall) => {
* if (nxmCall.status === nxmCall.CALL_STATUS.STARTED) {
* console.log('the call has started');
* }
* });
*
* application.inAppCall(usernames).then(() => {
* console.log('Calling user(s)...');
* }).catch((error) => {
* console.error(error);
* });
*/
async inAppCall(usernames) {
if (!usernames || !Array.isArray(usernames) || usernames.length === 0) {
return Promise.reject(new nexmoClientError_1.NexmoClientError('error:application:call:params'));
}
try {
const nxmCall = new nxmCall_1.default(this);
await nxmCall.createCall(usernames);
nxmCall.direction = nxmCall.CALL_DIRECTION.OUTBOUND;
return nxmCall;
}
catch (error) {
throw error;
}
}
/**
* Creates a call to phone a number.
* The call object is created under application.calls when the call has started.
* listen for it with application.on("call:status:changed")
*
* You don't need to start the stream, the SDK will play the audio for you
*
* @classdesc creates a call to a phone number
* @param {string} user the phone number or the username you want to call
* @param {string} [type="phone"] the type of the call you want to have. possible values "phone" or "app" (default is "phone")
* @param {object} [custom_data] custom data to be included in the call object, i.e. { yourCustomKey: yourCustomValue }
* @returns {Promise<NXMCall>}
* @example <caption>Create a call to a phone</caption>
* application.on("call:status:changed", (nxmCall) => {
* if (nxmCall.status === nxmCall.CALL_STATUS.STARTED) {
* console.log('the call has started');
* }
* });
*
* application.callServer(phone_number).then((nxmCall) => {
* console.log('Calling phone ' + phone_number);
* console.log('Call Object ': nxmCall);
* }).catch((error) => {
* console.error(error);
* });
*/
async callServer(user, type = 'phone', custom_data = {}) {
try {
const nxmCall = new nxmCall_1.default(this);
nxmCall.direction = nxmCall.CALL_DIRECTION.OUTBOUND;
await nxmCall.createServerCall(user, type, custom_data);
return nxmCall;
}
catch (error) {
throw error;
}
}
/**
* Reconnect a leg to an ongoing call.
* You don't need to start the stream, the SDK will play the audio for you
*
* @classdesc reconnect leg to an ongoing call
* @param {string} conversation_id the conversation that you want to reconnect
* @param {string} rtc_id the id of the leg that will be reconnected
* @param {object} [mediaParams] - MediaStream params (same as Media.enable())
* @returns {Promise<NXMCall>}
* @example <caption>Reconnect a leg to an ongoing call</caption>
* application.reconnectCall("conversation_id", "rtc_id").then((nxmCall) => {
* console.log(nxmCall);
* }).catch((error) => {
* console.error(error);
* });
*
* @example <caption>Reconnect a leg to an ongoing call without auto playing audio</caption>
* application.reconnectCall("conversation_id", "rtc_id", { autoPlayAudio: false }).then((nxmCall) => {
* console.log(nxmCall);
* }).catch((error) => {
* console.error(error);
* });
*
* @example <caption>Reconnect a leg to an ongoing call choosing device ID</caption>
* application.reconnectCall("conversation_id", "rtc_id", { audioConstraints: { deviceId: "device_id" } }).then((nxmCall) => {
* console.log(nxmCall);
* }).catch((error) => {
* console.error(error);
* });
*/
async reconnectCall(conversationId, rtcId, mediaParams = {}) {
try {
if (!conversationId || !rtcId) {
throw new nexmoClientError_1.NexmoClientError('error:missing:params');
}
const conversation = await this.getConversation(conversationId, Application.CONVERSATION_API_VERSION.v1);
await conversation.media.enable({ ...mediaParams, reconnectRtcId: rtcId });
const nxmCall = new nxmCall_1.default(this, conversation);
// assigning the correct call status taking into account the sip status (outbound)
// on inbound calls the reconnect will happen after the call is estabilished and both legs are answered
const event_types = Array.from(conversation.events.values()).map(event => event.type);
if (event_types.includes('sip:answered'))
nxmCall.status = nxmCall.CALL_STATUS.ANSWERED;
else if (event_types.includes('sip:ringing'))
nxmCall.status = nxmCall.CALL_STATUS.RINGING;
else
nxmCall.status = nxmCall.CALL_STATUS.STARTED;
nxmCall.rtcObjects = conversation.media.rtcObjects;
this.calls.set(conversation.id, nxmCall);
return nxmCall;
}
catch (error) {
throw error;
}
}
/**
* Query the service to create a new conversation
* The conversation name must be unique per application.
* @param {object} [params] - leave empty to get a GUID as name
* @param {string} params.name - the name of the conversation. A UID will be assigned if this is skipped
* @param {string} params.display_name - the display_name of the conversation.
* @returns {Promise<Conversation>} - the created Conversation
* @example <caption>Create a conversation and join</caption>
* application.newConversation().then((conversation) => {
* //join the created conversation
* conversation.join().then((member) => {
* //Get the user's member belonging in this conversation.
* //You can also access it via conversation.me
* console.log("Joined as " + member.user.name);
* });
* }).catch((error) => {
* console.error(error);
* });
*/
async newConversation(data = {}) {
try {
const response = await this.session.sendNetworkRequest({
type: 'POST',
path: 'conversations',
data
});
const conv = new conversation_1.default(this, response);
this.conversations.set(conv.id, conv);
// do a get conversation to get the whole model as shaped in the service,
return this.getConversation(conv.id, Application.CONVERSATION_API_VERSION.v1);
}
catch (error) {
throw new nexmoClientError_1.NexmoApiError(error);
}
}
/**
* Query the service to create a new conversation and join it
* The conversation name must be unique per application.
* @param {object} [params] - leave empty to get a GUID as name
* @param {string} params.name - the name of the conversation. A UID will be assigned if this is skipped
* @param {string} params.display_name - the display_name of the conversation.
* @returns {Promise<Conversation>} - the created Conversation
* @example <caption>Create a conversation and join</caption>
* application.newConversationAndJoin().then((conversation) => {
* console.log("Joined as " + conversation.me.display_name);
* }).catch((error) => {
* console.error("Error creating a conversation and joining ", error);
* });
*/
async newConversationAndJoin(params) {
const conversation = await this.newConversation(params);
await conversation.join();
return conversation;
}
/**
* Query the service to see if this conversation exists with the
* logged in user as a member and retrieve the data object
* Result added (or updated) in this.conversations
*
* @param {string} id - the id of the conversation to fetch
* @param {string} version=Application.CONVERSATION_API_VERSION.v3 {Application.CONVERSATION_API_VERSION.v1 || Application.CONVERSATION_API_VERSION.v3} - the version of the Conversation Service API to use (v1 includes the full list of the members of the conversation but v3 does not)
* @returns {Promise<Conversation>} - the requested conversation
* @example <caption>Get a conversation</caption>
* application.getConversation(id).then((conversation) => {
* console.log("Retrieved conversation: ", conversation);
* }).catch((error) => {
* console.error(error);
* });
*/
async getConversation(id, version = Application.CONVERSATION_API_VERSION.v3) {
if (version !== Application.CONVERSATION_API_VERSION.v1 && version !== Application.CONVERSATION_API_VERSION.v3) {
throw new nexmoClientError_1.NexmoClientError('error:conversation-service:version');
}
let response;
if (version === Application.CONVERSATION_API_VERSION.v1) {
try {
response = await this.session.sendNetworkRequest({
type: 'GET',
path: `conversations/${id}`
});
response['id'] = response['uuid'];
delete response['uuid'];
}
catch (error) {
throw new nexmoClientError_1.NexmoApiError(error);
}
}
else {
try {
response = await this.session.sendNetworkRequest({
type: 'GET',
path: `conversations/${id}`,
version: 'v0.3'
});
}
catch (error) {
throw new nexmoClientError_1.NexmoApiError(error);
}
}
const conversation_object = this.updateOrCreateConversation(response);
if (version === Application.CONVERSATION_API_VERSION.v3 && !conversation_object.me) {
try {
const member = await conversation_object.getMyMember();
conversation_object.me = member;
conversation_object.members.set(member.id, member);
}
catch (error) {
// add a retry in case of a failure in fetching the member
try {
const member = await conversation_object.getMyMember();
conversation_object.me = member;
conversation_object.members.set(member.id, member);
}
catch (error) {
this.log.warn(`You don't have any membership in ${conversation_object.id}`);
}
}
}
if (this.session.config.sync === 'full') {
// Populate the events
const { items } = await conversation_object.getEvents();
conversation_object.events = items;
return conversation_object;
}
else {
return conversation_object;
}
}
/**
* Query the service to obtain a complete list of conversations of which the
* logged-in user is a member with a state of `JOINED` or `INVITED`.
* @param {object} params configure defaults for paginated conversations query
* @param {string} params.order 'asc' or 'desc' ordering of resources based on creation time
* @param {number} params.page_size the number of resources returned in a single request list
* @param {string} [params.cursor] string to access the starting point of a dataset
*
* @returns {Promise<Page<Map<Conversation>>>} - Populate Application.conversations.
* @example <caption>Get Conversations</caption>
* application.getConversations({ page_size: 20 }).then((conversations_page) => {
* conversations_page.items.forEach(conversation => {
* render(conversation)
* })
* }).catch((error) => {
* console.error(error);
* });
*
*/
async getConversations(params = {}) {
const url = `${this.session.config.nexmo_api_url}/beta2/users/${this.me.id}/conversations`;
// Create pageConfig if some elements given otherwise use default
let pageConfig = Object.keys(params).length === 0 ? this.pageConfig : new page_config_1.default(params);
try {
const response = await utils_1.default.paginationRequest(url, pageConfig, this.session.config.token);
response.application = this;
const conversations_page = new conversations_page_1.default(response);
this.conversations_page_last = conversations_page;
return conversations_page;
}
catch (error) {
throw new nexmoClientError_1.NexmoApiError(error);
}
}
/**
* Application listening for sync status events.
*
* @event Application#sync:progress
*
* @property {number} status.sync_progress - Percentage of fetched conversations
* @example <caption>listen for changes in the synchronisation progress events on Application level</caption>
* application.on("sync:progress",(status) => {
* console.log(status.sync_progress);
* });
*/
/**
* Fetching all the conversations and sync progress events
*/
syncConversations(conversations) {
const conversation_array = Array.from(conversations.values());
const conversations_length = conversation_array.length;
const d = new Date();
this.start_sync_time = (typeof window !== 'undefined' && window.performance) ? window.performance.now() : d.getTime();
const fetchConversationForStorage = async () => {
this.synced_conversations_percentage = Number(((this.synced_conversations_count / conversations_length) * 100).toFixed(2));
const status_payload = {
sync_progress: this.synced_conversations_percentage
};
this.emit('sync:progress', status_payload);
this.log.info('Loading sync progress: ' + this.synced_conversations_count + '/' +
conversations_length + ' - ' + this.synced_conversations_percentage + '%');
if (this.synced_conversations_percentage >= 100) {
const d = new Date();
this.stop_sync_time = (typeof window !== 'undefined' && window.performance) ? window.performance.now() : d.getTime();
this.log.info('Loaded conversations in ' + (this.stop_sync_time - this.start_sync_time) + 'ms');
}
if (this.synced_conversations_count < conversations_length) {
await this.getConversation(conversation_array[this.synced_conversations_count].id);
fetchConversationForStorage();
this.synced_conversations_count++;
this.sync_progress_buffer++;
}
};
fetchConversationForStorage();
}
/**
* Get Details of a user by using their id. If no id is present, will return your own user details.
* @param {string} id - the id of the user to fetch, if skipped, it returns your own user details
* @returns {Promise<User>}
* @example <caption>Get User details</caption>
* application.getUser(id).then((user) => {
* console.log('User details: 'user);
* }).catch((error) => {
* console.error(error);
* });
*/
async getUser(user_id = this.me.id) {
try {
const response = await this.session.sendNetworkRequest({
type: 'GET',
path: `users/${user_id}`
});
return new user_1.default(this, response);
}
catch (error) {
throw new nexmoClientError_1.NexmoApiError(error);
}
}
/**
* Query the service to obtain a complete list of userSessions of a given user
* @param {object} params configure defaults for paginated user sessions query
* @param {string} params.order 'asc' or 'desc' ordering of resources based on creation time
* @param {number} params.page_size the number of resources returned in a single request list
* @param {string} [params.cursor] string to access the starting point of a dataset
* @param {string} [params.user_id] the user id that the sessions are being fetched
*
* @returns {Promise<Page<Map<UserSession>>>}
* @example <caption>Get User Sessions</caption>
* application.getUserSessions({ user_id: "id", page_size: 20 }).then((user_sessions_page) => {
* user_sessions_page.items.forEach(user_session => {
* render(user_session)
* })
* }).catch((error) => {
* console.error(error);
* });
*
*/
async getUserSessions(params = {}) {
var _a;
const user_id = ((_a = params) === null || _a === void 0 ? void 0 : _a.user_id) || this.me.id;
const url = `${this.session.config.nexmo_api_url}/v0.3/users/${user_id}/sessions`;
// Create pageConfig if some elements given otherwise use default
let pageConfig = Object.keys(params).length === 0 ? this.pageConfig : new page_config_1.default(params);
try {
const response = await utils_1.default.paginationRequest(url, pageConfig, this.session.config.token, Application.CONVERSATION_API_VERSION.v3);
response.application = this;
const user_sessions_page = new user_sessions_page_1.default(response);
this.user_sessions_page_last = user_sessions_page;
return user_sessions_page;
}
catch (error) {
throw new nexmoClientError_1.NexmoApiError(error);
}
}
}
exports.default = Application;
/**
* Enum for Application getConversation version.
* @readonly
* @enum {string}
* @alias Application.CONVERSATION_API_VERSION
*/
Application.CONVERSATION_API_VERSION = {
v1: 'v0.1',
v3: 'v0.3'
};
module.exports = Application;