diff --git a/.dockerignore b/.dockerignore index 49c17b2..eb9beb3 100644 --- a/.dockerignore +++ b/.dockerignore @@ -28,5 +28,9 @@ LICENSE README.md data/ cert/ +nginx-certs/ +nginx-vhost.d/ +nginx-html/ +nginx-acme/ **/target/ .idea/ diff --git a/.gitignore b/.gitignore index 07af3ac..1148345 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,10 @@ __pycache__/ cert/ data/ +nginx-certs/ +nginx-vhost.d/ +nginx-html/ +nginx-acme/ docker-compose.yml .env diff --git a/AGENTS.md b/AGENTS.md index 057cf60..cdb7844 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -72,7 +72,7 @@ Docker: `docker build -t tgxmb .` then `docker run --rm -d --name tgxmb --env-fi | `crates/x-media/src/site/pixiv/api.rs` | OAuth token exchange (hardcoded app client id/secret), access-token cache, ugoira zip→MP4 via ffmpeg in `spawn_blocking` | | `Dockerfile` | Multi-stage: cached dep layer via stub sources + `touch *.rs` mtime hack, static ffmpeg from ffmpeg.martin-riedl.de (`FFMPEG_URL` arg, `unzip -t` integrity check), `debian:bookworm-slim` runtime, entrypoint | | `docker-entrypoint.sh` | Privilege drop: `useradd` with `LOCAL_USER_ID` (default 9001) + `setpriv` (no gosu on bookworm-slim) | -| `docker-compose.yml.example` | Deployment env reference (real `docker-compose.yml` is gitignored) | +| `docker-compose.yml.example` | Deployment env reference (real `docker-compose.yml` is gitignored). Ships nginx-proxy + acme-companion: webhook mode needs TLS termination in front (teloxide's axum listener is HTTP-only; `WEBHOOK_CERT` only feeds `set_webhook`), bot exposes `VIRTUAL_HOST`/`VIRTUAL_PORT` on the shared `proxy` network, no host port | | `.github/workflows/docker.yml` | CI: build+push to Docker Hub on tag `v*`/master; **no test step**; buildx gha cache (`cache-from`/`cache-to`, scope `tgxmb-build`, `mode=max`) so cargo deps + ffmpeg layers are restored across runs | | `README.md` | Feature docs + command table (Chinese) | @@ -81,7 +81,7 @@ Docker: `docker build -t tgxmb .` then `docker run --rm -d --name tgxmb --env-fi - **Rust, stable, edition 2024**, workspace resolver 3. No `rust-version`/MSRV pin, no `rust-toolchain.toml` — recent stable is assumed. No nightly features. - Package manager: **Cargo** (workspace with path dep `x-media` ← `xmedia-bot`). No `[workspace.package]`/shared deps — each crate lists deps independently. - **Two reqwest versions coexist in the lock** (0.12.28 via teloxide, 0.13.3 in x-media) — don't unify casually. -- Config is **environment-variable driven** (dotenv loads `.env`, gitignored; no `.env.example` exists). Key vars: `TELOXIDE_TOKEN` (required), `PIXIV_REFRESH_TOKEN`, `BOT_ADMIN` (comma-separated ids), `EDIT_MESSAGE_TTL_SECONDS` (default 86400), `WEBHOOK`/`WEBHOOK_URL`/`WEBHOOK_LISTEN`/`WEBHOOK_PORT`/`WEBHOOK_CERT`/`WEBHOOK_SECRET_TOKEN` (webhook mode requires cert + port, `.expect`ed), `RUST_LOG`, `TELOXIDE_PROXY`, `LOCAL_USER_ID` (entrypoint only). +- Config is **environment-variable driven** (dotenv loads `.env`, gitignored; no `.env.example` exists). Key vars: `TELOXIDE_TOKEN` (required), `PIXIV_REFRESH_TOKEN`, `BOT_ADMIN` (comma-separated ids), `EDIT_MESSAGE_TTL_SECONDS` (default 86400), `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/task_queue.db` is CWD-relative — run from the workspace root, or `/app` in Docker. Mount `./data` and `./cert` volumes. - `.gitattributes` enforces LF for `*.sh` (CRLF breaks shebangs in containers). `.gitignore`: `.env`, `data/`, `cert/`, `docker-compose.yml`, `/target`, `.idea/`. - Docs are in Chinese; user-facing bot strings too. Keep that convention when editing captions/templates/docs. diff --git a/README.md b/README.md index a3468ef..8617cc5 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,49 @@ docker run --rm -d --name tgxmb --env-file .env -v ./data:/app/data tgxmb 环境变量:`TELOXIDE_TOKEN`(必填)、`PIXIV_REFRESH_TOKEN`、`BOT_ADMIN`、`EDIT_MESSAGE_TTL_SECONDS`、`RUST_LOG`、`WEBHOOK*`。 +### Webhook 部署(需要反向代理) + +`docker-compose.yml.example` 内置了 [nginx-proxy](https://github.com/nginx-proxy/nginx-proxy) + [acme-companion](https://github.com/nginx-proxy/acme-companion) 反向代理编排,按部署环境二选一: + +**有域名** +1. DNS A 记录指向服务器 +2. compose 里设 `VIRTUAL_HOST`、`LETSENCRYPT_HOST` 为域名,`WEBHOOK_URL` 设为 `https://域名/` +3. 证书自动签发与续期,无需手动处理 + +**只有 IP** +1. 生成自签证书(PEM 格式,见第 3 步): + `openssl req -x509 -newkey rsa:2048 -nodes -days 365 -keyout nginx-certs/default.key -out nginx-certs/default.crt` +2. compose 里 nginx-proxy 设 `DEFAULT_HOST`,bot 设 `WEBHOOK_CERT: './cert/cert.pem'`(须与代理所服务的为同一张证书) +3. 证书必须是 PEM 编码(ASCII BASE64,以 `-----BEGIN CERTIFICATE-----` 开头)—— Telegram 只接受该格式;若现有证书是 DER 二进制,转换: + `openssl x509 -in cert.der -inform DER -out cert.pem -outform PEM` + (私钥同理:`openssl rsa -in key.der -inform DER -out key.pem -outform PEM`) + +Telegram 只接受 443/80/88/8443 端口。 + +
+环境变量说明 + +| 变量 | 说明 | +|---|---| +| `TELOXIDE_TOKEN` | Bot token(必填) | +| `PIXIV_REFRESH_TOKEN` | Pixiv 刷新令牌;未设置则禁用 Pixiv | +| `BOT_ADMIN` | 管理员聊天 ID,逗号分隔;接收启动/停止通知 | +| `EDIT_MESSAGE_TTL_SECONDS` | 转发前编辑记录过期秒数,默认 86400 | +| `RUST_LOG` | 日志级别 | +| `LOCAL_USER_ID` | 容器内运行用户 UID,默认 9001 | +| `VIRTUAL_HOST` | 对外域名或 IP,nginx-proxy 按此路由 | +| `VIRTUAL_PORT` | bot 容器内监听端口,nginx-proxy 的转发目标 | +| `LETSENCRYPT_HOST` | 设为域名时由 acme-companion 自动签发/续期证书 | +| `DEFAULT_HOST` | nginx-proxy 将未知 Host 的请求路由到该 vhost(IP 访问时需要) | +| `DEFAULT_EMAIL` | acme-companion 证书通知邮箱 | +| `WEBHOOK` | `true` 启用 webhook 模式(默认轮询) | +| `WEBHOOK_LISTEN` / `WEBHOOK_PORT` | bot 容器内监听地址/端口 | +| `WEBHOOK_URL` | 对外公网 HTTPS 地址(`https://域名/`) | +| `WEBHOOK_CERT` | 自签证书路径(仅 IP 路径需要,须为 PEM 且与代理所服务的一致) | +| `WEBHOOK_SECRET_TOKEN` | 更新校验令牌(`X-Telegram-Bot-Api-Secret-Token`) | + +
+ ## 命令 | 命令 | 说明 | diff --git a/docker-compose.yml.example b/docker-compose.yml.example index ad938ec..9479def 100644 --- a/docker-compose.yml.example +++ b/docker-compose.yml.example @@ -1,30 +1,61 @@ services: + nginx-proxy: + image: nginxproxy/nginx-proxy:1.11.6-alpine + restart: always + ports: + - '80:80' + - '443:443' + environment: + # Bare-IP access only. + # DEFAULT_HOST: 'bot.example.com' + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro + - ./nginx-certs:/etc/nginx/certs:ro + - ./nginx-vhost.d:/etc/nginx/vhost.d:ro + - ./nginx-html:/usr/share/nginx/html:ro + networks: [proxy] + labels: + - 'com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy=true' + + acme-companion: + image: nginxproxy/acme-companion + restart: always + environment: + DEFAULT_EMAIL: 'admin@yoursfunny.top' + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - ./nginx-certs:/etc/nginx/certs:rw + - ./nginx-vhost.d:/etc/nginx/vhost.d:rw + - ./nginx-html:/usr/share/nginx/html:rw + - ./nginx-acme:/etc/acme.sh + networks: [proxy] + depends_on: + - nginx-proxy + tgxmb: image: yoursfunny/telegram-twitter-media-bot:latest restart: always -# ports: -# - "8443:8443" environment: - # docker-entrypoint.sh drops privileges to this uid. LOCAL_USER_ID: '1000' - # Bot token (BotFather). Required. TELOXIDE_TOKEN: '' - # Comma-separated admin chat ids; receives startup/shutdown notices. BOT_ADMIN: '' - # Required for pixiv support; pixiv is disabled when unset. PIXIV_REFRESH_TOKEN: '' - # Edit-before-forward records expire after this many seconds (default 86400 = 24h). EDIT_MESSAGE_TTL_SECONDS: '86400' RUST_LOG: 'info' - # Webhook mode is off by default (polling). The listener binds inside the - # container, so use 0.0.0.0 and publish the port if you enable it. - WEBHOOK: 'false' + VIRTUAL_HOST: 'bot.example.com' + VIRTUAL_PORT: '8443' + # LETSENCRYPT_HOST: 'bot.example.com' + WEBHOOK: 'true' WEBHOOK_LISTEN: '0.0.0.0' WEBHOOK_PORT: '8443' - WEBHOOK_URL: 'https://example.com' - WEBHOOK_CERT: './cert/cert.pem' - WEBHOOK_SECRET_TOKEN: 'secret-token' + WEBHOOK_URL: 'https://bot.example.com/' + # WEBHOOK_CERT: './cert/cert.pem' + WEBHOOK_SECRET_TOKEN: '' volumes: - ./data:/app/data -# - ./cert:/app/cert + networks: [proxy] container_name: tgxmb + +networks: + proxy: + name: proxy