correspond.channels.slack#

Slack: drafts in Slack’s own formatting; reading, polling and posting with a token; posting through a webhook.

Reference: slack:<channel>, a Slack channel by its name (# optional, lower-cased; 1-80 of a-z, 0-9, - and _) or by its id (C024BE91L, kept as Slack writes it). A post goes through the channel’s incoming webhook, which Slack ties to one channel when it is created: $CORRESPOND_SLACK_WEBHOOK_<CHANNEL> (upper case, - as _ and _ as __, so no two channels share a variable), else on macOS the Keychain item correspond-slack-webhook-<channel>. The webhook URL is a credential: it never appears in a reference, a plan or an error.

Three uses, one channel, each needing a little more set up:

  • A draft the operator sends. send(..., dry_run=True) needs nothing configured: the plan holds the text rendered as Slack mrkdwn (to_mrkdwn()), its length against Slack’s limit, and says nothing was sent. That is the whole interaction when an agent writes the reply and a person pastes it, including on a workspace where no app will ever be installed.

  • A post through a webhook. With the channel’s webhook, send posts the same text there, as the app that owns the webhook. A webhook cannot reply in a thread.

  • The Web API, with a token (CORRESPOND_SLACK_TOKEN: a bot token xoxb- acts as the app, a user token xoxp- as the person). read returns a channel’s latest top-level messages (conversations.history), listen polls for newer ones (the cursor is a message ts), and send posts with chat.postMessage, replying in a thread when reply_to is a message’s ts. A channel given by name is looked up once with conversations.list. Install the app in your own workspace: Slack limits a commercially distributed app outside its Marketplace to one history request a minute. Slack.verify() checks a request Slack sends (X-Slack-Signature, with the

    signing secret), graded crypto. A GovSlack workspace sets api_base to https://slack-gov.com/api (CORRESPOND_SLACK_API_BASE); the token then goes only to *.slack-gov.com hosts, Socket Mode included.

Slack’s mrkdwn is not Markdown: *bold*, _italic_, ~strike~, <url|text> links, no headings, no tables, and &, < and > escaped. to_mrkdwn() converts the common Markdown into it, so a draft written as GitHub Markdown does not paste as literal asterisks and pipes. Because every < in the text is escaped, a draft cannot carry a Slack mention (<!channel>, <@U...>) that would notify people.

Messages read are platform: Slack authenticated the account, and correspond fetched them itself. Neither the token nor a webhook URL appears in a plan or an error.

>>> to_mrkdwn("**Fixed** in [#12](https://example.org/12) & shipped")
'*Fixed* in <https://example.org/12|#12> &amp; shipped'

Module Attributes

MAX_TEXT_CHARS

Slack truncates a message's text past this many characters.

Functions

to_mrkdwn(markdown)

Common Markdown as Slack mrkdwn: bold, italic, strike, links, headings, lists, code, quotes, tables as code.

webhook_env(name)

The environment variable holding one name's webhook URL.

Classes

Slack(*[, http, keychain, run, clock, ...])

Slack: drafts in Slack's own formatting; reading, polling and posting with a token; posting through a webhook.

correspond.channels.slack.MAX_TEXT_CHARS = 40000#

Slack truncates a message’s text past this many characters.

class correspond.channels.slack.Slack(*, http=<function urllib_http>, keychain=None, run=<function run>, clock=<built-in function time>, sleep=<built-in function sleep>, events=None, socket=<function socket_window>, stamps=None)[source]#

Bases: object

Slack: drafts in Slack’s own formatting; reading, polling and posting with a token; posting through a webhook.

audience(ref, *, draft=None)[source]#

The channel’s members, never listed; a shared channel reaches other workspaces.

Return type:

Audience

property capabilities: Capabilities#

post; with nothing: a draft.

Type:

With a token

Type:

read, poll, post and reply in threads; with a webhook

property events: MutableMapping[str, Any]#

What the Events API receiver stored (read when delivery is events).

parse_ref(id)[source]#

A channel’s id as Slack writes it, or its name, # optional, lower-cased; /<thread ts> after it names one thread.

Return type:

ConversationRef

poll(ref, *, cursor=None, limit=None)[source]#

Top-level messages newer than cursor, oldest first; a first poll looks back a day.

Slack returns history newest first, so a poll walks forward through time windows (oldest = the cursor’s ts, latest = it plus a width, both included): each window comes back whole and is delivered before the next, and the cursor moves to its end, never past Slack’s own clock (its Date) less a minute. A window holding a full page is narrowed, from that page’s own spread, and asked again; an empty one widens the next. The cursor is <ts>/<width>, so a poll that only narrowed or widened still moves forward, and a plain ts (an older cursor) is read too. A poll stops after MAX_REQUESTS_PER_POLL requests, at limit events, or at a rate limit, keeping what it delivered: the next resumes there.

read(ref, *, since=None, limit=None)[source]#

The channel’s latest top-level messages (limit, else 50), oldest first; for a thread, its parent and replies.

Return type:

list[Message]

send(ref, draft, *, dry_run=False)[source]#

Render draft as mrkdwn, then post it with the token (chat.postMessage), else through the channel’s webhook.

draft.reply_to (a message’s ts) posts in that thread with a token; through a webhook, which cannot thread, it renders in a dry run for the operator to paste.

Return type:

SendResult

property stamps: Any#

shared with the Events API receiver (correspond.stores.arrival_stamps()).

Type:

Where a Socket Mode listen takes arrival stamps

verify(headers, body)[source]#

crypto when X-Slack-Signature matches the signing secret over a fresh timestamp, forged otherwise.

Raises MissingRequirement when the signing secret is unset (a configuration error, never a verdict on the request). Anything that is not a well-formed signed request (headers that are not a mapping, a body that is not bytes, a timestamp that is not a short run of digits) is graded forged, never raised. Replay protection is the five-minute window only: act idempotently on an event’s id.

Return type:

Authenticity

correspond.channels.slack.to_mrkdwn(markdown)[source]#

Common Markdown as Slack mrkdwn: bold, italic, strike, links, headings, lists, code, quotes, tables as code.

Only http, https and mailto targets become links: a Markdown link to !channel or @U123 stays text, so no draft can notify a channel or a person.

>>> to_mrkdwn("# Release\n- **new**: _faster_ export\n- ~~old~~ gone")
'*Release*\n• *new*: _faster_ export\n• ~old~ gone'
>>> to_mrkdwn("> quoted <b> & `a < b`")
'> quoted &lt;b&gt; &amp; `a &lt; b`'
>>> to_mrkdwn("[ping](!channel)")
'ping (!channel)'
Return type:

str

correspond.channels.slack.webhook_env(name)[source]#

The environment variable holding one name’s webhook URL.

>>> webhook_env("team-alerts"), webhook_env("team_alerts")
('CORRESPOND_SLACK_WEBHOOK_TEAM_ALERTS', 'CORRESPOND_SLACK_WEBHOOK_TEAM__ALERTS')
Return type:

str