mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-07 22:44:16 +00:00
fix(zalo): enforce group sender policy in groups
This commit is contained in:
@@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Security/Sandbox media: restrict sandbox media tmp-path allowances to OpenClaw-managed tmp roots instead of broad host `os.tmpdir()` trust, and add outbound/channel guardrails (tmp-path lint + media-root smoke tests) to prevent regressions in local media attachment reads.
|
||||
- Config/Plugins: treat stale removed `google-antigravity-auth` plugin references as compatibility warnings (not hard validation errors) across `plugins.entries`, `plugins.allow`, `plugins.deny`, and `plugins.slots.memory`, so startup no longer fails after antigravity removal. (#25538, #25862) Thanks @chilu18.
|
||||
- Security/Message actions: enforce local media root checks for `sendAttachment` and `setGroupIcon` when `sandboxRoot` is unset, preventing attachment hydration from reading arbitrary host files via local absolute paths. This ships in the next npm release. Thanks @GCXWLP for reporting.
|
||||
- Zalo/Group policy: enforce sender authorization for group messages with `groupPolicy` + `groupAllowFrom` (fallback to `allowFrom`), default runtime group behavior to fail-closed allowlist, and block unauthorized non-command group messages before dispatch. This ships in the next npm release. Thanks @tdjackey for reporting.
|
||||
- Security/Workspace FS: normalize `@`-prefixed paths before workspace-boundary checks (including workspace-only read/write/edit and sandbox mount path guards), preventing absolute-path escape attempts from bypassing guard validation. This ships in the next npm release. Thanks @tdjackey for reporting.
|
||||
- Security/Synology Chat: enforce fail-closed allowlist behavior for DM ingress so `dmPolicy: "allowlist"` with empty `allowedUserIds` rejects all senders instead of allowing unauthorized dispatch. This ships in the next npm release. Thanks @tdjackey for reporting.
|
||||
- Security/Native images: enforce `tools.fs.workspaceOnly` for native prompt image auto-load (including history refs), preventing out-of-workspace sandbox mounts from being implicitly ingested as vision input. This ships in the next npm release. Thanks @tdjackey for reporting.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
summary: "Group chat behavior across surfaces (WhatsApp/Telegram/Discord/Slack/Signal/iMessage/Microsoft Teams)"
|
||||
summary: "Group chat behavior across surfaces (WhatsApp/Telegram/Discord/Slack/Signal/iMessage/Microsoft Teams/Zalo)"
|
||||
read_when:
|
||||
- Changing group chat behavior or mention gating
|
||||
title: "Groups"
|
||||
@@ -7,7 +7,7 @@ title: "Groups"
|
||||
|
||||
# Groups
|
||||
|
||||
OpenClaw treats group chats consistently across surfaces: WhatsApp, Telegram, Discord, Slack, Signal, iMessage, Microsoft Teams.
|
||||
OpenClaw treats group chats consistently across surfaces: WhatsApp, Telegram, Discord, Slack, Signal, iMessage, Microsoft Teams, Zalo.
|
||||
|
||||
## Beginner intro (2 minutes)
|
||||
|
||||
@@ -183,7 +183,7 @@ Control how group/room messages are handled per channel:
|
||||
Notes:
|
||||
|
||||
- `groupPolicy` is separate from mention-gating (which requires @mentions).
|
||||
- WhatsApp/Telegram/Signal/iMessage/Microsoft Teams: use `groupAllowFrom` (fallback: explicit `allowFrom`).
|
||||
- WhatsApp/Telegram/Signal/iMessage/Microsoft Teams/Zalo: use `groupAllowFrom` (fallback: explicit `allowFrom`).
|
||||
- Discord: allowlist uses `channels.discord.guilds.<id>.channels`.
|
||||
- Slack: allowlist uses `channels.slack.channels`.
|
||||
- Matrix: allowlist uses `channels.matrix.groups` (room IDs, aliases, or names). Use `channels.matrix.groupAllowFrom` to restrict senders; per-room `users` allowlists are also supported.
|
||||
|
||||
@@ -7,7 +7,7 @@ title: "Zalo"
|
||||
|
||||
# Zalo (Bot API)
|
||||
|
||||
Status: experimental. Direct messages only; groups coming soon per Zalo docs.
|
||||
Status: experimental. DMs are supported; group handling is available with explicit group policy controls.
|
||||
|
||||
## Plugin required
|
||||
|
||||
@@ -51,7 +51,7 @@ It is a good fit for support or notifications where you want deterministic routi
|
||||
- A Zalo Bot API channel owned by the Gateway.
|
||||
- Deterministic routing: replies go back to Zalo; the model never chooses channels.
|
||||
- DMs share the agent's main session.
|
||||
- Groups are not yet supported (Zalo docs state "coming soon").
|
||||
- Groups are supported with policy controls (`groupPolicy` + `groupAllowFrom`) and default to fail-closed allowlist behavior.
|
||||
|
||||
## Setup (fast path)
|
||||
|
||||
@@ -107,6 +107,16 @@ Multi-account support: use `channels.zalo.accounts` with per-account tokens and
|
||||
- Pairing is the default token exchange. Details: [Pairing](/channels/pairing)
|
||||
- `channels.zalo.allowFrom` accepts numeric user IDs (no username lookup available).
|
||||
|
||||
## Access control (Groups)
|
||||
|
||||
- `channels.zalo.groupPolicy` controls group inbound handling: `open | allowlist | disabled`.
|
||||
- Default behavior is fail-closed: `allowlist`.
|
||||
- `channels.zalo.groupAllowFrom` restricts which sender IDs can trigger the bot in groups.
|
||||
- If `groupAllowFrom` is unset, Zalo falls back to `allowFrom` for sender checks.
|
||||
- `groupPolicy: "disabled"` blocks all group messages.
|
||||
- `groupPolicy: "open"` allows any group member (mention-gated).
|
||||
- Runtime note: if `channels.zalo` is missing entirely, runtime still falls back to `groupPolicy="allowlist"` for safety.
|
||||
|
||||
## Long-polling vs webhook
|
||||
|
||||
- Default: long-polling (no public URL required).
|
||||
@@ -130,16 +140,16 @@ Multi-account support: use `channels.zalo.accounts` with per-account tokens and
|
||||
|
||||
## Capabilities
|
||||
|
||||
| Feature | Status |
|
||||
| --------------- | ------------------------------ |
|
||||
| Direct messages | ✅ Supported |
|
||||
| Groups | ❌ Coming soon (per Zalo docs) |
|
||||
| Media (images) | ✅ Supported |
|
||||
| Reactions | ❌ Not supported |
|
||||
| Threads | ❌ Not supported |
|
||||
| Polls | ❌ Not supported |
|
||||
| Native commands | ❌ Not supported |
|
||||
| Streaming | ⚠️ Blocked (2000 char limit) |
|
||||
| Feature | Status |
|
||||
| --------------- | -------------------------------------------------------- |
|
||||
| Direct messages | ✅ Supported |
|
||||
| Groups | ⚠️ Supported with policy controls (allowlist by default) |
|
||||
| Media (images) | ✅ Supported |
|
||||
| Reactions | ❌ Not supported |
|
||||
| Threads | ❌ Not supported |
|
||||
| Polls | ❌ Not supported |
|
||||
| Native commands | ❌ Not supported |
|
||||
| Streaming | ⚠️ Blocked (2000 char limit) |
|
||||
|
||||
## Delivery targets (CLI/cron)
|
||||
|
||||
@@ -172,6 +182,8 @@ Provider options:
|
||||
- `channels.zalo.tokenFile`: read token from file path.
|
||||
- `channels.zalo.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing).
|
||||
- `channels.zalo.allowFrom`: DM allowlist (user IDs). `open` requires `"*"`. The wizard will ask for numeric IDs.
|
||||
- `channels.zalo.groupPolicy`: `open | allowlist | disabled` (default: allowlist).
|
||||
- `channels.zalo.groupAllowFrom`: group sender allowlist (user IDs). Falls back to `allowFrom` when unset.
|
||||
- `channels.zalo.mediaMaxMb`: inbound/outbound media cap (MB, default 5).
|
||||
- `channels.zalo.webhookUrl`: enable webhook mode (HTTPS required).
|
||||
- `channels.zalo.webhookSecret`: webhook secret (8-256 chars).
|
||||
@@ -186,6 +198,8 @@ Multi-account options:
|
||||
- `channels.zalo.accounts.<id>.enabled`: enable/disable account.
|
||||
- `channels.zalo.accounts.<id>.dmPolicy`: per-account DM policy.
|
||||
- `channels.zalo.accounts.<id>.allowFrom`: per-account allowlist.
|
||||
- `channels.zalo.accounts.<id>.groupPolicy`: per-account group policy.
|
||||
- `channels.zalo.accounts.<id>.groupAllowFrom`: per-account group sender allowlist.
|
||||
- `channels.zalo.accounts.<id>.webhookUrl`: per-account webhook URL.
|
||||
- `channels.zalo.accounts.<id>.webhookSecret`: per-account webhook secret.
|
||||
- `channels.zalo.accounts.<id>.webhookPath`: per-account webhook path.
|
||||
|
||||
@@ -16,6 +16,8 @@ import {
|
||||
migrateBaseNameToDefaultAccount,
|
||||
normalizeAccountId,
|
||||
PAIRING_APPROVED_MESSAGE,
|
||||
resolveDefaultGroupPolicy,
|
||||
resolveOpenProviderRuntimeGroupPolicy,
|
||||
resolveChannelAccountConfigBasePath,
|
||||
setAccountEnabledInConfigSection,
|
||||
} from "openclaw/plugin-sdk";
|
||||
@@ -56,7 +58,7 @@ function normalizeZaloMessagingTarget(raw: string): string | undefined {
|
||||
export const zaloDock: ChannelDock = {
|
||||
id: "zalo",
|
||||
capabilities: {
|
||||
chatTypes: ["direct"],
|
||||
chatTypes: ["direct", "group"],
|
||||
media: true,
|
||||
blockStreaming: true,
|
||||
},
|
||||
@@ -82,7 +84,7 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
||||
meta,
|
||||
onboarding: zaloOnboardingAdapter,
|
||||
capabilities: {
|
||||
chatTypes: ["direct"],
|
||||
chatTypes: ["direct", "group"],
|
||||
media: true,
|
||||
reactions: false,
|
||||
threads: false,
|
||||
@@ -143,6 +145,31 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
||||
normalizeEntry: (raw) => raw.replace(/^(zalo|zl):/i, ""),
|
||||
};
|
||||
},
|
||||
collectWarnings: ({ account, cfg }) => {
|
||||
const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);
|
||||
const { groupPolicy } = resolveOpenProviderRuntimeGroupPolicy({
|
||||
providerConfigPresent: cfg.channels?.zalo !== undefined,
|
||||
groupPolicy: account.config.groupPolicy,
|
||||
defaultGroupPolicy,
|
||||
});
|
||||
if (groupPolicy !== "open") {
|
||||
return [];
|
||||
}
|
||||
const explicitGroupAllowFrom = (account.config.groupAllowFrom ?? []).map((entry) =>
|
||||
String(entry),
|
||||
);
|
||||
const dmAllowFrom = (account.config.allowFrom ?? []).map((entry) => String(entry));
|
||||
const effectiveAllowFrom =
|
||||
explicitGroupAllowFrom.length > 0 ? explicitGroupAllowFrom : dmAllowFrom;
|
||||
if (effectiveAllowFrom.length > 0) {
|
||||
return [
|
||||
`- Zalo groups: groupPolicy="open" allows any member to trigger (mention-gated). Set channels.zalo.groupPolicy="allowlist" + channels.zalo.groupAllowFrom to restrict senders.`,
|
||||
];
|
||||
}
|
||||
return [
|
||||
`- Zalo groups: groupPolicy="open" with no groupAllowFrom/allowFrom allowlist; any member can trigger (mention-gated). Set channels.zalo.groupPolicy="allowlist" + channels.zalo.groupAllowFrom.`,
|
||||
];
|
||||
},
|
||||
},
|
||||
groups: {
|
||||
resolveRequireMention: () => true,
|
||||
|
||||
@@ -14,6 +14,8 @@ const zaloAccountSchema = z.object({
|
||||
webhookPath: z.string().optional(),
|
||||
dmPolicy: z.enum(["pairing", "allowlist", "open", "disabled"]).optional(),
|
||||
allowFrom: z.array(allowFromEntry).optional(),
|
||||
groupPolicy: z.enum(["disabled", "allowlist", "open"]).optional(),
|
||||
groupAllowFrom: z.array(allowFromEntry).optional(),
|
||||
mediaMaxMb: z.number().optional(),
|
||||
proxy: z.string().optional(),
|
||||
responsePrefix: z.string().optional(),
|
||||
|
||||
106
extensions/zalo/src/monitor.group-policy.test.ts
Normal file
106
extensions/zalo/src/monitor.group-policy.test.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { __testing } from "./monitor.js";
|
||||
|
||||
describe("zalo group policy access", () => {
|
||||
it("defaults missing provider config to allowlist", () => {
|
||||
const resolved = __testing.resolveZaloRuntimeGroupPolicy({
|
||||
providerConfigPresent: false,
|
||||
groupPolicy: undefined,
|
||||
defaultGroupPolicy: "open",
|
||||
});
|
||||
expect(resolved).toEqual({
|
||||
groupPolicy: "allowlist",
|
||||
providerMissingFallbackApplied: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("blocks all group messages when policy is disabled", () => {
|
||||
const decision = __testing.evaluateZaloGroupAccess({
|
||||
providerConfigPresent: true,
|
||||
configuredGroupPolicy: "disabled",
|
||||
defaultGroupPolicy: "open",
|
||||
groupAllowFrom: ["zalo:123"],
|
||||
senderId: "123",
|
||||
});
|
||||
expect(decision).toMatchObject({
|
||||
allowed: false,
|
||||
groupPolicy: "disabled",
|
||||
reason: "disabled",
|
||||
});
|
||||
});
|
||||
|
||||
it("blocks group messages on allowlist policy with empty allowlist", () => {
|
||||
const decision = __testing.evaluateZaloGroupAccess({
|
||||
providerConfigPresent: true,
|
||||
configuredGroupPolicy: "allowlist",
|
||||
defaultGroupPolicy: "open",
|
||||
groupAllowFrom: [],
|
||||
senderId: "attacker",
|
||||
});
|
||||
expect(decision).toMatchObject({
|
||||
allowed: false,
|
||||
groupPolicy: "allowlist",
|
||||
reason: "empty_allowlist",
|
||||
});
|
||||
});
|
||||
|
||||
it("blocks sender not in group allowlist", () => {
|
||||
const decision = __testing.evaluateZaloGroupAccess({
|
||||
providerConfigPresent: true,
|
||||
configuredGroupPolicy: "allowlist",
|
||||
defaultGroupPolicy: "open",
|
||||
groupAllowFrom: ["zalo:victim-user-001"],
|
||||
senderId: "attacker-user-999",
|
||||
});
|
||||
expect(decision).toMatchObject({
|
||||
allowed: false,
|
||||
groupPolicy: "allowlist",
|
||||
reason: "sender_not_allowlisted",
|
||||
});
|
||||
});
|
||||
|
||||
it("allows sender in group allowlist", () => {
|
||||
const decision = __testing.evaluateZaloGroupAccess({
|
||||
providerConfigPresent: true,
|
||||
configuredGroupPolicy: "allowlist",
|
||||
defaultGroupPolicy: "open",
|
||||
groupAllowFrom: ["zl:12345"],
|
||||
senderId: "12345",
|
||||
});
|
||||
expect(decision).toMatchObject({
|
||||
allowed: true,
|
||||
groupPolicy: "allowlist",
|
||||
reason: "allowed",
|
||||
});
|
||||
});
|
||||
|
||||
it("allows any sender with wildcard allowlist", () => {
|
||||
const decision = __testing.evaluateZaloGroupAccess({
|
||||
providerConfigPresent: true,
|
||||
configuredGroupPolicy: "allowlist",
|
||||
defaultGroupPolicy: "open",
|
||||
groupAllowFrom: ["*"],
|
||||
senderId: "random-user",
|
||||
});
|
||||
expect(decision).toMatchObject({
|
||||
allowed: true,
|
||||
groupPolicy: "allowlist",
|
||||
reason: "allowed",
|
||||
});
|
||||
});
|
||||
|
||||
it("allows all group senders on open policy", () => {
|
||||
const decision = __testing.evaluateZaloGroupAccess({
|
||||
providerConfigPresent: true,
|
||||
configuredGroupPolicy: "open",
|
||||
defaultGroupPolicy: "allowlist",
|
||||
groupAllowFrom: [],
|
||||
senderId: "attacker-user-999",
|
||||
});
|
||||
expect(decision).toMatchObject({
|
||||
allowed: true,
|
||||
groupPolicy: "open",
|
||||
reason: "allowed",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -10,9 +10,12 @@ import {
|
||||
resolveSingleWebhookTarget,
|
||||
resolveSenderCommandAuthorization,
|
||||
resolveOutboundMediaUrls,
|
||||
resolveDefaultGroupPolicy,
|
||||
resolveOpenProviderRuntimeGroupPolicy,
|
||||
sendMediaWithLeadingCaption,
|
||||
resolveWebhookPath,
|
||||
resolveWebhookTargets,
|
||||
warnMissingProviderGroupPolicyFallbackOnce,
|
||||
requestBodyErrorToText,
|
||||
} from "openclaw/plugin-sdk";
|
||||
import type { ResolvedZaloAccount } from "./accounts.js";
|
||||
@@ -62,6 +65,14 @@ const ZALO_WEBHOOK_COUNTER_LOG_EVERY = 25;
|
||||
|
||||
type ZaloCoreRuntime = ReturnType<typeof getZaloRuntime>;
|
||||
type WebhookRateLimitState = { count: number; windowStartMs: number };
|
||||
type ZaloGroupPolicy = "open" | "allowlist" | "disabled";
|
||||
type ZaloGroupAccessReason = "allowed" | "disabled" | "empty_allowlist" | "sender_not_allowlisted";
|
||||
type ZaloGroupAccessDecision = {
|
||||
allowed: boolean;
|
||||
groupPolicy: ZaloGroupPolicy;
|
||||
providerMissingFallbackApplied: boolean;
|
||||
reason: ZaloGroupAccessReason;
|
||||
};
|
||||
|
||||
function logVerbose(core: ZaloCoreRuntime, runtime: ZaloRuntimeEnv, message: string): void {
|
||||
if (core.logging.shouldLogVerbose()) {
|
||||
@@ -80,6 +91,67 @@ function isSenderAllowed(senderId: string, allowFrom: string[]): boolean {
|
||||
});
|
||||
}
|
||||
|
||||
function resolveZaloRuntimeGroupPolicy(params: {
|
||||
providerConfigPresent: boolean;
|
||||
groupPolicy?: ZaloGroupPolicy;
|
||||
defaultGroupPolicy?: ZaloGroupPolicy;
|
||||
}): {
|
||||
groupPolicy: ZaloGroupPolicy;
|
||||
providerMissingFallbackApplied: boolean;
|
||||
} {
|
||||
return resolveOpenProviderRuntimeGroupPolicy({
|
||||
providerConfigPresent: params.providerConfigPresent,
|
||||
groupPolicy: params.groupPolicy,
|
||||
defaultGroupPolicy: params.defaultGroupPolicy,
|
||||
});
|
||||
}
|
||||
|
||||
function evaluateZaloGroupAccess(params: {
|
||||
providerConfigPresent: boolean;
|
||||
configuredGroupPolicy?: ZaloGroupPolicy;
|
||||
defaultGroupPolicy?: ZaloGroupPolicy;
|
||||
groupAllowFrom: string[];
|
||||
senderId: string;
|
||||
}): ZaloGroupAccessDecision {
|
||||
const { groupPolicy, providerMissingFallbackApplied } = resolveZaloRuntimeGroupPolicy({
|
||||
providerConfigPresent: params.providerConfigPresent,
|
||||
groupPolicy: params.configuredGroupPolicy,
|
||||
defaultGroupPolicy: params.defaultGroupPolicy,
|
||||
});
|
||||
if (groupPolicy === "disabled") {
|
||||
return {
|
||||
allowed: false,
|
||||
groupPolicy,
|
||||
providerMissingFallbackApplied,
|
||||
reason: "disabled",
|
||||
};
|
||||
}
|
||||
if (groupPolicy === "allowlist") {
|
||||
if (params.groupAllowFrom.length === 0) {
|
||||
return {
|
||||
allowed: false,
|
||||
groupPolicy,
|
||||
providerMissingFallbackApplied,
|
||||
reason: "empty_allowlist",
|
||||
};
|
||||
}
|
||||
if (!isSenderAllowed(params.senderId, params.groupAllowFrom)) {
|
||||
return {
|
||||
allowed: false,
|
||||
groupPolicy,
|
||||
providerMissingFallbackApplied,
|
||||
reason: "sender_not_allowlisted",
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
allowed: true,
|
||||
groupPolicy,
|
||||
providerMissingFallbackApplied,
|
||||
reason: "allowed",
|
||||
};
|
||||
}
|
||||
|
||||
type WebhookTarget = {
|
||||
token: string;
|
||||
account: ResolvedZaloAccount;
|
||||
@@ -502,6 +574,42 @@ async function processMessageWithPipeline(params: {
|
||||
|
||||
const dmPolicy = account.config.dmPolicy ?? "pairing";
|
||||
const configAllowFrom = (account.config.allowFrom ?? []).map((v) => String(v));
|
||||
const configuredGroupAllowFrom = (account.config.groupAllowFrom ?? []).map((v) => String(v));
|
||||
const groupAllowFrom =
|
||||
configuredGroupAllowFrom.length > 0 ? configuredGroupAllowFrom : configAllowFrom;
|
||||
const defaultGroupPolicy = resolveDefaultGroupPolicy(config);
|
||||
const groupAccess = isGroup
|
||||
? evaluateZaloGroupAccess({
|
||||
providerConfigPresent: config.channels?.zalo !== undefined,
|
||||
configuredGroupPolicy: account.config.groupPolicy,
|
||||
defaultGroupPolicy,
|
||||
groupAllowFrom,
|
||||
senderId,
|
||||
})
|
||||
: undefined;
|
||||
if (groupAccess) {
|
||||
warnMissingProviderGroupPolicyFallbackOnce({
|
||||
providerMissingFallbackApplied: groupAccess.providerMissingFallbackApplied,
|
||||
providerKey: "zalo",
|
||||
accountId: account.accountId,
|
||||
log: (message) => logVerbose(core, runtime, message),
|
||||
});
|
||||
if (!groupAccess.allowed) {
|
||||
if (groupAccess.reason === "disabled") {
|
||||
logVerbose(core, runtime, `zalo: drop group ${chatId} (groupPolicy=disabled)`);
|
||||
} else if (groupAccess.reason === "empty_allowlist") {
|
||||
logVerbose(
|
||||
core,
|
||||
runtime,
|
||||
`zalo: drop group ${chatId} (groupPolicy=allowlist, no groupAllowFrom)`,
|
||||
);
|
||||
} else if (groupAccess.reason === "sender_not_allowlisted") {
|
||||
logVerbose(core, runtime, `zalo: drop group sender ${senderId} (groupPolicy=allowlist)`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const rawBody = text?.trim() || (mediaPath ? "<media:image>" : "");
|
||||
const { senderAllowedForCommands, commandAuthorized } = await resolveSenderCommandAuthorization({
|
||||
cfg: config,
|
||||
@@ -818,3 +926,8 @@ export async function monitorZaloProvider(options: ZaloMonitorOptions): Promise<
|
||||
|
||||
return { stop };
|
||||
}
|
||||
|
||||
export const __testing = {
|
||||
evaluateZaloGroupAccess,
|
||||
resolveZaloRuntimeGroupPolicy,
|
||||
};
|
||||
|
||||
@@ -17,6 +17,10 @@ export type ZaloAccountConfig = {
|
||||
dmPolicy?: "pairing" | "allowlist" | "open" | "disabled";
|
||||
/** Allowlist for DM senders (Zalo user IDs). */
|
||||
allowFrom?: Array<string | number>;
|
||||
/** Group-message access policy. */
|
||||
groupPolicy?: "open" | "allowlist" | "disabled";
|
||||
/** Allowlist for group senders (falls back to allowFrom when unset). */
|
||||
groupAllowFrom?: Array<string | number>;
|
||||
/** Max inbound media size in MB. */
|
||||
mediaMaxMb?: number;
|
||||
/** Proxy URL for API requests. */
|
||||
|
||||
Reference in New Issue
Block a user