2026-01-04 14:32:47 +00:00
import type { ClawdbotConfig } from "../../config/config.js" ;
2026-01-06 04:27:21 +01:00
import { resolveProviderGroupRequireMention } from "../../config/group-policy.js" ;
2026-01-04 05:47:21 +01:00
import type {
GroupKeyResolution ,
SessionEntry ,
} from "../../config/sessions.js" ;
import { normalizeGroupActivation } from "../group-activation.js" ;
import type { TemplateContext } from "../templating.js" ;
2026-01-05 13:55:32 +00:00
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 , "" ) ;
}
2026-01-07 11:23:04 +01:00
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 } ;
}
2026-01-07 11:59:48 +01:00
function resolveTelegramRequireMention ( params : {
2026-01-07 11:23:04 +01:00
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 ;
2026-01-07 11:59:48 +01:00
if ( typeof topicConfig ? . requireMention === "boolean" ) {
return topicConfig . requireMention ;
2026-01-07 11:23:04 +01:00
}
2026-01-07 11:59:48 +01:00
if ( typeof defaultTopicConfig ? . requireMention === "boolean" ) {
return defaultTopicConfig . requireMention ;
2026-01-07 11:23:04 +01:00
}
2026-01-07 11:59:48 +01:00
if ( typeof groupConfig ? . requireMention === "boolean" ) {
return groupConfig . requireMention ;
2026-01-07 11:23:04 +01:00
}
2026-01-07 11:59:48 +01:00
if ( typeof groupDefault ? . requireMention === "boolean" ) {
return groupDefault . requireMention ;
2026-01-07 11:23:04 +01:00
}
return undefined ;
}
2026-01-05 13:55:32 +00:00
function resolveDiscordGuildEntry (
guilds : NonNullable < ClawdbotConfig [ "discord" ] > [ "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 ;
}
2026-01-04 05:47:21 +01:00
export function resolveGroupRequireMention ( params : {
2026-01-04 14:32:47 +00:00
cfg : ClawdbotConfig ;
2026-01-04 05:47:21 +01:00
ctx : TemplateContext ;
groupResolution? : GroupKeyResolution ;
} ) : boolean {
const { cfg , ctx , groupResolution } = params ;
2026-01-06 18:25:37 +00:00
const provider =
groupResolution ? . provider ? ? ctx . Provider ? . trim ( ) . toLowerCase ( ) ;
2026-01-04 05:47:21 +01:00
const groupId = groupResolution ? . id ? ? ctx . From ? . replace ( /^group:/ , "" ) ;
2026-01-05 13:55:32 +00:00
const groupRoom = ctx . GroupRoom ? . trim ( ) ? ? ctx . GroupSubject ? . trim ( ) ;
const groupSpace = ctx . GroupSpace ? . trim ( ) ;
2026-01-07 11:23:04 +01:00
if ( provider === "telegram" ) {
const { chatId , topicId } = parseTelegramGroupId ( groupId ) ;
2026-01-07 11:59:48 +01:00
const requireMention = resolveTelegramRequireMention ( {
cfg ,
chatId ,
topicId ,
} ) ;
if ( typeof requireMention === "boolean" ) return requireMention ;
2026-01-07 11:23:04 +01:00
return resolveProviderGroupRequireMention ( {
cfg ,
provider ,
groupId : chatId ? ? groupId ,
} ) ;
}
if ( provider === "whatsapp" || provider === "imessage" ) {
2026-01-06 04:27:21 +01:00
return resolveProviderGroupRequireMention ( {
cfg ,
2026-01-06 18:25:37 +00:00
provider ,
2026-01-06 04:27:21 +01:00
groupId ,
} ) ;
2026-01-04 05:47:21 +01:00
}
2026-01-06 18:25:37 +00:00
if ( provider === "discord" ) {
2026-01-05 13:55:32 +00:00
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 ;
}
2026-01-06 18:25:37 +00:00
if ( provider === "slack" ) {
2026-01-05 13:55:32 +00:00
const channels = cfg . slack ? . 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 ) ;
2026-01-07 11:59:48 +01:00
let matched : { requireMention? : boolean } | undefined ;
2026-01-05 13:55:32 +00:00
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 ;
}
2026-01-04 05:47:21 +01:00
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 ( ) ;
2026-01-06 18:25:37 +00:00
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 ) } ` ;
2026-01-04 05:47:21 +01:00
} ) ( ) ;
const subjectLine = subject
2026-01-06 18:25:37 +00:00
? ` You are replying inside the ${ providerLabel } group " ${ subject } ". `
: ` You are replying inside a ${ providerLabel } group chat. ` ;
2026-01-04 05:47:21 +01:00
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"
2026-01-04 14:32:47 +00:00
? ` If no response is needed, reply with exactly " ${ params . silentToken } " (no other text) so Clawdbot stays silent. `
2026-01-04 05:47:21 +01:00
: undefined ;
const cautionLine =
activation === "always"
2026-01-07 08:14:47 +01:00
? "Be extremely selective: reply only when directly addressed or clearly helpful. Otherwise stay silent."
2026-01-04 05:47:21 +01:00
: undefined ;
const lurkLine =
2026-01-07 10:03:50 +00:00
"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." ;
2026-01-04 05:47:21 +01:00
return [
subjectLine ,
membersLine ,
activationLine ,
silenceLine ,
cautionLine ,
lurkLine ,
]
. filter ( Boolean )
. join ( " " )
. concat ( " Address the specific sender noted in the message context." ) ;
}