import * as z from "zod" import { defineAction } from "../../../automation/actions" import { getSlackApi } from "../lib/api" import { SLACK_REACTIONS_READ_SCOPE, SLACK_REACTIONS_WRITE_SCOPE, } from "../lib/scopes" import { SLACK_FILE_SCHEMA, SLACK_MESSAGE_SCHEMA, SLACK_PROVIDER_FILE_SCHEMA, SLACK_PROVIDER_MESSAGE_SCHEMA, SLACK_REACTION_SCHEMA, SLACK_RESPONSE_METADATA_SCHEMA, toSlackFile, toSlackMessage, toSlackResponseMetadata, } from "@automate.ax/integration-contracts/slack" const REACTION_RESPONSE_SCHEMA = z.looseObject({ ok: z.literal(true), }) const GET_REACTIONS_RESPONSE_SCHEMA = z.looseObject({ message: SLACK_PROVIDER_MESSAGE_SCHEMA, ok: z.literal(true), }) const PROVIDER_REACTION_LIST_ITEM_SCHEMA = z.discriminatedUnion("type", [ z.looseObject({ channel: z.string(), message: SLACK_PROVIDER_MESSAGE_SCHEMA, type: z.literal("message"), }), z.looseObject({ file: SLACK_PROVIDER_FILE_SCHEMA, type: z.literal("file"), }), z.looseObject({ comment: z.looseObject({ comment: z.string().optional(), id: z.string(), timestamp: z.number().int().nonnegative().optional(), user: z.string().optional(), }), file: SLACK_PROVIDER_FILE_SCHEMA, type: z.literal("file_comment"), }), ]) const LIST_REACTIONS_RESPONSE_SCHEMA = z.looseObject({ items: PROVIDER_REACTION_LIST_ITEM_SCHEMA.array(), ok: z.literal(true), response_metadata: z .looseObject({ next_cursor: z.string().optional() }) .optional(), }) const REACTION_LIST_ITEM_SCHEMA = z.discriminatedUnion("type", [ z.object({ conversationId: z.string(), message: SLACK_MESSAGE_SCHEMA, type: z.literal("message"), }), z.object({ file: SLACK_FILE_SCHEMA, type: z.literal("file") }), z.object({ comment: z.object({ id: z.string(), text: z.string().optional(), timestamp: z.number().int().nonnegative().optional(), userId: z.string().optional(), }), file: SLACK_FILE_SCHEMA, type: z.literal("fileComment"), }), ]) const REACTION_INPUT_SCHEMA = z.object({ /** Conversation containing the target message. */ conversationId: z.string().min(1), /** Emoji name without surrounding colons. */ emoji: z.string().min(1), /** Timestamp of the target message. */ timestamp: z.string().min(1), }) /** Adds an emoji reaction to a Slack message. */ export const addSlackReaction = defineAction("Add Slack reaction") .describe("Adds an emoji reaction to a Slack message.") .account("slack", SLACK_REACTIONS_WRITE_SCOPE) .input(REACTION_INPUT_SCHEMA) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await react(account.secret, "reactions.add", input) }) /** Removes the installed bot's emoji reaction from a Slack message. */ export const removeSlackReaction = defineAction("Remove Slack reaction") .describe("Removes the installed bot's reaction from a Slack message.") .account("slack", SLACK_REACTIONS_WRITE_SCOPE) .input(REACTION_INPUT_SCHEMA) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await react(account.secret, "reactions.remove", input) }) /** Gets all emoji reactions attached to one Slack message. */ export const getSlackMessageReactions = defineAction( "Get Slack message reactions", ) .describe("Gets emoji reactions attached to one Slack message.") .account("slack", SLACK_REACTIONS_READ_SCOPE) .input( z.object({ conversationId: z.string().min(1), includeAllUsers: z.boolean().optional(), timestamp: z.string().min(1), }), ) .output( z.object({ message: SLACK_MESSAGE_SCHEMA, reactions: SLACK_REACTION_SCHEMA.array(), }), ) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const message = toSlackMessage( ( await getSlackApi(account.secret).call("reactions.get", { query: { channel: input.conversationId, full: input.includeAllUsers, timestamp: input.timestamp, }, responseSchema: GET_REACTIONS_RESPONSE_SCHEMA, }) ).message, ) return { message, reactions: message.reactions ?? [] } }) /** Lists Slack items reacted to by one member. */ export const listSlackUserReactions = defineAction("List Slack user reactions") .describe("Lists messages and files reacted to by a Slack member.") .account("slack", SLACK_REACTIONS_READ_SCOPE) .input( z.object({ cursor: z.string().min(1).optional(), includeAllUsers: z.boolean().optional(), limit: z.number().int().min(1).optional(), userId: z.string().min(1).optional(), }), ) .output( z.object({ items: REACTION_LIST_ITEM_SCHEMA.array(), responseMetadata: SLACK_RESPONSE_METADATA_SCHEMA, }), ) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const result = await getSlackApi(account.secret).call("reactions.list", { query: { cursor: input.cursor, full: input.includeAllUsers, limit: input.limit, user: input.userId, }, responseSchema: LIST_REACTIONS_RESPONSE_SCHEMA, }) return { items: result.items.map((item) => { if (item.type === "message") { return { conversationId: item.channel, message: toSlackMessage(item.message), type: item.type, } as const } if (item.type === "file") { return { file: toSlackFile(item.file), type: item.type } as const } return { comment: { id: item.comment.id, text: item.comment.comment, timestamp: item.comment.timestamp, userId: item.comment.user, }, file: toSlackFile(item.file), type: "fileComment" as const, } }), responseMetadata: toSlackResponseMetadata(result.response_metadata), } }) /** * Applies one reaction mutation to a Slack message. * * @param secret - Resolved Slack integration secret. * @param method - Reaction method to call. * @param input - Target message and emoji. */ async function react( secret: Record, method: "reactions.add" | "reactions.remove", input: z.output, ) { await getSlackApi(secret).call(method, { body: { channel: input.conversationId, name: input.emoji, timestamp: input.timestamp, }, responseSchema: REACTION_RESPONSE_SCHEMA, }) }