From fabb8202e27286a16caa50382b84c7a13645eebe Mon Sep 17 00:00:00 2001 From: waleed Date: Fri, 5 Dec 2025 13:19:26 -0800 Subject: [PATCH] feat(tools): added more slack tools --- apps/docs/components/icons.tsx | 176 ++++------------ .../content/docs/en/tools/google_calendar.mdx | 6 +- .../content/docs/en/tools/google_drive.mdx | 4 +- .../content/docs/en/tools/google_sheets.mdx | 18 +- apps/docs/content/docs/en/tools/slack.mdx | 76 +++++++ apps/sim/blocks/blocks/slack.ts | 199 +++++++++++++++++- apps/sim/tools/registry.ts | 8 + apps/sim/tools/slack/get_user.ts | 155 ++++++++++++++ apps/sim/tools/slack/index.ts | 8 + apps/sim/tools/slack/list_channels.ts | 150 +++++++++++++ apps/sim/tools/slack/list_members.ts | 109 ++++++++++ apps/sim/tools/slack/list_users.ts | 141 +++++++++++++ apps/sim/tools/slack/types.ts | 96 +++++++++ 13 files changed, 999 insertions(+), 147 deletions(-) create mode 100644 apps/sim/tools/slack/get_user.ts create mode 100644 apps/sim/tools/slack/list_channels.ts create mode 100644 apps/sim/tools/slack/list_members.ts create mode 100644 apps/sim/tools/slack/list_users.ts diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 6564a6d21c0..062d7f479fa 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -696,8 +696,8 @@ export function GrafanaIcon(props: SVGProps) { y2='5.356' gradientUnits='userSpaceOnUse' > - - + + @@ -2757,111 +2757,19 @@ export function MicrosoftSharepointIcon(props: SVGProps) { export function MicrosoftPlannerIcon(props: SVGProps) { return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + ) } @@ -3344,29 +3252,10 @@ export function TrelloIcon(props: SVGProps) { export function AsanaIcon(props: SVGProps) { return ( - - - - - - - + + + + ) } @@ -3975,6 +3864,33 @@ export function DynamoDBIcon(props: SVGProps) { ) } +export function McpIcon(props: SVGProps) { + return ( + + + + + + + + + + + ) +} + export function WordpressIcon(props: SVGProps) { return ( diff --git a/apps/docs/content/docs/en/tools/google_calendar.mdx b/apps/docs/content/docs/en/tools/google_calendar.mdx index 4eb2d8b384a..b38eb3d9958 100644 --- a/apps/docs/content/docs/en/tools/google_calendar.mdx +++ b/apps/docs/content/docs/en/tools/google_calendar.mdx @@ -45,9 +45,9 @@ Create a new event in Google Calendar | `summary` | string | Yes | Event title/summary | | `description` | string | No | Event description | | `location` | string | No | Event location | -| `startDateTime` | string | Yes | Start date and time \(RFC3339 format, e.g., 2025-06-03T10:00:00-08:00\) | -| `endDateTime` | string | Yes | End date and time \(RFC3339 format, e.g., 2025-06-03T11:00:00-08:00\) | -| `timeZone` | string | No | Time zone \(e.g., America/Los_Angeles\) | +| `startDateTime` | string | Yes | Start date and time. MUST include timezone offset \(e.g., 2025-06-03T10:00:00-08:00\) OR provide timeZone parameter | +| `endDateTime` | string | Yes | End date and time. MUST include timezone offset \(e.g., 2025-06-03T11:00:00-08:00\) OR provide timeZone parameter | +| `timeZone` | string | No | Time zone \(e.g., America/Los_Angeles\). Required if datetime does not include offset. Defaults to America/Los_Angeles if not provided. | | `attendees` | array | No | Array of attendee email addresses | | `sendUpdates` | string | No | How to send updates to attendees: all, externalOnly, or none | diff --git a/apps/docs/content/docs/en/tools/google_drive.mdx b/apps/docs/content/docs/en/tools/google_drive.mdx index 35f18738e99..27de721e6e0 100644 --- a/apps/docs/content/docs/en/tools/google_drive.mdx +++ b/apps/docs/content/docs/en/tools/google_drive.mdx @@ -113,8 +113,8 @@ List files and folders in Google Drive | --------- | ---- | -------- | ----------- | | `folderSelector` | string | No | Select the folder to list files from | | `folderId` | string | No | The ID of the folder to list files from \(internal use\) | -| `query` | string | No | A query to filter the files | -| `pageSize` | number | No | The number of files to return | +| `query` | string | No | Search term to filter files by name \(e.g. "budget" finds files with "budget" in the name\). Do NOT use Google Drive query syntax here - just provide a plain search term. | +| `pageSize` | number | No | The maximum number of files to return \(default: 100\) | | `pageToken` | string | No | The page token to use for pagination | #### Output diff --git a/apps/docs/content/docs/en/tools/google_sheets.mdx b/apps/docs/content/docs/en/tools/google_sheets.mdx index b1fc2e8ef39..7efd5230660 100644 --- a/apps/docs/content/docs/en/tools/google_sheets.mdx +++ b/apps/docs/content/docs/en/tools/google_sheets.mdx @@ -91,8 +91,8 @@ Read data from a Google Sheets spreadsheet | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `spreadsheetId` | string | Yes | The ID of the spreadsheet to read from | -| `range` | string | No | The range of cells to read from | +| `spreadsheetId` | string | Yes | The ID of the spreadsheet \(found in the URL: docs.google.com/spreadsheets/d/\{SPREADSHEET_ID\}/edit\). | +| `range` | string | No | The A1 notation range to read \(e.g. "Sheet1!A1:D10", "A1:B5"\). Defaults to first sheet A1:Z1000 if not specified. | #### Output @@ -109,9 +109,9 @@ Write data to a Google Sheets spreadsheet | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `spreadsheetId` | string | Yes | The ID of the spreadsheet to write to | -| `range` | string | No | The range of cells to write to | -| `values` | array | Yes | The data to write to the spreadsheet | +| `spreadsheetId` | string | Yes | The ID of the spreadsheet | +| `range` | string | No | The A1 notation range to write to \(e.g. "Sheet1!A1:D10", "A1:B5"\) | +| `values` | array | Yes | The data to write as a 2D array \(e.g. \[\["Name", "Age"\], \["Alice", 30\], \["Bob", 25\]\]\) or array of objects. | | `valueInputOption` | string | No | The format of the data to write | | `includeValuesInResponse` | boolean | No | Whether to include the written values in the response | @@ -134,8 +134,8 @@ Update data in a Google Sheets spreadsheet | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `spreadsheetId` | string | Yes | The ID of the spreadsheet to update | -| `range` | string | No | The range of cells to update | -| `values` | array | Yes | The data to update in the spreadsheet | +| `range` | string | No | The A1 notation range to update \(e.g. "Sheet1!A1:D10", "A1:B5"\) | +| `values` | array | Yes | The data to update as a 2D array \(e.g. \[\["Name", "Age"\], \["Alice", 30\]\]\) or array of objects. | | `valueInputOption` | string | No | The format of the data to update | | `includeValuesInResponse` | boolean | No | Whether to include the updated values in the response | @@ -158,8 +158,8 @@ Append data to the end of a Google Sheets spreadsheet | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `spreadsheetId` | string | Yes | The ID of the spreadsheet to append to | -| `range` | string | No | The range of cells to append after | -| `values` | array | Yes | The data to append to the spreadsheet | +| `range` | string | No | The A1 notation range to append after \(e.g. "Sheet1", "Sheet1!A:D"\) | +| `values` | array | Yes | The data to append as a 2D array \(e.g. \[\["Alice", 30\], \["Bob", 25\]\]\) or array of objects. | | `valueInputOption` | string | No | The format of the data to append | | `insertDataOption` | string | No | How to insert the data \(OVERWRITE or INSERT_ROWS\) | | `includeValuesInResponse` | boolean | No | Whether to include the appended values in the response | diff --git a/apps/docs/content/docs/en/tools/slack.mdx b/apps/docs/content/docs/en/tools/slack.mdx index 3054a9a4b37..daf9a92aa54 100644 --- a/apps/docs/content/docs/en/tools/slack.mdx +++ b/apps/docs/content/docs/en/tools/slack.mdx @@ -122,6 +122,82 @@ Read the latest messages from Slack channels. Retrieve conversation history with | --------- | ---- | ----------- | | `messages` | array | Array of message objects from the channel | +### `slack_list_channels` + +List all channels in a Slack workspace. Returns public and private channels the bot has access to. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `authMethod` | string | No | Authentication method: oauth or bot_token | +| `botToken` | string | No | Bot token for Custom Bot | +| `includePrivate` | boolean | No | Include private channels the bot is a member of \(default: true\) | +| `excludeArchived` | boolean | No | Exclude archived channels \(default: true\) | +| `limit` | number | No | Maximum number of channels to return \(default: 100, max: 200\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `channels` | array | Array of channel objects from the workspace | + +### `slack_list_members` + +List all members (user IDs) in a Slack channel. Use with Get User Info to resolve IDs to names. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `authMethod` | string | No | Authentication method: oauth or bot_token | +| `botToken` | string | No | Bot token for Custom Bot | +| `channel` | string | Yes | Channel ID to list members from | +| `limit` | number | No | Maximum number of members to return \(default: 100, max: 200\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `members` | array | Array of user IDs who are members of the channel \(e.g., U1234567890\) | + +### `slack_list_users` + +List all users in a Slack workspace. Returns user profiles with names and avatars. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `authMethod` | string | No | Authentication method: oauth or bot_token | +| `botToken` | string | No | Bot token for Custom Bot | +| `includeDeleted` | boolean | No | Include deactivated/deleted users \(default: false\) | +| `limit` | number | No | Maximum number of users to return \(default: 100, max: 200\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `users` | array | Array of user objects from the workspace | + +### `slack_get_user` + +Get detailed information about a specific Slack user by their user ID. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `authMethod` | string | No | Authentication method: oauth or bot_token | +| `botToken` | string | No | Bot token for Custom Bot | +| `userId` | string | Yes | User ID to look up \(e.g., U1234567890\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `user` | object | Detailed user information | + ### `slack_download` Download a file from Slack diff --git a/apps/sim/blocks/blocks/slack.ts b/apps/sim/blocks/blocks/slack.ts index f06308b47ac..9e7cbc3f623 100644 --- a/apps/sim/blocks/blocks/slack.ts +++ b/apps/sim/blocks/blocks/slack.ts @@ -26,6 +26,10 @@ export const SlackBlock: BlockConfig = { { label: 'Send Message', id: 'send' }, { label: 'Create Canvas', id: 'canvas' }, { label: 'Read Messages', id: 'read' }, + { label: 'List Channels', id: 'list_channels' }, + { label: 'List Channel Members', id: 'list_members' }, + { label: 'List Users', id: 'list_users' }, + { label: 'Get User Info', id: 'get_user' }, { label: 'Download File', id: 'download' }, { label: 'Update Message', id: 'update' }, { label: 'Delete Message', id: 'delete' }, @@ -88,6 +92,11 @@ export const SlackBlock: BlockConfig = { placeholder: 'Select Slack channel', mode: 'basic', dependsOn: ['credential', 'authMethod'], + condition: { + field: 'operation', + value: ['list_channels', 'list_users', 'get_user'], + not: true, + }, }, // Manual channel ID input (advanced mode) { @@ -97,6 +106,11 @@ export const SlackBlock: BlockConfig = { canonicalParamId: 'channel', placeholder: 'Enter Slack channel ID (e.g., C1234567890)', mode: 'advanced', + condition: { + field: 'operation', + value: ['list_channels', 'list_users', 'get_user'], + not: true, + }, }, { id: 'text', @@ -178,6 +192,82 @@ export const SlackBlock: BlockConfig = { value: 'read', }, }, + // List Channels specific fields + { + id: 'includePrivate', + title: 'Include Private Channels', + type: 'dropdown', + options: [ + { label: 'Yes', id: 'true' }, + { label: 'No', id: 'false' }, + ], + value: () => 'true', + condition: { + field: 'operation', + value: 'list_channels', + }, + }, + { + id: 'channelLimit', + title: 'Channel Limit', + type: 'short-input', + canonicalParamId: 'limit', + placeholder: '100', + condition: { + field: 'operation', + value: 'list_channels', + }, + }, + // List Members specific fields + { + id: 'memberLimit', + title: 'Member Limit', + type: 'short-input', + canonicalParamId: 'limit', + placeholder: '100', + condition: { + field: 'operation', + value: 'list_members', + }, + }, + // List Users specific fields + { + id: 'includeDeleted', + title: 'Include Deactivated Users', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + condition: { + field: 'operation', + value: 'list_users', + }, + }, + { + id: 'userLimit', + title: 'User Limit', + type: 'short-input', + canonicalParamId: 'limit', + placeholder: '100', + condition: { + field: 'operation', + value: 'list_users', + }, + }, + // Get User specific fields + { + id: 'userId', + title: 'User ID', + type: 'short-input', + placeholder: 'Enter Slack user ID (e.g., U1234567890)', + condition: { + field: 'operation', + value: 'get_user', + }, + required: true, + }, { id: 'oldest', title: 'Oldest Timestamp', @@ -280,6 +370,10 @@ export const SlackBlock: BlockConfig = { 'slack_message', 'slack_canvas', 'slack_message_reader', + 'slack_list_channels', + 'slack_list_members', + 'slack_list_users', + 'slack_get_user', 'slack_download', 'slack_update_message', 'slack_delete_message', @@ -294,6 +388,14 @@ export const SlackBlock: BlockConfig = { return 'slack_canvas' case 'read': return 'slack_message_reader' + case 'list_channels': + return 'slack_list_channels' + case 'list_members': + return 'slack_list_members' + case 'list_users': + return 'slack_list_users' + case 'get_user': + return 'slack_get_user' case 'download': return 'slack_download' case 'update': @@ -327,18 +429,31 @@ export const SlackBlock: BlockConfig = { deleteTimestamp, reactionTimestamp, emojiName, + includePrivate, + channelLimit, + memberLimit, + includeDeleted, + userLimit, + userId, ...rest } = params // Handle both selector and manual channel input const effectiveChannel = (channel || manualChannel || '').trim() - if (!effectiveChannel) { + // Operations that don't require a channel + const noChannelOperations = ['list_channels', 'list_users', 'get_user'] + + // Channel is required for most operations + if (!effectiveChannel && !noChannelOperations.includes(operation)) { throw new Error('Channel is required.') } - const baseParams: Record = { - channel: effectiveChannel, + const baseParams: Record = {} + + // Only add channel if we have one (not needed for list_channels) + if (effectiveChannel) { + baseParams.channel = effectiveChannel } // Handle authentication based on method @@ -394,6 +509,43 @@ export const SlackBlock: BlockConfig = { } break + case 'list_channels': + baseParams.includePrivate = includePrivate !== 'false' + baseParams.excludeArchived = true + if (channelLimit) { + const parsedLimit = Number.parseInt(channelLimit, 10) + baseParams.limit = !Number.isNaN(parsedLimit) ? parsedLimit : 100 + } else { + baseParams.limit = 100 + } + break + + case 'list_members': + if (memberLimit) { + const parsedLimit = Number.parseInt(memberLimit, 10) + baseParams.limit = !Number.isNaN(parsedLimit) ? parsedLimit : 100 + } else { + baseParams.limit = 100 + } + break + + case 'list_users': + baseParams.includeDeleted = includeDeleted === 'true' + if (userLimit) { + const parsedLimit = Number.parseInt(userLimit, 10) + baseParams.limit = !Number.isNaN(parsedLimit) ? parsedLimit : 100 + } else { + baseParams.limit = 100 + } + break + + case 'get_user': + if (!userId) { + throw new Error('User ID is required for get user operation') + } + baseParams.userId = userId + break + case 'download': { const fileId = (rest as any).fileId const downloadFileName = (rest as any).downloadFileName @@ -461,6 +613,16 @@ export const SlackBlock: BlockConfig = { name: { type: 'string', description: 'Emoji name' }, threadTs: { type: 'string', description: 'Thread timestamp' }, thread_ts: { type: 'string', description: 'Thread timestamp for reply' }, + // List Channels inputs + includePrivate: { type: 'string', description: 'Include private channels (true/false)' }, + channelLimit: { type: 'string', description: 'Maximum number of channels to return' }, + // List Members inputs + memberLimit: { type: 'string', description: 'Maximum number of members to return' }, + // List Users inputs + includeDeleted: { type: 'string', description: 'Include deactivated users (true/false)' }, + userLimit: { type: 'string', description: 'Maximum number of users to return' }, + // Get User inputs + userId: { type: 'string', description: 'User ID to look up' }, }, outputs: { // slack_message outputs (send operation) @@ -488,6 +650,37 @@ export const SlackBlock: BlockConfig = { 'Array of message objects with comprehensive properties: text, user, timestamp, reactions, threads, files, attachments, blocks, stars, pins, and edit history', }, + // slack_list_channels outputs (list_channels operation) + channels: { + type: 'json', + description: + 'Array of channel objects with properties: id, name, is_private, is_archived, is_member, num_members, topic, purpose, created, creator', + }, + count: { + type: 'number', + description: 'Total number of items returned (channels, members, or users)', + }, + + // slack_list_members outputs (list_members operation) + members: { + type: 'json', + description: 'Array of user IDs who are members of the channel', + }, + + // slack_list_users outputs (list_users operation) + users: { + type: 'json', + description: + 'Array of user objects with properties: id, name, real_name, display_name, is_bot, is_admin, deleted, timezone, avatar, status_text, status_emoji', + }, + + // slack_get_user outputs (get_user operation) + user: { + type: 'json', + description: + 'Detailed user object with properties: id, name, real_name, display_name, first_name, last_name, title, is_bot, is_admin, deleted, timezone, avatars, status', + }, + // slack_download outputs file: { type: 'json', diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 5a0cb06b94d..34529446edf 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -987,6 +987,10 @@ import { slackCanvasTool, slackDeleteMessageTool, slackDownloadTool, + slackGetUserTool, + slackListChannelsTool, + slackListMembersTool, + slackListUsersTool, slackMessageReaderTool, slackMessageTool, slackUpdateMessageTool, @@ -1397,6 +1401,10 @@ export const tools: Record = { polymarket_get_trades: polymarketGetTradesTool, slack_message: slackMessageTool, slack_message_reader: slackMessageReaderTool, + slack_list_channels: slackListChannelsTool, + slack_list_members: slackListMembersTool, + slack_list_users: slackListUsersTool, + slack_get_user: slackGetUserTool, slack_canvas: slackCanvasTool, slack_download: slackDownloadTool, slack_update_message: slackUpdateMessageTool, diff --git a/apps/sim/tools/slack/get_user.ts b/apps/sim/tools/slack/get_user.ts new file mode 100644 index 00000000000..0ce8eccc05b --- /dev/null +++ b/apps/sim/tools/slack/get_user.ts @@ -0,0 +1,155 @@ +import type { SlackGetUserParams, SlackGetUserResponse } from '@/tools/slack/types' +import type { ToolConfig } from '@/tools/types' + +export const slackGetUserTool: ToolConfig = { + id: 'slack_get_user', + name: 'Slack Get User Info', + description: 'Get detailed information about a specific Slack user by their user ID.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'slack', + }, + + params: { + authMethod: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Authentication method: oauth or bot_token', + }, + botToken: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Bot token for Custom Bot', + }, + accessToken: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'OAuth access token or bot token for Slack API', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'User ID to look up (e.g., U1234567890)', + }, + }, + + request: { + url: (params: SlackGetUserParams) => { + const url = new URL('https://slack.com/api/users.info') + url.searchParams.append('user', params.userId) + return url.toString() + }, + method: 'GET', + headers: (params: SlackGetUserParams) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken || params.botToken}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!data.ok) { + if (data.error === 'user_not_found') { + throw new Error('User not found. Please check the user ID and try again.') + } + if (data.error === 'missing_scope') { + throw new Error( + 'Missing required permissions. Please reconnect your Slack account with the necessary scopes (users:read).' + ) + } + if (data.error === 'invalid_auth') { + throw new Error('Invalid authentication. Please check your Slack credentials.') + } + throw new Error(data.error || 'Failed to get user info from Slack') + } + + const user = data.user + const profile = user.profile || {} + + return { + success: true, + output: { + user: { + id: user.id, + name: user.name, + real_name: user.real_name || profile.real_name || '', + display_name: profile.display_name || '', + first_name: profile.first_name || '', + last_name: profile.last_name || '', + title: profile.title || '', + phone: profile.phone || '', + skype: profile.skype || '', + is_bot: user.is_bot || false, + is_admin: user.is_admin || false, + is_owner: user.is_owner || false, + is_primary_owner: user.is_primary_owner || false, + is_restricted: user.is_restricted || false, + is_ultra_restricted: user.is_ultra_restricted || false, + deleted: user.deleted || false, + timezone: user.tz, + timezone_label: user.tz_label, + timezone_offset: user.tz_offset, + avatar_24: profile.image_24, + avatar_48: profile.image_48, + avatar_72: profile.image_72, + avatar_192: profile.image_192, + avatar_512: profile.image_512, + status_text: profile.status_text || '', + status_emoji: profile.status_emoji || '', + status_expiration: profile.status_expiration, + updated: user.updated, + }, + }, + } + }, + + outputs: { + user: { + type: 'object', + description: 'Detailed user information', + properties: { + id: { type: 'string', description: 'User ID' }, + name: { type: 'string', description: 'Username (handle)' }, + real_name: { type: 'string', description: 'Full real name' }, + display_name: { type: 'string', description: 'Display name shown in Slack' }, + first_name: { type: 'string', description: 'First name' }, + last_name: { type: 'string', description: 'Last name' }, + title: { type: 'string', description: 'Job title' }, + phone: { type: 'string', description: 'Phone number' }, + skype: { type: 'string', description: 'Skype handle' }, + is_bot: { type: 'boolean', description: 'Whether the user is a bot' }, + is_admin: { type: 'boolean', description: 'Whether the user is a workspace admin' }, + is_owner: { type: 'boolean', description: 'Whether the user is the workspace owner' }, + is_primary_owner: { type: 'boolean', description: 'Whether the user is the primary owner' }, + is_restricted: { type: 'boolean', description: 'Whether the user is a guest (restricted)' }, + is_ultra_restricted: { + type: 'boolean', + description: 'Whether the user is a single-channel guest', + }, + deleted: { type: 'boolean', description: 'Whether the user is deactivated' }, + timezone: { + type: 'string', + description: 'Timezone identifier (e.g., America/Los_Angeles)', + }, + timezone_label: { type: 'string', description: 'Human-readable timezone label' }, + timezone_offset: { type: 'number', description: 'Timezone offset in seconds from UTC' }, + avatar_24: { type: 'string', description: 'URL to 24px avatar' }, + avatar_48: { type: 'string', description: 'URL to 48px avatar' }, + avatar_72: { type: 'string', description: 'URL to 72px avatar' }, + avatar_192: { type: 'string', description: 'URL to 192px avatar' }, + avatar_512: { type: 'string', description: 'URL to 512px avatar' }, + status_text: { type: 'string', description: 'Custom status text' }, + status_emoji: { type: 'string', description: 'Custom status emoji' }, + status_expiration: { type: 'number', description: 'Unix timestamp when status expires' }, + updated: { type: 'number', description: 'Unix timestamp of last profile update' }, + }, + }, + }, +} diff --git a/apps/sim/tools/slack/index.ts b/apps/sim/tools/slack/index.ts index 479520fbc8d..ea99459cd37 100644 --- a/apps/sim/tools/slack/index.ts +++ b/apps/sim/tools/slack/index.ts @@ -2,6 +2,10 @@ import { slackAddReactionTool } from '@/tools/slack/add_reaction' import { slackCanvasTool } from '@/tools/slack/canvas' import { slackDeleteMessageTool } from '@/tools/slack/delete_message' import { slackDownloadTool } from '@/tools/slack/download' +import { slackGetUserTool } from '@/tools/slack/get_user' +import { slackListChannelsTool } from '@/tools/slack/list_channels' +import { slackListMembersTool } from '@/tools/slack/list_members' +import { slackListUsersTool } from '@/tools/slack/list_users' import { slackMessageTool } from '@/tools/slack/message' import { slackMessageReaderTool } from '@/tools/slack/message_reader' import { slackUpdateMessageTool } from '@/tools/slack/update_message' @@ -14,4 +18,8 @@ export { slackUpdateMessageTool, slackDeleteMessageTool, slackAddReactionTool, + slackListChannelsTool, + slackListMembersTool, + slackListUsersTool, + slackGetUserTool, } diff --git a/apps/sim/tools/slack/list_channels.ts b/apps/sim/tools/slack/list_channels.ts new file mode 100644 index 00000000000..9bdbab8c104 --- /dev/null +++ b/apps/sim/tools/slack/list_channels.ts @@ -0,0 +1,150 @@ +import type { SlackListChannelsParams, SlackListChannelsResponse } from '@/tools/slack/types' +import type { ToolConfig } from '@/tools/types' + +export const slackListChannelsTool: ToolConfig = + { + id: 'slack_list_channels', + name: 'Slack List Channels', + description: + 'List all channels in a Slack workspace. Returns public and private channels the bot has access to.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'slack', + }, + + params: { + authMethod: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Authentication method: oauth or bot_token', + }, + botToken: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Bot token for Custom Bot', + }, + accessToken: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'OAuth access token or bot token for Slack API', + }, + includePrivate: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Include private channels the bot is a member of (default: true)', + }, + excludeArchived: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Exclude archived channels (default: true)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of channels to return (default: 100, max: 200)', + }, + }, + + request: { + url: (params: SlackListChannelsParams) => { + const url = new URL('https://slack.com/api/conversations.list') + + // Determine channel types to include + const includePrivate = params.includePrivate !== false + if (includePrivate) { + url.searchParams.append('types', 'public_channel,private_channel') + } else { + url.searchParams.append('types', 'public_channel') + } + + // Exclude archived by default + const excludeArchived = params.excludeArchived !== false + url.searchParams.append('exclude_archived', String(excludeArchived)) + + // Set limit (default 100, max 200) + const limit = params.limit ? Math.min(Number(params.limit), 200) : 100 + url.searchParams.append('limit', String(limit)) + + return url.toString() + }, + method: 'GET', + headers: (params: SlackListChannelsParams) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken || params.botToken}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!data.ok) { + if (data.error === 'missing_scope') { + throw new Error( + 'Missing required permissions. Please reconnect your Slack account with the necessary scopes (channels:read, groups:read).' + ) + } + if (data.error === 'invalid_auth') { + throw new Error('Invalid authentication. Please check your Slack credentials.') + } + throw new Error(data.error || 'Failed to list channels from Slack') + } + + const channels = (data.channels || []).map((channel: any) => ({ + id: channel.id, + name: channel.name, + is_private: channel.is_private || false, + is_archived: channel.is_archived || false, + is_member: channel.is_member || false, + num_members: channel.num_members, + topic: channel.topic?.value || '', + purpose: channel.purpose?.value || '', + created: channel.created, + creator: channel.creator, + })) + + return { + success: true, + output: { + channels, + count: channels.length, + }, + } + }, + + outputs: { + channels: { + type: 'array', + description: 'Array of channel objects from the workspace', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Channel ID (e.g., C1234567890)' }, + name: { type: 'string', description: 'Channel name without # prefix' }, + is_private: { type: 'boolean', description: 'Whether the channel is private' }, + is_archived: { type: 'boolean', description: 'Whether the channel is archived' }, + is_member: { + type: 'boolean', + description: 'Whether the bot is a member of the channel', + }, + num_members: { type: 'number', description: 'Number of members in the channel' }, + topic: { type: 'string', description: 'Channel topic' }, + purpose: { type: 'string', description: 'Channel purpose/description' }, + created: { type: 'number', description: 'Unix timestamp when channel was created' }, + creator: { type: 'string', description: 'User ID of channel creator' }, + }, + }, + }, + count: { + type: 'number', + description: 'Total number of channels returned', + }, + }, + } diff --git a/apps/sim/tools/slack/list_members.ts b/apps/sim/tools/slack/list_members.ts new file mode 100644 index 00000000000..8295776e58a --- /dev/null +++ b/apps/sim/tools/slack/list_members.ts @@ -0,0 +1,109 @@ +import type { SlackListMembersParams, SlackListMembersResponse } from '@/tools/slack/types' +import type { ToolConfig } from '@/tools/types' + +export const slackListMembersTool: ToolConfig = { + id: 'slack_list_members', + name: 'Slack List Channel Members', + description: + 'List all members (user IDs) in a Slack channel. Use with Get User Info to resolve IDs to names.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'slack', + }, + + params: { + authMethod: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Authentication method: oauth or bot_token', + }, + botToken: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Bot token for Custom Bot', + }, + accessToken: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'OAuth access token or bot token for Slack API', + }, + channel: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Channel ID to list members from', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of members to return (default: 100, max: 200)', + }, + }, + + request: { + url: (params: SlackListMembersParams) => { + const url = new URL('https://slack.com/api/conversations.members') + url.searchParams.append('channel', params.channel) + + // Set limit (default 100, max 200) + const limit = params.limit ? Math.min(Number(params.limit), 200) : 100 + url.searchParams.append('limit', String(limit)) + + return url.toString() + }, + method: 'GET', + headers: (params: SlackListMembersParams) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken || params.botToken}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!data.ok) { + if (data.error === 'channel_not_found') { + throw new Error('Channel not found. Please check the channel ID and try again.') + } + if (data.error === 'missing_scope') { + throw new Error( + 'Missing required permissions. Please reconnect your Slack account with the necessary scopes (channels:read, groups:read).' + ) + } + if (data.error === 'invalid_auth') { + throw new Error('Invalid authentication. Please check your Slack credentials.') + } + throw new Error(data.error || 'Failed to list channel members from Slack') + } + + const members = data.members || [] + + return { + success: true, + output: { + members, + count: members.length, + }, + } + }, + + outputs: { + members: { + type: 'array', + description: 'Array of user IDs who are members of the channel (e.g., U1234567890)', + items: { + type: 'string', + }, + }, + count: { + type: 'number', + description: 'Total number of members returned', + }, + }, +} diff --git a/apps/sim/tools/slack/list_users.ts b/apps/sim/tools/slack/list_users.ts new file mode 100644 index 00000000000..2d0e43bd23b --- /dev/null +++ b/apps/sim/tools/slack/list_users.ts @@ -0,0 +1,141 @@ +import type { SlackListUsersParams, SlackListUsersResponse } from '@/tools/slack/types' +import type { ToolConfig } from '@/tools/types' + +export const slackListUsersTool: ToolConfig = { + id: 'slack_list_users', + name: 'Slack List Users', + description: 'List all users in a Slack workspace. Returns user profiles with names and avatars.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'slack', + }, + + params: { + authMethod: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Authentication method: oauth or bot_token', + }, + botToken: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Bot token for Custom Bot', + }, + accessToken: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'OAuth access token or bot token for Slack API', + }, + includeDeleted: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Include deactivated/deleted users (default: false)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of users to return (default: 100, max: 200)', + }, + }, + + request: { + url: (params: SlackListUsersParams) => { + const url = new URL('https://slack.com/api/users.list') + + // Set limit (default 100, max 200) + const limit = params.limit ? Math.min(Number(params.limit), 200) : 100 + url.searchParams.append('limit', String(limit)) + + return url.toString() + }, + method: 'GET', + headers: (params: SlackListUsersParams) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken || params.botToken}`, + }), + }, + + transformResponse: async (response: Response, params?: SlackListUsersParams) => { + const data = await response.json() + + if (!data.ok) { + if (data.error === 'missing_scope') { + throw new Error( + 'Missing required permissions. Please reconnect your Slack account with the necessary scopes (users:read).' + ) + } + if (data.error === 'invalid_auth') { + throw new Error('Invalid authentication. Please check your Slack credentials.') + } + throw new Error(data.error || 'Failed to list users from Slack') + } + + const includeDeleted = params?.includeDeleted === true + + const users = (data.members || []) + .filter((user: any) => { + // Always filter out Slackbot + if (user.id === 'USLACKBOT') return false + // Filter deleted users unless includeDeleted is true + if (!includeDeleted && user.deleted) return false + return true + }) + .map((user: any) => ({ + id: user.id, + name: user.name, + real_name: user.real_name || user.profile?.real_name || '', + display_name: user.profile?.display_name || '', + is_bot: user.is_bot || false, + is_admin: user.is_admin || false, + is_owner: user.is_owner || false, + deleted: user.deleted || false, + timezone: user.tz, + avatar: user.profile?.image_72 || user.profile?.image_48 || '', + status_text: user.profile?.status_text || '', + status_emoji: user.profile?.status_emoji || '', + })) + + return { + success: true, + output: { + users, + count: users.length, + }, + } + }, + + outputs: { + users: { + type: 'array', + description: 'Array of user objects from the workspace', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'User ID (e.g., U1234567890)' }, + name: { type: 'string', description: 'Username (handle)' }, + real_name: { type: 'string', description: 'Full real name' }, + display_name: { type: 'string', description: 'Display name shown in Slack' }, + is_bot: { type: 'boolean', description: 'Whether the user is a bot' }, + is_admin: { type: 'boolean', description: 'Whether the user is a workspace admin' }, + is_owner: { type: 'boolean', description: 'Whether the user is the workspace owner' }, + deleted: { type: 'boolean', description: 'Whether the user is deactivated' }, + timezone: { type: 'string', description: 'User timezone identifier' }, + avatar: { type: 'string', description: 'URL to user avatar image' }, + status_text: { type: 'string', description: 'Custom status text' }, + status_emoji: { type: 'string', description: 'Custom status emoji' }, + }, + }, + }, + count: { + type: 'number', + description: 'Total number of users returned', + }, + }, +} diff --git a/apps/sim/tools/slack/types.ts b/apps/sim/tools/slack/types.ts index 4737f496398..38d3bc7ffeb 100644 --- a/apps/sim/tools/slack/types.ts +++ b/apps/sim/tools/slack/types.ts @@ -49,6 +49,26 @@ export interface SlackAddReactionParams extends SlackBaseParams { name: string } +export interface SlackListChannelsParams extends SlackBaseParams { + includePrivate?: boolean + excludeArchived?: boolean + limit?: number +} + +export interface SlackListMembersParams extends SlackBaseParams { + channel: string + limit?: number +} + +export interface SlackListUsersParams extends SlackBaseParams { + includeDeleted?: boolean + limit?: number +} + +export interface SlackGetUserParams extends SlackBaseParams { + userId: string +} + export interface SlackMessageResponse extends ToolResponse { output: { // Legacy properties for backward compatibility @@ -207,6 +227,78 @@ export interface SlackAddReactionResponse extends ToolResponse { } } +export interface SlackChannel { + id: string + name: string + is_private: boolean + is_archived: boolean + is_member: boolean + num_members?: number + topic?: string + purpose?: string + created?: number + creator?: string +} + +export interface SlackListChannelsResponse extends ToolResponse { + output: { + channels: SlackChannel[] + count: number + } +} + +export interface SlackListMembersResponse extends ToolResponse { + output: { + members: string[] + count: number + } +} + +export interface SlackUser { + id: string + name: string + real_name: string + display_name: string + first_name?: string + last_name?: string + title?: string + phone?: string + skype?: string + is_bot: boolean + is_admin: boolean + is_owner: boolean + is_primary_owner?: boolean + is_restricted?: boolean + is_ultra_restricted?: boolean + deleted: boolean + timezone?: string + timezone_label?: string + timezone_offset?: number + avatar?: string + avatar_24?: string + avatar_48?: string + avatar_72?: string + avatar_192?: string + avatar_512?: string + status_text?: string + status_emoji?: string + status_expiration?: number + updated?: number +} + +export interface SlackListUsersResponse extends ToolResponse { + output: { + users: SlackUser[] + count: number + } +} + +export interface SlackGetUserResponse extends ToolResponse { + output: { + user: SlackUser + } +} + export type SlackResponse = | SlackCanvasResponse | SlackMessageReaderResponse @@ -215,3 +307,7 @@ export type SlackResponse = | SlackUpdateMessageResponse | SlackDeleteMessageResponse | SlackAddReactionResponse + | SlackListChannelsResponse + | SlackListMembersResponse + | SlackListUsersResponse + | SlackGetUserResponse