cli-whatsapp 0.1.8 → 0.1.12
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.
|
@@ -210,7 +210,7 @@ export function pickFromList(title, allItems, initialFilter = '') {
|
|
|
210
210
|
return void ((index = Math.min(items.length - 1, index + 1)), render());
|
|
211
211
|
if (key.name === 'return') {
|
|
212
212
|
const it = items[index];
|
|
213
|
-
return done(it ? it.value : null);
|
|
213
|
+
return done(it ? (it.id ?? it.value) : null);
|
|
214
214
|
}
|
|
215
215
|
if (key.name === 'backspace') {
|
|
216
216
|
if (!filter)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import readline, { createInterface } from 'node:readline';
|
|
2
2
|
import { readdirSync, statSync, existsSync } from 'node:fs';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
4
|
+
import { join, basename } from 'node:path';
|
|
5
5
|
import { chats, contacts, search, history } from "./read.js";
|
|
6
6
|
import { reply, buildMediaContent, sendContent } from "./send.js";
|
|
7
7
|
import { media, downloadWithSocket } from "./media.js";
|
|
@@ -11,10 +11,10 @@ import { recent, pin, unpin, archive, unarchive } from "./manage.js";
|
|
|
11
11
|
import { status } from "./auth.js";
|
|
12
12
|
import { connect, hasSession } from "../../whatsapp/client.js";
|
|
13
13
|
import { registerPersistence, extractText, isGroupJid, messageType } from "../../whatsapp/events.js";
|
|
14
|
-
import { listChats, listContacts, findChat, upsertChat, getMessages, getMessageById, localIdForWaMessage, countMessages, } from "../../db/sqlite.js";
|
|
14
|
+
import { listChats, listContacts, listMedia, findChat, upsertChat, getMessages, getMessageById, localIdForWaMessage, countMessages, } from "../../db/sqlite.js";
|
|
15
15
|
import { resolveTarget } from "../../utils/jid.js";
|
|
16
16
|
import { loadConfig, saveConfig } from "../../config/index.js";
|
|
17
|
-
import { c, displayName, chatNumber, formatTime, truncate, renderMessages } from "../../utils/format.js";
|
|
17
|
+
import { c, displayName, chatNumber, cleanSender, formatTime, truncate, renderMessages } from "../../utils/format.js";
|
|
18
18
|
import { print, warn, printError } from "../../utils/ui.js";
|
|
19
19
|
import { notify } from "../../utils/notify.js";
|
|
20
20
|
// ---- tab completion --------------------------------------------------------
|
|
@@ -29,7 +29,8 @@ const SLASH_ITEMS = [
|
|
|
29
29
|
{ value: '/send', desc: 'send a message to a chat', args: true },
|
|
30
30
|
{ value: '/sendfile', desc: 'send a file (media)', args: true },
|
|
31
31
|
{ value: '/reply', desc: 'reply to a message by #id', args: true },
|
|
32
|
-
{ value: '/media', desc: 'list cached media' },
|
|
32
|
+
{ value: '/media', desc: 'list cached media (this chat, or all)' },
|
|
33
|
+
{ value: '/dl', desc: 'pick recent media to download & open' },
|
|
33
34
|
{ value: '/pin', desc: 'pin a chat to the top', args: true },
|
|
34
35
|
{ value: '/unpin', desc: 'unpin a chat', args: true },
|
|
35
36
|
{ value: '/archive', desc: 'archive (hide) a chat', args: true },
|
|
@@ -263,7 +264,20 @@ async function sendTo(state, jid, name, content, meta, replaceInput = false) {
|
|
|
263
264
|
return;
|
|
264
265
|
try {
|
|
265
266
|
await sendContent(conn.sock, jid, content, meta);
|
|
266
|
-
|
|
267
|
+
// Build the echo: text → the message; media → "[type] filename" plus any
|
|
268
|
+
// caption (never "[image] [image]"). The filename tells you which file.
|
|
269
|
+
let echo;
|
|
270
|
+
if (meta.type === 'text') {
|
|
271
|
+
echo = meta.previewText;
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
const fname = meta.mediaPath ? basename(meta.mediaPath) : '';
|
|
275
|
+
const caption = meta.previewText.startsWith('[') ? '' : meta.previewText;
|
|
276
|
+
echo =
|
|
277
|
+
`${c.dim(`[${meta.type}]`)}` +
|
|
278
|
+
(fname ? ` ${fname}` : '') +
|
|
279
|
+
(caption ? ` ${c.dim('—')} ${caption}` : '');
|
|
280
|
+
}
|
|
267
281
|
// Replace the just-typed input with the bubble so it shows once — but only
|
|
268
282
|
// when we were already live (no "connecting…" lines were printed in between).
|
|
269
283
|
if (replaceInput && wasLive)
|
|
@@ -301,6 +315,34 @@ async function downloadAndOpen(state, idRaw) {
|
|
|
301
315
|
warn(`Download failed: ${err.message}`);
|
|
302
316
|
}
|
|
303
317
|
}
|
|
318
|
+
/** `/dl` with no id: pick from recent, not-yet-downloaded media (this chat if
|
|
319
|
+
* open, else all), newest first — arrow-select, no id to type. */
|
|
320
|
+
async function pickMediaToDownload(state) {
|
|
321
|
+
const rows = listMedia(null, 100, state.active?.jid, true); // undownloaded only
|
|
322
|
+
if (!rows.length) {
|
|
323
|
+
warn(state.active ? 'No new media in this chat to download.' : 'No new media to download.');
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const time24h = loadConfig().preferences.time24h;
|
|
327
|
+
const items = rows.map((m) => {
|
|
328
|
+
const label = m.text
|
|
329
|
+
? truncate(m.text.replace(/\s+/g, ' '), 32)
|
|
330
|
+
: m.mediaPath
|
|
331
|
+
? basename(m.mediaPath)
|
|
332
|
+
: '';
|
|
333
|
+
return {
|
|
334
|
+
value: `[${m.type}]${label ? ' ' + label : ''}`,
|
|
335
|
+
desc: `${m.fromMe ? 'you' : cleanSender(m.sender)} · ${formatTime(m.timestamp, time24h)}`,
|
|
336
|
+
id: String(m.id),
|
|
337
|
+
};
|
|
338
|
+
});
|
|
339
|
+
const title = state.active
|
|
340
|
+
? `Media with ${displayName(state.active)} — ⏎ download & open`
|
|
341
|
+
: 'All media — ⏎ download & open';
|
|
342
|
+
const picked = await pickFromList(title, items, '');
|
|
343
|
+
if (picked)
|
|
344
|
+
await downloadAndOpen(state, picked);
|
|
345
|
+
}
|
|
304
346
|
// ---- chat context ----------------------------------------------------------
|
|
305
347
|
function openChat(state, target) {
|
|
306
348
|
// Resolve against chats, contacts, or a raw number/jid.
|
|
@@ -477,8 +519,10 @@ async function handle(state, raw) {
|
|
|
477
519
|
case 'download':
|
|
478
520
|
case 'dl':
|
|
479
521
|
case 'open-media':
|
|
480
|
-
if (
|
|
522
|
+
if (args[0])
|
|
481
523
|
await downloadAndOpen(state, args[0]);
|
|
524
|
+
else
|
|
525
|
+
await pickMediaToDownload(state); // no id → pick from recent media
|
|
482
526
|
return;
|
|
483
527
|
case 'status':
|
|
484
528
|
status();
|
package/dist/db/sqlite.js
CHANGED
|
@@ -249,7 +249,7 @@ export function importLidMappings() {
|
|
|
249
249
|
return n;
|
|
250
250
|
}
|
|
251
251
|
/** Media messages, optionally filtered by type (image/video/audio/document/sticker). */
|
|
252
|
-
export function listMedia(types, limit = 50, chatJid) {
|
|
252
|
+
export function listMedia(types, limit = 50, chatJid, undownloadedOnly = false) {
|
|
253
253
|
const kinds = types && types.length ? types : ['image', 'video', 'audio', 'document', 'sticker'];
|
|
254
254
|
const placeholders = kinds.map(() => '?').join(',');
|
|
255
255
|
const params = [...kinds];
|
|
@@ -258,6 +258,8 @@ export function listMedia(types, limit = 50, chatJid) {
|
|
|
258
258
|
where += ' AND chatJid = ?';
|
|
259
259
|
params.push(chatJid);
|
|
260
260
|
}
|
|
261
|
+
if (undownloadedOnly)
|
|
262
|
+
where += " AND (mediaPath IS NULL OR mediaPath = '')";
|
|
261
263
|
params.push(limit);
|
|
262
264
|
return getDb()
|
|
263
265
|
.prepare(`SELECT * FROM messages WHERE ${where} ORDER BY timestamp DESC LIMIT ?`)
|
package/dist/utils/format.js
CHANGED
|
@@ -79,13 +79,16 @@ export function renderChatList(chats, time24h = true) {
|
|
|
79
79
|
export function renderMessages(messages, time24h = true) {
|
|
80
80
|
if (messages.length === 0)
|
|
81
81
|
return c.dim('No messages.');
|
|
82
|
+
const MEDIA = ['image', 'video', 'audio', 'document', 'sticker'];
|
|
82
83
|
return messages
|
|
83
84
|
.map((m) => {
|
|
84
85
|
const time = c.gray(formatTime(m.timestamp, time24h));
|
|
85
86
|
const who = m.fromMe ? c.green('me') : c.cyan(cleanSender(m.sender));
|
|
87
|
+
const isMedia = m.type ? MEDIA.includes(m.type) : false;
|
|
86
88
|
const body = m.text ?? (m.type ? c.dim(`[${m.type}]`) : c.dim('[no text]'));
|
|
87
|
-
|
|
88
|
-
|
|
89
|
+
// Media → an actionable "/dl <id>"; text → a dim #id (for /reply).
|
|
90
|
+
const tail = isMedia && !m.fromMe ? c.green(`/dl ${m.id}`) : c.dim(`#${m.id}`);
|
|
91
|
+
return `${time} ${who}: ${body} ${tail}`;
|
|
89
92
|
})
|
|
90
93
|
.join('\n');
|
|
91
94
|
}
|
package/package.json
CHANGED