cli-whatsapp 0.1.2 → 0.1.4
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 +3 -3
- package/dist/cli/commands/shell.js +92 -23
- package/dist/cli/commands/sync.js +5 -1
- package/dist/db/schema.js +11 -1
- package/dist/db/sqlite.js +78 -15
- package/dist/utils/format.js +31 -5
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { listChats, listContacts, getMessages, searchMessages, findChat, } from "../../db/sqlite.js";
|
|
2
2
|
import { loadConfig } from "../../config/index.js";
|
|
3
|
-
import { renderChatList, renderMessages, displayName, formatTime, truncate, c, } from "../../utils/format.js";
|
|
3
|
+
import { renderChatList, renderMessages, displayName, chatNumber, cleanSender, formatTime, truncate, c, } from "../../utils/format.js";
|
|
4
4
|
import { print, info, fail } from "../../utils/ui.js";
|
|
5
5
|
/** wa chats [--all] [--limit N] */
|
|
6
6
|
export function chats(opts) {
|
|
@@ -34,7 +34,7 @@ export function open(target, opts) {
|
|
|
34
34
|
}
|
|
35
35
|
const cfg = loadConfig();
|
|
36
36
|
const limit = clampLimit(opts.limit, cfg.preferences.openLimit);
|
|
37
|
-
print(c.bold(displayName(chat)) + c.dim(` ${chat
|
|
37
|
+
print(c.bold(displayName(chat)) + (chatNumber(chat) ? c.dim(` ${chatNumber(chat)}`) : ''));
|
|
38
38
|
print(c.dim('─'.repeat(40)));
|
|
39
39
|
print(renderMessages(getMessages(chat.jid, limit), cfg.preferences.time24h));
|
|
40
40
|
}
|
|
@@ -57,7 +57,7 @@ export function search(term, opts) {
|
|
|
57
57
|
const chat = findChat(m.chatJid);
|
|
58
58
|
const where = c.magenta(chat ? displayName(chat) : m.chatJid.split('@')[0]);
|
|
59
59
|
const time = c.gray(formatTime(m.timestamp, cfg.preferences.time24h));
|
|
60
|
-
const who = m.fromMe ? c.green('me') : c.cyan(m.sender
|
|
60
|
+
const who = m.fromMe ? c.green('me') : c.cyan(cleanSender(m.sender));
|
|
61
61
|
const body = highlight(truncate(m.text ?? '', 80), term);
|
|
62
62
|
print(`${where} ${time} ${who}: ${body} ${c.dim(`#${m.id}`)}`);
|
|
63
63
|
}
|
|
@@ -10,10 +10,10 @@ import { recent, pin, unpin, archive, unarchive } from "./manage.js";
|
|
|
10
10
|
import { status } from "./auth.js";
|
|
11
11
|
import { connect, hasSession } from "../../whatsapp/client.js";
|
|
12
12
|
import { registerPersistence, extractText, isGroupJid } from "../../whatsapp/events.js";
|
|
13
|
-
import { listChats, findChat, getMessages, countMessages, } from "../../db/sqlite.js";
|
|
13
|
+
import { listChats, listContacts, findChat, upsertChat, getMessages, countMessages, } from "../../db/sqlite.js";
|
|
14
14
|
import { resolveTarget } from "../../utils/jid.js";
|
|
15
15
|
import { loadConfig, saveConfig } from "../../config/index.js";
|
|
16
|
-
import { c, displayName, formatTime, truncate, renderMessages } from "../../utils/format.js";
|
|
16
|
+
import { c, displayName, chatNumber, formatTime, truncate, renderMessages } from "../../utils/format.js";
|
|
17
17
|
import { print, warn, printError } from "../../utils/ui.js";
|
|
18
18
|
import { notify } from "../../utils/notify.js";
|
|
19
19
|
// ---- tab completion --------------------------------------------------------
|
|
@@ -249,16 +249,31 @@ async function sendTo(state, jid, name, content, meta) {
|
|
|
249
249
|
}
|
|
250
250
|
// ---- chat context ----------------------------------------------------------
|
|
251
251
|
function openChat(state, target) {
|
|
252
|
-
|
|
252
|
+
// Resolve against chats, contacts, or a raw number/jid.
|
|
253
|
+
const resolved = resolveTarget(target);
|
|
254
|
+
let chat = resolved ? findChat(resolved.jid) : findChat(target);
|
|
255
|
+
if (!chat && resolved) {
|
|
256
|
+
// Known contact / number with no conversation yet — start one.
|
|
257
|
+
upsertChat({
|
|
258
|
+
jid: resolved.jid,
|
|
259
|
+
name: resolved.name,
|
|
260
|
+
isGroup: resolved.jid.endsWith('@g.us') ? 1 : 0,
|
|
261
|
+
});
|
|
262
|
+
chat = findChat(resolved.jid);
|
|
263
|
+
}
|
|
253
264
|
if (!chat) {
|
|
254
|
-
warn(`No chat matching "${target}". Try /chats or /search.`);
|
|
265
|
+
warn(`No chat or contact matching "${target}". Try /chats or /search.`);
|
|
255
266
|
return;
|
|
256
267
|
}
|
|
257
268
|
state.active = chat;
|
|
258
269
|
setPrompt(state);
|
|
259
270
|
print(c.dim('─'.repeat(40)));
|
|
260
|
-
|
|
261
|
-
print(
|
|
271
|
+
const sub = chatNumber(chat);
|
|
272
|
+
print(c.bold(displayName(chat)) + (sub ? c.dim(` ${sub}`) : ''));
|
|
273
|
+
const msgs = getMessages(chat.jid, 15);
|
|
274
|
+
print(msgs.length
|
|
275
|
+
? renderMessages(msgs, loadConfig().preferences.time24h)
|
|
276
|
+
: c.dim('No messages yet — say hi 👋'));
|
|
262
277
|
print(c.dim(`─── in this chat: type to send · @ opens a file picker to attach · /close ───`));
|
|
263
278
|
}
|
|
264
279
|
// ---- settings --------------------------------------------------------------
|
|
@@ -544,6 +559,39 @@ export async function shell() {
|
|
|
544
559
|
// listener, so rl.line already includes the just-typed character.
|
|
545
560
|
readline.emitKeypressEvents(process.stdin);
|
|
546
561
|
let pickerOpen = false;
|
|
562
|
+
// Commands whose <chat> argument gets the live chat picker. "final" = chat is
|
|
563
|
+
// the last arg (run on pick); "then" = chat then more input (send/sendfile).
|
|
564
|
+
const CHAT_FINAL = new Set(['open', 'history', 'pin', 'unpin', 'archive', 'unarchive']);
|
|
565
|
+
const CHAT_THEN = new Set(['send', 'sendfile']);
|
|
566
|
+
const clearLine = () => rl.write(null, { ctrl: true, name: 'u' });
|
|
567
|
+
// Open the chat picker for a command, then insert/run with the chosen chat.
|
|
568
|
+
const chatPickInto = async (cmd) => {
|
|
569
|
+
// Existing chats first (names resolved via contacts + the LID map)…
|
|
570
|
+
const items = listChats(5000, true).map((ch) => ({
|
|
571
|
+
value: displayName(ch),
|
|
572
|
+
desc: truncate((ch.lastMessage ?? '').replace(/\s+/g, ' '), 30),
|
|
573
|
+
}));
|
|
574
|
+
// …then every other contact, so nobody is missing. Dedupe by name so a
|
|
575
|
+
// contact already shown as a chat doesn't appear twice.
|
|
576
|
+
const seen = new Set(items.map((it) => it.value.toLowerCase()));
|
|
577
|
+
for (const ct of listContacts(50000)) {
|
|
578
|
+
if (ct.name && !seen.has(ct.name.toLowerCase())) {
|
|
579
|
+
items.push({ value: ct.name, desc: 'contact' });
|
|
580
|
+
seen.add(ct.name.toLowerCase());
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
const chat = await pickFromList('Select a chat or contact', items, '');
|
|
584
|
+
clearLine();
|
|
585
|
+
if (!chat)
|
|
586
|
+
return; // cancelled → empty line
|
|
587
|
+
const q = /\s/.test(chat) ? `"${chat}"` : chat; // quote names with spaces
|
|
588
|
+
if (CHAT_THEN.has(cmd))
|
|
589
|
+
rl.write(`/${cmd} ${q} `); // wait for message/file
|
|
590
|
+
else {
|
|
591
|
+
queue.push(`/${cmd} ${q}`); // run right away
|
|
592
|
+
void pump();
|
|
593
|
+
}
|
|
594
|
+
};
|
|
547
595
|
process.stdin.on('keypress', (str, _key) => {
|
|
548
596
|
void _key;
|
|
549
597
|
if (!process.stdin.isTTY || pickerOpen || busy)
|
|
@@ -551,26 +599,48 @@ export async function shell() {
|
|
|
551
599
|
// `/` menu — only when it's the first character on the line.
|
|
552
600
|
if (str === '/' && rl.line === '/') {
|
|
553
601
|
pickerOpen = true;
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
602
|
+
void (async () => {
|
|
603
|
+
try {
|
|
604
|
+
const picked = await pickFromList('Slash Commands', SLASH_ITEMS, '');
|
|
605
|
+
clearLine();
|
|
606
|
+
if (!picked)
|
|
607
|
+
return; // cancelled — leave the line empty
|
|
608
|
+
const cmd = picked.slice(1);
|
|
609
|
+
if (CHAT_FINAL.has(cmd) || CHAT_THEN.has(cmd)) {
|
|
610
|
+
await chatPickInto(cmd); // chain straight into the chat picker
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
const item = SLASH_ITEMS.find((i) => i.value === picked);
|
|
614
|
+
if (item && !item.args) {
|
|
615
|
+
queue.push(picked); // no args → run now
|
|
616
|
+
void pump();
|
|
617
|
+
}
|
|
618
|
+
else {
|
|
619
|
+
rl.write(picked + ' '); // needs (non-chat) args → wait
|
|
620
|
+
}
|
|
564
621
|
}
|
|
565
|
-
|
|
566
|
-
|
|
622
|
+
finally {
|
|
623
|
+
pickerOpen = false;
|
|
567
624
|
}
|
|
568
|
-
})
|
|
569
|
-
.finally(() => {
|
|
570
|
-
pickerOpen = false;
|
|
571
|
-
});
|
|
625
|
+
})();
|
|
572
626
|
return;
|
|
573
627
|
}
|
|
628
|
+
// Space right after a chat command (typed manually) → open the chat picker.
|
|
629
|
+
if (str === ' ') {
|
|
630
|
+
const m = rl.line.match(/^\/(open|history|pin|unpin|archive|unarchive|send|sendfile) $/);
|
|
631
|
+
if (m) {
|
|
632
|
+
pickerOpen = true;
|
|
633
|
+
void (async () => {
|
|
634
|
+
try {
|
|
635
|
+
await chatPickInto(m[1]);
|
|
636
|
+
}
|
|
637
|
+
finally {
|
|
638
|
+
pickerOpen = false;
|
|
639
|
+
}
|
|
640
|
+
})();
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
574
644
|
// `@` file picker — only with a chat open and at a fresh token boundary.
|
|
575
645
|
if (str === '@' && state.active) {
|
|
576
646
|
const before = rl.line.slice(0, Math.max(0, rl.cursor - 1)).replace(/@$/, '');
|
|
@@ -579,7 +649,6 @@ export async function shell() {
|
|
|
579
649
|
pickerOpen = true;
|
|
580
650
|
pickFile(process.cwd())
|
|
581
651
|
.then((picked) => {
|
|
582
|
-
// Quote paths with spaces so the whole path is treated as one token.
|
|
583
652
|
if (picked)
|
|
584
653
|
rl.write(picked.includes(' ') ? `"${picked}"` : picked);
|
|
585
654
|
else
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { connect, runWithConnection } from "../../whatsapp/client.js";
|
|
2
2
|
import { registerPersistence, extractText, isGroupJid } from "../../whatsapp/events.js";
|
|
3
|
-
import { countMessages, listChats, findChat } from "../../db/sqlite.js";
|
|
3
|
+
import { countMessages, listChats, findChat, importLidMappings } from "../../db/sqlite.js";
|
|
4
4
|
import { loadConfig, saveConfig } from "../../config/index.js";
|
|
5
5
|
import { ok, info, print } from "../../utils/ui.js";
|
|
6
6
|
import { c, formatTime, displayName } from "../../utils/format.js";
|
|
@@ -22,10 +22,14 @@ export async function sync(opts) {
|
|
|
22
22
|
clearInterval(timer);
|
|
23
23
|
process.stdout.write('\r' + ' '.repeat(50) + '\r');
|
|
24
24
|
}, { requireSession: true, syncFullHistory: true });
|
|
25
|
+
// Bridge WhatsApp's @lid ids to phone numbers so contact names resolve.
|
|
26
|
+
const mapped = importLidMappings();
|
|
25
27
|
const after = { chats: listChats(5000).length, msgs: countMessages() };
|
|
26
28
|
const cfg = loadConfig();
|
|
27
29
|
cfg.session.lastSyncAt = Date.now();
|
|
28
30
|
saveConfig(cfg);
|
|
31
|
+
if (mapped)
|
|
32
|
+
info(`Resolved ${mapped} contact name mappings.`);
|
|
29
33
|
ok(`Sync complete. +${after.chats - before.chats} chats, +${after.msgs - before.msgs} messages ` +
|
|
30
34
|
`(${after.chats} chats, ${after.msgs} messages total).`);
|
|
31
35
|
}
|
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 = 3;
|
|
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
|
|
@@ -54,4 +54,14 @@ CREATE TABLE IF NOT EXISTS settings (
|
|
|
54
54
|
key TEXT PRIMARY KEY,
|
|
55
55
|
value TEXT
|
|
56
56
|
);
|
|
57
|
+
|
|
58
|
+
-- WhatsApp's "LID" identifiers ↔ phone-number jids. Individual chats are often
|
|
59
|
+
-- stored under a @lid jid while the contact is saved under the phone jid; this
|
|
60
|
+
-- table bridges them so names resolve. Populated from the session's mapping
|
|
61
|
+
-- files (and kept fresh on sync).
|
|
62
|
+
CREATE TABLE IF NOT EXISTS lid_map (
|
|
63
|
+
lid TEXT PRIMARY KEY, -- e.g. 112735023038675@lid
|
|
64
|
+
pn TEXT -- e.g. 918179514637@s.whatsapp.net
|
|
65
|
+
);
|
|
66
|
+
CREATE INDEX IF NOT EXISTS idx_lid_pn ON lid_map (pn);
|
|
57
67
|
`;
|
package/dist/db/sqlite.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
2
4
|
import { ensureDirs, PATHS } from "../config/index.js";
|
|
3
5
|
import { SCHEMA_SQL, SCHEMA_VERSION } from "./schema.js";
|
|
4
6
|
let db = null;
|
|
@@ -108,16 +110,30 @@ export function setMediaPath(id, path) {
|
|
|
108
110
|
getDb().prepare('UPDATE messages SET mediaPath = ? WHERE id = ?').run(path, id);
|
|
109
111
|
}
|
|
110
112
|
// ---- queries ---------------------------------------------------------------
|
|
111
|
-
// Chat rows with the display name resolved
|
|
112
|
-
//
|
|
113
|
-
//
|
|
113
|
+
// Chat rows with the display name resolved. Preference order:
|
|
114
|
+
// 1. the chat's own name
|
|
115
|
+
// 2. a saved contact for the same jid (direct phone-number chats)
|
|
116
|
+
// 3. a saved contact reached via the LID→phone map (chats stored under @lid)
|
|
117
|
+
// Column order/shape matches ChatRow. `NAME_EXPR` is reused wherever we need the
|
|
118
|
+
// same resolution (e.g. searching by name).
|
|
119
|
+
// Prefer, in order: the chat's own name, the contact reached via the LID map
|
|
120
|
+
// (the real saved name for @lid chats), then the direct contact — but never a
|
|
121
|
+
// WhatsApp privacy-masked placeholder like "+91∙∙∙∙∙∙∙∙37" (those contain the
|
|
122
|
+
// U+2219 bullet), which we treat as "no name".
|
|
123
|
+
const UNMASK = (col) => `CASE WHEN ${col} LIKE '%∙%' THEN NULL ELSE ${col} END`;
|
|
124
|
+
const NAME_EXPR = `COALESCE(${UNMASK('c.name')}, ${UNMASK('ctl.name')}, ${UNMASK('ct.name')})`;
|
|
125
|
+
const CHAT_JOINS = `
|
|
126
|
+
FROM chats c
|
|
127
|
+
LEFT JOIN contacts ct ON ct.jid = c.jid
|
|
128
|
+
LEFT JOIN lid_map lm ON lm.lid = c.jid
|
|
129
|
+
LEFT JOIN contacts ctl ON ctl.jid = lm.pn`;
|
|
114
130
|
const CHAT_SELECT = `
|
|
115
131
|
SELECT c.jid,
|
|
116
|
-
|
|
132
|
+
${NAME_EXPR} AS name,
|
|
117
133
|
c.lastMessage, c.lastTimestamp, c.unreadCount,
|
|
118
|
-
c.archived, c.pinned, c.isGroup
|
|
119
|
-
|
|
120
|
-
|
|
134
|
+
c.archived, c.pinned, c.isGroup,
|
|
135
|
+
lm.pn AS pn
|
|
136
|
+
${CHAT_JOINS}`;
|
|
121
137
|
export function listChats(limit = 50, includeArchived = false) {
|
|
122
138
|
return getDb()
|
|
123
139
|
.prepare(`${CHAT_SELECT}
|
|
@@ -127,8 +143,11 @@ export function listChats(limit = 50, includeArchived = false) {
|
|
|
127
143
|
.all(limit);
|
|
128
144
|
}
|
|
129
145
|
export function listContacts(limit = 500) {
|
|
146
|
+
// Exclude WhatsApp privacy-masked placeholder names (e.g. "+91∙∙∙∙∙∙∙∙37").
|
|
130
147
|
return getDb()
|
|
131
|
-
.prepare(`SELECT * FROM contacts
|
|
148
|
+
.prepare(`SELECT * FROM contacts
|
|
149
|
+
WHERE name IS NOT NULL AND name NOT LIKE '%∙%'
|
|
150
|
+
ORDER BY name COLLATE NOCASE LIMIT ?`)
|
|
132
151
|
.all(limit);
|
|
133
152
|
}
|
|
134
153
|
export function getMessages(chatJid, limit = 30) {
|
|
@@ -165,26 +184,70 @@ export function findChat(query) {
|
|
|
165
184
|
.get(query);
|
|
166
185
|
if (exact)
|
|
167
186
|
return exact;
|
|
168
|
-
// Bare phone number → match the number portion of a direct-chat jid
|
|
187
|
+
// Bare phone number → match the number portion of a direct-chat jid, or the
|
|
188
|
+
// phone of a @lid chat via the LID map.
|
|
169
189
|
const digits = query.replace(/[^\d]/g, '');
|
|
170
190
|
if (digits.length >= 5 && digits === query.replace(/[+\s()\-]/g, '')) {
|
|
171
191
|
const byNum = dbc
|
|
172
|
-
.prepare(`${CHAT_SELECT}
|
|
173
|
-
|
|
192
|
+
.prepare(`${CHAT_SELECT}
|
|
193
|
+
WHERE c.jid LIKE ? OR lm.pn LIKE ?
|
|
194
|
+
ORDER BY c.lastTimestamp DESC LIMIT 1`)
|
|
195
|
+
.get(`${digits}@%`, `${digits}@%`);
|
|
174
196
|
if (byNum)
|
|
175
197
|
return byNum;
|
|
176
198
|
}
|
|
177
|
-
// Case-insensitive name match (chat
|
|
178
|
-
// preferring the most recently active.
|
|
199
|
+
// Case-insensitive name match (chat name, direct contact, or LID-mapped
|
|
200
|
+
// contact), preferring an exact match then the most recently active.
|
|
179
201
|
const byName = dbc
|
|
180
202
|
.prepare(`${CHAT_SELECT}
|
|
181
|
-
WHERE
|
|
182
|
-
ORDER BY (
|
|
203
|
+
WHERE ${NAME_EXPR} LIKE ? ESCAPE '\\'
|
|
204
|
+
ORDER BY (${NAME_EXPR} = ? COLLATE NOCASE) DESC,
|
|
183
205
|
c.lastTimestamp DESC
|
|
184
206
|
LIMIT 1`)
|
|
185
207
|
.get(likePattern(query), query);
|
|
186
208
|
return byName ?? null;
|
|
187
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* Import the session's LID↔phone mappings into `lid_map`. Reads the
|
|
212
|
+
* `lid-mapping-<LID>_reverse.json` files (LID → phone number) that Baileys
|
|
213
|
+
* writes. Idempotent and fast (single transaction); safe to run on every sync.
|
|
214
|
+
* Returns the number of mappings imported.
|
|
215
|
+
*/
|
|
216
|
+
export function importLidMappings() {
|
|
217
|
+
let files;
|
|
218
|
+
try {
|
|
219
|
+
files = readdirSync(PATHS.sessions).filter((f) => f.startsWith('lid-mapping-') && f.endsWith('_reverse.json'));
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return 0;
|
|
223
|
+
}
|
|
224
|
+
const dbc = getDb();
|
|
225
|
+
const stmt = dbc.prepare(`INSERT INTO lid_map (lid, pn) VALUES (?, ?)
|
|
226
|
+
ON CONFLICT(lid) DO UPDATE SET pn = excluded.pn`);
|
|
227
|
+
dbc.exec('BEGIN');
|
|
228
|
+
let n = 0;
|
|
229
|
+
try {
|
|
230
|
+
for (const f of files) {
|
|
231
|
+
const lid = f.slice('lid-mapping-'.length, -'_reverse.json'.length);
|
|
232
|
+
let pn;
|
|
233
|
+
try {
|
|
234
|
+
pn = JSON.parse(readFileSync(join(PATHS.sessions, f), 'utf8'));
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (!lid || !pn)
|
|
240
|
+
continue;
|
|
241
|
+
stmt.run(`${lid}@lid`, `${pn}@s.whatsapp.net`);
|
|
242
|
+
n++;
|
|
243
|
+
}
|
|
244
|
+
dbc.exec('COMMIT');
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
dbc.exec('ROLLBACK');
|
|
248
|
+
}
|
|
249
|
+
return n;
|
|
250
|
+
}
|
|
188
251
|
/** Media messages, optionally filtered by type (image/video/audio/document/sticker). */
|
|
189
252
|
export function listMedia(types, limit = 50) {
|
|
190
253
|
const dbc = getDb();
|
package/dist/utils/format.js
CHANGED
|
@@ -45,11 +45,17 @@ export function formatTime(ms, time24h = true) {
|
|
|
45
45
|
return `${date} ${time}`;
|
|
46
46
|
}
|
|
47
47
|
export function displayName(chat) {
|
|
48
|
-
|
|
48
|
+
// Real saved/known name (never a privacy-masked placeholder).
|
|
49
|
+
if (chat.name && !chat.name.includes('∙'))
|
|
49
50
|
return chat.name;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
if (chat.isGroup)
|
|
52
|
+
return 'Group';
|
|
53
|
+
// Individual with no name: show a real phone number — never a @lid id.
|
|
54
|
+
// Prefer the LID→phone mapping, else the number of a phone jid.
|
|
55
|
+
const pnNum = chat.pn ? chat.pn.split('@')[0] : null;
|
|
56
|
+
const jidNum = chat.jid.endsWith('@s.whatsapp.net') ? chat.jid.split('@')[0] : null;
|
|
57
|
+
const num = pnNum ?? jidNum;
|
|
58
|
+
return num ? `+${num}` : 'Unknown contact';
|
|
53
59
|
}
|
|
54
60
|
/** Render the numbered chat list shown by `wa chats`. */
|
|
55
61
|
export function renderChatList(chats, time24h = true) {
|
|
@@ -76,13 +82,33 @@ export function renderMessages(messages, time24h = true) {
|
|
|
76
82
|
return messages
|
|
77
83
|
.map((m) => {
|
|
78
84
|
const time = c.gray(formatTime(m.timestamp, time24h));
|
|
79
|
-
const who = m.fromMe ? c.green('me') : c.cyan(m.sender
|
|
85
|
+
const who = m.fromMe ? c.green('me') : c.cyan(cleanSender(m.sender));
|
|
80
86
|
const body = m.text ?? (m.type ? c.dim(`[${m.type}]`) : c.dim('[no text]'));
|
|
81
87
|
const id = c.dim(`#${m.id}`);
|
|
82
88
|
return `${time} ${who}: ${body} ${id}`;
|
|
83
89
|
})
|
|
84
90
|
.join('\n');
|
|
85
91
|
}
|
|
92
|
+
/** A human sender label that never exposes a @lid id or a masked placeholder. */
|
|
93
|
+
export function cleanSender(sender) {
|
|
94
|
+
if (!sender)
|
|
95
|
+
return 'unknown';
|
|
96
|
+
if (sender.includes('∙'))
|
|
97
|
+
return 'unknown'; // masked placeholder
|
|
98
|
+
if (sender.includes('@')) {
|
|
99
|
+
if (sender.endsWith('@lid'))
|
|
100
|
+
return 'unknown'; // never show a lid
|
|
101
|
+
return `+${sender.split('@')[0]}`; // phone jid → number
|
|
102
|
+
}
|
|
103
|
+
return sender; // a real push name
|
|
104
|
+
}
|
|
105
|
+
/** Phone number to show under a chat header — never a @lid id. */
|
|
106
|
+
export function chatNumber(chat) {
|
|
107
|
+
const pnNum = chat.pn ? chat.pn.split('@')[0] : null;
|
|
108
|
+
const jidNum = chat.jid.endsWith('@s.whatsapp.net') ? chat.jid.split('@')[0] : null;
|
|
109
|
+
const num = pnNum ?? jidNum;
|
|
110
|
+
return num ? `+${num}` : '';
|
|
111
|
+
}
|
|
86
112
|
export function truncate(s, n) {
|
|
87
113
|
const oneLine = s.replace(/\s+/g, ' ').trim();
|
|
88
114
|
return oneLine.length > n ? oneLine.slice(0, n - 1) + '…' : oneLine;
|
package/package.json
CHANGED