refactor: drop autoReply, add topic requireMention

Co-authored-by: kitze <kristijan.mkd@gmail.com>
This commit is contained in:
Peter Steinberger
2026-01-07 11:59:48 +01:00
parent 25edac96cf
commit 1011640a13
13 changed files with 80 additions and 76 deletions

View File

@@ -15,6 +15,7 @@
- Model config schema changes (auth profiles + model lists); doctor auto-migrates and the gateway rewrites legacy configs on startup. - Model config schema changes (auth profiles + model lists); doctor auto-migrates and the gateway rewrites legacy configs on startup.
- Commands: gate all slash commands to authorized senders; add `/compact` to manually compact session context. - Commands: gate all slash commands to authorized senders; add `/compact` to manually compact session context.
- Groups: `whatsapp.groups`, `telegram.groups`, and `imessage.groups` now act as allowlists when set. Add `"*"` to keep allow-all behavior. - Groups: `whatsapp.groups`, `telegram.groups`, and `imessage.groups` now act as allowlists when set. Add `"*"` to keep allow-all behavior.
- Auto-reply: removed `autoReply` from Discord/Slack/Telegram channel configs; use `requireMention` instead (Telegram topics now support `requireMention` overrides).
### Fixes ### Fixes
- Pairing: generate DM pairing codes with CSPRNG, expire pending codes after 1 hour, and avoid re-sending codes for already pending requests. - Pairing: generate DM pairing codes with CSPRNG, expire pending codes after 1 hour, and avoid re-sending codes for already pending requests.

View File

@@ -468,12 +468,16 @@ Set `telegram.enabled: false` to disable automatic startup.
dmPolicy: "pairing", // pairing | allowlist | open | disabled dmPolicy: "pairing", // pairing | allowlist | open | disabled
allowFrom: ["tg:123456789"], // optional; "open" requires ["*"] allowFrom: ["tg:123456789"], // optional; "open" requires ["*"]
groups: { groups: {
"*": { requireMention: true, autoReply: false }, "*": { requireMention: true },
"-1001234567890": { "-1001234567890": {
allowFrom: ["@admin"], allowFrom: ["@admin"],
systemPrompt: "Keep answers brief.", systemPrompt: "Keep answers brief.",
topics: { topics: {
"99": { skills: ["search"], systemPrompt: "Stay on topic." } "99": {
requireMention: false,
skills: ["search"],
systemPrompt: "Stay on topic."
}
} }
} }
}, },
@@ -580,7 +584,7 @@ Slack runs in Socket Mode and requires both a bot token and app token:
C123: { allow: true, requireMention: true }, C123: { allow: true, requireMention: true },
"#general": { "#general": {
allow: true, allow: true,
autoReply: false, requireMention: true,
users: ["U123"], users: ["U123"],
skills: ["docs"], skills: ["docs"],
systemPrompt: "Short answers only." systemPrompt: "Short answers only."

View File

@@ -202,8 +202,7 @@ Notes:
requireMention: true, requireMention: true,
users: ["987654321098765432"], users: ["987654321098765432"],
skills: ["search", "docs"], skills: ["search", "docs"],
systemPrompt: "Keep answers short.", systemPrompt: "Keep answers short."
autoReply: false
} }
} }
} }
@@ -227,7 +226,6 @@ Ack reactions are controlled globally via `messages.ackReaction` +
- `guilds.<id>.users`: optional per-guild user allowlist (ids or names). - `guilds.<id>.users`: optional per-guild user allowlist (ids or names).
- `guilds.<id>.channels.<channel>.allow`: allow/deny the channel when `groupPolicy="allowlist"`. - `guilds.<id>.channels.<channel>.allow`: allow/deny the channel when `groupPolicy="allowlist"`.
- `guilds.<id>.channels.<channel>.requireMention`: mention gating for the channel. - `guilds.<id>.channels.<channel>.requireMention`: mention gating for the channel.
- `guilds.<id>.channels.<channel>.autoReply`: if `true`, reply to all messages (overrides `requireMention`).
- `guilds.<id>.channels.<channel>.users`: optional per-channel user allowlist. - `guilds.<id>.channels.<channel>.users`: optional per-channel user allowlist.
- `guilds.<id>.channels.<channel>.skills`: skill filter (omit = all skills, empty = none). - `guilds.<id>.channels.<channel>.skills`: skill filter (omit = all skills, empty = none).
- `guilds.<id>.channels.<channel>.systemPrompt`: extra system prompt for the channel (combined with channel topic). - `guilds.<id>.channels.<channel>.systemPrompt`: extra system prompt for the channel (combined with channel topic).

View File

@@ -159,7 +159,7 @@ Slack uses Socket Mode only (no HTTP webhook server). Provide both tokens:
"C123": { "allow": true, "requireMention": true }, "C123": { "allow": true, "requireMention": true },
"#general": { "#general": {
"allow": true, "allow": true,
"autoReply": false, "requireMention": true,
"users": ["U123"], "users": ["U123"],
"skills": ["search", "docs"], "skills": ["search", "docs"],
"systemPrompt": "Keep answers short." "systemPrompt": "Keep answers short."
@@ -212,7 +212,6 @@ Ack reactions are controlled globally via `messages.ackReaction` +
Channel options (`slack.channels.<id>` or `slack.channels.<name>`): Channel options (`slack.channels.<id>` or `slack.channels.<name>`):
- `allow`: allow/deny the channel when `groupPolicy="allowlist"`. - `allow`: allow/deny the channel when `groupPolicy="allowlist"`.
- `requireMention`: mention gating for the channel. - `requireMention`: mention gating for the channel.
- `autoReply`: if `true`, reply to every message (overrides `requireMention`).
- `users`: optional per-channel user allowlist. - `users`: optional per-channel user allowlist.
- `skills`: skill filter (omit = all skills, empty = none). - `skills`: skill filter (omit = all skills, empty = none).
- `systemPrompt`: extra system prompt for the channel (combined with topic/purpose). - `systemPrompt`: extra system prompt for the channel (combined with topic/purpose).

View File

@@ -117,12 +117,12 @@ Provider options:
- `telegram.groupAllowFrom`: group sender allowlist (ids/usernames). - `telegram.groupAllowFrom`: group sender allowlist (ids/usernames).
- `telegram.groups`: per-group defaults + allowlist (use `"*"` for global defaults). - `telegram.groups`: per-group defaults + allowlist (use `"*"` for global defaults).
- `telegram.groups.<id>.requireMention`: mention gating default. - `telegram.groups.<id>.requireMention`: mention gating default.
- `telegram.groups.<id>.autoReply`: reply to every message (overrides `requireMention`).
- `telegram.groups.<id>.skills`: skill filter (omit = all skills, empty = none). - `telegram.groups.<id>.skills`: skill filter (omit = all skills, empty = none).
- `telegram.groups.<id>.allowFrom`: per-group sender allowlist override. - `telegram.groups.<id>.allowFrom`: per-group sender allowlist override.
- `telegram.groups.<id>.systemPrompt`: extra system prompt for the group. - `telegram.groups.<id>.systemPrompt`: extra system prompt for the group.
- `telegram.groups.<id>.enabled`: disable the group when `false`. - `telegram.groups.<id>.enabled`: disable the group when `false`.
- `telegram.groups.<id>.topics.<threadId>.*`: per-topic overrides (same fields as group). - `telegram.groups.<id>.topics.<threadId>.*`: per-topic overrides (same fields as group).
- `telegram.groups.<id>.topics.<threadId>.requireMention`: per-topic mention gating override.
- `telegram.replyToMode`: `off | first | all`. - `telegram.replyToMode`: `off | first | all`.
- `telegram.textChunkLimit`: outbound chunk size (chars). - `telegram.textChunkLimit`: outbound chunk size (chars).
- `telegram.streamMode`: `off | partial | block` (draft streaming). - `telegram.streamMode`: `off | partial | block` (draft streaming).

View File

@@ -44,11 +44,7 @@ function parseTelegramGroupId(value?: string | null) {
return { chatId: raw, topicId: undefined }; return { chatId: raw, topicId: undefined };
} }
function hasOwn(obj: unknown, key: string): boolean { function resolveTelegramRequireMention(params: {
return Boolean(obj && typeof obj === "object" && Object.hasOwn(obj, key));
}
function resolveTelegramAutoReply(params: {
cfg: ClawdbotConfig; cfg: ClawdbotConfig;
chatId?: string; chatId?: string;
topicId?: string; topicId?: string;
@@ -61,17 +57,17 @@ function resolveTelegramAutoReply(params: {
topicId && groupConfig?.topics ? groupConfig.topics[topicId] : undefined; topicId && groupConfig?.topics ? groupConfig.topics[topicId] : undefined;
const defaultTopicConfig = const defaultTopicConfig =
topicId && groupDefault?.topics ? groupDefault.topics[topicId] : undefined; topicId && groupDefault?.topics ? groupDefault.topics[topicId] : undefined;
if (hasOwn(topicConfig, "autoReply")) { if (typeof topicConfig?.requireMention === "boolean") {
return (topicConfig as { autoReply?: boolean }).autoReply; return topicConfig.requireMention;
} }
if (hasOwn(defaultTopicConfig, "autoReply")) { if (typeof defaultTopicConfig?.requireMention === "boolean") {
return (defaultTopicConfig as { autoReply?: boolean }).autoReply; return defaultTopicConfig.requireMention;
} }
if (hasOwn(groupConfig, "autoReply")) { if (typeof groupConfig?.requireMention === "boolean") {
return (groupConfig as { autoReply?: boolean }).autoReply; return groupConfig.requireMention;
} }
if (hasOwn(groupDefault, "autoReply")) { if (typeof groupDefault?.requireMention === "boolean") {
return (groupDefault as { autoReply?: boolean }).autoReply; return groupDefault.requireMention;
} }
return undefined; return undefined;
} }
@@ -107,8 +103,12 @@ export function resolveGroupRequireMention(params: {
const groupSpace = ctx.GroupSpace?.trim(); const groupSpace = ctx.GroupSpace?.trim();
if (provider === "telegram") { if (provider === "telegram") {
const { chatId, topicId } = parseTelegramGroupId(groupId); const { chatId, topicId } = parseTelegramGroupId(groupId);
const autoReply = resolveTelegramAutoReply({ cfg, chatId, topicId }); const requireMention = resolveTelegramRequireMention({
if (typeof autoReply === "boolean") return !autoReply; cfg,
chatId,
topicId,
});
if (typeof requireMention === "boolean") return requireMention;
return resolveProviderGroupRequireMention({ return resolveProviderGroupRequireMention({
cfg, cfg,
provider, provider,
@@ -138,9 +138,6 @@ export function resolveGroupRequireMention(params: {
(groupRoom (groupRoom
? channelEntries[normalizeDiscordSlug(groupRoom)] ? channelEntries[normalizeDiscordSlug(groupRoom)]
: undefined); : undefined);
if (entry && typeof entry.autoReply === "boolean") {
return !entry.autoReply;
}
if (entry && typeof entry.requireMention === "boolean") { if (entry && typeof entry.requireMention === "boolean") {
return entry.requireMention; return entry.requireMention;
} }
@@ -163,7 +160,7 @@ export function resolveGroupRequireMention(params: {
channelName ?? "", channelName ?? "",
normalizedName, normalizedName,
].filter(Boolean); ].filter(Boolean);
let matched: { requireMention?: boolean; autoReply?: boolean } | undefined; let matched: { requireMention?: boolean } | undefined;
for (const candidate of candidates) { for (const candidate of candidates) {
if (candidate && channels[candidate]) { if (candidate && channels[candidate]) {
matched = channels[candidate]; matched = channels[candidate];
@@ -172,9 +169,6 @@ export function resolveGroupRequireMention(params: {
} }
const fallback = channels["*"]; const fallback = channels["*"];
const resolved = matched ?? fallback; const resolved = matched ?? fallback;
if (typeof resolved?.autoReply === "boolean") {
return !resolved.autoReply;
}
if (typeof resolved?.requireMention === "boolean") { if (typeof resolved?.requireMention === "boolean") {
return resolved.requireMention; return resolved.requireMention;
} }

View File

@@ -237,12 +237,11 @@ export type TelegramActionConfig = {
}; };
export type TelegramTopicConfig = { export type TelegramTopicConfig = {
requireMention?: boolean;
/** If specified, only load these skills for this topic. Omit = all skills; empty = no skills. */ /** If specified, only load these skills for this topic. Omit = all skills; empty = no skills. */
skills?: string[]; skills?: string[];
/** If false, disable the bot for this topic. */ /** If false, disable the bot for this topic. */
enabled?: boolean; enabled?: boolean;
/** If true, reply to every message (no mention required). */
autoReply?: boolean;
/** Optional allowlist for topic senders (ids or usernames). */ /** Optional allowlist for topic senders (ids or usernames). */
allowFrom?: Array<string | number>; allowFrom?: Array<string | number>;
/** Optional system prompt snippet for this topic. */ /** Optional system prompt snippet for this topic. */
@@ -257,8 +256,6 @@ export type TelegramGroupConfig = {
topics?: Record<string, TelegramTopicConfig>; topics?: Record<string, TelegramTopicConfig>;
/** If false, disable the bot for this group (and its topics). */ /** If false, disable the bot for this group (and its topics). */
enabled?: boolean; enabled?: boolean;
/** If true, reply to every message (no mention required). */
autoReply?: boolean;
/** Optional allowlist for group senders (ids or usernames). */ /** Optional allowlist for group senders (ids or usernames). */
allowFrom?: Array<string | number>; allowFrom?: Array<string | number>;
/** Optional system prompt snippet for this group. */ /** Optional system prompt snippet for this group. */
@@ -325,8 +322,6 @@ export type DiscordGuildChannelConfig = {
skills?: string[]; skills?: string[];
/** If false, disable the bot for this channel. */ /** If false, disable the bot for this channel. */
enabled?: boolean; enabled?: boolean;
/** If true, reply to every message (no mention required). */
autoReply?: boolean;
/** Optional allowlist for channel senders (ids or names). */ /** Optional allowlist for channel senders (ids or names). */
users?: Array<string | number>; users?: Array<string | number>;
/** Optional system prompt snippet for this channel. */ /** Optional system prompt snippet for this channel. */
@@ -412,8 +407,6 @@ export type SlackChannelConfig = {
allow?: boolean; allow?: boolean;
/** Require mentioning the bot to trigger replies. */ /** Require mentioning the bot to trigger replies. */
requireMention?: boolean; requireMention?: boolean;
/** Reply to all messages without needing a mention. */
autoReply?: boolean;
/** Allowlist of users that can invoke the bot in this channel. */ /** Allowlist of users that can invoke the bot in this channel. */
users?: Array<string | number>; users?: Array<string | number>;
/** Optional skill filter for this channel. */ /** Optional skill filter for this channel. */

View File

@@ -787,7 +787,6 @@ export const ClawdbotSchema = z.object({
requireMention: z.boolean().optional(), requireMention: z.boolean().optional(),
skills: z.array(z.string()).optional(), skills: z.array(z.string()).optional(),
enabled: z.boolean().optional(), enabled: z.boolean().optional(),
autoReply: z.boolean().optional(),
allowFrom: z.array(z.union([z.string(), z.number()])).optional(), allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
systemPrompt: z.string().optional(), systemPrompt: z.string().optional(),
topics: z topics: z
@@ -795,9 +794,9 @@ export const ClawdbotSchema = z.object({
z.string(), z.string(),
z z
.object({ .object({
requireMention: z.boolean().optional(),
skills: z.array(z.string()).optional(), skills: z.array(z.string()).optional(),
enabled: z.boolean().optional(), enabled: z.boolean().optional(),
autoReply: z.boolean().optional(),
allowFrom: z allowFrom: z
.array(z.union([z.string(), z.number()])) .array(z.union([z.string(), z.number()]))
.optional(), .optional(),
@@ -913,7 +912,6 @@ export const ClawdbotSchema = z.object({
requireMention: z.boolean().optional(), requireMention: z.boolean().optional(),
skills: z.array(z.string()).optional(), skills: z.array(z.string()).optional(),
enabled: z.boolean().optional(), enabled: z.boolean().optional(),
autoReply: z.boolean().optional(),
users: z users: z
.array(z.union([z.string(), z.number()])) .array(z.union([z.string(), z.number()]))
.optional(), .optional(),
@@ -990,7 +988,6 @@ export const ClawdbotSchema = z.object({
enabled: z.boolean().optional(), enabled: z.boolean().optional(),
allow: z.boolean().optional(), allow: z.boolean().optional(),
requireMention: z.boolean().optional(), requireMention: z.boolean().optional(),
autoReply: z.boolean().optional(),
users: z.array(z.union([z.string(), z.number()])).optional(), users: z.array(z.union([z.string(), z.number()])).optional(),
skills: z.array(z.string()).optional(), skills: z.array(z.string()).optional(),
systemPrompt: z.string().optional(), systemPrompt: z.string().optional(),

View File

@@ -101,7 +101,6 @@ describe("discord guild/channel resolution", () => {
requireMention: true, requireMention: true,
skills: ["search"], skills: ["search"],
enabled: false, enabled: false,
autoReply: true,
users: ["123"], users: ["123"],
systemPrompt: "Use short answers.", systemPrompt: "Use short answers.",
}, },
@@ -126,7 +125,6 @@ describe("discord guild/channel resolution", () => {
expect(help?.requireMention).toBe(true); expect(help?.requireMention).toBe(true);
expect(help?.skills).toEqual(["search"]); expect(help?.skills).toEqual(["search"]);
expect(help?.enabled).toBe(false); expect(help?.enabled).toBe(false);
expect(help?.autoReply).toBe(true);
expect(help?.users).toEqual(["123"]); expect(help?.users).toEqual(["123"]);
expect(help?.systemPrompt).toBe("Use short answers."); expect(help?.systemPrompt).toBe("Use short answers.");
}); });

View File

@@ -101,7 +101,6 @@ export type DiscordGuildEntryResolved = {
requireMention?: boolean; requireMention?: boolean;
skills?: string[]; skills?: string[];
enabled?: boolean; enabled?: boolean;
autoReply?: boolean;
users?: Array<string | number>; users?: Array<string | number>;
systemPrompt?: string; systemPrompt?: string;
} }
@@ -113,7 +112,6 @@ export type DiscordChannelConfigResolved = {
requireMention?: boolean; requireMention?: boolean;
skills?: string[]; skills?: string[];
enabled?: boolean; enabled?: boolean;
autoReply?: boolean;
users?: Array<string | number>; users?: Array<string | number>;
systemPrompt?: string; systemPrompt?: string;
}; };
@@ -601,14 +599,8 @@ export function createDiscordMessageHandler(params: {
guildHistories.set(message.channelId, history); guildHistories.set(message.channelId, history);
} }
const baseRequireMention =
channelConfig?.requireMention ?? guildInfo?.requireMention ?? true;
const shouldRequireMention = const shouldRequireMention =
channelConfig?.autoReply === true channelConfig?.requireMention ?? guildInfo?.requireMention ?? true;
? false
: channelConfig?.autoReply === false
? true
: baseRequireMention;
const hasAnyMention = Boolean( const hasAnyMention = Boolean(
!isDirectMessage && !isDirectMessage &&
(message.mentionedEveryone || (message.mentionedEveryone ||
@@ -1810,7 +1802,6 @@ export function resolveDiscordChannelConfig(params: {
requireMention: byId.requireMention, requireMention: byId.requireMention,
skills: byId.skills, skills: byId.skills,
enabled: byId.enabled, enabled: byId.enabled,
autoReply: byId.autoReply,
users: byId.users, users: byId.users,
systemPrompt: byId.systemPrompt, systemPrompt: byId.systemPrompt,
}; };
@@ -1821,7 +1812,6 @@ export function resolveDiscordChannelConfig(params: {
requireMention: entry.requireMention, requireMention: entry.requireMention,
skills: entry.skills, skills: entry.skills,
enabled: entry.enabled, enabled: entry.enabled,
autoReply: entry.autoReply,
users: entry.users, users: entry.users,
systemPrompt: entry.systemPrompt, systemPrompt: entry.systemPrompt,
}; };
@@ -1833,7 +1823,6 @@ export function resolveDiscordChannelConfig(params: {
requireMention: entry.requireMention, requireMention: entry.requireMention,
skills: entry.skills, skills: entry.skills,
enabled: entry.enabled, enabled: entry.enabled,
autoReply: entry.autoReply,
users: entry.users, users: entry.users,
systemPrompt: entry.systemPrompt, systemPrompt: entry.systemPrompt,
}; };

View File

@@ -159,7 +159,6 @@ type SlackThreadBroadcastEvent = {
type SlackChannelConfigResolved = { type SlackChannelConfigResolved = {
allowed: boolean; allowed: boolean;
requireMention: boolean; requireMention: boolean;
autoReply?: boolean;
users?: Array<string | number>; users?: Array<string | number>;
skills?: string[]; skills?: string[];
systemPrompt?: string; systemPrompt?: string;
@@ -284,7 +283,6 @@ function resolveSlackChannelConfig(params: {
enabled?: boolean; enabled?: boolean;
allow?: boolean; allow?: boolean;
requireMention?: boolean; requireMention?: boolean;
autoReply?: boolean;
users?: Array<string | number>; users?: Array<string | number>;
skills?: string[]; skills?: string[];
systemPrompt?: string; systemPrompt?: string;
@@ -308,7 +306,6 @@ function resolveSlackChannelConfig(params: {
enabled?: boolean; enabled?: boolean;
allow?: boolean; allow?: boolean;
requireMention?: boolean; requireMention?: boolean;
autoReply?: boolean;
users?: Array<string | number>; users?: Array<string | number>;
skills?: string[]; skills?: string[];
systemPrompt?: string; systemPrompt?: string;
@@ -341,14 +338,13 @@ function resolveSlackChannelConfig(params: {
const requireMention = const requireMention =
firstDefined(resolved.requireMention, fallback?.requireMention, true) ?? firstDefined(resolved.requireMention, fallback?.requireMention, true) ??
true; true;
const autoReply = firstDefined(resolved.autoReply, fallback?.autoReply);
const users = firstDefined(resolved.users, fallback?.users); const users = firstDefined(resolved.users, fallback?.users);
const skills = firstDefined(resolved.skills, fallback?.skills); const skills = firstDefined(resolved.skills, fallback?.skills);
const systemPrompt = firstDefined( const systemPrompt = firstDefined(
resolved.systemPrompt, resolved.systemPrompt,
fallback?.systemPrompt, fallback?.systemPrompt,
); );
return { allowed, requireMention, autoReply, users, skills, systemPrompt }; return { allowed, requireMention, users, skills, systemPrompt };
} }
async function resolveSlackMedia(params: { async function resolveSlackMedia(params: {
@@ -810,11 +806,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
surface: "slack", surface: "slack",
}); });
const shouldRequireMention = isRoom const shouldRequireMention = isRoom
? channelConfig?.autoReply === true ? (channelConfig?.requireMention ?? true)
? false
: channelConfig?.autoReply === false
? true
: (channelConfig?.requireMention ?? true)
: false; : false;
const shouldBypassMention = const shouldBypassMention =
allowTextCommands && allowTextCommands &&

View File

@@ -704,6 +704,50 @@ describe("createTelegramBot", () => {
expect(replySpy).toHaveBeenCalledTimes(1); expect(replySpy).toHaveBeenCalledTimes(1);
}); });
it("allows per-topic requireMention override", async () => {
onSpy.mockReset();
const replySpy = replyModule.__replySpy as unknown as ReturnType<
typeof vi.fn
>;
replySpy.mockReset();
loadConfig.mockReturnValue({
telegram: {
groups: {
"*": { requireMention: true },
"-1001234567890": {
requireMention: true,
topics: {
"99": { requireMention: false },
},
},
},
},
});
createTelegramBot({ token: "tok" });
const handler = onSpy.mock.calls[0][1] as (
ctx: Record<string, unknown>,
) => Promise<void>;
await handler({
message: {
chat: {
id: -1001234567890,
type: "supergroup",
title: "Forum Group",
is_forum: true,
},
text: "hello",
date: 1736380800,
message_thread_id: 99,
},
me: { username: "clawdbot_bot" },
getFile: async () => ({ download: async () => new Uint8Array() }),
});
expect(replySpy).toHaveBeenCalledTimes(1);
});
it("honors groups default when no explicit group override exists", async () => { it("honors groups default when no explicit group override exists", async () => {
onSpy.mockReset(); onSpy.mockReset();
const replySpy = replyModule.__replySpy as unknown as ReturnType< const replySpy = replyModule.__replySpy as unknown as ReturnType<

View File

@@ -381,16 +381,11 @@ export function createTelegramBot(opts: TelegramBotOptions) {
(ent) => ent.type === "mention", (ent) => ent.type === "mention",
); );
const baseRequireMention = resolveGroupRequireMention(chatId); const baseRequireMention = resolveGroupRequireMention(chatId);
const autoReplySetting = firstDefined( const requireMention = firstDefined(
topicConfig?.autoReply, topicConfig?.requireMention,
groupConfig?.autoReply, groupConfig?.requireMention,
baseRequireMention,
); );
const requireMention =
autoReplySetting === true
? false
: autoReplySetting === false
? true
: baseRequireMention;
const shouldBypassMention = const shouldBypassMention =
isGroup && isGroup &&
requireMention && requireMention &&