wagent 1.0.0
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/README.md +103 -0
- package/dist/channels/baileys/baileys.adapter.js +992 -0
- package/dist/channels/baileys/baileys.auth.js +96 -0
- package/dist/channels/baileys/baileys.events.js +284 -0
- package/dist/channels/baileys/baileys.version.js +86 -0
- package/dist/channels/channel.interface.js +6 -0
- package/dist/config.js +120 -0
- package/dist/constants.js +59 -0
- package/dist/db/client.js +160 -0
- package/dist/db/schema.js +189 -0
- package/dist/index.js +873 -0
- package/dist/services/instance-manager.js +185 -0
- package/dist/types/channel.types.js +4 -0
- package/dist/types/db.types.js +4 -0
- package/dist/utils/logger.js +39 -0
- package/package.json +43 -0
|
@@ -0,0 +1,992 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// WA MCP — Baileys Channel Adapter
|
|
3
|
+
// Full implementation of ChannelAdapter using
|
|
4
|
+
// @whiskeysockets/baileys for the WhatsApp Web protocol.
|
|
5
|
+
// ============================================================
|
|
6
|
+
import makeWASocket, { DisconnectReason, makeCacheableSignalKeyStore, generateForwardMessageContent, generateWAMessageFromContent, } from "@whiskeysockets/baileys";
|
|
7
|
+
import { useSqliteAuthState, clearAuthState } from "./baileys.auth.js";
|
|
8
|
+
import { getWaVersion } from "./baileys.version.js";
|
|
9
|
+
import { db } from "../../db/client.js";
|
|
10
|
+
import { contacts as contactsTable, messages as messagesTable } from "../../db/schema.js";
|
|
11
|
+
import { eq, and, like, or, desc } from "drizzle-orm";
|
|
12
|
+
import { normalizeMessagesUpsert, normalizeMessagesUpdate, normalizeMessagesDelete, normalizeMessagesReaction, normalizeMessageEdit, normalizePresenceUpdate, normalizeGroupsUpdate, normalizeGroupParticipantsUpdate, normalizeContactsUpdate, normalizeConnectionUpdate, normalizeCallEvent, normalizeLidInMessage, extractMessageType, extractTextContent, extractQuotedMessageId, getChatJid, getSenderJid, } from "./baileys.events.js";
|
|
13
|
+
import { DEFAULT_AUTO_RECONNECT, RECONNECT_INITIAL_DELAY_MS, RECONNECT_MAX_DELAY_MS, RECONNECT_MAX_ATTEMPTS, } from "../../constants.js";
|
|
14
|
+
import { createChildLogger } from "../../utils/logger.js";
|
|
15
|
+
const logger = createChildLogger({ service: "baileys-adapter" });
|
|
16
|
+
export class BaileysAdapter {
|
|
17
|
+
instanceId;
|
|
18
|
+
sock = null;
|
|
19
|
+
contactCache = new Map();
|
|
20
|
+
status = "disconnected";
|
|
21
|
+
qrCode = null;
|
|
22
|
+
pairingPhone = null;
|
|
23
|
+
pairingCodeValue = null;
|
|
24
|
+
pairingCodeResolver = null;
|
|
25
|
+
reconnectAttempt = 0;
|
|
26
|
+
reconnectTimer = null;
|
|
27
|
+
saveCreds = null;
|
|
28
|
+
autoReconnect;
|
|
29
|
+
listeners = new Map();
|
|
30
|
+
constructor(instanceId) {
|
|
31
|
+
this.instanceId = instanceId;
|
|
32
|
+
this.autoReconnect = process.env.WA_AUTO_RECONNECT !== "false" && DEFAULT_AUTO_RECONNECT;
|
|
33
|
+
}
|
|
34
|
+
// ---- Public helpers ----
|
|
35
|
+
/** Get own JID (the logged-in user's WhatsApp ID) */
|
|
36
|
+
getMyJid() {
|
|
37
|
+
return this.sock?.user?.id ?? null;
|
|
38
|
+
}
|
|
39
|
+
// ---- Private helpers ----
|
|
40
|
+
getSock() {
|
|
41
|
+
if (!this.sock || this.status !== "connected") {
|
|
42
|
+
throw new Error("Instance not connected");
|
|
43
|
+
}
|
|
44
|
+
return this.sock;
|
|
45
|
+
}
|
|
46
|
+
normalizeJid(input) {
|
|
47
|
+
if (input.includes("@"))
|
|
48
|
+
return input;
|
|
49
|
+
const cleaned = input.replace(/[^0-9]/g, "");
|
|
50
|
+
return `${cleaned}@s.whatsapp.net`;
|
|
51
|
+
}
|
|
52
|
+
/** Resolve a PN JID from a LID using the signal repository mapping */
|
|
53
|
+
async resolvePn(lidOrPn) {
|
|
54
|
+
if (!lidOrPn.endsWith("@lid") || !this.sock)
|
|
55
|
+
return lidOrPn;
|
|
56
|
+
const pn = await this.sock.signalRepository.lidMapping.getPNForLID(lidOrPn);
|
|
57
|
+
return pn ?? lidOrPn;
|
|
58
|
+
}
|
|
59
|
+
resolveMedia(input) {
|
|
60
|
+
if (input.startsWith("https://")) {
|
|
61
|
+
return { url: input };
|
|
62
|
+
}
|
|
63
|
+
if (input.startsWith("http://")) {
|
|
64
|
+
throw new Error("Media URLs must use HTTPS");
|
|
65
|
+
}
|
|
66
|
+
if (input.startsWith("file://") || input.startsWith("data:")) {
|
|
67
|
+
throw new Error("file:// and data: URLs are not allowed");
|
|
68
|
+
}
|
|
69
|
+
return Buffer.from(input, "base64");
|
|
70
|
+
}
|
|
71
|
+
generateVCard(name, phone) {
|
|
72
|
+
const cleaned = phone.replace(/[^0-9]/g, "");
|
|
73
|
+
return [
|
|
74
|
+
"BEGIN:VCARD",
|
|
75
|
+
"VERSION:3.0",
|
|
76
|
+
`FN:${name}`,
|
|
77
|
+
`TEL;type=CELL;waid=${cleaned}:+${cleaned}`,
|
|
78
|
+
"END:VCARD",
|
|
79
|
+
].join("\n");
|
|
80
|
+
}
|
|
81
|
+
makeQuotedStub(remoteJid, messageId) {
|
|
82
|
+
return {
|
|
83
|
+
key: { remoteJid, id: messageId, fromMe: false },
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
getMeJid() {
|
|
87
|
+
const meJid = this.sock?.user?.id;
|
|
88
|
+
if (!meJid)
|
|
89
|
+
throw new Error("Not authenticated");
|
|
90
|
+
return meJid;
|
|
91
|
+
}
|
|
92
|
+
// ---- Lifecycle ----
|
|
93
|
+
async connect() {
|
|
94
|
+
if (this.sock) {
|
|
95
|
+
logger.warn({ instanceId: this.instanceId }, "Already connected or connecting");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
this.status = "connecting";
|
|
99
|
+
const { state, saveCreds } = await useSqliteAuthState(this.instanceId);
|
|
100
|
+
this.saveCreds = saveCreds;
|
|
101
|
+
const { version } = await getWaVersion();
|
|
102
|
+
const sock = makeWASocket({
|
|
103
|
+
version,
|
|
104
|
+
auth: {
|
|
105
|
+
creds: state.creds,
|
|
106
|
+
keys: makeCacheableSignalKeyStore(state.keys, logger),
|
|
107
|
+
},
|
|
108
|
+
printQRInTerminal: false,
|
|
109
|
+
logger: logger.child({ instanceId: this.instanceId }),
|
|
110
|
+
generateHighQualityLinkPreview: true,
|
|
111
|
+
syncFullHistory: true,
|
|
112
|
+
markOnlineOnConnect: false,
|
|
113
|
+
});
|
|
114
|
+
this.sock = sock;
|
|
115
|
+
this.bindEvents(sock);
|
|
116
|
+
}
|
|
117
|
+
async disconnect() {
|
|
118
|
+
this.clearReconnectTimer();
|
|
119
|
+
if (this.sock) {
|
|
120
|
+
this.sock.end(undefined);
|
|
121
|
+
this.sock = null;
|
|
122
|
+
}
|
|
123
|
+
this.status = "disconnected";
|
|
124
|
+
}
|
|
125
|
+
getStatus() {
|
|
126
|
+
return this.status;
|
|
127
|
+
}
|
|
128
|
+
// ---- Authentication ----
|
|
129
|
+
async getQrCode() {
|
|
130
|
+
return this.qrCode;
|
|
131
|
+
}
|
|
132
|
+
async getPairingCode(phone) {
|
|
133
|
+
if (this.status === "disconnected" || !this.sock) {
|
|
134
|
+
throw new Error("Instance not connected");
|
|
135
|
+
}
|
|
136
|
+
// Already have the code cached
|
|
137
|
+
if (this.pairingCodeValue) {
|
|
138
|
+
return this.pairingCodeValue;
|
|
139
|
+
}
|
|
140
|
+
// QR event already fired before this call — request pairing code immediately
|
|
141
|
+
if (this.qrCode && this.sock) {
|
|
142
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
143
|
+
const code = await this.sock.requestPairingCode(phone);
|
|
144
|
+
this.pairingCodeValue = code ?? null;
|
|
145
|
+
return this.pairingCodeValue;
|
|
146
|
+
}
|
|
147
|
+
// QR event hasn't fired yet — register phone and wait for it (30s timeout)
|
|
148
|
+
this.pairingPhone = phone;
|
|
149
|
+
return new Promise((resolve, reject) => {
|
|
150
|
+
this.pairingCodeResolver = resolve;
|
|
151
|
+
setTimeout(() => {
|
|
152
|
+
this.pairingCodeResolver = null;
|
|
153
|
+
reject(new Error("Pairing code timeout — QR event did not fire within 30s"));
|
|
154
|
+
}, 30000);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
async setCredentials(_creds) {
|
|
158
|
+
throw new Error("setCredentials is only available for Cloud API instances");
|
|
159
|
+
}
|
|
160
|
+
// ---- Messaging ----
|
|
161
|
+
async sendMessage(to, content) {
|
|
162
|
+
const sock = this.getSock();
|
|
163
|
+
const jid = this.normalizeJid(to);
|
|
164
|
+
let msg;
|
|
165
|
+
const opts = {};
|
|
166
|
+
switch (content.type) {
|
|
167
|
+
case "text":
|
|
168
|
+
msg = { text: content.text };
|
|
169
|
+
if (content.quotedMessageId) {
|
|
170
|
+
opts.quoted = this.makeQuotedStub(jid, content.quotedMessageId);
|
|
171
|
+
}
|
|
172
|
+
break;
|
|
173
|
+
case "image":
|
|
174
|
+
msg = { image: this.resolveMedia(content.image), caption: content.caption };
|
|
175
|
+
if (content.quotedMessageId) {
|
|
176
|
+
opts.quoted = this.makeQuotedStub(jid, content.quotedMessageId);
|
|
177
|
+
}
|
|
178
|
+
break;
|
|
179
|
+
case "video":
|
|
180
|
+
msg = { video: this.resolveMedia(content.video), caption: content.caption };
|
|
181
|
+
if (content.quotedMessageId) {
|
|
182
|
+
opts.quoted = this.makeQuotedStub(jid, content.quotedMessageId);
|
|
183
|
+
}
|
|
184
|
+
break;
|
|
185
|
+
case "audio":
|
|
186
|
+
msg = {
|
|
187
|
+
audio: this.resolveMedia(content.audio),
|
|
188
|
+
ptt: content.ptt ?? false,
|
|
189
|
+
mimetype: "audio/ogg; codecs=opus",
|
|
190
|
+
};
|
|
191
|
+
break;
|
|
192
|
+
case "document":
|
|
193
|
+
msg = {
|
|
194
|
+
document: this.resolveMedia(content.document),
|
|
195
|
+
fileName: content.fileName,
|
|
196
|
+
mimetype: content.mimeType,
|
|
197
|
+
};
|
|
198
|
+
break;
|
|
199
|
+
case "location":
|
|
200
|
+
msg = {
|
|
201
|
+
location: {
|
|
202
|
+
degreesLatitude: content.latitude,
|
|
203
|
+
degreesLongitude: content.longitude,
|
|
204
|
+
name: content.name,
|
|
205
|
+
address: content.address,
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
break;
|
|
209
|
+
case "contact":
|
|
210
|
+
msg = {
|
|
211
|
+
contacts: {
|
|
212
|
+
displayName: content.contactName,
|
|
213
|
+
contacts: [{ vcard: this.generateVCard(content.contactName, content.contactPhone) }],
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
break;
|
|
217
|
+
case "poll":
|
|
218
|
+
msg = {
|
|
219
|
+
poll: {
|
|
220
|
+
name: content.question,
|
|
221
|
+
values: content.options,
|
|
222
|
+
selectableCount: content.multiSelect ? 0 : 1,
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
break;
|
|
226
|
+
case "reaction":
|
|
227
|
+
throw new Error("Use sendReaction() for reactions");
|
|
228
|
+
default:
|
|
229
|
+
throw new Error(`Unsupported message type: ${content.type}`);
|
|
230
|
+
}
|
|
231
|
+
const result = await sock.sendMessage(jid, msg, opts);
|
|
232
|
+
return { messageId: result?.key.id ?? "", timestamp: Date.now(), status: "sent" };
|
|
233
|
+
}
|
|
234
|
+
async editMessage(chatId, msgId, newText) {
|
|
235
|
+
const sock = this.getSock();
|
|
236
|
+
const jid = this.normalizeJid(chatId);
|
|
237
|
+
await sock.sendMessage(jid, {
|
|
238
|
+
text: newText,
|
|
239
|
+
edit: { remoteJid: jid, id: msgId, fromMe: true },
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
async deleteMessage(chatId, msgId) {
|
|
243
|
+
const sock = this.getSock();
|
|
244
|
+
const jid = this.normalizeJid(chatId);
|
|
245
|
+
await sock.sendMessage(jid, {
|
|
246
|
+
delete: { remoteJid: jid, id: msgId, fromMe: true },
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
async forwardMessage(to, msgId, fromChat) {
|
|
250
|
+
const sock = this.getSock();
|
|
251
|
+
const toJid = this.normalizeJid(to);
|
|
252
|
+
const fromJid = this.normalizeJid(fromChat);
|
|
253
|
+
// Use the store or load message; for now forward with minimal message stub
|
|
254
|
+
const stubMsg = {
|
|
255
|
+
key: { remoteJid: fromJid, id: msgId, fromMe: false },
|
|
256
|
+
message: { conversation: "" },
|
|
257
|
+
};
|
|
258
|
+
const forwardContent = generateForwardMessageContent(stubMsg, false);
|
|
259
|
+
const meJid = this.getMeJid();
|
|
260
|
+
const generatedMsg = generateWAMessageFromContent(toJid, forwardContent, { userJid: meJid });
|
|
261
|
+
await sock.relayMessage(toJid, generatedMsg.message, {
|
|
262
|
+
messageId: generatedMsg.key.id,
|
|
263
|
+
});
|
|
264
|
+
return { messageId: generatedMsg.key.id ?? "", timestamp: Date.now(), status: "sent" };
|
|
265
|
+
}
|
|
266
|
+
async sendReaction(chatId, msgId, emoji) {
|
|
267
|
+
const sock = this.getSock();
|
|
268
|
+
const jid = this.normalizeJid(chatId);
|
|
269
|
+
await sock.sendMessage(jid, {
|
|
270
|
+
react: { text: emoji, key: { remoteJid: jid, id: msgId } },
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
async pinMessage(chatId, msgId, pin) {
|
|
274
|
+
const sock = this.getSock();
|
|
275
|
+
const jid = this.normalizeJid(chatId);
|
|
276
|
+
const pinMsg = {
|
|
277
|
+
pin: { remoteJid: jid, id: msgId, fromMe: true },
|
|
278
|
+
type: pin ? 1 : 0,
|
|
279
|
+
time: pin ? 604800 : undefined,
|
|
280
|
+
};
|
|
281
|
+
await sock.sendMessage(jid, pinMsg);
|
|
282
|
+
}
|
|
283
|
+
async sendViewOnce(to, media, type) {
|
|
284
|
+
const sock = this.getSock();
|
|
285
|
+
const jid = this.normalizeJid(to);
|
|
286
|
+
const resolved = this.resolveMedia(media);
|
|
287
|
+
const msg = type === "image" ? { image: resolved, viewOnce: true } : { video: resolved, viewOnce: true };
|
|
288
|
+
const result = await sock.sendMessage(jid, msg);
|
|
289
|
+
return { messageId: result?.key.id ?? "", timestamp: Date.now(), status: "sent" };
|
|
290
|
+
}
|
|
291
|
+
async sendLinkPreview(to, text, _url) {
|
|
292
|
+
const sock = this.getSock();
|
|
293
|
+
const jid = this.normalizeJid(to);
|
|
294
|
+
const result = await sock.sendMessage(jid, { text });
|
|
295
|
+
return { messageId: result?.key.id ?? "", timestamp: Date.now(), status: "sent" };
|
|
296
|
+
}
|
|
297
|
+
// ---- Presence ----
|
|
298
|
+
async sendPresence(chatId, presenceStatus) {
|
|
299
|
+
const sock = this.getSock();
|
|
300
|
+
await sock.sendPresenceUpdate(presenceStatus, this.normalizeJid(chatId));
|
|
301
|
+
}
|
|
302
|
+
async markRead(chatId, messageIds) {
|
|
303
|
+
const sock = this.getSock();
|
|
304
|
+
const jid = this.normalizeJid(chatId);
|
|
305
|
+
const keys = messageIds.map((id) => ({ remoteJid: jid, id }));
|
|
306
|
+
await sock.readMessages(keys);
|
|
307
|
+
}
|
|
308
|
+
// ---- Chats ----
|
|
309
|
+
async modifyChat(chatId, modification) {
|
|
310
|
+
const sock = this.getSock();
|
|
311
|
+
const jid = this.normalizeJid(chatId);
|
|
312
|
+
switch (modification.action) {
|
|
313
|
+
case "archive":
|
|
314
|
+
await sock.chatModify({ archive: true, lastMessages: [] }, jid);
|
|
315
|
+
break;
|
|
316
|
+
case "unarchive":
|
|
317
|
+
await sock.chatModify({ archive: false, lastMessages: [] }, jid);
|
|
318
|
+
break;
|
|
319
|
+
case "pin":
|
|
320
|
+
await sock.chatModify({ pin: true }, jid);
|
|
321
|
+
break;
|
|
322
|
+
case "unpin":
|
|
323
|
+
await sock.chatModify({ pin: false }, jid);
|
|
324
|
+
break;
|
|
325
|
+
case "mute":
|
|
326
|
+
await sock.chatModify({ mute: modification.muteUntil ?? Date.now() + 8 * 60 * 60 * 1000 }, jid);
|
|
327
|
+
break;
|
|
328
|
+
case "unmute":
|
|
329
|
+
await sock.chatModify({ mute: null }, jid);
|
|
330
|
+
break;
|
|
331
|
+
case "delete":
|
|
332
|
+
await sock.chatModify({ delete: true, lastMessages: [] }, jid);
|
|
333
|
+
break;
|
|
334
|
+
case "clear":
|
|
335
|
+
await sock.chatModify({ clear: true, lastMessages: [] }, jid);
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
// ---- Groups ----
|
|
340
|
+
async createGroup(name, participants) {
|
|
341
|
+
const sock = this.getSock();
|
|
342
|
+
const jids = participants.map((p) => this.normalizeJid(p));
|
|
343
|
+
const result = await sock.groupCreate(name, jids);
|
|
344
|
+
return { groupId: result.id, inviteCode: undefined };
|
|
345
|
+
}
|
|
346
|
+
async modifyGroup(groupId, modification) {
|
|
347
|
+
const sock = this.getSock();
|
|
348
|
+
const jid = this.normalizeJid(groupId);
|
|
349
|
+
switch (modification.action) {
|
|
350
|
+
case "updateSubject":
|
|
351
|
+
await sock.groupUpdateSubject(jid, String(modification.value ?? ""));
|
|
352
|
+
break;
|
|
353
|
+
case "updateDescription":
|
|
354
|
+
await sock.groupUpdateDescription(jid, String(modification.value ?? ""));
|
|
355
|
+
break;
|
|
356
|
+
case "updateSettings":
|
|
357
|
+
await sock.groupSettingUpdate(jid, modification.value === "announcement" ? "announcement" : "not_announcement");
|
|
358
|
+
break;
|
|
359
|
+
case "leave":
|
|
360
|
+
await sock.groupLeave(jid);
|
|
361
|
+
break;
|
|
362
|
+
case "revokeInvite":
|
|
363
|
+
await sock.groupRevokeInvite(jid);
|
|
364
|
+
break;
|
|
365
|
+
case "toggleEphemeral": {
|
|
366
|
+
const duration = typeof modification.value === "number" ? modification.value : 0;
|
|
367
|
+
await sock.sendMessage(jid, { disappearingMessagesInChat: duration });
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
async modifyParticipants(groupId, participants, action) {
|
|
373
|
+
const sock = this.getSock();
|
|
374
|
+
const jid = this.normalizeJid(groupId);
|
|
375
|
+
const jids = participants.map((p) => this.normalizeJid(p));
|
|
376
|
+
await sock.groupParticipantsUpdate(jid, jids, action);
|
|
377
|
+
}
|
|
378
|
+
async getGroupMetadata(groupId) {
|
|
379
|
+
const sock = this.getSock();
|
|
380
|
+
const jid = this.normalizeJid(groupId);
|
|
381
|
+
const meta = await sock.groupMetadata(jid);
|
|
382
|
+
return {
|
|
383
|
+
jid: meta.id,
|
|
384
|
+
subject: meta.subject,
|
|
385
|
+
description: meta.desc ?? null,
|
|
386
|
+
ownerJid: meta.owner ?? null,
|
|
387
|
+
participants: meta.participants.map((p) => ({
|
|
388
|
+
jid: p.id,
|
|
389
|
+
isAdmin: p.admin === "admin" || p.admin === "superadmin",
|
|
390
|
+
isSuperAdmin: p.admin === "superadmin",
|
|
391
|
+
})),
|
|
392
|
+
participantCount: meta.participants.length,
|
|
393
|
+
isAnnounce: meta.announce ?? false,
|
|
394
|
+
isLocked: meta.restrict ?? false,
|
|
395
|
+
ephemeralDuration: meta.ephemeralDuration ?? null,
|
|
396
|
+
inviteCode: null,
|
|
397
|
+
createdAt: meta.creation ?? null,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
async getGroupInviteCode(groupId) {
|
|
401
|
+
const sock = this.getSock();
|
|
402
|
+
const code = await sock.groupInviteCode(this.normalizeJid(groupId));
|
|
403
|
+
return code ?? "";
|
|
404
|
+
}
|
|
405
|
+
async joinGroup(inviteCode) {
|
|
406
|
+
const sock = this.getSock();
|
|
407
|
+
const groupId = await sock.groupAcceptInvite(inviteCode);
|
|
408
|
+
return groupId ?? "";
|
|
409
|
+
}
|
|
410
|
+
async handleJoinRequest(groupId, participantJid, action) {
|
|
411
|
+
const sock = this.getSock();
|
|
412
|
+
const jid = this.normalizeJid(groupId);
|
|
413
|
+
const participant = this.normalizeJid(participantJid);
|
|
414
|
+
await sock.groupRequestParticipantsUpdate(jid, [participant], action);
|
|
415
|
+
}
|
|
416
|
+
// ---- Contacts ----
|
|
417
|
+
async checkNumberExists(phone) {
|
|
418
|
+
const sock = this.getSock();
|
|
419
|
+
const jid = this.normalizeJid(phone);
|
|
420
|
+
const results = await sock.onWhatsApp(jid);
|
|
421
|
+
const result = results?.[0];
|
|
422
|
+
return { exists: !!result?.exists, jid: result?.jid ?? null };
|
|
423
|
+
}
|
|
424
|
+
async blockContact(jid) {
|
|
425
|
+
const sock = this.getSock();
|
|
426
|
+
await sock.updateBlockStatus(this.normalizeJid(jid), "block");
|
|
427
|
+
}
|
|
428
|
+
async unblockContact(jid) {
|
|
429
|
+
const sock = this.getSock();
|
|
430
|
+
await sock.updateBlockStatus(this.normalizeJid(jid), "unblock");
|
|
431
|
+
}
|
|
432
|
+
async getBlocklist() {
|
|
433
|
+
const sock = this.getSock();
|
|
434
|
+
const list = await sock.fetchBlocklist();
|
|
435
|
+
return list.filter((jid) => jid !== undefined);
|
|
436
|
+
}
|
|
437
|
+
async getBusinessProfile(jid) {
|
|
438
|
+
const sock = this.getSock();
|
|
439
|
+
const profile = await sock.getBusinessProfile(this.normalizeJid(jid));
|
|
440
|
+
if (!profile)
|
|
441
|
+
return null;
|
|
442
|
+
return {
|
|
443
|
+
name: profile.wid ?? "",
|
|
444
|
+
description: profile.description ?? undefined,
|
|
445
|
+
category: profile.category ?? undefined,
|
|
446
|
+
website: profile.website?.[0] ?? undefined,
|
|
447
|
+
email: profile.email ?? undefined,
|
|
448
|
+
address: profile.address ?? undefined,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
// ---- Profile ----
|
|
452
|
+
async updateProfilePicture(image) {
|
|
453
|
+
const sock = this.getSock();
|
|
454
|
+
await sock.updateProfilePicture(this.getMeJid(), image);
|
|
455
|
+
}
|
|
456
|
+
async removeProfilePicture() {
|
|
457
|
+
const sock = this.getSock();
|
|
458
|
+
await sock.removeProfilePicture(this.getMeJid());
|
|
459
|
+
}
|
|
460
|
+
async updateProfileName(name) {
|
|
461
|
+
const sock = this.getSock();
|
|
462
|
+
await sock.updateProfileName(name);
|
|
463
|
+
}
|
|
464
|
+
async updateProfileStatus(statusText) {
|
|
465
|
+
const sock = this.getSock();
|
|
466
|
+
await sock.updateProfileStatus(statusText);
|
|
467
|
+
}
|
|
468
|
+
async updatePrivacy(setting, value) {
|
|
469
|
+
const sock = this.getSock();
|
|
470
|
+
// Baileys has separate privacy update methods per setting
|
|
471
|
+
switch (setting) {
|
|
472
|
+
case "lastSeen":
|
|
473
|
+
await sock.updateLastSeenPrivacy(value);
|
|
474
|
+
break;
|
|
475
|
+
case "online":
|
|
476
|
+
await sock.updateOnlinePrivacy(value);
|
|
477
|
+
break;
|
|
478
|
+
case "profilePic":
|
|
479
|
+
await sock.updateProfilePicturePrivacy(value);
|
|
480
|
+
break;
|
|
481
|
+
case "status":
|
|
482
|
+
await sock.updateStatusPrivacy(value);
|
|
483
|
+
break;
|
|
484
|
+
case "readReceipts":
|
|
485
|
+
await sock.updateReadReceiptsPrivacy(value);
|
|
486
|
+
break;
|
|
487
|
+
case "groupAdd":
|
|
488
|
+
await sock.updateGroupsAddPrivacy(value);
|
|
489
|
+
break;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
async getPrivacySettings() {
|
|
493
|
+
const sock = this.getSock();
|
|
494
|
+
const settings = await sock.fetchPrivacySettings(true);
|
|
495
|
+
return {
|
|
496
|
+
lastSeen: settings.last ?? "contacts",
|
|
497
|
+
online: settings.online ?? "all",
|
|
498
|
+
profilePic: settings.profile ?? "contacts",
|
|
499
|
+
status: settings.status ?? "contacts",
|
|
500
|
+
readReceipts: settings.readreceipts ?? "all",
|
|
501
|
+
groupAdd: settings.groupadd ?? "contacts",
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
async getProfilePicture(jid) {
|
|
505
|
+
const sock = this.getSock();
|
|
506
|
+
try {
|
|
507
|
+
const url = await sock.profilePictureUrl(this.normalizeJid(jid), "image");
|
|
508
|
+
return url ?? null;
|
|
509
|
+
}
|
|
510
|
+
catch {
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
// ---- Status / Stories ----
|
|
515
|
+
async sendStatus(content) {
|
|
516
|
+
const sock = this.getSock();
|
|
517
|
+
const statusJid = "status@broadcast";
|
|
518
|
+
switch (content.type) {
|
|
519
|
+
case "text":
|
|
520
|
+
await sock.sendMessage(statusJid, {
|
|
521
|
+
text: content.text ?? "",
|
|
522
|
+
backgroundColor: content.backgroundColor,
|
|
523
|
+
font: content.font,
|
|
524
|
+
});
|
|
525
|
+
break;
|
|
526
|
+
case "image":
|
|
527
|
+
await sock.sendMessage(statusJid, {
|
|
528
|
+
image: this.resolveMedia(content.media ?? ""),
|
|
529
|
+
caption: content.caption,
|
|
530
|
+
});
|
|
531
|
+
break;
|
|
532
|
+
case "video":
|
|
533
|
+
await sock.sendMessage(statusJid, {
|
|
534
|
+
video: this.resolveMedia(content.media ?? ""),
|
|
535
|
+
caption: content.caption,
|
|
536
|
+
});
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
// ---- Newsletter / Channels ----
|
|
541
|
+
async newsletterFollow(jid) {
|
|
542
|
+
const sock = this.getSock();
|
|
543
|
+
await sock.newsletterFollow(jid);
|
|
544
|
+
}
|
|
545
|
+
async newsletterUnfollow(jid) {
|
|
546
|
+
const sock = this.getSock();
|
|
547
|
+
await sock.newsletterUnfollow(jid);
|
|
548
|
+
}
|
|
549
|
+
async newsletterSend(jid, text) {
|
|
550
|
+
const sock = this.getSock();
|
|
551
|
+
const result = await sock.sendMessage(jid, { text });
|
|
552
|
+
return { messageId: result?.key.id ?? "", timestamp: Date.now(), status: "sent" };
|
|
553
|
+
}
|
|
554
|
+
// ---- Calls ----
|
|
555
|
+
async rejectCall(callId) {
|
|
556
|
+
const sock = this.getSock();
|
|
557
|
+
// rejectCall requires (callId, callFrom) — we pass empty string for callFrom
|
|
558
|
+
// as we don't have the caller info in this context
|
|
559
|
+
await sock.rejectCall(callId, "");
|
|
560
|
+
}
|
|
561
|
+
// ---- Data access ----
|
|
562
|
+
async getContacts(search) {
|
|
563
|
+
// Flush in-memory cache into DB (contacts arrive via contacts.upsert event)
|
|
564
|
+
if (this.contactCache.size > 0) {
|
|
565
|
+
const now = Date.now();
|
|
566
|
+
for (const c of this.contactCache.values()) {
|
|
567
|
+
let lid = c.lid ?? null;
|
|
568
|
+
const extractPhone = (jid) => jid.split("@")[0].split(":")[0];
|
|
569
|
+
let phone;
|
|
570
|
+
if (c.phoneNumber) {
|
|
571
|
+
phone = extractPhone(c.phoneNumber);
|
|
572
|
+
}
|
|
573
|
+
else if (c.id.endsWith("@lid")) {
|
|
574
|
+
lid = c.id;
|
|
575
|
+
try {
|
|
576
|
+
const pn = await this.resolvePn(c.id);
|
|
577
|
+
phone = pn !== c.id ? extractPhone(pn) : null;
|
|
578
|
+
}
|
|
579
|
+
catch {
|
|
580
|
+
phone = null;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
phone = extractPhone(c.id);
|
|
585
|
+
}
|
|
586
|
+
db.insert(contactsTable)
|
|
587
|
+
.values({
|
|
588
|
+
instanceId: this.instanceId,
|
|
589
|
+
jid: c.id,
|
|
590
|
+
name: c.name ?? null,
|
|
591
|
+
notifyName: c.notify ?? null,
|
|
592
|
+
phone,
|
|
593
|
+
lid,
|
|
594
|
+
isBusiness: c.verifiedName ? 1 : 0,
|
|
595
|
+
isBlocked: 0,
|
|
596
|
+
updatedAt: now,
|
|
597
|
+
})
|
|
598
|
+
.onConflictDoUpdate({
|
|
599
|
+
target: [contactsTable.instanceId, contactsTable.jid],
|
|
600
|
+
set: { name: c.name ?? null, notifyName: c.notify ?? null, phone, lid, updatedAt: now },
|
|
601
|
+
})
|
|
602
|
+
.run();
|
|
603
|
+
}
|
|
604
|
+
this.contactCache.clear();
|
|
605
|
+
}
|
|
606
|
+
let rows;
|
|
607
|
+
if (search) {
|
|
608
|
+
// Split query into words and match any word against any field
|
|
609
|
+
const words = search.trim().split(/\s+/).filter(Boolean);
|
|
610
|
+
const wordConditions = words.map((w) => or(like(contactsTable.name, `%${w}%`), like(contactsTable.notifyName, `%${w}%`), like(contactsTable.phone, `%${w}%`), like(contactsTable.lid, `%${w}%`)));
|
|
611
|
+
rows = db
|
|
612
|
+
.select()
|
|
613
|
+
.from(contactsTable)
|
|
614
|
+
.where(and(eq(contactsTable.instanceId, this.instanceId), or(...wordConditions)))
|
|
615
|
+
.all();
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
rows = db
|
|
619
|
+
.select()
|
|
620
|
+
.from(contactsTable)
|
|
621
|
+
.where(eq(contactsTable.instanceId, this.instanceId))
|
|
622
|
+
.all();
|
|
623
|
+
}
|
|
624
|
+
return rows.map((r) => ({
|
|
625
|
+
jid: r.jid,
|
|
626
|
+
name: r.name ?? r.notifyName ?? null,
|
|
627
|
+
notifyName: r.notifyName ?? null,
|
|
628
|
+
phone: r.phone ?? null,
|
|
629
|
+
profilePicUrl: r.profilePicUrl ?? null,
|
|
630
|
+
isBusiness: r.isBusiness === 1,
|
|
631
|
+
isBlocked: r.isBlocked === 1,
|
|
632
|
+
}));
|
|
633
|
+
}
|
|
634
|
+
async getChats() {
|
|
635
|
+
return [];
|
|
636
|
+
}
|
|
637
|
+
async getMessages(chatId, limit = 50) {
|
|
638
|
+
const rows = db
|
|
639
|
+
.select()
|
|
640
|
+
.from(messagesTable)
|
|
641
|
+
.where(and(eq(messagesTable.instanceId, this.instanceId), eq(messagesTable.chatId, chatId)))
|
|
642
|
+
.orderBy(desc(messagesTable.timestamp))
|
|
643
|
+
.limit(limit)
|
|
644
|
+
.all();
|
|
645
|
+
return rows.map((r) => ({
|
|
646
|
+
id: r.id,
|
|
647
|
+
chatId: r.chatId,
|
|
648
|
+
senderId: r.senderId,
|
|
649
|
+
type: r.type,
|
|
650
|
+
content: r.content,
|
|
651
|
+
mediaUrl: r.mediaUrl,
|
|
652
|
+
quotedMessageId: r.quotedId,
|
|
653
|
+
isFromMe: r.isFromMe === 1,
|
|
654
|
+
isForwarded: r.isForwarded === 1,
|
|
655
|
+
status: r.status,
|
|
656
|
+
timestamp: r.timestamp,
|
|
657
|
+
}));
|
|
658
|
+
}
|
|
659
|
+
async getProfileInfo() {
|
|
660
|
+
const sock = this.getSock();
|
|
661
|
+
const meJid = this.getMeJid();
|
|
662
|
+
let pictureUrl = null;
|
|
663
|
+
try {
|
|
664
|
+
const url = await sock.profilePictureUrl(meJid, "image");
|
|
665
|
+
pictureUrl = url ?? null;
|
|
666
|
+
}
|
|
667
|
+
catch {
|
|
668
|
+
// no profile picture
|
|
669
|
+
}
|
|
670
|
+
return {
|
|
671
|
+
name: sock.user?.name ?? "",
|
|
672
|
+
status: "",
|
|
673
|
+
pictureUrl,
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
// ---- Message Persistence ----
|
|
677
|
+
persistMessage(event) {
|
|
678
|
+
try {
|
|
679
|
+
const { message, chatId, instanceId } = event;
|
|
680
|
+
db.insert(messagesTable)
|
|
681
|
+
.values({
|
|
682
|
+
id: message.id,
|
|
683
|
+
instanceId,
|
|
684
|
+
chatId,
|
|
685
|
+
senderId: message.sender,
|
|
686
|
+
type: message.type,
|
|
687
|
+
content: message.content,
|
|
688
|
+
mediaUrl: message.mediaUrl,
|
|
689
|
+
quotedId: message.quotedMessageId,
|
|
690
|
+
isFromMe: message.isFromMe ? 1 : 0,
|
|
691
|
+
isForwarded: 0,
|
|
692
|
+
status: message.isFromMe ? "sent" : "received",
|
|
693
|
+
timestamp: message.timestamp,
|
|
694
|
+
})
|
|
695
|
+
.onConflictDoUpdate({
|
|
696
|
+
target: [messagesTable.instanceId, messagesTable.id],
|
|
697
|
+
set: {
|
|
698
|
+
content: message.content,
|
|
699
|
+
status: message.isFromMe ? "sent" : "received",
|
|
700
|
+
},
|
|
701
|
+
})
|
|
702
|
+
.run();
|
|
703
|
+
logger.info({ instanceId, messageId: message.id, chatId, fromMe: message.isFromMe }, "Message persisted to DB");
|
|
704
|
+
}
|
|
705
|
+
catch (err) {
|
|
706
|
+
logger.error({ err, messageId: event.message.id }, "Failed to persist message");
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
updateMessageStatus(messageId, status) {
|
|
710
|
+
try {
|
|
711
|
+
db.update(messagesTable)
|
|
712
|
+
.set({ status })
|
|
713
|
+
.where(and(eq(messagesTable.instanceId, this.instanceId), eq(messagesTable.id, messageId)))
|
|
714
|
+
.run();
|
|
715
|
+
}
|
|
716
|
+
catch (err) {
|
|
717
|
+
logger.error({ err, messageId }, "Failed to update message status");
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
// ---- Events ----
|
|
721
|
+
on(event, handler) {
|
|
722
|
+
if (!this.listeners.has(event)) {
|
|
723
|
+
this.listeners.set(event, new Set());
|
|
724
|
+
}
|
|
725
|
+
this.listeners.get(event).add(handler);
|
|
726
|
+
}
|
|
727
|
+
off(event, handler) {
|
|
728
|
+
this.listeners.get(event)?.delete(handler);
|
|
729
|
+
}
|
|
730
|
+
// ---- Private event helpers ----
|
|
731
|
+
emit(event, payload) {
|
|
732
|
+
const handlers = this.listeners.get(event);
|
|
733
|
+
if (handlers) {
|
|
734
|
+
for (const handler of handlers) {
|
|
735
|
+
try {
|
|
736
|
+
handler(payload);
|
|
737
|
+
}
|
|
738
|
+
catch (err) {
|
|
739
|
+
logger.error({ err, event }, "Error in event handler");
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
bindEvents(sock) {
|
|
745
|
+
sock.ev.on("connection.update", (update) => {
|
|
746
|
+
const { connection, lastDisconnect, qr } = update;
|
|
747
|
+
if (qr) {
|
|
748
|
+
this.qrCode = qr;
|
|
749
|
+
this.status = "qr_pending";
|
|
750
|
+
if (this.pairingPhone && sock) {
|
|
751
|
+
// Evolution API pattern: call requestPairingCode inside the QR event, with a small delay
|
|
752
|
+
setTimeout(async () => {
|
|
753
|
+
try {
|
|
754
|
+
const code = await sock.requestPairingCode(this.pairingPhone);
|
|
755
|
+
this.pairingCodeValue = code ?? null;
|
|
756
|
+
if (this.pairingCodeResolver) {
|
|
757
|
+
this.pairingCodeResolver(this.pairingCodeValue);
|
|
758
|
+
this.pairingCodeResolver = null;
|
|
759
|
+
}
|
|
760
|
+
logger.info({ instanceId: this.instanceId, code }, "Pairing code generated");
|
|
761
|
+
}
|
|
762
|
+
catch (err) {
|
|
763
|
+
logger.error({ instanceId: this.instanceId, err }, "Failed to generate pairing code");
|
|
764
|
+
if (this.pairingCodeResolver) {
|
|
765
|
+
this.pairingCodeResolver(null);
|
|
766
|
+
this.pairingCodeResolver = null;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}, 1000);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
if (connection === "open") {
|
|
773
|
+
this.status = "connected";
|
|
774
|
+
this.qrCode = null;
|
|
775
|
+
this.pairingPhone = null;
|
|
776
|
+
this.pairingCodeValue = null;
|
|
777
|
+
this.pairingCodeResolver = null;
|
|
778
|
+
this.reconnectAttempt = 0;
|
|
779
|
+
logger.info({ instanceId: this.instanceId }, "Connected to WhatsApp");
|
|
780
|
+
}
|
|
781
|
+
if (connection === "close") {
|
|
782
|
+
this.status = "disconnected";
|
|
783
|
+
const boom = lastDisconnect?.error?.output;
|
|
784
|
+
const statusCode = boom?.statusCode;
|
|
785
|
+
if (statusCode === DisconnectReason.loggedOut) {
|
|
786
|
+
logger.info({ instanceId: this.instanceId }, "Logged out, clearing auth");
|
|
787
|
+
clearAuthState(this.instanceId);
|
|
788
|
+
this.sock = null;
|
|
789
|
+
}
|
|
790
|
+
else if (this.autoReconnect) {
|
|
791
|
+
this.scheduleReconnect();
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
const normalized = normalizeConnectionUpdate(this.instanceId, update);
|
|
795
|
+
if (normalized) {
|
|
796
|
+
this.emit("connection.changed", normalized);
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
// creds.update is not buffered — use ev.on directly
|
|
800
|
+
sock.ev.on("creds.update", async () => {
|
|
801
|
+
await this.saveCreds?.();
|
|
802
|
+
});
|
|
803
|
+
const cacheContacts = (contacts) => {
|
|
804
|
+
for (const c of contacts) {
|
|
805
|
+
if (!c.id)
|
|
806
|
+
continue;
|
|
807
|
+
// When contact arrives as LID with a phoneNumber, use PN as primary key
|
|
808
|
+
let primaryId = c.id;
|
|
809
|
+
let lid = c.lid;
|
|
810
|
+
if (c.id.endsWith("@lid") && c.phoneNumber) {
|
|
811
|
+
lid = c.id;
|
|
812
|
+
primaryId = c.phoneNumber;
|
|
813
|
+
}
|
|
814
|
+
const existing = this.contactCache.get(primaryId);
|
|
815
|
+
// Preserve phonebook name (from app-state contactAction) if it differs from notify
|
|
816
|
+
const existingHasPhonebookName = existing?.name && existing.name !== existing.notify;
|
|
817
|
+
const newName = existingHasPhonebookName ? existing.name : (c.name ?? existing?.name);
|
|
818
|
+
this.contactCache.set(primaryId, {
|
|
819
|
+
id: primaryId,
|
|
820
|
+
name: newName,
|
|
821
|
+
notify: c.notify ?? existing?.notify,
|
|
822
|
+
verifiedName: c.verifiedName ?? existing?.verifiedName,
|
|
823
|
+
phoneNumber: c.phoneNumber ?? existing?.phoneNumber,
|
|
824
|
+
lid: lid ?? existing?.lid,
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
// Use ev.process() to receive ALL buffered events after sync flush
|
|
829
|
+
sock.ev.process((events) => {
|
|
830
|
+
const eventKeys = Object.keys(events);
|
|
831
|
+
if (eventKeys.length > 0) {
|
|
832
|
+
logger.debug({ instanceId: this.instanceId, events: eventKeys }, "ev.process fired");
|
|
833
|
+
}
|
|
834
|
+
// --- LID Mapping (v7+): store LID <-> PN mappings ---
|
|
835
|
+
if (events["lid-mapping.update"]) {
|
|
836
|
+
const mapping = events["lid-mapping.update"];
|
|
837
|
+
logger.debug({ instanceId: this.instanceId, lid: mapping.lid, pn: mapping.pn }, "LID mapping updated");
|
|
838
|
+
}
|
|
839
|
+
// --- Chats: extract phonebook names from chat metadata ---
|
|
840
|
+
if (events["chats.upsert"]) {
|
|
841
|
+
const namedChats = events["chats.upsert"].filter((ch) => !!ch.id && !!ch.name && !ch.id.endsWith("@g.us") && !ch.id.endsWith("@broadcast"));
|
|
842
|
+
if (namedChats.length > 0) {
|
|
843
|
+
cacheContacts(namedChats.map((ch) => ({ id: ch.id, name: ch.name })));
|
|
844
|
+
logger.debug({ instanceId: this.instanceId, count: namedChats.length }, "Contacts cached from chat names (chats.upsert)");
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
// --- Contacts (arrives buffered during initial sync) ---
|
|
848
|
+
if (events["contacts.upsert"]) {
|
|
849
|
+
const contacts = events["contacts.upsert"];
|
|
850
|
+
cacheContacts(contacts);
|
|
851
|
+
logger.debug({ instanceId: this.instanceId, count: contacts.length }, "Contacts cached (upsert)");
|
|
852
|
+
}
|
|
853
|
+
if (events["contacts.update"]) {
|
|
854
|
+
const normalized = normalizeContactsUpdate(this.instanceId, events["contacts.update"]);
|
|
855
|
+
for (const event of normalized)
|
|
856
|
+
this.emit("contact.updated", event);
|
|
857
|
+
}
|
|
858
|
+
// --- History sync: contacts arrive here during first sync ---
|
|
859
|
+
if (events["messaging-history.set"]) {
|
|
860
|
+
const { contacts = [], chats = [] } = events["messaging-history.set"];
|
|
861
|
+
const filtered = contacts.filter((c) => c.id && (c.notify || c.name));
|
|
862
|
+
if (filtered.length > 0) {
|
|
863
|
+
cacheContacts(filtered.map((c) => ({
|
|
864
|
+
id: c.id,
|
|
865
|
+
name: c.name ?? c.notify,
|
|
866
|
+
notify: c.notify,
|
|
867
|
+
verifiedName: c.verifiedName,
|
|
868
|
+
phoneNumber: c.phoneNumber,
|
|
869
|
+
lid: c.lid,
|
|
870
|
+
})));
|
|
871
|
+
logger.debug({ instanceId: this.instanceId, contacts: filtered.length, chats: chats.length }, "Contacts cached (history sync)");
|
|
872
|
+
}
|
|
873
|
+
// Extract contact names from chat list (may include phonebook names)
|
|
874
|
+
const chatContacts = chats.filter((ch) => !!ch.id && !!ch.name && !ch.id.endsWith("@g.us") && !ch.id.endsWith("@broadcast"));
|
|
875
|
+
if (chatContacts.length > 0) {
|
|
876
|
+
cacheContacts(chatContacts.map((ch) => ({
|
|
877
|
+
id: ch.id,
|
|
878
|
+
name: ch.name,
|
|
879
|
+
})));
|
|
880
|
+
logger.debug({ instanceId: this.instanceId, chatContacts: chatContacts.length }, "Contacts cached from chat names");
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
// --- Messages ---
|
|
884
|
+
if (events["messages.upsert"]) {
|
|
885
|
+
const data = events["messages.upsert"];
|
|
886
|
+
logger.debug({ instanceId: this.instanceId, type: data.type, count: data.messages.length }, "messages.upsert event received");
|
|
887
|
+
for (const msg of data.messages) {
|
|
888
|
+
normalizeLidInMessage(msg);
|
|
889
|
+
if (msg.message?.protocolMessage?.type === 14) {
|
|
890
|
+
const editEvent = normalizeMessageEdit(this.instanceId, msg, msg.key.remoteJid ?? "");
|
|
891
|
+
this.emit("message.edited", editEvent);
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
if (msg.pushName && msg.key.remoteJid && !msg.key.fromMe) {
|
|
895
|
+
cacheContacts([{ id: msg.key.remoteJid, name: msg.pushName }]);
|
|
896
|
+
}
|
|
897
|
+
// Persist ALL messages (notify + append) to DB
|
|
898
|
+
if (msg.key.id && msg.key.remoteJid && !msg.message?.protocolMessage) {
|
|
899
|
+
const normalized = {
|
|
900
|
+
instanceId: this.instanceId,
|
|
901
|
+
chatId: getChatJid(msg),
|
|
902
|
+
message: {
|
|
903
|
+
id: msg.key.id,
|
|
904
|
+
sender: getSenderJid(msg),
|
|
905
|
+
timestamp: typeof msg.messageTimestamp === "number"
|
|
906
|
+
? msg.messageTimestamp
|
|
907
|
+
: Number(msg.messageTimestamp ?? 0),
|
|
908
|
+
type: extractMessageType(msg),
|
|
909
|
+
content: extractTextContent(msg),
|
|
910
|
+
mediaUrl: null,
|
|
911
|
+
quotedMessageId: extractQuotedMessageId(msg),
|
|
912
|
+
isFromMe: msg.key.fromMe ?? false,
|
|
913
|
+
},
|
|
914
|
+
};
|
|
915
|
+
this.persistMessage(normalized);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
// Emit events only for incoming notify-type messages (not our own sends)
|
|
919
|
+
const notifyEvents = normalizeMessagesUpsert(this.instanceId, data);
|
|
920
|
+
logger.info({ instanceId: this.instanceId, notifyCount: notifyEvents.length }, "Notify messages for emission");
|
|
921
|
+
for (const event of notifyEvents) {
|
|
922
|
+
this.emit("message.received", event);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
if (events["messages.update"]) {
|
|
926
|
+
const normalized = normalizeMessagesUpdate(this.instanceId, events["messages.update"]);
|
|
927
|
+
for (const event of normalized) {
|
|
928
|
+
this.updateMessageStatus(event.messageId, event.status);
|
|
929
|
+
this.emit("message.updated", event);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
if (events["messages.delete"]) {
|
|
933
|
+
const normalized = normalizeMessagesDelete(this.instanceId, events["messages.delete"]);
|
|
934
|
+
for (const event of normalized)
|
|
935
|
+
this.emit("message.deleted", event);
|
|
936
|
+
}
|
|
937
|
+
if (events["messages.reaction"]) {
|
|
938
|
+
const normalized = normalizeMessagesReaction(this.instanceId, events["messages.reaction"]);
|
|
939
|
+
for (const event of normalized)
|
|
940
|
+
this.emit("message.reaction", event);
|
|
941
|
+
}
|
|
942
|
+
// --- Presence ---
|
|
943
|
+
if (events["presence.update"]) {
|
|
944
|
+
const normalized = normalizePresenceUpdate(this.instanceId, events["presence.update"]);
|
|
945
|
+
for (const event of normalized)
|
|
946
|
+
this.emit("presence.updated", event);
|
|
947
|
+
}
|
|
948
|
+
// --- Groups ---
|
|
949
|
+
if (events["groups.update"]) {
|
|
950
|
+
const normalized = normalizeGroupsUpdate(this.instanceId, events["groups.update"]);
|
|
951
|
+
for (const event of normalized)
|
|
952
|
+
this.emit("group.updated", event);
|
|
953
|
+
}
|
|
954
|
+
if (events["group-participants.update"]) {
|
|
955
|
+
const event = normalizeGroupParticipantsUpdate(this.instanceId, events["group-participants.update"]);
|
|
956
|
+
this.emit("group.participants_changed", event);
|
|
957
|
+
}
|
|
958
|
+
// --- Calls ---
|
|
959
|
+
if (events["call"]) {
|
|
960
|
+
const normalized = normalizeCallEvent(this.instanceId, events["call"]);
|
|
961
|
+
for (const event of normalized)
|
|
962
|
+
this.emit("call.received", event);
|
|
963
|
+
}
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
scheduleReconnect() {
|
|
967
|
+
if (this.reconnectAttempt >= RECONNECT_MAX_ATTEMPTS) {
|
|
968
|
+
logger.error({ instanceId: this.instanceId, attempts: this.reconnectAttempt }, "Max reconnect attempts reached");
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
const delay = Math.min(RECONNECT_INITIAL_DELAY_MS * Math.pow(2, this.reconnectAttempt), RECONNECT_MAX_DELAY_MS);
|
|
972
|
+
this.reconnectAttempt++;
|
|
973
|
+
logger.info({ instanceId: this.instanceId, attempt: this.reconnectAttempt, delayMs: delay }, "Scheduling reconnect");
|
|
974
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
975
|
+
try {
|
|
976
|
+
this.sock = null;
|
|
977
|
+
await this.connect();
|
|
978
|
+
}
|
|
979
|
+
catch (err) {
|
|
980
|
+
logger.error({ err, instanceId: this.instanceId }, "Reconnect failed");
|
|
981
|
+
this.scheduleReconnect();
|
|
982
|
+
}
|
|
983
|
+
}, delay);
|
|
984
|
+
}
|
|
985
|
+
clearReconnectTimer() {
|
|
986
|
+
if (this.reconnectTimer) {
|
|
987
|
+
clearTimeout(this.reconnectTimer);
|
|
988
|
+
this.reconnectTimer = null;
|
|
989
|
+
}
|
|
990
|
+
this.reconnectAttempt = 0;
|
|
991
|
+
}
|
|
992
|
+
}
|