import { DISCORD_BAN_SCHEMA, DISCORD_MEMBER_SCHEMA, DISCORD_PROVIDER_BAN_SCHEMA, DISCORD_PROVIDER_MEMBER_SCHEMA, toDiscordBan, toDiscordMember, } from "@automate.ax/integration-contracts/discord" import * as z from "zod" import { defineAction } from "../../../automation/actions" import { getDiscordApi } from "../lib/api" const GUILD_USER_SCHEMA = z.object({ guildId: z.string().regex(/^\d+$/), userId: z.string().regex(/^\d+$/), }) const MAXIMUM_TIMEOUT_MILLISECONDS = 28 * 24 * 60 * 60 * 1_000 interface MemberRoleRequestInput { guildId: string reason?: string roleId: string userId: string } /** Lists members in a Discord server. */ export const listDiscordMembers = defineAction("List Discord members") .describe("Lists a page of server members in user-ID order.") .account("discord") .input( z.object({ after: z.string().regex(/^\d+$/).optional(), guildId: z.string().regex(/^\d+$/), limit: z.number().int().min(1).max(1_000).prefault(100), }), ) .output(DISCORD_MEMBER_SCHEMA.array()) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => ( await getDiscordApi(account.secret).request( `guilds/${input.guildId}/members`, { query: { after: input.after, limit: input.limit }, responseSchema: DISCORD_PROVIDER_MEMBER_SCHEMA.array(), }, ) ).map(toDiscordMember), ) /** Retrieves one Discord server member. */ export const getDiscordMember = defineAction("Get Discord member") .describe("Retrieves one member by server and user ID.") .account("discord") .input(GUILD_USER_SCHEMA) .output(DISCORD_MEMBER_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => toDiscordMember( await getDiscordApi(account.secret).request( `guilds/${input.guildId}/members/${input.userId}`, { responseSchema: DISCORD_PROVIDER_MEMBER_SCHEMA }, ), ), ) /** Updates a Discord member's nickname, voice state, or timeout. */ export const updateDiscordMember = defineAction("Update Discord member") .describe( "Changes a member nickname, voice placement, or communication timeout.", ) .account("discord") .input( GUILD_USER_SCHEMA.extend({ channelId: z.string().regex(/^\d+$/).nullable().optional(), communicationDisabledUntil: z.iso .datetime({ offset: true }) .refine( (value) => Date.parse(value) <= Date.now() + MAXIMUM_TIMEOUT_MILLISECONDS, "Timeouts cannot end more than 28 days in the future.", ) .nullable() .optional(), deaf: z.boolean().optional(), mute: z.boolean().optional(), nickname: z.string().max(32).nullable().optional(), reason: z.string().max(512).optional(), }), ) .output(DISCORD_MEMBER_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => toDiscordMember( await getDiscordApi(account.secret).request( `guilds/${input.guildId}/members/${input.userId}`, { body: { channel_id: input.channelId, communication_disabled_until: input.communicationDisabledUntil, deaf: input.deaf, mute: input.mute, nick: input.nickname, }, method: "PATCH", reason: input.reason, responseSchema: DISCORD_PROVIDER_MEMBER_SCHEMA, }, ), ), ) /** Removes a member from a Discord server. */ export const kickDiscordMember = defineAction("Kick Discord member") .describe("Removes a member from a server without banning them.") .account("discord") .input(GUILD_USER_SCHEMA.extend({ reason: z.string().max(512).optional() })) .output(z.void()) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => getDiscordApi(account.secret).request( `guilds/${input.guildId}/members/${input.userId}`, { method: "DELETE", reason: input.reason, responseSchema: z.void(), }, ), ) /** Adds a role to a Discord server member. */ export const addDiscordMemberRole = defineAction("Add Discord member role") .describe("Assigns one server role to a member.") .account("discord") .input(memberRoleInputSchema()) .output(z.void()) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => memberRoleRequest(account.secret, input, "PUT"), ) /** Removes a role from a Discord server member. */ export const removeDiscordMemberRole = defineAction( "Remove Discord member role", ) .describe("Removes one server role from a member.") .account("discord") .input(memberRoleInputSchema()) .output(z.void()) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => memberRoleRequest(account.secret, input, "DELETE"), ) /** Lists bans in a Discord server. */ export const listDiscordBans = defineAction("List Discord bans") .describe("Lists a page of banned users and ban reasons.") .account("discord") .input( z .object({ after: z.string().regex(/^\d+$/).optional(), before: z.string().regex(/^\d+$/).optional(), guildId: z.string().regex(/^\d+$/), limit: z.number().int().min(1).max(1_000).prefault(100), }) .refine(({ after, before }) => !(after && before), { message: "Use either after or before, not both.", }), ) .output(DISCORD_BAN_SCHEMA.array()) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => ( await getDiscordApi(account.secret).request( `guilds/${input.guildId}/bans`, { query: { after: input.after, before: input.before, limit: input.limit, }, responseSchema: DISCORD_PROVIDER_BAN_SCHEMA.array(), }, ) ).map(toDiscordBan), ) /** Bans a user from a Discord server. */ export const banDiscordMember = defineAction("Ban Discord member") .describe("Bans a user and optionally deletes their recent messages.") .account("discord") .input( GUILD_USER_SCHEMA.extend({ deleteMessageSeconds: z.number().int().min(0).max(604_800).prefault(0), reason: z.string().max(512).optional(), }), ) .output(z.void()) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => getDiscordApi(account.secret).request( `guilds/${input.guildId}/bans/${input.userId}`, { body: { delete_message_seconds: input.deleteMessageSeconds }, method: "PUT", reason: input.reason, responseSchema: z.void(), }, ), ) /** Removes a Discord server ban. */ export const unbanDiscordMember = defineAction("Unban Discord member") .describe("Removes a user's ban from a server.") .account("discord") .input(GUILD_USER_SCHEMA.extend({ reason: z.string().max(512).optional() })) .output(z.void()) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => getDiscordApi(account.secret).request( `guilds/${input.guildId}/bans/${input.userId}`, { method: "DELETE", reason: input.reason, responseSchema: z.void(), }, ), ) /** Builds the shared member-role action input schema. */ function memberRoleInputSchema() { return GUILD_USER_SCHEMA.extend({ reason: z.string().max(512).optional(), roleId: z.string().regex(/^\d+$/), }) } /** * Adds or removes one role from a server member. * * @param secret - Connected Discord account secret. * @param input - Server, user, role, and audit-log fields. * @param method - Discord membership method. */ async function memberRoleRequest( secret: Record, input: MemberRoleRequestInput, method: "DELETE" | "PUT", ) { return await getDiscordApi(secret).request( `guilds/${input.guildId}/members/${input.userId}/roles/${input.roleId}`, { method, reason: input.reason, responseSchema: z.void() }, ) }