import { ChannelType, Client, Command, MessageCreateListener, MessageReactionAddListener, MessageReactionRemoveListener, MessageType, } from "@buape/carbon";
import { GatewayIntents, GatewayPlugin } from "@buape/carbon/gateway";
import { ApplicationCommandOptionType, Routes } from "discord-api-types/v10";
import { chunkMarkdownText, resolveTextChunkLimit } from "../auto-reply/chunk.js";
import { hasControlCommand } from "../auto-reply/command-detection.js";
import { buildCommandText, listNativeCommandSpecs, shouldHandleTextCommands, } from "../auto-reply/commands-registry.js";
import { formatAgentEnvelope } from "../auto-reply/envelope.js";
import { dispatchReplyFromConfig } from "../auto-reply/reply/dispatch-from-config.js";
import { buildMentionRegexes, matchesMentionPatterns } from "../auto-reply/reply/mentions.js";
import { createReplyDispatcher, createReplyDispatcherWithTyping, } from "../auto-reply/reply/reply-dispatcher.js";
import { getReplyFromConfig } from "../auto-reply/reply.js";
import { loadConfig } from "../config/config.js";
import { resolveStorePath, updateLastRoute } from "../config/sessions.js";
import { danger, logVerbose, shouldLogVerbose } from "../globals.js";
import { enqueueSystemEvent } from "../infra/system-events.js";
import { getChildLogger } from "../logging.js";
import { detectMime } from "../media/mime.js";
import { saveMediaBuffer } from "../media/store.js";
import { readProviderAllowFromStore, upsertProviderPairingRequest, } from "../pairing/pairing-store.js";
import { resolveAgentRoute } from "../routing/resolve-route.js";
import { loadWebMedia } from "../web/media.js";
import { fetchDiscordApplicationId } from "./probe.js";
import { reactMessageDiscord, sendMessageDiscord } from "./send.js";
import { normalizeDiscordToken } from "./token.js";
export function resolveDiscordReplyTarget(opts) {
    if (opts.replyToMode === "off")
        return undefined;
    const replyToId = opts.replyToId?.trim();
    if (!replyToId)
        return undefined;
    if (opts.replyToMode === "all")
        return replyToId;
    return opts.hasReplied ? undefined : replyToId;
}
function summarizeAllowList(list) {
    if (!list || list.length === 0)
        return "any";
    const sample = list.slice(0, 4).map((entry) => String(entry));
    const suffix = list.length > sample.length ? ` (+${list.length - sample.length})` : "";
    return `${sample.join(", ")}${suffix}`;
}
function summarizeGuilds(entries) {
    if (!entries || Object.keys(entries).length === 0)
        return "any";
    const keys = Object.keys(entries);
    const sample = keys.slice(0, 4);
    const suffix = keys.length > sample.length ? ` (+${keys.length - sample.length})` : "";
    return `${sample.join(", ")}${suffix}`;
}
export async function monitorDiscordProvider(opts = {}) {
    const cfg = loadConfig();
    const token = normalizeDiscordToken(
    // @ts-ignore -- cherry-pick upstream type mismatch
    // @ts-ignore -- cherry-pick upstream type mismatch
    opts.token ?? process.env.DISCORD_BOT_TOKEN ?? cfg.discord?.token ?? undefined);
    if (!token) {
        throw new Error("DISCORD_BOT_TOKEN or discord.token is required for Discord gateway");
    }
    const runtime = opts.runtime ?? {
        log: console.log,
        error: console.error,
        exit: (code) => {
            throw new Error(`exit ${code}`);
        },
    };
    // @ts-ignore -- cherry-pick upstream type mismatch
    // @ts-ignore -- cherry-pick upstream type mismatch
    // @ts-ignore -- cherry-pick upstream type mismatch
    const dmConfig = cfg.discord?.dm;
    // @ts-ignore -- cherry-pick upstream type mismatch
    const guildEntries = cfg.discord?.guilds;
    // @ts-ignore -- cherry-pick upstream type mismatch
    const groupPolicy = cfg.discord?.groupPolicy ?? "open";
    // @ts-ignore -- cherry-pick upstream type mismatch
    const allowFrom = dmConfig?.allowFrom;
    // @ts-ignore -- cherry-pick upstream type mismatch
    const mediaMaxBytes = (opts.mediaMaxMb ?? cfg.discord?.mediaMaxMb ?? 8) * 1024 * 1024;
    const textLimit = resolveTextChunkLimit(cfg, "discord");
    // @ts-ignore -- cherry-pick upstream type mismatch
    const historyLimit = Math.max(0, opts.historyLimit ?? cfg.discord?.historyLimit ?? 20);
    // @ts-ignore -- cherry-pick upstream type mismatch
    const replyToMode = opts.replyToMode ?? cfg.discord?.replyToMode ?? "off";
    const dmEnabled = dmConfig?.enabled ?? true;
    const dmPolicy = dmConfig?.policy ?? "pairing";
    const groupDmEnabled = dmConfig?.groupEnabled ?? false;
    const groupDmChannels = dmConfig?.groupChannels;
    const nativeEnabled = cfg.commands?.native === true;
    const nativeDisabledExplicit = cfg.commands?.native === false;
    const useAccessGroups = cfg.commands?.useAccessGroups !== false;
    const sessionPrefix = "discord:slash";
    const ephemeralDefault = true;
    if (shouldLogVerbose()) {
        logVerbose(`discord: config dm=${dmEnabled ? "on" : "off"} dmPolicy=${dmPolicy} allowFrom=${summarizeAllowList(allowFrom)} groupDm=${groupDmEnabled ? "on" : "off"} groupDmChannels=${summarizeAllowList(groupDmChannels)} groupPolicy=${groupPolicy} guilds=${summarizeGuilds(guildEntries)} historyLimit=${historyLimit} mediaMaxMb=${Math.round(mediaMaxBytes / (1024 * 1024))} native=${nativeEnabled ? "on" : "off"} accessGroups=${useAccessGroups ? "on" : "off"}`);
    }
    const applicationId = await fetchDiscordApplicationId(token, 4000);
    if (!applicationId) {
        throw new Error("Failed to resolve Discord application id");
    }
    const commandSpecs = nativeEnabled ? listNativeCommandSpecs() : [];
    const commands = commandSpecs.map((spec) => createDiscordNativeCommand({
        command: spec,
        cfg,
        sessionPrefix,
        ephemeralDefault,
    }));
    const client = new Client({
        baseUrl: "http://localhost",
        deploySecret: "a",
        clientId: applicationId,
        publicKey: "a",
        token,
        autoDeploy: nativeEnabled,
    }, {
        commands,
        listeners: [],
    }, [
        new GatewayPlugin({
            intents: GatewayIntents.Guilds |
                GatewayIntents.GuildMessages |
                GatewayIntents.MessageContent |
                GatewayIntents.DirectMessages |
                GatewayIntents.GuildMessageReactions |
                GatewayIntents.DirectMessageReactions,
            autoInteractions: true,
        }),
    ]);
    const logger = getChildLogger({ module: "discord-auto-reply" });
    const guildHistories = new Map();
    let botUserId;
    if (nativeDisabledExplicit) {
        await clearDiscordNativeCommands({
            client,
            applicationId,
            runtime,
        });
    }
    try {
        const botUser = await client.fetchUser("@me");
        botUserId = botUser?.id;
    }
    catch (err) {
        runtime.error?.(danger(`discord: failed to fetch bot identity: ${String(err)}`));
    }
    const messageHandler = createDiscordMessageHandler({
        cfg,
        token,
        runtime,
        botUserId,
        guildHistories,
        historyLimit,
        mediaMaxBytes,
        textLimit,
        replyToMode,
        dmEnabled,
        groupDmEnabled,
        groupDmChannels,
        allowFrom,
        guildEntries,
    });
    client.listeners.push(new DiscordMessageListener(messageHandler));
    client.listeners.push(new DiscordReactionListener({
        runtime,
        botUserId,
        guildEntries,
        logger,
    }));
    client.listeners.push(new DiscordReactionRemoveListener({
        runtime,
        botUserId,
        guildEntries,
        logger,
    }));
    runtime.log?.(`logged in to discord${botUserId ? ` as ${botUserId}` : ""}`);
    await new Promise((resolve) => {
        const onAbort = async () => {
            try {
                const gateway = client.getPlugin("gateway");
                gateway?.disconnect();
            }
            finally {
                resolve();
            }
        };
        opts.abortSignal?.addEventListener("abort", () => {
            void onAbort();
        });
    });
}
async function clearDiscordNativeCommands(params) {
    try {
        await params.client.rest.put(Routes.applicationCommands(params.applicationId), {
            body: [],
        });
        logVerbose("discord: cleared native commands (commands.native=false)");
    }
    catch (err) {
        params.runtime.error?.(danger(`discord: failed to clear native commands: ${String(err)}`));
    }
}
export function createDiscordMessageHandler(params) {
    const { cfg, token, runtime, botUserId, guildHistories, historyLimit, mediaMaxBytes, textLimit, replyToMode, dmEnabled, groupDmEnabled, groupDmChannels, 
    // @ts-ignore -- cherry-pick upstream type mismatch
    allowFrom, guildEntries, } = params;
    const logger = getChildLogger({ module: "discord-auto-reply" });
    const mentionRegexes = buildMentionRegexes(cfg);
    const ackReaction = (cfg.messages?.ackReaction ?? "").trim();
    const ackReactionScope = cfg.messages?.ackReactionScope ?? "group-mentions";
    // @ts-ignore -- cherry-pick upstream type mismatch
    const groupPolicy = cfg.discord?.groupPolicy ?? "open";
    return async (data, client) => {
        try {
            const message = data.message;
            const author = data.author;
            if (!author || author.bot)
                return;
            const isGuildMessage = Boolean(data.guild_id);
            const channelInfo = await resolveDiscordChannelInfo(client, message.channelId);
            const isDirectMessage = channelInfo?.type === ChannelType.DM;
            const isGroupDm = channelInfo?.type === ChannelType.GroupDM;
            if (isGroupDm && !groupDmEnabled) {
                // @ts-ignore -- cherry-pick upstream type mismatch
                logVerbose("discord: drop group dm (group dms disabled)");
                return;
            }
            if (isDirectMessage && !dmEnabled) {
                logVerbose("discord: drop dm (dms disabled)");
                return;
            }
            // @ts-ignore -- cherry-pick upstream type mismatch
            const dmPolicy = cfg.discord?.dm?.policy ?? "pairing";
            let commandAuthorized = true;
            if (isDirectMessage) {
                if (dmPolicy === "disabled") {
                    logVerbose("discord: drop dm (dmPolicy: disabled)");
                    return;
                }
                if (dmPolicy !== "open") {
                    const storeAllowFrom = await readProviderAllowFromStore("discord").catch(() => []);
                    const effectiveAllowFrom = [...(allowFrom ?? []), ...storeAllowFrom];
                    const allowList = normalizeDiscordAllowList(effectiveAllowFrom, ["discord:", "user:"]);
                    const permitted = allowList
                        ? allowListMatches(allowList, {
                            // @ts-ignore -- cherry-pick upstream type mismatch
                            id: author.id,
                            name: author.username,
                            tag: formatDiscordUserTag(author),
                        })
                        : false;
                    if (!permitted) {
                        commandAuthorized = false;
                        if (dmPolicy === "pairing") {
                            const { code } = await upsertProviderPairingRequest({
                                // @ts-ignore -- cherry-pick upstream type mismatch
                                provider: "discord",
                                id: author.id,
                                meta: {
                                    tag: formatDiscordUserTag(author),
                                    name: author.username ?? undefined,
                                },
                            });
                            logVerbose(`discord pairing request sender=${author.id} tag=${formatDiscordUserTag(author)} code=${code}`);
                            try {
                                await sendMessageDiscord(`user:${author.id}`, [
                                    "Clawdbot: access not configured.",
                                    "",
                                    `Pairing code: ${code}`,
                                    "",
                                    "Ask the bot owner to approve with:",
                                    "clawdbot pairing approve --provider discord <code>",
                                ].join("\n"), { token, rest: client.rest });
                            }
                            catch (err) {
                                logVerbose(`discord pairing reply failed for ${author.id}: ${String(err)}`);
                            }
                        }
                        else {
                            logVerbose(`Blocked unauthorized discord sender ${author.id} (dmPolicy=${dmPolicy})`);
                        }
                        return;
                    }
                    commandAuthorized = true;
                }
            }
            const botId = botUserId;
            const baseText = resolveDiscordMessageText(message);
            const wasMentioned = !isDirectMessage &&
                (Boolean(botId && message.mentionedUsers?.some((user) => user.id === botId)) ||
                    matchesMentionPatterns(baseText, mentionRegexes));
            if (shouldLogVerbose()) {
                logVerbose(`discord: inbound id=${message.id} guild=${message.guild?.id ?? "dm"} channel=${message.channelId} mention=${wasMentioned ? "yes" : "no"} type=${isDirectMessage ? "dm" : isGroupDm ? "group-dm" : "guild"} content=${baseText ? "yes" : "no"}`);
            }
            if (isGuildMessage &&
                (message.type === MessageType.ChatInputCommand ||
                    message.type === MessageType.ContextMenuCommand)) {
                logVerbose("discord: drop channel command message");
                return;
            }
            const guildInfo = isGuildMessage
                ? resolveDiscordGuildEntry({
                    guild: data.guild ?? undefined,
                    guildEntries,
                })
                : null;
            if (isGuildMessage && guildEntries && Object.keys(guildEntries).length > 0 && !guildInfo) {
                logVerbose(`Blocked discord guild ${data.guild_id ?? "unknown"} (not in discord.guilds)`);
                // @ts-ignore -- cherry-pick upstream type mismatch
                return;
            }
            const channelName = channelInfo?.name;
            const channelSlug = channelName ? normalizeDiscordSlug(channelName) : "";
            const guildSlug = guildInfo?.slug || (data.guild?.name ? normalizeDiscordSlug(data.guild.name) : "");
            const route = resolveAgentRoute({
                cfg,
                // @ts-ignore -- cherry-pick upstream type mismatch
                provider: "discord",
                guildId: data.guild_id ?? undefined,
                peer: {
                    kind: isDirectMessage ? "dm" : isGroupDm ? "group" : "channel",
                    id: isDirectMessage ? author.id : message.channelId,
                },
            });
            const channelConfig = isGuildMessage
                ? resolveDiscordChannelConfig({
                    guildInfo,
                    channelId: message.channelId,
                    channelName,
                    channelSlug,
                })
                : null;
            const groupDmAllowed = isGroupDm &&
                resolveGroupDmAllow({
                    channels: groupDmChannels,
                    channelId: message.channelId,
                    channelName,
                    channelSlug,
                });
            if (isGroupDm && !groupDmAllowed)
                return;
            const channelAllowlistConfigured = Boolean(guildInfo?.channels) && Object.keys(guildInfo?.channels ?? {}).length > 0;
            const channelAllowed = channelConfig?.allowed !== false;
            if (isGuildMessage &&
                !isDiscordGroupAllowedByPolicy({
                    groupPolicy,
                    channelAllowlistConfigured,
                    channelAllowed,
                })) {
                if (groupPolicy === "disabled") {
                    logVerbose("discord: drop guild message (groupPolicy: disabled)");
                }
                else if (!channelAllowlistConfigured) {
                    logVerbose("discord: drop guild message (groupPolicy: allowlist, no channel allowlist)");
                }
                else {
                    logVerbose(`Blocked discord channel ${message.channelId} not in guild channel allowlist (groupPolicy: allowlist)`);
                }
                return;
            }
            if (isGuildMessage && channelConfig?.allowed === false) {
                logVerbose(`Blocked discord channel ${message.channelId} not in guild channel allowlist`);
                return;
            }
            const textForHistory = resolveDiscordMessageText(message);
            if (isGuildMessage && historyLimit > 0 && textForHistory) {
                const history = guildHistories.get(message.channelId) ?? [];
                history.push({
                    sender: data.member?.nickname ?? author.globalName ?? author.username ?? author.id,
                    body: textForHistory,
                    timestamp: resolveTimestampMs(message.timestamp),
                    messageId: message.id,
                });
                while (history.length > historyLimit)
                    history.shift();
                guildHistories.set(message.channelId, history);
            }
            const resolvedRequireMention = channelConfig?.requireMention ?? guildInfo?.requireMention ?? true;
            const hasAnyMention = Boolean(!isDirectMessage &&
                (message.mentionedEveryone ||
                    (message.mentionedUsers?.length ?? 0) > 0 ||
                    (message.mentionedRoles?.length ?? 0) > 0));
            if (!isDirectMessage) {
                commandAuthorized = resolveDiscordCommandAuthorized({
                    isDirectMessage,
                    allowFrom,
                    guildInfo,
                    author,
                });
            }
            const allowTextCommands = shouldHandleTextCommands({
                cfg,
                surface: "discord",
            });
            const shouldBypassMention = allowTextCommands &&
                isGuildMessage &&
                resolvedRequireMention &&
                !wasMentioned &&
                !hasAnyMention &&
                commandAuthorized &&
                hasControlCommand(baseText);
            const canDetectMention = Boolean(botId) || mentionRegexes.length > 0;
            if (isGuildMessage && resolvedRequireMention) {
                if (botId && !wasMentioned && !shouldBypassMention) {
                    logVerbose(`discord: drop guild message (mention required, botId=${botId})`);
                    logger.info({
                        channelId: message.channelId,
                        reason: "no-mention",
                    }, "discord: skipping guild message");
                    return;
                }
            }
            if (isGuildMessage) {
                const userAllow = guildInfo?.users;
                if (Array.isArray(userAllow) && userAllow.length > 0) {
                    const users = normalizeDiscordAllowList(userAllow, ["discord:", "user:"]);
                    const userOk = !users ||
                        allowListMatches(users, {
                            id: author.id,
                            name: author.username,
                            tag: formatDiscordUserTag(author),
                        });
                    if (!userOk) {
                        logVerbose(`Blocked discord guild sender ${author.id} (not in guild users allowlist)`);
                        return;
                    }
                }
            }
            const systemLocation = resolveDiscordSystemLocation({
                isDirectMessage,
                isGroupDm,
                guild: data.guild ?? undefined,
                channelName: channelName ?? message.channelId,
            });
            const systemText = resolveDiscordSystemEvent(message, systemLocation);
            if (systemText) {
                enqueueSystemEvent(systemText, {
                    sessionKey: route.sessionKey,
                    contextKey: `discord:system:${message.channelId}:${message.id}`,
                });
                return;
            }
            const media = await resolveMedia(message, mediaMaxBytes);
            const text = message.content?.trim() || media?.placeholder || message.embeds?.[0]?.description || "";
            if (!text) {
                logVerbose(`discord: drop message ${message.id} (empty content)`);
                return;
            }
            const shouldAckReaction = () => {
                if (!ackReaction)
                    return false;
                if (ackReactionScope === "all")
                    return true;
                if (ackReactionScope === "direct")
                    return isDirectMessage;
                const isGroupChat = isGuildMessage || isGroupDm;
                if (ackReactionScope === "group-all")
                    return isGroupChat;
                if (ackReactionScope === "group-mentions") {
                    if (!isGuildMessage)
                        return false;
                    if (!resolvedRequireMention)
                        return false;
                    if (!canDetectMention)
                        return false;
                    return wasMentioned || shouldBypassMention;
                }
                return false;
            };
            if (shouldAckReaction()) {
                reactMessageDiscord(message.channelId, message.id, ackReaction, {
                    rest: client.rest,
                }).catch((err) => {
                    logVerbose(`discord react failed for channel ${message.channelId}: ${String(err)}`);
                });
            }
            // @ts-ignore -- cherry-pick upstream type mismatch
            const fromLabel = isDirectMessage
                ? buildDirectLabel(author)
                : buildGuildLabel({
                    guild: data.guild ?? undefined,
                    channelName: channelName ?? message.channelId,
                    channelId: message.channelId,
                });
            const groupRoom = isGuildMessage && channelSlug ? `#${channelSlug}` : undefined;
            const groupSubject = isDirectMessage ? undefined : groupRoom;
            let combinedBody = formatAgentEnvelope({
                // @ts-ignore -- cherry-pick upstream type mismatch
                provider: "Discord",
                // @ts-ignore -- cherry-pick upstream type mismatch
                from: fromLabel,
                timestamp: resolveTimestampMs(message.timestamp),
                body: text,
            });
            let shouldClearHistory = false;
            if (!isDirectMessage) {
                const history = historyLimit > 0 ? (guildHistories.get(message.channelId) ?? []) : [];
                const historyWithoutCurrent = history.length > 0 ? history.slice(0, -1) : [];
                if (historyWithoutCurrent.length > 0) {
                    const historyText = historyWithoutCurrent
                        .map((entry) => formatAgentEnvelope({
                        // @ts-ignore -- cherry-pick upstream type mismatch
                        provider: "Discord",
                        from: fromLabel,
                        timestamp: entry.timestamp,
                        body: `${entry.sender}: ${entry.body} [id:${entry.messageId ?? "unknown"} channel:${message.channelId}]`,
                    }))
                        .join("\n");
                    combinedBody = `[Chat messages since your last reply - for context]\n${historyText}\n\n[Current message - respond to this]\n${combinedBody}`;
                }
                const name = formatDiscordUserTag(author);
                const id = author.id;
                combinedBody = `${combinedBody}\n[from: ${name} user id:${id}]`;
                shouldClearHistory = true;
            }
            const replyContext = resolveReplyContext(message);
            if (replyContext) {
                combinedBody = `[Replied message - for context]\n${replyContext}\n\n${combinedBody}`;
            }
            const ctxPayload = {
                Body: combinedBody,
                From: isDirectMessage ? `discord:${author.id}` : `group:${message.channelId}`,
                To: isDirectMessage ? `user:${author.id}` : `channel:${message.channelId}`,
                SessionKey: route.sessionKey,
                AccountId: route.accountId,
                ChatType: isDirectMessage ? "direct" : "group",
                SenderName: data.member?.nickname ?? author.globalName ?? author.username,
                SenderId: author.id,
                SenderUsername: author.username,
                SenderTag: formatDiscordUserTag(author),
                GroupSubject: groupSubject,
                GroupRoom: groupRoom,
                GroupSpace: isGuildMessage ? (guildInfo?.id ?? guildSlug) || undefined : undefined,
                Provider: "discord",
                Surface: "discord",
                WasMentioned: wasMentioned,
                MessageSid: message.id,
                Timestamp: resolveTimestampMs(message.timestamp),
                MediaPath: media?.path,
                MediaType: media?.contentType,
                MediaUrl: media?.path,
                CommandAuthorized: commandAuthorized,
                CommandSource: "text",
            };
            const replyTarget = ctxPayload.To ?? undefined;
            // @ts-ignore -- cherry-pick upstream type mismatch
            if (!replyTarget) {
                runtime.error?.(danger("discord: missing reply target"));
                return;
            }
            if (isDirectMessage) {
                const sessionCfg = cfg.session;
                const storePath = resolveStorePath(sessionCfg?.store, {
                    agentId: route.agentId,
                });
                await updateLastRoute({
                    storePath,
                    sessionKey: route.mainSessionKey,
                    // @ts-ignore -- cherry-pick upstream type mismatch
                    provider: "discord",
                    to: `user:${author.id}`,
                    accountId: route.accountId,
                });
            }
            if (shouldLogVerbose()) {
                const preview = combinedBody.slice(0, 200).replace(/\n/g, "\\n");
                logVerbose(`discord inbound: channel=${message.channelId} from=${ctxPayload.From} preview="${preview}"`);
            }
            let didSendReply = false;
            const { dispatcher, replyOptions, markDispatchIdle } = createReplyDispatcherWithTyping({
                responsePrefix: cfg.messages?.responsePrefix,
                deliver: async (payload) => {
                    await deliverDiscordReply({
                        replies: [payload],
                        target: replyTarget,
                        token,
                        rest: client.rest,
                        runtime,
                        replyToMode,
                        textLimit,
                    });
                    didSendReply = true;
                },
                onError: (err, info) => {
                    runtime.error?.(danger(`discord ${info.kind} reply failed: ${String(err)}`));
                },
                onReplyStart: () => sendTyping(message),
            });
            const { queuedFinal, counts } = await dispatchReplyFromConfig({
                ctx: ctxPayload,
                cfg,
                dispatcher,
                replyOptions,
            });
            markDispatchIdle();
            if (!queuedFinal) {
                if (isGuildMessage && shouldClearHistory && historyLimit > 0 && didSendReply) {
                    guildHistories.set(message.channelId, []);
                }
                return;
            }
            didSendReply = true;
            if (shouldLogVerbose()) {
                const finalCount = counts.final;
                logVerbose(`discord: delivered ${finalCount} reply${finalCount === 1 ? "" : "ies"} to ${replyTarget}`);
            }
            if (isGuildMessage && shouldClearHistory && historyLimit > 0 && didSendReply) {
                guildHistories.set(message.channelId, []);
            }
        }
        catch (err) {
            runtime.error?.(danger(`handler failed: ${String(err)}`));
        }
    };
}
class DiscordMessageListener extends MessageCreateListener {
    handler;
    constructor(handler) {
        super();
        this.handler = handler;
    }
    async handle(data, client) {
        await this.handler(data, client);
    }
}
class DiscordReactionListener extends MessageReactionAddListener {
    params;
    constructor(params) {
        super();
        this.params = params;
    }
    async handle(data, client) {
        await handleDiscordReactionEvent({
            data,
            client,
            action: "added",
            botUserId: this.params.botUserId,
            guildEntries: this.params.guildEntries,
            logger: this.params.logger,
        });
    }
}
class DiscordReactionRemoveListener extends MessageReactionRemoveListener {
    params;
    constructor(params) {
        super();
        this.params = params;
    }
    async handle(data, client) {
        await handleDiscordReactionEvent({
            data,
            client,
            action: "removed",
            botUserId: this.params.botUserId,
            guildEntries: this.params.guildEntries,
            logger: this.params.logger,
        });
    }
}
async function handleDiscordReactionEvent(params) {
    try {
        const { data, client, action, botUserId, guildEntries } = params;
        if (!("user" in data))
            return;
        const user = data.user;
        if (!user || user.bot)
            return;
        if (!data.guild_id)
            return;
        const guildInfo = resolveDiscordGuildEntry({
            guild: data.guild ?? undefined,
            guildEntries,
        });
        if (guildEntries && Object.keys(guildEntries).length > 0 && !guildInfo) {
            return;
        }
        const channel = await client.fetchChannel(data.channel_id);
        if (!channel)
            return;
        const channelName = "name" in channel ? (channel.name ?? undefined) : undefined;
        const channelSlug = channelName ? normalizeDiscordSlug(channelName) : "";
        const channelConfig = resolveDiscordChannelConfig({
            guildInfo,
            channelId: data.channel_id,
            channelName,
            channelSlug,
        });
        if (channelConfig?.allowed === false)
            return;
        if (botUserId && user.id === botUserId)
            return;
        const reactionMode = guildInfo?.reactionNotifications ?? "own";
        const message = await data.message.fetch().catch(() => null);
        const messageAuthorId = message?.author?.id ?? undefined;
        const shouldNotify = shouldEmitDiscordReactionNotification({
            mode: reactionMode,
            botId: botUserId,
            messageAuthorId,
            userId: user.id,
            userName: user.username,
            userTag: formatDiscordUserTag(user),
            allowlist: guildInfo?.users,
        });
        if (!shouldNotify)
            return;
        const emojiLabel = formatDiscordReactionEmoji(data.emoji);
        // @ts-ignore -- cherry-pick upstream type mismatch
        const actorLabel = formatDiscordUserTag(user);
        const guildSlug = guildInfo?.slug || (data.guild?.name ? normalizeDiscordSlug(data.guild.name) : data.guild_id);
        const channelLabel = channelSlug
            ? `#${channelSlug}`
            : channelName
                ? `#${normalizeDiscordSlug(channelName)}`
                : `#${data.channel_id}`;
        const authorLabel = message?.author ? formatDiscordUserTag(message.author) : undefined;
        const baseText = `Discord reaction ${action}: ${emojiLabel} by ${actorLabel} on ${guildSlug} ${channelLabel} msg ${data.message_id}`;
        const text = authorLabel ? `${baseText} from ${authorLabel}` : baseText;
        const cfg = loadConfig();
        const route = resolveAgentRoute({
            cfg,
            // @ts-ignore -- cherry-pick upstream type mismatch
            provider: "discord",
            guildId: data.guild_id ?? undefined,
            peer: { kind: "channel", id: data.channel_id },
        });
        enqueueSystemEvent(text, {
            sessionKey: route.sessionKey,
            contextKey: `discord:reaction:${action}:${data.message_id}:${user.id}:${emojiLabel}`,
        });
    }
    catch (err) {
        params.logger.error(danger(`discord reaction handler failed: ${String(err)}`));
    }
}
function createDiscordNativeCommand(params) {
    const { command, cfg, sessionPrefix, ephemeralDefault } = params;
    return new (class extends Command {
        name = command.name;
        description = command.description;
        defer = true;
        ephemeral = ephemeralDefault;
        options = command.acceptsArgs
            ? [
                {
                    name: "input",
                    description: "Command input",
                    type: ApplicationCommandOptionType.String,
                    required: false,
                },
            ]
            : undefined;
        async run(interaction) {
            // @ts-ignore -- cherry-pick upstream type mismatch
            const useAccessGroups = cfg.commands?.useAccessGroups !== false;
            const user = interaction.user;
            if (!user)
                return;
            const channel = interaction.channel;
            const channelType = channel?.type;
            const isDirectMessage = channelType === ChannelType.DM;
            const isGroupDm = channelType === ChannelType.GroupDM;
            const channelName = channel && "name" in channel ? channel.name : undefined;
            const channelSlug = channelName ? normalizeDiscordSlug(channelName) : "";
            const prompt = buildCommandText(this.name, command.acceptsArgs ? interaction.options.getString("input") : undefined);
            // @ts-ignore -- cherry-pick upstream type mismatch
            const guildInfo = resolveDiscordGuildEntry({
                guild: interaction.guild ?? undefined,
                // @ts-ignore -- cherry-pick upstream type mismatch
                guildEntries: cfg.discord?.guilds,
            });
            if (useAccessGroups && interaction.guild) {
                const channelConfig = resolveDiscordChannelConfig({
                    guildInfo,
                    channelId: channel?.id ?? "",
                    channelName,
                    channelSlug,
                    // @ts-ignore -- cherry-pick upstream type mismatch
                });
                // @ts-ignore -- cherry-pick upstream type mismatch
                const channelAllowlistConfigured = Boolean(guildInfo?.channels) && Object.keys(guildInfo?.channels ?? {}).length > 0;
                const channelAllowed = channelConfig?.allowed !== false;
                const allowByPolicy = isDiscordGroupAllowedByPolicy({
                    // @ts-ignore -- cherry-pick upstream type mismatch
                    groupPolicy: cfg.discord?.groupPolicy ?? "open",
                    channelAllowlistConfigured,
                    channelAllowed,
                });
                // @ts-ignore -- cherry-pick upstream type mismatch
                if (!allowByPolicy) {
                    await interaction.reply({
                        content: "This channel is not allowed.",
                    });
                    return;
                }
            }
            // @ts-ignore -- cherry-pick upstream type mismatch
            const dmEnabled = cfg.discord?.dm?.enabled ?? true;
            // @ts-ignore -- cherry-pick upstream type mismatch
            const dmPolicy = cfg.discord?.dm?.policy ?? "pairing";
            let commandAuthorized = true;
            if (isDirectMessage) {
                // @ts-ignore -- cherry-pick upstream type mismatch
                if (!dmEnabled || dmPolicy === "disabled") {
                    await interaction.reply({ content: "Discord DMs are disabled." });
                    return;
                }
                if (dmPolicy !== "open") {
                    const storeAllowFrom = await readProviderAllowFromStore("discord").catch(() => []);
                    // @ts-ignore -- cherry-pick upstream type mismatch
                    const effectiveAllowFrom = [...(cfg.discord?.dm?.allowFrom ?? []), ...storeAllowFrom];
                    const allowList = normalizeDiscordAllowList(effectiveAllowFrom, ["discord:", "user:"]);
                    const permitted = allowList
                        ? allowListMatches(allowList, {
                            id: user.id,
                            name: user.username,
                            tag: formatDiscordUserTag(user),
                        })
                        : false;
                    if (!permitted) {
                        commandAuthorized = false;
                        if (dmPolicy === "pairing") {
                            const { code } = await upsertProviderPairingRequest({
                                // @ts-ignore -- cherry-pick upstream type mismatch
                                provider: "discord",
                                id: user.id,
                                meta: {
                                    tag: formatDiscordUserTag(user),
                                    name: user.username ?? undefined,
                                },
                            });
                            await interaction.reply({
                                content: [
                                    "Clawdbot: access not configured.",
                                    "",
                                    `Pairing code: ${code}`,
                                    "",
                                    "Ask the bot owner to approve with:",
                                    "clawdbot pairing approve --provider discord <code>",
                                ].join("\n"),
                                ephemeral: true,
                            });
                        }
                        else {
                            await interaction.reply({
                                content: "You are not authorized to use this command.",
                                ephemeral: true,
                            });
                        }
                        // @ts-ignore -- cherry-pick upstream type mismatch
                        return;
                    }
                    commandAuthorized = true;
                }
            }
            if (guildInfo?.users && !isDirectMessage) {
                const allowList = normalizeDiscordAllowList(guildInfo.users, ["discord:", "user:"]);
                if (allowList &&
                    !allowListMatches(allowList, {
                        // @ts-ignore -- cherry-pick upstream type mismatch
                        id: user.id,
                        name: user.username,
                        tag: formatDiscordUserTag(user),
                    })) {
                    await interaction.reply({
                        content: "You are not authorized to use this command.",
                    });
                    return;
                }
            }
            // @ts-ignore -- cherry-pick upstream type mismatch
            if (isGroupDm && cfg.discord?.dm?.groupEnabled === false) {
                await interaction.reply({ content: "Discord group DMs are disabled." });
                return;
            }
            const isGuild = Boolean(interaction.guild);
            const channelId = channel?.id ?? "unknown";
            const interactionId = interaction.rawData.id;
            const route = resolveAgentRoute({
                cfg,
                // @ts-ignore -- cherry-pick upstream type mismatch
                provider: "discord",
                guildId: interaction.guild?.id ?? undefined,
                peer: {
                    kind: isDirectMessage ? "dm" : isGroupDm ? "group" : "channel",
                    id: isDirectMessage ? user.id : channelId,
                },
            });
            const ctxPayload = {
                Body: prompt,
                From: isDirectMessage ? `discord:${user.id}` : `group:${channelId}`,
                To: `slash:${user.id}`,
                SessionKey: `agent:${route.agentId}:${sessionPrefix}:${user.id}`,
                CommandTargetSessionKey: route.sessionKey,
                AccountId: route.accountId,
                ChatType: isDirectMessage ? "direct" : "group",
                GroupSubject: isGuild ? interaction.guild?.name : undefined,
                SenderName: user.globalName ?? user.username,
                SenderId: user.id,
                SenderUsername: user.username,
                SenderTag: formatDiscordUserTag(user),
                Provider: "discord",
                Surface: "discord",
                WasMentioned: true,
                MessageSid: interactionId,
                Timestamp: Date.now(),
                CommandAuthorized: commandAuthorized,
                CommandSource: "native",
            };
            let didReply = false;
            const dispatcher = createReplyDispatcher({
                responsePrefix: cfg.messages?.responsePrefix,
                deliver: async (payload, _info) => {
                    await deliverDiscordInteractionReply({
                        interaction,
                        payload,
                        textLimit: resolveTextChunkLimit(cfg, "discord"),
                        preferFollowUp: didReply,
                    });
                    didReply = true;
                },
                onError: (err) => {
                    console.error(err);
                },
            });
            const replyResult = await getReplyFromConfig(ctxPayload, undefined, cfg);
            const replies = replyResult ? (Array.isArray(replyResult) ? replyResult : [replyResult]) : [];
            for (const reply of replies) {
                dispatcher.sendFinalReply(reply);
            }
            await dispatcher.waitForIdle();
        }
    })();
}
async function deliverDiscordInteractionReply(params) {
    const { interaction, payload, textLimit, preferFollowUp } = params;
    const mediaList = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
    const text = payload.text ?? "";
    const sendMessage = async (content, files) => {
        const payload = files && files.length > 0
            ? {
                content,
                files: files.map((file) => {
                    if (file.data instanceof Blob) {
                        return { name: file.name, data: file.data };
                    }
                    const arrayBuffer = Uint8Array.from(file.data).buffer;
                    return { name: file.name, data: new Blob([arrayBuffer]) };
                }),
            }
            : { content };
        if (!preferFollowUp) {
            await interaction.reply(payload);
            return;
        }
        await interaction.followUp(payload);
    };
    if (mediaList.length > 0) {
        const media = await Promise.all(mediaList.map(async (url) => {
            const loaded = await loadWebMedia(url);
            return {
                name: loaded.fileName ?? "upload",
                data: loaded.buffer,
            };
        }));
        const caption = text.length > textLimit ? text.slice(0, textLimit) : text;
        await sendMessage(caption, media);
        if (text.length > textLimit) {
            const remaining = text.slice(textLimit).trim();
            if (remaining) {
                for (const chunk of chunkMarkdownText(remaining, textLimit)) {
                    await interaction.followUp({ content: chunk });
                }
            }
        }
        return;
    }
    if (!text.trim())
        return;
    for (const chunk of chunkMarkdownText(text, textLimit)) {
        await sendMessage(chunk);
    }
}
async function deliverDiscordReply(params) {
    const chunkLimit = Math.min(params.textLimit, 2000);
    for (const payload of params.replies) {
        const mediaList = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
        const text = payload.text ?? "";
        if (!text && mediaList.length === 0)
            continue;
        if (mediaList.length === 0) {
            for (const chunk of chunkMarkdownText(text, chunkLimit)) {
                const trimmed = chunk.trim();
                if (!trimmed)
                    continue;
                await sendMessageDiscord(params.target, trimmed, {
                    token: params.token,
                    rest: params.rest,
                });
            }
            continue;
        }
        const firstMedia = mediaList[0];
        if (!firstMedia)
            continue;
        await sendMessageDiscord(params.target, text, {
            token: params.token,
            rest: params.rest,
            mediaUrl: firstMedia,
        });
        for (const extra of mediaList.slice(1)) {
            await sendMessageDiscord(params.target, "", {
                token: params.token,
                rest: params.rest,
                mediaUrl: extra,
            });
        }
    }
}
async function resolveDiscordChannelInfo(client, channelId) {
    try {
        const channel = await client.fetchChannel(channelId);
        if (!channel)
            return null;
        const name = "name" in channel ? (channel.name ?? undefined) : undefined;
        return { type: channel.type, name };
    }
    catch (err) {
        logVerbose(`discord: failed to fetch channel ${channelId}: ${String(err)}`);
        return null;
    }
}
async function resolveMedia(message, maxBytes) {
    const attachment = message.attachments?.[0];
    if (!attachment)
        return null;
    const res = await fetch(attachment.url);
    if (!res.ok) {
        throw new Error(`Failed to download discord attachment: HTTP ${res.status}`);
    }
    const buffer = Buffer.from(await res.arrayBuffer());
    const mime = await detectMime({
        buffer,
        headerMime: attachment.content_type ?? res.headers.get("content-type"),
        filePath: attachment.filename ?? attachment.url,
    });
    const saved = await saveMediaBuffer(buffer, mime, "inbound", maxBytes);
    return {
        path: saved.path,
        contentType: saved.contentType,
        placeholder: inferPlaceholder(attachment),
    };
}
function inferPlaceholder(attachment) {
    const mime = attachment.content_type ?? "";
    if (mime.startsWith("image/"))
        return "<media:image>";
    if (mime.startsWith("video/"))
        return "<media:video>";
    // @ts-ignore -- cherry-pick upstream type mismatch
    if (mime.startsWith("audio/"))
        return "<media:audio>";
    return "<media:document>";
}
function resolveDiscordMessageText(message, fallbackText) {
    const attachment = message.attachments?.[0];
    return (message.content?.trim() ||
        (attachment ? inferPlaceholder(attachment) : "") ||
        message.embeds?.[0]?.description ||
        fallbackText?.trim() ||
        "");
}
function resolveReplyContext(message) {
    const referenced = message.referencedMessage;
    if (!referenced?.author)
        return null;
    const referencedText = resolveDiscordMessageText(referenced);
    if (!referencedText)
        return null;
    const fromLabel = referenced.author ? buildDirectLabel(referenced.author) : "Unknown";
    const body = `${referencedText}\n[discord message id: ${referenced.id} channel: ${referenced.channelId} from: ${formatDiscordUserTag(referenced.author)} user id:${referenced.author?.id ?? "unknown"}]`;
    return formatAgentEnvelope({
        // @ts-ignore -- cherry-pick upstream type mismatch
        provider: "Discord",
        from: fromLabel,
        timestamp: resolveTimestampMs(referenced.timestamp),
        body,
    });
}
function buildDirectLabel(author) {
    const username = formatDiscordUserTag(author);
    return `${username} user id:${author.id}`;
}
function buildGuildLabel(params) {
    const { guild, channelName, channelId } = params;
    return `${guild?.name ?? "Guild"} #${channelName} channel id:${channelId}`;
}
function resolveDiscordSystemEvent(message, location) {
    switch (message.type) {
        case MessageType.ChannelPinnedMessage:
            return buildDiscordSystemEvent(message, location, "pinned a message");
        case MessageType.RecipientAdd:
            return buildDiscordSystemEvent(message, location, "added a recipient");
        case MessageType.RecipientRemove:
            return buildDiscordSystemEvent(message, location, "removed a recipient");
        case MessageType.UserJoin:
            return buildDiscordSystemEvent(message, location, "user joined");
        case MessageType.GuildBoost:
            return buildDiscordSystemEvent(message, location, "boosted the server");
        case MessageType.GuildBoostTier1:
            return buildDiscordSystemEvent(message, location, "boosted the server (Tier 1 reached)");
        case MessageType.GuildBoostTier2:
            return buildDiscordSystemEvent(message, location, "boosted the server (Tier 2 reached)");
        case MessageType.GuildBoostTier3:
            return buildDiscordSystemEvent(message, location, "boosted the server (Tier 3 reached)");
        case MessageType.ThreadCreated:
            return buildDiscordSystemEvent(message, location, "created a thread");
        case MessageType.AutoModerationAction:
            return buildDiscordSystemEvent(message, location, "auto moderation action");
        case MessageType.GuildIncidentAlertModeEnabled:
            return buildDiscordSystemEvent(message, location, "raid protection enabled");
        case MessageType.GuildIncidentAlertModeDisabled:
            return buildDiscordSystemEvent(message, location, "raid protection disabled");
        case MessageType.GuildIncidentReportRaid:
            return buildDiscordSystemEvent(message, location, "raid reported");
        case MessageType.GuildIncidentReportFalseAlarm:
            return buildDiscordSystemEvent(message, location, "raid report marked false alarm");
        case MessageType.StageStart:
            return buildDiscordSystemEvent(message, location, "stage started");
        case MessageType.StageEnd:
            return buildDiscordSystemEvent(message, location, "stage ended");
        case MessageType.StageSpeaker:
            return buildDiscordSystemEvent(message, location, "stage speaker updated");
        case MessageType.StageTopic:
            return buildDiscordSystemEvent(message, location, "stage topic updated");
        case MessageType.PollResult:
            return buildDiscordSystemEvent(message, location, "poll results posted");
        case MessageType.PurchaseNotification:
            return buildDiscordSystemEvent(message, location, "purchase notification");
        default:
            return null;
    }
}
function buildDiscordSystemEvent(message, location, action) {
    const authorLabel = message.author ? formatDiscordUserTag(message.author) : "";
    const actor = authorLabel ? `${authorLabel} ` : "";
    return `Discord system: ${actor}${action} in ${location}`;
}
function resolveDiscordSystemLocation(params) {
    const { isDirectMessage, isGroupDm, guild, channelName } = params;
    if (isDirectMessage)
        return "DM";
    if (isGroupDm)
        return `Group DM #${channelName}`;
    return guild?.name ? `${guild.name} #${channelName}` : `#${channelName}`;
}
function formatDiscordReactionEmoji(emoji) {
    if (emoji.id && emoji.name) {
        return `${emoji.name}:${emoji.id}`;
    }
    return emoji.name ?? "emoji";
}
function formatDiscordUserTag(user) {
    const discriminator = (user.discriminator ?? "").trim();
    if (discriminator && discriminator !== "0") {
        return `${user.username}#${discriminator}`;
    }
    return user.username ?? user.id;
}
function resolveTimestampMs(timestamp) {
    if (!timestamp)
        return undefined;
    const parsed = Date.parse(timestamp);
    return Number.isNaN(parsed) ? undefined : parsed;
}
export function normalizeDiscordAllowList(raw, prefixes) {
    if (!raw || raw.length === 0)
        return null;
    const ids = new Set();
    const names = new Set();
    const allowAll = raw.some((entry) => String(entry).trim() === "*");
    for (const entry of raw) {
        const text = String(entry).trim();
        if (!text || text === "*")
            continue;
        const normalized = normalizeDiscordSlug(text);
        const maybeId = text.replace(/^<@!?/, "").replace(/>$/, "");
        if (/^\d+$/.test(maybeId)) {
            ids.add(maybeId);
            continue;
        }
        const prefix = prefixes.find((entry) => text.startsWith(entry));
        if (prefix) {
            const candidate = text.slice(prefix.length);
            if (candidate)
                ids.add(candidate);
            continue;
        }
        if (normalized) {
            names.add(normalized);
        }
    }
    return { allowAll, ids, names };
}
export function normalizeDiscordSlug(value) {
    return value
        .trim()
        .toLowerCase()
        .replace(/^#/, "")
        .replace(/[^a-z0-9]+/g, "-")
        .replace(/^-+|-+$/g, "");
}
export function allowListMatches(list, candidate) {
    if (list.allowAll)
        return true;
    if (candidate.id && list.ids.has(candidate.id))
        return true;
    const slug = candidate.name ? normalizeDiscordSlug(candidate.name) : "";
    if (slug && list.names.has(slug))
        return true;
    if (candidate.tag && list.names.has(normalizeDiscordSlug(candidate.tag)))
        return true;
    return false;
}
export function resolveDiscordCommandAuthorized(params) {
    if (!params.isDirectMessage)
        return true;
    const allowList = normalizeDiscordAllowList(params.allowFrom, ["discord:", "user:"]);
    if (!allowList)
        return true;
    return allowListMatches(allowList, {
        id: params.author.id,
        name: params.author.username,
        tag: formatDiscordUserTag(params.author),
    });
}
export function resolveDiscordGuildEntry(params) {
    const guild = params.guild;
    const entries = params.guildEntries;
    if (!guild || !entries)
        return null;
    const byId = entries[guild.id];
    if (byId)
        return { ...byId, id: guild.id };
    const slug = normalizeDiscordSlug(guild.name ?? "");
    const bySlug = entries[slug];
    if (bySlug)
        return { ...bySlug, id: guild.id, slug: slug || bySlug.slug };
    const wildcard = entries["*"];
    if (wildcard)
        return { ...wildcard, id: guild.id, slug: slug || wildcard.slug };
    return null;
}
export function resolveDiscordChannelConfig(params) {
    const { guildInfo, channelId, channelName, channelSlug } = params;
    const channels = guildInfo?.channels;
    if (!channels)
        return null;
    const byId = channels[channelId];
    if (byId)
        return {
            allowed: byId.allow !== false,
            requireMention: byId.requireMention,
        };
    if (channelSlug && channels[channelSlug]) {
        const entry = channels[channelSlug];
        return {
            allowed: entry.allow !== false,
            requireMention: entry.requireMention,
        };
    }
    if (channelName && channels[channelName]) {
        const entry = channels[channelName];
        return {
            allowed: entry.allow !== false,
            requireMention: entry.requireMention,
        };
    }
    return { allowed: false };
}
export function isDiscordGroupAllowedByPolicy(params) {
    const { groupPolicy, channelAllowlistConfigured, channelAllowed } = params;
    if (groupPolicy === "disabled")
        return false;
    if (groupPolicy === "open")
        return true;
    if (!channelAllowlistConfigured)
        return false;
    return channelAllowed;
}
export function resolveGroupDmAllow(params) {
    const { channels, channelId, channelName, channelSlug } = params;
    if (!channels || channels.length === 0)
        return true;
    const allowList = channels.map((entry) => normalizeDiscordSlug(String(entry)));
    const candidates = [
        normalizeDiscordSlug(channelId),
        channelSlug,
        channelName ? normalizeDiscordSlug(channelName) : "",
    ].filter(Boolean);
    return allowList.includes("*") || candidates.some((candidate) => allowList.includes(candidate));
}
export function shouldEmitDiscordReactionNotification(params) {
    const mode = params.mode ?? "own";
    if (mode === "off")
        return false;
    if (mode === "all")
        return true;
    if (mode === "own") {
        return Boolean(params.botId && params.messageAuthorId === params.botId);
    }
    if (mode === "allowlist") {
        const list = normalizeDiscordAllowList(params.allowlist, ["discord:", "user:"]);
        if (!list)
            return false;
        return allowListMatches(list, {
            id: params.userId,
            name: params.userName,
            tag: params.userTag,
        });
    }
    return false;
}
async function sendTyping(params) {
    try {
        const channel = await params.client.fetchChannel(params.channelId);
        if (!channel)
            return;
        if ("triggerTyping" in channel && typeof channel.triggerTyping === "function") {
            await channel.triggerTyping();
        }
    }
    catch (err) {
        logVerbose(`discord typing cue failed for channel ${params.channelId}: ${String(err)}`);
    }
}
