Agents Files
This commit is contained in:
36
AGENTS.md
Normal file
36
AGENTS.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Repository Guidelines
|
||||
|
||||
## Project Structure & Module Organization
|
||||
|
||||
This repository is a small, script-first Python project. `bot.py` is the long-running Twitch IRC bot that controls MPD. `_twitch.py` holds shared Helix auth and token-refresh helpers for the CLI tools. `search-game.py` resolves free-form game queries to Twitch category IDs, and `set-channel.py` updates the channel title and game. `examples/stream.sh` is the interactive pre-stream wrapper that writes `examples/stream.json`. Keep secret-bearing runtime files in `.env` or `.env.broadcaster`; only the `*.example` templates are tracked.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
|
||||
There is no package build step. Use direct script execution:
|
||||
|
||||
```bash
|
||||
pip install python-mpd2
|
||||
python3 bot.py
|
||||
TWITCH_BOT_DEBUG=1 python3 bot.py
|
||||
./search-game.py "DOOM"
|
||||
./set-channel.py --dry-run --game-id 517520 "Stream title"
|
||||
./examples/stream.sh
|
||||
```
|
||||
|
||||
`python3 bot.py` runs the chat bot. `TWITCH_BOT_DEBUG=1` dumps raw IRC lines for troubleshooting. The two CLI helpers are intended to be run directly, and `stream.sh` composes them into a guided flow.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
|
||||
Match the existing style: Python 3.10+, standard library first, minimal dependencies, and small top-level scripts instead of packages. Use 4-space indentation in Python and keep helpers simple and explicit. Follow the current naming pattern: `snake_case` for functions and variables, short module names for single-purpose scripts, and uppercase constants such as `SKIP_COOLDOWN`. Shell scripts should keep `set -euo pipefail`.
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
There is no automated test suite yet. Before opening a PR, do targeted smoke tests for the code path you changed. Examples: run `python3 bot.py` against a local MPD instance, run `./search-game.py "Heroes"`, or use `./set-channel.py --dry-run ...` to validate argument handling without issuing a PATCH. If you add tests later, place them under a new `tests/` directory and use `test_*.py`.
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
|
||||
Git history is minimal; the current commit style is short and imperative (`Initial version`). Keep commits concise, scoped, and written in the imperative mood. PRs should explain the user-visible behavior change, note any `.env` or token-scope impact, and include terminal output or screenshots when changing interactive flows such as `examples/stream.sh`.
|
||||
|
||||
## Security & Configuration Tips
|
||||
|
||||
Do not merge `.env` and `.env.broadcaster`. The bot account and broadcaster account require different scopes and are intentionally separated. Never commit live tokens or generated `examples/stream.json`.
|
||||
102
CLAUDE.md
Normal file
102
CLAUDE.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## What this is
|
||||
|
||||
Standalone Twitch chat bot that controls a local MPD music daemon, with optional pre-stream channel-update tooling and going-live notifications. Extracted from the `ophi118` OBS rig and now a self-contained repo (its own `.git`, its own `.gitignore`).
|
||||
|
||||
The README.md is the user-facing doc — read it once. This file captures the parts a future Claude needs that aren't obvious from reading individual files.
|
||||
|
||||
## Run / develop
|
||||
|
||||
```bash
|
||||
pip install python-mpd2 # only runtime dep beyond stdlib
|
||||
python3 bot.py # runs the chat bot in foreground
|
||||
TWITCH_BOT_DEBUG=1 python3 bot.py # dump every received IRC line
|
||||
./search-game.py "Heroes" # CLI: <id>\t<name> on stdout
|
||||
./set-channel.py --dry-run --game-id 517520 "title" # no PATCH, just prints
|
||||
./examples/stream.sh # interactive pre-stream wrapper
|
||||
```
|
||||
|
||||
There are no tests, no linter, no build step. The two Twitch helpers (`search-game.py`, `set-channel.py`) are direct-callable; `examples/stream.sh` just composes them with `read` prompts.
|
||||
|
||||
## Two-account / two-env split (do not merge)
|
||||
|
||||
This is the single most load-bearing design decision in the repo:
|
||||
|
||||
| File | Account | Used by | Required scopes |
|
||||
|---|---|---|---|
|
||||
| `.env` | **Bot** account (`vault118`) | `bot.py` only | `chat:read`, `chat:edit` |
|
||||
| `.env.broadcaster` | **Broadcaster** account (`ophi118`) | `set-channel.py`, `search-game.py`, `examples/stream.sh` | `channel:manage:broadcast` |
|
||||
|
||||
Twitch enforces `broadcaster_id == token user_id` on `PATCH /helix/channels`, so the broadcaster token cannot be replaced with the bot token even if the bot account is moderator. Anything that walks new code through `_twitch.py` reads `.env.broadcaster`; `bot.py` has its own 12-line env loader and reads `.env`.
|
||||
|
||||
`_twitch.py` looks for `.env.broadcaster` first, falls back to `.env.ophi118` if present (legacy name from the original rig). New deployments should use `.env.broadcaster`.
|
||||
|
||||
## Auth model: 401 → refresh → retry, transparently
|
||||
|
||||
`_twitch.py:auth_call` is the only thing in the codebase that touches Twitch Helix. On a 401 it:
|
||||
|
||||
1. POSTs the refresh token to `twitchtokengenerator.com/api/refresh/<refresh>` (NOT Twitch's OAuth endpoint — twitchtokengenerator owns the client secret for tokens it minted).
|
||||
2. Persists `TWITCH_ACCESS_TOKEN` + `TWITCH_REFRESH_TOKEN` to `.env.broadcaster` via `update_env` (atomic temp-file rewrite, comments preserved).
|
||||
3. Mutates the in-process `env` dict so subsequent calls in the same process see the new token.
|
||||
4. Retries the original request once.
|
||||
|
||||
If you add a new Helix-touching script, route it through `auth_call` — never call `urllib.request` against Helix directly, or token expiry will surface as a hard 401 instead of a transparent recovery.
|
||||
|
||||
`bot.py` does NOT currently use the bot account's refresh token. Twitch IRC tokens are long-lived enough that this hasn't mattered, but if access tokens start dying mid-stream, refresh would land in the IRC reconnect handler in `TwitchBot.run` (the `RuntimeError` "auth failed" branch).
|
||||
|
||||
## Going-live watcher: transition detection on `started_at`, not `live`
|
||||
|
||||
`bot.py:live_watch` polls `STATUS_URL` (default `https://ophi118.com/api/status.json`) every 60s when `MM_HOOK` is set. The non-obvious behavior:
|
||||
|
||||
- Transitions key on `twitch.started_at` changing, NOT on `twitch.live` going false→true. Twitch Helix occasionally flaps the `live` flag inside one stream session — same `started_at`, different `live` reads. Keying on `started_at` ignores those phantom edges.
|
||||
- **Cold start mid-stream is silent on purpose.** If the bot starts up and sees `live=true` without ever having seen `live=false`, it records the current `started_at` so it won't fire for it later, but it doesn't announce. The `seen_offline` flag gates announcements; a real offline→live transition is required after process start.
|
||||
- The expected JSON shape is `{twitch: {live, started_at, title, game}}` — note the wrapping `twitch` key. Any consumer-side endpoint must serve that shape, not the flat shape shown in the README's high-level overview.
|
||||
|
||||
If you change the status JSON shape, update `live_watch`'s `(data or {}).get("twitch") or {}` accordingly — the bot tolerates a missing `twitch` key by treating it as not-live.
|
||||
|
||||
## Twitch chat duplicate-message codepoint
|
||||
|
||||
`TwitchBot._handle` strips characters in U+E0000–U+E007F before dispatch:
|
||||
|
||||
```python
|
||||
body = "".join(c for c in body if not (0xE0000 <= ord(c) <= 0xE007F))
|
||||
```
|
||||
|
||||
Twitch appends an invisible Tags-block codepoint to back-to-back identical messages from the same sender within ~30s as anti-duplicate protection. Without stripping it, every other `!skip` from a viewer in a series silently fails to match `head == "!skip"`. Do not remove this.
|
||||
|
||||
## search-game.py stdout discipline
|
||||
|
||||
`search-game.py` is callable from shell pipelines. **Only** the resolved `<id>\t<name>` line goes to stdout; every prompt, every error, every numbered list goes to stderr. The picker uses `print("Pick: ", file=sys.stderr); input()` instead of `input("Pick: ")` because `input`'s prompt argument writes to stdout and would corrupt the captured tuple.
|
||||
|
||||
A past bug: `examples/stream.sh` cached a stdout-leaked `Pick: 506462` string as `gameId` and passed it straight to `set-channel.py`. The script now validates `prev_game_id =~ ^[0-9]+$` before reusing — that regex is the post-mortem fix, not paranoia.
|
||||
|
||||
## stdlib-only
|
||||
|
||||
Both Twitch helpers and `_twitch.py` use only `urllib.request`. `bot.py` adds `python-mpd2` (its own `mpd.asyncio` async client) and nothing else. Reasons:
|
||||
|
||||
- The bot dir is intentionally venv-free; users should be able to `python3 bot.py` after one `pip install`.
|
||||
- `_twitch.py:load_env` / `bot.py:load_env` are 12-line `.env` parsers — pulling in `python-dotenv` for three vars wasn't worth the dependency.
|
||||
- `urllib.request` quirks: `auth_call` returns `(status, body)` and never raises on HTTP errors so callers can branch on 401 without `try/except HTTPError` boilerplate. Preserve that contract if you touch `http()`.
|
||||
|
||||
If a new caller really needs richer behavior (async HTTP, retries with backoff, etc.), revisit; until then resist adding requests/httpx/aiohttp.
|
||||
|
||||
## Per-user `!skip` cooldown, no moderator gate
|
||||
|
||||
`SKIP_COOLDOWN = 60.0` is per-chatter, keyed on lowercased nick in `TwitchBot._skip_seen` (a dict that grows with viewer count and is never pruned — fine at typical chat sizes, one float per chatter per session). There is intentionally **no moderator-only or VIP gate**: anyone in chat can skip. The 60s per-user cooldown is the only abuse mitigation.
|
||||
|
||||
If a future change adds role-gating, do it in `TwitchBot._cmd_skip` — the IRC PRIVMSG line carries `@badges=` tags but `bot.py` currently parses messages as plain `:user!user@... PRIVMSG` without the IRCv3 tag prefix. You'd need to re-enable tag capability (`CAP REQ :twitch.tv/tags`) in the connect handshake first.
|
||||
|
||||
## What this dir is NOT
|
||||
|
||||
- Not the OBS config dir. The parent dir `~/.config/obs-studio/` has its own CLAUDE.md covering OBS, scenes, audio architecture, MPD bridge (`bridges/mpd-state.py`), overlays, etc. This bot only consumes MPD; it does not publish to OBS WebSocket.
|
||||
- Not a packaged module. There is no `setup.py`, no `pyproject.toml`, no entry point. Run scripts directly.
|
||||
- Not where the now-playing display logic lives. Track names appear in stream overlays via `bridges/mpd-state.py` (in the parent OBS repo) broadcasting `mpd:state` events to OBS WebSocket. The bot only fires `mpd next` and reports the post-skip currentsong back to chat.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Editing `.env*` while the systemd unit is running:** the bot reads `.env` at startup only. After changing tokens, `systemctl --user restart obs-twitch-bot.service` (or whatever you named the unit). Token-refresh writes via `update_env` go to `.env.broadcaster`, NOT `.env` — the bot's own access token is never auto-rewritten.
|
||||
- **Brand-new bot accounts:** Twitch silently throttles unverified accounts. Bot connects, sees `JOIN`, never sees `PRIVMSG`. `TWITCH_BOT_DEBUG=1` will show this. Not a code bug.
|
||||
- **`examples/stream.sh` soft-fails Twitch sync.** If `set-channel.py` returns non-zero, `stream.json` is still written. This is intentional — the local manifest is decoupled from Twitch availability — but it means a green exit from `stream.sh` does not guarantee the Twitch channel was updated. Watch for the `! Twitch sync failed` line on stderr.
|
||||
Reference in New Issue
Block a user