cli-whatsapp 0.1.12 → 0.1.16
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/read.js +12 -4
- package/dist/cli/commands/shell.js +48 -16
- package/dist/cli/index.js +2 -2
- package/dist/db/schema.js +2 -1
- package/dist/db/sqlite.js +22 -10
- package/dist/utils/format.js +9 -2
- package/package.json +1 -1
|
@@ -10,13 +10,21 @@ export function chats(opts) {
|
|
|
10
10
|
print(renderChatList(rows, cfg.preferences.time24h));
|
|
11
11
|
}
|
|
12
12
|
/** wa contacts [--limit N] */
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
/** wa contacts [starts] — list contacts; `starts` filters by name prefix
|
|
14
|
+
* (e.g. `wa contacts a` → names starting with "a"). */
|
|
15
|
+
export function contacts(starts, opts) {
|
|
16
|
+
// Show everyone by default; honor an explicit --limit if given.
|
|
17
|
+
const limit = opts.limit ? Math.max(1, parseInt(opts.limit, 10) || 100000) : 100000;
|
|
18
|
+
const filter = starts?.replace(/^-+/, '').trim() || undefined; // tolerate "-a"
|
|
19
|
+
const rows = listContacts(limit, filter);
|
|
16
20
|
if (rows.length === 0) {
|
|
17
|
-
print(c.dim(
|
|
21
|
+
print(c.dim(filter
|
|
22
|
+
? `No contacts starting with "${filter}".`
|
|
23
|
+
: 'No contacts cached yet. Run `wa sync`.'));
|
|
18
24
|
return;
|
|
19
25
|
}
|
|
26
|
+
if (filter)
|
|
27
|
+
print(c.dim(`Contacts starting with "${filter}":`));
|
|
20
28
|
const width = String(rows.length).length;
|
|
21
29
|
print(rows
|
|
22
30
|
.map((r, i) => {
|
|
@@ -30,7 +30,8 @@ const SLASH_ITEMS = [
|
|
|
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
32
|
{ value: '/media', desc: 'list cached media (this chat, or all)' },
|
|
33
|
-
{ value: '/dl', desc: 'pick
|
|
33
|
+
{ value: '/dl', desc: 'pick un-downloaded media to download & open' },
|
|
34
|
+
{ value: '/saved', desc: 'pick already-downloaded media to open' },
|
|
34
35
|
{ value: '/pin', desc: 'pin a chat to the top', args: true },
|
|
35
36
|
{ value: '/unpin', desc: 'unpin a chat', args: true },
|
|
36
37
|
{ value: '/archive', desc: 'archive (hide) a chat', args: true },
|
|
@@ -315,16 +316,10 @@ async function downloadAndOpen(state, idRaw) {
|
|
|
315
316
|
warn(`Download failed: ${err.message}`);
|
|
316
317
|
}
|
|
317
318
|
}
|
|
318
|
-
/**
|
|
319
|
-
|
|
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
|
-
}
|
|
319
|
+
/** Build picker items from media rows (label + who/time, returns the id). */
|
|
320
|
+
function mediaItems(rows) {
|
|
326
321
|
const time24h = loadConfig().preferences.time24h;
|
|
327
|
-
|
|
322
|
+
return rows.map((m) => {
|
|
328
323
|
const label = m.text
|
|
329
324
|
? truncate(m.text.replace(/\s+/g, ' '), 32)
|
|
330
325
|
: m.mediaPath
|
|
@@ -336,13 +331,46 @@ async function pickMediaToDownload(state) {
|
|
|
336
331
|
id: String(m.id),
|
|
337
332
|
};
|
|
338
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
|
+
}
|
|
339
343
|
const title = state.active
|
|
340
|
-
? `
|
|
341
|
-
: '
|
|
342
|
-
const picked = await pickFromList(title,
|
|
344
|
+
? `Download media with ${displayName(state.active)} — ⏎ to save & open`
|
|
345
|
+
: 'Download media — ⏎ to save & open';
|
|
346
|
+
const picked = await pickFromList(title, mediaItems(rows), '');
|
|
343
347
|
if (picked)
|
|
344
348
|
await downloadAndOpen(state, picked);
|
|
345
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
|
+
}
|
|
346
374
|
// ---- chat context ----------------------------------------------------------
|
|
347
375
|
function openChat(state, target) {
|
|
348
376
|
// Resolve against chats, contacts, or a raw number/jid.
|
|
@@ -500,7 +528,7 @@ async function handle(state, raw) {
|
|
|
500
528
|
recent({ limit: args[0] });
|
|
501
529
|
return;
|
|
502
530
|
case 'contacts':
|
|
503
|
-
contacts(
|
|
531
|
+
contacts(args[0], {});
|
|
504
532
|
return;
|
|
505
533
|
case 'history':
|
|
506
534
|
if (state.active && (!args[0] || /^\d+$/.test(args[0])))
|
|
@@ -518,11 +546,15 @@ async function handle(state, raw) {
|
|
|
518
546
|
return;
|
|
519
547
|
case 'download':
|
|
520
548
|
case 'dl':
|
|
521
|
-
case 'open-media':
|
|
522
549
|
if (args[0])
|
|
523
550
|
await downloadAndOpen(state, args[0]);
|
|
524
551
|
else
|
|
525
|
-
await pickMediaToDownload(state); // no id → pick
|
|
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
|
|
526
558
|
return;
|
|
527
559
|
case 'status':
|
|
528
560
|
status();
|
package/dist/cli/index.js
CHANGED
|
@@ -80,8 +80,8 @@ program
|
|
|
80
80
|
.option('-n, --limit <n>', 'max chats to show')
|
|
81
81
|
.action(run(chats));
|
|
82
82
|
program
|
|
83
|
-
.command('contacts')
|
|
84
|
-
.description('List
|
|
83
|
+
.command('contacts [starts]')
|
|
84
|
+
.description('List contacts; pass a letter to filter (e.g. `wa contacts a`)')
|
|
85
85
|
.option('-n, --limit <n>', 'max contacts to show')
|
|
86
86
|
.action(run(contacts));
|
|
87
87
|
program
|
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:
|
|
@@ -142,13 +145,18 @@ export function listChats(limit = 50, includeArchived = false) {
|
|
|
142
145
|
LIMIT ?`)
|
|
143
146
|
.all(limit);
|
|
144
147
|
}
|
|
145
|
-
export function listContacts(limit = 500) {
|
|
148
|
+
export function listContacts(limit = 500, startsWith) {
|
|
146
149
|
// Exclude WhatsApp privacy-masked placeholder names (e.g. "+91∙∙∙∙∙∙∙∙37").
|
|
150
|
+
const params = [];
|
|
151
|
+
let where = "name IS NOT NULL AND name NOT LIKE '%∙%'";
|
|
152
|
+
if (startsWith) {
|
|
153
|
+
where += " AND name LIKE ? ESCAPE '\\'";
|
|
154
|
+
params.push(escapeLike(startsWith) + '%');
|
|
155
|
+
}
|
|
156
|
+
params.push(limit);
|
|
147
157
|
return getDb()
|
|
148
|
-
.prepare(`SELECT * FROM contacts
|
|
149
|
-
|
|
150
|
-
ORDER BY name COLLATE NOCASE LIMIT ?`)
|
|
151
|
-
.all(limit);
|
|
158
|
+
.prepare(`SELECT * FROM contacts WHERE ${where} ORDER BY name COLLATE NOCASE LIMIT ?`)
|
|
159
|
+
.all(...params);
|
|
152
160
|
}
|
|
153
161
|
export function getMessages(chatJid, limit = 30) {
|
|
154
162
|
// Newest N, returned oldest-first for natural chat reading order.
|
|
@@ -249,7 +257,7 @@ export function importLidMappings() {
|
|
|
249
257
|
return n;
|
|
250
258
|
}
|
|
251
259
|
/** Media messages, optionally filtered by type (image/video/audio/document/sticker). */
|
|
252
|
-
export function listMedia(types, limit = 50, chatJid,
|
|
260
|
+
export function listMedia(types, limit = 50, chatJid, saved) {
|
|
253
261
|
const kinds = types && types.length ? types : ['image', 'video', 'audio', 'document', 'sticker'];
|
|
254
262
|
const placeholders = kinds.map(() => '?').join(',');
|
|
255
263
|
const params = [...kinds];
|
|
@@ -258,11 +266,15 @@ export function listMedia(types, limit = 50, chatJid, undownloadedOnly = false)
|
|
|
258
266
|
where += ' AND chatJid = ?';
|
|
259
267
|
params.push(chatJid);
|
|
260
268
|
}
|
|
261
|
-
if (
|
|
269
|
+
if (saved === 'undownloaded')
|
|
262
270
|
where += " AND (mediaPath IS NULL OR mediaPath = '')";
|
|
271
|
+
else if (saved === 'downloaded')
|
|
272
|
+
where += " AND mediaPath IS NOT NULL AND mediaPath <> ''";
|
|
273
|
+
// Downloaded → newest *saved* first; otherwise newest *shared* first.
|
|
274
|
+
const orderBy = saved === 'downloaded' ? 'COALESCE(downloadedAt, timestamp) DESC' : 'timestamp DESC';
|
|
263
275
|
params.push(limit);
|
|
264
276
|
return getDb()
|
|
265
|
-
.prepare(`SELECT * FROM messages WHERE ${where} ORDER BY
|
|
277
|
+
.prepare(`SELECT * FROM messages WHERE ${where} ORDER BY ${orderBy} LIMIT ?`)
|
|
266
278
|
.all(...params);
|
|
267
279
|
}
|
|
268
280
|
/** Recent chats by activity, ignoring pin priority. */
|
package/dist/utils/format.js
CHANGED
|
@@ -85,9 +85,16 @@ export function renderMessages(messages, time24h = true) {
|
|
|
85
85
|
const time = c.gray(formatTime(m.timestamp, time24h));
|
|
86
86
|
const who = m.fromMe ? c.green('me') : c.cyan(cleanSender(m.sender));
|
|
87
87
|
const isMedia = m.type ? MEDIA.includes(m.type) : false;
|
|
88
|
+
const downloaded = isMedia && !!m.mediaPath;
|
|
88
89
|
const body = m.text ?? (m.type ? c.dim(`[${m.type}]`) : c.dim('[no text]'));
|
|
89
|
-
// Media
|
|
90
|
-
|
|
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}`);
|
|
91
98
|
return `${time} ${who}: ${body} ${tail}`;
|
|
92
99
|
})
|
|
93
100
|
.join('\n');
|
package/package.json
CHANGED