Browse modules

slack

v0.1.0 · compiled with katari 0.1.0

Overview

from the package README

slack — a Slack bot capability over Socket Mode

A single module, slack, plus its FFI sidecar src/slack.ts: watch a channel's messages and post replies — threads and file attachments included — the Slack twin of the Discord package. Socket Mode means no public URL and no request-signature verification: the sidecar opens an outbound WebSocket with the app-level token and Slack pushes events over it, while the bot token drives the Web API.

  • slack.provider(bot_token = ..., app_token = ...) — connects ONCE and serves the client handle for the extent of the continuation.
  • slack.watch_messages(channel, deliver_to) — serve a channel forever, delivering each incoming message (channel / user / text / thread_ts / files) to your agent. Bot posts (this bot's own replies included) are not delivered, so replying cannot loop.
  • slack.send_message(channel, text, files, thread_ts ?= null) — post to a channel; a message's ts as thread_ts replies in its thread, and files upload as attachments.
  • slack.send_files(channel, files, caption) — the tool shape of the above, for handing to a model.

Files are first-class on both directions: an incoming message's attachments arrive as file values (downloaded with the bot token, since Slack file URLs are private), and outgoing file values upload via files.uploadV2.

Threads: a delivered thread_ts is the thread the message was posted in, or null for a top-level message — pass it straight back to send_message to reply where the message came from (in its thread if it had one, in the channel otherwise).

Delivery is at-least-once: every event is acknowledged on arrival (so a slow handler does not turn into duplicates), but an acknowledgement lost to a dropped socket makes Slack re-send the event, and no dedup memory is kept here.

Slack app setup

  1. Create an app at api.slack.com/apps ("From scratch").
  2. Socket Mode: Settings → Socket Mode → enable it. Generate the app-level token with the connections:write scope — this is the xapp-… token (SLACK_APP_TOKEN).
  3. Scopes: Features → OAuth & Permissions → Bot Token Scopes: add chat:write (post messages), files:write (upload attachments), files:read (download incoming attachments).
  4. Events: Features → Event Subscriptions → enable, then under "Subscribe to bot events" add the message events for the conversations you watch: message.channels (public channels), message.groups (private channels), message.im (DMs). Socket Mode needs no Request URL.
  5. Install the app to your workspace: OAuth & Permissions → Install. The Bot User OAuth Token is the xoxb-… token (SLACK_BOT_TOKEN).
  6. Invite the bot to the channel (/invite @your-bot) and copy the channel id (the C… value in the channel's details).

Secrets / env

  • SLACK_BOT_TOKEN — the bot token (xoxb-…), used for every Web API call and attachment download.
  • SLACK_APP_TOKEN — the app-level token (xapp-…), used only to open the Socket Mode connection.

Store both in the runtime: katari env set SLACK_BOT_TOKEN --secret and katari env set SLACK_APP_TOKEN --secret. Each is a string of private, passed straight to the sidecar and never surfaced elsewhere.

Sidecar dependencies

src/slack.ts imports @slack/socket-mode, @slack/web-api and @katari-lang/port. They are declared in package.json; run pnpm install (or npm install) in this package so katari apply can bundle the sidecar. (A pure-Katari consumer that never applies this package does not need them.)

Usage

import slack
 
// Echo every message back where it came from (its thread if it had one), attachments included.
agent echo(channel: string, user: string, text: string, thread_ts: string | null, files: array[file]) -> null {
  slack.send_message(channel = channel, text = f"<@${user}> said: ${text}", files = files, thread_ts = thread_ts)
}
 
agent main() -> never {
  use slack.provider(
    bot_token = env.get_secret(key = "SLACK_BOT_TOKEN"),
    app_token = env.get_secret(key = "SLACK_APP_TOKEN"),
  )
  slack.watch_messages(channel = "C0123456789", deliver_to = echo)
}

Hand slack.send_files to an AI loop's tool list to let the model post images or documents on its own.

slack

12 declarations

get_slack_client#

request
request get_slack_client() -> string

The live Slack connection (the Socket Mode and Web API clients live in the sidecar; the handle is opaque).

Returns

string
Wire view (JSON Schema)

provider#

agent
agent provider[R, effect E](bot_token: string of private, app_token: string of private, continuation: agent(value: null) -> R with {...E, get_slack_client}) -> R with E | io | prelude.throw[slack_error]

Provide the Slack connection for the extent of the continuation: connect once over Socket Mode, then serve get_slack_client to everything downstream. A failed connect (a bad xapp-… token, a transient network fault) raises slack_error (auth_error | api_error) — the connection is fixed at start, so a bad credential surfaces here rather than on the first send.

Generics

Rtype
Eeffect

Parameters

bot_token

The bot token (xoxb-…) every Web API call authenticates with.

of private
string
app_token

The app-level token (xapp-…) the Socket Mode WebSocket opens with.

of private
string
continuation

Runs with get_slack_client served; its result is the provider's result.

agent
input
object
value
null
output
R
effects
override
base
E

Returns

R

Effects

Inferred type

agent(app_token: string of private, bot_token: string of private, continuation: agent(value: null) -> T0 with get_slack_client | E1) -> T0 with throw[api_error | auth_error] | E1 | io

Wire view — the wire shape is fixed at the call site, where the generic parameters are instantiated.

create_slack_client#

external agent
external agent create_slack_client(bot_token: string of private, app_token: string of private) -> string with prelude.throw[slack_error]

Connect to Slack — open the Socket Mode WebSocket and the Web API client — and return an opaque client handle. Raises slack_error (auth_error | api_error) when the connect fails — a bad app-level token is auth_error, a transient network fault api_error. Prefer the provider.

Parameters

bot_token

The bot token (xoxb-…) every Web API call authenticates with.

of private
string
app_token

The app-level token (xapp-…) the Socket Mode WebSocket opens with.

of private
string

Returns

string
Wire view (JSON Schema)

slack_close#

external agent
external agent slack_close(client: string) -> null

Close a Slack connection: disconnect the Socket Mode WebSocket and drop the client handle. The provider arms this as a finally, so the connection dies with the run — a socket left open keeps receiving events, and Slack round-robins each event across every socket open on the app token, so a dead run's zombie socket would swallow messages (acking them) that a live bot then never sees. Idempotent: closing an unknown or already-closed handle is a no-op, since a finalizer may run more than once.

Parameters

client

The opaque handle create_slack_client returned.

string

Returns

null
Wire view (JSON Schema)

auth_error#

data
data auth_error(message: string)

An authentication / authorization failure: the bot token is invalid, revoked, or missing a scope the call needs (Slack invalid_auth, token_revoked, missing_scope, …); message carries Slack's error string. The bot cannot recover on its own — the operator must fix the token or scopes and restart the run (the connection is fixed at start). Let it surface so the bot stops loudly rather than silently dropping every reply.

Parameters

message
string
Wire view (JSON Schema)

api_error#

data
data api_error(message: string)

A Slack API call failed for a non-auth reason: a rate limit, a transient network fault, a channel the bot is not in, a message too long; message carries Slack's error string. Usually per-message or transient, so a bot catches it to drop just that reply and keep serving.

Parameters

message
string
Wire view (JSON Schema)

slack_error#

type
type slack_error = auth_error | api_error

Definition

slack_send#

external agent
external agent slack_send(client: string, channel: string, text: string, thread_ts: string | null, files: array[file]) -> null with prelude.throw[slack_error]

Low-level: post to a channel on a given client — a thread reply when thread_ts carries a ts, a channel post when it is null; any files upload as attachments. Raises auth_error or api_error (slack_error) when the Slack API call fails. Prefer the send_message agent.

Parameters

client

The opaque handle create_slack_client returned.

string
channel

The id of the channel to post in (e.g. "C0123456789").

string
text

The message text (Slack mrkdwn); with files it becomes their caption.

string
thread_ts

The parent message's ts to reply in its thread, or null to post to the channel.

one of
string
null
files

Files to upload as the message's attachments ([] for a plain text post).

array
file

Returns

null
Wire view (JSON Schema)

slack_watch#

external agent
external agent slack_watch[effect E](client: string, channel: string, deliver_to: agent(channel: string, user: string, text: string, thread_ts: string | null, files: array[file]) -> null with E) -> never with E

Low-level: listen on a channel and deliver each incoming message (channel / user / text / thread_ts / attachments as files) to a callback. Prefer the watch_messages agent.

Generics

Eeffect

Parameters

client

The opaque handle create_slack_client returned.

string
channel

The id of the channel to listen on; messages elsewhere are ignored.

string
deliver_to

Called once per incoming message; its effects E flow to the caller's handlers.

agent
input
object
channel
string
user
string
text
string
thread_ts
one of
string
null
files
array
file
output
null
effects
E

Returns

never

Effects

E

Wire view — the wire shape is fixed at the call site, where the generic parameters are instantiated.

send_message#

agent
agent send_message(channel: string, text: string, files: array[file], thread_ts: string | null ?= null) -> null

Post a message to a channel using the ambient Slack client. Pass a message's ts as thread_ts to reply in its thread (an incoming message's thread_ts passed straight back replies where it came from); files upload as attachments (pass [] for a plain text post — parameter defaults only take literals, so the empty array cannot default). Raises slack_error (auth_error | api_error) when the send fails — catch api_error to keep a bot serving past a transient failure, and let auth_error surface so a bad token stops the bot loudly.

Parameters

channel

The id of the channel to post in (e.g. "C0123456789").

string
text

The message text (Slack mrkdwn); with files it becomes their caption.

string
files

Files to upload as the message's attachments ([] for a plain text post).

array
file
thread_ts?= null

The parent message's ts to reply in its thread; omit (or pass null) to post to the channel.

one of
string
null

Returns

null

Inferred type

agent(channel: string, files: array[file], text: string, thread_ts?: string) -> null with throw[api_error | auth_error] | get_slack_client | io
Wire view (JSON Schema)

send_files#

agent
agent send_files(channel: string, files: array[file], caption: string) -> null

Tool: post files (images, documents) to a Slack channel, with a caption. Use the channel id given in the conversation context; pass each file as its handle object — a bare {"$katari_ref": "<id>"} suffices.

Parameters

channel

The id of the channel to post in, from the conversation context.

string
files

The files to upload, each passed as its handle object.

array
file
caption

The text posted alongside the files.

string

Returns

null

Inferred type

agent(caption: string, channel: string, files: array[file]) -> null with throw[api_error | auth_error] | get_slack_client | io
Wire view (JSON Schema)

watch_messages#

agent
agent watch_messages[effect E](channel: string, deliver_to: agent(channel: string, user: string, text: string, thread_ts: string | null, files: array[file]) -> null with E) -> never with E | get_slack_client | io

Serve a channel forever, delivering each incoming message to deliver_to — shaped like time.watch / google_calendar.watch so it composes under parallel [ … ]. The bot's own posts (and other bots') are not delivered, so replying from deliver_to cannot loop. A delivered thread_ts is the thread the message was posted in, or null for a top-level message — pass it straight to send_message to reply where the message came from. Delivery is at-least-once: Slack re-sends an event whose acknowledgement was lost, and no dedup memory is kept here.

Generics

Eeffect

Parameters

channel

The id of the channel to listen on (e.g. "C0123456789").

string
deliver_to

Called once per incoming message; its effects E flow to the caller's handlers.

agent
input
object
channel
string
user
string
text
string
thread_ts
one of
string
null
files
array
file
output
null
effects
E

Returns

never

Effects

Inferred type

agent(channel: string, deliver_to: agent(channel: string, files: array[file], text: string, thread_ts: null | string, user: string) -> null with E3) -> never with get_slack_client | E3 | io

Wire view — the wire shape is fixed at the call site, where the generic parameters are instantiated.