From d3560dca52a53bf99587b7d8f9e7eee6a45386d8 Mon Sep 17 00:00:00 2001 From: YoursFunny Date: Sun, 20 Sep 2026 22:28:01 +0800 Subject: [PATCH] docs: add .env.example as the deployment template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `cp .env.example .env` is now the documented starting point: the tracked template carries every variable (grouped required / sites / bot behaviour / network / webhook / reverse proxy) with the defaults the code would use anyway, and the compose comment plus both READMEs point at it. The proxy note is spelled out where it matters — teloxide panics on a blank `TELOXIDE_PROXY`, and inside a container the proxy host must be `host.docker.internal`. Two follow-ups the template exposed: - `RUST_LOG=` (present but blank, which `.env` makes easy) silenced the log again: "unset" was handled, "empty" was not. A blank value now falls back to the same default. Verified: blank and unset both produce the full startup sequence. - `TWITTER_AUTH_TOKEN` was in the README prose but missing from the env table (both languages). Verified: `docker compose --env-file .env.example config -q` resolves, and a script comparing the compose's `${VAR}` references against the template's keys finds none missing. --- .env.example | 76 +++++++++++++++++++++++++++++++++++ AGENTS.md | 2 +- README.en.md | 4 ++ README.md | 4 ++ crates/xmedia-bot/src/main.rs | 13 +++--- 5 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..447e8c5 --- /dev/null +++ b/.env.example @@ -0,0 +1,76 @@ +# Copy to `.env` (gitignored) and fill in: +# +# cp .env.example .env +# +# `docker compose` reads it for the `${VAR}` substitutions in +# docker-compose.yml, and `cargo run` reads it through dotenv. Every variable is +# described in README.md ("环境变量说明" / "Environment variables") — this file +# only shows the shape, with the defaults the code would use anyway. + +# --- required ------------------------------------------------------------- +# Token from @BotFather. Without it the bot exits at startup. +TELOXIDE_TOKEN= + +# --- sites (all optional) ------------------------------------------------- +# Pixiv: refresh token. Unset = pixiv links answer "support is disabled". +PIXIV_REFRESH_TOKEN= +# Twitter/X: the `auth_token` cookie of a logged-in session, used only for +# NSFW tweets that the public syndication endpoint withholds. +TWITTER_AUTH_TOKEN= +# bilibili: the whole cookie string; only needed when the egress IP stays +# risk-controlled (device cookies are fetched automatically). +BILIBILI_COOKIE= + +# --- bot behaviour -------------------------------------------------------- +# Admin chat IDs, comma-separated: start/stop notices, admin-only commands. +BOT_ADMIN= +# Log level. Leave the line commented out for the default +# (`info,hyper_util=warn,reqwest=warn`); do not set it to an empty value. +# RUST_LOG=info,xmedia_bot=debug,x_media=debug +# Edit-before-forward record TTL (seconds). +EDIT_MESSAGE_TTL_SECONDS=86400 +# Link-result cache TTL (seconds). +LINK_CACHE_TTL_SECONDS=604800 +# Wrap a post's text in a collapsible blockquote from this many characters on; +# 0 disables the wrap. +CAPTION_QUOTE_TEXT_CHARS=200 +# State directory (local runs only — the container uses /app/data). +DATA_DIR=data + +# --- network -------------------------------------------------------------- +# HTTP proxy for the Bot API and site fetches. Two traps: teloxide panics on a +# *blank* value, so comment the line out rather than leaving it empty; and +# inside a container the proxy must be reachable from there (use +# host.docker.internal, not 127.0.0.1 — that is the container itself). +# docker-compose.yml does not pass this variable unless you add it to the bot +# service's `environment:` block. +# TELOXIDE_PROXY=http://127.0.0.1:10808 + +# --- webhook deployment (docker-compose.yml) ------------------------------ +# false = long polling (no public URL needed). true = webhook behind the +# bundled nginx-proxy — and then WEBHOOK_LISTEN/PORT/URL are required. +WEBHOOK=false +# WEBHOOK_LISTEN=0.0.0.0 +# WEBHOOK_PORT=8443 +# WEBHOOK_URL=https://your.domain/ +# Validation token Telegram echoes back as X-Telegram-Bot-Api-Secret-Token. +# WEBHOOK_SECRET_TOKEN= +# Self-signed certificate path, used only for Telegram-side validation (TLS is +# terminated by the reverse proxy); unneeded with acme-companion. Not passed by +# docker-compose.yml — add the line there if this deployment needs it. +# WEBHOOK_CERT=/app/cert/cert.pem + +# --- reverse proxy (docker-compose.yml) ----------------------------------- +# Public domain or IP that nginx-proxy routes for; empty = do not route. +VIRTUAL_HOST= +# Port inside the bot container nginx-proxy forwards to. +VIRTUAL_PORT=8443 +# Certificate notification address for acme-companion. +DEFAULT_EMAIL= +# UID the container runs as; it must be able to write ./data on the host. +LOCAL_USER_ID=1000 +# Uncomment (here and the matching line in docker-compose.yml) to have +# acme-companion issue the certificate for VIRTUAL_HOST. +# ACME_HOST= +# Send requests with an unknown Host to this vhost (needed for plain-IP access). +# DEFAULT_HOST= diff --git a/AGENTS.md b/AGENTS.md index 93153fd..3ddc70e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -98,7 +98,7 @@ Docker: `docker build -t tgxmb .` then `docker run --rm -d --name tgxmb --env-fi - Package manager: **Cargo** (workspace with path dep `x-media` ← `xmedia-bot`). No `[workspace.package]`/shared deps — each crate lists deps independently. - **TLS is rustls end-to-end** (no native-tls/openssl in the tree, no libssl in the Docker runtime image): `teloxide` is declared `default-features = false` with `["webhooks-axum", "macros", "rustls", "ctrlc_handler"]` (the removed `default` also carried `native-tls` and `ctrlc_handler` — the latter must stay); x-media's reqwest is `default-features = false` with `["json", "rustls-tls"]` (webpki-roots baked in, so the image ships no CA bundle). One reqwest 0.12.28 in the lock. - **Versioning**: bump the version in all three places (`crates/x-media/Cargo.toml`, `crates/xmedia-bot/Cargo.toml`, `Cargo.lock`) and **keep `README.md`, `README.en.md` and `AGENTS.md` in sync with the code on every bump**, then commit (`chore: bump version to X.Y.Z`), create an annotated tag `vX.Y.Z`, and push branch + tag (the tag push triggers the Docker Hub build). The tag must equal both crate versions: `.github/workflows/docker.yml` verifies that before building, and `--locked` verifies the lock file. -- Config is **environment-variable driven** (dotenv loads `.env`, gitignored; no `.env.example` exists). Key vars: `TELOXIDE_TOKEN` (required), `PIXIV_REFRESH_TOKEN`, `TWITTER_AUTH_TOKEN` (optional; x.com `auth_token` cookie — enables the logged-in GraphQL fallback that fetches NSFW tweets syndication withholds), `BILIBILI_COOKIE` (optional; whole bilibili cookie string — bilibili dynamics fetch anonymously and add their own device cookies, this only rescues an egress IP that bilibili has hard-flagged with `-352`/412), `BOT_ADMIN` (comma-separated ids), `EDIT_MESSAGE_TTL_SECONDS` (default 86400), `LINK_CACHE_TTL_SECONDS` (default 604800), `CAPTION_QUOTE_TEXT_CHARS` (default 200; a post whose text — the `title` plus `content` joined, see `site::compose_text` — reaches this length gets that text wrapped in an expandable blockquote inside its caption, the URL and author line staying outside; `0` disables it. Applied at the send boundary in `send::quote_long_caption`, which locates the text as what follows the author link, so a `/set_format` that moves `{title}`/`{content}` elsewhere and pixiv's title-inside-a-link layout opt out; `copy_messages` forwards and queued retries inherit the wrap, while the edit-before-forward rewrite stays unquoted by design), `DATA_DIR` (default `data`, CWD-relative; the SQLite dir, auto-created), `WEBHOOK`/`WEBHOOK_URL`/`WEBHOOK_LISTEN`/`WEBHOOK_PORT`/`WEBHOOK_CERT`/`WEBHOOK_SECRET_TOKEN` (webhook mode requires URL/listen/port, `.expect`ed; `WEBHOOK_CERT` is Telegram-facing self-signed validation only — TLS must be terminated by a reverse proxy), `RUST_LOG`, `TELOXIDE_PROXY`, `LOCAL_USER_ID` (entrypoint only). +- Config is **environment-variable driven** (dotenv loads `.env`, which is gitignored; `.env.example` is the tracked template — `cp .env.example .env` — and is also the file `docker compose` substitutes `${VAR}` from, so every variable the compose passes must be documented there). Key vars: `TELOXIDE_TOKEN` (required), `PIXIV_REFRESH_TOKEN`, `TWITTER_AUTH_TOKEN` (optional; x.com `auth_token` cookie — enables the logged-in GraphQL fallback that fetches NSFW tweets syndication withholds), `BILIBILI_COOKIE` (optional; whole bilibili cookie string — bilibili dynamics fetch anonymously and add their own device cookies, this only rescues an egress IP that bilibili has hard-flagged with `-352`/412), `BOT_ADMIN` (comma-separated ids), `EDIT_MESSAGE_TTL_SECONDS` (default 86400), `LINK_CACHE_TTL_SECONDS` (default 604800), `CAPTION_QUOTE_TEXT_CHARS` (default 200; a post whose text — the `title` plus `content` joined, see `site::compose_text` — reaches this length gets that text wrapped in an expandable blockquote inside its caption, the URL and author line staying outside; `0` disables it. Applied at the send boundary in `send::quote_long_caption`, which locates the text as what follows the author link, so a `/set_format` that moves `{title}`/`{content}` elsewhere and pixiv's title-inside-a-link layout opt out; `copy_messages` forwards and queued retries inherit the wrap, while the edit-before-forward rewrite stays unquoted by design), `DATA_DIR` (default `data`, CWD-relative; the SQLite dir, auto-created), `WEBHOOK`/`WEBHOOK_URL`/`WEBHOOK_LISTEN`/`WEBHOOK_PORT`/`WEBHOOK_CERT`/`WEBHOOK_SECRET_TOKEN` (webhook mode requires URL/listen/port, `.expect`ed; `WEBHOOK_CERT` is Telegram-facing self-signed validation only — TLS must be terminated by a reverse proxy), `RUST_LOG`, `TELOXIDE_PROXY`, `LOCAL_USER_ID` (entrypoint only). - SQLite via `rusqlite` with `bundled` feature (no system libsqlite needed). DB file `$DATA_DIR/task_queue.db` (default `data/task_queue.db`, CWD-relative — run from the workspace root, or `/app` in Docker; set `DATA_DIR` to pin state anywhere). Mount `./data` and `./cert` volumes. - `.gitattributes` enforces LF for `*.sh` (CRLF breaks shebangs in containers). `.gitignore`: `.env`, `data/`, `cert/`, `nginx-*` (proxy state), `/target`, `.idea/` (the compose file is tracked; only `.env` carries the deployment's own values). - Docs are in Chinese (README, AGENTS.md); user-facing bot strings are in English. Keep that split when editing user-facing strings and docs. diff --git a/README.en.md b/README.en.md index 33204d2..d73908c 100644 --- a/README.en.md +++ b/README.en.md @@ -30,8 +30,11 @@ cargo run -p xmedia-bot Docker deployment (`docker-compose.yml` in this repo is the orchestration; instance values live in the `.env` next to it, and compose substitutes every `${VAR}` from there): ```bash +cp .env.example .env # fill in TELOXIDE_TOKEN and the rest; every line is commented docker build -t tgxmb . docker run --rm -d --name tgxmb --env-file .env -v ./data:/app/data tgxmb +# or use the bundled orchestration (nginx-proxy + acme-companion): +docker compose up -d ``` Environment variables: `TELOXIDE_TOKEN` (required), `PIXIV_REFRESH_TOKEN`, `BOT_ADMIN`, `EDIT_MESSAGE_TTL_SECONDS`, `LINK_CACHE_TTL_SECONDS`, `RUST_LOG`, `TELOXIDE_PROXY`, `WEBHOOK*`, `TWITTER_AUTH_TOKEN` (optional), `BILIBILI_COOKIE` (optional). @@ -87,6 +90,7 @@ Telegram only accepts ports 443/80/88/8443. |---|---| | `TELOXIDE_TOKEN` | Bot token (required) | | `PIXIV_REFRESH_TOKEN` | Pixiv refresh token; Pixiv is disabled without it (a pixiv link then gets an explicit "site not enabled" reply instead of silence) | +| `TWITTER_AUTH_TOKEN` | Optional; the `auth_token` cookie of a logged-in x.com session, used only to fetch NSFW tweets' media | | `BILIBILI_COOKIE` | Optional bilibili cookie string (`SESSDATA=…; bili_jct=…`); only needed when the egress IP stays risk-controlled (device cookies are fetched automatically) | | `BOT_ADMIN` | Admin chat IDs, comma-separated; receives start/stop notifications | | `EDIT_MESSAGE_TTL_SECONDS` | Edit-before-forward record expiry in seconds, default 86400; once lapsed the prompt is rewritten in place to "expired — nothing was forwarded" (no extra message) | diff --git a/README.md b/README.md index 4b5b436..6db2594 100644 --- a/README.md +++ b/README.md @@ -30,8 +30,11 @@ cargo run -p xmedia-bot Docker 部署(编排见仓库里的 `docker-compose.yml`,实例相关的值写在同目录的 `.env`,compose 会自动替换其中的 `${VAR}`): ```bash +cp .env.example .env # 填 TELOXIDE_TOKEN 等,逐项都有注释 docker build -t tgxmb . docker run --rm -d --name tgxmb --env-file .env -v ./data:/app/data tgxmb +# 或者用仓库里的编排(含 nginx-proxy + acme-companion): +docker compose up -d ``` 环境变量:`TELOXIDE_TOKEN`(必填)、`PIXIV_REFRESH_TOKEN`、`BOT_ADMIN`、`EDIT_MESSAGE_TTL_SECONDS`、`LINK_CACHE_TTL_SECONDS`、`RUST_LOG`、`TELOXIDE_PROXY`、`WEBHOOK*`、`TWITTER_AUTH_TOKEN`(可选)、`BILIBILI_COOKIE`(可选)。 @@ -87,6 +90,7 @@ Telegram 只接受 443/80/88/8443 端口。 |---|---| | `TELOXIDE_TOKEN` | Bot token(必填) | | `PIXIV_REFRESH_TOKEN` | Pixiv 刷新令牌;未设置则禁用 Pixiv(此时收到 pixiv 链接会明确回复「站点未启用」,不会静默忽略) | +| `TWITTER_AUTH_TOKEN` | 可选;登录 x.com 后浏览器 Cookie 里的 `auth_token`,仅在遇到 NSFW 推文时以登录态获取媒体 | | `BILIBILI_COOKIE` | 可选的 B 站 Cookie 串(`SESSDATA=…; bili_jct=…`),仅在出口 IP 被持续风控时才需要(设备 cookie 由 bot 自动获取) | | `BOT_ADMIN` | 管理员聊天 ID,逗号分隔;接收启动/停止通知 | | `EDIT_MESSAGE_TTL_SECONDS` | 转发前编辑记录过期秒数,默认 86400;过期后提示消息会被就地改写为「已过期,未转发」(不额外发消息打扰) | diff --git a/crates/xmedia-bot/src/main.rs b/crates/xmedia-bot/src/main.rs index 056f293..be2fdb6 100644 --- a/crates/xmedia-bot/src/main.rs +++ b/crates/xmedia-bot/src/main.rs @@ -97,12 +97,15 @@ async fn main() { // with no logs; and at `debug` the HTTP client's own lines (hyper_util, // reqwest) outnumbered the bot's by two to one. The timed builder adds // the timestamp the plain `init` omitted, so a line can be compared with - // a user's report. An explicit RUST_LOG still wins outright. + // a user's report. An explicit RUST_LOG still wins outright — but a blank + // one (`RUST_LOG=` in `.env`, which is not "unset") must not silence the + // log the way its absence used to. + let filter = std::env::var("RUST_LOG") + .ok() + .filter(|value| !value.trim().is_empty()) + .unwrap_or_else(|| "info,hyper_util=warn,reqwest=warn".to_string()); pretty_env_logger::formatted_timed_builder() - .parse_filters( - &std::env::var("RUST_LOG") - .unwrap_or_else(|_| "info,hyper_util=warn,reqwest=warn".to_string()), - ) + .parse_filters(&filter) .init(); log::info!("Starting bot");