2026-03-15 22:38:49 -07:00
import { Separator , TextDisplay } from "@buape/carbon" ;
2026-03-17 04:24:01 +00:00
import {
buildAccountScopedAllowlistConfigEditor ,
resolveLegacyDmAllowlistConfigPaths ,
} from "openclaw/plugin-sdk/allowlist-config-edit" ;
2026-03-07 22:06:29 +00:00
import {
2026-03-07 22:37:59 +00:00
buildAccountScopedDmSecurityPolicy ,
2026-03-17 03:37:50 +00:00
collectOpenGroupPolicyConfiguredRouteWarnings ,
2026-03-16 21:13:56 -07:00
collectOpenProviderGroupPolicyWarnings ,
} from "openclaw/plugin-sdk/channel-config-helpers" ;
import { resolveOutboundSendDep } from "openclaw/plugin-sdk/channel-runtime" ;
import { normalizeMessageChannel } from "openclaw/plugin-sdk/channel-runtime" ;
2026-03-16 23:52:41 -07:00
import { buildOutboundBaseSessionKey , normalizeOutboundThreadId } from "openclaw/plugin-sdk/core" ;
2026-01-18 08:32:19 +00:00
import {
2026-03-07 20:01:41 +00:00
buildComputedAccountStatusSnapshot ,
2026-02-23 21:25:20 +00:00
buildTokenChannelStatusSummary ,
2026-01-18 08:32:19 +00:00
DEFAULT_ACCOUNT_ID ,
2026-03-16 21:13:56 -07:00
getChatChannelMeta ,
2026-01-18 08:32:19 +00:00
listDiscordDirectoryGroupsFromConfig ,
listDiscordDirectoryPeersFromConfig ,
PAIRING_APPROVED_MESSAGE ,
2026-03-05 23:07:13 -06:00
projectCredentialSnapshotFields ,
resolveConfiguredFromCredentialStatuses ,
2026-01-18 08:32:19 +00:00
resolveDiscordGroupRequireMention ,
2026-01-24 15:35:05 +13:00
resolveDiscordGroupToolPolicy ,
2026-01-18 11:00:19 +00:00
type ChannelMessageActionAdapter ,
2026-01-18 08:32:19 +00:00
type ChannelPlugin ,
2026-03-15 22:38:49 -07:00
type OpenClawConfig ,
2026-03-17 03:37:50 +00:00
} from "openclaw/plugin-sdk/discord" ;
2026-03-16 23:52:41 -07:00
import { resolveThreadSessionKeys , type RoutePeer } from "openclaw/plugin-sdk/routing" ;
2026-03-16 01:34:22 -07:00
import {
listDiscordAccountIds ,
resolveDiscordAccount ,
type ResolvedDiscordAccount ,
} from "./accounts.js" ;
2026-03-17 17:27:52 +01:00
import { auditDiscordChannelPermissions , collectDiscordAuditChannelIds } from "./audit.js" ;
2026-03-16 00:24:40 -07:00
import {
isDiscordExecApprovalClientEnabled ,
shouldSuppressLocalDiscordExecApprovalPrompt ,
} from "./exec-approvals.js" ;
2026-03-17 17:27:52 +01:00
import { monitorDiscordProvider } from "./monitor.js" ;
2026-03-16 01:34:22 -07:00
import {
looksLikeDiscordTargetId ,
normalizeDiscordMessagingTarget ,
normalizeDiscordOutboundTarget ,
} from "./normalize.js" ;
2026-03-17 17:27:52 +01:00
import { probeDiscord , type DiscordProbe } from "./probe.js" ;
2026-03-15 23:24:18 -07:00
import { resolveDiscordUserAllowlist } from "./resolve-users.js" ;
2026-01-18 11:00:19 +00:00
import { getDiscordRuntime } from "./runtime.js" ;
2026-03-15 22:52:56 -07:00
import { fetchChannelPermissionsDiscord } from "./send.js" ;
2026-03-16 18:49:04 -07:00
import { discordSetupAdapter } from "./setup-core.js" ;
2026-03-16 23:54:37 -07:00
import { createDiscordPluginBase , discordConfigAccessors } from "./shared.js" ;
2026-03-16 01:34:22 -07:00
import { collectDiscordStatusIssues } from "./status-issues.js" ;
2026-03-15 22:52:56 -07:00
import { parseDiscordTarget } from "./targets.js" ;
2026-03-15 22:38:49 -07:00
import { DiscordUiContainer } from "./ui.js" ;
2026-01-18 11:00:19 +00:00
2026-03-14 02:42:21 -07:00
type DiscordSendFn = ReturnType <
typeof getDiscordRuntime
> [ "channel" ] [ "discord" ] [ "sendMessageDiscord" ] ;
2026-03-16 21:13:56 -07:00
const meta = getChatChannelMeta ( "discord" ) ;
2026-03-15 22:52:56 -07:00
const REQUIRED_DISCORD_PERMISSIONS = [ "ViewChannel" , "SendMessages" ] as const ;
2026-01-18 08:32:19 +00:00
2026-03-15 22:52:56 -07:00
function formatDiscordIntents ( intents ? : {
messageContent? : string ;
guildMembers? : string ;
presence? : string ;
} ) {
if ( ! intents ) {
return "unknown" ;
}
return [
` messageContent= ${ intents . messageContent ? ? "unknown" } ` ,
` guildMembers= ${ intents . guildMembers ? ? "unknown" } ` ,
` presence= ${ intents . presence ? ? "unknown" } ` ,
] . join ( " " ) ;
}
2026-01-18 11:00:19 +00:00
const discordMessageActions : ChannelMessageActionAdapter = {
2026-03-18 00:06:55 +00:00
describeMessageTool : ( ctx ) = >
getDiscordRuntime ( ) . channel . discord . messageActions ? . describeMessageTool ? . ( ctx ) ? ? null ,
2026-02-09 10:05:38 -08:00
listActions : ( ctx ) = >
getDiscordRuntime ( ) . channel . discord . messageActions ? . listActions ? . ( ctx ) ? ? [ ] ,
2026-03-15 23:17:34 -07:00
getCapabilities : ( ctx ) = >
getDiscordRuntime ( ) . channel . discord . messageActions ? . getCapabilities ? . ( ctx ) ? ? [ ] ,
2026-03-17 23:48:04 +00:00
getToolSchema : ( ctx ) = >
getDiscordRuntime ( ) . channel . discord . messageActions ? . getToolSchema ? . ( ctx ) ? ? null ,
2026-02-09 10:05:38 -08:00
extractToolSend : ( ctx ) = >
getDiscordRuntime ( ) . channel . discord . messageActions ? . extractToolSend ? . ( ctx ) ? ? null ,
handleAction : async ( ctx ) = > {
const ma = getDiscordRuntime ( ) . channel . discord . messageActions ;
if ( ! ma ? . handleAction ) {
throw new Error ( "Discord message actions not available" ) ;
}
return ma . handleAction ( ctx ) ;
} ,
2026-03-15 22:38:49 -07:00
requiresTrustedRequesterSender : ( { action , toolContext } ) = >
Boolean ( toolContext && ( action === "timeout" || action === "kick" || action === "ban" ) ) ,
2026-01-18 11:00:19 +00:00
} ;
2026-03-15 22:38:49 -07:00
function buildDiscordCrossContextComponents ( params : {
originLabel : string ;
message : string ;
cfg : OpenClawConfig ;
accountId? : string | null ;
} ) {
const trimmed = params . message . trim ( ) ;
const components : Array < TextDisplay | Separator > = [ ] ;
if ( trimmed ) {
components . push ( new TextDisplay ( params . message ) ) ;
components . push ( new Separator ( { divider : true , spacing : "small" } ) ) ;
}
components . push ( new TextDisplay ( ` *From ${ params . originLabel } * ` ) ) ;
return [ new DiscordUiContainer ( { cfg : params.cfg , accountId : params.accountId , components } ) ] ;
}
function hasDiscordExecApprovalDmRoute ( cfg : OpenClawConfig ) : boolean {
return listDiscordAccountIds ( cfg ) . some ( ( accountId ) = > {
const execApprovals = resolveDiscordAccount ( { cfg , accountId } ) . config . execApprovals ;
if ( ! execApprovals ? . enabled || ( execApprovals . approvers ? . length ? ? 0 ) === 0 ) {
return false ;
}
const target = execApprovals . target ? ? "dm" ;
return target === "dm" || target === "both" ;
} ) ;
}
2026-03-15 23:24:18 -07:00
function readDiscordAllowlistConfig ( account : ResolvedDiscordAccount ) {
const groupOverrides : Array < { label : string ; entries : string [ ] } > = [ ] ;
for ( const [ guildKey , guildCfg ] of Object . entries ( account . config . guilds ? ? { } ) ) {
const entries = ( guildCfg ? . users ? ? [ ] ) . map ( String ) . filter ( Boolean ) ;
if ( entries . length > 0 ) {
groupOverrides . push ( { label : ` guild ${ guildKey } ` , entries } ) ;
}
for ( const [ channelKey , channelCfg ] of Object . entries ( guildCfg ? . channels ? ? { } ) ) {
const channelEntries = ( channelCfg ? . users ? ? [ ] ) . map ( String ) . filter ( Boolean ) ;
if ( channelEntries . length > 0 ) {
groupOverrides . push ( {
label : ` guild ${ guildKey } / channel ${ channelKey } ` ,
entries : channelEntries ,
} ) ;
}
}
}
return {
dmAllowFrom : ( account . config . allowFrom ? ? account . config . dm ? . allowFrom ? ? [ ] ) . map ( String ) ,
groupPolicy : account.config.groupPolicy ,
groupOverrides ,
} ;
}
async function resolveDiscordAllowlistNames ( params : {
cfg : Parameters < typeof resolveDiscordAccount > [ 0 ] [ "cfg" ] ;
accountId? : string | null ;
entries : string [ ] ;
} ) {
const account = resolveDiscordAccount ( { cfg : params.cfg , accountId : params.accountId } ) ;
const token = account . token ? . trim ( ) ;
if ( ! token ) {
return [ ] ;
}
return await resolveDiscordUserAllowlist ( { token , entries : params.entries } ) ;
}
function normalizeDiscordAcpConversationId ( conversationId : string ) {
const normalized = conversationId . trim ( ) ;
return normalized ? { conversationId : normalized } : null ;
}
function matchDiscordAcpConversation ( params : {
bindingConversationId : string ;
conversationId : string ;
parentConversationId? : string ;
} ) {
if ( params . bindingConversationId === params . conversationId ) {
return { conversationId : params.conversationId , matchPriority : 2 } ;
}
if (
params . parentConversationId &&
params . parentConversationId !== params . conversationId &&
params . bindingConversationId === params . parentConversationId
) {
return {
conversationId : params.parentConversationId ,
matchPriority : 1 ,
} ;
}
return null ;
}
function parseDiscordExplicitTarget ( raw : string ) {
try {
const target = parseDiscordTarget ( raw , { defaultKind : "channel" } ) ;
if ( ! target ) {
return null ;
}
return {
to : target.id ,
chatType : target.kind === "user" ? ( "direct" as const ) : ( "channel" as const ) ,
} ;
} catch {
return null ;
}
}
2026-03-16 00:09:28 -07:00
function buildDiscordBaseSessionKey ( params : {
cfg : OpenClawConfig ;
agentId : string ;
accountId? : string | null ;
peer : RoutePeer ;
} ) {
2026-03-17 05:40:03 +00:00
return buildOutboundBaseSessionKey ( { . . . params , channel : "discord" } ) ;
2026-03-16 00:09:28 -07:00
}
function resolveDiscordOutboundTargetKindHint ( params : {
target : string ;
resolvedTarget ? : { kind : string } ;
} ) : "user" | "channel" | undefined {
const resolvedKind = params . resolvedTarget ? . kind ;
if ( resolvedKind === "user" ) {
return "user" ;
}
if ( resolvedKind === "group" || resolvedKind === "channel" ) {
return "channel" ;
}
const target = params . target . trim ( ) ;
if ( /^channel:/i . test ( target ) ) {
return "channel" ;
}
if ( /^(user:|discord:|@|<@!?)/i . test ( target ) ) {
return "user" ;
}
return undefined ;
}
function resolveDiscordOutboundSessionRoute ( params : {
cfg : OpenClawConfig ;
agentId : string ;
accountId? : string | null ;
target : string ;
resolvedTarget ? : { kind : string } ;
replyToId? : string | null ;
threadId? : string | number | null ;
} ) {
const parsed = parseDiscordTarget ( params . target , {
defaultKind : resolveDiscordOutboundTargetKindHint ( params ) ,
} ) ;
if ( ! parsed ) {
return null ;
}
const isDm = parsed . kind === "user" ;
const peer : RoutePeer = {
kind : isDm ? "direct" : "channel" ,
id : parsed.id ,
} ;
const baseSessionKey = buildDiscordBaseSessionKey ( {
cfg : params.cfg ,
agentId : params.agentId ,
accountId : params.accountId ,
peer ,
} ) ;
const explicitThreadId = normalizeOutboundThreadId ( params . threadId ) ;
const threadCandidate = explicitThreadId ? ? normalizeOutboundThreadId ( params . replyToId ) ;
const threadKeys = resolveThreadSessionKeys ( {
baseSessionKey ,
threadId : threadCandidate ,
useSuffix : false ,
} ) ;
return {
sessionKey : threadKeys.sessionKey ,
baseSessionKey ,
peer ,
chatType : isDm ? ( "direct" as const ) : ( "channel" as const ) ,
from : isDm ? ` discord: ${ parsed . id } ` : ` discord:channel: ${ parsed . id } ` ,
to : isDm ? ` user: ${ parsed . id } ` : ` channel: ${ parsed . id } ` ,
threadId : explicitThreadId ? ? undefined ,
} ;
}
2026-01-18 08:32:19 +00:00
export const discordPlugin : ChannelPlugin < ResolvedDiscordAccount > = {
2026-03-17 05:34:13 +00:00
. . . createDiscordPluginBase ( {
setup : discordSetupAdapter ,
} ) ,
2026-01-18 08:32:19 +00:00
pairing : {
idLabel : "discordUserId" ,
normalizeAllowEntry : ( entry ) = > entry . replace ( /^(discord|user):/i , "" ) ,
notifyApproval : async ( { id } ) = > {
2026-01-18 11:00:19 +00:00
await getDiscordRuntime ( ) . channel . discord . sendMessageDiscord (
` user: ${ id } ` ,
PAIRING_APPROVED_MESSAGE ,
) ;
2026-01-18 08:32:19 +00:00
} ,
} ,
2026-03-15 23:24:18 -07:00
allowlist : {
supportsScope : ( { scope } ) = > scope === "dm" ,
readConfig : ( { cfg , accountId } ) = >
readDiscordAllowlistConfig ( resolveDiscordAccount ( { cfg , accountId } ) ) ,
resolveNames : async ( { cfg , accountId , entries } ) = >
await resolveDiscordAllowlistNames ( { cfg , accountId , entries } ) ,
2026-03-15 23:47:22 -07:00
applyConfigEdit : buildAccountScopedAllowlistConfigEditor ( {
channelId : "discord" ,
normalize : ( { cfg , accountId , values } ) = >
discordConfigAccessors . formatAllowFrom ! ( { cfg , accountId , allowFrom : values } ) ,
2026-03-17 04:24:01 +00:00
resolvePaths : resolveLegacyDmAllowlistConfigPaths ,
2026-03-15 23:47:22 -07:00
} ) ,
2026-03-15 23:24:18 -07:00
} ,
2026-01-18 08:32:19 +00:00
security : {
resolveDmPolicy : ( { cfg , accountId , account } ) = > {
2026-03-07 22:37:59 +00:00
return buildAccountScopedDmSecurityPolicy ( {
cfg ,
channelKey : "discord" ,
accountId ,
fallbackAccountId : account.accountId ? ? DEFAULT_ACCOUNT_ID ,
policy : account.config.dm?.policy ,
2026-01-18 08:32:19 +00:00
allowFrom : account.config.dm?.allowFrom ? ? [ ] ,
2026-03-07 22:37:59 +00:00
allowFromPathSuffix : "dm." ,
2026-01-18 08:32:19 +00:00
normalizeEntry : ( raw ) = > raw . replace ( /^(discord|user):/i , "" ) . replace ( /^<@!?(\d+)>$/ , "$1" ) ,
2026-03-07 22:37:59 +00:00
} ) ;
2026-01-18 08:32:19 +00:00
} ,
collectWarnings : ( { account , cfg } ) = > {
const guildEntries = account . config . guilds ? ? { } ;
const guildsConfigured = Object . keys ( guildEntries ) . length > 0 ;
const channelAllowlistConfigured = guildsConfigured ;
2026-03-07 23:59:44 +00:00
return collectOpenProviderGroupPolicyWarnings ( {
cfg ,
providerConfigPresent : cfg.channels?.discord !== undefined ,
configuredGroupPolicy : account.config.groupPolicy ,
collect : ( groupPolicy ) = >
collectOpenGroupPolicyConfiguredRouteWarnings ( {
groupPolicy ,
routeAllowlistConfigured : channelAllowlistConfigured ,
configureRouteAllowlist : {
surface : "Discord guilds" ,
openScope : "any channel not explicitly denied" ,
groupPolicyPath : "channels.discord.groupPolicy" ,
routeAllowlistPath : "channels.discord.guilds.<id>.channels" ,
} ,
missingRouteAllowlist : {
surface : "Discord guilds" ,
openBehavior :
"with no guild/channel allowlist; any channel can trigger (mention-gated)" ,
remediation :
'Set channels.discord.groupPolicy="allowlist" and configure channels.discord.guilds.<id>.channels' ,
} ,
} ) ,
} ) ;
2026-01-18 08:32:19 +00:00
} ,
} ,
groups : {
resolveRequireMention : resolveDiscordGroupRequireMention ,
2026-01-24 15:35:05 +13:00
resolveToolPolicy : resolveDiscordGroupToolPolicy ,
2026-01-18 08:32:19 +00:00
} ,
mentions : {
stripPatterns : ( ) = > [ "<@!?\\d+>" ] ,
} ,
threading : {
resolveReplyToMode : ( { cfg } ) = > cfg . channels ? . discord ? . replyToMode ? ? "off" ,
} ,
2026-02-15 21:19:25 -06:00
agentPrompt : {
messageToolHints : ( ) = > [
"- Discord components: set `components` when sending messages to include buttons, selects, or v2 containers." ,
"- Forms: add `components.modal` (title, fields). OpenClaw adds a trigger button and routes submissions as new messages." ,
] ,
} ,
2026-01-18 08:32:19 +00:00
messaging : {
normalizeTarget : normalizeDiscordMessagingTarget ,
2026-03-15 23:24:18 -07:00
parseExplicitTarget : ( { raw } ) = > parseDiscordExplicitTarget ( raw ) ,
inferTargetChatType : ( { to } ) = > parseDiscordExplicitTarget ( to ) ? . chatType ,
2026-03-15 22:38:49 -07:00
buildCrossContextComponents : buildDiscordCrossContextComponents ,
2026-03-16 00:09:28 -07:00
resolveOutboundSessionRoute : ( params ) = > resolveDiscordOutboundSessionRoute ( params ) ,
2026-01-18 08:32:19 +00:00
targetResolver : {
looksLikeId : looksLikeDiscordTargetId ,
hint : "<channelId|user:ID|channel:ID>" ,
} ,
} ,
2026-03-15 22:38:49 -07:00
execApprovals : {
getInitiatingSurfaceState : ( { cfg , accountId } ) = >
isDiscordExecApprovalClientEnabled ( { cfg , accountId } )
? { kind : "enabled" }
: { kind : "disabled" } ,
2026-03-16 00:24:40 -07:00
shouldSuppressLocalPrompt : ( { cfg , accountId , payload } ) = >
shouldSuppressLocalDiscordExecApprovalPrompt ( {
cfg ,
accountId ,
payload ,
} ) ,
2026-03-15 22:38:49 -07:00
hasConfiguredDmRoute : ( { cfg } ) = > hasDiscordExecApprovalDmRoute ( cfg ) ,
shouldSuppressForwardingFallback : ( { cfg , target } ) = >
( normalizeMessageChannel ( target . channel ) ? ? target . channel ) === "discord" &&
isDiscordExecApprovalClientEnabled ( { cfg , accountId : target.accountId } ) ,
} ,
2026-01-18 08:32:19 +00:00
directory : {
self : async ( ) = > null ,
listPeers : async ( params ) = > listDiscordDirectoryPeersFromConfig ( params ) ,
listGroups : async ( params ) = > listDiscordDirectoryGroupsFromConfig ( params ) ,
2026-01-18 11:00:19 +00:00
listPeersLive : async ( params ) = >
getDiscordRuntime ( ) . channel . discord . listDirectoryPeersLive ( params ) ,
listGroupsLive : async ( params ) = >
getDiscordRuntime ( ) . channel . discord . listDirectoryGroupsLive ( params ) ,
2026-01-18 08:32:19 +00:00
} ,
resolver : {
resolveTargets : async ( { cfg , accountId , inputs , kind } ) = > {
const account = resolveDiscordAccount ( { cfg , accountId } ) ;
const token = account . token ? . trim ( ) ;
if ( ! token ) {
return inputs . map ( ( input ) = > ( {
input ,
resolved : false ,
note : "missing Discord token" ,
} ) ) ;
}
if ( kind === "group" ) {
2026-01-18 11:00:19 +00:00
const resolved = await getDiscordRuntime ( ) . channel . discord . resolveChannelAllowlist ( {
token ,
entries : inputs ,
} ) ;
2026-01-18 08:32:19 +00:00
return resolved . map ( ( entry ) = > ( {
input : entry.input ,
resolved : entry.resolved ,
id : entry.channelId ? ? entry . guildId ,
name :
entry . channelName ? ?
entry . guildName ? ?
( entry . guildId && ! entry . channelId ? entry.guildId : undefined ) ,
note : entry.note ,
} ) ) ;
}
2026-01-18 11:00:19 +00:00
const resolved = await getDiscordRuntime ( ) . channel . discord . resolveUserAllowlist ( {
token ,
entries : inputs ,
} ) ;
2026-01-18 08:32:19 +00:00
return resolved . map ( ( entry ) = > ( {
input : entry.input ,
resolved : entry.resolved ,
id : entry.id ,
name : entry.name ,
note : entry.note ,
} ) ) ;
} ,
} ,
actions : discordMessageActions ,
2026-03-15 16:47:47 -07:00
setup : discordSetupAdapter ,
2026-01-18 08:32:19 +00:00
outbound : {
deliveryMode : "direct" ,
chunker : null ,
textChunkLimit : 2000 ,
pollMaxOptions : 10 ,
2026-02-13 07:09:49 -05:00
resolveTarget : ( { to } ) = > normalizeDiscordOutboundTarget ( to ) ,
2026-03-04 00:20:44 -06:00
sendText : async ( { cfg , to , text , accountId , deps , replyToId , silent } ) = > {
2026-03-14 02:42:21 -07:00
const send =
resolveOutboundSendDep < DiscordSendFn > ( deps , "discord" ) ? ?
getDiscordRuntime ( ) . channel . discord . sendMessageDiscord ;
2026-01-18 08:32:19 +00:00
const result = await send ( to , text , {
verbose : false ,
2026-03-04 00:20:44 -06:00
cfg ,
2026-01-18 08:32:19 +00:00
replyTo : replyToId ? ? undefined ,
accountId : accountId ? ? undefined ,
2026-02-14 18:34:30 +01:00
silent : silent ? ? undefined ,
2026-01-18 08:32:19 +00:00
} ) ;
return { channel : "discord" , . . . result } ;
} ,
2026-02-22 22:52:49 +01:00
sendMedia : async ( {
2026-03-04 00:20:44 -06:00
cfg ,
2026-02-22 22:52:49 +01:00
to ,
text ,
mediaUrl ,
mediaLocalRoots ,
accountId ,
deps ,
replyToId ,
silent ,
} ) = > {
2026-03-14 02:42:21 -07:00
const send =
resolveOutboundSendDep < DiscordSendFn > ( deps , "discord" ) ? ?
getDiscordRuntime ( ) . channel . discord . sendMessageDiscord ;
2026-01-18 08:32:19 +00:00
const result = await send ( to , text , {
verbose : false ,
2026-03-04 00:20:44 -06:00
cfg ,
2026-01-18 08:32:19 +00:00
mediaUrl ,
2026-02-22 22:52:49 +01:00
mediaLocalRoots ,
2026-01-18 08:32:19 +00:00
replyTo : replyToId ? ? undefined ,
accountId : accountId ? ? undefined ,
2026-02-14 18:34:30 +01:00
silent : silent ? ? undefined ,
2026-01-18 08:32:19 +00:00
} ) ;
return { channel : "discord" , . . . result } ;
} ,
2026-03-04 00:20:44 -06:00
sendPoll : async ( { cfg , to , poll , accountId , silent } ) = >
2026-01-18 11:00:19 +00:00
await getDiscordRuntime ( ) . channel . discord . sendPollDiscord ( to , poll , {
2026-03-04 00:20:44 -06:00
cfg ,
2026-01-18 08:32:19 +00:00
accountId : accountId ? ? undefined ,
2026-02-14 18:34:30 +01:00
silent : silent ? ? undefined ,
2026-01-18 08:32:19 +00:00
} ) ,
} ,
2026-03-17 17:27:52 +01:00
bindings : {
compileConfiguredBinding : ( { conversationId } ) = >
2026-03-15 23:24:18 -07:00
normalizeDiscordAcpConversationId ( conversationId ) ,
2026-03-17 17:27:52 +01:00
matchInboundConversation : ( { compiledBinding , conversationId , parentConversationId } ) = >
matchDiscordAcpConversation ( {
bindingConversationId : compiledBinding.conversationId ,
conversationId ,
parentConversationId ,
} ) ,
2026-03-15 23:24:18 -07:00
} ,
2026-01-18 08:32:19 +00:00
status : {
defaultRuntime : {
accountId : DEFAULT_ACCOUNT_ID ,
running : false ,
2026-03-02 00:23:07 +00:00
connected : false ,
reconnectAttempts : 0 ,
lastConnectedAt : null ,
lastDisconnect : null ,
lastEventAt : null ,
2026-01-18 08:32:19 +00:00
lastStartAt : null ,
lastStopAt : null ,
lastError : null ,
} ,
collectStatusIssues : collectDiscordStatusIssues ,
2026-02-23 21:25:20 +00:00
buildChannelSummary : ( { snapshot } ) = >
buildTokenChannelStatusSummary ( snapshot , { includeMode : false } ) ,
2026-01-18 08:32:19 +00:00
probeAccount : async ( { account , timeoutMs } ) = >
2026-03-17 17:27:52 +01:00
probeDiscord ( account . token , timeoutMs , {
2026-01-18 11:00:19 +00:00
includeApplication : true ,
} ) ,
2026-03-15 22:52:56 -07:00
formatCapabilitiesProbe : ( { probe } ) = > {
const discordProbe = probe as DiscordProbe | undefined ;
const lines = [ ] ;
if ( discordProbe ? . bot ? . username ) {
const botId = discordProbe . bot . id ? ` ( ${ discordProbe . bot . id } ) ` : "" ;
lines . push ( { text : ` Bot: @ ${ discordProbe . bot . username } ${ botId } ` } ) ;
}
if ( discordProbe ? . application ? . intents ) {
lines . push ( { text : ` Intents: ${ formatDiscordIntents ( discordProbe . application . intents ) } ` } ) ;
}
return lines ;
} ,
buildCapabilitiesDiagnostics : async ( { account , timeoutMs , target } ) = > {
if ( ! target ? . trim ( ) ) {
return undefined ;
}
const parsedTarget = parseDiscordTarget ( target . trim ( ) , { defaultKind : "channel" } ) ;
const details : Record < string , unknown > = {
target : {
raw : target ,
normalized : parsedTarget?.normalized ,
kind : parsedTarget?.kind ,
channelId : parsedTarget?.kind === "channel" ? parsedTarget.id : undefined ,
} ,
} ;
if ( ! parsedTarget || parsedTarget . kind !== "channel" ) {
return {
details ,
lines : [
{
text : "Permissions: Target looks like a DM user; pass channel:<id> to audit channel permissions." ,
tone : "error" ,
} ,
] ,
} ;
}
const token = account . token ? . trim ( ) ;
if ( ! token ) {
return {
details ,
lines : [
{
text : "Permissions: Discord bot token missing for permission audit." ,
tone : "error" ,
} ,
] ,
} ;
}
try {
const perms = await fetchChannelPermissionsDiscord ( parsedTarget . id , {
token ,
accountId : account.accountId ? ? undefined ,
} ) ;
const missingRequired = REQUIRED_DISCORD_PERMISSIONS . filter (
( permission ) = > ! perms . permissions . includes ( permission ) ,
) ;
details . permissions = {
channelId : perms.channelId ,
guildId : perms.guildId ,
isDm : perms.isDm ,
channelType : perms.channelType ,
permissions : perms.permissions ,
missingRequired ,
raw : perms.raw ,
} ;
return {
details ,
lines : [
{
text : ` Permissions ( ${ perms . channelId } ): ${ perms . permissions . length ? perms . permissions . join ( ", " ) : "none" } ` ,
} ,
missingRequired . length > 0
? { text : ` Missing required: ${ missingRequired . join ( ", " ) } ` , tone : "warn" }
: { text : "Missing required: none" , tone : "success" } ,
] ,
} ;
} catch ( err ) {
const message = err instanceof Error ? err.message : String ( err ) ;
details . permissions = { channelId : parsedTarget.id , error : message } ;
return {
details ,
lines : [ { text : ` Permissions: ${ message } ` , tone : "error" } ] ,
} ;
}
} ,
2026-01-18 08:32:19 +00:00
auditAccount : async ( { account , timeoutMs , cfg } ) = > {
const { channelIds , unresolvedChannels } = collectDiscordAuditChannelIds ( {
cfg ,
accountId : account.accountId ,
} ) ;
2026-01-31 22:13:48 +09:00
if ( ! channelIds . length && unresolvedChannels === 0 ) {
return undefined ;
}
2026-01-18 08:32:19 +00:00
const botToken = account . token ? . trim ( ) ;
if ( ! botToken ) {
return {
ok : unresolvedChannels === 0 ,
checkedChannels : 0 ,
unresolvedChannels ,
channels : [ ] ,
elapsedMs : 0 ,
} ;
}
2026-03-17 17:27:52 +01:00
const audit = await auditDiscordChannelPermissions ( {
2026-01-18 08:32:19 +00:00
token : botToken ,
accountId : account.accountId ,
channelIds ,
timeoutMs ,
} ) ;
return { . . . audit , unresolvedChannels } ;
} ,
buildAccountSnapshot : ( { account , runtime , probe , audit } ) = > {
2026-03-05 23:07:13 -06:00
const configured =
resolveConfiguredFromCredentialStatuses ( account ) ? ? Boolean ( account . token ? . trim ( ) ) ;
2026-01-18 08:32:19 +00:00
const app = runtime ? . application ? ? ( probe as { application? : unknown } ) ? . application ;
const bot = runtime ? . bot ? ? ( probe as { bot? : unknown } ) ? . bot ;
2026-03-07 20:01:41 +00:00
const base = buildComputedAccountStatusSnapshot ( {
2026-01-18 08:32:19 +00:00
accountId : account.accountId ,
name : account.name ,
enabled : account.enabled ,
configured ,
2026-03-07 20:01:41 +00:00
runtime ,
probe ,
} ) ;
return {
. . . base ,
2026-03-05 23:07:13 -06:00
. . . projectCredentialSnapshotFields ( account ) ,
2026-03-02 00:23:07 +00:00
connected : runtime?.connected ? ? false ,
reconnectAttempts : runtime?.reconnectAttempts ,
lastConnectedAt : runtime?.lastConnectedAt ? ? null ,
lastDisconnect : runtime?.lastDisconnect ? ? null ,
lastEventAt : runtime?.lastEventAt ? ? null ,
2026-01-18 08:32:19 +00:00
application : app ? ? undefined ,
bot : bot ? ? undefined ,
audit ,
} ;
} ,
} ,
gateway : {
startAccount : async ( ctx ) = > {
const account = ctx . account ;
const token = account . token . trim ( ) ;
let discordBotLabel = "" ;
try {
2026-03-17 17:27:52 +01:00
const probe = await probeDiscord ( token , 2500 , {
2026-01-18 08:32:19 +00:00
includeApplication : true ,
} ) ;
const username = probe . ok ? probe . bot ? . username ? . trim ( ) : null ;
2026-01-31 22:13:48 +09:00
if ( username ) {
discordBotLabel = ` (@ ${ username } ) ` ;
}
2026-01-18 08:32:19 +00:00
ctx . setStatus ( {
accountId : account.accountId ,
bot : probe.bot ,
application : probe.application ,
} ) ;
const messageContent = probe . application ? . intents ? . messageContent ;
if ( messageContent === "disabled" ) {
ctx . log ? . warn (
` [ ${ account . accountId } ] Discord Message Content Intent is disabled; bot may not respond to channel messages. Enable it in Discord Dev Portal (Bot → Privileged Gateway Intents) or require mentions. ` ,
) ;
} else if ( messageContent === "limited" ) {
ctx . log ? . info (
` [ ${ account . accountId } ] Discord Message Content Intent is limited; bots under 100 servers can use it without verification. ` ,
) ;
}
} catch ( err ) {
2026-01-18 11:00:19 +00:00
if ( getDiscordRuntime ( ) . 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 ${ discordBotLabel } ` ) ;
2026-03-17 17:27:52 +01:00
return monitorDiscordProvider ( {
2026-01-18 08:32:19 +00:00
token ,
accountId : account.accountId ,
config : ctx.cfg ,
runtime : ctx.runtime ,
abortSignal : ctx.abortSignal ,
mediaMaxMb : account.config.mediaMaxMb ,
historyLimit : account.config.historyLimit ,
2026-03-02 00:23:07 +00:00
setStatus : ( patch ) = > ctx . setStatus ( { accountId : account.accountId , . . . patch } ) ,
2026-01-18 08:32:19 +00:00
} ) ;
} ,
} ,
} ;