Skip to content

Commit ea4b50b

Browse files
fix: extract event-specific fields for Slack reaction webhooks
- Add fetchSlackReactionMessageText helper to fetch original message text via conversations.history API for reaction_added/reaction_removed events - Handle reaction events separately with correct field extraction: - channel: extract from event.item.channel (not event.channel) - timestamp: use item.ts (message timestamp) not event_ts (reaction time) - reaction: extract emoji name from event.reaction - item_user: extract message author from event.item_user - event_ts: new field for when the reaction was added/removed - text: fetch via API if bot token has channels:history scope - Gracefully handle missing permissions (missing_scope error) - Update Slack trigger outputs to document new reaction fields - Add channels:history and reactions:read scopes to setup instructions Co-authored-by: Emir Karabeg <emir-karabeg@users.noreply.github.com>
1 parent 1b8d666 commit ea4b50b

File tree

2 files changed

+128
-6
lines changed

2 files changed

+128
-6
lines changed

apps/sim/lib/webhooks/utils.server.ts

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,66 @@ export async function validateTwilioSignature(
530530
const SLACK_MAX_FILE_SIZE = 50 * 1024 * 1024 // 50 MB
531531
const SLACK_MAX_FILES = 15
532532

533+
/**
534+
* Fetches the original message text for a reaction event using conversations.history.
535+
* Reaction events don't include message text, so we need to fetch it separately.
536+
* Returns empty string if permissions are missing or fetch fails.
537+
*/
538+
async function fetchSlackReactionMessageText(
539+
channel: string,
540+
messageTs: string,
541+
botToken: string
542+
): Promise<{ text: string; user?: string }> {
543+
try {
544+
const url = new URL('https://slack.com/api/conversations.history')
545+
url.searchParams.append('channel', channel)
546+
url.searchParams.append('oldest', messageTs)
547+
url.searchParams.append('limit', '1')
548+
url.searchParams.append('inclusive', 'true')
549+
550+
const response = await fetch(url.toString(), {
551+
method: 'GET',
552+
headers: {
553+
'Content-Type': 'application/json',
554+
Authorization: `Bearer ${botToken}`,
555+
},
556+
})
557+
558+
const data = (await response.json()) as {
559+
ok: boolean
560+
error?: string
561+
messages?: Array<{ text?: string; user?: string }>
562+
}
563+
564+
if (!data.ok) {
565+
if (data.error === 'missing_scope') {
566+
logger.debug('Missing scope for fetching reaction message text - channels:history required')
567+
} else if (data.error === 'channel_not_found') {
568+
logger.debug('Channel not found when fetching reaction message text', { channel })
569+
} else {
570+
logger.warn('Failed to fetch reaction message text', { error: data.error, channel })
571+
}
572+
return { text: '' }
573+
}
574+
575+
const messages = data.messages || []
576+
if (messages.length === 0) {
577+
return { text: '' }
578+
}
579+
580+
return {
581+
text: messages[0].text || '',
582+
user: messages[0].user,
583+
}
584+
} catch (error) {
585+
logger.warn('Error fetching reaction message text', {
586+
error: error instanceof Error ? error.message : String(error),
587+
channel,
588+
})
589+
return { text: '' }
590+
}
591+
}
592+
533593
/**
534594
* Resolves the full file object from the Slack API when the event payload
535595
* only contains a partial file (e.g. missing url_private due to file_access restrictions).
@@ -953,6 +1013,51 @@ export async function formatWebhookInput(
9531013
})
9541014
}
9551015

1016+
const eventType = rawEvent?.type || body?.type || 'unknown'
1017+
1018+
// Handle reaction events (reaction_added, reaction_removed) which have a different payload structure
1019+
const isReactionEvent = eventType === 'reaction_added' || eventType === 'reaction_removed'
1020+
1021+
if (isReactionEvent) {
1022+
// Reaction events have item.channel instead of channel, and item.ts for the message timestamp
1023+
const itemChannel = rawEvent?.item?.channel || ''
1024+
const itemTs = rawEvent?.item?.ts || ''
1025+
const reaction = rawEvent?.reaction || ''
1026+
const itemUser = rawEvent?.item_user || ''
1027+
1028+
// Fetch the original message text if bot token is available
1029+
let messageText = ''
1030+
let messageAuthor = itemUser
1031+
if (botToken && itemChannel && itemTs) {
1032+
const messageData = await fetchSlackReactionMessageText(itemChannel, itemTs, botToken)
1033+
messageText = messageData.text
1034+
if (messageData.user) {
1035+
messageAuthor = messageData.user
1036+
}
1037+
}
1038+
1039+
return {
1040+
event: {
1041+
event_type: eventType,
1042+
channel: itemChannel,
1043+
channel_name: '',
1044+
user: rawEvent?.user || '',
1045+
user_name: '',
1046+
text: messageText,
1047+
reaction,
1048+
item_user: messageAuthor,
1049+
timestamp: itemTs,
1050+
event_ts: rawEvent?.event_ts || '',
1051+
thread_ts: '',
1052+
team_id: body?.team_id || rawEvent?.team || '',
1053+
event_id: body?.event_id || '',
1054+
hasFiles: false,
1055+
files: [],
1056+
},
1057+
}
1058+
}
1059+
1060+
// Standard message events
9561061
const rawFiles: any[] = rawEvent?.files ?? []
9571062
const hasFiles = rawFiles.length > 0
9581063

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

9661071
return {
9671072
event: {
968-
event_type: rawEvent?.type || body?.type || 'unknown',
1073+
event_type: eventType,
9691074
channel: rawEvent?.channel || '',
9701075
channel_name: '',
9711076
user: rawEvent?.user || '',

apps/sim/triggers/slack/webhook.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export const slackWebhookTrigger: TriggerConfig = {
6767
'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>',
6868
'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>',
6969
'Go to "Basic Information", find the "Signing Secret", and paste it in the field above.',
70-
'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>',
70+
'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>',
7171
'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>',
7272
'Go to "Install App" in the left sidebar and install the app into your desired Slack workspace and channel.',
7373
'Copy the "Bot User OAuth Token" (starts with <code>xoxb-</code>) and paste it in the Bot Token field above to enable file downloads.',
@@ -90,7 +90,8 @@ export const slackWebhookTrigger: TriggerConfig = {
9090
properties: {
9191
event_type: {
9292
type: 'string',
93-
description: 'Type of Slack event (e.g., app_mention, message)',
93+
description:
94+
'Type of Slack event (e.g., app_mention, message, reaction_added, reaction_removed)',
9495
},
9596
channel: {
9697
type: 'string',
@@ -102,19 +103,35 @@ export const slackWebhookTrigger: TriggerConfig = {
102103
},
103104
user: {
104105
type: 'string',
105-
description: 'User ID who triggered the event',
106+
description:
107+
'User ID who triggered the event (for reactions, the user who added/removed the reaction)',
106108
},
107109
user_name: {
108110
type: 'string',
109111
description: 'Username who triggered the event',
110112
},
111113
text: {
112114
type: 'string',
113-
description: 'Message text content',
115+
description:
116+
'Message text content. For reaction events, fetched via API if bot token has channels:history scope.',
117+
},
118+
reaction: {
119+
type: 'string',
120+
description: 'Emoji name for reaction events (e.g., "thumbsup", "heart")',
121+
},
122+
item_user: {
123+
type: 'string',
124+
description: 'User ID of the message author (for reaction events)',
114125
},
115126
timestamp: {
116127
type: 'string',
117-
description: 'Message timestamp from the triggering event',
128+
description:
129+
'Message timestamp. For reactions, this is the timestamp of the reacted-to message (item.ts).',
130+
},
131+
event_ts: {
132+
type: 'string',
133+
description:
134+
'Event timestamp. For reactions, this is when the reaction was added/removed.',
118135
},
119136
thread_ts: {
120137
type: 'string',

0 commit comments

Comments
 (0)