feat(channels): add resolve command + defaults
This commit is contained in:
@@ -324,6 +324,73 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
|
||||
return sliced as ChannelDirectoryEntry[];
|
||||
},
|
||||
},
|
||||
resolver: {
|
||||
resolveTargets: async ({ cfg, accountId, inputs, kind, runtime }) => {
|
||||
const results = [];
|
||||
for (const input of inputs) {
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed) {
|
||||
results.push({ input, resolved: false, note: "empty input" });
|
||||
continue;
|
||||
}
|
||||
if (/^\d+$/.test(trimmed)) {
|
||||
results.push({ input, resolved: true, id: trimmed });
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const account = resolveZalouserAccountSync({
|
||||
cfg: cfg as CoreConfig,
|
||||
accountId: accountId ?? DEFAULT_ACCOUNT_ID,
|
||||
});
|
||||
const args =
|
||||
kind === "user"
|
||||
? trimmed
|
||||
? ["friend", "find", trimmed]
|
||||
: ["friend", "list", "-j"]
|
||||
: ["group", "list", "-j"];
|
||||
const result = await runZca(args, { profile: account.profile, timeout: 15000 });
|
||||
if (!result.ok) throw new Error(result.stderr || "zca lookup failed");
|
||||
if (kind === "user") {
|
||||
const parsed = parseJsonOutput<ZcaFriend[]>(result.stdout) ?? [];
|
||||
const matches = Array.isArray(parsed)
|
||||
? parsed.map((f) => ({
|
||||
id: String(f.userId),
|
||||
name: f.displayName ?? undefined,
|
||||
}))
|
||||
: [];
|
||||
const best = matches[0];
|
||||
results.push({
|
||||
input,
|
||||
resolved: Boolean(best?.id),
|
||||
id: best?.id,
|
||||
name: best?.name,
|
||||
note: matches.length > 1 ? "multiple matches; chose first" : undefined,
|
||||
});
|
||||
} else {
|
||||
const parsed = parseJsonOutput<ZcaGroup[]>(result.stdout) ?? [];
|
||||
const matches = Array.isArray(parsed)
|
||||
? parsed.map((g) => ({
|
||||
id: String(g.groupId),
|
||||
name: g.name ?? undefined,
|
||||
}))
|
||||
: [];
|
||||
const best = matches.find((g) => g.name?.toLowerCase() === trimmed.toLowerCase()) ?? matches[0];
|
||||
results.push({
|
||||
input,
|
||||
resolved: Boolean(best?.id),
|
||||
id: best?.id,
|
||||
name: best?.name,
|
||||
note: matches.length > 1 ? "multiple matches; chose first" : undefined,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
runtime.error?.(`zalouser resolve failed: ${String(err)}`);
|
||||
results.push({ input, resolved: false, note: "lookup failed" });
|
||||
}
|
||||
}
|
||||
return results;
|
||||
},
|
||||
},
|
||||
pairing: {
|
||||
idLabel: "zalouserUserId",
|
||||
normalizeAllowEntry: (entry) => entry.replace(/^(zalouser|zlu):/i, ""),
|
||||
|
||||
@@ -9,8 +9,14 @@ import { finalizeInboundContext } from "../../../src/auto-reply/reply/inbound-co
|
||||
import { resolveCommandAuthorizedFromAuthorizers } from "../../../src/channels/command-gating.js";
|
||||
import { loadCoreChannelDeps, type CoreChannelDeps } from "./core-bridge.js";
|
||||
import { sendMessageZalouser } from "./send.js";
|
||||
import type { CoreConfig, ResolvedZalouserAccount, ZcaMessage } from "./types.js";
|
||||
import { runZcaStreaming } from "./zca.js";
|
||||
import type {
|
||||
CoreConfig,
|
||||
ResolvedZalouserAccount,
|
||||
ZcaFriend,
|
||||
ZcaGroup,
|
||||
ZcaMessage,
|
||||
} from "./types.js";
|
||||
import { parseJsonOutput, runZca, runZcaStreaming } from "./zca.js";
|
||||
|
||||
export type ZalouserMonitorOptions = {
|
||||
account: ResolvedZalouserAccount;
|
||||
@@ -26,6 +32,71 @@ export type ZalouserMonitorResult = {
|
||||
|
||||
const ZALOUSER_TEXT_LIMIT = 2000;
|
||||
|
||||
function mergeAllowlist(params: {
|
||||
existing?: Array<string | number>;
|
||||
additions: string[];
|
||||
}): string[] {
|
||||
const seen = new Set<string>();
|
||||
const merged: string[] = [];
|
||||
const push = (value: string) => {
|
||||
const normalized = value.trim();
|
||||
if (!normalized) return;
|
||||
const key = normalized.toLowerCase();
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
merged.push(normalized);
|
||||
};
|
||||
for (const entry of params.existing ?? []) {
|
||||
push(String(entry));
|
||||
}
|
||||
for (const entry of params.additions) {
|
||||
push(entry);
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
function summarizeMapping(
|
||||
label: string,
|
||||
mapping: string[],
|
||||
unresolved: string[],
|
||||
runtime: RuntimeEnv,
|
||||
) {
|
||||
const lines: string[] = [];
|
||||
if (mapping.length > 0) {
|
||||
const sample = mapping.slice(0, 6);
|
||||
const suffix = mapping.length > sample.length ? ` (+${mapping.length - sample.length})` : "";
|
||||
lines.push(`${label} resolved: ${sample.join(", ")}${suffix}`);
|
||||
}
|
||||
if (unresolved.length > 0) {
|
||||
const sample = unresolved.slice(0, 6);
|
||||
const suffix =
|
||||
unresolved.length > sample.length ? ` (+${unresolved.length - sample.length})` : "";
|
||||
lines.push(`${label} unresolved: ${sample.join(", ")}${suffix}`);
|
||||
}
|
||||
if (lines.length > 0) {
|
||||
runtime.log?.(lines.join("\n"));
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeZalouserEntry(entry: string): string {
|
||||
return entry.replace(/^(zalouser|zlu):/i, "").trim();
|
||||
}
|
||||
|
||||
function buildNameIndex<T>(
|
||||
items: T[],
|
||||
nameFn: (item: T) => string | undefined,
|
||||
): Map<string, T[]> {
|
||||
const index = new Map<string, T[]>();
|
||||
for (const item of items) {
|
||||
const name = nameFn(item)?.trim().toLowerCase();
|
||||
if (!name) continue;
|
||||
const list = index.get(name) ?? [];
|
||||
list.push(item);
|
||||
index.set(name, list);
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
function logVerbose(deps: CoreChannelDeps, runtime: RuntimeEnv, message: string): void {
|
||||
if (deps.shouldLogVerbose()) {
|
||||
runtime.log(`[zalouser] ${message}`);
|
||||
@@ -41,6 +112,39 @@ function isSenderAllowed(senderId: string, allowFrom: string[]): boolean {
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
groups: Record<string, { allow?: boolean; enabled?: boolean }>;
|
||||
}): boolean {
|
||||
const groups = params.groups ?? {};
|
||||
const keys = Object.keys(groups);
|
||||
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;
|
||||
}
|
||||
|
||||
function startZcaListener(
|
||||
runtime: RuntimeEnv,
|
||||
profile: string,
|
||||
@@ -106,8 +210,26 @@ async function processMessage(
|
||||
const isGroup = metadata?.isGroup ?? false;
|
||||
const senderId = metadata?.fromId ?? threadId;
|
||||
const senderName = metadata?.senderName ?? "";
|
||||
const groupName = metadata?.threadName ?? "";
|
||||
const chatId = threadId;
|
||||
|
||||
const defaultGroupPolicy = config.channels?.defaults?.groupPolicy;
|
||||
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "open";
|
||||
const groups = account.config.groups ?? {};
|
||||
if (isGroup) {
|
||||
if (groupPolicy === "disabled") {
|
||||
logVerbose(deps, runtime, `zalouser: drop group ${chatId} (groupPolicy=disabled)`);
|
||||
return;
|
||||
}
|
||||
if (groupPolicy === "allowlist") {
|
||||
const allowed = isGroupAllowed({ groupId: chatId, groupName, groups });
|
||||
if (!allowed) {
|
||||
logVerbose(deps, runtime, `zalouser: drop group ${chatId} (not allowlisted)`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const dmPolicy = account.config.dmPolicy ?? "pairing";
|
||||
const configAllowFrom = (account.config.allowFrom ?? []).map((v) => String(v));
|
||||
const rawBody = content.trim();
|
||||
@@ -194,11 +316,10 @@ async function processMessage(
|
||||
},
|
||||
});
|
||||
|
||||
const rawBody = content.trim();
|
||||
const fromLabel = isGroup ? `group:${chatId}` : senderName || `user:${senderId}`;
|
||||
const body = deps.formatAgentEnvelope({
|
||||
channel: "Zalo Personal",
|
||||
from: fromLabel,
|
||||
const fromLabel = isGroup ? `group:${chatId}` : senderName || `user:${senderId}`;
|
||||
const body = deps.formatAgentEnvelope({
|
||||
channel: "Zalo Personal",
|
||||
from: fromLabel,
|
||||
timestamp: timestamp ? timestamp * 1000 : undefined,
|
||||
body: rawBody,
|
||||
});
|
||||
@@ -301,7 +422,8 @@ async function deliverZalouserReply(params: {
|
||||
export async function monitorZalouserProvider(
|
||||
options: ZalouserMonitorOptions,
|
||||
): Promise<ZalouserMonitorResult> {
|
||||
const { account, config, abortSignal, statusSink, runtime } = options;
|
||||
let { account, config } = options;
|
||||
const { abortSignal, statusSink, runtime } = options;
|
||||
|
||||
const deps = await loadCoreChannelDeps();
|
||||
let stopped = false;
|
||||
@@ -309,6 +431,92 @@ export async function monitorZalouserProvider(
|
||||
let restartTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let resolveRunning: (() => void) | null = null;
|
||||
|
||||
try {
|
||||
const profile = account.profile;
|
||||
const allowFromEntries = (account.config.allowFrom ?? [])
|
||||
.map((entry) => normalizeZalouserEntry(String(entry)))
|
||||
.filter((entry) => entry && entry !== "*");
|
||||
|
||||
if (allowFromEntries.length > 0) {
|
||||
const result = await runZca(["friend", "list", "-j"], { profile, timeout: 15000 });
|
||||
if (result.ok) {
|
||||
const friends = parseJsonOutput<ZcaFriend[]>(result.stdout) ?? [];
|
||||
const byName = buildNameIndex(friends, (friend) => friend.displayName);
|
||||
const additions: string[] = [];
|
||||
const mapping: string[] = [];
|
||||
const unresolved: string[] = [];
|
||||
for (const entry of allowFromEntries) {
|
||||
if (/^\d+$/.test(entry)) {
|
||||
additions.push(entry);
|
||||
continue;
|
||||
}
|
||||
const matches = byName.get(entry.toLowerCase()) ?? [];
|
||||
const match = matches[0];
|
||||
const id = match?.userId ? String(match.userId) : undefined;
|
||||
if (id) {
|
||||
additions.push(id);
|
||||
mapping.push(`${entry}→${id}`);
|
||||
} else {
|
||||
unresolved.push(entry);
|
||||
}
|
||||
}
|
||||
const allowFrom = mergeAllowlist({ existing: account.config.allowFrom, additions });
|
||||
account = {
|
||||
...account,
|
||||
config: {
|
||||
...account.config,
|
||||
allowFrom,
|
||||
},
|
||||
};
|
||||
summarizeMapping("zalouser users", mapping, unresolved, runtime);
|
||||
} else {
|
||||
runtime.log?.(`zalouser user resolve failed; using config entries. ${result.stderr}`);
|
||||
}
|
||||
}
|
||||
|
||||
const groupsConfig = account.config.groups ?? {};
|
||||
const groupKeys = Object.keys(groupsConfig).filter((key) => key !== "*");
|
||||
if (groupKeys.length > 0) {
|
||||
const result = await runZca(["group", "list", "-j"], { profile, timeout: 15000 });
|
||||
if (result.ok) {
|
||||
const groups = parseJsonOutput<ZcaGroup[]>(result.stdout) ?? [];
|
||||
const byName = buildNameIndex(groups, (group) => group.name);
|
||||
const mapping: string[] = [];
|
||||
const unresolved: string[] = [];
|
||||
const nextGroups = { ...groupsConfig };
|
||||
for (const entry of groupKeys) {
|
||||
const cleaned = normalizeZalouserEntry(entry);
|
||||
if (/^\d+$/.test(cleaned)) {
|
||||
if (!nextGroups[cleaned]) nextGroups[cleaned] = groupsConfig[entry];
|
||||
mapping.push(`${entry}→${cleaned}`);
|
||||
continue;
|
||||
}
|
||||
const matches = byName.get(cleaned.toLowerCase()) ?? [];
|
||||
const match = matches[0];
|
||||
const id = match?.groupId ? String(match.groupId) : undefined;
|
||||
if (id) {
|
||||
if (!nextGroups[id]) nextGroups[id] = groupsConfig[entry];
|
||||
mapping.push(`${entry}→${id}`);
|
||||
} else {
|
||||
unresolved.push(entry);
|
||||
}
|
||||
}
|
||||
account = {
|
||||
...account,
|
||||
config: {
|
||||
...account.config,
|
||||
groups: nextGroups,
|
||||
},
|
||||
};
|
||||
summarizeMapping("zalouser groups", mapping, unresolved, runtime);
|
||||
} else {
|
||||
runtime.log?.(`zalouser group resolve failed; using config entries. ${result.stderr}`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
runtime.log?.(`zalouser resolve failed; using config entries. ${String(err)}`);
|
||||
}
|
||||
|
||||
const stop = () => {
|
||||
stopped = true;
|
||||
if (restartTimer) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ChannelOnboardingAdapter, ChannelOnboardingDmPolicy } from "../../../src/channels/plugins/onboarding-types.js";
|
||||
import type { WizardPrompter } from "../../../src/wizard/prompts.js";
|
||||
import { promptChannelAccessConfig } from "../../../src/channels/plugins/onboarding/channel-access.js";
|
||||
|
||||
import {
|
||||
listZalouserAccountIds,
|
||||
@@ -8,8 +9,8 @@ import {
|
||||
normalizeAccountId,
|
||||
checkZcaAuthenticated,
|
||||
} from "./accounts.js";
|
||||
import { runZcaInteractive, checkZcaInstalled } from "./zca.js";
|
||||
import { DEFAULT_ACCOUNT_ID, type CoreConfig } from "./types.js";
|
||||
import { runZca, runZcaInteractive, checkZcaInstalled, parseJsonOutput } from "./zca.js";
|
||||
import { DEFAULT_ACCOUNT_ID, type CoreConfig, type ZcaGroup } from "./types.js";
|
||||
|
||||
const channel = "zalouser" as const;
|
||||
|
||||
@@ -113,6 +114,115 @@ async function promptZalouserAllowFrom(params: {
|
||||
} as CoreConfig;
|
||||
}
|
||||
|
||||
function setZalouserGroupPolicy(
|
||||
cfg: CoreConfig,
|
||||
accountId: string,
|
||||
groupPolicy: "open" | "allowlist" | "disabled",
|
||||
): CoreConfig {
|
||||
if (accountId === DEFAULT_ACCOUNT_ID) {
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
zalouser: {
|
||||
...cfg.channels?.zalouser,
|
||||
enabled: true,
|
||||
groupPolicy,
|
||||
},
|
||||
},
|
||||
} as CoreConfig;
|
||||
}
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
zalouser: {
|
||||
...cfg.channels?.zalouser,
|
||||
enabled: true,
|
||||
accounts: {
|
||||
...(cfg.channels?.zalouser?.accounts ?? {}),
|
||||
[accountId]: {
|
||||
...(cfg.channels?.zalouser?.accounts?.[accountId] ?? {}),
|
||||
enabled: cfg.channels?.zalouser?.accounts?.[accountId]?.enabled ?? true,
|
||||
groupPolicy,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as CoreConfig;
|
||||
}
|
||||
|
||||
function setZalouserGroupAllowlist(
|
||||
cfg: CoreConfig,
|
||||
accountId: string,
|
||||
groupKeys: string[],
|
||||
): CoreConfig {
|
||||
const groups = Object.fromEntries(groupKeys.map((key) => [key, { allow: true }]));
|
||||
if (accountId === DEFAULT_ACCOUNT_ID) {
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
zalouser: {
|
||||
...cfg.channels?.zalouser,
|
||||
enabled: true,
|
||||
groups,
|
||||
},
|
||||
},
|
||||
} as CoreConfig;
|
||||
}
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
zalouser: {
|
||||
...cfg.channels?.zalouser,
|
||||
enabled: true,
|
||||
accounts: {
|
||||
...(cfg.channels?.zalouser?.accounts ?? {}),
|
||||
[accountId]: {
|
||||
...(cfg.channels?.zalouser?.accounts?.[accountId] ?? {}),
|
||||
enabled: cfg.channels?.zalouser?.accounts?.[accountId]?.enabled ?? true,
|
||||
groups,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as CoreConfig;
|
||||
}
|
||||
|
||||
async function resolveZalouserGroups(params: {
|
||||
cfg: CoreConfig;
|
||||
accountId: string;
|
||||
entries: string[];
|
||||
}): Promise<Array<{ input: string; resolved: boolean; id?: string }>> {
|
||||
const account = resolveZalouserAccountSync({ cfg: params.cfg, accountId: params.accountId });
|
||||
const result = await runZca(["group", "list", "-j"], { profile: account.profile, timeout: 15000 });
|
||||
if (!result.ok) throw new Error(result.stderr || "Failed to list groups");
|
||||
const groups = (parseJsonOutput<ZcaGroup[]>(result.stdout) ?? []).filter(
|
||||
(group) => Boolean(group.groupId),
|
||||
);
|
||||
const byName = new Map<string, ZcaGroup[]>();
|
||||
for (const group of groups) {
|
||||
const name = group.name?.trim().toLowerCase();
|
||||
if (!name) continue;
|
||||
const list = byName.get(name) ?? [];
|
||||
list.push(group);
|
||||
byName.set(name, list);
|
||||
}
|
||||
|
||||
return params.entries.map((input) => {
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed) return { input, resolved: false };
|
||||
if (/^\d+$/.test(trimmed)) return { input, resolved: true, id: trimmed };
|
||||
const matches = byName.get(trimmed.toLowerCase()) ?? [];
|
||||
const match = matches[0];
|
||||
return match?.groupId
|
||||
? { input, resolved: true, id: String(match.groupId) }
|
||||
: { input, resolved: false };
|
||||
});
|
||||
}
|
||||
|
||||
async function promptAccountId(params: {
|
||||
cfg: CoreConfig;
|
||||
prompter: WizardPrompter;
|
||||
@@ -307,6 +417,61 @@ export const zalouserOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
});
|
||||
}
|
||||
|
||||
const accessConfig = await promptChannelAccessConfig({
|
||||
prompter,
|
||||
label: "Zalo groups",
|
||||
currentPolicy: account.config.groupPolicy ?? "open",
|
||||
currentEntries: Object.keys(account.config.groups ?? {}),
|
||||
placeholder: "Family, Work, 123456789",
|
||||
updatePrompt: Boolean(account.config.groups),
|
||||
});
|
||||
if (accessConfig) {
|
||||
if (accessConfig.policy !== "allowlist") {
|
||||
next = setZalouserGroupPolicy(next, accountId, accessConfig.policy);
|
||||
} else {
|
||||
let keys = accessConfig.entries;
|
||||
if (accessConfig.entries.length > 0) {
|
||||
try {
|
||||
const resolved = await resolveZalouserGroups({
|
||||
cfg: next,
|
||||
accountId,
|
||||
entries: accessConfig.entries,
|
||||
});
|
||||
const resolvedIds = resolved
|
||||
.filter((entry) => entry.resolved && entry.id)
|
||||
.map((entry) => entry.id as string);
|
||||
const unresolved = resolved
|
||||
.filter((entry) => !entry.resolved)
|
||||
.map((entry) => entry.input);
|
||||
keys = [
|
||||
...resolvedIds,
|
||||
...unresolved.map((entry) => entry.trim()).filter(Boolean),
|
||||
];
|
||||
if (resolvedIds.length > 0 || unresolved.length > 0) {
|
||||
await prompter.note(
|
||||
[
|
||||
resolvedIds.length > 0 ? `Resolved: ${resolvedIds.join(", ")}` : undefined,
|
||||
unresolved.length > 0
|
||||
? `Unresolved (kept as typed): ${unresolved.join(", ")}`
|
||||
: undefined,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n"),
|
||||
"Zalo groups",
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
await prompter.note(
|
||||
`Group lookup failed; keeping entries as typed. ${String(err)}`,
|
||||
"Zalo groups",
|
||||
);
|
||||
}
|
||||
}
|
||||
next = setZalouserGroupPolicy(next, accountId, "allowlist");
|
||||
next = setZalouserGroupAllowlist(next, accountId, keys);
|
||||
}
|
||||
}
|
||||
|
||||
return { cfg: next, accountId };
|
||||
},
|
||||
};
|
||||
|
||||
@@ -77,6 +77,8 @@ export type ZalouserAccountConfig = {
|
||||
profile?: string;
|
||||
dmPolicy?: "pairing" | "allowlist" | "open" | "disabled";
|
||||
allowFrom?: Array<string | number>;
|
||||
groupPolicy?: "open" | "allowlist" | "disabled";
|
||||
groups?: Record<string, { allow?: boolean; enabled?: boolean }>;
|
||||
messagePrefix?: string;
|
||||
};
|
||||
|
||||
@@ -87,6 +89,8 @@ export type ZalouserConfig = {
|
||||
defaultAccount?: string;
|
||||
dmPolicy?: "pairing" | "allowlist" | "open" | "disabled";
|
||||
allowFrom?: Array<string | number>;
|
||||
groupPolicy?: "open" | "allowlist" | "disabled";
|
||||
groups?: Record<string, { allow?: boolean; enabled?: boolean }>;
|
||||
messagePrefix?: string;
|
||||
accounts?: Record<string, ZalouserAccountConfig>;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user