fc-nexmo-client1
Version:
Nexmo Client SDK for JavaScript
1,139 lines (1,138 loc) • 2.21 MB
JavaScript
(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
'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 = {}, ringingMuted = false) {
try {
const nxmCall = new nxmCall_1.default(this);
nxmCall.direction = nxmCall.CALL_DIRECTION.OUTBOUND;
await nxmCall.createServerCall(user, type, custom_data, ringingMuted);
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;
},{"./conversation":2,"./handlers/application_events":7,"./handlers/events_queue":9,"./handlers/rtc_events":10,"./handlers/sip_events":11,"./member":13,"./modules/nxmCall":16,"./nexmoClientError":20,"./pages/conversations_page":22,"./pages/page_config":26,"./pages/user_sessions_page":27,"./user":29,"./utils":31,"loglevel":63,"wildemitter":107}],2:[function(require,module,exports){
'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
* Conversation Object Model
*
* Copyright (c) Nexmo Inc.
*/
const WildEmitter = require('wildemitter');
const loglevel_1 = require("loglevel");
const nexmoClientError_1 = require("./nexmoClientError");
const member_1 = __importDefault(require("./member"));
const nxmEvent_1 = __importDefault(require("./events/nxmEvent"));
const text_event_1 = __importDefault(require("./events/text_event"));
const message_event_1 = __importDefault(require("./events/message_event"));
const media_1 = __importDefault(require("./modules/media"));
const conversation_events_1 = __importDefault(require("./handlers/conversation_events"));
const utils_1 = __importDefault(require("./utils"));
const page_config_1 = __importDefault(require("./pages/page_config"));
const events_page_1 = __importDefault(require("./pages/events_page"));
const members_page_1 = __importDefault(require("./pages/members_page"));
const application_1 = __importDefault(require("./application"));
/**
* A single conversation Object.
* @class Conversation
* @property {Member} me - my Member object that belongs to this conversation
* @property {Application} application - the parent Application
* @property {string} name - the name of the Conversation (unique)
* @property {string} [display_name] - the display_name of the Conversation
* @property {Map<string, Member>} [members] - the members of the Conversation keyed by a member's id
* @property {Map<string, NXMEvent>} [events] - the events of the Conversation keyed by an event's id
* @property {number} [sequence_number] - the last event id
*/
class Conversation {
constructor(application, params) {
this.log = loglevel_1.getLogger(this.constructor.name);
this.application = application;
this.id = null;
this.name = null;
this.display_name = null;
this.timestamp = null;
this.members = new Map();
this.events = new Map();
this.sequence_number = 0;
this.pageConfig = new page_config_1.default(((this.application.session || {}).config || {}).events_page_config);
this.events_page_last = null;
this.members_page_last = null;
this.conversationEventHandler = new conversation_events_1.default(application, this);
this.media = new media_1.default(this);
/**
* A Member Object representing the current user.
* Only set if the user is or has been a member of the Conversation,
* otherwise the value will be `null`.
* @type Member
*/
this.me = null; // We are not in the conversation ourselves by default
// Map the params (which includes the id)
this._updateObjectInstance(params);
WildEmitter.mixin(Conversation);
}
/** Update Conversation object params
* @property {object} params the params to update
* @private
*/
_updateObjectInstance(params) {
for (let key in params) {
switch (key) {
case 'id':
this.id = params.id;
break;
case 'name':
this.name = params.name;
break;
case 'display_name':
this.display_name = params.display_name;
break;
case 'members':
// update the conversation javascript object
params.members.forEach((m) => {
if (this.members.has(m.member_id)) {
this.members.get(m.member_id)._normalise(m);
if (m.user_id === this.application.me.id && m.state !== 'LEFT') {
this.me = this.members.get(m.member_id);
this.members.set(this.me.id, this.me);
}
}
else {
const member = new member_1.default(this, m);
if (m.user_id === this.application.me.id && m.state !== 'LEFT') {
this.me = member;
}
this.members.set(member.id, member);
}
});
break;
case 'timestamp':
this.timestamp = params.timestamp;
break;
case 'sequence_number':
this.sequence_number = params.sequence_number;
break;
case 'member_id':
// filter needed params to create the object
// the conversation list gives us the member_id to prepare the member/this object
const object_params = {
id: params.member_id,
state: params.state,
user: this.application.me
};
// update the member object or create a new instance
if (this.members.has(params.member_id)) {
const member_object = this.members.get(params.member_id);
Object.assign(member_object, object_params);
}
else {
const member = new member_1.default(this, object_params);
this.me = member;
this.members.set(member.id, member);
}
break;
}
}
}
/**
* Join the given User to this Conversation. Will typically be used this to join
* ourselves to a Conversation we create.
* Accept an invitation if our Member has state INVITED and no user_id / user_name is given
*
* @param {object} [params = this.application.me.id] The User to join (defaults to this)
* @param {string} params.user_name the user_name of the User to join
* @param {string} params.user_id the user_id of the User to join
* @returns {Promise<Member>}
*
* @example <caption>join a user to the Conversation</caption>
*
* conversation.join().then((member) => {
* console.log("joined as member: ", member)
* }).catch((error) => {
* console.error("error joining conversation ", error);
* });
*/
async join(params) {
var _a, _b;
try {
let data = {
state: 'joined',
channel: {
type: 'app'
},
user: {
...(!params && { name: this.application.me.name, id: this.application.me.id }),
...(params && params.user_name && { name: params.user_name }),
...(params && params.user_id && { id: params.user_id }),
},
};
if (((_a = this === null || this === void 0 ? void 0 : this.me) === null || _a === void 0 ? void 0 : _a.id) && ((_b = this === null || this === void 0 ? void 0 : this.me) === null || _b === void 0 ? void 0 : _b.state) !== 'LEFT') {
data["from"] = this.me.id;
}
const response = await this.application.session.sendNetworkRequest({
type: 'POST',
path: `conversations/${this.id}/members`,
version: 'v0.3',
data
});
const member = new member_1.default(this, response);
if (response._embedded.user.id === this.application.me.id) {
this.me = member;
this.members.set(member.id, member);
}
// use case where between the time we got the conversation and the time we finished joining
// the conversation object changed.
this.application.getConversation(this.id, application_1.default.CONVERSATION_API_VERSION.v3);
return member;
}
catch (error) {
throw new nexmoClientError_1.NexmoApiError(error);
}
}
/**
* Delete a conversation
* @returns {Promise}
* @example <caption>delete the Conversation</caption>
*
* conversation.del().then(() => {
* console.log("conversation deleted");
* }).catch((error) => {
* console.error("error deleting conversation ", error);
* });
*/
async del() {
try {
const response = await this.application.session.sendNetworkRequest({
type: 'DELETE',
path: `conversations/${this.id}`
});
this.application.conversations.delete(this.id);
return response;
}
catch (error) {
throw new nexmoClientError_1.NexmoApiError(error);
}
}
/**
* Delete an NXMEvent (e.g. Text)
* @param {NXMEvent} event
* @returns {Promise}
* @example <caption>delete an Event</caption>
*
* conversation.deleteEvent(eventToBeDeleted).then(() => {
* console.log("event was deleted");
* }).catch((error) => {
* console.error("error deleting the event ", error);
* });
*
*/
deleteEvent(event) {
return event.del();
}
/**
* Invite the given user (id or name) to this conversation
* @param {Member} params
* @param {string} [params.id or user_name] - the id or the username of the User to invite
*
* @returns {Promise<Member>}
*
* @example <caption>invite a user to a Conversation</caption>
* const user_id = 'id of User to invite';
* const user_name = 'username of User to invite';
*
* conversation.invite({
* id: user_id,
* user_name: user_name
* }).then((member) => {
* displayMessage(member.state + " user: " + user_id + " " + user_name);
* }).catch((error) => {
* console.error("error inviting user ", error);
* });
*
*/
async invite(params) {
var _a, _b;
if (!params || (!params.id && !params.user_name)) {
throw new nexmoClientError_1.NexmoClientError('error:invite:missing:params');
}
const data = {
state: 'invited',
user: {
...(params.id && { id: params.id }),
...(params.user_name && { name: params.user_name })
},
media: params.media,
channel: {
from: {
type: 'app'
},
to: {
type: 'app'
},
type: 'app'
}
};
if (((_a = this === null || this === void 0 ? void 0 : this.me) === null || _a === void 0 ? void 0 : _a.id) && ((_b = this === null || this === void 0 ? void 0 : this.me) === null || _b === void 0 ? void 0 : _b.state) !== 'LEFT') {
data["from"] = this.me.id;
}
try {
const response = await this.application.session.sendNetworkRequest({
type: 'POST',
path: `conversations/${this.id}/members`,
version: 'v0.3',
data
});
const member = new member_1.default(this, response);
return member;
}
catch (error) {
throw new nexmoClientError_1.NexmoApiError(error);
}
}
/**
* Invite the given user (id or name) to this conversation with media audio
* @param {Member} params
* @param {string} [params.id or user_name] - the id or the username of the User to invite
*
* @returns {Promise<Member>}
*
* @example <caption>invite a user to a conversation</caption>
* const user_id = 'id of User to invite';
* const user_name = 'username of User to invite';
*
* conversation.inviteWithAudio({
* id: user_id,
* user_name: user_name
* }).then((member) => {
* displayMessage(member.state + " user: " + user_id + " " + user_name);
* }).catch((error) => {
* console.error("error inviting user ", error);
* });
*
*/
inviteWithAudio(params) {
if (!params || (!params.id && !params.user_name)) {
return Promise.reject(new nexmoClientError_1.NexmoClientError('error:invite:missing:params'));
}
params.media = {
audio_settings: {
enabled: true,
muted: false,
earmuffed: false
}
};
return this.invite(params);
}
/**
* Leave from the Conversation
* @param {object} [reason] the reason for leaving the conversation
* @param {string} [reason.reason_code] the code of the reason
* @param {string} [reason.reason_text] the description of the reason
* @returns {Promise}
* @example <caption>leave the Conversation</caption>
*
* conversation.leave({reason_code: "mycode", reason_text: "my reason for leaving"}).then(() => {
* console.log("successfully left conversation");
* }).catch((error) => {
* console.error("error leaving conversation ", error);
* });
*
*/
leave(reason) {
return this.me.kick(reason);
}
/**
* Send a text message to the conversation, which will be relayed to every other member of the conversation
* @param {string} text - the text message to be sent
*
* @returns {Promise<TextEvent>} - the text message that was sent
*
* @example <caption> sending a text </caption>
* conversation.sendText("Hi Vonage").then((event) => {
* console.log("message was sent", event);
* }).catch((error)=>{
* console.error("error sending the message ", error);
* });
*
* @deprecated since version 8.3.0
*
*/
async sendText(text) {
try {
if (this.me === null) {
throw new nexmoClientError_1.NexmoClientError('error:self');
}
const msg = {
type: 'text',
cid: this.id,
from: this.me.id,
body: {
text
}
};
const { id, timestamp } = await this.application.session.sendNetworkRequest({
type: 'POST',
path: `conversations/${this.id}/events`,
data: msg
});
msg.id = id;
msg.body.timestamp = timestamp;
return new text_event_1.default(this, msg);
}
catch (error) {
throw new nexmoClientError_1.NexmoApiError(error);
}
}
/**
* Send a custom event to the Conversation
* @param {object} params - params of the custom event
* @param {string} params.type the name of the custom event. Must not exceed 100 char length and contain only alpha numerics and '-' and '_' characters.
* @param {object} params.body customizable key value pairs
*
* @returns {Promise<NXMEvent>} - the custom event that was sent
*
* @example <caption> sending a custom event </caption>
* conversation.sendCustomEvent({ type: "my-event", body: { mykey: "my value" }}).then((event) => {
* console.log("custom event was sent", event);
* }).catch((error)=>{
* console.error("error sending the custom event", error);
* });
*
*/
async sendCustomEvent({ type, body }) {
try {
if (this.me === null) {
throw new nexmoClientError_1.NexmoClientError('error:self');
}
else if (!type || typeof type !== 'string' || type.length < 1) {
throw new nexmoClientError_1.NexmoClientError('error:custom-event:invalid');
}
const data = {
type: `custom:${type}`,
cid: this.id,
from: this.me.id,
body
};
const { id, timestamp } = await this.application.session.sendNetworkRequest({
type: 'POST',
path: `conversations/${this.id}/events`,
data
});
data.id = id;
data.timestamp = timestamp;
return new nxmEvent_1.default(this, data);
}
catch (error) {
throw new nexmoClientError_1.NexmoApiError(error);
}
}
/**
* Uploads an Image to Media Service.
* implements xhr (https://xhr.spec.whatwg.org/) - this.imageRequest
*
* @param {File} file single input file (jpeg/jpg)
* @param {object} params - params of image sent
* @param {string} [params.quality_ratio = 100] a value between 0 and 100. 0 indicates 'maximum compression' and the lowest quality, 100 will result in the highest quality image
* @param {string} [params.medium_size_ratio = 50] a value between 1 and 100. 1 indicates the new image is 1% of original, 100 - same size as original
* @param {string} [params.thumbnail_size_ratio = 30] a value between 1 and 100. 1 indicates the new image is 1% of original, 100 - same size as original
*
* @returns {Promise<XMLHttpRequest>}
*
* @example <caption>uploading an image</caption>
* const params = {
* quality_ratio : "90",
* medium_size_ratio: "40",
* thumbnail_size_ratio: "20"
* }
* conversation.uploadImage(fileInput.files[0], params).then((uploadImageRequest) => {
* uploadImageRequest.onprogress = (e) => {
* console.log("Image request progress: ", e);
* console.log("Image progress: " + e.loaded + "/" + e.total);
* };
* uploadImageRequest.onabort = (e) => {
* console.log("Image request aborted: ", e);
* console.log("Image: " + e.type);
* };
* uploadImageRequest.onloadend = (e) => {
* console.log("Image request successful: ", e);
* console.log("Image: " + e.type);
* };
* uploadImageRequest.onreadystatechange = () => {
* if (uploadImageRequest.readyState === 4 && uploadImageRequest.status === 200) {
* const representations = JSON.parse(uploadImageRequest.responseText);
* console.log("Original image url: ", representations.original.url);
* console.log("Medium image url: ", representations.medium.url);
* console.log("Thumbnail image url: ", representations.thumbnail.url);
* }
* };
* }).catch((error) => {
* console.error("error uploading the image ", error);
* });
*/
async uploadImage(fileInput, params = {
quality_ratio: '100',
medium_size_ratio: '50',
thumbnail_size_ratio: '30'
}) {
const formData = new FormData();
formData.append('file', fileInput);
formData.append('quality_ratio', params.quality_ratio);
formData.append('medium_size_ratio', params.medium_size_ratio);
formData.append('thumbnail_size_ratio', params.thumbnail_size_ratio);
const imageRequest = await utils_1.default.networkRequest({
type: 'POST',
url: this.application.session.config.ips_url,
d