Skip to content

Slack as a connector: one flat DM that works like Telegram - #670

Open
woltspace-asharakeh-contrib[bot] wants to merge 12 commits into
jerpint:mainfrom
asharakeh:slack-integration
Open

woltspace-asharakeh-contrib[bot] wants to merge 12 commits into
jerpint:mainfrom
asharakeh:slack-integration

Conversation

@woltspace-asharakeh-contrib

@woltspace-asharakeh-contrib woltspace-asharakeh-contrib Bot commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

🦊 Bob the Planner (planner): ## TL;DR

Slack becomes a chat connector, the same kind as Telegram. It's one flat DM that works exactly like Telegram: commands, the dog, voice, images and files, reply routing and the inbox. Target: 0.7. The "hold your first message while you pick a wolt" change is now its own PR, stacked on this one.

For humans

The problem. Slack was the last chat app still wired into the lodge by hand, so it behaved differently from Telegram:

  • a picker for every new DM;
  • one thread per session;
  • no commands, no dog, text only;
  • it bypassed the inbox.

Every Slack fix was a second copy of something the shared chat code already did.

What changes.

  • Slack is one DM with an active wolt.
    • /wolt and /new switch, at the top of the DM. Slack keeps /apps for itself, so the apps list is /wapps.
    • Inside a thread, Slack doesn't run app slash commands, so typed !wolt, !new and !help work everywhere, and buttons cover the rest.
    • Replying in a thread under a wolt's message routes to that wolt.
    • Voice notes, images and files arrive like on Telegram.
    • Your text arrives as plain text: Slack's link, mention and escape markup is removed.
  • With no wolt chosen, Slack behaves like Telegram today: the dog answers, or you pick a wolt and send the message again.
  • Setup stays the same. The channels.slack config is unchanged. The Slack app gains the commands and files:read scopes and drops assistant:write, then needs one reinstall.

What goes away. Slack threads per session and Slack's "Agent View" processing status. This changes how an existing Slack setup behaves, so it targets 0.7.

For Woltspace contributors

Problem

container/bot/slack_adapter.py (776 lines) imported lodge code and kept its own routing, its own picker queue and its own outbound path. Slack also had special branches in:

  • server/notify.py and server/notification_senders.py;
  • container/lib/connector_registry.py;
  • src/woltspace/channels.py.

That was the opposite of the connector design (RFC #617: "the lodge decides, connectors translate").

Solution

flowchart TD
  SL[Slack DM] <--> P["container/connectors/slack/<br>connector.json + slack_connector.py<br>(translate only)"]
  P <-->|"socket: event / say / edit / ack / send"| CL["chat_lodge, lodge_messages, lodge_commands<br>(shared with Telegram)"]
  CL --> IN[inbox, sessions, dog]
  X["removed: bot/slack_adapter.py,<br>Slack branches in notify, registry, launcher, app"]
Loading
  • Slack folder connector (container/connectors/slack/):
    • Socket Mode in the connector process only, talking to the lodge through connector_client.
    • Owner filter: owner message.im only, failing closed.
    • Route field chat_id = the DM channel, so old Slack session records resolve to the flat DM through the generic codec.
    • Plain text in: the owner's text and captions, and a thread parent's text, are unwrapped from Slack markup (<url|label>, &lt;) before command parsing and routing.
    • Text commands use the ! prefix. Slash names that are Slack built-ins are mapped (/wapps → apps).
    • Thread reply under this bot's own message → reply_to.
    • Bot token on downloads: the token is only ever sent to Slack's own file host (https://files.slack.com).
    • Files out: every file that goes out (a notify --file or a dog image) passes the outbox check, and a dog image keeps its own file name.
    • Startup refusal: a manifest owner_pattern refuses to start without a usable owner id, with the remedy.
    • Wolf pings: a default_route_env keeps SLACK_NOTIFY_CHANNEL working.
  • Compatibility:
    • notify --slack C [T] and --to slack:C:T normalize to the flat route;
    • the channels.slack config keys are unchanged.

Where to focus

  1. The owner-DM fail-closed filter, and reply routing that trusts only this bot's own parent message.
  2. Markup normalization at the connector boundary (text, captions, thread parents).
  3. The removals: nothing Slack-specific is left in core.
  4. The legacy route normalization for stored Slack routes.

Validation

  • Tests: test-first throughout, with an independent reviewer's negative tests (all review findings fixed, including five from a maintainer-side read).
  • Full suite: 4,065 run, 0 new failures against the base's known list. No new Ruff/ty diagnostics.
  • Real Slack workspace, on a lodge running this work:
    • /wolt and /wapps;
    • !wolt in a thread;
    • a voice note;
    • a PDF;
    • a thread reply reaching the right wolt;
    • a link and a mention arriving as plain text;
    • a captioned image;
    • DMs in and out.
  • Size against main: production +535/−1,222, tests +585/−1,575, docs, skills and the setup manifest +155/−212, CI +5/−1.

Limits

  • Mentions: a mention arrives as the raw user id (@U…), not the display name. That's a possible follow-up.
  • Ingress: a connector-to-lodge crash before the inbox stores a message can still lose it, as before.
  • Telegram button data: the 64-byte limit fits wolt names up to 50 characters.
  • Behaviour changes: Slack voice notes are now opus files; wolf pings to Slack need the connector running; /sessions/new/slack is removed.
  • Follow-up PR: the held first message, for every connector, is stacked on this one.

Slack was the last chat app still outside the connector design: a
776-line adapter importing lodge code, a picker for every new DM, one
thread per session, Agent View status, no commands, no dog, text only,
and its own copies of the notify transport, launcher class and route
spec. It now runs from its own folder, container/connectors/slack/
(connector.json and slack_connector.py), and only translates: the
owner's DMs in, Block Kit replies out, over the lodge's connector
socket. The shared chat code does the rest, as for Telegram: an active
wolt, the eight commands, the dog, files and voice, reply routing, the
inbox.

- In: message.im from the one configured member ID in a D... channel,
  each Slack event once; shared files (files:read) saved in the
  connector's uploads dir; a thread reply carries its parent when this
  bot wrote it, so the lodge routes it to that message's session; slash
  commands and clicks, acknowledged before anything else. Slack runs no
  custom slash command in a thread, so commands may also be typed with
  "!" anywhere.
- Out: say, edit and ack as Block Kit in the DM; notify as
  chat.postMessage or a file upload, with Slack's answer mapped to
  delivered, refused, failed or ambiguous.
- Removed: bot/slack_adapter.py, Slack's sender and outbound entry in
  notify, its route spec in connector_registry, SlackConnector in the
  launcher, /sessions/new/slack.
- Kept: the channels.slack block (and its env names); the route field
  is chat_id, so every stored Slack record resolves to its DM.
  `notify --slack C T` and `--to slack:C:T` still work, the thread
  ignored; only flat routes are written. A connector may declare a
  default route env: SLACK_NOTIFY_CHANNEL stays the default for a
  notification that names no chat, and one of the lodge's own chats.

The setup skill, its app manifest (scopes chat:write, commands,
files:read, files:write, im:history; the eight slash commands; no
Agent View) and the Slack docs are rewritten. A 0.6 change: threads per
session and Agent View are gone, and the app needs one reinstall.
A reply under one of this bot's messages carried the parent's text as
Slack stores it, mrkdwn: the session link in its footer read
<https://...> or <https://...|label>, and the session parsed from it had
">" or "|label>" glued on. The connector now gives the lodge the text as
written (links unwrapped, &lt; &gt; &amp; restored); only this bot's own
messages still count.

An enabled Slack without a usable owner member ID started anyway and
ignored every message. A bundled connector may now declare its owner id
as a pattern (owner_pattern); without an owner that matches, the
launcher does not start it and names the setting to fix, as the Slack
adapter did.
A shared file's download link came from the event, and the connector
sent the bot token to whatever it named. It now downloads only from
https://files.slack.com, where Slack's url_private points, and refuses
any other link before a request is made.
Slack refuses to save an app manifest that registers one of its own
built-in slash commands, and /apps is one. Drop it from the setup
manifest and the docs; !apps works anywhere, as do the buttons.
Slack refuses an app manifest that registers one of its own built-in
slash commands. Register the lodge's apps command as /wapps, and map
it back to apps in the connector, so all eight commands stay slash
commands; !apps keeps working too.
Slack hands the connector a message as it stores it, mrkdwn: "<" as
&lt;, a link as <https://...|label>, a mention as <@U...>. That text went
to the wolt, and to the "!" command parser, as stored. It is now
unwrapped first, as a thread parent's already was, for messages and
file captions alike.

The connector sent any photo path the lodge named, while every file a
notification sends must come from the outbox. A dog's generated image
lives in its wolt's images folder, so the lodge now stages it in the
outbox before it says it, and the connector sends a photo only from
there; the outbox sweep removes the copy later. The Slack doc names the
release this lands in: 0.7.
…en resend

A dog's image is staged in the outbox for its connector, and only
notify's sweep removed old entries: a lodge whose wolts never send a
file kept every copy. Staging an image now sweeps the outbox first.

On its own, this connector answers a message sent with no wolt chosen
with the picker and asks for it again; the setup skill and the doc no
longer promise that the message is held.
The lodge staged a dog's image in the outbox under an id, and Slack
showed every one as "image.png". It is now staged in a folder of its
own in the outbox (image-<random>/<its name>), and the Slack connector
uploads it under the file's name. Image folders past the outbox's age
limit are removed when the next image is staged, as notify sweeps the
outbox's files.
@woltspace-asharakeh-contrib woltspace-asharakeh-contrib Bot changed the title Slack as a connector: one flat DM like Telegram, and a held first message for every connector Slack as a connector: one flat DM that works like Telegram Nov 7, 2026
On a Python that ships the standard library's own test package (as CI's
does), "from test.x import" and pytest_plugins = ["test.x"] resolve to
that package and collection fails. Use the plain module names the rest
of the suite uses.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants