@link-os/whatsapp 0.1.0 → 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.
- package/dist/client.d.ts +24 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +254 -64
- package/dist/client.js.map +1 -1
- package/dist/normalize.d.ts +17 -0
- package/dist/normalize.d.ts.map +1 -0
- package/dist/normalize.js +61 -0
- package/dist/normalize.js.map +1 -0
- package/package.json +2 -2
- package/src/client.ts +285 -67
- package/src/normalize.ts +70 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/auth_info_baileys/app-state-sync-key-AAAAANpA.json +0 -1
- package/auth_info_baileys/app-state-sync-key-AAAAANpB.json +0 -1
- package/auth_info_baileys/app-state-sync-key-AAAAANpC.json +0 -1
- package/auth_info_baileys/app-state-sync-key-AAAAANpD.json +0 -1
- package/auth_info_baileys/app-state-sync-key-AAAAANpE.json +0 -1
- package/auth_info_baileys/app-state-sync-key-AAAAANpF.json +0 -1
- package/auth_info_baileys/app-state-sync-key-AAAAANpG.json +0 -1
- package/auth_info_baileys/app-state-sync-key-AAAAANpI.json +0 -1
- package/auth_info_baileys/app-state-sync-key-AAAAANpJ.json +0 -1
- package/auth_info_baileys/app-state-sync-key-ADMAANpI.json +0 -1
- package/auth_info_baileys/app-state-sync-version-regular_low.json +0 -1
- package/auth_info_baileys/creds.json +0 -1
- package/auth_info_baileys/pre-key-1.json +0 -1
- package/auth_info_baileys/pre-key-10.json +0 -1
- package/auth_info_baileys/pre-key-11.json +0 -1
- package/auth_info_baileys/pre-key-12.json +0 -1
- package/auth_info_baileys/pre-key-13.json +0 -1
- package/auth_info_baileys/pre-key-14.json +0 -1
- package/auth_info_baileys/pre-key-15.json +0 -1
- package/auth_info_baileys/pre-key-16.json +0 -1
- package/auth_info_baileys/pre-key-17.json +0 -1
- package/auth_info_baileys/pre-key-18.json +0 -1
- package/auth_info_baileys/pre-key-19.json +0 -1
- package/auth_info_baileys/pre-key-2.json +0 -1
- package/auth_info_baileys/pre-key-20.json +0 -1
- package/auth_info_baileys/pre-key-22.json +0 -1
- package/auth_info_baileys/pre-key-23.json +0 -1
- package/auth_info_baileys/pre-key-24.json +0 -1
- package/auth_info_baileys/pre-key-25.json +0 -1
- package/auth_info_baileys/pre-key-26.json +0 -1
- package/auth_info_baileys/pre-key-27.json +0 -1
- package/auth_info_baileys/pre-key-28.json +0 -1
- package/auth_info_baileys/pre-key-29.json +0 -1
- package/auth_info_baileys/pre-key-3.json +0 -1
- package/auth_info_baileys/pre-key-30.json +0 -1
- package/auth_info_baileys/pre-key-4.json +0 -1
- package/auth_info_baileys/pre-key-5.json +0 -1
- package/auth_info_baileys/pre-key-6.json +0 -1
- package/auth_info_baileys/pre-key-7.json +0 -1
- package/auth_info_baileys/pre-key-8.json +0 -1
- package/auth_info_baileys/pre-key-9.json +0 -1
- 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,124 @@ 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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
// Clear any pending reset
|
|
79
|
+
if (this.resetTimeout) {
|
|
80
|
+
clearTimeout(this.resetTimeout);
|
|
81
|
+
this.resetTimeout = null;
|
|
83
82
|
}
|
|
84
83
|
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
const isDefaultImport = !!(makeWASocket as any).default;
|
|
97
|
+
const sockOptions = {
|
|
98
|
+
auth: {
|
|
99
|
+
creds: state.creds,
|
|
100
|
+
keys: makeCacheableSignalKeyStore(state.keys, this.logger),
|
|
101
|
+
},
|
|
102
|
+
version,
|
|
103
|
+
logger: this.logger,
|
|
104
|
+
printQRInTerminal: false,
|
|
105
|
+
browser: ['Linkos', 'Chrome', '121.0.6167.140'] as [string, string, string],
|
|
106
|
+
syncFullHistory: false,
|
|
107
|
+
markOnlineOnConnect: true,
|
|
108
|
+
connectTimeoutMs: 60000,
|
|
109
|
+
defaultQueryTimeoutMs: 60000,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
this.sock = isDefaultImport ? (makeWASocket as any).default(sockOptions) : makeWASocket(sockOptions);
|
|
99
113
|
|
|
100
|
-
|
|
114
|
+
if (this.sock.ws && typeof this.sock.ws.on === 'function') {
|
|
115
|
+
this.sock.ws.on('error', (err: Error) => {
|
|
116
|
+
console.error('WebSocket error:', err.message);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
this.sock.ev.on('connection.update', async (update: any) => {
|
|
121
|
+
const { connection, lastDisconnect, qr } = update;
|
|
101
122
|
|
|
102
|
-
if (
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
this.
|
|
107
|
-
|
|
108
|
-
}, 5000);
|
|
123
|
+
if (qr) {
|
|
124
|
+
console.log('\n📱 Scan this QR code with WhatsApp (Linked Devices):\n');
|
|
125
|
+
qrcode.generate(qr, { small: true });
|
|
126
|
+
if (this.statusHandler) {
|
|
127
|
+
this.statusHandler({ type: 'qr', data: qr });
|
|
128
|
+
}
|
|
109
129
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
130
|
+
|
|
131
|
+
if (connection === 'close') {
|
|
132
|
+
const statusCode = (lastDisconnect?.error as Boom)?.output?.statusCode;
|
|
133
|
+
const shouldReconnect = statusCode !== DisconnectReason.loggedOut;
|
|
134
|
+
|
|
135
|
+
console.log(`Connection closed. Status: ${statusCode}, Will reconnect: ${shouldReconnect}`);
|
|
136
|
+
|
|
137
|
+
if (this.resetTimeout) {
|
|
138
|
+
clearTimeout(this.resetTimeout);
|
|
139
|
+
this.resetTimeout = null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (shouldReconnect && !this.reconnecting && !this.stopped) {
|
|
143
|
+
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
144
|
+
this.reconnectAttempts++;
|
|
145
|
+
this.reconnecting = true;
|
|
146
|
+
if (this.statusHandler) {
|
|
147
|
+
this.statusHandler({ type: 'reconnecting', data: { attempt: this.reconnectAttempts, max: this.maxReconnectAttempts } });
|
|
148
|
+
}
|
|
149
|
+
console.log(`Reconnecting (Attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts}) in 5 seconds...`);
|
|
150
|
+
setTimeout(() => {
|
|
151
|
+
this.reconnecting = false;
|
|
152
|
+
this.start();
|
|
153
|
+
}, 5000);
|
|
154
|
+
} else {
|
|
155
|
+
console.error(`[WhatsApp] Max reconnection attempts (${this.maxReconnectAttempts}) reached. Stopping.`);
|
|
156
|
+
if (this.statusHandler) {
|
|
157
|
+
this.statusHandler({ type: 'disconnected', data: { reason: 'max_retries' } });
|
|
158
|
+
}
|
|
159
|
+
this.stop();
|
|
160
|
+
}
|
|
161
|
+
} else if (this.statusHandler && !shouldReconnect) {
|
|
162
|
+
this.statusHandler({ type: 'disconnected' });
|
|
163
|
+
}
|
|
164
|
+
} else if (connection === 'open') {
|
|
165
|
+
console.log('✅ Connected to WhatsApp');
|
|
166
|
+
this.isStarting = false;
|
|
167
|
+
|
|
168
|
+
// Only reset attempts after 10 seconds of stable connection
|
|
169
|
+
if (this.resetTimeout) clearTimeout(this.resetTimeout);
|
|
170
|
+
this.resetTimeout = setTimeout(() => {
|
|
171
|
+
this.reconnectAttempts = 0;
|
|
172
|
+
this.resetTimeout = null;
|
|
173
|
+
}, 10000);
|
|
174
|
+
|
|
175
|
+
if (this.statusHandler) {
|
|
176
|
+
this.statusHandler({ type: 'connected' });
|
|
177
|
+
}
|
|
114
178
|
}
|
|
115
|
-
}
|
|
116
|
-
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
this.sock.ev.on('creds.update', saveCreds);
|
|
117
182
|
|
|
118
|
-
|
|
183
|
+
} catch (error) {
|
|
184
|
+
this.isStarting = false;
|
|
185
|
+
console.error('[WhatsApp] Failed to start:', error);
|
|
186
|
+
if (this.statusHandler) {
|
|
187
|
+
this.statusHandler({ type: 'error', data: error });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
119
190
|
|
|
120
191
|
this.sock.ev.on('messages.upsert', async ({ messages, type }: { messages: any[]; type: string }) => {
|
|
121
192
|
if (type !== 'notify') return;
|
|
@@ -124,28 +195,89 @@ export class WhatsAppClient implements PlatformClient {
|
|
|
124
195
|
if (msg.key.fromMe) continue;
|
|
125
196
|
if (msg.key.remoteJid === 'status@broadcast') continue;
|
|
126
197
|
|
|
198
|
+
const remoteJid = msg.key.remoteJid;
|
|
199
|
+
const participant = msg.key.participant || remoteJid;
|
|
200
|
+
const isGroup = remoteJid?.endsWith('@g.us') || false;
|
|
201
|
+
|
|
202
|
+
// Allowlist Check
|
|
203
|
+
if (this.allowedJids.length > 0) {
|
|
204
|
+
const isAllowed = this.allowedJids.some(allowed =>
|
|
205
|
+
remoteJid?.includes(allowed) || participant?.includes(allowed)
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
if (!isAllowed) {
|
|
209
|
+
// console.log(`🚫 Ignoring message from unauthorized source: ${remoteJid}`);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Group Mention Policy: Only respond if tagged
|
|
215
|
+
if (isGroup && this.sock?.user?.id) {
|
|
216
|
+
const botJid = normalizeWhatsAppTarget(this.sock.user.id);
|
|
217
|
+
const botLid = this.sock.user.lid ? normalizeWhatsAppTarget(this.sock.user.lid) : null;
|
|
218
|
+
|
|
219
|
+
const message = msg.message;
|
|
220
|
+
const contextInfo = message?.extendedTextMessage?.contextInfo ||
|
|
221
|
+
message?.imageMessage?.contextInfo ||
|
|
222
|
+
message?.videoMessage?.contextInfo ||
|
|
223
|
+
message?.documentMessage?.contextInfo ||
|
|
224
|
+
message?.audioMessage?.contextInfo ||
|
|
225
|
+
(message as any)?.contextInfo;
|
|
226
|
+
|
|
227
|
+
const mentions = contextInfo?.mentionedJid || [];
|
|
228
|
+
|
|
229
|
+
// Normalize all mentions and the bot ID for comparison
|
|
230
|
+
const isMentioned = mentions.some((m: string) => {
|
|
231
|
+
const normM = normalizeWhatsAppTarget(m);
|
|
232
|
+
const match = (botJid && normM === botJid) ||
|
|
233
|
+
(botLid && normM === botLid) ||
|
|
234
|
+
m === this.sock.user.id ||
|
|
235
|
+
(this.sock.user.lid && m === this.sock.user.lid);
|
|
236
|
+
|
|
237
|
+
return match;
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// Check if it's a reply to the bot
|
|
241
|
+
const quotedParticipant = contextInfo?.participant;
|
|
242
|
+
const isReplyToBot = quotedParticipant && (
|
|
243
|
+
normalizeWhatsAppTarget(quotedParticipant) === botJid ||
|
|
244
|
+
normalizeWhatsAppTarget(quotedParticipant) === botLid ||
|
|
245
|
+
quotedParticipant === this.sock.user.id ||
|
|
246
|
+
(this.sock.user.lid && quotedParticipant === this.sock.user.lid)
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
if (!isMentioned && !isReplyToBot) {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
127
254
|
const content = this.extractMessageContent(msg);
|
|
128
255
|
if (!content) continue;
|
|
129
256
|
|
|
130
257
|
if (!this.messageHandler) continue;
|
|
131
258
|
|
|
132
|
-
const isGroup = msg.key.remoteJid?.endsWith('@g.us') || false;
|
|
133
|
-
|
|
134
259
|
const unifiedMessage: UnifiedMessage = {
|
|
135
260
|
id: msg.key.id || `wa_${Date.now()}`,
|
|
136
261
|
platform: 'whatsapp',
|
|
137
|
-
userId:
|
|
262
|
+
userId: remoteJid || '',
|
|
138
263
|
sessionId: this.options.sessionId,
|
|
139
264
|
content,
|
|
140
265
|
messageType: 'text',
|
|
141
266
|
timestamp: new Date((msg.messageTimestamp as number) * 1000),
|
|
142
267
|
metadata: {
|
|
143
|
-
pushName: msg.pushName,
|
|
144
|
-
|
|
145
|
-
isGroup
|
|
268
|
+
pushName: msg.pushName || 'Unknown User',
|
|
269
|
+
isGroup,
|
|
270
|
+
participant: isGroup ? participant : undefined,
|
|
271
|
+
jid: remoteJid
|
|
146
272
|
}
|
|
147
273
|
};
|
|
148
274
|
|
|
275
|
+
// Typing indicator (optional, but keep for UX if stable)
|
|
276
|
+
try {
|
|
277
|
+
// Only start typing if jid is valid
|
|
278
|
+
if (remoteJid) await this.startTyping(remoteJid);
|
|
279
|
+
} catch (e) { /* ignore */ }
|
|
280
|
+
|
|
149
281
|
await this.messageHandler(unifiedMessage);
|
|
150
282
|
}
|
|
151
283
|
});
|
|
@@ -167,15 +299,101 @@ export class WhatsAppClient implements PlatformClient {
|
|
|
167
299
|
|
|
168
300
|
async sendMessage(to: string, text: string): Promise<void> {
|
|
169
301
|
if (!this.sock) throw new Error('Not connected');
|
|
170
|
-
|
|
302
|
+
|
|
303
|
+
const jid = normalizeWhatsAppTarget(to);
|
|
304
|
+
if (!jid) {
|
|
305
|
+
console.error(`[WhatsApp] Invalid message target: ${to}`);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
171
309
|
await this.sock.sendMessage(jid, { text });
|
|
172
310
|
}
|
|
173
311
|
|
|
312
|
+
async startTyping(jid: string): Promise<void> {
|
|
313
|
+
if (!this.sock) return;
|
|
314
|
+
await this.sock.sendPresenceUpdate('composing', jid);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async stopTyping(jid: string): Promise<void> {
|
|
318
|
+
if (!this.sock) return;
|
|
319
|
+
await this.sock.sendPresenceUpdate('paused', jid);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async reactToMessage(jid: string, key: any, emoji: string): Promise<void> {
|
|
323
|
+
if (!this.sock) return;
|
|
324
|
+
await this.sock.sendMessage(jid, { react: { text: emoji, key } });
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async markRead(jid: string, key: any): Promise<void> {
|
|
328
|
+
if (!this.sock) return;
|
|
329
|
+
await this.sock.readMessages([key]);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async updateConfiguration(config: Partial<WhatsAppClientOptions>): Promise<void> {
|
|
333
|
+
if (config.allowedContexts) {
|
|
334
|
+
this.allowedJids = config.allowedContexts
|
|
335
|
+
.map(ctx => normalizeWhatsAppTarget(ctx.allowedJid))
|
|
336
|
+
.filter((jid): jid is string => !!jid);
|
|
337
|
+
console.log(`🔄 Configuration updated: Allowlist now has ${this.allowedJids.length} normalized IDs.`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
174
341
|
async stop(): Promise<void> {
|
|
175
342
|
this.stopped = true;
|
|
343
|
+
this.isStarting = false;
|
|
344
|
+
if (this.resetTimeout) {
|
|
345
|
+
clearTimeout(this.resetTimeout);
|
|
346
|
+
this.resetTimeout = null;
|
|
347
|
+
}
|
|
176
348
|
if (this.sock) {
|
|
177
349
|
this.sock.end(undefined);
|
|
178
350
|
this.sock = null;
|
|
179
351
|
}
|
|
180
352
|
}
|
|
353
|
+
|
|
354
|
+
async deleteSession(): Promise<void> {
|
|
355
|
+
await this.stop();
|
|
356
|
+
if (this.options.authDir) {
|
|
357
|
+
const fs = await import('fs/promises');
|
|
358
|
+
try {
|
|
359
|
+
await fs.rm(this.options.authDir, { recursive: true, force: true });
|
|
360
|
+
console.log(`Deleted session directory: ${this.options.authDir}`);
|
|
361
|
+
} catch (error: any) {
|
|
362
|
+
console.error(`Failed to delete session directory: ${error.message}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async getAvailableContexts(): Promise<{ id: string; name: string; type: 'group' | 'user'; image?: string }[]> {
|
|
368
|
+
if (!this.sock) return [];
|
|
369
|
+
|
|
370
|
+
const contexts: { id: string; name: string; type: 'group' | 'user'; image?: string }[] = [];
|
|
371
|
+
|
|
372
|
+
try {
|
|
373
|
+
// 1. Groups from direct fetch (most reliable for groups)
|
|
374
|
+
const groups = await this.sock.groupFetchAllParticipating();
|
|
375
|
+
for (const [id, metadata] of Object.entries(groups)) {
|
|
376
|
+
contexts.push({
|
|
377
|
+
id,
|
|
378
|
+
name: (metadata as any).subject || 'Unknown Group',
|
|
379
|
+
type: 'group'
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
} catch (error) {
|
|
383
|
+
console.error('Failed to fetch contexts:', error);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Fetch Profile Pictures (best effort, parallel)
|
|
387
|
+
await Promise.all(contexts.map(async (ctx) => {
|
|
388
|
+
try {
|
|
389
|
+
// Only fetch if we don't have one or to refresh
|
|
390
|
+
ctx.image = await this.sock.profilePictureUrl(ctx.id, 'preview');
|
|
391
|
+
} catch (e) {
|
|
392
|
+
// Ignore error (no profile pic or privacy restricted)
|
|
393
|
+
ctx.image = undefined;
|
|
394
|
+
}
|
|
395
|
+
}));
|
|
396
|
+
|
|
397
|
+
return contexts;
|
|
398
|
+
}
|
|
181
399
|
}
|
package/src/normalize.ts
ADDED
|
@@ -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
|
+
}
|