import * as z from "zod" import type { JsonObject } from "type-fest" import { defineAction } from "../../../automation/actions" import { getSlackApi } from "../lib/api" import { SLACK_CHANNEL_READ_REQUIREMENT, SLACK_CHANNELS_JOIN_SCOPE, SLACK_CHANNELS_MANAGE_SCOPE, SLACK_CHANNELS_READ_SCOPE, SLACK_CONVERSATION_HISTORY_REQUIREMENT, SLACK_CONVERSATION_READ_REQUIREMENT, SLACK_GROUPS_READ_SCOPE, SLACK_IM_READ_SCOPE, SLACK_MPIM_READ_SCOPE, SLACK_MPIM_WRITE_SCOPE, SLACK_PRIVATE_CHANNEL_INVITE_REQUIREMENT, SLACK_PRIVATE_CHANNEL_TOPIC_REQUIREMENT, SLACK_PRIVATE_CHANNEL_WRITE_SCOPE, SLACK_PUBLIC_CHANNEL_INVITE_REQUIREMENT, SLACK_PUBLIC_CHANNEL_MANAGE_REQUIREMENT, SLACK_PUBLIC_CHANNEL_TOPIC_REQUIREMENT, } from "../lib/scopes" import { SLACK_CONVERSATION_SCHEMA, SLACK_CURSOR_INPUT_SCHEMA, SLACK_MESSAGE_SCHEMA, SLACK_PROVIDER_CONVERSATION_SCHEMA, SLACK_PROVIDER_MESSAGE_SCHEMA, SLACK_PROVIDER_RESPONSE_METADATA_SCHEMA, SLACK_RESPONSE_METADATA_SCHEMA, toSlackConversation, toSlackMessage, toSlackResponseMetadata, } from "@automate.ax/integration-contracts/slack" const CONVERSATION_LIST_RESPONSE_SCHEMA = z.looseObject({ channels: SLACK_PROVIDER_CONVERSATION_SCHEMA.array(), ok: z.literal(true), response_metadata: SLACK_PROVIDER_RESPONSE_METADATA_SCHEMA, }) const CONVERSATION_INFO_RESPONSE_SCHEMA = z.looseObject({ channel: SLACK_PROVIDER_CONVERSATION_SCHEMA, ok: z.literal(true), }) const CONVERSATION_MEMBERS_RESPONSE_SCHEMA = z.looseObject({ members: z.string().array(), ok: z.literal(true), response_metadata: SLACK_PROVIDER_RESPONSE_METADATA_SCHEMA, }) const CONVERSATION_MESSAGES_RESPONSE_SCHEMA = z.looseObject({ has_more: z.boolean().optional(), messages: SLACK_PROVIDER_MESSAGE_SCHEMA.array(), ok: z.literal(true), response_metadata: SLACK_PROVIDER_RESPONSE_METADATA_SCHEMA, }) const CONVERSATION_MUTATION_RESPONSE_SCHEMA = z.looseObject({ channel: SLACK_PROVIDER_CONVERSATION_SCHEMA, ok: z.literal(true), }) const EMPTY_CONVERSATION_MUTATION_RESPONSE_SCHEMA = z.looseObject({ ok: z.literal(true), }) const CLOSE_CONVERSATION_RESPONSE_SCHEMA = z.looseObject({ already_closed: z.boolean().optional(), no_op: z.boolean().optional(), ok: z.literal(true), }) const OPEN_GROUP_DIRECT_MESSAGE_RESPONSE_SCHEMA = z.looseObject({ already_open: z.boolean().optional(), channel: SLACK_PROVIDER_CONVERSATION_SCHEMA, no_op: z.boolean().optional(), ok: z.literal(true), }) const CONVERSATION_PAGE_SCHEMA = z.object({ /** Conversations in the current page. */ conversations: SLACK_CONVERSATION_SCHEMA.array(), /** Pagination metadata for requesting the next page. */ responseMetadata: SLACK_RESPONSE_METADATA_SCHEMA, }) const CONVERSATION_MEMBER_PAGE_SCHEMA = z.object({ /** Slack user IDs belonging to the conversation. */ memberIds: z.string().array(), /** Pagination metadata for requesting the next page. */ responseMetadata: SLACK_RESPONSE_METADATA_SCHEMA, }) const MESSAGE_PAGE_SCHEMA = z.object({ /** Whether more time-window results are available. */ hasMore: z.boolean(), /** Messages in the current page. */ messages: SLACK_MESSAGE_SCHEMA.array(), /** Pagination metadata for requesting the next page. */ responseMetadata: SLACK_RESPONSE_METADATA_SCHEMA, }) const HISTORY_INPUT_SCHEMA = z.object({ /** Stable Slack conversation ID. */ conversationId: z.string().min(1), /** Opaque cursor returned by the previous page. */ cursor: z.string().min(1).optional(), /** Whether messages matching `oldest` or `latest` should be included. */ inclusive: z.boolean().optional(), /** Only messages before this Slack timestamp are returned. */ latest: z.string().min(1).optional(), /** * Maximum messages in one page. * * Slack limits newly distributed non-Marketplace apps to 15. */ limit: z.number().int().min(1).max(15).optional(), /** Only messages after this Slack timestamp are returned. */ oldest: z.string().min(1).optional(), }) const CHANNEL_NAME_SCHEMA = z .string() .trim() .min(1) .max(80) .regex( /^[a-z0-9_-]+$/, "Slack channel names use lowercase letters, numbers, hyphens, and underscores.", ) const CHANNEL_ID_INPUT_SCHEMA = z.object({ /** Stable Slack channel ID. */ channelId: z.string().min(1), }) const CHANNEL_USERS_INPUT_SCHEMA = CHANNEL_ID_INPUT_SCHEMA.extend({ /** Continue inviting valid users when another supplied user is invalid. */ force: z.boolean().optional(), /** Slack member IDs to invite. */ userIds: z.string().min(1).array().min(1).max(1000), }) const CHANNEL_USER_INPUT_SCHEMA = CHANNEL_ID_INPUT_SCHEMA.extend({ /** Slack member to remove. */ userId: z.string().min(1), }) const CHANNEL_NAME_INPUT_SCHEMA = CHANNEL_ID_INPUT_SCHEMA.extend({ /** New Slack channel name. */ name: CHANNEL_NAME_SCHEMA, }) const CHANNEL_PURPOSE_INPUT_SCHEMA = CHANNEL_ID_INPUT_SCHEMA.extend({ /** New plain-text channel description. */ purpose: z.string(), }) const CHANNEL_TOPIC_INPUT_SCHEMA = CHANNEL_ID_INPUT_SCHEMA.extend({ /** New plain-text channel topic. */ topic: z.string(), }) /** Lists public channels visible to the installed Slack bot. */ export const listSlackPublicChannels = defineAction( "List Slack public channels", ) .describe("Lists public channels visible to the installed Slack bot.") .account("slack", SLACK_CHANNELS_READ_SCOPE) .input( SLACK_CURSOR_INPUT_SCHEMA.extend({ /** Whether archived channels should be omitted. */ excludeArchived: z.boolean().optional(), }), ) .output(CONVERSATION_PAGE_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => listConversations(account.secret, { ...input, types: "public_channel", }), ) /** Lists private channels the installed Slack bot can access. */ export const listSlackPrivateChannels = defineAction( "List Slack private channels", ) .describe("Lists private channels the installed Slack bot can access.") .account("slack", SLACK_GROUPS_READ_SCOPE) .input( SLACK_CURSOR_INPUT_SCHEMA.extend({ /** Whether archived channels should be omitted. */ excludeArchived: z.boolean().optional(), }), ) .output(CONVERSATION_PAGE_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => listConversations(account.secret, { ...input, types: "private_channel", }), ) /** Lists one-to-one direct-message conversations visible to the bot. */ export const listSlackDirectMessages = defineAction( "List Slack direct messages", ) .describe("Lists direct-message conversations visible to the Slack bot.") .account("slack", SLACK_IM_READ_SCOPE) .input(SLACK_CURSOR_INPUT_SCHEMA) .output(CONVERSATION_PAGE_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => listConversations(account.secret, { ...input, types: "im" }), ) /** Lists group direct-message conversations visible to the bot. */ export const listSlackGroupDirectMessages = defineAction( "List Slack group direct messages", ) .describe("Lists group direct-message conversations visible to the bot.") .account("slack", SLACK_MPIM_READ_SCOPE) .input(SLACK_CURSOR_INPUT_SCHEMA) .output(CONVERSATION_PAGE_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => listConversations(account.secret, { ...input, types: "mpim" }), ) /** Finds an accessible public or private Slack channel by exact name or ID. */ export const findSlackConversation = defineAction("Find Slack conversation") .describe("Finds an accessible Slack channel by exact name or ID.") .account("slack", SLACK_CHANNEL_READ_REQUIREMENT) .input( z.object({ /** Public or private channel name or stable Slack conversation ID. */ conversation: z.string().trim().min(1), /** Whether archived channels should be considered. */ includeArchived: z.boolean().optional(), }), ) .output(SLACK_CONVERSATION_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => resolveSlackConversation(account.secret, input), ) /** * Gets complete metadata for any Slack conversation type. * * All conversation read scopes are required because public, private, direct, * and group-direct conversation IDs share this action. */ export const getSlackConversation = defineAction("Get Slack conversation") .describe("Gets complete metadata for a Slack conversation.") .account("slack", SLACK_CONVERSATION_READ_REQUIREMENT) .input( z.object({ /** Stable Slack conversation ID. */ conversationId: z.string().min(1), /** Whether Slack should include the member count. */ includeMemberCount: z.boolean().optional(), }), ) .output(SLACK_CONVERSATION_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => toSlackConversation( ( await getSlackApi(account.secret).call("conversations.info", { query: { channel: input.conversationId, include_num_members: input.includeMemberCount, }, responseSchema: CONVERSATION_INFO_RESPONSE_SCHEMA, }) ).channel, ), ) /** Lists Slack user IDs belonging to any conversation type. */ export const listSlackConversationMembers = defineAction( "List Slack conversation members", ) .describe("Lists member IDs for a Slack conversation.") .account("slack", SLACK_CONVERSATION_READ_REQUIREMENT) .input( SLACK_CURSOR_INPUT_SCHEMA.extend({ /** Stable Slack conversation ID. */ conversationId: z.string().min(1), }), ) .output(CONVERSATION_MEMBER_PAGE_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const result = await getSlackApi(account.secret).call( "conversations.members", { query: { channel: input.conversationId, cursor: input.cursor, limit: input.limit, }, responseSchema: CONVERSATION_MEMBERS_RESPONSE_SCHEMA, }, ) return { memberIds: result.members, responseMetadata: toSlackResponseMetadata(result.response_metadata), } }) /** Gets a page of messages from any Slack conversation type. */ export const getSlackConversationHistory = defineAction( "Get Slack conversation history", ) .describe("Gets a cursor-paginated page of Slack conversation messages.") .account("slack", SLACK_CONVERSATION_HISTORY_REQUIREMENT) .input(HISTORY_INPUT_SCHEMA) .output(MESSAGE_PAGE_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => toMessagePage( await getSlackApi(account.secret).call("conversations.history", { query: { channel: input.conversationId, cursor: input.cursor, inclusive: input.inclusive, latest: input.latest, limit: input.limit, oldest: input.oldest, }, responseSchema: CONVERSATION_MESSAGES_RESPONSE_SCHEMA, }), ), ) /** Gets a cursor-paginated Slack message thread. */ export const getSlackThreadReplies = defineAction("Get Slack thread replies") .describe("Gets a cursor-paginated Slack message thread.") .account("slack", SLACK_CONVERSATION_HISTORY_REQUIREMENT) .input( HISTORY_INPUT_SCHEMA.extend({ /** Timestamp of the parent message whose thread should be read. */ threadTimestamp: z.string().min(1), }), ) .output(MESSAGE_PAGE_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => toMessagePage( await getSlackApi(account.secret).call("conversations.replies", { query: { channel: input.conversationId, cursor: input.cursor, inclusive: input.inclusive, latest: input.latest, limit: input.limit, oldest: input.oldest, ts: input.threadTimestamp, }, responseSchema: CONVERSATION_MESSAGES_RESPONSE_SCHEMA, }), ), ) /** Creates a public Slack channel. */ export const createSlackPublicChannel = defineAction( "Create Slack public channel", ) .describe("Creates a public Slack channel.") .account("slack", SLACK_PUBLIC_CHANNEL_MANAGE_REQUIREMENT) .input(z.object({ name: CHANNEL_NAME_SCHEMA })) .output(SLACK_CONVERSATION_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => mutateConversation(account.secret, "conversations.create", { is_private: false, name: input.name, }), ) /** Creates a private Slack channel. */ export const createSlackPrivateChannel = defineAction( "Create Slack private channel", ) .describe("Creates a private Slack channel.") .account("slack", SLACK_PRIVATE_CHANNEL_WRITE_SCOPE) .input(z.object({ name: CHANNEL_NAME_SCHEMA })) .output(SLACK_CONVERSATION_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => mutateConversation(account.secret, "conversations.create", { is_private: true, name: input.name, }), ) /** Invites members to a public Slack channel. */ export const inviteSlackUsersToPublicChannel = defineAction( "Invite users to Slack public channel", ) .describe("Invites up to 1,000 members to a public Slack channel.") .account("slack", SLACK_PUBLIC_CHANNEL_INVITE_REQUIREMENT) .input(CHANNEL_USERS_INPUT_SCHEMA) .output(SLACK_CONVERSATION_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => inviteConversationUsers(account.secret, input), ) /** Invites members to a private Slack channel. */ export const inviteSlackUsersToPrivateChannel = defineAction( "Invite users to Slack private channel", ) .describe("Invites up to 1,000 members to a private Slack channel.") .account("slack", SLACK_PRIVATE_CHANNEL_INVITE_REQUIREMENT) .input(CHANNEL_USERS_INPUT_SCHEMA) .output(SLACK_CONVERSATION_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => inviteConversationUsers(account.secret, input), ) /** Removes a member from a public Slack channel. */ export const removeSlackUserFromPublicChannel = defineAction( "Remove user from Slack public channel", ) .describe("Removes a member from a public Slack channel.") .account("slack", SLACK_CHANNELS_MANAGE_SCOPE) .input(CHANNEL_USER_INPUT_SCHEMA) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => mutateConversationWithoutOutput(account.secret, "conversations.kick", { channel: input.channelId, user: input.userId, }), ) /** Removes a member from a private Slack channel. */ export const removeSlackUserFromPrivateChannel = defineAction( "Remove user from Slack private channel", ) .describe("Removes a member from a private Slack channel.") .account("slack", SLACK_PRIVATE_CHANNEL_WRITE_SCOPE) .input(CHANNEL_USER_INPUT_SCHEMA) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => mutateConversationWithoutOutput(account.secret, "conversations.kick", { channel: input.channelId, user: input.userId, }), ) /** Joins an existing public Slack channel. */ export const joinSlackPublicChannel = defineAction("Join Slack public channel") .describe("Joins an existing public Slack channel.") .account("slack", SLACK_CHANNELS_JOIN_SCOPE) .input(CHANNEL_ID_INPUT_SCHEMA) .output(SLACK_CONVERSATION_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => mutateConversation(account.secret, "conversations.join", { channel: input.channelId, }), ) /** Leaves a public Slack channel. */ export const leaveSlackPublicChannel = defineAction( "Leave Slack public channel", ) .describe("Leaves a public Slack channel.") .account("slack", SLACK_PUBLIC_CHANNEL_MANAGE_REQUIREMENT) .input(CHANNEL_ID_INPUT_SCHEMA) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => mutateConversationWithoutOutput(account.secret, "conversations.leave", { channel: input.channelId, }), ) /** Leaves a private Slack channel. */ export const leaveSlackPrivateChannel = defineAction( "Leave Slack private channel", ) .describe("Leaves a private Slack channel.") .account("slack", SLACK_PRIVATE_CHANNEL_WRITE_SCOPE) .input(CHANNEL_ID_INPUT_SCHEMA) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => mutateConversationWithoutOutput(account.secret, "conversations.leave", { channel: input.channelId, }), ) /** Renames a public Slack channel. */ export const renameSlackPublicChannel = defineAction( "Rename Slack public channel", ) .describe("Renames a public Slack channel.") .account("slack", SLACK_PUBLIC_CHANNEL_MANAGE_REQUIREMENT) .input(CHANNEL_NAME_INPUT_SCHEMA) .output(SLACK_CONVERSATION_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => mutateConversation(account.secret, "conversations.rename", { channel: input.channelId, name: input.name, }), ) /** Renames a private Slack channel. */ export const renameSlackPrivateChannel = defineAction( "Rename Slack private channel", ) .describe("Renames a private Slack channel.") .account("slack", SLACK_PRIVATE_CHANNEL_WRITE_SCOPE) .input(CHANNEL_NAME_INPUT_SCHEMA) .output(SLACK_CONVERSATION_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => mutateConversation(account.secret, "conversations.rename", { channel: input.channelId, name: input.name, }), ) /** Sets a public Slack channel's description. */ export const setSlackPublicChannelPurpose = defineAction( "Set Slack public channel purpose", ) .describe("Sets the description of a public Slack channel.") .account("slack", SLACK_PUBLIC_CHANNEL_TOPIC_REQUIREMENT) .input(CHANNEL_PURPOSE_INPUT_SCHEMA) .output(SLACK_CONVERSATION_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => mutateConversation(account.secret, "conversations.setPurpose", { channel: input.channelId, purpose: input.purpose, }), ) /** Sets a private Slack channel's description. */ export const setSlackPrivateChannelPurpose = defineAction( "Set Slack private channel purpose", ) .describe("Sets the description of a private Slack channel.") .account("slack", SLACK_PRIVATE_CHANNEL_TOPIC_REQUIREMENT) .input(CHANNEL_PURPOSE_INPUT_SCHEMA) .output(SLACK_CONVERSATION_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => mutateConversation(account.secret, "conversations.setPurpose", { channel: input.channelId, purpose: input.purpose, }), ) /** Sets a public Slack channel's topic. */ export const setSlackPublicChannelTopic = defineAction( "Set Slack public channel topic", ) .describe("Sets the plain-text topic of a public Slack channel.") .account("slack", SLACK_PUBLIC_CHANNEL_TOPIC_REQUIREMENT) .input(CHANNEL_TOPIC_INPUT_SCHEMA) .output(SLACK_CONVERSATION_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => mutateConversation(account.secret, "conversations.setTopic", { channel: input.channelId, topic: input.topic, }), ) /** Sets a private Slack channel's topic. */ export const setSlackPrivateChannelTopic = defineAction( "Set Slack private channel topic", ) .describe("Sets the plain-text topic of a private Slack channel.") .account("slack", SLACK_PRIVATE_CHANNEL_TOPIC_REQUIREMENT) .input(CHANNEL_TOPIC_INPUT_SCHEMA) .output(SLACK_CONVERSATION_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => mutateConversation(account.secret, "conversations.setTopic", { channel: input.channelId, topic: input.topic, }), ) /** Archives a public Slack channel. */ export const archiveSlackPublicChannel = defineAction( "Archive Slack public channel", ) .describe("Archives a public Slack channel.") .account("slack", SLACK_PUBLIC_CHANNEL_MANAGE_REQUIREMENT) .input(CHANNEL_ID_INPUT_SCHEMA) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => mutateConversationWithoutOutput(account.secret, "conversations.archive", { channel: input.channelId, }), ) /** Archives a private Slack channel. */ export const archiveSlackPrivateChannel = defineAction( "Archive Slack private channel", ) .describe("Archives a private Slack channel.") .account("slack", SLACK_PRIVATE_CHANNEL_WRITE_SCOPE) .input(CHANNEL_ID_INPUT_SCHEMA) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => mutateConversationWithoutOutput(account.secret, "conversations.archive", { channel: input.channelId, }), ) /** Opens or resumes a multi-person direct message. */ export const openSlackGroupDirectMessage = defineAction( "Open Slack group direct message", ) .describe("Opens or resumes a multi-person Slack direct message.") .account("slack", SLACK_MPIM_WRITE_SCOPE) .input( z.object({ /** Prevent creating a new conversation when none already exists. */ preventCreation: z.boolean().optional(), /** At least two other Slack members in the conversation. */ userIds: z.string().min(1).array().min(2).max(8), }), ) .output( z.object({ alreadyOpen: z.boolean(), conversation: SLACK_CONVERSATION_SCHEMA, }), ) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const result = await getSlackApi(account.secret).call( "conversations.open", { body: { prevent_creation: input.preventCreation ?? false, return_im: true, users: input.userIds.join(","), }, responseSchema: OPEN_GROUP_DIRECT_MESSAGE_RESPONSE_SCHEMA, }, ) return { alreadyOpen: result.already_open ?? result.no_op ?? false, conversation: toSlackConversation(result.channel), } }) /** Closes a multi-person direct message. */ export const closeSlackGroupDirectMessage = defineAction( "Close Slack group direct message", ) .describe("Closes a multi-person Slack direct message.") .account("slack", SLACK_MPIM_WRITE_SCOPE) .input(z.object({ conversationId: z.string().min(1) })) .output(z.object({ alreadyClosed: z.boolean() })) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const result = await getSlackApi(account.secret).call( "conversations.close", { body: { channel: input.conversationId }, responseSchema: CLOSE_CONVERSATION_RESPONSE_SCHEMA, }, ) return { alreadyClosed: result.already_closed ?? result.no_op ?? false } }) /** * Runs a Slack conversation mutation that returns the affected conversation. * * @param secret - Resolved Slack integration secret. * @param method - Conversation method to invoke. * @param body - Provider-native request body. */ async function mutateConversation( secret: Record, method: string, body: JsonObject, ) { return toSlackConversation( ( await getSlackApi(secret).call(method, { body, responseSchema: CONVERSATION_MUTATION_RESPONSE_SCHEMA, }) ).channel, ) } /** * Runs a Slack conversation mutation whose success response has no entity. * * @param secret - Resolved Slack integration secret. * @param method - Conversation method to invoke. * @param body - Provider-native request body. */ async function mutateConversationWithoutOutput( secret: Record, method: string, body: JsonObject, ) { await getSlackApi(secret).call(method, { body, responseSchema: EMPTY_CONVERSATION_MUTATION_RESPONSE_SCHEMA, }) } /** * Invites one validated batch of users to a Slack channel. * * @param secret - Resolved Slack integration secret. * @param input - Channel, user IDs, and partial-success preference. */ async function inviteConversationUsers( secret: Record, input: z.output, ) { return mutateConversation(secret, "conversations.invite", { channel: input.channelId, ...(input.force !== undefined && { force: input.force }), users: input.userIds.join(","), }) } /** * Lists one provider conversation type. * * @param secret - Resolved Slack integration secret. * @param options - Conversation type, filters, and pagination. * @param options.cursor - Cursor returned by the previous page. * @param options.excludeArchived - Whether archived channels are omitted. * @param options.limit - Maximum requested page size. * @param options.types - Slack conversation type. */ async function listConversations( secret: Record, options: { cursor?: string excludeArchived?: boolean limit?: number types: "im" | "mpim" | "private_channel" | "public_channel" }, ) { const result = await getSlackApi(secret).call("conversations.list", { query: { cursor: options.cursor, exclude_archived: options.excludeArchived, limit: options.limit, types: options.types, }, responseSchema: CONVERSATION_LIST_RESPONSE_SCHEMA, }) return { conversations: result.channels.map(toSlackConversation), responseMetadata: toSlackResponseMetadata(result.response_metadata), } } /** * Finds an accessible Slack channel without guessing the provider's ID format. * * @param secret - Resolved Slack integration secret. * @param options - Exact conversation reference and archive behavior. * @param options.conversation - Exact channel name or conversation ID. * @param options.includeArchived - Whether archived channels can match. */ async function resolveSlackConversation( secret: Record, options: { conversation: string; includeArchived?: boolean }, ) { const normalizedReference = options.conversation .replace(/^#/, "") .toLowerCase() const nameMatches: z.output[] = [] let cursor: string | undefined do { const result = await getSlackApi(secret).call("conversations.list", { query: { cursor, exclude_archived: !(options.includeArchived ?? false), limit: 200, types: "public_channel,private_channel", }, responseSchema: CONVERSATION_LIST_RESPONSE_SCHEMA, }) const idMatch = result.channels.find( ({ id }) => id === options.conversation, ) if (idMatch) return toSlackConversation(idMatch) nameMatches.push( ...result.channels.filter( ({ name, name_normalized }) => name?.toLowerCase() === normalizedReference || name_normalized?.toLowerCase() === normalizedReference, ), ) cursor = result.response_metadata?.next_cursor || undefined } while (cursor) if (nameMatches.length === 1) return toSlackConversation(nameMatches[0]!) if (nameMatches.length > 1) { throw new Error( `Slack conversation "${options.conversation}" is ambiguous; matching IDs: ${nameMatches.map(({ id }) => id).join(", ")}.`, ) } throw new Error(`Slack conversation "${options.conversation}" was not found.`) } /** * Normalizes one Slack message page. * * @param result - Validated provider response. */ function toMessagePage( result: z.output, ) { return { hasMore: result.has_more ?? false, messages: result.messages.map(toSlackMessage), responseMetadata: toSlackResponseMetadata(result.response_metadata), } }