Skip to content

Commit 300aaa5

Browse files
feat(slack): ability to have DM channels as destination for slack tools (#2388)
* feat(slack): tool to allow dms * don't make new tool but separate out destination * add log for message limit * consolidate slack selector code * add scopes correctly * fix zod validation * update message logs * add console logs * fix * remove from tools where feature not needed * add correct condition * fix type * fix cond eval logic
1 parent bdcc42e commit 300aaa5

File tree

23 files changed

+911
-460
lines changed

23 files changed

+911
-460
lines changed

apps/sim/app/api/tools/slack/add-reaction/route.ts

Lines changed: 1 addition & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,21 @@
11
import { type NextRequest, NextResponse } from 'next/server'
22
import { z } from 'zod'
33
import { checkHybridAuth } from '@/lib/auth/hybrid'
4-
import { generateRequestId } from '@/lib/core/utils/request'
5-
import { createLogger } from '@/lib/logs/console/logger'
64

75
export const dynamic = 'force-dynamic'
86

9-
const logger = createLogger('SlackAddReactionAPI')
10-
117
const SlackAddReactionSchema = z.object({
128
accessToken: z.string().min(1, 'Access token is required'),
13-
channel: z.string().min(1, 'Channel ID is required'),
9+
channel: z.string().min(1, 'Channel is required'),
1410
timestamp: z.string().min(1, 'Message timestamp is required'),
1511
name: z.string().min(1, 'Emoji name is required'),
1612
})
1713

1814
export async function POST(request: NextRequest) {
19-
const requestId = generateRequestId()
20-
2115
try {
2216
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
2317

2418
if (!authResult.success) {
25-
logger.warn(`[${requestId}] Unauthorized Slack add reaction attempt: ${authResult.error}`)
2619
return NextResponse.json(
2720
{
2821
success: false,
@@ -32,22 +25,9 @@ export async function POST(request: NextRequest) {
3225
)
3326
}
3427

35-
logger.info(
36-
`[${requestId}] Authenticated Slack add reaction request via ${authResult.authType}`,
37-
{
38-
userId: authResult.userId,
39-
}
40-
)
41-
4228
const body = await request.json()
4329
const validatedData = SlackAddReactionSchema.parse(body)
4430

45-
logger.info(`[${requestId}] Adding Slack reaction`, {
46-
channel: validatedData.channel,
47-
timestamp: validatedData.timestamp,
48-
emoji: validatedData.name,
49-
})
50-
5131
const slackResponse = await fetch('https://slack.com/api/reactions.add', {
5232
method: 'POST',
5333
headers: {
@@ -64,7 +44,6 @@ export async function POST(request: NextRequest) {
6444
const data = await slackResponse.json()
6545

6646
if (!data.ok) {
67-
logger.error(`[${requestId}] Slack API error:`, data)
6847
return NextResponse.json(
6948
{
7049
success: false,
@@ -74,12 +53,6 @@ export async function POST(request: NextRequest) {
7453
)
7554
}
7655

77-
logger.info(`[${requestId}] Reaction added successfully`, {
78-
channel: validatedData.channel,
79-
timestamp: validatedData.timestamp,
80-
reaction: validatedData.name,
81-
})
82-
8356
return NextResponse.json({
8457
success: true,
8558
output: {
@@ -93,7 +66,6 @@ export async function POST(request: NextRequest) {
9366
})
9467
} catch (error) {
9568
if (error instanceof z.ZodError) {
96-
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
9769
return NextResponse.json(
9870
{
9971
success: false,
@@ -104,7 +76,6 @@ export async function POST(request: NextRequest) {
10476
)
10577
}
10678

107-
logger.error(`[${requestId}] Error adding Slack reaction:`, error)
10879
return NextResponse.json(
10980
{
11081
success: false,

apps/sim/app/api/tools/slack/delete-message/route.ts

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,20 @@
11
import { type NextRequest, NextResponse } from 'next/server'
22
import { z } from 'zod'
33
import { checkHybridAuth } from '@/lib/auth/hybrid'
4-
import { generateRequestId } from '@/lib/core/utils/request'
5-
import { createLogger } from '@/lib/logs/console/logger'
64

75
export const dynamic = 'force-dynamic'
86

9-
const logger = createLogger('SlackDeleteMessageAPI')
10-
117
const SlackDeleteMessageSchema = z.object({
128
accessToken: z.string().min(1, 'Access token is required'),
13-
channel: z.string().min(1, 'Channel ID is required'),
9+
channel: z.string().min(1, 'Channel is required'),
1410
timestamp: z.string().min(1, 'Message timestamp is required'),
1511
})
1612

1713
export async function POST(request: NextRequest) {
18-
const requestId = generateRequestId()
19-
2014
try {
2115
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
2216

2317
if (!authResult.success) {
24-
logger.warn(`[${requestId}] Unauthorized Slack delete message attempt: ${authResult.error}`)
2518
return NextResponse.json(
2619
{
2720
success: false,
@@ -31,21 +24,9 @@ export async function POST(request: NextRequest) {
3124
)
3225
}
3326

34-
logger.info(
35-
`[${requestId}] Authenticated Slack delete message request via ${authResult.authType}`,
36-
{
37-
userId: authResult.userId,
38-
}
39-
)
40-
4127
const body = await request.json()
4228
const validatedData = SlackDeleteMessageSchema.parse(body)
4329

44-
logger.info(`[${requestId}] Deleting Slack message`, {
45-
channel: validatedData.channel,
46-
timestamp: validatedData.timestamp,
47-
})
48-
4930
const slackResponse = await fetch('https://slack.com/api/chat.delete', {
5031
method: 'POST',
5132
headers: {
@@ -61,7 +42,6 @@ export async function POST(request: NextRequest) {
6142
const data = await slackResponse.json()
6243

6344
if (!data.ok) {
64-
logger.error(`[${requestId}] Slack API error:`, data)
6545
return NextResponse.json(
6646
{
6747
success: false,
@@ -71,11 +51,6 @@ export async function POST(request: NextRequest) {
7151
)
7252
}
7353

74-
logger.info(`[${requestId}] Message deleted successfully`, {
75-
channel: data.channel,
76-
timestamp: data.ts,
77-
})
78-
7954
return NextResponse.json({
8055
success: true,
8156
output: {
@@ -88,7 +63,6 @@ export async function POST(request: NextRequest) {
8863
})
8964
} catch (error) {
9065
if (error instanceof z.ZodError) {
91-
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
9266
return NextResponse.json(
9367
{
9468
success: false,
@@ -99,7 +73,6 @@ export async function POST(request: NextRequest) {
9973
)
10074
}
10175

102-
logger.error(`[${requestId}] Error deleting Slack message:`, error)
10376
return NextResponse.json(
10477
{
10578
success: false,
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import { type NextRequest, NextResponse } from 'next/server'
2+
import { z } from 'zod'
3+
import { checkHybridAuth } from '@/lib/auth/hybrid'
4+
import { generateRequestId } from '@/lib/core/utils/request'
5+
import { createLogger } from '@/lib/logs/console/logger'
6+
import { openDMChannel } from '../utils'
7+
8+
export const dynamic = 'force-dynamic'
9+
10+
const logger = createLogger('SlackReadMessagesAPI')
11+
12+
const SlackReadMessagesSchema = z
13+
.object({
14+
accessToken: z.string().min(1, 'Access token is required'),
15+
channel: z.string().optional().nullable(),
16+
userId: z.string().optional().nullable(),
17+
limit: z.number().optional().nullable(),
18+
oldest: z.string().optional().nullable(),
19+
latest: z.string().optional().nullable(),
20+
})
21+
.refine((data) => data.channel || data.userId, {
22+
message: 'Either channel or userId is required',
23+
})
24+
25+
export async function POST(request: NextRequest) {
26+
const requestId = generateRequestId()
27+
28+
try {
29+
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
30+
31+
if (!authResult.success) {
32+
logger.warn(`[${requestId}] Unauthorized Slack read messages attempt: ${authResult.error}`)
33+
return NextResponse.json(
34+
{
35+
success: false,
36+
error: authResult.error || 'Authentication required',
37+
},
38+
{ status: 401 }
39+
)
40+
}
41+
42+
logger.info(
43+
`[${requestId}] Authenticated Slack read messages request via ${authResult.authType}`,
44+
{
45+
userId: authResult.userId,
46+
}
47+
)
48+
49+
const body = await request.json()
50+
const validatedData = SlackReadMessagesSchema.parse(body)
51+
52+
let channel = validatedData.channel
53+
if (!channel && validatedData.userId) {
54+
logger.info(`[${requestId}] Opening DM channel for user: ${validatedData.userId}`)
55+
channel = await openDMChannel(
56+
validatedData.accessToken,
57+
validatedData.userId,
58+
requestId,
59+
logger
60+
)
61+
}
62+
63+
const url = new URL('https://slack.com/api/conversations.history')
64+
url.searchParams.append('channel', channel!)
65+
const limit = validatedData.limit ? Number(validatedData.limit) : 10
66+
url.searchParams.append('limit', String(Math.min(limit, 15)))
67+
68+
if (validatedData.oldest) {
69+
url.searchParams.append('oldest', validatedData.oldest)
70+
}
71+
if (validatedData.latest) {
72+
url.searchParams.append('latest', validatedData.latest)
73+
}
74+
75+
logger.info(`[${requestId}] Reading Slack messages`, {
76+
channel,
77+
limit,
78+
})
79+
80+
const slackResponse = await fetch(url.toString(), {
81+
method: 'GET',
82+
headers: {
83+
'Content-Type': 'application/json',
84+
Authorization: `Bearer ${validatedData.accessToken}`,
85+
},
86+
})
87+
88+
const data = await slackResponse.json()
89+
90+
if (!data.ok) {
91+
logger.error(`[${requestId}] Slack API error:`, data)
92+
93+
if (data.error === 'not_in_channel') {
94+
return NextResponse.json(
95+
{
96+
success: false,
97+
error:
98+
'Bot is not in the channel. Please invite the Sim bot to your Slack channel by typing: /invite @Sim Studio',
99+
},
100+
{ status: 400 }
101+
)
102+
}
103+
if (data.error === 'channel_not_found') {
104+
return NextResponse.json(
105+
{
106+
success: false,
107+
error: 'Channel not found. Please check the channel ID and try again.',
108+
},
109+
{ status: 400 }
110+
)
111+
}
112+
if (data.error === 'missing_scope') {
113+
return NextResponse.json(
114+
{
115+
success: false,
116+
error:
117+
'Missing required permissions. Please reconnect your Slack account with the necessary scopes (channels:history, groups:history, im:history).',
118+
},
119+
{ status: 400 }
120+
)
121+
}
122+
123+
return NextResponse.json(
124+
{
125+
success: false,
126+
error: data.error || 'Failed to fetch messages',
127+
},
128+
{ status: 400 }
129+
)
130+
}
131+
132+
const messages = (data.messages || []).map((message: any) => ({
133+
type: message.type || 'message',
134+
ts: message.ts,
135+
text: message.text || '',
136+
user: message.user,
137+
bot_id: message.bot_id,
138+
username: message.username,
139+
channel: message.channel,
140+
team: message.team,
141+
thread_ts: message.thread_ts,
142+
parent_user_id: message.parent_user_id,
143+
reply_count: message.reply_count,
144+
reply_users_count: message.reply_users_count,
145+
latest_reply: message.latest_reply,
146+
subscribed: message.subscribed,
147+
last_read: message.last_read,
148+
unread_count: message.unread_count,
149+
subtype: message.subtype,
150+
reactions: message.reactions?.map((reaction: any) => ({
151+
name: reaction.name,
152+
count: reaction.count,
153+
users: reaction.users || [],
154+
})),
155+
is_starred: message.is_starred,
156+
pinned_to: message.pinned_to,
157+
files: message.files?.map((file: any) => ({
158+
id: file.id,
159+
name: file.name,
160+
mimetype: file.mimetype,
161+
size: file.size,
162+
url_private: file.url_private,
163+
permalink: file.permalink,
164+
mode: file.mode,
165+
})),
166+
attachments: message.attachments,
167+
blocks: message.blocks,
168+
edited: message.edited
169+
? {
170+
user: message.edited.user,
171+
ts: message.edited.ts,
172+
}
173+
: undefined,
174+
permalink: message.permalink,
175+
}))
176+
177+
logger.info(`[${requestId}] Successfully read ${messages.length} messages`)
178+
179+
return NextResponse.json({
180+
success: true,
181+
output: {
182+
messages,
183+
},
184+
})
185+
} catch (error) {
186+
if (error instanceof z.ZodError) {
187+
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
188+
return NextResponse.json(
189+
{
190+
success: false,
191+
error: 'Invalid request data',
192+
details: error.errors,
193+
},
194+
{ status: 400 }
195+
)
196+
}
197+
198+
logger.error(`[${requestId}] Error reading Slack messages:`, error)
199+
return NextResponse.json(
200+
{
201+
success: false,
202+
error: error instanceof Error ? error.message : 'Unknown error occurred',
203+
},
204+
{ status: 500 }
205+
)
206+
}
207+
}

0 commit comments

Comments
 (0)