import * as z from "zod" import { defineAction } from "../../../automation/actions" import { getSlackApi } from "../lib/api" import { SLACK_BOOKMARKS_READ_SCOPE, SLACK_BOOKMARKS_WRITE_SCOPE, } from "../lib/scopes" const PROVIDER_BOOKMARK_SCHEMA = z.looseObject({ channel_id: z.string(), date_created: z.number().int().nonnegative().optional(), date_updated: z.number().int().nonnegative().optional(), emoji: z.string().optional(), id: z.string(), last_updated_by_user_id: z.string().optional(), link: z.string(), title: z.string(), type: z.literal("link"), }) const BOOKMARK_SCHEMA = z.object({ /** Slack conversation containing the bookmark. */ conversationId: z.string(), /** Unix timestamp when Slack created the bookmark. */ createdAt: z.number().int().nonnegative().optional(), /** Emoji displayed with the bookmark. */ emoji: z.string().optional(), /** Stable Slack bookmark ID. */ id: z.string(), /** Slack member who last changed the bookmark. */ lastUpdatedByUserId: z.string().optional(), /** Human-readable bookmark title. */ title: z.string(), /** Provider-supported bookmark category. */ type: z.literal("link"), /** Unix timestamp when Slack last changed the bookmark. */ updatedAt: z.number().int().nonnegative().optional(), /** External link opened by the bookmark. */ url: z.string(), }) const BOOKMARK_RESPONSE_SCHEMA = z.looseObject({ bookmark: PROVIDER_BOOKMARK_SCHEMA, ok: z.literal(true), }) const LIST_BOOKMARKS_RESPONSE_SCHEMA = z.looseObject({ bookmarks: PROVIDER_BOOKMARK_SCHEMA.array(), ok: z.literal(true), }) const EMPTY_BOOKMARK_RESPONSE_SCHEMA = z.looseObject({ ok: z.literal(true) }) /** Adds an external-link bookmark to a Slack conversation. */ export const addSlackBookmark = defineAction("Add Slack bookmark") .describe("Adds an external-link bookmark to a Slack conversation.") .account("slack", SLACK_BOOKMARKS_WRITE_SCOPE) .input( z.object({ conversationId: z.string().min(1), emoji: z.string().min(1).optional(), title: z.string().min(1), url: z.url(), }), ) .output(BOOKMARK_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => toSlackBookmark( ( await getSlackApi(account.secret).call("bookmarks.add", { body: { channel_id: input.conversationId, ...(input.emoji !== undefined && { emoji: input.emoji }), link: input.url, title: input.title, type: "link", }, responseSchema: BOOKMARK_RESPONSE_SCHEMA, }) ).bookmark, ), ) /** Updates an existing Slack channel bookmark. */ export const updateSlackBookmark = defineAction("Update Slack bookmark") .describe("Updates an existing Slack channel bookmark.") .account("slack", SLACK_BOOKMARKS_WRITE_SCOPE) .input( z.object({ bookmarkId: z.string().min(1), conversationId: z.string().min(1), emoji: z.string().min(1).optional(), title: z.string().min(1).optional(), url: z.url().optional(), }), ) .output(BOOKMARK_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => toSlackBookmark( ( await getSlackApi(account.secret).call("bookmarks.edit", { body: { bookmark_id: input.bookmarkId, channel_id: input.conversationId, ...(input.emoji !== undefined && { emoji: input.emoji }), ...(input.url !== undefined && { link: input.url }), ...(input.title !== undefined && { title: input.title }), }, responseSchema: BOOKMARK_RESPONSE_SCHEMA, }) ).bookmark, ), ) /** Removes a Slack channel bookmark. */ export const removeSlackBookmark = defineAction("Remove Slack bookmark") .describe("Removes a Slack channel bookmark.") .account("slack", SLACK_BOOKMARKS_WRITE_SCOPE) .input( z.object({ bookmarkId: z.string().min(1), conversationId: z.string().min(1), }), ) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await getSlackApi(account.secret).call("bookmarks.remove", { body: { bookmark_id: input.bookmarkId, channel_id: input.conversationId, }, responseSchema: EMPTY_BOOKMARK_RESPONSE_SCHEMA, }) }) /** Lists bookmarks in a Slack conversation. */ export const listSlackBookmarks = defineAction("List Slack bookmarks") .describe("Lists bookmarks in a Slack conversation.") .account("slack", SLACK_BOOKMARKS_READ_SCOPE) .input(z.object({ conversationId: z.string().min(1) })) .output(BOOKMARK_SCHEMA.array()) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { return ( await getSlackApi(account.secret).call("bookmarks.list", { body: { channel_id: input.conversationId }, responseSchema: LIST_BOOKMARKS_RESPONSE_SCHEMA, }) ).bookmarks.map(toSlackBookmark) }) /** * Converts one provider bookmark into the public casing contract. * * @param bookmark - Provider-native bookmark fields. */ function toSlackBookmark(bookmark: z.output) { return { conversationId: bookmark.channel_id, createdAt: bookmark.date_created, emoji: bookmark.emoji, id: bookmark.id, lastUpdatedByUserId: bookmark.last_updated_by_user_id, title: bookmark.title, type: bookmark.type, updatedAt: bookmark.date_updated, url: bookmark.link, } }