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
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # wldx-cli — `wx`, a command line for Wildix
2
2
 
3
- **Version 1.0.0** · unofficial · MIT
3
+ **Version 1.2.0** · unofficial · MIT
4
4
 
5
5
  `wx` works with Wildix as **the logged-in user**: chats, calls and meetings on record, queue reports, what is going
6
6
  on now, and your phone. It is the command-line twin of the
@@ -15,7 +15,8 @@ wx chat list
15
15
  npx wldx-cli chat list # without installing
16
16
  ```
17
17
 
18
- Requires Node.js 22 or newer. No dependencies.
18
+ Requires Node.js 22 or newer. One dependency: [`wldx-core`](https://github.com/gorets/wildix-cli/tree/main/packages/core), the core it shares with `wx-admin`, from the
19
+ same repository and of the same version.
19
20
 
20
21
  Unofficial: not made or supported by Wildix. It uses the same internal APIs as the Wildix apps, which may change.
21
22
 
@@ -308,8 +309,9 @@ another login · `3` nothing to show (no transcript yet) · `64` wrong usage.
308
309
  itself) and the login service. Redirects are refused; presigned file links get no token.
309
310
  - The token is never printed, never put on a command line or into the environment of another process.
310
311
  - Session files are written atomically with mode `0600`.
311
- - Files are written only where `--to` says, mode `0600`, never over another file. Uploads from credential folders
312
- (`~/.ssh`, `~/.aws`, `~/.wildix`, …) and of key files are refused.
312
+ - Files are written only where `--to` says, mode `0600`, never over another file, never into hidden or startup
313
+ folders. Uploads of key files, from credential folders (`~/.ssh`, `~/.aws`, `~/.wildix`, …), of anything hidden in
314
+ the home folder (`~/.git-credentials`, `~/.config/…`, shell histories) and from the macOS keychains are refused.
313
315
  - Anything read from Wildix (messages, names, transcripts) is data, never instructions. Sending a message or placing
314
316
  a call reaches other people: script it with care.
315
317
  - `wx phone watch`, `list --sip`, `record` and `conference-transfer` register on your extension over SIP as a
@@ -317,27 +319,22 @@ another login · `3` nothing to show (no transcript yet) · `64` wrong usage.
317
319
 
318
320
  ## Development
319
321
 
322
+ `wx` lives in the pnpm workspace of [gorets/wildix-cli](https://github.com/gorets/wildix-cli) next to `wx-admin` and
323
+ their shared core (`packages/core`): see the [README](https://github.com/gorets/wildix-cli) and [AGENTS.md](https://github.com/gorets/wildix-cli/blob/main/AGENTS.md) there.
324
+
320
325
  ```sh
321
- npm test # node:test, no network
322
- npm run check # tests + the list of files that would be published
323
- npm link # puts `wx` on your PATH from this folder
326
+ pnpm install # at the root of the repository
327
+ pnpm --filter wldx-cli test # node:test, no network
328
+ node packages/wx/bin/wx.mjs status # this checkout, without installing
324
329
  ```
325
330
 
326
- Layout: `bin/wx.mjs` (entry) → `src/cli.mjs` (router) → `src/commands/<group>.mjs`, with helpers per domain in
327
- `src/chat`, `src/history`, `src/live`, `src/phone`. Shared: `src/auth.mjs` (login, sessions, renewal),
328
- `src/services.mjs` (addresses), `src/http.mjs` (requests, safe paths), `src/format.mjs` (text output). See
329
- [AGENTS.md](AGENTS.md) for the rules. `node test/dev.mjs <module> <args>` runs a command module that is not listed
330
- yet; after changing commands, regenerate the reference: `node tools/readme-commands.mjs`.
331
+ Layout: `bin/wx.mjs` (entry) → `src/cli.mjs` (the spec on the router of the core) → `src/commands/<group>.mjs`,
332
+ with helpers per domain in `src/chat`, `src/history`, `src/live`, `src/phone`. Login, sessions, addresses, requests
333
+ and text output come from `wldx-core`. See [AGENTS.md](https://github.com/gorets/wildix-cli/blob/main/packages/wx/AGENTS.md).
334
+ In `packages/wx`: `node test/dev.mjs <module> <args>` runs a command module that is not listed yet; after changing
335
+ commands, regenerate the reference with `node tools/readme-commands.mjs` and paste it between the markers.
331
336
 
332
337
  ## Releasing
333
338
 
334
- 1. Bump `version` in `package.json`, add a `CHANGELOG.md` entry, update the version at the top of this file.
335
- 2. Commit, tag `vX.Y.Z`, push the tag: `.github/workflows/release.yml` checks the tag against `package.json`, runs
336
- the tests and **stages** the version with provenance through npm trusted publishing (no npm token in the
337
- repository).
338
- 3. Approve it with 2FA, which makes it public: `npm stage list wldx-cli`, then `npm stage approve <stage-id>`
339
- (`npm stage download <stage-id>` to look at the tarball first).
340
-
341
- The trusted publisher on npmjs.com (package settings → Trusted publishing): GitHub Actions, `gorets/wildix-cli`,
342
- workflow `release.yml`, permission "npm stage publish". Version 0.1.0 was published by hand, because a trusted
343
- publisher can be configured only for a package that exists.
339
+ Together with `wx-admin` and the core, one version for the three: see Releasing in the
340
+ [README of the repository](https://github.com/gorets/wildix-cli#releasing).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wldx-cli",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Unofficial command line for Wildix (x-bees chats and more), acting as the logged-in user",
5
5
  "keywords": [
6
6
  "wildix",
@@ -12,9 +12,10 @@
12
12
  "author": "Vladimir Gorobets",
13
13
  "repository": {
14
14
  "type": "git",
15
- "url": "git+https://github.com/gorets/wildix-cli.git"
15
+ "url": "git+https://github.com/gorets/wildix-cli.git",
16
+ "directory": "packages/wx"
16
17
  },
17
- "homepage": "https://github.com/gorets/wildix-cli#readme",
18
+ "homepage": "https://github.com/gorets/wildix-cli/tree/main/packages/wx#readme",
18
19
  "bugs": "https://github.com/gorets/wildix-cli/issues",
19
20
  "type": "module",
20
21
  "bin": {
@@ -27,8 +28,10 @@
27
28
  "engines": {
28
29
  "node": ">=22"
29
30
  },
31
+ "dependencies": {
32
+ "wldx-core": "1.2.0"
33
+ },
30
34
  "scripts": {
31
- "test": "node --test test/*.test.mjs",
32
- "check": "npm test && npm pack --dry-run"
35
+ "test": "node --test test/*.test.mjs"
33
36
  }
34
- }
37
+ }
@@ -1,10 +1,12 @@
1
1
  // Text output of the chat commands: one line per channel, message, member or person, local times, names of
2
2
  // people instead of their ids (looked up by the command, see withNames in lib.mjs).
3
- import {localTime, oneLine, paint, table} from '../format.mjs';
3
+ import {localTime, oneLine, paint, table} from 'wldx-core/format';
4
4
  import {displayName, nameIn} from './lib.mjs';
5
5
 
6
6
  const reactions = (counts) => {
7
- const parts = Object.entries(counts ?? {}).filter(([, n]) => n > 0).map(([t, n]) => `${t} ${n}`);
7
+ const parts = Object.entries(counts ?? {})
8
+ .filter(([, n]) => n > 0)
9
+ .map(([t, n]) => `${t} ${n}`);
8
10
  return parts.length ? `[${parts.join(', ')}]` : '';
9
11
  };
10
12
 
@@ -31,11 +33,20 @@ export function messageText(m, data, {max = 2000} = {}) {
31
33
  export function messageLines(messages, data, ctx) {
32
34
  const c = paint(ctx);
33
35
  const sorted = [...(messages ?? [])].sort((a, b) => (a.timelineSeq ?? 0) - (b.timelineSeq ?? 0));
34
- return table(sorted.map((m) => [c.dim(localTime(m.createdAt)), c.bold(nameIn(data, m.userId) || '?'), `${messageText(m, data)} ${c.dim(m.messageId)}`]));
36
+ return table(
37
+ sorted.map((m) => [
38
+ c.dim(localTime(m.createdAt)),
39
+ c.bold(nameIn(data, m.userId) || '?'),
40
+ `${messageText(m, data)} ${c.dim(m.messageId)}`,
41
+ ]),
42
+ );
35
43
  }
36
44
 
37
45
  // The title of a channel: its subject, or for a 1:1 channel the name of the other person.
38
- export const channelTitle = (ch, data) => ch?.subject || (ch?.peerId ? nameIn(data, ch.peerId) : '') || (ch?.channelType === 'direct' ? '(direct)' : '(no subject)');
46
+ export const channelTitle = (ch, data) =>
47
+ ch?.subject ||
48
+ (ch?.peerId ? nameIn(data, ch.peerId) : '') ||
49
+ (ch?.channelType === 'direct' ? '(direct)' : '(no subject)');
39
50
 
40
51
  // The people a channel line names: the other person of a 1:1 channel, the author of the last message.
41
52
  export const channelUserIds = (ch) => [ch?.peerId, ch?.lastMessage?.userId];
@@ -62,7 +73,15 @@ const preview = (ch, data) => {
62
73
  // Channels: last activity, id, inbox flags, title, the last message.
63
74
  export function channelLines(channels, data, ctx) {
64
75
  const c = paint(ctx);
65
- return table((channels ?? []).map((ch) => [c.dim(localTime(ch.lastActivityAt)), ch.channelId, flags(ch, c), c.bold(oneLine(channelTitle(ch, data), 50)), c.dim(preview(ch, data))]));
76
+ return table(
77
+ (channels ?? []).map((ch) => [
78
+ c.dim(localTime(ch.lastActivityAt)),
79
+ ch.channelId,
80
+ flags(ch, c),
81
+ c.bold(oneLine(channelTitle(ch, data), 50)),
82
+ c.dim(preview(ch, data)),
83
+ ]),
84
+ );
66
85
  }
67
86
 
68
87
  export function channelDetails(ch, data, ctx) {
@@ -71,7 +90,10 @@ export function channelDetails(ch, data, ctx) {
71
90
  const rows = [
72
91
  ['Channel', ch.channelId],
73
92
  ['Title', c.bold(channelTitle(ch, data))],
74
- ['Type', [ch.channelType, ch.access, ch.broadcast && 'broadcast', ch.external && 'external'].filter(Boolean).join(' · ')],
93
+ [
94
+ 'Type',
95
+ [ch.channelType, ch.access, ch.broadcast && 'broadcast', ch.external && 'external'].filter(Boolean).join(' · '),
96
+ ],
75
97
  ];
76
98
  if (ch.description) rows.push(['Description', oneLine(ch.description, 300)]);
77
99
  if (ch.peerId) rows.push(['With', `${nameIn(data, ch.peerId)} (${ch.peerId})`]);
@@ -80,7 +102,10 @@ export function channelDetails(ch, data, ctx) {
80
102
  rows.push(['Last activity', localTime(ch.lastActivityAt)]);
81
103
  if (ch.pinnedMessageIds?.length) rows.push(['Pinned', ch.pinnedMessageIds.join(', ')]);
82
104
  if (ch.autoRecord !== undefined || ch.autoTranscribe !== undefined) {
83
- rows.push(['Conferences', [ch.autoRecord && 'auto record', ch.autoTranscribe && 'auto transcribe'].filter(Boolean).join(', ') || 'manual']);
105
+ rows.push([
106
+ 'Conferences',
107
+ [ch.autoRecord && 'auto record', ch.autoTranscribe && 'auto transcribe'].filter(Boolean).join(', ') || 'manual',
108
+ ]);
84
109
  }
85
110
  if (ch.inbox) rows.push(['Inbox', flags(ch, c) || 'read']);
86
111
  return table(rows);
@@ -88,7 +113,14 @@ export function channelDetails(ch, data, ctx) {
88
113
 
89
114
  // A person on one line: name, id, e-mail, extension, bot.
90
115
  export function userLine(u, c) {
91
- return [c.bold(displayName(u) ?? ''), u.id, u.email ?? '', u.pbxExtension ? `ext ${u.pbxExtension}` : '', u.bot ? 'bot' : '', u.deactivatedAt ? c.dim('deactivated') : ''];
116
+ return [
117
+ c.bold(displayName(u) ?? ''),
118
+ u.id,
119
+ u.email ?? '',
120
+ u.pbxExtension ? `ext ${u.pbxExtension}` : '',
121
+ u.bot ? 'bot' : '',
122
+ u.deactivatedAt ? c.dim('deactivated') : '',
123
+ ];
92
124
  }
93
125
 
94
126
  export const nextPage = (cursor, c) => (cursor ? [c.dim(`More: --cursor ${cursor}`)] : []);
package/src/chat/lib.mjs CHANGED
@@ -4,10 +4,10 @@
4
4
  import {randomUUID} from 'node:crypto';
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
7
- import {jwtClaims} from '../auth.mjs';
8
- import {UsageError} from '../errors.mjs';
9
- import {readForUpload, refuseUnsafeWrite} from '../files.mjs';
10
- import {apiPath, request} from '../http.mjs';
7
+ import {jwtClaims} from 'wldx-core/auth';
8
+ import {UsageError} from 'wldx-core/errors';
9
+ import {readForUpload, refuseUnsafeWrite} from 'wldx-core/files';
10
+ import {apiPath, request} from 'wldx-core/http';
11
11
  import {streamClient} from '../stream.mjs';
12
12
 
13
13
  // The operations the service answers with HTTP 400 "Operation <Name> is not implemented." (checked 2026-11-05).
@@ -20,19 +20,22 @@ export const notImplemented = (operation) =>
20
20
  export function checkInt(name, value, min, max) {
21
21
  if (value === undefined) return undefined;
22
22
  const s = String(value);
23
- if (!/^\d{1,12}$/.test(s) || Number(s) < min || Number(s) > max) throw new UsageError(`--${name} must be a whole number from ${min} to ${max}: ${value}`);
23
+ if (!/^\d{1,12}$/.test(s) || Number(s) < min || Number(s) > max)
24
+ throw new UsageError(`--${name} must be a whole number from ${min} to ${max}: ${value}`);
24
25
  return Number(s);
25
26
  }
26
27
 
27
28
  // An id that goes into a request path or a body: id characters only, never "/", "?", "#", ".." or nothing.
28
29
  export function checkId(what, value) {
29
30
  const v = String(value ?? '');
30
- if (!/^[A-Za-z0-9_+][A-Za-z0-9_.:+-]*$/.test(v) || v.includes('..')) throw new UsageError(`${what} is not a valid id: ${value}`);
31
+ if (!/^[A-Za-z0-9_+][A-Za-z0-9_.:+-]*$/.test(v) || v.includes('..'))
32
+ throw new UsageError(`${what} is not a valid id: ${value}`);
31
33
  return v;
32
34
  }
33
35
 
34
36
  export function checkReaction(type) {
35
- if (!/^[a-z0-9_+-]{1,64}$/.test(String(type))) throw new UsageError(`A reaction type must match [a-z0-9_+-] (up to 64): ${type}`);
37
+ if (!/^[a-z0-9_+-]{1,64}$/.test(String(type)))
38
+ throw new UsageError(`A reaction type must match [a-z0-9_+-] (up to 64): ${type}`);
36
39
  return type;
37
40
  }
38
41
 
@@ -90,7 +93,7 @@ export async function withNames(ctx, data, ids, client = api(ctx)) {
90
93
  Object.defineProperty(data, NAMES, {value: users, enumerable: false});
91
94
  return data;
92
95
  }
93
- export const nameIn = (data, id) => (id ? displayName(data?.[NAMES]?.get(id)) ?? id : '');
96
+ export const nameIn = (data, id) => (id ? (displayName(data?.[NAMES]?.get(id)) ?? id) : '');
94
97
 
95
98
  // The ids of people a message names: its author, the author of its quote.
96
99
  export const messageUserIds = (m) => [m?.userId, m?.quote?.userId, m?.pinnedBy];
@@ -98,16 +101,42 @@ export const messageUserIds = (m) => [m?.userId, m?.quote?.userId, m?.pinnedBy];
98
101
  // ── files ───────────────────────────────────────────────────────────────────────────────────────────────────
99
102
 
100
103
  // The checks of files to attach and to save: src/files.mjs.
101
- export {refuseSecretPath} from '../files.mjs';
104
+ export {refuseSecretPath} from 'wldx-core/files';
102
105
 
103
106
  const MIME = {
104
- txt: 'text/plain', md: 'text/markdown', csv: 'text/csv', html: 'text/html', htm: 'text/html', css: 'text/css', js: 'text/javascript',
105
- json: 'application/json', xml: 'application/xml', pdf: 'application/pdf', zip: 'application/zip', gz: 'application/gzip',
106
- tar: 'application/x-tar', doc: 'application/msword', docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
107
- xls: 'application/vnd.ms-excel', xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
108
- ppt: 'application/vnd.ms-powerpoint', pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
109
- png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp', svg: 'image/svg+xml', heic: 'image/heic',
110
- mp3: 'audio/mpeg', wav: 'audio/wav', ogg: 'audio/ogg', m4a: 'audio/mp4', mp4: 'video/mp4', mov: 'video/quicktime', webm: 'video/webm',
107
+ txt: 'text/plain',
108
+ md: 'text/markdown',
109
+ csv: 'text/csv',
110
+ html: 'text/html',
111
+ htm: 'text/html',
112
+ css: 'text/css',
113
+ js: 'text/javascript',
114
+ json: 'application/json',
115
+ xml: 'application/xml',
116
+ pdf: 'application/pdf',
117
+ zip: 'application/zip',
118
+ gz: 'application/gzip',
119
+ tar: 'application/x-tar',
120
+ doc: 'application/msword',
121
+ docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
122
+ xls: 'application/vnd.ms-excel',
123
+ xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
124
+ ppt: 'application/vnd.ms-powerpoint',
125
+ pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
126
+ png: 'image/png',
127
+ jpg: 'image/jpeg',
128
+ jpeg: 'image/jpeg',
129
+ gif: 'image/gif',
130
+ webp: 'image/webp',
131
+ svg: 'image/svg+xml',
132
+ heic: 'image/heic',
133
+ mp3: 'audio/mpeg',
134
+ wav: 'audio/wav',
135
+ ogg: 'audio/ogg',
136
+ m4a: 'audio/mp4',
137
+ mp4: 'video/mp4',
138
+ mov: 'video/quicktime',
139
+ webm: 'video/webm',
111
140
  };
112
141
  export const mimeOf = (file) => MIME[path.extname(file).slice(1).toLowerCase()] ?? 'application/octet-stream';
113
142
 
@@ -119,12 +148,14 @@ function presigned(url, env) {
119
148
  } catch {
120
149
  throw new Error('The service gave no valid storage link');
121
150
  }
122
- if (u.protocol !== 'https:' && !(env.WILDIX_ALLOW_CUSTOM_URL === '1' && u.protocol === 'http:')) throw new Error(`The storage link is not https: ${u.protocol}`);
151
+ if (u.protocol !== 'https:' && !(env.WILDIX_ALLOW_CUSTOM_URL === '1' && u.protocol === 'http:'))
152
+ throw new Error(`The storage link is not https: ${u.protocol}`);
123
153
  return u.href;
124
154
  }
125
155
 
126
156
  const ATTACHMENT_FIELDS = ['fsId', 'name', 'size', 'mime', 'width', 'height', 'thumbnail'];
127
- export const attachmentOf = (f) => Object.fromEntries(ATTACHMENT_FIELDS.filter((k) => f?.[k] !== undefined && f?.[k] !== null).map((k) => [k, f[k]]));
157
+ export const attachmentOf = (f) =>
158
+ Object.fromEntries(ATTACHMENT_FIELDS.filter((k) => f?.[k] !== undefined && f?.[k] !== null).map((k) => [k, f[k]]));
128
159
 
129
160
  // Uploads a file to a channel and returns the attachment object to put into a message (fsId, name, size, mime…).
130
161
  export async function uploadFile(ctx, channel, file, client = api(ctx)) {
@@ -133,7 +164,14 @@ export async function uploadFile(ctx, channel, file, client = api(ctx)) {
133
164
  const r = await client.call('POST', apiPath`/channels/${channel}/files`, {name});
134
165
  if (!r?.fsId || !r?.presignedUploadUrl) throw new Error('Unexpected answer of the upload: no fsId or link');
135
166
  const fsId = checkId('The file id of the answer', r.fsId);
136
- await request(presigned(r.presignedUploadUrl, ctx.env), {method: 'PUT', raw: {data, contentType: mimeOf(file)}, responseType: 'text', fetchImpl: ctx.fetch, timeoutMs: 600_000, service: 'the storage'});
167
+ await request(presigned(r.presignedUploadUrl, ctx.env), {
168
+ method: 'PUT',
169
+ raw: {data, contentType: mimeOf(file)},
170
+ responseType: 'text',
171
+ fetchImpl: ctx.fetch,
172
+ timeoutMs: 600_000,
173
+ service: 'the storage',
174
+ });
137
175
  const info = await client.call('GET', apiPath`/channels/${channel}/files/${fsId}`);
138
176
  return attachmentOf(info?.file ?? {fsId, name, size: data.length});
139
177
  }
@@ -142,12 +180,22 @@ export async function uploadFile(ctx, channel, file, client = api(ctx)) {
142
180
  export async function downloadFile(ctx, channel, message, fsId, to, client = api(ctx)) {
143
181
  const out = path.resolve(ctx.cwd ?? process.cwd(), to);
144
182
  refuseUnsafeWrite(out, ctx.env);
145
- if (fs.existsSync(out)) throw new UsageError(`${to} exists already: choose another path (nothing was overwritten or deleted)`);
183
+ if (fs.existsSync(out))
184
+ throw new UsageError(`${to} exists already: choose another path (nothing was overwritten or deleted)`);
146
185
  const dir = path.dirname(out);
147
- if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) throw new UsageError(`The folder of ${to} does not exist`);
148
- const r = await client.call('GET', apiPath`/channels/${channel}/messages/${message}/files/${fsId}/download-url` + '?download=1');
186
+ if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory())
187
+ throw new UsageError(`The folder of ${to} does not exist`);
188
+ const r = await client.call(
189
+ 'GET',
190
+ apiPath`/channels/${channel}/messages/${message}/files/${fsId}/download-url` + '?download=1',
191
+ );
149
192
  if (!r?.presignedDownloadUrl) throw new Error('No download link in the answer');
150
- const data = await request(presigned(r.presignedDownloadUrl, ctx.env), {responseType: 'buffer', fetchImpl: ctx.fetch, timeoutMs: 600_000, service: 'the storage'});
193
+ const data = await request(presigned(r.presignedDownloadUrl, ctx.env), {
194
+ responseType: 'buffer',
195
+ fetchImpl: ctx.fetch,
196
+ timeoutMs: 600_000,
197
+ service: 'the storage',
198
+ });
151
199
  const tmp = path.join(dir, `.download.${process.pid}.${randomUUID().slice(0, 8)}`);
152
200
  fs.writeFileSync(tmp, data, {flag: 'wx', mode: 0o600});
153
201
  try {
@@ -3,10 +3,10 @@
3
3
  // ctx.emit(); READY when subscribed, RECONNECTING / RECONNECTED while the connection is renewed (the service closes
4
4
  // one after two hours; the token, checked only when a connection opens, is renewed with ctx.freshToken()), END at
5
5
  // the end (ctx.signal, --count or --for).
6
- import {jwtClaims} from '../auth.mjs';
7
- import {AuthError} from '../errors.mjs';
8
- import {localTime, oneLine, paint} from '../format.mjs';
9
- import {streamWsUrl} from '../services.mjs';
6
+ import {jwtClaims} from 'wldx-core/auth';
7
+ import {AuthError} from 'wldx-core/errors';
8
+ import {localTime, oneLine, paint} from 'wldx-core/format';
9
+ import {streamWsUrl} from 'wldx-core/services';
10
10
  import {streamClient} from '../stream.mjs';
11
11
  import {displayName, meId} from './lib.mjs';
12
12
 
@@ -16,8 +16,17 @@ export const EVENT_GROUPS = {
16
16
  reads: ['read.cursor.updated', 'read.receipt.updated'],
17
17
  members: ['member.added', 'member.updated', 'member.removed'],
18
18
  channels: [
19
- 'channel.created', 'channel.updated', 'channel.truncated', 'channel.added', 'channel.removed', 'channel.hidden',
20
- 'channel.visible', 'channel.muted', 'channel.unmuted', 'channel.pinned', 'channel.unpinned',
19
+ 'channel.created',
20
+ 'channel.updated',
21
+ 'channel.truncated',
22
+ 'channel.added',
23
+ 'channel.removed',
24
+ 'channel.hidden',
25
+ 'channel.visible',
26
+ 'channel.muted',
27
+ 'channel.unmuted',
28
+ 'channel.pinned',
29
+ 'channel.unpinned',
21
30
  ],
22
31
  typing: ['typing.start', 'typing.stop'],
23
32
  };
@@ -26,7 +35,8 @@ export const EVENT_CHOICES = [...Object.keys(EVENT_GROUPS), ...Object.values(EVE
26
35
  const KEEPALIVE_MS = 4 * 60 * 1000; // the gateway drops a connection that is quiet for 10 minutes
27
36
  const BACKOFF = [1, 2, 5, 10]; // seconds, then 30
28
37
  const drop = (o) => JSON.parse(JSON.stringify(o)); // removes the undefined fields
29
- const unreadOf = (counters) => (counters ? Object.values(counters).reduce((n, c) => n + (c?.unmutedUnreadMessageCount ?? 0), 0) : undefined);
38
+ const unreadOf = (counters) =>
39
+ counters ? Object.values(counters).reduce((n, c) => n + (c?.unmutedUnreadMessageCount ?? 0), 0) : undefined;
30
40
 
31
41
  // Whether an event passes the filters. A reaction event does not say who reacted (its message is someone else's
32
42
  // work), so a reaction has no author: it is never taken for the user's own action, and --from leaves it out.
@@ -34,7 +44,7 @@ export function wanted(e, {channels, senders, types, mine, me}) {
34
44
  if (types.size && !types.has(e.eventType)) return false;
35
45
  if (channels.size && !channels.has(e.channelId)) return false;
36
46
  const isReaction = e.eventType.startsWith('reaction.');
37
- const author = isReaction ? undefined : e.message?.userId ?? e.readerUserId ?? e.typingUserId;
47
+ const author = isReaction ? undefined : (e.message?.userId ?? e.readerUserId ?? e.typingUserId);
38
48
  if (isReaction && senders.size) return false;
39
49
  if (senders.size && !senders.has(author)) return false;
40
50
  if (!mine) {
@@ -74,7 +84,18 @@ export async function view(e, nameOf) {
74
84
  const tokenExp = (token) => (Number.isFinite(jwtClaims(token).exp) ? jwtClaims(token).exp * 1000 : 0);
75
85
 
76
86
  // Runs until ctx.signal, `count` events (0: no limit) or `seconds` (0: no limit). Resolves when it stops.
77
- export async function watch(ctx, {channels = new Set(), senders = new Set(), types = new Set(), mine = false, raw = false, count = 0, seconds = 0} = {}) {
87
+ export async function watch(
88
+ ctx,
89
+ {
90
+ channels = new Set(),
91
+ senders = new Set(),
92
+ types = new Set(),
93
+ mine = false,
94
+ raw = false,
95
+ count = 0,
96
+ seconds = 0,
97
+ } = {},
98
+ ) {
78
99
  let token = ctx.token;
79
100
  let client = streamClient({token, env: ctx.env, fetchImpl: ctx.fetch});
80
101
  const me = await meId(ctx, client);
@@ -88,7 +109,9 @@ export async function watch(ctx, {channels = new Set(), senders = new Set(), typ
88
109
  if (!names.has(id)) {
89
110
  let name;
90
111
  try {
91
- name = displayName((await client.call('POST', '/users/query', {query: {ids: [id]}}, {timeoutMs: 10_000}))?.users?.[0]);
112
+ name = displayName(
113
+ (await client.call('POST', '/users/query', {query: {ids: [id]}}, {timeoutMs: 10_000}))?.users?.[0],
114
+ );
92
115
  } catch {
93
116
  // a name is a nicety: the id is printed anyway
94
117
  }
@@ -203,11 +226,17 @@ export async function watch(ctx, {channels = new Set(), senders = new Set(), typ
203
226
  }
204
227
  attempts += 1;
205
228
  if (!opened && attempts >= 5) {
206
- finish(new AuthError(`The chat service did not accept the connection (code ${ev?.code}): log in again (wx login) if this repeats`));
229
+ finish(
230
+ new AuthError(
231
+ `The chat service did not accept the connection (code ${ev?.code}): log in again (wx login) if this repeats`,
232
+ ),
233
+ );
207
234
  return;
208
235
  }
209
236
  const wait = BACKOFF[attempts - 1] ?? 30;
210
- queue = queue.then(() => !stopping && ctx.emit({event: 'RECONNECTING', code: ev?.code, attempt: attempts, inSeconds: wait}));
237
+ queue = queue.then(
238
+ () => !stopping && ctx.emit({event: 'RECONNECTING', code: ev?.code, attempt: attempts, inSeconds: wait}),
239
+ );
211
240
  retry = setTimeout(again, wait * 1000);
212
241
  };
213
242
  };
@@ -219,8 +248,8 @@ export async function watch(ctx, {channels = new Set(), senders = new Set(), typ
219
248
  export function eventText(e, ctx) {
220
249
  const c = paint(ctx);
221
250
  const at = c.dim(localTime(ctx?.clock ? ctx.clock() : Date.now(), {seconds: true}));
222
- const where = e.channel ? c.bold(oneLine(e.channel, 40)) : e.channelId ?? '';
223
- const who = (p) => (p ? p.name ?? p.id : '?');
251
+ const where = e.channel ? c.bold(oneLine(e.channel, 40)) : (e.channelId ?? '');
252
+ const who = (p) => (p ? (p.name ?? p.id) : '?');
224
253
  switch (true) {
225
254
  case e.event === 'READY':
226
255
  return `${at} ${c.green('watching the chats')}${e.types ? ` (${e.types.join(', ')})` : ''}${e.channels ? ` in ${e.channels.join(', ')}` : ''}${e.from ? ` from ${e.from.join(', ')}` : ''}. Ctrl-C stops.`;
@@ -236,7 +265,9 @@ export function eventText(e, ctx) {
236
265
  return `${at} ${e.event.padEnd(15)} ${where} ${c.bold(who(e.from))}: ${text}`;
237
266
  }
238
267
  case e.event?.startsWith('reaction.'): {
239
- const counts = Object.entries(e.reactions ?? {}).map(([t, n]) => `${t} ${n}`).join(', ');
268
+ const counts = Object.entries(e.reactions ?? {})
269
+ .map(([t, n]) => `${t} ${n}`)
270
+ .join(', ');
240
271
  return `${at} ${e.event.padEnd(15)} ${where} on a message of ${who(e.messageAuthor)}: ${counts ? `[${counts}]` : '(none)'}`;
241
272
  }
242
273
  case e.event?.startsWith('read.'):