From ced21d2f3c6b6d4e644561d6ae489d6e02955a2a Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 15 Dec 2025 14:37:40 -0800 Subject: [PATCH 01/13] feat(slack): tool to allow dms --- apps/sim/app/api/tools/slack/send-dm/route.ts | 85 ++++++ .../app/api/tools/slack/send-message/route.ts | 220 +------------- apps/sim/app/api/tools/slack/utils.ts | 277 ++++++++++++++++++ apps/sim/blocks/blocks/slack.ts | 56 +++- apps/sim/tools/registry.ts | 2 + apps/sim/tools/slack/dm.ts | 101 +++++++ apps/sim/tools/slack/index.ts | 2 + apps/sim/tools/slack/message.ts | 4 +- apps/sim/tools/slack/types.ts | 17 ++ 9 files changed, 547 insertions(+), 217 deletions(-) create mode 100644 apps/sim/app/api/tools/slack/send-dm/route.ts create mode 100644 apps/sim/app/api/tools/slack/utils.ts create mode 100644 apps/sim/tools/slack/dm.ts diff --git a/apps/sim/app/api/tools/slack/send-dm/route.ts b/apps/sim/app/api/tools/slack/send-dm/route.ts new file mode 100644 index 0000000000..5a6840f143 --- /dev/null +++ b/apps/sim/app/api/tools/slack/send-dm/route.ts @@ -0,0 +1,85 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkHybridAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { createLogger } from '@/lib/logs/console/logger' +import { openDMChannel, sendSlackMessage } from '../utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('SlackSendDMAPI') + +const SlackSendDMSchema = z.object({ + accessToken: z.string().min(1, 'Access token is required'), + userId: z.string().min(1, 'User ID is required'), + text: z.string().min(1, 'Message text is required'), + thread_ts: z.string().optional().nullable(), + files: z.array(z.any()).optional().nullable(), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) + + if (!authResult.success) { + logger.warn(`[${requestId}] Unauthorized Slack DM send attempt: ${authResult.error}`) + return NextResponse.json( + { + success: false, + error: authResult.error || 'Authentication required', + }, + { status: 401 } + ) + } + + logger.info(`[${requestId}] Authenticated Slack DM request via ${authResult.authType}`, { + userId: authResult.userId, + }) + + const body = await request.json() + const validatedData = SlackSendDMSchema.parse(body) + + logger.info(`[${requestId}] Sending Slack DM`, { + targetUserId: validatedData.userId, + hasFiles: !!(validatedData.files && validatedData.files.length > 0), + fileCount: validatedData.files?.length || 0, + }) + + // Open DM channel with the user + const dmChannelId = await openDMChannel( + validatedData.accessToken, + validatedData.userId, + requestId, + logger + ) + + const result = await sendSlackMessage( + { + accessToken: validatedData.accessToken, + channel: dmChannelId, + text: validatedData.text, + threadTs: validatedData.thread_ts, + files: validatedData.files, + }, + requestId, + logger + ) + + if (!result.success) { + return NextResponse.json({ success: false, error: result.error }, { status: 400 }) + } + + return NextResponse.json({ success: true, output: result.output }) + } catch (error) { + logger.error(`[${requestId}] Error sending Slack DM:`, error) + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Unknown error occurred', + }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/slack/send-message/route.ts b/apps/sim/app/api/tools/slack/send-message/route.ts index 9a82b6e5a7..28e3084dc3 100644 --- a/apps/sim/app/api/tools/slack/send-message/route.ts +++ b/apps/sim/app/api/tools/slack/send-message/route.ts @@ -3,8 +3,7 @@ import { z } from 'zod' import { checkHybridAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { createLogger } from '@/lib/logs/console/logger' -import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' -import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' +import { sendSlackMessage } from '../utils' export const dynamic = 'force-dynamic' @@ -48,216 +47,23 @@ export async function POST(request: NextRequest) { fileCount: validatedData.files?.length || 0, }) - if (!validatedData.files || validatedData.files.length === 0) { - logger.info(`[${requestId}] No files, using chat.postMessage`) - - const response = await fetch('https://slack.com/api/chat.postMessage', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${validatedData.accessToken}`, - }, - body: JSON.stringify({ - channel: validatedData.channel, - text: validatedData.text, - ...(validatedData.thread_ts && { thread_ts: validatedData.thread_ts }), - }), - }) - - const data = await response.json() - - if (!data.ok) { - logger.error(`[${requestId}] Slack API error:`, data.error) - return NextResponse.json( - { - success: false, - error: data.error || 'Failed to send message', - }, - { status: 400 } - ) - } - - logger.info(`[${requestId}] Message sent successfully`) - const messageObj = data.message || { - type: 'message', - ts: data.ts, - text: validatedData.text, - channel: data.channel, - } - return NextResponse.json({ - success: true, - output: { - message: messageObj, - ts: data.ts, - channel: data.channel, - }, - }) - } - - logger.info(`[${requestId}] Processing ${validatedData.files.length} file(s)`) - - const userFiles = processFilesToUserFiles(validatedData.files, requestId, logger) - - if (userFiles.length === 0) { - logger.warn(`[${requestId}] No valid files to upload`) - const response = await fetch('https://slack.com/api/chat.postMessage', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${validatedData.accessToken}`, - }, - body: JSON.stringify({ - channel: validatedData.channel, - text: validatedData.text, - ...(validatedData.thread_ts && { thread_ts: validatedData.thread_ts }), - }), - }) - - const data = await response.json() - const messageObj = data.message || { - type: 'message', - ts: data.ts, - text: validatedData.text, - channel: data.channel, - } - return NextResponse.json({ - success: true, - output: { - message: messageObj, - ts: data.ts, - channel: data.channel, - }, - }) - } - - const uploadedFileIds: string[] = [] - - for (const userFile of userFiles) { - logger.info(`[${requestId}] Uploading file: ${userFile.name}`) - - const buffer = await downloadFileFromStorage(userFile, requestId, logger) - - const getUrlResponse = await fetch('https://slack.com/api/files.getUploadURLExternal', { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - Authorization: `Bearer ${validatedData.accessToken}`, - }, - body: new URLSearchParams({ - filename: userFile.name, - length: buffer.length.toString(), - }), - }) - - const urlData = await getUrlResponse.json() - - if (!urlData.ok) { - logger.error(`[${requestId}] Failed to get upload URL:`, urlData.error) - continue - } - - logger.info(`[${requestId}] Got upload URL for ${userFile.name}, file_id: ${urlData.file_id}`) - - const uploadResponse = await fetch(urlData.upload_url, { - method: 'POST', - body: new Uint8Array(buffer), - }) - - if (!uploadResponse.ok) { - logger.error(`[${requestId}] Failed to upload file data: ${uploadResponse.status}`) - continue - } - - logger.info(`[${requestId}] File data uploaded successfully`) - uploadedFileIds.push(urlData.file_id) - } - - if (uploadedFileIds.length === 0) { - logger.warn(`[${requestId}] No files uploaded successfully, sending text-only message`) - const response = await fetch('https://slack.com/api/chat.postMessage', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${validatedData.accessToken}`, - }, - body: JSON.stringify({ - channel: validatedData.channel, - text: validatedData.text, - ...(validatedData.thread_ts && { thread_ts: validatedData.thread_ts }), - }), - }) - - const data = await response.json() - const messageObj = data.message || { - type: 'message', - ts: data.ts, + const result = await sendSlackMessage( + { + accessToken: validatedData.accessToken, + channel: validatedData.channel, text: validatedData.text, - channel: data.channel, - } - return NextResponse.json({ - success: true, - output: { - message: messageObj, - ts: data.ts, - channel: data.channel, - }, - }) - } - - const completeResponse = await fetch('https://slack.com/api/files.completeUploadExternal', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${validatedData.accessToken}`, + threadTs: validatedData.thread_ts, + files: validatedData.files, }, - body: JSON.stringify({ - files: uploadedFileIds.map((id) => ({ id })), - channel_id: validatedData.channel, - initial_comment: validatedData.text, - }), - }) - - const completeData = await completeResponse.json() - - if (!completeData.ok) { - logger.error(`[${requestId}] Failed to complete upload:`, completeData.error) - return NextResponse.json( - { - success: false, - error: completeData.error || 'Failed to complete file upload', - }, - { status: 400 } - ) - } - - logger.info(`[${requestId}] Files uploaded and shared successfully`) + requestId, + logger + ) - // For file uploads, construct a message object - const fileTs = completeData.files?.[0]?.created?.toString() || (Date.now() / 1000).toString() - const fileMessage = { - type: 'message', - ts: fileTs, - text: validatedData.text, - channel: validatedData.channel, - files: completeData.files?.map((file: any) => ({ - id: file?.id, - name: file?.name, - mimetype: file?.mimetype, - size: file?.size, - url_private: file?.url_private, - permalink: file?.permalink, - })), + if (!result.success) { + return NextResponse.json({ success: false, error: result.error }, { status: 400 }) } - return NextResponse.json({ - success: true, - output: { - message: fileMessage, - ts: fileTs, - channel: validatedData.channel, - fileCount: uploadedFileIds.length, - }, - }) + return NextResponse.json({ success: true, output: result.output }) } catch (error) { logger.error(`[${requestId}] Error sending Slack message:`, error) return NextResponse.json( diff --git a/apps/sim/app/api/tools/slack/utils.ts b/apps/sim/app/api/tools/slack/utils.ts new file mode 100644 index 0000000000..f65802a65f --- /dev/null +++ b/apps/sim/app/api/tools/slack/utils.ts @@ -0,0 +1,277 @@ +import type { Logger } from '@/lib/logs/console/logger' +import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' +import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' + +/** + * Sends a message to a Slack channel using chat.postMessage + */ +export async function postSlackMessage( + accessToken: string, + channel: string, + text: string, + threadTs?: string | null +): Promise<{ ok: boolean; ts?: string; channel?: string; message?: any; error?: string }> { + const response = await fetch('https://slack.com/api/chat.postMessage', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ + channel, + text, + ...(threadTs && { thread_ts: threadTs }), + }), + }) + + return response.json() +} + +/** + * Creates a default message object when the API doesn't return one + */ +export function createDefaultMessageObject( + ts: string, + text: string, + channel: string +): Record { + return { + type: 'message', + ts, + text, + channel, + } +} + +/** + * Formats the success response for a sent message + */ +export function formatMessageSuccessResponse( + data: any, + text: string +): { + message: any + ts: string + channel: string +} { + const messageObj = data.message || createDefaultMessageObject(data.ts, text, data.channel) + return { + message: messageObj, + ts: data.ts, + channel: data.channel, + } +} + +/** + * Uploads files to Slack and returns the uploaded file IDs + */ +export async function uploadFilesToSlack( + files: any[], + accessToken: string, + requestId: string, + logger: Logger +): Promise { + const userFiles = processFilesToUserFiles(files, requestId, logger) + const uploadedFileIds: string[] = [] + + for (const userFile of userFiles) { + logger.info(`[${requestId}] Uploading file: ${userFile.name}`) + + const buffer = await downloadFileFromStorage(userFile, requestId, logger) + + const getUrlResponse = await fetch('https://slack.com/api/files.getUploadURLExternal', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: `Bearer ${accessToken}`, + }, + body: new URLSearchParams({ + filename: userFile.name, + length: buffer.length.toString(), + }), + }) + + const urlData = await getUrlResponse.json() + + if (!urlData.ok) { + logger.error(`[${requestId}] Failed to get upload URL:`, urlData.error) + continue + } + + logger.info(`[${requestId}] Got upload URL for ${userFile.name}, file_id: ${urlData.file_id}`) + + const uploadResponse = await fetch(urlData.upload_url, { + method: 'POST', + body: new Uint8Array(buffer), + }) + + if (!uploadResponse.ok) { + logger.error(`[${requestId}] Failed to upload file data: ${uploadResponse.status}`) + continue + } + + logger.info(`[${requestId}] File data uploaded successfully`) + uploadedFileIds.push(urlData.file_id) + } + + return uploadedFileIds +} + +/** + * Completes the file upload process by associating files with a channel + */ +export async function completeSlackFileUpload( + uploadedFileIds: string[], + channel: string, + text: string, + accessToken: string +): Promise<{ ok: boolean; files?: any[]; error?: string }> { + const response = await fetch('https://slack.com/api/files.completeUploadExternal', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ + files: uploadedFileIds.map((id) => ({ id })), + channel_id: channel, + initial_comment: text, + }), + }) + + return response.json() +} + +/** + * Creates a message object for file uploads + */ +export function createFileMessageObject( + text: string, + channel: string, + files: any[] +): Record { + const fileTs = files?.[0]?.created?.toString() || (Date.now() / 1000).toString() + return { + type: 'message', + ts: fileTs, + text, + channel, + files: files?.map((file: any) => ({ + id: file?.id, + name: file?.name, + mimetype: file?.mimetype, + size: file?.size, + url_private: file?.url_private, + permalink: file?.permalink, + })), + } +} + +/** + * Opens a DM channel with a user and returns the channel ID + */ +export async function openDMChannel( + accessToken: string, + userId: string, + requestId: string, + logger: Logger +): Promise { + const response = await fetch('https://slack.com/api/conversations.open', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ + users: userId, + }), + }) + + const data = await response.json() + + if (!data.ok) { + logger.error(`[${requestId}] Failed to open DM channel:`, data.error) + throw new Error(data.error || 'Failed to open DM channel with user') + } + + logger.info(`[${requestId}] Opened DM channel: ${data.channel.id}`) + return data.channel.id +} + +export interface SlackMessageParams { + accessToken: string + channel: string + text: string + threadTs?: string | null + files?: any[] | null +} + +/** + * Sends a Slack message with optional file attachments + * This is the main function that handles the complete flow of sending messages + */ +export async function sendSlackMessage( + params: SlackMessageParams, + requestId: string, + logger: Logger +): Promise<{ + success: boolean + output?: { message: any; ts: string; channel: string; fileCount?: number } + error?: string +}> { + const { accessToken, channel, text, threadTs, files } = params + + // No files - simple message + if (!files || files.length === 0) { + logger.info(`[${requestId}] No files, using chat.postMessage`) + + const data = await postSlackMessage(accessToken, channel, text, threadTs) + + if (!data.ok) { + logger.error(`[${requestId}] Slack API error:`, data.error) + return { success: false, error: data.error || 'Failed to send message' } + } + + logger.info(`[${requestId}] Message sent successfully`) + return { success: true, output: formatMessageSuccessResponse(data, text) } + } + + // Process files + logger.info(`[${requestId}] Processing ${files.length} file(s)`) + const uploadedFileIds = await uploadFilesToSlack(files, accessToken, requestId, logger) + + // No valid files uploaded - send text-only + if (uploadedFileIds.length === 0) { + logger.warn(`[${requestId}] No valid files to upload, sending text-only message`) + + const data = await postSlackMessage(accessToken, channel, text, threadTs) + + if (!data.ok) { + return { success: false, error: data.error || 'Failed to send message' } + } + + return { success: true, output: formatMessageSuccessResponse(data, text) } + } + + // Complete file upload + const completeData = await completeSlackFileUpload(uploadedFileIds, channel, text, accessToken) + + if (!completeData.ok) { + logger.error(`[${requestId}] Failed to complete upload:`, completeData.error) + return { success: false, error: completeData.error || 'Failed to complete file upload' } + } + + logger.info(`[${requestId}] Files uploaded and shared successfully`) + + const fileMessage = createFileMessageObject(text, channel, completeData.files || []) + + return { + success: true, + output: { + message: fileMessage, + ts: fileMessage.ts, + channel, + fileCount: uploadedFileIds.length, + }, + } +} diff --git a/apps/sim/blocks/blocks/slack.ts b/apps/sim/blocks/blocks/slack.ts index 88434484e6..7ae7037c05 100644 --- a/apps/sim/blocks/blocks/slack.ts +++ b/apps/sim/blocks/blocks/slack.ts @@ -23,7 +23,8 @@ export const SlackBlock: BlockConfig = { title: 'Operation', type: 'dropdown', options: [ - { label: 'Send Message', id: 'send' }, + { label: 'Send Channel Message', id: 'send' }, + { label: 'Send DM', id: 'dm' }, { label: 'Create Canvas', id: 'canvas' }, { label: 'Read Messages', id: 'read' }, { label: 'List Channels', id: 'list_channels' }, @@ -60,6 +61,7 @@ export const SlackBlock: BlockConfig = { 'groups:history', 'chat:write', 'chat:write.public', + 'im:write', 'users:read', 'files:write', 'files:read', @@ -96,7 +98,7 @@ export const SlackBlock: BlockConfig = { dependsOn: { all: ['authMethod'], any: ['credential', 'botToken'] }, condition: { field: 'operation', - value: ['list_channels', 'list_users', 'get_user'], + value: ['list_channels', 'list_users', 'get_user', 'dm'], not: true, }, }, @@ -110,10 +112,22 @@ export const SlackBlock: BlockConfig = { mode: 'advanced', condition: { field: 'operation', - value: ['list_channels', 'list_users', 'get_user'], + value: ['list_channels', 'list_users', 'get_user', 'dm'], not: true, }, }, + // DM recipient user ID + { + id: 'dmUserId', + title: 'User ID', + type: 'short-input', + placeholder: 'Enter Slack user ID to DM (e.g., U1234567890)', + condition: { + field: 'operation', + value: 'dm', + }, + required: true, + }, { id: 'text', title: 'Message', @@ -121,7 +135,7 @@ export const SlackBlock: BlockConfig = { placeholder: 'Enter your message (supports Slack mrkdwn)', condition: { field: 'operation', - value: 'send', + value: ['send', 'dm'], }, required: true, }, @@ -133,7 +147,7 @@ export const SlackBlock: BlockConfig = { placeholder: 'Reply to thread (e.g., 1405894322.002768)', condition: { field: 'operation', - value: 'send', + value: ['send', 'dm'], }, required: false, }, @@ -144,7 +158,7 @@ export const SlackBlock: BlockConfig = { type: 'file-upload', canonicalParamId: 'files', placeholder: 'Upload files to attach', - condition: { field: 'operation', value: 'send' }, + condition: { field: 'operation', value: ['send', 'dm'] }, mode: 'basic', multiple: true, required: false, @@ -156,7 +170,7 @@ export const SlackBlock: BlockConfig = { type: 'short-input', canonicalParamId: 'files', placeholder: 'Reference files from previous blocks', - condition: { field: 'operation', value: 'send' }, + condition: { field: 'operation', value: ['send', 'dm'] }, mode: 'advanced', required: false, }, @@ -370,6 +384,7 @@ export const SlackBlock: BlockConfig = { tools: { access: [ 'slack_message', + 'slack_dm', 'slack_canvas', 'slack_message_reader', 'slack_list_channels', @@ -386,6 +401,8 @@ export const SlackBlock: BlockConfig = { switch (params.operation) { case 'send': return 'slack_message' + case 'dm': + return 'slack_dm' case 'canvas': return 'slack_canvas' case 'read': @@ -418,6 +435,7 @@ export const SlackBlock: BlockConfig = { operation, channel, manualChannel, + dmUserId, text, title, content, @@ -444,7 +462,7 @@ export const SlackBlock: BlockConfig = { const effectiveChannel = (channel || manualChannel || '').trim() // Operations that don't require a channel - const noChannelOperations = ['list_channels', 'list_users', 'get_user'] + const noChannelOperations = ['list_channels', 'list_users', 'get_user', 'dm'] // Channel is required for most operations if (!effectiveChannel && !noChannelOperations.includes(operation)) { @@ -491,6 +509,27 @@ export const SlackBlock: BlockConfig = { break } + case 'dm': { + if (!dmUserId || dmUserId.trim() === '') { + throw new Error('User ID is required for DM operation') + } + if (!text || text.trim() === '') { + throw new Error('Message text is required for DM operation') + } + baseParams.userId = dmUserId + baseParams.text = text + // Add thread_ts if provided + if (threadTs) { + baseParams.thread_ts = threadTs + } + // Add files if provided + const dmFileParam = attachmentFiles || files + if (dmFileParam) { + baseParams.files = dmFileParam + } + break + } + case 'canvas': if (!title || !content) { throw new Error('Title and content are required for canvas operation') @@ -596,6 +635,7 @@ export const SlackBlock: BlockConfig = { botToken: { type: 'string', description: 'Bot token' }, channel: { type: 'string', description: 'Channel identifier' }, manualChannel: { type: 'string', description: 'Manual channel identifier' }, + dmUserId: { type: 'string', description: 'User ID for DM recipient' }, text: { type: 'string', description: 'Message text' }, attachmentFiles: { type: 'json', description: 'Files to attach (UI upload)' }, files: { type: 'array', description: 'Files to attach (UserFile array)' }, diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index ed28b15e1b..1c539462a6 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1003,6 +1003,7 @@ import { slackAddReactionTool, slackCanvasTool, slackDeleteMessageTool, + slackDMTool, slackDownloadTool, slackGetUserTool, slackListChannelsTool, @@ -1507,6 +1508,7 @@ export const tools: Record = { polymarket_get_positions: polymarketGetPositionsTool, polymarket_get_trades: polymarketGetTradesTool, slack_message: slackMessageTool, + slack_dm: slackDMTool, slack_message_reader: slackMessageReaderTool, slack_list_channels: slackListChannelsTool, slack_list_members: slackListMembersTool, diff --git a/apps/sim/tools/slack/dm.ts b/apps/sim/tools/slack/dm.ts new file mode 100644 index 0000000000..f1bd3929ea --- /dev/null +++ b/apps/sim/tools/slack/dm.ts @@ -0,0 +1,101 @@ +import type { SlackDMParams, SlackDMResponse } from '@/tools/slack/types' +import type { ToolConfig } from '@/tools/types' + +export const slackDMTool: ToolConfig = { + id: 'slack_dm', + name: 'Slack DM', + description: + 'Send direct messages to Slack users through the Slack API. Supports Slack mrkdwn formatting.', + 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-only', + description: 'Target Slack user ID to send the direct message to', + }, + text: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Message text to send (supports Slack mrkdwn formatting)', + }, + thread_ts: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Thread timestamp to reply to (creates thread reply)', + }, + files: { + type: 'file[]', + required: false, + visibility: 'user-only', + description: 'Files to attach to the message', + }, + }, + + request: { + url: '/api/tools/slack/send-dm', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params: SlackDMParams) => { + return { + accessToken: params.accessToken || params.botToken, + userId: params.userId, + text: params.text, + thread_ts: params.thread_ts || undefined, + files: params.files || null, + } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!data.success) { + throw new Error(data.error || 'Failed to send Slack DM') + } + return { + success: true, + output: data.output, + } + }, + + outputs: { + message: { + type: 'object', + description: 'Complete message object with all properties returned by Slack', + }, + ts: { type: 'string', description: 'Message timestamp' }, + channel: { type: 'string', description: 'DM channel ID where message was sent' }, + fileCount: { + type: 'number', + description: 'Number of files uploaded (when files are attached)', + }, + }, +} diff --git a/apps/sim/tools/slack/index.ts b/apps/sim/tools/slack/index.ts index ea99459cd3..727dd5a486 100644 --- a/apps/sim/tools/slack/index.ts +++ b/apps/sim/tools/slack/index.ts @@ -1,6 +1,7 @@ import { slackAddReactionTool } from '@/tools/slack/add_reaction' import { slackCanvasTool } from '@/tools/slack/canvas' import { slackDeleteMessageTool } from '@/tools/slack/delete_message' +import { slackDMTool } from '@/tools/slack/dm' import { slackDownloadTool } from '@/tools/slack/download' import { slackGetUserTool } from '@/tools/slack/get_user' import { slackListChannelsTool } from '@/tools/slack/list_channels' @@ -12,6 +13,7 @@ import { slackUpdateMessageTool } from '@/tools/slack/update_message' export { slackMessageTool, + slackDMTool, slackCanvasTool, slackMessageReaderTool, slackDownloadTool, diff --git a/apps/sim/tools/slack/message.ts b/apps/sim/tools/slack/message.ts index 55d92bd874..2b747a8f8c 100644 --- a/apps/sim/tools/slack/message.ts +++ b/apps/sim/tools/slack/message.ts @@ -3,9 +3,9 @@ import type { ToolConfig } from '@/tools/types' export const slackMessageTool: ToolConfig = { id: 'slack_message', - name: 'Slack Message', + name: 'Slack Channel Message', description: - 'Send messages to Slack channels or users through the Slack API. Supports Slack mrkdwn formatting.', + 'Send messages to Slack channels through the Slack API. Supports Slack mrkdwn formatting.', version: '1.0.0', oauth: { diff --git a/apps/sim/tools/slack/types.ts b/apps/sim/tools/slack/types.ts index 38d3bc7ffe..16ec1e7199 100644 --- a/apps/sim/tools/slack/types.ts +++ b/apps/sim/tools/slack/types.ts @@ -69,6 +69,13 @@ export interface SlackGetUserParams extends SlackBaseParams { userId: string } +export interface SlackDMParams extends SlackBaseParams { + userId: string + text: string + thread_ts?: string + files?: any[] +} + export interface SlackMessageResponse extends ToolResponse { output: { // Legacy properties for backward compatibility @@ -299,6 +306,15 @@ export interface SlackGetUserResponse extends ToolResponse { } } +export interface SlackDMResponse extends ToolResponse { + output: { + message: SlackMessage + ts: string + channel: string + fileCount?: number + } +} + export type SlackResponse = | SlackCanvasResponse | SlackMessageReaderResponse @@ -311,3 +327,4 @@ export type SlackResponse = | SlackListMembersResponse | SlackListUsersResponse | SlackGetUserResponse + | SlackDMResponse From f4e48f74b8188e52dff9224726504926546a3b31 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 15 Dec 2025 15:04:38 -0800 Subject: [PATCH 02/13] don't make new tool but separate out destination --- .../app/api/tools/slack/add-reaction/route.ts | 37 +++- .../api/tools/slack/delete-message/route.ts | 31 ++- .../api/tools/slack/read-messages/route.ts | 207 ++++++++++++++++++ apps/sim/app/api/tools/slack/send-dm/route.ts | 85 ------- .../app/api/tools/slack/send-message/route.ts | 23 +- .../api/tools/slack/update-message/route.ts | 33 ++- apps/sim/app/api/tools/slack/users/route.ts | 113 ++++++++++ apps/sim/app/api/tools/slack/utils.ts | 17 +- .../components/sub-block/components/index.ts | 1 + .../user-selector/user-selector-input.tsx | 123 +++++++++++ .../editor/components/sub-block/sub-block.tsx | 12 + apps/sim/blocks/blocks/slack.ts | 116 +++++----- apps/sim/blocks/types.ts | 2 + apps/sim/hooks/selectors/registry.ts | 25 +++ apps/sim/hooks/selectors/resolution.ts | 17 ++ apps/sim/hooks/selectors/types.ts | 1 + apps/sim/tools/registry.ts | 2 - apps/sim/tools/slack/add_reaction.ts | 9 +- apps/sim/tools/slack/delete_message.ts | 9 +- apps/sim/tools/slack/dm.ts | 101 --------- apps/sim/tools/slack/index.ts | 2 - apps/sim/tools/slack/message.ts | 13 +- apps/sim/tools/slack/message_reader.ts | 113 ++-------- apps/sim/tools/slack/types.ts | 32 +-- apps/sim/tools/slack/update_message.ts | 9 +- 25 files changed, 737 insertions(+), 396 deletions(-) create mode 100644 apps/sim/app/api/tools/slack/read-messages/route.ts delete mode 100644 apps/sim/app/api/tools/slack/send-dm/route.ts create mode 100644 apps/sim/app/api/tools/slack/users/route.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/user-selector/user-selector-input.tsx delete mode 100644 apps/sim/tools/slack/dm.ts diff --git a/apps/sim/app/api/tools/slack/add-reaction/route.ts b/apps/sim/app/api/tools/slack/add-reaction/route.ts index f6fba4a906..f386974949 100644 --- a/apps/sim/app/api/tools/slack/add-reaction/route.ts +++ b/apps/sim/app/api/tools/slack/add-reaction/route.ts @@ -3,17 +3,23 @@ import { z } from 'zod' import { checkHybridAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { createLogger } from '@/lib/logs/console/logger' +import { openDMChannel } from '../utils' export const dynamic = 'force-dynamic' const logger = createLogger('SlackAddReactionAPI') -const SlackAddReactionSchema = z.object({ - accessToken: z.string().min(1, 'Access token is required'), - channel: z.string().min(1, 'Channel ID is required'), - timestamp: z.string().min(1, 'Message timestamp is required'), - name: z.string().min(1, 'Emoji name is required'), -}) +const SlackAddReactionSchema = z + .object({ + accessToken: z.string().min(1, 'Access token is required'), + channel: z.string().optional(), + userId: z.string().optional(), + timestamp: z.string().min(1, 'Message timestamp is required'), + name: z.string().min(1, 'Emoji name is required'), + }) + .refine((data) => data.channel || data.userId, { + message: 'Either channel or userId is required', + }) export async function POST(request: NextRequest) { const requestId = generateRequestId() @@ -42,8 +48,19 @@ export async function POST(request: NextRequest) { const body = await request.json() const validatedData = SlackAddReactionSchema.parse(body) + let channel = validatedData.channel + if (!channel && validatedData.userId) { + logger.info(`[${requestId}] Opening DM channel for user: ${validatedData.userId}`) + channel = await openDMChannel( + validatedData.accessToken, + validatedData.userId, + requestId, + logger + ) + } + logger.info(`[${requestId}] Adding Slack reaction`, { - channel: validatedData.channel, + channel, timestamp: validatedData.timestamp, emoji: validatedData.name, }) @@ -55,7 +72,7 @@ export async function POST(request: NextRequest) { Authorization: `Bearer ${validatedData.accessToken}`, }, body: JSON.stringify({ - channel: validatedData.channel, + channel, timestamp: validatedData.timestamp, name: validatedData.name, }), @@ -75,7 +92,7 @@ export async function POST(request: NextRequest) { } logger.info(`[${requestId}] Reaction added successfully`, { - channel: validatedData.channel, + channel, timestamp: validatedData.timestamp, reaction: validatedData.name, }) @@ -85,7 +102,7 @@ export async function POST(request: NextRequest) { output: { content: `Successfully added :${validatedData.name}: reaction`, metadata: { - channel: validatedData.channel, + channel, timestamp: validatedData.timestamp, reaction: validatedData.name, }, diff --git a/apps/sim/app/api/tools/slack/delete-message/route.ts b/apps/sim/app/api/tools/slack/delete-message/route.ts index 02116bec52..3bccb1bec4 100644 --- a/apps/sim/app/api/tools/slack/delete-message/route.ts +++ b/apps/sim/app/api/tools/slack/delete-message/route.ts @@ -3,16 +3,22 @@ import { z } from 'zod' import { checkHybridAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { createLogger } from '@/lib/logs/console/logger' +import { openDMChannel } from '../utils' export const dynamic = 'force-dynamic' const logger = createLogger('SlackDeleteMessageAPI') -const SlackDeleteMessageSchema = z.object({ - accessToken: z.string().min(1, 'Access token is required'), - channel: z.string().min(1, 'Channel ID is required'), - timestamp: z.string().min(1, 'Message timestamp is required'), -}) +const SlackDeleteMessageSchema = z + .object({ + accessToken: z.string().min(1, 'Access token is required'), + channel: z.string().optional(), + userId: z.string().optional(), + timestamp: z.string().min(1, 'Message timestamp is required'), + }) + .refine((data) => data.channel || data.userId, { + message: 'Either channel or userId is required', + }) export async function POST(request: NextRequest) { const requestId = generateRequestId() @@ -41,8 +47,19 @@ export async function POST(request: NextRequest) { const body = await request.json() const validatedData = SlackDeleteMessageSchema.parse(body) + let channel = validatedData.channel + if (!channel && validatedData.userId) { + logger.info(`[${requestId}] Opening DM channel for user: ${validatedData.userId}`) + channel = await openDMChannel( + validatedData.accessToken, + validatedData.userId, + requestId, + logger + ) + } + logger.info(`[${requestId}] Deleting Slack message`, { - channel: validatedData.channel, + channel, timestamp: validatedData.timestamp, }) @@ -53,7 +70,7 @@ export async function POST(request: NextRequest) { Authorization: `Bearer ${validatedData.accessToken}`, }, body: JSON.stringify({ - channel: validatedData.channel, + channel, ts: validatedData.timestamp, }), }) diff --git a/apps/sim/app/api/tools/slack/read-messages/route.ts b/apps/sim/app/api/tools/slack/read-messages/route.ts new file mode 100644 index 0000000000..e012f2dc40 --- /dev/null +++ b/apps/sim/app/api/tools/slack/read-messages/route.ts @@ -0,0 +1,207 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkHybridAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { createLogger } from '@/lib/logs/console/logger' +import { openDMChannel } from '../utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('SlackReadMessagesAPI') + +const SlackReadMessagesSchema = z + .object({ + accessToken: z.string().min(1, 'Access token is required'), + channel: z.string().optional(), + userId: z.string().optional(), + limit: z.number().optional(), + oldest: z.string().optional(), + latest: z.string().optional(), + }) + .refine((data) => data.channel || data.userId, { + message: 'Either channel or userId is required', + }) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) + + if (!authResult.success) { + logger.warn(`[${requestId}] Unauthorized Slack read messages attempt: ${authResult.error}`) + return NextResponse.json( + { + success: false, + error: authResult.error || 'Authentication required', + }, + { status: 401 } + ) + } + + logger.info( + `[${requestId}] Authenticated Slack read messages request via ${authResult.authType}`, + { + userId: authResult.userId, + } + ) + + const body = await request.json() + const validatedData = SlackReadMessagesSchema.parse(body) + + let channel = validatedData.channel + if (!channel && validatedData.userId) { + logger.info(`[${requestId}] Opening DM channel for user: ${validatedData.userId}`) + channel = await openDMChannel( + validatedData.accessToken, + validatedData.userId, + requestId, + logger + ) + } + + const url = new URL('https://slack.com/api/conversations.history') + url.searchParams.append('channel', channel!) + const limit = validatedData.limit ? Number(validatedData.limit) : 10 + url.searchParams.append('limit', String(Math.min(limit, 15))) + + if (validatedData.oldest) { + url.searchParams.append('oldest', validatedData.oldest) + } + if (validatedData.latest) { + url.searchParams.append('latest', validatedData.latest) + } + + logger.info(`[${requestId}] Reading Slack messages`, { + channel, + limit, + }) + + const slackResponse = await fetch(url.toString(), { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${validatedData.accessToken}`, + }, + }) + + const data = await slackResponse.json() + + if (!data.ok) { + logger.error(`[${requestId}] Slack API error:`, data) + + if (data.error === 'not_in_channel') { + return NextResponse.json( + { + success: false, + error: + 'Bot is not in the channel. Please invite the Sim bot to your Slack channel by typing: /invite @Sim Studio', + }, + { status: 400 } + ) + } + if (data.error === 'channel_not_found') { + return NextResponse.json( + { + success: false, + error: 'Channel not found. Please check the channel ID and try again.', + }, + { status: 400 } + ) + } + if (data.error === 'missing_scope') { + return NextResponse.json( + { + success: false, + error: + 'Missing required permissions. Please reconnect your Slack account with the necessary scopes (channels:history, groups:history, im:history).', + }, + { status: 400 } + ) + } + + return NextResponse.json( + { + success: false, + error: data.error || 'Failed to fetch messages', + }, + { status: 400 } + ) + } + + const messages = (data.messages || []).map((message: any) => ({ + type: message.type || 'message', + ts: message.ts, + text: message.text || '', + user: message.user, + bot_id: message.bot_id, + username: message.username, + channel: message.channel, + team: message.team, + thread_ts: message.thread_ts, + parent_user_id: message.parent_user_id, + reply_count: message.reply_count, + reply_users_count: message.reply_users_count, + latest_reply: message.latest_reply, + subscribed: message.subscribed, + last_read: message.last_read, + unread_count: message.unread_count, + subtype: message.subtype, + reactions: message.reactions?.map((reaction: any) => ({ + name: reaction.name, + count: reaction.count, + users: reaction.users || [], + })), + is_starred: message.is_starred, + pinned_to: message.pinned_to, + files: message.files?.map((file: any) => ({ + id: file.id, + name: file.name, + mimetype: file.mimetype, + size: file.size, + url_private: file.url_private, + permalink: file.permalink, + mode: file.mode, + })), + attachments: message.attachments, + blocks: message.blocks, + edited: message.edited + ? { + user: message.edited.user, + ts: message.edited.ts, + } + : undefined, + permalink: message.permalink, + })) + + logger.info(`[${requestId}] Successfully read ${messages.length} messages`) + + return NextResponse.json({ + success: true, + output: { + messages, + }, + }) + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) + return NextResponse.json( + { + success: false, + error: 'Invalid request data', + details: error.errors, + }, + { status: 400 } + ) + } + + logger.error(`[${requestId}] Error reading Slack messages:`, error) + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Unknown error occurred', + }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/slack/send-dm/route.ts b/apps/sim/app/api/tools/slack/send-dm/route.ts deleted file mode 100644 index 5a6840f143..0000000000 --- a/apps/sim/app/api/tools/slack/send-dm/route.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { type NextRequest, NextResponse } from 'next/server' -import { z } from 'zod' -import { checkHybridAuth } from '@/lib/auth/hybrid' -import { generateRequestId } from '@/lib/core/utils/request' -import { createLogger } from '@/lib/logs/console/logger' -import { openDMChannel, sendSlackMessage } from '../utils' - -export const dynamic = 'force-dynamic' - -const logger = createLogger('SlackSendDMAPI') - -const SlackSendDMSchema = z.object({ - accessToken: z.string().min(1, 'Access token is required'), - userId: z.string().min(1, 'User ID is required'), - text: z.string().min(1, 'Message text is required'), - thread_ts: z.string().optional().nullable(), - files: z.array(z.any()).optional().nullable(), -}) - -export async function POST(request: NextRequest) { - const requestId = generateRequestId() - - try { - const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) - - if (!authResult.success) { - logger.warn(`[${requestId}] Unauthorized Slack DM send attempt: ${authResult.error}`) - return NextResponse.json( - { - success: false, - error: authResult.error || 'Authentication required', - }, - { status: 401 } - ) - } - - logger.info(`[${requestId}] Authenticated Slack DM request via ${authResult.authType}`, { - userId: authResult.userId, - }) - - const body = await request.json() - const validatedData = SlackSendDMSchema.parse(body) - - logger.info(`[${requestId}] Sending Slack DM`, { - targetUserId: validatedData.userId, - hasFiles: !!(validatedData.files && validatedData.files.length > 0), - fileCount: validatedData.files?.length || 0, - }) - - // Open DM channel with the user - const dmChannelId = await openDMChannel( - validatedData.accessToken, - validatedData.userId, - requestId, - logger - ) - - const result = await sendSlackMessage( - { - accessToken: validatedData.accessToken, - channel: dmChannelId, - text: validatedData.text, - threadTs: validatedData.thread_ts, - files: validatedData.files, - }, - requestId, - logger - ) - - if (!result.success) { - return NextResponse.json({ success: false, error: result.error }, { status: 400 }) - } - - return NextResponse.json({ success: true, output: result.output }) - } catch (error) { - logger.error(`[${requestId}] Error sending Slack DM:`, error) - return NextResponse.json( - { - success: false, - error: error instanceof Error ? error.message : 'Unknown error occurred', - }, - { status: 500 } - ) - } -} diff --git a/apps/sim/app/api/tools/slack/send-message/route.ts b/apps/sim/app/api/tools/slack/send-message/route.ts index 28e3084dc3..cd9ab6fed8 100644 --- a/apps/sim/app/api/tools/slack/send-message/route.ts +++ b/apps/sim/app/api/tools/slack/send-message/route.ts @@ -9,13 +9,18 @@ export const dynamic = 'force-dynamic' const logger = createLogger('SlackSendMessageAPI') -const SlackSendMessageSchema = z.object({ - accessToken: z.string().min(1, 'Access token is required'), - channel: z.string().min(1, 'Channel is required'), - text: z.string().min(1, 'Message text is required'), - thread_ts: z.string().optional().nullable(), - files: z.array(z.any()).optional().nullable(), -}) +const SlackSendMessageSchema = z + .object({ + accessToken: z.string().min(1, 'Access token is required'), + channel: z.string().optional(), + userId: z.string().optional(), + text: z.string().min(1, 'Message text is required'), + thread_ts: z.string().optional().nullable(), + files: z.array(z.any()).optional().nullable(), + }) + .refine((data) => data.channel || data.userId, { + message: 'Either channel or userId is required', + }) export async function POST(request: NextRequest) { const requestId = generateRequestId() @@ -41,8 +46,11 @@ export async function POST(request: NextRequest) { const body = await request.json() const validatedData = SlackSendMessageSchema.parse(body) + const isDM = !!validatedData.userId logger.info(`[${requestId}] Sending Slack message`, { channel: validatedData.channel, + userId: validatedData.userId, + isDM, hasFiles: !!(validatedData.files && validatedData.files.length > 0), fileCount: validatedData.files?.length || 0, }) @@ -51,6 +59,7 @@ export async function POST(request: NextRequest) { { accessToken: validatedData.accessToken, channel: validatedData.channel, + userId: validatedData.userId, text: validatedData.text, threadTs: validatedData.thread_ts, files: validatedData.files, diff --git a/apps/sim/app/api/tools/slack/update-message/route.ts b/apps/sim/app/api/tools/slack/update-message/route.ts index c40b6d34c0..791a9e6725 100644 --- a/apps/sim/app/api/tools/slack/update-message/route.ts +++ b/apps/sim/app/api/tools/slack/update-message/route.ts @@ -3,17 +3,23 @@ import { z } from 'zod' import { checkHybridAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { createLogger } from '@/lib/logs/console/logger' +import { openDMChannel } from '../utils' export const dynamic = 'force-dynamic' const logger = createLogger('SlackUpdateMessageAPI') -const SlackUpdateMessageSchema = z.object({ - accessToken: z.string().min(1, 'Access token is required'), - channel: z.string().min(1, 'Channel ID is required'), - timestamp: z.string().min(1, 'Message timestamp is required'), - text: z.string().min(1, 'Message text is required'), -}) +const SlackUpdateMessageSchema = z + .object({ + accessToken: z.string().min(1, 'Access token is required'), + channel: z.string().optional(), + userId: z.string().optional(), + timestamp: z.string().min(1, 'Message timestamp is required'), + text: z.string().min(1, 'Message text is required'), + }) + .refine((data) => data.channel || data.userId, { + message: 'Either channel or userId is required', + }) export async function POST(request: NextRequest) { const requestId = generateRequestId() @@ -42,8 +48,19 @@ export async function POST(request: NextRequest) { const body = await request.json() const validatedData = SlackUpdateMessageSchema.parse(body) + let channel = validatedData.channel + if (!channel && validatedData.userId) { + logger.info(`[${requestId}] Opening DM channel for user: ${validatedData.userId}`) + channel = await openDMChannel( + validatedData.accessToken, + validatedData.userId, + requestId, + logger + ) + } + logger.info(`[${requestId}] Updating Slack message`, { - channel: validatedData.channel, + channel, timestamp: validatedData.timestamp, }) @@ -54,7 +71,7 @@ export async function POST(request: NextRequest) { Authorization: `Bearer ${validatedData.accessToken}`, }, body: JSON.stringify({ - channel: validatedData.channel, + channel, ts: validatedData.timestamp, text: validatedData.text, }), diff --git a/apps/sim/app/api/tools/slack/users/route.ts b/apps/sim/app/api/tools/slack/users/route.ts new file mode 100644 index 0000000000..97d73c88d1 --- /dev/null +++ b/apps/sim/app/api/tools/slack/users/route.ts @@ -0,0 +1,113 @@ +import { NextResponse } from 'next/server' +import { authorizeCredentialUse } from '@/lib/auth/credential-access' +import { generateRequestId } from '@/lib/core/utils/request' +import { createLogger } from '@/lib/logs/console/logger' +import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('SlackUsersAPI') + +interface SlackUser { + id: string + name: string + real_name: string + deleted: boolean + is_bot: boolean +} + +export async function POST(request: Request) { + try { + const requestId = generateRequestId() + const body = await request.json() + const { credential, workflowId } = body + + if (!credential) { + logger.error('Missing credential in request') + return NextResponse.json({ error: 'Credential is required' }, { status: 400 }) + } + + let accessToken: string + const isBotToken = credential.startsWith('xoxb-') + + if (isBotToken) { + accessToken = credential + logger.info('Using direct bot token for Slack API') + } else { + const authz = await authorizeCredentialUse(request as any, { + credentialId: credential, + workflowId, + }) + if (!authz.ok || !authz.credentialOwnerUserId) { + return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 }) + } + const resolvedToken = await refreshAccessTokenIfNeeded( + credential, + authz.credentialOwnerUserId, + requestId + ) + if (!resolvedToken) { + logger.error('Failed to get access token', { + credentialId: credential, + userId: authz.credentialOwnerUserId, + }) + return NextResponse.json( + { + error: 'Could not retrieve access token', + authRequired: true, + }, + { status: 401 } + ) + } + accessToken = resolvedToken + logger.info('Using OAuth token for Slack API') + } + + const data = await fetchSlackUsers(accessToken) + + const users = (data.members || []) + .filter((user: SlackUser) => !user.deleted && !user.is_bot) + .map((user: SlackUser) => ({ + id: user.id, + name: user.name, + real_name: user.real_name || user.name, + })) + + logger.info(`Successfully fetched ${users.length} Slack users`, { + total: data.members?.length || 0, + tokenType: isBotToken ? 'bot_token' : 'oauth', + }) + return NextResponse.json({ users }) + } catch (error) { + logger.error('Error processing Slack users request:', error) + return NextResponse.json( + { error: 'Failed to retrieve Slack users', details: (error as Error).message }, + { status: 500 } + ) + } +} + +async function fetchSlackUsers(accessToken: string) { + const url = new URL('https://slack.com/api/users.list') + url.searchParams.append('limit', '200') + + const response = await fetch(url.toString(), { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }) + + if (!response.ok) { + throw new Error(`Slack API error: ${response.status} ${response.statusText}`) + } + + const data = await response.json() + + if (!data.ok) { + throw new Error(data.error || 'Failed to fetch users') + } + + return data +} diff --git a/apps/sim/app/api/tools/slack/utils.ts b/apps/sim/app/api/tools/slack/utils.ts index f65802a65f..b52d734203 100644 --- a/apps/sim/app/api/tools/slack/utils.ts +++ b/apps/sim/app/api/tools/slack/utils.ts @@ -200,7 +200,8 @@ export async function openDMChannel( export interface SlackMessageParams { accessToken: string - channel: string + channel?: string + userId?: string text: string threadTs?: string | null files?: any[] | null @@ -208,7 +209,7 @@ export interface SlackMessageParams { /** * Sends a Slack message with optional file attachments - * This is the main function that handles the complete flow of sending messages + * Supports both channel messages and direct messages via userId */ export async function sendSlackMessage( params: SlackMessageParams, @@ -219,7 +220,17 @@ export async function sendSlackMessage( output?: { message: any; ts: string; channel: string; fileCount?: number } error?: string }> { - const { accessToken, channel, text, threadTs, files } = params + const { accessToken, text, threadTs, files } = params + let { channel } = params + + if (!channel && params.userId) { + logger.info(`[${requestId}] Opening DM channel for user: ${params.userId}`) + channel = await openDMChannel(accessToken, params.userId, requestId, logger) + } + + if (!channel) { + return { success: false, error: 'Either channel or userId is required' } + } // No files - simple message if (!files || files.length === 0) { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/index.ts index 28f9a609aa..8208cfa33b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/index.ts @@ -33,4 +33,5 @@ export { Text } from './text/text' export { TimeInput } from './time-input/time-input' export { ToolInput } from './tool-input/tool-input' export { TriggerSave } from './trigger-save/trigger-save' +export { UserSelectorInput } from './user-selector/user-selector-input' export { VariablesInput } from './variables-input/variables-input' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/user-selector/user-selector-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/user-selector/user-selector-input.tsx new file mode 100644 index 0000000000..f60cba2744 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/user-selector/user-selector-input.tsx @@ -0,0 +1,123 @@ +'use client' + +import { useEffect, useMemo, useState } from 'react' +import { useParams } from 'next/navigation' +import { Tooltip } from '@/components/emcn' +import { getProviderIdFromServiceId } from '@/lib/oauth/oauth' +import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox' +import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate' +import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential' +import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' +import type { SubBlockConfig } from '@/blocks/types' +import type { SelectorContext } from '@/hooks/selectors/types' + +interface UserSelectorInputProps { + blockId: string + subBlock: SubBlockConfig + disabled?: boolean + onUserSelect?: (userId: string) => void + isPreview?: boolean + previewValue?: any | null + previewContextValues?: Record +} + +export function UserSelectorInput({ + blockId, + subBlock, + disabled = false, + onUserSelect, + isPreview = false, + previewValue, + previewContextValues, +}: UserSelectorInputProps) { + const params = useParams() + const workflowIdFromUrl = (params?.workflowId as string) || '' + const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlock.id) + const [authMethod] = useSubBlockValue(blockId, 'authMethod') + const [botToken] = useSubBlockValue(blockId, 'botToken') + const [connectedCredential] = useSubBlockValue(blockId, 'credential') + + const effectiveAuthMethod = previewContextValues?.authMethod ?? authMethod + const effectiveBotToken = previewContextValues?.botToken ?? botToken + const effectiveCredential = previewContextValues?.credential ?? connectedCredential + const [_userInfo, setUserInfo] = useState(null) + + const serviceId = subBlock.serviceId || '' + const effectiveProviderId = useMemo(() => getProviderIdFromServiceId(serviceId), [serviceId]) + const isSlack = serviceId === 'slack' + + const { finalDisabled, dependsOn } = useDependsOnGate(blockId, subBlock, { + disabled, + isPreview, + previewContextValues, + }) + + const credential: string = + (effectiveAuthMethod as string) === 'bot_token' + ? (effectiveBotToken as string) || '' + : (effectiveCredential as string) || '' + + const { isForeignCredential } = useForeignCredential( + effectiveProviderId, + (effectiveAuthMethod as string) === 'bot_token' ? '' : (effectiveCredential as string) || '' + ) + + useEffect(() => { + const val = isPreview && previewValue !== undefined ? previewValue : storeValue + if (typeof val === 'string') { + setUserInfo(val) + } + }, [isPreview, previewValue, storeValue]) + + const requiresCredential = dependsOn.includes('credential') + const missingCredential = !credential || credential.trim().length === 0 + const shouldForceDisable = requiresCredential && (missingCredential || isForeignCredential) + + const context: SelectorContext = useMemo( + () => ({ + credentialId: credential, + workflowId: workflowIdFromUrl, + }), + [credential, workflowIdFromUrl] + ) + + if (!isSlack) { + return ( + + +
+ User selector not supported for service: {serviceId || 'unknown'} +
+
+ +

This user selector is not yet implemented for {serviceId || 'unknown'}

+
+
+ ) + } + + return ( + + +
+ { + setUserInfo(value) + if (!isPreview) { + onUserSelect?.(value) + } + }} + /> +
+
+
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx index 395c4c61d6..be8d4ac7ab 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx @@ -39,6 +39,7 @@ import { TimeInput, ToolInput, TriggerSave, + UserSelectorInput, VariablesInput, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components' import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate' @@ -742,6 +743,17 @@ function SubBlockComponent({ /> ) + case 'user-selector': + return ( + + ) + case 'mcp-server-selector': return ( = { title: 'Operation', type: 'dropdown', options: [ - { label: 'Send Channel Message', id: 'send' }, - { label: 'Send DM', id: 'dm' }, + { label: 'Send Message', id: 'send' }, { label: 'Create Canvas', id: 'canvas' }, { label: 'Read Messages', id: 'read' }, { label: 'List Channels', id: 'list_channels' }, @@ -49,6 +48,20 @@ export const SlackBlock: BlockConfig = { value: () => 'oauth', required: true, }, + { + id: 'destinationType', + title: 'Destination', + type: 'dropdown', + options: [ + { label: 'Channel', id: 'channel' }, + { label: 'Direct Message', id: 'dm' }, + ], + value: () => 'channel', + condition: { + field: 'operation', + value: ['send', 'read', 'update', 'delete', 'react'], + }, + }, { id: 'credential', title: 'Slack Account', @@ -62,6 +75,8 @@ export const SlackBlock: BlockConfig = { 'chat:write', 'chat:write.public', 'im:write', + 'im:history', + 'im:read', 'users:read', 'files:write', 'files:read', @@ -98,11 +113,14 @@ export const SlackBlock: BlockConfig = { dependsOn: { all: ['authMethod'], any: ['credential', 'botToken'] }, condition: { field: 'operation', - value: ['list_channels', 'list_users', 'get_user', 'dm'], + value: ['list_channels', 'list_users', 'get_user'], not: true, + and: { + field: 'destinationType', + value: 'channel', + }, }, }, - // Manual channel ID input (advanced mode) { id: 'manualChannel', title: 'Channel ID', @@ -112,21 +130,39 @@ export const SlackBlock: BlockConfig = { mode: 'advanced', condition: { field: 'operation', - value: ['list_channels', 'list_users', 'get_user', 'dm'], + value: ['list_channels', 'list_users', 'get_user'], not: true, + and: { + field: 'destinationType', + value: 'channel', + }, }, }, - // DM recipient user ID { id: 'dmUserId', + title: 'User', + type: 'user-selector', + canonicalParamId: 'userId', + serviceId: 'slack', + placeholder: 'Select Slack user', + mode: 'basic', + dependsOn: { all: ['authMethod'], any: ['credential', 'botToken'] }, + condition: { + field: 'destinationType', + value: 'dm', + }, + }, + { + id: 'manualDmUserId', title: 'User ID', type: 'short-input', - placeholder: 'Enter Slack user ID to DM (e.g., U1234567890)', + canonicalParamId: 'userId', + placeholder: 'Enter Slack user ID (e.g., U1234567890)', + mode: 'advanced', condition: { - field: 'operation', + field: 'destinationType', value: 'dm', }, - required: true, }, { id: 'text', @@ -135,7 +171,7 @@ export const SlackBlock: BlockConfig = { placeholder: 'Enter your message (supports Slack mrkdwn)', condition: { field: 'operation', - value: ['send', 'dm'], + value: 'send', }, required: true, }, @@ -147,30 +183,28 @@ export const SlackBlock: BlockConfig = { placeholder: 'Reply to thread (e.g., 1405894322.002768)', condition: { field: 'operation', - value: ['send', 'dm'], + value: 'send', }, required: false, }, - // File upload (basic mode) { id: 'attachmentFiles', title: 'Attachments', type: 'file-upload', canonicalParamId: 'files', placeholder: 'Upload files to attach', - condition: { field: 'operation', value: ['send', 'dm'] }, + condition: { field: 'operation', value: 'send' }, mode: 'basic', multiple: true, required: false, }, - // Variable reference (advanced mode) { id: 'files', title: 'File Attachments', type: 'short-input', canonicalParamId: 'files', placeholder: 'Reference files from previous blocks', - condition: { field: 'operation', value: ['send', 'dm'] }, + condition: { field: 'operation', value: 'send' }, mode: 'advanced', required: false, }, @@ -384,7 +418,6 @@ export const SlackBlock: BlockConfig = { tools: { access: [ 'slack_message', - 'slack_dm', 'slack_canvas', 'slack_message_reader', 'slack_list_channels', @@ -401,8 +434,6 @@ export const SlackBlock: BlockConfig = { switch (params.operation) { case 'send': return 'slack_message' - case 'dm': - return 'slack_dm' case 'canvas': return 'slack_canvas' case 'read': @@ -433,9 +464,11 @@ export const SlackBlock: BlockConfig = { authMethod, botToken, operation, + destinationType, channel, manualChannel, dmUserId, + manualDmUserId, text, title, content, @@ -458,21 +491,26 @@ export const SlackBlock: BlockConfig = { ...rest } = params - // Handle both selector and manual channel input + const isDM = destinationType === 'dm' const effectiveChannel = (channel || manualChannel || '').trim() + const effectiveUserId = (dmUserId || manualDmUserId || '').trim() - // Operations that don't require a channel - const noChannelOperations = ['list_channels', 'list_users', 'get_user', 'dm'] + const noChannelOperations = ['list_channels', 'list_users', 'get_user'] + const dmSupportedOperations = ['send', 'read', 'update', 'delete', 'react'] - // Channel is required for most operations - if (!effectiveChannel && !noChannelOperations.includes(operation)) { + if (isDM && dmSupportedOperations.includes(operation)) { + if (!effectiveUserId) { + throw new Error('User is required for DM operations.') + } + } else if (!effectiveChannel && !noChannelOperations.includes(operation)) { throw new Error('Channel is required.') } const baseParams: Record = {} - // Only add channel if we have one (not needed for list_channels) - if (effectiveChannel) { + if (isDM && dmSupportedOperations.includes(operation)) { + baseParams.userId = effectiveUserId + } else if (effectiveChannel) { baseParams.channel = effectiveChannel } @@ -490,18 +528,15 @@ export const SlackBlock: BlockConfig = { baseParams.credential = credential } - // Handle operation-specific params switch (operation) { case 'send': { if (!text || text.trim() === '') { throw new Error('Message text is required for send operation') } baseParams.text = text - // Add thread_ts if provided if (threadTs) { baseParams.thread_ts = threadTs } - // Add files if provided const fileParam = attachmentFiles || files if (fileParam) { baseParams.files = fileParam @@ -509,27 +544,6 @@ export const SlackBlock: BlockConfig = { break } - case 'dm': { - if (!dmUserId || dmUserId.trim() === '') { - throw new Error('User ID is required for DM operation') - } - if (!text || text.trim() === '') { - throw new Error('Message text is required for DM operation') - } - baseParams.userId = dmUserId - baseParams.text = text - // Add thread_ts if provided - if (threadTs) { - baseParams.thread_ts = threadTs - } - // Add files if provided - const dmFileParam = attachmentFiles || files - if (dmFileParam) { - baseParams.files = dmFileParam - } - break - } - case 'canvas': if (!title || !content) { throw new Error('Title and content are required for canvas operation') @@ -631,11 +645,13 @@ export const SlackBlock: BlockConfig = { inputs: { operation: { type: 'string', description: 'Operation to perform' }, authMethod: { type: 'string', description: 'Authentication method' }, + destinationType: { type: 'string', description: 'Destination type (channel or dm)' }, credential: { type: 'string', description: 'Slack access token' }, botToken: { type: 'string', description: 'Bot token' }, channel: { type: 'string', description: 'Channel identifier' }, manualChannel: { type: 'string', description: 'Manual channel identifier' }, - dmUserId: { type: 'string', description: 'User ID for DM recipient' }, + dmUserId: { type: 'string', description: 'User ID for DM recipient (selector)' }, + manualDmUserId: { type: 'string', description: 'User ID for DM recipient (manual input)' }, text: { type: 'string', description: 'Message text' }, attachmentFiles: { type: 'json', description: 'Files to attach (UI upload)' }, files: { type: 'array', description: 'Files to attach (UserFile array)' }, diff --git a/apps/sim/blocks/types.ts b/apps/sim/blocks/types.ts index 7cc0116c98..64c1a89a24 100644 --- a/apps/sim/blocks/types.ts +++ b/apps/sim/blocks/types.ts @@ -59,6 +59,7 @@ export type SubBlockType = | 'file-selector' // File selector for Google Drive, etc. | 'project-selector' // Project selector for Jira, Discord, etc. | 'channel-selector' // Channel selector for Slack, Discord, etc. + | 'user-selector' // User selector for Slack, etc. | 'folder-selector' // Folder selector for Gmail, etc. | 'knowledge-base-selector' // Knowledge base selector | 'knowledge-tag-filters' // Multiple tag filters for knowledge bases @@ -85,6 +86,7 @@ export type SubBlockType = export const SELECTOR_TYPES_HYDRATION_REQUIRED: SubBlockType[] = [ 'oauth-input', 'channel-selector', + 'user-selector', 'file-selector', 'folder-selector', 'project-selector', diff --git a/apps/sim/hooks/selectors/registry.ts b/apps/sim/hooks/selectors/registry.ts index 4137e3065f..39844c2979 100644 --- a/apps/sim/hooks/selectors/registry.ts +++ b/apps/sim/hooks/selectors/registry.ts @@ -10,6 +10,7 @@ import type { const SELECTOR_STALE = 60 * 1000 type SlackChannel = { id: string; name: string } +type SlackUser = { id: string; name: string; real_name: string } type FolderResponse = { id: string; name: string } type PlannerTask = { id: string; title: string } @@ -59,6 +60,30 @@ const registry: Record = { })) }, }, + 'slack.users': { + key: 'slack.users', + staleTime: SELECTOR_STALE, + getQueryKey: ({ context }: SelectorQueryArgs) => [ + 'selectors', + 'slack.users', + context.credentialId ?? 'none', + ], + enabled: ({ context }) => Boolean(context.credentialId), + fetchList: async ({ context }: SelectorQueryArgs) => { + const body = JSON.stringify({ + credential: context.credentialId, + workflowId: context.workflowId, + }) + const data = await fetchJson<{ users: SlackUser[] }>('/api/tools/slack/users', { + method: 'POST', + body, + }) + return (data.users || []).map((user) => ({ + id: user.id, + label: user.real_name || user.name, + })) + }, + }, 'gmail.labels': { key: 'gmail.labels', staleTime: SELECTOR_STALE, diff --git a/apps/sim/hooks/selectors/resolution.ts b/apps/sim/hooks/selectors/resolution.ts index 76e3f2117b..78af03f935 100644 --- a/apps/sim/hooks/selectors/resolution.ts +++ b/apps/sim/hooks/selectors/resolution.ts @@ -32,6 +32,8 @@ export function resolveSelectorForSubBlock( return resolveFolderSelector(subBlock, args) case 'channel-selector': return resolveChannelSelector(subBlock, args) + case 'user-selector': + return resolveUserSelector(subBlock, args) case 'project-selector': return resolveProjectSelector(subBlock, args) case 'document-selector': @@ -157,6 +159,21 @@ function resolveChannelSelector( } } +function resolveUserSelector( + subBlock: SubBlockConfig, + args: SelectorResolutionArgs +): SelectorResolution { + const serviceId = subBlock.serviceId + if (serviceId !== 'slack') { + return { key: null, context: buildBaseContext(args), allowSearch: true } + } + return { + key: 'slack.users', + context: buildBaseContext(args), + allowSearch: true, + } +} + function resolveProjectSelector( subBlock: SubBlockConfig, args: SelectorResolutionArgs diff --git a/apps/sim/hooks/selectors/types.ts b/apps/sim/hooks/selectors/types.ts index d186c4d50e..e9da5996a2 100644 --- a/apps/sim/hooks/selectors/types.ts +++ b/apps/sim/hooks/selectors/types.ts @@ -3,6 +3,7 @@ import type { QueryKey } from '@tanstack/react-query' export type SelectorKey = | 'slack.channels' + | 'slack.users' | 'gmail.labels' | 'outlook.folders' | 'google.calendar' diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 1c539462a6..ed28b15e1b 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1003,7 +1003,6 @@ import { slackAddReactionTool, slackCanvasTool, slackDeleteMessageTool, - slackDMTool, slackDownloadTool, slackGetUserTool, slackListChannelsTool, @@ -1508,7 +1507,6 @@ export const tools: Record = { polymarket_get_positions: polymarketGetPositionsTool, polymarket_get_trades: polymarketGetTradesTool, slack_message: slackMessageTool, - slack_dm: slackDMTool, slack_message_reader: slackMessageReaderTool, slack_list_channels: slackListChannelsTool, slack_list_members: slackListMembersTool, diff --git a/apps/sim/tools/slack/add_reaction.ts b/apps/sim/tools/slack/add_reaction.ts index 22b955d945..2103a28032 100644 --- a/apps/sim/tools/slack/add_reaction.ts +++ b/apps/sim/tools/slack/add_reaction.ts @@ -33,10 +33,16 @@ export const slackAddReactionTool: ToolConfig ({ accessToken: params.accessToken || params.botToken, channel: params.channel, + userId: params.userId, timestamp: params.timestamp, name: params.name, }), diff --git a/apps/sim/tools/slack/delete_message.ts b/apps/sim/tools/slack/delete_message.ts index 6d30f4970d..672f27ac0d 100644 --- a/apps/sim/tools/slack/delete_message.ts +++ b/apps/sim/tools/slack/delete_message.ts @@ -36,10 +36,16 @@ export const slackDeleteMessageTool: ToolConfig< }, channel: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'Channel ID where the message was posted (e.g., C1234567890)', }, + userId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'User ID for DM messages (e.g., U1234567890)', + }, timestamp: { type: 'string', required: true, @@ -57,6 +63,7 @@ export const slackDeleteMessageTool: ToolConfig< body: (params: SlackDeleteMessageParams) => ({ accessToken: params.accessToken || params.botToken, channel: params.channel, + userId: params.userId, timestamp: params.timestamp, }), }, diff --git a/apps/sim/tools/slack/dm.ts b/apps/sim/tools/slack/dm.ts deleted file mode 100644 index f1bd3929ea..0000000000 --- a/apps/sim/tools/slack/dm.ts +++ /dev/null @@ -1,101 +0,0 @@ -import type { SlackDMParams, SlackDMResponse } from '@/tools/slack/types' -import type { ToolConfig } from '@/tools/types' - -export const slackDMTool: ToolConfig = { - id: 'slack_dm', - name: 'Slack DM', - description: - 'Send direct messages to Slack users through the Slack API. Supports Slack mrkdwn formatting.', - 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-only', - description: 'Target Slack user ID to send the direct message to', - }, - text: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'Message text to send (supports Slack mrkdwn formatting)', - }, - thread_ts: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Thread timestamp to reply to (creates thread reply)', - }, - files: { - type: 'file[]', - required: false, - visibility: 'user-only', - description: 'Files to attach to the message', - }, - }, - - request: { - url: '/api/tools/slack/send-dm', - method: 'POST', - headers: () => ({ - 'Content-Type': 'application/json', - }), - body: (params: SlackDMParams) => { - return { - accessToken: params.accessToken || params.botToken, - userId: params.userId, - text: params.text, - thread_ts: params.thread_ts || undefined, - files: params.files || null, - } - }, - }, - - transformResponse: async (response: Response) => { - const data = await response.json() - if (!data.success) { - throw new Error(data.error || 'Failed to send Slack DM') - } - return { - success: true, - output: data.output, - } - }, - - outputs: { - message: { - type: 'object', - description: 'Complete message object with all properties returned by Slack', - }, - ts: { type: 'string', description: 'Message timestamp' }, - channel: { type: 'string', description: 'DM channel ID where message was sent' }, - fileCount: { - type: 'number', - description: 'Number of files uploaded (when files are attached)', - }, - }, -} diff --git a/apps/sim/tools/slack/index.ts b/apps/sim/tools/slack/index.ts index 727dd5a486..ea99459cd3 100644 --- a/apps/sim/tools/slack/index.ts +++ b/apps/sim/tools/slack/index.ts @@ -1,7 +1,6 @@ import { slackAddReactionTool } from '@/tools/slack/add_reaction' import { slackCanvasTool } from '@/tools/slack/canvas' import { slackDeleteMessageTool } from '@/tools/slack/delete_message' -import { slackDMTool } from '@/tools/slack/dm' import { slackDownloadTool } from '@/tools/slack/download' import { slackGetUserTool } from '@/tools/slack/get_user' import { slackListChannelsTool } from '@/tools/slack/list_channels' @@ -13,7 +12,6 @@ import { slackUpdateMessageTool } from '@/tools/slack/update_message' export { slackMessageTool, - slackDMTool, slackCanvasTool, slackMessageReaderTool, slackDownloadTool, diff --git a/apps/sim/tools/slack/message.ts b/apps/sim/tools/slack/message.ts index 2b747a8f8c..d8c9e36b87 100644 --- a/apps/sim/tools/slack/message.ts +++ b/apps/sim/tools/slack/message.ts @@ -3,9 +3,9 @@ import type { ToolConfig } from '@/tools/types' export const slackMessageTool: ToolConfig = { id: 'slack_message', - name: 'Slack Channel Message', + name: 'Slack Message', description: - 'Send messages to Slack channels through the Slack API. Supports Slack mrkdwn formatting.', + 'Send messages to Slack channels or direct messages. Supports Slack mrkdwn formatting.', version: '1.0.0', oauth: { @@ -34,10 +34,16 @@ export const slackMessageTool: ToolConfig { - const url = new URL('https://slack.com/api/conversations.history') - url.searchParams.append('channel', params.channel) - // Cap limit at 15 due to Slack API restrictions for non-Marketplace apps - const limit = params.limit ? Number(params.limit) : 10 - url.searchParams.append('limit', String(Math.min(limit, 15))) - - if (params.oldest) { - url.searchParams.append('oldest', params.oldest) - } - if (params.latest) { - url.searchParams.append('latest', params.latest) - } - - return url.toString() - }, - method: 'GET', - headers: (params: SlackMessageReaderParams) => ({ + url: '/api/tools/slack/read-messages', + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json', - Authorization: `Bearer ${params.accessToken || params.botToken}`, + }), + body: (params: SlackMessageReaderParams) => ({ + accessToken: params.accessToken || params.botToken, + channel: params.channel, + userId: params.userId, + limit: params.limit, + oldest: params.oldest, + latest: params.latest, }), }, transformResponse: async (response: Response) => { const data = await response.json() - if (!data.ok) { - if (data.error === 'not_in_channel') { - throw new Error( - 'Bot is not in the channel. Please invite the Sim bot to your Slack channel by typing: /invite @Sim Studio' - ) - } - 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:history, groups:history).' - ) - } + if (!data.success) { throw new Error(data.error || 'Failed to fetch messages from Slack') } - const messages = (data.messages || []).map((message: any) => ({ - // Core properties - type: message.type || 'message', - ts: message.ts, - text: message.text || '', - user: message.user, - bot_id: message.bot_id, - username: message.username, - channel: message.channel, - team: message.team, - - // Thread properties - thread_ts: message.thread_ts, - parent_user_id: message.parent_user_id, - reply_count: message.reply_count, - reply_users_count: message.reply_users_count, - latest_reply: message.latest_reply, - subscribed: message.subscribed, - last_read: message.last_read, - unread_count: message.unread_count, - - // Message subtype - subtype: message.subtype, - - // Reactions and interactions - reactions: message.reactions?.map((reaction: any) => ({ - name: reaction.name, - count: reaction.count, - users: reaction.users || [], - })), - is_starred: message.is_starred, - pinned_to: message.pinned_to, - - // Content attachments - files: message.files?.map((file: any) => ({ - id: file.id, - name: file.name, - mimetype: file.mimetype, - size: file.size, - url_private: file.url_private, - permalink: file.permalink, - mode: file.mode, - })), - attachments: message.attachments, - blocks: message.blocks, - - // Metadata - edited: message.edited - ? { - user: message.edited.user, - ts: message.edited.ts, - } - : undefined, - permalink: message.permalink, - })) - return { success: true, - output: { - messages, - }, + output: data.output, } }, diff --git a/apps/sim/tools/slack/types.ts b/apps/sim/tools/slack/types.ts index 16ec1e7199..9a1784df88 100644 --- a/apps/sim/tools/slack/types.ts +++ b/apps/sim/tools/slack/types.ts @@ -7,7 +7,8 @@ export interface SlackBaseParams { } export interface SlackMessageParams extends SlackBaseParams { - channel: string + channel?: string + userId?: string text: string thread_ts?: string files?: any[] @@ -21,7 +22,8 @@ export interface SlackCanvasParams extends SlackBaseParams { } export interface SlackMessageReaderParams extends SlackBaseParams { - channel: string + channel?: string + userId?: string limit?: number oldest?: string latest?: string @@ -33,18 +35,21 @@ export interface SlackDownloadParams extends SlackBaseParams { } export interface SlackUpdateMessageParams extends SlackBaseParams { - channel: string + channel?: string + userId?: string timestamp: string text: string } export interface SlackDeleteMessageParams extends SlackBaseParams { - channel: string + channel?: string + userId?: string timestamp: string } export interface SlackAddReactionParams extends SlackBaseParams { - channel: string + channel?: string + userId?: string timestamp: string name: string } @@ -69,13 +74,6 @@ export interface SlackGetUserParams extends SlackBaseParams { userId: string } -export interface SlackDMParams extends SlackBaseParams { - userId: string - text: string - thread_ts?: string - files?: any[] -} - export interface SlackMessageResponse extends ToolResponse { output: { // Legacy properties for backward compatibility @@ -306,15 +304,6 @@ export interface SlackGetUserResponse extends ToolResponse { } } -export interface SlackDMResponse extends ToolResponse { - output: { - message: SlackMessage - ts: string - channel: string - fileCount?: number - } -} - export type SlackResponse = | SlackCanvasResponse | SlackMessageReaderResponse @@ -327,4 +316,3 @@ export type SlackResponse = | SlackListMembersResponse | SlackListUsersResponse | SlackGetUserResponse - | SlackDMResponse diff --git a/apps/sim/tools/slack/update_message.ts b/apps/sim/tools/slack/update_message.ts index 27f197a36e..fe63a3d76c 100644 --- a/apps/sim/tools/slack/update_message.ts +++ b/apps/sim/tools/slack/update_message.ts @@ -36,10 +36,16 @@ export const slackUpdateMessageTool: ToolConfig< }, channel: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'Channel ID where the message was posted (e.g., C1234567890)', }, + userId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'User ID for DM messages (e.g., U1234567890)', + }, timestamp: { type: 'string', required: true, @@ -63,6 +69,7 @@ export const slackUpdateMessageTool: ToolConfig< body: (params: SlackUpdateMessageParams) => ({ accessToken: params.accessToken || params.botToken, channel: params.channel, + userId: params.userId, timestamp: params.timestamp, text: params.text, }), From cc9084eb7c66b742f91e7b984cde9dce66516492 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 15 Dec 2025 15:13:36 -0800 Subject: [PATCH 03/13] add log for message limit --- apps/sim/app/api/tools/slack/read-messages/route.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/sim/app/api/tools/slack/read-messages/route.ts b/apps/sim/app/api/tools/slack/read-messages/route.ts index e012f2dc40..aafea3b838 100644 --- a/apps/sim/app/api/tools/slack/read-messages/route.ts +++ b/apps/sim/app/api/tools/slack/read-messages/route.ts @@ -47,6 +47,12 @@ export async function POST(request: NextRequest) { ) const body = await request.json() + + logger.info(`[${requestId}] Raw request body`, { + limit: body.limit, + limitType: typeof body.limit, + }) + const validatedData = SlackReadMessagesSchema.parse(body) let channel = validatedData.channel From c21ef94181fb9ed7d4bb582bddea3eaf05706782 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 15 Dec 2025 15:25:09 -0800 Subject: [PATCH 04/13] consolidate slack selector code --- .../channel-selector-input.tsx | 128 ------------------ .../components/sub-block/components/index.ts | 3 +- .../slack-selector-input.tsx} | 54 ++++++-- .../components/tool-input/tool-input.tsx | 6 +- .../editor/components/sub-block/sub-block.tsx | 15 +- 5 files changed, 45 insertions(+), 161 deletions(-) delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/channel-selector/channel-selector-input.tsx rename apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/{user-selector/user-selector-input.tsx => slack-selector/slack-selector-input.tsx} (74%) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/channel-selector/channel-selector-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/channel-selector/channel-selector-input.tsx deleted file mode 100644 index 245dbd51cf..0000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/channel-selector/channel-selector-input.tsx +++ /dev/null @@ -1,128 +0,0 @@ -'use client' - -import { useEffect, useMemo, useState } from 'react' -import { useParams } from 'next/navigation' -import { Tooltip } from '@/components/emcn' -import { getProviderIdFromServiceId } from '@/lib/oauth/oauth' -import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox' -import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate' -import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential' -import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' -import type { SubBlockConfig } from '@/blocks/types' -import type { SelectorContext } from '@/hooks/selectors/types' - -interface ChannelSelectorInputProps { - blockId: string - subBlock: SubBlockConfig - disabled?: boolean - onChannelSelect?: (channelId: string) => void - isPreview?: boolean - previewValue?: any | null - previewContextValues?: Record -} - -export function ChannelSelectorInput({ - blockId, - subBlock, - disabled = false, - onChannelSelect, - isPreview = false, - previewValue, - previewContextValues, -}: ChannelSelectorInputProps) { - const params = useParams() - const workflowIdFromUrl = (params?.workflowId as string) || '' - const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlock.id) - const [authMethod] = useSubBlockValue(blockId, 'authMethod') - const [botToken] = useSubBlockValue(blockId, 'botToken') - const [connectedCredential] = useSubBlockValue(blockId, 'credential') - - const effectiveAuthMethod = previewContextValues?.authMethod ?? authMethod - const effectiveBotToken = previewContextValues?.botToken ?? botToken - const effectiveCredential = previewContextValues?.credential ?? connectedCredential - const [_channelInfo, setChannelInfo] = useState(null) - - // Use serviceId to identify the service and derive providerId for credential lookup - const serviceId = subBlock.serviceId || '' - const effectiveProviderId = useMemo(() => getProviderIdFromServiceId(serviceId), [serviceId]) - const isSlack = serviceId === 'slack' - - // Central dependsOn gating - const { finalDisabled, dependsOn } = useDependsOnGate(blockId, subBlock, { - disabled, - isPreview, - previewContextValues, - }) - - // Choose credential strictly based on auth method - use effective values - const credential: string = - (effectiveAuthMethod as string) === 'bot_token' - ? (effectiveBotToken as string) || '' - : (effectiveCredential as string) || '' - - // Determine if connected OAuth credential is foreign (not applicable for bot tokens) - const { isForeignCredential } = useForeignCredential( - effectiveProviderId, - (effectiveAuthMethod as string) === 'bot_token' ? '' : (effectiveCredential as string) || '' - ) - - // Get the current value from the store or prop value if in preview mode (same pattern as file-selector) - useEffect(() => { - const val = isPreview && previewValue !== undefined ? previewValue : storeValue - if (typeof val === 'string') { - setChannelInfo(val) - } - }, [isPreview, previewValue, storeValue]) - - const requiresCredential = dependsOn.includes('credential') - const missingCredential = !credential || credential.trim().length === 0 - const shouldForceDisable = requiresCredential && (missingCredential || isForeignCredential) - - const context: SelectorContext = useMemo( - () => ({ - credentialId: credential, - workflowId: workflowIdFromUrl, - }), - [credential, workflowIdFromUrl] - ) - - if (!isSlack) { - return ( - - -
- Channel selector not supported for service: {serviceId || 'unknown'} -
-
- -

This channel selector is not yet implemented for {serviceId || 'unknown'}

-
-
- ) - } - - return ( - - -
- { - setChannelInfo(value) - if (!isPreview) { - onChannelSelect?.(value) - } - }} - /> -
-
-
- ) -} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/index.ts index 8208cfa33b..dc5ba115e0 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/index.ts @@ -1,4 +1,3 @@ -export { ChannelSelectorInput } from './channel-selector/channel-selector-input' export { CheckboxList } from './checkbox-list/checkbox-list' export { Code } from './code/code' export { ComboBox } from './combobox/combobox' @@ -24,6 +23,7 @@ export { ProjectSelectorInput } from './project-selector/project-selector-input' export { ResponseFormat } from './response/response-format' export { ScheduleSave } from './schedule-save/schedule-save' export { ShortInput } from './short-input/short-input' +export { SlackSelectorInput } from './slack-selector/slack-selector-input' export { SliderInput } from './slider-input/slider-input' export { InputFormat } from './starter/input-format' export { SubBlockInputController } from './sub-block-input-controller' @@ -33,5 +33,4 @@ export { Text } from './text/text' export { TimeInput } from './time-input/time-input' export { ToolInput } from './tool-input/tool-input' export { TriggerSave } from './trigger-save/trigger-save' -export { UserSelectorInput } from './user-selector/user-selector-input' export { VariablesInput } from './variables-input/variables-input' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/user-selector/user-selector-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-selector/slack-selector-input.tsx similarity index 74% rename from apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/user-selector/user-selector-input.tsx rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-selector/slack-selector-input.tsx index f60cba2744..9267ff174d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/user-selector/user-selector-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-selector/slack-selector-input.tsx @@ -9,30 +9,51 @@ import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/c import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' import type { SubBlockConfig } from '@/blocks/types' -import type { SelectorContext } from '@/hooks/selectors/types' +import type { SelectorContext, SelectorKey } from '@/hooks/selectors/types' -interface UserSelectorInputProps { +type SlackSelectorType = 'channel-selector' | 'user-selector' + +const SELECTOR_CONFIG: Record< + SlackSelectorType, + { selectorKey: SelectorKey; placeholder: string; label: string } +> = { + 'channel-selector': { + selectorKey: 'slack.channels', + placeholder: 'Select Slack channel', + label: 'Channel', + }, + 'user-selector': { + selectorKey: 'slack.users', + placeholder: 'Select Slack user', + label: 'User', + }, +} + +interface SlackSelectorInputProps { blockId: string subBlock: SubBlockConfig disabled?: boolean - onUserSelect?: (userId: string) => void + onSelect?: (value: string) => void isPreview?: boolean previewValue?: any | null previewContextValues?: Record } -export function UserSelectorInput({ +export function SlackSelectorInput({ blockId, subBlock, disabled = false, - onUserSelect, + onSelect, isPreview = false, previewValue, previewContextValues, -}: UserSelectorInputProps) { +}: SlackSelectorInputProps) { + const selectorType = subBlock.type as SlackSelectorType + const config = SELECTOR_CONFIG[selectorType] + const params = useParams() const workflowIdFromUrl = (params?.workflowId as string) || '' - const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlock.id) + const [storeValue] = useSubBlockValue(blockId, subBlock.id) const [authMethod] = useSubBlockValue(blockId, 'authMethod') const [botToken] = useSubBlockValue(blockId, 'botToken') const [connectedCredential] = useSubBlockValue(blockId, 'credential') @@ -40,7 +61,7 @@ export function UserSelectorInput({ const effectiveAuthMethod = previewContextValues?.authMethod ?? authMethod const effectiveBotToken = previewContextValues?.botToken ?? botToken const effectiveCredential = previewContextValues?.credential ?? connectedCredential - const [_userInfo, setUserInfo] = useState(null) + const [_selectedValue, setSelectedValue] = useState(null) const serviceId = subBlock.serviceId || '' const effectiveProviderId = useMemo(() => getProviderIdFromServiceId(serviceId), [serviceId]) @@ -65,7 +86,7 @@ export function UserSelectorInput({ useEffect(() => { const val = isPreview && previewValue !== undefined ? previewValue : storeValue if (typeof val === 'string') { - setUserInfo(val) + setSelectedValue(val) } }, [isPreview, previewValue, storeValue]) @@ -86,11 +107,14 @@ export function UserSelectorInput({
- User selector not supported for service: {serviceId || 'unknown'} + {config.label} selector not supported for service: {serviceId || 'unknown'}
-

This user selector is not yet implemented for {serviceId || 'unknown'}

+

+ This {config.label.toLowerCase()} selector is not yet implemented for{' '} + {serviceId || 'unknown'} +

) @@ -103,16 +127,16 @@ export function UserSelectorInput({ { - setUserInfo(value) + setSelectedValue(value) if (!isPreview) { - onUserSelect?.(value) + onSelect?.(value) } }} /> diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx index eaeecce099..d3f9534d99 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx @@ -24,7 +24,6 @@ import { type OAuthService, } from '@/lib/oauth/oauth' import { - ChannelSelectorInput, CheckboxList, Code, ComboBox, @@ -33,6 +32,7 @@ import { LongInput, ProjectSelectorInput, ShortInput, + SlackSelectorInput, SliderInput, Table, TimeInput, @@ -520,7 +520,7 @@ function ChannelSelectorSyncWrapper({ }) { return ( - diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx index be8d4ac7ab..0fb5dc6eda 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx @@ -5,7 +5,6 @@ import { Button } from '@/components/ui/button' import { cn } from '@/lib/core/utils/cn' import type { FieldDiffStatus } from '@/lib/workflows/diff/types' import { - ChannelSelectorInput, CheckboxList, Code, ComboBox, @@ -32,6 +31,7 @@ import { ResponseFormat, ScheduleSave, ShortInput, + SlackSelectorInput, SliderInput, Switch, Table, @@ -39,7 +39,6 @@ import { TimeInput, ToolInput, TriggerSave, - UserSelectorInput, VariablesInput, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components' import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate' @@ -733,19 +732,9 @@ function SubBlockComponent({ ) case 'channel-selector': - return ( - - ) - case 'user-selector': return ( - Date: Mon, 15 Dec 2025 15:29:23 -0800 Subject: [PATCH 05/13] add scopes correctly --- .../credential-selector/components/oauth-required-modal.tsx | 3 +++ apps/sim/lib/auth/auth.ts | 3 +++ apps/sim/lib/oauth/oauth.ts | 3 +++ 3 files changed, 9 insertions(+) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx index 3a3078c951..818defe02f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx @@ -179,6 +179,9 @@ const SCOPE_DESCRIPTIONS: Record = { 'groups:history': 'Read private messages', 'chat:write': 'Send messages', 'chat:write.public': 'Post to public channels', + 'im:write': 'Send direct messages', + 'im:history': 'Read direct message history', + 'im:read': 'View direct message channels', 'users:read': 'View workspace users', 'files:write': 'Upload files', 'files:read': 'Download and read files', diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts index 0efdee78f5..eec70eaa7f 100644 --- a/apps/sim/lib/auth/auth.ts +++ b/apps/sim/lib/auth/auth.ts @@ -1640,6 +1640,9 @@ export const auth = betterAuth({ 'groups:history', 'chat:write', 'chat:write.public', + 'im:write', + 'im:history', + 'im:read', 'users:read', 'files:write', 'files:read', diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index 7b4f53caaf..847ff59e62 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -637,6 +637,9 @@ export const OAUTH_PROVIDERS: Record = { 'groups:history', 'chat:write', 'chat:write.public', + 'im:write', + 'im:history', + 'im:read', 'users:read', 'files:write', 'files:read', From 7666b1cd3c5ce3a5ea55386e3c020ba45629afbc Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 15 Dec 2025 15:34:41 -0800 Subject: [PATCH 06/13] fix zod validation --- apps/sim/app/api/tools/slack/add-reaction/route.ts | 4 ++-- apps/sim/app/api/tools/slack/delete-message/route.ts | 4 ++-- apps/sim/app/api/tools/slack/read-messages/route.ts | 10 +++++----- apps/sim/app/api/tools/slack/send-message/route.ts | 4 ++-- apps/sim/app/api/tools/slack/update-message/route.ts | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/apps/sim/app/api/tools/slack/add-reaction/route.ts b/apps/sim/app/api/tools/slack/add-reaction/route.ts index f386974949..706ff9da03 100644 --- a/apps/sim/app/api/tools/slack/add-reaction/route.ts +++ b/apps/sim/app/api/tools/slack/add-reaction/route.ts @@ -12,8 +12,8 @@ const logger = createLogger('SlackAddReactionAPI') const SlackAddReactionSchema = z .object({ accessToken: z.string().min(1, 'Access token is required'), - channel: z.string().optional(), - userId: z.string().optional(), + channel: z.string().optional().nullable(), + userId: z.string().optional().nullable(), timestamp: z.string().min(1, 'Message timestamp is required'), name: z.string().min(1, 'Emoji name is required'), }) diff --git a/apps/sim/app/api/tools/slack/delete-message/route.ts b/apps/sim/app/api/tools/slack/delete-message/route.ts index 3bccb1bec4..2a44fe64ee 100644 --- a/apps/sim/app/api/tools/slack/delete-message/route.ts +++ b/apps/sim/app/api/tools/slack/delete-message/route.ts @@ -12,8 +12,8 @@ const logger = createLogger('SlackDeleteMessageAPI') const SlackDeleteMessageSchema = z .object({ accessToken: z.string().min(1, 'Access token is required'), - channel: z.string().optional(), - userId: z.string().optional(), + channel: z.string().optional().nullable(), + userId: z.string().optional().nullable(), timestamp: z.string().min(1, 'Message timestamp is required'), }) .refine((data) => data.channel || data.userId, { diff --git a/apps/sim/app/api/tools/slack/read-messages/route.ts b/apps/sim/app/api/tools/slack/read-messages/route.ts index aafea3b838..d81dbca878 100644 --- a/apps/sim/app/api/tools/slack/read-messages/route.ts +++ b/apps/sim/app/api/tools/slack/read-messages/route.ts @@ -12,11 +12,11 @@ const logger = createLogger('SlackReadMessagesAPI') const SlackReadMessagesSchema = z .object({ accessToken: z.string().min(1, 'Access token is required'), - channel: z.string().optional(), - userId: z.string().optional(), - limit: z.number().optional(), - oldest: z.string().optional(), - latest: z.string().optional(), + channel: z.string().optional().nullable(), + userId: z.string().optional().nullable(), + limit: z.number().optional().nullable(), + oldest: z.string().optional().nullable(), + latest: z.string().optional().nullable(), }) .refine((data) => data.channel || data.userId, { message: 'Either channel or userId is required', diff --git a/apps/sim/app/api/tools/slack/send-message/route.ts b/apps/sim/app/api/tools/slack/send-message/route.ts index cd9ab6fed8..4b498d09ea 100644 --- a/apps/sim/app/api/tools/slack/send-message/route.ts +++ b/apps/sim/app/api/tools/slack/send-message/route.ts @@ -12,8 +12,8 @@ const logger = createLogger('SlackSendMessageAPI') const SlackSendMessageSchema = z .object({ accessToken: z.string().min(1, 'Access token is required'), - channel: z.string().optional(), - userId: z.string().optional(), + channel: z.string().optional().nullable(), + userId: z.string().optional().nullable(), text: z.string().min(1, 'Message text is required'), thread_ts: z.string().optional().nullable(), files: z.array(z.any()).optional().nullable(), diff --git a/apps/sim/app/api/tools/slack/update-message/route.ts b/apps/sim/app/api/tools/slack/update-message/route.ts index 791a9e6725..42832aa1b2 100644 --- a/apps/sim/app/api/tools/slack/update-message/route.ts +++ b/apps/sim/app/api/tools/slack/update-message/route.ts @@ -12,8 +12,8 @@ const logger = createLogger('SlackUpdateMessageAPI') const SlackUpdateMessageSchema = z .object({ accessToken: z.string().min(1, 'Access token is required'), - channel: z.string().optional(), - userId: z.string().optional(), + channel: z.string().optional().nullable(), + userId: z.string().optional().nullable(), timestamp: z.string().min(1, 'Message timestamp is required'), text: z.string().min(1, 'Message text is required'), }) From 6a3c8aac94950b16eb07f5eebc7cfbe307d66fb5 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 15 Dec 2025 16:11:17 -0800 Subject: [PATCH 07/13] update message logs --- apps/sim/app/api/tools/slack/update-message/route.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/sim/app/api/tools/slack/update-message/route.ts b/apps/sim/app/api/tools/slack/update-message/route.ts index 42832aa1b2..d8ea1974dd 100644 --- a/apps/sim/app/api/tools/slack/update-message/route.ts +++ b/apps/sim/app/api/tools/slack/update-message/route.ts @@ -46,6 +46,11 @@ export async function POST(request: NextRequest) { ) const body = await request.json() + logger.info(`[${requestId}] Raw request body:`, { + hasTimestamp: 'timestamp' in body, + timestamp: body.timestamp, + timestampType: typeof body.timestamp, + }) const validatedData = SlackUpdateMessageSchema.parse(body) let channel = validatedData.channel From ac18c35f30f0448341bdf152de6941cd071200fe Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 15 Dec 2025 16:16:45 -0800 Subject: [PATCH 08/13] add console logs --- apps/sim/blocks/blocks/slack.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/sim/blocks/blocks/slack.ts b/apps/sim/blocks/blocks/slack.ts index 189b227ec9..b30885e067 100644 --- a/apps/sim/blocks/blocks/slack.ts +++ b/apps/sim/blocks/blocks/slack.ts @@ -615,6 +615,12 @@ export const SlackBlock: BlockConfig = { } case 'update': + console.log('[Slack Block Debug] update params:', { + updateTimestamp, + updateText, + hasUpdateTimestamp: 'updateTimestamp' in params, + allParamKeys: Object.keys(params), + }) if (!updateTimestamp || !updateText) { throw new Error('Timestamp and text are required for update operation') } From 9f6c403f4c7accb3c3e2a06af47c31221705edd0 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 15 Dec 2025 16:30:41 -0800 Subject: [PATCH 09/13] fix --- apps/sim/app/api/tools/slack/update-message/route.ts | 5 ----- apps/sim/blocks/blocks/slack.ts | 10 ++-------- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/apps/sim/app/api/tools/slack/update-message/route.ts b/apps/sim/app/api/tools/slack/update-message/route.ts index d8ea1974dd..42832aa1b2 100644 --- a/apps/sim/app/api/tools/slack/update-message/route.ts +++ b/apps/sim/app/api/tools/slack/update-message/route.ts @@ -46,11 +46,6 @@ export async function POST(request: NextRequest) { ) const body = await request.json() - logger.info(`[${requestId}] Raw request body:`, { - hasTimestamp: 'timestamp' in body, - timestamp: body.timestamp, - timestampType: typeof body.timestamp, - }) const validatedData = SlackUpdateMessageSchema.parse(body) let channel = validatedData.channel diff --git a/apps/sim/blocks/blocks/slack.ts b/apps/sim/blocks/blocks/slack.ts index b30885e067..db22218614 100644 --- a/apps/sim/blocks/blocks/slack.ts +++ b/apps/sim/blocks/blocks/slack.ts @@ -142,7 +142,7 @@ export const SlackBlock: BlockConfig = { id: 'dmUserId', title: 'User', type: 'user-selector', - canonicalParamId: 'userId', + canonicalParamId: 'dmUserId', serviceId: 'slack', placeholder: 'Select Slack user', mode: 'basic', @@ -156,7 +156,7 @@ export const SlackBlock: BlockConfig = { id: 'manualDmUserId', title: 'User ID', type: 'short-input', - canonicalParamId: 'userId', + canonicalParamId: 'dmUserId', placeholder: 'Enter Slack user ID (e.g., U1234567890)', mode: 'advanced', condition: { @@ -615,12 +615,6 @@ export const SlackBlock: BlockConfig = { } case 'update': - console.log('[Slack Block Debug] update params:', { - updateTimestamp, - updateText, - hasUpdateTimestamp: 'updateTimestamp' in params, - allParamKeys: Object.keys(params), - }) if (!updateTimestamp || !updateText) { throw new Error('Timestamp and text are required for update operation') } From 0873450554b63a45ca41afc44f2796046fd786c4 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 15 Dec 2025 17:00:34 -0800 Subject: [PATCH 10/13] remove from tools where feature not needed --- .../app/api/tools/slack/add-reaction/route.ts | 37 +++++-------------- .../api/tools/slack/delete-message/route.ts | 31 ++++------------ .../api/tools/slack/read-messages/route.ts | 6 --- .../api/tools/slack/update-message/route.ts | 33 ++++------------- apps/sim/blocks/blocks/slack.ts | 4 +- apps/sim/tools/slack/add_reaction.ts | 9 +---- apps/sim/tools/slack/delete_message.ts | 9 +---- apps/sim/tools/slack/types.ts | 9 ++--- apps/sim/tools/slack/update_message.ts | 9 +---- 9 files changed, 33 insertions(+), 114 deletions(-) diff --git a/apps/sim/app/api/tools/slack/add-reaction/route.ts b/apps/sim/app/api/tools/slack/add-reaction/route.ts index 706ff9da03..ab33b29d7c 100644 --- a/apps/sim/app/api/tools/slack/add-reaction/route.ts +++ b/apps/sim/app/api/tools/slack/add-reaction/route.ts @@ -3,23 +3,17 @@ import { z } from 'zod' import { checkHybridAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { createLogger } from '@/lib/logs/console/logger' -import { openDMChannel } from '../utils' export const dynamic = 'force-dynamic' const logger = createLogger('SlackAddReactionAPI') -const SlackAddReactionSchema = z - .object({ - accessToken: z.string().min(1, 'Access token is required'), - channel: z.string().optional().nullable(), - userId: z.string().optional().nullable(), - timestamp: z.string().min(1, 'Message timestamp is required'), - name: z.string().min(1, 'Emoji name is required'), - }) - .refine((data) => data.channel || data.userId, { - message: 'Either channel or userId is required', - }) +const SlackAddReactionSchema = z.object({ + accessToken: z.string().min(1, 'Access token is required'), + channel: z.string().min(1, 'Channel is required'), + timestamp: z.string().min(1, 'Message timestamp is required'), + name: z.string().min(1, 'Emoji name is required'), +}) export async function POST(request: NextRequest) { const requestId = generateRequestId() @@ -48,19 +42,8 @@ export async function POST(request: NextRequest) { const body = await request.json() const validatedData = SlackAddReactionSchema.parse(body) - let channel = validatedData.channel - if (!channel && validatedData.userId) { - logger.info(`[${requestId}] Opening DM channel for user: ${validatedData.userId}`) - channel = await openDMChannel( - validatedData.accessToken, - validatedData.userId, - requestId, - logger - ) - } - logger.info(`[${requestId}] Adding Slack reaction`, { - channel, + channel: validatedData.channel, timestamp: validatedData.timestamp, emoji: validatedData.name, }) @@ -72,7 +55,7 @@ export async function POST(request: NextRequest) { Authorization: `Bearer ${validatedData.accessToken}`, }, body: JSON.stringify({ - channel, + channel: validatedData.channel, timestamp: validatedData.timestamp, name: validatedData.name, }), @@ -92,7 +75,7 @@ export async function POST(request: NextRequest) { } logger.info(`[${requestId}] Reaction added successfully`, { - channel, + channel: validatedData.channel, timestamp: validatedData.timestamp, reaction: validatedData.name, }) @@ -102,7 +85,7 @@ export async function POST(request: NextRequest) { output: { content: `Successfully added :${validatedData.name}: reaction`, metadata: { - channel, + channel: validatedData.channel, timestamp: validatedData.timestamp, reaction: validatedData.name, }, diff --git a/apps/sim/app/api/tools/slack/delete-message/route.ts b/apps/sim/app/api/tools/slack/delete-message/route.ts index 2a44fe64ee..50dc592024 100644 --- a/apps/sim/app/api/tools/slack/delete-message/route.ts +++ b/apps/sim/app/api/tools/slack/delete-message/route.ts @@ -3,22 +3,16 @@ import { z } from 'zod' import { checkHybridAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { createLogger } from '@/lib/logs/console/logger' -import { openDMChannel } from '../utils' export const dynamic = 'force-dynamic' const logger = createLogger('SlackDeleteMessageAPI') -const SlackDeleteMessageSchema = z - .object({ - accessToken: z.string().min(1, 'Access token is required'), - channel: z.string().optional().nullable(), - userId: z.string().optional().nullable(), - timestamp: z.string().min(1, 'Message timestamp is required'), - }) - .refine((data) => data.channel || data.userId, { - message: 'Either channel or userId is required', - }) +const SlackDeleteMessageSchema = z.object({ + accessToken: z.string().min(1, 'Access token is required'), + channel: z.string().min(1, 'Channel is required'), + timestamp: z.string().min(1, 'Message timestamp is required'), +}) export async function POST(request: NextRequest) { const requestId = generateRequestId() @@ -47,19 +41,8 @@ export async function POST(request: NextRequest) { const body = await request.json() const validatedData = SlackDeleteMessageSchema.parse(body) - let channel = validatedData.channel - if (!channel && validatedData.userId) { - logger.info(`[${requestId}] Opening DM channel for user: ${validatedData.userId}`) - channel = await openDMChannel( - validatedData.accessToken, - validatedData.userId, - requestId, - logger - ) - } - logger.info(`[${requestId}] Deleting Slack message`, { - channel, + channel: validatedData.channel, timestamp: validatedData.timestamp, }) @@ -70,7 +53,7 @@ export async function POST(request: NextRequest) { Authorization: `Bearer ${validatedData.accessToken}`, }, body: JSON.stringify({ - channel, + channel: validatedData.channel, ts: validatedData.timestamp, }), }) diff --git a/apps/sim/app/api/tools/slack/read-messages/route.ts b/apps/sim/app/api/tools/slack/read-messages/route.ts index d81dbca878..74d9d9742d 100644 --- a/apps/sim/app/api/tools/slack/read-messages/route.ts +++ b/apps/sim/app/api/tools/slack/read-messages/route.ts @@ -47,12 +47,6 @@ export async function POST(request: NextRequest) { ) const body = await request.json() - - logger.info(`[${requestId}] Raw request body`, { - limit: body.limit, - limitType: typeof body.limit, - }) - const validatedData = SlackReadMessagesSchema.parse(body) let channel = validatedData.channel diff --git a/apps/sim/app/api/tools/slack/update-message/route.ts b/apps/sim/app/api/tools/slack/update-message/route.ts index 42832aa1b2..d89f9b0a9f 100644 --- a/apps/sim/app/api/tools/slack/update-message/route.ts +++ b/apps/sim/app/api/tools/slack/update-message/route.ts @@ -3,23 +3,17 @@ import { z } from 'zod' import { checkHybridAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { createLogger } from '@/lib/logs/console/logger' -import { openDMChannel } from '../utils' export const dynamic = 'force-dynamic' const logger = createLogger('SlackUpdateMessageAPI') -const SlackUpdateMessageSchema = z - .object({ - accessToken: z.string().min(1, 'Access token is required'), - channel: z.string().optional().nullable(), - userId: z.string().optional().nullable(), - timestamp: z.string().min(1, 'Message timestamp is required'), - text: z.string().min(1, 'Message text is required'), - }) - .refine((data) => data.channel || data.userId, { - message: 'Either channel or userId is required', - }) +const SlackUpdateMessageSchema = z.object({ + accessToken: z.string().min(1, 'Access token is required'), + channel: z.string().min(1, 'Channel is required'), + timestamp: z.string().min(1, 'Message timestamp is required'), + text: z.string().min(1, 'Message text is required'), +}) export async function POST(request: NextRequest) { const requestId = generateRequestId() @@ -48,19 +42,8 @@ export async function POST(request: NextRequest) { const body = await request.json() const validatedData = SlackUpdateMessageSchema.parse(body) - let channel = validatedData.channel - if (!channel && validatedData.userId) { - logger.info(`[${requestId}] Opening DM channel for user: ${validatedData.userId}`) - channel = await openDMChannel( - validatedData.accessToken, - validatedData.userId, - requestId, - logger - ) - } - logger.info(`[${requestId}] Updating Slack message`, { - channel, + channel: validatedData.channel, timestamp: validatedData.timestamp, }) @@ -71,7 +54,7 @@ export async function POST(request: NextRequest) { Authorization: `Bearer ${validatedData.accessToken}`, }, body: JSON.stringify({ - channel, + channel: validatedData.channel, ts: validatedData.timestamp, text: validatedData.text, }), diff --git a/apps/sim/blocks/blocks/slack.ts b/apps/sim/blocks/blocks/slack.ts index db22218614..d8cdb7e140 100644 --- a/apps/sim/blocks/blocks/slack.ts +++ b/apps/sim/blocks/blocks/slack.ts @@ -59,7 +59,7 @@ export const SlackBlock: BlockConfig = { value: () => 'channel', condition: { field: 'operation', - value: ['send', 'read', 'update', 'delete', 'react'], + value: ['send', 'read'], }, }, { @@ -496,7 +496,7 @@ export const SlackBlock: BlockConfig = { const effectiveUserId = (dmUserId || manualDmUserId || '').trim() const noChannelOperations = ['list_channels', 'list_users', 'get_user'] - const dmSupportedOperations = ['send', 'read', 'update', 'delete', 'react'] + const dmSupportedOperations = ['send', 'read'] if (isDM && dmSupportedOperations.includes(operation)) { if (!effectiveUserId) { diff --git a/apps/sim/tools/slack/add_reaction.ts b/apps/sim/tools/slack/add_reaction.ts index 2103a28032..22b955d945 100644 --- a/apps/sim/tools/slack/add_reaction.ts +++ b/apps/sim/tools/slack/add_reaction.ts @@ -33,16 +33,10 @@ export const slackAddReactionTool: ToolConfig ({ accessToken: params.accessToken || params.botToken, channel: params.channel, - userId: params.userId, timestamp: params.timestamp, name: params.name, }), diff --git a/apps/sim/tools/slack/delete_message.ts b/apps/sim/tools/slack/delete_message.ts index 672f27ac0d..6d30f4970d 100644 --- a/apps/sim/tools/slack/delete_message.ts +++ b/apps/sim/tools/slack/delete_message.ts @@ -36,16 +36,10 @@ export const slackDeleteMessageTool: ToolConfig< }, channel: { type: 'string', - required: false, + required: true, visibility: 'user-only', description: 'Channel ID where the message was posted (e.g., C1234567890)', }, - userId: { - type: 'string', - required: false, - visibility: 'user-only', - description: 'User ID for DM messages (e.g., U1234567890)', - }, timestamp: { type: 'string', required: true, @@ -63,7 +57,6 @@ export const slackDeleteMessageTool: ToolConfig< body: (params: SlackDeleteMessageParams) => ({ accessToken: params.accessToken || params.botToken, channel: params.channel, - userId: params.userId, timestamp: params.timestamp, }), }, diff --git a/apps/sim/tools/slack/types.ts b/apps/sim/tools/slack/types.ts index 9a1784df88..b6eada4c99 100644 --- a/apps/sim/tools/slack/types.ts +++ b/apps/sim/tools/slack/types.ts @@ -35,21 +35,18 @@ export interface SlackDownloadParams extends SlackBaseParams { } export interface SlackUpdateMessageParams extends SlackBaseParams { - channel?: string - userId?: string + channel: string timestamp: string text: string } export interface SlackDeleteMessageParams extends SlackBaseParams { - channel?: string - userId?: string + channel: string timestamp: string } export interface SlackAddReactionParams extends SlackBaseParams { - channel?: string - userId?: string + channel: string timestamp: string name: string } diff --git a/apps/sim/tools/slack/update_message.ts b/apps/sim/tools/slack/update_message.ts index fe63a3d76c..27f197a36e 100644 --- a/apps/sim/tools/slack/update_message.ts +++ b/apps/sim/tools/slack/update_message.ts @@ -36,16 +36,10 @@ export const slackUpdateMessageTool: ToolConfig< }, channel: { type: 'string', - required: false, + required: true, visibility: 'user-only', description: 'Channel ID where the message was posted (e.g., C1234567890)', }, - userId: { - type: 'string', - required: false, - visibility: 'user-only', - description: 'User ID for DM messages (e.g., U1234567890)', - }, timestamp: { type: 'string', required: true, @@ -69,7 +63,6 @@ export const slackUpdateMessageTool: ToolConfig< body: (params: SlackUpdateMessageParams) => ({ accessToken: params.accessToken || params.botToken, channel: params.channel, - userId: params.userId, timestamp: params.timestamp, text: params.text, }), From faaee983a043829280e631ab4c45c0094d7c3bb6 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 15 Dec 2025 17:07:06 -0800 Subject: [PATCH 11/13] add correct condition --- .../app/api/tools/slack/add-reaction/route.ts | 29 ------------------- .../api/tools/slack/delete-message/route.ts | 27 ----------------- apps/sim/blocks/blocks/slack.ts | 6 ++-- 3 files changed, 4 insertions(+), 58 deletions(-) diff --git a/apps/sim/app/api/tools/slack/add-reaction/route.ts b/apps/sim/app/api/tools/slack/add-reaction/route.ts index ab33b29d7c..79a48008bf 100644 --- a/apps/sim/app/api/tools/slack/add-reaction/route.ts +++ b/apps/sim/app/api/tools/slack/add-reaction/route.ts @@ -1,13 +1,9 @@ import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkHybridAuth } from '@/lib/auth/hybrid' -import { generateRequestId } from '@/lib/core/utils/request' -import { createLogger } from '@/lib/logs/console/logger' export const dynamic = 'force-dynamic' -const logger = createLogger('SlackAddReactionAPI') - const SlackAddReactionSchema = z.object({ accessToken: z.string().min(1, 'Access token is required'), channel: z.string().min(1, 'Channel is required'), @@ -16,13 +12,10 @@ const SlackAddReactionSchema = z.object({ }) export async function POST(request: NextRequest) { - const requestId = generateRequestId() - try { const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) if (!authResult.success) { - logger.warn(`[${requestId}] Unauthorized Slack add reaction attempt: ${authResult.error}`) return NextResponse.json( { success: false, @@ -32,22 +25,9 @@ export async function POST(request: NextRequest) { ) } - logger.info( - `[${requestId}] Authenticated Slack add reaction request via ${authResult.authType}`, - { - userId: authResult.userId, - } - ) - const body = await request.json() const validatedData = SlackAddReactionSchema.parse(body) - logger.info(`[${requestId}] Adding Slack reaction`, { - channel: validatedData.channel, - timestamp: validatedData.timestamp, - emoji: validatedData.name, - }) - const slackResponse = await fetch('https://slack.com/api/reactions.add', { method: 'POST', headers: { @@ -64,7 +44,6 @@ export async function POST(request: NextRequest) { const data = await slackResponse.json() if (!data.ok) { - logger.error(`[${requestId}] Slack API error:`, data) return NextResponse.json( { success: false, @@ -74,12 +53,6 @@ export async function POST(request: NextRequest) { ) } - logger.info(`[${requestId}] Reaction added successfully`, { - channel: validatedData.channel, - timestamp: validatedData.timestamp, - reaction: validatedData.name, - }) - return NextResponse.json({ success: true, output: { @@ -93,7 +66,6 @@ export async function POST(request: NextRequest) { }) } catch (error) { if (error instanceof z.ZodError) { - logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) return NextResponse.json( { success: false, @@ -104,7 +76,6 @@ export async function POST(request: NextRequest) { ) } - logger.error(`[${requestId}] Error adding Slack reaction:`, error) return NextResponse.json( { success: false, diff --git a/apps/sim/app/api/tools/slack/delete-message/route.ts b/apps/sim/app/api/tools/slack/delete-message/route.ts index 50dc592024..25cea4c014 100644 --- a/apps/sim/app/api/tools/slack/delete-message/route.ts +++ b/apps/sim/app/api/tools/slack/delete-message/route.ts @@ -1,13 +1,9 @@ import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkHybridAuth } from '@/lib/auth/hybrid' -import { generateRequestId } from '@/lib/core/utils/request' -import { createLogger } from '@/lib/logs/console/logger' export const dynamic = 'force-dynamic' -const logger = createLogger('SlackDeleteMessageAPI') - const SlackDeleteMessageSchema = z.object({ accessToken: z.string().min(1, 'Access token is required'), channel: z.string().min(1, 'Channel is required'), @@ -15,13 +11,10 @@ const SlackDeleteMessageSchema = z.object({ }) export async function POST(request: NextRequest) { - const requestId = generateRequestId() - try { const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) if (!authResult.success) { - logger.warn(`[${requestId}] Unauthorized Slack delete message attempt: ${authResult.error}`) return NextResponse.json( { success: false, @@ -31,21 +24,9 @@ export async function POST(request: NextRequest) { ) } - logger.info( - `[${requestId}] Authenticated Slack delete message request via ${authResult.authType}`, - { - userId: authResult.userId, - } - ) - const body = await request.json() const validatedData = SlackDeleteMessageSchema.parse(body) - logger.info(`[${requestId}] Deleting Slack message`, { - channel: validatedData.channel, - timestamp: validatedData.timestamp, - }) - const slackResponse = await fetch('https://slack.com/api/chat.delete', { method: 'POST', headers: { @@ -61,7 +42,6 @@ export async function POST(request: NextRequest) { const data = await slackResponse.json() if (!data.ok) { - logger.error(`[${requestId}] Slack API error:`, data) return NextResponse.json( { success: false, @@ -71,11 +51,6 @@ export async function POST(request: NextRequest) { ) } - logger.info(`[${requestId}] Message deleted successfully`, { - channel: data.channel, - timestamp: data.ts, - }) - return NextResponse.json({ success: true, output: { @@ -88,7 +63,6 @@ export async function POST(request: NextRequest) { }) } catch (error) { if (error instanceof z.ZodError) { - logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) return NextResponse.json( { success: false, @@ -99,7 +73,6 @@ export async function POST(request: NextRequest) { ) } - logger.error(`[${requestId}] Error deleting Slack message:`, error) return NextResponse.json( { success: false, diff --git a/apps/sim/blocks/blocks/slack.ts b/apps/sim/blocks/blocks/slack.ts index d8cdb7e140..f5c16c1b88 100644 --- a/apps/sim/blocks/blocks/slack.ts +++ b/apps/sim/blocks/blocks/slack.ts @@ -117,7 +117,8 @@ export const SlackBlock: BlockConfig = { not: true, and: { field: 'destinationType', - value: 'channel', + value: 'dm', + not: true, }, }, }, @@ -134,7 +135,8 @@ export const SlackBlock: BlockConfig = { not: true, and: { field: 'destinationType', - value: 'channel', + value: 'dm', + not: true, }, }, }, From 58d6644c734c0db5f9e2fef863f246d026e030a6 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 15 Dec 2025 17:14:38 -0800 Subject: [PATCH 12/13] fix type --- apps/sim/app/api/tools/slack/send-message/route.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/sim/app/api/tools/slack/send-message/route.ts b/apps/sim/app/api/tools/slack/send-message/route.ts index 4b498d09ea..592721d0de 100644 --- a/apps/sim/app/api/tools/slack/send-message/route.ts +++ b/apps/sim/app/api/tools/slack/send-message/route.ts @@ -58,11 +58,11 @@ export async function POST(request: NextRequest) { const result = await sendSlackMessage( { accessToken: validatedData.accessToken, - channel: validatedData.channel, - userId: validatedData.userId, + channel: validatedData.channel ?? undefined, + userId: validatedData.userId ?? undefined, text: validatedData.text, - threadTs: validatedData.thread_ts, - files: validatedData.files, + threadTs: validatedData.thread_ts ?? undefined, + files: validatedData.files ?? undefined, }, requestId, logger From d8499aa8b9fc615312a585447f495f5ee6516d16 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 15 Dec 2025 17:30:15 -0800 Subject: [PATCH 13/13] fix cond eval logic --- apps/sim/serializer/index.ts | 120 ++++++++++++++++++++--------------- 1 file changed, 68 insertions(+), 52 deletions(-) diff --git a/apps/sim/serializer/index.ts b/apps/sim/serializer/index.ts index d61c3c344b..db9401824c 100644 --- a/apps/sim/serializer/index.ts +++ b/apps/sim/serializer/index.ts @@ -38,6 +38,57 @@ function shouldIncludeField(subBlockConfig: SubBlockConfig, isAdvancedMode: bool return true } +/** + * Evaluates a condition object against current field values. + * Used to determine if a conditionally-visible field should be included in params. + */ +function evaluateCondition( + condition: + | { + field: string + value: any + not?: boolean + and?: { field: string; value: any; not?: boolean } + } + | (() => { + field: string + value: any + not?: boolean + and?: { field: string; value: any; not?: boolean } + }) + | undefined, + values: Record +): boolean { + if (!condition) return true + + const actual = typeof condition === 'function' ? condition() : condition + const fieldValue = values[actual.field] + + const valueMatch = Array.isArray(actual.value) + ? fieldValue != null && + (actual.not ? !actual.value.includes(fieldValue) : actual.value.includes(fieldValue)) + : actual.not + ? fieldValue !== actual.value + : fieldValue === actual.value + + const andMatch = !actual.and + ? true + : (() => { + const andFieldValue = values[actual.and!.field] + const andValueMatch = Array.isArray(actual.and!.value) + ? andFieldValue != null && + (actual.and!.not + ? !actual.and!.value.includes(andFieldValue) + : actual.and!.value.includes(andFieldValue)) + : actual.and!.not + ? andFieldValue !== actual.and!.value + : andFieldValue === actual.and!.value + return andValueMatch + })() + + return valueMatch && andMatch +} + /** * Helper function to migrate agent block params from old format to messages array * Transforms systemPrompt/userPrompt into messages array format @@ -343,9 +394,15 @@ export class Serializer { const isStarterBlock = block.type === 'starter' const isAgentBlock = block.type === 'agent' - // First collect all current values from subBlocks, filtering by mode + // First pass: collect ALL raw values for condition evaluation + const allValues: Record = {} Object.entries(block.subBlocks).forEach(([id, subBlock]) => { - // Find the corresponding subblock config to check its mode + allValues[id] = subBlock.value + }) + + // Second pass: filter by mode and conditions + Object.entries(block.subBlocks).forEach(([id, subBlock]) => { + // Find the corresponding subblock config to check its mode and condition const subBlockConfig = blockConfig.subBlocks.find((config) => config.id === id) // Include field if it matches current mode OR if it's the starter inputFormat with values @@ -360,9 +417,14 @@ export class Serializer { const isLegacyAgentField = isAgentBlock && ['systemPrompt', 'userPrompt', 'memories'].includes(id) + // Check if field's condition is met (conditionally-hidden fields should be excluded) + const conditionMet = subBlockConfig + ? evaluateCondition(subBlockConfig.condition, allValues) + : true + if ( - (subBlockConfig && - (shouldIncludeField(subBlockConfig, isAdvancedMode) || hasStarterInputFormatValues)) || + (subBlockConfig && shouldIncludeField(subBlockConfig, isAdvancedMode) && conditionMet) || + hasStarterInputFormatValues || isLegacyAgentField ) { params[id] = subBlock.value @@ -475,52 +537,6 @@ export class Serializer { // Check required user-only parameters for the current tool const missingFields: string[] = [] - // Helper function to evaluate conditions - const evalCond = ( - condition: - | { - field: string - value: any - not?: boolean - and?: { field: string; value: any; not?: boolean } - } - | (() => { - field: string - value: any - not?: boolean - and?: { field: string; value: any; not?: boolean } - }) - | undefined, - values: Record - ): boolean => { - if (!condition) return true - const actual = typeof condition === 'function' ? condition() : condition - const fieldValue = values[actual.field] - - const valueMatch = Array.isArray(actual.value) - ? fieldValue != null && - (actual.not ? !actual.value.includes(fieldValue) : actual.value.includes(fieldValue)) - : actual.not - ? fieldValue !== actual.value - : fieldValue === actual.value - - const andMatch = !actual.and - ? true - : (() => { - const andFieldValue = values[actual.and!.field] - return Array.isArray(actual.and!.value) - ? andFieldValue != null && - (actual.and!.not - ? !actual.and!.value.includes(andFieldValue) - : actual.and!.value.includes(andFieldValue)) - : actual.and!.not - ? andFieldValue !== actual.and!.value - : andFieldValue === actual.and!.value - })() - - return valueMatch && andMatch - } - // Iterate through the tool's parameters, not the block's subBlocks Object.entries(currentTool.params || {}).forEach(([paramId, paramConfig]) => { if (paramConfig.required && paramConfig.visibility === 'user-only') { @@ -533,14 +549,14 @@ export class Serializer { const includedByMode = shouldIncludeField(subBlockConfig, isAdvancedMode) // Check visibility condition - const includedByCondition = evalCond(subBlockConfig.condition, params) + const includedByCondition = evaluateCondition(subBlockConfig.condition, params) // Check if field is required based on its required condition (if it's a condition object) const isRequired = (() => { if (!subBlockConfig.required) return false if (typeof subBlockConfig.required === 'boolean') return subBlockConfig.required // If required is a condition object, evaluate it - return evalCond(subBlockConfig.required, params) + return evaluateCondition(subBlockConfig.required, params) })() shouldValidateParam = includedByMode && includedByCondition && isRequired