fix(trigger): handle Slack reaction_added/reaction_removed event payloads (#3280)

* fix(trigger): handle Slack reaction_added/reaction_removed event payloads

* fix(trigger): use oldest param for conversations.history consistency

* fix oldest param

* fix(trigger): use reactions.get API to fetch message text for thread replies
This commit is contained in:
Waleed
2026-02-20 17:23:06 -08:00
committed by GitHub
parent a1400caea0
commit 765a481864
2 changed files with 84 additions and 6 deletions
+72 -4
View File
@@ -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
*/
@@ -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
@@ -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,
},
+12 -2
View File
@@ -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.',
@@ -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',