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
107 changes: 106 additions & 1 deletion apps/sim/lib/webhooks/utils.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,66 @@ export async function validateTwilioSignature(
const SLACK_MAX_FILE_SIZE = 50 * 1024 * 1024 // 50 MB
const SLACK_MAX_FILES = 15

/**
* Fetches the original message text for a reaction event using conversations.history.
* Reaction events don't include message text, so we need to fetch it separately.
* Returns empty string if permissions are missing or fetch fails.
*/
async function fetchSlackReactionMessageText(
channel: string,
messageTs: string,
botToken: string
): Promise<{ text: string; user?: string }> {
try {
const url = new URL('https://slack.com/api/conversations.history')
url.searchParams.append('channel', channel)
url.searchParams.append('oldest', messageTs)
url.searchParams.append('limit', '1')
url.searchParams.append('inclusive', 'true')

const response = await fetch(url.toString(), {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${botToken}`,
},
})

const data = (await response.json()) as {
ok: boolean
error?: string
messages?: Array<{ text?: string; user?: string }>
}

if (!data.ok) {
if (data.error === 'missing_scope') {
logger.debug('Missing scope for fetching reaction message text - channels:history required')
} else if (data.error === 'channel_not_found') {
logger.debug('Channel not found when fetching reaction message text', { channel })
} else {
logger.warn('Failed to fetch reaction message text', { error: data.error, channel })
}
return { text: '' }
}

const messages = data.messages || []
if (messages.length === 0) {
return { text: '' }
}

return {
text: messages[0].text || '',
user: messages[0].user,
}
} catch (error) {
logger.warn('Error fetching reaction message text', {
error: error instanceof Error ? error.message : String(error),
channel,
})
return { text: '' }
}
}

/**
* Resolves the full file object from the Slack API when the event payload
* only contains a partial file (e.g. missing url_private due to file_access restrictions).
Expand Down Expand Up @@ -953,6 +1013,51 @@ export async function formatWebhookInput(
})
}

const eventType = rawEvent?.type || body?.type || 'unknown'

// Handle reaction events (reaction_added, reaction_removed) which have a different payload structure
const isReactionEvent = eventType === 'reaction_added' || eventType === 'reaction_removed'

if (isReactionEvent) {
// Reaction events have item.channel instead of channel, and item.ts for the message timestamp
const itemChannel = rawEvent?.item?.channel || ''
const itemTs = rawEvent?.item?.ts || ''
const reaction = rawEvent?.reaction || ''
const itemUser = rawEvent?.item_user || ''

// Fetch the original message text if bot token is available
let messageText = ''
let messageAuthor = itemUser
if (botToken && itemChannel && itemTs) {
const messageData = await fetchSlackReactionMessageText(itemChannel, itemTs, botToken)
messageText = messageData.text
if (messageData.user) {
messageAuthor = messageData.user
}
}

return {
event: {
event_type: eventType,
channel: itemChannel,
channel_name: '',
user: rawEvent?.user || '',
user_name: '',
text: messageText,
reaction,
item_user: messageAuthor,
timestamp: itemTs,
event_ts: rawEvent?.event_ts || '',
thread_ts: '',
team_id: body?.team_id || rawEvent?.team || '',
event_id: body?.event_id || '',
hasFiles: false,
files: [],
},
}
}

// Standard message events
const rawFiles: any[] = rawEvent?.files ?? []
const hasFiles = rawFiles.length > 0

Expand All @@ -965,7 +1070,7 @@ export async function formatWebhookInput(

return {
event: {
event_type: rawEvent?.type || body?.type || 'unknown',
event_type: eventType,
channel: rawEvent?.channel || '',
channel_name: '',
user: rawEvent?.user || '',
Expand Down
27 changes: 22 additions & 5 deletions apps/sim/triggers/slack/webhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ 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 "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>channels:history</code> - To fetch message text for reaction events</li><li><code>reactions:read</code> - To receive reaction events</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 "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.',
Expand All @@ -90,7 +90,8 @@ export const slackWebhookTrigger: TriggerConfig = {
properties: {
event_type: {
type: 'string',
description: 'Type of Slack event (e.g., app_mention, message)',
description:
'Type of Slack event (e.g., app_mention, message, reaction_added, reaction_removed)',
},
channel: {
type: 'string',
Expand All @@ -102,19 +103,35 @@ export const slackWebhookTrigger: TriggerConfig = {
},
user: {
type: 'string',
description: 'User ID who triggered the event',
description:
'User ID who triggered the event (for reactions, the user who added/removed the reaction)',
},
user_name: {
type: 'string',
description: 'Username who triggered the event',
},
text: {
type: 'string',
description: 'Message text content',
description:
'Message text content. For reaction events, fetched via API if bot token has channels:history scope.',
},
reaction: {
type: 'string',
description: 'Emoji name for reaction events (e.g., "thumbsup", "heart")',
},
item_user: {
type: 'string',
description: 'User ID of the message author (for reaction events)',
},
timestamp: {
type: 'string',
description: 'Message timestamp from the triggering event',
description:
'Message timestamp. For reactions, this is the timestamp of the reacted-to message (item.ts).',
},
event_ts: {
type: 'string',
description:
'Event timestamp. For reactions, this is when the reaction was added/removed.',
},
thread_ts: {
type: 'string',
Expand Down