UNPKG

whatsauto.js

Version:

Easy WhatsApp Automation with Session

841 lines (840 loc) • 34.3 kB
import path from "path"; import fs from "fs"; import makeWASocket, { DisconnectReason, downloadMediaMessage, Browsers, fetchLatestBaileysVersion, useMultiFileAuthState, proto, } from "@whiskeysockets/baileys"; import { CREDENTIALS, Messages } from "../Defaults/index.js"; import { ValidationError, AutoWAError } from "../Error/index.js"; import { parseMessageStatusCodeToReadable, getMediaMimeType, phoneToJid, createDelay, isSessionExist, getRandomFromArrays, getContextInfo, } from "../Utils/helper.js"; import AutoWAEvent from "./AutoWAEvent.js"; import mime from "mime"; import Logger from "../Logger/index.js"; import { makeWebpBuffer } from "../Utils/make-stiker.js"; import { sessions } from "./index.js"; import pino from "pino"; import qrcode from "qrcode-terminal"; const P = pino({ level: "silent", }); export class AutoWA { logger; retryCount; sock; sessionId; options; events = new AutoWAEvent(); pairingCode; defaultStickerProps = { pack: "whatsauto.js", author: "freack21", media: null, }; constructor(sessionId, options) { if (isSessionExist(sessionId) && sessions.get(sessionId)) throw new ValidationError(Messages.sessionAlreadyExist(sessionId)); const defaultOptions = { printQR: true, logging: true, }; this.sessionId = sessionId; this.options = { ...defaultOptions, ...options }; this.retryCount = 0; this.logger = new Logger(sessionId, this); sessions.set(sessionId, this); this.logger.info("Created!"); } async setLogging(logging) { this.options.logging = logging; } async initialize() { this.logger.info("Initializing..."); await this.startWhatsApp(this.sessionId, this.options); } async startWhatsApp(sessionId = "mySession", options = { printQR: true }) { if (typeof options.phoneNumber == "string") { if (options.phoneNumber === "") throw new ValidationError(Messages.paremetersNotValid("phoneNumber")); options.printQR = false; options.phoneNumber = phoneToJid({ from: options.phoneNumber, }); } return this.startSocket(sessionId, options); } async startSocket(sessionId, options) { try { const { version } = await fetchLatestBaileysVersion(); const { state, saveCreds } = await useMultiFileAuthState(path.resolve(CREDENTIALS.DIR_NAME, sessionId + CREDENTIALS.PREFIX)); const [platform, browser] = getRandomFromArrays([Browsers.macOS, Browsers.ubuntu, Browsers.windows], ["Chrome", "Firefox", "Safari"]); this.sock = makeWASocket({ version, auth: state, logger: P, markOnlineOnConnect: false, browser: platform(browser), }); return this.setupWASocket(saveCreds); } catch (error) { const msg = `Failed initiliaze WASocket: ${error.message}`; this.logger.error(msg); throw new AutoWAError(msg); } } async setupWASocket(saveCreds) { try { if (typeof this.options.phoneNumber == "string" && !this.options.printQR && !this.pairingCode && !this.sock.authState.creds.registered) { const phoneNumber = phoneToJid({ from: this.options.phoneNumber, reverse: true }); try { this.pairingCode = await this.sock.requestPairingCode(phoneNumber); this.logger.info(`Pairing Code: ${this.pairingCode}`); this.events.emit("pairing-code", this.pairingCode); this.retryCount = 0; } catch (error) { this.retryCount++; this.logger.warn(`Retry get pairing code for ${phoneNumber} (${this.retryCount}x)`); await createDelay(1000); return await this.startSocket(this.sessionId, this.options); } } this.sock.ev.on("connection.update", async (update) => { const { connection, lastDisconnect, qr } = update; if (this.options.printQR && qr) { this.logger.info("QR Updated!"); if (this.options.printQR) { qrcode.generate(qr, { small: true }); } this.events.emit("qr", qr); } if (connection == "connecting") { this.logger.info("Connecting..."); this.events.emit("connecting"); } if (connection === "close" && !this.pairingCode) { const code = lastDisconnect?.error?.output?.statusCode; let shouldRetry = false; if (code == DisconnectReason.restartRequired) { this.logger.info("Restarting..."); return await this.startSocket(this.sessionId, this.options); } else if (code == DisconnectReason.connectionLost) { this.logger.warn("No Internet!"); shouldRetry = true; } else if (code == DisconnectReason.connectionClosed) { this.logger.warn("Connection Closed!"); shouldRetry = true; } else if (code == DisconnectReason.loggedOut) { this.logger.warn("Logged Out!"); } else if (code != DisconnectReason.loggedOut && this.retryCount < 10) { this.logger.warn("Connection Status : " + code); shouldRetry = true; } if (shouldRetry) { this.logger.warn("Retry connecting..."); this.retryCount++; await createDelay(5000); return await this.startSocket(this.sessionId, this.options); } else { this.retryCount = 0; this.logger.warn("Disconnected!"); this.events.emit("disconnected"); try { await this.destroy(true); } catch (error) { } return; } } if (connection == "open") { this.logger.info("Connected!"); this.retryCount = 0; this.events.emit("connected"); } }); this.sock.ev.on("creds.update", async () => { await saveCreds(); }); this.sock.ev.on("messages.update", async (message) => { const msg = message[0]; const data = { sessionId: this.sessionId, messageStatus: parseMessageStatusCodeToReadable(msg.update.status), ...msg, }; this.events.emit("message-updated", data); }); this.sock.ev.on("messages.upsert", async (new_message) => { if (new_message.type == "append") return; const myJid = phoneToJid({ from: this.sock.user.id }); let msg = new_message.messages?.[0]; const isDeletedMsg = new_message.messages?.[0].message?.protocolMessage?.type == proto.Message.ProtocolMessage.Type.REVOKE; if (isDeletedMsg) { msg = { ...new_message.messages?.[0], deletedMessage: { key: { id: new_message.messages?.[0].message?.protocolMessage?.key?.id, }, }, }; } else { if (msg.message?.documentWithCaptionMessage) msg = { ...msg, message: msg.message.documentWithCaptionMessage.message, }; else if (msg.message?.ephemeralMessage) msg = { ...msg, message: msg.message.ephemeralMessage?.message, }; msg.sessionId = this.sessionId; let quotedMessage = null; const msgContextInfo = getContextInfo(msg); if (msgContextInfo?.quotedMessage) { quotedMessage = { key: { remoteJid: msg.key?.remoteJid, remoteJidAlt: msg.key?.remoteJidAlt, id: msgContextInfo?.stanzaId, participant: msgContextInfo?.participant, fromMe: msgContextInfo?.participant == myJid, }, message: msgContextInfo?.quotedMessage, }; } if (quotedMessage?.message?.documentWithCaptionMessage) { quotedMessage = { ...quotedMessage, message: quotedMessage.message.documentWithCaptionMessage.message, }; } msg.quotedMessage = quotedMessage; } const mediaTypes = ["image", "audio", "video", "document"]; const setupMsg = (msg) => { const text = msg.message?.conversation || msg.message?.extendedTextMessage?.text || msg.message?.imageMessage?.caption || msg.message?.videoMessage?.caption || msg.message?.documentMessage?.caption || ""; msg.text = text; const mimeType = getMediaMimeType(msg); const ext = mime.getExtension(mimeType); msg.hasMedia = mimeType !== ""; msg.mediaType = ""; if (mimeType) msg.mediaType = mediaTypes[mediaTypes.indexOf(mimeType.split("/")[0]) !== -1 ? mediaTypes.indexOf(mimeType.split("/")[0]) : 3]; msg.downloadMedia = async () => Promise.resolve(null); msg.toSticker = async () => Promise.resolve([null, false]); if (msg.hasMedia) { msg.downloadMedia = async (opts = {}) => this.downloadMedia(msg, opts, ext); } if (msg.hasMedia || msg.quotedMessage?.hasMedia) { msg.toSticker = async (props) => { let mediaBuf; if (msg.hasMedia && ["image", "video"].includes(msg.mediaType)) { mediaBuf = await msg.downloadMedia({ asBuffer: true }); } else if (msg.quotedMessage && msg.quotedMessage.hasMedia && ["image", "video"].includes(msg.quotedMessage.mediaType)) { mediaBuf = await msg.quotedMessage.downloadMedia({ asBuffer: true }); } if (!mediaBuf) return [null, false]; const stickerProps = { ...this.defaultStickerProps, ...props, media: mediaBuf, }; const buffer = await makeWebpBuffer(stickerProps); return [buffer, true]; }; } const rJidAlt = msg.key?.remoteJidAlt || ""; const rJid = msg.key?.remoteJid || ""; const from = rJidAlt.includes("whatsapp") ? rJidAlt : rJid.includes("whatsapp") ? rJid : (rJid || rJidAlt); const participant = msg.key?.participant || ""; const isGroup = from.includes("@g.us"); const isStory = msg.key?.remoteJidAlt?.includes("status@broadcast") || msg.key?.remoteJid?.includes("status@broadcast") || msg.broadcast || false; const isReaction = msg.message?.reactionMessage ? true : false; if (msg.key?.fromMe) { msg.receiver = from; msg.author = myJid; } else { msg.receiver = myJid; msg.author = from; if (isGroup || isStory) msg.author = participant; if (isGroup) msg.receiver = from; } msg.from = from; msg.isGroup = isGroup; msg.isStory = isStory; msg.isReaction = isReaction; if (isReaction) msg.text = msg.message?.reactionMessage?.text; msg.replyWithText = async (text, opts) => { return await this.sendText({ ...opts, text, to: from, answering: msg }); }; msg.replyWithAudio = async (media, opts) => { return await this.sendAudio({ media, ...opts, to: from, answering: msg }); }; msg.replyWithImage = async (media, opts) => { return await this.sendImage({ media, ...opts, to: from, answering: msg }); }; msg.replyWithVideo = async (media, opts) => { return await this.sendVideo({ media, ...opts, to: from, answering: msg }); }; msg.replyWithSticker = async (sticker, opts) => { return await this.sendSticker({ sticker, ...opts, to: from, answering: msg, }); }; msg.replyWithTyping = async (callback) => { return await this.sendTyping({ to: from, callback }); }; msg.replyWithRecording = async (callback) => { return await this.sendRecording({ to: from, callback }); }; msg.read = async () => { return await this.readMessage([msg]); }; msg.react = async (reaction) => { return await this.sendReaction({ to: from, answering: msg, text: reaction }); }; msg.forward = async (to, opts) => { return await this.forwardMessage({ to, msg, ...opts }); }; }; msg.quotedMessage && setupMsg(msg.quotedMessage); setupMsg(msg); const { isStory, isReaction, isGroup } = msg; if (msg.key.fromMe) { this.events.emit("message-sent", msg); if (isStory) { this.events.emit("story-sent", msg); } else if (isReaction) { this.events.emit("reaction-sent", msg); if (isGroup) this.events.emit("group-reaction-sent", msg); else this.events.emit("private-reaction-sent", msg); } else if (isGroup) { this.events.emit("group-message-sent", msg); } else { this.events.emit("private-message-sent", msg); } } else { this.events.emit("message-received", msg); if (isStory) { this.events.emit("story-received", msg); } else if (isReaction) { this.events.emit("reaction-received", msg); if (isGroup) this.events.emit("group-reaction-received", msg); else this.events.emit("private-reaction-received", msg); } else if (isGroup) { this.events.emit("group-message-received", msg); } else { this.events.emit("private-message-received", msg); } } if (isDeletedMsg) { this.events.emit("message-deleted", msg); } else if (isStory) { this.events.emit("story", msg); } else if (isReaction) { this.events.emit("reaction", msg); if (isGroup) this.events.emit("group-reaction", msg); else this.events.emit("private-reaction", msg); } else if (isGroup) { this.events.emit("group-message", msg); } else { this.events.emit("private-message", msg); } this.events.emit("message", msg); }); this.sock.ev.on("group-participants.update", async (data) => { const msg = { ...data, sessionId: this.sessionId, }; msg.replyWithText = async (text, opts) => { return await this.sendText({ ...opts, text, to: data.id }); }; msg.replyWithAudio = async (media, opts) => { return await this.sendAudio({ media, ...opts, to: data.id }); }; msg.replyWithImage = async (media, opts) => { return await this.sendImage({ media, ...opts, to: data.id }); }; msg.replyWithVideo = async (media, opts) => { return await this.sendVideo({ media, ...opts, to: data.id }); }; msg.replyWithSticker = async (sticker, opts) => { return await this.sendSticker({ sticker, ...opts, to: data.id }); }; msg.replyWithTyping = async (callback) => { return await this.sendTyping({ to: data.id, callback }); }; msg.replyWithRecording = async (callback) => { return await this.sendRecording({ to: data.id, callback }); }; this.events.emit("group-member-update", msg); }); this.sock.ev.on("messages.delete", async (msgs) => { this.logger.info("Msg Deleted : " + JSON.stringify(msgs, null, 2)); }); return this.sock; } catch (error) { const msg = `Failed setup WASocket: ${error.message}`; this.logger.error(msg); throw new AutoWAError(msg); } } async destroy(full) { this.logger.info("Destroying..."); let error = false; let msg = ""; try { await this.sock.logout(); } catch (err) { msg = `Logout failed: ${err.message}`; error = true; } finally { this.sock.end(undefined); if (full) { const dir = path.resolve(CREDENTIALS.DIR_NAME, this.sessionId + CREDENTIALS.PREFIX); if (fs.existsSync(dir)) { fs.rmSync(dir, { force: true, recursive: true }); } } this.logger.info("Destroyed!"); } if (error) { this.logger.error(msg); throw new AutoWAError(msg); } } async isExist({ from, isGroup = false }) { try { const receiver = phoneToJid({ from: from, isGroup, }); if (receiver.includes("@broadcast")) { return true; } else if (!receiver.includes("@g.us")) { return Boolean((await this.sock.onWhatsApp(receiver))?.[0]?.exists); } else { return Boolean((await this.sock.groupMetadata(receiver)).id); } } catch (error) { const msg = `Failed get exist status: ${error.message}`; this.logger.error(msg); throw new AutoWAError(msg); } } async downloadMedia(msg, opts, ext) { const filePath = path.join(process.cwd(), (opts.path || "my_media") + "." + ext); const buf = await downloadMediaMessage(msg, "buffer", {}); if (opts.asBuffer) return Promise.resolve(buf); fs.writeFileSync(filePath, buf); return Promise.resolve(filePath); } async validateReceiver({ from, isGroup = false }) { const oldPhone = from; from = phoneToJid({ from, isGroup }); const isRegistered = await this.isExist({ from, isGroup, }); if (!isRegistered) { return { msg: `${oldPhone} is not registered on Whatsapp`, }; } return { receiver: from, }; } async sendText({ to, text = "", isGroup = false, ...props }) { const { receiver, msg } = await this.validateReceiver({ from: to, isGroup, }); if (msg) throw new AutoWAError(msg); return await this.sock.sendMessage(receiver, { text: text, mentions: props.mentions, }, { quoted: props.answering, }); } async sendImage({ to, text = "", isGroup = false, media, failMsg, ...props }) { if (!media) throw new AutoWAError("'media' parameter must be Buffer or String URL"); const { receiver, msg } = await this.validateReceiver({ from: to, isGroup, }); if (msg) throw new AutoWAError(msg); try { return await this.sock.sendMessage(receiver, { image: typeof media == "string" ? { url: media, } : media, caption: text, mentions: props.mentions, }, { quoted: props.answering, }); } catch (error) { this.logger.error("Failed send media:" + error.message); return await this.sendText({ to: receiver, text: failMsg || "There is error while trying to send the image🄹", ...props, }); } } async sendVideo({ to, text = "", isGroup = false, media, failMsg, ...props }) { if (!media) throw new AutoWAError("'media' parameter must be Buffer or String URL"); const { receiver, msg } = await this.validateReceiver({ from: to, isGroup, }); if (msg) throw new AutoWAError(msg); try { return await this.sock.sendMessage(receiver, { video: typeof media == "string" ? { url: media, } : media, caption: text, mentions: props.mentions, }, { quoted: props.answering, }); } catch (error) { this.logger.error("Failed send media:" + error.message); return await this.sendText({ to: receiver, text: failMsg || "There is error while trying to send the video🄹", ...props, }); } } async sendDocument({ to, text = "", isGroup = false, media, filename, failMsg, ...props }) { if (!media) throw new AutoWAError("'media' parameter must be Buffer or String URL"); const mimetype = mime.getType(filename); if (!mimetype) throw new AutoWAError(`Filename must include valid extension`); const { receiver, msg } = await this.validateReceiver({ from: to, isGroup, }); if (msg) throw new AutoWAError(msg); try { return await this.sock.sendMessage(receiver, { fileName: filename, document: typeof media == "string" ? { url: media, } : media, mimetype: mimetype, caption: text, mentions: props.mentions, }, { quoted: props.answering, }); } catch (error) { this.logger.error("Failed send media:" + error.message); return await this.sendText({ to: receiver, text: failMsg || "There is error while trying to send the document", ...props, }); } } async sendAudio({ to, isGroup = false, media, voiceNote = false, failMsg, ...props }) { if (!media) throw new AutoWAError("'media' parameter must be Buffer or String URL"); const { receiver, msg } = await this.validateReceiver({ from: to, isGroup, }); if (msg) throw new AutoWAError(msg); try { return await this.sock.sendMessage(receiver, { audio: typeof media == "string" ? { url: media, } : media, ptt: voiceNote, mentions: props.mentions, }, { quoted: props.answering, }); } catch (error) { this.logger.error("Failed send media:" + error.message); return await this.sendText({ to: receiver, text: failMsg || "There is error while trying to send the audio🄹", ...props, }); } } async sendReaction({ to, text, isGroup = false, answering }) { const { receiver, msg } = await this.validateReceiver({ from: to, isGroup, }); if (msg) throw new AutoWAError(msg); return await this.sock.sendMessage(receiver, { react: { text, key: answering.key, }, }); } async sendTyping({ to, callback, isGroup = false }) { const { receiver, msg } = await this.validateReceiver({ from: to, isGroup, }); if (msg) throw new AutoWAError(msg); await this.sock.sendPresenceUpdate("composing", receiver); await callback(); await this.sock.sendPresenceUpdate("available", receiver); } async sendRecording({ to, callback, isGroup = false }) { const { receiver, msg } = await this.validateReceiver({ from: to, isGroup, }); if (msg) throw new AutoWAError(msg); await this.sock.sendPresenceUpdate("recording", receiver); await callback(); await this.sock.sendPresenceUpdate("available", receiver); } async readMessage(msgs) { await this.sock.readMessages(msgs.map((msg) => msg.key)); } async sendSticker({ to, isGroup, sticker, media, failMsg, hasMedia, ...props }) { const { receiver, msg } = await this.validateReceiver({ from: to, isGroup, }); if (msg) throw new AutoWAError(msg); if (!media && !sticker && !hasMedia) throw new AutoWAError("'media' or 'sticker' parameter must be filled"); if (!sticker) { if (!(typeof media === "string" || Buffer.isBuffer(media)) && !hasMedia) { throw new AutoWAError("'media' parameter must be string or buffer"); } const stickerProps = { ...this.defaultStickerProps, media, ...props, }; sticker = await makeWebpBuffer(stickerProps); } if (!sticker || !Buffer.isBuffer(sticker)) { return await this.sendText({ to, text: failMsg || "There is error while creating the sticker🄹", isGroup, ...props, }); } try { return await this.sock.sendMessage(receiver, { sticker, mentions: props.mentions, }, { quoted: props.answering, }); } catch (error) { this.logger.error("Failed send media:" + error.message); return await this.sendText({ to: receiver, text: failMsg || "There is error while trying to send the sticker🄹", ...props, }); } } async forwardMessage({ to, msg, isGroup = false, ...props }) { const { receiver, msg: err_msg } = await this.validateReceiver({ from: to, isGroup, }); if (err_msg) throw new AutoWAError(err_msg); try { return await this.sock.sendMessage(receiver, { forward: msg, mentions: props.mentions, force: true, }); } catch (error) { this.logger.error("Failed forward a message!"); } } async getProfileInfo(target) { const { receiver, msg } = await this.validateReceiver({ from: target, }); if (msg) throw new AutoWAError(msg); try { const [profilePictureUrl, status] = await Promise.allSettled([ this.sock.profilePictureUrl(receiver, "image", 5000), this.sock.fetchStatus(receiver), ]); return { profilePictureUrl: profilePictureUrl.status === "fulfilled" ? profilePictureUrl.value || null : null, status: status.status === "fulfilled" ? status.value || null : null, }; } catch (error) { const msg = `Failed get profile info: ${error.message}`; this.logger.error(msg); } return null; } async getGroupInfo(target) { const { receiver, msg } = await this.validateReceiver({ from: target, isGroup: true, }); if (msg) throw new AutoWAError(msg); try { return await this.sock.groupMetadata(receiver); } catch (error) { const msg = `Failed get group info: ${error.message}`; this.logger.error(msg); } return null; } async addMemberToGroup({ participants, to }) { const { receiver: group, msg } = await this.validateReceiver({ from: to, isGroup: true, }); if (msg) throw new AutoWAError(msg); participants = participants.map((d) => phoneToJid({ from: d })); return await this.sock.groupParticipantsUpdate(group, participants, "add"); } async removeMemberFromGroup({ participants, to }) { const { receiver: group, msg } = await this.validateReceiver({ from: to, isGroup: true, }); if (msg) throw new AutoWAError(msg); participants = participants.map((d) => phoneToJid({ from: d })); return await this.sock.groupParticipantsUpdate(group, participants, "remove"); } async promoteMemberGroup({ participants, to }) { const { receiver: group, msg } = await this.validateReceiver({ from: to, isGroup: true, }); if (msg) throw new AutoWAError(msg); participants = participants.map((d) => phoneToJid({ from: d })); return await this.sock.groupParticipantsUpdate(group, participants, "promote"); } async demoteMemberGroup({ participants, to }) { const { receiver: group, msg } = await this.validateReceiver({ from: to, isGroup: true, }); if (msg) throw new AutoWAError(msg); participants = participants.map((d) => phoneToJid({ from: d })); return await this.sock.groupParticipantsUpdate(group, participants, "demote"); } on(event, listener) { this.events.on(event, listener); } once(event, listener) { this.events.once(event, listener); } off(event, listener) { this.events.off(event, listener); } emit(event, ...args) { return this.events.emit(event, ...args); } removeAllListeners(event) { this.events.removeAllListeners(event); } }