whatsauto.js
Version:
Easy WhatsApp Automation with Session
841 lines (840 loc) ⢠34.3 kB
JavaScript
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);
}
}