Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions apps/sim/app/api/tools/slack/ephemeral-message/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { openDMChannel, postSlackEphemeralMessage } from '../utils'

export const dynamic = 'force-dynamic'

const logger = createLogger('SlackEphemeralMessageAPI')

const SlackEphemeralMessageSchema = z
.object({
accessToken: z.string().min(1, 'Access token is required'),
channel: z.string().optional().nullable(),
dmUserId: z.string().optional().nullable(),
userId: z.string().min(1, 'User ID is required'),
text: z.string().min(1, 'Message text is required'),
thread_ts: z.string().optional().nullable(),
})
.refine((data) => data.channel || data.dmUserId, {
message: 'Either channel or dmUserId is required',
})

export async function POST(request: NextRequest) {
const requestId = generateRequestId()

try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })

if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized Slack ephemeral send attempt: ${authResult.error}`)
return NextResponse.json(
{
success: false,
error: authResult.error || 'Authentication required',
},
{ status: 401 }
)
}

logger.info(
`[${requestId}] Authenticated Slack ephemeral send request via ${authResult.authType}`,
{
userId: authResult.userId,
}
)

const body = await request.json()
const validatedData = SlackEphemeralMessageSchema.parse(body)

let channel = validatedData.channel

if (!channel && validatedData.dmUserId) {
logger.info(`[${requestId}] Opening DM channel for user: ${validatedData.dmUserId}`)
channel = await openDMChannel(
validatedData.accessToken,
validatedData.dmUserId,
requestId,
logger
)
}

if (!channel) {
return NextResponse.json(
{ success: false, error: 'Either channel or dmUserId is required' },
{ status: 400 }
)
}

logger.info(`[${requestId}] Sending Slack ephemeral message`, {
channel,
targetUser: validatedData.userId,
hasThread: !!validatedData.thread_ts,
})

const result = await postSlackEphemeralMessage(
validatedData.accessToken,
channel,
validatedData.userId,
validatedData.text,
validatedData.thread_ts
)

if (!result.ok) {
logger.error(`[${requestId}] Slack API error:`, result.error)
return NextResponse.json(
{ success: false, error: result.error || 'Failed to send ephemeral message' },
{ status: 400 }
)
}

logger.info(`[${requestId}] Ephemeral message sent successfully`)

return NextResponse.json({
success: true,
output: {
message_ts: result.message_ts,
channel,
user: validatedData.userId,
},
})
} catch (error) {
logger.error(`[${requestId}] Error sending Slack ephemeral message:`, error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred',
},
{ status: 500 }
)
}
}
28 changes: 28 additions & 0 deletions apps/sim/app/api/tools/slack/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,34 @@ export async function postSlackMessage(
return response.json()
}

/**
* Sends an ephemeral message to a Slack channel using chat.postEphemeral
* Ephemeral messages are only visible to the specified user and do not persist
*/
export async function postSlackEphemeralMessage(
accessToken: string,
channel: string,
user: string,
text: string,
threadTs?: string | null
): Promise<{ ok: boolean; message_ts?: string; error?: string }> {
const response = await fetch('https://slack.com/api/chat.postEphemeral', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({
channel,
user,
text,
...(threadTs && { thread_ts: threadTs }),
}),
})

return response.json()
}

/**
* Creates a default message object when the API doesn't return one
*/
Expand Down
2 changes: 2 additions & 0 deletions apps/sim/tools/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1541,6 +1541,7 @@ import {
slackCanvasTool,
slackDeleteMessageTool,
slackDownloadTool,
slackEphemeralMessageTool,
slackGetMessageTool,
slackGetThreadTool,
slackGetUserTool,
Expand Down Expand Up @@ -2208,6 +2209,7 @@ export const tools: Record<string, ToolConfig> = {
polymarket_get_holders: polymarketGetHoldersTool,
slack_message: slackMessageTool,
slack_message_reader: slackMessageReaderTool,
slack_ephemeral_message: slackEphemeralMessageTool,
slack_list_channels: slackListChannelsTool,
slack_list_members: slackListMembersTool,
slack_list_users: slackListUsersTool,
Expand Down
111 changes: 111 additions & 0 deletions apps/sim/tools/slack/ephemeral_message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import type { SlackEphemeralMessageParams, SlackEphemeralMessageResponse } from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'

export const slackEphemeralMessageTool: ToolConfig<
SlackEphemeralMessageParams,
SlackEphemeralMessageResponse
> = {
id: 'slack_ephemeral_message',
name: 'Slack Ephemeral Message',
description:
'Send ephemeral messages visible only to a specific user in Slack channels or threads. Messages are temporary and do not persist across sessions.',
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',
},
destinationType: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Destination type: channel or dm',
},
botToken: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Bot token for Custom Bot',
},
accessToken: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'OAuth access token or bot token for Slack API',
},
channel: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Slack channel ID (e.g., C1234567890)',
},
dmUserId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Slack user ID for direct messages (e.g., U1234567890)',
},
userId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'User ID who will see the ephemeral message (e.g., U1234567890)',
},
text: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Message text to send (supports Slack mrkdwn formatting)',
},
threadTs: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Thread timestamp to reply to (creates ephemeral thread reply)',
},
},

request: {
url: '/api/tools/slack/ephemeral-message',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params: SlackEphemeralMessageParams) => {
const isDM = params.destinationType === 'dm'
return {
accessToken: params.accessToken || params.botToken,
channel: isDM ? undefined : params.channel,
dmUserId: isDM ? params.dmUserId : undefined,
userId: params.userId,
text: params.text,
thread_ts: params.threadTs || undefined,
}
},
},

transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
throw new Error(data.error || 'Failed to send Slack ephemeral message')
}
return {
success: true,
output: data.output,
}
},

outputs: {
message_ts: { type: 'string', description: 'Ephemeral message timestamp' },
channel: { type: 'string', description: 'Channel ID where message was sent' },
user: { type: 'string', description: 'User ID who received the ephemeral message' },
},
}
2 changes: 2 additions & 0 deletions apps/sim/tools/slack/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { slackAddReactionTool } from '@/tools/slack/add_reaction'
import { slackCanvasTool } from '@/tools/slack/canvas'
import { slackDeleteMessageTool } from '@/tools/slack/delete_message'
import { slackDownloadTool } from '@/tools/slack/download'
import { slackEphemeralMessageTool } from '@/tools/slack/ephemeral_message'
import { slackGetMessageTool } from '@/tools/slack/get_message'
import { slackGetThreadTool } from '@/tools/slack/get_thread'
import { slackGetUserTool } from '@/tools/slack/get_user'
Expand All @@ -26,4 +27,5 @@ export {
slackGetUserTool,
slackGetMessageTool,
slackGetThreadTool,
slackEphemeralMessageTool,
}
18 changes: 18 additions & 0 deletions apps/sim/tools/slack/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,15 @@ export interface SlackGetThreadParams extends SlackBaseParams {
limit?: number
}

export interface SlackEphemeralMessageParams extends SlackBaseParams {
destinationType?: 'channel' | 'dm'
channel?: string
dmUserId?: string
userId: string
text: string
threadTs?: string
}

export interface SlackMessageResponse extends ToolResponse {
output: {
// Legacy properties for backward compatibility
Expand Down Expand Up @@ -841,6 +850,14 @@ export interface SlackGetThreadResponse extends ToolResponse {
}
}

export interface SlackEphemeralMessageResponse extends ToolResponse {
output: {
message_ts: string
channel: string
user: string
}
}

export type SlackResponse =
| SlackCanvasResponse
| SlackMessageReaderResponse
Expand All @@ -855,3 +872,4 @@ export type SlackResponse =
| SlackGetUserResponse
| SlackGetMessageResponse
| SlackGetThreadResponse
| SlackEphemeralMessageResponse
Loading