refactor(zalouser): extract policy and message helpers

This commit is contained in:
Peter Steinberger
2026-03-02 22:16:37 +00:00
parent 7253e91300
commit 19fafed11d
12 changed files with 678 additions and 353 deletions

View File

@@ -33,6 +33,8 @@ import {
type ResolvedZalouserAccount,
} from "./accounts.js";
import { ZalouserConfigSchema } from "./config-schema.js";
import { buildZalouserGroupCandidates, findZalouserGroupEntry } from "./group-policy.js";
import { resolveZalouserReactionMessageIds } from "./message-sid.js";
import { zalouserOnboardingAdapter } from "./onboarding.js";
import { probeZalouser } from "./probe.js";
import { sendMessageZalouser, sendReactionZalouser } from "./send.js";
@@ -122,18 +124,15 @@ function resolveZalouserGroupToolPolicy(
accountId: params.accountId ?? undefined,
});
const groups = account.config.groups ?? {};
const groupId = params.groupId?.trim();
const groupChannel = params.groupChannel?.trim();
const candidates = [groupId, groupChannel, "*"].filter((value): value is string =>
Boolean(value),
const entry = findZalouserGroupEntry(
groups,
buildZalouserGroupCandidates({
groupId: params.groupId,
groupChannel: params.groupChannel,
includeWildcard: true,
}),
);
for (const key of candidates) {
const entry = groups[key];
if (entry?.tools) {
return entry.tools;
}
}
return undefined;
return entry?.tools;
}
function resolveZalouserRequireMention(params: ChannelGroupContext): boolean {
@@ -142,52 +141,20 @@ function resolveZalouserRequireMention(params: ChannelGroupContext): boolean {
accountId: params.accountId ?? undefined,
});
const groups = account.config.groups ?? {};
const candidates = [params.groupId?.trim(), params.groupChannel?.trim()].filter(
(value): value is string => Boolean(value),
const entry = findZalouserGroupEntry(
groups,
buildZalouserGroupCandidates({
groupId: params.groupId,
groupChannel: params.groupChannel,
includeWildcard: true,
}),
);
for (const candidate of candidates) {
const entry = groups[candidate];
if (typeof entry?.requireMention === "boolean") {
return entry.requireMention;
}
}
if (typeof groups["*"]?.requireMention === "boolean") {
return groups["*"].requireMention;
if (typeof entry?.requireMention === "boolean") {
return entry.requireMention;
}
return true;
}
function resolveZalouserReactionMessageIds(params: {
messageId?: string;
cliMsgId?: string;
currentMessageId?: string | number;
}): { msgId: string; cliMsgId: string } | null {
const explicitMessageId = params.messageId?.trim() ?? "";
const explicitCliMsgId = params.cliMsgId?.trim() ?? "";
if (explicitMessageId && explicitCliMsgId) {
return { msgId: explicitMessageId, cliMsgId: explicitCliMsgId };
}
const current =
typeof params.currentMessageId === "number" ? String(params.currentMessageId) : "";
const currentRaw =
typeof params.currentMessageId === "string" ? params.currentMessageId.trim() : current;
if (!currentRaw) {
return null;
}
const [msgIdPart, cliMsgIdPart] = currentRaw.split(":").map((value) => value.trim());
if (msgIdPart && cliMsgIdPart) {
return { msgId: msgIdPart, cliMsgId: cliMsgIdPart };
}
if (explicitMessageId && !explicitCliMsgId) {
return { msgId: explicitMessageId, cliMsgId: currentRaw };
}
if (!explicitMessageId && explicitCliMsgId) {
return { msgId: currentRaw, cliMsgId: explicitCliMsgId };
}
return { msgId: currentRaw, cliMsgId: currentRaw };
}
const zalouserMessageActions: ChannelMessageActionAdapter = {
listActions: ({ cfg }) => {
const accounts = listZalouserAccountIds(cfg)

View File

@@ -0,0 +1,49 @@
import { describe, expect, it } from "vitest";
import {
buildZalouserGroupCandidates,
findZalouserGroupEntry,
isZalouserGroupEntryAllowed,
normalizeZalouserGroupSlug,
} from "./group-policy.js";
describe("zalouser group policy helpers", () => {
it("normalizes group slug names", () => {
expect(normalizeZalouserGroupSlug(" Team Alpha ")).toBe("team-alpha");
expect(normalizeZalouserGroupSlug("#Roadmap Updates")).toBe("roadmap-updates");
});
it("builds ordered candidates with optional aliases", () => {
expect(
buildZalouserGroupCandidates({
groupId: "123",
groupChannel: "chan-1",
groupName: "Team Alpha",
includeGroupIdAlias: true,
}),
).toEqual(["123", "group:123", "chan-1", "Team Alpha", "team-alpha", "*"]);
});
it("finds the first matching group entry", () => {
const groups = {
"group:123": { allow: true },
"team-alpha": { requireMention: false },
"*": { requireMention: true },
};
const entry = findZalouserGroupEntry(
groups,
buildZalouserGroupCandidates({
groupId: "123",
groupName: "Team Alpha",
includeGroupIdAlias: true,
}),
);
expect(entry).toEqual({ allow: true });
});
it("evaluates allow/enable flags", () => {
expect(isZalouserGroupEntryAllowed({ allow: true, enabled: true })).toBe(true);
expect(isZalouserGroupEntryAllowed({ allow: false })).toBe(false);
expect(isZalouserGroupEntryAllowed({ enabled: false })).toBe(false);
expect(isZalouserGroupEntryAllowed(undefined)).toBe(false);
});
});

View File

@@ -0,0 +1,78 @@
import type { ZalouserGroupConfig } from "./types.js";
type ZalouserGroups = Record<string, ZalouserGroupConfig>;
function toGroupCandidate(value?: string | null): string {
return value?.trim() ?? "";
}
export function normalizeZalouserGroupSlug(raw?: string | null): string {
const trimmed = raw?.trim().toLowerCase() ?? "";
if (!trimmed) {
return "";
}
return trimmed
.replace(/^#/, "")
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
}
export function buildZalouserGroupCandidates(params: {
groupId?: string | null;
groupChannel?: string | null;
groupName?: string | null;
includeGroupIdAlias?: boolean;
includeWildcard?: boolean;
}): string[] {
const seen = new Set<string>();
const out: string[] = [];
const push = (value?: string | null) => {
const normalized = toGroupCandidate(value);
if (!normalized || seen.has(normalized)) {
return;
}
seen.add(normalized);
out.push(normalized);
};
const groupId = toGroupCandidate(params.groupId);
const groupChannel = toGroupCandidate(params.groupChannel);
const groupName = toGroupCandidate(params.groupName);
push(groupId);
if (params.includeGroupIdAlias === true && groupId) {
push(`group:${groupId}`);
}
push(groupChannel);
push(groupName);
if (groupName) {
push(normalizeZalouserGroupSlug(groupName));
}
if (params.includeWildcard !== false) {
push("*");
}
return out;
}
export function findZalouserGroupEntry(
groups: ZalouserGroups | undefined,
candidates: string[],
): ZalouserGroupConfig | undefined {
if (!groups) {
return undefined;
}
for (const candidate of candidates) {
const entry = groups[candidate];
if (entry) {
return entry;
}
}
return undefined;
}
export function isZalouserGroupEntryAllowed(entry: ZalouserGroupConfig | undefined): boolean {
if (!entry) {
return false;
}
return entry.allow !== false && entry.enabled !== false;
}

View File

@@ -0,0 +1,66 @@
import { describe, expect, it } from "vitest";
import {
formatZalouserMessageSidFull,
parseZalouserMessageSidFull,
resolveZalouserMessageSid,
resolveZalouserReactionMessageIds,
} from "./message-sid.js";
describe("zalouser message sid helpers", () => {
it("parses MessageSidFull pairs", () => {
expect(parseZalouserMessageSidFull("111:222")).toEqual({
msgId: "111",
cliMsgId: "222",
});
expect(parseZalouserMessageSidFull("111")).toBeNull();
expect(parseZalouserMessageSidFull(undefined)).toBeNull();
});
it("resolves reaction ids from explicit params first", () => {
expect(
resolveZalouserReactionMessageIds({
messageId: "m-1",
cliMsgId: "c-1",
currentMessageId: "x:y",
}),
).toEqual({
msgId: "m-1",
cliMsgId: "c-1",
});
});
it("resolves reaction ids from current message sid full", () => {
expect(
resolveZalouserReactionMessageIds({
currentMessageId: "m-2:c-2",
}),
).toEqual({
msgId: "m-2",
cliMsgId: "c-2",
});
});
it("falls back to duplicated current id when no pair is available", () => {
expect(
resolveZalouserReactionMessageIds({
currentMessageId: "solo",
}),
).toEqual({
msgId: "solo",
cliMsgId: "solo",
});
});
it("formats message sid fields for context payload", () => {
expect(formatZalouserMessageSidFull({ msgId: "1", cliMsgId: "2" })).toBe("1:2");
expect(formatZalouserMessageSidFull({ msgId: "1" })).toBe("1");
expect(formatZalouserMessageSidFull({ cliMsgId: "2" })).toBe("2");
expect(formatZalouserMessageSidFull({})).toBeUndefined();
});
it("resolves primary message sid with fallback timestamp", () => {
expect(resolveZalouserMessageSid({ msgId: "1", cliMsgId: "2", fallback: "t" })).toBe("1");
expect(resolveZalouserMessageSid({ cliMsgId: "2", fallback: "t" })).toBe("2");
expect(resolveZalouserMessageSid({ fallback: "t" })).toBe("t");
});
});

View File

@@ -0,0 +1,80 @@
function toMessageSidPart(value?: string | number | null): string {
if (typeof value === "string") {
return value.trim();
}
if (typeof value === "number" && Number.isFinite(value)) {
return String(Math.trunc(value));
}
return "";
}
export function parseZalouserMessageSidFull(
value?: string | number | null,
): { msgId: string; cliMsgId: string } | null {
const raw = toMessageSidPart(value);
if (!raw) {
return null;
}
const [msgIdPart, cliMsgIdPart] = raw.split(":").map((entry) => entry.trim());
if (!msgIdPart || !cliMsgIdPart) {
return null;
}
return { msgId: msgIdPart, cliMsgId: cliMsgIdPart };
}
export function resolveZalouserReactionMessageIds(params: {
messageId?: string;
cliMsgId?: string;
currentMessageId?: string | number;
}): { msgId: string; cliMsgId: string } | null {
const explicitMessageId = toMessageSidPart(params.messageId);
const explicitCliMsgId = toMessageSidPart(params.cliMsgId);
if (explicitMessageId && explicitCliMsgId) {
return { msgId: explicitMessageId, cliMsgId: explicitCliMsgId };
}
const parsedFromCurrent = parseZalouserMessageSidFull(params.currentMessageId);
if (parsedFromCurrent) {
return parsedFromCurrent;
}
const currentRaw = toMessageSidPart(params.currentMessageId);
if (!currentRaw) {
return null;
}
if (explicitMessageId && !explicitCliMsgId) {
return { msgId: explicitMessageId, cliMsgId: currentRaw };
}
if (!explicitMessageId && explicitCliMsgId) {
return { msgId: currentRaw, cliMsgId: explicitCliMsgId };
}
return { msgId: currentRaw, cliMsgId: currentRaw };
}
export function formatZalouserMessageSidFull(params: {
msgId?: string | null;
cliMsgId?: string | null;
}): string | undefined {
const msgId = toMessageSidPart(params.msgId);
const cliMsgId = toMessageSidPart(params.cliMsgId);
if (!msgId && !cliMsgId) {
return undefined;
}
if (msgId && cliMsgId) {
return `${msgId}:${cliMsgId}`;
}
return msgId || cliMsgId || undefined;
}
export function resolveZalouserMessageSid(params: {
msgId?: string | null;
cliMsgId?: string | null;
fallback?: string | null;
}): string | undefined {
const msgId = toMessageSidPart(params.msgId);
const cliMsgId = toMessageSidPart(params.cliMsgId);
if (msgId || cliMsgId) {
return msgId || cliMsgId;
}
return toMessageSidPart(params.fallback) || undefined;
}

View File

@@ -18,6 +18,12 @@ import {
summarizeMapping,
warnMissingProviderGroupPolicyFallbackOnce,
} from "openclaw/plugin-sdk";
import {
buildZalouserGroupCandidates,
findZalouserGroupEntry,
isZalouserGroupEntryAllowed,
} from "./group-policy.js";
import { formatZalouserMessageSidFull, resolveZalouserMessageSid } from "./message-sid.js";
import { getZalouserRuntime } from "./runtime.js";
import {
sendDeliveredZalouser,
@@ -87,17 +93,6 @@ function isSenderAllowed(senderId: string | undefined, allowFrom: string[]): boo
});
}
function normalizeGroupSlug(raw?: string | null): string {
const trimmed = raw?.trim().toLowerCase() ?? "";
if (!trimmed) {
return "";
}
return trimmed
.replace(/^#/, "")
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
}
function isGroupAllowed(params: {
groupId: string;
groupName?: string | null;
@@ -108,24 +103,16 @@ function isGroupAllowed(params: {
if (keys.length === 0) {
return false;
}
const candidates = [
params.groupId,
`group:${params.groupId}`,
params.groupName ?? "",
normalizeGroupSlug(params.groupName ?? ""),
].filter(Boolean);
for (const candidate of candidates) {
const entry = groups[candidate];
if (!entry) {
continue;
}
return entry.allow !== false && entry.enabled !== false;
}
const wildcard = groups["*"];
if (wildcard) {
return wildcard.allow !== false && wildcard.enabled !== false;
}
return false;
const entry = findZalouserGroupEntry(
groups,
buildZalouserGroupCandidates({
groupId: params.groupId,
groupName: params.groupName,
includeGroupIdAlias: true,
includeWildcard: true,
}),
);
return isZalouserGroupEntryAllowed(entry);
}
function resolveGroupRequireMention(params: {
@@ -133,21 +120,17 @@ function resolveGroupRequireMention(params: {
groupName?: string | null;
groups: Record<string, { allow?: boolean; enabled?: boolean; requireMention?: boolean }>;
}): boolean {
const groups = params.groups ?? {};
const candidates = [
params.groupId,
`group:${params.groupId}`,
params.groupName ?? "",
normalizeGroupSlug(params.groupName ?? ""),
].filter(Boolean);
for (const candidate of candidates) {
const entry = groups[candidate];
if (typeof entry?.requireMention === "boolean") {
return entry.requireMention;
}
}
if (typeof groups["*"]?.requireMention === "boolean") {
return groups["*"].requireMention;
const entry = findZalouserGroupEntry(
params.groups ?? {},
buildZalouserGroupCandidates({
groupId: params.groupId,
groupName: params.groupName,
includeGroupIdAlias: true,
includeWildcard: true,
}),
);
if (typeof entry?.requireMention === "boolean") {
return entry.requireMention;
}
return true;
}
@@ -419,11 +402,15 @@ async function processMessage(
CommandAuthorized: commandAuthorized,
Provider: "zalouser",
Surface: "zalouser",
MessageSid: message.msgId ?? message.cliMsgId ?? `${message.timestampMs}`,
MessageSidFull:
message.msgId && message.cliMsgId
? `${message.msgId}:${message.cliMsgId}`
: (message.msgId ?? message.cliMsgId ?? undefined),
MessageSid: resolveZalouserMessageSid({
msgId: message.msgId,
cliMsgId: message.cliMsgId,
fallback: `${message.timestampMs}`,
}),
MessageSidFull: formatZalouserMessageSidFull({
msgId: message.msgId,
cliMsgId: message.cliMsgId,
}),
OriginatingChannel: "zalouser",
OriginatingTo: `zalouser:${chatId}`,
});

View File

@@ -0,0 +1,19 @@
import { describe, expect, it } from "vitest";
import { normalizeZaloReactionIcon } from "./reaction.js";
describe("zalouser reaction alias normalization", () => {
it("maps common aliases", () => {
expect(normalizeZaloReactionIcon("like")).toBe("/-strong");
expect(normalizeZaloReactionIcon("👍")).toBe("/-strong");
expect(normalizeZaloReactionIcon("heart")).toBe("/-heart");
expect(normalizeZaloReactionIcon("😂")).toBe(":>");
});
it("defaults empty icon to like", () => {
expect(normalizeZaloReactionIcon("")).toBe("/-strong");
});
it("passes through unknown custom reactions", () => {
expect(normalizeZaloReactionIcon("/custom")).toBe("/custom");
});
});

View File

@@ -0,0 +1,29 @@
import { Reactions } from "./zca-client.js";
const REACTION_ALIAS_MAP = new Map<string, string>([
["like", Reactions.LIKE],
["👍", Reactions.LIKE],
[":+1:", Reactions.LIKE],
["heart", Reactions.HEART],
["❤️", Reactions.HEART],
["<3", Reactions.HEART],
["haha", Reactions.HAHA],
["laugh", Reactions.HAHA],
["😂", Reactions.HAHA],
["wow", Reactions.WOW],
["😮", Reactions.WOW],
["cry", Reactions.CRY],
["😢", Reactions.CRY],
["angry", Reactions.ANGRY],
["😡", Reactions.ANGRY],
]);
export function normalizeZaloReactionIcon(raw: string): string {
const trimmed = raw.trim();
if (!trimmed) {
return Reactions.LIKE;
}
return (
REACTION_ALIAS_MAP.get(trimmed.toLowerCase()) ?? REACTION_ALIAS_MAP.get(trimmed) ?? trimmed
);
}

View File

@@ -77,9 +77,9 @@ export type ZaloAuthStatus = {
message: string;
};
type ZalouserToolConfig = { allow?: string[]; deny?: string[] };
export type ZalouserToolConfig = { allow?: string[]; deny?: string[] };
type ZalouserGroupConfig = {
export type ZalouserGroupConfig = {
allow?: boolean;
enabled?: boolean;
requireMention?: boolean;

View File

@@ -4,18 +4,7 @@ import fsp from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk";
import {
LoginQRCallbackEventType,
Reactions,
ThreadType,
Zalo,
type API,
type Credentials,
type GroupInfo,
type LoginQRCallbackEvent,
type Message,
type User,
} from "zca-js";
import { normalizeZaloReactionIcon } from "./reaction.js";
import { getZalouserRuntime } from "./runtime.js";
import type {
ZaloAuthStatus,
@@ -29,6 +18,17 @@ import type {
ZcaFriend,
ZcaUserInfo,
} from "./types.js";
import {
LoginQRCallbackEventType,
ThreadType,
Zalo,
type API,
type Credentials,
type GroupInfo,
type LoginQRCallbackEvent,
type Message,
type User,
} from "./zca-client.js";
const API_LOGIN_TIMEOUT_MS = 20_000;
const QR_LOGIN_TTL_MS = 3 * 60_000;
@@ -36,6 +36,7 @@ const DEFAULT_QR_START_TIMEOUT_MS = 30_000;
const DEFAULT_QR_WAIT_TIMEOUT_MS = 120_000;
const GROUP_INFO_CHUNK_SIZE = 80;
const GROUP_CONTEXT_CACHE_TTL_MS = 5 * 60_000;
const GROUP_CONTEXT_CACHE_MAX_ENTRIES = 500;
const apiByProfile = new Map<string, API>();
const apiInitByProfile = new Map<string, Promise<API>>();
@@ -241,33 +242,6 @@ function buildEventMessage(data: Record<string, unknown>): ZaloEventMessage | un
};
}
function normalizeReactionIcon(raw: string): string {
const trimmed = raw.trim();
if (!trimmed) {
return Reactions.LIKE;
}
const lower = trimmed.toLowerCase();
if (lower === "like" || trimmed === "👍" || trimmed === ":+1:") {
return Reactions.LIKE;
}
if (lower === "heart" || trimmed === "❤️" || trimmed === "<3") {
return Reactions.HEART;
}
if (lower === "haha" || lower === "laugh" || trimmed === "😂") {
return Reactions.HAHA;
}
if (lower === "wow" || trimmed === "😮") {
return Reactions.WOW;
}
if (lower === "cry" || trimmed === "😢") {
return Reactions.CRY;
}
if (lower === "angry" || trimmed === "😡") {
return Reactions.ANGRY;
}
return trimmed;
}
function extractSendMessageId(result: unknown): string | undefined {
if (!result || typeof result !== "object") {
return undefined;
@@ -539,15 +513,39 @@ function readCachedGroupContext(profile: string, groupId: string): ZaloGroupCont
groupContextCache.delete(key);
return null;
}
// Bump recency so hot groups stay in cache when enforcing max entries.
groupContextCache.delete(key);
groupContextCache.set(key, cached);
return cached.value;
}
function trimGroupContextCache(now: number): void {
for (const [key, value] of groupContextCache) {
if (value.expiresAt > now) {
continue;
}
groupContextCache.delete(key);
}
while (groupContextCache.size > GROUP_CONTEXT_CACHE_MAX_ENTRIES) {
const oldestKey = groupContextCache.keys().next().value;
if (!oldestKey) {
break;
}
groupContextCache.delete(oldestKey);
}
}
function writeCachedGroupContext(profile: string, context: ZaloGroupContext): void {
const now = Date.now();
const key = makeGroupContextCacheKey(profile, context.groupId);
if (groupContextCache.has(key)) {
groupContextCache.delete(key);
}
groupContextCache.set(key, {
value: context,
expiresAt: Date.now() + GROUP_CONTEXT_CACHE_TTL_MS,
expiresAt: now + GROUP_CONTEXT_CACHE_TTL_MS,
});
trimGroupContextCache(now);
}
function clearCachedGroupContext(profile: string): void {
@@ -919,7 +917,7 @@ export async function sendZaloReaction(params: {
const type = params.isGroup ? ThreadType.Group : ThreadType.User;
const icon = params.remove
? { rType: -1, source: 6, icon: "" }
: normalizeReactionIcon(params.emoji);
: normalizeZaloReactionIcon(params.emoji);
await api.addReaction(icon, {
data: { msgId, cliMsgId },
threadId,

View File

@@ -0,0 +1,249 @@
import {
LoginQRCallbackEventType as LoginQRCallbackEventTypeRuntime,
Reactions as ReactionsRuntime,
ThreadType as ThreadTypeRuntime,
Zalo as ZaloRuntime,
} from "zca-js";
export const ThreadType = ThreadTypeRuntime as {
User: 0;
Group: 1;
};
export const LoginQRCallbackEventType = LoginQRCallbackEventTypeRuntime as {
QRCodeGenerated: 0;
QRCodeExpired: 1;
QRCodeScanned: 2;
QRCodeDeclined: 3;
GotLoginInfo: 4;
};
export const Reactions = ReactionsRuntime as Record<string, string> & {
HEART: string;
LIKE: string;
HAHA: string;
WOW: string;
CRY: string;
ANGRY: string;
NONE: string;
};
export type Credentials = {
imei: string;
cookie: unknown;
userAgent: string;
language?: string;
};
export type User = {
userId: string;
username: string;
displayName: string;
zaloName: string;
avatar: string;
};
export type GroupInfo = {
groupId: string;
name: string;
totalMember?: number;
memberIds?: unknown[];
currentMems?: Array<{
id?: unknown;
dName?: string;
zaloName?: string;
avatar?: string;
}>;
};
export type Message = {
type: number;
threadId: string;
isSelf: boolean;
data: Record<string, unknown>;
};
export type LoginQRCallbackEvent =
| {
type: 0;
data: {
code: string;
image: string;
};
actions: {
saveToFile: (qrPath?: string) => Promise<unknown>;
retry: () => unknown;
abort: () => unknown;
};
}
| {
type: 1;
data: null;
actions: {
retry: () => unknown;
abort: () => unknown;
};
}
| {
type: 2;
data: {
avatar: string;
display_name: string;
};
actions: {
retry: () => unknown;
abort: () => unknown;
};
}
| {
type: 3;
data: {
code: string;
};
actions: {
retry: () => unknown;
abort: () => unknown;
};
}
| {
type: 4;
data: {
cookie: unknown;
imei: string;
userAgent: string;
};
actions: null;
};
export type Listener = {
on(event: "message", callback: (message: Message) => void): void;
on(event: "error", callback: (error: unknown) => void): void;
on(event: "closed", callback: (code: number, reason: string) => void): void;
off(event: "message", callback: (message: Message) => void): void;
off(event: "error", callback: (error: unknown) => void): void;
off(event: "closed", callback: (code: number, reason: string) => void): void;
start(opts?: { retryOnClose?: boolean }): void;
stop(): void;
};
export type API = {
listener: Listener;
getContext(): {
imei: string;
userAgent: string;
language?: string;
};
getCookie(): {
toJSON(): {
cookies: unknown[];
};
};
fetchAccountInfo(): Promise<{ profile: User } | User>;
getAllFriends(): Promise<User[]>;
getOwnId(): string;
getAllGroups(): Promise<{
gridVerMap: Record<string, string>;
}>;
getGroupInfo(groupId: string | string[]): Promise<{
gridInfoMap: Record<string, GroupInfo & { memVerList?: unknown }>;
}>;
getGroupMembersInfo(memberId: string | string[]): Promise<{
profiles: Record<
string,
{
id?: string;
displayName?: string;
zaloName?: string;
avatar?: string;
}
>;
}>;
sendMessage(
message: string | Record<string, unknown>,
threadId: string,
type?: number,
): Promise<{
message?: { msgId?: string | number } | null;
attachment?: Array<{ msgId?: string | number }>;
}>;
sendLink(
payload: { link: string; msg?: string },
threadId: string,
type?: number,
): Promise<{ msgId?: string | number }>;
sendTypingEvent(threadId: string, type?: number, destType?: number): Promise<{ status: number }>;
addReaction(
icon: string | { rType: number; source: number; icon: string },
dest: {
data: {
msgId: string;
cliMsgId: string;
};
threadId: string;
type: number;
},
): Promise<unknown>;
sendDeliveredEvent(
isSeen: boolean,
messages:
| {
msgId: string;
cliMsgId: string;
uidFrom: string;
idTo: string;
msgType: string;
st: number;
at: number;
cmd: number;
ts: string | number;
}
| Array<{
msgId: string;
cliMsgId: string;
uidFrom: string;
idTo: string;
msgType: string;
st: number;
at: number;
cmd: number;
ts: string | number;
}>,
type?: number,
): Promise<unknown>;
sendSeenEvent(
messages:
| {
msgId: string;
cliMsgId: string;
uidFrom: string;
idTo: string;
msgType: string;
st: number;
at: number;
cmd: number;
ts: string | number;
}
| Array<{
msgId: string;
cliMsgId: string;
uidFrom: string;
idTo: string;
msgType: string;
st: number;
at: number;
cmd: number;
ts: string | number;
}>,
type?: number,
): Promise<unknown>;
};
type ZaloCtor = new (options?: { logging?: boolean; selfListen?: boolean }) => {
login(credentials: Credentials): Promise<API>;
loginQR(
options?: { userAgent?: string; language?: string; qrPath?: string },
callback?: (event: LoginQRCallbackEvent) => unknown,
): Promise<API>;
};
export const Zalo = ZaloRuntime as unknown as ZaloCtor;

View File

@@ -1,219 +1,22 @@
declare module "zca-js" {
export enum ThreadType {
User = 0,
Group = 1,
}
export enum Reactions {
HEART = "/-heart",
LIKE = "/-strong",
HAHA = ":>",
WOW = ":o",
CRY = ":-((",
ANGRY = ":-h",
KISS = ":-*",
TEARS_OF_JOY = ":')",
NONE = "",
}
export enum LoginQRCallbackEventType {
QRCodeGenerated = 0,
QRCodeExpired = 1,
QRCodeScanned = 2,
QRCodeDeclined = 3,
GotLoginInfo = 4,
}
export type Credentials = {
imei: string;
cookie: unknown;
userAgent: string;
language?: string;
export const ThreadType: {
User: number;
Group: number;
};
export type User = {
userId: string;
username: string;
displayName: string;
zaloName: string;
avatar: string;
export const LoginQRCallbackEventType: {
QRCodeGenerated: number;
QRCodeExpired: number;
QRCodeScanned: number;
QRCodeDeclined: number;
GotLoginInfo: number;
};
export type GroupInfo = {
groupId: string;
name: string;
totalMember?: number;
memberIds?: unknown[];
currentMems?: Array<{
id?: unknown;
dName?: string;
zaloName?: string;
avatar?: string;
}>;
};
export type Message = {
type: ThreadType;
threadId: string;
isSelf: boolean;
data: Record<string, unknown>;
};
export type LoginQRCallbackEvent =
| {
type: LoginQRCallbackEventType.QRCodeGenerated;
data: {
code: string;
image: string;
};
actions: {
saveToFile: (qrPath?: string) => Promise<unknown>;
retry: () => unknown;
abort: () => unknown;
};
}
| {
type: LoginQRCallbackEventType.QRCodeExpired;
data: null;
actions: {
retry: () => unknown;
abort: () => unknown;
};
}
| {
type: LoginQRCallbackEventType.QRCodeScanned;
data: {
avatar: string;
display_name: string;
};
actions: {
retry: () => unknown;
abort: () => unknown;
};
}
| {
type: LoginQRCallbackEventType.QRCodeDeclined;
data: {
code: string;
};
actions: {
retry: () => unknown;
abort: () => unknown;
};
}
| {
type: LoginQRCallbackEventType.GotLoginInfo;
data: {
cookie: unknown;
imei: string;
userAgent: string;
};
actions: null;
};
export type Listener = {
on(event: "message", callback: (message: Message) => void): void;
on(event: "error", callback: (error: unknown) => void): void;
on(event: "closed", callback: (code: number, reason: string) => void): void;
off(event: "message", callback: (message: Message) => void): void;
off(event: "error", callback: (error: unknown) => void): void;
off(event: "closed", callback: (code: number, reason: string) => void): void;
start(opts?: { retryOnClose?: boolean }): void;
stop(): void;
};
export type ZaloEventMessageParams = {
msgId: string;
cliMsgId: string;
uidFrom: string;
idTo: string;
msgType: string;
st: number;
at: number;
cmd: number;
ts: string | number;
};
export type AddReactionDestination = {
data: {
msgId: string;
cliMsgId: string;
};
threadId: string;
type: ThreadType;
};
export class API {
listener: Listener;
getContext(): {
imei: string;
userAgent: string;
language?: string;
};
getCookie(): {
toJSON(): {
cookies: unknown[];
};
};
fetchAccountInfo(): Promise<{ profile: User } | User>;
getAllFriends(): Promise<User[]>;
getOwnId(): string;
getAllGroups(): Promise<{
gridVerMap: Record<string, string>;
}>;
getGroupInfo(groupId: string | string[]): Promise<{
gridInfoMap: Record<string, GroupInfo & { memVerList?: unknown }>;
}>;
getGroupMembersInfo(memberId: string | string[]): Promise<{
profiles: Record<
string,
{
id?: string;
displayName?: string;
zaloName?: string;
avatar?: string;
}
>;
}>;
sendMessage(
message: string | Record<string, unknown>,
threadId: string,
type?: ThreadType,
): Promise<{
message?: { msgId?: string | number } | null;
attachment?: Array<{ msgId?: string | number }>;
}>;
sendLink(
payload: { link: string; msg?: string },
threadId: string,
type?: ThreadType,
): Promise<{ msgId?: string | number }>;
sendTypingEvent(
threadId: string,
type?: ThreadType,
destType?: number,
): Promise<{ status: number }>;
addReaction(
icon: Reactions | string | { rType: number; source: number; icon: string },
dest: AddReactionDestination,
): Promise<unknown>;
sendDeliveredEvent(
isSeen: boolean,
messages: ZaloEventMessageParams | ZaloEventMessageParams[],
type?: ThreadType,
): Promise<unknown>;
sendSeenEvent(
messages: ZaloEventMessageParams | ZaloEventMessageParams[],
type?: ThreadType,
): Promise<unknown>;
}
export const Reactions: Record<string, string>;
export class Zalo {
constructor(options?: { logging?: boolean; selfListen?: boolean });
login(credentials: Credentials): Promise<API>;
loginQR(
options?: { userAgent?: string; language?: string; qrPath?: string },
callback?: (event: LoginQRCallbackEvent) => unknown,
): Promise<API>;
login(credentials: unknown): Promise<unknown>;
loginQR(options?: unknown, callback?: (event: unknown) => unknown): Promise<unknown>;
}
}