wldx-cli 0.1.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +281 -37
- package/package.json +9 -6
- package/src/chat/format.mjs +129 -0
- package/src/chat/lib.mjs +210 -0
- package/src/chat/watch.mjs +282 -0
- package/src/cli.mjs +13 -132
- package/src/commands/calls.mjs +470 -0
- package/src/commands/channel.mjs +404 -0
- package/src/commands/chat.mjs +444 -61
- package/src/commands/conversations.mjs +368 -0
- package/src/commands/file.mjs +80 -0
- package/src/commands/inbox.mjs +108 -0
- package/src/commands/index.mjs +33 -12
- package/src/commands/live.mjs +462 -0
- package/src/commands/meetings.mjs +386 -0
- package/src/commands/phone.mjs +564 -0
- package/src/commands/reports.mjs +606 -0
- package/src/commands/section.mjs +101 -0
- package/src/commands/user.mjs +159 -0
- package/src/history/text.mjs +86 -0
- package/src/history/wda.mjs +171 -0
- package/src/live/socket.mjs +139 -0
- package/src/live/stats.mjs +243 -0
- package/src/live/text.mjs +318 -0
- package/src/live/views.mjs +391 -0
- package/src/phone/control.mjs +437 -0
- package/src/phone/events.mjs +220 -0
- package/src/phone/pbx.mjs +158 -0
- package/src/phone/sip.mjs +386 -0
- package/src/phone/xml.mjs +57 -0
- package/src/stream.mjs +12 -14
- package/src/version.mjs +0 -1
- package/src/auth.mjs +0 -128
- package/src/commands/auth.mjs +0 -32
- package/src/completion.mjs +0 -105
- package/src/errors.mjs +0 -30
- package/src/http.mjs +0 -47
package/src/commands/chat.mjs
CHANGED
|
@@ -1,72 +1,455 @@
|
|
|
1
|
-
// wx chat: channels and messages of the Wildix stream service, as the logged-in user
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import {
|
|
1
|
+
// wx chat: channels and messages of the Wildix stream service, as the logged-in user (the agent skill's
|
|
2
|
+
// messages.sh, mentions.sh, search.sh messages and watch.sh). The inbox state is `wx inbox`, channels and their
|
|
3
|
+
// members `wx channel`, files `wx file`, people `wx user`.
|
|
4
|
+
import {AuthError, UsageError} from 'wldx-core/errors';
|
|
5
|
+
import {localTime, oneLine, paint, table} from 'wldx-core/format';
|
|
6
|
+
import {apiPath, query} from 'wldx-core/http';
|
|
7
|
+
import {channelId, messageId} from '../stream.mjs';
|
|
8
|
+
import {channelLines, channelUserIds, done, messageLines, messageText, nextPage} from '../chat/format.mjs';
|
|
9
|
+
import {
|
|
10
|
+
api,
|
|
11
|
+
attachmentOf,
|
|
12
|
+
checkId,
|
|
13
|
+
checkInt,
|
|
14
|
+
checkReaction,
|
|
15
|
+
meId,
|
|
16
|
+
messageUserIds,
|
|
17
|
+
nameIn,
|
|
18
|
+
readText,
|
|
19
|
+
uploadFile,
|
|
20
|
+
withNames,
|
|
21
|
+
} from '../chat/lib.mjs';
|
|
22
|
+
import {EVENT_CHOICES, EVENT_GROUPS, eventText, watch} from '../chat/watch.mjs';
|
|
5
23
|
|
|
6
24
|
const SEQ_MAX = 999_999_999_999;
|
|
25
|
+
const seq = (name, value) => checkInt(name, value, 0, SEQ_MAX);
|
|
7
26
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
27
|
+
const target = (positionals) => ({ch: channelId(positionals[0]), msg: messageId(positionals[1])});
|
|
28
|
+
|
|
29
|
+
// Stops (exit 2) unless the message is the user's own; returns the message.
|
|
30
|
+
async function ownMessage(ctx, client, ch, msg) {
|
|
31
|
+
const r = await client.call('GET', apiPath`/channels/${ch}/messages/${msg}`);
|
|
32
|
+
const author = r?.message?.userId;
|
|
33
|
+
const me = await meId(ctx, client);
|
|
34
|
+
if (!author || author !== me)
|
|
35
|
+
throw new AuthError(`The message is not yours (author: ${author ?? 'unknown'}, you: ${me}). Nothing was changed.`);
|
|
36
|
+
return r.message;
|
|
12
37
|
}
|
|
13
38
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
39
|
+
// The body of a full update (PUT): the new text with what the message already has, so nothing is lost. Never
|
|
40
|
+
// `silent`.
|
|
41
|
+
export function editBody(message, text, markdown) {
|
|
42
|
+
const ref = (o) => (o ? {channelId: o.channelId, messageId: o.messageId} : undefined);
|
|
43
|
+
const body = {
|
|
44
|
+
text,
|
|
45
|
+
mentions: message.mentions ?? [],
|
|
46
|
+
attachments: (message.attachments ?? []).map(attachmentOf),
|
|
47
|
+
elements: message.elements,
|
|
48
|
+
quote: ref(message.quote),
|
|
49
|
+
forward: ref(message.forward),
|
|
50
|
+
markdown: markdown ? true : message.markdown,
|
|
51
|
+
};
|
|
52
|
+
return Object.fromEntries(
|
|
53
|
+
Object.entries(body).filter(([, v]) => v !== undefined && v !== null && !(Array.isArray(v) && !v.length)),
|
|
54
|
+
);
|
|
19
55
|
}
|
|
20
56
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
57
|
+
const sentText = (verb) => (r, ctx) => {
|
|
58
|
+
const m = r?.message;
|
|
59
|
+
return m ? `${verb} ${m.messageId}${m.createdAt ? ` at ${localTime(m.createdAt)}` : ''}` : verb;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const list = {
|
|
63
|
+
summary: 'your channels, newest activity first',
|
|
64
|
+
usage: 'wx chat list [--filter unread|archived] [--section ID] [--cursor C]',
|
|
65
|
+
options: {
|
|
66
|
+
filter: {type: 'string', description: 'only unread or archived channels', choices: ['unread', 'archived']},
|
|
67
|
+
section: {type: 'string', description: 'only the channels of a section'},
|
|
68
|
+
cursor: {type: 'string', description: 'the next page (nextCursor of the previous one)'},
|
|
69
|
+
},
|
|
70
|
+
args: [0, 0],
|
|
71
|
+
async run(ctx) {
|
|
72
|
+
const {values} = ctx;
|
|
73
|
+
const client = api(ctx);
|
|
74
|
+
const r = await client.call(
|
|
75
|
+
'GET',
|
|
76
|
+
`/channels${query({filter: values.filter, section: values.section, cursor: values.cursor})}`,
|
|
77
|
+
);
|
|
78
|
+
return withNames(ctx, r, (r?.channels ?? []).flatMap(channelUserIds), client);
|
|
79
|
+
},
|
|
80
|
+
text(r, ctx) {
|
|
81
|
+
if (!r?.channels?.length) return 'No channels.';
|
|
82
|
+
return [...channelLines(r.channels, r, ctx), ...nextPage(r.nextCursor, paint(ctx))];
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const read = {
|
|
87
|
+
summary: 'messages of a channel, newest first, 30 per page',
|
|
88
|
+
usage: 'wx chat read <channel> [--unread] [--from SEQ] [--to SEQ]',
|
|
89
|
+
details: [
|
|
90
|
+
'A channel is an id or an inbox link. --from and --to are inclusive timelineSeq bounds: for the next older page,',
|
|
91
|
+
'pass the smallest timelineSeq of the page minus 1 as --to. The text output shows the page oldest first.',
|
|
92
|
+
].join('\n'),
|
|
93
|
+
options: {
|
|
94
|
+
unread: {type: 'boolean', description: 'start at the first unread message'},
|
|
95
|
+
from: {type: 'string', description: 'from this sequence number'},
|
|
96
|
+
to: {type: 'string', description: 'up to this sequence number'},
|
|
97
|
+
},
|
|
98
|
+
args: [1, 1],
|
|
99
|
+
async run(ctx) {
|
|
100
|
+
const {values, positionals} = ctx;
|
|
101
|
+
const ch = channelId(positionals[0]);
|
|
102
|
+
const q = query({from: seq('from', values.from), to: seq('to', values.to), anchor: values.unread && 'firstUnread'});
|
|
103
|
+
const client = api(ctx);
|
|
104
|
+
const r = await client.call('GET', apiPath`/channels/${ch}/messages` + q);
|
|
105
|
+
return withNames(ctx, r, (r?.messages ?? []).flatMap(messageUserIds), client);
|
|
106
|
+
},
|
|
107
|
+
text(r, ctx) {
|
|
108
|
+
const messages = r?.messages ?? [];
|
|
109
|
+
if (!messages.length) return 'No messages.';
|
|
110
|
+
const lines = messageLines(messages, r, ctx);
|
|
111
|
+
const oldest = Math.min(...messages.map((m) => m.timelineSeq ?? Infinity));
|
|
112
|
+
if (messages.length >= 30 && Number.isFinite(oldest) && oldest > 0)
|
|
113
|
+
lines.push(paint(ctx).dim(`Older: --to ${oldest - 1}`));
|
|
114
|
+
return lines;
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const send = {
|
|
119
|
+
summary: 'send a message to a channel ("-" reads the text from stdin)',
|
|
120
|
+
usage: 'wx chat send <channel> <text|-> [--markdown] [--reply-to MESSAGE] [--mention USERID]... [--file PATH]...',
|
|
121
|
+
details:
|
|
122
|
+
'Sends as the logged-in user. --file uploads the file and attaches it (files in credential folders are refused).',
|
|
123
|
+
options: {
|
|
124
|
+
markdown: {type: 'boolean', description: 'the text is markdown'},
|
|
125
|
+
'reply-to': {type: 'string', description: 'quote this message (id or inbox link)'},
|
|
126
|
+
mention: {type: 'string', multiple: true, placeholder: 'USERID', description: 'mention this user'},
|
|
127
|
+
file: {type: 'string', multiple: true, placeholder: 'PATH', description: 'attach this file'},
|
|
128
|
+
},
|
|
129
|
+
args: [2, 2],
|
|
130
|
+
async run(ctx) {
|
|
131
|
+
const {values, positionals, stdin} = ctx;
|
|
132
|
+
const ch = channelId(positionals[0]);
|
|
133
|
+
const text = await readText(positionals[1], stdin);
|
|
134
|
+
const files = values.file ?? [];
|
|
135
|
+
if (!text.trim() && !files.length) throw new UsageError('Nothing to send: the text is empty');
|
|
136
|
+
const body = {};
|
|
137
|
+
if (text.trim()) body.text = text;
|
|
138
|
+
if (values.markdown) body.markdown = true;
|
|
139
|
+
if (values['reply-to']) body.quote = {channelId: ch, messageId: messageId(values['reply-to'])};
|
|
140
|
+
if (values.mention?.length) body.mentions = values.mention.map((u) => checkId('--mention', u));
|
|
141
|
+
const client = api(ctx);
|
|
142
|
+
if (files.length) {
|
|
143
|
+
body.attachments = [];
|
|
144
|
+
for (const f of files) body.attachments.push(await uploadFile(ctx, ch, f, client));
|
|
145
|
+
}
|
|
146
|
+
return client.call('POST', apiPath`/channels/${ch}/messages`, body);
|
|
147
|
+
},
|
|
148
|
+
text: sentText('Sent'),
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const get = {
|
|
152
|
+
summary: 'one message',
|
|
153
|
+
usage: 'wx chat get <channel> <message>',
|
|
154
|
+
details: 'A message is an id or an inbox link to it. Who reacted: wx chat reactions.',
|
|
155
|
+
options: {},
|
|
156
|
+
args: [2, 2],
|
|
157
|
+
async run(ctx) {
|
|
158
|
+
const {ch, msg} = target(ctx.positionals);
|
|
159
|
+
const client = api(ctx);
|
|
160
|
+
const r = await client.call('GET', apiPath`/channels/${ch}/messages/${msg}`);
|
|
161
|
+
return withNames(ctx, r, [...messageUserIds(r?.message), ...(r?.message?.mentions ?? [])], client);
|
|
162
|
+
},
|
|
163
|
+
text(r, ctx) {
|
|
164
|
+
const m = r?.message;
|
|
165
|
+
if (!m) return 'No message.';
|
|
166
|
+
const c = paint(ctx);
|
|
167
|
+
const rows = [
|
|
168
|
+
['Message', `${m.messageId} (#${m.timelineSeq})`],
|
|
169
|
+
['From', c.bold(nameIn(r, m.userId))],
|
|
170
|
+
['At', `${localTime(m.createdAt)}${m.editedAt ? `, edited ${localTime(m.editedAt)}` : ''}`],
|
|
171
|
+
];
|
|
172
|
+
if (m.quote) rows.push(['Reply to', `${nameIn(r, m.quote.userId)}: ${oneLine(m.quote.text ?? '', 100)}`]);
|
|
173
|
+
if (m.mentions?.length) rows.push(['Mentions', m.mentions.map((id) => nameIn(r, id)).join(', ')]);
|
|
174
|
+
for (const f of m.attachments ?? [])
|
|
175
|
+
rows.push(['File', `${f.name} (${f.mime ?? '?'}, ${f.size ?? '?'} bytes) fsId ${f.fsId}`]);
|
|
176
|
+
const counts = Object.entries(m.reactionCounts ?? {})
|
|
177
|
+
.map(([t, n]) => `${t} ${n}`)
|
|
178
|
+
.join(', ');
|
|
179
|
+
if (counts) rows.push(['Reactions', counts]);
|
|
180
|
+
if (m.pinned) rows.push(['Pinned', `${localTime(m.pinnedAt)}${m.pinnedBy ? ` by ${nameIn(r, m.pinnedBy)}` : ''}`]);
|
|
181
|
+
const body = m.messageType === 'deleted' || m.deletedAt ? ['(deleted)'] : String(m.text ?? '').split('\n');
|
|
182
|
+
return [...table(rows), '', ...body];
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const edit = {
|
|
187
|
+
summary: 'edit your own message ("-" reads the new text from stdin)',
|
|
188
|
+
usage: 'wx chat edit <channel> <message> <text|-> [--markdown]',
|
|
189
|
+
details: [
|
|
190
|
+
'Only your own messages (else exit 2, nothing changed). The full update (PUT) is used, since the partial one',
|
|
191
|
+
'(PartialUpdateMessage) is not implemented by the service yet: the new text is sent with what the message has',
|
|
192
|
+
'(attachments, quote, mentions). --markdown is accepted, but the service does not store it on an edit yet.',
|
|
193
|
+
].join('\n'),
|
|
194
|
+
options: {markdown: {type: 'boolean', description: 'the new text is markdown'}},
|
|
195
|
+
args: [3, 3],
|
|
196
|
+
async run(ctx) {
|
|
197
|
+
const {ch, msg} = target(ctx.positionals);
|
|
198
|
+
const text = await readText(ctx.positionals[2], ctx.stdin);
|
|
199
|
+
if (!text.trim()) throw new UsageError('The new text is empty');
|
|
200
|
+
const client = api(ctx);
|
|
201
|
+
const message = await ownMessage(ctx, client, ch, msg);
|
|
202
|
+
return client.call('PUT', apiPath`/channels/${ch}/messages/${msg}`, editBody(message, text, ctx.values.markdown));
|
|
203
|
+
},
|
|
204
|
+
text: sentText('Edited'),
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const del = {
|
|
208
|
+
summary: 'delete your own message, for everyone (needs --yes)',
|
|
209
|
+
usage: 'wx chat delete <channel> <message> --yes',
|
|
210
|
+
details:
|
|
211
|
+
'Only your own messages (else exit 2, nothing changed). Confirm with the person first: --yes is not the confirmation.',
|
|
212
|
+
options: {yes: {type: 'boolean', description: 'yes, delete it for everyone'}},
|
|
213
|
+
args: [2, 2],
|
|
214
|
+
async run(ctx) {
|
|
215
|
+
const {ch, msg} = target(ctx.positionals);
|
|
216
|
+
if (!ctx.values.yes)
|
|
217
|
+
throw new UsageError(
|
|
218
|
+
'delete removes the message for everyone: confirm with the person, then add --yes',
|
|
219
|
+
ctx.usage(),
|
|
220
|
+
);
|
|
221
|
+
const client = api(ctx);
|
|
222
|
+
await ownMessage(ctx, client, ch, msg);
|
|
223
|
+
return client.call('DELETE', apiPath`/channels/${ch}/messages/${msg}`);
|
|
224
|
+
},
|
|
225
|
+
text: done('Deleted'),
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const pinCmd = (pin) => ({
|
|
229
|
+
summary: pin ? 'pin a message in its channel (clients then show it as edited)' : 'unpin a message',
|
|
230
|
+
usage: `wx chat ${pin ? 'pin' : 'unpin'} <channel> <message>`,
|
|
231
|
+
options: {},
|
|
232
|
+
args: [2, 2],
|
|
233
|
+
async run(ctx) {
|
|
234
|
+
const {ch, msg} = target(ctx.positionals);
|
|
235
|
+
return api(ctx).call(pin ? 'PUT' : 'DELETE', apiPath`/channels/${ch}/messages/${msg}/pin`);
|
|
236
|
+
},
|
|
237
|
+
text: done(pin ? 'Pinned' : 'Unpinned'),
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const reactCmd = (add) => ({
|
|
241
|
+
summary: add ? 'react to a message (eyes, white_check_mark, thumbsup, heart…)' : 'take your reaction back',
|
|
242
|
+
usage: `wx chat ${add ? 'react' : 'unreact'} <channel> <message> <type>`,
|
|
243
|
+
details: 'A type is a name of [a-z0-9_+-] (up to 64). One reaction of each type per person; twice is harmless.',
|
|
244
|
+
options: {},
|
|
245
|
+
args: [3, 3],
|
|
246
|
+
async run(ctx) {
|
|
247
|
+
const {ch, msg} = target(ctx.positionals);
|
|
248
|
+
const type = checkReaction(ctx.positionals[2]);
|
|
249
|
+
return api(ctx).call(add ? 'PUT' : 'DELETE', apiPath`/channels/${ch}/messages/${msg}/reactions/${type}`);
|
|
250
|
+
},
|
|
251
|
+
text: (r, ctx) => (add ? `Reacted with ${ctx.positionals[2]}` : `Removed ${ctx.positionals[2]}`),
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const reactions = {
|
|
255
|
+
summary: 'who reacted to a message: one entry per person and type',
|
|
256
|
+
usage: 'wx chat reactions <channel> <message> [--type TYPE] [--users]',
|
|
257
|
+
details:
|
|
258
|
+
'The JSON gives user ids; --users adds the name, e-mail and bot of each (the text output always shows names).',
|
|
259
|
+
options: {
|
|
260
|
+
type: {type: 'string', description: 'only this reaction type'},
|
|
261
|
+
users: {type: 'boolean', description: 'add the name, e-mail and bot of each person (JSON)'},
|
|
262
|
+
},
|
|
263
|
+
args: [2, 2],
|
|
264
|
+
async run(ctx) {
|
|
265
|
+
const {ch, msg} = target(ctx.positionals);
|
|
266
|
+
const body = {messageIds: [msg]};
|
|
267
|
+
if (ctx.values.type !== undefined) body.type = checkReaction(ctx.values.type);
|
|
268
|
+
const client = api(ctx);
|
|
269
|
+
const r = await client.call('POST', apiPath`/channels/${ch}/reactions/query`, body);
|
|
270
|
+
const ids = [...new Set((r?.reactions ?? []).map((x) => x.userId))];
|
|
271
|
+
if (ctx.values.users && ids.length) {
|
|
272
|
+
const u = await client.call('POST', '/users/query', {query: {ids}});
|
|
273
|
+
const byId = new Map((u?.users ?? []).map((p) => [p.id, p]));
|
|
274
|
+
r.reactions = r.reactions.map((x) => {
|
|
275
|
+
const p = byId.get(x.userId);
|
|
276
|
+
return {...x, user: p ? {name: p.displayName ?? p.name, email: p.email, bot: p.bot} : null};
|
|
277
|
+
});
|
|
278
|
+
return r;
|
|
279
|
+
}
|
|
280
|
+
return withNames(ctx, r, ids, client);
|
|
281
|
+
},
|
|
282
|
+
text(r) {
|
|
283
|
+
const list = r?.reactions ?? [];
|
|
284
|
+
if (!list.length) return 'No reactions.';
|
|
285
|
+
const byType = new Map();
|
|
286
|
+
for (const x of list) byType.set(x.type, [...(byType.get(x.type) ?? []), x.user?.name ?? nameIn(r, x.userId)]);
|
|
287
|
+
return table([...byType].map(([type, people]) => [type, String(people.length), people.join(', ')]));
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const mentions = {
|
|
292
|
+
summary: 'messages that mention you',
|
|
293
|
+
usage: 'wx chat mentions [--unread] [--cursor C]',
|
|
294
|
+
options: {
|
|
295
|
+
unread: {type: 'boolean', description: 'only the unread ones'},
|
|
296
|
+
cursor: {type: 'string', description: 'the next page (nextCursor of the previous one)'},
|
|
297
|
+
},
|
|
298
|
+
args: [0, 0],
|
|
299
|
+
async run(ctx) {
|
|
300
|
+
const client = api(ctx);
|
|
301
|
+
const r = await client.call(
|
|
302
|
+
'GET',
|
|
303
|
+
`/mentions${query({filter: ctx.values.unread && 'unread', cursor: ctx.values.cursor})}`,
|
|
304
|
+
);
|
|
305
|
+
return withNames(
|
|
306
|
+
ctx,
|
|
307
|
+
r,
|
|
308
|
+
(r?.mentions ?? []).flatMap((x) => [...messageUserIds(x.message), x.channel?.peerId]),
|
|
309
|
+
client,
|
|
310
|
+
);
|
|
311
|
+
},
|
|
312
|
+
text(r, ctx) {
|
|
313
|
+
const c = paint(ctx);
|
|
314
|
+
const list = r?.mentions ?? [];
|
|
315
|
+
if (!list.length) return 'No mentions.';
|
|
316
|
+
const rows = list.map((x) => [
|
|
317
|
+
c.dim(localTime(x.message?.createdAt ?? x.mentionAddedAt)),
|
|
318
|
+
x.read ? '' : c.yellow('new'),
|
|
319
|
+
c.bold(x.channel?.subject || (x.channel?.peerId ? nameIn(r, x.channel.peerId) : x.channelId)),
|
|
320
|
+
`${nameIn(r, x.message?.userId)}: ${messageText(x.message, r, {max: 300})}`,
|
|
321
|
+
c.dim(`${x.channelId} ${x.messageId}`),
|
|
322
|
+
]);
|
|
323
|
+
return [...table(rows), ...nextPage(r.nextCursor, c)];
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const SORT = ['relevance', 'recency', 'recency_asc'];
|
|
328
|
+
|
|
329
|
+
const search = {
|
|
330
|
+
summary: 'search messages in your channels',
|
|
331
|
+
usage:
|
|
332
|
+
'wx chat search <query> [--channel C] [--sender USERID]... [--attachments] [--links] [--size 1-50] [--cursor C] [--sort relevance|recency|recency_asc]',
|
|
333
|
+
options: {
|
|
334
|
+
channel: {type: 'string', description: 'only in this channel (id or inbox link)'},
|
|
335
|
+
sender: {type: 'string', multiple: true, placeholder: 'USERID', description: 'only messages of this person'},
|
|
336
|
+
attachments: {type: 'boolean', description: 'only messages with files'},
|
|
337
|
+
links: {type: 'boolean', description: 'only messages with links'},
|
|
338
|
+
size: {type: 'string', placeholder: '1-50', description: 'results per page (default 20)'},
|
|
339
|
+
cursor: {type: 'string', description: 'the next page (nextCursor of the previous one)'},
|
|
340
|
+
sort: {type: 'string', choices: SORT, description: 'the order of the results'},
|
|
341
|
+
},
|
|
342
|
+
args: [1, 1],
|
|
343
|
+
async run(ctx) {
|
|
344
|
+
const {values, positionals} = ctx;
|
|
345
|
+
const q = query({
|
|
346
|
+
q: positionals[0],
|
|
347
|
+
channelId: values.channel === undefined ? undefined : channelId(values.channel),
|
|
348
|
+
hasAttachments: values.attachments && 'true',
|
|
349
|
+
hasLinks: values.links && 'true',
|
|
350
|
+
size: checkInt('size', values.size, 1, 50),
|
|
351
|
+
cursor: values.cursor,
|
|
352
|
+
sort: values.sort,
|
|
353
|
+
senderIds: (values.sender ?? []).map((s) => checkId('--sender', s)),
|
|
354
|
+
});
|
|
355
|
+
const client = api(ctx);
|
|
356
|
+
const r = await client.call('GET', `/search/messages${q}`);
|
|
357
|
+
return withNames(
|
|
358
|
+
ctx,
|
|
359
|
+
r,
|
|
360
|
+
(r?.results ?? []).flatMap((x) => messageUserIds(x.message)),
|
|
361
|
+
client,
|
|
362
|
+
);
|
|
363
|
+
},
|
|
364
|
+
text(r, ctx) {
|
|
365
|
+
const c = paint(ctx);
|
|
366
|
+
const list = r?.results ?? [];
|
|
367
|
+
if (!list.length) return 'Nothing found.';
|
|
368
|
+
const rows = list.map((x) => [
|
|
369
|
+
c.dim(localTime(x.message?.createdAt)),
|
|
370
|
+
c.bold(nameIn(r, x.message?.userId)),
|
|
371
|
+
messageText(x.message, r, {max: 300}),
|
|
372
|
+
c.dim(`${x.channelId} ${x.message?.messageId ?? ''}`),
|
|
373
|
+
]);
|
|
374
|
+
const total = r.totalCount !== undefined ? [c.dim(`${list.length} of ${r.totalCount}`)] : [];
|
|
375
|
+
return [...table(rows), ...total, ...nextPage(r.nextCursor, c)];
|
|
376
|
+
},
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const watchCmd = {
|
|
380
|
+
summary: 'follow your chats live: messages, reactions, reads, members, channel changes',
|
|
381
|
+
usage:
|
|
382
|
+
'wx chat watch [--channel C]... [--from USERID]... [--type TYPE]... [--mine] [--count N] [--for SECONDS] [--raw]',
|
|
383
|
+
details: [
|
|
384
|
+
'Read only: nothing is sent and nothing is marked read. One line per event (one JSON object per line with',
|
|
385
|
+
'--output json): READY when listening, then message.new, reaction.new, read.receipt.updated, member.added,',
|
|
386
|
+
'channel.updated, typing.start…; RECONNECTING / RECONNECTED while the connection is renewed, END at the end.',
|
|
387
|
+
'Types: the groups messages, reactions, reads, members, channels, typing, or one event. By default your own',
|
|
388
|
+
'messages and reading are left out (--mine keeps them). A reaction event does not say who reacted (see',
|
|
389
|
+
'wx chat reactions), so --from leaves reactions out. Delivery is best effort: after RECONNECTED, read the',
|
|
390
|
+
'channel when nothing may be missed. Waiting for one answer:',
|
|
391
|
+
' wx chat watch --channel C --from USERID --type message.new --count 1 --for 3600',
|
|
392
|
+
].join('\n'),
|
|
393
|
+
options: {
|
|
394
|
+
channel: {type: 'string', multiple: true, placeholder: 'C', description: 'only this channel (id or inbox link)'},
|
|
395
|
+
from: {
|
|
396
|
+
type: 'string',
|
|
397
|
+
multiple: true,
|
|
398
|
+
placeholder: 'USERID',
|
|
399
|
+
description: 'only what this person did (messages, reading, typing)',
|
|
52
400
|
},
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
'reply-to': {type: 'string', description: 'quote this message (id or inbox link)'},
|
|
59
|
-
},
|
|
60
|
-
args: [2, 2],
|
|
61
|
-
async run({values, positionals, client, stdin}) {
|
|
62
|
-
const ch = channelId(positionals[0]);
|
|
63
|
-
const text = await readText(positionals[1], stdin);
|
|
64
|
-
if (!text.trim()) throw new UsageError('Nothing to send: the text is empty');
|
|
65
|
-
const body = {text};
|
|
66
|
-
if (values.markdown) body.markdown = true;
|
|
67
|
-
if (values['reply-to']) body.quote = {channelId: ch, messageId: messageId(values['reply-to'])};
|
|
68
|
-
return client(streamClient).call('POST', apiPath`/channels/${ch}/messages`, body);
|
|
69
|
-
},
|
|
401
|
+
type: {
|
|
402
|
+
type: 'string',
|
|
403
|
+
multiple: true,
|
|
404
|
+
choices: EVENT_CHOICES,
|
|
405
|
+
description: 'only these events: a group or one event',
|
|
70
406
|
},
|
|
407
|
+
mine: {type: 'boolean', description: 'also what you did yourself'},
|
|
408
|
+
count: {type: 'string', placeholder: 'N', description: 'stop after N events (1-100000)'},
|
|
409
|
+
for: {type: 'string', placeholder: 'SECONDS', description: 'stop after this long (1-604800)'},
|
|
410
|
+
raw: {type: 'boolean', description: 'each event as the service sends it'},
|
|
411
|
+
},
|
|
412
|
+
args: [0, 0],
|
|
413
|
+
async run(ctx) {
|
|
414
|
+
const {values} = ctx;
|
|
415
|
+
const types = new Set((values.type ?? []).flatMap((t) => EVENT_GROUPS[t] ?? [t]));
|
|
416
|
+
await watch(ctx, {
|
|
417
|
+
channels: new Set((values.channel ?? []).map(channelId)),
|
|
418
|
+
senders: new Set((values.from ?? []).map((u) => checkId('--from', u))),
|
|
419
|
+
types,
|
|
420
|
+
mine: Boolean(values.mine),
|
|
421
|
+
raw: Boolean(values.raw),
|
|
422
|
+
count: checkInt('count', values.count, 1, 100_000) ?? 0,
|
|
423
|
+
seconds: checkInt('for', values.for, 1, 604_800) ?? 0,
|
|
424
|
+
});
|
|
425
|
+
},
|
|
426
|
+
textEvent(e, ctx) {
|
|
427
|
+
if (ctx.values.raw && !['READY', 'RECONNECTING', 'RECONNECTED', 'END'].includes(e.event))
|
|
428
|
+
return `${e.event} ${JSON.stringify(e)}`;
|
|
429
|
+
return eventText(e, ctx);
|
|
71
430
|
},
|
|
72
431
|
};
|
|
432
|
+
|
|
433
|
+
const chat = {
|
|
434
|
+
summary: 'chats: channels, messages, reactions, mentions, search, the live feed',
|
|
435
|
+
auth: true,
|
|
436
|
+
commands: {
|
|
437
|
+
list,
|
|
438
|
+
read,
|
|
439
|
+
send,
|
|
440
|
+
get,
|
|
441
|
+
edit,
|
|
442
|
+
delete: del,
|
|
443
|
+
pin: pinCmd(true),
|
|
444
|
+
unpin: pinCmd(false),
|
|
445
|
+
react: reactCmd(true),
|
|
446
|
+
unreact: reactCmd(false),
|
|
447
|
+
reactions,
|
|
448
|
+
mentions,
|
|
449
|
+
search,
|
|
450
|
+
watch: watchCmd,
|
|
451
|
+
},
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
export const groups = {chat};
|
|
455
|
+
export default chat;
|