mirror of
https://github.com/tiennm99/tiennm99bot.git
synced 2026-10-11 12:28:54 +00:00
The beta renderer changed style, so /gachabeta now reads "Gacha (beta version)" in its command description, package doc, README row, and deploy guide instead of naming one look. The clip length sent to Telegram is 11 seconds to match the renderer's 10.5-second beta animation.
243 lines
12 KiB
Markdown
243 lines
12 KiB
Markdown
# Deploy: Self-host (Coolify + MongoDB Atlas)
|
|
|
|
Run `miti99bot` as a long-lived container on [Coolify](https://coolify.io) with
|
|
[MongoDB Atlas](https://www.mongodb.com/atlas) (free M0) for storage.
|
|
|
|
## Architecture
|
|
|
|
```
|
|
Telegram <── long poll (getUpdates) ── container (outbound only)
|
|
in-process scheduler ───────────────────> module crons
|
|
MongoDB Atlas (db / one collection per module + system metadata)
|
|
Coolify env vars (plain secrets)
|
|
NO public ingress (polling = outbound only; no domain, no /webhook, no TLS in)
|
|
```
|
|
|
|
- **Storage** — `mongodb` auto-selected when `MONGO_URL` is set (no `KV_PROVIDER`).
|
|
- **Cron** — an in-process scheduler (`internal/cron`) runs unconditionally and
|
|
fires each module cron on its `Schedule`, evaluated in UTC. The only cron
|
|
today is the `lol` daily digest at `0 1 * * *` (08:00 ICT).
|
|
- **Transport** — long polling (`b.Start`) is the **only** transport. The bot
|
|
opens an outbound connection to Telegram and pulls updates, so there is no
|
|
public domain, no `/webhook`, and no webhook secret. The container clears any
|
|
leftover webhook on startup (`deleteWebhook`) before polling.
|
|
|
|
## Environment
|
|
|
|
Copy [`.env.example`](../.env.example) → `.env` (gitignored) and fill in.
|
|
|
|
| Var | Required | Notes |
|
|
|---|---|---|
|
|
| `TELEGRAM_BOT_TOKEN` | ✅ | from @BotFather; startup fails without it |
|
|
| `MONGO_URL` | ✅ | Atlas SRV string **incl. credentials** — secret, never logged |
|
|
| `MONGO_DATABASE` | ✅ | e.g. `miti99bot` |
|
|
| `MODULES` | optional | CSV; empty = all modules, including any added later |
|
|
| `OWNER_ID` | optional | Telegram user id for owner-only commands, the deploy DM, and the `/addsticker` pack owner. Unset = owner-only commands are denied and `/addsticker` refuses |
|
|
| `ADMIN_IDS` | optional | CSV of Telegram user ids for admin-only commands |
|
|
| `STICKER_PACK_NAME` | optional | set `/addsticker` writes to; default `miti99_by_miti99bot`. See [sticker packs](sticker-packs.md) |
|
|
| `LOL_PANDASCORE_TOKEN` | ✅ for lol module | PandaScore API token (free tier) — secret, never logged; without it every `/lol*` fetch fails (stale cache may still serve briefly) |
|
|
| `WHEELOFNAMES_API_URL` | optional | full `/api/gif` endpoint for remote `/wheelofnames` GIF rendering |
|
|
| `WHEELOFNAMES_API_TOKEN` | optional | bearer token matching the wheelofnames service `API_TOKEN` |
|
|
| `LOG_LEVEL` | optional | `debug`, `info` (default), `warn`, or `error`; logs are JSON on stdout |
|
|
| `GOLD_VNAPP_API_KEY` | leave unset | VNAppMob key; unset = the gold module fetches one and caches it in MongoDB |
|
|
| `KV_PROVIDER` | leave unset | `memory` or `mongodb`; unset = `mongodb` when `MONGO_URL` is set, otherwise `memory` |
|
|
| `PORT` | leave unset | health server port; default `8080` |
|
|
| `SOURCE_COMMIT` | never set | provided by Coolify at runtime for the deploy DM (see step 6 below) |
|
|
|
|
Stock, coin, and gold provider URL overrides are not supported in runtime env;
|
|
modules use coded defaults. There is no `TELEGRAM_WEBHOOK_SECRET`: long polling
|
|
has no webhook.
|
|
|
|
> Cron runs in-process (`internal/cron`) — there is no `/cron` HTTP route and no
|
|
> `CRON_SHARED_SECRET`. The scheduler is the sole trigger; nothing inbound.
|
|
|
|
### Optional wheelofnames renderer
|
|
|
|
`/wheelofnames` uses a remote GIF renderer when `WHEELOFNAMES_API_URL` is set.
|
|
The standard renderer is a deployment of
|
|
[`tiennm99/wheelofnames`](https://github.com/tiennm99/wheelofnames), but any
|
|
service that implements the same `/api/gif` contract can be used. Set the URL
|
|
to the full GIF endpoint and set the token to the same value as the service
|
|
`API_TOKEN`:
|
|
|
|
```env
|
|
WHEELOFNAMES_API_URL=http://wheelofnames:3000/api/gif
|
|
WHEELOFNAMES_API_TOKEN=<same value as wheelofnames API_TOKEN>
|
|
```
|
|
|
|
Use a public HTTPS URL instead when the bot cannot reach the service on a
|
|
private Coolify/Docker network:
|
|
|
|
```env
|
|
WHEELOFNAMES_API_URL=https://wheelofnames.example.com/api/gif
|
|
WHEELOFNAMES_API_TOKEN=<same value as wheelofnames API_TOKEN>
|
|
```
|
|
|
|
The bot sends outbound HTTP only; no public bot ingress is required. Remote
|
|
renders use `512px`, `20fps`, and `7` seconds total by default. If the remote
|
|
service is unset, unavailable, unauthorized, or returns a non-GIF response,
|
|
`/wheelofnames` falls back to the same plain text winner reply as `/random`.
|
|
Successful GIF replies include the result behind Telegram spoiler formatting.
|
|
|
|
When a renderer is configured, the bot posts a `Spinning...` holding message
|
|
first, because the render takes several seconds. The GIF then replaces it; a
|
|
render or upload failure edits that same message into the plain text winner
|
|
instead. With no renderer configured there is no holding message — the winner
|
|
reply is immediate.
|
|
|
|
`/gacha` uses the same service and token: the bot swaps the URL's last path
|
|
segment, so `.../api/gif` becomes `.../api/gacha`. It renders a 7-second
|
|
`640x360` silent MP4 wish animation, posts `Wishing...` while it renders, and
|
|
falls back to a text reply such as `★★★★★ Pizza` on the same failures. Every
|
|
option is equally likely, as with `/random`; the rarity only sets what the
|
|
animation and reply show. Options are 5★ by default; prefix `4*` or `3*` to
|
|
lower one, e.g. `/gacha Pizza, 4* Pho, 3* Rice`.
|
|
|
|
The unlisted `/gachabeta` takes the same input and renders the beta version
|
|
of the wish from `.../api/gachabeta` on the same service, with the same text
|
|
fallback.
|
|
|
|
## 1. MongoDB Atlas (M0)
|
|
|
|
1. Create a free **M0** cluster (512 MB — ample for the tiny paper-trading KV).
|
|
2. **Database user (least privilege):** create a user with role
|
|
**`readWrite` on the single app database only** (e.g. `miti99bot`) — never
|
|
Atlas admin or cluster-wide. Use a **strong unique password**.
|
|
3. **Network access:** add `0.0.0.0/0`.
|
|
|
|
> **Accepted trade-off.** The Coolify host has no stable
|
|
> egress IP, so the Atlas IP allow-list is open to the internet. This widens
|
|
> the database surface. The mandatory compensating controls are: (1) strong
|
|
> unique password, (2) least-privilege `readWrite`-on-one-db user, (3) the
|
|
> connection string is a secret and is never logged (the bot logs only the
|
|
> database name on startup).
|
|
|
|
4. Copy the `mongodb+srv://…` connection string into `MONGO_URL` and put the
|
|
db name in `MONGO_DATABASE`.
|
|
|
|
### Storage layout
|
|
|
|
- **One collection per module.** Each document is a flattened native document
|
|
— `{ _id: <user key>, ...payload fields, version, updatedAt }` with no `value`
|
|
envelope. Payload fields are hoisted to the document root so they expand and
|
|
are queryable in Compass. The two non-object values are wrapped in a named
|
|
field: `lol` schedule subscribers under `subscribers` (array) and the daily
|
|
push date under `date`. Concurrency uses the `version` field (optimistic
|
|
lock); `updatedAt` is a BSON Date.
|
|
- **`stats`** uses queryable aggregate documents for command/user counts and
|
|
creates its indexes on startup. Deleted legacy command rows are retained with
|
|
`deleted: true`, and `/stats` filters them from visible results.
|
|
- **`stock`** stores cash as `vnd`, embeds positions as
|
|
`assets.<symbol>.{quantity,base,openedAt}`, and retains normalized per-user
|
|
SSI dividend history under `dividends.<symbol>.<ssi_event_id>`. The
|
|
[README](../README.md#stock-dividend-commands) describes how those records
|
|
are replayed and expired.
|
|
- **`coin`** stores cash as `usd` and embeds positions as
|
|
`assets.<symbol>.{quantity,base}`.
|
|
- **`system`** holds one marker per completed one-time startup migration. Keep
|
|
those records as audit history. The current markers are
|
|
`migration:stats-delete-stock-dividend-v1` (retires historical
|
|
`/stock_dividend` stats rows without erasing them),
|
|
`migration:stock-dividend-history-v1` (removes the retired dividend cursor
|
|
and hashed applied-event ledger), and
|
|
`migration:sticker-drop-legacy-packs-v1` (removes records left by the retired
|
|
per-user sticker pack commands).
|
|
|
|
## 2. Coolify
|
|
|
|
1. New resource → from this Git repo (Docker Compose), or a prebuilt image.
|
|
The committed [`compose.yml`](../compose.yml) defines the single
|
|
`bot` service.
|
|
2. **Enable submodule checkout.** The `monkeyd` module builds against
|
|
`third_party/monkeyd-crawler`, a git submodule wired in through a `go.mod`
|
|
`replace` directive. Coolify must clone submodules, or the Docker build
|
|
fails at `go mod download` with an unresolved
|
|
`github.com/tiennm99/monkeyd-crawler`. Turn on Coolify's recursive-clone /
|
|
submodule option for the resource. There is no build without it: leaving
|
|
`monkeyd` out of `MODULES` only disables the commands at runtime.
|
|
3. Set the env vars above in Coolify.
|
|
4. **No public domain / port** is needed — polling is outbound-only. Do not
|
|
publish a port or attach a domain. `expose: 8080` keeps the health endpoint
|
|
reachable only inside Coolify's network.
|
|
5. **Exactly one replica.** Telegram permits only one `getUpdates` consumer per
|
|
bot token; a second poller gets HTTP 409, and a second in-process scheduler
|
|
double-fires crons. Prefer **stop-first redeploys** so two containers never
|
|
overlap near a cron time.
|
|
6. **deploynotify commit SHA:** `SOURCE_COMMIT` is a Coolify predefined
|
|
variable. The bot reads it at startup and DMs the owner on every boot;
|
|
outside Coolify (local `docker compose up`) it is unset and the DM shows
|
|
`unknown`. Keep "Include Source Commit in Build" disabled: that setting
|
|
affects build args only, is not needed for this runtime path, and would
|
|
invalidate Docker cache on every commit. Do not add `SOURCE_COMMIT` to
|
|
`compose.yml`; an interpolated empty value can override Coolify's runtime
|
|
env-file value.
|
|
7. **Health check:** use Coolify's HTTP monitor against `GET /` (returns
|
|
`text/plain` `miti99bot ok`). The committed `compose.yml` defines no
|
|
`healthcheck`, and `cmd/server` has no `-healthcheck` flag. Note: `/`
|
|
reports healthy even if Mongo is unreachable (the driver auto-reconnects on
|
|
the next op); a DB outage will not auto-restart the container — accepted
|
|
trade-off.
|
|
|
|
## 3. Command menu
|
|
|
|
The bot registers its Telegram command menu from the loaded modules' public
|
|
commands on every startup. The Go module registry is the single source of
|
|
truth, so no separate command-menu file or manual registration step is
|
|
required. See [Command discovery](../README.md#command-discovery) for how the
|
|
menu text is built.
|
|
|
|
## Operations
|
|
|
|
The live deployment is the Coolify container and MongoDB is the sole system of
|
|
record. Keep exactly one replica running. To confirm Telegram is in polling mode:
|
|
|
|
```sh
|
|
# POSIX shells (Linux/macOS)
|
|
curl "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getWebhookInfo"
|
|
```
|
|
|
|
```powershell
|
|
# PowerShell
|
|
Invoke-RestMethod "https://api.telegram.org/bot$env:TELEGRAM_BOT_TOKEN/getWebhookInfo"
|
|
```
|
|
|
|
`url` should be empty. If needed, clear the webhook explicitly:
|
|
|
|
```sh
|
|
# POSIX shells (Linux/macOS)
|
|
curl -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/deleteWebhook" \
|
|
--data "drop_pending_updates=false"
|
|
```
|
|
|
|
```powershell
|
|
# PowerShell
|
|
Invoke-RestMethod -Method Post -Uri "https://api.telegram.org/bot$env:TELEGRAM_BOT_TOKEN/deleteWebhook" -Body @{ drop_pending_updates = "false" }
|
|
```
|
|
|
|
## Local smoke test
|
|
|
|
```powershell
|
|
# PowerShell
|
|
Copy-Item .env.example .env # fill TELEGRAM_BOT_TOKEN, MONGO_URL, MONGO_DATABASE
|
|
docker compose up --build
|
|
```
|
|
|
|
```sh
|
|
# POSIX shells (Linux/macOS)
|
|
cp .env.example .env # fill TELEGRAM_BOT_TOKEN, MONGO_URL, MONGO_DATABASE
|
|
docker compose up --build
|
|
```
|
|
|
|
Boot logs are JSON lines. Look for `"msg":"storage backend"` with
|
|
`"backend":"mongodb"` and the database name (never the connection string),
|
|
`"msg":"cron scheduler started"`, and `"msg":"telegram long polling started"`.
|
|
`compose.yml` does not publish port 8080 to the host, so check the health
|
|
endpoint from inside the container:
|
|
|
|
```sh
|
|
docker compose exec bot wget -qO- http://127.0.0.1:8080/
|
|
```
|
|
|
|
It returns `miti99bot ok`. The bot's webhook must be unset (the container
|
|
clears it on startup) or `getUpdates` 409s.
|