import type { ClawdbotConfig } from "../../config/config.js"; import { resolveProviderGroupRequireMention } from "../../config/group-policy.js"; import type { GroupKeyResolution, SessionEntry, } from "../../config/sessions.js"; import { resolveSlackAccount } from "../../slack/accounts.js"; import { normalizeGroupActivation } from "../group-activation.js"; import type { TemplateContext } from "../templating.js"; function normalizeDiscordSlug(value?: string | null) { if (!value) return ""; let text = value.trim().toLowerCase(); if (!text) return ""; text = text.replace(/^[@#]+/, ""); text = text.replace(/[\s_]+/g, "-"); text = text.replace(/[^a-z0-9-]+/g, "-"); text = text.replace(/-{2,}/g, "-").replace(/^-+|-+$/g, ""); return text; } function normalizeSlackSlug(raw?: string | null) { const trimmed = raw?.trim().toLowerCase() ?? ""; if (!trimmed) return ""; const dashed = trimmed.replace(/\s+/g, "-"); const cleaned = dashed.replace(/[^a-z0-9#@._+-]+/g, "-"); return cleaned.replace(/-{2,}/g, "-").replace(/^[-.]+|[-.]+$/g, ""); } function parseTelegramGroupId(value?: string | null) { const raw = value?.trim() ?? ""; if (!raw) return { chatId: undefined, topicId: undefined }; const parts = raw.split(":").filter(Boolean); if ( parts.length >= 3 && parts[1] === "topic" && /^-?\d+$/.test(parts[0]) && /^\d+$/.test(parts[2]) ) { return { chatId: parts[0], topicId: parts[2] }; } if (parts.length >= 2 && /^-?\d+$/.test(parts[0]) && /^\d+$/.test(parts[1])) { return { chatId: parts[0], topicId: parts[1] }; } return { chatId: raw, topicId: undefined }; } function resolveTelegramRequireMention(params: { cfg: ClawdbotConfig; chatId?: string; topicId?: string; }): boolean | undefined { const { cfg, chatId, topicId } = params; if (!chatId) return undefined; const groupConfig = cfg.telegram?.groups?.[chatId]; const groupDefault = cfg.telegram?.groups?.["*"]; const topicConfig = topicId && groupConfig?.topics ? groupConfig.topics[topicId] : undefined; const defaultTopicConfig = topicId && groupDefault?.topics ? groupDefault.topics[topicId] : undefined; if (typeof topicConfig?.requireMention === "boolean") { return topicConfig.requireMention; } if (typeof defaultTopicConfig?.requireMention === "boolean") { return defaultTopicConfig.requireMention; } if (typeof groupConfig?.requireMention === "boolean") { return groupConfig.requireMention; } if (typeof groupDefault?.requireMention === "boolean") { return groupDefault.requireMention; } return undefined; } function resolveDiscordGuildEntry( guilds: NonNullable["guilds"], groupSpace?: string, ) { if (!guilds || Object.keys(guilds).length === 0) return null; const space = groupSpace?.trim(); if (space && guilds[space]) return guilds[space]; const normalized = normalizeDiscordSlug(space); if (normalized && guilds[normalized]) return guilds[normalized]; if (normalized) { const match = Object.values(guilds).find( (entry) => normalizeDiscordSlug(entry?.slug ?? undefined) === normalized, ); if (match) return match; } return guilds["*"] ?? null; } export function resolveGroupRequireMention(params: { cfg: ClawdbotConfig; ctx: TemplateContext; groupResolution?: GroupKeyResolution; }): boolean { const { cfg, ctx, groupResolution } = params; const provider = groupResolution?.provider ?? ctx.Provider?.trim().toLowerCase(); const groupId = groupResolution?.id ?? ctx.From?.replace(/^group:/, ""); const groupRoom = ctx.GroupRoom?.trim() ?? ctx.GroupSubject?.trim(); const groupSpace = ctx.GroupSpace?.trim(); if (provider === "telegram") { const { chatId, topicId } = parseTelegramGroupId(groupId); const requireMention = resolveTelegramRequireMention({ cfg, chatId, topicId, }); if (typeof requireMention === "boolean") return requireMention; return resolveProviderGroupRequireMention({ cfg, provider, groupId: chatId ?? groupId, }); } if (provider === "whatsapp" || provider === "imessage") { return resolveProviderGroupRequireMention({ cfg, provider, groupId, }); } if (provider === "discord") { const guildEntry = resolveDiscordGuildEntry( cfg.discord?.guilds, groupSpace, ); const channelEntries = guildEntry?.channels; if (channelEntries && Object.keys(channelEntries).length > 0) { const channelSlug = normalizeDiscordSlug(groupRoom); const entry = (groupId ? channelEntries[groupId] : undefined) ?? (channelSlug ? (channelEntries[channelSlug] ?? channelEntries[`#${channelSlug}`]) : undefined) ?? (groupRoom ? channelEntries[normalizeDiscordSlug(groupRoom)] : undefined); if (entry && typeof entry.requireMention === "boolean") { return entry.requireMention; } } if (typeof guildEntry?.requireMention === "boolean") { return guildEntry.requireMention; } return true; } if (provider === "slack") { const account = resolveSlackAccount({ cfg, accountId: ctx.AccountId }); const channels = account.channels ?? {}; const keys = Object.keys(channels); if (keys.length === 0) return true; const channelId = groupId?.trim(); const channelName = groupRoom?.replace(/^#/, ""); const normalizedName = normalizeSlackSlug(channelName); const candidates = [ channelId ?? "", channelName ? `#${channelName}` : "", channelName ?? "", normalizedName, ].filter(Boolean); let matched: { requireMention?: boolean } | undefined; for (const candidate of candidates) { if (candidate && channels[candidate]) { matched = channels[candidate]; break; } } const fallback = channels["*"]; const resolved = matched ?? fallback; if (typeof resolved?.requireMention === "boolean") { return resolved.requireMention; } return true; } return true; } export function defaultGroupActivation( requireMention: boolean, ): "always" | "mention" { return requireMention === false ? "always" : "mention"; } export function buildGroupIntro(params: { sessionCtx: TemplateContext; sessionEntry?: SessionEntry; defaultActivation: "always" | "mention"; silentToken: string; }): string { const activation = normalizeGroupActivation(params.sessionEntry?.groupActivation) ?? params.defaultActivation; const subject = params.sessionCtx.GroupSubject?.trim(); const members = params.sessionCtx.GroupMembers?.trim(); const provider = params.sessionCtx.Provider?.trim().toLowerCase(); const providerLabel = (() => { if (!provider) return "chat"; if (provider === "whatsapp") return "WhatsApp"; if (provider === "telegram") return "Telegram"; if (provider === "discord") return "Discord"; if (provider === "webchat") return "WebChat"; return `${provider.at(0)?.toUpperCase() ?? ""}${provider.slice(1)}`; })(); const subjectLine = subject ? `You are replying inside the ${providerLabel} group "${subject}".` : `You are replying inside a ${providerLabel} group chat.`; const membersLine = members ? `Group members: ${members}.` : undefined; const activationLine = activation === "always" ? "Activation: always-on (you receive every group message)." : "Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included)."; const silenceLine = activation === "always" ? `If no response is needed, reply with exactly "${params.silentToken}" (no other text) so Clawdbot stays silent.` : undefined; const cautionLine = activation === "always" ? "Be extremely selective: reply only when directly addressed or clearly helpful. Otherwise stay silent." : undefined; const lurkLine = "Be a good group participant: mostly lurk and follow the conversation; reply only when directly addressed or you can add clear value. Emoji reactions are welcome when available."; return [ subjectLine, membersLine, activationLine, silenceLine, cautionLine, lurkLine, ] .filter(Boolean) .join(" ") .concat(" Address the specific sender noted in the message context."); }