@dora-cell/sdk 0.1.1-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +276 -0
- package/dist/index.d.mts +210 -0
- package/dist/index.d.ts +210 -0
- package/dist/index.js +664 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +645 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +60 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var JsSIP = require('jssip');
|
|
6
|
+
var EventEmitter = require('eventemitter3');
|
|
7
|
+
|
|
8
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
|
+
|
|
10
|
+
var JsSIP__default = /*#__PURE__*/_interopDefault(JsSIP);
|
|
11
|
+
var EventEmitter__default = /*#__PURE__*/_interopDefault(EventEmitter);
|
|
12
|
+
|
|
13
|
+
// src/core/DoraCell.ts
|
|
14
|
+
|
|
15
|
+
// src/types/index.ts
|
|
16
|
+
var DoraCellError = class extends Error {
|
|
17
|
+
constructor(message, code, details) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.code = code;
|
|
20
|
+
this.details = details;
|
|
21
|
+
this.name = "DoraCellError";
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
var AuthenticationError = class extends DoraCellError {
|
|
25
|
+
constructor(message, details) {
|
|
26
|
+
super(message, "AUTH_ERROR", details);
|
|
27
|
+
this.name = "AuthenticationError";
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
var CallError = class extends DoraCellError {
|
|
31
|
+
constructor(message, details) {
|
|
32
|
+
super(message, "CALL_ERROR", details);
|
|
33
|
+
this.name = "CallError";
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
var ConnectionError = class extends DoraCellError {
|
|
37
|
+
constructor(message, details) {
|
|
38
|
+
super(message, "CONNECTION_ERROR", details);
|
|
39
|
+
this.name = "ConnectionError";
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// src/core/AuthProvider.ts
|
|
44
|
+
var ApiTokenAuthProvider = class {
|
|
45
|
+
constructor(apiToken, apiBaseUrl) {
|
|
46
|
+
this.credentials = null;
|
|
47
|
+
this.apiToken = apiToken;
|
|
48
|
+
this.apiBaseUrl = apiBaseUrl || process.env.NEXT_PUBLIC_BASE_API_URL || "";
|
|
49
|
+
}
|
|
50
|
+
async authenticate(config) {
|
|
51
|
+
if (config.type !== "api-token") {
|
|
52
|
+
throw new AuthenticationError("Invalid auth config type for ApiTokenAuthProvider");
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const response = await fetch(`${this.apiBaseUrl}/api/sip-credentials`, {
|
|
56
|
+
method: "GET",
|
|
57
|
+
headers: {
|
|
58
|
+
"Authorization": `Bearer ${this.apiToken}`,
|
|
59
|
+
"Content-Type": "application/json",
|
|
60
|
+
"Accept": "application/json"
|
|
61
|
+
},
|
|
62
|
+
credentials: "include"
|
|
63
|
+
});
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
if (response.status === 401 || response.status === 403) {
|
|
66
|
+
throw new AuthenticationError(
|
|
67
|
+
"Invalid API token or insufficient permissions",
|
|
68
|
+
{ status: response.status }
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
throw new AuthenticationError(
|
|
72
|
+
`Failed to fetch SIP credentials: ${response.status}`,
|
|
73
|
+
{ status: response.status }
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
const data = await response.json();
|
|
77
|
+
this.credentials = this.parseCredentials(data);
|
|
78
|
+
return this.credentials;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (error instanceof AuthenticationError) {
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
throw new AuthenticationError(
|
|
84
|
+
`Authentication failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
85
|
+
{ originalError: error }
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async refreshCredentials() {
|
|
90
|
+
return this.authenticate({ type: "api-token", apiToken: this.apiToken });
|
|
91
|
+
}
|
|
92
|
+
isAuthenticated() {
|
|
93
|
+
return this.credentials !== null;
|
|
94
|
+
}
|
|
95
|
+
parseCredentials(data) {
|
|
96
|
+
const wsUrl = data.ws_url || data.wsUrl;
|
|
97
|
+
const sipUri = data.sip_uri || data.sipUri;
|
|
98
|
+
const password = data.password;
|
|
99
|
+
const sipDomain = data.sip_domain || data.sipDomain;
|
|
100
|
+
const extensions = data.extensions;
|
|
101
|
+
const expiresIn = data.expires_in || data.expiresIn;
|
|
102
|
+
if (!wsUrl || !sipUri) {
|
|
103
|
+
throw new AuthenticationError(
|
|
104
|
+
"Invalid credentials response: missing ws_url or sip_uri"
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
wsUrl,
|
|
109
|
+
sipUri,
|
|
110
|
+
password: password || "",
|
|
111
|
+
sipDomain,
|
|
112
|
+
extensions,
|
|
113
|
+
expiresIn
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
var DirectCredentialsAuthProvider = class {
|
|
118
|
+
constructor() {
|
|
119
|
+
this.credentials = null;
|
|
120
|
+
}
|
|
121
|
+
async authenticate(config) {
|
|
122
|
+
if (config.type !== "direct") {
|
|
123
|
+
throw new AuthenticationError("Invalid auth config type for DirectCredentialsAuthProvider");
|
|
124
|
+
}
|
|
125
|
+
this.credentials = {
|
|
126
|
+
wsUrl: config.wsUrl,
|
|
127
|
+
sipUri: config.sipUri,
|
|
128
|
+
password: config.password
|
|
129
|
+
};
|
|
130
|
+
return this.credentials;
|
|
131
|
+
}
|
|
132
|
+
isAuthenticated() {
|
|
133
|
+
return this.credentials !== null;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
function createAuthProvider(config) {
|
|
137
|
+
switch (config.type) {
|
|
138
|
+
case "api-token":
|
|
139
|
+
return new ApiTokenAuthProvider(config.apiToken, config.apiBaseUrl);
|
|
140
|
+
case "direct":
|
|
141
|
+
return new DirectCredentialsAuthProvider();
|
|
142
|
+
default:
|
|
143
|
+
throw new AuthenticationError("Unknown authentication type");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// src/utils/phoneFormatter.ts
|
|
148
|
+
function formatPhoneToSIP(phone, sipDomain) {
|
|
149
|
+
let cleaned = phone.replace(/\D/g, "");
|
|
150
|
+
if (cleaned.length <= 4) {
|
|
151
|
+
return `sip:${cleaned}@${sipDomain}`;
|
|
152
|
+
}
|
|
153
|
+
if (cleaned.startsWith("0")) {
|
|
154
|
+
cleaned = "234" + cleaned.substring(1);
|
|
155
|
+
return `sip:${cleaned}@${sipDomain}`;
|
|
156
|
+
}
|
|
157
|
+
if (cleaned.startsWith("234")) {
|
|
158
|
+
return `sip:${cleaned}@${sipDomain}`;
|
|
159
|
+
}
|
|
160
|
+
cleaned = "234" + cleaned;
|
|
161
|
+
return `sip:${cleaned}@${sipDomain}`;
|
|
162
|
+
}
|
|
163
|
+
function normalizePhoneNumber(phone, countryCode = "234") {
|
|
164
|
+
let cleaned = phone.replace(/\D/g, "");
|
|
165
|
+
if (cleaned.length <= 4) {
|
|
166
|
+
return cleaned;
|
|
167
|
+
}
|
|
168
|
+
if (cleaned.startsWith("0")) {
|
|
169
|
+
return countryCode + cleaned.substring(1);
|
|
170
|
+
}
|
|
171
|
+
if (cleaned.startsWith(countryCode)) {
|
|
172
|
+
return cleaned;
|
|
173
|
+
}
|
|
174
|
+
return countryCode + cleaned;
|
|
175
|
+
}
|
|
176
|
+
function extractNumberFromSipUri(sipUri) {
|
|
177
|
+
const match = sipUri.match(/sip:([^@]+)@/);
|
|
178
|
+
return match ? match[1] : sipUri;
|
|
179
|
+
}
|
|
180
|
+
function isValidPhoneNumber(phone, minLength = 3) {
|
|
181
|
+
const cleaned = phone.replace(/\D/g, "");
|
|
182
|
+
return cleaned.length >= minLength;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// src/core/CallManager.ts
|
|
186
|
+
var CallSession = class {
|
|
187
|
+
constructor(session, direction, remoteNumber, localExtension, events) {
|
|
188
|
+
this.status = "idle";
|
|
189
|
+
this.duration = 0;
|
|
190
|
+
// JsSIP RTCSession
|
|
191
|
+
this._isMuted = false;
|
|
192
|
+
this.remoteStreamValue = null;
|
|
193
|
+
this.id = this.generateCallId();
|
|
194
|
+
this.session = session;
|
|
195
|
+
this.direction = direction;
|
|
196
|
+
this.remoteNumber = remoteNumber;
|
|
197
|
+
this.localExtension = localExtension;
|
|
198
|
+
this.events = events;
|
|
199
|
+
this.setupSessionHandlers();
|
|
200
|
+
}
|
|
201
|
+
generateCallId() {
|
|
202
|
+
return `call_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
203
|
+
}
|
|
204
|
+
setupSessionHandlers() {
|
|
205
|
+
this.session.on("progress", (evt) => {
|
|
206
|
+
const code = evt.response?.status_code;
|
|
207
|
+
if (code === 180 || code === 183) {
|
|
208
|
+
this.status = "ringing";
|
|
209
|
+
this.events.emit("call:ringing", this);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
this.session.on("confirmed", () => {
|
|
213
|
+
this.status = "ongoing";
|
|
214
|
+
this.startTime = Date.now();
|
|
215
|
+
this.startDurationTimer();
|
|
216
|
+
this.events.emit("call:connected", this);
|
|
217
|
+
});
|
|
218
|
+
this.session.on("peerconnection", (evt) => {
|
|
219
|
+
evt.peerconnection.ontrack = (event) => {
|
|
220
|
+
if (event.streams && event.streams[0]) {
|
|
221
|
+
this.remoteStreamValue = event.streams[0];
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
});
|
|
225
|
+
this.session.on("ended", (evt) => {
|
|
226
|
+
this.handleCallEnd(evt?.cause);
|
|
227
|
+
});
|
|
228
|
+
this.session.on("failed", (evt) => {
|
|
229
|
+
this.handleCallEnd(evt?.cause || "Call failed");
|
|
230
|
+
});
|
|
231
|
+
this.session.on("rejected", (evt) => {
|
|
232
|
+
this.handleCallEnd(evt?.cause || "Call rejected");
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
handleCallEnd(reason) {
|
|
236
|
+
this.status = "ended";
|
|
237
|
+
this.endTime = Date.now();
|
|
238
|
+
this.stopDurationTimer();
|
|
239
|
+
this.remoteStreamValue = null;
|
|
240
|
+
this._isMuted = false;
|
|
241
|
+
this.events.emit("call:ended", this, reason);
|
|
242
|
+
}
|
|
243
|
+
startDurationTimer() {
|
|
244
|
+
this.durationInterval = window.setInterval(() => {
|
|
245
|
+
if (this.startTime) {
|
|
246
|
+
this.duration = Math.floor((Date.now() - this.startTime) / 1e3);
|
|
247
|
+
}
|
|
248
|
+
}, 1e3);
|
|
249
|
+
}
|
|
250
|
+
stopDurationTimer() {
|
|
251
|
+
if (this.durationInterval) {
|
|
252
|
+
clearInterval(this.durationInterval);
|
|
253
|
+
this.durationInterval = void 0;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// Public methods
|
|
257
|
+
mute() {
|
|
258
|
+
if (this.session && !this._isMuted) {
|
|
259
|
+
this.session.mute({ audio: true });
|
|
260
|
+
this._isMuted = true;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
unmute() {
|
|
264
|
+
if (this.session && this._isMuted) {
|
|
265
|
+
this.session.unmute({ audio: true });
|
|
266
|
+
this._isMuted = false;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
hangup() {
|
|
270
|
+
if (this.session) {
|
|
271
|
+
this.session.terminate();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
isMuted() {
|
|
275
|
+
return this._isMuted;
|
|
276
|
+
}
|
|
277
|
+
getRemoteStream() {
|
|
278
|
+
return this.remoteStreamValue;
|
|
279
|
+
}
|
|
280
|
+
getFormattedDuration() {
|
|
281
|
+
const mm = String(Math.floor(this.duration / 60)).padStart(2, "0");
|
|
282
|
+
const ss = String(this.duration % 60).padStart(2, "0");
|
|
283
|
+
return `${mm}:${ss}`;
|
|
284
|
+
}
|
|
285
|
+
destroy() {
|
|
286
|
+
this.stopDurationTimer();
|
|
287
|
+
this.session = null;
|
|
288
|
+
this.remoteStreamValue = null;
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
var CallManager = class {
|
|
292
|
+
constructor(credentials, events, callConfig) {
|
|
293
|
+
this.ua = null;
|
|
294
|
+
// JsSIP User Agent
|
|
295
|
+
this.currentCall = null;
|
|
296
|
+
this.credentials = credentials;
|
|
297
|
+
this.events = events;
|
|
298
|
+
this.callConfig = callConfig;
|
|
299
|
+
}
|
|
300
|
+
setUserAgent(ua) {
|
|
301
|
+
this.ua = ua;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Initiate an outbound call
|
|
305
|
+
*/
|
|
306
|
+
async initiateCall(targetNumber, options) {
|
|
307
|
+
if (!this.ua) {
|
|
308
|
+
throw new CallError("User Agent not initialized");
|
|
309
|
+
}
|
|
310
|
+
if (this.currentCall && this.currentCall.status !== "ended") {
|
|
311
|
+
throw new CallError("A call is already in progress");
|
|
312
|
+
}
|
|
313
|
+
try {
|
|
314
|
+
await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
315
|
+
const extension = options?.extension || this.getDefaultExtension();
|
|
316
|
+
const sipDomain = this.extractDomain(this.credentials.sipUri);
|
|
317
|
+
const sipTarget = formatPhoneToSIP(targetNumber, sipDomain);
|
|
318
|
+
const session = this.ua.call(sipTarget, {
|
|
319
|
+
mediaConstraints: options?.mediaConstraints || { audio: true },
|
|
320
|
+
pcConfig: this.callConfig.pcConfig
|
|
321
|
+
});
|
|
322
|
+
const call = new CallSession(
|
|
323
|
+
session,
|
|
324
|
+
"outbound",
|
|
325
|
+
targetNumber,
|
|
326
|
+
extension,
|
|
327
|
+
this.events
|
|
328
|
+
);
|
|
329
|
+
call.status = "connecting";
|
|
330
|
+
this.currentCall = call;
|
|
331
|
+
this.events.emit("call:outgoing", call);
|
|
332
|
+
return call;
|
|
333
|
+
} catch (error) {
|
|
334
|
+
throw new CallError(
|
|
335
|
+
`Failed to initiate call: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
336
|
+
{ originalError: error }
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Answer an incoming call
|
|
342
|
+
*/
|
|
343
|
+
answerCall(session) {
|
|
344
|
+
if (!session) {
|
|
345
|
+
throw new CallError("No session provided");
|
|
346
|
+
}
|
|
347
|
+
try {
|
|
348
|
+
const remoteUri = session.remote_identity?.uri?.toString() || "Unknown";
|
|
349
|
+
const extension = this.getDefaultExtension();
|
|
350
|
+
session.answer({
|
|
351
|
+
mediaConstraints: { audio: true },
|
|
352
|
+
pcConfig: this.callConfig.pcConfig
|
|
353
|
+
});
|
|
354
|
+
const call = new CallSession(
|
|
355
|
+
session,
|
|
356
|
+
"inbound",
|
|
357
|
+
remoteUri,
|
|
358
|
+
extension,
|
|
359
|
+
this.events
|
|
360
|
+
);
|
|
361
|
+
this.currentCall = call;
|
|
362
|
+
return call;
|
|
363
|
+
} catch (error) {
|
|
364
|
+
throw new CallError(
|
|
365
|
+
`Failed to answer call: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
366
|
+
{ originalError: error }
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Handle incoming call from JsSIP
|
|
372
|
+
*/
|
|
373
|
+
handleIncomingCall(session) {
|
|
374
|
+
const remoteUri = session.remote_identity?.uri?.toString() || "Unknown";
|
|
375
|
+
const extension = this.getDefaultExtension();
|
|
376
|
+
const call = new CallSession(
|
|
377
|
+
session,
|
|
378
|
+
"inbound",
|
|
379
|
+
remoteUri,
|
|
380
|
+
extension,
|
|
381
|
+
this.events
|
|
382
|
+
);
|
|
383
|
+
call.status = "ringing";
|
|
384
|
+
this.currentCall = call;
|
|
385
|
+
this.events.emit("call:incoming", call);
|
|
386
|
+
return call;
|
|
387
|
+
}
|
|
388
|
+
getCurrentCall() {
|
|
389
|
+
return this.currentCall;
|
|
390
|
+
}
|
|
391
|
+
clearCurrentCall() {
|
|
392
|
+
if (this.currentCall) {
|
|
393
|
+
this.currentCall.destroy();
|
|
394
|
+
this.currentCall = null;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
getDefaultExtension() {
|
|
398
|
+
if (this.credentials.extensions && this.credentials.extensions.length > 0) {
|
|
399
|
+
return this.credentials.extensions[0].extension;
|
|
400
|
+
}
|
|
401
|
+
return this.extractExtension(this.credentials.sipUri);
|
|
402
|
+
}
|
|
403
|
+
extractExtension(sipUri) {
|
|
404
|
+
const match = sipUri.match(/sip:([^@]+)@/);
|
|
405
|
+
return match ? match[1] : "unknown";
|
|
406
|
+
}
|
|
407
|
+
extractDomain(sipUri) {
|
|
408
|
+
const match = sipUri.match(/@(.+)$/);
|
|
409
|
+
return match ? match[1] : "";
|
|
410
|
+
}
|
|
411
|
+
destroy() {
|
|
412
|
+
this.clearCurrentCall();
|
|
413
|
+
this.ua = null;
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
// src/core/DoraCell.ts
|
|
418
|
+
var DoraCell = class {
|
|
419
|
+
constructor(config) {
|
|
420
|
+
this.credentials = null;
|
|
421
|
+
this.ua = null;
|
|
422
|
+
// JsSIP User Agent
|
|
423
|
+
this.callManager = null;
|
|
424
|
+
this.connectionStatus = "disconnected";
|
|
425
|
+
this.retryCount = 0;
|
|
426
|
+
this.maxRetries = 3;
|
|
427
|
+
this.config = {
|
|
428
|
+
autoSelectExtension: true,
|
|
429
|
+
debug: false,
|
|
430
|
+
...config
|
|
431
|
+
};
|
|
432
|
+
this.events = new EventEmitter__default.default();
|
|
433
|
+
this.authProvider = createAuthProvider(config.auth);
|
|
434
|
+
if (this.config.debug) {
|
|
435
|
+
JsSIP__default.default.debug.enable("JsSIP:*");
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Initialize the SDK - authenticate and connect to SIP server
|
|
440
|
+
*/
|
|
441
|
+
async initialize() {
|
|
442
|
+
try {
|
|
443
|
+
this.credentials = await this.authProvider.authenticate(this.config.auth);
|
|
444
|
+
await this.initializeUserAgent();
|
|
445
|
+
this.initializeCallManager();
|
|
446
|
+
} catch (error) {
|
|
447
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
448
|
+
this.emitError(new ConnectionError(`Initialization failed: ${errorMessage}`));
|
|
449
|
+
throw error;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
async initializeUserAgent() {
|
|
453
|
+
if (!this.credentials) {
|
|
454
|
+
throw new ConnectionError("No credentials available");
|
|
455
|
+
}
|
|
456
|
+
try {
|
|
457
|
+
const socket = new JsSIP__default.default.WebSocketInterface(this.credentials.wsUrl);
|
|
458
|
+
const pcConfig = {
|
|
459
|
+
iceServers: this.config.turnServers || this.getDefaultTurnServers()
|
|
460
|
+
};
|
|
461
|
+
const uaConfig = {
|
|
462
|
+
uri: this.credentials.sipUri,
|
|
463
|
+
password: this.credentials.password,
|
|
464
|
+
sockets: [socket],
|
|
465
|
+
register: true,
|
|
466
|
+
display_name: this.getDisplayName(),
|
|
467
|
+
session_timers: false,
|
|
468
|
+
use_preloaded_route: false,
|
|
469
|
+
pcConfig,
|
|
470
|
+
instance_id: this.generateInstanceId()
|
|
471
|
+
};
|
|
472
|
+
this.ua = new JsSIP__default.default.UA(uaConfig);
|
|
473
|
+
this.setupUserAgentHandlers();
|
|
474
|
+
this.ua.start();
|
|
475
|
+
} catch (error) {
|
|
476
|
+
throw new ConnectionError(
|
|
477
|
+
`Failed to initialize User Agent: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
setupUserAgentHandlers() {
|
|
482
|
+
if (!this.ua) return;
|
|
483
|
+
this.ua.on("connected", () => {
|
|
484
|
+
this.connectionStatus = "connected";
|
|
485
|
+
this.emitConnectionStatus();
|
|
486
|
+
});
|
|
487
|
+
this.ua.on("disconnected", () => {
|
|
488
|
+
this.connectionStatus = "disconnected";
|
|
489
|
+
this.emitConnectionStatus();
|
|
490
|
+
});
|
|
491
|
+
this.ua.on("registered", () => {
|
|
492
|
+
this.connectionStatus = "registered";
|
|
493
|
+
this.retryCount = 0;
|
|
494
|
+
this.emitConnectionStatus();
|
|
495
|
+
});
|
|
496
|
+
this.ua.on("registrationFailed", (e) => {
|
|
497
|
+
this.connectionStatus = "registrationFailed";
|
|
498
|
+
this.emitConnectionStatus(e.cause);
|
|
499
|
+
if (this.retryCount < this.maxRetries) {
|
|
500
|
+
this.retryCount++;
|
|
501
|
+
setTimeout(() => {
|
|
502
|
+
this.ua?.register();
|
|
503
|
+
}, 3e3);
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
this.ua.on("newRTCSession", (e) => {
|
|
507
|
+
const session = e.session;
|
|
508
|
+
if (session.direction === "incoming") {
|
|
509
|
+
this.callManager?.handleIncomingCall(session);
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
initializeCallManager() {
|
|
514
|
+
if (!this.credentials) {
|
|
515
|
+
throw new ConnectionError("No credentials available for call manager");
|
|
516
|
+
}
|
|
517
|
+
const pcConfig = {
|
|
518
|
+
iceServers: this.config.turnServers || this.getDefaultTurnServers()
|
|
519
|
+
};
|
|
520
|
+
this.callManager = new CallManager(
|
|
521
|
+
this.credentials,
|
|
522
|
+
this.events,
|
|
523
|
+
{ pcConfig }
|
|
524
|
+
);
|
|
525
|
+
this.callManager.setUserAgent(this.ua);
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Make an outbound call
|
|
529
|
+
*/
|
|
530
|
+
async call(phoneNumber, options) {
|
|
531
|
+
if (!this.callManager) {
|
|
532
|
+
throw new CallError("SDK not initialized. Call initialize() first.");
|
|
533
|
+
}
|
|
534
|
+
if (this.connectionStatus !== "registered") {
|
|
535
|
+
throw new CallError("Not registered to SIP server");
|
|
536
|
+
}
|
|
537
|
+
return this.callManager.initiateCall(phoneNumber, options);
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Answer an incoming call
|
|
541
|
+
*/
|
|
542
|
+
answerCall() {
|
|
543
|
+
const currentCall = this.callManager?.getCurrentCall();
|
|
544
|
+
if (!currentCall) {
|
|
545
|
+
throw new CallError("No incoming call to answer");
|
|
546
|
+
}
|
|
547
|
+
if (currentCall.direction !== "inbound") {
|
|
548
|
+
throw new CallError("Current call is not an incoming call");
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Hangup the current call
|
|
553
|
+
*/
|
|
554
|
+
hangup() {
|
|
555
|
+
const currentCall = this.callManager?.getCurrentCall();
|
|
556
|
+
if (!currentCall) {
|
|
557
|
+
throw new CallError("No active call to hang up");
|
|
558
|
+
}
|
|
559
|
+
currentCall.hangup();
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Get current call
|
|
563
|
+
*/
|
|
564
|
+
getCurrentCall() {
|
|
565
|
+
return this.callManager?.getCurrentCall() || null;
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Get connection status
|
|
569
|
+
*/
|
|
570
|
+
getStatus() {
|
|
571
|
+
return this.connectionStatus;
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Get available extensions
|
|
575
|
+
*/
|
|
576
|
+
getExtensions() {
|
|
577
|
+
if (!this.credentials?.extensions) {
|
|
578
|
+
return [];
|
|
579
|
+
}
|
|
580
|
+
return this.credentials.extensions.map((ext) => ext.extension);
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Event listener management
|
|
584
|
+
*/
|
|
585
|
+
on(event, handler) {
|
|
586
|
+
this.events.on(event, handler);
|
|
587
|
+
}
|
|
588
|
+
off(event, handler) {
|
|
589
|
+
this.events.off(event, handler);
|
|
590
|
+
}
|
|
591
|
+
once(event, handler) {
|
|
592
|
+
this.events.once(event, handler);
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Cleanup and destroy the SDK instance
|
|
596
|
+
*/
|
|
597
|
+
destroy() {
|
|
598
|
+
if (this.ua) {
|
|
599
|
+
this.ua.stop();
|
|
600
|
+
this.ua = null;
|
|
601
|
+
}
|
|
602
|
+
if (this.callManager) {
|
|
603
|
+
this.callManager.destroy();
|
|
604
|
+
this.callManager = null;
|
|
605
|
+
}
|
|
606
|
+
this.events.removeAllListeners();
|
|
607
|
+
this.connectionStatus = "disconnected";
|
|
608
|
+
this.credentials = null;
|
|
609
|
+
}
|
|
610
|
+
// Helper methods
|
|
611
|
+
emitConnectionStatus(error) {
|
|
612
|
+
const state = {
|
|
613
|
+
status: this.connectionStatus,
|
|
614
|
+
extension: this.credentials?.extensions?.[0]?.extension,
|
|
615
|
+
error
|
|
616
|
+
};
|
|
617
|
+
this.events.emit("connection:status", state);
|
|
618
|
+
}
|
|
619
|
+
emitError(error) {
|
|
620
|
+
this.events.emit("error", error);
|
|
621
|
+
}
|
|
622
|
+
getDefaultTurnServers() {
|
|
623
|
+
const turnUri = process.env.NEXT_PUBLIC_TURN_URI || "turn:64.227.10.164:3478";
|
|
624
|
+
const turnUser = process.env.NEXT_PUBLIC_TURN_USER || "02018890089";
|
|
625
|
+
const turnPass = process.env.NEXT_PUBLIC_TURN_PASS || "dora12345";
|
|
626
|
+
return [
|
|
627
|
+
{
|
|
628
|
+
urls: turnUri,
|
|
629
|
+
username: turnUser,
|
|
630
|
+
credential: turnPass
|
|
631
|
+
}
|
|
632
|
+
];
|
|
633
|
+
}
|
|
634
|
+
getDisplayName() {
|
|
635
|
+
if (this.credentials?.extensions?.[0]?.displayName) {
|
|
636
|
+
return this.credentials.extensions[0].displayName;
|
|
637
|
+
}
|
|
638
|
+
if (this.credentials?.extensions?.[0]?.extension) {
|
|
639
|
+
return `Ext ${this.credentials.extensions[0].extension}`;
|
|
640
|
+
}
|
|
641
|
+
return "Dora Cell User";
|
|
642
|
+
}
|
|
643
|
+
generateInstanceId() {
|
|
644
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
645
|
+
const r = Math.random() * 16 | 0;
|
|
646
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
647
|
+
return v.toString(16);
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
exports.AuthenticationError = AuthenticationError;
|
|
653
|
+
exports.CallError = CallError;
|
|
654
|
+
exports.CallSession = CallSession;
|
|
655
|
+
exports.ConnectionError = ConnectionError;
|
|
656
|
+
exports.DoraCell = DoraCell;
|
|
657
|
+
exports.DoraCellError = DoraCellError;
|
|
658
|
+
exports.default = DoraCell;
|
|
659
|
+
exports.extractNumberFromSipUri = extractNumberFromSipUri;
|
|
660
|
+
exports.formatPhoneToSIP = formatPhoneToSIP;
|
|
661
|
+
exports.isValidPhoneNumber = isValidPhoneNumber;
|
|
662
|
+
exports.normalizePhoneNumber = normalizePhoneNumber;
|
|
663
|
+
//# sourceMappingURL=index.js.map
|
|
664
|
+
//# sourceMappingURL=index.js.map
|