wldx-cli 1.0.0 → 1.2.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.
Files changed (42) hide show
  1. package/README.md +19 -22
  2. package/package.json +9 -6
  3. package/src/chat/format.mjs +40 -8
  4. package/src/chat/lib.mjs +71 -23
  5. package/src/chat/watch.mjs +46 -15
  6. package/src/cli.mjs +13 -252
  7. package/src/commands/calls.mjs +135 -32
  8. package/src/commands/channel.mjs +99 -21
  9. package/src/commands/chat.mjs +77 -22
  10. package/src/commands/conversations.mjs +146 -29
  11. package/src/commands/file.mjs +13 -5
  12. package/src/commands/inbox.mjs +20 -13
  13. package/src/commands/index.mjs +5 -29
  14. package/src/commands/live.mjs +82 -24
  15. package/src/commands/meetings.mjs +114 -27
  16. package/src/commands/phone.mjs +145 -33
  17. package/src/commands/reports.mjs +219 -44
  18. package/src/commands/section.mjs +49 -8
  19. package/src/commands/user.mjs +42 -13
  20. package/src/history/text.mjs +30 -4
  21. package/src/history/wda.mjs +20 -10
  22. package/src/live/socket.mjs +25 -7
  23. package/src/live/stats.mjs +41 -9
  24. package/src/live/text.mjs +122 -23
  25. package/src/live/views.mjs +93 -23
  26. package/src/phone/control.mjs +93 -33
  27. package/src/phone/events.mjs +38 -7
  28. package/src/phone/pbx.mjs +29 -11
  29. package/src/phone/sip.mjs +87 -17
  30. package/src/phone/xml.mjs +2 -1
  31. package/src/stream.mjs +10 -5
  32. package/src/version.mjs +0 -1
  33. package/src/auth.mjs +0 -274
  34. package/src/commands/login.mjs +0 -126
  35. package/src/commands/profile.mjs +0 -51
  36. package/src/commands/status.mjs +0 -95
  37. package/src/completion.mjs +0 -132
  38. package/src/errors.mjs +0 -30
  39. package/src/files.mjs +0 -98
  40. package/src/format.mjs +0 -82
  41. package/src/http.mjs +0 -64
  42. package/src/services.mjs +0 -58
@@ -1,11 +1,22 @@
1
1
  // wx meetings: conferences (x-bees meetings) after the fact: the record, a readable card, the transcript, the AI
2
2
  // summary and insights, who spoke when, the recording link. Read only, except schedule-summary (asks the service
3
3
  // to generate the summary again). Data of WDA History and WDA Insights.
4
- import {UsageError} from '../errors.mjs';
5
- import {localTime, oneLine, table} from '../format.mjs';
6
- import {apiPath, query} from '../http.mjs';
4
+ import {UsageError} from 'wldx-core/errors';
5
+ import {localTime, oneLine, table} from 'wldx-core/format';
6
+ import {apiPath, query} from 'wldx-core/http';
7
7
  import {insightLines, millis, sectionLines, summaryLines} from '../history/text.mjs';
8
- import {NoDataError, alt, compactInsights, conferenceId, isoSeconds, optional, participantNames, plural, wdaClient, writePrivate} from '../history/wda.mjs';
8
+ import {
9
+ NoDataError,
10
+ alt,
11
+ compactInsights,
12
+ conferenceId,
13
+ isoSeconds,
14
+ optional,
15
+ participantNames,
16
+ plural,
17
+ wdaClient,
18
+ writePrivate,
19
+ } from '../history/wda.mjs';
9
20
  import {annotationLines, insightsText, stateText, structuredLines} from './calls.mjs';
10
21
 
11
22
  const CONF = '<conference>';
@@ -45,7 +56,11 @@ export function briefConference(id, rec, sum, ins) {
45
56
  hasRecording: (c.recordings ?? []).length > 0,
46
57
  summary:
47
58
  sum?.status === 'SUCCEEDED'
48
- ? {title: s.title ?? null, brief: s.brief ?? null, ...Object.fromEntries(SUMMARY_KEYS.map((k) => [k, s.json?.[k] ?? []]))}
59
+ ? {
60
+ title: s.title ?? null,
61
+ brief: s.brief ?? null,
62
+ ...Object.fromEntries(SUMMARY_KEYS.map((k) => [k, s.json?.[k] ?? []])),
63
+ }
49
64
  : {status: sum?.status ?? 'UNKNOWN'},
50
65
  insights: compactInsights(ins),
51
66
  };
@@ -53,7 +68,11 @@ export function briefConference(id, rec, sum, ins) {
53
68
 
54
69
  async function briefOne(wda, id, info) {
55
70
  const base = apiPath`/${id}`;
56
- const [rec, ins, sum] = await Promise.all([wda.get(`/history/conferences${base}`), optional(wda.get(`/insights/conferences${base}/insights`), info), optional(wda.get(`/insights/conferences${base}/summary`), info)]);
71
+ const [rec, ins, sum] = await Promise.all([
72
+ wda.get(`/history/conferences${base}`),
73
+ optional(wda.get(`/insights/conferences${base}/insights`), info),
74
+ optional(wda.get(`/insights/conferences${base}/summary`), info),
75
+ ]);
57
76
  return briefConference(id, rec, sum, ins);
58
77
  }
59
78
 
@@ -76,9 +95,22 @@ export function transcriptHeader(id, c, lines) {
76
95
  const NO_TRANSCRIPT = 'This conference has no transcription, or there is no such conference.';
77
96
 
78
97
  function briefLines(b) {
79
- const head = [localTime(b.start), `${b.durationMin} min`, b.status, b.direction, b.transcriptionStatus && `transcript ${b.transcriptionStatus}${b.transcriptionLanguage ? ` (${b.transcriptionLanguage})` : ''}`, b.hasRecording && 'recording'].filter(Boolean).join(' · ');
98
+ const head = [
99
+ localTime(b.start),
100
+ `${b.durationMin} min`,
101
+ b.status,
102
+ b.direction,
103
+ b.transcriptionStatus &&
104
+ `transcript ${b.transcriptionStatus}${b.transcriptionLanguage ? ` (${b.transcriptionLanguage})` : ''}`,
105
+ b.hasRecording && 'recording',
106
+ ]
107
+ .filter(Boolean)
108
+ .join(' · ');
80
109
  const lines = [oneLine(b.subject || 'Conference', 200), ` ${head}`, ` ${b.id}`];
81
- if (b.participants.length) lines.push(` Participants: ${b.participants.map((p) => `${p.name}${p.speakMin ? ` (${p.speakMin} min)` : ''}`).join(', ')}`);
110
+ if (b.participants.length)
111
+ lines.push(
112
+ ` Participants: ${b.participants.map((p) => `${p.name}${p.speakMin ? ` (${p.speakMin} min)` : ''}`).join(', ')}`,
113
+ );
82
114
  if (b.summary.status) lines.push(` Summary: ${b.summary.status}`);
83
115
  else {
84
116
  if (b.summary.title) lines.push(` Summary: ${oneLine(b.summary.title, 200)}`);
@@ -91,7 +123,8 @@ function briefLines(b) {
91
123
  }
92
124
 
93
125
  const conf = (ctx) => conferenceId(ctx.positionals[0]);
94
- const get = (ctx, suffix) => ctx.client(wdaClient).get(`/${suffix[0]}/conferences${apiPath`/${conf(ctx)}`}${suffix[1] ?? ''}`);
126
+ const get = (ctx, suffix) =>
127
+ ctx.client(wdaClient).get(`/${suffix[0]}/conferences${apiPath`/${conf(ctx)}`}${suffix[1] ?? ''}`);
95
128
 
96
129
  const group = {
97
130
  summary: 'meetings (conferences) after the fact: record, transcript, summary, insights, recording',
@@ -146,8 +179,15 @@ const group = {
146
179
  usage: `wx meetings transcript ${CONF} [--to FILE] [--structured]`,
147
180
  details: `${CONF_HINT} Text: a header (subject, id, link, start in UTC, duration, language, participants, lines) and the dialogue "[MM:SS] Name: text". --output json: the header fields under "conference", and the text and its chunks as the service gives them. --structured: the transcription record with the interaction data. --to writes the header and the dialogue to a new private file (an existing file only when it is the transcript of the same meeting) and prints its path.`,
148
181
  options: {
149
- to: {type: 'string', placeholder: 'FILE', description: 'write the transcript to this file (private) and print its path'},
150
- structured: {type: 'boolean', description: 'the transcription record: chunks with timing, sentiment, the interaction'},
182
+ to: {
183
+ type: 'string',
184
+ placeholder: 'FILE',
185
+ description: 'write the transcript to this file (private) and print its path',
186
+ },
187
+ structured: {
188
+ type: 'boolean',
189
+ description: 'the transcription record: chunks with timing, sentiment, the interaction',
190
+ },
151
191
  },
152
192
  args: [1, 1],
153
193
  async run(ctx) {
@@ -156,26 +196,47 @@ const group = {
156
196
  const base = apiPath`/${id}`;
157
197
  if (ctx.values.structured) {
158
198
  if (ctx.values.to) throw new UsageError('--to writes the dialogue: leave out --structured');
159
- const [r, rec] = await Promise.all([wda.soft(`/history/conferences${base}/transcription`), ctx.output === 'text' ? optional(wda.get(`/history/conferences${base}`), ctx.info) : null]);
199
+ const [r, rec] = await Promise.all([
200
+ wda.soft(`/history/conferences${base}/transcription`),
201
+ ctx.output === 'text' ? optional(wda.get(`/history/conferences${base}`), ctx.info) : null,
202
+ ]);
160
203
  if (!r) throw new NoDataError(NO_TRANSCRIPT);
161
- if (rec) Object.defineProperty(r, 'names', {value: Object.fromEntries((rec.conference?.participants ?? []).map((p) => [p.jid, alt(p.info?.name, p.info?.email, p.jid)])), enumerable: false});
204
+ if (rec)
205
+ Object.defineProperty(r, 'names', {
206
+ value: Object.fromEntries(
207
+ (rec.conference?.participants ?? []).map((p) => [p.jid, alt(p.info?.name, p.info?.email, p.jid)]),
208
+ ),
209
+ enumerable: false,
210
+ });
162
211
  return r;
163
212
  }
164
- const [text, rec] = await Promise.allSettled([wda.soft(`/history/conferences${base}/transcription/text`), wda.get(`/history/conferences${base}`)]);
213
+ const [text, rec] = await Promise.allSettled([
214
+ wda.soft(`/history/conferences${base}/transcription/text`),
215
+ wda.get(`/history/conferences${base}`),
216
+ ]);
165
217
  if (text.status === 'rejected') throw text.reason;
166
218
  if (!text.value) throw new NoDataError(NO_TRANSCRIPT);
167
219
  if (rec.status === 'rejected') throw rec.reason;
168
220
  const c = rec.value.conference ?? {};
169
221
  const body = text.value;
170
222
  const dialogue = (body.text ?? '').trim();
171
- if (c.transcriptionStatus === 'UNAVAILABLE') throw new NoDataError("Transcription is not available for this conference (transcriptionStatus 'UNAVAILABLE'): it was most likely never enabled on the call.");
223
+ if (c.transcriptionStatus === 'UNAVAILABLE')
224
+ throw new NoDataError(
225
+ "Transcription is not available for this conference (transcriptionStatus 'UNAVAILABLE'): it was most likely never enabled on the call.",
226
+ );
172
227
  if (!dialogue) {
173
- if (c.transcriptionStatus === 'POST_TRANSCRIPTION') throw new NoDataError("The transcription is still being processed (transcriptionStatus 'POST_TRANSCRIPTION'): try again later.");
228
+ if (c.transcriptionStatus === 'POST_TRANSCRIPTION')
229
+ throw new NoDataError(
230
+ "The transcription is still being processed (transcriptionStatus 'POST_TRANSCRIPTION'): try again later.",
231
+ );
174
232
  throw new NoDataError('The transcription is empty: nothing was transcribed on this call.');
175
233
  }
176
234
  const lines = (body.chunks ?? []).length;
177
235
  const header = transcriptHeader(id, c, lines);
178
- if (ctx.values.to) return {file: writePrivate(ctx.values.to, `${header}${dialogue}\n`, {cwd: ctx.cwd, conferenceId: id, env: ctx.env})};
236
+ if (ctx.values.to)
237
+ return {
238
+ file: writePrivate(ctx.values.to, `${header}${dialogue}\n`, {cwd: ctx.cwd, conferenceId: id, env: ctx.env}),
239
+ };
179
240
  const out = {
180
241
  conference: {
181
242
  id,
@@ -195,7 +256,9 @@ const group = {
195
256
  text(r, ctx) {
196
257
  if (r.file) return r.file;
197
258
  if (ctx.values.structured) {
198
- return (r.transcription?.transcriptions ?? []).flatMap((t) => structuredLines(t.chunks ?? [], t.start, r.names ?? {}));
259
+ return (r.transcription?.transcriptions ?? []).flatMap((t) =>
260
+ structuredLines(t.chunks ?? [], t.start, r.names ?? {}),
261
+ );
199
262
  }
200
263
  return `${r.header}${(r.text ?? '').trim()}`;
201
264
  },
@@ -243,10 +306,15 @@ const group = {
243
306
  async run(ctx) {
244
307
  const wda = ctx.client(wdaClient);
245
308
  const base = apiPath`/${conf(ctx)}`;
246
- const [r, rec] = await Promise.all([wda.get(`/insights/conferences${base}/timeframes`), ctx.output === 'text' ? optional(wda.get(`/history/conferences${base}`), ctx.info) : null]);
309
+ const [r, rec] = await Promise.all([
310
+ wda.get(`/insights/conferences${base}/timeframes`),
311
+ ctx.output === 'text' ? optional(wda.get(`/history/conferences${base}`), ctx.info) : null,
312
+ ]);
247
313
  if (rec) {
248
314
  const c = rec.conference ?? {};
249
- const names = Object.fromEntries((c.participants ?? []).map((p) => [p.jid, alt(p.info?.name, p.info?.email, p.jid)]));
315
+ const names = Object.fromEntries(
316
+ (c.participants ?? []).map((p) => [p.jid, alt(p.info?.name, p.info?.email, p.jid)]),
317
+ );
250
318
  Object.defineProperty(r, 'names', {value: names, enumerable: false});
251
319
  Object.defineProperty(r, 'startTime', {value: c.startTime, enumerable: false});
252
320
  }
@@ -261,8 +329,16 @@ const group = {
261
329
  list.map((t) => {
262
330
  const frames = t.frames ?? [];
263
331
  const total = frames.reduce((n, f) => n + Math.max(0, (f.end ?? f.start) - f.start), 0);
264
- const spans = frames.slice(0, 12).map((f) => `${at(f.start)}-${at(f.end ?? f.start)}`).join(' ');
265
- return [oneLine(r.names?.[t.jid] ?? t.jid, 40), millis(total), plural(frames.length, 'time'), spans + (frames.length > 12 ? ' …' : '')];
332
+ const spans = frames
333
+ .slice(0, 12)
334
+ .map((f) => `${at(f.start)}-${at(f.end ?? f.start)}`)
335
+ .join(' ');
336
+ return [
337
+ oneLine(r.names?.[t.jid] ?? t.jid, 40),
338
+ millis(total),
339
+ plural(frames.length, 'time'),
340
+ spans + (frames.length > 12 ? ' …' : ''),
341
+ ];
266
342
  }),
267
343
  );
268
344
  },
@@ -270,18 +346,28 @@ const group = {
270
346
  'recording-url': {
271
347
  summary: 'a link to download or play a recording of a meeting',
272
348
  usage: 'wx meetings recording-url --file-id ID --category channels|recordings|wizyconfRecordings [--download]',
273
- details: 'The file id is the identifier of the recording file as the record gives it. The link is presigned: whoever has it can open the file until it expires.',
349
+ details:
350
+ 'The file id is the identifier of the recording file as the record gives it. The link is presigned: whoever has it can open the file until it expires.',
274
351
  options: {
275
352
  'file-id': {type: 'string', placeholder: 'ID', description: 'the identifier of the recording file'},
276
- category: {type: 'string', choices: ['channels', 'recordings', 'wizyconfRecordings'], description: 'the category of the file'},
353
+ category: {
354
+ type: 'string',
355
+ choices: ['channels', 'recordings', 'wizyconfRecordings'],
356
+ description: 'the category of the file',
357
+ },
277
358
  download: {type: 'boolean', description: 'a link that always downloads (else audio and video play)'},
278
359
  },
279
360
  args: [0, 0],
280
361
  async run(ctx) {
281
362
  const v = ctx.values;
282
363
  if (!v['file-id'] || !v.category) throw new UsageError('--file-id and --category are required', ctx.usage());
283
- if (v['file-id'].length > 1024 || /[\s\0]/.test(v['file-id'])) throw new UsageError(`not a file id: ${v['file-id']}`);
284
- return ctx.client(wdaClient).get(`/insights/conferences/recording/download_url${query({fileId: v['file-id'], category: v.category, download: v.download ? '1' : undefined})}`);
364
+ if (v['file-id'].length > 1024 || /[\s\0]/.test(v['file-id']))
365
+ throw new UsageError(`not a file id: ${v['file-id']}`);
366
+ return ctx
367
+ .client(wdaClient)
368
+ .get(
369
+ `/insights/conferences/recording/download_url${query({fileId: v['file-id'], category: v.category, download: v.download ? '1' : undefined})}`,
370
+ );
285
371
  },
286
372
  text: (r) => r.presignedDownloadUrl ?? '',
287
373
  },
@@ -290,7 +376,8 @@ const group = {
290
376
  usage: `wx meetings schedule-summary ${CONF}`,
291
377
  options: {},
292
378
  args: [1, 1],
293
- run: (ctx) => ctx.client(wdaClient).post(`/insights/conferences${apiPath`/${conf(ctx)}`}/schedule_summary_generation`),
379
+ run: (ctx) =>
380
+ ctx.client(wdaClient).post(`/insights/conferences${apiPath`/${conf(ctx)}`}/schedule_summary_generation`),
294
381
  text: () => 'The summary was scheduled: read it later with wx meetings summary.',
295
382
  },
296
383
  },
@@ -5,13 +5,26 @@
5
5
  // - SIP over a WebSocket (wss://<pbx>/sip/): a headless SIP user of the extension (no media; it turns every call
6
6
  // offered to it down) for the live view and what only SIP has: recording and the transfer through a conference.
7
7
  // The user acts on their own calls and devices only; the admin-only `user` parameter of the API is left out.
8
- import {jwtClaims} from '../auth.mjs';
9
- import {UsageError} from '../errors.mjs';
10
- import {duration, localTime, paint, table} from '../format.mjs';
11
- import {pbxUrl} from '../services.mjs';
8
+ import {jwtClaims} from 'wldx-core/auth';
9
+ import {UsageError} from 'wldx-core/errors';
10
+ import {duration, localTime, paint, table} from 'wldx-core/format';
11
+ import {pbxUrl} from 'wldx-core/services';
12
12
  import {CallControl, RECORD_ACTIONS} from '../phone/control.mjs';
13
13
  import {deviceName} from '../phone/events.mjs';
14
- import {PbxError, WATCHER_USER_AGENT, checkCallId, checkContact, checkDigits, extensionOf, isWatcher, normNumber, pbxAction, pbxCall, sipEndpoint, sipPassword} from '../phone/pbx.mjs';
14
+ import {
15
+ PbxError,
16
+ WATCHER_USER_AGENT,
17
+ checkCallId,
18
+ checkContact,
19
+ checkDigits,
20
+ extensionOf,
21
+ isWatcher,
22
+ normNumber,
23
+ pbxAction,
24
+ pbxCall,
25
+ sipEndpoint,
26
+ sipPassword,
27
+ } from '../phone/pbx.mjs';
15
28
 
16
29
  // Tests replace the WebSocket (a fake PBX) and the timings of the SIP channel here.
17
30
  export const sipIo = {WebSocket: undefined, options: {}};
@@ -35,10 +48,27 @@ const party = (number, name) => [number, name].filter((v) => v !== undefined &&
35
48
  // A call of the REST list or of the SIP view, as table cells.
36
49
  function callRow(call, c) {
37
50
  if (call.sipCallId !== undefined) {
38
- return [callColor(c, call.state), duration(Number(call.duration)), party(call.callerNumber, call.callerName), '→', party(call.calleeNumber, call.calleeName), c.dim(call.sipCallId)];
51
+ return [
52
+ callColor(c, call.state),
53
+ duration(Number(call.duration)),
54
+ party(call.callerNumber, call.callerName),
55
+ '→',
56
+ party(call.calleeNumber, call.calleeName),
57
+ c.dim(call.sipCallId),
58
+ ];
39
59
  }
40
- const extras = [call.recording && call.recording !== 'off' && `recording ${call.recording}`, call.tags?.length && `[${call.tags.join(', ')}]`, call.endReason && `(${call.endReason})`];
41
- return [callColor(c, call.state), call.direction === 'outbound' ? 'to' : 'from', party(call.remoteNumber, call.remoteName), extras.filter(Boolean).join(' '), c.dim(call.id)];
60
+ const extras = [
61
+ call.recording && call.recording !== 'off' && `recording ${call.recording}`,
62
+ call.tags?.length && `[${call.tags.join(', ')}]`,
63
+ call.endReason && `(${call.endReason})`,
64
+ ];
65
+ return [
66
+ callColor(c, call.state),
67
+ call.direction === 'outbound' ? 'to' : 'from',
68
+ party(call.remoteNumber, call.remoteName),
69
+ extras.filter(Boolean).join(' '),
70
+ c.dim(call.id),
71
+ ];
42
72
  }
43
73
 
44
74
  // table() without the columns that are empty in every row.
@@ -56,15 +86,35 @@ function callsText(calls, ctx) {
56
86
  function devicesText(devices, ctx) {
57
87
  if (!devices?.length) return 'No device of yours is registered now.';
58
88
  const c = paint(ctx);
59
- return compactTable(devices.map((d) => [[d.active && c.green('active'), d.watcher && c.dim('watcher')].filter(Boolean).join(' '), d.name ?? deviceName(d.userAgent), d.contact ?? '']));
89
+ return compactTable(
90
+ devices.map((d) => [
91
+ [d.active && c.green('active'), d.watcher && c.dim('watcher')].filter(Boolean).join(' '),
92
+ d.name ?? deviceName(d.userAgent),
93
+ d.contact ?? '',
94
+ ]),
95
+ );
60
96
  }
61
97
 
62
98
  const actionText = (r) => (typeof r.message === 'string' ? r.message : `OK ${JSON.stringify(r.message ?? {})}`);
63
99
 
64
100
  // ---- the SIP channel
65
101
 
66
- const callView = (c) => ({id: c.id, direction: c.direction, state: c.state, remoteNumber: c.remoteNumber, remoteName: c.remoteName, tags: c.tags, recording: c.recording, endReason: c.endReason});
67
- const deviceView = (d) => ({contact: d.uri, name: d.deviceName, userAgent: d.userAgent, ...(isWatcher(d.userAgent) || d.isLocalRegistration ? {watcher: true} : {})});
102
+ const callView = (c) => ({
103
+ id: c.id,
104
+ direction: c.direction,
105
+ state: c.state,
106
+ remoteNumber: c.remoteNumber,
107
+ remoteName: c.remoteName,
108
+ tags: c.tags,
109
+ recording: c.recording,
110
+ endReason: c.endReason,
111
+ });
112
+ const deviceView = (d) => ({
113
+ contact: d.uri,
114
+ name: d.deviceName,
115
+ userAgent: d.userAgent,
116
+ ...(isWatcher(d.userAgent) || d.isLocalRegistration ? {watcher: true} : {}),
117
+ });
68
118
 
69
119
  // Connects as a SIP user of the extension and waits for the first call state. setup(cc, extension) adds the
70
120
  // listeners before the connection. The registration asks for 120 s, so a watcher killed instead of stopped leaves
@@ -73,7 +123,16 @@ async function openSip(ctx, setup) {
73
123
  const extension = extensionOf(ctx.token);
74
124
  const {wsUrl, domain} = sipEndpoint(ctx.token, ctx.env);
75
125
  const password = await sipPassword(ctx, extension);
76
- const cc = new CallControl({domain, wsUrl, extension, password, userAgent: WATCHER_USER_AGENT, expires: 120, WebSocketImpl: sipIo.WebSocket ?? globalThis.WebSocket, ...sipIo.options});
126
+ const cc = new CallControl({
127
+ domain,
128
+ wsUrl,
129
+ extension,
130
+ password,
131
+ userAgent: WATCHER_USER_AGENT,
132
+ expires: 120,
133
+ WebSocketImpl: sipIo.WebSocket ?? globalThis.WebSocket,
134
+ ...sipIo.options,
135
+ });
77
136
  cc.on('error', () => {}); // a one-shot command reports only its own failure
78
137
  setup?.(cc, extension);
79
138
  try {
@@ -90,7 +149,10 @@ async function openSip(ctx, setup) {
90
149
  async function sipCommand(ctx, command, callId, action) {
91
150
  const cc = await openSip(ctx);
92
151
  try {
93
- if (!cc.getActiveCalls().some((call) => call.id === callId)) throw new PbxError(`no current call of this user has the id ${callId} (see: wx phone list --sip or wx phone list)`);
152
+ if (!cc.getActiveCalls().some((call) => call.id === callId))
153
+ throw new PbxError(
154
+ `no current call of this user has the id ${callId} (see: wx phone list --sip or wx phone list)`,
155
+ );
94
156
  try {
95
157
  await action(cc);
96
158
  } catch (e) {
@@ -130,7 +192,11 @@ function eventText(e, ctx) {
130
192
  if (d.presence?.status) parts.push(`presence ${d.presence.status}`);
131
193
  if (d.devices) parts.push(`${d.devices.filter((x) => !x.watcher).length} device(s)`);
132
194
  if (d.activeDevice) parts.push(`ringing ${activeDeviceText(d.activeDevice)}`);
133
- line = [c.bold(e.event === 'READY' ? 'ready' : 'reconnected'), parts.join(', '), ...(d.calls ?? []).map((call) => `\n ${compactTable([callRow(call, c)])[0]}`)].join(' ');
195
+ line = [
196
+ c.bold(e.event === 'READY' ? 'ready' : 'reconnected'),
197
+ parts.join(', '),
198
+ ...(d.calls ?? []).map((call) => `\n ${compactTable([callRow(call, c)])[0]}`),
199
+ ].join(' ');
134
200
  break;
135
201
  }
136
202
  case 'CALL_START':
@@ -202,7 +268,14 @@ const phone = {
202
268
  };
203
269
  },
204
270
  text(r) {
205
- const rows = [['Extension', r.extension], ['Name', r.name], ['E-mail', r.email], ['Role', r.role], ['License', r.license], ['PBX', r.pbx]];
271
+ const rows = [
272
+ ['Extension', r.extension],
273
+ ['Name', r.name],
274
+ ['E-mail', r.email],
275
+ ['Role', r.role],
276
+ ['License', r.license],
277
+ ['PBX', r.pbx],
278
+ ];
206
279
  return table(rows.filter(([, v]) => v !== null && v !== undefined && v !== ''));
207
280
  },
208
281
  },
@@ -212,7 +285,9 @@ const phone = {
212
285
  details:
213
286
  'REST (default): {calls: [{sipCallId, callerNumber, callerName, calleeNumber, calleeName, state, duration}], total}; state is the PBX channel state (Up also when held).\n' +
214
287
  '--sip: {calls: [{id, direction, state ringing|answered|held, remoteNumber, remoteName, tags, recording}]} over a short SIP registration (it shows hold and the number dialled).',
215
- options: {sip: {type: 'boolean', description: 'the calls as SIP sees them (held, direction, recording, number dialled)'}},
288
+ options: {
289
+ sip: {type: 'boolean', description: 'the calls as SIP sees them (held, direction, recording, number dialled)'},
290
+ },
216
291
  args: [0, 0],
217
292
  async run(ctx) {
218
293
  if (!ctx.values.sip) return pbxCall(ctx, 'GET', '/list-calls/');
@@ -224,7 +299,8 @@ const phone = {
224
299
  text: (r, ctx) => callsText(r.calls, ctx),
225
300
  },
226
301
  call: {
227
- summary: 'place a call from your device: it rings first and dials when you pick up, or starts at once on --device',
302
+ summary:
303
+ 'place a call from your device: it rings first and dials when you pick up, or starts at once on --device',
228
304
  usage: 'wx phone call <number> [--mobile | --any | --device CONTACT]',
229
305
  details:
230
306
  'Click-to-call. A number is an extension, a phone number (spaces, dashes, dots and brackets are dropped) or a code like *76.\n' +
@@ -234,30 +310,43 @@ const phone = {
234
310
  options: {
235
311
  mobile: {type: 'boolean', description: 'ring your mobile number (from the PBX settings) first'},
236
312
  any: {type: 'boolean', description: 'ring all your devices first'},
237
- device: {type: 'string', placeholder: 'CONTACT', description: 'place the call on this device (contact from wx phone devices); on the active one at once'},
313
+ device: {
314
+ type: 'string',
315
+ placeholder: 'CONTACT',
316
+ description: 'place the call on this device (contact from wx phone devices); on the active one at once',
317
+ },
238
318
  },
239
319
  args: [1, 1],
240
320
  async run(ctx) {
241
321
  const {values} = ctx;
242
322
  const destination = normNumber(ctx.positionals[0]);
243
- if ([values.mobile, values.any, values.device !== undefined].filter(Boolean).length > 1) throw new UsageError('give one of --mobile, --any, --device', ctx.usage());
323
+ if ([values.mobile, values.any, values.device !== undefined].filter(Boolean).length > 1)
324
+ throw new UsageError('give one of --mobile, --any, --device', ctx.usage());
244
325
  const body = {destination};
245
326
  if (values.mobile) body.callType = 'mobility';
246
327
  else if (values.any) body.callType = 'any';
247
- else if (values.device !== undefined) Object.assign(body, {callType: 'device', deviceContact: checkContact(values.device)});
328
+ else if (values.device !== undefined)
329
+ Object.assign(body, {callType: 'device', deviceContact: checkContact(values.device)});
248
330
  else {
249
331
  // With no active device the PBX takes the first registered one, which may be a SIP watcher (it turns every
250
332
  // call down). Pick the device here: the active one, else the first real one.
251
333
  const list = await pbxCall(ctx, 'GET', '/list-devices/');
252
334
  const real = (list?.devices ?? []).filter((d) => !isWatcher(d.userAgent));
253
335
  const device = [...real.filter((d) => d.active), ...real][0]?.contact;
254
- if (!device) throw new PbxError('no device of this user is online to place the call from: open x-bees or a Wildix phone, or call through the mobile (--mobile)');
336
+ if (!device)
337
+ throw new PbxError(
338
+ 'no device of this user is online to place the call from: open x-bees or a Wildix phone, or call through the mobile (--mobile)',
339
+ );
255
340
  body.deviceContact = device;
256
341
  }
257
342
  return pbxAction(ctx, 'POST', '/make-call/', body);
258
343
  },
259
344
  text(r, ctx) {
260
- if (ctx.values.device !== undefined) return [actionText(r), 'The call is placed on the device you named (on your active device it starts at once).'];
345
+ if (ctx.values.device !== undefined)
346
+ return [
347
+ actionText(r),
348
+ 'The call is placed on the device you named (on your active device it starts at once).',
349
+ ];
261
350
  const where = ctx.values.mobile ? 'your mobile' : ctx.values.any ? 'your devices' : 'your device';
262
351
  return [actionText(r), `The PBX rings ${where} first; it dials the number when you pick up.`];
263
352
  },
@@ -265,7 +354,13 @@ const phone = {
265
354
  answer: {
266
355
  summary: 'answer a ringing call (on a device that can answer remotely: a Wildix phone, x-bees)',
267
356
  usage: 'wx phone answer <callId> [--device CONTACT]',
268
- options: {device: {type: 'string', placeholder: 'CONTACT', description: 'answer on this device (its contact from wx phone devices)'}},
357
+ options: {
358
+ device: {
359
+ type: 'string',
360
+ placeholder: 'CONTACT',
361
+ description: 'answer on this device (its contact from wx phone devices)',
362
+ },
363
+ },
269
364
  ...callIdArg,
270
365
  async run(ctx) {
271
366
  const body = {sipCallId: checkCallId(ctx.positionals[0])};
@@ -284,7 +379,10 @@ const phone = {
284
379
  args: [2, 2],
285
380
  async run(ctx) {
286
381
  const [id, number] = ctx.positionals;
287
- return pbxAction(ctx, 'POST', '/blind-transfer/', {sipCallId: checkCallId(id), destination: normNumber(number)});
382
+ return pbxAction(ctx, 'POST', '/blind-transfer/', {
383
+ sipCallId: checkCallId(id),
384
+ destination: normNumber(number),
385
+ });
288
386
  },
289
387
  text: actionText,
290
388
  },
@@ -309,7 +407,8 @@ const phone = {
309
407
  const id = checkCallId(ctx.positionals[0]);
310
408
  const digits = checkDigits(ctx.positionals[1]);
311
409
  let out;
312
- for (let i = 0; i < digits.length; i += DTMF_PART) out = await pbxAction(ctx, 'POST', '/dtmf/', {sipCallId: id, digits: digits.slice(i, i + DTMF_PART)});
410
+ for (let i = 0; i < digits.length; i += DTMF_PART)
411
+ out = await pbxAction(ctx, 'POST', '/dtmf/', {sipCallId: id, digits: digits.slice(i, i + DTMF_PART)});
313
412
  return out;
314
413
  },
315
414
  text: actionText,
@@ -325,9 +424,11 @@ const phone = {
325
424
  async run(ctx) {
326
425
  const {values} = ctx;
327
426
  const body = {sipCallId: checkCallId(ctx.positionals[0])};
328
- if (values.name === undefined && values.number === undefined) throw new UsageError('give --name, --number or both', ctx.usage());
427
+ if (values.name === undefined && values.number === undefined)
428
+ throw new UsageError('give --name, --number or both', ctx.usage());
329
429
  if (values.name !== undefined) {
330
- if (!values.name || values.name.length > NAME_MAX) throw new UsageError(`--name takes 1 to ${NAME_MAX} characters`);
430
+ if (!values.name || values.name.length > NAME_MAX)
431
+ throw new UsageError(`--name takes 1 to ${NAME_MAX} characters`);
331
432
  body.name = values.name;
332
433
  }
333
434
  if (values.number !== undefined) body.number = normNumber(values.number);
@@ -342,7 +443,8 @@ const phone = {
342
443
  args: [0, 0],
343
444
  async run(ctx) {
344
445
  const r = await pbxCall(ctx, 'GET', '/list-devices/');
345
- if (Array.isArray(r?.devices)) r.devices = r.devices.map((d) => (isWatcher(d.userAgent) ? {...d, watcher: true} : d));
446
+ if (Array.isArray(r?.devices))
447
+ r.devices = r.devices.map((d) => (isWatcher(d.userAgent) ? {...d, watcher: true} : d));
346
448
  return r;
347
449
  },
348
450
  text: (r, ctx) => devicesText(r.devices, ctx),
@@ -357,7 +459,8 @@ const phone = {
357
459
  args: [0, 1],
358
460
  async run(ctx) {
359
461
  const {values, positionals} = ctx;
360
- if ([values.mobile, values.any, positionals.length > 0].filter(Boolean).length !== 1) throw new UsageError('give one device contact, --mobile or --any', ctx.usage());
462
+ if ([values.mobile, values.any, positionals.length > 0].filter(Boolean).length !== 1)
463
+ throw new UsageError('give one device contact, --mobile or --any', ctx.usage());
361
464
  let body;
362
465
  if (values.mobile) body = {deviceType: 'mobility'};
363
466
  else if (values.any) body = {deviceType: 'any'};
@@ -366,7 +469,8 @@ const phone = {
366
469
  // The PBX wants the device's user agent too: take it from the registration of that contact.
367
470
  const list = await pbxCall(ctx, 'GET', '/list-devices/');
368
471
  const ua = (list?.devices ?? []).find((d) => d.contact === contact)?.userAgent;
369
- if (!ua) throw new PbxError(`no registered device of this user has the contact ${contact} (see: wx phone devices)`);
472
+ if (!ua)
473
+ throw new PbxError(`no registered device of this user has the contact ${contact} (see: wx phone devices)`);
370
474
  if (isWatcher(ua)) throw new PbxError('this is a SIP watcher, not a phone: it cannot carry a call');
371
475
  body = {deviceType: 'device', contact, userAgent: ua};
372
476
  }
@@ -389,7 +493,8 @@ const phone = {
389
493
  args: [0, 0],
390
494
  async run(ctx) {
391
495
  const {values} = ctx;
392
- if (values.for !== undefined && !/^\d{1,7}$/.test(values.for)) throw new UsageError(`--for takes whole seconds: ${values.for}`);
496
+ if (values.for !== undefined && !/^\d{1,7}$/.test(values.for))
497
+ throw new UsageError(`--for takes whole seconds: ${values.for}`);
393
498
  const seconds = Number(values.for ?? 0);
394
499
  const all = !values['calls-only'];
395
500
  const emit = (event, data) => ctx.emit({event, data: data ?? {}});
@@ -404,7 +509,13 @@ const phone = {
404
509
  emit(readyOnce ? 'RECONNECTED' : 'READY', {
405
510
  extension,
406
511
  calls: cc.getActiveCalls().map(callView),
407
- ...(all ? {devices: cc.getDevices().map(deviceView), presence: cc.getPresence(), activeDevice: cc.getActiveDevice()} : {}),
512
+ ...(all
513
+ ? {
514
+ devices: cc.getDevices().map(deviceView),
515
+ presence: cc.getPresence(),
516
+ activeDevice: cc.getActiveDevice(),
517
+ }
518
+ : {}),
408
519
  });
409
520
  readyOnce = true;
410
521
  });
@@ -427,7 +538,8 @@ const phone = {
427
538
  async run(ctx) {
428
539
  const id = checkCallId(ctx.positionals[0]);
429
540
  const action = ctx.positionals[1];
430
- if (!RECORD_ACTIONS.includes(action)) throw new UsageError(`record takes ${RECORD_ACTIONS.join(', ')}: ${action}`, ctx.usage());
541
+ if (!RECORD_ACTIONS.includes(action))
542
+ throw new UsageError(`record takes ${RECORD_ACTIONS.join(', ')}: ${action}`, ctx.usage());
431
543
  return sipCommand(ctx, 'record', id, (cc) => cc.record(id, action));
432
544
  },
433
545
  text: (r, ctx) => `record ${ctx.positionals[1]}: the PBX took it (call ${r.callId})`,