From d51bcbd22411f85c56c712bf985ccf5000e24608 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Wed, 30 Sep 2026 14:17:31 +0700 Subject: [PATCH] docs: correct README, deploy guide and module docs against current code --- .env.example | 2 +- AGENTS.md | 6 +- README.md | 58 ++++++++---- compose.yml | 4 +- docs/aliases.md | 12 +-- docs/command-parameter-conventions.md | 4 +- docs/deploy-coolify-selfhosted.md | 125 ++++++++++++++------------ docs/sticker-packs.md | 8 +- 8 files changed, 124 insertions(+), 95 deletions(-) diff --git a/.env.example b/.env.example index 73efc87..ec7a8ed 100644 --- a/.env.example +++ b/.env.example @@ -15,7 +15,7 @@ MONGO_DATABASE=miti99bot # Comma-separated module list. Empty = load every module, including any module # added later — so list them explicitly when a deployment should only gain a new # module deliberately. -MODULES=util,misc,amlich,wordle,loldle,lol,stock,gold,coin,stats,monkeyd,sticker,alias +MODULES=util,misc,amlich,wordle,loldle,lol,stock,gold,coin,stats,monkeyd,sticker,alias,blacklist # Telegram user id for owner-only commands (renamed from BOT_OWNER_ID). OWNER_ID= # Comma-separated admin Telegram user ids (renamed from ADMIN_USER_IDS). diff --git a/AGENTS.md b/AGENTS.md index f876e8a..7906bc7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,8 @@ `miti99bot` is a Go Telegram bot with pluggable modules under `internal/modules`. Runtime storage is MongoDB when `MONGO_URL` is set and -in-memory storage in tests. Read `README.md` before implementation work. +in-memory otherwise, which is what tests and local no-database runs use. Read +`README.md` before implementation work. `third_party/monkeyd-crawler` is a git submodule resolved through a `go.mod` `replace` directive, not a versioned dependency. Go commands fail until it is @@ -33,7 +34,8 @@ deleting commands, update all related surfaces: - command parameter metadata used by Telegram and `/help` - handler usage text and user-facing error text - tests for registration, handlers, and command menu behavior -- README/docs when behavior changes are user-visible +- the README module table, plus the feature doc under `docs/` when one exists, + when behavior changes are user-visible Follow `docs/command-parameter-conventions.md` for all command parameter metadata and usage text. Keep metadata, usage errors, examples, and tests exact. diff --git a/README.md b/README.md index d01419f..689990b 100644 --- a/README.md +++ b/README.md @@ -7,22 +7,27 @@ Atlas via long polling and an in-process cron scheduler. | Module | What it does | |---|---| -| `util` | `/help`, `/info`, `/stickerid` | -| `misc` | `/ping`, `/ping_stats`, `/random`, `/wheelofnames`, `/ff`, `/xlt1`, `/giaxang`, `/the_answer`, `/trongtruonghop` + `/tth`, `/trongtruonghopvng` + `/tthvng` disclaimers | +| `util` | `/help`, `/info` (admin), `/stickerid` (owner) | +| `misc` | `/ping`, `/ping_stats` (admin), `/random`, `/wheelofnames`, `/ff` (admin), `/xlt1`, `/giaxang` (Petrolimex retail fuel prices), `/the_answer` (owner), `/trongtruonghop` + `/tth`, `/trongtruonghopvng` + `/tthvng` disclaimers | | `amlich` | Vietnamese lunar calendar: `/amlich` (dương lịch → âm lịch, defaults to today), `/duonglich` (âm lịch → dương lịch, `nhuan` flag for leap months); dates accept `d`, `d/m`, or `d/m/yyyy` — missing parts fill from today in the input's calendar. Years 1800–2199 only | -| `wordle` | Daily Wordle game | -| `loldle` | League-of-Legends "guess the champion" | -| `lol` | Pro-match schedule (`/lol`, `/lol_tomorrow`, `/lol_this_week`, `/lol_next_week`), per-chat digest opt-in (`/lol_subscribe`, `/lol_unsubscribe`) + daily push at 08:00 ICT | -| `stock` | VN-stocks paper trading | -| `gold` | Gold paper trading (VNAppMob SJC buy/sell VND/luong) | -| `coin` | Crypto paper trading in USD (Binance -> Coinbase -> CoinGecko price fallback) | +| `wordle` | Daily Wordle game: `/wordle [word]`, `/wordle_new`, `/wordle_giveup`, `/wordle_stats` | +| `loldle` | League-of-Legends "guess the champion": `/loldle [champion]`, `/loldle_giveup`, `/loldle_stats`, `/loldle_setmax` (owner) | +| `lol` | Pro-match schedule (`/lol [date]`, `/lol_tomorrow`, `/lol_this_week`, `/lol_next_week`), per-chat digest opt-in (`/lol_subscribe`, `/lol_unsubscribe`) + daily push at 08:00 ICT | +| `stock` | VN-stocks paper trading: `/stock_price`, `/stock_info`, `/stock_events`, `/stock_topup`, `/stock_buy`, `/stock_sell`, `/stock_cash_dividend`, `/stock_share_dividend`, `/stock_portfolio` | +| `gold` | Gold paper trading (VNAppMob SJC buy/sell VND/luong): `/gold_price`, `/gold_topup`, `/gold_buy`, `/gold_sell`, `/gold_portfolio` | +| `coin` | Crypto paper trading in USD (Binance -> Coinbase -> CoinGecko price fallback): `/coin_price`, `/coin_topup`, `/coin_buy`, `/coin_sell`, `/coin_portfolio` | | `stats` | `/stats` (top commands), `/stats users`, `/stats user `, `/stats cmd ` | | `sticker` | `/addsticker` — append a replied sticker, image, video or GIF to one shared pack. See [docs/sticker-packs.md](docs/sticker-packs.md) | | `alias` | `/alias ` save a replied message under a name, then send it back with `/insert `, bare `/`, or inline `@botname `; `/aliases` lists, `/unalias` deletes. See [docs/aliases.md](docs/aliases.md) | -| `blacklist` | Per-topic text deny-list with whitelist exceptions: `/blacklist_add`, `/blacklist_del`, `/whitelist_add`, `/whitelist_del`, `/blacklist_rules` lists both, `/blacklist_check` judges a text. Passive — the bot never scans chat. See [docs/blacklist.md](docs/blacklist.md) | +| `blacklist` | Per-topic text deny-list with whitelist exceptions: `/blacklist_add`, `/blacklist_del`, `/whitelist_add`, `/whitelist_del`, `/blacklist_rules` lists both, `/blacklist_check` judges a text, `/blacklist` does either, `/whitelist_rnd` picks a random exception. Passive — the bot never scans chat. See [docs/blacklist.md](docs/blacklist.md) | | `monkeyd` | `/monkeyd_crawl [font_size]` export a monkeydd.com novel as a PDF, `/monkeyd_tags ` list its tags as hashtags | -Disable modules with the `MODULES` environment variable. +Commands marked (admin) require a user ID in `ADMIN_IDS` or the owner; commands +marked (owner) require `OWNER_ID`. Both kinds are hidden from `/help` and the +Telegram command menu. Every other command is public. + +Choose modules with the `MODULES` environment variable, a comma-separated list; +empty loads every module. ## Command discovery @@ -42,6 +47,8 @@ Future commands must follow the [command parameter conventions](docs/command-parameter-conventions.md). Keep command metadata, handler usage text, tests, and documentation aligned. +## Feature notes + ### Lunar calendar accuracy `/amlich` and `/duonglich` port Hồ Ngọc Đức's truncated-Meeus algorithm, the @@ -220,17 +227,26 @@ all. cmd/server/ entrypoint (long polling + in-process cron + HTTP health) internal/server/ HTTP route (/ health only; cron has no HTTP route) internal/telegram/ Telegram long-polling bot wrapper -internal/cron/ in-process cron scheduler +internal/cron/ in-process cron scheduler (UTC) +internal/deploynotify/ startup DM to the owner with the deployed commit internal/modules/ Module framework, registry, dispatchers, modules internal/storage/ typed DocStore[T] (Provider + Typed); mongodb runtime + memory (tests). Values persist as flattened native BSON root documents internal/systemstate/ shared `system` collection helper for startup migration records +internal/log/, metrics/ JSON logging (LOG_LEVEL) and periodic metrics flush third_party/monkeyd-crawler/ git submodule; resolved by a go.mod replace directive compose.yml Coolify self-host stack (single bot service) -docs/deploy-coolify-selfhosted.md Self-host deploy and operations guide -docs/command-parameter-conventions.md Command parameter syntax rules -docs/amlich-known-issues.md Lunar algorithm decision and known edge cases ``` +## Documentation + +- [Self-host deploy and operations](docs/deploy-coolify-selfhosted.md), including the full environment variable list +- [Command parameter conventions](docs/command-parameter-conventions.md) +- [Aliases](docs/aliases.md) +- [Blacklist](docs/blacklist.md) +- [Sticker pack](docs/sticker-packs.md) +- [Lunar calendar algorithm and known issues](docs/amlich-known-issues.md) +- [Agent and contributor rules](AGENTS.md) + ## Run locally Clone with submodules — the `monkeyd` module builds against @@ -270,8 +286,9 @@ dev bot is created manually; its token is injected through the environment. The `lol` module reads its schedule from the PandaScore REST API and needs `LOL_PANDASCORE_TOKEN` (free tier). Without it every `/lol*` fetch fails while -the rest of the bot runs normally. See [`.env.example`](.env.example) for the -full variable list. +the rest of the bot runs normally. See the +[environment table](docs/deploy-coolify-selfhosted.md#environment) for every +variable, and [`.env.example`](.env.example) for a template. Persistent MongoDB locally (auto-selected when `MONGO_URL` is set): @@ -295,18 +312,21 @@ use `go test -v ./...` to see individual skip reasons. ```sh go vet ./... +golangci-lint run go test -count=1 ./... go build ./... ``` -CI additionally runs the test suite with Go's race detector. +CI runs the same steps, with the test suite under Go's race detector +(`go test -race`), plus an informational `govulncheck` and a `docker build`. +Lint settings live in [`.golangci.yml`](.golangci.yml). ## Deploy [`docs/deploy-coolify-selfhosted.md`](docs/deploy-coolify-selfhosted.md) covers Coolify + MongoDB Atlas (free M0), long polling (no public ingress), and -in-process cron. Storage auto-selects `mongodb` when `MONGO_URL` is set; the -cron scheduler runs by default. +in-process cron. Coolify redeploys on every push to `main`. Storage +auto-selects `mongodb` when `MONGO_URL` is set; the cron scheduler always runs. ## License diff --git a/compose.yml b/compose.yml index e99ba06..99a0ad4 100644 --- a/compose.yml +++ b/compose.yml @@ -33,7 +33,7 @@ services: # Coolify network for the container health monitor against GET / only. expose: - "8080" - # No compose healthcheck: distroless has no shell/curl and cmd/server has no - # -healthcheck flag. Configure Coolify's HTTP monitor against GET / instead + # No compose healthcheck: Coolify's own HTTP monitor covers it, so none is + # defined here. Configure that monitor against GET / instead # (returns text/plain "miti99bot ok"). Note: a plain / check does not verify # Mongo connectivity — see docs/deploy-coolify-selfhosted.md. diff --git a/docs/aliases.md b/docs/aliases.md index 18724d0..68667e3 100644 --- a/docs/aliases.md +++ b/docs/aliases.md @@ -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 -`` 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 `` span +so tapping it copies a command ready to send: ``` 3 aliases: diff --git a/docs/command-parameter-conventions.md b/docs/command-parameter-conventions.md index 897892f..1111256 100644 --- a/docs/command-parameter-conventions.md +++ b/docs/command-parameter-conventions.md @@ -15,7 +15,7 @@ remain responsible for parsing and validation. |---|---|---| | Required value | `` | `` | | Required comma-separated values | `` | `` | -| Required remaining text | `` | `` | +| Required remaining text | `` | `` | | Optional value | `[name]` | `[date]` | | Optional remaining text | `[name...]` | `[target...]` | | Alternatives in an optional group | `[literal | literal ]` | `[users | user ]` | @@ -41,7 +41,7 @@ language or extra punctuation without a user-facing need. ```text /stock_buy -/renamepack +/blacklist_del /lol [date] /trongtruonghop [target...] /stats [users | user | cmd ] diff --git a/docs/deploy-coolify-selfhosted.md b/docs/deploy-coolify-selfhosted.md index 183a85c..0ac14b1 100644 --- a/docs/deploy-coolify-selfhosted.md +++ b/docs/deploy-coolify-selfhosted.md @@ -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: , ...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..{quantity,base,openedAt}`, and -> retains normalized per-user SSI history under -> `dividends..`. 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..{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: , ...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..{quantity,base,openedAt}`, and retains normalized per-user + SSI dividend history under `dividends..`. 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..{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. diff --git a/docs/sticker-packs.md b/docs/sticker-packs.md index 33bc84f..82ffc16 100644 --- a/docs/sticker-packs.md +++ b/docs/sticker-packs.md @@ -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_`** — Telegram requires that suffix on every set a bot creates, and refuses to let a bot edit