diff --git a/apps/sim/lib/webhooks/utils.server.ts b/apps/sim/lib/webhooks/utils.server.ts index 2d74986275..974e9552b8 100644 --- a/apps/sim/lib/webhooks/utils.server.ts +++ b/apps/sim/lib/webhooks/utils.server.ts @@ -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 { + 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, }, diff --git a/apps/sim/triggers/slack/webhook.ts b/apps/sim/triggers/slack/webhook.ts index 3d22e3be20..3f1bbe2c0f 100644 --- a/apps/sim/triggers/slack/webhook.ts +++ b/apps/sim/triggers/slack/webhook.ts @@ -67,8 +67,8 @@ export const slackWebhookTrigger: TriggerConfig = { 'Go to Slack Apps page', 'If you don\'t have an app:
', 'Go to "Basic Information", find the "Signing Secret", and paste it in the field above.', - 'Go to "OAuth & Permissions" and add bot token scopes:
', - 'Go to "Event Subscriptions":
', + 'Go to "OAuth & Permissions" and add bot token scopes:
', + 'Go to "Event Subscriptions":
', '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 xoxb-) 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',