docs: correct README, deploy guide and module docs against current code

This commit is contained in:
tiennm99 committed 2026-09-30 14:17:31 +07:00
1 parent 97aaba83ec
commit d51bcbd224
8 files changed
+124 -95

No files matched your search

+3 -9
View File
@@ -149,15 +149,9 @@ notes cannot, and Telegram's send methods for them have no caption field at all.
## Listing
`/aliases` prints the count and every name, sorted, in one message.
**Names only, not what each holds.** The store answers "which keys exist" in a
single call, while naming each kind would cost one read per alias — a round trip
each against MongoDB, on a dispatcher that serves one update at a time. To find
out what a name holds, `/insert` it.
One line per alias, showing what the name holds, with the invocation in a
`<code>` span so tapping it copies a command ready to send:
`/aliases` prints the count and every name, sorted, in one message — one line
per alias, showing what the name holds, with the invocation in a `<code>` span
so tapping it copies a command ready to send:
```
3 aliases:
+2 -2
View File
@@ -15,7 +15,7 @@ remain responsible for parsing and validation.
|---|---|---|
| Required value | `<name>` | `<ticker>` |
| Required comma-separated values | `<name,...>` | `<option,...>` |
| Required remaining text | `<name...>` | `<title...>` |
| Required remaining text | `<name...>` | `<text...>` |
| Optional value | `[name]` | `[date]` |
| Optional remaining text | `[name...]` | `[target...]` |
| Alternatives in an optional group | `[literal | literal <name>]` | `[users | user <username>]` |
@@ -41,7 +41,7 @@ language or extra punctuation without a user-facing need.
```text
/stock_buy <quantity> <ticker>
/renamepack <title...>
/blacklist_del <text...>
/lol [date]
/trongtruonghop [target...]
/stats [users | user <username> | cmd <command_name>]
+69 -56
View File
@@ -15,31 +15,38 @@ Run `miti99bot` as a long-lived container on [Coolify](https://coolify.io) with
- **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` (UTC).
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.
## Required environment
## Environment
Copy [`.env.example`](../.env.example) → `.env` (gitignored) and fill in.
| Var | Required | Notes |
|---|---|---|
| `TELEGRAM_BOT_TOKEN` | ✅ | from @BotFather |
| `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 |
| `OWNER_ID` | optional | owner-only commands (renamed from `BOT_OWNER_ID`) |
| `ADMIN_IDS` | optional | CSV of admin ids (renamed from `ADMIN_USER_IDS`) |
| `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` |
| `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) |
| `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) |
**Leave UNSET on self-host:** `KV_PROVIDER`, `PORT`,
`TELEGRAM_WEBHOOK_SECRET`, and `GOLD_VNAPP_API_KEY`. Stock, coin, and gold URL
overrides are not supported in runtime env; modules use coded defaults.
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.
@@ -86,7 +93,7 @@ reply is immediate.
Atlas admin or cluster-wide. Use a **strong unique password**.
3. **Network access:** add `0.0.0.0/0`.
> **Accepted trade-off (validated decision).** The Coolify host has no stable
> **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
@@ -96,31 +103,33 @@ reply is immediate.
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.
>
> The `stats` collection uses queryable aggregate documents for command/user
> counts and creates indexes on startup. Deleted legacy command rows are
> retained with `deleted: true`; `/stats` queries filter those rows from visible
> results. Stats startup uses the idempotent
> `migration:stats-delete-stock-dividend-v1` migration to retire historical
> `/stock_dividend` rows without erasing them. A historical `system` collection may remain in MongoDB with completed
> migration records; keep those records as audit history. Stock stores cash as
> `vnd`, embeds positions as `assets.<symbol>.{quantity,base,openedAt}`, and
> retains normalized per-user SSI history under
> `dividends.<symbol>.<ssi_event_id>`. Unprocessed retained dividend events are
> replayed on every `/stock_portfolio` until they are processed or expire after
> 90 days; events with no Record date stay informational while SSI is
> rechecked, and later SSI responses that omit an event do not delete the
> retained record. Coin stores cash as `usd` and embeds positions as
> `assets.<symbol>.{quantity,base}`. Stock startup maintenance runs the
> idempotent `migration:stock-dividend-history-v1` migration to remove the
> retired dividend cursor and hashed applied-event ledger.
### 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
@@ -132,9 +141,8 @@ reply is immediate.
`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. If submodules cannot be enabled, drop the
module instead by setting `MODULES` to the list without `monkeyd` — the build
still needs the submodule, so this is only a runtime opt-out.
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
@@ -152,21 +160,19 @@ reply is immediate.
`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`). Do **not** use a compose `healthcheck` — the
distroless image has no shell/curl 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.
`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 loaded public modules on
every startup. The Go module registry is the single source of truth; no separate
command-menu file or manual registration step is required. A command's
description plus optional `Parameters` metadata feed both surfaces. Telegram
renders the command name separately and accepts only a single-line plain-text
description. Both the native menu and `/help` show syntax plus the summary and
omit example invocations.
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
@@ -210,8 +216,15 @@ cp .env.example .env # fill TELEGRAM_BOT_TOKEN, MONGO_URL, MONGO_DATABASE
docker compose up --build
```
Boot logs should show `storage backend backend=mongodb database=…` (no
connection string), `cron scheduler started`, and `telegram long polling
started`. A request to `http://localhost:8080/` returns `miti99bot ok` (use
`Invoke-WebRequest` in PowerShell or `curl` in a POSIX shell). The bot's webhook
must be unset (the container clears it on startup) or `getUpdates` 409s.
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.
+4 -4
View File
@@ -15,10 +15,10 @@ written afterwards.
| Command | Parameters | Reply to | What it does |
|---|---|---|---|
| `/addsticker` | `[emoji...]` | sticker, photo, or image document | Adds it to the shared pack and replies with the link |
| `/addsticker` | `[emoji...]` | sticker, photo, image document, video, GIF, or video note | Adds it to the shared pack and replies with the link |
Single-shot: one message, optionally replying to a sticker or image. No
conversation state.
Single-shot: one message replying to the media to add. No conversation state.
[What it accepts](#what-it-accepts) lists every supported kind.
## Configuration
@@ -34,7 +34,7 @@ the configured pack belongs to someone else.
The caller's identity is used nowhere. That is what makes the command stateless:
no records, no keys, no per-user locks, and no ownership checks. It is also why
`/addsticker` needs no storage and fits in `util`.
`/addsticker` needs no per-user storage.
`STICKER_PACK_NAME` **must end in `_by_<this bot's username>`** — Telegram
requires that suffix on every set a bot creates, and refuses to let a bot edit