@tarunzyraclavis/zyra-twilio-wrapper 1.0.0 → 1.0.8
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/dist/index.d.mts +38 -57
- package/dist/index.d.ts +38 -57
- package/dist/index.js +200 -297
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +200 -297
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
- package/src/index.ts +352 -438
- package/types/interface.ts +4 -40
package/src/index.ts
CHANGED
|
@@ -1,510 +1,424 @@
|
|
|
1
1
|
import axios from "axios";
|
|
2
2
|
import { Device, Call } from "@twilio/voice-sdk";
|
|
3
|
-
import { handleAxiosError } from
|
|
3
|
+
import { handleAxiosError } from "../utils/errorHandler";
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
} from
|
|
23
|
-
import { createSuccessResult, createErrorResult } from
|
|
5
|
+
AddParticipantPayload,
|
|
6
|
+
CallResponse,
|
|
7
|
+
Config,
|
|
8
|
+
GetConferencePayload,
|
|
9
|
+
GetConferenceResult,
|
|
10
|
+
GetParticipantsPayload,
|
|
11
|
+
GetParticipantsResult,
|
|
12
|
+
HoldAndAddResult,
|
|
13
|
+
HoldCallPayload,
|
|
14
|
+
HoldPayload,
|
|
15
|
+
RemoveParticipantPayload,
|
|
16
|
+
RemoveParticipantResult,
|
|
17
|
+
ResumeCallPayload,
|
|
18
|
+
TokenRequestPayload,
|
|
19
|
+
TokenResponse,
|
|
20
|
+
TwilioConference,
|
|
21
|
+
TwilioConferenceParticipant,
|
|
22
|
+
} from "../types/interface";
|
|
23
|
+
import { createSuccessResult, createErrorResult } from "../utils/helper";
|
|
24
24
|
|
|
25
25
|
export default class ZyraTwilioWrapper {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
26
|
+
private readonly serverUrl: string;
|
|
27
|
+
private readonly identity: string;
|
|
28
|
+
private readonly sdkToken: string;
|
|
29
|
+
|
|
30
|
+
private device: Device | null = null;
|
|
31
|
+
private activeConnection: Call | null = null;
|
|
32
|
+
|
|
33
|
+
private isInitialized = false;
|
|
34
|
+
private isInitializing = false;
|
|
35
|
+
private isAuthenticated = false;
|
|
36
|
+
|
|
37
|
+
private axiosInstance = axios.create();
|
|
38
|
+
|
|
39
|
+
public activeCallSid: string | null = null;
|
|
40
|
+
public conferenceName: string;
|
|
41
|
+
public conferenceDetail: TwilioConference | null = null;
|
|
42
|
+
public participants: TwilioConferenceParticipant[] = [];
|
|
43
|
+
public waitUrl = "https://api.twilio.com/cowbell.mp3";
|
|
44
|
+
|
|
45
|
+
// Optional event handlers
|
|
46
|
+
public onReady?: () => void;
|
|
47
|
+
public onIncoming?: (conn: Call) => void;
|
|
48
|
+
public onDisconnect?: () => void;
|
|
49
|
+
public onError?: (error: Error) => void;
|
|
50
|
+
public onConnect?: (conn: Call) => void;
|
|
51
|
+
public onMissedCall?: () => void;
|
|
52
|
+
|
|
53
|
+
constructor(config: Config) {
|
|
54
|
+
this.serverUrl = config.serverUrl;
|
|
55
|
+
this.identity = config.identity;
|
|
56
|
+
this.sdkToken = config.sdkToken;
|
|
57
|
+
this.waitUrl = config.waitUrl || this.waitUrl;
|
|
58
|
+
|
|
59
|
+
// safer conference name
|
|
60
|
+
this.conferenceName = `conf_${this.identity}_${Date.now()}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* ------------------------------------------------------------------ */
|
|
64
|
+
/* AUTH */
|
|
65
|
+
/* ------------------------------------------------------------------ */
|
|
66
|
+
|
|
67
|
+
private setupAxiosAuth(token: string) {
|
|
68
|
+
this.axiosInstance.defaults.headers.common["Authorization"] = `Bearer ${token}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private ensureAuthenticated() {
|
|
72
|
+
if (!this.isAuthenticated) {
|
|
73
|
+
throw new Error("SDK not authenticated.");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public async verifySDKToken(): Promise<boolean> {
|
|
78
|
+
if (!this.sdkToken) {
|
|
79
|
+
throw new Error("SDK token not provided");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const res = await this.axiosInstance.post(
|
|
84
|
+
`${this.serverUrl}/voipSdk.verifySdkToken`,
|
|
85
|
+
{},
|
|
86
|
+
{ headers: { Authorization: `Bearer ${this.sdkToken}` } }
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
console.log("verifySDKToken :", res.data.result.data.verified);
|
|
90
|
+
|
|
91
|
+
if (res?.data?.result?.data?.verified) {
|
|
92
|
+
this.isAuthenticated = true;
|
|
93
|
+
this.setupAxiosAuth(this.sdkToken);
|
|
94
|
+
console.info("[verifySDKToken] SDK token verified");
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
throw new Error("Invalid SDK token");
|
|
99
|
+
} catch (error) {
|
|
100
|
+
this.isAuthenticated = false;
|
|
101
|
+
throw new Error("SDK authentication failed");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// /**
|
|
106
|
+
// * Initialize the Twilio device and get token
|
|
107
|
+
// */
|
|
57
108
|
/**
|
|
58
109
|
* Initialize the SDK. Can only be called once.
|
|
59
110
|
*
|
|
60
111
|
* @throws {Error} If already initialized or initialization fails
|
|
61
112
|
* @returns Promise that resolves when initialization is complete
|
|
62
113
|
*/
|
|
63
|
-
public async init(): Promise<void> {
|
|
64
|
-
|
|
65
|
-
throw new Error(
|
|
66
|
-
"SDK is already initialized. Call destroy() first to reinitialize."
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (this.isInitializing) {
|
|
71
|
-
throw new Error("SDK initialization is already in progress");
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
this.isInitializing = true;
|
|
75
|
-
|
|
76
|
-
try {
|
|
77
|
-
// Validate configuration
|
|
78
|
-
if (!this.serverUrl) {
|
|
79
|
-
throw new Error("serverUrl is required");
|
|
80
|
-
}
|
|
81
|
-
if (!this.identity) {
|
|
82
|
-
throw new Error("identity is required");
|
|
83
|
-
}
|
|
114
|
+
public async init(): Promise<void> {
|
|
115
|
+
this.ensureAuthenticated();
|
|
84
116
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
// Setup event handlers
|
|
90
|
-
this.setupDeviceEventHandlers();
|
|
91
|
-
|
|
92
|
-
// Register the device
|
|
93
|
-
await this.device.register();
|
|
94
|
-
|
|
95
|
-
this.isInitialized = true;
|
|
96
|
-
console.info("[init] Device registration completed");
|
|
97
|
-
} catch (error: unknown) {
|
|
98
|
-
handleAxiosError(error, this.onError, "Failed to fetch token");
|
|
99
|
-
throw error;
|
|
100
|
-
} finally {
|
|
101
|
-
// Reset initializing flag even if init fails
|
|
102
|
-
this.isInitializing = false;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
117
|
+
if (this.isInitialized) {
|
|
118
|
+
throw new Error("SDK already initialized");
|
|
119
|
+
}
|
|
105
120
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const payload: TokenRequestPayload = {
|
|
110
|
-
identity: this.identity,
|
|
111
|
-
ttl: 3600,
|
|
112
|
-
incomingAllow: true,
|
|
113
|
-
};
|
|
121
|
+
if (this.isInitializing) {
|
|
122
|
+
throw new Error("SDK initialization already in progress");
|
|
123
|
+
}
|
|
114
124
|
|
|
115
|
-
|
|
125
|
+
this.isInitializing = true;
|
|
116
126
|
|
|
117
|
-
|
|
127
|
+
try {
|
|
128
|
+
const token = await this.generateToken();
|
|
129
|
+
this.device = new Device(token, { logLevel: "info" });
|
|
118
130
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
131
|
+
this.setupDeviceEventHandlers();
|
|
132
|
+
await this.device.register();
|
|
122
133
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
134
|
+
this.isInitialized = true;
|
|
135
|
+
console.info("[init] Device registered");
|
|
136
|
+
} finally {
|
|
137
|
+
this.isInitializing = false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Private methods for get token for initialize the device
|
|
141
|
+
|
|
142
|
+
private async generateToken(): Promise<string> {
|
|
143
|
+
try {
|
|
144
|
+
const payload: TokenRequestPayload = {
|
|
145
|
+
json: {
|
|
146
|
+
identity: this.identity,
|
|
147
|
+
ttl: 3600,
|
|
148
|
+
incomingAllow: true,
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
console.log("Generating token with payload:", payload);
|
|
153
|
+
|
|
154
|
+
const res = await this.axiosInstance.post<TokenResponse>(
|
|
155
|
+
`https://nextdev.zyratalk.com/api/trpc/voipSdk.generateToken`,
|
|
156
|
+
payload
|
|
157
|
+
); // This will be changed to this.serverUrl in production
|
|
158
|
+
|
|
159
|
+
const token = res?.data?.result?.data?.token;
|
|
160
|
+
if (!token) throw new Error("Token missing");
|
|
161
|
+
|
|
162
|
+
return token;
|
|
163
|
+
} catch (error) {
|
|
164
|
+
handleAxiosError(error, this.onError, "Token generation failed");
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
127
167
|
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Private methods for handling the event for device
|
|
131
|
-
private setupDeviceEventHandlers(): void {
|
|
132
|
-
if (!this.device) return;
|
|
133
|
-
|
|
134
|
-
this.device.on("registered", () => {
|
|
135
|
-
console.info("[Device] Registered");
|
|
136
|
-
this.onReady?.();
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
this.device.on("incoming", (conn: Call) => {
|
|
140
|
-
console.info("[Device] Incoming call detected");
|
|
141
|
-
conn.on("cancel", () => {
|
|
142
|
-
console.warn("[Device] Caller hung up before answer");
|
|
143
|
-
this.activeConnection = null;
|
|
144
|
-
this.onMissedCall?.();
|
|
145
|
-
});
|
|
146
|
-
conn.on("reject", () => {
|
|
147
|
-
console.warn("[Device] Call rejected");
|
|
148
|
-
this.activeConnection = null;
|
|
149
|
-
});
|
|
150
|
-
conn.on("accept", () => console.info("[Device] Call accepted"));
|
|
151
|
-
this.activeConnection = conn;
|
|
152
|
-
this.onIncoming?.(conn);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
this.device.on("disconnect", () => {
|
|
156
|
-
console.info("[Device] Call disconnected");
|
|
157
|
-
this.activeConnection = null;
|
|
158
|
-
this.onDisconnect?.();
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
this.device.on("connect", (conn: Call) => {
|
|
162
|
-
console.info("[Device] Call connected");
|
|
163
|
-
this.activeConnection = conn;
|
|
164
|
-
this.onConnect?.(conn);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
this.device.on("error", (error: Error) => {
|
|
168
|
-
handleAxiosError(error, this.onError, "Device encountered an error");
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
168
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
*/
|
|
175
|
-
get initialized(): boolean {
|
|
176
|
-
return this.isInitialized;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Destroy the SDK instance and clean up resources
|
|
181
|
-
*/
|
|
182
|
-
public destroy(): void {
|
|
183
|
-
if (this.device) {
|
|
184
|
-
// Remove all event listeners
|
|
185
|
-
this.device.removeAllListeners();
|
|
186
|
-
|
|
187
|
-
// Optionally, explicitly handle any final error events
|
|
188
|
-
this.device.on("error", (error: Error) => {
|
|
189
|
-
handleAxiosError(error, this.onError, "Device encountered an error during destroy");
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
// When Token expires then get new and update with twilio
|
|
193
|
-
this.device.on('tokenWillExpire', async () => {
|
|
194
|
-
const token = await this.generateToken();
|
|
195
|
-
this.device?.updateToken(token);
|
|
196
|
-
});
|
|
197
|
-
// Shutdown device
|
|
198
|
-
this.device.destroy();
|
|
199
|
-
this.device = null;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Reset state flags
|
|
203
|
-
this.isInitialized = false;
|
|
204
|
-
this.isInitializing = false;
|
|
205
|
-
this.activeConnection = null;
|
|
206
|
-
|
|
207
|
-
console.info("[destroy] Device destroyed and SDK state reset");
|
|
208
|
-
}
|
|
169
|
+
private setupDeviceEventHandlers() {
|
|
170
|
+
if (!this.device) return;
|
|
209
171
|
|
|
172
|
+
this.device.on("registered", () => this.onReady?.());
|
|
210
173
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
*/
|
|
214
|
-
public async makeCall(to: string): Promise<CallResponse<Call>> {
|
|
215
|
-
if (!this.device) {
|
|
216
|
-
return createErrorResult("Device not initialized");
|
|
217
|
-
}
|
|
174
|
+
this.device.on("incoming", (conn: Call) => {
|
|
175
|
+
this.activeConnection = conn;
|
|
218
176
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
177
|
+
conn.on("cancel", () => {
|
|
178
|
+
this.activeConnection = null;
|
|
179
|
+
this.onMissedCall?.();
|
|
180
|
+
});
|
|
223
181
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const connection = await this.device.connect({
|
|
227
|
-
params: { To: to, conferenceName: this.conferenceName },
|
|
228
|
-
});
|
|
182
|
+
conn.on("reject", () => (this.activeConnection = null));
|
|
183
|
+
conn.on("accept", () => console.info("[Device] Call accepted"));
|
|
229
184
|
|
|
230
|
-
|
|
185
|
+
this.onIncoming?.(conn);
|
|
186
|
+
});
|
|
231
187
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
this.onAnswerOutGoing?.(conn);
|
|
236
|
-
await this.getConferenceId();
|
|
188
|
+
this.device.on("connect", (conn: Call) => {
|
|
189
|
+
this.activeConnection = conn;
|
|
190
|
+
this.onConnect?.(conn);
|
|
237
191
|
});
|
|
238
192
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
193
|
+
this.device.on("disconnect", () => {
|
|
194
|
+
this.activeConnection = null;
|
|
195
|
+
this.onDisconnect?.();
|
|
242
196
|
});
|
|
243
197
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
this.activeConnection = null;
|
|
247
|
-
this.onDisconnect?.();
|
|
198
|
+
this.device.on("error", (error: Error) => {
|
|
199
|
+
handleAxiosError(error, this.onError, "Device error");
|
|
248
200
|
});
|
|
249
201
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
console.error("[makeCall] Failed to initiate call:", err.message);
|
|
255
|
-
this.onError?.(err);
|
|
256
|
-
return createErrorResult(`[makeCall] Failed to initiate call: ${err.message}`);
|
|
257
|
-
}
|
|
258
|
-
} else {
|
|
259
|
-
try {
|
|
260
|
-
console.info("[makeCall] Active call found. Holding and adding new participant...");
|
|
261
|
-
await this.holdAndAddParticipant(this.activeConnection.parameters.CallSid, to);
|
|
262
|
-
return createSuccessResult(`[makeCall] Outgoing participant ${to} added`, this.activeConnection);
|
|
263
|
-
} catch (error: unknown) {
|
|
264
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
265
|
-
console.error("[makeCall] Failed to add participant:", err.message);
|
|
266
|
-
this.onError?.(err);
|
|
267
|
-
return createErrorResult(`[makeCall] Failed to add participant: ${err.message}`);
|
|
268
|
-
}
|
|
202
|
+
this.device.on("tokenWillExpire", async () => {
|
|
203
|
+
const token = await this.generateToken();
|
|
204
|
+
this.device?.updateToken(token);
|
|
205
|
+
});
|
|
269
206
|
}
|
|
270
|
-
}
|
|
271
207
|
|
|
272
|
-
|
|
273
|
-
|
|
208
|
+
/* ------------------------------------------------------------------ */
|
|
209
|
+
/* CALL CONTROL */
|
|
210
|
+
/* ------------------------------------------------------------------ */
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Make an outgoing call or add participant to an existing call
|
|
274
214
|
*/
|
|
275
|
-
public acceptCall(): CallResponse<Call> {
|
|
276
|
-
if (!this.device || !this.activeConnection) {
|
|
277
|
-
const msg = !this.device
|
|
278
|
-
? "Device not initialized."
|
|
279
|
-
: "No active call to accept.";
|
|
280
|
-
console.warn(`[acceptCall] ${msg}`);
|
|
281
|
-
return createErrorResult(`[acceptCall] ${msg}`);
|
|
282
|
-
}
|
|
283
215
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
216
|
+
public async makeCall(to: string): Promise<CallResponse<Call>> {
|
|
217
|
+
this.ensureAuthenticated();
|
|
218
|
+
|
|
219
|
+
if (!this.device) return createErrorResult("Device not initialized");
|
|
220
|
+
if (!to) return createErrorResult("Destination required");
|
|
221
|
+
|
|
222
|
+
if (!this.activeConnection) {
|
|
223
|
+
const conn = await this.device.connect({
|
|
224
|
+
params: { To: to, conferenceName: this.conferenceName },
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
this.activeConnection = conn;
|
|
228
|
+
|
|
229
|
+
conn.on("accept", async () => {
|
|
230
|
+
this.activeCallSid = conn.parameters?.CallSid ?? null;
|
|
231
|
+
await this.getConferenceId();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
return createSuccessResult("Call started", conn);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
await this.holdAndAddParticipant(
|
|
238
|
+
this.activeConnection.parameters.CallSid,
|
|
239
|
+
to
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
return createSuccessResult("Participant added", this.activeConnection);
|
|
293
243
|
}
|
|
294
|
-
}
|
|
295
244
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
245
|
+
/**
|
|
246
|
+
* Accept incoming call
|
|
247
|
+
*/
|
|
248
|
+
|
|
249
|
+
public acceptCall(): CallResponse<Call> {
|
|
250
|
+
if (!this.activeConnection) {
|
|
251
|
+
return createErrorResult("No incoming call");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
this.activeConnection.accept();
|
|
255
|
+
return createSuccessResult("Call accepted", this.activeConnection);
|
|
306
256
|
}
|
|
307
257
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
258
|
+
/**
|
|
259
|
+
* Hang up active call
|
|
260
|
+
*/
|
|
261
|
+
|
|
262
|
+
public hangup(): CallResponse<null> {
|
|
263
|
+
if (!this.device) return createErrorResult("Device not initialized");
|
|
264
|
+
|
|
265
|
+
this.device.disconnectAll();
|
|
266
|
+
this.activeConnection = null;
|
|
267
|
+
|
|
268
|
+
return createSuccessResult("Call ended", null);
|
|
318
269
|
}
|
|
319
|
-
}
|
|
320
270
|
|
|
321
|
-
|
|
271
|
+
/**
|
|
322
272
|
* Reject incoming call
|
|
323
273
|
*/
|
|
324
|
-
public rejectCall(): CallResponse<Call | null> {
|
|
325
|
-
if (!this.device || !this.activeConnection) {
|
|
326
|
-
const msg = !this.device
|
|
327
|
-
? "Device not initialized."
|
|
328
|
-
: "No active call to reject.";
|
|
329
|
-
console.warn(`[rejectCall] ${msg}`);
|
|
330
|
-
return createErrorResult(`[rejectCall] ${msg}`);
|
|
331
|
-
}
|
|
332
274
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
console.error("[rejectCall] Failed:", err.message);
|
|
341
|
-
this.onError?.(err);
|
|
342
|
-
return createErrorResult(`Failed to reject call: ${err.message}`);
|
|
275
|
+
public rejectCall(): CallResponse<null> {
|
|
276
|
+
if (!this.activeConnection) return createErrorResult("No call to reject");
|
|
277
|
+
|
|
278
|
+
this.activeConnection.reject();
|
|
279
|
+
this.activeConnection = null;
|
|
280
|
+
|
|
281
|
+
return createSuccessResult("Call rejected", null);
|
|
343
282
|
}
|
|
344
|
-
}
|
|
345
283
|
|
|
346
|
-
|
|
284
|
+
/**
|
|
347
285
|
* Toggle mute/unmute
|
|
348
286
|
*/
|
|
349
|
-
public toggleMute(mute: boolean): CallResponse<Call> {
|
|
350
|
-
if (!this.activeConnection) {
|
|
351
|
-
console.warn("[toggleMute] No active call to mute/unmute.");
|
|
352
|
-
return createErrorResult("[toggleMute] No active call to mute/unmute.");
|
|
353
|
-
}
|
|
354
287
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
361
|
-
console.error("[toggleMute] Failed:", err.message);
|
|
362
|
-
this.onError?.(err);
|
|
363
|
-
return createErrorResult(`Failed to toggle mute: ${err.message}`);
|
|
288
|
+
public toggleMute(mute: boolean): CallResponse<Call> {
|
|
289
|
+
if (!this.activeConnection) return createErrorResult("No active call");
|
|
290
|
+
|
|
291
|
+
this.activeConnection.mute(mute);
|
|
292
|
+
return createSuccessResult(`Mute ${mute}`, this.activeConnection);
|
|
364
293
|
}
|
|
365
|
-
}
|
|
366
294
|
|
|
367
|
-
|
|
295
|
+
/* ------------------------------------------------------------------ */
|
|
296
|
+
/* CONFERENCE API */
|
|
297
|
+
/* ------------------------------------------------------------------ */
|
|
298
|
+
|
|
299
|
+
/**
|
|
368
300
|
* Hold a call
|
|
369
301
|
*/
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
handleAxiosError(error, this.onError, "Failed to hold call");
|
|
382
|
-
const errMessage = error instanceof Error ? error.message : String(error);
|
|
383
|
-
return createErrorResult(`Failed to hold call: ${errMessage}`);
|
|
302
|
+
public async hold(callSid: string) {
|
|
303
|
+
this.ensureAuthenticated();
|
|
304
|
+
|
|
305
|
+
const payload: HoldCallPayload = {
|
|
306
|
+
callSid,
|
|
307
|
+
conferenceName: this.conferenceName,
|
|
308
|
+
waitUrl: this.waitUrl,
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
await this.axiosInstance.post(`${this.serverUrl}/outgoingCall.hold`, payload);
|
|
312
|
+
return createSuccessResult("Call held", this.activeConnection);
|
|
384
313
|
}
|
|
385
|
-
}
|
|
386
314
|
|
|
387
|
-
|
|
315
|
+
/**
|
|
388
316
|
* Resume a held call
|
|
389
317
|
*/
|
|
390
|
-
public async resume(callSid: string): Promise<CallResponse<Call>> {
|
|
391
|
-
if (!this.activeConnection) return createErrorResult("No active call to resume.");
|
|
392
|
-
if (!callSid) return createErrorResult("Please provide callSid.");
|
|
393
|
-
|
|
394
|
-
try {
|
|
395
|
-
const payload: ResumeCallPayload = { callSid, conferenceName: this.conferenceName };
|
|
396
|
-
await axios.post(`${this.serverUrl}/outgoingCall.unhold`, payload);
|
|
397
|
-
console.info("[resume] Call resumed successfully.");
|
|
398
|
-
return createSuccessResult("Call resumed successfully.", this.activeConnection);
|
|
399
|
-
} catch (error: unknown) {
|
|
400
|
-
handleAxiosError(error, this.onError, "Failed to resume call");
|
|
401
|
-
const errMessage = error instanceof Error ? error.message : String(error);
|
|
402
|
-
return createErrorResult(`Failed to resume call: ${errMessage}`);
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
318
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
console.info("[holdAndAddParticipant] Call held and participant added successfully.");
|
|
422
|
-
return {
|
|
423
|
-
message: "Call held and participant added successfully.",
|
|
424
|
-
status: true,
|
|
425
|
-
holdResponse: holdRes.data,
|
|
426
|
-
addParticipantResponse: addRes.data,
|
|
427
|
-
};
|
|
428
|
-
} catch (error: unknown) {
|
|
429
|
-
handleAxiosError(error, this.onError, "Failed to hold and add participant");
|
|
430
|
-
const errMessage = error instanceof Error ? error.message : String(error);
|
|
431
|
-
return createErrorResult(`Failed to hold/add participant: ${errMessage}`);
|
|
319
|
+
public async resume(callSid: string) {
|
|
320
|
+
this.ensureAuthenticated();
|
|
321
|
+
|
|
322
|
+
const payload: ResumeCallPayload = {
|
|
323
|
+
callSid,
|
|
324
|
+
conferenceName: this.conferenceName,
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
await this.axiosInstance.post(
|
|
328
|
+
`${this.serverUrl}/outgoingCall.unhold`,
|
|
329
|
+
payload
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
return createSuccessResult("Call resumed", this.activeConnection);
|
|
432
333
|
}
|
|
433
|
-
}
|
|
434
334
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
public async getConferenceId(): Promise<GetConferenceResult | CallResponse<TwilioConference>> {
|
|
439
|
-
if (!this.activeConnection) return createErrorResult("No active call.");
|
|
335
|
+
/**
|
|
336
|
+
* Hold active call and add a new participant
|
|
337
|
+
*/
|
|
440
338
|
|
|
441
|
-
|
|
339
|
+
public async holdAndAddParticipant(callSid: string, number: string) {
|
|
340
|
+
this.ensureAuthenticated();
|
|
442
341
|
|
|
443
|
-
|
|
444
|
-
const res = await axios.post(`${this.serverUrl}/outgoingCall.getConferenceId`, payload);
|
|
342
|
+
await this.hold(callSid);
|
|
445
343
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
conferences: res.data.result.data.conferences,
|
|
344
|
+
const payload: AddParticipantPayload = {
|
|
345
|
+
conferenceName: this.conferenceName,
|
|
346
|
+
newParticipantNo: number,
|
|
450
347
|
};
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
} catch (error: unknown) {
|
|
459
|
-
handleAxiosError(error, this.onError, "Failed to get conference ID");
|
|
460
|
-
const errMessage = error instanceof Error ? error.message : String(error);
|
|
461
|
-
return createErrorResult(`Failed to get conference ID: ${errMessage}`);
|
|
348
|
+
|
|
349
|
+
const res = await this.axiosInstance.post(
|
|
350
|
+
`${this.serverUrl}/outgoingCall.addNewCallee`,
|
|
351
|
+
payload
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
return createSuccessResult("Participant added", res.data);
|
|
462
355
|
}
|
|
463
|
-
}
|
|
464
356
|
|
|
465
|
-
|
|
357
|
+
/**
|
|
358
|
+
* Fetch conference details
|
|
359
|
+
*/
|
|
360
|
+
|
|
361
|
+
public async getConferenceId() {
|
|
362
|
+
this.ensureAuthenticated();
|
|
363
|
+
|
|
364
|
+
const payload: GetConferencePayload = {
|
|
365
|
+
conferenceName: this.conferenceName,
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const res = await this.axiosInstance.post(
|
|
369
|
+
`${this.serverUrl}/outgoingCall.getConferenceId`,
|
|
370
|
+
payload
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
this.conferenceDetail = res?.data?.result?.data ?? null;
|
|
374
|
+
return createSuccessResult("Conference fetched", this.conferenceDetail);
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
466
377
|
* Get participants of a conference
|
|
467
378
|
*/
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
return createSuccessResult("Participants fetched
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
console.warn("[getParticipants] No participants returned from server.");
|
|
483
|
-
return { message: "No participants returned.", status: false };
|
|
484
|
-
} catch (error: unknown) {
|
|
485
|
-
handleAxiosError(error, this.onError, "Failed to get participants");
|
|
486
|
-
const errMessage = error instanceof Error ? error.message : String(error);
|
|
487
|
-
return createErrorResult(`Failed to get participants: ${errMessage}`);
|
|
379
|
+
public async getParticipants(conferenceSid: string) {
|
|
380
|
+
this.ensureAuthenticated();
|
|
381
|
+
|
|
382
|
+
const payload: GetParticipantsPayload = { conferenceSid };
|
|
383
|
+
|
|
384
|
+
const res = await this.axiosInstance.post(
|
|
385
|
+
`${this.serverUrl}/outgoingCall.getParticipants`,
|
|
386
|
+
payload
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
this.participants = res?.data?.result?.data?.participants ?? [];
|
|
390
|
+
return createSuccessResult("Participants fetched", this.participants);
|
|
488
391
|
}
|
|
489
|
-
}
|
|
490
392
|
|
|
491
|
-
|
|
393
|
+
/**
|
|
492
394
|
* Remove a participant from a conference
|
|
493
|
-
*/
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
} catch (error: unknown) {
|
|
505
|
-
handleAxiosError(error, this.onError, "Failed to remove participant");
|
|
506
|
-
const errMessage = error instanceof Error ? error.message : String(error);
|
|
507
|
-
return createErrorResult(`Failed to remove participant: ${errMessage}`);
|
|
395
|
+
*/ public async removeParticipant(conferenceSid: string, callSid: string) {
|
|
396
|
+
this.ensureAuthenticated();
|
|
397
|
+
|
|
398
|
+
const payload: RemoveParticipantPayload = { conferenceSid, callSid };
|
|
399
|
+
|
|
400
|
+
const res = await this.axiosInstance.post(
|
|
401
|
+
`${this.serverUrl}/outgoingCall.removeParticipant`,
|
|
402
|
+
payload
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
return createSuccessResult("Participant removed", res.data);
|
|
508
406
|
}
|
|
509
|
-
|
|
510
|
-
|
|
407
|
+
|
|
408
|
+
/* ------------------------------------------------------------------ */
|
|
409
|
+
/* CLEANUP */
|
|
410
|
+
/* ------------------------------------------------------------------ */
|
|
411
|
+
|
|
412
|
+
public destroy() {
|
|
413
|
+
if (this.device) {
|
|
414
|
+
this.device.removeAllListeners();
|
|
415
|
+
this.device.destroy();
|
|
416
|
+
this.device = null;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
this.activeConnection = null;
|
|
420
|
+
this.isInitialized = false;
|
|
421
|
+
this.isInitializing = false;
|
|
422
|
+
this.isAuthenticated = false;
|
|
423
|
+
}
|
|
424
|
+
}
|