mirror of
https://github.com/tiennm99/tiennm99bot.git
synced 2026-10-11 03:13:46 +00:00
/random, /wheelofnames, and /gacha move from misc into a new random module with unchanged command names, so usage stats carry over. The new unlisted /gachabeta takes the same input as /gacha and renders the toon-shaded beta wish from /api/gachabeta on the same service, with the same text fallback.
244 lines
12 KiB
Markdown
244 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 8-second beta
|
|
style from `.../api/gachabeta` on the same service (a toon-shaded sky, a comet
|
|
bursting through a cloud, and an `S`/`SS`/`SSS` rank for 3★/4★/5★), 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.
|