cli-whatsapp 0.1.8 → 0.1.14
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/dist/cli/commands/filepicker.js +1 -1
- package/dist/cli/commands/shell.js +83 -7
- package/dist/db/schema.js +2 -1
- package/dist/db/sqlite.js +13 -4
- package/dist/utils/format.js +12 -2
- package/package.json +1 -1
|
@@ -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,9 @@ 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 un-downloaded media to download & open' },
|
|
34
|
+
{ value: '/saved', desc: 'pick already-downloaded media to open' },
|
|
33
35
|
{ value: '/pin', desc: 'pin a chat to the top', args: true },
|
|
34
36
|
{ value: '/unpin', desc: 'unpin a chat', args: true },
|
|
35
37
|
{ value: '/archive', desc: 'archive (hide) a chat', args: true },
|
|
@@ -263,7 +265,20 @@ async function sendTo(state, jid, name, content, meta, replaceInput = false) {
|
|
|
263
265
|
return;
|
|
264
266
|
try {
|
|
265
267
|
await sendContent(conn.sock, jid, content, meta);
|
|
266
|
-
|
|
268
|
+
// Build the echo: text → the message; media → "[type] filename" plus any
|
|
269
|
+
// caption (never "[image] [image]"). The filename tells you which file.
|
|
270
|
+
let echo;
|
|
271
|
+
if (meta.type === 'text') {
|
|
272
|
+
echo = meta.previewText;
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
const fname = meta.mediaPath ? basename(meta.mediaPath) : '';
|
|
276
|
+
const caption = meta.previewText.startsWith('[') ? '' : meta.previewText;
|
|
277
|
+
echo =
|
|
278
|
+
`${c.dim(`[${meta.type}]`)}` +
|
|
279
|
+
(fname ? ` ${fname}` : '') +
|
|
280
|
+
(caption ? ` ${c.dim('—')} ${caption}` : '');
|
|
281
|
+
}
|
|
267
282
|
// Replace the just-typed input with the bubble so it shows once — but only
|
|
268
283
|
// when we were already live (no "connecting…" lines were printed in between).
|
|
269
284
|
if (replaceInput && wasLive)
|
|
@@ -301,6 +316,61 @@ async function downloadAndOpen(state, idRaw) {
|
|
|
301
316
|
warn(`Download failed: ${err.message}`);
|
|
302
317
|
}
|
|
303
318
|
}
|
|
319
|
+
/** Build picker items from media rows (label + who/time, returns the id). */
|
|
320
|
+
function mediaItems(rows) {
|
|
321
|
+
const time24h = loadConfig().preferences.time24h;
|
|
322
|
+
return rows.map((m) => {
|
|
323
|
+
const label = m.text
|
|
324
|
+
? truncate(m.text.replace(/\s+/g, ' '), 32)
|
|
325
|
+
: m.mediaPath
|
|
326
|
+
? basename(m.mediaPath)
|
|
327
|
+
: '';
|
|
328
|
+
return {
|
|
329
|
+
value: `[${m.type}]${label ? ' ' + label : ''}`,
|
|
330
|
+
desc: `${m.fromMe ? 'you' : cleanSender(m.sender)} · ${formatTime(m.timestamp, time24h)}`,
|
|
331
|
+
id: String(m.id),
|
|
332
|
+
};
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
/** `/dl` with no id: pick from recent, not-yet-downloaded media (this chat if
|
|
336
|
+
* open, else all), newest first — arrow-select, no id to type. */
|
|
337
|
+
async function pickMediaToDownload(state) {
|
|
338
|
+
const rows = listMedia(null, 100, state.active?.jid, 'undownloaded');
|
|
339
|
+
if (!rows.length) {
|
|
340
|
+
warn(state.active ? 'No new media in this chat to download.' : 'No new media to download.');
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const title = state.active
|
|
344
|
+
? `Download media with ${displayName(state.active)} — ⏎ to save & open`
|
|
345
|
+
: 'Download media — ⏎ to save & open';
|
|
346
|
+
const picked = await pickFromList(title, mediaItems(rows), '');
|
|
347
|
+
if (picked)
|
|
348
|
+
await downloadAndOpen(state, picked);
|
|
349
|
+
}
|
|
350
|
+
/** `/saved`: pick from already-downloaded media (this chat if open, else all)
|
|
351
|
+
* and open it — no re-download. */
|
|
352
|
+
async function pickMediaToOpen(state) {
|
|
353
|
+
const rows = listMedia(null, 100, state.active?.jid, 'downloaded');
|
|
354
|
+
if (!rows.length) {
|
|
355
|
+
warn((state.active ? 'No downloaded media in this chat yet. ' : 'No downloaded media yet. ') +
|
|
356
|
+
'Use /dl to download some first.');
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const title = state.active
|
|
360
|
+
? `Open saved media with ${displayName(state.active)} — ⏎ to open`
|
|
361
|
+
: 'Open saved media — ⏎ to open';
|
|
362
|
+
const picked = await pickFromList(title, mediaItems(rows), '');
|
|
363
|
+
if (!picked)
|
|
364
|
+
return;
|
|
365
|
+
const m = getMessageById(parseInt(picked, 10));
|
|
366
|
+
if (m?.mediaPath && existsSync(m.mediaPath)) {
|
|
367
|
+
openFile(m.mediaPath);
|
|
368
|
+
printAbove(state, `${c.green('✓')} Opened ${c.dim(m.mediaPath)}`);
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
warn('That file is missing — re-download it with /dl.');
|
|
372
|
+
}
|
|
373
|
+
}
|
|
304
374
|
// ---- chat context ----------------------------------------------------------
|
|
305
375
|
function openChat(state, target) {
|
|
306
376
|
// Resolve against chats, contacts, or a raw number/jid.
|
|
@@ -476,9 +546,15 @@ async function handle(state, raw) {
|
|
|
476
546
|
return;
|
|
477
547
|
case 'download':
|
|
478
548
|
case 'dl':
|
|
479
|
-
|
|
480
|
-
if (need(args[0], 'id'))
|
|
549
|
+
if (args[0])
|
|
481
550
|
await downloadAndOpen(state, args[0]);
|
|
551
|
+
else
|
|
552
|
+
await pickMediaToDownload(state); // no id → pick un-downloaded media
|
|
553
|
+
return;
|
|
554
|
+
case 'saved':
|
|
555
|
+
case 'downloads':
|
|
556
|
+
case 'opened':
|
|
557
|
+
await pickMediaToOpen(state); // pick already-downloaded media to open
|
|
482
558
|
return;
|
|
483
559
|
case 'status':
|
|
484
560
|
status();
|
package/dist/db/schema.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* `schema_version` in the `settings` table lets us evolve this safely later.
|
|
6
6
|
*/
|
|
7
|
-
export const SCHEMA_VERSION =
|
|
7
|
+
export const SCHEMA_VERSION = 4;
|
|
8
8
|
export const SCHEMA_SQL = /* sql */ `
|
|
9
9
|
PRAGMA journal_mode = WAL; -- concurrent read while writing (watch + reads)
|
|
10
10
|
PRAGMA synchronous = NORMAL; -- fast, safe enough for a local cache
|
|
@@ -39,6 +39,7 @@ CREATE TABLE IF NOT EXISTS messages (
|
|
|
39
39
|
type TEXT,
|
|
40
40
|
fromMe INTEGER NOT NULL DEFAULT 0,
|
|
41
41
|
mediaPath TEXT,
|
|
42
|
+
downloadedAt INTEGER, -- when the media file was saved locally (for "recently saved" sort)
|
|
42
43
|
raw TEXT, -- JSON of the message content, kept for media so it can be downloaded later
|
|
43
44
|
UNIQUE (chatJid, messageId)
|
|
44
45
|
);
|
package/dist/db/sqlite.js
CHANGED
|
@@ -18,6 +18,7 @@ export function getDb() {
|
|
|
18
18
|
/** Additive, idempotent migrations for databases created by older versions. */
|
|
19
19
|
function migrate(dbc) {
|
|
20
20
|
ensureColumn(dbc, 'messages', 'raw', 'TEXT');
|
|
21
|
+
ensureColumn(dbc, 'messages', 'downloadedAt', 'INTEGER');
|
|
21
22
|
}
|
|
22
23
|
function ensureColumn(dbc, table, column, type) {
|
|
23
24
|
const cols = dbc.prepare(`PRAGMA table_info(${table})`).all();
|
|
@@ -105,9 +106,11 @@ export function insertMessage(m) {
|
|
|
105
106
|
raw: m.raw ?? null,
|
|
106
107
|
});
|
|
107
108
|
}
|
|
108
|
-
/** Update the local media file path once a media message is downloaded. */
|
|
109
|
+
/** Update the local media file path (and save time) once a media message is downloaded. */
|
|
109
110
|
export function setMediaPath(id, path) {
|
|
110
|
-
getDb()
|
|
111
|
+
getDb()
|
|
112
|
+
.prepare('UPDATE messages SET mediaPath = ?, downloadedAt = ? WHERE id = ?')
|
|
113
|
+
.run(path, Date.now(), id);
|
|
111
114
|
}
|
|
112
115
|
// ---- queries ---------------------------------------------------------------
|
|
113
116
|
// Chat rows with the display name resolved. Preference order:
|
|
@@ -249,7 +252,7 @@ export function importLidMappings() {
|
|
|
249
252
|
return n;
|
|
250
253
|
}
|
|
251
254
|
/** Media messages, optionally filtered by type (image/video/audio/document/sticker). */
|
|
252
|
-
export function listMedia(types, limit = 50, chatJid) {
|
|
255
|
+
export function listMedia(types, limit = 50, chatJid, saved) {
|
|
253
256
|
const kinds = types && types.length ? types : ['image', 'video', 'audio', 'document', 'sticker'];
|
|
254
257
|
const placeholders = kinds.map(() => '?').join(',');
|
|
255
258
|
const params = [...kinds];
|
|
@@ -258,9 +261,15 @@ export function listMedia(types, limit = 50, chatJid) {
|
|
|
258
261
|
where += ' AND chatJid = ?';
|
|
259
262
|
params.push(chatJid);
|
|
260
263
|
}
|
|
264
|
+
if (saved === 'undownloaded')
|
|
265
|
+
where += " AND (mediaPath IS NULL OR mediaPath = '')";
|
|
266
|
+
else if (saved === 'downloaded')
|
|
267
|
+
where += " AND mediaPath IS NOT NULL AND mediaPath <> ''";
|
|
268
|
+
// Downloaded → newest *saved* first; otherwise newest *shared* first.
|
|
269
|
+
const orderBy = saved === 'downloaded' ? 'COALESCE(downloadedAt, timestamp) DESC' : 'timestamp DESC';
|
|
261
270
|
params.push(limit);
|
|
262
271
|
return getDb()
|
|
263
|
-
.prepare(`SELECT * FROM messages WHERE ${where} ORDER BY
|
|
272
|
+
.prepare(`SELECT * FROM messages WHERE ${where} ORDER BY ${orderBy} LIMIT ?`)
|
|
264
273
|
.all(...params);
|
|
265
274
|
}
|
|
266
275
|
/** Recent chats by activity, ignoring pin priority. */
|
package/dist/utils/format.js
CHANGED
|
@@ -79,13 +79,23 @@ 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;
|
|
88
|
+
const downloaded = isMedia && !!m.mediaPath;
|
|
86
89
|
const body = m.text ?? (m.type ? c.dim(`[${m.type}]`) : c.dim('[no text]'));
|
|
87
|
-
|
|
88
|
-
|
|
90
|
+
// Media: "✓ saved" (downloaded) or "/dl <id>" (not yet); text: dim #id.
|
|
91
|
+
let tail;
|
|
92
|
+
if (downloaded)
|
|
93
|
+
tail = c.green('✓ saved') + ' ' + c.dim(`#${m.id}`);
|
|
94
|
+
else if (isMedia && !m.fromMe)
|
|
95
|
+
tail = c.green(`/dl ${m.id}`);
|
|
96
|
+
else
|
|
97
|
+
tail = c.dim(`#${m.id}`);
|
|
98
|
+
return `${time} ${who}: ${body} ${tail}`;
|
|
89
99
|
})
|
|
90
100
|
.join('\n');
|
|
91
101
|
}
|
package/package.json
CHANGED