@operor/provider-baileys 0.1.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/dist/index.d.ts +77 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +352 -0
- package/dist/index.js.map +1 -0
- package/package.json +23 -0
- package/src/BaileysProvider.ts +530 -0
- package/src/index.ts +2 -0
- package/tsconfig.json +9 -0
- package/tsdown.config.ts +10 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import EventEmitter from "eventemitter3";
|
|
2
|
+
import * as _whiskeysockets_baileys0 from "@whiskeysockets/baileys";
|
|
3
|
+
import { MessageProvider, OutgoingMessage } from "@operor/core";
|
|
4
|
+
|
|
5
|
+
//#region src/BaileysProvider.d.ts
|
|
6
|
+
interface BaileysConfig {
|
|
7
|
+
authFolder?: string;
|
|
8
|
+
printQRInTerminal?: boolean;
|
|
9
|
+
logLevel?: 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace' | 'silent';
|
|
10
|
+
groupMode?: 'mention-only' | 'all' | 'ignore';
|
|
11
|
+
}
|
|
12
|
+
declare class BaileysProvider extends EventEmitter implements MessageProvider {
|
|
13
|
+
readonly name = "baileys";
|
|
14
|
+
private sock;
|
|
15
|
+
private config;
|
|
16
|
+
private isConnected;
|
|
17
|
+
private authFolder;
|
|
18
|
+
private jidMap;
|
|
19
|
+
private lidToPhone;
|
|
20
|
+
private phoneToLid;
|
|
21
|
+
constructor(config?: BaileysConfig);
|
|
22
|
+
/**
|
|
23
|
+
* Connect to WhatsApp
|
|
24
|
+
*/
|
|
25
|
+
connect(): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Disconnect from WhatsApp
|
|
28
|
+
*/
|
|
29
|
+
disconnect(): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Show "composing…" typing indicator in WhatsApp
|
|
32
|
+
*/
|
|
33
|
+
sendTypingIndicator(to: string): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Send a message
|
|
36
|
+
*/
|
|
37
|
+
sendMessage(to: string, message: OutgoingMessage): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Get the bot's phone number from its JID.
|
|
40
|
+
*/
|
|
41
|
+
private getBotNumbers;
|
|
42
|
+
/**
|
|
43
|
+
* Remove @mention patterns from text using the mentionedJid array.
|
|
44
|
+
*/
|
|
45
|
+
private stripMentions;
|
|
46
|
+
/**
|
|
47
|
+
* Extract contextInfo from whichever message type carries it.
|
|
48
|
+
*/
|
|
49
|
+
private getContextInfo;
|
|
50
|
+
/**
|
|
51
|
+
* Handle incoming message from Baileys
|
|
52
|
+
*/
|
|
53
|
+
private handleIncomingMessage;
|
|
54
|
+
/**
|
|
55
|
+
* Extract text from message
|
|
56
|
+
*/
|
|
57
|
+
private extractMessageText;
|
|
58
|
+
/**
|
|
59
|
+
* Get message type
|
|
60
|
+
*/
|
|
61
|
+
private getMessageType;
|
|
62
|
+
/**
|
|
63
|
+
* Format phone number to JID
|
|
64
|
+
*/
|
|
65
|
+
private formatJID;
|
|
66
|
+
/**
|
|
67
|
+
* Check if connected
|
|
68
|
+
*/
|
|
69
|
+
isActive(): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Get current user info
|
|
72
|
+
*/
|
|
73
|
+
getUserInfo(): _whiskeysockets_baileys0.Contact | undefined;
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
export { type BaileysConfig, BaileysProvider };
|
|
77
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/BaileysProvider.ts"],"mappings":";;;;;UAiBiB,aAAA;EACf,UAAA;EACA,iBAAA;EACA,QAAA;EACA,SAAA;AAAA;AAAA,cAGW,eAAA,SAAwB,YAAA,YAAwB,eAAA;EAAA,SAC3C,IAAA;EAAA,QACR,IAAA;EAAA,QACA,MAAA;EAAA,QACA,WAAA;EAAA,QACA,UAAA;EAAA,QACA,MAAA;EAAA,QACA,UAAA;EAAA,QACA,UAAA;cAEI,MAAA,GAAQ,aAAA;EAAA;;;EAcd,OAAA,CAAA,GAAW,OAAA;EA+JsB;;;EAzBjC,UAAA,CAAA,GAAc,OAAA;EA9JuC;;;EA0KrD,mBAAA,CAAoB,EAAA,WAAa,OAAA;EA1KoB;;;EAuLrD,WAAA,CAAY,EAAA,UAAY,OAAA,EAAS,eAAA,GAAkB,OAAA;EAnLjD;;;EAAA,QA8NA,aAAA;EA1NA;;;EAAA,QAwOA,aAAA;EAxNF;;;EAAA,QAoOE,cAAA;EAlFF;;;EAAA,QAiGQ,qBAAA;EApFI;;;EAAA,QA6OV,kBAAA;EAlMA;;;EAAA,QAqOA,cAAA;EAnCA;;;EAAA,QAuDA,SAAA;EA2BR;;;EAPA,QAAA,CAAA;;;;EAOA,WAAA,CAAA,GA/TgE,wBAAA,CA+TrD,OAAA;AAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import EventEmitter from "eventemitter3";
|
|
2
|
+
import makeWASocket, { DisconnectReason, useMultiFileAuthState } from "@whiskeysockets/baileys";
|
|
3
|
+
import qrcode from "qrcode-terminal";
|
|
4
|
+
import pino from "pino";
|
|
5
|
+
import * as fs from "fs";
|
|
6
|
+
|
|
7
|
+
//#region src/BaileysProvider.ts
|
|
8
|
+
var BaileysProvider = class extends EventEmitter {
|
|
9
|
+
name = "baileys";
|
|
10
|
+
sock = null;
|
|
11
|
+
config;
|
|
12
|
+
isConnected = false;
|
|
13
|
+
authFolder;
|
|
14
|
+
jidMap = /* @__PURE__ */ new Map();
|
|
15
|
+
lidToPhone = /* @__PURE__ */ new Map();
|
|
16
|
+
phoneToLid = /* @__PURE__ */ new Map();
|
|
17
|
+
constructor(config = {}) {
|
|
18
|
+
super();
|
|
19
|
+
this.config = {
|
|
20
|
+
authFolder: config.authFolder || "./baileys_auth",
|
|
21
|
+
printQRInTerminal: config.printQRInTerminal !== false,
|
|
22
|
+
logLevel: config.logLevel || "silent",
|
|
23
|
+
groupMode: config.groupMode || "mention-only"
|
|
24
|
+
};
|
|
25
|
+
this.authFolder = this.config.authFolder;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Connect to WhatsApp
|
|
29
|
+
*/
|
|
30
|
+
async connect() {
|
|
31
|
+
if (this.sock && this.isConnected) {
|
|
32
|
+
console.log("⚠️ Already connected, skipping...");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (this.sock) {
|
|
36
|
+
console.log("🧹 Cleaning up existing socket...");
|
|
37
|
+
this.sock.ev.removeAllListeners("connection.update");
|
|
38
|
+
this.sock.ev.removeAllListeners("creds.update");
|
|
39
|
+
this.sock.ev.removeAllListeners("messages.upsert");
|
|
40
|
+
this.sock.ev.removeAllListeners("messages.update");
|
|
41
|
+
this.sock = null;
|
|
42
|
+
}
|
|
43
|
+
if (!fs.existsSync(this.authFolder)) fs.mkdirSync(this.authFolder, { recursive: true });
|
|
44
|
+
const { state, saveCreds } = await useMultiFileAuthState(this.authFolder);
|
|
45
|
+
this.sock = makeWASocket({
|
|
46
|
+
auth: state,
|
|
47
|
+
printQRInTerminal: this.config.printQRInTerminal,
|
|
48
|
+
logger: pino({ level: "fatal" }),
|
|
49
|
+
browser: [
|
|
50
|
+
"Operor",
|
|
51
|
+
"Chrome",
|
|
52
|
+
"1.0.0"
|
|
53
|
+
],
|
|
54
|
+
msgRetryCounterCache: {
|
|
55
|
+
get: (key) => void 0,
|
|
56
|
+
set: (key, value) => {},
|
|
57
|
+
del: (key) => {},
|
|
58
|
+
flushAll: () => {}
|
|
59
|
+
},
|
|
60
|
+
generateHighQualityLinkPreview: true
|
|
61
|
+
});
|
|
62
|
+
this.sock.ev.on("connection.update", (update) => {
|
|
63
|
+
const { connection, lastDisconnect, qr } = update;
|
|
64
|
+
if (qr && this.config.printQRInTerminal) {
|
|
65
|
+
console.log("\n📱 Scan this QR code with WhatsApp:\n");
|
|
66
|
+
qrcode.generate(qr, { small: true });
|
|
67
|
+
console.log("");
|
|
68
|
+
this.emit("qr", qr);
|
|
69
|
+
}
|
|
70
|
+
if (connection === "close") {
|
|
71
|
+
this.isConnected = false;
|
|
72
|
+
const statusCode = (lastDisconnect?.error)?.output?.statusCode;
|
|
73
|
+
const shouldReconnect = statusCode !== DisconnectReason.loggedOut;
|
|
74
|
+
console.log(`❌ Connection closed`);
|
|
75
|
+
console.log(` Status code: ${statusCode}`);
|
|
76
|
+
console.log(` Should reconnect: ${shouldReconnect}`);
|
|
77
|
+
if (shouldReconnect) {
|
|
78
|
+
console.log("🔄 Reconnecting in 5 seconds...");
|
|
79
|
+
setTimeout(() => {
|
|
80
|
+
this.connect();
|
|
81
|
+
}, 5e3);
|
|
82
|
+
} else {
|
|
83
|
+
console.log("⚠️ Logged out - please delete baileys_auth/ and scan QR again");
|
|
84
|
+
this.emit("disconnected", "logged_out");
|
|
85
|
+
}
|
|
86
|
+
} else if (connection === "open") {
|
|
87
|
+
this.isConnected = true;
|
|
88
|
+
console.log("✅ Baileys connected to WhatsApp!");
|
|
89
|
+
console.log("⏳ Waiting for message sync...");
|
|
90
|
+
setTimeout(() => {
|
|
91
|
+
console.log("✅ Baileys ready to receive messages!");
|
|
92
|
+
this.emit("ready");
|
|
93
|
+
}, 3e3);
|
|
94
|
+
} else if (connection === "connecting") console.log("🔄 Connecting to WhatsApp...");
|
|
95
|
+
});
|
|
96
|
+
this.sock.ev.on("creds.update", saveCreds);
|
|
97
|
+
this.sock.ev.on("lid-mapping.update", (mapping) => {
|
|
98
|
+
for (const [lid, phone] of Object.entries(mapping)) {
|
|
99
|
+
const lidNum = lid.split("@")[0].split(":")[0];
|
|
100
|
+
const phoneNum = phone.split("@")[0].split(":")[0];
|
|
101
|
+
if (lidNum && phoneNum) {
|
|
102
|
+
this.lidToPhone.set(lidNum, phoneNum);
|
|
103
|
+
this.phoneToLid.set(phoneNum, lidNum);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
this.sock.ev.on("messages.upsert", async (m) => {
|
|
108
|
+
console.log("🔔 [Baileys Event] messages.upsert triggered!");
|
|
109
|
+
console.log(` Number of messages: ${m.messages.length}`);
|
|
110
|
+
console.log(` Type: ${m.type}`);
|
|
111
|
+
for (const msg of m.messages) {
|
|
112
|
+
console.log(` Processing message: ${msg.key.id}`);
|
|
113
|
+
await this.handleIncomingMessage(msg);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
this.sock.ev.on("messages.update", (updates) => {
|
|
117
|
+
for (const update of updates) if (update.update.status) this.emit("message:status", {
|
|
118
|
+
messageId: update.key.id,
|
|
119
|
+
status: update.update.status
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Disconnect from WhatsApp
|
|
125
|
+
*/
|
|
126
|
+
async disconnect() {
|
|
127
|
+
if (this.sock) {
|
|
128
|
+
await this.sock.logout();
|
|
129
|
+
this.sock = null;
|
|
130
|
+
this.isConnected = false;
|
|
131
|
+
console.log("Baileys disconnected");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Show "composing…" typing indicator in WhatsApp
|
|
136
|
+
*/
|
|
137
|
+
async sendTypingIndicator(to) {
|
|
138
|
+
if (!this.sock || !this.isConnected) return;
|
|
139
|
+
const jid = this.jidMap.get(to) || this.formatJID(to);
|
|
140
|
+
try {
|
|
141
|
+
await this.sock.sendPresenceUpdate("composing", jid);
|
|
142
|
+
} catch {}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Send a message
|
|
146
|
+
*/
|
|
147
|
+
async sendMessage(to, message) {
|
|
148
|
+
if (!this.sock || !this.isConnected) throw new Error("Baileys not connected");
|
|
149
|
+
const jid = this.jidMap.get(to) || this.formatJID(to);
|
|
150
|
+
try {
|
|
151
|
+
if (message.mediaBuffer && message.mediaFileName) if (message.mediaMimeType?.startsWith("image/")) await this.sock.sendMessage(jid, {
|
|
152
|
+
image: message.mediaBuffer,
|
|
153
|
+
caption: message.text
|
|
154
|
+
});
|
|
155
|
+
else await this.sock.sendMessage(jid, {
|
|
156
|
+
document: message.mediaBuffer,
|
|
157
|
+
mimetype: message.mediaMimeType || "application/octet-stream",
|
|
158
|
+
fileName: message.mediaFileName,
|
|
159
|
+
caption: message.text
|
|
160
|
+
});
|
|
161
|
+
else await this.sock.sendMessage(jid, { text: message.text });
|
|
162
|
+
console.log(`📤 [WhatsApp] Sent reply to ${to}\n`);
|
|
163
|
+
await this.sock.sendPresenceUpdate("paused", jid).catch(() => {});
|
|
164
|
+
} catch (error) {
|
|
165
|
+
console.error(`❌ [WhatsApp] Failed to send message:`, error);
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Get the bot's phone number from its JID.
|
|
171
|
+
*/
|
|
172
|
+
getBotNumbers() {
|
|
173
|
+
const user = this.sock?.user;
|
|
174
|
+
if (!user) return [];
|
|
175
|
+
const nums = /* @__PURE__ */ new Set();
|
|
176
|
+
if (user.id) nums.add(user.id.split(":")[0].split("@")[0]);
|
|
177
|
+
if (user.lid) nums.add(user.lid.split(":")[0].split("@")[0]);
|
|
178
|
+
if (user.phoneNumber) nums.add(user.phoneNumber.split(":")[0].split("@")[0]);
|
|
179
|
+
return [...nums];
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Remove @mention patterns from text using the mentionedJid array.
|
|
183
|
+
*/
|
|
184
|
+
stripMentions(text, mentionedJids) {
|
|
185
|
+
let result = text;
|
|
186
|
+
for (const jid of mentionedJids) {
|
|
187
|
+
const number = jid.split("@")[0].split(":")[0];
|
|
188
|
+
result = result.replace(new RegExp(`@${number}\\b`, "g"), "");
|
|
189
|
+
}
|
|
190
|
+
return result.trim();
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Extract contextInfo from whichever message type carries it.
|
|
194
|
+
*/
|
|
195
|
+
getContextInfo(msg) {
|
|
196
|
+
const content = msg.message;
|
|
197
|
+
if (!content) return null;
|
|
198
|
+
return content.extendedTextMessage?.contextInfo || content.imageMessage?.contextInfo || content.videoMessage?.contextInfo || content.documentMessage?.contextInfo || null;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Handle incoming message from Baileys
|
|
202
|
+
*/
|
|
203
|
+
async handleIncomingMessage(msg) {
|
|
204
|
+
if (msg.key.fromMe) return;
|
|
205
|
+
if (msg.key.remoteJid === "status@broadcast") return;
|
|
206
|
+
const remoteJid = msg.key.remoteJid || "";
|
|
207
|
+
const isGroup = remoteJid.endsWith("@g.us");
|
|
208
|
+
if (isGroup) {
|
|
209
|
+
const groupMode = this.config.groupMode || "mention-only";
|
|
210
|
+
if (groupMode === "ignore") return;
|
|
211
|
+
if (groupMode === "mention-only") {
|
|
212
|
+
const mentionedJids = this.getContextInfo(msg)?.mentionedJid || [];
|
|
213
|
+
const botNumbers = this.getBotNumbers();
|
|
214
|
+
if (botNumbers.length === 0) return;
|
|
215
|
+
if (!mentionedJids.some((jid) => {
|
|
216
|
+
const num = jid.split(":")[0].split("@")[0];
|
|
217
|
+
return botNumbers.includes(num);
|
|
218
|
+
})) return;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
let messageText = this.extractMessageText(msg);
|
|
222
|
+
if (!messageText) return;
|
|
223
|
+
if (isGroup) {
|
|
224
|
+
const mentionedJids = this.getContextInfo(msg)?.mentionedJid || [];
|
|
225
|
+
if (mentionedJids.length > 0) {
|
|
226
|
+
messageText = this.stripMentions(messageText, mentionedJids);
|
|
227
|
+
if (!messageText) return;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const remoteJidAlt = msg.key.remoteJidAlt || "";
|
|
231
|
+
const participant = msg.key.participant || "";
|
|
232
|
+
const isLid = (suffix) => suffix === "lid";
|
|
233
|
+
const isPhone = (suffix) => suffix === "s.whatsapp.net";
|
|
234
|
+
let phoneNumber;
|
|
235
|
+
if (isGroup) {
|
|
236
|
+
const participantNumber = participant.split("@")[0].split(":")[0];
|
|
237
|
+
const participantSuffix = participant.split("@")[1] || "";
|
|
238
|
+
if (isPhone(participantSuffix)) phoneNumber = participantNumber;
|
|
239
|
+
else if (isLid(participantSuffix) && this.lidToPhone.has(participantNumber)) phoneNumber = this.lidToPhone.get(participantNumber);
|
|
240
|
+
else if (isLid(participantSuffix)) try {
|
|
241
|
+
const pn = await this.sock?.signalRepository?.lidMapping?.getPNForLID(participant);
|
|
242
|
+
if (pn) {
|
|
243
|
+
const resolved = pn.split("@")[0].split(":")[0];
|
|
244
|
+
this.lidToPhone.set(participantNumber, resolved);
|
|
245
|
+
this.phoneToLid.set(resolved, participantNumber);
|
|
246
|
+
phoneNumber = resolved;
|
|
247
|
+
} else phoneNumber = participantNumber;
|
|
248
|
+
} catch {
|
|
249
|
+
phoneNumber = participantNumber;
|
|
250
|
+
}
|
|
251
|
+
else phoneNumber = participantNumber || remoteJid.split("@")[0];
|
|
252
|
+
} else {
|
|
253
|
+
const remoteNumber = remoteJid.split("@")[0];
|
|
254
|
+
const remoteJidSuffix = remoteJid.split("@")[1] || "";
|
|
255
|
+
const altNumber = remoteJidAlt.split("@")[0];
|
|
256
|
+
const altJidSuffix = remoteJidAlt.split("@")[1] || "";
|
|
257
|
+
if (isLid(remoteJidSuffix) && isPhone(altJidSuffix) && altNumber) {
|
|
258
|
+
this.lidToPhone.set(remoteNumber, altNumber);
|
|
259
|
+
this.phoneToLid.set(altNumber, remoteNumber);
|
|
260
|
+
} else if (isPhone(remoteJidSuffix) && isLid(altJidSuffix) && altNumber) {
|
|
261
|
+
this.lidToPhone.set(altNumber, remoteNumber);
|
|
262
|
+
this.phoneToLid.set(remoteNumber, altNumber);
|
|
263
|
+
}
|
|
264
|
+
if (isPhone(altJidSuffix) && altNumber) phoneNumber = altNumber;
|
|
265
|
+
else if (isPhone(remoteJidSuffix)) phoneNumber = remoteNumber;
|
|
266
|
+
else if (isLid(remoteJidSuffix) && this.lidToPhone.has(remoteNumber)) phoneNumber = this.lidToPhone.get(remoteNumber);
|
|
267
|
+
else phoneNumber = participant ? participant.split("@")[0] : remoteNumber;
|
|
268
|
+
}
|
|
269
|
+
const replyJid = isGroup ? remoteJid : remoteJid || remoteJidAlt;
|
|
270
|
+
this.jidMap.set(phoneNumber, replyJid);
|
|
271
|
+
const incomingMessage = {
|
|
272
|
+
id: msg.key.id || `msg_${Date.now()}`,
|
|
273
|
+
from: phoneNumber,
|
|
274
|
+
text: messageText,
|
|
275
|
+
timestamp: msg.messageTimestamp ? Number(msg.messageTimestamp) * 1e3 : Date.now(),
|
|
276
|
+
channel: "whatsapp",
|
|
277
|
+
provider: this.name,
|
|
278
|
+
metadata: {
|
|
279
|
+
jid: isGroup ? participant : remoteJid,
|
|
280
|
+
remoteJid: msg.key.remoteJid,
|
|
281
|
+
remoteJidAlt: msg.key.remoteJidAlt,
|
|
282
|
+
participant: msg.key.participant,
|
|
283
|
+
pushName: msg.pushName,
|
|
284
|
+
messageType: this.getMessageType(msg),
|
|
285
|
+
...isGroup && {
|
|
286
|
+
isGroup: true,
|
|
287
|
+
groupJid: remoteJid,
|
|
288
|
+
groupParticipant: participant
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
const logPrefix = isGroup ? `[Group] ` : "";
|
|
293
|
+
console.log(`\n📥 [WhatsApp] ${logPrefix}New message from ${msg.pushName || phoneNumber}`);
|
|
294
|
+
console.log(` Message: "${messageText}"`);
|
|
295
|
+
this.emit("message", incomingMessage);
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Extract text from message
|
|
299
|
+
*/
|
|
300
|
+
extractMessageText(msg) {
|
|
301
|
+
const messageContent = msg.message;
|
|
302
|
+
if (!messageContent) return null;
|
|
303
|
+
if (messageContent.conversation) return messageContent.conversation;
|
|
304
|
+
if (messageContent.extendedTextMessage) return messageContent.extendedTextMessage.text || null;
|
|
305
|
+
if (messageContent.imageMessage?.caption) return messageContent.imageMessage.caption;
|
|
306
|
+
if (messageContent.videoMessage?.caption) return messageContent.videoMessage.caption;
|
|
307
|
+
if (messageContent.documentMessage?.caption) return messageContent.documentMessage.caption;
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Get message type
|
|
312
|
+
*/
|
|
313
|
+
getMessageType(msg) {
|
|
314
|
+
const content = msg.message;
|
|
315
|
+
if (!content) return "unknown";
|
|
316
|
+
if (content.conversation) return "text";
|
|
317
|
+
if (content.extendedTextMessage) return "text";
|
|
318
|
+
if (content.imageMessage) return "image";
|
|
319
|
+
if (content.videoMessage) return "video";
|
|
320
|
+
if (content.audioMessage) return "audio";
|
|
321
|
+
if (content.documentMessage) return "document";
|
|
322
|
+
if (content.stickerMessage) return "sticker";
|
|
323
|
+
if (content.locationMessage) return "location";
|
|
324
|
+
if (content.contactMessage) return "contact";
|
|
325
|
+
return "other";
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Format phone number to JID
|
|
329
|
+
*/
|
|
330
|
+
formatJID(phone) {
|
|
331
|
+
let cleaned = phone.replace(/[^\d+]/g, "");
|
|
332
|
+
if (cleaned.startsWith("+")) cleaned = cleaned.substring(1);
|
|
333
|
+
if (!cleaned.includes("@")) cleaned = `${cleaned}@s.whatsapp.net`;
|
|
334
|
+
return cleaned;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Check if connected
|
|
338
|
+
*/
|
|
339
|
+
isActive() {
|
|
340
|
+
return this.isConnected;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Get current user info
|
|
344
|
+
*/
|
|
345
|
+
getUserInfo() {
|
|
346
|
+
return this.sock?.user;
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
//#endregion
|
|
351
|
+
export { BaileysProvider };
|
|
352
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/BaileysProvider.ts"],"sourcesContent":["import EventEmitter from 'eventemitter3';\nimport makeWASocket, {\n DisconnectReason,\n useMultiFileAuthState,\n WASocket,\n proto,\n WAMessage,\n BaileysEventMap,\n ConnectionState,\n} from '@whiskeysockets/baileys';\nimport type { MessageProvider, IncomingMessage, OutgoingMessage } from '@operor/core';\n// @ts-ignore - no types available\nimport qrcode from 'qrcode-terminal';\nimport pino from 'pino';\nimport * as fs from 'fs';\nimport * as path from 'path';\n\nexport interface BaileysConfig {\n authFolder?: string;\n printQRInTerminal?: boolean;\n logLevel?: 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace' | 'silent';\n groupMode?: 'mention-only' | 'all' | 'ignore';\n}\n\nexport class BaileysProvider extends EventEmitter implements MessageProvider {\n public readonly name = 'baileys';\n private sock: WASocket | null = null;\n private config: BaileysConfig;\n private isConnected = false;\n private authFolder: string;\n private jidMap: Map<string, string> = new Map(); // phone/LID number → full JID for replies\n private lidToPhone: Map<string, string> = new Map(); // LID number → phone number\n private phoneToLid: Map<string, string> = new Map(); // phone number → LID number\n\n constructor(config: BaileysConfig = {}) {\n super();\n this.config = {\n authFolder: config.authFolder || './baileys_auth',\n printQRInTerminal: config.printQRInTerminal !== false,\n logLevel: config.logLevel || 'silent', // Suppress all Baileys internal logs\n groupMode: config.groupMode || 'mention-only',\n };\n this.authFolder = this.config.authFolder!;\n }\n\n /**\n * Connect to WhatsApp\n */\n async connect(): Promise<void> {\n // Prevent multiple simultaneous connections\n if (this.sock && this.isConnected) {\n console.log('⚠️ Already connected, skipping...');\n return;\n }\n\n // Clean up existing socket if any\n if (this.sock) {\n console.log('🧹 Cleaning up existing socket...');\n this.sock.ev.removeAllListeners('connection.update');\n this.sock.ev.removeAllListeners('creds.update');\n this.sock.ev.removeAllListeners('messages.upsert');\n this.sock.ev.removeAllListeners('messages.update');\n this.sock = null;\n }\n\n // Ensure auth folder exists\n if (!fs.existsSync(this.authFolder)) {\n fs.mkdirSync(this.authFolder, { recursive: true });\n }\n\n // Load auth state\n const { state, saveCreds } = await useMultiFileAuthState(this.authFolder);\n\n // Create socket\n this.sock = makeWASocket({\n auth: state,\n printQRInTerminal: this.config.printQRInTerminal,\n logger: pino({ level: 'fatal' }), // Use 'fatal' to suppress almost all logs\n // Browser info\n browser: ['Operor', 'Chrome', '1.0.0'],\n // Message retry (create proper cache store)\n msgRetryCounterCache: {\n get: (key: any) => undefined,\n set: (key: any, value: any) => {},\n del: (key: any) => {},\n flushAll: () => {},\n } as any,\n // Generate high quality link previews\n generateHighQualityLinkPreview: true,\n });\n\n // Handle connection updates\n this.sock.ev.on('connection.update', (update: Partial<ConnectionState>) => {\n const { connection, lastDisconnect, qr } = update;\n\n // Display QR code\n if (qr && this.config.printQRInTerminal) {\n console.log('\\n📱 Scan this QR code with WhatsApp:\\n');\n qrcode.generate(qr, { small: true });\n console.log('');\n this.emit('qr', qr);\n }\n\n // Handle connection status\n if (connection === 'close') {\n this.isConnected = false;\n \n const statusCode = (lastDisconnect?.error as any)?.output?.statusCode;\n const shouldReconnect = statusCode !== DisconnectReason.loggedOut;\n\n console.log(`❌ Connection closed`);\n console.log(` Status code: ${statusCode}`);\n console.log(` Should reconnect: ${shouldReconnect}`);\n\n if (shouldReconnect) {\n console.log('🔄 Reconnecting in 5 seconds...');\n setTimeout(() => {\n this.connect();\n }, 5000); // Increased to 5 seconds to avoid rapid reconnection\n } else {\n console.log('⚠️ Logged out - please delete baileys_auth/ and scan QR again');\n this.emit('disconnected', 'logged_out');\n }\n } else if (connection === 'open') {\n this.isConnected = true;\n console.log('✅ Baileys connected to WhatsApp!');\n \n // Important: Give Baileys time to sync\n console.log('⏳ Waiting for message sync...');\n setTimeout(() => {\n console.log('✅ Baileys ready to receive messages!');\n this.emit('ready');\n }, 3000); // Wait 3 seconds for sync\n } else if (connection === 'connecting') {\n console.log('🔄 Connecting to WhatsApp...');\n }\n });\n\n // Save credentials when updated\n this.sock.ev.on('creds.update', saveCreds);\n\n // Listen for LID↔phone mappings from WhatsApp\n this.sock.ev.on('lid-mapping.update' as any, (mapping: Record<string, string>) => {\n for (const [lid, phone] of Object.entries(mapping)) {\n const lidNum = lid.split('@')[0].split(':')[0];\n const phoneNum = phone.split('@')[0].split(':')[0];\n if (lidNum && phoneNum) {\n this.lidToPhone.set(lidNum, phoneNum);\n this.phoneToLid.set(phoneNum, lidNum);\n }\n }\n });\n\n // Handle incoming messages\n this.sock.ev.on('messages.upsert', async (m) => {\n console.log('🔔 [Baileys Event] messages.upsert triggered!');\n console.log(` Number of messages: ${m.messages.length}`);\n console.log(` Type: ${m.type}`);\n \n for (const msg of m.messages) {\n console.log(` Processing message: ${msg.key.id}`);\n await this.handleIncomingMessage(msg);\n }\n });\n\n // Handle message updates (delivery, read receipts, etc.)\n this.sock.ev.on('messages.update', (updates) => {\n for (const update of updates) {\n // You can handle message status updates here if needed\n if (update.update.status) {\n this.emit('message:status', {\n messageId: update.key.id,\n status: update.update.status,\n });\n }\n }\n });\n }\n\n /**\n * Disconnect from WhatsApp\n */\n async disconnect(): Promise<void> {\n if (this.sock) {\n await this.sock.logout();\n this.sock = null;\n this.isConnected = false;\n console.log('Baileys disconnected');\n }\n }\n\n /**\n * Show \"composing…\" typing indicator in WhatsApp\n */\n async sendTypingIndicator(to: string): Promise<void> {\n if (!this.sock || !this.isConnected) return;\n const jid = this.jidMap.get(to) || this.formatJID(to);\n try {\n await this.sock.sendPresenceUpdate('composing', jid);\n } catch {\n // Non-critical — silently ignore failures\n }\n }\n\n /**\n * Send a message\n */\n async sendMessage(to: string, message: OutgoingMessage): Promise<void> {\n if (!this.sock || !this.isConnected) {\n throw new Error('Baileys not connected');\n }\n\n // Use stored JID from incoming message, fall back to formatJID\n const jid = this.jidMap.get(to) || this.formatJID(to);\n\n try {\n // Send media attachment if present\n if (message.mediaBuffer && message.mediaFileName) {\n const isImage = message.mediaMimeType?.startsWith('image/');\n if (isImage) {\n await this.sock.sendMessage(jid, {\n image: message.mediaBuffer,\n caption: message.text,\n });\n } else {\n await this.sock.sendMessage(jid, {\n document: message.mediaBuffer,\n mimetype: message.mediaMimeType || 'application/octet-stream',\n fileName: message.mediaFileName,\n caption: message.text,\n });\n }\n } else {\n await this.sock.sendMessage(jid, {\n text: message.text,\n });\n }\n\n console.log(`📤 [WhatsApp] Sent reply to ${to}\\n`);\n // Clear typing indicator\n await this.sock.sendPresenceUpdate('paused', jid).catch(() => {});\n } catch (error) {\n console.error(`❌ [WhatsApp] Failed to send message:`, error);\n throw error;\n }\n }\n\n /**\n * Get the bot's phone number from its JID.\n */\n private getBotNumbers(): string[] {\n const user = this.sock?.user;\n if (!user) return [];\n const nums = new Set<string>();\n // id may be phone or LID format: \"123:5@s.whatsapp.net\" or \"ABC:5@lid\"\n if (user.id) nums.add(user.id.split(':')[0].split('@')[0]);\n if (user.lid) nums.add(user.lid.split(':')[0].split('@')[0]);\n if ((user as any).phoneNumber) nums.add((user as any).phoneNumber.split(':')[0].split('@')[0]);\n return [...nums];\n }\n\n /**\n * Remove @mention patterns from text using the mentionedJid array.\n */\n private stripMentions(text: string, mentionedJids: string[]): string {\n let result = text;\n for (const jid of mentionedJids) {\n const number = jid.split('@')[0].split(':')[0];\n result = result.replace(new RegExp(`@${number}\\\\b`, 'g'), '');\n }\n return result.trim();\n }\n\n /**\n * Extract contextInfo from whichever message type carries it.\n */\n private getContextInfo(msg: WAMessage): any {\n const content = msg.message;\n if (!content) return null;\n return (\n content.extendedTextMessage?.contextInfo ||\n content.imageMessage?.contextInfo ||\n content.videoMessage?.contextInfo ||\n content.documentMessage?.contextInfo ||\n null\n );\n }\n\n /**\n * Handle incoming message from Baileys\n */\n private async handleIncomingMessage(msg: WAMessage): Promise<void> {\n // Ignore messages from self\n if (msg.key.fromMe) {\n return;\n }\n\n // Ignore broadcast messages\n if (msg.key.remoteJid === 'status@broadcast') {\n return;\n }\n\n const remoteJid = msg.key.remoteJid || '';\n const isGroup = remoteJid.endsWith('@g.us');\n\n // Apply group mode filtering\n if (isGroup) {\n const groupMode = this.config.groupMode || 'mention-only';\n if (groupMode === 'ignore') return;\n\n if (groupMode === 'mention-only') {\n const contextInfo = this.getContextInfo(msg);\n const mentionedJids: string[] = contextInfo?.mentionedJid || [];\n const botNumbers = this.getBotNumbers();\n if (botNumbers.length === 0) return; // Bot JID not available yet during connection setup\n const isMentioned = mentionedJids.some(jid => {\n const num = jid.split(':')[0].split('@')[0];\n return botNumbers.includes(num);\n });\n if (!isMentioned) return;\n }\n }\n\n // Extract message text\n let messageText = this.extractMessageText(msg);\n\n if (!messageText) {\n return;\n }\n\n // Strip @mentions from text in group messages\n if (isGroup) {\n const contextInfo = this.getContextInfo(msg);\n const mentionedJids: string[] = contextInfo?.mentionedJid || [];\n if (mentionedJids.length > 0) {\n messageText = this.stripMentions(messageText, mentionedJids);\n if (!messageText) return; // Message was only a mention with no content\n }\n }\n\n // Extract JIDs — WhatsApp v7+ uses LID (Linked Identity) JIDs alongside phone JIDs.\n const remoteJidAlt = msg.key.remoteJidAlt || '';\n const participant = msg.key.participant || '';\n\n const isLid = (suffix: string) => suffix === 'lid';\n const isPhone = (suffix: string) => suffix === 's.whatsapp.net';\n\n let phoneNumber: string;\n\n if (isGroup) {\n // In groups, extract sender phone from participant (not remoteJid which is the group ID)\n const participantNumber = participant.split('@')[0].split(':')[0];\n const participantSuffix = participant.split('@')[1] || '';\n\n // Resolve LID→phone for group participants\n if (isPhone(participantSuffix)) {\n phoneNumber = participantNumber;\n } else if (isLid(participantSuffix) && this.lidToPhone.has(participantNumber)) {\n phoneNumber = this.lidToPhone.get(participantNumber)!;\n } else if (isLid(participantSuffix)) {\n // Try Baileys' built-in LID mapping store as fallback\n try {\n const pn = await this.sock?.signalRepository?.lidMapping?.getPNForLID(participant);\n if (pn) {\n const resolved = pn.split('@')[0].split(':')[0];\n this.lidToPhone.set(participantNumber, resolved);\n this.phoneToLid.set(resolved, participantNumber);\n phoneNumber = resolved;\n } else {\n phoneNumber = participantNumber;\n }\n } catch {\n phoneNumber = participantNumber;\n }\n } else {\n phoneNumber = participantNumber || remoteJid.split('@')[0];\n }\n } else {\n // DM: use existing LID↔phone resolution logic\n const remoteNumber = remoteJid.split('@')[0];\n const remoteJidSuffix = remoteJid.split('@')[1] || '';\n const altNumber = remoteJidAlt.split('@')[0];\n const altJidSuffix = remoteJidAlt.split('@')[1] || '';\n\n // Build bidirectional LID↔phone mapping when both are available\n if (isLid(remoteJidSuffix) && isPhone(altJidSuffix) && altNumber) {\n this.lidToPhone.set(remoteNumber, altNumber);\n this.phoneToLid.set(altNumber, remoteNumber);\n } else if (isPhone(remoteJidSuffix) && isLid(altJidSuffix) && altNumber) {\n this.lidToPhone.set(altNumber, remoteNumber);\n this.phoneToLid.set(remoteNumber, altNumber);\n }\n\n // Resolve the sender to a phone number\n if (isPhone(altJidSuffix) && altNumber) {\n phoneNumber = altNumber;\n } else if (isPhone(remoteJidSuffix)) {\n phoneNumber = remoteNumber;\n } else if (isLid(remoteJidSuffix) && this.lidToPhone.has(remoteNumber)) {\n phoneNumber = this.lidToPhone.get(remoteNumber)!;\n } else {\n phoneNumber = participant ? participant.split('@')[0] : remoteNumber;\n }\n }\n\n // Store the reply JID — for groups, reply to the group; for DMs, use remoteJid\n const replyJid = isGroup ? remoteJid : (remoteJid || remoteJidAlt);\n this.jidMap.set(phoneNumber, replyJid);\n\n // Create standardized message\n const incomingMessage: IncomingMessage = {\n id: msg.key.id || `msg_${Date.now()}`,\n from: phoneNumber,\n text: messageText,\n timestamp: msg.messageTimestamp ? Number(msg.messageTimestamp) * 1000 : Date.now(),\n channel: 'whatsapp',\n provider: this.name,\n metadata: {\n jid: isGroup ? participant : remoteJid,\n remoteJid: msg.key.remoteJid,\n remoteJidAlt: msg.key.remoteJidAlt,\n participant: msg.key.participant,\n pushName: msg.pushName,\n messageType: this.getMessageType(msg),\n ...(isGroup && {\n isGroup: true,\n groupJid: remoteJid,\n groupParticipant: participant,\n }),\n },\n };\n\n // Clean user-friendly log\n const logPrefix = isGroup ? `[Group] ` : '';\n console.log(`\\n📥 [WhatsApp] ${logPrefix}New message from ${msg.pushName || phoneNumber}`);\n console.log(` Message: \"${messageText}\"`);\n\n // Emit message event\n this.emit('message', incomingMessage);\n }\n\n /**\n * Extract text from message\n */\n private extractMessageText(msg: WAMessage): string | null {\n const messageContent = msg.message;\n if (!messageContent) return null;\n\n // Text message\n if (messageContent.conversation) {\n return messageContent.conversation;\n }\n\n // Extended text message\n if (messageContent.extendedTextMessage) {\n return messageContent.extendedTextMessage.text || null;\n }\n\n // Image with caption\n if (messageContent.imageMessage?.caption) {\n return messageContent.imageMessage.caption;\n }\n\n // Video with caption\n if (messageContent.videoMessage?.caption) {\n return messageContent.videoMessage.caption;\n }\n\n // Document with caption\n if (messageContent.documentMessage?.caption) {\n return messageContent.documentMessage.caption;\n }\n\n return null;\n }\n\n /**\n * Get message type\n */\n private getMessageType(msg: WAMessage): string {\n const content = msg.message;\n if (!content) return 'unknown';\n\n if (content.conversation) return 'text';\n if (content.extendedTextMessage) return 'text';\n if (content.imageMessage) return 'image';\n if (content.videoMessage) return 'video';\n if (content.audioMessage) return 'audio';\n if (content.documentMessage) return 'document';\n if (content.stickerMessage) return 'sticker';\n if (content.locationMessage) return 'location';\n if (content.contactMessage) return 'contact';\n\n return 'other';\n }\n\n /**\n * Format phone number to JID\n */\n private formatJID(phone: string): string {\n // Remove any non-numeric characters except +\n let cleaned = phone.replace(/[^\\d+]/g, '');\n\n // Remove leading + if present\n if (cleaned.startsWith('+')) {\n cleaned = cleaned.substring(1);\n }\n\n // Add @s.whatsapp.net if not already present\n if (!cleaned.includes('@')) {\n cleaned = `${cleaned}@s.whatsapp.net`;\n }\n\n return cleaned;\n }\n\n /**\n * Check if connected\n */\n isActive(): boolean {\n return this.isConnected;\n }\n\n /**\n * Get current user info\n */\n getUserInfo() {\n return this.sock?.user;\n }\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@operor/provider-baileys",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Baileys WhatsApp provider for Agent OS",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"@whiskeysockets/baileys": "^7.0.0-rc.9",
|
|
10
|
+
"eventemitter3": "^5.0.1",
|
|
11
|
+
"qrcode-terminal": "^0.12.0",
|
|
12
|
+
"pino": "^10.0.0",
|
|
13
|
+
"@operor/core": "0.1.0"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"tsdown": "^0.20.3",
|
|
17
|
+
"typescript": "^5.3.3"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsdown",
|
|
21
|
+
"dev": "tsdown --watch"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
import EventEmitter from 'eventemitter3';
|
|
2
|
+
import makeWASocket, {
|
|
3
|
+
DisconnectReason,
|
|
4
|
+
useMultiFileAuthState,
|
|
5
|
+
WASocket,
|
|
6
|
+
proto,
|
|
7
|
+
WAMessage,
|
|
8
|
+
BaileysEventMap,
|
|
9
|
+
ConnectionState,
|
|
10
|
+
} from '@whiskeysockets/baileys';
|
|
11
|
+
import type { MessageProvider, IncomingMessage, OutgoingMessage } from '@operor/core';
|
|
12
|
+
// @ts-ignore - no types available
|
|
13
|
+
import qrcode from 'qrcode-terminal';
|
|
14
|
+
import pino from 'pino';
|
|
15
|
+
import * as fs from 'fs';
|
|
16
|
+
import * as path from 'path';
|
|
17
|
+
|
|
18
|
+
export interface BaileysConfig {
|
|
19
|
+
authFolder?: string;
|
|
20
|
+
printQRInTerminal?: boolean;
|
|
21
|
+
logLevel?: 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace' | 'silent';
|
|
22
|
+
groupMode?: 'mention-only' | 'all' | 'ignore';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class BaileysProvider extends EventEmitter implements MessageProvider {
|
|
26
|
+
public readonly name = 'baileys';
|
|
27
|
+
private sock: WASocket | null = null;
|
|
28
|
+
private config: BaileysConfig;
|
|
29
|
+
private isConnected = false;
|
|
30
|
+
private authFolder: string;
|
|
31
|
+
private jidMap: Map<string, string> = new Map(); // phone/LID number → full JID for replies
|
|
32
|
+
private lidToPhone: Map<string, string> = new Map(); // LID number → phone number
|
|
33
|
+
private phoneToLid: Map<string, string> = new Map(); // phone number → LID number
|
|
34
|
+
|
|
35
|
+
constructor(config: BaileysConfig = {}) {
|
|
36
|
+
super();
|
|
37
|
+
this.config = {
|
|
38
|
+
authFolder: config.authFolder || './baileys_auth',
|
|
39
|
+
printQRInTerminal: config.printQRInTerminal !== false,
|
|
40
|
+
logLevel: config.logLevel || 'silent', // Suppress all Baileys internal logs
|
|
41
|
+
groupMode: config.groupMode || 'mention-only',
|
|
42
|
+
};
|
|
43
|
+
this.authFolder = this.config.authFolder!;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Connect to WhatsApp
|
|
48
|
+
*/
|
|
49
|
+
async connect(): Promise<void> {
|
|
50
|
+
// Prevent multiple simultaneous connections
|
|
51
|
+
if (this.sock && this.isConnected) {
|
|
52
|
+
console.log('⚠️ Already connected, skipping...');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Clean up existing socket if any
|
|
57
|
+
if (this.sock) {
|
|
58
|
+
console.log('🧹 Cleaning up existing socket...');
|
|
59
|
+
this.sock.ev.removeAllListeners('connection.update');
|
|
60
|
+
this.sock.ev.removeAllListeners('creds.update');
|
|
61
|
+
this.sock.ev.removeAllListeners('messages.upsert');
|
|
62
|
+
this.sock.ev.removeAllListeners('messages.update');
|
|
63
|
+
this.sock = null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Ensure auth folder exists
|
|
67
|
+
if (!fs.existsSync(this.authFolder)) {
|
|
68
|
+
fs.mkdirSync(this.authFolder, { recursive: true });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Load auth state
|
|
72
|
+
const { state, saveCreds } = await useMultiFileAuthState(this.authFolder);
|
|
73
|
+
|
|
74
|
+
// Create socket
|
|
75
|
+
this.sock = makeWASocket({
|
|
76
|
+
auth: state,
|
|
77
|
+
printQRInTerminal: this.config.printQRInTerminal,
|
|
78
|
+
logger: pino({ level: 'fatal' }), // Use 'fatal' to suppress almost all logs
|
|
79
|
+
// Browser info
|
|
80
|
+
browser: ['Operor', 'Chrome', '1.0.0'],
|
|
81
|
+
// Message retry (create proper cache store)
|
|
82
|
+
msgRetryCounterCache: {
|
|
83
|
+
get: (key: any) => undefined,
|
|
84
|
+
set: (key: any, value: any) => {},
|
|
85
|
+
del: (key: any) => {},
|
|
86
|
+
flushAll: () => {},
|
|
87
|
+
} as any,
|
|
88
|
+
// Generate high quality link previews
|
|
89
|
+
generateHighQualityLinkPreview: true,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Handle connection updates
|
|
93
|
+
this.sock.ev.on('connection.update', (update: Partial<ConnectionState>) => {
|
|
94
|
+
const { connection, lastDisconnect, qr } = update;
|
|
95
|
+
|
|
96
|
+
// Display QR code
|
|
97
|
+
if (qr && this.config.printQRInTerminal) {
|
|
98
|
+
console.log('\n📱 Scan this QR code with WhatsApp:\n');
|
|
99
|
+
qrcode.generate(qr, { small: true });
|
|
100
|
+
console.log('');
|
|
101
|
+
this.emit('qr', qr);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Handle connection status
|
|
105
|
+
if (connection === 'close') {
|
|
106
|
+
this.isConnected = false;
|
|
107
|
+
|
|
108
|
+
const statusCode = (lastDisconnect?.error as any)?.output?.statusCode;
|
|
109
|
+
const shouldReconnect = statusCode !== DisconnectReason.loggedOut;
|
|
110
|
+
|
|
111
|
+
console.log(`❌ Connection closed`);
|
|
112
|
+
console.log(` Status code: ${statusCode}`);
|
|
113
|
+
console.log(` Should reconnect: ${shouldReconnect}`);
|
|
114
|
+
|
|
115
|
+
if (shouldReconnect) {
|
|
116
|
+
console.log('🔄 Reconnecting in 5 seconds...');
|
|
117
|
+
setTimeout(() => {
|
|
118
|
+
this.connect();
|
|
119
|
+
}, 5000); // Increased to 5 seconds to avoid rapid reconnection
|
|
120
|
+
} else {
|
|
121
|
+
console.log('⚠️ Logged out - please delete baileys_auth/ and scan QR again');
|
|
122
|
+
this.emit('disconnected', 'logged_out');
|
|
123
|
+
}
|
|
124
|
+
} else if (connection === 'open') {
|
|
125
|
+
this.isConnected = true;
|
|
126
|
+
console.log('✅ Baileys connected to WhatsApp!');
|
|
127
|
+
|
|
128
|
+
// Important: Give Baileys time to sync
|
|
129
|
+
console.log('⏳ Waiting for message sync...');
|
|
130
|
+
setTimeout(() => {
|
|
131
|
+
console.log('✅ Baileys ready to receive messages!');
|
|
132
|
+
this.emit('ready');
|
|
133
|
+
}, 3000); // Wait 3 seconds for sync
|
|
134
|
+
} else if (connection === 'connecting') {
|
|
135
|
+
console.log('🔄 Connecting to WhatsApp...');
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Save credentials when updated
|
|
140
|
+
this.sock.ev.on('creds.update', saveCreds);
|
|
141
|
+
|
|
142
|
+
// Listen for LID↔phone mappings from WhatsApp
|
|
143
|
+
this.sock.ev.on('lid-mapping.update' as any, (mapping: Record<string, string>) => {
|
|
144
|
+
for (const [lid, phone] of Object.entries(mapping)) {
|
|
145
|
+
const lidNum = lid.split('@')[0].split(':')[0];
|
|
146
|
+
const phoneNum = phone.split('@')[0].split(':')[0];
|
|
147
|
+
if (lidNum && phoneNum) {
|
|
148
|
+
this.lidToPhone.set(lidNum, phoneNum);
|
|
149
|
+
this.phoneToLid.set(phoneNum, lidNum);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Handle incoming messages
|
|
155
|
+
this.sock.ev.on('messages.upsert', async (m) => {
|
|
156
|
+
console.log('🔔 [Baileys Event] messages.upsert triggered!');
|
|
157
|
+
console.log(` Number of messages: ${m.messages.length}`);
|
|
158
|
+
console.log(` Type: ${m.type}`);
|
|
159
|
+
|
|
160
|
+
for (const msg of m.messages) {
|
|
161
|
+
console.log(` Processing message: ${msg.key.id}`);
|
|
162
|
+
await this.handleIncomingMessage(msg);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Handle message updates (delivery, read receipts, etc.)
|
|
167
|
+
this.sock.ev.on('messages.update', (updates) => {
|
|
168
|
+
for (const update of updates) {
|
|
169
|
+
// You can handle message status updates here if needed
|
|
170
|
+
if (update.update.status) {
|
|
171
|
+
this.emit('message:status', {
|
|
172
|
+
messageId: update.key.id,
|
|
173
|
+
status: update.update.status,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Disconnect from WhatsApp
|
|
182
|
+
*/
|
|
183
|
+
async disconnect(): Promise<void> {
|
|
184
|
+
if (this.sock) {
|
|
185
|
+
await this.sock.logout();
|
|
186
|
+
this.sock = null;
|
|
187
|
+
this.isConnected = false;
|
|
188
|
+
console.log('Baileys disconnected');
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Show "composing…" typing indicator in WhatsApp
|
|
194
|
+
*/
|
|
195
|
+
async sendTypingIndicator(to: string): Promise<void> {
|
|
196
|
+
if (!this.sock || !this.isConnected) return;
|
|
197
|
+
const jid = this.jidMap.get(to) || this.formatJID(to);
|
|
198
|
+
try {
|
|
199
|
+
await this.sock.sendPresenceUpdate('composing', jid);
|
|
200
|
+
} catch {
|
|
201
|
+
// Non-critical — silently ignore failures
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Send a message
|
|
207
|
+
*/
|
|
208
|
+
async sendMessage(to: string, message: OutgoingMessage): Promise<void> {
|
|
209
|
+
if (!this.sock || !this.isConnected) {
|
|
210
|
+
throw new Error('Baileys not connected');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Use stored JID from incoming message, fall back to formatJID
|
|
214
|
+
const jid = this.jidMap.get(to) || this.formatJID(to);
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
// Send media attachment if present
|
|
218
|
+
if (message.mediaBuffer && message.mediaFileName) {
|
|
219
|
+
const isImage = message.mediaMimeType?.startsWith('image/');
|
|
220
|
+
if (isImage) {
|
|
221
|
+
await this.sock.sendMessage(jid, {
|
|
222
|
+
image: message.mediaBuffer,
|
|
223
|
+
caption: message.text,
|
|
224
|
+
});
|
|
225
|
+
} else {
|
|
226
|
+
await this.sock.sendMessage(jid, {
|
|
227
|
+
document: message.mediaBuffer,
|
|
228
|
+
mimetype: message.mediaMimeType || 'application/octet-stream',
|
|
229
|
+
fileName: message.mediaFileName,
|
|
230
|
+
caption: message.text,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
await this.sock.sendMessage(jid, {
|
|
235
|
+
text: message.text,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
console.log(`📤 [WhatsApp] Sent reply to ${to}\n`);
|
|
240
|
+
// Clear typing indicator
|
|
241
|
+
await this.sock.sendPresenceUpdate('paused', jid).catch(() => {});
|
|
242
|
+
} catch (error) {
|
|
243
|
+
console.error(`❌ [WhatsApp] Failed to send message:`, error);
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get the bot's phone number from its JID.
|
|
250
|
+
*/
|
|
251
|
+
private getBotNumbers(): string[] {
|
|
252
|
+
const user = this.sock?.user;
|
|
253
|
+
if (!user) return [];
|
|
254
|
+
const nums = new Set<string>();
|
|
255
|
+
// id may be phone or LID format: "123:5@s.whatsapp.net" or "ABC:5@lid"
|
|
256
|
+
if (user.id) nums.add(user.id.split(':')[0].split('@')[0]);
|
|
257
|
+
if (user.lid) nums.add(user.lid.split(':')[0].split('@')[0]);
|
|
258
|
+
if ((user as any).phoneNumber) nums.add((user as any).phoneNumber.split(':')[0].split('@')[0]);
|
|
259
|
+
return [...nums];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Remove @mention patterns from text using the mentionedJid array.
|
|
264
|
+
*/
|
|
265
|
+
private stripMentions(text: string, mentionedJids: string[]): string {
|
|
266
|
+
let result = text;
|
|
267
|
+
for (const jid of mentionedJids) {
|
|
268
|
+
const number = jid.split('@')[0].split(':')[0];
|
|
269
|
+
result = result.replace(new RegExp(`@${number}\\b`, 'g'), '');
|
|
270
|
+
}
|
|
271
|
+
return result.trim();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Extract contextInfo from whichever message type carries it.
|
|
276
|
+
*/
|
|
277
|
+
private getContextInfo(msg: WAMessage): any {
|
|
278
|
+
const content = msg.message;
|
|
279
|
+
if (!content) return null;
|
|
280
|
+
return (
|
|
281
|
+
content.extendedTextMessage?.contextInfo ||
|
|
282
|
+
content.imageMessage?.contextInfo ||
|
|
283
|
+
content.videoMessage?.contextInfo ||
|
|
284
|
+
content.documentMessage?.contextInfo ||
|
|
285
|
+
null
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Handle incoming message from Baileys
|
|
291
|
+
*/
|
|
292
|
+
private async handleIncomingMessage(msg: WAMessage): Promise<void> {
|
|
293
|
+
// Ignore messages from self
|
|
294
|
+
if (msg.key.fromMe) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Ignore broadcast messages
|
|
299
|
+
if (msg.key.remoteJid === 'status@broadcast') {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const remoteJid = msg.key.remoteJid || '';
|
|
304
|
+
const isGroup = remoteJid.endsWith('@g.us');
|
|
305
|
+
|
|
306
|
+
// Apply group mode filtering
|
|
307
|
+
if (isGroup) {
|
|
308
|
+
const groupMode = this.config.groupMode || 'mention-only';
|
|
309
|
+
if (groupMode === 'ignore') return;
|
|
310
|
+
|
|
311
|
+
if (groupMode === 'mention-only') {
|
|
312
|
+
const contextInfo = this.getContextInfo(msg);
|
|
313
|
+
const mentionedJids: string[] = contextInfo?.mentionedJid || [];
|
|
314
|
+
const botNumbers = this.getBotNumbers();
|
|
315
|
+
if (botNumbers.length === 0) return; // Bot JID not available yet during connection setup
|
|
316
|
+
const isMentioned = mentionedJids.some(jid => {
|
|
317
|
+
const num = jid.split(':')[0].split('@')[0];
|
|
318
|
+
return botNumbers.includes(num);
|
|
319
|
+
});
|
|
320
|
+
if (!isMentioned) return;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Extract message text
|
|
325
|
+
let messageText = this.extractMessageText(msg);
|
|
326
|
+
|
|
327
|
+
if (!messageText) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Strip @mentions from text in group messages
|
|
332
|
+
if (isGroup) {
|
|
333
|
+
const contextInfo = this.getContextInfo(msg);
|
|
334
|
+
const mentionedJids: string[] = contextInfo?.mentionedJid || [];
|
|
335
|
+
if (mentionedJids.length > 0) {
|
|
336
|
+
messageText = this.stripMentions(messageText, mentionedJids);
|
|
337
|
+
if (!messageText) return; // Message was only a mention with no content
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Extract JIDs — WhatsApp v7+ uses LID (Linked Identity) JIDs alongside phone JIDs.
|
|
342
|
+
const remoteJidAlt = msg.key.remoteJidAlt || '';
|
|
343
|
+
const participant = msg.key.participant || '';
|
|
344
|
+
|
|
345
|
+
const isLid = (suffix: string) => suffix === 'lid';
|
|
346
|
+
const isPhone = (suffix: string) => suffix === 's.whatsapp.net';
|
|
347
|
+
|
|
348
|
+
let phoneNumber: string;
|
|
349
|
+
|
|
350
|
+
if (isGroup) {
|
|
351
|
+
// In groups, extract sender phone from participant (not remoteJid which is the group ID)
|
|
352
|
+
const participantNumber = participant.split('@')[0].split(':')[0];
|
|
353
|
+
const participantSuffix = participant.split('@')[1] || '';
|
|
354
|
+
|
|
355
|
+
// Resolve LID→phone for group participants
|
|
356
|
+
if (isPhone(participantSuffix)) {
|
|
357
|
+
phoneNumber = participantNumber;
|
|
358
|
+
} else if (isLid(participantSuffix) && this.lidToPhone.has(participantNumber)) {
|
|
359
|
+
phoneNumber = this.lidToPhone.get(participantNumber)!;
|
|
360
|
+
} else if (isLid(participantSuffix)) {
|
|
361
|
+
// Try Baileys' built-in LID mapping store as fallback
|
|
362
|
+
try {
|
|
363
|
+
const pn = await this.sock?.signalRepository?.lidMapping?.getPNForLID(participant);
|
|
364
|
+
if (pn) {
|
|
365
|
+
const resolved = pn.split('@')[0].split(':')[0];
|
|
366
|
+
this.lidToPhone.set(participantNumber, resolved);
|
|
367
|
+
this.phoneToLid.set(resolved, participantNumber);
|
|
368
|
+
phoneNumber = resolved;
|
|
369
|
+
} else {
|
|
370
|
+
phoneNumber = participantNumber;
|
|
371
|
+
}
|
|
372
|
+
} catch {
|
|
373
|
+
phoneNumber = participantNumber;
|
|
374
|
+
}
|
|
375
|
+
} else {
|
|
376
|
+
phoneNumber = participantNumber || remoteJid.split('@')[0];
|
|
377
|
+
}
|
|
378
|
+
} else {
|
|
379
|
+
// DM: use existing LID↔phone resolution logic
|
|
380
|
+
const remoteNumber = remoteJid.split('@')[0];
|
|
381
|
+
const remoteJidSuffix = remoteJid.split('@')[1] || '';
|
|
382
|
+
const altNumber = remoteJidAlt.split('@')[0];
|
|
383
|
+
const altJidSuffix = remoteJidAlt.split('@')[1] || '';
|
|
384
|
+
|
|
385
|
+
// Build bidirectional LID↔phone mapping when both are available
|
|
386
|
+
if (isLid(remoteJidSuffix) && isPhone(altJidSuffix) && altNumber) {
|
|
387
|
+
this.lidToPhone.set(remoteNumber, altNumber);
|
|
388
|
+
this.phoneToLid.set(altNumber, remoteNumber);
|
|
389
|
+
} else if (isPhone(remoteJidSuffix) && isLid(altJidSuffix) && altNumber) {
|
|
390
|
+
this.lidToPhone.set(altNumber, remoteNumber);
|
|
391
|
+
this.phoneToLid.set(remoteNumber, altNumber);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Resolve the sender to a phone number
|
|
395
|
+
if (isPhone(altJidSuffix) && altNumber) {
|
|
396
|
+
phoneNumber = altNumber;
|
|
397
|
+
} else if (isPhone(remoteJidSuffix)) {
|
|
398
|
+
phoneNumber = remoteNumber;
|
|
399
|
+
} else if (isLid(remoteJidSuffix) && this.lidToPhone.has(remoteNumber)) {
|
|
400
|
+
phoneNumber = this.lidToPhone.get(remoteNumber)!;
|
|
401
|
+
} else {
|
|
402
|
+
phoneNumber = participant ? participant.split('@')[0] : remoteNumber;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Store the reply JID — for groups, reply to the group; for DMs, use remoteJid
|
|
407
|
+
const replyJid = isGroup ? remoteJid : (remoteJid || remoteJidAlt);
|
|
408
|
+
this.jidMap.set(phoneNumber, replyJid);
|
|
409
|
+
|
|
410
|
+
// Create standardized message
|
|
411
|
+
const incomingMessage: IncomingMessage = {
|
|
412
|
+
id: msg.key.id || `msg_${Date.now()}`,
|
|
413
|
+
from: phoneNumber,
|
|
414
|
+
text: messageText,
|
|
415
|
+
timestamp: msg.messageTimestamp ? Number(msg.messageTimestamp) * 1000 : Date.now(),
|
|
416
|
+
channel: 'whatsapp',
|
|
417
|
+
provider: this.name,
|
|
418
|
+
metadata: {
|
|
419
|
+
jid: isGroup ? participant : remoteJid,
|
|
420
|
+
remoteJid: msg.key.remoteJid,
|
|
421
|
+
remoteJidAlt: msg.key.remoteJidAlt,
|
|
422
|
+
participant: msg.key.participant,
|
|
423
|
+
pushName: msg.pushName,
|
|
424
|
+
messageType: this.getMessageType(msg),
|
|
425
|
+
...(isGroup && {
|
|
426
|
+
isGroup: true,
|
|
427
|
+
groupJid: remoteJid,
|
|
428
|
+
groupParticipant: participant,
|
|
429
|
+
}),
|
|
430
|
+
},
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
// Clean user-friendly log
|
|
434
|
+
const logPrefix = isGroup ? `[Group] ` : '';
|
|
435
|
+
console.log(`\n📥 [WhatsApp] ${logPrefix}New message from ${msg.pushName || phoneNumber}`);
|
|
436
|
+
console.log(` Message: "${messageText}"`);
|
|
437
|
+
|
|
438
|
+
// Emit message event
|
|
439
|
+
this.emit('message', incomingMessage);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Extract text from message
|
|
444
|
+
*/
|
|
445
|
+
private extractMessageText(msg: WAMessage): string | null {
|
|
446
|
+
const messageContent = msg.message;
|
|
447
|
+
if (!messageContent) return null;
|
|
448
|
+
|
|
449
|
+
// Text message
|
|
450
|
+
if (messageContent.conversation) {
|
|
451
|
+
return messageContent.conversation;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Extended text message
|
|
455
|
+
if (messageContent.extendedTextMessage) {
|
|
456
|
+
return messageContent.extendedTextMessage.text || null;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Image with caption
|
|
460
|
+
if (messageContent.imageMessage?.caption) {
|
|
461
|
+
return messageContent.imageMessage.caption;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Video with caption
|
|
465
|
+
if (messageContent.videoMessage?.caption) {
|
|
466
|
+
return messageContent.videoMessage.caption;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Document with caption
|
|
470
|
+
if (messageContent.documentMessage?.caption) {
|
|
471
|
+
return messageContent.documentMessage.caption;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Get message type
|
|
479
|
+
*/
|
|
480
|
+
private getMessageType(msg: WAMessage): string {
|
|
481
|
+
const content = msg.message;
|
|
482
|
+
if (!content) return 'unknown';
|
|
483
|
+
|
|
484
|
+
if (content.conversation) return 'text';
|
|
485
|
+
if (content.extendedTextMessage) return 'text';
|
|
486
|
+
if (content.imageMessage) return 'image';
|
|
487
|
+
if (content.videoMessage) return 'video';
|
|
488
|
+
if (content.audioMessage) return 'audio';
|
|
489
|
+
if (content.documentMessage) return 'document';
|
|
490
|
+
if (content.stickerMessage) return 'sticker';
|
|
491
|
+
if (content.locationMessage) return 'location';
|
|
492
|
+
if (content.contactMessage) return 'contact';
|
|
493
|
+
|
|
494
|
+
return 'other';
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Format phone number to JID
|
|
499
|
+
*/
|
|
500
|
+
private formatJID(phone: string): string {
|
|
501
|
+
// Remove any non-numeric characters except +
|
|
502
|
+
let cleaned = phone.replace(/[^\d+]/g, '');
|
|
503
|
+
|
|
504
|
+
// Remove leading + if present
|
|
505
|
+
if (cleaned.startsWith('+')) {
|
|
506
|
+
cleaned = cleaned.substring(1);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Add @s.whatsapp.net if not already present
|
|
510
|
+
if (!cleaned.includes('@')) {
|
|
511
|
+
cleaned = `${cleaned}@s.whatsapp.net`;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return cleaned;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Check if connected
|
|
519
|
+
*/
|
|
520
|
+
isActive(): boolean {
|
|
521
|
+
return this.isConnected;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Get current user info
|
|
526
|
+
*/
|
|
527
|
+
getUserInfo() {
|
|
528
|
+
return this.sock?.user;
|
|
529
|
+
}
|
|
530
|
+
}
|
package/src/index.ts
ADDED
package/tsconfig.json
ADDED