# Slack Bot Integration


The Slack Bot integration lets a VM drive a real Slack bot: post as the bot,
read history, react, and respond live to mentions and DMs — while the bot's
tokens stay off the VM entirely. You bring your own Slack app; exe.dev holds
its tokens server-side and injects them on the way to Slack.

This is the two-way sibling of the send-only
[Slack Integration](integrations-slack). Use that one if all you need is
"post a message to a channel."

## How it works

Your VM talks to the integration hostname exactly as it would talk to
`slack.com`:

```
curl -X POST https://mybot.int.exe.xyz/api/chat.postMessage --json '{"channel":"C0123","text":"hi"}'
```

exe.dev forwards `/api/<method>` calls to the Slack Web API with your bot
token attached. Any Web API method works; what the bot may actually do is
bounded by the scopes you granted your own app.

For *receiving* events (mentions, DMs), the integration supports Slack's
[Socket Mode](https://api.slack.com/apis/socket-mode): the VM calls one
special method, `apps.connections.open`, and gets back a single-use ticketed
`wss://` URL. The VM connects to that URL directly — an outbound WebSocket,
no inbound ports, no tokens — and events stream in over it.

## Step 1: create your Slack app

Go to [api.slack.com/apps](https://api.slack.com/apps) → **Create New App** →
**From an app manifest**, pick your workspace, and paste:

```json
{
  "display_information": { "name": "my-exe-bot" },
  "features": {
    "bot_user": { "display_name": "my-exe-bot", "always_online": true }
  },
  "oauth_config": {
    "scopes": {
      "bot": ["chat:write", "app_mentions:read", "im:history"]
    }
  },
  "settings": {
    "event_subscriptions": {
      "bot_events": ["app_mention", "message.im"]
    },
    "socket_mode_enabled": true
  }
}
```

This is the minimal talk-to-able bot: it can post, and it hears mentions and
DMs. Add scopes as your bot needs them (`channels:history` to read channels,
`reactions:write` to react, `files:write` to upload, ...). Each event
subscription needs its matching read scope or Slack won't deliver it.

Then collect two tokens:

1. **Bot token** (`xoxb-…`): **Install App** to your workspace, then copy the
   Bot User OAuth Token from **OAuth & Permissions**.
2. **App-level token** (`xapp-…`), only needed for receiving events:
   **Basic Information → App-Level Tokens → Generate**, with the
   `connections:write` scope.

Finally, invite the bot to a channel: `/invite @my-exe-bot`.

## Step 2: create the integration

On the [Integrations page](/integrations), click the **Slack Bot** tile and
paste the tokens — the Test button verifies them against Slack and fills in
the bot's name. Or via SSH, passing `-` so the tokens are
[never on the command line](integrations#providing-secrets-to-integrations-commands).
At the interactive prompt, each `-` asks for its token with hidden input:

```
exe.dev ▶ integrations add slack --name mybot --bot-token=- --app-token=-
Bot token (xoxb-...):
App-level token (xapp-...):
exe.dev ▶ integrations attach mybot vm:my-vm
```

Scripted (`ssh exe.dev integrations add ...`), stdin carries the tokens, one
per line — bot token first:

```
$ printf '%s\n%s\n' "$SLACK_BOT_TOKEN" "$SLACK_APP_TOKEN" | \
    ssh exe.dev integrations add slack --name mybot --bot-token=- --app-token=-
```

The `--app-token` is optional; without it the bot can call the Web API but
not receive events.

## Step 3: use it from the VM

The examples below use the integration hostname
`mybot.int.exe.xyz` — substitute the exact URL printed when you created the
integration (it varies by integration name and environment).

Post as the bot:

```
curl -X POST https://mybot.int.exe.xyz/api/chat.postMessage \
  --json '{"channel":"C0123456789","text":"hello from my VM"}'
```

Sanity-check the token routing:

```
curl -X POST https://mybot.int.exe.xyz/api/auth.test
```

Receive events over Socket Mode. Standard Slack SDKs work once pointed at the
integration host; here is the bare-bones version with just `websockets`:

```python
import asyncio, json, urllib.request, websockets

GW = "https://mybot.int.exe.xyz/api/"  # your integration URL (as printed on creation) + /api/

def gw_post(method, payload=None):
    data = json.dumps(payload).encode() if payload is not None else b""
    req = urllib.request.Request(GW + method, data=data,
        headers={"Content-Type": "application/json"}, method="POST")
    with urllib.request.urlopen(req) as resp:
        return json.load(resp)

async def main():
    url = gw_post("apps.connections.open")["url"]   # ticketed wss URL
    async with websockets.connect(url) as ws:
        async for raw in ws:
            msg = json.loads(raw)
            if "envelope_id" in msg:                 # ack everything
                await ws.send(json.dumps({"envelope_id": msg["envelope_id"]}))
            ev = msg.get("payload", {}).get("event", {})
            if ev.get("type") == "app_mention":
                gw_post("chat.postMessage",
                        {"channel": ev["channel"], "text": "you rang?"})

asyncio.run(main())
```

Mention the bot in Slack and it answers — and at no point did an `xoxb-` or
`xapp-` token exist on the VM. The only credential the VM ever holds is the
single-use WebSocket ticket.

## Notes

- Slack rate-limits the Web API per method. When Slack says slow down,
  exe.dev honors the `Retry-After` and rejects further calls to that method
  locally until the window passes; other methods are unaffected.
- `apps.connections.open` without a stored app token returns 409 with the fix
  (`integrations edit mybot --app-token=-`).
- Slack asks Socket Mode clients to reconnect periodically; SDKs handle this
  automatically. If you hand-roll, just re-open a fresh ticket when the
  socket closes.
- Slack allows at most 10 concurrent Socket Mode connections per app.
- Rotating tokens (e.g. after adding scopes, which requires reinstalling the
  app): `integrations edit mybot --bot-token=- --app-token=-`.
