TheOrb/README.md
2026-05-15 15:46:17 -04:00

256 lines
8.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Archive Bot
An AIO Discord bot foundation for The Mithral Archive. The first module is status monitoring: it checks configured URLs and keeps one live status message updated in the Discord `status` channel.
The message uses a summary embed plus one small embed per service, so each service gets its own green or red Discord color bar.
It does not need Discord gateway intents or slash commands for the status module. It only needs a bot token, the `status` channel ID, and permission to send/edit its own messages.
The bot also includes a small web dashboard for editing monitored services and forcing immediate Discord refreshes.
It also has a media catalog module. The dashboard syncs movies and shows from Jellyfin, tracks additions/removals between syncs, and publishes a compact Discord catalog post.
## Discord Bot Setup
Create a Discord application and bot in the Discord Developer Portal, then invite it with:
```text
https://discord.com/oauth2/authorize?client_id=YOUR_CLIENT_ID&permissions=84992&scope=bot
```
Required channel permissions for each dashboard-selected channel:
- View Channel
- Send Messages
- Embed Links
- Attach Files
- Read Message History
Channel IDs are configured from the dashboard and stored in `state/bot-settings.json`.
## Local Setup
```sh
cp .env.example .env
cp services.example.json services.json
```
Edit `.env` and set:
- `DISCORD_BOT_TOKEN`
- `DISCORD_GATEWAY_ENABLED=true`
- `DASHBOARD_USERNAME`
- `DASHBOARD_PASSWORD_HASH`
Use the raw Discord bot token value. Do not include a `Bot ` prefix.
Generate the dashboard password hash:
```sh
python3 status_bot.py --hash-password
```
Paste the output into `DASHBOARD_PASSWORD_HASH`. The dashboard does not store a reusable browser token; it uses an HttpOnly session cookie and CSRF token after login.
Dashboard auth includes:
- PBKDF2-SHA256 password hashing
- HttpOnly `SameSite=Strict` session cookie
- CSRF token required for write actions
- basic failed-login throttling
The bot keeps a Gateway connection open so Discord shows it as online even when no status refresh is happening.
Edit `services.json` with the URLs you want displayed. Keep private/internal URLs out of Git if they should not be shared.
Preview the Discord embed payload:
```sh
ARCHIVE_STATUS_CONFIG=services.json python3 status_bot.py --preview
```
Open `preview.html` in a browser to see an approximate Discord-style render. Paste the JSON from `--preview` into the textarea and render it to test changes before sending anything to Discord.
Run it:
```sh
python3 status_bot.py
```
Local runs automatically read `.env` from the current directory.
For dashboard testing without touching Discord, set:
```env
DISCORD_DRY_RUN=true
ARCHIVE_STATUS_STATE=state/status-message.json
```
Open the dashboard:
```text
http://127.0.0.1:8787
```
Sign in with `DASHBOARD_USERNAME` and the password you used when generating `DASHBOARD_PASSWORD_HASH`.
Set the Status and Media channel IDs from the dashboard before publishing Discord updates.
## Docker Setup
```sh
cp .env.deploy.example .env
cp services.example.json services.json
python3 status_bot.py --hash-password
docker compose up -d --build
```
Paste the generated password hash into `.env` before starting the container.
Make sure Docker can read the service config and write runtime state:
```sh
chmod 600 .env
chmod 644 services.json
mkdir -p state
chmod 755 state
```
The container runs as `1001:1001` inside the image. If the mounted `services.json` or `state/` were created by another user, fix ownership once:
```sh
sudo chown -R 1001:1001 services.json state
```
If `state/` or `services.json` were created by a previous container as another user, fix ownership once:
```sh
sudo chown -R "$(id -u):$(id -g)" services.json state
```
The bot stores channel settings in `state/bot-settings.json` and Discord message IDs in `state/status-message.json` and `state/media-catalog.json`. Keep `state/` mounted so the bot edits the same messages after restarts.
The deploy compose joins your existing reverse-proxy network:
```yaml
networks:
mediaserver_default:
external: true
```
If that network does not already exist on the deploy host, create it once:
```sh
docker network create mediaserver_default
```
The dashboard is exposed inside Docker on port `8787` for your reverse proxy. It is not published directly to the host by default.
Use this target from your proxy:
```text
http://archive-status-bot:8787
```
For Nginx Proxy Manager, put the dashboard behind an Access List or basic auth on the proxy host and disable the app's own login:
```env
DASHBOARD_AUTH_DISABLED=true
DASHBOARD_COOKIE_SECURE=true
```
NPMs Access Lists use browser `Authorization` headers, so the app never needs to receive your username/password directly. Leave `DASHBOARD_AUTH_DISABLED=false` only for direct localhost testing.
NPMs own docs call out that Access List basic auth and app-side auth both use `Authorization`, so the app-side login is the one to disable in production.
If `DASHBOARD_USERNAME` and `DASHBOARD_PASSWORD_HASH` are omitted, the app now falls back to proxy-only mode automatically.
For direct local Docker testing without a proxy:
```sh
docker compose -f compose.yaml -f compose.local.yaml up -d --build
```
Then open:
```text
http://127.0.0.1:8787
```
## Dashboard
The dashboard currently supports:
- viewing monitored services
- selecting the Discord channel used by the status updater
- adding/removing service rows
- editing check URL, display URL, expected statuses, timeout, and keyword
- saving `services.json`
- forcing an immediate check and Discord message update
- syncing movies and shows from Jellyfin
- tracking additions and removals between Jellyfin syncs
- editing synced movies and shows before publishing
- publishing a compact Markdown media catalog to a selected Discord channel
The sidebar leaves room for future modules like polls, webhooks, automations, and service integrations without changing the bot shape later.
## Media Catalog
Open the dashboard and switch to `Media`. Set the target Discord channel ID, configure Jellyfin, then run `Sync now`.
The editor supports adding, editing, and deleting movie/show rows before saving or publishing. Publishing sends the currently edited dashboard library, not the raw uploaded files.
Discord publishing uses one message with an attached `media-catalog.md` file so the channel does not get flooded by a long embed wall.
The dashboard also serves a read-only catalog page at `/catalog`. Set the public reverse-proxy URL for that page in the `Media` tabs `Catalog URL` field, or with `PUBLIC_CATALOG_URL`, and Discord posts will include an `Open Catalog` button.
In Jellyfin, create an API key from the admin dashboard, then enter the Jellyfin URL and key in the `Media` tab. If you have duplicate free/premium libraries, enter only the premium Jellyfin library names in the `Libraries` field, separated by commas. `Sync now` replaces the editable library with the current Jellyfin movies and shows while comparing against the previously saved library. `Auto-sync changes` checks Jellyfin periodically and republishes only when the catalog fingerprint changes. Jellyfin results are deduplicated across included libraries using provider IDs first, then normalized title and year.
Channel selections are stored in:
```env
BOT_SETTINGS_STATE=state/bot-settings.json
```
The bot stores media catalog message IDs in:
```env
MEDIA_CATALOG_STATE=state/media-catalog.json
```
The editable media library is stored in:
```env
MEDIA_LIBRARY_STATE=state/media-library.json
```
Republishing deletes the previous media catalog message and posts a fresh compact Markdown attachment.
The auto-sync interval defaults to 15 minutes:
```env
JELLYFIN_SYNC_INTERVAL_SECONDS=900
```
## Service Config
Each service supports:
- `name`: label shown in Discord
- `url`: URL checked by the bot
- `displayUrl`: URL linked in the embed
- `method`: optional, defaults to `GET`
- `timeoutSeconds`: optional, defaults to `10`
- `expectedStatuses`: optional list such as `["200-399"]` or `[200, 204]`
- `keyword`: optional text that must appear in the response body
Example:
```json
{
"name": "Jellyfin",
"url": "https://jellyfin.mithraic.cloud/health",
"displayUrl": "https://jellyfin.mithraic.cloud",
"expectedStatuses": ["200-399"]
}
```