@snakechat/commskit-node 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +87 -2
- package/dist/adapters-8ztjEYtz.d.ts +83 -0
- package/dist/call.d.ts +34 -0
- package/dist/call.js +51 -0
- package/dist/call.js.map +1 -0
- package/dist/chat.d.ts +64 -0
- package/dist/chat.js +30 -0
- package/dist/chat.js.map +1 -0
- package/dist/chunk-5BJ2SNM5.js +26 -0
- package/dist/chunk-5BJ2SNM5.js.map +1 -0
- package/dist/chunk-6I27CI32.js +586 -0
- package/dist/chunk-6I27CI32.js.map +1 -0
- package/dist/chunk-BBFVG7QU.js +52 -0
- package/dist/chunk-BBFVG7QU.js.map +1 -0
- package/dist/chunk-GR4PMGCS.js +82 -0
- package/dist/chunk-GR4PMGCS.js.map +1 -0
- package/dist/chunk-KL4WRZKZ.js +799 -0
- package/dist/chunk-KL4WRZKZ.js.map +1 -0
- package/dist/chunk-KLKRK6OG.js +107 -0
- package/dist/chunk-KLKRK6OG.js.map +1 -0
- package/dist/chunk-LY2GJWN7.js +708 -0
- package/dist/chunk-LY2GJWN7.js.map +1 -0
- package/dist/chunk-SEDRF62Q.js +39 -0
- package/dist/chunk-SEDRF62Q.js.map +1 -0
- package/dist/chunk-U4Z5INIW.js +1192 -0
- package/dist/chunk-U4Z5INIW.js.map +1 -0
- package/dist/chunk-XOXZZYSW.js +247 -0
- package/dist/chunk-XOXZZYSW.js.map +1 -0
- package/dist/chunk-XQ6FLRX7.js +179 -0
- package/dist/chunk-XQ6FLRX7.js.map +1 -0
- package/dist/chunk-XQFQN73U.js +38 -0
- package/dist/chunk-XQFQN73U.js.map +1 -0
- package/dist/connections-CGEPiZPF.d.ts +128 -0
- package/dist/core.d.ts +26 -0
- package/dist/core.js +89 -0
- package/dist/core.js.map +1 -0
- package/dist/devices-oLIB_M19.d.ts +97 -0
- package/dist/express.d.ts +19 -0
- package/dist/express.js +34 -0
- package/dist/express.js.map +1 -0
- package/dist/fastify.d.ts +22 -0
- package/dist/fastify.js +42 -0
- package/dist/fastify.js.map +1 -0
- package/dist/feature-B2bEXLZG.d.ts +233 -0
- package/dist/feature-DQUbnYE-.d.ts +284 -0
- package/dist/feature-DwKkuOBh.d.ts +195 -0
- package/dist/hub-CPrWmIvB.d.ts +174 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +138 -0
- package/dist/index.js.map +1 -0
- package/dist/integration.d.ts +97 -0
- package/dist/integration.js +151 -0
- package/dist/integration.js.map +1 -0
- package/dist/models-D1lehXIu.d.ts +31 -0
- package/dist/models-DTXEX1Q8.d.ts +34 -0
- package/dist/moderation.d.ts +9 -0
- package/dist/moderation.js +48 -0
- package/dist/moderation.js.map +1 -0
- package/dist/push.d.ts +198 -0
- package/dist/push.js +475 -0
- package/dist/push.js.map +1 -0
- package/dist/redis.d.ts +103 -0
- package/dist/redis.js +296 -0
- package/dist/redis.js.map +1 -0
- package/dist/s3.d.ts +75 -0
- package/dist/s3.js +315 -0
- package/dist/s3.js.map +1 -0
- package/dist/util-B32rIBVA.d.ts +22 -0
- package/package.json +148 -6
|
@@ -0,0 +1,799 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HttpError,
|
|
3
|
+
dbBool,
|
|
4
|
+
dbNow,
|
|
5
|
+
dbTimestamp,
|
|
6
|
+
detectDialect,
|
|
7
|
+
ok
|
|
8
|
+
} from "./chunk-GR4PMGCS.js";
|
|
9
|
+
import {
|
|
10
|
+
RateLimit,
|
|
11
|
+
retryAfterSeconds
|
|
12
|
+
} from "./chunk-BBFVG7QU.js";
|
|
13
|
+
import {
|
|
14
|
+
LRU,
|
|
15
|
+
nowIso,
|
|
16
|
+
parseDbDate,
|
|
17
|
+
pyBool,
|
|
18
|
+
pyStr
|
|
19
|
+
} from "./chunk-KLKRK6OG.js";
|
|
20
|
+
import {
|
|
21
|
+
logger
|
|
22
|
+
} from "./chunk-5BJ2SNM5.js";
|
|
23
|
+
|
|
24
|
+
// src/call/config.ts
|
|
25
|
+
var defaultCallConfig = () => ({
|
|
26
|
+
ringTimeout: 45,
|
|
27
|
+
ringingDisconnectGrace: 20,
|
|
28
|
+
answeredDisconnectGrace: 90,
|
|
29
|
+
maxCallHours: 4,
|
|
30
|
+
ringWindowSlack: 15,
|
|
31
|
+
ringtoneChannel: "call_ringtone",
|
|
32
|
+
missedChannel: "messages",
|
|
33
|
+
turnSecret: null,
|
|
34
|
+
turnUris: [],
|
|
35
|
+
turnTtl: 3600,
|
|
36
|
+
initiateRate: new RateLimit(5, 60)
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// src/call/payloads.ts
|
|
40
|
+
function callPayload(event, info, extra = {}) {
|
|
41
|
+
return {
|
|
42
|
+
type: event,
|
|
43
|
+
call_id: info.callId,
|
|
44
|
+
chat_id: info.chatId,
|
|
45
|
+
caller_id: info.callerId,
|
|
46
|
+
receiver_id: info.receiverId,
|
|
47
|
+
call_type: info.callType,
|
|
48
|
+
webrtc_session_id: info.callId,
|
|
49
|
+
timestamp: nowIso(),
|
|
50
|
+
...extra
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
var callerView = (info) => ({
|
|
54
|
+
caller_id: info.receiverId,
|
|
55
|
+
caller_name: info.receiverId
|
|
56
|
+
});
|
|
57
|
+
function busyPayload(callerId, receiverId, callType) {
|
|
58
|
+
return {
|
|
59
|
+
type: "call_rejected",
|
|
60
|
+
call_id: null,
|
|
61
|
+
status: "busy",
|
|
62
|
+
reason: "busy",
|
|
63
|
+
caller_id: callerId,
|
|
64
|
+
receiver_id: receiverId,
|
|
65
|
+
call_type: callType,
|
|
66
|
+
timestamp: nowIso()
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
var callError = (error, extra = {}) => ({
|
|
70
|
+
type: "error",
|
|
71
|
+
error,
|
|
72
|
+
...extra,
|
|
73
|
+
context: "call"
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// src/call/service.ts
|
|
77
|
+
import { randomUUID } from "crypto";
|
|
78
|
+
var ACTIVE_CALL_STATUSES = ["ringing", "answered"];
|
|
79
|
+
var CallInfo = class _CallInfo {
|
|
80
|
+
constructor(callId, chatId, callerId, receiverId, isVideoCall, status, duration = 0, endReason = null) {
|
|
81
|
+
this.callId = callId;
|
|
82
|
+
this.chatId = chatId;
|
|
83
|
+
this.callerId = callerId;
|
|
84
|
+
this.receiverId = receiverId;
|
|
85
|
+
this.isVideoCall = isVideoCall;
|
|
86
|
+
this.status = status;
|
|
87
|
+
this.duration = duration;
|
|
88
|
+
this.endReason = endReason;
|
|
89
|
+
}
|
|
90
|
+
callId;
|
|
91
|
+
chatId;
|
|
92
|
+
callerId;
|
|
93
|
+
receiverId;
|
|
94
|
+
isVideoCall;
|
|
95
|
+
status;
|
|
96
|
+
duration;
|
|
97
|
+
endReason;
|
|
98
|
+
/** Filled by a chat bridge during finalize: the call's chat entry. */
|
|
99
|
+
messageId = null;
|
|
100
|
+
logText = null;
|
|
101
|
+
static fromRow(row) {
|
|
102
|
+
return new _CallInfo(
|
|
103
|
+
row.call_id,
|
|
104
|
+
row.chat_id === null || row.chat_id === void 0 ? null : Number(row.chat_id),
|
|
105
|
+
row.caller_id,
|
|
106
|
+
row.receiver_id,
|
|
107
|
+
Boolean(row.is_video_call),
|
|
108
|
+
row.call_status,
|
|
109
|
+
row.duration ?? 0,
|
|
110
|
+
row.end_reason ?? null
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
get callType() {
|
|
114
|
+
return this.isVideoCall ? "video" : "audio";
|
|
115
|
+
}
|
|
116
|
+
otherParty(userId) {
|
|
117
|
+
if (userId === this.callerId) return this.receiverId;
|
|
118
|
+
if (userId === this.receiverId) return this.callerId;
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
clone(patch = {}) {
|
|
122
|
+
return Object.assign(Object.create(_CallInfo.prototype), this, patch);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
function callLogText(status, isVideoCall, duration, _endReason = null) {
|
|
126
|
+
const kind = isVideoCall ? "Video call" : "Voice call";
|
|
127
|
+
if (status === "missed") return `Missed ${kind.toLowerCase()}`;
|
|
128
|
+
if (status === "rejected") return `Declined ${kind.toLowerCase()}`;
|
|
129
|
+
const total = Math.max(0, Math.trunc(duration));
|
|
130
|
+
const hours = Math.floor(total / 3600);
|
|
131
|
+
const mins = Math.floor(total % 3600 / 60);
|
|
132
|
+
const secs = total % 60;
|
|
133
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
134
|
+
return `${kind} \xB7 ${hours ? `${hours}:${p(mins)}:${p(secs)}` : `${p(mins)}:${p(secs)}`}`;
|
|
135
|
+
}
|
|
136
|
+
var CallService = class {
|
|
137
|
+
constructor(db, config, bridge = null, dialect) {
|
|
138
|
+
this.db = db;
|
|
139
|
+
this.bridge = bridge;
|
|
140
|
+
this.config = config ?? defaultCallConfig();
|
|
141
|
+
this.dialect = dialect ?? detectDialect(db);
|
|
142
|
+
}
|
|
143
|
+
db;
|
|
144
|
+
bridge;
|
|
145
|
+
dialect;
|
|
146
|
+
config;
|
|
147
|
+
ts(d = dbNow()) {
|
|
148
|
+
return dbTimestamp(this.dialect, d);
|
|
149
|
+
}
|
|
150
|
+
async createCall(chatId, callerId, receiverId, isVideoCall = false) {
|
|
151
|
+
const callId = randomUUID();
|
|
152
|
+
await this.db.insertInto("calls").values({
|
|
153
|
+
call_id: callId,
|
|
154
|
+
chat_id: chatId,
|
|
155
|
+
caller_id: callerId,
|
|
156
|
+
receiver_id: receiverId,
|
|
157
|
+
call_status: "ringing",
|
|
158
|
+
is_video_call: dbBool(this.dialect, isVideoCall),
|
|
159
|
+
started_at: this.ts()
|
|
160
|
+
}).execute();
|
|
161
|
+
return new CallInfo(callId, chatId, callerId, receiverId, isVideoCall, "ringing");
|
|
162
|
+
}
|
|
163
|
+
async getCallById(callId) {
|
|
164
|
+
return this.db.selectFrom("calls").selectAll().where("call_id", "=", callId).executeTakeFirst();
|
|
165
|
+
}
|
|
166
|
+
async getCallInfo(callId) {
|
|
167
|
+
const row = await this.getCallById(callId);
|
|
168
|
+
return row ? CallInfo.fromRow(row) : null;
|
|
169
|
+
}
|
|
170
|
+
async saveOfferSdp(callId, offerSdp) {
|
|
171
|
+
await this.db.updateTable("calls").set({ offer_sdp: offerSdp }).where("call_id", "=", callId).execute();
|
|
172
|
+
}
|
|
173
|
+
async getOfferSdp(callId) {
|
|
174
|
+
const r = await this.db.selectFrom("calls").select("offer_sdp").where("call_id", "=", callId).executeTakeFirst();
|
|
175
|
+
return r?.offer_sdp ?? null;
|
|
176
|
+
}
|
|
177
|
+
liveCallsQuery(userId) {
|
|
178
|
+
const now = dbNow();
|
|
179
|
+
const cfg = this.config;
|
|
180
|
+
const ringCutoff = this.ts(new Date(now.getTime() - (cfg.ringTimeout + cfg.ringWindowSlack) * 1e3));
|
|
181
|
+
const answeredCutoff = this.ts(new Date(now.getTime() - cfg.maxCallHours * 3600 * 1e3));
|
|
182
|
+
return this.db.selectFrom("calls").selectAll().where(
|
|
183
|
+
(eb) => eb.and([
|
|
184
|
+
eb.or([eb("caller_id", "=", userId), eb("receiver_id", "=", userId)]),
|
|
185
|
+
eb.or([
|
|
186
|
+
eb.and([eb("call_status", "=", "ringing"), eb("started_at", ">=", ringCutoff)]),
|
|
187
|
+
eb.and([eb("call_status", "=", "answered"), eb("answered_at", ">=", answeredCutoff)])
|
|
188
|
+
])
|
|
189
|
+
])
|
|
190
|
+
).orderBy("started_at", "desc").orderBy("id", "desc");
|
|
191
|
+
}
|
|
192
|
+
async getActiveCallForUser(userId) {
|
|
193
|
+
const row = await this.liveCallsQuery(userId).limit(1).executeTakeFirst();
|
|
194
|
+
return row ? CallInfo.fromRow(row) : null;
|
|
195
|
+
}
|
|
196
|
+
async getActiveCallsForUsers(userId) {
|
|
197
|
+
return (await this.liveCallsQuery(userId).execute()).map((r) => CallInfo.fromRow(r));
|
|
198
|
+
}
|
|
199
|
+
/** ringing -> answered. Only the receiver can answer, and only once. */
|
|
200
|
+
async markAnswered(callId, receiverId) {
|
|
201
|
+
const res = await this.db.updateTable("calls").set({ call_status: "answered", answered_at: this.ts() }).where("call_id", "=", callId).where("receiver_id", "=", receiverId).where("call_status", "=", "ringing").executeTakeFirst();
|
|
202
|
+
if (Number(res.numUpdatedRows) !== 1) return null;
|
|
203
|
+
return this.getCallInfo(callId);
|
|
204
|
+
}
|
|
205
|
+
/** Move a live call to a terminal status. null if another path already did. */
|
|
206
|
+
async finalizeCall(callId, status, endReason, onlyFrom = ACTIVE_CALL_STATUSES) {
|
|
207
|
+
const call = await this.getCallById(callId);
|
|
208
|
+
if (!call || !onlyFrom.includes(call.call_status)) return null;
|
|
209
|
+
const fromStatus = call.call_status;
|
|
210
|
+
const snapshot = CallInfo.fromRow(call);
|
|
211
|
+
const now = dbNow();
|
|
212
|
+
let duration = 0;
|
|
213
|
+
const answeredAt = parseDbDate(call.answered_at);
|
|
214
|
+
if (fromStatus === "answered" && answeredAt) duration = Math.max(0, Math.trunc((now.getTime() - answeredAt.getTime()) / 1e3));
|
|
215
|
+
const values = { call_status: status, end_reason: endReason, ended_at: this.ts(now), duration };
|
|
216
|
+
if (status === "missed") values.missed_at = this.ts(now);
|
|
217
|
+
const info = snapshot.clone({ status, duration, endReason });
|
|
218
|
+
const bridge = this.bridge;
|
|
219
|
+
const done = await this.db.transaction().execute(async (trx) => {
|
|
220
|
+
const res = await trx.updateTable("calls").set(values).where("call_id", "=", callId).where("call_status", "=", fromStatus).executeTakeFirst();
|
|
221
|
+
if (Number(res.numUpdatedRows) !== 1) return false;
|
|
222
|
+
if (bridge) await bridge.onCallFinalizing(trx, info);
|
|
223
|
+
return true;
|
|
224
|
+
});
|
|
225
|
+
return done ? info : null;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// src/call/handlers.ts
|
|
230
|
+
var CallHandlers = class {
|
|
231
|
+
constructor(feature) {
|
|
232
|
+
this.feature = feature;
|
|
233
|
+
}
|
|
234
|
+
feature;
|
|
235
|
+
get hub() {
|
|
236
|
+
return this.feature.hub;
|
|
237
|
+
}
|
|
238
|
+
get lifecycle() {
|
|
239
|
+
return this.feature.lifecycle;
|
|
240
|
+
}
|
|
241
|
+
register() {
|
|
242
|
+
const on = this.hub.on.bind(this.hub);
|
|
243
|
+
on("call_initiate", this.guard((u, m, d) => this.callInitiate(u, m, d), "Failed to initiate call"));
|
|
244
|
+
on("answer_call", this.guard((u, m, d) => this.answerCall(u, m, d), "Failed to answer call"));
|
|
245
|
+
on("reject_call", this.guard((u, m, d) => this.endCall(u, m, d), "Failed to reject call"));
|
|
246
|
+
on("end_call", this.guard((u, m, d) => this.endCall(u, m, d), "Failed to end call"));
|
|
247
|
+
on("sdp_offer", this.guard((u, m, d) => this.relaySdp(u, m, d, "sdp_offer"), null));
|
|
248
|
+
on("sdp_answer", this.guard((u, m, d) => this.relaySdp(u, m, d, "sdp_answer"), null));
|
|
249
|
+
on("ice_candidate", this.guard((u, m, d) => this.iceCandidate(u, m, d), null));
|
|
250
|
+
on("start_recording", async () => void 0);
|
|
251
|
+
}
|
|
252
|
+
/** Crash -> call-tagged error (label) or logged only (signalling relays). */
|
|
253
|
+
guard(fn, label) {
|
|
254
|
+
return async (userId, message, db) => {
|
|
255
|
+
try {
|
|
256
|
+
await fn(userId, message, db);
|
|
257
|
+
} catch (err) {
|
|
258
|
+
logger.error(`${message.type} handler failed`, err);
|
|
259
|
+
await this.hub.reportHandlerError(userId, message, err);
|
|
260
|
+
if (label) await this.error(userId, `${label}: ${err instanceof Error ? err.message : String(err)}`);
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
async error(userId, error, extra = {}) {
|
|
265
|
+
await this.hub.send(userId, callError(error, extra));
|
|
266
|
+
}
|
|
267
|
+
// ------------------------------------------------------------ initiate
|
|
268
|
+
async callInitiate(userId, message, db) {
|
|
269
|
+
const lifecycle = this.lifecycle;
|
|
270
|
+
const connections = this.hub.connections;
|
|
271
|
+
const rawReceiver = message.receiver_id;
|
|
272
|
+
const callType = message.call_type === "video" ? "video" : "audio";
|
|
273
|
+
if (!pyBool(rawReceiver)) {
|
|
274
|
+
await this.error(userId, "Missing required fields: receiver_id");
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const receiverId = pyStr(rawReceiver);
|
|
278
|
+
if (receiverId === userId) {
|
|
279
|
+
await this.error(userId, "You cannot call yourself.");
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const limit = this.feature.config.initiateRate;
|
|
283
|
+
const wait = limit ? await this.hub.rateLimiter.hit(`call.initiate:${userId}`, limit) : 0;
|
|
284
|
+
if (wait) {
|
|
285
|
+
const seconds = retryAfterSeconds(wait);
|
|
286
|
+
await this.error(userId, `Too many calls. Try again in ${seconds} s.`, { code: "rate_limited", retry_after: seconds });
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const service = lifecycle.service(db);
|
|
290
|
+
const ownCall = await service.getActiveCallForUser(userId);
|
|
291
|
+
if (ownCall && ownCall.status === "ringing") {
|
|
292
|
+
await this.error(userId, "You are already in a call.");
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (ownCall) {
|
|
296
|
+
await lifecycle.finishCall(db, ownCall, userId);
|
|
297
|
+
}
|
|
298
|
+
let receiverCall = await service.getActiveCallForUser(receiverId);
|
|
299
|
+
if (receiverCall && !(await connections.isConnected(receiverCall.callerId) || await connections.isConnected(receiverCall.receiverId))) {
|
|
300
|
+
const stale = await service.finalizeCall(
|
|
301
|
+
receiverCall.callId,
|
|
302
|
+
receiverCall.status === "answered" ? "ended" : "missed",
|
|
303
|
+
"stale",
|
|
304
|
+
[receiverCall.status]
|
|
305
|
+
);
|
|
306
|
+
if (stale) await lifecycle.finalized(stale);
|
|
307
|
+
receiverCall = await service.getActiveCallForUser(receiverId);
|
|
308
|
+
}
|
|
309
|
+
if (receiverCall) {
|
|
310
|
+
await this.hub.send(userId, busyPayload(userId, receiverId, callType));
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const decision = await this.feature.policy.authorize(db, userId, receiverId, message);
|
|
314
|
+
if (!decision.allowed) {
|
|
315
|
+
const error = { ...decision.error ?? { error: "Call not allowed." } };
|
|
316
|
+
if (!("error" in error)) error.error = "Call not allowed.";
|
|
317
|
+
await this.hub.send(userId, { type: "error", ...error, context: "call" });
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const bridge = this.feature.chatBridge;
|
|
321
|
+
const chatId = bridge ? await bridge.resolveChatId(db, userId, receiverId, message.chat_id) : null;
|
|
322
|
+
const info = await service.createCall(chatId, userId, receiverId, callType === "video");
|
|
323
|
+
lifecycle.scheduleRingTimeout(info.callId);
|
|
324
|
+
if (bridge) await bridge.onCallCreated(db, info);
|
|
325
|
+
lifecycle.remember(info);
|
|
326
|
+
await this.hub.send(userId, callPayload("call_initiated", info, { status: "calling", ...callerView(info) }));
|
|
327
|
+
const incoming = await lifecycle.incomingPayload(info);
|
|
328
|
+
const delivered = await this.hub.send(receiverId, incoming);
|
|
329
|
+
if (delivered) {
|
|
330
|
+
await this.hub.send(userId, callPayload("call_ringing", info, { status: "ringing", ...callerView(info) }));
|
|
331
|
+
} else {
|
|
332
|
+
this.hub.spawn(lifecycle.pushIncoming(info, incoming.caller_name, incoming.caller_avatar));
|
|
333
|
+
}
|
|
334
|
+
logger.info(`Call ${info.callId} initiated ${userId} -> ${receiverId}`);
|
|
335
|
+
}
|
|
336
|
+
// -------------------------------------------------------------- answer
|
|
337
|
+
async answerCall(userId, message, db) {
|
|
338
|
+
if (!pyBool(message.call_id)) {
|
|
339
|
+
await this.error(userId, "Missing required fields: call_id");
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const callId = pyStr(message.call_id);
|
|
343
|
+
const lifecycle = this.lifecycle;
|
|
344
|
+
const info = await lifecycle.service(db).markAnswered(callId, userId);
|
|
345
|
+
if (!info) {
|
|
346
|
+
const call = await lifecycle.loadCallFor(db, callId, userId);
|
|
347
|
+
if (!call) {
|
|
348
|
+
await this.error(userId, "Call not found", { call_id: callId });
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (call.status === "answered") return;
|
|
352
|
+
await this.hub.send(
|
|
353
|
+
userId,
|
|
354
|
+
callPayload("call_ended", call, { status: "call_ended", end_reason: call.status, duration_seconds: call.duration })
|
|
355
|
+
);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
lifecycle.remember(info);
|
|
359
|
+
const payload = callPayload("call_answered", info, { status: "answered", answerer_id: userId });
|
|
360
|
+
await this.hub.send(info.callerId, payload);
|
|
361
|
+
await this.hub.send(userId, payload);
|
|
362
|
+
}
|
|
363
|
+
// ------------------------------------------------------- reject / end
|
|
364
|
+
async endCall(userId, message, db) {
|
|
365
|
+
if (!pyBool(message.call_id)) {
|
|
366
|
+
await this.error(userId, "Missing required fields: call_id");
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const call = await this.lifecycle.loadCallFor(db, pyStr(message.call_id), userId);
|
|
370
|
+
if (!call) {
|
|
371
|
+
await this.error(userId, "Call not found", { call_id: message.call_id });
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
await this.lifecycle.finishCall(db, call, userId);
|
|
375
|
+
}
|
|
376
|
+
// ---------------------------------------------------------- signalling
|
|
377
|
+
async relaySdp(userId, message, db, event) {
|
|
378
|
+
if (!pyBool(message.call_id) || !pyBool(message.sdp)) {
|
|
379
|
+
await this.error(userId, "Missing required fields: sdp, call_id");
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const callId = pyStr(message.call_id);
|
|
383
|
+
const sdp = message.sdp;
|
|
384
|
+
const call = await this.lifecycle.loadCallFor(db, callId, userId);
|
|
385
|
+
if (!call || !ACTIVE_CALL_STATUSES.includes(call.status)) {
|
|
386
|
+
await this.error(userId, "No active call found.", { call_id: callId });
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (event === "sdp_offer" && userId === call.callerId) {
|
|
390
|
+
await this.lifecycle.service(db).saveOfferSdp(callId, JSON.stringify(sdp));
|
|
391
|
+
}
|
|
392
|
+
const other = call.otherParty(userId);
|
|
393
|
+
if (other) await this.hub.send(other, { type: event, call_id: callId, from_user_id: userId, sdp, timestamp: nowIso() });
|
|
394
|
+
}
|
|
395
|
+
async iceCandidate(userId, message, db) {
|
|
396
|
+
if (!pyBool(message.call_id) || !pyBool(message.candidate)) return;
|
|
397
|
+
const callId = pyStr(message.call_id);
|
|
398
|
+
let parties = this.lifecycle.parties.get(callId);
|
|
399
|
+
if (!parties) {
|
|
400
|
+
const call = await this.lifecycle.loadCallFor(db, callId, userId);
|
|
401
|
+
if (!call) return;
|
|
402
|
+
parties = [call.callerId, call.receiverId];
|
|
403
|
+
}
|
|
404
|
+
const [callerId, receiverId] = parties;
|
|
405
|
+
if (!parties.includes(userId)) return;
|
|
406
|
+
await this.hub.send(userId === callerId ? receiverId : callerId, {
|
|
407
|
+
type: "ice_candidate",
|
|
408
|
+
call_id: callId,
|
|
409
|
+
from_user_id: userId,
|
|
410
|
+
candidate: message.candidate,
|
|
411
|
+
timestamp: nowIso()
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
// src/call/lifecycle.ts
|
|
417
|
+
function endStatus(call, fromUser) {
|
|
418
|
+
if (call.status === "answered") return ["ended", "hangup"];
|
|
419
|
+
if (fromUser === call.callerId) return ["missed", "caller_cancelled"];
|
|
420
|
+
return ["rejected", "declined"];
|
|
421
|
+
}
|
|
422
|
+
var CallLifecycle = class {
|
|
423
|
+
constructor(feature) {
|
|
424
|
+
this.feature = feature;
|
|
425
|
+
}
|
|
426
|
+
feature;
|
|
427
|
+
/** callId -> [callerId, receiverId]: participants never change, so this per-worker cache keeps ICE off the DB. */
|
|
428
|
+
parties = new LRU(5e3);
|
|
429
|
+
timers = /* @__PURE__ */ new Set();
|
|
430
|
+
stopped = false;
|
|
431
|
+
get hub() {
|
|
432
|
+
return this.feature.hub;
|
|
433
|
+
}
|
|
434
|
+
get config() {
|
|
435
|
+
return this.feature.config;
|
|
436
|
+
}
|
|
437
|
+
service(db) {
|
|
438
|
+
return new CallService(db, this.config, this.feature.chatBridge, this.hub.dialect);
|
|
439
|
+
}
|
|
440
|
+
remember(info) {
|
|
441
|
+
this.parties.put(info.callId, [info.callerId, info.receiverId]);
|
|
442
|
+
}
|
|
443
|
+
/** Emit `call.finalized` (the chat integration sends `call_log` from it). */
|
|
444
|
+
async finalized(info) {
|
|
445
|
+
await this.hub.events.emit("call.finalized", info);
|
|
446
|
+
}
|
|
447
|
+
/** The call, only if `userId` is a participant. */
|
|
448
|
+
async loadCallFor(db, callId, userId) {
|
|
449
|
+
const call = await this.service(db).getCallInfo(String(callId));
|
|
450
|
+
if (!call || call.otherParty(userId) === null) return null;
|
|
451
|
+
this.remember(call);
|
|
452
|
+
return call;
|
|
453
|
+
}
|
|
454
|
+
// ----------------------------------------------------------- finishing
|
|
455
|
+
/** Hangup / reject / cancel by `fromUser`; outcome decided from call state. */
|
|
456
|
+
async finishCall(db, callIn, fromUser) {
|
|
457
|
+
const service = this.service(db);
|
|
458
|
+
let call = callIn;
|
|
459
|
+
let info = null;
|
|
460
|
+
let reason = "";
|
|
461
|
+
for (let i = 0; i < 2; i++) {
|
|
462
|
+
if (!ACTIVE_CALL_STATUSES.includes(call.status)) break;
|
|
463
|
+
let status;
|
|
464
|
+
[status, reason] = endStatus(call, fromUser);
|
|
465
|
+
info = await service.finalizeCall(call.callId, status, reason, [call.status]);
|
|
466
|
+
if (info) break;
|
|
467
|
+
call = await service.getCallInfo(call.callId) ?? call;
|
|
468
|
+
}
|
|
469
|
+
if (!info) {
|
|
470
|
+
await this.hub.send(
|
|
471
|
+
fromUser,
|
|
472
|
+
callPayload("call_ended", call, {
|
|
473
|
+
status: "call_ended",
|
|
474
|
+
end_reason: call.status,
|
|
475
|
+
ended_by: fromUser,
|
|
476
|
+
duration_seconds: call.duration,
|
|
477
|
+
caller_name: call.callerId
|
|
478
|
+
})
|
|
479
|
+
);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
const rejected = info.status === "rejected";
|
|
483
|
+
const payload = callPayload(rejected ? "call_rejected" : "call_ended", info, {
|
|
484
|
+
status: rejected ? "call_rejected" : "call_ended",
|
|
485
|
+
end_reason: reason,
|
|
486
|
+
duration_seconds: info.duration,
|
|
487
|
+
caller_name: info.callerId,
|
|
488
|
+
...rejected ? { rejector_id: fromUser } : { ended_by: fromUser }
|
|
489
|
+
});
|
|
490
|
+
const other = info.otherParty(fromUser);
|
|
491
|
+
if (other) await this.hub.send(other, payload);
|
|
492
|
+
await this.hub.send(fromUser, payload);
|
|
493
|
+
await this.finalized(info);
|
|
494
|
+
if (info.status === "missed" && fromUser === info.callerId) {
|
|
495
|
+
this.hub.spawn(this.pushMissedCall(info));
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* End every live call between two users with `endReason` (e.g. "blocked"). Answered calls become `ended`,
|
|
500
|
+
* ringing ones `missed`. Both parties get `call_ended`; `call.finalized` is emitted.
|
|
501
|
+
*/
|
|
502
|
+
async endCallsBetween(userA, userB, endReason, endedBy = null) {
|
|
503
|
+
const ended = [];
|
|
504
|
+
const service = this.service(this.hub.db);
|
|
505
|
+
for (const call of await service.getActiveCallsForUsers(userA)) {
|
|
506
|
+
if (call.otherParty(userA) !== userB) continue;
|
|
507
|
+
const toStatus = call.status === "answered" ? "ended" : "missed";
|
|
508
|
+
const info = await service.finalizeCall(call.callId, toStatus, endReason, [call.status]);
|
|
509
|
+
if (info) ended.push(info);
|
|
510
|
+
}
|
|
511
|
+
for (const info of ended) {
|
|
512
|
+
const payload = callPayload("call_ended", info, {
|
|
513
|
+
status: "call_ended",
|
|
514
|
+
end_reason: endReason,
|
|
515
|
+
ended_by: endedBy ?? userA,
|
|
516
|
+
duration_seconds: info.duration,
|
|
517
|
+
caller_name: info.callerId
|
|
518
|
+
});
|
|
519
|
+
await this.hub.send(info.callerId, payload);
|
|
520
|
+
await this.hub.send(info.receiverId, payload);
|
|
521
|
+
await this.finalized(info);
|
|
522
|
+
if (info.status === "missed") this.hub.spawn(this.pushMissedCall(info));
|
|
523
|
+
}
|
|
524
|
+
return ended;
|
|
525
|
+
}
|
|
526
|
+
async rejectFromNotification(db, callId, userId) {
|
|
527
|
+
const call = await this.loadCallFor(db, callId, userId);
|
|
528
|
+
if (!call) return false;
|
|
529
|
+
await this.finishCall(db, call, userId);
|
|
530
|
+
return true;
|
|
531
|
+
}
|
|
532
|
+
// -------------------------------------------------------------- timers
|
|
533
|
+
later(seconds, fn) {
|
|
534
|
+
if (this.stopped) return;
|
|
535
|
+
const t = setTimeout(() => {
|
|
536
|
+
this.timers.delete(t);
|
|
537
|
+
this.hub.spawn(fn());
|
|
538
|
+
}, seconds * 1e3);
|
|
539
|
+
t.unref?.();
|
|
540
|
+
this.timers.add(t);
|
|
541
|
+
}
|
|
542
|
+
/** Cancel every pending ring-timeout / disconnect-grace timer (hub shutdown). */
|
|
543
|
+
cancelAll() {
|
|
544
|
+
this.stopped = true;
|
|
545
|
+
for (const t of this.timers) clearTimeout(t);
|
|
546
|
+
this.timers.clear();
|
|
547
|
+
}
|
|
548
|
+
scheduleRingTimeout(callId) {
|
|
549
|
+
this.later(this.config.ringTimeout, () => this.expireUnanswered(callId));
|
|
550
|
+
}
|
|
551
|
+
/** Ring timeout: ringing -> missed (no_answer). */
|
|
552
|
+
async expireUnanswered(callId) {
|
|
553
|
+
try {
|
|
554
|
+
const info = await this.service(this.hub.db).finalizeCall(callId, "missed", "no_answer", ["ringing"]);
|
|
555
|
+
if (!info) return;
|
|
556
|
+
const payload = callPayload("call_missed", info, { status: "missed", end_reason: "no_answer" });
|
|
557
|
+
await this.hub.send(info.callerId, payload);
|
|
558
|
+
await this.hub.send(info.receiverId, payload);
|
|
559
|
+
await this.finalized(info);
|
|
560
|
+
await this.pushMissedCall(info);
|
|
561
|
+
} catch (err) {
|
|
562
|
+
logger.error(`Ring timeout handling failed for ${callId}`, err);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
async endUserCalls(userId, fromStatus, toStatus) {
|
|
566
|
+
const ended = [];
|
|
567
|
+
const service = this.service(this.hub.db);
|
|
568
|
+
for (const call of await service.getActiveCallsForUsers(userId)) {
|
|
569
|
+
if (call.status !== fromStatus) continue;
|
|
570
|
+
const info = await service.finalizeCall(call.callId, toStatus, "disconnected", [fromStatus]);
|
|
571
|
+
if (info) ended.push(info);
|
|
572
|
+
}
|
|
573
|
+
for (const info of ended) {
|
|
574
|
+
const other = info.otherParty(userId);
|
|
575
|
+
if (other) {
|
|
576
|
+
await this.hub.send(
|
|
577
|
+
other,
|
|
578
|
+
callPayload("call_ended", info, {
|
|
579
|
+
status: "call_ended",
|
|
580
|
+
ended_by: userId,
|
|
581
|
+
end_reason: "disconnected",
|
|
582
|
+
duration_seconds: info.duration
|
|
583
|
+
})
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
await this.finalized(info);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
/** Offline hook: after a grace period end calls the user is still in (answered ones get longer). */
|
|
590
|
+
endCallsAfterDisconnect(userId) {
|
|
591
|
+
const cfg = this.config;
|
|
592
|
+
this.later(cfg.ringingDisconnectGrace, async () => {
|
|
593
|
+
try {
|
|
594
|
+
if (await this.hub.connections.isConnected(userId)) return;
|
|
595
|
+
await this.endUserCalls(userId, "ringing", "missed");
|
|
596
|
+
this.later(Math.max(0, cfg.answeredDisconnectGrace - cfg.ringingDisconnectGrace), async () => {
|
|
597
|
+
try {
|
|
598
|
+
if (await this.hub.connections.isConnected(userId)) return;
|
|
599
|
+
await this.endUserCalls(userId, "answered", "ended");
|
|
600
|
+
} catch (err) {
|
|
601
|
+
logger.error(`Ending calls for disconnected ${userId} failed`, err);
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
} catch (err) {
|
|
605
|
+
logger.error(`Ending calls for disconnected ${userId} failed`, err);
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
// ------------------------------------------------------------- connect
|
|
610
|
+
/** Re-ring a call still waiting for this user, on the new socket only, with the stored offer SDP. */
|
|
611
|
+
async onConnect(userId, ws) {
|
|
612
|
+
const service = this.service(this.hub.db);
|
|
613
|
+
const call = await service.getActiveCallForUser(userId);
|
|
614
|
+
if (!call || call.status !== "ringing" || call.receiverId !== userId) return;
|
|
615
|
+
const offerSdp = await service.getOfferSdp(call.callId);
|
|
616
|
+
this.remember(call);
|
|
617
|
+
const payload = await this.incomingPayload(call);
|
|
618
|
+
if (offerSdp) {
|
|
619
|
+
try {
|
|
620
|
+
payload.sdp = JSON.parse(offerSdp);
|
|
621
|
+
} catch {
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
await this.hub.connections.sendToSocket(ws, payload);
|
|
625
|
+
}
|
|
626
|
+
async incomingPayload(info) {
|
|
627
|
+
const profile = await this.hub.profile(info.callerId);
|
|
628
|
+
return callPayload("incoming_call", info, {
|
|
629
|
+
caller_name: profile?.name ? profile.name : "Someone",
|
|
630
|
+
caller_avatar: profile ? profile.avatar ?? null : null
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
// --------------------------------------------------------------- push
|
|
634
|
+
async pushIncoming(info, callerName, callerAvatar) {
|
|
635
|
+
await this.hub.push(
|
|
636
|
+
info.receiverId,
|
|
637
|
+
callerName,
|
|
638
|
+
`Incoming ${info.callType} call`,
|
|
639
|
+
{
|
|
640
|
+
type: "incoming_call",
|
|
641
|
+
call_id: info.callId,
|
|
642
|
+
chat_id: info.chatId,
|
|
643
|
+
caller_id: info.callerId,
|
|
644
|
+
caller_name: callerName,
|
|
645
|
+
caller_avatar: callerAvatar,
|
|
646
|
+
call_type: info.callType
|
|
647
|
+
},
|
|
648
|
+
this.config.ringtoneChannel,
|
|
649
|
+
Math.trunc(this.config.ringTimeout)
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
/** Receiver's app is not in the foreground: leave a missed-call notification (also stops a ringing one). */
|
|
653
|
+
async pushMissedCall(info) {
|
|
654
|
+
if (await this.hub.connections.isOnline(info.receiverId)) return;
|
|
655
|
+
const callerName = await this.hub.displayName(info.callerId);
|
|
656
|
+
await this.hub.push(
|
|
657
|
+
info.receiverId,
|
|
658
|
+
callerName,
|
|
659
|
+
`Missed ${info.callType} call`,
|
|
660
|
+
{ type: "missed_call", call_id: info.callId, chat_id: info.chatId, caller_id: info.callerId, call_type: info.callType },
|
|
661
|
+
this.config.missedChannel
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
// src/call/policy.ts
|
|
667
|
+
var allow = () => ({ allowed: true });
|
|
668
|
+
var deny = (error, fields = {}) => ({
|
|
669
|
+
allowed: false,
|
|
670
|
+
error: { error, ...fields }
|
|
671
|
+
});
|
|
672
|
+
var AllowAllCallPolicy = class {
|
|
673
|
+
authorize() {
|
|
674
|
+
return allow();
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
// src/call/turn.ts
|
|
679
|
+
import { createHmac } from "crypto";
|
|
680
|
+
function turnCredentials(secret, uris, ttl = 3600, userId = null, now = null) {
|
|
681
|
+
const expiry = Math.trunc(now ?? Date.now() / 1e3) + ttl;
|
|
682
|
+
const username = userId ? `${expiry}:${userId}` : String(expiry);
|
|
683
|
+
return {
|
|
684
|
+
username,
|
|
685
|
+
password: createHmac("sha1", secret).update(username).digest("base64"),
|
|
686
|
+
ttl,
|
|
687
|
+
uris: [...uris]
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// src/call/router.ts
|
|
692
|
+
function buildCallRoutes(feature) {
|
|
693
|
+
const hub = feature.hub;
|
|
694
|
+
return [
|
|
695
|
+
{
|
|
696
|
+
method: "POST",
|
|
697
|
+
path: "/call/turn-credentials",
|
|
698
|
+
handler() {
|
|
699
|
+
const cfg = feature.config;
|
|
700
|
+
if (!cfg.turnSecret) throw new HttpError(503, "TURN is not configured");
|
|
701
|
+
return ok(turnCredentials(cfg.turnSecret, cfg.turnUris, cfg.turnTtl));
|
|
702
|
+
}
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
// Decline from the call notification while the app has no socket.
|
|
706
|
+
method: "POST",
|
|
707
|
+
path: "/call/:callId/reject",
|
|
708
|
+
async handler(ctx) {
|
|
709
|
+
const found = await feature.lifecycle.rejectFromNotification(hub.db, ctx.params.callId, ctx.userId);
|
|
710
|
+
if (!found) return { status: 404, body: { status: false, message: "Call not found" } };
|
|
711
|
+
return ok({ status: true });
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
];
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// src/call/feature.ts
|
|
718
|
+
var CallFeature = class {
|
|
719
|
+
name = "call";
|
|
720
|
+
policy;
|
|
721
|
+
config;
|
|
722
|
+
chatBridge = null;
|
|
723
|
+
lifecycle;
|
|
724
|
+
handlers;
|
|
725
|
+
_hub = null;
|
|
726
|
+
constructor(opts = {}) {
|
|
727
|
+
this.policy = opts.policy ?? new AllowAllCallPolicy();
|
|
728
|
+
this.config = { ...defaultCallConfig(), ...opts.config ?? {} };
|
|
729
|
+
}
|
|
730
|
+
get hub() {
|
|
731
|
+
if (!this._hub) throw new Error("CallFeature is not installed; call hub.install(feature) first");
|
|
732
|
+
return this._hub;
|
|
733
|
+
}
|
|
734
|
+
install(hub) {
|
|
735
|
+
hub.registerFeature(this.name, this);
|
|
736
|
+
this._hub = hub;
|
|
737
|
+
this.lifecycle = new CallLifecycle(this);
|
|
738
|
+
this.handlers = new CallHandlers(this);
|
|
739
|
+
this.handlers.register();
|
|
740
|
+
hub.onConnect((uid, ws) => this.lifecycle.onConnect(uid, ws));
|
|
741
|
+
hub.onDisconnect((uid) => this.lifecycle.endCallsAfterDisconnect(uid));
|
|
742
|
+
hub.includeRoutes(buildCallRoutes(this));
|
|
743
|
+
const shutdown = hub.shutdown.bind(hub);
|
|
744
|
+
hub.shutdown = async () => {
|
|
745
|
+
this.lifecycle.cancelAll();
|
|
746
|
+
await shutdown();
|
|
747
|
+
};
|
|
748
|
+
return this;
|
|
749
|
+
}
|
|
750
|
+
/** End every live call between two users (e.g. on block). Both get `call_ended`; `call.finalized` is emitted. */
|
|
751
|
+
endCallsBetween(userA, userB, endReason, endedBy = null) {
|
|
752
|
+
return this.lifecycle.endCallsBetween(userA, userB, endReason, endedBy);
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Account deletion: remove every calls row where the user is caller or receiver. Live calls are not
|
|
756
|
+
* signalled; disconnect the user first (or end their calls) if needed.
|
|
757
|
+
*/
|
|
758
|
+
async deleteUserData(userId) {
|
|
759
|
+
const uid = String(userId);
|
|
760
|
+
const res = await this.hub.db.deleteFrom("calls").where((eb) => eb.or([eb("caller_id", "=", uid), eb("receiver_id", "=", uid)])).executeTakeFirst();
|
|
761
|
+
return { calls: Number(res.numDeletedRows) };
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
|
|
765
|
+
// src/call/uuid.ts
|
|
766
|
+
import { createHash } from "crypto";
|
|
767
|
+
var NAMESPACE_URL = "6ba7b811-9dad-11d1-80b4-00c04fd430c8";
|
|
768
|
+
function uuidv5(name, namespace = NAMESPACE_URL) {
|
|
769
|
+
const ns = Buffer.from(namespace.replace(/-/g, ""), "hex");
|
|
770
|
+
const h = createHash("sha1").update(ns).update(Buffer.from(name, "utf8")).digest().subarray(0, 16);
|
|
771
|
+
h[6] = h[6] & 15 | 80;
|
|
772
|
+
h[8] = h[8] & 63 | 128;
|
|
773
|
+
const x = h.toString("hex");
|
|
774
|
+
return `${x.slice(0, 8)}-${x.slice(8, 12)}-${x.slice(12, 16)}-${x.slice(16, 20)}-${x.slice(20)}`;
|
|
775
|
+
}
|
|
776
|
+
var callkitUuid = (callId) => uuidv5(`commskit:call:${callId}`);
|
|
777
|
+
|
|
778
|
+
export {
|
|
779
|
+
defaultCallConfig,
|
|
780
|
+
callPayload,
|
|
781
|
+
callerView,
|
|
782
|
+
busyPayload,
|
|
783
|
+
callError,
|
|
784
|
+
ACTIVE_CALL_STATUSES,
|
|
785
|
+
CallInfo,
|
|
786
|
+
callLogText,
|
|
787
|
+
CallService,
|
|
788
|
+
CallHandlers,
|
|
789
|
+
endStatus,
|
|
790
|
+
CallLifecycle,
|
|
791
|
+
allow,
|
|
792
|
+
deny,
|
|
793
|
+
AllowAllCallPolicy,
|
|
794
|
+
turnCredentials,
|
|
795
|
+
CallFeature,
|
|
796
|
+
uuidv5,
|
|
797
|
+
callkitUuid
|
|
798
|
+
};
|
|
799
|
+
//# sourceMappingURL=chunk-KL4WRZKZ.js.map
|