import type { Client } from "@buape/carbon"; import { createChannelInboundDebouncer, shouldDebounceTextInbound, } from "../../channels/inbound-debounce-policy.js"; import { resolveOpenProviderRuntimeGroupPolicy } from "../../config/runtime-group-policy.js"; import { danger } from "../../globals.js"; import type { DiscordMessageEvent, DiscordMessageHandler } from "./listeners.js"; import { preflightDiscordMessage } from "./message-handler.preflight.js"; import type { DiscordMessagePreflightParams } from "./message-handler.preflight.types.js"; import { processDiscordMessage } from "./message-handler.process.js"; import { hasDiscordMessageStickers, resolveDiscordMessageChannelId, resolveDiscordMessageText, } from "./message-utils.js"; type DiscordMessageHandlerParams = Omit< DiscordMessagePreflightParams, "ackReactionScope" | "groupPolicy" | "data" | "client" >; export function createDiscordMessageHandler( params: DiscordMessageHandlerParams, ): DiscordMessageHandler { const { groupPolicy } = resolveOpenProviderRuntimeGroupPolicy({ providerConfigPresent: params.cfg.channels?.discord !== undefined, groupPolicy: params.discordConfig?.groupPolicy, defaultGroupPolicy: params.cfg.channels?.defaults?.groupPolicy, }); const ackReactionScope = params.discordConfig?.ackReactionScope ?? params.cfg.messages?.ackReactionScope ?? "group-mentions"; const { debouncer } = createChannelInboundDebouncer<{ data: DiscordMessageEvent; client: Client; }>({ cfg: params.cfg, channel: "discord", buildKey: (entry) => { const message = entry.data.message; const authorId = entry.data.author?.id; if (!message || !authorId) { return null; } const channelId = resolveDiscordMessageChannelId({ message, eventChannelId: entry.data.channel_id, }); if (!channelId) { return null; } return `discord:${params.accountId}:${channelId}:${authorId}`; }, shouldDebounce: (entry) => { const message = entry.data.message; if (!message) { return false; } const baseText = resolveDiscordMessageText(message, { includeForwarded: false }); return shouldDebounceTextInbound({ text: baseText, cfg: params.cfg, hasMedia: Boolean( (message.attachments && message.attachments.length > 0) || hasDiscordMessageStickers(message), ), }); }, onFlush: async (entries) => { const last = entries.at(-1); if (!last) { return; } if (entries.length === 1) { const ctx = await preflightDiscordMessage({ ...params, ackReactionScope, groupPolicy, data: last.data, client: last.client, }); if (!ctx) { return; } void processDiscordMessage(ctx).catch((err) => { params.runtime.error?.(danger(`discord process failed: ${String(err)}`)); }); return; } const combinedBaseText = entries .map((entry) => resolveDiscordMessageText(entry.data.message, { includeForwarded: false })) .filter(Boolean) .join("\n"); const syntheticMessage = { ...last.data.message, content: combinedBaseText, attachments: [], message_snapshots: (last.data.message as { message_snapshots?: unknown }).message_snapshots, messageSnapshots: (last.data.message as { messageSnapshots?: unknown }).messageSnapshots, rawData: { ...(last.data.message as { rawData?: Record }).rawData, }, }; const syntheticData: DiscordMessageEvent = { ...last.data, message: syntheticMessage, }; const ctx = await preflightDiscordMessage({ ...params, ackReactionScope, groupPolicy, data: syntheticData, client: last.client, }); if (!ctx) { return; } if (entries.length > 1) { const ids = entries.map((entry) => entry.data.message?.id).filter(Boolean) as string[]; if (ids.length > 0) { const ctxBatch = ctx as typeof ctx & { MessageSids?: string[]; MessageSidFirst?: string; MessageSidLast?: string; }; ctxBatch.MessageSids = ids; ctxBatch.MessageSidFirst = ids[0]; ctxBatch.MessageSidLast = ids[ids.length - 1]; } } void processDiscordMessage(ctx).catch((err) => { params.runtime.error?.(danger(`discord process failed: ${String(err)}`)); }); }, onError: (err) => { params.runtime.error?.(danger(`discord debounce flush failed: ${String(err)}`)); }, }); return async (data, client) => { try { await debouncer.enqueue({ data, client }); } catch (err) { params.runtime.error?.(danger(`handler failed: ${String(err)}`)); } }; }