691 lines
24 KiB
JavaScript
691 lines
24 KiB
JavaScript
'use strict';
|
|
|
|
var cryptojs = require('crypto-js');
|
|
var crypto = require('node:crypto');
|
|
var fs = require('node:fs');
|
|
var path = require('node:path');
|
|
var pako = require('pako');
|
|
var SparkMD5 = require('spark-md5');
|
|
var toughCookie = require('tough-cookie');
|
|
var JSONBig = require('json-bigint');
|
|
var context = require('./context.cjs');
|
|
var ZaloApiError = require('./Errors/ZaloApiError.cjs');
|
|
var ZaloApiMissingImageMetadataGetter = require('./Errors/ZaloApiMissingImageMetadataGetter.cjs');
|
|
var FriendEvent = require('./models/FriendEvent.cjs');
|
|
var GroupEvent = require('./models/GroupEvent.cjs');
|
|
|
|
const isBun = typeof Bun !== "undefined";
|
|
function hasOwn(obj, key) {
|
|
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
}
|
|
/**
|
|
* Get signed key for API requests.
|
|
*
|
|
* @param type
|
|
* @param params
|
|
* @returns MD5 hash
|
|
*
|
|
*/
|
|
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
|
|
*
|
|
*/
|
|
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();
|
|
}
|
|
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.ZaloApiError("getEncryptKey: didn't create encryptKey yet");
|
|
return this.encryptKey;
|
|
}
|
|
createZcid(type, imei, firstLaunchTime) {
|
|
if (!type || !imei || !firstLaunchTime)
|
|
throw new ZaloApiError.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.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;
|
|
}
|
|
}
|
|
}
|
|
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);
|
|
}
|
|
function decodeBase64ToBuffer(data) {
|
|
return Buffer.from(data, "base64");
|
|
}
|
|
function decodeUnit8Array(data) {
|
|
try {
|
|
return new TextDecoder().decode(data);
|
|
}
|
|
catch (_a) {
|
|
return null;
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
async function getDefaultHeaders(ctx, origin = "https://chat.zalo.me") {
|
|
if (!ctx.cookie)
|
|
throw new ZaloApiError.ZaloApiError("Cookie is not available");
|
|
if (!ctx.userAgent)
|
|
throw new ZaloApiError.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,
|
|
};
|
|
}
|
|
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;
|
|
}
|
|
async function getImageMetaData(ctx, filePath) {
|
|
if (!ctx.options.imageMetadataGetter) {
|
|
throw new ZaloApiMissingImageMetadataGetter.ZaloApiMissingImageMetadataGetter();
|
|
}
|
|
const imageData = await ctx.options.imageMetadataGetter(filePath);
|
|
if (!imageData) {
|
|
throw new ZaloApiError.ZaloApiError("Failed to get image metadata");
|
|
}
|
|
const fileName = filePath.split("/").pop();
|
|
return {
|
|
fileName,
|
|
totalSize: imageData.size,
|
|
width: imageData.width,
|
|
height: imageData.height,
|
|
};
|
|
}
|
|
async function getFileSize(filePath) {
|
|
return fs.promises.stat(filePath).then((s) => s.size);
|
|
}
|
|
async function getGifMetaData(ctx, filePath) {
|
|
if (!ctx.options.imageMetadataGetter) {
|
|
throw new ZaloApiMissingImageMetadataGetter.ZaloApiMissingImageMetadataGetter();
|
|
}
|
|
const gifData = await ctx.options.imageMetadataGetter(filePath);
|
|
if (!gifData) {
|
|
throw new ZaloApiError.ZaloApiError("Failed to get gif metadata");
|
|
}
|
|
const fileName = path.basename(filePath);
|
|
return {
|
|
fileName,
|
|
totalSize: gifData.size,
|
|
width: gifData.width,
|
|
height: gifData.height,
|
|
};
|
|
}
|
|
async function decodeEventData(parsed, cipherKey) {
|
|
if (typeof parsed.data !== "string")
|
|
throw new ZaloApiError.ZaloApiError(`Invalid data, expected string but got ${typeof parsed.data}`);
|
|
if (typeof parsed.encrypt !== "number")
|
|
throw new ZaloApiError.ZaloApiError(`Invalid encrypt type, expected number but got ${typeof parsed.encrypt}`);
|
|
if (parsed.encrypt < 0 || parsed.encrypt > 3)
|
|
throw new ZaloApiError.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.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);
|
|
}
|
|
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();
|
|
});
|
|
}
|
|
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);
|
|
},
|
|
});
|
|
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;
|
|
}
|
|
function strPadLeft(e, t, n) {
|
|
const a = (e = "" + e).length;
|
|
return a === n ? e : a > n ? e.slice(-n) : t.repeat(n - a) + e;
|
|
}
|
|
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;
|
|
}
|
|
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());
|
|
}
|
|
function getFileExtension(e) {
|
|
return path.extname(e).slice(1);
|
|
}
|
|
function getFileName(e) {
|
|
return path.basename(e);
|
|
}
|
|
function removeUndefinedKeys(e) {
|
|
for (const t in e)
|
|
if (e[t] === undefined)
|
|
delete e[t];
|
|
return e;
|
|
}
|
|
function getGroupEventType(act) {
|
|
if (act == "join_request")
|
|
return GroupEvent.GroupEventType.JOIN_REQUEST;
|
|
if (act == "join")
|
|
return GroupEvent.GroupEventType.JOIN;
|
|
if (act == "leave")
|
|
return GroupEvent.GroupEventType.LEAVE;
|
|
if (act == "remove_member")
|
|
return GroupEvent.GroupEventType.REMOVE_MEMBER;
|
|
if (act == "block_member")
|
|
return GroupEvent.GroupEventType.BLOCK_MEMBER;
|
|
if (act == "update_setting")
|
|
return GroupEvent.GroupEventType.UPDATE_SETTING;
|
|
if (act == "update_avatar")
|
|
return GroupEvent.GroupEventType.UPDATE_AVATAR;
|
|
if (act == "update")
|
|
return GroupEvent.GroupEventType.UPDATE;
|
|
if (act == "new_link")
|
|
return GroupEvent.GroupEventType.NEW_LINK;
|
|
if (act == "add_admin")
|
|
return GroupEvent.GroupEventType.ADD_ADMIN;
|
|
if (act == "remove_admin")
|
|
return GroupEvent.GroupEventType.REMOVE_ADMIN;
|
|
if (act == "new_pin_topic")
|
|
return GroupEvent.GroupEventType.NEW_PIN_TOPIC;
|
|
if (act == "update_pin_topic")
|
|
return GroupEvent.GroupEventType.UPDATE_PIN_TOPIC;
|
|
if (act == "update_topic")
|
|
return GroupEvent.GroupEventType.UPDATE_TOPIC;
|
|
if (act == "update_board")
|
|
return GroupEvent.GroupEventType.UPDATE_BOARD;
|
|
if (act == "remove_board")
|
|
return GroupEvent.GroupEventType.REMOVE_BOARD;
|
|
if (act == "reorder_pin_topic")
|
|
return GroupEvent.GroupEventType.REORDER_PIN_TOPIC;
|
|
if (act == "unpin_topic")
|
|
return GroupEvent.GroupEventType.UNPIN_TOPIC;
|
|
if (act == "remove_topic")
|
|
return GroupEvent.GroupEventType.REMOVE_TOPIC;
|
|
if (act == "accept_remind")
|
|
return GroupEvent.GroupEventType.ACCEPT_REMIND;
|
|
if (act == "reject_remind")
|
|
return GroupEvent.GroupEventType.REJECT_REMIND;
|
|
if (act == "remind_topic")
|
|
return GroupEvent.GroupEventType.REMIND_TOPIC;
|
|
return GroupEvent.GroupEventType.UNKNOWN;
|
|
}
|
|
function getFriendEventType(act) {
|
|
if (act == "add")
|
|
return FriendEvent.FriendEventType.ADD;
|
|
if (act == "remove")
|
|
return FriendEvent.FriendEventType.REMOVE;
|
|
if (act == "block")
|
|
return FriendEvent.FriendEventType.BLOCK;
|
|
if (act == "unblock")
|
|
return FriendEvent.FriendEventType.UNBLOCK;
|
|
if (act == "block_call")
|
|
return FriendEvent.FriendEventType.BLOCK_CALL;
|
|
if (act == "unblock_call")
|
|
return FriendEvent.FriendEventType.UNBLOCK_CALL;
|
|
if (act == "req_v2")
|
|
return FriendEvent.FriendEventType.REQUEST;
|
|
if (act == "reject")
|
|
return FriendEvent.FriendEventType.REJECT_REQUEST;
|
|
if (act == "undo_req")
|
|
return FriendEvent.FriendEventType.UNDO_REQUEST;
|
|
if (act == "seen_fr_req")
|
|
return FriendEvent.FriendEventType.SEEN_FRIEND_REQUEST;
|
|
if (act == "pin_unpin")
|
|
return FriendEvent.FriendEventType.PIN_UNPIN;
|
|
if (act == "pin_create")
|
|
return FriendEvent.FriendEventType.PIN_CREATE;
|
|
return FriendEvent.FriendEventType.UNKNOWN;
|
|
}
|
|
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;
|
|
}
|
|
async function resolveResponse(ctx, res, cb, isEncrypted) {
|
|
const result = await handleZaloResponse(ctx, res, isEncrypted);
|
|
if (result.error)
|
|
throw new ZaloApiError.ZaloApiError(result.error.message, result.error.code);
|
|
if (cb)
|
|
return cb(result);
|
|
return result.data;
|
|
}
|
|
function apiFactory() {
|
|
return (callback) => {
|
|
return (ctx, api) => {
|
|
if (!context.isContextSession(ctx))
|
|
throw new ZaloApiError.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);
|
|
};
|
|
};
|
|
}
|
|
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
|
|
*/
|
|
function encryptPin(pin) {
|
|
return crypto.createHash("md5").update(pin).digest("hex");
|
|
}
|
|
|
|
exports.ParamsEncryptor = ParamsEncryptor;
|
|
exports.apiFactory = apiFactory;
|
|
exports.decodeAES = decodeAES;
|
|
exports.decodeBase64ToBuffer = decodeBase64ToBuffer;
|
|
exports.decodeEventData = decodeEventData;
|
|
exports.decodeUnit8Array = decodeUnit8Array;
|
|
exports.decryptResp = decryptResp;
|
|
exports.encodeAES = encodeAES;
|
|
exports.encryptPin = encryptPin;
|
|
exports.formatTime = formatTime;
|
|
exports.generateZaloUUID = generateZaloUUID;
|
|
exports.getClientMessageType = getClientMessageType;
|
|
exports.getDefaultHeaders = getDefaultHeaders;
|
|
exports.getFileExtension = getFileExtension;
|
|
exports.getFileName = getFileName;
|
|
exports.getFileSize = getFileSize;
|
|
exports.getFriendEventType = getFriendEventType;
|
|
exports.getFullTimeFromMillisecond = getFullTimeFromMillisecond;
|
|
exports.getGifMetaData = getGifMetaData;
|
|
exports.getGroupEventType = getGroupEventType;
|
|
exports.getImageMetaData = getImageMetaData;
|
|
exports.getMd5LargeFileObject = getMd5LargeFileObject;
|
|
exports.getSignKey = getSignKey;
|
|
exports.handleZaloResponse = handleZaloResponse;
|
|
exports.hasOwn = hasOwn;
|
|
exports.isBun = isBun;
|
|
exports.logger = logger;
|
|
exports.makeURL = makeURL;
|
|
exports.removeUndefinedKeys = removeUndefinedKeys;
|
|
exports.request = request;
|
|
exports.resolveResponse = resolveResponse;
|
|
exports.strPadLeft = strPadLeft;
|