708 lines
24 KiB
JavaScript
708 lines
24 KiB
JavaScript
import cryptojs from "crypto-js";
|
|
import crypto from "node:crypto";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import pako from "pako";
|
|
import SparkMD5 from "spark-md5";
|
|
import toughCookie from "tough-cookie";
|
|
import JSONBig from "json-bigint";
|
|
import { isContextSession } from "./context.js";
|
|
import { ZaloApiError, ZaloApiMissingImageMetadataGetter } from "./Errors/index.js";
|
|
import { FriendEventType } from "./models/FriendEvent.js";
|
|
import { GroupEventType } from "./models/GroupEvent.js";
|
|
export const isBun = typeof Bun !== "undefined";
|
|
export function hasOwn(obj, key) {
|
|
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
}
|
|
/**
|
|
* Get signed key for API requests.
|
|
*
|
|
* @param type
|
|
* @param params
|
|
* @returns MD5 hash
|
|
*
|
|
*/
|
|
export function getSignKey(type, params) {
|
|
const n = [];
|
|
for (const s in params) {
|
|
if (hasOwn(params, s)) {
|
|
n.push(s);
|
|
}
|
|
}
|
|
n.sort();
|
|
let a = "zsecure" + type;
|
|
for (let s = 0; s < n.length; s++)
|
|
a += params[n[s]];
|
|
return cryptojs.MD5(a).toString();
|
|
}
|
|
/**
|
|
*
|
|
* @param baseURL
|
|
* @param params
|
|
* @param apiVersion automatically add zalo api version to url params
|
|
* @returns
|
|
*
|
|
*/
|
|
export function makeURL(ctx, baseURL, params = {}, apiVersion = true) {
|
|
const url = new URL(baseURL);
|
|
for (const key in params) {
|
|
if (hasOwn(params, key)) {
|
|
url.searchParams.append(key, params[key].toString());
|
|
}
|
|
}
|
|
if (apiVersion) {
|
|
if (!url.searchParams.has("zpw_ver"))
|
|
url.searchParams.set("zpw_ver", ctx.API_VERSION.toString());
|
|
if (!url.searchParams.has("zpw_type"))
|
|
url.searchParams.set("zpw_type", ctx.API_TYPE.toString());
|
|
}
|
|
return url.toString();
|
|
}
|
|
export class ParamsEncryptor {
|
|
constructor({ type, imei, firstLaunchTime }) {
|
|
this.zcid = null;
|
|
this.enc_ver = "v2";
|
|
this.zcid = null;
|
|
this.encryptKey = null;
|
|
this.createZcid(type, imei, firstLaunchTime);
|
|
this.zcid_ext = ParamsEncryptor.randomString();
|
|
this.createEncryptKey();
|
|
}
|
|
getEncryptKey() {
|
|
if (!this.encryptKey)
|
|
throw new ZaloApiError("getEncryptKey: didn't create encryptKey yet");
|
|
return this.encryptKey;
|
|
}
|
|
createZcid(type, imei, firstLaunchTime) {
|
|
if (!type || !imei || !firstLaunchTime)
|
|
throw new ZaloApiError("createZcid: missing params");
|
|
const msg = `${type},${imei},${firstLaunchTime}`;
|
|
const s = ParamsEncryptor.encodeAES("3FC4F0D2AB50057BCE0D90D9187A22B1", msg, "hex", true);
|
|
this.zcid = s;
|
|
}
|
|
createEncryptKey(e = 0) {
|
|
const t = (e, t) => {
|
|
const { even: n } = ParamsEncryptor.processStr(e), { even: a, odd: s } = ParamsEncryptor.processStr(t);
|
|
if (!n || !a || !s)
|
|
return !1;
|
|
const i = n.slice(0, 8).join("") + a.slice(0, 12).join("") + s.reverse().slice(0, 12).join("");
|
|
return (this.encryptKey = i), !0;
|
|
};
|
|
if (!this.zcid || !this.zcid_ext)
|
|
throw new ZaloApiError("createEncryptKey: zcid or zcid_ext is null");
|
|
try {
|
|
const n = cryptojs.MD5(this.zcid_ext).toString().toUpperCase();
|
|
if (t(n, this.zcid) || !(e < 3))
|
|
return !1;
|
|
this.createEncryptKey(e + 1);
|
|
}
|
|
catch (_a) {
|
|
if (e < 3)
|
|
this.createEncryptKey(e + 1);
|
|
}
|
|
return !0;
|
|
}
|
|
getParams() {
|
|
return this.zcid
|
|
? {
|
|
zcid: this.zcid,
|
|
zcid_ext: this.zcid_ext,
|
|
enc_ver: this.enc_ver,
|
|
}
|
|
: null;
|
|
}
|
|
static processStr(e) {
|
|
if (!e || "string" != typeof e)
|
|
return {
|
|
even: null,
|
|
odd: null,
|
|
};
|
|
const [t, n] = [...e].reduce((e, t, n) => (e[n % 2].push(t), e), [[], []]);
|
|
return {
|
|
even: t,
|
|
odd: n,
|
|
};
|
|
}
|
|
static randomString(e, t) {
|
|
const n = e || 6, a = t && e && t > e ? t : 12;
|
|
let s = Math.floor(Math.random() * (a - n + 1)) + n;
|
|
if (s > 12) {
|
|
let e = "";
|
|
for (; s > 0;) {
|
|
e += Math.random()
|
|
.toString(16)
|
|
.substr(2, s > 12 ? 12 : s);
|
|
s -= 12;
|
|
}
|
|
return e;
|
|
}
|
|
return Math.random().toString(16).substr(2, s);
|
|
}
|
|
static encodeAES(e, message, type, uppercase, s = 0) {
|
|
if (!message)
|
|
return null;
|
|
try {
|
|
{
|
|
const encoder = "hex" == type ? cryptojs.enc.Hex : cryptojs.enc.Base64;
|
|
const key = cryptojs.enc.Utf8.parse(e);
|
|
const cfg = {
|
|
words: [0, 0, 0, 0],
|
|
sigBytes: 16,
|
|
};
|
|
const encrypted = cryptojs.AES.encrypt(message, key, {
|
|
iv: cfg,
|
|
mode: cryptojs.mode.CBC,
|
|
padding: cryptojs.pad.Pkcs7,
|
|
}).ciphertext.toString(encoder);
|
|
return uppercase ? encrypted.toUpperCase() : encrypted;
|
|
}
|
|
}
|
|
catch (_a) {
|
|
return s < 3 ? ParamsEncryptor.encodeAES(e, message, type, uppercase, s + 1) : null;
|
|
}
|
|
}
|
|
}
|
|
export function decryptResp(key, data) {
|
|
let n = null;
|
|
try {
|
|
n = decodeRespAES(key, data);
|
|
const parsed = JSON.parse(n);
|
|
return parsed;
|
|
}
|
|
catch (_a) {
|
|
return n;
|
|
}
|
|
}
|
|
function decodeRespAES(key, data) {
|
|
data = decodeURIComponent(data);
|
|
const parsedKey = cryptojs.enc.Utf8.parse(key);
|
|
const n = {
|
|
words: [0, 0, 0, 0],
|
|
sigBytes: 16,
|
|
};
|
|
return cryptojs.AES.decrypt({
|
|
ciphertext: cryptojs.enc.Base64.parse(data),
|
|
}, parsedKey, {
|
|
iv: n,
|
|
mode: cryptojs.mode.CBC,
|
|
padding: cryptojs.pad.Pkcs7,
|
|
}).toString(cryptojs.enc.Utf8);
|
|
}
|
|
export function decodeBase64ToBuffer(data) {
|
|
return Buffer.from(data, "base64");
|
|
}
|
|
export function decodeUnit8Array(data) {
|
|
try {
|
|
return new TextDecoder().decode(data);
|
|
}
|
|
catch (_a) {
|
|
return null;
|
|
}
|
|
}
|
|
export function encodeAES(secretKey, data, t = 0) {
|
|
try {
|
|
const key = cryptojs.enc.Base64.parse(secretKey);
|
|
return cryptojs.AES.encrypt(data, key, {
|
|
iv: cryptojs.enc.Hex.parse("00000000000000000000000000000000"),
|
|
mode: cryptojs.mode.CBC,
|
|
padding: cryptojs.pad.Pkcs7,
|
|
}).ciphertext.toString(cryptojs.enc.Base64);
|
|
}
|
|
catch (_a) {
|
|
return t < 3 ? encodeAES(secretKey, data, t + 1) : null;
|
|
}
|
|
}
|
|
export function decodeAES(secretKey, data, t = 0) {
|
|
try {
|
|
data = decodeURIComponent(data);
|
|
const key = cryptojs.enc.Base64.parse(secretKey);
|
|
return cryptojs.AES.decrypt({
|
|
ciphertext: cryptojs.enc.Base64.parse(data),
|
|
}, key, {
|
|
iv: cryptojs.enc.Hex.parse("00000000000000000000000000000000"),
|
|
mode: cryptojs.mode.CBC,
|
|
padding: cryptojs.pad.Pkcs7,
|
|
}).toString(cryptojs.enc.Utf8);
|
|
}
|
|
catch (_a) {
|
|
return t < 3 ? decodeAES(secretKey, data, t + 1) : null;
|
|
}
|
|
}
|
|
export async function getDefaultHeaders(ctx, origin = "https://chat.zalo.me") {
|
|
if (!ctx.cookie)
|
|
throw new ZaloApiError("Cookie is not available");
|
|
if (!ctx.userAgent)
|
|
throw new ZaloApiError("User agent is not available");
|
|
return {
|
|
Accept: "application/json, text/plain, */*",
|
|
"Accept-Encoding": "gzip, deflate, br, zstd",
|
|
"Accept-Language": "en-US,en;q=0.9",
|
|
"content-type": "application/x-www-form-urlencoded",
|
|
Cookie: await ctx.cookie.getCookieString(origin),
|
|
Origin: "https://chat.zalo.me",
|
|
Referer: "https://chat.zalo.me/",
|
|
"User-Agent": ctx.userAgent,
|
|
};
|
|
}
|
|
export async function request(ctx, url, options, raw = false) {
|
|
var _a, _b;
|
|
if (!ctx.cookie)
|
|
ctx.cookie = new toughCookie.CookieJar();
|
|
const origin = new URL(url).origin;
|
|
const defaultHeaders = await getDefaultHeaders(ctx, origin);
|
|
if (!raw) {
|
|
if (options) {
|
|
options.headers = Object.assign(defaultHeaders, options.headers || {});
|
|
}
|
|
else
|
|
options = { headers: defaultHeaders };
|
|
}
|
|
const _options = Object.assign(Object.assign({}, (options !== null && options !== void 0 ? options : {})), (isBun ? {
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
// @ts-ignore
|
|
proxy: (_b = (_a = ctx.options.agent) === null || _a === void 0 ? void 0 : _a.proxy) === null || _b === void 0 ? void 0 : _b.href
|
|
} : { agent: ctx.options.agent }));
|
|
const response = await ctx.options.polyfill(url, _options);
|
|
const setCookieRaw = response.headers.get("set-cookie");
|
|
if (setCookieRaw && !raw) {
|
|
// Use getSetCookie() (Node.js 18+) to properly parse multi-cookie headers.
|
|
// The split(", ") method breaks cookies containing Expires dates with commas
|
|
// (e.g., "Expires=Wed, 18 Mar 2026..."), causing critical cookies like
|
|
// zpsid and zpw_sek to be lost during QR login flow.
|
|
let cookieStrings;
|
|
if (typeof response.headers.getSetCookie === "function") {
|
|
cookieStrings = response.headers.getSetCookie();
|
|
}
|
|
else {
|
|
cookieStrings = setCookieRaw.split(", ");
|
|
}
|
|
for (const cookie of cookieStrings) {
|
|
const parsed = toughCookie.Cookie.parse(cookie);
|
|
try {
|
|
if (parsed)
|
|
await ctx.cookie.setCookie(parsed, parsed.domain != "zalo.me" ? `https://${parsed.domain}` : origin);
|
|
}
|
|
catch (error) {
|
|
logger(ctx).error(error);
|
|
}
|
|
}
|
|
}
|
|
const redirectURL = response.headers.get("location");
|
|
if (redirectURL) {
|
|
const redirectOptions = Object.assign({}, options);
|
|
redirectOptions.method = "GET";
|
|
if (!raw) {
|
|
redirectOptions.headers = new Headers(redirectOptions.headers);
|
|
redirectOptions.headers.set("Referer", "https://id.zalo.me/");
|
|
}
|
|
return await request(ctx, redirectURL, redirectOptions);
|
|
}
|
|
return response;
|
|
}
|
|
export async function getImageMetaData(ctx, filePath) {
|
|
if (!ctx.options.imageMetadataGetter) {
|
|
throw new ZaloApiMissingImageMetadataGetter();
|
|
}
|
|
const imageData = await ctx.options.imageMetadataGetter(filePath);
|
|
if (!imageData) {
|
|
throw new ZaloApiError("Failed to get image metadata");
|
|
}
|
|
const fileName = filePath.split("/").pop();
|
|
return {
|
|
fileName,
|
|
totalSize: imageData.size,
|
|
width: imageData.width,
|
|
height: imageData.height,
|
|
};
|
|
}
|
|
export async function getFileSize(filePath) {
|
|
return fs.promises.stat(filePath).then((s) => s.size);
|
|
}
|
|
export async function getGifMetaData(ctx, filePath) {
|
|
if (!ctx.options.imageMetadataGetter) {
|
|
throw new ZaloApiMissingImageMetadataGetter();
|
|
}
|
|
const gifData = await ctx.options.imageMetadataGetter(filePath);
|
|
if (!gifData) {
|
|
throw new ZaloApiError("Failed to get gif metadata");
|
|
}
|
|
const fileName = path.basename(filePath);
|
|
return {
|
|
fileName,
|
|
totalSize: gifData.size,
|
|
width: gifData.width,
|
|
height: gifData.height,
|
|
};
|
|
}
|
|
export async function decodeEventData(parsed, cipherKey) {
|
|
if (typeof parsed.data !== "string")
|
|
throw new ZaloApiError(`Invalid data, expected string but got ${typeof parsed.data}`);
|
|
if (typeof parsed.encrypt !== "number")
|
|
throw new ZaloApiError(`Invalid encrypt type, expected number but got ${typeof parsed.encrypt}`);
|
|
if (parsed.encrypt < 0 || parsed.encrypt > 3)
|
|
throw new ZaloApiError(`Invalid encrypt type, expected 0-3 but got ${parsed.encrypt}`);
|
|
const rawData = parsed.data;
|
|
const encryptType = parsed.encrypt;
|
|
if (encryptType === 0)
|
|
return JSON.parse(rawData);
|
|
const decodedBuffer = decodeBase64ToBuffer(encryptType === 1 ? rawData : decodeURIComponent(rawData));
|
|
let decryptedBuffer = decodedBuffer;
|
|
if (encryptType !== 1) {
|
|
if (cipherKey && decodedBuffer.length >= 48) {
|
|
const algorithm = {
|
|
name: "AES-GCM",
|
|
iv: decodedBuffer.subarray(0, 16),
|
|
tagLength: 128,
|
|
additionalData: decodedBuffer.subarray(16, 32),
|
|
};
|
|
const dataSource = decodedBuffer.subarray(32);
|
|
const cryptoKey = await crypto.subtle.importKey("raw", decodeBase64ToBuffer(cipherKey), algorithm, false, [
|
|
"decrypt",
|
|
]);
|
|
decryptedBuffer = await crypto.subtle.decrypt(algorithm, cryptoKey, dataSource);
|
|
}
|
|
else {
|
|
throw new ZaloApiError("Invalid data length or missing cipher key");
|
|
}
|
|
}
|
|
const decompressedBuffer = encryptType === 3 ? new Uint8Array(decryptedBuffer) : pako.inflate(decryptedBuffer);
|
|
const decodedData = decodeUnit8Array(decompressedBuffer);
|
|
if (!decodedData)
|
|
return;
|
|
return JSONBig.parse(decodedData);
|
|
}
|
|
export async function getMd5LargeFileObject(source, fileSize) {
|
|
const buffer = typeof source == "string" ? await fs.promises.readFile(source) : source.data;
|
|
return new Promise((resolve) => {
|
|
let currentChunk = 0;
|
|
const chunkSize = 2097152, // Read in chunks of 2MB
|
|
chunks = Math.ceil(fileSize / chunkSize), spark = new SparkMD5.ArrayBuffer();
|
|
function loadNext() {
|
|
const start = currentChunk * chunkSize, end = start + chunkSize >= fileSize ? fileSize : start + chunkSize;
|
|
spark.append(new Uint8Array(buffer.subarray(start, end)).buffer);
|
|
currentChunk++;
|
|
if (currentChunk < chunks) {
|
|
loadNext();
|
|
}
|
|
else {
|
|
resolve({
|
|
currentChunk,
|
|
data: spark.end(),
|
|
});
|
|
}
|
|
}
|
|
loadNext();
|
|
});
|
|
}
|
|
export const logger = (ctx) => ({
|
|
verbose: (...args) => {
|
|
if (ctx.options.logging)
|
|
console.log("\x1b[35m🚀 VERBOSE\x1b[0m", ...args);
|
|
},
|
|
info: (...args) => {
|
|
if (ctx.options.logging)
|
|
console.log("\x1b[34mINFO\x1b[0m", ...args);
|
|
},
|
|
warn: (...args) => {
|
|
if (ctx.options.logging)
|
|
console.log("\x1b[33mWARN\x1b[0m", ...args);
|
|
},
|
|
error: (...args) => {
|
|
if (ctx.options.logging)
|
|
console.log("\x1b[31mERROR\x1b[0m", ...args);
|
|
},
|
|
success: (...args) => {
|
|
if (ctx.options.logging)
|
|
console.log("\x1b[32mSUCCESS\x1b[0m", ...args);
|
|
},
|
|
timestamp: (...args) => {
|
|
const now = new Date().toISOString();
|
|
if (ctx.options.logging)
|
|
console.log(`\x1b[90m[${now}]\x1b[0m`, ...args);
|
|
},
|
|
});
|
|
export function getClientMessageType(msgType) {
|
|
if (msgType === "webchat")
|
|
return 1;
|
|
if (msgType === "chat.voice")
|
|
return 31;
|
|
if (msgType === "chat.photo")
|
|
return 32;
|
|
if (msgType === "chat.sticker")
|
|
return 36;
|
|
if (msgType === "chat.doodle")
|
|
return 37;
|
|
if (msgType === "chat.recommended")
|
|
return 38;
|
|
if (msgType === "chat.link")
|
|
return 38; // don't know || if (msgType === "chat.link") return 1;
|
|
if (msgType === "chat.video.msg")
|
|
return 44; // not sure
|
|
if (msgType === "share.file")
|
|
return 46;
|
|
if (msgType === "chat.gif")
|
|
return 49;
|
|
if (msgType === "chat.location.new")
|
|
return 43;
|
|
return 1;
|
|
}
|
|
export function strPadLeft(e, t, n) {
|
|
const a = (e = "" + e).length;
|
|
return a === n ? e : a > n ? e.slice(-n) : t.repeat(n - a) + e;
|
|
}
|
|
export function formatTime(format, timestamp = Date.now()) {
|
|
const date = new Date(timestamp);
|
|
// using lib Intl
|
|
const options = {
|
|
year: "numeric",
|
|
month: "2-digit",
|
|
day: "2-digit",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
second: "2-digit",
|
|
// hour12: false, // true or false is same <(")
|
|
};
|
|
const formatted = new Intl.DateTimeFormat("vi-VN", options).format(date);
|
|
if (format.includes("%H") || format.includes("%d")) {
|
|
return format
|
|
.replace("%H", date.getHours().toString().padStart(2, "0"))
|
|
.replace("%M", date.getMinutes().toString().padStart(2, "0"))
|
|
.replace("%S", date.getSeconds().toString().padStart(2, "0"))
|
|
.replace("%d", date.getDate().toString().padStart(2, "0"))
|
|
.replace("%m", (date.getMonth() + 1).toString().padStart(2, "0"))
|
|
.replace("%Y", date.getFullYear().toString());
|
|
}
|
|
return formatted;
|
|
}
|
|
export function getFullTimeFromMillisecond(e) {
|
|
const t = new Date(e);
|
|
return (strPadLeft(t.getHours(), "0", 2) +
|
|
":" +
|
|
strPadLeft(t.getMinutes(), "0", 2) +
|
|
" " +
|
|
strPadLeft(t.getDate(), "0", 2) +
|
|
"/" +
|
|
strPadLeft(t.getMonth() + 1, "0", 2) +
|
|
"/" +
|
|
t.getFullYear());
|
|
}
|
|
export function getFileExtension(e) {
|
|
return path.extname(e).slice(1);
|
|
}
|
|
export function getFileName(e) {
|
|
return path.basename(e);
|
|
}
|
|
export function removeUndefinedKeys(e) {
|
|
for (const t in e)
|
|
if (e[t] === undefined)
|
|
delete e[t];
|
|
return e;
|
|
}
|
|
export function getGroupEventType(act) {
|
|
if (act == "join_request")
|
|
return GroupEventType.JOIN_REQUEST;
|
|
if (act == "join")
|
|
return GroupEventType.JOIN;
|
|
if (act == "leave")
|
|
return GroupEventType.LEAVE;
|
|
if (act == "remove_member")
|
|
return GroupEventType.REMOVE_MEMBER;
|
|
if (act == "block_member")
|
|
return GroupEventType.BLOCK_MEMBER;
|
|
if (act == "update_setting")
|
|
return GroupEventType.UPDATE_SETTING;
|
|
if (act == "update_avatar")
|
|
return GroupEventType.UPDATE_AVATAR;
|
|
if (act == "update")
|
|
return GroupEventType.UPDATE;
|
|
if (act == "new_link")
|
|
return GroupEventType.NEW_LINK;
|
|
if (act == "add_admin")
|
|
return GroupEventType.ADD_ADMIN;
|
|
if (act == "remove_admin")
|
|
return GroupEventType.REMOVE_ADMIN;
|
|
if (act == "new_pin_topic")
|
|
return GroupEventType.NEW_PIN_TOPIC;
|
|
if (act == "update_pin_topic")
|
|
return GroupEventType.UPDATE_PIN_TOPIC;
|
|
if (act == "update_topic")
|
|
return GroupEventType.UPDATE_TOPIC;
|
|
if (act == "update_board")
|
|
return GroupEventType.UPDATE_BOARD;
|
|
if (act == "remove_board")
|
|
return GroupEventType.REMOVE_BOARD;
|
|
if (act == "reorder_pin_topic")
|
|
return GroupEventType.REORDER_PIN_TOPIC;
|
|
if (act == "unpin_topic")
|
|
return GroupEventType.UNPIN_TOPIC;
|
|
if (act == "remove_topic")
|
|
return GroupEventType.REMOVE_TOPIC;
|
|
if (act == "accept_remind")
|
|
return GroupEventType.ACCEPT_REMIND;
|
|
if (act == "reject_remind")
|
|
return GroupEventType.REJECT_REMIND;
|
|
if (act == "remind_topic")
|
|
return GroupEventType.REMIND_TOPIC;
|
|
return GroupEventType.UNKNOWN;
|
|
}
|
|
export function getFriendEventType(act) {
|
|
if (act == "add")
|
|
return FriendEventType.ADD;
|
|
if (act == "remove")
|
|
return FriendEventType.REMOVE;
|
|
if (act == "block")
|
|
return FriendEventType.BLOCK;
|
|
if (act == "unblock")
|
|
return FriendEventType.UNBLOCK;
|
|
if (act == "block_call")
|
|
return FriendEventType.BLOCK_CALL;
|
|
if (act == "unblock_call")
|
|
return FriendEventType.UNBLOCK_CALL;
|
|
if (act == "req_v2")
|
|
return FriendEventType.REQUEST;
|
|
if (act == "reject")
|
|
return FriendEventType.REJECT_REQUEST;
|
|
if (act == "undo_req")
|
|
return FriendEventType.UNDO_REQUEST;
|
|
if (act == "seen_fr_req")
|
|
return FriendEventType.SEEN_FRIEND_REQUEST;
|
|
if (act == "pin_unpin")
|
|
return FriendEventType.PIN_UNPIN;
|
|
if (act == "pin_create")
|
|
return FriendEventType.PIN_CREATE;
|
|
return FriendEventType.UNKNOWN;
|
|
}
|
|
export async function handleZaloResponse(ctx, response, isEncrypted = true) {
|
|
const result = {
|
|
data: null,
|
|
error: null,
|
|
};
|
|
if (!response.ok) {
|
|
result.error = {
|
|
message: "Request failed with status code " + response.status,
|
|
};
|
|
return result;
|
|
}
|
|
try {
|
|
const jsonData = await response.json();
|
|
if (jsonData.error_code != 0) {
|
|
result.error = {
|
|
message: jsonData.error_message,
|
|
code: jsonData.error_code,
|
|
};
|
|
return result;
|
|
}
|
|
const decodedData = isEncrypted ? JSON.parse(decodeAES(ctx.secretKey, jsonData.data)) : jsonData;
|
|
if (decodedData.error_code != 0) {
|
|
result.error = {
|
|
message: decodedData.error_message,
|
|
code: decodedData.error_code,
|
|
};
|
|
return result;
|
|
}
|
|
result.data = decodedData.data;
|
|
}
|
|
catch (error) {
|
|
logger(ctx).error("Failed to parse response data:", error);
|
|
result.error = {
|
|
message: "Failed to parse response data",
|
|
};
|
|
}
|
|
return result;
|
|
}
|
|
export async function resolveResponse(ctx, res, cb, isEncrypted) {
|
|
const result = await handleZaloResponse(ctx, res, isEncrypted);
|
|
if (result.error)
|
|
throw new ZaloApiError(result.error.message, result.error.code);
|
|
if (cb)
|
|
return cb(result);
|
|
return result.data;
|
|
}
|
|
export function apiFactory() {
|
|
return (callback) => {
|
|
return (ctx, api) => {
|
|
if (!isContextSession(ctx))
|
|
throw new ZaloApiError("Invalid context " + JSON.stringify(ctx, null, 2));
|
|
const utils = {
|
|
makeURL(baseURL, params, apiVersion) {
|
|
return makeURL(ctx, baseURL, params, apiVersion);
|
|
},
|
|
encodeAES(data, t) {
|
|
return encodeAES(ctx.secretKey, data, t);
|
|
},
|
|
request(url, options, raw) {
|
|
return request(ctx, url, options, raw);
|
|
},
|
|
logger: logger(ctx),
|
|
resolve: (res, cb, isEncrypted) => resolveResponse(ctx, res, cb, isEncrypted),
|
|
};
|
|
return callback(api, ctx, utils);
|
|
};
|
|
};
|
|
}
|
|
export function generateZaloUUID(userAgent) {
|
|
return crypto.randomUUID() + "-" + cryptojs.MD5(userAgent).toString();
|
|
}
|
|
/**
|
|
* Encrypts a 4-digit PIN to a 32-character hex string
|
|
* @param pin 4-digit PIN number
|
|
* @returns 32-character hex string
|
|
*/
|
|
export function encryptPin(pin) {
|
|
return crypto.createHash("md5").update(pin).digest("hex");
|
|
}
|
|
/**
|
|
* Decrypts a 32-character hex string back to 4-digit PIN
|
|
* Note: This is a one-way hash, so we can only verify if a PIN matches the hash
|
|
* @param encryptedPin 32-character hex string
|
|
* @param pin 4-digit PIN to verify
|
|
* @returns true if the PIN matches the hash
|
|
*
|
|
* @example
|
|
* const encryptedPin = (await api.getHiddenConversations()).pin;
|
|
* checking pin created..
|
|
* const isValid = validatePin(encryptedPin, "1234"); // true if pin created is 1234
|
|
* const isInvalid = validatePin(encryptedPin, "5678"); // false if not pin created is 5678
|
|
*/
|
|
export function validatePin(encryptedPin, pin) {
|
|
const hash = crypto.createHash("md5").update(pin).digest("hex");
|
|
return hash === encryptedPin;
|
|
}
|
|
/**
|
|
* Converts a hex color code to a negative color number used by Zalo API
|
|
* @param hex Hex color code (e.g. '#00FF00' or '00FF00')
|
|
* @returns Negative color number (e.g. -16711936)
|
|
*
|
|
* @example
|
|
* const negativeColor = hexToNegativeColor('#00FF00'); // Result: -16711936
|
|
*/
|
|
export function hexToNegativeColor(hex) {
|
|
if (!hex.startsWith("#")) {
|
|
hex = "#" + hex;
|
|
}
|
|
// rgb no alpha
|
|
// const decimal = parseInt(hex.slice(1), 16);
|
|
// return decimal - 4294967296;
|
|
// rgb with alpha
|
|
let hexValue = hex.slice(1);
|
|
if (hexValue.length === 6) {
|
|
hexValue = "FF" + hexValue;
|
|
}
|
|
const decimal = parseInt(hexValue, 16);
|
|
return decimal > 0x7fffffff ? decimal - 4294967296 : decimal;
|
|
}
|
|
/**
|
|
* Converts a negative color number from Zalo API to hex color code
|
|
* @param negativeColor Negative color number (e.g. -16711936)
|
|
* @returns Hex color code (e.g. '#00FF00')
|
|
*
|
|
* @example
|
|
* const hexColor = negativeColorToHex(-16711936); // Result: '#00FF00'
|
|
*/
|
|
export function negativeColorToHex(negativeColor) {
|
|
// Add 2^32 to get positive number
|
|
const positiveColor = negativeColor + 4294967296;
|
|
// return "#" + positiveColor.toString(16).padStart(6, "0"); // rgb no alpha
|
|
return "#" + positiveColor.toString(16).slice(-6).padStart(6, "0"); // rgb with alpha
|
|
}
|