sipgate-mcp 0.3.0 → 0.5.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/CHANGELOG.md +137 -0
- package/README.md +161 -13
- package/SKILL.md +58 -8
- package/dist/backend/access-controlled-backend.d.ts +152 -1
- package/dist/backend/access-controlled-backend.d.ts.map +1 -1
- package/dist/backend/access-controlled-backend.js +1213 -25
- package/dist/backend/access-controlled-backend.js.map +1 -1
- package/dist/backend/sipgate-backend.d.ts +149 -1
- package/dist/backend/sipgate-backend.d.ts.map +1 -1
- package/dist/backend/sipgate-backend.js +1299 -12
- package/dist/backend/sipgate-backend.js.map +1 -1
- package/dist/backend/sipgate-client.d.ts +3 -0
- package/dist/backend/sipgate-client.d.ts.map +1 -1
- package/dist/backend/sipgate-client.js +22 -12
- package/dist/backend/sipgate-client.js.map +1 -1
- package/dist/backend/telephony-backend.d.ts +301 -0
- package/dist/backend/telephony-backend.d.ts.map +1 -1
- package/dist/credentials.d.ts +1 -1
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js +11 -5
- package/dist/credentials.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +3 -2
- package/dist/server.js.map +1 -1
- package/dist/setup.d.ts +13 -2
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +72 -17
- package/dist/setup.js.map +1 -1
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +1455 -60
- package/dist/tools/definitions.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SipgateApiError, SipgateClient } from "./sipgate-client.js";
|
|
2
|
-
const SENSITIVE_KEY = /authorization|credential|password|secret|token/i;
|
|
2
|
+
const SENSITIVE_KEY = /authorization|credential|password|secret|token|puk\d?|iccid/i;
|
|
3
3
|
function sanitize(value) {
|
|
4
4
|
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
5
5
|
return value;
|
|
@@ -17,13 +17,117 @@ function asItems(value) {
|
|
|
17
17
|
const items = value.items;
|
|
18
18
|
return Array.isArray(items) ? items : [];
|
|
19
19
|
}
|
|
20
|
+
function asCalls(value) {
|
|
21
|
+
if (!value || Array.isArray(value) || typeof value !== "object")
|
|
22
|
+
return [];
|
|
23
|
+
return Array.isArray(value.data)
|
|
24
|
+
? value.data.filter((item) => Boolean(item) && !Array.isArray(item) && typeof item === "object")
|
|
25
|
+
: [];
|
|
26
|
+
}
|
|
20
27
|
function stringField(value, key) {
|
|
21
28
|
const field = value[key];
|
|
22
29
|
return typeof field === "string" ? field : undefined;
|
|
23
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Account-wide writes are confirmed at the access boundary, but SipgateBackend
|
|
33
|
+
* is exported on its own, so the invariant is repeated here rather than trusted.
|
|
34
|
+
*/
|
|
35
|
+
function assertConfirmed(confirmed, action) {
|
|
36
|
+
if (confirmed === true)
|
|
37
|
+
return;
|
|
38
|
+
throw new SipgateApiError(`Refusing to ${action} without an explicit account-wide confirmation.`, 400);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Webhook URLs routinely carry their authentication in the query string, and
|
|
42
|
+
* the log returns them as plain strings that key-based redaction cannot see.
|
|
43
|
+
*/
|
|
44
|
+
function stripUrlQueries(value) {
|
|
45
|
+
if (typeof value === "string") {
|
|
46
|
+
const separator = value.indexOf("?");
|
|
47
|
+
return value.startsWith("http") && separator !== -1
|
|
48
|
+
? `${value.slice(0, separator)}?[REDACTED]`
|
|
49
|
+
: value;
|
|
50
|
+
}
|
|
51
|
+
if (Array.isArray(value))
|
|
52
|
+
return value.map(stripUrlQueries);
|
|
53
|
+
if (value !== null && typeof value === "object") {
|
|
54
|
+
return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, stripUrlQueries(child)]));
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
24
58
|
function encodeId(id) {
|
|
25
59
|
return encodeURIComponent(id);
|
|
26
60
|
}
|
|
61
|
+
function unavailableNote(status, subject) {
|
|
62
|
+
if (status === 403) {
|
|
63
|
+
return `sipgate denied access to ${subject} (HTTP 403). Either this account does not provide it, or the Personal Access Token lacks the scope for it.`;
|
|
64
|
+
}
|
|
65
|
+
if (status === 404) {
|
|
66
|
+
return `sipgate reports no ${subject} for this account (HTTP 404).`;
|
|
67
|
+
}
|
|
68
|
+
return `This sipgate account does not provide ${subject}.`;
|
|
69
|
+
}
|
|
70
|
+
function phonelineUnavailable(items = false, status) {
|
|
71
|
+
return {
|
|
72
|
+
...(items ? { items: [] } : {}),
|
|
73
|
+
phonelinesAvailable: false,
|
|
74
|
+
...(status === undefined ? {} : { httpStatus: status }),
|
|
75
|
+
note: unavailableNote(status, "the phoneline feature"),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function phonelineMutationUnavailable(status) {
|
|
79
|
+
return {
|
|
80
|
+
before: null,
|
|
81
|
+
after: {
|
|
82
|
+
changed: false,
|
|
83
|
+
phonelinesAvailable: false,
|
|
84
|
+
...(status === undefined ? {} : { httpStatus: status }),
|
|
85
|
+
note: `${unavailableNote(status, "the phoneline feature")} No change was attempted.`,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function unavailableFeature(subject, status, items = false) {
|
|
90
|
+
return {
|
|
91
|
+
...(items ? { items: [] } : {}),
|
|
92
|
+
available: false,
|
|
93
|
+
...(status === undefined ? {} : { httpStatus: status }),
|
|
94
|
+
note: unavailableNote(status, subject),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function unavailableFeatureMutation(subject, status) {
|
|
98
|
+
return {
|
|
99
|
+
before: null,
|
|
100
|
+
after: {
|
|
101
|
+
changed: false,
|
|
102
|
+
...unavailableFeature(subject, status),
|
|
103
|
+
note: `${unavailableNote(status, subject)} No change was attempted.`,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* sipgate answers 403/404 for endpoints an account does not provide. Newer
|
|
109
|
+
* accounts have no phoneline layer at all: their numbers hang directly off a
|
|
110
|
+
* device. Treat those statuses as "feature absent" instead of a hard failure so
|
|
111
|
+
* a missing phoneline layer cannot take down number, history, and settings
|
|
112
|
+
* lookups that can be served from devices instead.
|
|
113
|
+
*/
|
|
114
|
+
const UNAVAILABLE_STATUS = new Set([403, 404]);
|
|
115
|
+
async function optional(request) {
|
|
116
|
+
try {
|
|
117
|
+
return { value: await request, available: true };
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
if (error instanceof SipgateApiError
|
|
121
|
+
&& error.status !== undefined
|
|
122
|
+
&& UNAVAILABLE_STATUS.has(error.status)) {
|
|
123
|
+
return { available: false, status: error.status };
|
|
124
|
+
}
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function idSet(items) {
|
|
129
|
+
return new Set(items.map((item) => stringField(item, "id")).filter((id) => Boolean(id)));
|
|
130
|
+
}
|
|
27
131
|
export class SipgateBackend {
|
|
28
132
|
client;
|
|
29
133
|
constructor(client) {
|
|
@@ -61,11 +165,120 @@ export class SipgateBackend {
|
|
|
61
165
|
return sanitize({ items, pagination: { offset, limit, returned: items.length } });
|
|
62
166
|
}
|
|
63
167
|
async listPhonelines(userId) {
|
|
64
|
-
const
|
|
168
|
+
const { value, available } = await this.tryListPhonelines(userId);
|
|
169
|
+
if (!available)
|
|
170
|
+
return { items: [], phonelinesAvailable: false };
|
|
171
|
+
return sanitize(value ?? { items: [] });
|
|
172
|
+
}
|
|
173
|
+
tryListPhonelines(userId) {
|
|
174
|
+
return optional(this.client.request(`/${encodeId(userId)}/phonelines`));
|
|
175
|
+
}
|
|
176
|
+
phonelinePath(userId, phonelineId, suffix = "") {
|
|
177
|
+
return `/${encodeId(userId)}/phonelines/${encodeId(phonelineId)}${suffix}`;
|
|
178
|
+
}
|
|
179
|
+
async optionalPhonelineRead(path, list = false) {
|
|
180
|
+
const { value, available, status } = await optional(this.client.request(path));
|
|
181
|
+
return available ? sanitize(value ?? (list ? { items: [] } : {})) : phonelineUnavailable(list, status);
|
|
182
|
+
}
|
|
183
|
+
getPhoneline(userId, phonelineId) {
|
|
184
|
+
return this.optionalPhonelineRead(this.phonelinePath(userId, phonelineId));
|
|
185
|
+
}
|
|
186
|
+
getPhonelineBlockAnonymous(userId, phonelineId) {
|
|
187
|
+
return this.optionalPhonelineRead(this.phonelinePath(userId, phonelineId, "/blockanonymous"));
|
|
188
|
+
}
|
|
189
|
+
listPhonelineDevices(userId, phonelineId) {
|
|
190
|
+
return this.optionalPhonelineRead(this.phonelinePath(userId, phonelineId, "/devices"), true);
|
|
191
|
+
}
|
|
192
|
+
listParallelForwardings(userId, phonelineId) {
|
|
193
|
+
return this.optionalPhonelineRead(this.phonelinePath(userId, phonelineId, "/parallelforwardings"), true);
|
|
194
|
+
}
|
|
195
|
+
listPhonelineVoicemails(userId, phonelineId) {
|
|
196
|
+
return this.optionalPhonelineRead(this.phonelinePath(userId, phonelineId, "/voicemails"), true);
|
|
197
|
+
}
|
|
198
|
+
listVoicemailGreetings(userId, phonelineId, voicemailId) {
|
|
199
|
+
return this.optionalPhonelineRead(this.phonelinePath(userId, phonelineId, `/voicemails/${encodeId(voicemailId)}/greetings`), true);
|
|
200
|
+
}
|
|
201
|
+
async listVoicemails() {
|
|
202
|
+
const response = await this.client.request("/voicemails");
|
|
65
203
|
return sanitize(response ?? { items: [] });
|
|
66
204
|
}
|
|
205
|
+
async getVoicemail(voicemailId) {
|
|
206
|
+
const response = await this.client.request(`/voicemails/${encodeId(voicemailId)}`);
|
|
207
|
+
return sanitize(response ?? {});
|
|
208
|
+
}
|
|
209
|
+
async listAutorecordingGreetings() {
|
|
210
|
+
const { value, available } = await optional(this.client.request("/autorecordings/greetings"));
|
|
211
|
+
return available
|
|
212
|
+
? sanitize(value ?? {})
|
|
213
|
+
: {
|
|
214
|
+
autorecordingsAvailable: false,
|
|
215
|
+
note: "Automated call recording is not activated for this sipgate account.",
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
async getAutorecordingSettings(extension) {
|
|
219
|
+
const { value, available } = await optional(this.client.request(`/autorecordings/${encodeId(extension)}/settings`));
|
|
220
|
+
return available
|
|
221
|
+
? sanitize(value ?? {})
|
|
222
|
+
: {
|
|
223
|
+
autorecordingsAvailable: false,
|
|
224
|
+
note: "Automated call recording is not activated for this extension.",
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
async listDeviceIds(userId) {
|
|
228
|
+
const response = await this.client.request(`/${encodeId(userId)}/devices`);
|
|
229
|
+
return idSet(asItems(response));
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Fallback for accounts without a phoneline layer: numbers are matched to the
|
|
233
|
+
* user through the device their endpoint points at.
|
|
234
|
+
*/
|
|
235
|
+
async listAllAccountNumbers() {
|
|
236
|
+
const pageSize = 1000;
|
|
237
|
+
const all = [];
|
|
238
|
+
for (let offset = 0;; offset += pageSize) {
|
|
239
|
+
const response = await this.client.request("/numbers", {
|
|
240
|
+
query: { offset, limit: pageSize },
|
|
241
|
+
});
|
|
242
|
+
const page = asItems(response);
|
|
243
|
+
all.push(...page);
|
|
244
|
+
const envelope = response && !Array.isArray(response) && typeof response === "object"
|
|
245
|
+
? response
|
|
246
|
+
: {};
|
|
247
|
+
const totalCount = typeof envelope.totalCount === "number" ? envelope.totalCount : undefined;
|
|
248
|
+
// sipgate may clamp the requested limit, so advance by what it actually
|
|
249
|
+
// returned and stop once a page comes back empty or the count is reached.
|
|
250
|
+
if (page.length === 0)
|
|
251
|
+
break;
|
|
252
|
+
if (totalCount !== undefined && all.length >= totalCount)
|
|
253
|
+
break;
|
|
254
|
+
if (page.length < pageSize)
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
return all;
|
|
258
|
+
}
|
|
259
|
+
async listDeviceNumbers(userId, { offset, limit }, phonelinesAvailable) {
|
|
260
|
+
const [deviceIds, accountNumbers] = await Promise.all([
|
|
261
|
+
this.listDeviceIds(userId),
|
|
262
|
+
this.listAllAccountNumbers(),
|
|
263
|
+
]);
|
|
264
|
+
const owned = accountNumbers.filter((number) => {
|
|
265
|
+
const endpointId = stringField(number, "endpointId");
|
|
266
|
+
return endpointId !== undefined && deviceIds.has(endpointId);
|
|
267
|
+
});
|
|
268
|
+
const page = owned.slice(offset, offset + limit);
|
|
269
|
+
return sanitize({
|
|
270
|
+
items: page,
|
|
271
|
+
pagination: { offset, limit, returned: page.length, totalCount: owned.length },
|
|
272
|
+
source: "devices",
|
|
273
|
+
phonelinesAvailable,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
67
276
|
async listUserNumbers(userId, { offset, limit }) {
|
|
68
|
-
const
|
|
277
|
+
const { value: phonelinesValue, available } = await this.tryListPhonelines(userId);
|
|
278
|
+
const phonelines = asItems(phonelinesValue);
|
|
279
|
+
if (!available || phonelines.length === 0) {
|
|
280
|
+
return this.listDeviceNumbers(userId, { offset, limit }, available);
|
|
281
|
+
}
|
|
69
282
|
const numberGroups = await Promise.all(phonelines.map(async (phoneline) => {
|
|
70
283
|
const phonelineId = stringField(phoneline, "id");
|
|
71
284
|
if (!phonelineId)
|
|
@@ -87,6 +300,10 @@ export class SipgateBackend {
|
|
|
87
300
|
pagination: { offset, limit, returned: page.length, totalCount: uniqueNumbers.length },
|
|
88
301
|
});
|
|
89
302
|
}
|
|
303
|
+
async getUserNumbers(userId) {
|
|
304
|
+
const response = await this.client.request(`/${encodeId(userId)}/numbers`);
|
|
305
|
+
return sanitize(response ?? { items: [] });
|
|
306
|
+
}
|
|
90
307
|
async listDevices(userId, types) {
|
|
91
308
|
const userIds = userId ? [userId] : asItems(await this.client.request("/users"))
|
|
92
309
|
.map((user) => stringField(user, "id"))
|
|
@@ -99,6 +316,104 @@ export class SipgateBackend {
|
|
|
99
316
|
const items = responses.flatMap(({ userId: ownerId, response }) => asItems(response).map((device) => ({ ...device, userId: ownerId })));
|
|
100
317
|
return sanitize({ items });
|
|
101
318
|
}
|
|
319
|
+
async getDevice(deviceId) {
|
|
320
|
+
const response = await this.client.request(`/devices/${encodeId(deviceId)}`);
|
|
321
|
+
return sanitize(response ?? {});
|
|
322
|
+
}
|
|
323
|
+
async getDeviceCallerId(deviceId) {
|
|
324
|
+
const response = await this.client.request(`/devices/${encodeId(deviceId)}/callerid`);
|
|
325
|
+
return sanitize(response ?? {});
|
|
326
|
+
}
|
|
327
|
+
async getDeviceLocalPrefix(deviceId) {
|
|
328
|
+
const response = await this.client.request(`/devices/${encodeId(deviceId)}/localprefix`);
|
|
329
|
+
return sanitize(response ?? {});
|
|
330
|
+
}
|
|
331
|
+
async getDeviceTariffAnnouncement(deviceId) {
|
|
332
|
+
const response = await this.client.request(`/devices/${encodeId(deviceId)}/tariffannouncement`);
|
|
333
|
+
return sanitize(response ?? {});
|
|
334
|
+
}
|
|
335
|
+
async getDeviceSingleRowDisplay(deviceId) {
|
|
336
|
+
const response = await this.client.request(`/devices/${encodeId(deviceId)}/singlerowdisplay`);
|
|
337
|
+
return sanitize(response ?? {});
|
|
338
|
+
}
|
|
339
|
+
async getDeviceContingents(userId, deviceId) {
|
|
340
|
+
const response = await this.client.request(`/${encodeId(userId)}/devices/${encodeId(deviceId)}/contingents`);
|
|
341
|
+
return sanitize(response ?? { contingents: [] });
|
|
342
|
+
}
|
|
343
|
+
async listAddresses() {
|
|
344
|
+
const response = await this.client.request("/addresses");
|
|
345
|
+
return sanitize(response ?? { items: [] });
|
|
346
|
+
}
|
|
347
|
+
async getAddress(addressId) {
|
|
348
|
+
const response = await this.client.request(`/addresses/${addressId}`);
|
|
349
|
+
return sanitize(response ?? {});
|
|
350
|
+
}
|
|
351
|
+
async listAddressNumbers(addressId) {
|
|
352
|
+
const response = await this.client.request(`/addresses/${addressId}/numbers`);
|
|
353
|
+
return sanitize(response ?? { items: [] });
|
|
354
|
+
}
|
|
355
|
+
async validateQuickDialNumber(quickDialNumber) {
|
|
356
|
+
const response = await this.client.request(`/numbers/quickdial/validation/${encodeId(quickDialNumber)}`);
|
|
357
|
+
return sanitize(response ?? {});
|
|
358
|
+
}
|
|
359
|
+
async listContacts(query) {
|
|
360
|
+
const response = await this.client.request("/contacts", {
|
|
361
|
+
query: {
|
|
362
|
+
phonenumbers: query.phoneNumbers,
|
|
363
|
+
limit: query.limit,
|
|
364
|
+
offset: query.offset,
|
|
365
|
+
lastId: query.lastId,
|
|
366
|
+
scopes: query.scopes,
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
return sanitize(response ?? { items: [], totalCount: 0 });
|
|
370
|
+
}
|
|
371
|
+
async getContact(contactId) {
|
|
372
|
+
const response = await this.client.request(`/contacts/${encodeId(contactId)}`);
|
|
373
|
+
return sanitize(response ?? {});
|
|
374
|
+
}
|
|
375
|
+
async listInternalContacts() {
|
|
376
|
+
const response = await this.client.request("/contacts/internal");
|
|
377
|
+
return sanitize(response ?? { items: [] });
|
|
378
|
+
}
|
|
379
|
+
async exportContactsCsv(scopes) {
|
|
380
|
+
const content = await this.client.requestText("/contacts/csv", {
|
|
381
|
+
query: { scope: scopes },
|
|
382
|
+
accept: "text/csv",
|
|
383
|
+
});
|
|
384
|
+
return { content, contentType: "text/csv" };
|
|
385
|
+
}
|
|
386
|
+
async getContactsVcard(query) {
|
|
387
|
+
const response = await this.client.request("/contacts/vcard", {
|
|
388
|
+
query: {
|
|
389
|
+
scope: query.scopes,
|
|
390
|
+
labels: query.labels,
|
|
391
|
+
contactIds: query.contactIds,
|
|
392
|
+
wantedFields: query.wantedFields,
|
|
393
|
+
filter: query.filter,
|
|
394
|
+
limit: query.limit,
|
|
395
|
+
offset: query.offset,
|
|
396
|
+
lastId: query.lastId,
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
|
+
return sanitize(response ?? { contacts: [], overallMatches: 0 });
|
|
400
|
+
}
|
|
401
|
+
async listIncomingBlacklist() {
|
|
402
|
+
const response = await this.client.request("/blacklist/incoming");
|
|
403
|
+
return sanitize(response ?? { items: [] });
|
|
404
|
+
}
|
|
405
|
+
async getCallRestrictions(userIds) {
|
|
406
|
+
const response = await this.client.request("/callrestrictions", {
|
|
407
|
+
query: { userIds },
|
|
408
|
+
});
|
|
409
|
+
return sanitize(response ?? {});
|
|
410
|
+
}
|
|
411
|
+
async getRestrictions(userId, restrictions) {
|
|
412
|
+
const response = await this.client.request("/restrictions", {
|
|
413
|
+
query: { userId, restriction: restrictions },
|
|
414
|
+
});
|
|
415
|
+
return sanitize(response ?? { items: [] });
|
|
416
|
+
}
|
|
102
417
|
async getRouting(userId) {
|
|
103
418
|
const [numbersResponse, usersResponse] = await Promise.all([
|
|
104
419
|
userId
|
|
@@ -110,8 +425,8 @@ export class SipgateBackend {
|
|
|
110
425
|
.map((user) => stringField(user, "id"))
|
|
111
426
|
.filter((id) => Boolean(id));
|
|
112
427
|
const users = await Promise.all(userIds.map(async (id) => {
|
|
113
|
-
const
|
|
114
|
-
const phonelines = await Promise.all(asItems(
|
|
428
|
+
const phonelinesResult = await this.tryListPhonelines(id);
|
|
429
|
+
const phonelines = await Promise.all(asItems(phonelinesResult.value).map(async (phoneline) => {
|
|
115
430
|
const phonelineId = stringField(phoneline, "id");
|
|
116
431
|
if (!phonelineId)
|
|
117
432
|
return { ...phoneline, numbers: [], forwardings: [] };
|
|
@@ -130,10 +445,13 @@ export class SipgateBackend {
|
|
|
130
445
|
forwardings: asItems(forwardings),
|
|
131
446
|
};
|
|
132
447
|
}));
|
|
133
|
-
return { userId: id, phonelines };
|
|
448
|
+
return { userId: id, phonelines, phonelinesAvailable: phonelinesResult.available };
|
|
134
449
|
}));
|
|
450
|
+
const routedNumbers = users.flatMap(({ phonelines }) => phonelines.flatMap((phoneline) => phoneline.numbers));
|
|
135
451
|
const numbers = userId
|
|
136
|
-
?
|
|
452
|
+
? (routedNumbers.length > 0
|
|
453
|
+
? routedNumbers
|
|
454
|
+
: asItems(await this.listUserNumbers(userId, { offset: 0, limit: 1000 })))
|
|
137
455
|
: asItems(numbersResponse);
|
|
138
456
|
const uniqueNumbers = [...new Map(numbers.map((number) => [
|
|
139
457
|
stringField(number, "id") ?? stringField(number, "number") ?? JSON.stringify(number),
|
|
@@ -141,6 +459,9 @@ export class SipgateBackend {
|
|
|
141
459
|
])).values()];
|
|
142
460
|
return sanitize({ numbers: uniqueNumbers, users });
|
|
143
461
|
}
|
|
462
|
+
async getHistoryEntry(entryId) {
|
|
463
|
+
return sanitize(await this.client.request(`/history/${encodeId(entryId)}`) ?? {});
|
|
464
|
+
}
|
|
144
465
|
async getCallHistory(query) {
|
|
145
466
|
const response = await this.client.request("/history", {
|
|
146
467
|
query: {
|
|
@@ -152,6 +473,8 @@ export class SipgateBackend {
|
|
|
152
473
|
from: query.from,
|
|
153
474
|
to: query.to,
|
|
154
475
|
phonenumber: query.phoneNumber,
|
|
476
|
+
archived: query.archived,
|
|
477
|
+
starred: query.starred,
|
|
155
478
|
},
|
|
156
479
|
});
|
|
157
480
|
const object = response && !Array.isArray(response) && typeof response === "object" ? response : {};
|
|
@@ -163,6 +486,43 @@ export class SipgateBackend {
|
|
|
163
486
|
pagination: { offset: query.offset, limit: query.limit, totalCount, nextOffset },
|
|
164
487
|
});
|
|
165
488
|
}
|
|
489
|
+
async exportHistory(query) {
|
|
490
|
+
const content = await this.client.requestText("/history/export", {
|
|
491
|
+
query: {
|
|
492
|
+
connectionIds: query.connectionIds,
|
|
493
|
+
types: query.types,
|
|
494
|
+
directions: query.directions,
|
|
495
|
+
offset: query.offset,
|
|
496
|
+
limit: query.limit,
|
|
497
|
+
archived: query.archived,
|
|
498
|
+
starred: query.starred,
|
|
499
|
+
from: query.from,
|
|
500
|
+
to: query.to,
|
|
501
|
+
},
|
|
502
|
+
accept: "application/octet-stream",
|
|
503
|
+
});
|
|
504
|
+
return { content, contentType: "text/csv" };
|
|
505
|
+
}
|
|
506
|
+
async listCalls() {
|
|
507
|
+
const response = await this.client.request("/calls");
|
|
508
|
+
return sanitize(response ?? { data: [] });
|
|
509
|
+
}
|
|
510
|
+
async listNotifications(userId) {
|
|
511
|
+
const response = await this.client.request(`/${encodeId(userId)}/notifications`);
|
|
512
|
+
return sanitize(response ?? { call: [], fax: [], sms: [], voicemail: [] });
|
|
513
|
+
}
|
|
514
|
+
async listFaxlines(userId) {
|
|
515
|
+
const response = await this.client.request(`/${encodeId(userId)}/faxlines`);
|
|
516
|
+
return sanitize(response ?? { items: [] });
|
|
517
|
+
}
|
|
518
|
+
async listFaxlineNumbers(userId, faxlineId) {
|
|
519
|
+
const response = await this.client.request(`/${encodeId(userId)}/faxlines/${encodeId(faxlineId)}/numbers`);
|
|
520
|
+
return sanitize(response ?? { items: [] });
|
|
521
|
+
}
|
|
522
|
+
async getFaxlineCallerId(userId, faxlineId) {
|
|
523
|
+
const response = await this.client.request(`/${encodeId(userId)}/faxlines/${encodeId(faxlineId)}/callerid`);
|
|
524
|
+
return sanitize(response ?? {});
|
|
525
|
+
}
|
|
166
526
|
async getSettings(userId) {
|
|
167
527
|
const users = userId
|
|
168
528
|
? [await this.getUser(userId)]
|
|
@@ -174,11 +534,11 @@ export class SipgateBackend {
|
|
|
174
534
|
const id = stringField(user, "id");
|
|
175
535
|
if (!id)
|
|
176
536
|
return null;
|
|
177
|
-
const [devicesResponse,
|
|
537
|
+
const [devicesResponse, phonelinesResult] = await Promise.all([
|
|
178
538
|
this.client.request(`/${encodeId(id)}/devices`),
|
|
179
|
-
this.
|
|
539
|
+
this.tryListPhonelines(id),
|
|
180
540
|
]);
|
|
181
|
-
const phonelines = await Promise.all(asItems(
|
|
541
|
+
const phonelines = await Promise.all(asItems(phonelinesResult.value).map(async (line) => {
|
|
182
542
|
const lineId = stringField(line, "id");
|
|
183
543
|
if (!lineId)
|
|
184
544
|
return line;
|
|
@@ -192,10 +552,304 @@ export class SipgateBackend {
|
|
|
192
552
|
devices: asItems(devicesResponse),
|
|
193
553
|
},
|
|
194
554
|
phonelines,
|
|
555
|
+
phonelinesAvailable: phonelinesResult.available,
|
|
195
556
|
};
|
|
196
557
|
}));
|
|
197
558
|
return sanitize({ users: settings.filter((item) => item !== null) });
|
|
198
559
|
}
|
|
560
|
+
async getBalance() {
|
|
561
|
+
const response = await this.client.request("/balance");
|
|
562
|
+
return sanitize(response ?? {});
|
|
563
|
+
}
|
|
564
|
+
async listPortings() {
|
|
565
|
+
const response = await this.client.request("/portings");
|
|
566
|
+
return sanitize(response ?? { items: [] });
|
|
567
|
+
}
|
|
568
|
+
async getPorting(portingId) {
|
|
569
|
+
const response = await this.client.request(`/portings/${portingId}`);
|
|
570
|
+
return sanitize(response ?? {});
|
|
571
|
+
}
|
|
572
|
+
async getSipgateIoSettings() {
|
|
573
|
+
const { value, available, status } = await optional(this.client.request("/settings/sipgateio"));
|
|
574
|
+
return available
|
|
575
|
+
? sanitize(value ?? {})
|
|
576
|
+
: unavailableFeature("the account-wide sipgate.io settings feature", status);
|
|
577
|
+
}
|
|
578
|
+
async listWebhookLogs() {
|
|
579
|
+
const { value, available, status } = await optional(this.client.request("/log/webhooks"));
|
|
580
|
+
if (!available) {
|
|
581
|
+
return unavailableFeature("the account-wide sipgate.io webhook log", status, true);
|
|
582
|
+
}
|
|
583
|
+
const logs = sanitize(value ?? { items: [] });
|
|
584
|
+
return stripUrlQueries(logs);
|
|
585
|
+
}
|
|
586
|
+
async createContact(input, confirmAccountWide) {
|
|
587
|
+
assertConfirmed(confirmAccountWide, "create an account-wide contact");
|
|
588
|
+
const response = await this.client.request("/contacts", {
|
|
589
|
+
method: "POST",
|
|
590
|
+
body: this.contactBody(input),
|
|
591
|
+
});
|
|
592
|
+
return {
|
|
593
|
+
before: null,
|
|
594
|
+
after: sanitize({
|
|
595
|
+
created: true,
|
|
596
|
+
response: response ?? null,
|
|
597
|
+
note: "sipgate does not return the created contact ID, so no exact post-create read-back is possible.",
|
|
598
|
+
}),
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
async updateContact(contactId, input, confirmAccountWide) {
|
|
602
|
+
assertConfirmed(confirmAccountWide, "change an account-wide contact");
|
|
603
|
+
const path = `/contacts/${encodeId(contactId)}`;
|
|
604
|
+
const before = await this.client.request(path);
|
|
605
|
+
await this.client.request(path, {
|
|
606
|
+
method: "PUT",
|
|
607
|
+
body: this.contactBody(input),
|
|
608
|
+
});
|
|
609
|
+
const after = await this.client.request(path);
|
|
610
|
+
return { before: sanitize(before ?? {}), after: sanitize(after ?? {}) };
|
|
611
|
+
}
|
|
612
|
+
async deleteContact(contactId, scopes, confirmAccountWide) {
|
|
613
|
+
assertConfirmed(confirmAccountWide, "delete an account-wide contact");
|
|
614
|
+
const before = await this.client.request(`/contacts/${encodeId(contactId)}`);
|
|
615
|
+
const response = await this.client.request(`/contacts/${encodeId(contactId)}`, { method: "DELETE", query: { scope: scopes } });
|
|
616
|
+
return {
|
|
617
|
+
before: sanitize(before ?? {}),
|
|
618
|
+
after: sanitize({
|
|
619
|
+
deleted: true,
|
|
620
|
+
response: response ?? null,
|
|
621
|
+
note: "sipgate exposes no read-back for a deleted contact.",
|
|
622
|
+
}),
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
async deleteContacts(input, confirmAccountWide) {
|
|
626
|
+
assertConfirmed(confirmAccountWide, "delete account-wide contacts");
|
|
627
|
+
const before = input.contactIds && input.contactIds.length > 0
|
|
628
|
+
? await this.readContacts(input.contactIds)
|
|
629
|
+
: input.source === undefined
|
|
630
|
+
? await this.listAllContacts(input.scope?.flatMap((scope) => scope === "PRIVATE,SHARED" ? ["PRIVATE", "SHARED"] : [scope]))
|
|
631
|
+
: null;
|
|
632
|
+
const response = await this.client.request("/contacts", {
|
|
633
|
+
method: "DELETE",
|
|
634
|
+
body: {
|
|
635
|
+
...(input.contactIds === undefined ? {} : { contactIds: input.contactIds }),
|
|
636
|
+
...(input.scope === undefined ? {} : { scope: input.scope }),
|
|
637
|
+
...(input.source === undefined ? {} : { source: input.source }),
|
|
638
|
+
},
|
|
639
|
+
});
|
|
640
|
+
return {
|
|
641
|
+
before: sanitize(before),
|
|
642
|
+
after: sanitize({
|
|
643
|
+
deleted: true,
|
|
644
|
+
response: response ?? null,
|
|
645
|
+
note: input.source !== undefined && !input.contactIds
|
|
646
|
+
? "sipgate's contact read endpoint has no source filter, so before is null for a source-selected delete; deleted contacts have no read-back."
|
|
647
|
+
: "sipgate exposes no read-back for contacts removed by the bulk-delete endpoint.",
|
|
648
|
+
}),
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
async importContactsCsv(base64Content, confirmAccountWide) {
|
|
652
|
+
assertConfirmed(confirmAccountWide, "import account-wide contacts");
|
|
653
|
+
const before = await this.listAllContacts();
|
|
654
|
+
await this.client.request("/contacts/import/csv", {
|
|
655
|
+
method: "POST",
|
|
656
|
+
body: { base64Content },
|
|
657
|
+
});
|
|
658
|
+
const after = await this.listAllContacts();
|
|
659
|
+
return { before, after };
|
|
660
|
+
}
|
|
661
|
+
async putContactsVcard(scope, data, confirmAccountWide) {
|
|
662
|
+
assertConfirmed(confirmAccountWide, "replace account-wide contacts");
|
|
663
|
+
const contactIds = data
|
|
664
|
+
.map((entry) => entry.contactId)
|
|
665
|
+
.filter((contactId) => Boolean(contactId));
|
|
666
|
+
const beforeQuery = {
|
|
667
|
+
scopes: [scope],
|
|
668
|
+
...(contactIds.length === 0 ? {} : { contactIds }),
|
|
669
|
+
};
|
|
670
|
+
const before = contactIds.length === 0 ? null : await this.getContactsVcard(beforeQuery);
|
|
671
|
+
const response = await this.client.request("/contacts/vcard", {
|
|
672
|
+
method: "PUT",
|
|
673
|
+
query: { scope },
|
|
674
|
+
body: { data: sanitize(data) },
|
|
675
|
+
});
|
|
676
|
+
const responseObject = response && !Array.isArray(response) && typeof response === "object"
|
|
677
|
+
? response
|
|
678
|
+
: {};
|
|
679
|
+
const resultItems = Array.isArray(responseObject.result)
|
|
680
|
+
? responseObject.result.filter((item) => Boolean(item) && !Array.isArray(item) && typeof item === "object")
|
|
681
|
+
: [];
|
|
682
|
+
const resultIds = resultItems
|
|
683
|
+
.map((entry) => stringField(entry, "contactId"))
|
|
684
|
+
.filter((contactId) => Boolean(contactId));
|
|
685
|
+
const readbackIds = [...new Set([...contactIds, ...resultIds])];
|
|
686
|
+
const after = await this.getContactsVcard({
|
|
687
|
+
scopes: [scope],
|
|
688
|
+
...(readbackIds.length === 0 ? {} : { contactIds: readbackIds }),
|
|
689
|
+
});
|
|
690
|
+
return {
|
|
691
|
+
before,
|
|
692
|
+
after: sanitize({
|
|
693
|
+
contacts: after,
|
|
694
|
+
response: response ?? null,
|
|
695
|
+
...(contactIds.length === data.length ? {} : {
|
|
696
|
+
note: "New vCards had no before-state; sipgate's result IDs were used for post-write read-back when available.",
|
|
697
|
+
}),
|
|
698
|
+
}),
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
async addIncomingBlacklist(phoneNumber, isBlock, confirmAccountWide) {
|
|
702
|
+
assertConfirmed(confirmAccountWide, "change the account-wide incoming blocklist");
|
|
703
|
+
const before = await this.listIncomingBlacklist();
|
|
704
|
+
await this.client.request("/blacklist/incoming", {
|
|
705
|
+
method: "POST",
|
|
706
|
+
body: { phoneNumber, ...(isBlock === undefined ? {} : { isBlock }) },
|
|
707
|
+
});
|
|
708
|
+
const after = await this.listIncomingBlacklist();
|
|
709
|
+
return { before, after };
|
|
710
|
+
}
|
|
711
|
+
async removeIncomingBlacklist(phoneNumber, confirmAccountWide) {
|
|
712
|
+
assertConfirmed(confirmAccountWide, "change the account-wide incoming blocklist");
|
|
713
|
+
const entries = asItems(await this.listIncomingBlacklist());
|
|
714
|
+
const normalized = phoneNumber.startsWith("+") ? phoneNumber : `+${phoneNumber}`;
|
|
715
|
+
const before = entries.find((entry) => stringField(entry, "phoneNumber") === normalized) ?? null;
|
|
716
|
+
const response = await this.client.request(`/blacklist/incoming/${encodeId(phoneNumber)}`, { method: "DELETE" });
|
|
717
|
+
return {
|
|
718
|
+
before: sanitize(before),
|
|
719
|
+
after: sanitize({
|
|
720
|
+
deleted: true,
|
|
721
|
+
response: response ?? null,
|
|
722
|
+
note: "sipgate exposes no read-back for a deleted incoming blacklist entry.",
|
|
723
|
+
}),
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
async setCallRestriction(restriction, enabled) {
|
|
727
|
+
const { userId } = await this.getAuthenticatedUser();
|
|
728
|
+
const before = await this.getCallRestrictions([userId]);
|
|
729
|
+
await this.client.request(`/${encodeId(userId)}/callrestrictions/${encodeId(restriction)}`, {
|
|
730
|
+
method: "POST",
|
|
731
|
+
body: { ...(enabled === undefined ? {} : { enabled }) },
|
|
732
|
+
});
|
|
733
|
+
const after = await this.getCallRestrictions([userId]);
|
|
734
|
+
return { before, after };
|
|
735
|
+
}
|
|
736
|
+
setHistoryRead(entryId, value) {
|
|
737
|
+
return this.mutateHistoryWithReadback(entryId, `/history/${encodeId(entryId)}/read`, { ...(value === undefined ? {} : { value }) });
|
|
738
|
+
}
|
|
739
|
+
setHistoryNote(entryId, note) {
|
|
740
|
+
return this.mutateHistoryWithReadback(entryId, `/history/${encodeId(entryId)}/note`, { note });
|
|
741
|
+
}
|
|
742
|
+
setHistoryArchive(entryId, value) {
|
|
743
|
+
return this.mutateHistoryWithReadback(entryId, `/history/${encodeId(entryId)}/archive`, { ...(value === undefined ? {} : { value }) });
|
|
744
|
+
}
|
|
745
|
+
updateHistoryEntry(entryId, input) {
|
|
746
|
+
return this.mutateHistoryWithReadback(entryId, `/history/${encodeId(entryId)}`, {
|
|
747
|
+
...(input.archived === undefined ? {} : { archived: input.archived }),
|
|
748
|
+
...(input.note === undefined ? {} : { note: input.note }),
|
|
749
|
+
...(input.read === undefined ? {} : { read: input.read }),
|
|
750
|
+
...(input.starred === undefined ? {} : { starred: input.starred }),
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
async deleteHistoryEntry(entryId) {
|
|
754
|
+
const before = await this.client.request(`/history/${encodeId(entryId)}`);
|
|
755
|
+
const response = await this.client.request(`/history/${encodeId(entryId)}`, {
|
|
756
|
+
method: "DELETE",
|
|
757
|
+
});
|
|
758
|
+
return {
|
|
759
|
+
before: sanitize(before ?? {}),
|
|
760
|
+
after: sanitize({
|
|
761
|
+
deleted: true,
|
|
762
|
+
response: response ?? null,
|
|
763
|
+
note: "History deletion is permanent; sipgate exposes no deleted-entry read-back.",
|
|
764
|
+
}),
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
assertBulkHistoryLimit(count) {
|
|
768
|
+
if (count < 150)
|
|
769
|
+
return;
|
|
770
|
+
throw new SipgateApiError("sipgate accepts fewer than 150 history entries per bulk update.", 400);
|
|
771
|
+
}
|
|
772
|
+
async updateHistoryEntries(inputs) {
|
|
773
|
+
this.assertBulkHistoryLimit(inputs.length);
|
|
774
|
+
const before = await this.readHistoryEntries(inputs.map((input) => input.id));
|
|
775
|
+
await this.client.request("/history", {
|
|
776
|
+
method: "PUT",
|
|
777
|
+
body: inputs.map((input) => ({
|
|
778
|
+
id: input.id,
|
|
779
|
+
...(input.archived === undefined ? {} : { archived: input.archived }),
|
|
780
|
+
...(input.read === undefined ? {} : { read: input.read }),
|
|
781
|
+
...(input.starred === undefined ? {} : { starred: input.starred }),
|
|
782
|
+
})),
|
|
783
|
+
});
|
|
784
|
+
const after = await this.readHistoryEntries(inputs.map((input) => input.id));
|
|
785
|
+
return { before: sanitize(before), after: sanitize(after) };
|
|
786
|
+
}
|
|
787
|
+
async deleteHistoryEntries(entryIds) {
|
|
788
|
+
if (entryIds !== undefined && entryIds.length === 0) {
|
|
789
|
+
// An empty list serializes to no query parameter at all, which sipgate
|
|
790
|
+
// reads as "delete the entire account history". Refuse it outright.
|
|
791
|
+
throw new SipgateApiError("Refusing to delete history with an empty entry list: omit the list deliberately to target the whole account.", 400);
|
|
792
|
+
}
|
|
793
|
+
const before = entryIds === undefined
|
|
794
|
+
? await this.listAllHistoryEntries()
|
|
795
|
+
: await this.readHistoryEntries(entryIds);
|
|
796
|
+
const response = await this.client.request("/history", {
|
|
797
|
+
method: "DELETE",
|
|
798
|
+
query: { id: entryIds },
|
|
799
|
+
});
|
|
800
|
+
return {
|
|
801
|
+
before: sanitize(before),
|
|
802
|
+
after: sanitize({
|
|
803
|
+
deleted: true,
|
|
804
|
+
entryIds: entryIds ?? null,
|
|
805
|
+
response: response ?? null,
|
|
806
|
+
note: "History deletion is permanent; sipgate exposes no deleted-entry read-back.",
|
|
807
|
+
}),
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
async cancelPorting(portingId, confirmAccountWide) {
|
|
811
|
+
assertConfirmed(confirmAccountWide, "cancel a number porting");
|
|
812
|
+
const before = await this.getPorting(portingId);
|
|
813
|
+
const response = await this.client.request(`/portings/${portingId}`, {
|
|
814
|
+
method: "DELETE",
|
|
815
|
+
});
|
|
816
|
+
return {
|
|
817
|
+
before,
|
|
818
|
+
after: sanitize({
|
|
819
|
+
cancelled: true,
|
|
820
|
+
response: response ?? null,
|
|
821
|
+
note: "Cancelling a number porting is irreversible through the sipgate v2 API.",
|
|
822
|
+
}),
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
async updateSipgateIoSettings(input, confirmAccountWide) {
|
|
826
|
+
assertConfirmed(confirmAccountWide, "change the account-wide sipgate.io settings");
|
|
827
|
+
const before = await optional(this.client.request("/settings/sipgateio"));
|
|
828
|
+
if (!before.available) {
|
|
829
|
+
return unavailableFeatureMutation("the account-wide sipgate.io settings feature", before.status);
|
|
830
|
+
}
|
|
831
|
+
await this.client.request("/settings/sipgateio", {
|
|
832
|
+
method: "PUT",
|
|
833
|
+
body: {
|
|
834
|
+
incomingUrl: input.incomingUrl,
|
|
835
|
+
outgoingUrl: input.outgoingUrl,
|
|
836
|
+
...(input.log === undefined ? {} : { log: input.log }),
|
|
837
|
+
...(input.pushApiVersion === undefined ? {} : { pushApiVersion: input.pushApiVersion }),
|
|
838
|
+
...(input.whitelist === undefined ? {} : { whitelist: input.whitelist }),
|
|
839
|
+
},
|
|
840
|
+
});
|
|
841
|
+
const after = await optional(this.client.request("/settings/sipgateio"));
|
|
842
|
+
return {
|
|
843
|
+
before: sanitize(before.value ?? {}),
|
|
844
|
+
after: after.available
|
|
845
|
+
? sanitize(after.value ?? {})
|
|
846
|
+
: {
|
|
847
|
+
...unavailableFeature("the account-wide sipgate.io settings feature", after.status),
|
|
848
|
+
changed: true,
|
|
849
|
+
note: "The change was accepted, but sipgate denied the settings read-back.",
|
|
850
|
+
},
|
|
851
|
+
};
|
|
852
|
+
}
|
|
199
853
|
async setNumberRouting(numberId, endpointId) {
|
|
200
854
|
const before = await this.findNumber(numberId);
|
|
201
855
|
await this.client.request(`/numbers/${encodeId(numberId)}`, {
|
|
@@ -224,6 +878,209 @@ export class SipgateBackend {
|
|
|
224
878
|
const after = await this.client.request(path);
|
|
225
879
|
return { before: sanitize(before ?? { items: [] }), after: sanitize(after ?? { items: [] }) };
|
|
226
880
|
}
|
|
881
|
+
async createPhoneline(userId) {
|
|
882
|
+
const { value, available } = await optional(this.client.request(`/${encodeId(userId)}/phonelines`, { method: "POST" }));
|
|
883
|
+
if (!available)
|
|
884
|
+
return phonelineMutationUnavailable();
|
|
885
|
+
return {
|
|
886
|
+
before: null,
|
|
887
|
+
after: sanitize({
|
|
888
|
+
phoneline: value ?? {},
|
|
889
|
+
created: true,
|
|
890
|
+
note: "No phoneline existed before this create operation; the returned phoneline is the initial state.",
|
|
891
|
+
}),
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
updatePhonelineAlias(userId, phonelineId, alias) {
|
|
895
|
+
const path = this.phonelinePath(userId, phonelineId);
|
|
896
|
+
return this.mutateOptionalPhonelineWithReadback(path, path, "PUT", { ...(alias === undefined ? {} : { alias }) });
|
|
897
|
+
}
|
|
898
|
+
async deletePhoneline(userId, phonelineId) {
|
|
899
|
+
const path = this.phonelinePath(userId, phonelineId);
|
|
900
|
+
const { value: before, available } = await optional(this.client.request(path));
|
|
901
|
+
if (!available)
|
|
902
|
+
return phonelineMutationUnavailable();
|
|
903
|
+
const response = await this.client.request(path, { method: "DELETE" });
|
|
904
|
+
return {
|
|
905
|
+
before: sanitize(before ?? {}),
|
|
906
|
+
after: sanitize({
|
|
907
|
+
deleted: true,
|
|
908
|
+
response: response ?? null,
|
|
909
|
+
note: "sipgate does not provide a deleted-phoneline read-back endpoint.",
|
|
910
|
+
}),
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
setPhonelineBlockAnonymous(userId, phonelineId, input) {
|
|
914
|
+
const path = this.phonelinePath(userId, phonelineId, "/blockanonymous");
|
|
915
|
+
return this.mutateOptionalPhonelineWithReadback(path, path, "PUT", {
|
|
916
|
+
...(input.enabled === undefined ? {} : { enabled: input.enabled }),
|
|
917
|
+
...(input.target === undefined ? {} : { target: input.target }),
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
attachDeviceToPhoneline(userId, phonelineId, deviceId) {
|
|
921
|
+
const path = this.phonelinePath(userId, phonelineId, "/devices");
|
|
922
|
+
return this.mutateOptionalPhonelineWithReadback(path, path, "POST", { deviceId }, true);
|
|
923
|
+
}
|
|
924
|
+
detachDeviceFromPhoneline(userId, phonelineId, deviceId) {
|
|
925
|
+
const readPath = this.phonelinePath(userId, phonelineId, "/devices");
|
|
926
|
+
return this.mutateOptionalPhonelineWithReadback(readPath, `${readPath}/${encodeId(deviceId)}`, "DELETE", undefined, true);
|
|
927
|
+
}
|
|
928
|
+
createParallelForwarding(userId, phonelineId, input) {
|
|
929
|
+
const path = this.phonelinePath(userId, phonelineId, "/parallelforwardings");
|
|
930
|
+
return this.mutateOptionalPhonelineWithReadback(path, path, "POST", this.parallelForwardingBody(input), true);
|
|
931
|
+
}
|
|
932
|
+
updateParallelForwarding(userId, phonelineId, parallelForwardingId, input) {
|
|
933
|
+
const readPath = this.phonelinePath(userId, phonelineId, "/parallelforwardings");
|
|
934
|
+
return this.mutateOptionalPhonelineWithReadback(readPath, `${readPath}/${encodeId(parallelForwardingId)}`, "PUT", this.parallelForwardingBody(input), true);
|
|
935
|
+
}
|
|
936
|
+
deleteParallelForwarding(userId, phonelineId, parallelForwardingId) {
|
|
937
|
+
const readPath = this.phonelinePath(userId, phonelineId, "/parallelforwardings");
|
|
938
|
+
return this.mutateOptionalPhonelineWithReadback(readPath, `${readPath}/${encodeId(parallelForwardingId)}`, "DELETE", undefined, true);
|
|
939
|
+
}
|
|
940
|
+
updateVoicemail(userId, phonelineId, voicemailId, input) {
|
|
941
|
+
const readPath = this.phonelinePath(userId, phonelineId, "/voicemails");
|
|
942
|
+
return this.mutateOptionalPhonelineWithReadback(readPath, `${readPath}/${encodeId(voicemailId)}`, "PUT", {
|
|
943
|
+
active: input.active,
|
|
944
|
+
transcription: input.transcription,
|
|
945
|
+
...(input.timeout === undefined ? {} : { timeout: input.timeout }),
|
|
946
|
+
}, true);
|
|
947
|
+
}
|
|
948
|
+
createVoicemailGreeting(userId, phonelineId, voicemailId, input) {
|
|
949
|
+
const path = this.phonelinePath(userId, phonelineId, `/voicemails/${encodeId(voicemailId)}/greetings`);
|
|
950
|
+
return this.mutateOptionalPhonelineWithReadback(path, path, "POST", this.greetingBody(input), true);
|
|
951
|
+
}
|
|
952
|
+
updateVoicemailGreeting(userId, phonelineId, voicemailId, greetingId, active) {
|
|
953
|
+
const readPath = this.phonelinePath(userId, phonelineId, `/voicemails/${encodeId(voicemailId)}/greetings`);
|
|
954
|
+
return this.mutateOptionalPhonelineWithReadback(readPath, `${readPath}/${encodeId(greetingId)}`, "PUT", { ...(active === undefined ? {} : { active }) }, true);
|
|
955
|
+
}
|
|
956
|
+
deleteVoicemailGreeting(userId, phonelineId, voicemailId, greetingId) {
|
|
957
|
+
const readPath = this.phonelinePath(userId, phonelineId, `/voicemails/${encodeId(voicemailId)}/greetings`);
|
|
958
|
+
return this.mutateOptionalPhonelineWithReadback(readPath, `${readPath}/${encodeId(greetingId)}`, "DELETE", undefined, true);
|
|
959
|
+
}
|
|
960
|
+
setVoicemailTranscription(userId, phonelineId, voicemailId, active) {
|
|
961
|
+
const readPath = this.phonelinePath(userId, phonelineId, "/voicemails");
|
|
962
|
+
return this.mutateOptionalPhonelineWithReadback(readPath, `${readPath}/${encodeId(voicemailId)}/transcriptions`, "PUT", { ...(active === undefined ? {} : { active }) }, true);
|
|
963
|
+
}
|
|
964
|
+
async playVoicemail(input) {
|
|
965
|
+
const response = await this.client.request("/sessions/voicemail/play", {
|
|
966
|
+
method: "POST",
|
|
967
|
+
body: {
|
|
968
|
+
...(input.dataId === undefined ? {} : { datadId: input.dataId }),
|
|
969
|
+
...(input.deviceId === undefined ? {} : { deviceId: input.deviceId }),
|
|
970
|
+
},
|
|
971
|
+
});
|
|
972
|
+
return this.sessionMutationResult(response, "voicemail playback");
|
|
973
|
+
}
|
|
974
|
+
async recordVoicemailGreeting(input) {
|
|
975
|
+
const response = await this.client.request("/sessions/voicemail/recording", {
|
|
976
|
+
method: "POST",
|
|
977
|
+
body: {
|
|
978
|
+
...(input.deviceId === undefined ? {} : { deviceId: input.deviceId }),
|
|
979
|
+
...(input.endpoint === undefined ? {} : { endpoint: input.endpoint }),
|
|
980
|
+
...(input.targetId === undefined ? {} : { targetId: input.targetId }),
|
|
981
|
+
},
|
|
982
|
+
});
|
|
983
|
+
return this.sessionMutationResult(response, "voicemail-greeting recording");
|
|
984
|
+
}
|
|
985
|
+
async createAutorecordingGreeting(input) {
|
|
986
|
+
const before = await optional(this.client.request("/autorecordings/greetings"));
|
|
987
|
+
if (!before.available)
|
|
988
|
+
return this.autorecordingUnavailableMutation();
|
|
989
|
+
await this.client.request("/autorecordings/greetings", {
|
|
990
|
+
method: "POST",
|
|
991
|
+
body: this.greetingBody(input),
|
|
992
|
+
});
|
|
993
|
+
const after = await optional(this.client.request("/autorecordings/greetings"));
|
|
994
|
+
return {
|
|
995
|
+
before: sanitize(before.value ?? {}),
|
|
996
|
+
after: after.available
|
|
997
|
+
? sanitize(after.value ?? {})
|
|
998
|
+
: {
|
|
999
|
+
autorecordingsAvailable: false,
|
|
1000
|
+
note: "The greeting was accepted, but automated call recording was unavailable during read-back.",
|
|
1001
|
+
},
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
async deleteAutorecordingGreeting(greetingId) {
|
|
1005
|
+
const before = await optional(this.client.request("/autorecordings/greetings"));
|
|
1006
|
+
if (!before.available)
|
|
1007
|
+
return this.autorecordingUnavailableMutation();
|
|
1008
|
+
const current = before.value && !Array.isArray(before.value) && typeof before.value === "object"
|
|
1009
|
+
? before.value
|
|
1010
|
+
: undefined;
|
|
1011
|
+
const currentId = current === undefined ? undefined : stringField(current, "id");
|
|
1012
|
+
if (currentId !== undefined && currentId !== greetingId) {
|
|
1013
|
+
throw new SipgateApiError("The requested automated-recording greeting is not the one currently configured.", 404);
|
|
1014
|
+
}
|
|
1015
|
+
const response = await this.client.request(`/autorecordings/greetings/${encodeId(greetingId)}`, { method: "DELETE" });
|
|
1016
|
+
return {
|
|
1017
|
+
before: sanitize(before.value ?? {}),
|
|
1018
|
+
after: sanitize({
|
|
1019
|
+
deleted: true,
|
|
1020
|
+
response: response ?? null,
|
|
1021
|
+
note: "sipgate exposes no separate read-back for a deleted automated-recording greeting.",
|
|
1022
|
+
}),
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
async setAutorecordingSettings(extension, active) {
|
|
1026
|
+
const path = `/autorecordings/${encodeId(extension)}/settings`;
|
|
1027
|
+
const before = await optional(this.client.request(path));
|
|
1028
|
+
if (!before.available)
|
|
1029
|
+
return this.autorecordingUnavailableMutation();
|
|
1030
|
+
await this.client.request(path, {
|
|
1031
|
+
method: "PUT",
|
|
1032
|
+
body: { ...(active === undefined ? {} : { active }) },
|
|
1033
|
+
});
|
|
1034
|
+
const after = await optional(this.client.request(path));
|
|
1035
|
+
return {
|
|
1036
|
+
before: sanitize(before.value ?? {}),
|
|
1037
|
+
after: after.available
|
|
1038
|
+
? sanitize(after.value ?? {})
|
|
1039
|
+
: {
|
|
1040
|
+
autorecordingsAvailable: false,
|
|
1041
|
+
note: "The setting was accepted, but automated call recording was unavailable during read-back.",
|
|
1042
|
+
},
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
async createFaxline(userId) {
|
|
1046
|
+
const response = await this.client.request(`/${encodeId(userId)}/faxlines`, {
|
|
1047
|
+
method: "POST",
|
|
1048
|
+
});
|
|
1049
|
+
return {
|
|
1050
|
+
before: null,
|
|
1051
|
+
after: sanitize({
|
|
1052
|
+
faxline: response ?? {},
|
|
1053
|
+
created: true,
|
|
1054
|
+
note: "No faxline existed before this create operation; the returned faxline is the initial state.",
|
|
1055
|
+
}),
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
updateFaxlineAlias(userId, faxlineId, alias) {
|
|
1059
|
+
return this.mutateFaxlineWithReadback(userId, faxlineId, "", {
|
|
1060
|
+
...(alias === undefined ? {} : { alias }),
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
async deleteFaxline(userId, faxlineId) {
|
|
1064
|
+
const before = await this.findFaxline(userId, faxlineId);
|
|
1065
|
+
const response = await this.client.request(`/${encodeId(userId)}/faxlines/${encodeId(faxlineId)}`, { method: "DELETE" });
|
|
1066
|
+
return {
|
|
1067
|
+
before: sanitize(before),
|
|
1068
|
+
after: sanitize({
|
|
1069
|
+
deleted: true,
|
|
1070
|
+
response: response ?? null,
|
|
1071
|
+
note: "sipgate does not provide a deleted-faxline read-back endpoint.",
|
|
1072
|
+
}),
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
setFaxlineCallerId(userId, faxlineId, value) {
|
|
1076
|
+
const path = `/${encodeId(userId)}/faxlines/${encodeId(faxlineId)}/callerid`;
|
|
1077
|
+
return this.mutateWithReadback(() => this.client.request(path), path, { ...(value === undefined ? {} : { value }) });
|
|
1078
|
+
}
|
|
1079
|
+
setFaxlineTagline(userId, faxlineId, value) {
|
|
1080
|
+
return this.mutateFaxlineWithReadback(userId, faxlineId, "/tagline", {
|
|
1081
|
+
...(value === undefined ? {} : { value }),
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
227
1084
|
async setDnd(deviceId, enabled) {
|
|
228
1085
|
const path = `/devices/${encodeId(deviceId)}`;
|
|
229
1086
|
const before = await this.client.request(path);
|
|
@@ -231,6 +1088,137 @@ export class SipgateBackend {
|
|
|
231
1088
|
const after = await this.client.request(path);
|
|
232
1089
|
return { before: sanitize(before ?? {}), after: sanitize(after ?? {}) };
|
|
233
1090
|
}
|
|
1091
|
+
async updateDevice(deviceId, settings) {
|
|
1092
|
+
const path = `/devices/${encodeId(deviceId)}`;
|
|
1093
|
+
return this.mutateWithReadback(() => this.client.request(path), path, {
|
|
1094
|
+
...(settings.dnd === undefined ? {} : { dnd: settings.dnd }),
|
|
1095
|
+
...(settings.emergencyAddressId === undefined
|
|
1096
|
+
? {}
|
|
1097
|
+
: { emergencyAddressId: settings.emergencyAddressId }),
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
async deleteDevice(deviceId) {
|
|
1101
|
+
const path = `/devices/${encodeId(deviceId)}`;
|
|
1102
|
+
const before = await this.client.request(path);
|
|
1103
|
+
const response = await this.client.request(path, { method: "DELETE" });
|
|
1104
|
+
return {
|
|
1105
|
+
before: sanitize(before ?? {}),
|
|
1106
|
+
after: sanitize({
|
|
1107
|
+
deleted: true,
|
|
1108
|
+
response: response ?? null,
|
|
1109
|
+
note: "sipgate does not provide a deleted-device read-back endpoint.",
|
|
1110
|
+
}),
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
setDeviceAlias(deviceId, value) {
|
|
1114
|
+
return this.mutateDeviceWithReadback(deviceId, `/devices/${encodeId(deviceId)}/alias`, {
|
|
1115
|
+
...(value === undefined ? {} : { value }),
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
async setDeviceCallerId(deviceId, value) {
|
|
1119
|
+
const path = `/devices/${encodeId(deviceId)}/callerid`;
|
|
1120
|
+
return this.mutateWithReadback(() => this.client.request(path), path, { ...(value === undefined ? {} : { value }) });
|
|
1121
|
+
}
|
|
1122
|
+
async setDeviceLocalPrefix(deviceId, input) {
|
|
1123
|
+
const path = `/devices/${encodeId(deviceId)}/localprefix`;
|
|
1124
|
+
return this.mutateWithReadback(() => this.client.request(path), path, {
|
|
1125
|
+
...(input.active === undefined ? {} : { active: input.active }),
|
|
1126
|
+
...(input.value === undefined ? {} : { value: input.value }),
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
async setDeviceTariffAnnouncement(deviceId, enabled) {
|
|
1130
|
+
const path = `/devices/${encodeId(deviceId)}/tariffannouncement`;
|
|
1131
|
+
return this.mutateWithReadback(() => this.client.request(path), path, { ...(enabled === undefined ? {} : { enabled }) });
|
|
1132
|
+
}
|
|
1133
|
+
async setDeviceSingleRowDisplay(deviceId, enabled) {
|
|
1134
|
+
const path = `/devices/${encodeId(deviceId)}/singlerowdisplay`;
|
|
1135
|
+
return this.mutateWithReadback(() => this.client.request(path), path, { ...(enabled === undefined ? {} : { enabled }) });
|
|
1136
|
+
}
|
|
1137
|
+
setExternalDeviceTargetNumber(deviceId, number) {
|
|
1138
|
+
return this.mutateDeviceWithReadback(deviceId, `/devices/${encodeId(deviceId)}/external/targetnumber`, { ...(number === undefined ? {} : { number }) });
|
|
1139
|
+
}
|
|
1140
|
+
setExternalDeviceIncomingCallDisplay(deviceId, incomingCallDisplay) {
|
|
1141
|
+
return this.mutateDeviceWithReadback(deviceId, `/devices/${encodeId(deviceId)}/external/incomingcalldisplay`, { incomingCallDisplay });
|
|
1142
|
+
}
|
|
1143
|
+
async changeDevicePassword(deviceId) {
|
|
1144
|
+
const devicePath = `/devices/${encodeId(deviceId)}`;
|
|
1145
|
+
const before = await this.client.request(devicePath);
|
|
1146
|
+
const response = sanitize(await this.client.request(`${devicePath}/credentials/password`, { method: "POST" }) ?? {});
|
|
1147
|
+
return {
|
|
1148
|
+
before: sanitize(before ?? {}),
|
|
1149
|
+
after: sanitize({
|
|
1150
|
+
credentials: response,
|
|
1151
|
+
passwordChanged: true,
|
|
1152
|
+
note: "sipgate returns the new credential only once; credential values are intentionally redacted.",
|
|
1153
|
+
}),
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
createRegisterDevice(userId, alias) {
|
|
1157
|
+
return this.createDevice(`/${encodeId(userId)}/devices/register`, { ...(alias === undefined ? {} : { alias }) });
|
|
1158
|
+
}
|
|
1159
|
+
createMobileDevice(userId, alias) {
|
|
1160
|
+
return this.createDevice(`/${encodeId(userId)}/devices/mobile`, { ...(alias === undefined ? {} : { alias }) });
|
|
1161
|
+
}
|
|
1162
|
+
createExternalDevice(userId, alias, number) {
|
|
1163
|
+
return this.createDevice(`/${encodeId(userId)}/devices/external`, {
|
|
1164
|
+
...(alias === undefined ? {} : { alias }),
|
|
1165
|
+
...(number === undefined ? {} : { number }),
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
async createQuickDial(input) {
|
|
1169
|
+
const response = await this.client.request("/numbers/quickdial", {
|
|
1170
|
+
method: "POST",
|
|
1171
|
+
body: {
|
|
1172
|
+
userId: input.userId,
|
|
1173
|
+
...(input.number === undefined ? {} : { number: input.number }),
|
|
1174
|
+
},
|
|
1175
|
+
});
|
|
1176
|
+
return {
|
|
1177
|
+
before: null,
|
|
1178
|
+
after: sanitize({
|
|
1179
|
+
response: response ?? null,
|
|
1180
|
+
requestAccepted: true,
|
|
1181
|
+
note: "sipgate does not document a quick-dial creation read-back response; use list_user_numbers to inspect the result.",
|
|
1182
|
+
}),
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
async updateQuickDial(quickDialId, input) {
|
|
1186
|
+
const before = await this.findNumber(quickDialId);
|
|
1187
|
+
await this.client.request(`/numbers/quickdial/${encodeId(quickDialId)}`, {
|
|
1188
|
+
method: "PUT",
|
|
1189
|
+
body: {
|
|
1190
|
+
userId: input.userId,
|
|
1191
|
+
...(input.number === undefined ? {} : { number: input.number }),
|
|
1192
|
+
},
|
|
1193
|
+
});
|
|
1194
|
+
const after = await this.findNumber(quickDialId);
|
|
1195
|
+
return { before: sanitize(before), after: sanitize(after) };
|
|
1196
|
+
}
|
|
1197
|
+
async deleteQuickDial(numberId) {
|
|
1198
|
+
const before = await this.findNumber(numberId);
|
|
1199
|
+
const response = await this.client.request(`/numbers/quickdial/${encodeId(numberId)}`, { method: "DELETE" });
|
|
1200
|
+
return {
|
|
1201
|
+
before: sanitize(before),
|
|
1202
|
+
after: sanitize({
|
|
1203
|
+
deleted: true,
|
|
1204
|
+
response: response ?? null,
|
|
1205
|
+
note: "sipgate does not provide a deleted quick-dial read-back endpoint.",
|
|
1206
|
+
}),
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
async updateAddress(addressId, input) {
|
|
1210
|
+
const path = `/addresses/${addressId}`;
|
|
1211
|
+
return this.mutateWithReadback(() => this.client.request(path), path, {
|
|
1212
|
+
city: input.city,
|
|
1213
|
+
countrycode: input.countrycode,
|
|
1214
|
+
postcode: input.postcode,
|
|
1215
|
+
...(input.address1 === undefined ? {} : { address1: input.address1 }),
|
|
1216
|
+
...(input.address2 === undefined ? {} : { address2: input.address2 }),
|
|
1217
|
+
...(input.number === undefined ? {} : { number: input.number }),
|
|
1218
|
+
...(input.state === undefined ? {} : { state: input.state }),
|
|
1219
|
+
...(input.street === undefined ? {} : { street: input.street }),
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
234
1222
|
async sendSms(input) {
|
|
235
1223
|
const extensions = asItems(await this.client.request(`/${encodeId(input.userId)}/sms`));
|
|
236
1224
|
const extension = input.smsId
|
|
@@ -304,9 +1292,148 @@ export class SipgateBackend {
|
|
|
304
1292
|
},
|
|
305
1293
|
};
|
|
306
1294
|
}
|
|
1295
|
+
createCallEmailNotification(input) {
|
|
1296
|
+
return this.createNotification(input.userId, "/call/email", {
|
|
1297
|
+
cause: input.cause,
|
|
1298
|
+
direction: input.direction,
|
|
1299
|
+
email: input.email,
|
|
1300
|
+
endpointId: input.endpointId,
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
createCallSmsNotification(input) {
|
|
1304
|
+
return this.createNotification(input.userId, "/call/sms", {
|
|
1305
|
+
cause: input.cause,
|
|
1306
|
+
direction: input.direction,
|
|
1307
|
+
endpointId: input.endpointId,
|
|
1308
|
+
number: input.number,
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
createFaxEmailNotification(input) {
|
|
1312
|
+
return this.createNotification(input.userId, "/fax/email", {
|
|
1313
|
+
direction: input.direction,
|
|
1314
|
+
email: input.email,
|
|
1315
|
+
faxlineId: input.faxlineId,
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
createFaxSmsNotification(input) {
|
|
1319
|
+
return this.createNotification(input.userId, "/fax/sms", {
|
|
1320
|
+
direction: input.direction,
|
|
1321
|
+
faxlineId: input.faxlineId,
|
|
1322
|
+
number: input.number,
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
createFaxReportNotification(input) {
|
|
1326
|
+
return this.createNotification(input.userId, "/fax/report", { email: input.email, faxlineId: input.faxlineId });
|
|
1327
|
+
}
|
|
1328
|
+
createSmsEmailNotification(input) {
|
|
1329
|
+
return this.createNotification(input.userId, "/sms/email", { email: input.email, endpointId: input.endpointId });
|
|
1330
|
+
}
|
|
1331
|
+
createVoicemailEmailNotification(input) {
|
|
1332
|
+
return this.createNotification(input.userId, "/voicemail/email", { email: input.email, voicemailId: input.voicemailId });
|
|
1333
|
+
}
|
|
1334
|
+
createVoicemailSmsNotification(input) {
|
|
1335
|
+
return this.createNotification(input.userId, "/voicemail/sms", { number: input.number, voicemailId: input.voicemailId });
|
|
1336
|
+
}
|
|
1337
|
+
async deleteNotification(userId, notificationId) {
|
|
1338
|
+
const before = await this.listNotifications(userId);
|
|
1339
|
+
await this.client.request(`/${encodeId(userId)}/notifications/${encodeId(notificationId)}`, { method: "DELETE" });
|
|
1340
|
+
const after = await this.listNotifications(userId);
|
|
1341
|
+
return { before, after };
|
|
1342
|
+
}
|
|
1343
|
+
async hangupCall(callId) {
|
|
1344
|
+
return this.mutateCallWithReadback(callId, `/calls/${encodeId(callId)}`, "DELETE");
|
|
1345
|
+
}
|
|
1346
|
+
setCallHold(callId, value) {
|
|
1347
|
+
return this.mutateCallWithReadback(callId, `/calls/${encodeId(callId)}/hold`, "PUT", { value });
|
|
1348
|
+
}
|
|
1349
|
+
setCallMuted(callId, value) {
|
|
1350
|
+
return this.mutateCallWithReadback(callId, `/calls/${encodeId(callId)}/muted`, "PUT", { value });
|
|
1351
|
+
}
|
|
1352
|
+
setCallRecording(callId, value, announcement) {
|
|
1353
|
+
return this.mutateCallWithReadback(callId, `/calls/${encodeId(callId)}/recording`, "PUT", { value, ...(announcement === undefined ? {} : { announcement }) });
|
|
1354
|
+
}
|
|
1355
|
+
transferCall(callId, input) {
|
|
1356
|
+
return this.mutateCallWithReadback(callId, `/calls/${encodeId(callId)}/transfer`, "POST", {
|
|
1357
|
+
attended: input.attended,
|
|
1358
|
+
phoneNumber: input.phoneNumber,
|
|
1359
|
+
...(input.callerId === undefined ? {} : { callerId: input.callerId }),
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
sendCallDtmf(callId, sequence) {
|
|
1363
|
+
return this.mutateCallWithReadback(callId, `/calls/${encodeId(callId)}/dtmf`, "POST", { sequence });
|
|
1364
|
+
}
|
|
1365
|
+
startCallAnnouncement(callId, url) {
|
|
1366
|
+
return this.mutateCallWithReadback(callId, `/calls/${encodeId(callId)}/announcements`, "POST", { url });
|
|
1367
|
+
}
|
|
1368
|
+
async sendFax(input) {
|
|
1369
|
+
const response = await this.client.request("/sessions/fax", {
|
|
1370
|
+
method: "POST",
|
|
1371
|
+
body: {
|
|
1372
|
+
base64Content: input.base64Content,
|
|
1373
|
+
faxlineId: input.faxlineId,
|
|
1374
|
+
filename: input.filename,
|
|
1375
|
+
recipient: input.recipient,
|
|
1376
|
+
},
|
|
1377
|
+
});
|
|
1378
|
+
return {
|
|
1379
|
+
before: null,
|
|
1380
|
+
after: sanitize({
|
|
1381
|
+
session: response ?? null,
|
|
1382
|
+
requestAccepted: true,
|
|
1383
|
+
note: "sipgate does not expose a synchronous fax-session read-back; sending a fax may incur charges.",
|
|
1384
|
+
}),
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
async resendFax(input) {
|
|
1388
|
+
const response = await this.client.request("/sessions/fax/resend", {
|
|
1389
|
+
method: "POST",
|
|
1390
|
+
body: {
|
|
1391
|
+
faxId: input.faxId,
|
|
1392
|
+
...(input.faxlineId === undefined ? {} : { faxlineId: input.faxlineId }),
|
|
1393
|
+
},
|
|
1394
|
+
});
|
|
1395
|
+
return {
|
|
1396
|
+
before: null,
|
|
1397
|
+
after: sanitize({
|
|
1398
|
+
response: response ?? null,
|
|
1399
|
+
requestAccepted: true,
|
|
1400
|
+
note: "sipgate does not document a fax-resend read-back response; resending a fax may incur charges.",
|
|
1401
|
+
}),
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1404
|
+
async createNotification(userId, suffix, body) {
|
|
1405
|
+
const before = await this.listNotifications(userId);
|
|
1406
|
+
await this.client.request(`/${encodeId(userId)}/notifications${suffix}`, { method: "POST", body });
|
|
1407
|
+
const after = await this.listNotifications(userId);
|
|
1408
|
+
return { before, after };
|
|
1409
|
+
}
|
|
1410
|
+
async findActiveCall(callId) {
|
|
1411
|
+
const response = await this.client.request("/calls");
|
|
1412
|
+
return asCalls(response).find((call) => stringField(call, "callId") === callId);
|
|
1413
|
+
}
|
|
1414
|
+
async mutateCallWithReadback(callId, path, method, body) {
|
|
1415
|
+
const before = await this.findActiveCall(callId);
|
|
1416
|
+
if (!before)
|
|
1417
|
+
throw new SipgateApiError("The requested active sipgate call was not found.", 404);
|
|
1418
|
+
await this.client.request(path, {
|
|
1419
|
+
method,
|
|
1420
|
+
...(body === undefined ? {} : { body }),
|
|
1421
|
+
});
|
|
1422
|
+
const after = await this.findActiveCall(callId);
|
|
1423
|
+
return {
|
|
1424
|
+
before: sanitize(before),
|
|
1425
|
+
after: after
|
|
1426
|
+
? sanitize(after)
|
|
1427
|
+
: {
|
|
1428
|
+
call: null,
|
|
1429
|
+
active: false,
|
|
1430
|
+
note: "The call no longer appears in sipgate's established-calls list after the operation.",
|
|
1431
|
+
},
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
307
1434
|
async findNumber(numberId) {
|
|
308
|
-
const
|
|
309
|
-
|
|
1435
|
+
const number = (await this.listAllAccountNumbers())
|
|
1436
|
+
.find((item) => stringField(item, "id") === numberId);
|
|
310
1437
|
if (!number)
|
|
311
1438
|
throw new SipgateApiError("The requested sipgate phone number was not found.", 404);
|
|
312
1439
|
return number;
|
|
@@ -331,5 +1458,165 @@ export class SipgateBackend {
|
|
|
331
1458
|
});
|
|
332
1459
|
return asItems(response)[0] ?? null;
|
|
333
1460
|
}
|
|
1461
|
+
async mutateWithReadback(read, path, body) {
|
|
1462
|
+
const before = await read();
|
|
1463
|
+
await this.client.request(path, { method: "PUT", body });
|
|
1464
|
+
const after = await read();
|
|
1465
|
+
return { before: sanitize(before ?? {}), after: sanitize(after ?? {}) };
|
|
1466
|
+
}
|
|
1467
|
+
mutateDeviceWithReadback(deviceId, path, body) {
|
|
1468
|
+
return this.mutateWithReadback(() => this.client.request(`/devices/${encodeId(deviceId)}`), path, body);
|
|
1469
|
+
}
|
|
1470
|
+
async mutateOptionalPhonelineWithReadback(readPath, writePath, method, body, list = false) {
|
|
1471
|
+
const before = await optional(this.client.request(readPath));
|
|
1472
|
+
if (!before.available)
|
|
1473
|
+
return phonelineMutationUnavailable(before.status);
|
|
1474
|
+
await this.client.request(writePath, {
|
|
1475
|
+
method,
|
|
1476
|
+
...(body === undefined ? {} : { body }),
|
|
1477
|
+
});
|
|
1478
|
+
const after = await optional(this.client.request(readPath));
|
|
1479
|
+
return {
|
|
1480
|
+
before: sanitize(before.value ?? (list ? { items: [] } : {})),
|
|
1481
|
+
after: after.available
|
|
1482
|
+
? sanitize(after.value ?? (list ? { items: [] } : {}))
|
|
1483
|
+
: {
|
|
1484
|
+
...phonelineUnavailable(list, after.status),
|
|
1485
|
+
changed: true,
|
|
1486
|
+
note: "The change was applied, but sipgate denied the read-back so the new state could not be confirmed.",
|
|
1487
|
+
},
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
parallelForwardingBody(input) {
|
|
1491
|
+
return {
|
|
1492
|
+
...(input.active === undefined ? {} : { active: input.active }),
|
|
1493
|
+
...(input.alias === undefined ? {} : { alias: input.alias }),
|
|
1494
|
+
...(input.destination === undefined ? {} : { destination: input.destination }),
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
greetingBody(input) {
|
|
1498
|
+
return {
|
|
1499
|
+
...(input.base64Content === undefined ? {} : { base64Content: input.base64Content }),
|
|
1500
|
+
...(input.filename === undefined ? {} : { filename: input.filename }),
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
sessionMutationResult(response, action) {
|
|
1504
|
+
return {
|
|
1505
|
+
before: null,
|
|
1506
|
+
after: sanitize({
|
|
1507
|
+
session: response ?? null,
|
|
1508
|
+
requestAccepted: true,
|
|
1509
|
+
note: `sipgate exposes no synchronous ${action} read-back. The call may incur charges; the caller is responsible for consent where recording law requires it.`,
|
|
1510
|
+
}),
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
autorecordingUnavailableMutation() {
|
|
1514
|
+
return {
|
|
1515
|
+
before: null,
|
|
1516
|
+
after: {
|
|
1517
|
+
changed: false,
|
|
1518
|
+
autorecordingsAvailable: false,
|
|
1519
|
+
note: "Automated call recording is not activated for this sipgate account; no change was attempted.",
|
|
1520
|
+
},
|
|
1521
|
+
};
|
|
1522
|
+
}
|
|
1523
|
+
async findFaxline(userId, faxlineId) {
|
|
1524
|
+
const response = await this.listFaxlines(userId);
|
|
1525
|
+
const faxline = asItems(response).find((item) => stringField(item, "id") === faxlineId);
|
|
1526
|
+
if (!faxline)
|
|
1527
|
+
throw new SipgateApiError("The requested sipgate faxline was not found.", 404);
|
|
1528
|
+
return faxline;
|
|
1529
|
+
}
|
|
1530
|
+
async mutateFaxlineWithReadback(userId, faxlineId, suffix, body) {
|
|
1531
|
+
const before = await this.findFaxline(userId, faxlineId);
|
|
1532
|
+
await this.client.request(`/${encodeId(userId)}/faxlines/${encodeId(faxlineId)}${suffix}`, { method: "PUT", body });
|
|
1533
|
+
const after = await this.findFaxline(userId, faxlineId);
|
|
1534
|
+
return { before: sanitize(before), after: sanitize(after) };
|
|
1535
|
+
}
|
|
1536
|
+
contactBody(input) {
|
|
1537
|
+
return {
|
|
1538
|
+
...(input.addresses === undefined ? {} : { addresses: sanitize(input.addresses) }),
|
|
1539
|
+
...(input.emails === undefined ? {} : { emails: sanitize(input.emails) }),
|
|
1540
|
+
...(input.family === undefined ? {} : { family: input.family }),
|
|
1541
|
+
...(input.given === undefined ? {} : { given: input.given }),
|
|
1542
|
+
...("id" in input && typeof input.id === "string" ? { id: input.id } : {}),
|
|
1543
|
+
...(input.name === undefined ? {} : { name: input.name }),
|
|
1544
|
+
...(input.note === undefined ? {} : { note: input.note }),
|
|
1545
|
+
...(input.numbers === undefined ? {} : { numbers: sanitize(input.numbers) }),
|
|
1546
|
+
...(input.organization === undefined ? {} : { organization: input.organization }),
|
|
1547
|
+
...(input.picture === undefined ? {} : { picture: input.picture }),
|
|
1548
|
+
...(input.scope === undefined ? {} : { scope: input.scope }),
|
|
1549
|
+
...(input.websites === undefined ? {} : { websites: sanitize(input.websites) }),
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
async readContacts(contactIds) {
|
|
1553
|
+
const contacts = [];
|
|
1554
|
+
for (const contactId of contactIds)
|
|
1555
|
+
contacts.push(await this.getContact(contactId));
|
|
1556
|
+
return contacts;
|
|
1557
|
+
}
|
|
1558
|
+
async listAllContacts(scopes) {
|
|
1559
|
+
const all = [];
|
|
1560
|
+
const limit = 5000;
|
|
1561
|
+
let lastId;
|
|
1562
|
+
for (;;) {
|
|
1563
|
+
const response = await this.listContacts({
|
|
1564
|
+
limit,
|
|
1565
|
+
...(lastId === undefined ? {} : { lastId }),
|
|
1566
|
+
...(scopes === undefined ? {} : { scopes }),
|
|
1567
|
+
});
|
|
1568
|
+
const page = asItems(response);
|
|
1569
|
+
all.push(...page);
|
|
1570
|
+
if (page.length < limit)
|
|
1571
|
+
break;
|
|
1572
|
+
const nextLastId = stringField(page[page.length - 1] ?? {}, "id");
|
|
1573
|
+
if (!nextLastId || nextLastId === lastId)
|
|
1574
|
+
break;
|
|
1575
|
+
lastId = nextLastId;
|
|
1576
|
+
}
|
|
1577
|
+
return sanitize({ items: all, totalCount: all.length });
|
|
1578
|
+
}
|
|
1579
|
+
async readHistoryEntries(entryIds) {
|
|
1580
|
+
const entries = [];
|
|
1581
|
+
for (const entryId of entryIds)
|
|
1582
|
+
entries.push(await this.getHistoryEntry(entryId));
|
|
1583
|
+
return entries;
|
|
1584
|
+
}
|
|
1585
|
+
async listAllHistoryEntries() {
|
|
1586
|
+
const all = [];
|
|
1587
|
+
const pageSize = 1000;
|
|
1588
|
+
for (const archived of [false, true]) {
|
|
1589
|
+
for (let offset = 0;; offset += pageSize) {
|
|
1590
|
+
const response = await this.getCallHistory({
|
|
1591
|
+
archived,
|
|
1592
|
+
offset,
|
|
1593
|
+
limit: pageSize,
|
|
1594
|
+
types: ["CALL", "VOICEMAIL", "SMS", "FAX"],
|
|
1595
|
+
});
|
|
1596
|
+
const page = asItems(response);
|
|
1597
|
+
all.push(...page);
|
|
1598
|
+
if (page.length < pageSize)
|
|
1599
|
+
break;
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
return all;
|
|
1603
|
+
}
|
|
1604
|
+
async mutateHistoryWithReadback(entryId, path, body) {
|
|
1605
|
+
const before = await this.client.request(`/history/${encodeId(entryId)}`);
|
|
1606
|
+
await this.client.request(path, { method: "PUT", body });
|
|
1607
|
+
const after = await this.client.request(`/history/${encodeId(entryId)}`);
|
|
1608
|
+
return { before: sanitize(before ?? {}), after: sanitize(after ?? {}) };
|
|
1609
|
+
}
|
|
1610
|
+
async createDevice(path, body) {
|
|
1611
|
+
const response = await this.client.request(path, { method: "POST", body });
|
|
1612
|
+
return {
|
|
1613
|
+
before: null,
|
|
1614
|
+
after: sanitize({
|
|
1615
|
+
device: response ?? {},
|
|
1616
|
+
created: true,
|
|
1617
|
+
note: "No device existed before this create operation; the returned device is the initial state.",
|
|
1618
|
+
}),
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
334
1621
|
}
|
|
335
1622
|
//# sourceMappingURL=sipgate-backend.js.map
|