Vanguard media stack: ingest, copyparty, Syncthing, normalize sweep, Authentik, Jellyfin + Vanguard Metadata plugin, MariaDB, Gitea, bot runtime
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
108
bot/AGENT.md
Normal file
108
bot/AGENT.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# Vanguard Discord bot — brief for the agent building it
|
||||
|
||||
You are building the Discord bot for Vanguard. What the bot *does* is up to the community and you.
|
||||
This file only covers where it runs, what it is given, and the few rules that keep it from breaking
|
||||
the box it shares. Nothing here obliges you to integrate with the other services on that box.
|
||||
|
||||
## 1. Hosting
|
||||
|
||||
One Debian server runs a set of Docker Compose services from `~/vanguard-stream/`. You own exactly
|
||||
one of them: the `discord-bot` container. You do not own the compose file, the reverse proxy (Caddy),
|
||||
the firewall, the identity provider (Authentik), or the database server config. If you need any of
|
||||
those changed, ask the admin with the exact change (see §6).
|
||||
|
||||
What you are given:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Runtime** | `python:3.12-slim` + ffmpeg + curl. Dependencies from `app/requirements.txt`, installed at image build (`docker compose build discord-bot`). |
|
||||
| **Your code** | bind-mounted at `/app` from `bot/app/`. Not baked into the image. Edit, then `docker compose restart discord-bot`. |
|
||||
| **Entry point** | the container runs `python -u /app/main.py` and restarts it if it exits. With no `main.py` it idles. |
|
||||
| **User** | uid 1000 |
|
||||
| **Database** | MariaDB, database `vanguardbot`, user `bot`, reachable at `DB_HOST:DB_PORT`. Yours alone. Other databases on the same server belong to other services; never touch them. |
|
||||
| **Persistent disk** | `/data`, yours |
|
||||
| **Web UI** | bind `WEB_HOST:WEB_PORT` (0.0.0.0:8000). The proxy publishes it as `https://bot.<domain>/` behind login (§3). |
|
||||
| **Discord** | `DISCORD_BOT_TOKEN`, `DISCORD_GUILD_ID`. The bot user belongs to the community's existing Discord application. |
|
||||
| **Clock** | `TZ=America/New_York` |
|
||||
|
||||
### Environment variables
|
||||
`DISCORD_BOT_TOKEN`, `DISCORD_GUILD_ID`, `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD`,
|
||||
`WEB_HOST`, `WEB_PORT`, `WEB_PUBLIC_URL`, `AUTH_HEADER_USER`, `AUTH_HEADER_GROUPS`, `AUTH_HEADER_EMAIL`,
|
||||
`MEDIA_CLIPS`, `TZ`. Secrets arrive only through env. Never write them to the repo, `/data`, or logs.
|
||||
|
||||
SQLAlchemy URL: `mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}` (async: `mysql+aiomysql`).
|
||||
|
||||
## 2. Process model
|
||||
|
||||
- One process, one asyncio loop: the Discord gateway client and the web server run together
|
||||
(`discord.py` plus `aiohttp` or FastAPI via uvicorn's programmatic API). Do not spawn a second container.
|
||||
- Log to stdout. The admin reads `docker compose logs -f discord-bot`.
|
||||
- On fatal misconfiguration (no token, DB unreachable after retries) log the reason and exit non-zero;
|
||||
the restart policy retries.
|
||||
- Handle SIGTERM: close the gateway and the web server cleanly.
|
||||
|
||||
## 3. Web UI: login is handled outside your code
|
||||
|
||||
Every request to `https://bot.<domain>/` passes through Caddy, which sends the user to Authentik
|
||||
(Discord login) and, once authenticated, forwards the request to you with identity headers:
|
||||
|
||||
| header | content |
|
||||
|---|---|
|
||||
| `X-Authentik-Username` | the user's account name |
|
||||
| `X-Authentik-Groups` | group names, separated by `\|` or `,` — the community uses `admins`, `editors`, `streamers`; others may be added |
|
||||
| `X-Authentik-Email`, `X-Authentik-Name`, `X-Authentik-Uid` | extras |
|
||||
|
||||
Rules:
|
||||
1. **Implement no login page, no sessions, no passwords.** Identity is the headers, per request.
|
||||
2. Port 8000 is loopback-only on the host, so the headers can be trusted. Still fail closed: a request
|
||||
without the username header gets 401.
|
||||
3. Authorize by group, read from the header each time. Don't cache roles in your DB.
|
||||
4. Serve under `/`; the proxy does no path rewriting for this host.
|
||||
5. Build absolute links from `WEB_PUBLIC_URL`.
|
||||
6. The Authentik username may differ from the Discord username. Use the header value as the user key;
|
||||
if you need the Discord snowflake, resolve it through your gateway client and cache it in your DB.
|
||||
|
||||
Until the admin creates the matching Authentik application, the hostname returns an error, never an
|
||||
open page. Test against `http://127.0.0.1:8000/` on the server with the headers set by hand.
|
||||
|
||||
## 4. Repository layout (recommended, not enforced)
|
||||
|
||||
```
|
||||
bot/
|
||||
AGENT.md, README.md this brief; the ops cheat-sheet (keep README current)
|
||||
Dockerfile, entrypoint.sh admin-owned; ask before changing
|
||||
app/
|
||||
main.py config → migrations → gateway + web in one loop → clean shutdown
|
||||
requirements.txt pin versions once past the prototype
|
||||
config.py the only module that reads os.environ
|
||||
bot/ discord.py client, cogs/ (one feature per cog), permission checks
|
||||
web/ routes/, templates/, static/, auth.py (header → user/groups)
|
||||
db/ SQLAlchemy models, session, alembic/ migrations (run at startup)
|
||||
services/ the actual logic, shared by cogs and web; no Discord or HTTP types
|
||||
tests/ pytest; must pass offline with no token
|
||||
```
|
||||
Slash commands scoped to the guild so they appear instantly. Prefer slash commands over reading
|
||||
message content; Message Content is a privileged intent, enable it only if you need it.
|
||||
|
||||
## 5. Shared ground on the box
|
||||
|
||||
- `/media/clips` is mounted read-write. It is the community's clip storage, organized as one folder per
|
||||
user. You may **add** files there; never modify, move or delete files you did not create (parts of it
|
||||
are mirrored from members' machines and will fight back). If you write a video, write
|
||||
`<name>.part` then rename to `<name>.mp4`, and put a `<name>.json` beside it with at least
|
||||
`{"timestamp": "<UTC ISO-8601 creation time>", "source": "...", "uploader": "<discord username>"}`.
|
||||
Other services on the box index that folder and read the sidecar. If you never touch this mount,
|
||||
none of this applies.
|
||||
- The same bot user is used by another service for REST calls (posting notices, reading scheduled
|
||||
events). Sharing a token is fine. Don't rename the bot or change its avatar; pick your own channels.
|
||||
- Don't bind other ports, don't start other containers, don't write outside `/app`, `/data`, `/media/clips`.
|
||||
|
||||
## 6. Working with the admin
|
||||
|
||||
- **Source**: push to the community Gitea, `ssh://git@git.<domain>:2222/vanguard/discord-bot.git`.
|
||||
The admin deploys by pulling into `bot/app` and restarting. CI on the `python` runner label runs your tests.
|
||||
- **Need infra?** Open a Gitea issue titled `infra: …` stating the exact change: a new env var and why,
|
||||
a mount, a Caddy path that should skip login (e.g. a public webhook endpoint), a new database.
|
||||
Don't work around the proxy or the firewall.
|
||||
- **Dependencies changed?** Say so in the PR; the image must be rebuilt.
|
||||
- Keep `README.md` updated with every command, env var, and table you add.
|
||||
Reference in New Issue
Block a user