Created
September 9, 2021 20:14
-
-
Save balataca/3db96827cd114aa0cec1763f58eef4e6 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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