All notable changes to this project are documented here. The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
0.2.2 - 2026-07-09
Robustness release: no user action, handler return value, or malformed Slack frame can take a transport down.
Fixed
- A
view_submissionhandler can no longer take down the transport. When a handler returned an{:ack, map}whose payload wasn't JSON-encodable (e.g. it held a tuple), encoding the ACK frame raised inside the transport process — on Socket Mode this crashed the connection and the supervisor's restart re-read the same envelope, producing a crash-reconnect loop. The payload is now checked for encodability inSlink.Dispatcher.ack_result/3; a non-encodable one is logged and degrades to%{}(closing the modal), like a handler crash already does. - A malformed Slack frame can no longer crash the Socket Mode connection.
Slink.Eventparsing and accessors usedget_in/2, which raises when a value Slack normally nests as a map (achannel,view,item, or the envelopepayloaditself) arrives as a string, list, or null. Sinceevent_id/1and event construction run in the socket process, that raise dropped the connection. All parsing and accessors are now total — a wrong-shaped payload yieldsnil/empty defaults instead of raising — and the socket wraps per-message handling as a final backstop. - A bad reply body no longer crashes a channel's rate-limit worker. If a
handler's reply carried a body the Web API client couldn't encode, the raise
killed the per-channel
Slink.Rate.Channelworker and dropped everything else queued for that channel. The send is now wrapped so a raising call is logged and draining continues, andSlink.Ratetolerates a worker that fails to start rather than crashing the caller. - The Events API plug no longer 500s on a form body with invalid
percent-encoding —
URI.decode_query/1raising now degrades to an empty payload.
0.2.1 - 2026-07-09
Fixed
reply/3to a reaction event no longer crashes.reaction_added/reaction_removednest their target underpayload["item"], soEvent.channel/1,ts/1now read it there — previously a reply to a reaction raisedArgumentError(no channel) andworking/3was a no-op.
Changed
Event.command/1unwraps Slack link markup to plain text. A linkified address (<mailto:a@b.com|a@b.com>) now comes through as the barea@b.com, and<@U1|alice>/<#C1|general>/<https://x|label>reduce to their name/url — so the addressed text reads as a human typed it. A bare<@U1>(e.g. the bot's own mention) still drops out.
0.2.0 - 2026-07-09
Added
- Slash commands, end to end. Both transports now decode slash-command
payloads (
application/x-www-form-urlencodedover HTTP).reply/3routes by event kind — a channel post for messages and interactions, or theresponse_urlfor a slash command (to: :ephemeraldefault, or:channel) — and raises clearly when an event has no channel to reply to (e.g. aview_submission). NewSlink.Eventaccessors:command_name/1,response_url/1,trigger_id/1. - Interactivity & modals. Interactions are routed on their inner type
(
:block_actions,:view_submission,:shortcut, …). Aview_submissionhandler may return{:ack, map}to control the modal (validation errors, update, push) — it runs synchronously so Slack gets the response in time. New helperopen_modal/2(imported byuse Slink) andSlink.API.open_view/3,update_view/3,push_view/3,publish_view/3(App Home). NewSlink.Eventaccessors:actions/1,action_id/1,action_value/1,callback_id/1,view/1,view_values/1. - Event deduplication (
Slink.Dedup). Slack's retried deliveries (sameevent_id) dispatch only once. On by default;config :slink, :dedup, falseto disable,:dedup_ttl_msto tune. NewSlink.Event.event_id/1,retry?/1,retry_attempt/1. - Wider Web API surface (
Slink.API):update_message/5,delete_message/3,post_ephemeral/5,get_permalink/3,user_info/2, andrespond/2(post to aresponse_url).call/3now retries HTTP429s, honouringRetry-After.
0.1.0 - 2026-07-08
Initial release.
Added
- One event-handling contract, two transports. Write a bot once against the
Slinkbehaviour and run it over either transport unchanged.Slink.SocketMode— dials out to Slack over a WebSocket (Mint.WebSocket), no public endpoint required. Opens the connection viaapps.connections.open, auto-reconnects, and optionally auto-joins channels on boot (:join).Slink.EventsApi.Plug— aPlugfor Slack's HTTP event callbacks. Mount it in a Plug/Phoenix router or run it standalone with Bandit.
- Request hardening (Events API): HMAC-SHA256 signature verification against
the raw body, constant-time comparison, timestamp-freshness (replay) window,
request-body size cap, and automatic
url_verificationhandshake. - Normalised events.
Slink.Eventcollapses Socket Mode envelopes and HTTP payloads into one shape; both transports acknowledge to Slack before your handler runs and dispatch off-process, so a slow handler never blows Slack's ~3s ACK window. - Handler helpers (imported by
use Slink):send_message/3,4,reply/2,3,working/2,3, andin_thread?/1. - Per-channel outbound rate limiting (
Slink.Rate) to stay within Slack's ~1 message/sec/channel limit. Tunable viaconfig :slink, :rate_interval_ms. - Web API client (
Slink.API) built onReq. Slink.enabled?/1to conditionally start a bot from config.- A shippable app manifest (
manifest.json) and a runnableSlink.ExampleBot.