UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

1,139 lines (1,138 loc) 47.1 kB
import { c as normalizeOptionalString, p as readStringValue } from "./string-coerce-mnp54Vah.js"; import { r as getChildLogger } from "./logger-Brhy0cvC.js"; import { a as shouldLogVerbose, r as logVerbose } from "./globals-GTrXU4s9.js"; import { t as logDebug } from "./logger-lqqYRtFw.js"; import { l as mimeTypeFromFilePath } from "./mime-C8mVE2Bw.js"; import { n as formatAllowlistMatchMeta } from "./allowlist-match-B04-UeMC.js"; import { n as isAbortRequestText } from "./abort-primitives-CNNnD8oN.js"; import { t as hasControlCommand } from "./command-detection-iKcBJ8it.js"; import { a as enqueueSystemEvent } from "./system-events-C5WI3S5a.js"; import "./logging-core-BH_8JYZP.js"; import "./runtime-env-D_2q8-VK.js"; import "./string-coerce-runtime-CEGJWkQ_.js"; import { n as shouldHandleTextCommands } from "./commands-text-routing-B25gG5ny.js"; import { i as matchesMentionWithExplicit, n as buildMentionRegexes } from "./mentions-Be0DZkGs.js"; import { E as toInboundMediaFacts, i as recordDroppedChannelInboundHistory } from "./kernel-BdraY_qZ.js"; import { n as resolveInboundMentionDecision, t as implicitMentionKindWhen } from "./mention-gating-3P8aSD7o.js"; import { n as recordChannelActivity } from "./channel-activity-4piA219h.js"; import "./allow-from-DCaV3hzI.js"; import { n as isDangerousNameMatchingEnabled } from "./dangerous-name-matching-D4QRC91G.js"; import { n as logInboundDrop } from "./logging-gUWPKC5g.js"; import "./system-event-runtime-D80J5tzu.js"; import "./media-mime-v9DSctG7.js"; import "./dangerous-name-runtime-zP2lSdMQ.js"; import "./command-primitives-runtime-BC_tCClg.js"; import "./command-detection-C5wNjT-o.js"; import "./command-surface-C3BkEX9f.js"; import { i as resolveUnmentionedGroupInboundPolicy, r as classifyChannelInboundEvent } from "./channel-inbound-bl7VmTdr.js"; import { o as resolveDefaultDiscordAccountId } from "./accounts-BI_6uJlY.js"; import { It as ChannelType, Ut as MessageReferenceType, Wt as MessageType, ot as getChannelMessage, x as Message } from "./discord-DdqXEGEm.js"; import { i as resolveTimestampMs, n as formatDiscordUserTag, r as resolveDiscordSystemLocation } from "./format-DZW075F7.js"; import { _ as resolveGroupDmAllow, a as normalizeDiscordSlug, c as resolveDiscordChannelConfigWithFallback, d as resolveDiscordGuildEntry, f as resolveDiscordMemberAccessState, g as resolveDiscordShouldRequireMention, i as normalizeDiscordDisplaySlug, n as isDiscordGroupAllowedByPolicy } from "./allow-list-CBhA2l4a.js"; import { t as resolveDiscordConversationIdentity } from "./conversation-identity-LwFFXgXY.js"; import { d as isRecentlyUnboundThreadWebhookMessage } from "./thread-bindings.state-DOSLgEaL.js"; import "./thread-bindings-BCzrjL-7.js"; import { n as resolveDiscordChannelInfoSafe, r as resolveDiscordChannelNameSafe } from "./channel-access-C12aDZ0p.js"; import { a as shouldIgnoreStaleDiscordRouteBinding, c as resolveDiscordTextCommandAccess, i as resolveDiscordEffectiveRoute, o as handleDiscordDmCommandDecision, r as resolveDiscordConversationRoute, s as resolveDiscordDmCommandAccess, t as buildDiscordRoutePeer } from "./route-resolution-CHy64Xc7.js"; import { d as resolveDiscordMessageChannelId, l as resolveDiscordMessageStickers, o as resolveMediaList, r as resolveDiscordMessageText, u as resolveDiscordChannelInfo } from "./message-utils-BjIDHgFC.js"; import { n as resolveDiscordWebhookId, t as resolveDiscordSenderIdentity } from "./sender-identity-BrSQQ4mW.js"; //#region extensions/discord/src/monitor/message-handler.dm-preflight.ts let conversationRuntimePromise$1; let discordSendRuntimePromise; async function loadConversationRuntime$1() { conversationRuntimePromise$1 ??= import("./plugin-sdk/conversation-binding-runtime.js"); return await conversationRuntimePromise$1; } async function loadDiscordSendRuntime() { discordSendRuntimePromise ??= import("./send-B-l8DwLG.js"); return await discordSendRuntimePromise; } function resolveDiscordDmPairingSenderId(sender) { return sender.isPluralKit ? `pk:${sender.id}` : sender.id; } async function resolveDiscordDmPreflightAccess(params) { if (params.dmPolicy === "disabled") { logVerbose("discord: drop dm (dmPolicy: disabled)"); return null; } const directBindingConversationId = resolveDiscordConversationIdentity({ isDirectMessage: true, userId: params.author.id }) ?? `user:${params.author.id}`; const directBindingRecord = (await loadConversationRuntime$1()).getSessionBindingService().resolveByConversation({ channel: "discord", accountId: params.preflight.accountId, conversationId: directBindingConversationId }); const dmAccess = await resolveDiscordDmCommandAccess({ accountId: params.resolvedAccountId, dmPolicy: params.dmPolicy, configuredAllowFrom: params.preflight.allowFrom ?? [], sender: { id: params.sender.id, name: params.sender.name, tag: params.sender.tag }, allowNameMatching: params.allowNameMatching, cfg: params.preflight.cfg, token: params.preflight.token, rest: params.preflight.client.rest }); const commandAuthorized = dmAccess.senderAccess.allowed && dmAccess.commandAccess.authorized || directBindingRecord != null; if (dmAccess.senderAccess.decision === "allow") return { commandAuthorized }; if (directBindingRecord) { logVerbose(`discord: allow bound DM conversation ${directBindingConversationId} despite dmPolicy=${params.dmPolicy}`); return { commandAuthorized }; } await handleDiscordDmCommandDecision({ senderAccess: dmAccess.senderAccess, accountId: params.resolvedAccountId, sender: { id: resolveDiscordDmPairingSenderId(params.sender), tag: params.sender.tag ?? formatDiscordUserTag(params.author), name: params.sender.name ?? params.author.username ?? void 0 }, onPairingCreated: async (code) => { logVerbose(`discord pairing request sender=${params.author.id} tag=${formatDiscordUserTag(params.author)} reason=${dmAccess.senderAccess.reasonCode}`); try { const conversationRuntime = await loadConversationRuntime$1(); const { sendMessageDiscord } = await loadDiscordSendRuntime(); await sendMessageDiscord(`user:${params.author.id}`, conversationRuntime.buildPairingReply({ channel: "discord", idLine: `Your Discord user id: ${params.author.id}`, code }), { cfg: params.preflight.cfg, token: params.preflight.token, rest: params.preflight.client.rest, accountId: params.preflight.accountId }); } catch (err) { logVerbose(`discord pairing reply failed for ${params.author.id}: ${String(err)}`); } }, onUnauthorized: async () => { logVerbose(`Blocked unauthorized discord sender ${params.sender.id} (dmPolicy=${params.dmPolicy}, reason=${dmAccess.senderAccess.reasonCode})`); } }); return null; } //#endregion //#region extensions/discord/src/monitor/message-handler.hydration.ts function mergeFetchedDiscordMessage(base, fetched) { const baseRawData = readMessageRawData(base); const baseFallback = readMessageFallback(base); const rawData = { ...baseRawData, ...fetched, id: fetched.id ?? baseRawData.id ?? baseFallback.id, channel_id: fetched.channel_id ?? baseRawData.channel_id ?? baseFallback.channel_id, content: fetched.content ?? baseRawData.content ?? baseFallback.content, author: fetched.author ?? baseRawData.author ?? baseFallback.author, attachments: fetched.attachments ?? baseRawData.attachments ?? baseFallback.attachments, embeds: fetched.embeds ?? baseRawData.embeds ?? baseFallback.embeds, mentions: fetched.mentions ?? baseRawData.mentions ?? baseFallback.mentions, mention_roles: fetched.mention_roles ?? baseRawData.mention_roles ?? baseFallback.mention_roles, mention_everyone: fetched.mention_everyone ?? baseRawData.mention_everyone ?? baseFallback.mention_everyone, timestamp: fetched.timestamp ?? baseRawData.timestamp ?? baseFallback.timestamp, tts: fetched.tts ?? baseRawData.tts ?? false, pinned: fetched.pinned ?? baseRawData.pinned ?? false, type: fetched.type ?? baseRawData.type ?? 0, message_snapshots: fetched.message_snapshots ?? baseRawData.message_snapshots ?? baseFallback.message_snapshots, sticker_items: fetched.sticker_items ?? baseRawData.sticker_items ?? baseFallback.sticker_items }; const hydrated = new Message(readMessageClient(base), rawData); copyRuntimeMessageFields(base, hydrated); return hydrated; } function readMessageClient(message) { return message.client; } function readMessageRawData(message) { try { const rawData = message.rawData; return rawData && typeof rawData === "object" ? rawData : {}; } catch { return {}; } } function readMessageFallback(message) { const value = message; return { id: typeof value.id === "string" ? value.id : "", channel_id: readStringValue(value.channel_id) ?? readStringValue(value.channelId) ?? "", content: typeof value.content === "string" ? value.content : "", author: normalizeApiUser(value.author), attachments: Array.isArray(value.attachments) ? value.attachments : [], embeds: Array.isArray(value.embeds) ? value.embeds : [], mentions: normalizeApiUsers(value.mentionedUsers), mention_roles: normalizeStringArray(value.mentionedRoles), mention_everyone: value.mentionedEveryone === true, timestamp: readStringValue(value.timestamp) ?? "1970-01-01T00:00:00.000Z", sticker_items: Array.isArray(value.sticker_items) ? value.sticker_items : Array.isArray(value.stickers) ? value.stickers : void 0, message_snapshots: Array.isArray(value.message_snapshots) ? value.message_snapshots : void 0 }; } function normalizeStringArray(value) { return Array.isArray(value) ? value.flatMap((entry) => typeof entry === "string" ? [entry] : []) : []; } function normalizeApiUsers(value) { return Array.isArray(value) ? value.flatMap((entry) => { const user = normalizeApiUser(entry); return user.id ? [user] : []; }) : []; } function normalizeApiUser(value) { if (!value || typeof value !== "object") return { id: "", username: "", discriminator: "0", global_name: null, avatar: null }; const input = value; return { id: readStringValue(input.id) ?? "", username: readStringValue(input.username) ?? "", discriminator: readStringValue(input.discriminator) ?? "0", global_name: readStringValue(input.global_name) ?? readStringValue(input.globalName) ?? null, avatar: input.avatar === null ? null : readStringValue(input.avatar) ?? null, ...typeof input.bot === "boolean" ? { bot: input.bot } : {} }; } function copyRuntimeMessageFields(source, target) { const channelDescriptor = Object.getOwnPropertyDescriptor(source, "channel"); if (channelDescriptor) Object.defineProperty(target, "channel", channelDescriptor); } function shouldHydrateDiscordMessage(params) { if (hasMissingReferencedMessagePayload(params.message)) return true; let currentText; try { currentText = resolveDiscordMessageText(params.message, { includeForwarded: true }); } catch { return true; } if (!currentText) return true; if ((params.message.mentionedUsers?.length ?? 0) > 0 || (params.message.mentionedRoles?.length ?? 0) > 0 || params.message.mentionedEveryone) return false; return /<@!?\d+>|<@&\d+>|@everyone|@here/u.test(currentText); } function hasMissingReferencedMessagePayload(message) { const reference = message.messageReference; if (!reference?.message_id) return false; if (reference.type != null && reference.type !== MessageReferenceType.Default) return false; if (message.type != null && message.type !== MessageType.Reply) return false; return !Object.hasOwn(readMessageRawData(message), "referenced_message"); } async function hydrateDiscordMessageIfNeeded(params) { params.channelInfo; if (!shouldHydrateDiscordMessage({ message: params.message })) return params.message; try { const fetched = await getChannelMessage(params.client.rest, params.messageChannelId, params.message.id); if (!fetched) return params.message; logVerbose(`discord: hydrated inbound payload via REST for ${params.message.id}`); return mergeFetchedDiscordMessage(params.message, fetched); } catch (err) { logVerbose(`discord: failed to hydrate message ${params.message.id}: ${String(err)}`); return params.message; } } //#endregion //#region extensions/discord/src/monitor/message-handler.preflight-channel-access.ts function resolveDiscordPreflightChannelAccess(params) { if (params.isGuildMessage && params.channelConfig?.enabled === false) { logDebug(`[discord-preflight] drop: channel disabled`); logVerbose(`Blocked discord channel ${params.messageChannelId} (channel disabled, ${params.channelMatchMeta})`); return { allowed: false, channelAllowlistConfigured: false, channelAllowed: false }; } const groupDmAllowed = params.isGroupDm && resolveGroupDmAllow({ channels: params.groupDmChannels, channelId: params.messageChannelId, channelName: params.displayChannelName, channelSlug: params.displayChannelSlug }); if (params.isGroupDm && !groupDmAllowed) return { allowed: false, channelAllowlistConfigured: false, channelAllowed: false }; const channelAllowlistConfigured = Boolean(params.guildInfo?.channels) && Object.keys(params.guildInfo?.channels ?? {}).length > 0; const channelAllowed = params.channelConfig?.allowed !== false; if (params.isGuildMessage && !isDiscordGroupAllowedByPolicy({ groupPolicy: params.groupPolicy, guildAllowlisted: Boolean(params.guildInfo), channelAllowlistConfigured, channelAllowed })) { if (params.groupPolicy === "disabled") { logDebug(`[discord-preflight] drop: groupPolicy disabled`); logVerbose(`discord: drop guild message (groupPolicy: disabled, ${params.channelMatchMeta})`); } else if (!channelAllowlistConfigured) { logDebug(`[discord-preflight] drop: groupPolicy allowlist, no channel allowlist configured`); logVerbose(`discord: drop guild message (groupPolicy: allowlist, no channel allowlist, ${params.channelMatchMeta})`); } else { logDebug(`[discord] Ignored message from channel ${params.messageChannelId} (not in guild allowlist). Add to guilds.<guildId>.channels to enable.`); logVerbose(`Blocked discord channel ${params.messageChannelId} not in guild channel allowlist (groupPolicy: allowlist, ${params.channelMatchMeta})`); } return { allowed: false, channelAllowlistConfigured, channelAllowed }; } if (params.isGuildMessage && params.channelConfig?.allowed === false) { logDebug(`[discord-preflight] drop: channelConfig.allowed===false`); logVerbose(`Blocked discord channel ${params.messageChannelId} not in guild channel allowlist (${params.channelMatchMeta})`); return { allowed: false, channelAllowlistConfigured, channelAllowed }; } if (params.isGuildMessage) { logDebug(`[discord-preflight] pass: channel allowed`); logVerbose(`discord: allow channel ${params.messageChannelId} (${params.channelMatchMeta})`); } return { allowed: true, channelAllowlistConfigured, channelAllowed }; } //#endregion //#region extensions/discord/src/monitor/message-handler.preflight-channel-context.ts function resolveDiscordPreflightChannelContext(params) { const threadName = params.threadChannel?.name; const configChannelName = params.threadParentName ?? params.channelName; const configChannelSlug = configChannelName ? normalizeDiscordSlug(configChannelName) : ""; const displayChannelName = threadName ?? params.channelName; const displayChannelSlug = displayChannelName ? normalizeDiscordDisplaySlug(displayChannelName) : ""; const guildSlug = params.guildInfo?.slug || (params.guildName ? normalizeDiscordSlug(params.guildName) : ""); const threadChannelSlug = params.channelName ? normalizeDiscordSlug(params.channelName) : ""; const threadParentSlug = params.threadParentName ? normalizeDiscordSlug(params.threadParentName) : ""; return { threadName, configChannelName, configChannelSlug, displayChannelName, displayChannelSlug, guildSlug, threadChannelSlug, threadParentSlug, channelConfig: params.isGuildMessage ? resolveDiscordChannelConfigWithFallback({ guildInfo: params.guildInfo, channelId: params.messageChannelId, channelName: params.channelName, channelSlug: threadChannelSlug, parentId: params.threadParentId, parentName: params.threadParentName, parentSlug: threadParentSlug, scope: params.threadChannel ? "thread" : "channel" }) : null }; } //#endregion //#region extensions/discord/src/monitor/message-handler.preflight-context.ts function buildDiscordMessagePreflightContext({ preflightParams, ...fields }) { return { cfg: preflightParams.cfg, discordConfig: preflightParams.discordConfig, accountId: preflightParams.accountId, token: preflightParams.token, runtime: preflightParams.runtime, botUserId: preflightParams.botUserId, abortSignal: preflightParams.abortSignal, guildHistories: preflightParams.guildHistories, historyLimit: preflightParams.historyLimit, mediaMaxBytes: preflightParams.mediaMaxBytes, textLimit: preflightParams.textLimit, replyToMode: preflightParams.replyToMode, ackReactionScope: preflightParams.ackReactionScope, groupPolicy: preflightParams.groupPolicy, ...fields, threadBindings: preflightParams.threadBindings, discordRestFetch: preflightParams.discordRestFetch }; } //#endregion //#region extensions/discord/src/monitor/message-handler.preflight-helpers.ts const DISCORD_BOUND_THREAD_SYSTEM_PREFIXES = [ "⚙️", "🤖", "🧰" ]; function isBoundThreadBotSystemMessage(params) { if (!params.isBoundThreadSession || !params.isBotAuthor) return false; const text = params.text?.trim(); if (!text) return false; return DISCORD_BOUND_THREAD_SYSTEM_PREFIXES.some((prefix) => text.startsWith(prefix)); } function isDiscordThreadChannelType(type) { return type === ChannelType.PublicThread || type === ChannelType.PrivateThread || type === ChannelType.AnnouncementThread; } function isDiscordThreadChannelMessage(params) { if (!params.isGuildMessage) return false; const channel = "channel" in params.message ? params.message.channel : void 0; return Boolean(channel && typeof channel === "object" && "isThread" in channel && typeof channel.isThread === "function" && channel.isThread() || isDiscordThreadChannelType(params.channelInfo?.type)); } function resolveInjectedBoundThreadLookupRecord(params) { const getByThreadId = params.threadBindings.getByThreadId; if (typeof getByThreadId !== "function") return; const binding = getByThreadId(params.threadId); return binding && typeof binding === "object" ? binding : void 0; } function resolveDiscordMentionState(params) { if (params.isDirectMessage) return { implicitMentionKinds: [], wasMentioned: false }; const wasMentioned = params.mentionedEveryone && (!params.authorIsBot || params.senderIsPluralKit) || matchesMentionWithExplicit({ text: params.mentionText, mentionRegexes: params.mentionRegexes, explicit: { hasAnyMention: params.hasAnyMention, isExplicitlyMentioned: params.isExplicitlyMentioned, canResolveExplicit: Boolean(params.botId) }, transcript: params.transcript }); return { implicitMentionKinds: implicitMentionKindWhen("reply_to_bot", Boolean(params.botId) && Boolean(params.referencedAuthorId) && params.referencedAuthorId === params.botId), wasMentioned }; } function resolvePreflightMentionRequirement(params) { if (!params.shouldRequireMention) return false; return !params.bypassMentionRequirement; } function shouldIgnoreBoundThreadWebhookMessage(params) { const webhookId = normalizeOptionalString(params.webhookId) ?? ""; if (!webhookId) return false; const boundWebhookId = normalizeOptionalString(params.threadBinding?.webhookId) ?? normalizeOptionalString(params.threadBinding?.metadata?.webhookId) ?? ""; if (boundWebhookId && webhookId === boundWebhookId) return true; const threadId = normalizeOptionalString(params.threadId) ?? ""; if (!threadId) return false; if (params.threadBinding) return true; return isRecentlyUnboundThreadWebhookMessage({ accountId: params.accountId, threadId, webhookId }); } //#endregion //#region extensions/discord/src/monitor/message-handler.preflight-history.ts function buildDiscordPreflightHistoryEntry(params) { const textForHistory = resolveDiscordMessageText(params.message, { includeForwarded: true }); return params.isGuildMessage && params.historyLimit > 0 && textForHistory ? { sender: params.senderLabel, body: textForHistory, timestamp: resolveTimestampMs(params.message.timestamp), messageId: params.message.id } : void 0; } //#endregion //#region extensions/discord/src/monitor/message-handler.preflight-logging.ts function logDiscordPreflightChannelConfig(params) { if (!shouldLogVerbose()) return; logDebug(`[discord-preflight] channelConfig=${params.channelConfig ? `allowed=${params.channelConfig.allowed} enabled=${params.channelConfig.enabled ?? "unset"} requireMention=${params.channelConfig.requireMention ?? "unset"} ignoreOtherMentions=${params.channelConfig.ignoreOtherMentions ?? "unset"} matchKey=${params.channelConfig.matchKey ?? "none"} matchSource=${params.channelConfig.matchSource ?? "none"} users=${params.channelConfig.users?.length ?? 0} roles=${params.channelConfig.roles?.length ?? 0} skills=${params.channelConfig.skills?.length ?? 0}` : "none"} channelMatchMeta=${params.channelMatchMeta} channelId=${params.channelId}`); } function logDiscordPreflightInboundSummary(params) { if (!shouldLogVerbose()) return; logVerbose(`discord: inbound id=${params.messageId} guild=${params.guildId ?? "dm"} channel=${params.channelId} mention=${params.wasMentioned ? "yes" : "no"} type=${params.isDirectMessage ? "dm" : params.isGroupDm ? "group-dm" : "guild"} content=${params.hasContent ? "yes" : "no"}`); } //#endregion //#region extensions/discord/src/monitor/message-handler.preflight-runtime.ts let pluralkitRuntimePromise; let preflightAudioRuntimePromise; let systemEventsRuntimePromise; let discordThreadingRuntimePromise; async function loadPluralKitRuntime() { pluralkitRuntimePromise ??= import("./pluralkit-Dx0-7Dil.js"); return await pluralkitRuntimePromise; } async function loadPreflightAudioRuntime() { preflightAudioRuntimePromise ??= import("./preflight-audio-CyHzvWAy.js"); return await preflightAudioRuntimePromise; } async function loadSystemEventsRuntime() { systemEventsRuntimePromise ??= import("./system-events-BcDTm183.js"); return await systemEventsRuntimePromise; } async function loadDiscordThreadingRuntime() { discordThreadingRuntimePromise ??= import("./threading-vvIRMl3-.js"); return await discordThreadingRuntimePromise; } function isPreflightAborted(abortSignal) { return Boolean(abortSignal?.aborted); } //#endregion //#region extensions/discord/src/monitor/message-handler.preflight-pluralkit.ts async function resolveDiscordPreflightPluralKitInfo(params) { if (!params.config?.enabled) return null; try { const { fetchPluralKitMessageInfo } = await loadPluralKitRuntime(); const info = await fetchPluralKitMessageInfo({ messageId: params.message.id, config: params.config }); return isPreflightAborted(params.abortSignal) ? null : info; } catch (err) { logVerbose(`discord: pluralkit lookup failed for ${params.message.id}: ${String(err)}`); return null; } } //#endregion //#region extensions/discord/src/monitor/message-handler.preflight-thread.ts async function resolveDiscordPreflightThreadContext(params) { const { resolveDiscordThreadChannel, resolveDiscordThreadParentInfo } = await loadDiscordThreadingRuntime(); const earlyThreadChannel = resolveDiscordThreadChannel({ isGuildMessage: params.isGuildMessage, message: params.message, channelInfo: params.channelInfo, messageChannelId: params.messageChannelId }); if (!earlyThreadChannel) return { earlyThreadChannel: null }; const parentInfo = await resolveDiscordThreadParentInfo({ client: params.client, threadChannel: earlyThreadChannel, channelInfo: params.channelInfo }); if (isPreflightAborted(params.abortSignal)) return null; return { earlyThreadChannel, earlyThreadParentId: parentInfo.id, earlyThreadParentName: parentInfo.name, earlyThreadParentType: parentInfo.type }; } //#endregion //#region extensions/discord/src/monitor/message-handler.routing-preflight.ts let conversationRuntimePromise; async function loadConversationRuntime() { conversationRuntimePromise ??= import("./plugin-sdk/conversation-binding-runtime.js"); return await conversationRuntimePromise; } async function resolveDiscordPreflightRoute(params) { const conversationRuntime = await loadConversationRuntime(); const route = resolveDiscordConversationRoute({ cfg: params.preflight.cfg, accountId: params.preflight.accountId, guildId: params.preflight.data.guild_id ?? void 0, memberRoleIds: params.memberRoleIds, peer: buildDiscordRoutePeer({ isDirectMessage: params.isDirectMessage, isGroupDm: params.isGroupDm, directUserId: params.author.id, conversationId: params.messageChannelId }), parentConversationId: params.earlyThreadParentId }); const bindingConversationId = params.isDirectMessage ? resolveDiscordConversationIdentity({ isDirectMessage: true, userId: params.author.id }) ?? `user:${params.author.id}` : params.messageChannelId; let runtimeRoute = conversationRuntime.resolveRuntimeConversationBindingRoute({ route, conversation: { channel: "discord", accountId: params.preflight.accountId, conversationId: bindingConversationId, parentConversationId: params.earlyThreadParentId } }); if (shouldIgnoreStaleDiscordRouteBinding({ bindingRecord: runtimeRoute.bindingRecord, route })) { logVerbose(`discord: ignoring stale route binding for conversation ${bindingConversationId} (${runtimeRoute.bindingRecord?.targetSessionKey} -> ${route.sessionKey})`); runtimeRoute = { bindingRecord: null, route }; } let threadBinding = runtimeRoute.bindingRecord ?? void 0; const configuredRoute = threadBinding == null ? conversationRuntime.resolveConfiguredBindingRoute({ cfg: params.preflight.cfg, route, conversation: { channel: "discord", accountId: params.preflight.accountId, conversationId: params.messageChannelId, parentConversationId: params.earlyThreadParentId } }) : null; const configuredBinding = configuredRoute?.bindingResolution ?? null; if (!threadBinding && configuredBinding) threadBinding = configuredBinding.record; const boundSessionKey = conversationRuntime.isPluginOwnedSessionBindingRecord(threadBinding) ? "" : runtimeRoute.boundSessionKey ?? threadBinding?.targetSessionKey?.trim(); const effectiveRoute = runtimeRoute.boundSessionKey ? runtimeRoute.route : resolveDiscordEffectiveRoute({ route, boundSessionKey, configuredRoute, matchedBy: "binding.channel" }); return { conversationRuntime, threadBinding, configuredBinding, boundSessionKey, effectiveRoute, boundAgentId: boundSessionKey ? effectiveRoute.agentId : void 0, baseSessionKey: effectiveRoute.sessionKey }; } //#endregion //#region extensions/discord/src/monitor/message-handler.preflight.ts const DISCORD_HISTORY_MEDIA_MAX_ATTACHMENTS = 4; const DISCORD_HISTORY_MEDIA_MAX_BYTES = 10 * 1024 * 1024; const DISCORD_HISTORY_MEDIA_IDLE_TIMEOUT_MS = 1e3; const DISCORD_HISTORY_MEDIA_TOTAL_TIMEOUT_MS = 3e3; function resolveDiscordPreflightConversationKind(params) { const isGroupDm = params.channelType === ChannelType.GroupDM; return { isDirectMessage: params.channelType === ChannelType.DM || !params.isGuildMessage && !isGroupDm && params.channelType == null, isGroupDm }; } function isDiscordImageAttachmentCandidate(attachment) { if ((attachment.content_type?.split(";")[0]?.trim().toLowerCase())?.startsWith("image/")) return true; return Boolean(mimeTypeFromFilePath(attachment.filename)?.startsWith("image/") || mimeTypeFromFilePath(attachment.url)?.startsWith("image/")); } async function resolveDiscordHistoryMediaForPendingRecord(params) { const imageAttachments = (params.message.attachments ?? []).filter(isDiscordImageAttachmentCandidate).slice(0, DISCORD_HISTORY_MEDIA_MAX_ATTACHMENTS); const stickers = resolveDiscordMessageStickers(params.message).slice(0, Math.max(0, DISCORD_HISTORY_MEDIA_MAX_ATTACHMENTS - imageAttachments.length)); if (imageAttachments.length === 0 && stickers.length === 0) return []; const rawData = (() => { try { return params.message.rawData; } catch { return {}; } })(); const mediaMessage = Object.assign(Object.create(Object.getPrototypeOf(params.message)), params.message); Object.defineProperties(mediaMessage, { attachments: { value: imageAttachments }, rawData: { value: { ...rawData, attachments: imageAttachments, sticker_items: stickers, stickers } }, stickers: { value: stickers } }); return toInboundMediaFacts(await resolveMediaList(mediaMessage, Math.min(params.preflight.mediaMaxBytes, DISCORD_HISTORY_MEDIA_MAX_BYTES), { fetchImpl: params.preflight.discordRestFetch, ssrfPolicy: params.preflight.cfg.browser?.ssrfPolicy, readIdleTimeoutMs: DISCORD_HISTORY_MEDIA_IDLE_TIMEOUT_MS, totalTimeoutMs: DISCORD_HISTORY_MEDIA_TOTAL_TIMEOUT_MS, abortSignal: params.preflight.abortSignal }), { kind: "image", messageId: params.message.id }); } async function recordDiscordPendingHistoryEntry(params) { if (params.preflight.historyLimit <= 0) return; await recordDroppedChannelInboundHistory({ input: { id: params.message.id, timestamp: params.entry?.timestamp, rawText: params.entry?.body ?? "", textForAgent: params.entry?.body, raw: params.message }, admission: { kind: "drop", reason: "discord-preflight", recordHistory: true }, preflight: { message: params.entry ? { rawBody: params.entry.body, body: params.entry.body, bodyForAgent: params.entry.body, senderLabel: params.entry.sender, envelopeFrom: params.entry.sender } : void 0, history: { key: params.historyKey, historyMap: params.preflight.guildHistories, limit: params.preflight.historyLimit, recordOnDrop: true, mediaLimit: DISCORD_HISTORY_MEDIA_MAX_ATTACHMENTS, shouldRecord: () => !isPreflightAborted(params.preflight.abortSignal) }, media: () => resolveDiscordHistoryMediaForPendingRecord({ preflight: params.preflight, message: params.message }) } }); } async function preflightDiscordMessage(params) { if (isPreflightAborted(params.abortSignal)) return null; const logger = getChildLogger({ module: "discord-auto-reply" }); let message = params.data.message; const author = params.data.author; if (!author) return null; const messageChannelId = resolveDiscordMessageChannelId({ message, eventChannelId: params.data.channel_id }); if (!messageChannelId) { logVerbose(`discord: drop message ${message.id} (missing channel id)`); return null; } const allowBotsSetting = params.discordConfig?.allowBots; const allowBotsMode = allowBotsSetting === "mentions" ? "mentions" : allowBotsSetting === true ? "all" : "off"; if (params.botUserId && author.id === params.botUserId) return null; message = await hydrateDiscordMessageIfNeeded({ client: params.client, message, messageChannelId }); if (isPreflightAborted(params.abortSignal)) return null; const pluralkitConfig = params.discordConfig?.pluralkit; const webhookId = resolveDiscordWebhookId(message); const isGuildMessage = Boolean(params.data.guild_id); const channelInfo = await resolveDiscordChannelInfo(params.client, messageChannelId); if (isPreflightAborted(params.abortSignal)) return null; const { isDirectMessage, isGroupDm } = resolveDiscordPreflightConversationKind({ isGuildMessage, channelType: channelInfo?.type }); const messageText = resolveDiscordMessageText(message, { includeForwarded: true }); const injectedBoundThreadBinding = !isDirectMessage && !isGroupDm ? resolveInjectedBoundThreadLookupRecord({ threadBindings: params.threadBindings, threadId: messageChannelId }) : void 0; if (shouldIgnoreBoundThreadWebhookMessage({ accountId: params.accountId, threadId: messageChannelId, webhookId, threadBinding: injectedBoundThreadBinding })) { logVerbose(`discord: drop bound-thread webhook echo message ${message.id}`); return null; } if (isBoundThreadBotSystemMessage({ isBoundThreadSession: Boolean(injectedBoundThreadBinding) && isDiscordThreadChannelMessage({ isGuildMessage, message, channelInfo }), isBotAuthor: Boolean(author.bot), text: messageText })) { logVerbose(`discord: drop bound-thread bot system message ${message.id}`); return null; } const pluralkitInfo = await resolveDiscordPreflightPluralKitInfo({ message, config: pluralkitConfig, abortSignal: params.abortSignal }); if (isPreflightAborted(params.abortSignal)) return null; const sender = resolveDiscordSenderIdentity({ author, member: params.data.member, pluralkitInfo }); if (author.bot) { if (allowBotsMode === "off" && !sender.isPluralKit) { logVerbose("discord: drop bot message (allowBots=false)"); return null; } } const data = message === params.data.message ? params.data : { ...params.data, message }; logDebug(`[discord-preflight] channelId=${messageChannelId} guild_id=${params.data.guild_id} channelType=${channelInfo?.type} isGuild=${isGuildMessage} isDM=${isDirectMessage} isGroupDm=${isGroupDm}`); if (isGroupDm && !params.groupDmEnabled) { logVerbose("discord: drop group dm (group dms disabled)"); return null; } if (isDirectMessage && !params.dmEnabled) { logVerbose("discord: drop dm (dms disabled)"); return null; } const dmPolicy = params.dmPolicy; const resolvedAccountId = params.accountId ?? resolveDefaultDiscordAccountId(params.cfg); const allowNameMatching = isDangerousNameMatchingEnabled(params.discordConfig); let commandAuthorized = true; if (isDirectMessage) { const access = await resolveDiscordDmPreflightAccess({ preflight: params, author, sender, dmPolicy, resolvedAccountId, allowNameMatching }); if (isPreflightAborted(params.abortSignal)) return null; if (!access) return null; commandAuthorized = access.commandAuthorized; } const botId = params.botUserId; const baseText = resolveDiscordMessageText(message, { includeForwarded: false }); recordChannelActivity({ channel: "discord", accountId: params.accountId, direction: "inbound" }); const channelName = channelInfo?.name ?? (isGuildMessage || isGroupDm ? resolveDiscordChannelNameSafe("channel" in message ? message.channel : void 0) : void 0); const threadContext = await resolveDiscordPreflightThreadContext({ client: params.client, isGuildMessage, message, channelInfo, messageChannelId, abortSignal: params.abortSignal }); if (!threadContext) return null; const { earlyThreadChannel, earlyThreadParentId, earlyThreadParentName, earlyThreadParentType } = threadContext; const memberRoleIds = Array.isArray(params.data.rawMember?.roles) ? params.data.rawMember.roles : []; const { conversationRuntime, threadBinding, configuredBinding, boundSessionKey, effectiveRoute, boundAgentId, baseSessionKey } = await resolveDiscordPreflightRoute({ preflight: params, author, isDirectMessage, isGroupDm, messageChannelId, memberRoleIds, earlyThreadParentId }); if (shouldIgnoreBoundThreadWebhookMessage({ accountId: params.accountId, threadId: messageChannelId, webhookId, threadBinding })) { logVerbose(`discord: drop bound-thread webhook echo message ${message.id}`); return null; } const isBoundThreadSession = Boolean(threadBinding && earlyThreadChannel); const bypassMentionRequirement = isBoundThreadSession; if (isBoundThreadBotSystemMessage({ isBoundThreadSession, isBotAuthor: Boolean(author.bot), text: messageText })) { logVerbose(`discord: drop bound-thread bot system message ${message.id}`); return null; } const mentionRegexes = buildMentionRegexes(params.cfg, effectiveRoute.agentId, { provider: "discord", conversationId: messageChannelId, providerPolicy: params.discordConfig?.mentionPatterns }); const explicitlyMentioned = Boolean(botId && message.mentionedUsers?.some((user) => user.id === botId)); const hasAnyMention = !isDirectMessage && ((message.mentionedUsers?.length ?? 0) > 0 || (message.mentionedRoles?.length ?? 0) > 0 || message.mentionedEveryone && (!author.bot || sender.isPluralKit)); const hasUserOrRoleMention = !isDirectMessage && ((message.mentionedUsers?.length ?? 0) > 0 || (message.mentionedRoles?.length ?? 0) > 0); if (isGuildMessage && (message.type === MessageType.ChatInputCommand || message.type === MessageType.ContextMenuCommand)) { logVerbose("discord: drop channel command message"); return null; } const guildInfo = isGuildMessage ? resolveDiscordGuildEntry({ guild: params.data.guild ?? void 0, guildId: params.data.guild_id ?? void 0, guildEntries: params.guildEntries }) : null; logDebug(`[discord-preflight] guild_id=${params.data.guild_id} guild_obj=${Boolean(params.data.guild)} guild_obj_id=${params.data.guild?.id} guildInfo=${Boolean(guildInfo)} guildEntries=${params.guildEntries ? Object.keys(params.guildEntries).join(",") : "none"}`); if (isGuildMessage && params.guildEntries && Object.keys(params.guildEntries).length > 0 && !guildInfo) { logDebug(`[discord-preflight] guild blocked: guild_id=${params.data.guild_id} guildEntries keys=${Object.keys(params.guildEntries).join(",")}`); logVerbose(`Blocked discord guild ${params.data.guild_id ?? "unknown"} (not in discord.guilds)`); return null; } const threadChannel = earlyThreadChannel; const threadParentId = earlyThreadParentId; const threadParentName = earlyThreadParentName; const threadParentType = earlyThreadParentType; const { threadName, configChannelName, configChannelSlug, displayChannelName, displayChannelSlug, guildSlug, channelConfig } = resolveDiscordPreflightChannelContext({ isGuildMessage, messageChannelId, channelName, guildName: params.data.guild?.name, guildInfo, threadChannel, threadParentId, threadParentName }); const channelMatchMeta = formatAllowlistMatchMeta(channelConfig); logDiscordPreflightChannelConfig({ channelConfig, channelMatchMeta, channelId: messageChannelId }); const channelAccess = resolveDiscordPreflightChannelAccess({ isGuildMessage, isGroupDm, groupPolicy: params.groupPolicy, groupDmChannels: params.groupDmChannels, messageChannelId, displayChannelName, displayChannelSlug, guildInfo, channelConfig, channelMatchMeta }); if (!channelAccess.allowed) return null; const { channelAllowlistConfigured, channelAllowed } = channelAccess; const historyEntry = buildDiscordPreflightHistoryEntry({ isGuildMessage, historyLimit: params.historyLimit, message, senderLabel: sender.label }); const threadOwnerId = threadChannel ? resolveDiscordChannelInfoSafe(threadChannel).ownerId ?? channelInfo?.ownerId : void 0; const shouldRequireMentionByConfig = resolveDiscordShouldRequireMention({ isGuildMessage, isThread: Boolean(threadChannel), botId, threadOwnerId, channelConfig, guildInfo }); const shouldRequireMention = resolvePreflightMentionRequirement({ shouldRequireMention: shouldRequireMentionByConfig, bypassMentionRequirement }); const { hasAccessRestrictions, memberAllowed } = resolveDiscordMemberAccessState({ channelConfig, guildInfo, memberRoleIds, sender, allowNameMatching }); if (isGuildMessage && hasAccessRestrictions && !memberAllowed) { logDebug(`[discord-preflight] drop: member not allowed`); logVerbose("Blocked discord guild sender (not in users/roles allowlist)"); return null; } const { resolveDiscordPreflightAudioMentionContext } = await loadPreflightAudioRuntime(); const { hasTypedText, transcript: preflightTranscript } = await resolveDiscordPreflightAudioMentionContext({ message, isDirectMessage, shouldRequireMention, mentionRegexes, cfg: params.cfg, abortSignal: params.abortSignal }); if (isPreflightAborted(params.abortSignal)) return null; const mentionText = hasTypedText ? baseText : ""; const { implicitMentionKinds, wasMentioned } = resolveDiscordMentionState({ authorIsBot: Boolean(author.bot), botId, hasAnyMention, isDirectMessage, isExplicitlyMentioned: explicitlyMentioned, mentionRegexes, mentionText, mentionedEveryone: message.mentionedEveryone, referencedAuthorId: message.referencedMessage?.author?.id, senderIsPluralKit: sender.isPluralKit, transcript: preflightTranscript }); logDiscordPreflightInboundSummary({ messageId: message.id, guildId: params.data.guild_id ?? void 0, channelId: messageChannelId, wasMentioned, isDirectMessage, isGroupDm, hasContent: Boolean(messageText) }); const allowTextCommands = shouldHandleTextCommands({ cfg: params.cfg, surface: "discord" }); const hasControlCommandInMessage = hasControlCommand(baseText, params.cfg); const hasAbortRequest = isAbortRequestText(baseText); if (!isDirectMessage) { const commandAccess = await resolveDiscordTextCommandAccess({ accountId: params.accountId, cfg: params.cfg, ownerAllowFrom: params.allowFrom, sender: { id: sender.id, name: sender.name, tag: sender.tag }, memberAccessConfigured: hasAccessRestrictions, memberAllowed, allowNameMatching, allowTextCommands, hasControlCommand: hasControlCommandInMessage }); commandAuthorized = commandAccess.authorized; if (commandAccess.shouldBlockControlCommand) { logInboundDrop({ log: logVerbose, channel: "discord", reason: "control command (unauthorized)", target: sender.id }); return null; } } const canDetectMention = Boolean(botId) || mentionRegexes.length > 0; const mentionDecision = resolveInboundMentionDecision({ facts: { canDetectMention, wasMentioned, hasAnyMention, implicitMentionKinds }, policy: { isGroup: isGuildMessage, requireMention: shouldRequireMention, allowTextCommands, hasControlCommand: hasControlCommandInMessage, commandAuthorized } }); const effectiveWasMentioned = mentionDecision.effectiveWasMentioned; const inboundEventKind = classifyChannelInboundEvent({ conversation: { kind: isDirectMessage ? "direct" : isGroupDm ? "group" : "channel" }, unmentionedGroupPolicy: resolveUnmentionedGroupInboundPolicy({ cfg: params.cfg, agentId: effectiveRoute.agentId }), wasMentioned: effectiveWasMentioned, hasControlCommand: hasControlCommandInMessage, hasAbortRequest }); logDebug(`[discord-preflight] shouldRequireMention=${shouldRequireMention} baseRequireMention=${shouldRequireMentionByConfig} boundThreadSession=${isBoundThreadSession} mentionDecision.shouldSkip=${mentionDecision.shouldSkip} wasMentioned=${wasMentioned}`); if (isGuildMessage && shouldRequireMention) { if (mentionDecision.shouldSkip) { logDebug(`[discord-preflight] drop: no-mention`); logVerbose(`discord: drop guild message (mention required, botId=${botId ?? "<missing>"})`); logger.info({ channelId: messageChannelId, reason: "no-mention" }, "discord: skipping guild message"); await recordDiscordPendingHistoryEntry({ preflight: params, historyKey: messageChannelId, message, entry: historyEntry }); return null; } } if (author.bot && !sender.isPluralKit && allowBotsMode === "mentions") { if (!(isDirectMessage || wasMentioned || mentionDecision.implicitMention)) { logDebug(`[discord-preflight] drop: bot message missing mention (allowBots=mentions)`); logVerbose("discord: drop bot message (allowBots=mentions, missing mention)"); return null; } } const ignoreOtherMentions = channelConfig?.ignoreOtherMentions ?? guildInfo?.ignoreOtherMentions ?? false; if (isGuildMessage && ignoreOtherMentions && hasUserOrRoleMention && !wasMentioned && !mentionDecision.implicitMention) { logDebug(`[discord-preflight] drop: other-mention`); logVerbose(`discord: drop guild message (another user/role mentioned, ignoreOtherMentions=true, botId=${botId})`); await recordDiscordPendingHistoryEntry({ preflight: params, historyKey: messageChannelId, message, entry: historyEntry }); return null; } const systemLocation = resolveDiscordSystemLocation({ isDirectMessage, isGroupDm, guild: params.data.guild ?? void 0, channelName: channelName ?? messageChannelId }); const { resolveDiscordSystemEvent } = await loadSystemEventsRuntime(); const systemText = resolveDiscordSystemEvent(message, systemLocation); if (systemText) { logDebug(`[discord-preflight] drop: system event`); enqueueSystemEvent(systemText, { sessionKey: effectiveRoute.sessionKey, contextKey: `discord:system:${messageChannelId}:${message.id}` }); return null; } if (!messageText) { logDebug(`[discord-preflight] drop: empty content`); logVerbose(`discord: drop message ${message.id} (empty content)`); return null; } if (configuredBinding) { const ensured = await conversationRuntime.ensureConfiguredBindingRouteReady({ cfg: params.cfg, bindingResolution: configuredBinding }); if (!ensured.ok) { logVerbose(`discord: configured ACP binding unavailable for channel ${configuredBinding.record.conversation.conversationId}: ${ensured.error}`); return null; } } const botLoopProtection = author.bot && !sender.isPluralKit && allowBotsMode !== "off" && params.botUserId && author.id !== params.botUserId ? { scopeId: params.accountId, conversationId: messageChannelId, senderId: author.id, receiverId: params.botUserId, config: params.discordConfig?.botLoopProtection, defaultsConfig: params.cfg.channels?.defaults?.botLoopProtection, defaultEnabled: true, nowMs: resolveTimestampMs(message.timestamp) } : void 0; logDebug(`[discord-preflight] success: route=${effectiveRoute.agentId} sessionKey=${effectiveRoute.sessionKey}`); return buildDiscordMessagePreflightContext({ preflightParams: params, data, client: params.client, message, messageChannelId, author, sender, canonicalMessageId: pluralkitInfo?.original?.trim() || void 0, memberRoleIds, channelInfo, channelName, isGuildMessage, isDirectMessage, isGroupDm, commandAuthorized, baseText, messageText, ...preflightTranscript !== void 0 ? { preflightAudioTranscript: preflightTranscript } : {}, wasMentioned, route: effectiveRoute, threadBinding, boundSessionKey: boundSessionKey || void 0, boundAgentId, guildInfo, guildSlug, threadChannel, threadParentId, threadParentName, threadParentType, threadName, configChannelName, configChannelSlug, displayChannelName, displayChannelSlug, baseSessionKey, channelConfig, channelAllowlistConfigured, channelAllowed, shouldRequireMention, hasAnyMention, hasControlCommand: hasControlCommandInMessage, allowTextCommands, shouldBypassMention: mentionDecision.shouldBypassMention, effectiveWasMentioned, inboundEventKind, canDetectMention, historyEntry, botLoopProtection }); } //#endregion export { preflightDiscordMessage, resolvePreflightMentionRequirement, shouldIgnoreBoundThreadWebhookMessage };