fix(whatsapp): handle group mentions and caller allowlists

This commit is contained in:
xmanrui 2026-08-26 17:07:29 +08:00
parent 54468bbe1e
commit 9664e85599
13 changed files with 563 additions and 214 deletions

View file

@ -129,7 +129,7 @@ Use the proxy URL required by your network and restart the Host after changing i
Each Telegram bot has its own access-mode control on its bot card. Existing and newly connected bots both default to **Compatible mode**: DMs receive replies, while group messages require a mention of or reply to the bot. Restrictions apply only after explicitly switching that bot to **Safe mode (private-chat allowlist)**. Safe mode ignores every group message and admits only numeric User IDs in that bot's allowlist. Enter one ID per line. Switching back to Compatible mode retains the allowlist without enforcing it, so it is available when Safe mode is enabled again. An empty allowlist in Safe mode rejects all inbound messages for that bot.
Each WhatsApp bot also has its own access mode. Existing bots migrate to **Only me**, which is also the default for newly linked bots and accepts only self-chat messages from the linked account. **Selected contacts** additionally accepts direct messages from allowlisted phone numbers and ignores groups. Enter one number with its country or region code per line; a leading `+` is optional. **Open responses** accepts all direct messages, group messages sent by the linked account, and mentions of or replies to that account from other group members; this also lets an owner-only group act as a separate conversation. Switching modes retains the allowlist. An empty Selected contacts allowlist behaves like Only me, and rejected messages are ignored silently.
Each WhatsApp bot also has its own access mode. Existing bots migrate to **Only me**, which is also the default for newly linked bots and accepts only self-chat messages from the linked account. **Selected contacts** additionally accepts direct messages from phone numbers in its list and ignores groups. **Open responses** accepts all direct messages, group messages sent by the linked account, and mentions of or replies to that account from members in its separate group number list. Leaving the group number list empty allows every group member; this also lets an owner-only group act as a separate conversation. The two number lists are stored independently. Enter one number with its country or region code per line; a leading `+` is optional. An empty Selected contacts list behaves like Only me, and rejected messages are ignored silently.
## Bot commands
@ -194,7 +194,7 @@ If the Slack desktop app has no native Slash Command registered with the same na
- `/session` accepts exactly one Session ID obtained from `/sessionlist`. It neither creates a session nor immediately prompts the model; later messages in the current chat continue the bound session. Regular archived sessions can be bound without being unarchived, while subagent sessions cannot be bound.
- `/session` locates the session's unique workspace automatically. Binding inside the current workspace replaces only this chat's mapping. A cross-workspace binding switches the bot workspace, clears the old session mappings for all of that bot's chats, and then binds this chat, so it affects the bot's other chats. A reply already being generated may still finish.
- Workspace switches and session bindings only clear or replace dsh-im chat mappings. They never delete, empty, or archive old Session contents; an old Session can still be listed and bound again.
- Any user admitted by the current channel access policy can run these commands; there is no separate administrator role. Telegram Compatible mode follows the original DM and group mention/reply rules, while Safe mode admits only allowlisted private users. WhatsApp Only me accepts self-chat only, Selected contacts accepts self-chat plus allowlisted direct messages, and Open responses accepts every direct message, group messages from the linked account, and mentions or replies from other group members.
- Any user admitted by the current channel access policy can run these commands; there is no separate administrator role. Telegram Compatible mode follows the original DM and group mention/reply rules, while Safe mode admits only allowlisted private users. WhatsApp Only me accepts self-chat only, Selected contacts accepts self-chat plus allowlisted direct messages, and Open responses accepts every direct message, group messages from the linked account, and mentions or replies from listed group members; an empty number list allows every group member.
- Agent Preset names and IDs come from the same Harness Host, and any command-authorized user can change the Preset used by all future new Sessions across this bot's chats. Expose `/presetlist` and `/preset` only to trusted users.
- The list comes from the Harness Host's global registry and can include local absolute paths for other bots, other channels, or non-IM projects. Restrict the bot's visibility to trusted users.
- Session results also come from the global Harness Host. Session IDs and titles can belong to other bots, other channels, or non-IM projects, and may contain sensitive metadata. Enable these commands only when every user in the bot's visibility scope is trusted.

View file

@ -132,7 +132,7 @@ dsh web
每个 Telegram 机器人都可以在自己的卡片中切换访问模式。旧机器人和新接入机器人均默认使用**兼容模式**:私聊直接响应,群聊仅在提及机器人或回复机器人消息时响应。只有主动切换到**安全模式(私聊白名单)**后,机器人才会忽略全部群聊,并只接受该机器人白名单中的数字 User ID。白名单每行一个 ID、按机器人独立保存;切回兼容模式时会保留但不使用,再切回安全模式即可继续使用。安全模式的空白名单会拒绝该机器人的所有入站消息。
每个 WhatsApp 机器人也有独立的访问模式。旧机器人升级后和新接入机器人都默认使用**仅自己模式**,只响应已绑定账号的自聊消息。**指定联系人模式**额外接受白名单电话号码的私聊并忽略群聊;号码需包含国家或地区代码,每行一个,可带开头的 `+`。**开放响应模式**响应所有私聊、已绑定账号自己发出的群聊消息,以及其他群成员对该账号的提及或回复;因此也可以把“仅自己”的群当作独立会话使用。切换模式会保留白名单;指定联系人模式的空白名单等同于仅自己模式。未授权消息会被静默忽略。
每个 WhatsApp 机器人也有独立的访问模式。旧机器人升级后和新接入机器人都默认使用**仅自己模式**,只响应已绑定账号的自聊消息。**指定联系人模式**额外接受其号码列表中联系人的私聊并忽略群聊。**开放响应模式**响应所有私聊、已绑定账号自己发出的群聊消息,以及其独立群聊号码列表中成员对该账号的提及或回复;群聊号码列表留空时允许所有群成员,因此也可以把“仅自己”的群当作独立会话使用。两个号码列表分别保存、互不影响;号码需包含国家或地区代码,每行一个,可带开头的 `+`。指定联系人模式的空列表等同于仅自己模式。未授权消息会被静默忽略。
## 机器人命令
@ -197,7 +197,7 @@ Slack 桌面端若未注册同名的原生 Slash Command,会拦截直接以 `/
- `/session` 只接受一个由 `/sessionlist` 获得的 Session ID。它不会新建会话或立即向模型发送消息;绑定成功后,当前聊天的后续消息会继续该会话。普通归档会话可以绑定但不会自动取消归档,子代理会话不能绑定。
- `/session` 会自动定位会话唯一所属的工作区。同工作区绑定只替换当前聊天的映射;跨工作区绑定会切换该机器人的工作区、清除该机器人所有聊天的旧会话映射,再绑定当前聊天,因此会影响该机器人的其他聊天。已经开始生成的回复仍可完成。
- 工作区切换和会话绑定只会清除或替换 dsh-im 的聊天映射,不会删除、清空或归档任何旧 Session 内容;旧 Session 仍可再次列出和绑定。
- 任何通过当前渠道访问策略的用户都可以执行这些命令,不另行区分管理员和普通用户。Telegram 兼容模式遵循原有私聊及群聊提及/回复规则;安全模式只允许当前机器人白名单中的私聊用户执行。WhatsApp 仅自己模式只接受自聊,指定联系人模式接受自聊和白名单私聊,开放响应模式接受所有私聊、已绑定账号自己发出的群聊消息,以及其他群成员的提及或回复。
- 任何通过当前渠道访问策略的用户都可以执行这些命令,不另行区分管理员和普通用户。Telegram 兼容模式遵循原有私聊及群聊提及/回复规则;安全模式只允许当前机器人白名单中的私聊用户执行。WhatsApp 仅自己模式只接受自聊,指定联系人模式接受自聊和白名单私聊,开放响应模式接受所有私聊、已绑定账号自己发出的群聊消息,以及允许号码对应群成员的提及或回复;号码列表留空时允许所有群成员。
- Agent Preset 名称和 ID 来自同一个 Harness Host,且任何有命令权限的用户都能修改该机器人所有聊天未来新 Session 的 Preset;请只向可信用户开放 `/presetlist` 和 `/preset`。
- 工作区列表来自 Harness Host 的全局登记信息,可能包含其他机器人、其他渠道或非 IM 项目的本机绝对路径。请将机器人可见范围限制给可信用户。
- 会话列表同样来自该全局 Harness Host;会话 ID 和标题可能属于其他机器人、其他渠道或非 IM 项目,并可能包含敏感元数据。开放命令前请确保所有可见用户都可信。

View file

@ -681,9 +681,12 @@ var EN = Object.freeze({
"\u53EA\u54CD\u5E94\u5DF2\u7ED1\u5B9A WhatsApp \u8D26\u53F7\u7684\u81EA\u804A\u6D88\u606F\u3002": "Only respond to self-chat messages from the linked WhatsApp account.",
"\u54CD\u5E94\u81EA\u804A\u548C\u767D\u540D\u5355\u8054\u7CFB\u4EBA\u7684\u79C1\u804A\uFF0C\u5FFD\u7565\u7FA4\u804A\u3002": "Respond to self-chat and allowlisted direct messages; ignore group messages.",
"\u54CD\u5E94\u6240\u6709\u79C1\u804A\u3001\u5DF2\u7ED1\u5B9A\u8D26\u53F7\u81EA\u5DF1\u53D1\u51FA\u7684\u7FA4\u804A\u6D88\u606F\uFF0C\u4EE5\u53CA\u5176\u4ED6\u7FA4\u6210\u5458\u7684\u63D0\u53CA\u6216\u56DE\u590D\u3002": "Respond to all direct messages, group messages sent by the linked account, and mentions or replies from other group members.",
"\u54CD\u5E94\u6240\u6709\u79C1\u804A\u3001\u5DF2\u7ED1\u5B9A\u8D26\u53F7\u81EA\u5DF1\u53D1\u51FA\u7684\u7FA4\u804A\u6D88\u606F\uFF0C\u4EE5\u53CA\u5141\u8BB8\u6210\u5458\u7684\u63D0\u53CA\u6216\u56DE\u590D\uFF1B\u7FA4\u804A\u53F7\u7801\u5217\u8868\u7559\u7A7A\u65F6\u5141\u8BB8\u6240\u6709\u7FA4\u6210\u5458\u3002": "Respond to all direct messages, group messages sent by the linked account, and mentions or replies from allowed members; leaving the group number list empty allows every group member.",
"\u5141\u8BB8\u79C1\u804A\u7684 WhatsApp \u7535\u8BDD\u53F7\u7801": "WhatsApp phone numbers allowed to send direct messages",
"\u5141\u8BB8\u5728\u7FA4\u804A\u4E2D\u547C\u53EB\u673A\u5668\u4EBA\u7684 WhatsApp \u7535\u8BDD\u53F7\u7801": "WhatsApp phone numbers allowed to call the bot in group chats",
"\u6BCF\u884C\u4E00\u4E2A\u542B\u56FD\u5BB6\u6216\u5730\u533A\u4EE3\u7801\u7684\u53F7\u7801": "One number with country or region code per line",
"\u53EF\u4EE5\u5305\u542B\u5F00\u5934\u7684 +\uFF0C\u4FDD\u5B58\u65F6\u4F1A\u81EA\u52A8\u79FB\u9664\u3002": "A leading + is allowed and removed when saved.",
"\u7559\u7A7A\u8868\u793A\u6240\u6709\u7FA4\u6210\u5458\u90FD\u53EF\u4EE5\u901A\u8FC7\u63D0\u53CA\u6216\u56DE\u590D\u547C\u53EB\u673A\u5668\u4EBA\u3002": "Leave empty to let every group member call the bot by mentioning it or replying to it.",
"\u4EC5\u6307\u5B9A\u8054\u7CFB\u4EBA\u6A21\u5F0F\u4F7F\u7528\u767D\u540D\u5355\uFF0C\u5207\u6362\u6A21\u5F0F\u65F6\u4F1A\u4FDD\u7559\u3002": "Only Selected contacts uses the allowlist; it is retained when modes change.",
"\u767D\u540D\u5355\u4E3A\u7A7A\uFF1B\u4FDD\u5B58\u540E\u5C06\u53EA\u63A5\u53D7\u81EA\u804A\u6D88\u606F\u3002": "The allowlist is empty; only self-chat messages will be accepted after saving.",
"\u7535\u8BDD\u53F7\u7801\u5FC5\u987B\u5305\u542B\u56FD\u5BB6\u6216\u5730\u533A\u4EE3\u7801\uFF0C\u6BCF\u884C\u4E00\u4E2A\u3002": "Each phone number must include a country or region code on its own line.",
@ -9406,7 +9409,8 @@ function normalizeBot6(value) {
accessMode: ["self-only", "private-allowlist", "open"].includes(
value.accessPolicy?.accessMode
) ? value.accessPolicy.accessMode : "self-only",
allowedNumbers: Array.isArray(value.accessPolicy?.allowedNumbers) ? [...new Set(value.accessPolicy.allowedNumbers.filter((entry) => typeof entry === "string" && /^[1-9]\d{4,14}$/.test(entry)))] : []
allowedNumbers: Array.isArray(value.accessPolicy?.allowedNumbers) ? [...new Set(value.accessPolicy.allowedNumbers.filter((entry) => typeof entry === "string" && /^[1-9]\d{4,14}$/.test(entry)))] : [],
groupAllowedNumbers: Array.isArray(value.accessPolicy?.groupAllowedNumbers) ? [...new Set(value.accessPolicy.groupAllowedNumbers.filter((entry) => typeof entry === "string" && /^[1-9]\d{4,14}$/.test(entry)))] : []
},
bot: {
name: text5(value.bot?.name, "WhatsApp\u673A\u5668\u4EBA", 100),
@ -9505,7 +9509,8 @@ function accessPolicyFor(account) {
) ? account.accessPolicy.accessMode : "self-only";
return {
accessMode,
allowedNumbers: Array.isArray(account?.accessPolicy?.allowedNumbers) ? account.accessPolicy.allowedNumbers : []
allowedNumbers: Array.isArray(account?.accessPolicy?.allowedNumbers) ? account.accessPolicy.allowedNumbers : [],
groupAllowedNumbers: Array.isArray(account?.accessPolicy?.groupAllowedNumbers) ? account.accessPolicy.groupAllowedNumbers : []
};
}
function allowedNumbersFromText(value) {
@ -9519,27 +9524,36 @@ function allowedNumbersFromText(value) {
function WhatsappAccessSettings({ account, busy = false, onSave }) {
const policy = accessPolicyFor(account);
const sourceNumbers = policy.allowedNumbers.join("\n");
const sourceGroupNumbers = policy.groupAllowedNumbers.join("\n");
const helpId = React19.useId();
const [accessMode, setAccessMode] = React19.useState(policy.accessMode);
const [allowedNumbers, setAllowedNumbers] = React19.useState(sourceNumbers);
const [groupAllowedNumbers, setGroupAllowedNumbers] = React19.useState(sourceGroupNumbers);
const [error, setError] = React19.useState(null);
React19.useEffect(() => {
setAccessMode(policy.accessMode);
setAllowedNumbers(sourceNumbers);
setGroupAllowedNumbers(sourceGroupNumbers);
setError(null);
}, [policy.accessMode, sourceNumbers]);
}, [policy.accessMode, sourceNumbers, sourceGroupNumbers]);
const save = async (event) => {
event.preventDefault();
setError(null);
try {
const normalized = allowedNumbersFromText(allowedNumbers);
const normalizedGroup = allowedNumbersFromText(groupAllowedNumbers);
if (typeof onSave !== "function") throw new Error("WhatsApp \u8BBF\u95EE\u8BBE\u7F6E\u6682\u4E0D\u53EF\u7528\u3002");
await onSave({ accessMode, allowedNumbers: normalized });
await onSave({
accessMode,
allowedNumbers: normalized,
groupAllowedNumbers: normalizedGroup
});
} catch (caught) {
setError(caught?.message ?? "WhatsApp \u8BBF\u95EE\u8BBE\u7F6E\u4FDD\u5B58\u5931\u8D25\u3002");
}
};
const allowlistEnabled = accessMode === "private-allowlist";
const groupAllowlistEnabled = accessMode === "open";
const labels = {
"self-only": "\u4EC5\u81EA\u5DF1\u6A21\u5F0F",
"private-allowlist": "\u6307\u5B9A\u8054\u7CFB\u4EBA\u6A21\u5F0F",
@ -9588,7 +9602,7 @@ function WhatsappAccessSettings({ account, busy = false, onSave }) {
"span",
{ className: "dwa-accessTooltipItem" },
h2("strong", null, "\u5F00\u653E\u54CD\u5E94\u6A21\u5F0F"),
h2("span", null, "\u54CD\u5E94\u6240\u6709\u79C1\u804A\u3001\u5DF2\u7ED1\u5B9A\u8D26\u53F7\u81EA\u5DF1\u53D1\u51FA\u7684\u7FA4\u804A\u6D88\u606F\uFF0C\u4EE5\u53CA\u5176\u4ED6\u7FA4\u6210\u5458\u7684\u63D0\u53CA\u6216\u56DE\u590D\u3002")
h2("span", null, "\u54CD\u5E94\u6240\u6709\u79C1\u804A\u3001\u5DF2\u7ED1\u5B9A\u8D26\u53F7\u81EA\u5DF1\u53D1\u51FA\u7684\u7FA4\u804A\u6D88\u606F\uFF0C\u4EE5\u53CA\u5141\u8BB8\u6210\u5458\u7684\u63D0\u53CA\u6216\u56DE\u590D\uFF1B\u7FA4\u804A\u53F7\u7801\u5217\u8868\u7559\u7A7A\u65F6\u5141\u8BB8\u6240\u6709\u7FA4\u6210\u5458\u3002")
)
)
)
@ -9614,22 +9628,23 @@ function WhatsappAccessSettings({ account, busy = false, onSave }) {
h2("option", { value: "open" }, "\u5F00\u653E\u54CD\u5E94\u6A21\u5F0F")
)
),
allowlistEnabled ? h2(
allowlistEnabled || groupAllowlistEnabled ? h2(
"label",
{ className: "dwa-accessField" },
h2("span", null, "\u5141\u8BB8\u79C1\u804A\u7684 WhatsApp \u7535\u8BDD\u53F7\u7801"),
h2("span", null, allowlistEnabled ? "\u5141\u8BB8\u79C1\u804A\u7684 WhatsApp \u7535\u8BDD\u53F7\u7801" : "\u5141\u8BB8\u5728\u7FA4\u804A\u4E2D\u547C\u53EB\u673A\u5668\u4EBA\u7684 WhatsApp \u7535\u8BDD\u53F7\u7801"),
h2("textarea", {
value: allowedNumbers,
value: allowlistEnabled ? allowedNumbers : groupAllowedNumbers,
disabled: busy,
rows: 3,
placeholder: "\u6BCF\u884C\u4E00\u4E2A\u542B\u56FD\u5BB6\u6216\u5730\u533A\u4EE3\u7801\u7684\u53F7\u7801",
"aria-label": "\u5141\u8BB8\u79C1\u804A\u7684 WhatsApp \u7535\u8BDD\u53F7\u7801",
"aria-label": allowlistEnabled ? "\u5141\u8BB8\u79C1\u804A\u7684 WhatsApp \u7535\u8BDD\u53F7\u7801" : "\u5141\u8BB8\u5728\u7FA4\u804A\u4E2D\u547C\u53EB\u673A\u5668\u4EBA\u7684 WhatsApp \u7535\u8BDD\u53F7\u7801",
onChange: (event) => {
setAllowedNumbers(event.target.value);
if (allowlistEnabled) setAllowedNumbers(event.target.value);
else setGroupAllowedNumbers(event.target.value);
setError(null);
}
}),
h2("small", null, "\u53EF\u4EE5\u5305\u542B\u5F00\u5934\u7684 +\uFF0C\u4FDD\u5B58\u65F6\u4F1A\u81EA\u52A8\u79FB\u9664\u3002")
h2("small", null, allowlistEnabled ? "\u53EF\u4EE5\u5305\u542B\u5F00\u5934\u7684 +\uFF0C\u4FDD\u5B58\u65F6\u4F1A\u81EA\u52A8\u79FB\u9664\u3002" : "\u7559\u7A7A\u8868\u793A\u6240\u6709\u7FA4\u6210\u5458\u90FD\u53EF\u4EE5\u901A\u8FC7\u63D0\u53CA\u6216\u56DE\u590D\u547C\u53EB\u673A\u5668\u4EBA\u3002")
) : null,
allowlistEnabled && allowedNumbers.trim() === "" ? h2(
"p",

File diff suppressed because one or more lines are too long

View file

@ -97,6 +97,11 @@ function normalizeBot(value) {
typeof entry === 'string' && /^[1-9]\d{4,14}$/.test(entry)
)))]
: [],
groupAllowedNumbers: Array.isArray(value.accessPolicy?.groupAllowedNumbers)
? [...new Set(value.accessPolicy.groupAllowedNumbers.filter((entry) => (
typeof entry === 'string' && /^[1-9]\d{4,14}$/.test(entry)
)))]
: [],
},
bot: {
name: text(value.bot?.name, 'WhatsApp机器人', 100),

View file

@ -37,6 +37,8 @@ function accessPolicyFor(account) {
accessMode,
allowedNumbers: Array.isArray(account?.accessPolicy?.allowedNumbers)
? account.accessPolicy.allowedNumbers : [],
groupAllowedNumbers: Array.isArray(account?.accessPolicy?.groupAllowedNumbers)
? account.accessPolicy.groupAllowedNumbers : [],
};
}
@ -52,30 +54,39 @@ function allowedNumbersFromText(value) {
export function WhatsappAccessSettings({ account, busy = false, onSave }) {
const policy = accessPolicyFor(account);
const sourceNumbers = policy.allowedNumbers.join('\n');
const sourceGroupNumbers = policy.groupAllowedNumbers.join('\n');
const helpId = React.useId();
const [accessMode, setAccessMode] = React.useState(policy.accessMode);
const [allowedNumbers, setAllowedNumbers] = React.useState(sourceNumbers);
const [groupAllowedNumbers, setGroupAllowedNumbers] = React.useState(sourceGroupNumbers);
const [error, setError] = React.useState(null);
React.useEffect(() => {
setAccessMode(policy.accessMode);
setAllowedNumbers(sourceNumbers);
setGroupAllowedNumbers(sourceGroupNumbers);
setError(null);
}, [policy.accessMode, sourceNumbers]);
}, [policy.accessMode, sourceNumbers, sourceGroupNumbers]);
const save = async (event) => {
event.preventDefault();
setError(null);
try {
const normalized = allowedNumbersFromText(allowedNumbers);
const normalizedGroup = allowedNumbersFromText(groupAllowedNumbers);
if (typeof onSave !== 'function') throw new Error('WhatsApp 访问设置暂不可用。');
await onSave({ accessMode, allowedNumbers: normalized });
await onSave({
accessMode,
allowedNumbers: normalized,
groupAllowedNumbers: normalizedGroup,
});
} catch (caught) {
setError(caught?.message ?? 'WhatsApp 访问设置保存失败。');
}
};
const allowlistEnabled = accessMode === 'private-allowlist';
const groupAllowlistEnabled = accessMode === 'open';
const labels = {
'self-only': '仅自己模式',
'private-allowlist': '指定联系人模式',
@ -103,7 +114,7 @@ export function WhatsappAccessSettings({ account, busy = false, onSave }) {
h('span', null, '响应自聊和白名单联系人的私聊,忽略群聊。')),
h('span', { className: 'dwa-accessTooltipItem' },
h('strong', null, '开放响应模式'),
h('span', null, '响应所有私聊、已绑定账号自己发出的群聊消息,以及其他群成员的提及或回复。')))))),
h('span', null, '响应所有私聊、已绑定账号自己发出的群聊消息,以及允许成员的提及或回复;群聊号码列表留空时允许所有群成员。')))))),
h('label', { className: 'dwa-accessField' },
h('span', null, '模式'),
h('select', {
@ -115,18 +126,28 @@ export function WhatsappAccessSettings({ account, busy = false, onSave }) {
h('option', { value: 'self-only' }, '仅自己模式(默认)'),
h('option', { value: 'private-allowlist' }, '指定联系人模式'),
h('option', { value: 'open' }, '开放响应模式'))),
allowlistEnabled
(allowlistEnabled || groupAllowlistEnabled)
? h('label', { className: 'dwa-accessField' },
h('span', null, '允许私聊的 WhatsApp 电话号码'),
h('span', null, allowlistEnabled
? '允许私聊的 WhatsApp 电话号码'
: '允许在群聊中呼叫机器人的 WhatsApp 电话号码'),
h('textarea', {
value: allowedNumbers,
value: allowlistEnabled ? allowedNumbers : groupAllowedNumbers,
disabled: busy,
rows: 3,
placeholder: '每行一个含国家或地区代码的号码',
'aria-label': '允许私聊的 WhatsApp 电话号码',
onChange: (event) => { setAllowedNumbers(event.target.value); setError(null); },
'aria-label': allowlistEnabled
? '允许私聊的 WhatsApp 电话号码'
: '允许在群聊中呼叫机器人的 WhatsApp 电话号码',
onChange: (event) => {
if (allowlistEnabled) setAllowedNumbers(event.target.value);
else setGroupAllowedNumbers(event.target.value);
setError(null);
},
}),
h('small', null, '可以包含开头的 +,保存时会自动移除。'))
h('small', null, allowlistEnabled
? '可以包含开头的 +,保存时会自动移除。'
: '留空表示所有群成员都可以通过提及或回复呼叫机器人。'))
: null,
allowlistEnabled && allowedNumbers.trim() === ''
? h('p', { className: 'dwa-accessWarning', role: 'status' },

View file

@ -380,9 +380,12 @@ const EN = Object.freeze({
'只响应已绑定 WhatsApp 账号的自聊消息。': 'Only respond to self-chat messages from the linked WhatsApp account.',
'响应自聊和白名单联系人的私聊,忽略群聊。': 'Respond to self-chat and allowlisted direct messages; ignore group messages.',
'响应所有私聊、已绑定账号自己发出的群聊消息,以及其他群成员的提及或回复。': 'Respond to all direct messages, group messages sent by the linked account, and mentions or replies from other group members.',
'响应所有私聊、已绑定账号自己发出的群聊消息,以及允许成员的提及或回复;群聊号码列表留空时允许所有群成员。': 'Respond to all direct messages, group messages sent by the linked account, and mentions or replies from allowed members; leaving the group number list empty allows every group member.',
'允许私聊的 WhatsApp 电话号码': 'WhatsApp phone numbers allowed to send direct messages',
'允许在群聊中呼叫机器人的 WhatsApp 电话号码': 'WhatsApp phone numbers allowed to call the bot in group chats',
'每行一个含国家或地区代码的号码': 'One number with country or region code per line',
'可以包含开头的 +,保存时会自动移除。': 'A leading + is allowed and removed when saved.',
'留空表示所有群成员都可以通过提及或回复呼叫机器人。': 'Leave empty to let every group member call the bot by mentioning it or replying to it.',
'仅指定联系人模式使用白名单,切换模式时会保留。': 'Only Selected contacts uses the allowlist; it is retained when modes change.',
'白名单为空;保存后将只接受自聊消息。': 'The allowlist is empty; only self-chat messages will be accepted after saving.',
'电话号码必须包含国家或地区代码,每行一个。': 'Each phone number must include a country or region code on its own line.',

View file

@ -53,8 +53,8 @@ function payloadFailure(endpoint, payload) {
&& payload.confirm === true ? null : 'bot.delete requires a botId and confirm=true.';
}
if (endpoint === WHATSAPP_ENDPOINTS.setAccessPolicy) {
if (!exactKeys(payload, ['botId', 'accessMode', 'allowedNumbers'])
|| Object.keys(payload).length !== 3
if (!exactKeys(payload, ['botId', 'accessMode', 'allowedNumbers', 'groupAllowedNumbers'])
|| Object.keys(payload).length !== 4
|| !validId(payload.botId)) return '请输入有效的 WhatsApp 访问模式和电话号码。';
try {
normalizeWhatsappAccessPolicy(payload);

View file

@ -63,6 +63,7 @@ export function normalizeWhatsappAccessPolicy(value = {}) {
return Object.freeze({
accessMode,
allowedNumbers: normalizeWhatsappAllowedNumbers(value.allowedNumbers),
groupAllowedNumbers: normalizeWhatsappAllowedNumbers(value.groupAllowedNumbers),
});
}

View file

@ -334,6 +334,7 @@ export class WhatsappController {
connectedAt: new Date().toISOString(),
accessMode: previous?.accessMode,
allowedNumbers: previous?.allowedNumbers,
groupAllowedNumbers: previous?.groupAllowedNumbers,
};
try {
if (record.controller.signal.aborted || this.#closed) throw Object.assign(new Error(), { name: 'AbortError' });

View file

@ -3,6 +3,7 @@ import { createHash, randomBytes } from 'node:crypto';
import {
areJidsSameUser,
downloadMediaMessage,
jidNormalizedUser,
normalizeMessageContent,
} from '@whiskeysockets/baileys';
@ -13,6 +14,7 @@ import { trackOutboundArtifactProviderPromise } from '../shared/semantic/artifac
import { createWhatsappBridgeStatus, WhatsappHarnessBridge } from './whatsapp-bridge.mjs';
import {
WHATSAPP_ACCESS_MODES,
normalizeWhatsappAccountJid,
normalizeWhatsappAccessPolicy,
} from './config-store.mjs';
import { createWhatsappWebSession } from './whatsapp-web-session.mjs';
@ -204,8 +206,21 @@ export function createWhatsappMediaDownloader({
});
}
function whatsappAccountMatcher(accountJid, aliases) {
const accountJids = new Set(
[accountJid, ...(Array.isArray(aliases) ? aliases : [])]
.map((jid) => normalizeWhatsappAccountJid(jidNormalizedUser(jid)))
.filter(Boolean),
);
return (jid) => {
const normalized = normalizeWhatsappAccountJid(jidNormalizedUser(jid));
return normalized !== null && accountJids.has(normalized);
};
}
export function normalizeWhatsappMessage(message, accountJid, {
download = downloadMediaMessage,
accountAliases = [],
} = {}) {
const remoteJid = typeof message?.key?.remoteJid === 'string' ? message.key.remoteJid : '';
const alternateRemoteJid = typeof message?.key?.remoteJidAlt === 'string'
@ -215,8 +230,9 @@ export function normalizeWhatsappMessage(message, accountJid, {
|| remoteJid.endsWith('@newsletter')) return null;
const group = remoteJid.endsWith('@g.us');
const fromMe = message.key.fromMe === true;
const matchesAccount = whatsappAccountMatcher(accountJid, accountAliases);
const selfChat = fromMe && !group
&& [remoteJid, alternateRemoteJid].some((jid) => jid && areJidsSameUser(jid, accountJid));
&& [remoteJid, alternateRemoteJid].some(matchesAccount);
if (fromMe && !selfChat && !group) return null;
const senderJid = fromMe ? accountJid : group ? message.key.participant : remoteJid;
const senderAlternateJid = group && !fromMe ? message.key.participantAlt : alternateRemoteJid;
@ -225,9 +241,9 @@ export function normalizeWhatsappMessage(message, accountJid, {
const content = normalizeMessageContent(message.message);
const context = messageContext(content);
const mentioned = Array.isArray(context?.mentionedJid)
&& context.mentionedJid.some((jid) => areJidsSameUser(jid, accountJid));
&& context.mentionedJid.some(matchesAccount);
const replyToSelf = typeof context?.participant === 'string'
&& areJidsSameUser(context.participant, accountJid);
&& matchesAccount(context.participant);
const image = whatsappImageSource(message, content, download, { viewOnce });
const file = whatsappFileSource(message, content, download);
return {
@ -235,6 +251,7 @@ export function normalizeWhatsappMessage(message, accountJid, {
providerMessageId: messageId,
senderId: senderJid,
senderAlternateId: typeof senderAlternateJid === 'string' ? senderAlternateJid : '',
senderIsSelf: fromMe,
senderIsBot: false,
kind: group ? 'group' : 'direct',
conversationId: remoteJid,
@ -253,12 +270,22 @@ export function normalizeWhatsappMessage(message, accountJid, {
export function whatsappInboundAllowed(message, {
accessMode = WHATSAPP_ACCESS_MODES.selfOnly,
allowedNumbers = new Set(),
groupAllowedNumbers = new Set(),
} = {}) {
if (accessMode === WHATSAPP_ACCESS_MODES.open) return true;
if (accessMode === WHATSAPP_ACCESS_MODES.open) {
if (message?.kind !== 'group' || message.senderIsSelf === true) return true;
if (!(groupAllowedNumbers instanceof Set)) return false;
if (groupAllowedNumbers.size === 0) return true;
const senderJids = [message.senderId, message.senderAlternateId]
.filter((jid) => typeof jid === 'string' && jid.endsWith('@s.whatsapp.net'));
return [...groupAllowedNumbers].some((number) => senderJids.some((jid) => (
areJidsSameUser(jid, `${number}@s.whatsapp.net`)
)));
}
if (message?.kind !== 'direct') return false;
if (message.selfChat === true) return true;
if (accessMode !== WHATSAPP_ACCESS_MODES.privateAllowlist
|| !(allowedNumbers instanceof Set)) return false;
if (accessMode !== WHATSAPP_ACCESS_MODES.privateAllowlist) return false;
if (!(allowedNumbers instanceof Set)) return false;
const senderJids = [message.senderId, message.senderAlternateId]
.filter((jid) => typeof jid === 'string' && jid.endsWith('@s.whatsapp.net'));
return [...allowedNumbers].some((number) => senderJids.some((jid) => (
@ -542,6 +569,7 @@ export class WhatsappRuntime {
#mediaUploadTimeoutMs;
#accessMode;
#allowedPrivateNumbers;
#allowedGroupNumbers;
#createSession;
#status = createWhatsappRuntimeStatus();
#abortController = null;
@ -590,6 +618,7 @@ export class WhatsappRuntime {
const policy = normalizeWhatsappAccessPolicy(value);
this.#accessMode = policy.accessMode;
this.#allowedPrivateNumbers = new Set(policy.allowedNumbers);
this.#allowedGroupNumbers = new Set(policy.groupAllowedNumbers);
this.#config = { ...this.#config, ...policy };
return policy;
}
@ -624,7 +653,13 @@ export class WhatsappRuntime {
{ code: 'relink-required' },
)),
onMessage: async (raw, context) => {
const linkedAccount = context?.socket?.user;
const message = normalizeWhatsappMessage(raw, this.#config.accountJid, {
accountAliases: [
linkedAccount?.id,
linkedAccount?.lid,
linkedAccount?.phoneNumber,
],
download: createWhatsappMediaDownloader({
socket: context?.socket,
logger: this.#logger,
@ -635,6 +670,7 @@ export class WhatsappRuntime {
if (!whatsappInboundAllowed(message, {
accessMode: this.#accessMode,
allowedNumbers: this.#allowedPrivateNumbers,
groupAllowedNumbers: this.#allowedGroupNumbers,
})) {
this.#status.messagesRejected += 1;
this.#status.lastRejectedAt = new Date().toISOString();

View file

@ -14,6 +14,7 @@ import {
WhatsappAccountCard,
WhatsappSettingsTab,
} from '../../../plugin-src/client/channels/whatsapp/index.js';
import { normalizeSnapshot } from '../../../plugin-src/client/channels/whatsapp/api.js';
import { en, setImTranslator } from '../../../plugin-src/client/i18n.js';
const { act, create } = TestRenderer;
@ -79,13 +80,35 @@ test('WhatsApp account card uses the unified compact channel layout', () => {
assert.match(markup, /role="status"[^>]*>测试消息已发送/);
});
test('WhatsApp client keeps private and group number lists separate', () => {
const snapshot = normalizeSnapshot({
bots: [{
botId: 'whatsapp_test',
accessPolicy: {
accessMode: 'open',
allowedNumbers: ['16505550999'],
groupAllowedNumbers: ['33620607448'],
},
}],
});
assert.deepEqual(snapshot.bots[0].accessPolicy, {
accessMode: 'open',
allowedNumbers: ['16505550999'],
groupAllowedNumbers: ['33620607448'],
});
});
test('WhatsApp access settings save a normalized selected-contact allowlist', async () => {
const saved = [];
let renderer;
await act(async () => {
renderer = create(React.createElement(WhatsappAccessSettings, {
account: {
accessPolicy: { accessMode: 'self-only', allowedNumbers: [] },
accessPolicy: {
accessMode: 'self-only',
allowedNumbers: [],
groupAllowedNumbers: ['33620607448'],
},
},
onSave: async (value) => saved.push(value),
}));
@ -107,16 +130,54 @@ test('WhatsApp access settings save a normalized selected-contact allowlist', as
assert.deepEqual(saved, [{
accessMode: 'private-allowlist',
allowedNumbers: ['16505550999'],
groupAllowedNumbers: ['33620607448'],
}]);
await act(async () => { renderer.unmount(); });
});
test('WhatsApp access settings only show the allowlist for selected contacts', async () => {
test('WhatsApp access settings save a normalized open-mode group caller allowlist', async () => {
const saved = [];
let renderer;
await act(async () => {
renderer = create(React.createElement(WhatsappAccessSettings, {
account: {
accessPolicy: { accessMode: 'self-only', allowedNumbers: ['16505550999'] },
accessPolicy: {
accessMode: 'open',
allowedNumbers: ['16505550000'],
groupAllowedNumbers: [],
},
},
onSave: async (value) => saved.push(value),
}));
});
const textarea = renderer.root.findByProps({
'aria-label': '允许在群聊中呼叫机器人的 WhatsApp 电话号码',
});
await act(async () => {
textarea.props.onChange({ target: { value: '+16505550999\n16505550999' } });
});
await act(async () => {
renderer.root.findByType('form').props.onSubmit({ preventDefault() {} });
await flushMicrotasks();
});
assert.deepEqual(saved, [{
accessMode: 'open',
allowedNumbers: ['16505550000'],
groupAllowedNumbers: ['16505550999'],
}]);
await act(async () => { renderer.unmount(); });
});
test('WhatsApp access settings show mode-specific number allowlists', async () => {
let renderer;
await act(async () => {
renderer = create(React.createElement(WhatsappAccessSettings, {
account: {
accessPolicy: {
accessMode: 'self-only',
allowedNumbers: ['16505550999'],
groupAllowedNumbers: ['33620607448'],
},
},
onSave: async () => {},
}));
@ -125,16 +186,25 @@ test('WhatsApp access settings only show the allowlist for selected contacts', a
const allowlistFields = () => renderer.root.findAllByProps({
'aria-label': '允许私聊的 WhatsApp 电话号码',
});
const groupCallerFields = () => renderer.root.findAllByProps({
'aria-label': '允许在群聊中呼叫机器人的 WhatsApp 电话号码',
});
assert.equal(allowlistFields().length, 0);
assert.equal(groupCallerFields().length, 0);
await act(async () => {
select.props.onChange({ target: { value: 'private-allowlist' } });
});
assert.equal(allowlistFields().length, 1);
assert.equal(groupCallerFields().length, 0);
assert.equal(allowlistFields()[0].props.value, '16505550999');
await act(async () => {
select.props.onChange({ target: { value: 'open' } });
});
assert.equal(allowlistFields().length, 0);
assert.equal(groupCallerFields().length, 1);
assert.equal(groupCallerFields()[0].props.value, '33620607448');
assert.match(textOf(renderer.root), /留空表示所有群成员/);
await act(async () => { renderer.unmount(); });
});

View file

@ -40,6 +40,7 @@ import {
} from '../../../plugin-src/host/channels/whatsapp/rpc.mjs';
const ACCOUNT_JID = '16505550123@s.whatsapp.net';
const ACCOUNT_LID = '123456789012345@lid';
const AUTH_DIRECTORY = '7fe8c17e-4fb7-4c5b-a9dc-c36525575dd1';
function deferred() {
@ -140,6 +141,7 @@ function linkedConfig(overrides = {}) {
name: 'Harness WhatsApp',
accessMode: WHATSAPP_ACCESS_MODES.open,
allowedNumbers: [],
groupAllowedNumbers: [],
createdAt: new Date().toISOString(),
connectedAt: new Date().toISOString(),
...overrides,
@ -163,19 +165,31 @@ test('WhatsApp migrates existing bots to self-only mode and validates access pol
const legacy = linkedConfig();
delete legacy.accessMode;
delete legacy.allowedNumbers;
delete legacy.groupAllowedNumbers;
await writeFile(path, `${JSON.stringify({ version: 2, bots: [legacy] })}\n`);
const store = await new WhatsappConfigStore(path).load();
assert.deepEqual(store.get(legacy.botId), {
...legacy,
accessMode: WHATSAPP_ACCESS_MODES.selfOnly,
allowedNumbers: [],
groupAllowedNumbers: [],
});
assert.deepEqual(normalizeWhatsappAccessPolicy({
accessMode: WHATSAPP_ACCESS_MODES.privateAllowlist,
allowedNumbers: ['+16505550999', '16505550999'],
groupAllowedNumbers: ['+33620607448', '33620607448'],
}), {
accessMode: WHATSAPP_ACCESS_MODES.privateAllowlist,
allowedNumbers: ['16505550999'],
groupAllowedNumbers: ['33620607448'],
});
assert.deepEqual(normalizeWhatsappAccessPolicy({
accessMode: WHATSAPP_ACCESS_MODES.open,
allowedNumbers: ['16505550999'],
}), {
accessMode: WHATSAPP_ACCESS_MODES.open,
allowedNumbers: ['16505550999'],
groupAllowedNumbers: [],
});
assert.throws(() => normalizeWhatsappAccessPolicy({
accessMode: 'compatible',
@ -400,7 +414,57 @@ test('WhatsApp normalizes direct, linked-account, and explicitly mentioned group
}, ACCOUNT_JID), null);
});
test('WhatsApp access modes allow self-chat, selected contacts, or the existing open behavior', () => {
test('WhatsApp recognizes the linked account LID in group mentions and replies', () => {
const lidMention = normalizeWhatsappMessage({
key: {
remoteJid: '120363000000000002@g.us',
participant: '16505550999@s.whatsapp.net',
id: 'group-lid-mention',
fromMe: false,
},
message: {
extendedTextMessage: {
text: '@Harness question',
contextInfo: { mentionedJid: [`${ACCOUNT_LID.split('@')[0]}:9@lid`] },
},
},
}, ACCOUNT_JID, { accountAliases: [ACCOUNT_LID] });
assert.equal(lidMention.addressed, true);
const lidReply = normalizeWhatsappMessage({
key: {
remoteJid: '120363000000000002@g.us',
participant: '16505550999@s.whatsapp.net',
id: 'group-lid-reply',
fromMe: false,
},
message: {
extendedTextMessage: {
text: 'reply to the linked account',
contextInfo: { participant: `${ACCOUNT_LID.split('@')[0]}:9@lid` },
},
},
}, ACCOUNT_JID, { accountAliases: [ACCOUNT_LID] });
assert.equal(lidReply.addressed, true);
const unrelatedLidMention = normalizeWhatsappMessage({
key: {
remoteJid: '120363000000000002@g.us',
participant: '16505550999@s.whatsapp.net',
id: 'group-other-lid-mention',
fromMe: false,
},
message: {
extendedTextMessage: {
text: '@SomeoneElse question',
contextInfo: { mentionedJid: ['999999999999999@lid'] },
},
},
}, ACCOUNT_JID, { accountAliases: [ACCOUNT_LID] });
assert.equal(unrelatedLidMention.addressed, false);
});
test('WhatsApp access modes keep existing behavior and optionally restrict open group callers', () => {
const direct = normalizeWhatsappMessage({
key: {
remoteJid: '987654321098765@lid',
@ -431,6 +495,16 @@ test('WhatsApp access modes allow self-chat, selected contacts, or the existing
},
message: { conversation: 'hello from the linked account' },
}, ACCOUNT_JID);
const lidAddressedGroup = normalizeWhatsappMessage({
key: {
remoteJid: '120363000000000002@g.us',
participant: '987654321098765@lid',
participantAlt: '16505550999@s.whatsapp.net',
id: 'access-lid-group',
fromMe: false,
},
message: { conversation: 'hello from a LID-addressed member' },
}, ACCOUNT_JID);
assert.equal(whatsappInboundAllowed(selfChat), true);
assert.equal(whatsappInboundAllowed(direct), false);
@ -445,10 +519,34 @@ test('WhatsApp access modes allow self-chat, selected contacts, or the existing
allowedNumbers: new Set(['16505550000']),
}), false);
assert.equal(whatsappInboundAllowed(group, {
accessMode: WHATSAPP_ACCESS_MODES.privateAllowlist,
allowedNumbers: new Set(['16505550999']),
}), false);
assert.equal(whatsappInboundAllowed(direct, {
accessMode: WHATSAPP_ACCESS_MODES.open,
allowedNumbers: new Set(['16505550000']),
groupAllowedNumbers: new Set(['16505550001']),
}), true);
assert.equal(whatsappInboundAllowed(group, {
accessMode: WHATSAPP_ACCESS_MODES.open,
}), true);
assert.equal(whatsappInboundAllowed(group, {
accessMode: WHATSAPP_ACCESS_MODES.open,
allowedNumbers: new Set(['16505550000']),
groupAllowedNumbers: new Set(['16505550999']),
}), true);
assert.equal(whatsappInboundAllowed(group, {
accessMode: WHATSAPP_ACCESS_MODES.open,
allowedNumbers: new Set(['16505550999']),
groupAllowedNumbers: new Set(['16505550000']),
}), false);
assert.equal(whatsappInboundAllowed(lidAddressedGroup, {
accessMode: WHATSAPP_ACCESS_MODES.open,
groupAllowedNumbers: new Set(['16505550999']),
}), true);
assert.equal(whatsappInboundAllowed(linkedAccountGroup, {
accessMode: WHATSAPP_ACCESS_MODES.open,
groupAllowedNumbers: new Set(['16505550000']),
}), true);
});
@ -661,7 +759,11 @@ test('WhatsApp runtime filters messages before the bridge and applies policy upd
assert.equal(runtime.status.ready, true);
assert.equal(runtime.status.messagesRejected, 1);
assert.equal(calls.length, 0);
runtime.setAccessPolicy({ accessMode: WHATSAPP_ACCESS_MODES.open, allowedNumbers: [] });
runtime.setAccessPolicy({
accessMode: WHATSAPP_ACCESS_MODES.open,
allowedNumbers: [],
groupAllowedNumbers: [],
});
await callbacks.onMessage({
key: { remoteJid: '16505550999@s.whatsapp.net', id: 'direct-3', fromMe: false },
message: { conversation: 'hello again' },
@ -671,6 +773,95 @@ test('WhatsApp runtime filters messages before the bridge and applies policy upd
await runtime.stop();
});
test('WhatsApp open mode uses the account LID and restricts group callers by phone number', async (t) => {
const groupJid = '120363000000000003@g.us';
let callbacks;
let askCount = 0;
const sent = [];
const socket = {
user: {
id: '16505550123:9@s.whatsapp.net',
lid: `${ACCOUNT_LID.split('@')[0]}:9@lid`,
name: 'Harness WhatsApp',
},
sendPresenceUpdate: async () => {},
readMessages: async () => {},
sendMessage: async (jid, content, options = {}) => {
sent.push({ jid, content, options });
return { key: { id: options.messageId ?? 'group-lid-reply' } };
},
};
const runtime = new WhatsappRuntime({
config: linkedConfig({
accessMode: WHATSAPP_ACCESS_MODES.open,
allowedNumbers: ['16505550000'],
groupAllowedNumbers: ['16505550999'],
}),
authDir: '/tmp/test-whatsapp-lid-group-mention',
harness: {
ensureRunning: async () => {},
sessionExists: async () => true,
ask: async () => {
askCount += 1;
return 'Harness LID answer';
},
},
state: artifactState('session-lid-group-mention'),
createSession: async (options) => {
callbacks = options;
return {
socket,
ready: Promise.resolve({ accountJid: ACCOUNT_JID, name: 'Harness WhatsApp' }),
close: async () => {},
logout: async () => {},
};
},
});
t.after(() => runtime.stop());
await runtime.start();
const mentioned = {
key: {
remoteJid: groupJid,
participant: '987654321098765@lid',
participantAlt: '16505550999@s.whatsapp.net',
id: 'runtime-group-lid-mention',
fromMe: false,
},
message: {
extendedTextMessage: {
text: '@Harness question',
contextInfo: { mentionedJid: [ACCOUNT_LID] },
},
},
};
await callbacks.onMessage(mentioned, { socket });
assert.equal(askCount, 1);
assert.ok(sent.some(({ jid, content }) => (
jid === groupJid && content.text === 'Harness LID answer'
)));
await callbacks.onMessage({
key: {
remoteJid: groupJid,
participant: '999999999999999@lid',
participantAlt: '16505550000@s.whatsapp.net',
id: 'runtime-group-disallowed-caller',
fromMe: false,
},
message: {
extendedTextMessage: {
text: '@Harness disallowed question',
contextInfo: { mentionedJid: [ACCOUNT_LID] },
},
},
}, { socket });
assert.equal(askCount, 1);
assert.equal(runtime.status.messagesRejected, 1);
});
test('WhatsApp open mode answers linked-account group messages without processing reply echoes', async (t) => {
const groupJid = '120363000000000001@g.us';
let callbacks;
@ -1519,6 +1710,7 @@ test('WhatsApp QR controller and RPC keep the raw QR and linked identity host-on
setAccessPolicy: (value) => appliedPolicies.push({
accessMode: value.accessMode,
allowedNumbers: value.allowedNumbers,
groupAllowedNumbers: value.groupAllowedNumbers,
}),
}),
deleteAuth: async (name) => deletedAuth.push(name),
@ -1543,26 +1735,31 @@ test('WhatsApp QR controller and RPC keep the raw QR and linked identity host-on
assert.deepEqual(status.value.bots[0].accessPolicy, {
accessMode: WHATSAPP_ACCESS_MODES.selfOnly,
allowedNumbers: [],
groupAllowedNumbers: [],
});
assert.doesNotMatch(JSON.stringify(status.value), /16505550123@s\.whatsapp\.net|authDirectory/);
const updated = await handler(WHATSAPP_ENDPOINTS.setAccessPolicy, {
botId: status.value.bots[0].botId,
accessMode: WHATSAPP_ACCESS_MODES.privateAllowlist,
allowedNumbers: ['+16505550999'],
groupAllowedNumbers: ['+33620607448'],
});
assert.equal(updated.ok, true);
assert.deepEqual(updated.value.bots[0].accessPolicy, {
accessMode: WHATSAPP_ACCESS_MODES.privateAllowlist,
allowedNumbers: ['16505550999'],
groupAllowedNumbers: ['33620607448'],
});
assert.deepEqual(appliedPolicies, [{
accessMode: WHATSAPP_ACCESS_MODES.privateAllowlist,
allowedNumbers: ['16505550999'],
groupAllowedNumbers: ['33620607448'],
}]);
const invalidPolicy = await handler(WHATSAPP_ENDPOINTS.setAccessPolicy, {
botId: status.value.bots[0].botId,
accessMode: 'compatible',
allowedNumbers: [],
groupAllowedNumbers: [],
});
assert.equal(invalidPolicy.ok, false);
assert.equal(invalidPolicy.error.code, 'bad-request');