UNPKG

@warriorteam/zalo-personal

Version:

Unofficial Zalo Personal API for JavaScript - A powerful library for interacting with Zalo personal accounts with URL attachment support, auto-reply, product catalog, and business features

366 lines (365 loc) 15.9 kB
import { CookieJar } from "tough-cookie"; import { writeFile } from "node:fs/promises"; import { ZaloApiError } from "../Errors/ZaloApiError.js"; import { logger, request } from "../utils.js"; import { ZaloApiLoginQRAborted } from "../Errors/ZaloApiLoginQRAborted.js"; import { ZaloApiLoginQRDeclined } from "../Errors/ZaloApiLoginQRDeclined.js"; export var LoginQRCallbackEventType; (function (LoginQRCallbackEventType) { LoginQRCallbackEventType[LoginQRCallbackEventType["QRCodeGenerated"] = 0] = "QRCodeGenerated"; LoginQRCallbackEventType[LoginQRCallbackEventType["QRCodeExpired"] = 1] = "QRCodeExpired"; LoginQRCallbackEventType[LoginQRCallbackEventType["QRCodeScanned"] = 2] = "QRCodeScanned"; LoginQRCallbackEventType[LoginQRCallbackEventType["QRCodeDeclined"] = 3] = "QRCodeDeclined"; LoginQRCallbackEventType[LoginQRCallbackEventType["GotLoginInfo"] = 4] = "GotLoginInfo"; })(LoginQRCallbackEventType || (LoginQRCallbackEventType = {})); async function loadLoginPage(ctx) { const response = await request(ctx, "https://id.zalo.me/account?continue=https%3A%2F%2Fchat.zalo.me%2F", { headers: { accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "accept-language": "vi-VN,vi;q=0.9,fr-FR;q=0.8,fr;q=0.7,en-US;q=0.6,en;q=0.5", "cache-control": "max-age=0", priority: "u=0, i", "sec-ch-ua": '"Chromium";v="130", "Google Chrome";v="130", "Not?A_Brand";v="99"', "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": '"Windows"', "sec-fetch-dest": "document", "sec-fetch-mode": "navigate", "sec-fetch-site": "same-site", "sec-fetch-user": "?1", "upgrade-insecure-requests": "1", Referer: "https://chat.zalo.me/", "Referrer-Policy": "strict-origin-when-cross-origin", }, method: "GET", }); const html = await response.text(); const regex = /https:\/\/stc-zlogin\.zdn\.vn\/main-([\d.]+)\.js/; const match = html.match(regex); return match === null || match === void 0 ? void 0 : match[1]; } async function getLoginInfo(ctx, version) { const form = new URLSearchParams(); form.append("continue", "https://zalo.me/pc"); form.append("v", version); return await request(ctx, "https://id.zalo.me/account/logininfo", { headers: { accept: "*/*", "accept-language": "vi-VN,vi;q=0.9,fr-FR;q=0.8,fr;q=0.7,en-US;q=0.6,en;q=0.5", "content-type": "application/x-www-form-urlencoded", priority: "u=1, i", "sec-ch-ua": '"Chromium";v="130", "Google Chrome";v="130", "Not?A_Brand";v="99"', "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": '"Windows"', "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-origin", Referer: "https://id.zalo.me/account?continue=https%3A%2F%2Fzalo.me%2Fpc", "Referrer-Policy": "strict-origin-when-cross-origin", }, body: form, method: "POST", }) .then((res) => res.json()) .catch(logger(ctx).error); } async function verifyClient(ctx, version) { const form = new URLSearchParams(); form.append("type", "device"); form.append("continue", "https://zalo.me/pc"); form.append("v", version); return await request(ctx, "https://id.zalo.me/account/verify-client", { headers: { accept: "*/*", "accept-language": "vi-VN,vi;q=0.9,fr-FR;q=0.8,fr;q=0.7,en-US;q=0.6,en;q=0.5", "content-type": "application/x-www-form-urlencoded", priority: "u=1, i", "sec-ch-ua": '"Chromium";v="130", "Google Chrome";v="130", "Not?A_Brand";v="99"', "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": '"Windows"', "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-origin", Referer: "https://id.zalo.me/account?continue=https%3A%2F%2Fzalo.me%2Fpc", "Referrer-Policy": "strict-origin-when-cross-origin", }, body: form, method: "POST", }) .then((res) => res.json()) .catch(logger(ctx).error); } async function generate(ctx, version) { const form = new URLSearchParams(); form.append("continue", "https://zalo.me/pc"); form.append("v", version); return await request(ctx, "https://id.zalo.me/account/authen/qr/generate", { headers: { accept: "*/*", "accept-language": "vi-VN,vi;q=0.9,fr-FR;q=0.8,fr;q=0.7,en-US;q=0.6,en;q=0.5", "content-type": "application/x-www-form-urlencoded", priority: "u=1, i", "sec-ch-ua": '"Chromium";v="130", "Google Chrome";v="130", "Not?A_Brand";v="99"', "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": '"Windows"', "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-origin", Referer: "https://id.zalo.me/account?continue=https%3A%2F%2Fzalo.me%2Fpc", "Referrer-Policy": "strict-origin-when-cross-origin", }, body: form, method: "POST", }) .then((res) => res.json()) .catch(logger(ctx).error); } async function saveQRCodeToFile(filepath, imageData) { await writeFile(filepath, imageData, "base64"); } async function waitingScan(ctx, version, code, signal) { const form = new URLSearchParams(); form.append("code", code); form.append("continue", "https://chat.zalo.me/"); form.append("v", version); return await request(ctx, "https://id.zalo.me/account/authen/qr/waiting-scan", { headers: { accept: "*/*", "accept-language": "vi-VN,vi;q=0.9,fr-FR;q=0.8,fr;q=0.7,en-US;q=0.6,en;q=0.5", "content-type": "application/x-www-form-urlencoded", priority: "u=1, i", "sec-ch-ua": '"Chromium";v="130", "Google Chrome";v="130", "Not?A_Brand";v="99"', "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": '"Windows"', "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-origin", Referer: "https://id.zalo.me/account?continue=https%3A%2F%2Fchat.zalo.me%2F", "Referrer-Policy": "strict-origin-when-cross-origin", }, body: form, method: "POST", signal, }) .then((res) => res.json()) .then((data) => { if (data.error_code == 8) { return waitingScan(ctx, version, code, signal); } return data; }) .catch((e) => { if (!signal.aborted) logger(ctx).error(e); }); } async function waitingConfirm(ctx, version, code, signal) { const form = new URLSearchParams(); form.append("code", code); form.append("gToken", ""); form.append("gAction", "CONFIRM_QR"); form.append("continue", "https://chat.zalo.me/"); form.append("v", version); logger(ctx).info("Please confirm on your phone"); return await request(ctx, "https://id.zalo.me/account/authen/qr/waiting-confirm", { headers: { accept: "*/*", "accept-language": "vi-VN,vi;q=0.9,fr-FR;q=0.8,fr;q=0.7,en-US;q=0.6,en;q=0.5", "content-type": "application/x-www-form-urlencoded", priority: "u=1, i", "sec-ch-ua": '"Chromium";v="130", "Google Chrome";v="130", "Not?A_Brand";v="99"', "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": '"Windows"', "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-origin", Referer: "https://id.zalo.me/account?continue=https%3A%2F%2Fchat.zalo.me%2F", "Referrer-Policy": "strict-origin-when-cross-origin", }, body: form, method: "POST", signal, }) .then((res) => res.json()) .then((data) => { if (data.error_code == 8) { return waitingConfirm(ctx, version, code, signal); } return data; }) .catch((e) => { if (!signal.aborted) logger(ctx).error(e); }); } async function checkSession(ctx) { return await request(ctx, "https://id.zalo.me/account/checksession?continue=https%3A%2F%2Fchat.zalo.me%2Findex.html", { headers: { accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "accept-language": "vi-VN,vi;q=0.9,fr-FR;q=0.8,fr;q=0.7,en-US;q=0.6,en;q=0.5", priority: "u=0, i", "sec-ch-ua": '"Chromium";v="130", "Google Chrome";v="130", "Not?A_Brand";v="99"', "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": '"Windows"', "sec-fetch-dest": "document", "sec-fetch-mode": "navigate", "sec-fetch-site": "same-origin", "upgrade-insecure-requests": "1", Referer: "https://id.zalo.me/account?continue=https%3A%2F%2Fchat.zalo.me%2F", "Referrer-Policy": "strict-origin-when-cross-origin", }, redirect: "manual", method: "GET", }).catch(logger(ctx).error); } async function getUserInfo(ctx) { return await request(ctx, "https://jr.chat.zalo.me/jr/userinfo", { headers: { accept: "*/*", "accept-language": "vi-VN,vi;q=0.9,fr-FR;q=0.8,fr;q=0.7,en-US;q=0.6,en;q=0.5", priority: "u=1, i", "sec-ch-ua": '"Chromium";v="130", "Google Chrome";v="130", "Not?A_Brand";v="99"', "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": '"Windows"', "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-site", Referer: "https://chat.zalo.me/", "Referrer-Policy": "strict-origin-when-cross-origin", }, method: "GET", }) .then((res) => res.json()) .catch(logger(ctx).error); } export async function loginQR(ctx, options, callback) { ctx.cookie = new CookieJar(); ctx.userAgent = options.userAgent; return new Promise(async (resolve, reject) => { var _a; const controller = new AbortController(); let qrTimeout = null; function cleanUp() { controller.abort(); if (qrTimeout) { clearTimeout(qrTimeout); qrTimeout = null; } } try { function retry() { cleanUp(); return resolve(loginQR(ctx, options, callback)); } function abort() { cleanUp(); return reject(new ZaloApiLoginQRAborted()); } if (ctx.options.logging) console.log(); const loginVersion = await loadLoginPage(ctx); if (!loginVersion) throw new ZaloApiError("Cannot get API login version"); logger(ctx).info("Got login version:", loginVersion); await getLoginInfo(ctx, loginVersion); await verifyClient(ctx, loginVersion); const qrGenResult = await generate(ctx, loginVersion); if (!qrGenResult || !qrGenResult.data) throw new ZaloApiError(`Unable to generate QRCode\nResponse: ${JSON.stringify(qrGenResult, null, 2)}`); const qrData = qrGenResult.data; if (callback) { callback({ type: LoginQRCallbackEventType.QRCodeGenerated, data: Object.assign(Object.assign({}, qrGenResult.data), { image: qrGenResult.data.image.replace(/^data:image\/png;base64,/, "") }), actions: { async saveToFile(qrPath) { var _a; if (qrPath === void 0) { qrPath = (_a = options.qrPath) !== null && _a !== void 0 ? _a : "qr.png"; } await saveQRCodeToFile(qrPath, qrData.image.replace(/^data:image\/png;base64,/, "")); logger(ctx).info("Scan the QR code at", `'${qrPath}'`, "to proceed with login"); }, retry, abort, }, }); } else { const qrPath = (_a = options.qrPath) !== null && _a !== void 0 ? _a : "qr.png"; await saveQRCodeToFile(qrPath, qrData.image.replace(/^data:image\/png;base64,/, "")); logger(ctx).info("Scan the QR code at", `'${qrPath}'`, "to proceed with login"); } qrTimeout = setTimeout(() => { cleanUp(); logger(ctx).info("QR expired!"); if (callback) { callback({ type: LoginQRCallbackEventType.QRCodeExpired, data: null, actions: { retry, abort, }, }); } else { retry(); } }, 100000); const scanResult = await waitingScan(ctx, loginVersion, qrGenResult.data.code, controller.signal); if (!scanResult || !scanResult.data) throw new ZaloApiError("Cannot get scan result"); if (callback) { callback({ type: LoginQRCallbackEventType.QRCodeScanned, data: scanResult.data, actions: { retry, abort, }, }); } const confirmResult = await waitingConfirm(ctx, loginVersion, qrGenResult.data.code, controller.signal); if (!confirmResult) throw new ZaloApiError("Cannot get confirm result"); clearTimeout(qrTimeout); if (confirmResult.error_code == -13) { if (callback) { callback({ type: LoginQRCallbackEventType.QRCodeDeclined, data: { code: qrData.code, }, actions: { retry, abort, }, }); } else { logger(ctx).error("QRCode login declined"); throw new ZaloApiLoginQRDeclined(); } return; } else if (confirmResult.error_code != 0) { throw new ZaloApiError(`An error has occurred.\nResponse: ${JSON.stringify(confirmResult, null, 2)}`); } const checkSessionResult = await checkSession(ctx); if (!checkSessionResult) throw new ZaloApiError("Cannot get session, login failed"); logger(ctx).info("Successfully logged into the account", scanResult.data.display_name); const userInfo = await getUserInfo(ctx); if (!userInfo || !userInfo.data) throw new ZaloApiError("Can't get account info"); if (!userInfo.data.logged) throw new ZaloApiError("Can't login"); resolve({ cookies: ctx.cookie.toJSON().cookies, userInfo: userInfo.data.info, }); } catch (error) { cleanUp(); reject(error); } }); }