import type { AgentToolResult } from "@mariozechner/pi-agent-core"; import type { DiscordActionConfig } from "../../config/config.js"; import { createThreadDiscord, deleteMessageDiscord, editMessageDiscord, fetchChannelPermissionsDiscord, fetchMessageDiscord, fetchReactionsDiscord, listPinsDiscord, listThreadsDiscord, pinMessageDiscord, reactMessageDiscord, readMessagesDiscord, removeOwnReactionsDiscord, removeReactionDiscord, searchMessagesDiscord, sendMessageDiscord, sendPollDiscord, sendStickerDiscord, unpinMessageDiscord, } from "../../discord/send.js"; import { type ActionGate, jsonResult, readReactionParams, readStringArrayParam, readStringParam, } from "./common.js"; import { withNormalizedTimestamp } from "../date-time.js"; import { resolveDiscordChannelId } from "../../discord/targets.js"; function parseDiscordMessageLink(link: string) { const normalized = link.trim(); const match = normalized.match( /^(?:https?:\/\/)?(?:ptb\.|canary\.)?discord(?:app)?\.com\/channels\/(\d+)\/(\d+)\/(\d+)(?:\/?|\?.*)$/i, ); if (!match) { throw new Error( "Invalid Discord message link. Expected https://discord.com/channels///.", ); } return { guildId: match[1], channelId: match[2], messageId: match[3], }; } export async function handleDiscordMessagingAction( action: string, params: Record, isActionEnabled: ActionGate, ): Promise> { const resolveChannelId = () => resolveDiscordChannelId( readStringParam(params, "channelId", { required: true, }), ); const accountId = readStringParam(params, "accountId"); const normalizeMessage = (message: unknown) => { if (!message || typeof message !== "object") return message; return withNormalizedTimestamp( message as Record, (message as { timestamp?: unknown }).timestamp, ); }; switch (action) { case "react": { if (!isActionEnabled("reactions")) { throw new Error("Discord reactions are disabled."); } const channelId = resolveChannelId(); const messageId = readStringParam(params, "messageId", { required: true, }); const { emoji, remove, isEmpty } = readReactionParams(params, { removeErrorMessage: "Emoji is required to remove a Discord reaction.", }); if (remove) { if (accountId) { await removeReactionDiscord(channelId, messageId, emoji, { accountId }); } else { await removeReactionDiscord(channelId, messageId, emoji); } return jsonResult({ ok: true, removed: emoji }); } if (isEmpty) { const removed = accountId ? await removeOwnReactionsDiscord(channelId, messageId, { accountId }) : await removeOwnReactionsDiscord(channelId, messageId); return jsonResult({ ok: true, removed: removed.removed }); } if (accountId) { await reactMessageDiscord(channelId, messageId, emoji, { accountId }); } else { await reactMessageDiscord(channelId, messageId, emoji); } return jsonResult({ ok: true, added: emoji }); } case "reactions": { if (!isActionEnabled("reactions")) { throw new Error("Discord reactions are disabled."); } const channelId = resolveChannelId(); const messageId = readStringParam(params, "messageId", { required: true, }); const limitRaw = params.limit; const limit = typeof limitRaw === "number" && Number.isFinite(limitRaw) ? limitRaw : undefined; const reactions = await fetchReactionsDiscord(channelId, messageId, { ...(accountId ? { accountId } : {}), limit, }); return jsonResult({ ok: true, reactions }); } case "sticker": { if (!isActionEnabled("stickers")) { throw new Error("Discord stickers are disabled."); } const to = readStringParam(params, "to", { required: true }); const content = readStringParam(params, "content"); const stickerIds = readStringArrayParam(params, "stickerIds", { required: true, label: "stickerIds", }); await sendStickerDiscord(to, stickerIds, { ...(accountId ? { accountId } : {}), content, }); return jsonResult({ ok: true }); } case "poll": { if (!isActionEnabled("polls")) { throw new Error("Discord polls are disabled."); } const to = readStringParam(params, "to", { required: true }); const content = readStringParam(params, "content"); const question = readStringParam(params, "question", { required: true, }); const answers = readStringArrayParam(params, "answers", { required: true, label: "answers", }); const allowMultiselectRaw = params.allowMultiselect; const allowMultiselect = typeof allowMultiselectRaw === "boolean" ? allowMultiselectRaw : undefined; const durationRaw = params.durationHours; const durationHours = typeof durationRaw === "number" && Number.isFinite(durationRaw) ? durationRaw : undefined; const maxSelections = allowMultiselect ? Math.max(2, answers.length) : 1; await sendPollDiscord( to, { question, options: answers, maxSelections, durationHours }, { ...(accountId ? { accountId } : {}), content }, ); return jsonResult({ ok: true }); } case "permissions": { if (!isActionEnabled("permissions")) { throw new Error("Discord permissions are disabled."); } const channelId = resolveChannelId(); const permissions = accountId ? await fetchChannelPermissionsDiscord(channelId, { accountId }) : await fetchChannelPermissionsDiscord(channelId); return jsonResult({ ok: true, permissions }); } case "fetchMessage": { if (!isActionEnabled("messages")) { throw new Error("Discord message reads are disabled."); } const messageLink = readStringParam(params, "messageLink"); let guildId = readStringParam(params, "guildId"); let channelId = readStringParam(params, "channelId"); let messageId = readStringParam(params, "messageId"); if (messageLink) { const parsed = parseDiscordMessageLink(messageLink); guildId = parsed.guildId; channelId = parsed.channelId; messageId = parsed.messageId; } if (!guildId || !channelId || !messageId) { throw new Error( "Discord message fetch requires guildId, channelId, and messageId (or a valid messageLink).", ); } const message = accountId ? await fetchMessageDiscord(channelId, messageId, { accountId }) : await fetchMessageDiscord(channelId, messageId); return jsonResult({ ok: true, message: normalizeMessage(message), guildId, channelId, messageId, }); } case "readMessages": { if (!isActionEnabled("messages")) { throw new Error("Discord message reads are disabled."); } const channelId = resolveChannelId(); const query = { limit: typeof params.limit === "number" && Number.isFinite(params.limit) ? params.limit : undefined, before: readStringParam(params, "before"), after: readStringParam(params, "after"), around: readStringParam(params, "around"), }; const messages = accountId ? await readMessagesDiscord(channelId, query, { accountId }) : await readMessagesDiscord(channelId, query); return jsonResult({ ok: true, messages: messages.map((message) => normalizeMessage(message)), }); } case "sendMessage": { if (!isActionEnabled("messages")) { throw new Error("Discord message sends are disabled."); } const to = readStringParam(params, "to", { required: true }); const content = readStringParam(params, "content", { required: true, }); const mediaUrl = readStringParam(params, "mediaUrl"); const replyTo = readStringParam(params, "replyTo"); const embeds = Array.isArray(params.embeds) && params.embeds.length > 0 ? params.embeds : undefined; const result = await sendMessageDiscord(to, content, { ...(accountId ? { accountId } : {}), mediaUrl, replyTo, embeds, }); return jsonResult({ ok: true, result }); } case "editMessage": { if (!isActionEnabled("messages")) { throw new Error("Discord message edits are disabled."); } const channelId = resolveChannelId(); const messageId = readStringParam(params, "messageId", { required: true, }); const content = readStringParam(params, "content", { required: true, }); const message = accountId ? await editMessageDiscord(channelId, messageId, { content }, { accountId }) : await editMessageDiscord(channelId, messageId, { content }); return jsonResult({ ok: true, message }); } case "deleteMessage": { if (!isActionEnabled("messages")) { throw new Error("Discord message deletes are disabled."); } const channelId = resolveChannelId(); const messageId = readStringParam(params, "messageId", { required: true, }); if (accountId) { await deleteMessageDiscord(channelId, messageId, { accountId }); } else { await deleteMessageDiscord(channelId, messageId); } return jsonResult({ ok: true }); } case "threadCreate": { if (!isActionEnabled("threads")) { throw new Error("Discord threads are disabled."); } const channelId = resolveChannelId(); const name = readStringParam(params, "name", { required: true }); const messageId = readStringParam(params, "messageId"); const autoArchiveMinutesRaw = params.autoArchiveMinutes; const autoArchiveMinutes = typeof autoArchiveMinutesRaw === "number" && Number.isFinite(autoArchiveMinutesRaw) ? autoArchiveMinutesRaw : undefined; const thread = accountId ? await createThreadDiscord( channelId, { name, messageId, autoArchiveMinutes }, { accountId }, ) : await createThreadDiscord(channelId, { name, messageId, autoArchiveMinutes }); return jsonResult({ ok: true, thread }); } case "threadList": { if (!isActionEnabled("threads")) { throw new Error("Discord threads are disabled."); } const guildId = readStringParam(params, "guildId", { required: true, }); const channelId = readStringParam(params, "channelId"); const includeArchived = typeof params.includeArchived === "boolean" ? params.includeArchived : undefined; const before = readStringParam(params, "before"); const limit = typeof params.limit === "number" && Number.isFinite(params.limit) ? params.limit : undefined; const threads = accountId ? await listThreadsDiscord( { guildId, channelId, includeArchived, before, limit, }, { accountId }, ) : await listThreadsDiscord({ guildId, channelId, includeArchived, before, limit, }); return jsonResult({ ok: true, threads }); } case "threadReply": { if (!isActionEnabled("threads")) { throw new Error("Discord threads are disabled."); } const channelId = resolveChannelId(); const content = readStringParam(params, "content", { required: true, }); const mediaUrl = readStringParam(params, "mediaUrl"); const replyTo = readStringParam(params, "replyTo"); const result = await sendMessageDiscord(`channel:${channelId}`, content, { ...(accountId ? { accountId } : {}), mediaUrl, replyTo, }); return jsonResult({ ok: true, result }); } case "pinMessage": { if (!isActionEnabled("pins")) { throw new Error("Discord pins are disabled."); } const channelId = resolveChannelId(); const messageId = readStringParam(params, "messageId", { required: true, }); if (accountId) { await pinMessageDiscord(channelId, messageId, { accountId }); } else { await pinMessageDiscord(channelId, messageId); } return jsonResult({ ok: true }); } case "unpinMessage": { if (!isActionEnabled("pins")) { throw new Error("Discord pins are disabled."); } const channelId = resolveChannelId(); const messageId = readStringParam(params, "messageId", { required: true, }); if (accountId) { await unpinMessageDiscord(channelId, messageId, { accountId }); } else { await unpinMessageDiscord(channelId, messageId); } return jsonResult({ ok: true }); } case "listPins": { if (!isActionEnabled("pins")) { throw new Error("Discord pins are disabled."); } const channelId = resolveChannelId(); const pins = accountId ? await listPinsDiscord(channelId, { accountId }) : await listPinsDiscord(channelId); return jsonResult({ ok: true, pins: pins.map((pin) => normalizeMessage(pin)) }); } case "searchMessages": { if (!isActionEnabled("search")) { throw new Error("Discord search is disabled."); } const guildId = readStringParam(params, "guildId", { required: true, }); const content = readStringParam(params, "content", { required: true, }); const channelId = readStringParam(params, "channelId"); const channelIds = readStringArrayParam(params, "channelIds"); const authorId = readStringParam(params, "authorId"); const authorIds = readStringArrayParam(params, "authorIds"); const limit = typeof params.limit === "number" && Number.isFinite(params.limit) ? params.limit : undefined; const channelIdList = [...(channelIds ?? []), ...(channelId ? [channelId] : [])]; const authorIdList = [...(authorIds ?? []), ...(authorId ? [authorId] : [])]; const results = accountId ? await searchMessagesDiscord( { guildId, content, channelIds: channelIdList.length ? channelIdList : undefined, authorIds: authorIdList.length ? authorIdList : undefined, limit, }, { accountId }, ) : await searchMessagesDiscord({ guildId, content, channelIds: channelIdList.length ? channelIdList : undefined, authorIds: authorIdList.length ? authorIdList : undefined, limit, }); if (!results || typeof results !== "object") { return jsonResult({ ok: true, results }); } const resultsRecord = results as Record; const messages = resultsRecord.messages; const normalizedMessages = Array.isArray(messages) ? messages.map((group) => Array.isArray(group) ? group.map((msg) => normalizeMessage(msg)) : group, ) : messages; return jsonResult({ ok: true, results: { ...resultsRecord, messages: normalizedMessages, }, }); } default: throw new Error(`Unknown action: ${action}`); } }