diff options
| author | Elvis Claros Castro <elvis@claros.ar> | 2026-09-26 19:26:33 -0300 |
|---|---|---|
| committer | Elvis Claros Castro <elvis@claros.ar> | 2026-09-26 19:26:33 -0300 |
| commit | d80640d01eeb84907e36ff4af33add9081eda17c (patch) | |
| tree | 27698b34e06e28d111c2196700015d9c82f78274 /src/helpers.js | |
| download | wa-cli-d80640d01eeb84907e36ff4af33add9081eda17c.tar.gz wa-cli-d80640d01eeb84907e36ff4af33add9081eda17c.zip | |
Diffstat (limited to 'src/helpers.js')
| -rw-r--r-- | src/helpers.js | 165 |
1 files changed, 165 insertions, 0 deletions
diff --git a/src/helpers.js b/src/helpers.js new file mode 100644 index 0000000..e902768 --- /dev/null +++ b/src/helpers.js @@ -0,0 +1,165 @@ +// `wa +something` helpers: shortcuts for interactive use on top of the generic API +// (like `gws gmail +send`). Everything they do can be done with `wa <resource> <method>`. + +import { getMethod, validate } from './api/index.js'; +import { print, readStdin } from './output.js'; +import { WaError } from './rpc.js'; + +const cwd = process.cwd(); +const need = (v, what) => { + if (!v) throw new WaError('EINVAL', `missing ${what}`); + return v; +}; + +// Text from the arguments, or stdin if it is "-" or there is no text and stdin is not a terminal. +function bodyFrom(words, { allowEmpty }) { + const text = words.join(' '); + if (text === '-' || (!text && !allowEmpty && !process.stdin.isTTY)) return readStdin(); + return text || undefined; +} + +async function callAndPrint(session, method, params, values, defaultFormat = 'json') { + const p = validate(method, params, { cwd }); + const result = await session.call(method, p); + print(result, values.format || defaultFormat, { columns: getMethod(method).columns, output: values.output }); + return 0; +} + +const contentOptions = { + file: { type: 'string', short: 'f', description: 'Attach a file (the text becomes its caption)' }, + document: { type: 'boolean', description: 'Send the file as a document' }, + voice: { type: 'boolean', description: 'Send audio as a voice note' }, +}; + +export const HELPERS = { + '+send': { + summary: 'Send a message or a file', + usage: 'wa +send <chat> [text...] [--file PATH] [--reply-to MSG_ID]', + details: '<chat> can be a JID, a phone number (+54 9 11 ...) or the exact chat name.\n' + + 'Without text (or with "-") it reads the text from stdin.', + examples: [ + 'wa +send "Mom" there in 10', + 'wa +send +5491122334455 --file report.pdf "here is the report"', + 'fortune | wa +send 5491122334455@c.us', + ], + options: { ...contentOptions, 'reply-to': { type: 'string', short: 'r', description: 'ID of the message to quote' } }, + run: (session, [chat, ...words], v) => callAndPrint(session, 'messages.send', { + chatId: need(chat, 'the target chat'), + body: bodyFrom(words, { allowEmpty: Boolean(v.file) }), + media: v.file, + quotedMessageId: v['reply-to'], + asDocument: v.document, + asVoice: v.voice, + }, v), + }, + + '+reply': { + summary: 'Reply quoting a message', + usage: 'wa +reply <messageId> [text...] [--file PATH]', + examples: ['wa +reply "false_5491122334455@c.us_3EB0C4..." sure, see you'], + options: contentOptions, + run: (session, [id, ...words], v) => callAndPrint(session, 'messages.reply', { + messageId: need(id, 'the message ID'), + body: bodyFrom(words, { allowEmpty: Boolean(v.file) }), + media: v.file, + asDocument: v.document, + asVoice: v.voice, + }, v), + }, + + '+read': { + summary: 'Read a conversation', + usage: 'wa +read <chat> [-n N] [--mark-read]', + examples: ['wa +read "Family" -n 30', 'wa +read 5491122334455 --format json | jq .[].body'], + options: { + limit: { type: 'string', short: 'n', description: 'Number of messages (default 30)' }, + 'mark-read': { type: 'boolean', description: 'Mark the chat as read' }, + }, + run: async (session, [chat], v) => { + const chatId = need(chat, 'the chat'); + await callAndPrint(session, 'messages.list', { chatId, limit: v.limit ?? 30 }, v, 'text'); + if (v['mark-read']) await session.call('chats.read', validate('chats.read', { chatId })); + return 0; + }, + }, + + '+triage': { + summary: 'Summary of chats with unread messages', + usage: 'wa +triage [-n N]', + examples: ['wa +triage', 'wa +triage --format json'], + options: { limit: { type: 'string', short: 'n', description: 'Maximum number of chats' } }, + run: async (session, _pos, v) => { + const p = validate('chats.list', { unread: true, archived: false, limit: v.limit }, { cwd }); + const result = await session.call('chats.list', p); + print(result, v.format || 'table', { + columns: ['name', 'unreadCount', 'date', 'lastMessage.body', 'id'], + output: v.output, + }); + return 0; + }, + }, + + '+watch': { + summary: 'Stream of new messages (NDJSON)', + usage: 'wa +watch [--events LIST] [--chat CHAT]', + details: 'Default: the "message" event (incoming). "message_create" includes sent ones; "all" everything.\n' + + 'With --format text it prints one readable line per message. Ctrl-C to stop.', + examples: ['wa +watch | jq -r .data.body', 'wa +watch --events message_create --format text', 'wa +watch --chat "Family"'], + options: { + events: { type: 'string', short: 'e', description: 'Comma-separated events' }, + chat: { type: 'string', short: 'c', description: 'Only events from this chat' }, + }, + run: async (session, _pos, v) => { + const chatId = v.chat ? (await session.call('chats.resolve', { query: v.chat })).id : null; + const p = validate('events.watch', { events: v.events }, { cwd }); + const fmt = v.format || 'ndjson'; + await session.call('events.watch', p, { + onEvent: (ev) => { + const d = ev.data || {}; + const evChat = d.chatId ?? d.message?.chatId ?? d.chat?.id; + if (chatId && evChat !== chatId) return; + print(ev, fmt === 'text' ? 'text' : fmt === 'json' ? 'ndjson' : fmt); + }, + }); + return 0; + }, + }, + + '+download': { + summary: 'Download the media of a message', + usage: 'wa +download <messageId> [-d DIR | -o FILE]', + examples: ['wa +download "false_...@c.us_3EB0..." -d ~/Downloads'], + options: { dir: { type: 'string', short: 'd', description: 'Target directory' } }, + run: async (session, [id], v) => { + const p = validate('messages.download', { messageId: need(id, 'the message ID'), dir: v.dir, output: v.output }, { cwd }); + print(await session.call('messages.download', p), v.format || 'json'); + return 0; + }, + }, + + '+export': { + summary: 'Export a chat as mbox (for neomutt)', + usage: 'wa +export <chat> [-n N] [--since DATE] [--media] [-o FILE]', + details: 'mboxrd output: each message is an email (From = sender, Message-ID/In-Reply-To\n' + + 'build threads from quoted replies). --media includes attachments (slower).', + examples: ['wa +export "Family" -n 500 --media -o family.mbox && neomutt -R -f family.mbox'], + options: { + limit: { type: 'string', short: 'n', description: 'Number of messages (default 200)' }, + since: { type: 'string', description: 'Since a date (ISO 8601, e.g. 2026-09-01) or epoch s' }, + media: { type: 'boolean', description: 'Download and include attachments' }, + }, + run: async (session, [chat], v) => { + let since; + if (v.since) { + since = /^\d+$/.test(v.since) ? Number(v.since) : Math.floor(new Date(v.since).getTime() / 1000); + if (Number.isNaN(since)) throw new WaError('EINVAL', `--since: invalid date: ${v.since}`); + } + return callAndPrint(session, 'messages.list', { + chatId: need(chat, 'the chat'), + limit: v.limit ?? 200, + since, + includeMedia: Boolean(v.media), + }, v, 'mbox'); + }, + }, +}; |