2026-01-18 08:32:19 +00:00
import {
applyAccountNameToChannelSection ,
buildChannelConfigSchema ,
collectTelegramStatusIssues ,
DEFAULT_ACCOUNT_ID ,
deleteAccountFromConfigSection ,
formatPairingApproveHint ,
getChatChannelMeta ,
listTelegramAccountIds ,
listTelegramDirectoryGroupsFromConfig ,
listTelegramDirectoryPeersFromConfig ,
looksLikeTelegramTargetId ,
migrateBaseNameToDefaultAccount ,
normalizeAccountId ,
normalizeTelegramMessagingTarget ,
PAIRING_APPROVED_MESSAGE ,
2026-02-15 01:07:37 +00:00
parseTelegramReplyToMessageId ,
parseTelegramThreadId ,
2026-01-18 08:32:19 +00:00
resolveDefaultTelegramAccountId ,
resolveTelegramAccount ,
resolveTelegramGroupRequireMention ,
2026-01-24 15:35:05 +13:00
resolveTelegramGroupToolPolicy ,
2026-01-18 08:32:19 +00:00
setAccountEnabledInConfigSection ,
telegramOnboardingAdapter ,
TelegramConfigSchema ,
2026-01-18 11:00:19 +00:00
type ChannelMessageActionAdapter ,
2026-01-18 08:32:19 +00:00
type ChannelPlugin ,
2026-01-30 03:15:10 +01:00
type OpenClawConfig ,
2026-01-18 08:32:19 +00:00
type ResolvedTelegramAccount ,
2026-02-04 10:09:28 +00:00
type TelegramProbe ,
2026-01-30 03:15:10 +01:00
} from "openclaw/plugin-sdk" ;
2026-01-18 11:00:19 +00:00
import { getTelegramRuntime } from "./runtime.js" ;
2026-01-18 08:32:19 +00:00
const meta = getChatChannelMeta ( "telegram" ) ;
2026-01-18 11:00:19 +00:00
const telegramMessageActions : ChannelMessageActionAdapter = {
2026-02-09 10:05:38 -08:00
listActions : ( ctx ) = >
getTelegramRuntime ( ) . channel . telegram . messageActions ? . listActions ? . ( ctx ) ? ? [ ] ,
2026-01-18 11:00:19 +00:00
extractToolSend : ( ctx ) = >
2026-02-09 10:05:38 -08:00
getTelegramRuntime ( ) . channel . telegram . messageActions ? . extractToolSend ? . ( ctx ) ? ? null ,
handleAction : async ( ctx ) = > {
const ma = getTelegramRuntime ( ) . channel . telegram . messageActions ;
if ( ! ma ? . handleAction ) {
throw new Error ( "Telegram message actions not available" ) ;
}
return ma . handleAction ( ctx ) ;
} ,
2026-01-18 11:00:19 +00:00
} ;
2026-02-04 10:09:28 +00:00
export const telegramPlugin : ChannelPlugin < ResolvedTelegramAccount , TelegramProbe > = {
2026-01-18 08:32:19 +00:00
id : "telegram" ,
meta : {
. . . meta ,
quickstartAllowFrom : true ,
} ,
onboarding : telegramOnboardingAdapter ,
pairing : {
idLabel : "telegramUserId" ,
normalizeAllowEntry : ( entry ) = > entry . replace ( /^(telegram|tg):/i , "" ) ,
notifyApproval : async ( { cfg , id } ) = > {
2026-01-18 11:00:19 +00:00
const { token } = getTelegramRuntime ( ) . channel . telegram . resolveTelegramToken ( cfg ) ;
2026-01-31 22:13:48 +09:00
if ( ! token ) {
throw new Error ( "telegram token not configured" ) ;
}
2026-01-31 21:13:13 +09:00
await getTelegramRuntime ( ) . channel . telegram . sendMessageTelegram (
id ,
PAIRING_APPROVED_MESSAGE ,
{
token ,
} ,
) ;
2026-01-18 08:32:19 +00:00
} ,
} ,
capabilities : {
chatTypes : [ "direct" , "group" , "channel" , "thread" ] ,
reactions : true ,
threads : true ,
media : true ,
2026-02-14 18:34:30 +01:00
polls : true ,
2026-01-18 08:32:19 +00:00
nativeCommands : true ,
blockStreaming : true ,
} ,
reload : { configPrefixes : [ "channels.telegram" ] } ,
configSchema : buildChannelConfigSchema ( TelegramConfigSchema ) ,
config : {
listAccountIds : ( cfg ) = > listTelegramAccountIds ( cfg ) ,
resolveAccount : ( cfg , accountId ) = > resolveTelegramAccount ( { cfg , accountId } ) ,
defaultAccountId : ( cfg ) = > resolveDefaultTelegramAccountId ( cfg ) ,
setAccountEnabled : ( { cfg , accountId , enabled } ) = >
setAccountEnabledInConfigSection ( {
cfg ,
sectionKey : "telegram" ,
accountId ,
enabled ,
allowTopLevel : true ,
} ) ,
deleteAccount : ( { cfg , accountId } ) = >
deleteAccountFromConfigSection ( {
cfg ,
sectionKey : "telegram" ,
accountId ,
clearBaseFields : [ "botToken" , "tokenFile" , "name" ] ,
} ) ,
isConfigured : ( account ) = > Boolean ( account . token ? . trim ( ) ) ,
describeAccount : ( account ) = > ( {
accountId : account.accountId ,
name : account.name ,
enabled : account.enabled ,
configured : Boolean ( account . token ? . trim ( ) ) ,
tokenSource : account.tokenSource ,
} ) ,
resolveAllowFrom : ( { cfg , accountId } ) = >
( resolveTelegramAccount ( { cfg , accountId } ) . config . allowFrom ? ? [ ] ) . map ( ( entry ) = >
String ( entry ) ,
) ,
formatAllowFrom : ( { allowFrom } ) = >
allowFrom
. map ( ( entry ) = > String ( entry ) . trim ( ) )
. filter ( Boolean )
. map ( ( entry ) = > entry . replace ( /^(telegram|tg):/i , "" ) )
. map ( ( entry ) = > entry . toLowerCase ( ) ) ,
} ,
security : {
resolveDmPolicy : ( { cfg , accountId , account } ) = > {
const resolvedAccountId = accountId ? ? account . accountId ? ? DEFAULT_ACCOUNT_ID ;
const useAccountPath = Boolean ( cfg . channels ? . telegram ? . accounts ? . [ resolvedAccountId ] ) ;
const basePath = useAccountPath
? ` channels.telegram.accounts. ${ resolvedAccountId } . `
: "channels.telegram." ;
return {
policy : account.config.dmPolicy ? ? "pairing" ,
allowFrom : account.config.allowFrom ? ? [ ] ,
policyPath : ` ${ basePath } dmPolicy ` ,
allowFromPath : basePath ,
approveHint : formatPairingApproveHint ( "telegram" ) ,
normalizeEntry : ( raw ) = > raw . replace ( /^(telegram|tg):/i , "" ) ,
} ;
} ,
collectWarnings : ( { account , cfg } ) = > {
const defaultGroupPolicy = cfg . channels ? . defaults ? . groupPolicy ;
const groupPolicy = account . config . groupPolicy ? ? defaultGroupPolicy ? ? "allowlist" ;
2026-01-31 22:13:48 +09:00
if ( groupPolicy !== "open" ) {
return [ ] ;
}
2026-01-18 08:32:19 +00:00
const groupAllowlistConfigured =
account . config . groups && Object . keys ( account . config . groups ) . length > 0 ;
if ( groupAllowlistConfigured ) {
return [
` - Telegram groups: groupPolicy="open" allows any member in allowed groups to trigger (mention-gated). Set channels.telegram.groupPolicy="allowlist" + channels.telegram.groupAllowFrom to restrict senders. ` ,
] ;
}
return [
` - Telegram groups: groupPolicy="open" with no channels.telegram.groups allowlist; any group can add + ping (mention-gated). Set channels.telegram.groupPolicy="allowlist" + channels.telegram.groupAllowFrom or configure channels.telegram.groups. ` ,
] ;
} ,
} ,
groups : {
resolveRequireMention : resolveTelegramGroupRequireMention ,
2026-01-24 15:35:05 +13:00
resolveToolPolicy : resolveTelegramGroupToolPolicy ,
2026-01-18 08:32:19 +00:00
} ,
threading : {
2026-02-13 22:11:55 -08:00
resolveReplyToMode : ( { cfg } ) = > cfg . channels ? . telegram ? . replyToMode ? ? "off" ,
2026-01-18 08:32:19 +00:00
} ,
messaging : {
normalizeTarget : normalizeTelegramMessagingTarget ,
targetResolver : {
looksLikeId : looksLikeTelegramTargetId ,
hint : "<chatId>" ,
} ,
} ,
directory : {
self : async ( ) = > null ,
listPeers : async ( params ) = > listTelegramDirectoryPeersFromConfig ( params ) ,
listGroups : async ( params ) = > listTelegramDirectoryGroupsFromConfig ( params ) ,
} ,
actions : telegramMessageActions ,
setup : {
resolveAccountId : ( { accountId } ) = > normalizeAccountId ( accountId ) ,
applyAccountName : ( { cfg , accountId , name } ) = >
applyAccountNameToChannelSection ( {
cfg ,
channelKey : "telegram" ,
accountId ,
name ,
} ) ,
validateInput : ( { accountId , input } ) = > {
if ( input . useEnv && accountId !== DEFAULT_ACCOUNT_ID ) {
return "TELEGRAM_BOT_TOKEN can only be used for the default account." ;
}
if ( ! input . useEnv && ! input . token && ! input . tokenFile ) {
return "Telegram requires token or --token-file (or --use-env)." ;
}
return null ;
} ,
applyAccountConfig : ( { cfg , accountId , input } ) = > {
const namedConfig = applyAccountNameToChannelSection ( {
cfg ,
channelKey : "telegram" ,
accountId ,
name : input.name ,
} ) ;
const next =
accountId !== DEFAULT_ACCOUNT_ID
? migrateBaseNameToDefaultAccount ( {
cfg : namedConfig ,
channelKey : "telegram" ,
} )
: namedConfig ;
if ( accountId === DEFAULT_ACCOUNT_ID ) {
return {
. . . next ,
channels : {
. . . next . channels ,
telegram : {
. . . next . channels ? . telegram ,
enabled : true ,
. . . ( input . useEnv
? { }
: input . tokenFile
? { tokenFile : input.tokenFile }
: input . token
? { botToken : input.token }
: { } ) ,
} ,
} ,
} ;
}
return {
. . . next ,
channels : {
. . . next . channels ,
telegram : {
. . . next . channels ? . telegram ,
enabled : true ,
accounts : {
. . . next . channels ? . telegram ? . accounts ,
[ accountId ] : {
. . . next . channels ? . telegram ? . accounts ? . [ accountId ] ,
enabled : true ,
. . . ( input . tokenFile
? { tokenFile : input.tokenFile }
: input . token
? { botToken : input.token }
: { } ) ,
} ,
} ,
} ,
} ,
} ;
} ,
} ,
outbound : {
deliveryMode : "direct" ,
2026-01-18 11:00:19 +00:00
chunker : ( text , limit ) = > getTelegramRuntime ( ) . channel . text . chunkMarkdownText ( text , limit ) ,
2026-01-25 04:05:14 +00:00
chunkerMode : "markdown" ,
2026-01-18 08:32:19 +00:00
textChunkLimit : 4000 ,
2026-02-14 18:34:30 +01:00
pollMaxOptions : 10 ,
sendText : async ( { to , text , accountId , deps , replyToId , threadId , silent } ) = > {
2026-01-31 21:13:13 +09:00
const send = deps ? . sendTelegram ? ? getTelegramRuntime ( ) . channel . telegram . sendMessageTelegram ;
2026-02-15 01:07:37 +00:00
const replyToMessageId = parseTelegramReplyToMessageId ( replyToId ) ;
const messageThreadId = parseTelegramThreadId ( threadId ) ;
2026-01-18 08:32:19 +00:00
const result = await send ( to , text , {
verbose : false ,
messageThreadId ,
replyToMessageId ,
accountId : accountId ? ? undefined ,
2026-02-14 18:34:30 +01:00
silent : silent ? ? undefined ,
2026-01-18 08:32:19 +00:00
} ) ;
return { channel : "telegram" , . . . result } ;
} ,
2026-02-14 18:34:30 +01:00
sendMedia : async ( { to , text , mediaUrl , accountId , deps , replyToId , threadId , silent } ) = > {
2026-01-31 21:13:13 +09:00
const send = deps ? . sendTelegram ? ? getTelegramRuntime ( ) . channel . telegram . sendMessageTelegram ;
2026-02-15 01:07:37 +00:00
const replyToMessageId = parseTelegramReplyToMessageId ( replyToId ) ;
const messageThreadId = parseTelegramThreadId ( threadId ) ;
2026-01-18 08:32:19 +00:00
const result = await send ( to , text , {
verbose : false ,
mediaUrl ,
messageThreadId ,
replyToMessageId ,
accountId : accountId ? ? undefined ,
2026-02-14 18:34:30 +01:00
silent : silent ? ? undefined ,
2026-01-18 08:32:19 +00:00
} ) ;
return { channel : "telegram" , . . . result } ;
} ,
2026-02-14 18:34:30 +01:00
sendPoll : async ( { to , poll , accountId , threadId , silent , isAnonymous } ) = >
await getTelegramRuntime ( ) . channel . telegram . sendPollTelegram ( to , poll , {
accountId : accountId ? ? undefined ,
2026-02-15 01:07:37 +00:00
messageThreadId : parseTelegramThreadId ( threadId ) ,
2026-02-14 18:34:30 +01:00
silent : silent ? ? undefined ,
isAnonymous : isAnonymous ? ? undefined ,
} ) ,
2026-01-18 08:32:19 +00:00
} ,
status : {
defaultRuntime : {
accountId : DEFAULT_ACCOUNT_ID ,
running : false ,
lastStartAt : null ,
lastStopAt : null ,
lastError : null ,
} ,
collectStatusIssues : collectTelegramStatusIssues ,
buildChannelSummary : ( { snapshot } ) = > ( {
configured : snapshot.configured ? ? false ,
tokenSource : snapshot.tokenSource ? ? "none" ,
running : snapshot.running ? ? false ,
mode : snapshot.mode ? ? null ,
lastStartAt : snapshot.lastStartAt ? ? null ,
lastStopAt : snapshot.lastStopAt ? ? null ,
lastError : snapshot.lastError ? ? null ,
probe : snapshot.probe ,
lastProbeAt : snapshot.lastProbeAt ? ? null ,
} ) ,
probeAccount : async ( { account , timeoutMs } ) = >
2026-01-18 11:00:19 +00:00
getTelegramRuntime ( ) . channel . telegram . probeTelegram (
account . token ,
timeoutMs ,
account . config . proxy ,
) ,
2026-01-18 08:32:19 +00:00
auditAccount : async ( { account , timeoutMs , probe , cfg } ) = > {
const groups =
cfg . channels ? . telegram ? . accounts ? . [ account . accountId ] ? . groups ? ?
cfg . channels ? . telegram ? . groups ;
const { groupIds , unresolvedGroups , hasWildcardUnmentionedGroups } =
2026-01-18 11:00:19 +00:00
getTelegramRuntime ( ) . channel . telegram . collectUnmentionedGroupIds ( groups ) ;
2026-01-18 08:32:19 +00:00
if ( ! groupIds . length && unresolvedGroups === 0 && ! hasWildcardUnmentionedGroups ) {
return undefined ;
}
2026-02-04 10:09:28 +00:00
const botId = probe ? . ok && probe . bot ? . id != null ? probe.bot.id : null ;
2026-01-18 08:32:19 +00:00
if ( ! botId ) {
return {
ok : unresolvedGroups === 0 && ! hasWildcardUnmentionedGroups ,
checkedGroups : 0 ,
unresolvedGroups ,
hasWildcardUnmentionedGroups ,
groups : [ ] ,
elapsedMs : 0 ,
} ;
}
2026-01-18 11:00:19 +00:00
const audit = await getTelegramRuntime ( ) . channel . telegram . auditGroupMembership ( {
2026-01-18 08:32:19 +00:00
token : account.token ,
botId ,
groupIds ,
proxyUrl : account.config.proxy ,
timeoutMs ,
} ) ;
return { . . . audit , unresolvedGroups , hasWildcardUnmentionedGroups } ;
} ,
buildAccountSnapshot : ( { account , cfg , runtime , probe , audit } ) = > {
const configured = Boolean ( account . token ? . trim ( ) ) ;
const groups =
cfg . channels ? . telegram ? . accounts ? . [ account . accountId ] ? . groups ? ?
cfg . channels ? . telegram ? . groups ;
const allowUnmentionedGroups =
2026-02-04 10:09:28 +00:00
groups ? . [ "*" ] ? . requireMention === false ||
2026-01-18 08:32:19 +00:00
Object . entries ( groups ? ? { } ) . some (
2026-02-04 10:09:28 +00:00
( [ key , value ] ) = > key !== "*" && value ? . requireMention === false ,
2026-01-18 08:32:19 +00:00
) ;
return {
accountId : account.accountId ,
name : account.name ,
enabled : account.enabled ,
configured ,
tokenSource : account.tokenSource ,
running : runtime?.running ? ? false ,
lastStartAt : runtime?.lastStartAt ? ? null ,
lastStopAt : runtime?.lastStopAt ? ? null ,
lastError : runtime?.lastError ? ? null ,
mode : runtime?.mode ? ? ( account . config . webhookUrl ? "webhook" : "polling" ) ,
probe ,
audit ,
allowUnmentionedGroups ,
lastInboundAt : runtime?.lastInboundAt ? ? null ,
lastOutboundAt : runtime?.lastOutboundAt ? ? null ,
} ;
} ,
} ,
gateway : {
startAccount : async ( ctx ) = > {
const account = ctx . account ;
const token = account . token . trim ( ) ;
let telegramBotLabel = "" ;
try {
2026-01-18 11:00:19 +00:00
const probe = await getTelegramRuntime ( ) . channel . telegram . probeTelegram (
token ,
2500 ,
account . config . proxy ,
) ;
2026-01-18 08:32:19 +00:00
const username = probe . ok ? probe . bot ? . username ? . trim ( ) : null ;
2026-01-31 22:13:48 +09:00
if ( username ) {
telegramBotLabel = ` (@ ${ username } ) ` ;
}
2026-01-18 08:32:19 +00:00
} catch ( err ) {
2026-01-18 11:00:19 +00:00
if ( getTelegramRuntime ( ) . logging . shouldLogVerbose ( ) ) {
2026-01-18 08:32:19 +00:00
ctx . log ? . debug ? . ( ` [ ${ account . accountId } ] bot probe failed: ${ String ( err ) } ` ) ;
}
}
ctx . log ? . info ( ` [ ${ account . accountId } ] starting provider ${ telegramBotLabel } ` ) ;
2026-01-18 11:00:19 +00:00
return getTelegramRuntime ( ) . channel . telegram . monitorTelegramProvider ( {
2026-01-18 08:32:19 +00:00
token ,
accountId : account.accountId ,
config : ctx.cfg ,
runtime : ctx.runtime ,
abortSignal : ctx.abortSignal ,
useWebhook : Boolean ( account . config . webhookUrl ) ,
webhookUrl : account.config.webhookUrl ,
webhookSecret : account.config.webhookSecret ,
webhookPath : account.config.webhookPath ,
2026-02-14 01:39:56 +10:00
webhookHost : account.config.webhookHost ,
2026-01-18 08:32:19 +00:00
} ) ;
} ,
logoutAccount : async ( { accountId , cfg } ) = > {
const envToken = process . env . TELEGRAM_BOT_TOKEN ? . trim ( ) ? ? "" ;
2026-01-30 03:15:10 +01:00
const nextCfg = { . . . cfg } as OpenClawConfig ;
2026-01-18 08:32:19 +00:00
const nextTelegram = cfg . channels ? . telegram ? { . . . cfg . channels . telegram } : undefined ;
let cleared = false ;
let changed = false ;
if ( nextTelegram ) {
if ( accountId === DEFAULT_ACCOUNT_ID && nextTelegram . botToken ) {
delete nextTelegram . botToken ;
cleared = true ;
changed = true ;
}
const accounts =
nextTelegram . accounts && typeof nextTelegram . accounts === "object"
? { . . . nextTelegram . accounts }
: undefined ;
if ( accounts && accountId in accounts ) {
const entry = accounts [ accountId ] ;
if ( entry && typeof entry === "object" ) {
const nextEntry = { . . . entry } as Record < string , unknown > ;
if ( "botToken" in nextEntry ) {
const token = nextEntry . botToken ;
if ( typeof token === "string" ? token . trim ( ) : token ) {
cleared = true ;
}
delete nextEntry . botToken ;
changed = true ;
}
if ( Object . keys ( nextEntry ) . length === 0 ) {
delete accounts [ accountId ] ;
changed = true ;
} else {
accounts [ accountId ] = nextEntry as typeof entry ;
}
}
}
if ( accounts ) {
if ( Object . keys ( accounts ) . length === 0 ) {
delete nextTelegram . accounts ;
changed = true ;
} else {
nextTelegram . accounts = accounts ;
}
}
}
if ( changed ) {
if ( nextTelegram && Object . keys ( nextTelegram ) . length > 0 ) {
nextCfg . channels = { . . . nextCfg . channels , telegram : nextTelegram } ;
} else {
const nextChannels = { . . . nextCfg . channels } ;
delete nextChannels . telegram ;
if ( Object . keys ( nextChannels ) . length > 0 ) {
nextCfg . channels = nextChannels ;
} else {
delete nextCfg . channels ;
}
}
}
const resolved = resolveTelegramAccount ( {
cfg : changed ? nextCfg : cfg ,
accountId ,
} ) ;
const loggedOut = resolved . tokenSource === "none" ;
if ( changed ) {
2026-01-18 11:00:19 +00:00
await getTelegramRuntime ( ) . config . writeConfigFile ( nextCfg ) ;
2026-01-18 08:32:19 +00:00
}
return { cleared , envToken : Boolean ( envToken ) , loggedOut } ;
} ,
} ,
} ;