@link-os/whatsapp 0.1.0 → 0.1.1

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 (54) hide show
  1. package/dist/client.d.ts +24 -0
  2. package/dist/client.d.ts.map +1 -1
  3. package/dist/client.js +261 -64
  4. package/dist/client.js.map +1 -1
  5. package/dist/normalize.d.ts +17 -0
  6. package/dist/normalize.d.ts.map +1 -0
  7. package/dist/normalize.js +61 -0
  8. package/dist/normalize.js.map +1 -0
  9. package/package.json +2 -2
  10. package/src/client.ts +291 -67
  11. package/src/normalize.ts +70 -0
  12. package/tsconfig.tsbuildinfo +1 -1
  13. package/auth_info_baileys/app-state-sync-key-AAAAANpA.json +0 -1
  14. package/auth_info_baileys/app-state-sync-key-AAAAANpB.json +0 -1
  15. package/auth_info_baileys/app-state-sync-key-AAAAANpC.json +0 -1
  16. package/auth_info_baileys/app-state-sync-key-AAAAANpD.json +0 -1
  17. package/auth_info_baileys/app-state-sync-key-AAAAANpE.json +0 -1
  18. package/auth_info_baileys/app-state-sync-key-AAAAANpF.json +0 -1
  19. package/auth_info_baileys/app-state-sync-key-AAAAANpG.json +0 -1
  20. package/auth_info_baileys/app-state-sync-key-AAAAANpI.json +0 -1
  21. package/auth_info_baileys/app-state-sync-key-AAAAANpJ.json +0 -1
  22. package/auth_info_baileys/app-state-sync-key-ADMAANpI.json +0 -1
  23. package/auth_info_baileys/app-state-sync-version-regular_low.json +0 -1
  24. package/auth_info_baileys/creds.json +0 -1
  25. package/auth_info_baileys/pre-key-1.json +0 -1
  26. package/auth_info_baileys/pre-key-10.json +0 -1
  27. package/auth_info_baileys/pre-key-11.json +0 -1
  28. package/auth_info_baileys/pre-key-12.json +0 -1
  29. package/auth_info_baileys/pre-key-13.json +0 -1
  30. package/auth_info_baileys/pre-key-14.json +0 -1
  31. package/auth_info_baileys/pre-key-15.json +0 -1
  32. package/auth_info_baileys/pre-key-16.json +0 -1
  33. package/auth_info_baileys/pre-key-17.json +0 -1
  34. package/auth_info_baileys/pre-key-18.json +0 -1
  35. package/auth_info_baileys/pre-key-19.json +0 -1
  36. package/auth_info_baileys/pre-key-2.json +0 -1
  37. package/auth_info_baileys/pre-key-20.json +0 -1
  38. package/auth_info_baileys/pre-key-22.json +0 -1
  39. package/auth_info_baileys/pre-key-23.json +0 -1
  40. package/auth_info_baileys/pre-key-24.json +0 -1
  41. package/auth_info_baileys/pre-key-25.json +0 -1
  42. package/auth_info_baileys/pre-key-26.json +0 -1
  43. package/auth_info_baileys/pre-key-27.json +0 -1
  44. package/auth_info_baileys/pre-key-28.json +0 -1
  45. package/auth_info_baileys/pre-key-29.json +0 -1
  46. package/auth_info_baileys/pre-key-3.json +0 -1
  47. package/auth_info_baileys/pre-key-30.json +0 -1
  48. package/auth_info_baileys/pre-key-4.json +0 -1
  49. package/auth_info_baileys/pre-key-5.json +0 -1
  50. package/auth_info_baileys/pre-key-6.json +0 -1
  51. package/auth_info_baileys/pre-key-7.json +0 -1
  52. package/auth_info_baileys/pre-key-8.json +0 -1
  53. package/auth_info_baileys/pre-key-9.json +0 -1
  54. package/auth_info_baileys/session-918477881793.0.json +0 -1
package/src/client.ts CHANGED
@@ -10,12 +10,21 @@ import qrcode from 'qrcode-terminal';
10
10
  import pino from 'pino';
11
11
 
12
12
  import type { PlatformClient, UnifiedMessage, Platform } from '@link-os/types';
13
+ import { normalizeWhatsAppTarget } from './normalize.js';
13
14
 
14
15
  const VERSION = '0.1.0';
15
16
 
17
+ export interface AllowedContext {
18
+ allowedJid: string; // The JID is stored here
19
+ name: string;
20
+ type: string;
21
+ image?: string;
22
+ }
23
+
16
24
  export interface WhatsAppClientOptions {
17
25
  sessionId: string;
18
26
  authDir?: string;
27
+ allowedContexts?: AllowedContext[];
19
28
  }
20
29
 
21
30
  export class WhatsAppClient implements PlatformClient {
@@ -27,12 +36,28 @@ export class WhatsAppClient implements PlatformClient {
27
36
  private messageHandler?: (message: UnifiedMessage) => Promise<void>;
28
37
  private statusHandler?: (status: { type: string; data?: any }) => void;
29
38
  private stopped = false;
39
+ private isStarting = false;
40
+ private reconnectAttempts = 0;
41
+ private maxReconnectAttempts = 3;
42
+ private resetTimeout: NodeJS.Timeout | null = null;
43
+ private allowedJids: string[] = [];
44
+ // TODO: Implement persistent storage to save contacts across restarts.
45
+ // Currently, contacts are only in-memory and lost on restart.
30
46
 
31
47
  constructor(options: WhatsAppClientOptions) {
32
48
  this.options = {
33
49
  authDir: options.authDir || `.auth/whatsapp/${options.sessionId}`,
34
50
  ...options
35
51
  };
52
+
53
+ // Initialize allowedJids from allowedContexts
54
+ if (options.allowedContexts && options.allowedContexts.length > 0) {
55
+ this.allowedJids = options.allowedContexts
56
+ .map(ctx => normalizeWhatsAppTarget(ctx.allowedJid))
57
+ .filter((jid): jid is string => !!jid);
58
+ } else {
59
+ this.allowedJids = [];
60
+ }
36
61
  }
37
62
 
38
63
  on(event: 'message' | 'status', handler: any): void {
@@ -44,78 +69,130 @@ export class WhatsAppClient implements PlatformClient {
44
69
  }
45
70
 
46
71
  async start(): Promise<void> {
72
+ if (this.isStarting) {
73
+ return;
74
+ }
75
+ this.isStarting = true;
47
76
  this.stopped = false;
48
- const { state, saveCreds } = await useMultiFileAuthState(this.options.authDir!);
49
- const { version } = await fetchLatestBaileysVersion();
50
-
51
- console.log(`Using Baileys version: ${version.join('.')}`);
52
-
53
- this.sock = (makeWASocket as any).default ? (makeWASocket as any).default({
54
- auth: {
55
- creds: state.creds,
56
- keys: (makeCacheableSignalKeyStore as any)(state.keys, this.logger),
57
- },
58
- version,
59
- logger: this.logger,
60
- printQRInTerminal: false,
61
- browser: ['linkos', 'cli', VERSION],
62
- syncFullHistory: false,
63
- markOnlineOnConnect: false,
64
- shouldIgnoreJid: (jid: string) => jid.includes('@broadcast') || jid.includes('@newsletter')
65
- }) : (makeWASocket as any)({
66
- auth: {
67
- creds: state.creds,
68
- keys: (makeCacheableSignalKeyStore as any)(state.keys, this.logger),
69
- },
70
- version,
71
- logger: this.logger,
72
- printQRInTerminal: false,
73
- browser: ['linkos', 'cli', VERSION],
74
- syncFullHistory: false,
75
- markOnlineOnConnect: false,
76
- shouldIgnoreJid: (jid: string) => jid.includes('@broadcast') || jid.includes('@newsletter')
77
- });
78
77
 
79
- if (this.sock.ws && typeof this.sock.ws.on === 'function') {
80
- this.sock.ws.on('error', (err: Error) => {
81
- console.error('WebSocket error:', err.message);
82
- });
78
+ // Clear any pending reset
79
+ if (this.resetTimeout) {
80
+ clearTimeout(this.resetTimeout);
81
+ this.resetTimeout = null;
83
82
  }
84
83
 
85
- this.sock.ev.on('connection.update', async (update: any) => {
86
- const { connection, lastDisconnect, qr } = update;
84
+ try {
85
+ console.log(`[WhatsAppClient] Initializing auth state for ${this.options.sessionId}...`);
86
+ const { state, saveCreds } = await useMultiFileAuthState(this.options.authDir!);
87
+ console.log(`[WhatsAppClient] Fetching latest Baileys version...`);
88
+ const { version } = await fetchLatestBaileysVersion();
87
89
 
88
- if (qr) {
89
- console.log('\n📱 Scan this QR code with WhatsApp (Linked Devices):\n');
90
- qrcode.generate(qr, { small: true });
91
- if (this.statusHandler) {
92
- this.statusHandler({ type: 'qr', data: qr });
93
- }
90
+ console.log(`Using Baileys version: ${version.join('.')}`);
91
+ // ... (rest of start)
92
+ if (this.allowedJids.length > 0) {
93
+ console.log(`🔒 Allowlist enabled: ${this.allowedJids.length} IDs allowed.`);
94
94
  }
95
95
 
96
- if (connection === 'close') {
97
- const statusCode = (lastDisconnect?.error as Boom)?.output?.statusCode;
98
- const shouldReconnect = statusCode !== DisconnectReason.loggedOut;
96
+ this.sock = (makeWASocket as any).default ? (makeWASocket as any).default({
97
+ auth: {
98
+ creds: state.creds,
99
+ keys: (makeCacheableSignalKeyStore as any)(state.keys, this.logger),
100
+ },
101
+ version,
102
+ logger: this.logger,
103
+ printQRInTerminal: false,
104
+ browser: ['Ubuntu', 'Chrome', '20.0.04'],
105
+ syncFullHistory: true, // Recommended for better desktop state emulation
106
+ markOnlineOnConnect: false,
107
+ }) : (makeWASocket as any)({
108
+ auth: {
109
+ creds: state.creds,
110
+ keys: (makeCacheableSignalKeyStore as any)(state.keys, this.logger),
111
+ },
112
+ version,
113
+ logger: this.logger,
114
+ printQRInTerminal: false,
115
+ browser: ['Ubuntu', 'Chrome', '20.0.04'],
116
+ syncFullHistory: true,
117
+ markOnlineOnConnect: false,
118
+ });
119
+
120
+ if (this.sock.ws && typeof this.sock.ws.on === 'function') {
121
+ this.sock.ws.on('error', (err: Error) => {
122
+ console.error('WebSocket error:', err.message);
123
+ });
124
+ }
99
125
 
100
- console.log(`Connection closed. Status: ${statusCode}, Will reconnect: ${shouldReconnect}`);
126
+ this.sock.ev.on('connection.update', async (update: any) => {
127
+ const { connection, lastDisconnect, qr } = update;
101
128
 
102
- if (shouldReconnect && !this.reconnecting && !this.stopped) {
103
- this.reconnecting = true;
104
- console.log('Reconnecting in 5 seconds...');
105
- setTimeout(() => {
106
- this.reconnecting = false;
107
- this.start();
108
- }, 5000);
129
+ if (qr) {
130
+ console.log('\n📱 Scan this QR code with WhatsApp (Linked Devices):\n');
131
+ qrcode.generate(qr, { small: true });
132
+ if (this.statusHandler) {
133
+ this.statusHandler({ type: 'qr', data: qr });
134
+ }
109
135
  }
110
- } else if (connection === 'open') {
111
- console.log('✅ Connected to WhatsApp');
112
- if (this.statusHandler) {
113
- this.statusHandler({ type: 'connected' });
136
+
137
+ if (connection === 'close') {
138
+ const statusCode = (lastDisconnect?.error as Boom)?.output?.statusCode;
139
+ const shouldReconnect = statusCode !== DisconnectReason.loggedOut;
140
+
141
+ console.log(`Connection closed. Status: ${statusCode}, Will reconnect: ${shouldReconnect}`);
142
+
143
+ if (this.resetTimeout) {
144
+ clearTimeout(this.resetTimeout);
145
+ this.resetTimeout = null;
146
+ }
147
+
148
+ if (shouldReconnect && !this.reconnecting && !this.stopped) {
149
+ if (this.reconnectAttempts < this.maxReconnectAttempts) {
150
+ this.reconnectAttempts++;
151
+ this.reconnecting = true;
152
+ if (this.statusHandler) {
153
+ this.statusHandler({ type: 'reconnecting', data: { attempt: this.reconnectAttempts, max: this.maxReconnectAttempts } });
154
+ }
155
+ console.log(`Reconnecting (Attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts}) in 5 seconds...`);
156
+ setTimeout(() => {
157
+ this.reconnecting = false;
158
+ this.start();
159
+ }, 5000);
160
+ } else {
161
+ console.error(`[WhatsApp] Max reconnection attempts (${this.maxReconnectAttempts}) reached. Stopping.`);
162
+ if (this.statusHandler) {
163
+ this.statusHandler({ type: 'disconnected', data: { reason: 'max_retries' } });
164
+ }
165
+ this.stop();
166
+ }
167
+ } else if (this.statusHandler && !shouldReconnect) {
168
+ this.statusHandler({ type: 'disconnected' });
169
+ }
170
+ } else if (connection === 'open') {
171
+ console.log('✅ Connected to WhatsApp');
172
+ this.isStarting = false;
173
+
174
+ // Only reset attempts after 10 seconds of stable connection
175
+ if (this.resetTimeout) clearTimeout(this.resetTimeout);
176
+ this.resetTimeout = setTimeout(() => {
177
+ this.reconnectAttempts = 0;
178
+ this.resetTimeout = null;
179
+ }, 10000);
180
+
181
+ if (this.statusHandler) {
182
+ this.statusHandler({ type: 'connected' });
183
+ }
114
184
  }
115
- }
116
- });
185
+ });
186
+
187
+ this.sock.ev.on('creds.update', saveCreds);
117
188
 
118
- this.sock.ev.on('creds.update', saveCreds);
189
+ } catch (error) {
190
+ this.isStarting = false;
191
+ console.error('[WhatsApp] Failed to start:', error);
192
+ if (this.statusHandler) {
193
+ this.statusHandler({ type: 'error', data: error });
194
+ }
195
+ }
119
196
 
120
197
  this.sock.ev.on('messages.upsert', async ({ messages, type }: { messages: any[]; type: string }) => {
121
198
  if (type !== 'notify') return;
@@ -124,28 +201,89 @@ export class WhatsAppClient implements PlatformClient {
124
201
  if (msg.key.fromMe) continue;
125
202
  if (msg.key.remoteJid === 'status@broadcast') continue;
126
203
 
204
+ const remoteJid = msg.key.remoteJid;
205
+ const participant = msg.key.participant || remoteJid;
206
+ const isGroup = remoteJid?.endsWith('@g.us') || false;
207
+
208
+ // Allowlist Check
209
+ if (this.allowedJids.length > 0) {
210
+ const isAllowed = this.allowedJids.some(allowed =>
211
+ remoteJid?.includes(allowed) || participant?.includes(allowed)
212
+ );
213
+
214
+ if (!isAllowed) {
215
+ // console.log(`🚫 Ignoring message from unauthorized source: ${remoteJid}`);
216
+ continue;
217
+ }
218
+ }
219
+
220
+ // Group Mention Policy: Only respond if tagged
221
+ if (isGroup && this.sock?.user?.id) {
222
+ const botJid = normalizeWhatsAppTarget(this.sock.user.id);
223
+ const botLid = this.sock.user.lid ? normalizeWhatsAppTarget(this.sock.user.lid) : null;
224
+
225
+ const message = msg.message;
226
+ const contextInfo = message?.extendedTextMessage?.contextInfo ||
227
+ message?.imageMessage?.contextInfo ||
228
+ message?.videoMessage?.contextInfo ||
229
+ message?.documentMessage?.contextInfo ||
230
+ message?.audioMessage?.contextInfo ||
231
+ (message as any)?.contextInfo;
232
+
233
+ const mentions = contextInfo?.mentionedJid || [];
234
+
235
+ // Normalize all mentions and the bot ID for comparison
236
+ const isMentioned = mentions.some((m: string) => {
237
+ const normM = normalizeWhatsAppTarget(m);
238
+ const match = (botJid && normM === botJid) ||
239
+ (botLid && normM === botLid) ||
240
+ m === this.sock.user.id ||
241
+ (this.sock.user.lid && m === this.sock.user.lid);
242
+
243
+ return match;
244
+ });
245
+
246
+ // Check if it's a reply to the bot
247
+ const quotedParticipant = contextInfo?.participant;
248
+ const isReplyToBot = quotedParticipant && (
249
+ normalizeWhatsAppTarget(quotedParticipant) === botJid ||
250
+ normalizeWhatsAppTarget(quotedParticipant) === botLid ||
251
+ quotedParticipant === this.sock.user.id ||
252
+ (this.sock.user.lid && quotedParticipant === this.sock.user.lid)
253
+ );
254
+
255
+ if (!isMentioned && !isReplyToBot) {
256
+ continue;
257
+ }
258
+ }
259
+
127
260
  const content = this.extractMessageContent(msg);
128
261
  if (!content) continue;
129
262
 
130
263
  if (!this.messageHandler) continue;
131
264
 
132
- const isGroup = msg.key.remoteJid?.endsWith('@g.us') || false;
133
-
134
265
  const unifiedMessage: UnifiedMessage = {
135
266
  id: msg.key.id || `wa_${Date.now()}`,
136
267
  platform: 'whatsapp',
137
- userId: msg.key.remoteJid?.replace('@s.whatsapp.net', '') || '',
268
+ userId: remoteJid || '',
138
269
  sessionId: this.options.sessionId,
139
270
  content,
140
271
  messageType: 'text',
141
272
  timestamp: new Date((msg.messageTimestamp as number) * 1000),
142
273
  metadata: {
143
- pushName: msg.pushName,
144
- jid: msg.key.remoteJid,
145
- isGroup
274
+ pushName: msg.pushName || 'Unknown User',
275
+ isGroup,
276
+ participant: isGroup ? participant : undefined,
277
+ jid: remoteJid
146
278
  }
147
279
  };
148
280
 
281
+ // Typing indicator (optional, but keep for UX if stable)
282
+ try {
283
+ // Only start typing if jid is valid
284
+ if (remoteJid) await this.startTyping(remoteJid);
285
+ } catch (e) { /* ignore */ }
286
+
149
287
  await this.messageHandler(unifiedMessage);
150
288
  }
151
289
  });
@@ -167,15 +305,101 @@ export class WhatsAppClient implements PlatformClient {
167
305
 
168
306
  async sendMessage(to: string, text: string): Promise<void> {
169
307
  if (!this.sock) throw new Error('Not connected');
170
- const jid = to.includes('@') ? to : `${to}@s.whatsapp.net`;
308
+
309
+ const jid = normalizeWhatsAppTarget(to);
310
+ if (!jid) {
311
+ console.error(`[WhatsApp] Invalid message target: ${to}`);
312
+ return;
313
+ }
314
+
171
315
  await this.sock.sendMessage(jid, { text });
172
316
  }
173
317
 
318
+ async startTyping(jid: string): Promise<void> {
319
+ if (!this.sock) return;
320
+ await this.sock.sendPresenceUpdate('composing', jid);
321
+ }
322
+
323
+ async stopTyping(jid: string): Promise<void> {
324
+ if (!this.sock) return;
325
+ await this.sock.sendPresenceUpdate('paused', jid);
326
+ }
327
+
328
+ async reactToMessage(jid: string, key: any, emoji: string): Promise<void> {
329
+ if (!this.sock) return;
330
+ await this.sock.sendMessage(jid, { react: { text: emoji, key } });
331
+ }
332
+
333
+ async markRead(jid: string, key: any): Promise<void> {
334
+ if (!this.sock) return;
335
+ await this.sock.readMessages([key]);
336
+ }
337
+
338
+ async updateConfiguration(config: Partial<WhatsAppClientOptions>): Promise<void> {
339
+ if (config.allowedContexts) {
340
+ this.allowedJids = config.allowedContexts
341
+ .map(ctx => normalizeWhatsAppTarget(ctx.allowedJid))
342
+ .filter((jid): jid is string => !!jid);
343
+ console.log(`🔄 Configuration updated: Allowlist now has ${this.allowedJids.length} normalized IDs.`);
344
+ }
345
+ }
346
+
174
347
  async stop(): Promise<void> {
175
348
  this.stopped = true;
349
+ this.isStarting = false;
350
+ if (this.resetTimeout) {
351
+ clearTimeout(this.resetTimeout);
352
+ this.resetTimeout = null;
353
+ }
176
354
  if (this.sock) {
177
355
  this.sock.end(undefined);
178
356
  this.sock = null;
179
357
  }
180
358
  }
359
+
360
+ async deleteSession(): Promise<void> {
361
+ await this.stop();
362
+ if (this.options.authDir) {
363
+ const fs = await import('fs/promises');
364
+ try {
365
+ await fs.rm(this.options.authDir, { recursive: true, force: true });
366
+ console.log(`Deleted session directory: ${this.options.authDir}`);
367
+ } catch (error: any) {
368
+ console.error(`Failed to delete session directory: ${error.message}`);
369
+ }
370
+ }
371
+ }
372
+
373
+ async getAvailableContexts(): Promise<{ id: string; name: string; type: 'group' | 'user'; image?: string }[]> {
374
+ if (!this.sock) return [];
375
+
376
+ const contexts: { id: string; name: string; type: 'group' | 'user'; image?: string }[] = [];
377
+
378
+ try {
379
+ // 1. Groups from direct fetch (most reliable for groups)
380
+ const groups = await this.sock.groupFetchAllParticipating();
381
+ for (const [id, metadata] of Object.entries(groups)) {
382
+ contexts.push({
383
+ id,
384
+ name: (metadata as any).subject || 'Unknown Group',
385
+ type: 'group'
386
+ });
387
+ }
388
+ } catch (error) {
389
+ console.error('Failed to fetch contexts:', error);
390
+ }
391
+
392
+ // Fetch Profile Pictures (best effort, parallel)
393
+ await Promise.all(contexts.map(async (ctx) => {
394
+ try {
395
+ // Only fetch if we don't have one or to refresh
396
+ ctx.image = await this.sock.profilePictureUrl(ctx.id, 'preview');
397
+ } catch (e) {
398
+ // Ignore error (no profile pic or privacy restricted)
399
+ ctx.image = undefined;
400
+ }
401
+ }));
402
+
403
+ return contexts;
404
+ }
181
405
  }
@@ -0,0 +1,70 @@
1
+
2
+ /**
3
+ * WhatsApp JID Normalization following OpenClaw/Baileys patterns.
4
+ */
5
+
6
+ const WHATSAPP_USER_JID_RE = /^(\d+)(?::\d+)?@s\.whatsapp\.net$/i;
7
+ const WHATSAPP_LID_RE = /^(\d+)(?::\d+)?@lid$/i;
8
+
9
+ /**
10
+ * Clean phone numbers to digits only.
11
+ */
12
+ export function normalizeE164(value: string): string {
13
+ return value.replace(/\D/g, "");
14
+ }
15
+
16
+ function stripWhatsAppTargetPrefixes(value: string): string {
17
+ let candidate = value.trim();
18
+ for (; ;) {
19
+ const before = candidate;
20
+ candidate = candidate.replace(/^whatsapp:/i, "").trim();
21
+ if (candidate === before) {
22
+ return candidate;
23
+ }
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Check if the JID belongs to a group.
29
+ */
30
+ export function isWhatsAppGroupJid(value: string): boolean {
31
+ const candidate = stripWhatsAppTargetPrefixes(value);
32
+ const lower = candidate.toLowerCase();
33
+ return lower.endsWith("@g.us");
34
+ }
35
+
36
+ /**
37
+ * Normalize input string into a valid Baileys JID.
38
+ * Handles phone numbers, group IDs, and existing JIDs with device suffixes.
39
+ */
40
+ export function normalizeWhatsAppTarget(value: string): string | null {
41
+ const candidate = stripWhatsAppTargetPrefixes(value);
42
+ if (!candidate) return null;
43
+
44
+ // Handle Groups
45
+ if (isWhatsAppGroupJid(candidate)) {
46
+ const localPart = candidate.slice(0, candidate.length - "@g.us".length);
47
+ return `${localPart}@g.us`;
48
+ }
49
+
50
+ // Handle standard user JIDs (strip device suffixes like :5)
51
+ if (WHATSAPP_USER_JID_RE.test(candidate)) {
52
+ const match = candidate.match(WHATSAPP_USER_JID_RE);
53
+ return `${match![1]}@s.whatsapp.net`;
54
+ }
55
+
56
+ // Handle LIDs
57
+ if (WHATSAPP_LID_RE.test(candidate)) {
58
+ const match = candidate.match(WHATSAPP_LID_RE);
59
+ return `${match![1]}@lid`;
60
+ }
61
+
62
+ // Handle raw phone numbers or strings without @
63
+ if (!candidate.includes("@")) {
64
+ const normalized = normalizeE164(candidate);
65
+ // Only accept if it has at least 5 digits (sanity check for phone numbers)
66
+ return normalized.length >= 5 ? `${normalized}@s.whatsapp.net` : null;
67
+ }
68
+
69
+ return null;
70
+ }