> ## Documentation Index
> Fetch the complete documentation index at: https://acaas.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Cookbook: quiet down a noisy Slack channel

> Build a Python tool that detects shouty Slack messages and runs them through /v1/whisper before display — for the colleague who treats Caps Lock as a personality trait.

Some people send every Slack message in ALL CAPS!!!! This walkthrough builds
a small Python tool that detects those messages, runs them through
[`/v1/whisper`](/api-reference/whisper) to lowercase them, and emits a calmer
version. You can drop the result into a chat display, an internal feed, or a
late-night digest where everyone is too tired for shouting.

Two paths are covered:

* **Batch mode.** Read a Slack export (or any list of messages) from JSON,
  rewrite the shouty ones, write the result back. Runnable without any Slack
  credentials.
* **Live mode.** A Slack Bolt app that listens to channel messages and posts
  a calmed reply in-thread when shoutiness crosses a threshold.

## What you will build

Given an input file `messages.json`:

```json theme={"dark"}
[
  {"user": "U01ALEX", "ts": "1714060800.000100", "text": "GOOD MORNING TEAM!!! WHO IS READY TO SHIP?!?!"},
  {"user": "U02JANE", "ts": "1714060860.000200", "text": "morning! coffee first, ship second"},
  {"user": "U01ALEX", "ts": "1714060920.000300", "text": "I AM ALREADY ON MY THIRD CUP LET'S GOOOOO"},
  {"user": "U03SAM",  "ts": "1714061040.000400", "text": "the staging deploy is green, fyi"}
]
```

The tool produces `quiet.json`:

```json theme={"dark"}
[
  {"user": "U01ALEX", "ts": "1714060800.000100", "text": "good morning team!!! who is ready to ship?!?!", "calmed": true},
  {"user": "U02JANE", "ts": "1714060860.000200", "text": "morning! coffee first, ship second", "calmed": false},
  {"user": "U01ALEX", "ts": "1714060920.000300", "text": "i am already on my third cup let's gooooo", "calmed": true},
  {"user": "U03SAM",  "ts": "1714061040.000400", "text": "the staging deploy is green, fyi", "calmed": false}
]
```

Lowercase messages pass through unchanged. Only the screamers get whispered.

## Prerequisites

* Python 3.10 or newer.
* `pip install requests` for batch mode.
* `pip install slack-bolt` for live mode (optional).
* An ACAAS API key in `ACAAS_API_KEY`. The demo key works.
* For live mode: a Slack workspace where you can install a bot, plus a bot
  token (`SLACK_BOT_TOKEN`) and an app-level token (`SLACK_APP_TOKEN`).

```bash theme={"dark"}
export ACAAS_API_KEY="demo"
```

## Step 1: Decide what counts as shouty

Before calling the API, decide which messages deserve a whisper. A simple
heuristic combines two signals: how much of the message is uppercase, and
how many exclamation marks it carries.

```python shoutiness.py theme={"dark"}
import string


def uppercase_ratio(text: str) -> float:
    letters = [c for c in text if c in string.ascii_letters]
    if not letters:
        return 0.0
    upper = sum(1 for c in letters if c.isupper())
    return upper / len(letters)


def exclamation_density(text: str) -> float:
    if not text:
        return 0.0
    return text.count("!") / len(text)


def is_shouty(text: str, *, min_chars: int = 8) -> bool:
    if len(text) < min_chars:
        return False  # Single words are allowed to be loud.
    return uppercase_ratio(text) > 0.6 or exclamation_density(text) > 0.05
```

The thresholds are deliberately conservative. A message is only flagged as
shouty if more than 60% of its letters are uppercase **or** if exclamation
marks make up more than 5% of the text. Tune them to taste — a more
permissive `0.4` ratio also catches mixed-case yelling like "Why Are We
Doing This Again."

A quick sanity check:

```python theme={"dark"}
>>> is_shouty("good morning team")
False
>>> is_shouty("GOOD MORNING TEAM!!!")
True
>>> is_shouty("ok")
False
>>> is_shouty("the staging deploy is green, fyi")
False
```

## Step 2: Whisper a single message

Wrap `/v1/whisper` in a function. Reuse the `Client` pattern from the
[release notes cookbook](/cookbook/release-notes), or use this minimal
inline version:

```python whisperer.py theme={"dark"}
import os

import requests

BASE_URL = "https://api.acaas.example.com"


def whisper(text: str) -> str:
    api_key = os.environ["ACAAS_API_KEY"]
    resp = requests.post(
        f"{BASE_URL}/v1/whisper",
        headers={"X-API-Key": api_key, "Content-Type": "application/json"},
        json={"text": text},
        timeout=10,
    )
    resp.raise_for_status()
    return resp.json()["result"]
```

For production code, fold in retry handling for `429` and `5xx` as shown in
the [errors reference](/errors). For this walkthrough, the bare call keeps
the focus on the use case.

## Step 3: Process a batch

Wire the heuristic and the whisperer together. The function below preserves
message order and metadata, only mutating `text` when shoutiness fires.

```python calm_batch.py theme={"dark"}
import json
from pathlib import Path

from shoutiness import is_shouty
from whisperer import whisper


def calm_messages(input_path: Path, output_path: Path) -> None:
    messages = json.loads(input_path.read_text())
    calmed = []

    for message in messages:
        if is_shouty(message["text"]):
            calmed.append({**message, "text": whisper(message["text"]), "calmed": True})
        else:
            calmed.append({**message, "calmed": False})

    output_path.write_text(json.dumps(calmed, indent=2) + "\n")


if __name__ == "__main__":
    calm_messages(Path("messages.json"), Path("quiet.json"))
```

Run it:

```bash theme={"dark"}
python calm_batch.py
```

The shouty messages now read at conversational volume. Original timestamps,
user IDs, and any other fields you carry through are preserved untouched —
only `text` is rewritten and a `calmed` boolean is added so downstream code
can highlight or count rewrites.

## Step 4: Wire it to a live Slack channel

Skip this section if batch processing is enough. For real-time calming, use
[Slack Bolt for Python](https://slack.dev/bolt-python/) with Socket Mode so
you do not need a public URL.

```python quiet_bot.py theme={"dark"}
import os

from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler

from shoutiness import is_shouty
from whisperer import whisper

app = App(token=os.environ["SLACK_BOT_TOKEN"])


@app.event("message")
def calm_if_needed(event, client):
    if event.get("subtype") or event.get("bot_id"):
        return  # Ignore edits, joins, and the bot's own posts.

    text = event.get("text", "")
    if not is_shouty(text):
        return

    client.chat_postMessage(
        channel=event["channel"],
        thread_ts=event["ts"],
        text=f"_at conversational volume:_\n> {whisper(text)}",
    )


if __name__ == "__main__":
    SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"]).start()
```

A few choices worth calling out:

* **Reply in-thread, do not edit.** Slack's API only lets a bot edit messages
  it posted itself. Posting a calmed reply in-thread keeps the original
  message intact and lets the author see the contrast.
* **Skip subtypes and bot messages.** Channel joins, edits, and the bot's
  own posts arrive as the same `message` event. Filtering them prevents
  loops and noise.
* **Italics for the prefix.** `_at conversational volume:_` reads as a
  gentle aside in Slack, not a callout.

Required scopes for the bot: `chat:write`, `channels:history`,
`groups:history`, `im:history`, `mpim:history`, plus the corresponding
`message.*` event subscriptions. Install the app to a test channel before
turning it loose on `#general`.

<Warning>
  Auto-rewriting other people's messages — even as a thread reply — can
  feel passive-aggressive. Pilot the bot in an opt-in channel first, and
  consider a slash command (`/whisper`) as a less invasive alternative.
</Warning>

## Step 5: Smoke-test the heuristic

Before turning the bot on, run the heuristic against a varied corpus to
make sure the threshold is tuned for your team. The included fixture
covers the usual cases:

```python smoke.py theme={"dark"}
import json
from pathlib import Path

from shoutiness import is_shouty

messages = json.loads(Path("messages.json").read_text())
for m in messages:
    flag = "SHOUTY" if is_shouty(m["text"]) else "ok"
    print(f"{flag:>6}  {m['text']}")
```

Expected output on the sample fixture:

```text theme={"dark"}
SHOUTY  GOOD MORNING TEAM!!! WHO IS READY TO SHIP?!?!
    ok  morning! coffee first, ship second
SHOUTY  I AM ALREADY ON MY THIRD CUP LET'S GOOOOO
    ok  the staging deploy is green, fyi
```

Adjust thresholds in `shoutiness.py` until your own corpus produces
sensible flags before connecting to a live workspace.

## What you learned

* **Heuristic before API call.** A cheap local check (`is_shouty`) keeps
  most messages out of the request path entirely. Only the loud ones cost
  a quota slot.
* **Preserve metadata, mutate only what you must.** Carrying `user`, `ts`,
  and any other fields through unchanged makes the calmed output a
  drop-in replacement for the original.
* **Reply, do not rewrite.** Posting a thread reply respects authorship
  and Slack's API permissions.
* **Pilot before you scale.** Auto-rewriting tone is a social intervention
  as much as a technical one. Start in a small, consenting channel.

## Next steps

<CardGroup cols={2}>
  <Card title="Release notes amplifier" icon="megaphone" href="/cookbook/release-notes">
    The companion cookbook — uses scream and emphasize end-to-end.
  </Card>

  <Card title="Whisper reference" icon="volume-low" href="/api-reference/whisper">
    Full request and response schema for `/v1/whisper`.
  </Card>

  <Card title="Errors" icon="circle-exclamation" href="/errors">
    Add retry handling once you move past the demo key.
  </Card>

  <Card title="Rate limiting" icon="gauge-high" href="/rate-limiting">
    Pace the bot if your channel is genuinely chatty.
  </Card>
</CardGroup>
