cli-whatsapp 0.1.3 → 0.1.5
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 +56 -16
- 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 --------------------------------------------------------
|
|
@@ -162,6 +162,19 @@ function printAbove(state, text) {
|
|
|
162
162
|
process.stdout.write(text + '\n');
|
|
163
163
|
state.rl.prompt(true);
|
|
164
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* Replace the line the user just submitted (their echoed input) with `text`, so
|
|
167
|
+
* an outgoing message shows once as a bubble instead of appearing twice. Called
|
|
168
|
+
* synchronously while processing the line, when the cursor sits just below the
|
|
169
|
+
* submitted input.
|
|
170
|
+
*/
|
|
171
|
+
function replaceSubmitted(state, text) {
|
|
172
|
+
readline.moveCursor(process.stdout, 0, -1); // up onto the submitted-input line
|
|
173
|
+
readline.cursorTo(process.stdout, 0);
|
|
174
|
+
readline.clearLine(process.stdout, 0);
|
|
175
|
+
process.stdout.write(text + '\n');
|
|
176
|
+
state.rl.prompt(true);
|
|
177
|
+
}
|
|
165
178
|
function bubble(who, text, incoming) {
|
|
166
179
|
const time = c.gray(formatTime(Date.now(), loadConfig().preferences.time24h));
|
|
167
180
|
const name = incoming ? c.cyan(who) : c.green('me');
|
|
@@ -233,14 +246,20 @@ async function ensureConnected(state) {
|
|
|
233
246
|
}
|
|
234
247
|
}
|
|
235
248
|
// ---- sending ---------------------------------------------------------------
|
|
236
|
-
async function sendTo(state, jid, name, content, meta) {
|
|
249
|
+
async function sendTo(state, jid, name, content, meta, replaceInput = false) {
|
|
250
|
+
const wasLive = state.status === 'live';
|
|
237
251
|
const conn = await ensureConnected(state);
|
|
238
252
|
if (!conn)
|
|
239
253
|
return;
|
|
240
254
|
try {
|
|
241
255
|
await sendContent(conn.sock, jid, content, meta);
|
|
242
256
|
const echo = meta.type === 'text' ? meta.previewText : `[${meta.type}] ${meta.previewText}`;
|
|
243
|
-
|
|
257
|
+
// Replace the just-typed input with the bubble so it shows once — but only
|
|
258
|
+
// when we were already live (no "connecting…" lines were printed in between).
|
|
259
|
+
if (replaceInput && wasLive)
|
|
260
|
+
replaceSubmitted(state, bubble(name, echo, false));
|
|
261
|
+
else
|
|
262
|
+
printAbove(state, bubble(name, echo, false));
|
|
244
263
|
refreshNames();
|
|
245
264
|
}
|
|
246
265
|
catch (err) {
|
|
@@ -249,16 +268,31 @@ async function sendTo(state, jid, name, content, meta) {
|
|
|
249
268
|
}
|
|
250
269
|
// ---- chat context ----------------------------------------------------------
|
|
251
270
|
function openChat(state, target) {
|
|
252
|
-
|
|
271
|
+
// Resolve against chats, contacts, or a raw number/jid.
|
|
272
|
+
const resolved = resolveTarget(target);
|
|
273
|
+
let chat = resolved ? findChat(resolved.jid) : findChat(target);
|
|
274
|
+
if (!chat && resolved) {
|
|
275
|
+
// Known contact / number with no conversation yet — start one.
|
|
276
|
+
upsertChat({
|
|
277
|
+
jid: resolved.jid,
|
|
278
|
+
name: resolved.name,
|
|
279
|
+
isGroup: resolved.jid.endsWith('@g.us') ? 1 : 0,
|
|
280
|
+
});
|
|
281
|
+
chat = findChat(resolved.jid);
|
|
282
|
+
}
|
|
253
283
|
if (!chat) {
|
|
254
|
-
warn(`No chat matching "${target}". Try /chats or /search.`);
|
|
284
|
+
warn(`No chat or contact matching "${target}". Try /chats or /search.`);
|
|
255
285
|
return;
|
|
256
286
|
}
|
|
257
287
|
state.active = chat;
|
|
258
288
|
setPrompt(state);
|
|
259
289
|
print(c.dim('─'.repeat(40)));
|
|
260
|
-
|
|
261
|
-
print(
|
|
290
|
+
const sub = chatNumber(chat);
|
|
291
|
+
print(c.bold(displayName(chat)) + (sub ? c.dim(` ${sub}`) : ''));
|
|
292
|
+
const msgs = getMessages(chat.jid, 15);
|
|
293
|
+
print(msgs.length
|
|
294
|
+
? renderMessages(msgs, loadConfig().preferences.time24h)
|
|
295
|
+
: c.dim('No messages yet — say hi 👋'));
|
|
262
296
|
print(c.dim(`─── in this chat: type to send · @ opens a file picker to attach · /close ───`));
|
|
263
297
|
}
|
|
264
298
|
// ---- settings --------------------------------------------------------------
|
|
@@ -334,11 +368,7 @@ async function handle(state, raw) {
|
|
|
334
368
|
if (at) {
|
|
335
369
|
try {
|
|
336
370
|
const built = buildMediaContent(at.file, at.caption || undefined);
|
|
337
|
-
await sendTo(state, jid, name, built.content, {
|
|
338
|
-
previewText: at.caption || `[${built.kind}]`,
|
|
339
|
-
type: built.kind,
|
|
340
|
-
mediaPath: built.path,
|
|
341
|
-
});
|
|
371
|
+
await sendTo(state, jid, name, built.content, { previewText: at.caption || `[${built.kind}]`, type: built.kind, mediaPath: built.path }, true);
|
|
342
372
|
}
|
|
343
373
|
catch (err) {
|
|
344
374
|
printError(err);
|
|
@@ -346,7 +376,7 @@ async function handle(state, raw) {
|
|
|
346
376
|
return;
|
|
347
377
|
}
|
|
348
378
|
// Otherwise plain text.
|
|
349
|
-
await sendTo(state, jid, name, { text: line }, { previewText: line, type: 'text' });
|
|
379
|
+
await sendTo(state, jid, name, { text: line }, { previewText: line, type: 'text' }, true);
|
|
350
380
|
return;
|
|
351
381
|
}
|
|
352
382
|
const [cmdRaw, ...args] = tokenize(line);
|
|
@@ -551,11 +581,21 @@ export async function shell() {
|
|
|
551
581
|
const clearLine = () => rl.write(null, { ctrl: true, name: 'u' });
|
|
552
582
|
// Open the chat picker for a command, then insert/run with the chosen chat.
|
|
553
583
|
const chatPickInto = async (cmd) => {
|
|
554
|
-
|
|
584
|
+
// Existing chats first (names resolved via contacts + the LID map)…
|
|
585
|
+
const items = listChats(5000, true).map((ch) => ({
|
|
555
586
|
value: displayName(ch),
|
|
556
587
|
desc: truncate((ch.lastMessage ?? '').replace(/\s+/g, ' '), 30),
|
|
557
588
|
}));
|
|
558
|
-
|
|
589
|
+
// …then every other contact, so nobody is missing. Dedupe by name so a
|
|
590
|
+
// contact already shown as a chat doesn't appear twice.
|
|
591
|
+
const seen = new Set(items.map((it) => it.value.toLowerCase()));
|
|
592
|
+
for (const ct of listContacts(50000)) {
|
|
593
|
+
if (ct.name && !seen.has(ct.name.toLowerCase())) {
|
|
594
|
+
items.push({ value: ct.name, desc: 'contact' });
|
|
595
|
+
seen.add(ct.name.toLowerCase());
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
const chat = await pickFromList('Select a chat or contact', items, '');
|
|
559
599
|
clearLine();
|
|
560
600
|
if (!chat)
|
|
561
601
|
return; // cancelled → empty line
|
|
@@ -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