cli-whatsapp 0.1.1 → 0.1.2

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.
Files changed (45) hide show
  1. package/bin/wa.js +4 -3
  2. package/dist/cli/commands/auth.js +220 -0
  3. package/dist/cli/commands/export.js +51 -0
  4. package/dist/cli/commands/filepicker.js +232 -0
  5. package/dist/cli/commands/group.js +139 -0
  6. package/dist/cli/commands/manage.js +41 -0
  7. package/dist/cli/commands/media.js +105 -0
  8. package/dist/cli/commands/misc.js +112 -0
  9. package/dist/cli/commands/read.js +79 -0
  10. package/dist/cli/commands/send.js +137 -0
  11. package/dist/cli/commands/shell.js +611 -0
  12. package/dist/cli/commands/sync.js +67 -0
  13. package/dist/cli/index.js +174 -0
  14. package/dist/config/index.js +59 -0
  15. package/{src/db/schema.ts → dist/db/schema.js} +0 -1
  16. package/dist/db/sqlite.js +237 -0
  17. package/dist/utils/format.js +89 -0
  18. package/dist/utils/jid.js +40 -0
  19. package/dist/utils/notify.js +29 -0
  20. package/dist/utils/ui.js +29 -0
  21. package/dist/whatsapp/client.js +157 -0
  22. package/dist/whatsapp/events.js +161 -0
  23. package/dist/whatsapp/logger.js +52 -0
  24. package/package.json +5 -3
  25. package/src/cli/commands/auth.ts +0 -236
  26. package/src/cli/commands/export.ts +0 -60
  27. package/src/cli/commands/filepicker.ts +0 -247
  28. package/src/cli/commands/group.ts +0 -139
  29. package/src/cli/commands/manage.ts +0 -46
  30. package/src/cli/commands/media.ts +0 -125
  31. package/src/cli/commands/misc.ts +0 -113
  32. package/src/cli/commands/read.ts +0 -99
  33. package/src/cli/commands/send.ts +0 -201
  34. package/src/cli/commands/shell.ts +0 -596
  35. package/src/cli/commands/sync.ts +0 -80
  36. package/src/cli/index.ts +0 -198
  37. package/src/config/index.ts +0 -92
  38. package/src/db/sqlite.ts +0 -354
  39. package/src/utils/format.ts +0 -102
  40. package/src/utils/jid.ts +0 -51
  41. package/src/utils/notify.ts +0 -29
  42. package/src/utils/ui.ts +0 -35
  43. package/src/whatsapp/client.ts +0 -207
  44. package/src/whatsapp/events.ts +0 -166
  45. package/src/whatsapp/logger.ts +0 -71
@@ -1,102 +0,0 @@
1
- import type { ChatRow, MessageRow } from '../db/sqlite.ts';
2
-
3
- // Minimal ANSI styling — no dependency, respects NO_COLOR.
4
- const useColor = !process.env.NO_COLOR && process.stdout.isTTY;
5
- const wrap = (code: string) => (s: string | number) =>
6
- useColor ? `\x1b[${code}m${s}\x1b[0m` : String(s);
7
- // 24-bit truecolor for brand colors (modern terminals). Falls back to plain
8
- // text under NO_COLOR / non-TTY.
9
- const rgb = (r: number, g: number, b: number) => (s: string | number) =>
10
- useColor ? `\x1b[38;2;${r};${g};${b}m${s}\x1b[0m` : String(s);
11
-
12
- // WhatsApp brand palette.
13
- export const BRAND = {
14
- green: [37, 211, 102] as const, // #25D366 signature green
15
- teal: [18, 140, 126] as const, // #128C7E
16
- darkTeal: [7, 94, 84] as const, // #075E54
17
- };
18
-
19
- export const c = {
20
- bold: wrap('1'),
21
- dim: wrap('2'),
22
- green: rgb(...BRAND.green), // WhatsApp green (used for "me", ✓, accents)
23
- teal: rgb(...BRAND.teal),
24
- darkTeal: rgb(...BRAND.darkTeal),
25
- cyan: wrap('36'),
26
- yellow: wrap('33'),
27
- magenta: wrap('35'),
28
- red: wrap('31'),
29
- gray: wrap('90'),
30
- };
31
-
32
- /** Human-friendly timestamp. Absolute time for today, date otherwise. */
33
- export function formatTime(ms: number, time24h = true): string {
34
- if (!ms) return '';
35
- const d = new Date(ms);
36
- const now = new Date();
37
- const sameDay =
38
- d.getFullYear() === now.getFullYear() &&
39
- d.getMonth() === now.getMonth() &&
40
- d.getDate() === now.getDate();
41
- const time = d.toLocaleTimeString(undefined, {
42
- hour: '2-digit',
43
- minute: '2-digit',
44
- hour12: !time24h,
45
- });
46
- if (sameDay) return time;
47
- const date = d.toLocaleDateString(undefined, {
48
- month: 'short',
49
- day: 'numeric',
50
- });
51
- return `${date} ${time}`;
52
- }
53
-
54
- export function displayName(chat: ChatRow): string {
55
- if (chat.name) return chat.name;
56
- // Fall back to the phone number portion of a direct jid.
57
- const num = chat.jid.split('@')[0];
58
- return chat.isGroup ? `Group ${num}` : `+${num}`;
59
- }
60
-
61
- /** Render the numbered chat list shown by `wa chats`. */
62
- export function renderChatList(chats: ChatRow[], time24h = true): string {
63
- if (chats.length === 0) {
64
- return c.dim('No chats yet. Run `wa sync` after logging in.');
65
- }
66
- const width = String(chats.length).length;
67
- return chats
68
- .map((chat, i) => {
69
- const idx = c.dim(String(i + 1).padStart(width) + '.');
70
- const name = c.bold(displayName(chat));
71
- const pin = chat.pinned ? c.yellow(' 📌') : '';
72
- const unread =
73
- chat.unreadCount > 0 ? c.green(` (${chat.unreadCount})`) : '';
74
- const time = c.gray(formatTime(chat.lastTimestamp, time24h));
75
- const preview = c.dim(truncate(chat.lastMessage ?? '', 48));
76
- return `${idx} ${name}${pin}${unread} ${time}\n ${preview}`;
77
- })
78
- .join('\n');
79
- }
80
-
81
- /** Render a conversation transcript for `wa open` / `wa history`. */
82
- export function renderMessages(
83
- messages: MessageRow[],
84
- time24h = true,
85
- ): string {
86
- if (messages.length === 0) return c.dim('No messages.');
87
- return messages
88
- .map((m) => {
89
- const time = c.gray(formatTime(m.timestamp, time24h));
90
- const who = m.fromMe ? c.green('me') : c.cyan(m.sender ?? 'unknown');
91
- const body =
92
- m.text ?? (m.type ? c.dim(`[${m.type}]`) : c.dim('[no text]'));
93
- const id = c.dim(`#${m.id}`);
94
- return `${time} ${who}: ${body} ${id}`;
95
- })
96
- .join('\n');
97
- }
98
-
99
- export function truncate(s: string, n: number): string {
100
- const oneLine = s.replace(/\s+/g, ' ').trim();
101
- return oneLine.length > n ? oneLine.slice(0, n - 1) + '…' : oneLine;
102
- }
package/src/utils/jid.ts DELETED
@@ -1,51 +0,0 @@
1
- import { findChat, findContact, type ChatRow } from '../db/sqlite.ts';
2
-
3
- /** A raw phone number like +1 (415) 555-0000 → 14155550000@s.whatsapp.net */
4
- export function phoneToJid(input: string): string | null {
5
- const digits = input.replace(/[^\d]/g, '');
6
- if (digits.length < 7) return null;
7
- return `${digits}@s.whatsapp.net`;
8
- }
9
-
10
- export interface ResolvedTarget {
11
- jid: string;
12
- name: string;
13
- /** true when we matched a known chat, false when built from a raw number. */
14
- known: boolean;
15
- }
16
-
17
- /**
18
- * Resolve a user-supplied chat target to a jid.
19
- * Order: exact jid → known chat (name/fuzzy) → raw phone number.
20
- * Returns null when nothing plausible matches.
21
- */
22
- export function resolveTarget(query: string): ResolvedTarget | null {
23
- const q = query.trim();
24
- if (!q) return null;
25
-
26
- // Already a full jid (ends with a WhatsApp domain, no spaces). Note: a
27
- // contact name can contain "@" (e.g. "Vishwas @Mobius"), so we must match the
28
- // domain suffix, not merely the presence of "@".
29
- if (/^\S+@(s\.whatsapp\.net|g\.us|lid|c\.us|broadcast)$/.test(q)) {
30
- return { jid: q, name: q, known: false };
31
- }
32
-
33
- const chat: ChatRow | null = findChat(q);
34
- if (chat) {
35
- return { jid: chat.jid, name: chat.name ?? chat.jid, known: true };
36
- }
37
-
38
- // Known contact with no existing chat yet (e.g. first message to someone).
39
- const contact = findContact(q);
40
- if (contact) {
41
- return { jid: contact.jid, name: contact.name ?? contact.jid, known: true };
42
- }
43
-
44
- // Looks like a phone number.
45
- if (/^[+\d][\d\s()\-]{5,}$/.test(q)) {
46
- const jid = phoneToJid(q);
47
- if (jid) return { jid, name: q, known: false };
48
- }
49
-
50
- return null;
51
- }
@@ -1,29 +0,0 @@
1
- import { execFile } from 'node:child_process';
2
- import { platform } from 'node:os';
3
-
4
- /**
5
- * Fire a native desktop notification. Best-effort and non-blocking: shells out
6
- * to the platform notifier (macOS `osascript`, Linux `notify-send`). Any
7
- * failure (tool missing, no display) is silently ignored — notifications must
8
- * never break the CLI.
9
- */
10
- export function notify(title: string, body: string): void {
11
- try {
12
- const p = platform();
13
- if (p === 'darwin') {
14
- const esc = (s: string) => s.replace(/[\\"]/g, '\\$&');
15
- const script = `display notification "${esc(clip(body))}" with title "${esc(clip(title))}"`;
16
- execFile('osascript', ['-e', script], () => {});
17
- } else if (p === 'linux') {
18
- execFile('notify-send', ['-a', 'WhatsApp CLI', clip(title), clip(body)], () => {});
19
- }
20
- // Windows: no reliable dependency-free path; skipped intentionally.
21
- } catch {
22
- /* notifications are best-effort */
23
- }
24
- }
25
-
26
- function clip(s: string): string {
27
- const oneLine = s.replace(/\s+/g, ' ').trim();
28
- return oneLine.length > 120 ? oneLine.slice(0, 119) + '…' : oneLine;
29
- }
package/src/utils/ui.ts DELETED
@@ -1,35 +0,0 @@
1
- import { c } from './format.ts';
2
-
3
- export function ok(msg: string): void {
4
- process.stdout.write(`${c.green('✓')} ${msg}\n`);
5
- }
6
-
7
- export function info(msg: string): void {
8
- process.stdout.write(`${c.cyan('›')} ${msg}\n`);
9
- }
10
-
11
- export function warn(msg: string): void {
12
- process.stderr.write(`${c.yellow('!')} ${msg}\n`);
13
- }
14
-
15
- /** A user-facing, expected error (bad input, no session, etc.). */
16
- export class AppError extends Error {}
17
-
18
- /**
19
- * Abort the current command with a user-facing message. Throws an AppError so
20
- * callers decide how to handle it: the CLI prints it and exits non-zero; the
21
- * interactive shell prints it and keeps the session alive.
22
- */
23
- export function fail(msg: string): never {
24
- throw new AppError(msg);
25
- }
26
-
27
- /** Render an error the way the CLI/shell should display it. */
28
- export function printError(err: unknown): void {
29
- const msg = err instanceof Error ? err.message : String(err);
30
- process.stderr.write(`${c.red('✗')} ${msg}\n`);
31
- }
32
-
33
- export function print(msg = ''): void {
34
- process.stdout.write(msg + '\n');
35
- }
@@ -1,207 +0,0 @@
1
- import makeWASocket, {
2
- useMultiFileAuthState,
3
- fetchLatestBaileysVersion,
4
- makeCacheableSignalKeyStore,
5
- Browsers,
6
- DisconnectReason,
7
- } from '@whiskeysockets/baileys';
8
- import type { WASocket } from '@whiskeysockets/baileys';
9
- import { existsSync, readFileSync, rmSync, mkdirSync } from 'node:fs';
10
- import { join } from 'node:path';
11
- import { PATHS, ensureDirs } from '../config/index.ts';
12
- import { logger } from './logger.ts';
13
-
14
- export type Socket = WASocket;
15
-
16
- export interface ConnectOptions {
17
- /** Called with each QR string so the caller can render it (login only). */
18
- onQR?: (qr: string) => void;
19
- /** Pull full message history on connect (used by `sync`). */
20
- syncFullHistory?: boolean;
21
- /** Reject instead of showing a QR when no valid session exists. */
22
- requireSession?: boolean;
23
- /**
24
- * Log in with a short pairing code instead of a QR. Provide the phone number
25
- * in international format (digits only). The code is delivered via onPairingCode.
26
- */
27
- pairingPhone?: string;
28
- onPairingCode?: (code: string) => void;
29
- }
30
-
31
- export interface Connection {
32
- sock: Socket;
33
- /** Persist updated credentials to disk. */
34
- saveCreds: () => Promise<void>;
35
- /** Resolves when the socket reaches the "open" state. */
36
- ready: Promise<void>;
37
- /** Cleanly end the socket. */
38
- close: () => void;
39
- }
40
-
41
- /** Extract the numeric status code from a Baileys disconnect error. */
42
- function statusCode(err: unknown): number | undefined {
43
- return (err as { output?: { statusCode?: number } })?.output?.statusCode;
44
- }
45
-
46
- /**
47
- * Serialized chain of pending credential writes. WhatsApp streams `creds.update`
48
- * events whose `saveCreds()` writes to disk asynchronously; exiting the process
49
- * mid-write corrupts the session. Callers must `await whenWritesSettled()`
50
- * before `process.exit()`.
51
- */
52
- let pendingWrites: Promise<void> = Promise.resolve();
53
-
54
- export function whenWritesSettled(): Promise<void> {
55
- return pendingWrites;
56
- }
57
-
58
- /**
59
- * A session counts as linked when creds.json exists, parses, and carries the
60
- * account identity (`me.id`) — that's set only after a device is fully linked
61
- * and it persists. (The `registered` flag is unreliable: it can remain false
62
- * after a successful QR link due to write ordering across the 515 reconnect.)
63
- * A missing, corrupt, or half-written creds file has no `me.id` → "not linked".
64
- */
65
- export function hasSession(): boolean {
66
- const p = join(PATHS.sessions, 'creds.json');
67
- if (!existsSync(p)) return false;
68
- try {
69
- const creds = JSON.parse(readFileSync(p, 'utf8'));
70
- return !!creds?.me?.id || creds?.registered === true;
71
- } catch {
72
- return false;
73
- }
74
- }
75
-
76
- /** Delete the local session (used when it's dead / logged out server-side). */
77
- export function clearSession(): void {
78
- rmSync(PATHS.sessions, { recursive: true, force: true });
79
- mkdirSync(PATHS.sessions, { recursive: true });
80
- }
81
-
82
- /**
83
- * Establish a connection with a single QR/open lifecycle. Reconnection on
84
- * transient drops is handled by callers via {@link runWithConnection}, which
85
- * recreates the connection when appropriate.
86
- */
87
- export async function connect(opts: ConnectOptions = {}): Promise<Connection> {
88
- ensureDirs();
89
-
90
- if (opts.requireSession && !hasSession()) {
91
- throw new Error('Not logged in. Run `wa login` first.');
92
- }
93
-
94
- const { state, saveCreds } = await useMultiFileAuthState(PATHS.sessions);
95
- const { version } = await fetchLatestBaileysVersion();
96
-
97
- const sock = makeWASocket({
98
- version,
99
- logger: logger as never,
100
- printQRInTerminal: false,
101
- auth: {
102
- creds: state.creds,
103
- keys: makeCacheableSignalKeyStore(state.keys, logger as never),
104
- },
105
- browser: Browsers.macOS('WhatsApp CLI'),
106
- syncFullHistory: opts.syncFullHistory ?? false,
107
- markOnlineOnConnect: false,
108
- generateHighQualityLinkPreview: false,
109
- });
110
-
111
- // Serialize creds writes and track them so the process never exits mid-write.
112
- sock.ev.on('creds.update', () => {
113
- pendingWrites = pendingWrites.then(() => saveCreds()).catch(() => {});
114
- });
115
-
116
- let resolveReady!: () => void;
117
- let rejectReady!: (err: Error) => void;
118
- const ready = new Promise<void>((res, rej) => {
119
- resolveReady = res;
120
- rejectReady = rej;
121
- });
122
-
123
- let pairingRequested = false;
124
- sock.ev.on('connection.update', (update) => {
125
- const { connection, lastDisconnect, qr } = update;
126
- if (qr) {
127
- if (opts.pairingPhone) {
128
- // Pairing-code mode: request the code once on the first `qr` (which
129
- // signals the socket is ready to pair). Ignore all later qr refreshes —
130
- // never render a QR, and never re-request (that would change the code).
131
- if (!pairingRequested) {
132
- pairingRequested = true;
133
- void sock
134
- .requestPairingCode(opts.pairingPhone)
135
- .then((code) => opts.onPairingCode?.(code))
136
- .catch((e) =>
137
- rejectReady(new Error(`PAIRING_FAILED:${(e as Error).message}`)),
138
- );
139
- }
140
- } else if (opts.onQR) {
141
- opts.onQR(qr);
142
- }
143
- }
144
- if (connection === 'open') {
145
- resolveReady();
146
- } else if (connection === 'close') {
147
- const code = statusCode(lastDisconnect?.error);
148
- const loggedOut = code === DisconnectReason.loggedOut;
149
- const err = new Error(
150
- loggedOut
151
- ? 'LOGGED_OUT'
152
- : `CONNECTION_CLOSED:${code ?? 'unknown'}`,
153
- );
154
- rejectReady(err);
155
- }
156
- });
157
-
158
- return {
159
- sock,
160
- saveCreds,
161
- ready,
162
- close: () => {
163
- try {
164
- sock.end(undefined);
165
- } catch {
166
- /* already closed */
167
- }
168
- },
169
- };
170
- }
171
-
172
- /**
173
- * Run a task with an open connection, auto-reconnecting on transient drops.
174
- * Rejects on logout or after exhausting retries. Always closes the socket.
175
- */
176
- export async function runWithConnection<T>(
177
- task: (conn: Connection) => Promise<T>,
178
- opts: ConnectOptions & { maxRetries?: number } = {},
179
- ): Promise<T> {
180
- const maxRetries = opts.maxRetries ?? 3;
181
- let attempt = 0;
182
- // eslint-disable-next-line no-constant-condition
183
- while (true) {
184
- const conn = await connect(opts);
185
- try {
186
- await conn.ready;
187
- const result = await task(conn);
188
- return result;
189
- } catch (err) {
190
- const msg = (err as Error).message ?? '';
191
- if (msg === 'LOGGED_OUT') {
192
- conn.close();
193
- throw new Error(
194
- 'Session logged out. Run `wa logout` then `wa login` again.',
195
- );
196
- }
197
- if (attempt++ >= maxRetries) {
198
- conn.close();
199
- throw err;
200
- }
201
- // brief backoff before reconnecting
202
- await new Promise((r) => setTimeout(r, 1000 * attempt));
203
- } finally {
204
- conn.close();
205
- }
206
- }
207
- }
@@ -1,166 +0,0 @@
1
- import { proto } from '@whiskeysockets/baileys';
2
- import type { WAMessage } from '@whiskeysockets/baileys';
3
- import type { Socket } from './client.ts';
4
- import { insertMessage, upsertChat, upsertContact } from '../db/sqlite.ts';
5
-
6
- /** A value that is either a plain number or a protobuf Long (has toNumber). */
7
- type NumberLike = number | { toNumber: () => number } | null | undefined;
8
-
9
- /** Baileys timestamps are seconds (sometimes Long); normalize to ms. */
10
- export function toMs(ts: NumberLike): number {
11
- if (ts == null) return 0;
12
- const n = typeof ts === 'number' ? ts : ts.toNumber();
13
- if (!Number.isFinite(n) || n <= 0) return 0;
14
- return n < 1e12 ? n * 1000 : n; // treat >=1e12 as already-ms
15
- }
16
-
17
- export function isGroupJid(jid: string | null | undefined): boolean {
18
- return !!jid && jid.endsWith('@g.us');
19
- }
20
-
21
- /** Best-effort plain-text extraction from any WhatsApp message content. */
22
- export function extractText(message: proto.IMessage | null | undefined): string {
23
- if (!message) return '';
24
- return (
25
- message.conversation ||
26
- message.extendedTextMessage?.text ||
27
- message.imageMessage?.caption ||
28
- message.videoMessage?.caption ||
29
- message.documentMessage?.caption ||
30
- message.documentWithCaptionMessage?.message?.documentMessage?.caption ||
31
- message.buttonsResponseMessage?.selectedDisplayText ||
32
- message.listResponseMessage?.title ||
33
- message.templateButtonReplyMessage?.selectedDisplayText ||
34
- (message.reactionMessage?.text
35
- ? `reacted ${message.reactionMessage.text}`
36
- : '') ||
37
- ''
38
- );
39
- }
40
-
41
- /** Classify a message by its dominant content type (for filtering/media). */
42
- export function messageType(message: proto.IMessage | null | undefined): string {
43
- if (!message) return 'unknown';
44
- if (message.conversation || message.extendedTextMessage) return 'text';
45
- if (message.imageMessage) return 'image';
46
- if (message.videoMessage) return 'video';
47
- if (message.audioMessage) return 'audio';
48
- if (message.documentMessage || message.documentWithCaptionMessage)
49
- return 'document';
50
- if (message.stickerMessage) return 'sticker';
51
- if (message.locationMessage) return 'location';
52
- if (message.contactMessage) return 'contact';
53
- if (message.reactionMessage) return 'reaction';
54
- return 'other';
55
- }
56
-
57
- /** Persist a single Baileys message into the local cache. */
58
- export function persistMessage(msg: WAMessage): void {
59
- const jid = msg.key.remoteJid;
60
- if (!jid || jid === 'status@broadcast') return;
61
-
62
- const text = extractText(msg.message);
63
- const type = messageType(msg.message);
64
- const ts = toMs(msg.messageTimestamp as number);
65
- const fromMe = !!msg.key.fromMe;
66
- const sender = fromMe ? 'me' : msg.pushName || msg.key.participant || jid;
67
-
68
- // For media, keep the raw content so `wa download` can decrypt it later.
69
- const isMedia = ['image', 'video', 'audio', 'document', 'sticker'].includes(type);
70
- let raw: string | null = null;
71
- if (isMedia && msg.message) {
72
- try {
73
- // Protobuf-encode so binary fields (mediaKey, etc.) survive round-trips.
74
- raw = Buffer.from(proto.Message.encode(msg.message).finish()).toString('base64');
75
- } catch {
76
- raw = null;
77
- }
78
- }
79
-
80
- insertMessage({
81
- chatJid: jid,
82
- messageId: msg.key.id ?? `${ts}`,
83
- sender,
84
- text: text || null,
85
- timestamp: ts,
86
- status: undefined,
87
- type,
88
- fromMe,
89
- raw,
90
- });
91
-
92
- // Keep the chat row's "last message" preview fresh.
93
- upsertChat({
94
- jid,
95
- name: isGroupJid(jid) ? undefined : !fromMe ? msg.pushName || undefined : undefined,
96
- lastMessage: text || `[${type}]`,
97
- lastTimestamp: ts,
98
- isGroup: isGroupJid(jid) ? 1 : 0,
99
- });
100
-
101
- // Record the direct contact's push name when we learn it.
102
- if (!isGroupJid(jid) && !fromMe && msg.pushName) {
103
- upsertContact({ jid, name: msg.pushName });
104
- }
105
- }
106
-
107
- /**
108
- * Register all persistence handlers on a live socket. Used by `sync` (batch
109
- * history) and `watch` (live stream). Returns nothing; handlers persist to DB.
110
- */
111
- export function registerPersistence(
112
- sock: Socket,
113
- hooks: { onMessage?: (msg: WAMessage) => void } = {},
114
- ): void {
115
- // Live + recent messages.
116
- sock.ev.on('messages.upsert', ({ messages }) => {
117
- for (const msg of messages) {
118
- persistMessage(msg);
119
- hooks.onMessage?.(msg);
120
- }
121
- });
122
-
123
- // Bulk history sync delivered after login / on demand.
124
- sock.ev.on('messaging-history.set', ({ chats, contacts, messages }) => {
125
- for (const c of chats) {
126
- if (!c.id) continue;
127
- upsertChat({
128
- jid: c.id,
129
- name: c.name ?? undefined,
130
- unreadCount: c.unreadCount ?? 0,
131
- archived: c.archived ? 1 : 0,
132
- pinned: c.pinned ? 1 : 0,
133
- lastTimestamp: toMs(c.conversationTimestamp as number),
134
- isGroup: isGroupJid(c.id) ? 1 : 0,
135
- });
136
- }
137
- for (const ct of contacts) {
138
- if (ct.id) upsertContact({ jid: ct.id, name: ct.name ?? ct.notify ?? undefined });
139
- }
140
- for (const msg of messages) persistMessage(msg);
141
- });
142
-
143
- // Chat metadata updates.
144
- sock.ev.on('chats.upsert', (chats) => {
145
- for (const c of chats) {
146
- if (!c.id) continue;
147
- upsertChat({
148
- jid: c.id,
149
- name: c.name ?? undefined,
150
- unreadCount: c.unreadCount ?? 0,
151
- archived: c.archived ? 1 : 0,
152
- pinned: c.pinned ? 1 : 0,
153
- isGroup: isGroupJid(c.id) ? 1 : 0,
154
- });
155
- }
156
- });
157
-
158
- // Contact names.
159
- const onContacts = (contacts: { id: string; name?: string | null; notify?: string | null }[]) => {
160
- for (const ct of contacts) {
161
- if (ct.id) upsertContact({ jid: ct.id, name: ct.name ?? ct.notify ?? undefined });
162
- }
163
- };
164
- sock.ev.on('contacts.upsert', onContacts);
165
- sock.ev.on('contacts.update', onContacts as never);
166
- }
@@ -1,71 +0,0 @@
1
- /**
2
- * Baileys expects a pino-compatible logger. We keep the terminal clean by
3
- * default and only surface logs when WA_DEBUG is set. This avoids pulling in
4
- * pino itself, keeping dependencies minimal.
5
- */
6
- export interface BaileysLogger {
7
- level: string;
8
- trace: (...a: unknown[]) => void;
9
- debug: (...a: unknown[]) => void;
10
- info: (...a: unknown[]) => void;
11
- warn: (...a: unknown[]) => void;
12
- error: (...a: unknown[]) => void;
13
- fatal: (...a: unknown[]) => void;
14
- child: () => BaileysLogger;
15
- }
16
-
17
- const DEBUG = !!process.env.WA_DEBUG;
18
-
19
- function make(level: string): BaileysLogger {
20
- const emit =
21
- (tag: string) =>
22
- (...args: unknown[]) => {
23
- if (!DEBUG) return;
24
- // Baileys calls logger.error(obj, msg); print compactly.
25
- process.stderr.write(`[wa:${tag}] ${args.map(fmt).join(' ')}\n`);
26
- };
27
- return {
28
- level,
29
- trace: emit('trace'),
30
- debug: emit('debug'),
31
- info: emit('info'),
32
- warn: emit('warn'),
33
- error: emit('error'),
34
- fatal: emit('fatal'),
35
- child: () => make(level),
36
- };
37
- }
38
-
39
- function fmt(v: unknown): string {
40
- if (typeof v === 'string') return v;
41
- try {
42
- return JSON.stringify(v);
43
- } catch {
44
- return String(v);
45
- }
46
- }
47
-
48
- export const logger: BaileysLogger = make(DEBUG ? 'debug' : 'silent');
49
-
50
- /**
51
- * Baileys' vendored libsignal logs session churn via `console.log`/`console.error`
52
- * directly, bypassing the logger above and cluttering the terminal (e.g.
53
- * "Closing session: SessionEntry { … }"). Filter those specific lines out
54
- * unless debugging. Call once at startup.
55
- */
56
- // libsignal (node_modules/libsignal/src/session_record.js) logs session churn
57
- // directly via console.info/warn/error, bypassing the logger above.
58
- const NOISE =
59
- /^(Closing (open |stale )?session|Opening session|Session already (closed|open)|Migrating session|Removing old closed session|V1 session storage migration|Session error|Bad MAC|SessionEntry)/;
60
-
61
- export function installConsoleFilter(): void {
62
- if (DEBUG) return;
63
- for (const method of ['log', 'info', 'error', 'warn'] as const) {
64
- const original = console[method].bind(console);
65
- console[method] = (...args: unknown[]) => {
66
- const first = args[0];
67
- if (typeof first === 'string' && NOISE.test(first)) return;
68
- original(...args);
69
- };
70
- }
71
- }