import { Server } from "@hapi/hapi"; import { Service } from "@hapipal/schmervice"; import makeWASocket, { DisconnectReason, proto, downloadContentFromMessage, MediaType, fetchLatestBaileysVersion, isJidBroadcast, isJidStatusBroadcast, useMultiFileAuthState, } from "@whiskeysockets/baileys"; import fs from "fs"; export type AuthCompleteCallback = (error?: string) => void; export default class WhatsappService extends Service { connections: { [key: string]: any } = {}; loginConnections: { [key: string]: any } = {}; static browserDescription: [string, string, string] = [ "Bridge", "Chrome", "2.0", ]; constructor(server: Server, options: never) { super(server, options); } getBaseDirectory(): string { return `/home/node/baileys`; } getBotDirectory(id: string): string { return `${this.getBaseDirectory()}/${id}`; } getAuthDirectory(id: string): string { return `${this.getBotDirectory(id)}/auth`; } async initialize(): Promise { this.updateConnections(); } async teardown(): Promise { this.resetConnections(); } private async sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } private async resetConnections() { for (const connection of Object.values(this.connections)) { try { connection.end(null); } catch (error) { console.log(error); } } this.connections = {}; } private async createConnection( botID: string, server: Server, options: any, authCompleteCallback?: any, ) { const authDirectory = this.getAuthDirectory(botID); const { state, saveCreds } = await useMultiFileAuthState(authDirectory); const msgRetryCounterMap: any = {}; const socket = makeWASocket({ ...options, auth: state, msgRetryCounterMap, shouldIgnoreJid: (jid) => isJidBroadcast(jid) || isJidStatusBroadcast(jid), }); let pause = 5000; socket.ev.process(async (events) => { if (events["connection.update"]) { const update = events["connection.update"]; const { connection: connectionState, lastDisconnect, qr, isNewLogin, } = update; if (qr) { console.log("got qr code"); const botDirectory = this.getBotDirectory(botID); const qrPath = `${botDirectory}/qr.txt`; fs.writeFileSync(qrPath, qr, "utf8"); const verifiedFile = `${botDirectory}/verified`; if (fs.existsSync(verifiedFile)) { fs.rmSync(verifiedFile); } } else if (isNewLogin) { console.log("got new login"); const botDirectory = this.getBotDirectory(botID); const verifiedFile = `${botDirectory}/verified`; fs.writeFileSync(verifiedFile, ""); } else if (connectionState === "open") { console.log("opened connection"); } else if (connectionState === "close") { console.log("connection closed due to ", lastDisconnect?.error); const disconnectStatusCode = (lastDisconnect?.error as any)?.output ?.statusCode; if (disconnectStatusCode === DisconnectReason.restartRequired) { console.log("reconnecting after got new login"); await this.createConnection(botID, server, options); authCompleteCallback?.(); } else if (disconnectStatusCode !== DisconnectReason.loggedOut) { console.log("reconnecting"); await this.sleep(pause); pause *= 2; this.createConnection(botID, server, options); } } } if (events["creds.update"]) { console.log("creds update"); await saveCreds(); } if (events["messages.upsert"]) { console.log("messages upsert"); const upsert = events["messages.upsert"]; const { messages } = upsert; if (messages) { await this.queueUnreadMessages(botID, messages); } } }); this.connections[botID] = { socket, msgRetryCounterMap }; } private async updateConnections() { this.resetConnections(); const baseDirectory = this.getBaseDirectory(); const botIDs = fs.readdirSync(baseDirectory); console.log({ botIDs }); for await (const botID of botIDs) { const directory = this.getBotDirectory(botID); const verifiedFile = `${directory}/verified`; if (fs.existsSync(verifiedFile)) { const { version, isLatest } = await fetchLatestBaileysVersion(); console.log(`using WA v${version.join(".")}, isLatest: ${isLatest}`); await this.createConnection(botID, this.server, { browser: WhatsappService.browserDescription, printQRInTerminal: true, version, }); } } } private async queueMessage( botID: string, webMessageInfo: proto.IWebMessageInfo, ) { const { key: { id, fromMe, remoteJid }, message, messageTimestamp, } = webMessageInfo; if (!fromMe && message && remoteJid !== "status@broadcast") { const { audioMessage, documentMessage, imageMessage, videoMessage } = message; const isMediaMessage = audioMessage || documentMessage || imageMessage || videoMessage; const messageContent = Object.values(message)[0]; let messageType: MediaType; let attachment: string; let filename: string | null | undefined; let mimetype: string | null | undefined; if (isMediaMessage) { if (audioMessage) { messageType = "audio"; filename = id + "." + audioMessage.mimetype?.split("/").pop(); mimetype = audioMessage.mimetype; } else if (documentMessage) { messageType = "document"; filename = documentMessage.fileName; mimetype = documentMessage.mimetype; } else if (imageMessage) { messageType = "image"; filename = id + "." + imageMessage.mimetype?.split("/").pop(); mimetype = imageMessage.mimetype; } else if (videoMessage) { messageType = "video"; filename = id + "." + videoMessage.mimetype?.split("/").pop(); mimetype = videoMessage.mimetype; } const stream = await downloadContentFromMessage( messageContent, // @ts-ignore messageType, ); let buffer = Buffer.from([]); for await (const chunk of stream) { buffer = Buffer.concat([buffer, chunk]); } attachment = buffer.toString("base64"); } // @ts-ignore if (messageContent || attachment) { const receivedMessage = { waMessageId: id, waMessage: JSON.stringify(webMessageInfo), waTimestamp: new Date((messageTimestamp as number) * 1000), // @ts-ignore attachment, filename, mimetype, whatsappBotId: botID, }; const message = webMessageInfo?.message?.conversation ?? webMessageInfo?.message?.extendedTextMessage?.text ?? webMessageInfo?.message?.imageMessage?.caption ?? webMessageInfo?.message?.videoMessage?.caption; const payload = { message, sender: webMessageInfo.key.remoteJid?.split("@")[0], }; await fetch( `${process.env.BRIDGE_FRONTEND_URL}/api/whatsapp/bots/${botID}/receive`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(payload), }, ); } } } private async queueUnreadMessages( botID: string, messages: proto.IWebMessageInfo[], ) { for await (const message of messages) { await this.queueMessage(botID, message); } } getBot(botID: string): Record { const botDirectory = this.getBotDirectory(botID); const qrPath = `${botDirectory}/qr.txt`; const verifiedFile = `${botDirectory}/verified`; const qr = fs.existsSync(qrPath) ? fs.readFileSync(qrPath, "utf8") : null; const verified = fs.existsSync(verifiedFile); return { qr, verified }; } async unverify(botID: string): Promise { const botDirectory = this.getBotDirectory(botID); fs.rmSync(botDirectory, { recursive: true, force: true }); } async register( botID: string, callback?: AuthCompleteCallback, ): Promise { const { version } = await fetchLatestBaileysVersion(); await this.createConnection( botID, this.server, { version, browser: WhatsappService.browserDescription }, callback, ); callback?.(); } async send( botID: string, phoneNumber: string, message: string, ): Promise { const connection = this.connections[botID]?.socket; const recipient = `${phoneNumber.replace(/\D+/g, "")}@s.whatsapp.net`; await connection.sendMessage(recipient, { text: message }); } async receive( botID: string, _lastReceivedDate: Date, ): Promise { const connection = this.connections[botID]?.socket; console.log({ connection }); const messages = await connection.loadAllUnreadMessages(); return messages; } }