import { DISCORD_ACTIVE_THREADS_SCHEMA, DISCORD_CHANNEL_SCHEMA, DISCORD_PROVIDER_ACTIVE_THREADS_SCHEMA, DISCORD_PROVIDER_CHANNEL_SCHEMA, DISCORD_PROVIDER_THREAD_MEMBER_SCHEMA, DISCORD_THREAD_MEMBER_SCHEMA, toDiscordActiveThreads, toDiscordChannel, toDiscordThreadMember, } from "@automate.ax/integration-contracts/discord" import * as z from "zod" import { defineAction } from "../../../automation/actions" import { getDiscordApi } from "../lib/api" const THREAD_INPUT_SCHEMA = z.object({ autoArchiveDuration: z.enum(["60", "1440", "4320", "10080"]).prefault("1440"), channelId: z.string().regex(/^\d+$/), name: z.string().trim().min(1).max(100), rateLimitPerUser: z.number().int().min(0).max(21_600).optional(), }) interface ThreadUserInput { threadId: string userId: string } /** Creates a Discord thread from an existing message. */ export const createDiscordThreadFromMessage = defineAction( "Create Discord thread from message", ) .describe("Starts a public thread attached to an existing channel message.") .account("discord") .input( THREAD_INPUT_SCHEMA.extend({ messageId: z.string().regex(/^\d+$/), reason: z.string().max(512).optional(), }), ) .output(DISCORD_CHANNEL_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => toDiscordChannel( await getDiscordApi(account.secret).request( `channels/${input.channelId}/messages/${input.messageId}/threads`, { body: { auto_archive_duration: Number(input.autoArchiveDuration), name: input.name, rate_limit_per_user: input.rateLimitPerUser, }, method: "POST", reason: input.reason, responseSchema: DISCORD_PROVIDER_CHANNEL_SCHEMA, }, ), ), ) /** Creates a standalone Discord thread. */ export const createDiscordThread = defineAction("Create Discord thread") .describe( "Creates a public, private, or announcement thread without a starter message.", ) .account("discord") .input( THREAD_INPUT_SCHEMA.extend({ invitable: z.boolean().optional(), reason: z.string().max(512).optional(), type: z.enum(["announcement", "public", "private"]).prefault("public"), }).superRefine((input, context) => { if (input.type !== "private" && input.invitable !== undefined) { context.addIssue({ code: "custom", message: "invitable is only available for private threads.", path: ["invitable"], }) } }), ) .output(DISCORD_CHANNEL_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => toDiscordChannel( await getDiscordApi(account.secret).request( `channels/${input.channelId}/threads`, { body: { auto_archive_duration: Number(input.autoArchiveDuration), invitable: input.type === "private" ? input.invitable : undefined, name: input.name, rate_limit_per_user: input.rateLimitPerUser, type: { announcement: 10, private: 12, public: 11 }[input.type], }, method: "POST", reason: input.reason, responseSchema: DISCORD_PROVIDER_CHANNEL_SCHEMA, }, ), ), ) /** Joins the connected bot to a Discord thread. */ export const joinDiscordThread = defineAction("Join Discord thread") .describe("Adds the connected bot to a thread.") .account("discord") .input(z.object({ threadId: z.string().regex(/^\d+$/) })) .output(z.void()) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => threadMembershipRequest(account.secret, input.threadId, "PUT"), ) /** Leaves the connected bot from a Discord thread. */ export const leaveDiscordThread = defineAction("Leave Discord thread") .describe("Removes the connected bot from a thread.") .account("discord") .input(z.object({ threadId: z.string().regex(/^\d+$/) })) .output(z.void()) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => threadMembershipRequest(account.secret, input.threadId, "DELETE"), ) /** Adds a user to a Discord thread. */ export const addDiscordThreadMember = defineAction("Add Discord thread member") .describe("Adds one user to a thread.") .account("discord") .input(threadUserInputSchema()) .output(z.void()) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => threadUserMembershipRequest(account.secret, input, "PUT"), ) /** Removes a user from a Discord thread. */ export const removeDiscordThreadMember = defineAction( "Remove Discord thread member", ) .describe("Removes one user from a thread.") .account("discord") .input(threadUserInputSchema()) .output(z.void()) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => threadUserMembershipRequest(account.secret, input, "DELETE"), ) /** Lists members of a Discord thread. */ export const listDiscordThreadMembers = defineAction( "List Discord thread members", ) .describe("Lists a page of thread membership records.") .account("discord") .input( z.object({ after: z.string().regex(/^\d+$/).optional(), limit: z.number().int().min(1).max(100).prefault(100), threadId: z.string().regex(/^\d+$/), }), ) .output(DISCORD_THREAD_MEMBER_SCHEMA.array()) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => ( await getDiscordApi(account.secret).request( `channels/${input.threadId}/thread-members`, { query: { after: input.after, limit: input.limit, with_member: true }, responseSchema: DISCORD_PROVIDER_THREAD_MEMBER_SCHEMA.array(), }, ) ).map(toDiscordThreadMember), ) /** Lists active threads in a Discord server. */ export const listDiscordActiveThreads = defineAction( "List Discord active threads", ) .describe("Lists active public and private threads visible to the bot.") .account("discord") .input(z.object({ guildId: z.string().regex(/^\d+$/) })) .output(DISCORD_ACTIVE_THREADS_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => toDiscordActiveThreads( await getDiscordApi(account.secret).request( `guilds/${input.guildId}/threads/active`, { responseSchema: DISCORD_PROVIDER_ACTIVE_THREADS_SCHEMA }, ), ), ) /** * Joins or leaves the connected bot from a thread. * * @param secret - Connected Discord account secret. * @param threadId - Discord thread ID. * @param method - Discord membership method. */ async function threadMembershipRequest( secret: Record, threadId: string, method: "DELETE" | "PUT", ) { return await getDiscordApi(secret).request( `channels/${threadId}/thread-members/@me`, { method, responseSchema: z.void() }, ) } /** Builds the shared thread-user action input schema. */ function threadUserInputSchema() { return z.object({ threadId: z.string().regex(/^\d+$/), userId: z.string().regex(/^\d+$/), }) } /** * Adds or removes one user from a thread. * * @param secret - Connected Discord account secret. * @param input - Thread and user IDs. * @param method - Discord membership method. */ async function threadUserMembershipRequest( secret: Record, input: ThreadUserInput, method: "DELETE" | "PUT", ) { return await getDiscordApi(secret).request( `channels/${input.threadId}/thread-members/${input.userId}`, { method, responseSchema: z.void() }, ) }