Skip to content

Instantly share code, notes, and snippets.

@balataca
Created September 9, 2021 20:14
Show Gist options
  • Save balataca/3db96827cd114aa0cec1763f58eef4e6 to your computer and use it in GitHub Desktop.
Save balataca/3db96827cd114aa0cec1763f58eef4e6 to your computer and use it in GitHub Desktop.
const puppeteer = require("puppeteer");
const qrcode = require("qrcode-terminal");
const { from, merge } = require('rxjs');
const { take } = require('rxjs/operators');
const path = require('path');
const readline = require('readline');
var rimraf = require("rimraf");
let browser = null;
let page = null;
let counter = { fails: 0, success: 0 }
const tmpPath = path.resolve(__dirname, '../tmp');
const SELECTORS = {
LOADING: "progress",
INSIDE_CHAT: "document.getElementsByClassName('two')[0]",
QRCODE_PAGE: "body > div > div > .landing-wrapper",
QRCODE_DATA: "div[data-ref]",
QRCODE_DATA_ATTR: "data-ref",
SEND_BUTTON: 'div:nth-child(2) > button > span[data-icon="send"]'
};
/**
* Initialize browser, page and setup page desktop mode
*/
async function start({ showBrowser = false, qrCodeData = false, session = true } = {}) {
if (!session) {
deleteSession(tmpPath);
}
const args = {
headless: !showBrowser,
userDataDir: tmpPath,
args: ["--no-sandbox",
// "--blink-settings=imagesEnabled=false"]
]
}
try {
browser = await puppeteer.launch(args);
page = await browser.newPage();
// prevent dialog blocking page and just accept it(necessary when a message is sent too fast)
page.on("dialog", async dialog => { await dialog.accept(); });
// fix the chrome headless mode true issues
// https://gitmemory.com/issue/GoogleChrome/puppeteer/1766/482797370
await page.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36");
page.setDefaultTimeout(60000);
await page.goto("https://web.whatsapp.com");
if (session && await isAuthenticated()) {
return;
}
else {
if (qrCodeData) {
console.log('Getting QRCode data...');
console.log('Note: You should use wbm.waitQRCode() inside wbm.start() to avoid errors.');
return await getQRCodeData();
} else {
await generateQRCode();
}
}
} catch (err) {
deleteSession(tmpPath);
throw err;
}
}
/**
* Check if needs to scan qr code or already is is inside the chat
*/
function isAuthenticated() {
console.log('Authenticating...');
return merge(needsToScan(page), isInsideChat(page))
.pipe(take(1))
.toPromise();
}
function needsToScan() {
return from(
page
.waitForSelector(SELECTORS.QRCODE_PAGE, {
timeout: 0,
}).then(() => false)
);
}
function isInsideChat() {
return from(
page
.waitForFunction(SELECTORS.INSIDE_CHAT,
{
timeout: 0,
}).then(() => true)
);
}
function deleteSession() {
rimraf.sync(tmpPath);
}
/**
* return the data used to create the QR Code
*/
async function getQRCodeData() {
await page.waitForSelector(SELECTORS.QRCODE_DATA, { timeout: 60000 });
const qrcodeData = await page.evaluate((SELECTORS) => {
let qrcodeDiv = document.querySelector(SELECTORS.QRCODE_DATA);
return qrcodeDiv.getAttribute(SELECTORS.QRCODE_DATA_ATTR);
}, SELECTORS);
return await qrcodeData;
}
/**
* Access whatsapp web page, get QR Code data and generate it on terminal
*/
async function generateQRCode() {
try {
console.log("generating QRCode...");
const qrcodeData = await getQRCodeData();
qrcode.generate(qrcodeData, { small: true });
console.log("QRCode generated! Scan it using Whatsapp App.");
} catch (err) {
throw await QRCodeExeption("QR Code can't be generated(maybe your connection is too slow).");
}
await waitQRCode();
}
/**
* Wait 30s to the qrCode be hidden on page
*/
async function waitQRCode() {
// if user scan QR Code it will be hidden
try {
await page.waitForSelector(SELECTORS.QRCODE_PAGE, { timeout: 30000, hidden: true });
} catch (err) {
throw await QRCodeExeption("Dont't be late to scan the QR Code.");
}
}
/**
* Close browser and show an error message
* @param {string} msg
*/
async function QRCodeExeption(msg) {
await browser.close();
return "QRCodeException: " + msg;
}
/**
* @param {string} phone phone number: '5535988841854'
* @param {string} message Message to send to phone number
* Send message to a phone number
*/
async function sendTo(phoneOrContact, message) {
let phone = phoneOrContact;
if (typeof phoneOrContact === "object") {
phone = phoneOrContact.phone;
message = generateCustomMessage(phoneOrContact, message);
}
try {
process.stdout.write("Sending Message...\r");
await page.goto(`https://web.whatsapp.com/send?phone=${phone}&text=${encodeURIComponent(message)}`);
await page.waitForSelector(SELECTORS.LOADING, { hidden: true, timeout: 60000 });
await page.waitForSelector(SELECTORS.SEND_BUTTON, { timeout: 5000 });
await page.keyboard.press("Enter");
await page.waitFor(1000);
readline.clearLine(process.stdout);
readline.cursorTo(process.stdout, 0);
process.stdout.write(`${phone} Sent\n`);
counter.success++;
} catch (err) {
readline.clearLine(process.stdout);
process.stdout.cursorTo(0);
process.stdout.write(`${phone} Failed\n`);
counter.fails++;
}
}
/**
* @param {array} phones Array of phone numbers: ['5535988841854', ...]
* @param {string} message Message to send to every phone number
* Send same message to every phone number
*/
async function send(phoneOrContacts, message) {
for (let phoneOrContact of phoneOrContacts) {
await sendTo(phoneOrContact, message);
}
}
/**
* @param {object} contact contact with several properties defined by the user
* @param {string} messagePrototype Custom message to send to every phone number
* @returns {string} message
* Replace all text between {{}} to respective contact property
*/
function generateCustomMessage(contact, messagePrototype) {
let message = messagePrototype;
for (let property in contact) {
message = message.replace(new RegExp(`{{${property}}}`, "g"), contact[property]);
}
return message;
}
/**
* Close browser and show results(number of messages sent and failed)
*/
async function end() {
await browser.close();
console.log(`Result: ${counter.success} sent, ${counter.fails} failed`);
}
module.exports = {
start,
send,
sendTo,
end,
waitQRCode
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment