Skip to content
Open
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
2 changes: 2 additions & 0 deletions apps/docs/components/ui/icon-mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,8 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
similarweb: SimilarwebIcon,
sixtyfour: SixtyfourIcon,
slack: SlackIcon,
slack_app: SlackIcon,
slack_v2: SlackIcon,
smartlead: SmartleadIcon,
smtp: SmtpIcon,
snowflake: SnowflakeIcon,
Expand Down
23 changes: 16 additions & 7 deletions apps/docs/content/docs/en/integrations/slack.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ description: Send, update, delete messages, manage views and modals, add or remo
import { BlockInfoCard } from "@/components/ui/block-info-card"

<BlockInfoCard
type="slack"
type="slack_v2"
color="#611f69"
/>

Expand Down Expand Up @@ -1872,18 +1872,27 @@ Set the purpose (description) for a Slack channel (max 250 characters).

A **Trigger** is a block that starts a workflow when an event happens in this service.

### Slack Webhook
### Slack

Trigger workflow from Slack events like mentions, messages, and reactions
Trigger from Slack events (mentions, messages, reactions)

#### Configuration

| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `signingSecret` | string | Yes | The signing secret from your Slack app to validate request authenticity. |
| `botToken` | string | No | The bot token from your Slack app. Required for downloading files attached to messages. |
| `includeFiles` | boolean | No | Download and include file attachments from messages. Requires a bot token with files:read scope. |
| `setupWizard` | modal | No | Walk through manifest creation, app install, and pasting credentials. |
| `eventType` | string | Yes | The single Slack event this trigger fires on. Add another trigger block for another event. |
| `customBotCredential` | string | Yes | Choose a custom Slack bot you set up once and reuse across triggers. |
| `manualBotCredential` | string | Yes | Set the custom bot credential ID directly. |
| `source` | string | No | Restrict to direct messages, public channels, or private channels. Leave empty to match any. |
| `channelFilter` | channel-selector | No | Restrict to specific channels. Leave empty to trigger on any channel the bot has been added to. |
| `manualChannelFilter` | string | No | Comma-separated channel IDs to restrict to. Set IDs directly here. |
| `threads` | string | No | Include thread replies, exclude them \(top-level only\), or fire only on thread replies. |
| `emoji` | string | No | Comma-separated emoji names to restrict to. Leave empty to match any emoji. |
| `nameContains` | string | No | Only fire when the created channel name contains this text. |
| `interactionFilter` | string | No | Comma-separated action_ids \(buttons/selects\) or callback_ids \(modals\) to restrict to. Leave empty to fire on any interaction. |
| `filterBotMessages` | boolean | No | Ignore messages sent by other bots. This app's own output is always ignored. |
| `includeOwnMessages` | boolean | No | Also fire on this app's own messages and reactions. Can cause loops — use with care. |
| `includeFiles` | boolean | No | Download and include file attachments from messages. Requires files:read. |

#### Output

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ Webhook triggers receive callbacks from the provider and must be able to verify
| Variable | Needed for |
|---|---|
| `SLACK_SIGNING_SECRET` | Verifying Slack event and slash-command signatures |
| `SLACK_EXTENDED_SCOPES` / `NEXT_PUBLIC_SLACK_EXTENDED_SCOPES` | Requesting the broader Slack scope set |
| `SLACK_EXTENDED_SCOPES` / `NEXT_PUBLIC_SLACK_EXTENDED_SCOPES` | Enabling the native Sim-app trigger and its broader Slack scope set; set both to the same value |

Your deployment must also be reachable from the provider's servers for webhook triggers to fire — a Sim instance on a private network can use polling triggers but not webhook triggers. Polling triggers additionally require the scheduler; see [Background Jobs](/platform/self-hosting/background-jobs).

Expand Down
17 changes: 17 additions & 0 deletions apps/sim/app/api/auth/oauth/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,23 @@ describe('OAuth Utils', () => {
expect(result.accessToken).toBe('xoxb-tok')
})

it('returns the bot token for an action-only Slack bot without a signing secret', async () => {
mockSelectChain([
{
type: 'service_account',
providerId: SLACK_CUSTOM_BOT_PROVIDER_ID,
encryptedServiceAccountKey: 'enc',
},
])
mockDecryptSecret.mockResolvedValueOnce({
decrypted: JSON.stringify({ botToken: 'xoxb-action' }),
})

const result = await resolveServiceAccountToken('cred-1', SLACK_CUSTOM_BOT_PROVIDER_ID)

expect(result.accessToken).toBe('xoxb-action')
})

it('throws when the Slack bot credential is missing', async () => {
mockSelectChain([])
await expect(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,19 @@ describe('Slack custom-bot webhook route', () => {
expect(mockDispatchResolvedWebhookTarget).not.toHaveBeenCalled()
})

it('404s an action-only bot credential without a signing secret', async () => {
mockGetSlackBotCredential.mockResolvedValue({
botToken: 'xoxb-x',
teamId: 'T1',
})

const res = await POST(makeRequest(), context)

expect(res.status).toBe(404)
expect(mockVerifySignature).not.toHaveBeenCalled()
expect(mockFindWebhooksByRoutingKey).not.toHaveBeenCalled()
})

it('verifies with the credential signing secret and rejects a bad signature', async () => {
mockVerifySignature.mockReturnValue(new Response(null, { status: 401 }))
const res = await POST(makeRequest(), context)
Expand Down Expand Up @@ -133,4 +146,18 @@ describe('Slack custom-bot webhook route', () => {
expect(mockDispatchResolvedWebhookTarget).toHaveBeenCalledTimes(1)
expect(res.status).toBe(200)
})

it('returns the dispatch failure when no target is acknowledged', async () => {
mockDispatchResolvedWebhookTarget.mockResolvedValue({
outcome: 'failed',
response: new Response('Preprocessing failed', { status: 500 }),
reason: 'preprocessing',
})

const res = await POST(makeRequest(), context)

expect(mockDispatchResolvedWebhookTarget).toHaveBeenCalledTimes(1)
expect(res.status).toBe(500)
await expect(res.text()).resolves.toBe('Preprocessing failed')
})
})
48 changes: 24 additions & 24 deletions apps/sim/app/api/webhooks/slack/custom/[credentialId]/route.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { admissionRejectedResponse, tryAdmit } from '@/lib/core/admission/gate'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { getSlackBotCredential } from '@/lib/oauth/credential-service'
import { findWebhooksByRoutingKey, parseWebhookBody } from '@/lib/webhooks/processor'
import { handleSlackChallenge, verifySlackRequestSignature } from '@/lib/webhooks/providers/slack'
import { dispatchSlackWebhooks } from '@/lib/webhooks/slack-dispatch'

const logger = createLogger('SlackCustomBotWebhookAPI')
import { parseWebhookBody } from '@/lib/webhooks/processor'
import { handleSlackChallenge } from '@/lib/webhooks/providers/slack'
import {
dispatchSlackCustomBotCredential,
verifySlackCustomBotCredentialRequest,
} from '@/lib/webhooks/slack-custom-ingress'

export const dynamic = 'force-dynamic'
export const runtime = 'nodejs'
Expand Down Expand Up @@ -58,31 +57,32 @@ async function handleSlackCustomBotWebhook(
return challenge
}

const botCredential = await getSlackBotCredential(credentialId)
if (!botCredential) {
logger.warn(`[${requestId}] Unknown Slack bot credential ${credentialId}`)
return new NextResponse(null, { status: 404 })
}

const authError = verifySlackRequestSignature(
botCredential.signingSecret,
const authError = await verifySlackCustomBotCredentialRequest({
credentialId,
request,
rawBody,
requestId
)
requestId,
})
if (authError) {
return authError
}

const webhooks = await findWebhooksByRoutingKey(credentialId, requestId, 'slack')
if (webhooks.length === 0) {
logger.info(
`[${requestId}] No active trigger for bot credential ${credentialId}; nothing to run`
const dispatchResults = await dispatchSlackCustomBotCredential({
credentialId,
body,
request,
requestId,
receivedAt,
})
const acknowledged = dispatchResults.some(
(result) => result.outcome !== 'failed' && result.reason !== 'block-missing'
)
if (!acknowledged) {
const failure = dispatchResults.find(
(result) => result.outcome === 'failed' || result.reason === 'block-missing'
)
return new NextResponse(null, { status: 200 })
if (failure) return failure.response
}

await dispatchSlackWebhooks(webhooks, { body, request, requestId, receivedAt })

return new NextResponse(null, { status: 200 })
}
Loading
Loading