Package slack (github.com/coder/chat/adapters/slack) is the Slack adapter for the chat runtime (root package github.com/coder/chat). [doc.go:slack]
- It receives Slack deliveries over HTTP (
Adapter.Webhook), over Socket Mode (Adapter.Listen), or over both at the same time. One delivery handler serves both transports, so a payload becomes the samechat.Eventon either transport. [doc.go:slack], [slack.go:deliveryHandler] - It posts messages, ephemeral messages, Block Kit content, modals, and
response_urlreplies. [slack.go:PostMessage,PostEphemeralMessage], [interactive.go:PostNative,OpenModal,RespondURL] - It reads live history through
Adapter.ReadHistory. [history.go:ReadHistory]
The adapter runs in one of two modes. Single-install mode uses a static Options.BotToken for one workspace. Multi-tenant mode uses Options.InstallStore (chat.InstallStore) and looks up a per-workspace bot token by team_id. [doc.go:slack]
The adapter sends every Slack Web API call and every response_url post through the child package slackapi, which owns rate-limit retry. [doc.go:slack]
Newbuilds anAdapterfromOptionsafter validation. [slack.go:New]Optionsholds tokens (SigningSecret,AppToken,BotToken), team and bot IDs,InstallStore,APIBaseURL,Client,Now,SignatureTolerance,DisableNativeEphemeral,Logger,Observer, andRetryPolicy. [slack.go:Options]SlackInstallis the per-workspace credential (BotToken,BotUserID) carried onchat.Install.Credential. [slack.go:SlackInstall]Adapteris the Slack adapter. [slack.go:Adapter]RetryPolicyis a type alias ofslackapi.RetryPolicy. [retry.go:RetryPolicy]RateLimitedis a type alias ofslackapi.RateLimited. [retry.go:RateLimited]
Adapter.Namereturns"slack". [slack.go:Name]Adapter.Initdiscovers the bot identity withauth.testin single-install mode, unlessTeamID,BotUserID, andBotIDare all configured. It does nothing in multi-tenant mode. [slack.go:Init]Adapter.Shutdownreturns nil. [slack.go:Shutdown]Adapter.Webhookreturns the HTTP transport handler for achat.DispatchFunc. [slack.go:Webhook]Adapter.Listenruns the Socket Mode transport. It implementschat.Listener. [slack.go:Listen]Adapter.ValidateThreadIDdecodes a Thread ID into achat.ThreadRef. [slack.go:ValidateThreadID]Adapter.PostMessageposts a message to a thread. [slack.go:PostMessage]Adapter.PostEphemeralMessageposts an ephemeral message, with an optional DM fallback. It implementschat.EphemeralPoster. [slack.go:PostEphemeralMessage], [interactive.go:EphemeralPoster assertion]Adapter.BotActorreturns the bot as achat.Actor. [slack.go:BotActor]Adapter.ReadHistoryreads thread replies or DM history, newest first. It implementschat.HistoryReader. [history.go:ReadHistory]Adapter.PostNativeposts Block Kit blocks to a thread. It implementschat.NativeContentPoster. [interactive.go:PostNative]Adapter.OpenModalandAdapter.OpenModalForTenantopen a modal with atrigger_id. [interactive.go:OpenModal,OpenModalForTenant]Adapter.OpenModalFromRawandAdapter.OpenModalForTenantFromRawopen a modal with thetrigger_idfrom a raw inbound payload. [interactive.go:OpenModalFromRaw,OpenModalForTenantFromRaw]Adapter.RespondURLposts an ephemeral reply to theresponse_urlfrom a raw inbound payload. [interactive.go:RespondURL]
The code has compile-time assertions for chat.Listener, chat.HistoryReader, chat.NativeContentPoster, and chat.EphemeralPoster. [slack.go:Listener assertion], [history.go:HistoryReader assertion], [interactive.go:NativeContentPoster assertion]
- slackapi: low-level Slack Web API client, inbound transports (
Webhook, Socket ModeListen), payload decoding, and formats.
NewrequiresSigningSecretorAppToken. [slack.go:New]Newrequires exactly one ofBotTokenorInstallStore. Both, or neither, is an error. [slack.go:New], [doc.go:slack]Newrejects a negativeSignatureToleranceand usesslackapi.DefaultSignatureTolerancewhen it is zero. [slack.go:New]Initrequires non-emptyteam_id,user_id, andbot_idfromauth.test. Each must match any configured value. [slack.go:Init,requireMatchingAuthField]- Without a
SigningSecret,Webhookanswers 404 to each request. Without anAppToken,Listenreturns an error. [slack.go:Webhook,Listen]
- When the runtime rejects a delivery with
chat.ErrAdmissionRejected, the adapter acks a slash command with a busy reply. It returns a wrappedslackapi.ErrBusyfor other deliveries, so Slack does not get an ack. [slack.go:deliveryHandler] - An unknown delivery type, a non-
event_callbackenvelope, and an interaction type other thanblock_actionsare Ignored Events. [slack.go:normalizeDelivery,normalizeEvent], [interactive.go:normalizeInteraction] - A malformed payload returns an error that unwraps to
slackapi.ErrInvalidPayload. [slack.go:invalidPayload] - The adapter accepts
app_mentionevents, andmessageevents with an empty orbot_messagesubtype. [slack.go:supportedMessageEvent] - A message event needs envelope
team_idandevent_id, and event channel,ts, and an author ID. [slack.go:messageEvent] - A slash command needs
command,team_id,channel_id,user_id, andtrigger_id. Its event ID is deterministic. [interactive.go:commandEvent,commandEventID] - An interaction's event ID joins team, channel, user, action ID, and the first non-empty value of
action_ts,trigger_id, the message ts, and the thread ts. A redelivered activation repeats its ID, and each new click gets a new one. [interactive.go:interactionEventID] - Single-install mode rejects events, commands, and interactions from another team as invalid payloads (
slackapi.ErrInvalidPayload). Over HTTP,slackapi.Webhookanswers them with 400. [slack.go:messageEvent,invalidPayload], [interactive.go:commandEvent,interactionEvent] - Multi-tenant mode drops the bot's own messages when the bot user ID matches. [slack.go:messageEvent]
- In multi-tenant mode, the bot user ID comes from
chat.Install.BotActorIDwhen it is set, else fromSlackInstall.BotUserID. [slack.go:resolveInstall,SlackInstall]
- An empty tenant or
chat.ErrInstallNotFoundfromInstallStore.Lookupis an Ignored Event. Any other lookup error is a transport failure. [slack.go:resolveInstall], [doc.go:slack] - An install credential must be a
SlackInstallor a non-nil*SlackInstallwith aBotToken. [slack.go:slackCredential,resolveInstall,postToken] - An outbound post in multi-tenant mode requires a non-empty tenant. [slack.go:postToken]
OpenModalfails in multi-tenant mode. UseOpenModalForTenant. [interactive.go:OpenModal]
- A Thread ID starts with
slack:v1:. It needs a team and a channel, and a root unless the thread is a direct message. [slack.go:encodeThreadID,decodeThreadID] - A channel message thread is rooted at
thread_ts, elsets. A direct message thread has no root. [slack.go:threadIDForEvent] - Posts accept only
chat.MessageFormatTextandchat.MessageFormatMarkdown. [slack.go:slackMessageFields] PostEphemeralMessagefalls back to a DM only on anok:falserejection, and only whenFallbackToDMis set. WithDisableNativeEphemeralset and no fallback, it returns(nil, nil). [slack.go:PostEphemeralMessage]PostNativerequirescontent.Adapter == "slack"and a non-nil payload. It does not downgrade the content. [interactive.go:PostNative]OpenModalrequires atrigger_idand a non-nil view.RespondURLrequires aresponse_url. [interactive.go:OpenModal,RespondURL]
ReadHistoryuses 100 whenLimit <= 0and capsLimitat 1000. [history.go:ReadHistory,slackDefaultHistoryLimit,slackMaxHistoryLimit]ReadHistorydoes no storage, dedupe, or caching. [doc.go:slack]ReadHistoryreturns an empty page with no Slack call when theBeforecursor equals the thread root. [history.go:ReadHistory]- Retry never sleeps past the caller's context deadline. The default
MaxElapsed(2 s) also keeps the total backoff sleep under Slack's 3-second ack window. Neither bound covers the time of each HTTP attempt, so underDispatchSynca delivery stays inside the window only when the request context or the HTTP client (Options.Client) bounds those attempts. [doc.go:slack], [slackapi/retry.go:RetryPolicy.withDefaults,Client.doWithRetry]
- In single-install mode,
Initcallsauth.testthroughslackapi.Client.AuthTestunless all three IDs are configured, and writes the team, bot user, and bot IDs on theAdapter. [slack.go:Init] - Posts call
chat.postMessage,chat.postEphemeral, andconversations.open(DM fallback) through the typedslackapi.ClientmethodsPostMessage,PostEphemeral, andOpenConversation. [slack.go:PostMessage,PostEphemeralMessage,postEphemeralFallback] PostNativesends its payload tochat.postMessageasblockswithout decoding it, andOpenModalcallsviews.open. Both go throughslackapi.Client.Call. [interactive.go:PostNative,nativePostPayload,openModalWithToken]ReadHistorycallsconversations.repliesfor threads andconversations.historyfor direct messages. [history.go:ReadHistory]RespondURLPOSTs to theresponse_urlthroughslackapi.Client.PostResponseURL, with no bot token. [interactive.go:RespondURL]Listenopens a Socket Mode websocket throughslackapi.Client.Listenwith the app token. [slack.go:Listen]Webhookserves inbound HTTP through aslackapi.Webhookthat verifies signatures. [slack.go:Webhook]- The delivery handler calls the runtime
chat.DispatchFuncfor each normalized event. [slack.go:deliveryHandler] - Multi-tenant mode calls
InstallStore.Lookupfor each supported delivery with a team ID (anevent_callbackenvelope, a slash command, or ablock_actionsinteraction), and for each outbound call that needs a bot token. Ignored delivery types andRespondURL, which sends no token, do no lookup. The adapter does not cache install records. [slack.go:normalizeDelivery,normalizeEvent,resolveInstall,postToken], [interactive.go:normalizeCommand,normalizeInteraction,RespondURL], [doc.go:slack] - Retry sleeps, observation events (
chat.ObsAdapterCall,chat.ObsRateLimit), and throttle logs come fromslackapi.NewpassesClient,RetryPolicy,Observer, andLoggertoslackapi.New. [slack.go:New], [doc.go:slack] - The package starts no goroutines and holds no mutable package-level state. [slack.go:New]
- Run:
go test ./adapters/slack/... - Emulator tier (opt-in):
mise run emulate, thenmise run test:emulate. It runsTestEmulate*inemulate_test.go, which skip withoutCHAT_EMULATE_SLACK_URL. See "Emulator Tests" inCONTRIBUTING.md. [emulate_test.go:newEmulateSlack]