Skip to content
Merged
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
76 changes: 72 additions & 4 deletions apps/sim/lib/webhooks/utils.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,55 @@ async function downloadSlackFiles(
return downloaded
}

const SLACK_REACTION_EVENTS = new Set(['reaction_added', 'reaction_removed'])

/**
* Fetches the text of a reacted-to message from Slack using the reactions.get API.
* Unlike conversations.history, reactions.get works for both top-level messages and
* thread replies, since it looks up the item directly by channel + timestamp.
* Requires the bot token to have the reactions:read scope.
*/
async function fetchSlackMessageText(
channel: string,
messageTs: string,
botToken: string
): Promise<string> {
try {
const params = new URLSearchParams({
channel,
timestamp: messageTs,
})
const response = await fetch(`https://slack.com/api/reactions.get?${params}`, {
headers: { Authorization: `Bearer ${botToken}` },
})

const data = (await response.json()) as {
ok: boolean
error?: string
type?: string
message?: { text?: string }
}

if (!data.ok) {
logger.warn('Slack reactions.get failed — message text unavailable', {
channel,
messageTs,
error: data.error,
})
return ''
}

return data.message?.text ?? ''
} catch (error) {
logger.warn('Error fetching Slack message text', {
channel,
messageTs,
error: error instanceof Error ? error.message : String(error),
})
return ''
}
}

/**
* Format webhook input based on provider
*/
Expand Down Expand Up @@ -953,6 +1002,23 @@ export async function formatWebhookInput(
})
}

const eventType: string = rawEvent?.type || body?.type || 'unknown'
const isReactionEvent = SLACK_REACTION_EVENTS.has(eventType)

// Reaction events nest channel/ts inside event.item
const channel: string = isReactionEvent
? rawEvent?.item?.channel || ''
: rawEvent?.channel || ''
const messageTs: string = isReactionEvent
? rawEvent?.item?.ts || ''
: rawEvent?.ts || rawEvent?.event_ts || ''

// For reaction events, attempt to fetch the original message text
let text: string = rawEvent?.text || ''
if (isReactionEvent && channel && messageTs && botToken) {
text = await fetchSlackMessageText(channel, messageTs, botToken)
}

const rawFiles: any[] = rawEvent?.files ?? []
const hasFiles = rawFiles.length > 0

Expand All @@ -965,16 +1031,18 @@ export async function formatWebhookInput(

return {
event: {
event_type: rawEvent?.type || body?.type || 'unknown',
channel: rawEvent?.channel || '',
event_type: eventType,
channel,
channel_name: '',
user: rawEvent?.user || '',
user_name: '',
text: rawEvent?.text || '',
timestamp: rawEvent?.ts || rawEvent?.event_ts || '',
text,
timestamp: messageTs,
thread_ts: rawEvent?.thread_ts || '',
team_id: body?.team_id || rawEvent?.team || '',
event_id: body?.event_id || '',
reaction: rawEvent?.reaction || '',
item_user: rawEvent?.item_user || '',
hasFiles,
files,
},
Expand Down
14 changes: 12 additions & 2 deletions apps/sim/triggers/slack/webhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ export const slackWebhookTrigger: TriggerConfig = {
'Go to <a href="https://api.slack.com/apps" target="_blank" rel="noopener noreferrer" class="text-muted-foreground underline transition-colors hover:text-muted-foreground/80">Slack Apps page</a>',
'If you don\'t have an app:<br><ul class="mt-1 ml-5 list-disc"><li>Create an app from scratch</li><li>Give it a name and select your workspace</li></ul>',
'Go to "Basic Information", find the "Signing Secret", and paste it in the field above.',
'Go to "OAuth & Permissions" and add bot token scopes:<br><ul class="mt-1 ml-5 list-disc"><li><code>app_mentions:read</code> - For viewing messages that tag your bot with an @</li><li><code>chat:write</code> - To send messages to channels your bot is a part of</li><li><code>files:read</code> - To access files and images shared in messages</li></ul>',
'Go to "Event Subscriptions":<br><ul class="mt-1 ml-5 list-disc"><li>Enable events</li><li>Under "Subscribe to Bot Events", add <code>app_mention</code> to listen to messages that mention your bot</li><li>Paste the Webhook URL above into the "Request URL" field</li></ul>',
'Go to "OAuth & Permissions" and add bot token scopes:<br><ul class="mt-1 ml-5 list-disc"><li><code>app_mentions:read</code> - For viewing messages that tag your bot with an @</li><li><code>chat:write</code> - To send messages to channels your bot is a part of</li><li><code>files:read</code> - To access files and images shared in messages</li><li><code>reactions:read</code> - For listening to emoji reactions and fetching reacted-to message text</li></ul>',
'Go to "Event Subscriptions":<br><ul class="mt-1 ml-5 list-disc"><li>Enable events</li><li>Under "Subscribe to Bot Events", add <code>app_mention</code> to listen to messages that mention your bot</li><li>For reaction events, also add <code>reaction_added</code> and/or <code>reaction_removed</code></li><li>Paste the Webhook URL above into the "Request URL" field</li></ul>',
'Go to "Install App" in the left sidebar and install the app into your desired Slack workspace and channel.',
'Copy the "Bot User OAuth Token" (starts with <code>xoxb-</code>) and paste it in the Bot Token field above to enable file downloads.',
'Save changes in both Slack and here.',
Expand Down Expand Up @@ -128,6 +128,16 @@ export const slackWebhookTrigger: TriggerConfig = {
type: 'string',
description: 'Unique event identifier',
},
reaction: {
type: 'string',
description:
'Emoji reaction name (e.g., thumbsup). Present for reaction_added/reaction_removed events',
},
item_user: {
type: 'string',
description:
'User ID of the original message author. Present for reaction_added/reaction_removed events',
},
hasFiles: {
type: 'boolean',
description: 'Whether the message has file attachments',
Expand Down