ringcentral-call-control
Version:
[](https://coveralls.io/github/ringcentral/ringcentral-call-control-js?branch=master) [ • 12.3 kB
text/typescript
import { EventEmitter } from 'events';
import { SDK as RingCentralSDK } from '@ringcentral/sdk';
import { formatParty } from './formatParty';
import { ringOutInboundLegCheck } from './helper';
import {
PartyStatusCode,
Session,
SessionData,
} from './Session';
import { USER_AGENT } from './userAgent';
export interface SessionsMap {
[key: string]: any;
}
export interface SessionMessage {
event: string;
body: any;
}
export interface Device {
id: string;
linePooling: string;
name: string;
uri: string;
type: 'SoftPhone' | 'OtherPhone' | 'HardPhone'
serial: string;
computerName: string;
boxBillingId: Number;
useAsCommonPhone: boolean;
inCompanyNet: boolean;
model: any;
extension: any;
emergencyServiceAddress: any;
phoneLines: any[];
shipping: any;
sku: any;
status: 'Initial' | 'Offline' | 'Online';
site: any;
lastLocationReportTime: string;
}
export interface EventSequenceData {
sequence: number;
updatedAt: number;
telephonySessionId: string;
}
export interface EventSequenceMap {
[key: string]: EventSequenceData;
}
export interface Account {
id: string;
}
export interface Extension {
id: string;
uri: string;
account: Account;
contact: any;
departments: any[];
extensionNumber: string;
name: string;
partnerId: string;
permissions: any[];
profileImage: any;
references: any[];
roles: any[];
regionalSettings: any;
serviceFeatures: any[];
setupWizardState: string;
status: string;
statusInfo: string;
type: string;
callQueueExtensionInfo: any;
hidden: boolean;
}
export interface CallOutToParams {
phoneNumber?: string;
extensionNumber?: string;
}
export class RingCentralCallControl extends EventEmitter {
private _sdk: RingCentralSDK;
private _sessionsMap: SessionsMap;
private _devices: Device[];
private _currentExtension: Extension;
private _accountLevel: boolean;
private _ready: boolean;
private _initializePromise: any;
private _preloadSessions: boolean;
private _preloadDevices: boolean;
private _userAgent: string;
private _eventSequenceMap: EventSequenceMap = {};
constructor({
sdk,
accountLevel,
preloadSessions = true,
preloadDevices = true,
extensionInfo,
userAgent,
} : {
sdk: RingCentralSDK,
accountLevel?: boolean,
preloadSessions?: boolean,
preloadDevices?: boolean,
extensionInfo?: Extension,
userAgent?: string,
}) {
super();
this._accountLevel = !!accountLevel;
this._sdk = sdk;
this._userAgent = userAgent;
this._sessionsMap = new Map;
this._devices = [];
this._ready = false;
this._initializePromise = null;
this._preloadSessions = preloadSessions;
this._preloadDevices = preloadDevices;
this._currentExtension = extensionInfo;
this.initialize();
}
public async initialize() {
if (this._ready) {
return;
}
if (!this._initializePromise) {
this._initializePromise = this._initialize();
}
await this._initializePromise;
this._initializePromise = null;
}
private async _initialize() {
if (!this._currentExtension) {
await this.loadCurrentExtension();
}
if (this._preloadSessions) {
await this.preloadSessions();
}
if (this._preloadDevices) {
await this.loadDevices();
}
this._ready = true;
this.emit('initialized');
}
public onNotificationEvent(message: SessionMessage) {
if (message.event.indexOf('/telephony/sessions') === -1) {
return;
}
const { eventTime, telephonySessionId, ...newData } = message.body;
if (!telephonySessionId) {
return;
}
const validatedSequence = this.checkSequence(message.body);
if (!validatedSequence) {
return;
}
const existedSession = this._sessionsMap.get(telephonySessionId);
newData.id = telephonySessionId;
newData.extensionId = this.extensionId;
newData.accountId = this.accountId;
newData.parties = newData.parties.map(p => formatParty(p));
if (!existedSession) {
const disconnectedParties = newData.parties.filter(p => p.status.code === 'Disconnected');
if (disconnectedParties.length === newData.parties.length) {
return;
}
// use first event's eventTime as session creationTime
newData.creationTime = eventTime;
// if new session is the inbound leg of ringout call then abandon it
const checkResult = ringOutInboundLegCheck(newData, this.sessions);
if (checkResult.isRingOutInboundLeg) {
return;
}
if(!checkResult.isRingOutInboundLeg && checkResult.legSessionId) {
// if find an inbound leg then remove it from sessions
this._sessionsMap.delete(checkResult.legSessionId)
}
const newSession = new Session(newData, this._sdk, this._accountLevel);
newSession.on('status', () => {
this.onSessionStatusUpdated(newSession);
});
this._sessionsMap.set(telephonySessionId, newSession);
if (newSession.party) {
this.emit('new', newSession);
}
return;
}
const party = existedSession.party;
existedSession.onUpdated(newData);
if (!party && existedSession.party) {
this.emit('new', existedSession);
}
}
private checkSequence({ sequence, telephonySessionId, parties }) {
let result = true;
const partyId = parties[0] && parties[0].id;
const eventSequenceData = this._eventSequenceMap[partyId];
if (eventSequenceData && eventSequenceData.sequence > sequence) {
result = false;
} else {
this._eventSequenceMap[partyId] = {
sequence,
telephonySessionId,
updatedAt: Date.now(),
};
}
this.cleanExpiredSequenceData();
return result;
}
private cleanExpiredSequenceData() {
Object.keys(this._eventSequenceMap).forEach((partyId) => {
const eventSequenceData = this._eventSequenceMap[partyId];
const existedSession = this._sessionsMap.get(eventSequenceData.telephonySessionId);
if (!existedSession && eventSequenceData.updatedAt + 60000 < Date.now()) {
delete this._eventSequenceMap[partyId];
}
});
}
get sessions(): Session[] {
return Array.from(this._sessionsMap.values());
}
get sessionsMap() {
return this._sessionsMap;
}
private async loadCurrentExtension() {
try {
const response =
await this._sdk.platform().get('/restapi/v1.0/account/~/extension/~', undefined, this.requestOptions);
this._currentExtension = await response.json();
} catch (e) {
console.error('Fetch extension info error', e);
}
}
private async preloadSessions() {
const activeCalls = await this.loadActiveCalls();
await this.loadSessions(activeCalls);
}
private async loadActiveCalls() {
let presenceUrl = '/restapi/v1.0/account/~/extension/~/presence?detailedTelephonyState=true&sipData=true';
if (this._accountLevel) {
presenceUrl = '/restapi/v1.0/account/~/presence?detailedTelephonyState=true&sipData=true';
}
try {
const response = await this._sdk.platform().get(presenceUrl, undefined, this.requestOptions);
const data = await response.json();
if (this._accountLevel) {
const presences = data.records;
let activeCalls = [];
presences.forEach((presence) => {
if (presence.activeCalls) {
activeCalls = activeCalls.concat(presence.activeCalls);
}
});
return activeCalls;
}
return data.activeCalls || [];
} catch (e) {
console.error('Fetch presence error', e);
return [];
}
}
public async loadSessions(activeCalls) {
if (activeCalls.length === 0) {
return;
}
try {
await Promise.all(activeCalls.map(async (activeCall) => {
const response =
await this._sdk.platform().get(
`/restapi/v1.0/account/~/telephony/sessions/${activeCall.telephonySessionId}`,
undefined,
this.requestOptions,
);
const data = await response.json();
data.extensionId = this.extensionId;
data.accountId = this.accountId;
data.parties = data.parties.map(p => formatParty(p));
// since call session status API not provide the `sessionId`, so pick from presence here.
data.sessionId = activeCall.sessionId;
const session = new Session(data, this._sdk, this._accountLevel);
this._sessionsMap.set(
activeCall.telephonySessionId,
session,
);
session.on('status', () => {
this.onSessionStatusUpdated(session);
});
}));
} catch (e) {
console.error('load sessions error', e);
}
}
public restoreSessions(sessionDatas: SessionData[]) {
const oldSessionMap = this._sessionsMap;
this._sessionsMap = new Map();
sessionDatas.forEach((sessionData) => {
if (oldSessionMap.get(sessionData.id)) {
const oldSession = oldSessionMap.get(sessionData.id);
oldSession.restore(sessionData);
this._sessionsMap.set(sessionData.id, oldSession);
return;
}
this._sessionsMap.set(
sessionData.id,
new Session(sessionData, this._sdk, this._accountLevel)
);
});
}
private async loadDevices() {
try {
const response =
await this._sdk.platform().get(
'/restapi/v1.0/account/~/extension/~/device',
undefined,
this.requestOptions,
);
const data = await response.json();
this._devices = data.records || [];
} catch (e) {
console.error('Fetch presence error', e);
}
}
private onSessionStatusUpdated(session: Session) {
const party = session.party;
if (
party &&
party.status.code === PartyStatusCode.disconnected &&
party.status.reason !== 'Pickup' && // don't end when call switched
party.status.reason !== 'CallSwitch' // don't end when call switched
) {
this._sessionsMap.delete(session.id);
}
}
public async refreshDevices() {
await this.loadDevices();
}
public async createCall(deviceId: string, to: CallOutToParams) {
const response = await this._sdk.platform().post('/restapi/v1.0/account/~/telephony/call-out', {
from: { deviceId },
to,
}, undefined, this.requestOptions);
const sessionData = (await response.json()).session;
sessionData.extensionId = this.extensionId;
sessionData.accountId = this.accountId;
sessionData.parties = sessionData.parties.map(p => formatParty(p));
const session = new Session(sessionData, this._sdk, this._accountLevel);
this._sessionsMap.set(
sessionData.id,
session,
);
session.on('status', () => {
this.onSessionStatusUpdated(session);
});
return session;
}
// Fucntion to create conference session
// The session's parties are empty
// Join as HOST with voice by using webphone sdk to call session.voiceCallToken
// Then bring in other telephony session into this conference
public async createConference() {
const response =
await this._sdk.platform().post(
'/restapi/v1.0/account/~/telephony/conference',
{},
undefined,
this.requestOptions
);
const sessionData = (await response.json()).session;
sessionData.extensionId = this.extensionId;
sessionData.accountId = this.accountId;
sessionData.parties = (sessionData.parties || []).map(p => formatParty(p));
const session = new Session(sessionData, this._sdk, this._accountLevel);
this._sessionsMap.set(
sessionData.id,
session,
);
session.on('status', () => {
this.onSessionStatusUpdated(session);
});
return session;
}
get accountId() {
return this._currentExtension && String(this._currentExtension.account.id);
}
get extensionId() {
return this._currentExtension && String(this._currentExtension.id);
}
get devices() {
return this._devices;
}
get ready() {
return this._ready;
}
get requestOptions() {
return {
userAgent: this._userAgent ? `${this._userAgent} ${USER_AGENT}` : USER_AGENT,
};
}
get eventSequenceMap() {
return this._eventSequenceMap;
}
}