Expand description
The Slack Web API client: one uniform POST per method, form params, Bearer token, and the envelope rules that turn a logical failure into an error.
Slack answers {"ok": false, "error": "..."} with HTTP 200 for a
logical failure, so a 200 is not success. Only a 429 arrives as a
status, and it carries Retry-After.
Re-exportsยง
use std::collections::HashMap;๐use std::collections::HashSet;๐use std::hash::BuildHasher;๐use std::sync::Arc;๐use std::time::Duration;๐use tokio::sync::oneshot;๐use forge_primitives::slack::SlackBookmark;๐use forge_primitives::slack::SlackConversation;๐use forge_primitives::slack::SlackFile;๐use forge_primitives::slack::SlackFollowedThread;๐use forge_primitives::slack::SlackMessage;๐use forge_primitives::slack::SlackPin;๐use forge_primitives::slack::SlackSearchMatch;๐use forge_primitives::slack::SlackSubscription;๐use forge_primitives::slack::SlackSubscriptionTarget;๐use forge_primitives::slack::SlackThreadOwner;๐use forge_primitives::slack::SlackUser;๐use forge_primitives::slack::SlackWatchMode;๐use serde::Deserialize;๐use serde::Deserialize;๐use serde::de::DeserializeOwned;๐
Modulesยง
- testing
- The pumpโs throttle reporter, for another crateโs log-filter test.
Structsยง
- Auth
Test auth.test- who the token belongs to.- BotProfile ๐
- A bot messageโs own profile.
nameis the appโs name, which is the only display name a bot message carries. - Conversations
Page ๐ - One page with the cursor normalised: Slack sends an empty string on the last page, which would otherwise read as a cursor to page from.
- Envelope ๐
- Slack wraps every payload in
ok, and reports logical failures with HTTP 200. - Message
Page - One page of a conversationโs messages. Both
conversations.historyandconversations.repliesanswer with this shape. - Metadata ๐
- OwnIdentity ๐
- The workspaceโs own identity, read from
auth.testat most once a sweep. - Posted
Message - What
chat.postMessageanswers with.channelis the conversation the message actually landed in: a request addressed by a user id or a channel name does not spell the id the sweep reads it under. - RawConversations
Page ๐ - One page of
users.conversationsas Slack sends it. - RawEdited ๐
- The wireโs
editedobject. Only the editโs owntsis decision-bearing: whose edit it was changes nothing the sweep decides. - RawMessage
Page ๐ - RawWire
Message ๐ - Search
Page - One page of
search.messages, newest first. - Slack
Client Debugis hand-written because the token must never be printed.- Slack
History Message - One message off
conversations.historyorconversations.replies. - Sweep
Outcome ๐ - What one sweep did.
- Throttle
Episode ๐ - Reports throttling once per episode: warn on entry, debug inside it, info when the first clean pass ends it.
- Users
Page ๐ - One page of
users.list, with Slackโs empty last-page cursor normalised the way the conversations page is.
Enumsยง
- Slack
Error - Sweep
Scope ๐ - Which conversations one pass reads. The DM class covers every DM rather than a list, so it costs one history read per DM however many subscriptions exist - which is why it is swept on its own clock, and why a pass reads only what it is for.
Constantsยง
- API_
ROOT ๐ - BODY_
PREFIX_ ๐LEN - Bounded so a gatewayโs error page cannot flood the log or the tool output.
- CONVERSATION_
TYPES ๐ typesmust be passed explicitly or Slack returns public channels only, andlimitmust be passed or the call gets a stricter cap.- FULL_
THREAD_ ๐READ_ INTERVAL - How often a participating thread is read whole. The per-tick walk is cheap - it re-reads from the newest reply - so this is the only pass that finds a correction to a reply below that one, and reading every long thread whole on every tick would spend the sweepโs rate budget.
- MAX_
CONVERSATION_ ๐PAGES - A cursor Slack hands back unchanged must not spin the walk forever: at 200 per page this is far past any real workspace, and the failure mode is an endless request loop against a rate-limited API.
- MAX_
FULL_ ๐THREAD_ READS_ PER_ SWEEP - Whole-thread reads one conversation may spend per sweep. Each pages through its thread, so the budget keeps one long thread from using calls the rest of the sweep needs.
- MAX_
HISTORY_ ๐PAGES - A cursor Slack hands back unchanged must not spin the history walk forever, the same guard the conversation walk carries.
- MAX_
REPLY_ ๐PAGES - A cursor Slack hands back unchanged must not spin the reply walk forever, the same guard the conversation walk carries.
- MAX_
SEARCH_ ๐PAGES - A cursor Slack hands back unchanged must not spin the mention walk forever, the same guard the other walks carry.
- MAX_
USER_ ๐PAGES - A cursor Slack hands back unchanged must not spin the user walk forever: at 200 per page this is far past any real workspace, and the failure mode is an endless request loop against a rate-limited API.
- MENTION_
CURSOR - The watermark key for a workspaceโs whole mention stream. One cursor, not one per conversation: both sides share this constant so a typo cannot silently reset it and re-deliver everything.
- MENTION_
SEARCH_ ๐COUNT - Mentions fetched per search. The workspaceโs newest, since the query is timestamp-sorted.
- POST_
LIMIT - The largest text
chat.postMessagecarries without truncating. The probe measured a silent cut at 4000 withok: true, so a longer draft is split and posted in sequence rather than quietly halved. - POST_
PART_ ๐BUDGET - Budget per part once a draft is split, leaving room for the
[i/n]prefix each part carries. - REQUEST_
TIMEOUT - How long one Web API call gets. The pump waits this long, and every Web API request carries it, so a post that stalls cannot pin its in-flight mark past it. File bodies are not Web API calls and keep their own transfer behaviour.
- RETRY_
CAP ๐ - Ceiling for a hostile or broken
Retry-After. - RETRY_
FALLBACK ๐ - Used when the response carries no
Retry-After. - RETRY_
FLOOR ๐ - Floor for a
Retry-Afterthat asks for no wait at all.Retry-After: 0is legal, and retrying at once is the opposite of what it asked. - SEARCH_
COUNT_ ๐CAP - The APIโs ceiling for one search page.
- SWEEP_
LIMIT ๐ - Messages fetched per
conversations.historycall. - USER_
PAGE_ ๐LIMIT - Slackโs page ceiling for
users.list.
Traitsยง
- Slack
Api - The Web API calls this connector makes, behind a trait so a sweep or an outbound call can be driven from a test double instead of a live workspace.
- Slack
Host - What the pump needs from the workspace. The connector holds no state of its own, so the subscription set, the watermarks and the liveness flag all live behind this.
- Users
Pages ๐ - One page of
users.list. The walk is split from the HTTP client so its page cap is reachable from a double that never stops handing back a cursor.
Functionsยง
- author_
of ๐ - The name to show for a messageโs author, asked once per user per
conversation, or
Nonewhen no name is known - the block drops the clause rather than printing a raw id. - body_
prefix ๐ - covers ๐
- Whether one subscription watches this conversation at all, whatever its mode and whoever wrote the message.
- decode_
bookmarks ๐ - Split out of the async path so the decode is testable without HTTP.
- decode_
conversations_ ๐page - Split out of the async path so the paging rules are testable without HTTP.
- decode_
envelope ๐ - Split out of
callso the envelope rules are testable without HTTP. - decode_
file ๐ - Split out of the async path so the decode is testable without HTTP.
- decode_
message_ ๐page - Split out of the async path so the decode is testable without HTTP.
- decode_
pins ๐ - Split out of the async path so the decode is testable without HTTP.
- decode_
search ๐ - Split out of the async path so the decode is testable without HTTP.
- decode_
user ๐ - Split out of the async path so the decode is testable without HTTP.
- decode_
users_ ๐page - Split out of the async path so the decode is testable without HTTP.
- encode_
form ๐ application/x-www-form-urlencodedbody for one call.- fetch_
history ๐ - Every history message after each page boundary, following the cursor.
One page is
SWEEP_LIMITmessages, and a sweep that read only the first would advance the watermark past everything it never fetched, losing those messages silently. - fetch_
recent ๐ - The conversationโs newest page, and only that page. Not
fetch_history: walking back from a cursor would page through the channelโs whole past, and this is a window over what is recent rather than a read position. - fetch_
replies ๐ - Every reply in a thread, following the cursor. Dedupe is the callerโs:
Slack repeats the parent on every page, so the caller takes the newest by
comparing
tsrather than by taking the last one it read. - first_
bold_ ๐closer - The first
*afteropenable to close a span opened there: non-space before it, no word character after it, and no ability to open a span of its own - otherwise the two stars of a pair of globs would close a span between them. - flatten_
mrkdwn - The prose a message reads as when a card quotes it: Slackโs mrkdwn is
flattened - a mention is
@namefromnames(the bare id when it cannot be named), a link its label or address, and*text*its text. A token this does not understand is left exactly as written. Flattening stops oncelimitnon-space characters have been emitted, which is enough for the excerpt bound; a single long token still scans the text after the bound. - flatten_
token ๐ - One
<...>token flattened. A mention takes its name fromnames, a link its label or address; a channel reference, a broadcast and any token this does not recognise keep their own text. - followed_
parent_ ๐of - The thread a delivered message anchors: its own thread when it is a reply, the thread it starts when it is a parent that already has replies, and nothing for a plain top-level message - or every message would grow the tracked set.
- history_
params ๐ - The params
conversations.historyis called with. Slack calls the newest boundlatest, and both bounds are exclusive. - in_
scope ๐ - Whether this pass is for this conversation.
targetshas already said the conversation is watched by someone; this narrows that to the clock the sweep is running on. - is_
markup_ ๐token - Whether
<token>is one of Slackโs markup tokens rather than text that happens to sit between two brackets. - is_
newer ๐ - Whether
tsis strictly afterwatermark. Timestamps are compared as the strings Slack sent: they carry a microsecond fraction, and parsing one to a number loses it and silently moves the cursor. - is_news ๐
- Whether a message carries news the cursor has not passed: the message
is newer than the cursor, or it was edited after the cursor. An edit
keeps the messageโs own
ts, so the ts test alone drops every correction. - is_
thread_ ๐parent - Whether a message is a threadโs parent, which is what decides whose
replies the sweep fetches. Slack sends a parent with
thread_tsequal to its ownts;thread_tsbeing present therefore says nothing about whether a message is a reply, and a parent is instead the message that reports replies and is its own thread root. - is_
user_ ๐id - Whether a token is a Slack user id rather than some other
<@...>text. - just_
below ๐ - The exclusive read position just below
ts. - matches ๐
- Whether one subscription wants this message. forge posts as the user, so a message is refused by identity - its ts in the outbound ledger - and never by author: refusing every message the user wrote drops what Ved types himself, and a thread he is in then reads one-sided.
- mention_
ids - Every user id a messageโs text tags, once each, in order of appearance.
Slack writes a mention as
<@U...>, optionally carrying a display label after a|; a channel reference and a link start with a different sigil and are not people. - mentions
- Whether
textcarries a mention ofuser_id. - newest_
stamp ๐ - The newest cursor value a message owns: its
ts, or the later ts of its newest edit. A cursor advanced over a handed edit has to clear the edit too, or the next sweep re-offers the message forever and, once the delivery memory lapses, redispatches it. - next_
interval ๐ - The wait before the next sweep: never shorter than the configured interval, and at least the clamped Retry-After when one was reported.
- one_
line_ excerpt - One flattened line of a messageโs text, for a card that quotes it: the
whitespace collapses, and a line past
limitcharacters stops at the last space before the bound where there is one, marked with an ellipsis. - opens_
bold ๐ - A
*opens a bold span only at a word boundary, with text after it and no doubled marker either side - the**of a literal and the*of a glob are literals rather than openers. The rule mirrorsstrip_emphasisinforge-serverโsenvelope.rs; a change there should move here. - own_
name ๐ - The name the tokenโs own user goes by. A failed
auth.testfalls back to the name the host already learned;Noneonly when neither is known, and the caller then uses whatever the wire offered. - owns_
threads ๐ - Whether this subscription wants a threadโs replies. Watching a
conversation is not the same as wanting everything in it: a
mentions-mode subscription asked to be told when it is named, and it is told through the match, so it owns no thread - or every thread the sweep anchors would deliver every reply to a session that asked for mentions. AMentionstarget is swept by search and owns no conversation at all. - page_
from_ ๐raw - participating ๐
- Whether a thread is one the user is in, read from its replies: his own message, or one that names him.
- read_
thread - Every reply in a thread, the parent first, for a caller reading the thread
rather than advancing a cursor. Slack repeats the parent at the top of
every page, so the result is deduped by
ts; the first occurrence wins, which keeps the parent where the contract puts it.limitbounds the rows returned, and the walk stops as soon as it has them. - resolve_
author ๐ - Resolves one author outright, without the per-conversation memo
author_ofkeeps. - retry_
after_ ๐of - Slackโs whole-second
Retry-After, when the response carries one. - retry_
delay ๐ - What to wait after a 429. Slack sends whole seconds.
- run_
dm_ ๐pump - Sweep the DM class on its own task and its own clock, until
shutdownfires. The first pass runs at once - a restart has a gap to catch up on - and every one after it waits the interval. - run_
workspace_ pump - Sweep one workspace forever, until
shutdownfires. The first sweep is delayed by the interval so a boot with many workspaces does not burst. - search_
params ๐ - The params
search.messagesis always called with.sort=timestampbecause the default is relevance, which during the probe ranked a 22-hour-old match above a 2.6-hour-old one. - shared_
messages - Every message a file was shared as, as
(conversation, ts). The conversation is the shareโs own - the channel Slack posted into, which need not be the string the upload was addressed with. Both the uploadโs answer andfiles.infocarry these when Slack fills them. - split_
for_ post - Split
textinto parts each underPOST_LIMIT, numbered so a reader can see the order. Text at or under the limit is one part. - status_
failure ๐ - A non-success status other than 429. The body may be an HTML error page, which the envelope decoder would report as a JSON parse failure
- sweep_
mentions ๐ - Sweep the workspaceโs mention stream. Returns at once when no
Mentionstarget exists, so a workspace with only conversation subscriptions never spends a search call on every tick. - sweep_
scope ๐ - One pass over the conversations in
scope: fetch what is newer than each conversationโs watermark, follow the threads of the parents that report replies, and hand every survivor to the host. - targets ๐
- Whether any subscription names this conversation, ignoring the mode and the author. Decides which conversations a sweep fetches history for: a mentions-only subscription still has to look, or it would never see a mention arrive.
- thread_
parent - The parent message of the thread
tsstarts, which is the message a draftโs card names. An error is the callerโs to draw around: the card names the thread โa threadโ rather than inventing a name. - user_
display_ name - The display name for a user id, from the hostโs cache or a
users.infolookup that is then remembered.Nonewhen the id cannot be named - the caller draws the id rather than a guess. A failure that is not settled is not remembered, so a later ask tries again. - walk_
users ๐ - Every member in the workspace, following the cursor. A cursor that never ends returns what was read, the way the history and reply walks do, rather than failing the whole lookup.
- wants
- Whether any of
subscriptionswants this message.