mirror of
https://github.com/tiennm99/openai-status-bot.git
synced 2026-10-11 12:19:08 +00:00
chore(project): remove plans folder
This commit is contained in:
1 parent
76d7495b4b
commit
89c84b7e1b
14 files changed
-1233
No files matched your search
@@ -1,65 +0,0 @@
|
||||
---
|
||||
phase: 1
|
||||
title: "Suitability & Decision"
|
||||
status: complete
|
||||
effort: "research-only (no code)"
|
||||
---
|
||||
|
||||
# Phase 1: Suitability & Decision
|
||||
|
||||
## Overview
|
||||
|
||||
Decide whether MongoDB is the right datastore for this bot before writing code. This phase is the answer to "is it suitable for our case." No code; output is the recorded verdict below.
|
||||
|
||||
## Workload facts (from codebase)
|
||||
|
||||
- State is tiny: one set of subscribers (count = number of Telegram chats), per-subscriber settings, ~dozens of component statuses, a handful of pending component events, incident-update version markers, one `initialized` flag, one telegram offset, and short-lived per-event delivery markers.
|
||||
- Access pattern: one poll/minute (default `POLL_INTERVAL=1m`) + occasional command handling. No hot path, no high QPS, no large values.
|
||||
- Already abstracted behind `poller.Store` / `bot.Store` interfaces (`internal/poller/poller.go:17`, `internal/bot/bot.go:25`).
|
||||
- Redis-specific mechanisms in use: a Lua script for atomic check-then-set (`internal/redisstore/subscriber_settings.go:16`), `EXPIRE` for 7-day delivery dedup TTL (`internal/redisstore/checkpoint.go:137`), and `TxPipeline` for multi-key atomicity (RemoveSubscriber, MarkDelivered, MarkIncidentUpdateVersion).
|
||||
|
||||
## Redis → MongoDB capability mapping
|
||||
|
||||
| Redis usage | MongoDB equivalent | Notes |
|
||||
|---|---|---|
|
||||
| `subscribers` SET + `subscriber-settings` HASH | single `subscribers` collection, one doc per subscriber (`_id` = subscriber key, `types`/`components` fields) | **Collapses two keys into one doc**; eliminates the SRem+HDel `TxPipeline` in RemoveSubscriber |
|
||||
| Lua check-then-set on settings | `UpdateOne(filter{_id}, $set)`, read `MatchedCount` | `MatchedCount==0` ⇒ not subscribed; replaces the Lua script |
|
||||
| `component-statuses` HASH | `component_statuses` collection, doc per component | upsert via `UpdateOne(SetUpsert)` |
|
||||
| `pending-component-events` HASH (JSON) | `pending_component_events` collection, doc per component | native BSON fields, no manual JSON marshal needed |
|
||||
| `incident-update-versions` HASH + legacy `incident-updates` SET | `incident_update_versions` collection, doc per updateID | **legacy SET + migration fallback (`checkpoint.go:89-108`) dropped** — fresh DB, no legacy data |
|
||||
| `event-delivery:<sha>` SET + `EXPIRE 7d` | `delivery` collection, doc per (eventKey, subscriber) + **TTL index** on `expiresAt` | `MarkDelivered` becomes one upsert (no SAdd+Expire pair); sha256 key-hashing dropped (Mongo handles arbitrary string values) |
|
||||
| `initialized` flag, `telegram-offset` | `meta` collection, doc per key | trivial FindOne/upsert |
|
||||
|
||||
All driver specifics confirmed against MongoDB Go driver **v2** (`go.mongodb.org/mongo-driver/v2`, latest v2.7.0, 2026): `ApplyURI` parses `mongodb+srv://` Atlas URIs; TTL via `IndexModel` + `SetExpireAfterSeconds`; `UpdateOne`/`MatchedCount` and `SetUpsert(true)` cover conditional set and upsert.
|
||||
|
||||
## Verdict
|
||||
|
||||
**Suitable: YES. Justified: YES** (given the stated motivation — team already runs MongoDB, so dropping Redis consolidates ops onto one datastore).
|
||||
|
||||
Functionally, MongoDB does everything Redis does here, and the document model **simplifies** the code: three multi-key `TxPipeline` sequences collapse into single-document operations, the Lua script disappears, and the legacy incident-dedup migration path is deleted (no data to migrate). Latency and TTL-sweep granularity (~60s) are irrelevant at one poll/minute with a 7-day dedup window.
|
||||
|
||||
### Trade-offs to accept (be honest)
|
||||
|
||||
1. **Testing regression (the real cost).** `miniredis` is pure-Go, in-memory, zero-binary, instant. MongoDB has **no equivalent** — every option runs a real `mongod`: `testcontainers-go` (needs Docker in CI, ~2-3s/suite) or `memongo` (downloads a ~100MB `mongod` binary, UNIX-only, primary repo stale — prefer the `tryvium-travels/memongo` fork). CI gains a Docker/binary dependency and slows down. This is a genuine downgrade in test ergonomics and the main argument *against* switching absent the ops motivation.
|
||||
2. Heavier runtime dependency and image than the tiny `go-redis` client (acceptable; not a hot path).
|
||||
3. Loss of Redis's trivial single-process local dev (`redis:7-alpine`); replaced by a **required Atlas connection** (no local DB — both dev and prod use Atlas, separated by database name).
|
||||
|
||||
### When NOT to switch
|
||||
If the only driver were "exploring" with no infra reason, staying on Redis would be the KISS choice — Redis is the better technical fit for this exact workload. The switch is recommended **only because** MongoDB is already operated and consolidation has real ops value.
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Confirm the motivation still holds (already-run MongoDB / managed external host) before committing to Phases 2-5.
|
||||
2. Accept the testing trade-off. **Decided (Session 1):** testcontainers-go, gated behind `//go:build integration` and run manually later — default `go test` stays Docker-free (Phase 5).
|
||||
3. Proceed to Phase 2.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [x] Verdict reviewed and motivation reconfirmed (already-run MongoDB / Atlas).
|
||||
- [x] Test backend decided: testcontainers, gated + deferred (Session 1).
|
||||
- [x] Testing trade-off accepted.
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
- Risk: team underestimates CI impact of losing `miniredis`. Mitigation: Phase 5 makes the test backend explicit and a gating decision here.
|
||||
- Risk: managed MongoDB connectivity/auth from the bot's deploy environment. Mitigation: validate `MONGODB_URI` reachability early (Phase 2 ping on startup, same as current Redis ping in `main.go:38`).
|
||||
@@ -1,59 +0,0 @@
|
||||
---
|
||||
phase: 2
|
||||
title: "Store Package & Connection"
|
||||
status: complete
|
||||
effort: "S"
|
||||
---
|
||||
|
||||
# Phase 2: Store Package & Connection
|
||||
|
||||
## Overview
|
||||
|
||||
Create `internal/mongostore` with the connection plumbing, collection handles, index bootstrap, and the storage-agnostic domain types/logic moved over verbatim from `redisstore`. No consumer wiring yet (Phase 4); `redisstore` still exists in parallel until Phase 5.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Functional: open a MongoDB client from a URI, ping on startup, expose a `Store` holding collection handles, ensure required indexes exist idempotently.
|
||||
- Non-functional: keep exported type/constant names identical to `redisstore` so Phase 5 is a qualifier swap, not a rewrite of consumers.
|
||||
|
||||
## Architecture
|
||||
|
||||
Collections (database name from config, default `openai_status_bot`):
|
||||
|
||||
- `subscribers` — `{_id: "<chatID>"|"<chatID:threadID>", chatID, threadID, types[], components[]}`
|
||||
- `component_statuses` — `{_id: componentID, status}`
|
||||
- `pending_component_events` — `{_id: componentID, componentName, status, updatedAt, position, previousStatus, deliveryKey}`
|
||||
- `incident_update_versions` — `{_id: updateID, version}`
|
||||
- `delivery` — `{_id: "<eventKey>|<subscriber>", eventKey, subscriber, expiresAt}` (TTL index on `expiresAt`)
|
||||
- `meta` — `{_id: "initialized"|"telegramOffset", value}`
|
||||
|
||||
Indexes ensured on startup:
|
||||
- `delivery`: TTL index `{expiresAt:1}` with `ExpireAfterSeconds(604800)` (7 days). The compound key is encoded in `_id`, so no extra unique index needed; an additional non-unique `{eventKey:1}` index speeds `DeliveredSubscribers`/`ClearDelivery` lookups.
|
||||
- All other collections use `_id` only — no extra indexes.
|
||||
|
||||
## Related Code Files
|
||||
|
||||
- Create: `internal/mongostore/store.go` — `Store` struct, `New(client *mongo.Client, dbName string) *Store`, collection handles, collection-name constants (replacing the `*Key` constants in `redisstore/store.go`), `SubscriptionType*` constants.
|
||||
- Create: `internal/mongostore/indexes.go` — `EnsureIndexes(ctx) error` (TTL + eventKey index).
|
||||
- Create: `internal/mongostore/subscriber.go` — move `Subscriber`, `NewSubscriber`, `ParseSubscriberKey`, `Key()`, `Accepts`, `DefaultSubscriptionTypes` **verbatim** (pure logic, storage-agnostic).
|
||||
- Create: `internal/mongostore/subscriber_normalization.go` — move `normalizeTypes`, `normalizeComponents`, `containsFold` **verbatim**.
|
||||
- Reference (driver, Phase 4 adds to go.mod): `go.mongodb.org/mongo-driver/v2/mongo`, `.../mongo/options`, `.../bson`.
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. `go get go.mongodb.org/mongo-driver/v2@latest` (adds to go.mod; full dep cleanup in Phase 4).
|
||||
2. Write `store.go`: define collection-name constants, `Store` struct holding `*mongo.Database` + per-collection `*mongo.Collection` handles, and `New(client, dbName)`.
|
||||
3. Write `subscriber.go` and `subscriber_normalization.go` by copying the storage-agnostic code from the matching `redisstore` files unchanged except the package name.
|
||||
4. Write `indexes.go` with `EnsureIndexes` creating the TTL index (`options.Index().SetExpireAfterSeconds(604800)`) and the `{eventKey:1}` index via `Indexes().CreateMany`. Idempotent — re-creating an existing index is a no-op/ignored error.
|
||||
5. `go build ./internal/mongostore/...` to confirm the package compiles in isolation.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] `internal/mongostore` compiles standalone.
|
||||
- [ ] Exported names match `redisstore` (`Subscriber`, `PendingComponentEvent` [added Phase 3], `NewSubscriber`, `DefaultSubscriptionTypes`, `SubscriptionTypeIncident/Component`).
|
||||
- [ ] `EnsureIndexes` creates the TTL index with 604800s expiry.
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
- Risk: driver v2 import path differs from v1 examples. Mitigation: pin `go.mongodb.org/mongo-driver/v2`; `mongo.Connect(options.Client().ApplyURI(uri))` (no separate `ctx` arg in v2 `Connect`).
|
||||
- Risk: index creation race on multi-instance deploy. Mitigation: single-instance bot; `CreateMany` is idempotent regardless.
|
||||
@@ -1,67 +0,0 @@
|
||||
---
|
||||
phase: 3
|
||||
title: "Implement Store Methods"
|
||||
status: complete
|
||||
effort: "M"
|
||||
---
|
||||
|
||||
# Phase 3: Implement Store Methods
|
||||
|
||||
## Overview
|
||||
|
||||
Implement every method required by `poller.Store` and `bot.Store` against MongoDB, matching the existing semantics exactly (return values, "exists" booleans, defaults, self-heal-on-corrupt behavior where applicable).
|
||||
|
||||
## Requirements
|
||||
|
||||
Satisfy the full interface surface (from `internal/poller/poller.go:17` and `internal/bot/bot.go:25`):
|
||||
|
||||
Subscribers/settings: `AddSubscriber`, `RemoveSubscriber`, `ListSubscribers`, `GetSubscriber`, `UpdateSubscriberTypes`, `UpdateSubscriberComponents`, `UpdateSubscriberSettings`.
|
||||
Checkpoint/state: `ComponentStatuses`, `SaveComponentStatus`, `PendingComponentEvents`, `SavePendingComponentEvent`, `RemovePendingComponentEvent`, `HasIncidentUpdateVersion`, `MarkIncidentUpdateVersion`, `DeliveredSubscribers`, `MarkDelivered`, `ClearDelivery`, `IsInitialized`, `SetInitialized`, `TelegramOffset`, `SaveTelegramOffset`.
|
||||
|
||||
## Architecture / semantics mapping
|
||||
|
||||
- `PendingComponentEvent` struct moves to `mongostore` with BSON tags; stored as native fields (drop the manual `json.Marshal`/`Unmarshal` from `checkpoint.go:58-75`).
|
||||
- **AddSubscriber**: `UpdateOne({_id:key}, {$set:{chatID,threadID}, $setOnInsert:{types:defaults, components:[]}}, upsert)`. Preserves existing settings on re-`/start` (matches current load-then-save behavior at `subscriber.go:73-83`).
|
||||
- **RemoveSubscriber**: `DeleteOne({_id:key})` — single op (was SRem+HDel TxPipeline).
|
||||
- **ListSubscribers**: `Find({})` + `cursor.All`. Decode straight into `Subscriber` (was SMEMBERS + HGETALL); keep the malformed-key self-heal: on decode/parse failure for a doc, `DeleteOne` it and continue or error (preserve current behavior at `subscriber.go:108-125`).
|
||||
- **GetSubscriber**: `FindOne({_id:key})`; `ErrNoDocuments` ⇒ `(zero,false,nil)`.
|
||||
- **UpdateSubscriberTypes/Components/Settings**: `UpdateOne({_id:key}, {$set:{...}})`; `MatchedCount==0` ⇒ `(false,nil)` (replaces the Lua check-then-set). Apply `normalizeTypes`/`normalizeComponents` before write, same as today.
|
||||
- **ComponentStatuses**: `Find({})` → `map[string]string{_id: status}`.
|
||||
- **SaveComponentStatus**: `UpdateOne({_id}, {$set:{status}}, upsert)`.
|
||||
- **PendingComponentEvents**: `Find({})` → `map[string]PendingComponentEvent`.
|
||||
- **SavePendingComponentEvent / RemovePendingComponentEvent**: upsert / `DeleteOne` by `_id=componentID`.
|
||||
- **HasIncidentUpdateVersion**: `FindOne({_id:updateID})`; compare stored `version`. **Drop the legacy `incident-updates` SET fallback** (`checkpoint.go:97-106`) — no legacy data on a fresh DB.
|
||||
- **MarkIncidentUpdateVersion**: `UpdateOne({_id:updateID}, {$set:{version}}, upsert)` — single op (was HSet+SAdd TxPipeline).
|
||||
- **DeliveredSubscribers**: `Find({eventKey})` → `map[string]bool{subscriber:true}`.
|
||||
- **MarkDelivered**: `UpdateOne({_id: eventKey+"|"+subscriber}, {$set:{eventKey,subscriber,expiresAt: now+7d}}, upsert)` — single op with TTL (was SAdd+Expire TxPipeline). `now` from `time.Now()`.
|
||||
- **ClearDelivery**: `DeleteMany({eventKey})`. Drop sha256 key-hashing (`deliveryStateKey`, `checkpoint.go:168`).
|
||||
- **IsInitialized / SetInitialized**: `meta` doc `_id:"initialized"` — `FindOne` exists / upsert `{value:true}`.
|
||||
- **TelegramOffset / SaveTelegramOffset**: `meta` doc `_id:"telegramOffset"` — `FindOne` (default 0 on `ErrNoDocuments`, and clear-on-invalid like `checkpoint.go:155-160` is no longer needed since BSON stores an int natively) / upsert `{value:offset}`.
|
||||
|
||||
## Related Code Files
|
||||
|
||||
- Create: `internal/mongostore/subscriber.go` (extend Phase 2 file with the subscriber CRUD methods).
|
||||
- Create: `internal/mongostore/subscriber_settings.go` — `subscriberSettings` decode/normalize helpers (drop the Lua script; keep normalization), or fold into subscriber.go if small (KISS).
|
||||
- Create: `internal/mongostore/checkpoint.go` — `PendingComponentEvent` (BSON tags) + all checkpoint/delivery/meta/component methods.
|
||||
- Reference: `internal/poller/poller.go:17`, `internal/bot/bot.go:25` (interface contracts to satisfy).
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Add `PendingComponentEvent` with BSON tags to `checkpoint.go`.
|
||||
2. Implement subscriber CRUD + settings methods; verify against `bot.Store` signatures.
|
||||
3. Implement checkpoint/delivery/meta/component methods; verify against `poller.Store` signatures.
|
||||
4. Add a compile-time assertion file or inline `var _ poller.Store = (*Store)(nil)` / `var _ bot.Store = (*Store)(nil)` (only after Phase 5 imports align — or assert against locally redeclared interfaces during this phase) to catch signature drift.
|
||||
5. `go build ./internal/mongostore/...`.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] All listed methods implemented with matching signatures and semantics.
|
||||
- [ ] No `TxPipeline`/Lua equivalents needed — each former multi-key op is a single document op.
|
||||
- [ ] Legacy incident-dedup fallback and sha256 delivery-key hashing removed.
|
||||
- [ ] Package compiles.
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
- Risk: `MatchedCount` vs `ModifiedCount` confusion in conditional updates. Mitigation: use `MatchedCount` for "did the subscriber exist" (a no-op `$set` to identical values still matches but may not modify).
|
||||
- Risk: telegram offset stored as BSON int32 vs int64. Mitigation: store/read as `int64` explicitly in BSON.
|
||||
- Risk: decode of self-healed malformed subscriber docs. Mitigation: replicate current delete-and-continue/error path; cover in Phase 5 tests.
|
||||
@@ -1,51 +0,0 @@
|
||||
---
|
||||
phase: 4
|
||||
title: "Config Wiring & Deployment"
|
||||
status: complete
|
||||
effort: "S"
|
||||
---
|
||||
|
||||
# Phase 4: Config Wiring & Deployment
|
||||
|
||||
## Overview
|
||||
|
||||
Replace Redis configuration, connection bootstrap, and deployment artifacts with MongoDB equivalents. After this phase `main.go` builds the Mongo client; consumer type swap is Phase 5.
|
||||
|
||||
<!-- Updated: Validation Session 1 - Atlas for both envs; one cluster, two DBs selected by MONGODB_DATABASE; no local mongo service -->
|
||||
|
||||
## Requirements
|
||||
|
||||
- New env: `MONGODB_URI` (Atlas `mongodb+srv://...`, **required**, no localhost default — there is no local DB), `MONGODB_DATABASE` (default `openai_status_bot`; dev env sets a separate name e.g. `openai_status_bot_dev`). Remove `REDIS_URL`.
|
||||
- `main.go` connects, pings, ensures indexes, constructs `mongostore.New(client, dbName)`.
|
||||
- Deployment (validated): **Atlas for both prod and dev** — single cluster, two databases differentiated by `MONGODB_DATABASE`. **No local `mongo` service** in any compose file.
|
||||
|
||||
## Architecture
|
||||
|
||||
`config.Config`: replace `RedisOptions *redis.Options` with `MongoURI string` + `MongoDatabase string`. Drop `parseRedisURL`, `minRedisDB`/`maxRedisDB`. Validation: `MONGODB_URI` **required** (error if empty, like `TELEGRAM_BOT_TOKEN` at `config.go:35-38` — no localhost fallback since both envs use Atlas); `MONGODB_DATABASE` defaults to `openai_status_bot`. Keep validation minimal; the driver validates the URI on connect/ping.
|
||||
|
||||
## Related Code Files
|
||||
|
||||
- Modify: `internal/config/config.go` — swap Redis fields/parsing for Mongo fields; keep `getEnv` default pattern.
|
||||
- Modify: `internal/config/config_test.go` — replace Redis URL cases with Mongo URI/DB cases.
|
||||
- Modify: `cmd/openai-status-bot/main.go` — replace `redis.NewClient`+`Ping` (`main.go:37-42`) with `mongo.Connect(options.Client().ApplyURI(cfg.MongoURI))`, `client.Ping`, `defer client.Disconnect(ctx)`; call `store.EnsureIndexes(ctx)`; `store := mongostore.New(client, cfg.MongoDatabase)`. Update the connect-error log fields (drop redis-specific `Network/Addr/DB/TLS`).
|
||||
- Modify: `.env.example` — `MONGODB_URI=mongodb+srv://<user>:<pass>@<cluster>/` (placeholder), `MONGODB_DATABASE=openai_status_bot`; remove `REDIS_URL`.
|
||||
- Modify: `docker-compose.yml` — **remove the `redis` service and `redis-data` volume entirely** (no local DB). Becomes bot-only, connecting to Atlas via `.env`; this is the **development** path, so set `MONGODB_DATABASE` to the dev database (e.g. `openai_status_bot_dev`). No `depends_on`.
|
||||
- Modify: `docker-compose.bot.yml` — bot-only, connects to Atlas via `.env` with the **production** `MONGODB_DATABASE` (`openai_status_bot`). With no local DB, this and `docker-compose.yml` differ only by database name; keep both for the dev/prod split. Optionally collapse to one file — but keeping both matches the existing two-file convention (YAGNI: don't restructure beyond the DB-name change).
|
||||
- Modify: `README.md` — Configuration table (`MONGODB_URI`, `MONGODB_DATABASE` rows; drop `REDIS_URL` and the percent-encoding note, or rewrite the note for Atlas SRV URIs), feature bullet "Uses Redis for…" → MongoDB, Compose/Quick-Start description (Atlas, no local DB; dev vs prod database), intro line. Note that no local datastore runs in Compose — an Atlas URI is required to start.
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Edit `config.go` + `config_test.go`; `go test ./internal/config/`.
|
||||
2. Edit `main.go` for Mongo client lifecycle + `EnsureIndexes`. (Will not fully build until Phase 5 swaps `redisstore`→`mongostore` in poller/bot — acceptable; Phases 4-5 land together.)
|
||||
3. Update `.env.example`, both compose files, README.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] `config` package + tests pass with Mongo env vars; no Redis symbols remain in config.
|
||||
- [ ] `main.go` opens/pings Mongo, ensures indexes, builds `mongostore`.
|
||||
- [ ] Compose, `.env.example`, README reference only MongoDB.
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
- Risk: managed URIs carry credentials/`+srv`. Mitigation: `ApplyURI` handles `mongodb+srv://`; never log the URI (log db name only); keep `.env` out of git (already in `.gitignore`).
|
||||
- Risk: dropping `REDIS_URL` breaks existing deploys silently. Mitigation: README "Notes" + this plan flag the env rename; no compatibility shim (clean cutover, intended).
|
||||
@@ -1,69 +0,0 @@
|
||||
---
|
||||
phase: 5
|
||||
title: "Cutover & Tests"
|
||||
status: complete
|
||||
effort: "M"
|
||||
---
|
||||
|
||||
# Phase 5: Cutover & Tests
|
||||
|
||||
## Overview
|
||||
|
||||
Swap all consumers from `redisstore` to `mongostore`, delete `redisstore`, purge the Redis dependencies, and re-establish the test suite on a MongoDB-backed harness (replacing `miniredis`). After this phase the project builds and tests green with zero Redis.
|
||||
|
||||
## Requirements
|
||||
|
||||
- All `redisstore.X` references → `mongostore.X` (type names identical; only the qualifier changes).
|
||||
- `internal/redisstore` deleted; `go-redis` and `miniredis` removed from go.mod/go.sum.
|
||||
- Tests pass against a real `mongod` test harness.
|
||||
|
||||
## Architecture: test backend
|
||||
|
||||
<!-- Updated: Validation Session 1 - testcontainers chosen but gated behind build tag; not run this phase; poller/bot use in-test fake -->
|
||||
|
||||
`miniredis` (pure-Go, in-memory, no binary) has **no MongoDB equivalent** — real-backend tests must run a real `mongod`.
|
||||
|
||||
**Decision (validated): `testcontainers-go/modules/mongodb`, but gated and deferred.** The `mongostore` integration suite is written behind a build tag (`//go:build integration`) so it is **NOT part of default `go test ./...`** in this phase. Default test runs must stay Docker-free. The user runs `go test -tags=integration ./internal/mongostore/...` later, when Docker is available.
|
||||
|
||||
- `internal/mongostore/store_test.go` → tag `//go:build integration`; a `testMongo(t)` helper starts a `mongo:7` container once, returns a `*mongostore.Store` against a unique per-test database, and `t.Cleanup` drops it.
|
||||
- **Poller/bot tests use an in-test fake `Store`** (hand-written, satisfies the interface) — fast, Docker-free, runs in default `go test`. No real DB in those suites.
|
||||
- So in this phase: `go test ./...` (no tag) passes with zero Docker — poller/bot/config/etc. on fakes, `mongostore` integration tests skipped by the build tag. CI stays green without Docker; real-backend validation is a deliberate manual/later step.
|
||||
|
||||
## Related Code Files
|
||||
|
||||
- Modify (qualifier swap `redisstore.` → `mongostore.`, update imports):
|
||||
- `internal/poller/poller.go`, `internal/poller/event_collection.go`, `internal/poller/delivery.go`
|
||||
- `internal/bot/bot.go`, `internal/bot/commands.go`, `internal/bot/helpers.go`, `internal/bot/subscription_format.go`
|
||||
- `internal/telegram/client.go` (`Subscriber` param at `client.go:108`)
|
||||
- Create: `internal/mongostore/store_test.go` — `//go:build integration` tag; port the 243-line `redisstore/store_test.go` to the testcontainers harness (same assertions: subscriber CRUD, settings normalization/self-heal, delivery dedup + TTL-index presence, incident version dedup, offset, initialized). Excluded from default `go test`.
|
||||
- Modify: `internal/poller/poller_test.go`, `internal/bot/bot_test.go` — swap `redisstore` import; replace any `miniredis` usage with a lightweight **in-test fake** of the `Store` interface (Docker-free, fast). Do not point these at the real harness.
|
||||
- Create (if not already present in those tests): a shared fake `Store` (e.g. an in-memory map-backed type in a test helper) satisfying both `poller.Store` and `bot.Store`.
|
||||
- Delete: `internal/redisstore/` (all 6 files).
|
||||
- Modify: `go.mod`/`go.sum` — remove `github.com/redis/go-redis/v9`, `github.com/alicebob/miniredis/v2` (+ transitive `gopher-lua`, `go-rendezvous`, `xxhash` if now unused); add `go.mongodb.org/mongo-driver/v2` and the chosen test dep. Run `go mod tidy`.
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Add compile-time interface assertions in `mongostore` (`var _ poller.Store`, `var _ bot.Store`) — now imports resolve both ways; fix any signature drift from Phase 3.
|
||||
2. Swap qualifiers across poller/bot/telegram (grep `redisstore\.` → confirm each maps to an identical `mongostore` symbol).
|
||||
3. `rm -rf internal/redisstore`; `go build ./...` and fix remaining references.
|
||||
4. Build the in-test fake `Store`; swap poller/bot tests onto it. Port `store_test.go` to the testcontainers harness under `//go:build integration`.
|
||||
5. `go mod tidy`; confirm Redis deps gone (`grep redis go.mod` empty).
|
||||
6. `go test ./...` (no tag) — must pass **without Docker** (poller/bot on fakes, integration suite excluded). `go vet ./...`. Optionally `go build -tags=integration ./...` to confirm the gated suite compiles.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] `go build ./...` clean; no `redisstore` references; `internal/redisstore` deleted.
|
||||
- [ ] `grep -ri redis` finds no code/config/doc references (outside this plan).
|
||||
- [ ] `go test ./...` (no tag) green **without Docker**; gated `mongostore` suite compiles under `-tags=integration` (run later by the user).
|
||||
- [ ] `go mod tidy` removed `go-redis` + `miniredis`; added mongo driver + testcontainers (test-only).
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
- Risk: CI lacks Docker for testcontainers. Resolved by decision: integration suite is behind `//go:build integration` and excluded from default `go test`; user runs it manually later. Document the `-tags=integration` + Docker requirement in README.
|
||||
- Risk: in-test fake `Store` drifts from real `mongostore` semantics (the fake passes but Mongo behaves differently). Mitigation: the gated integration suite is the real-behavior backstop; user must run it before trusting the cutover in production.
|
||||
- Risk: TTL behavior not directly assertable (sweep ~60s). Mitigation: assert the TTL **index exists** with `ExpireAfterSeconds==604800` rather than waiting for expiry; trust MongoDB to enforce.
|
||||
- Risk: hidden semantic drift between fake `Store` and real `mongostore` in poller/bot tests. Mitigation: keep at least the `mongostore` suite exercising real behavior end-to-end for the store contract.
|
||||
|
||||
## Post-cutover docs
|
||||
|
||||
Update the three docs that reference Redis: `docs/system-architecture.md`, `docs/setup-guide.md`, `docs/component-id-monitoring-report.md` (verify each via `grep -li redis docs/`). Add a README note that switching from a prior Redis deploy starts from empty state (no migration; subscribers re-`/start`).
|
||||
@@ -1,73 +0,0 @@
|
||||
---
|
||||
title: "Migrate datastore from Redis to MongoDB"
|
||||
description: ""
|
||||
status: complete
|
||||
priority: P2
|
||||
branch: "main"
|
||||
tags: []
|
||||
blockedBy: []
|
||||
blocks: []
|
||||
created: "2026-06-26T04:22:22.185Z"
|
||||
createdBy: "ck:plan"
|
||||
source: skill
|
||||
---
|
||||
|
||||
# Migrate datastore from Redis to MongoDB
|
||||
|
||||
## Overview
|
||||
|
||||
Replace Redis with MongoDB as the bot's datastore. No data migration: cutover is a clean reseed (first poll re-seeds state, subscribers re-issue `/start`). Motivation: team already operates MongoDB; goal is removing the Redis dependency to consolidate on one datastore. Hosting (validated): **managed Atlas for both environments** — one cluster with two databases (production + development) selected via `MONGODB_DATABASE`. **No local MongoDB service**; both compose files connect to Atlas, differing only by database name.
|
||||
|
||||
**Verdict (Phase 1, full reasoning there):** Suitable and justified. Every Redis op maps cleanly to MongoDB, and the document model actually *simplifies* several multi-key sequences. One real regression to accept: unit tests lose the pure-Go in-memory `miniredis`; MongoDB has no equivalent, so tests need a real `mongod` (testcontainers needs Docker, or `memongo` downloads a binary). CI gets slower and gains a Docker/binary dependency.
|
||||
|
||||
## Approach (straight replace, no dual backend)
|
||||
|
||||
`poller.Store` and `bot.Store` are already interfaces, and the only cross-package coupling is the domain types `redisstore.Subscriber`, `redisstore.PendingComponentEvent`, the `SubscriptionType*` constants, `NewSubscriber`, and `DefaultSubscriptionTypes` (~90 references, type *names* not bodies). Plan: create `internal/mongostore` exporting the **same type/constant names**, move the storage-agnostic domain logic verbatim, reimplement only the client-touching methods against MongoDB, swap consumer qualifiers `redisstore.` → `mongostore.`, then delete `internal/redisstore`. YAGNI: no pluggable-backend abstraction since only one backend is wanted.
|
||||
|
||||
## Phases
|
||||
|
||||
| Phase | Name | Status |
|
||||
|-------|------|--------|
|
||||
| 1 | [Suitability & Decision](./phase-01-suitability-decision.md) | Done |
|
||||
| 2 | [Store Package & Connection](./phase-02-store-package-connection.md) | Done |
|
||||
| 3 | [Implement Store Methods](./phase-03-implement-store-methods.md) | Done |
|
||||
| 4 | [Config Wiring & Deployment](./phase-04-config-wiring-deployment.md) | Done |
|
||||
| 5 | [Cutover & Tests](./phase-05-cutover-tests.md) | Done |
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] Bot builds and runs with no `go-redis` / `miniredis` dependency.
|
||||
- [x] All store operations (subscribers, settings, component statuses, pending component events, incident-update versions, delivery dedup w/ 7-day expiry, initialized flag, telegram offset) work against MongoDB.
|
||||
- [x] `poller.Store` and `bot.Store` interfaces satisfied by `mongostore`; existing poller/bot/telegram tests pass against the new backend.
|
||||
- [x] Config, `.env.example`, compose files, and README reference `MONGODB_URI`/`MONGODB_DATABASE`; no Redis references remain.
|
||||
- [x] TTL index on delivery collection enforces 7-day expiry; conditional "update-only-if-subscribed" preserved via `MatchedCount`.
|
||||
|
||||
## Dependencies
|
||||
|
||||
None (no cross-plan dependencies; no in-repo data migration).
|
||||
|
||||
## Validation Log
|
||||
|
||||
### Session 1 — 2026-06-26
|
||||
|
||||
**Verification Results**
|
||||
- Claims checked: 7 anchors + driver facts (Full tier, 5 phases)
|
||||
- Verified: 7 | Failed: 0 | Unverified: 0
|
||||
- Evidence: `poller.go:17` & `bot.go:25` (Store interfaces), `subscriber_settings.go:16` (Lua script), `checkpoint.go:137` (`Expire` 7d), `telegram/client.go:108` (`redisstore.Subscriber` param), 92 `redisstore.` refs across `internal`+`cmd`, `.env` gitignored. Driver = `go.mongodb.org/mongo-driver/v2` (v2.7.0).
|
||||
|
||||
**Decisions confirmed**
|
||||
1. **Test backend** → testcontainers-go chosen, but **gated/deferred**: integration suite behind a build tag (`//go:build integration`), NOT run in Phase 5. Default `go test ./...` must stay Docker-free; user runs the integration suite later. (Phase 5)
|
||||
2. **Hosting** → Atlas for both envs; **one cluster, two databases** (prod + dev) selected by `MONGODB_DATABASE`. **No local `mongo` service** in any compose file. (Phase 4)
|
||||
3. **Cutover** → clean break: remove `REDIS_URL`, no compat shim, state resets (subscribers re-`/start`, checkpoints reseed). (Phase 4/5)
|
||||
4. **Poller/bot tests** → in-test fake `Store` (Docker-free); real-backend coverage stays in the gated `mongostore` suite. (Phase 5)
|
||||
|
||||
### Whole-Plan Consistency Sweep
|
||||
- Re-read `plan.md` + all 5 phase files after propagation. Local-`mongo`-service references removed (Overview, Phase 4); test-gating note added (Phase 5). No stale "local mongo" / "docker-compose mongo service" terms remain. Verdict (Phase 1) unaffected by these deployment/test decisions. **0 unresolved contradictions.**
|
||||
|
||||
### Implementation — 2026-06-26 (all phases complete)
|
||||
- New `internal/mongostore` (store/subscriber/subscriber_normalization/checkpoint/indexes) on driver `go.mongodb.org/mongo-driver/v2 v2.7.0`; `internal/redisstore` deleted; `go-redis`+`miniredis` removed via `go mod tidy`.
|
||||
- Consumers (`poller`, `bot`, `telegram`) swapped by qualifier only (`redisstore.`→`mongostore.`); identical exported names. Interface conformance enforced at compile time via `main.go` wiring (no `var _` assertion — would cycle).
|
||||
- `config` now requires `MONGODB_URI`, defaults `MONGODB_DATABASE=openai_status_bot`; `main.go` connects/pings/`EnsureIndexes`. `.env.example`, both compose files (bot-only, Atlas, dev/prod by DB name), README, and 3 docs updated; no Redis refs remain.
|
||||
- Tests: poller/bot/config on Docker-free fakes; `mongostore` has pure-logic unit tests (default) + gated `//go:build integration` testcontainers suite (run via `go test -tags=integration ./internal/mongostore/...`). `go build`/`go test`/`go vet ./...` all green without Docker.
|
||||
- `code-reviewer`: DONE, 0 Critical/High/Medium; one cosmetic note (subscriber `threadID` null-vs-omit) applied.
|
||||
- Deferred: user runs the gated integration suite with Docker before trusting the cutover in production.
|
||||
-83
@@ -1,83 +0,0 @@
|
||||
---
|
||||
phase: 1
|
||||
title: "Dependency and architecture setup"
|
||||
status: completed
|
||||
priority: P1
|
||||
dependencies: []
|
||||
---
|
||||
|
||||
# Phase 1: Dependency and architecture setup
|
||||
|
||||
## Overview
|
||||
|
||||
Introduce `go-telegram/bot` and define the local architecture boundaries before moving command behavior. This phase should compile toward the new dependency but may keep old runtime code until later phases finish.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Functional: add `github.com/go-telegram/bot@v1.21.0`; identify startup, command, notification, and error integration points.
|
||||
- Non-functional: keep dependency footprint small; no webhook mode; no debug logging of Telegram requests.
|
||||
|
||||
## Architecture
|
||||
|
||||
Keep these boundaries:
|
||||
|
||||
- `cmd/openai-status-bot/main.go` owns process wiring and framework lifecycle.
|
||||
- `internal/bot` owns command behavior and handler registration.
|
||||
- `internal/telegram` owns notification sending and framework error translation for poller use.
|
||||
- `internal/poller` should not import `github.com/go-telegram/bot` directly.
|
||||
|
||||
Preferred naming:
|
||||
|
||||
- Import framework package as `tgbot` inside project package `internal/bot` to avoid `bot.Bot` naming conflicts.
|
||||
- Import models as `tgmodels`.
|
||||
- If refactoring internal command runtime, prefer `type App struct` over another `type Bot struct`.
|
||||
|
||||
## Related Code Files
|
||||
|
||||
| Action | File | Notes |
|
||||
|---|---|---|
|
||||
| Modify | `go.mod` / `go.sum` | Add `github.com/go-telegram/bot@v1.21.0` |
|
||||
| Modify | `cmd/openai-status-bot/main.go` | Prepare framework construction and lifecycle wiring |
|
||||
| Modify | `internal/bot/bot.go` | Plan rename/split from polling loop to app handler container |
|
||||
| Modify | `internal/telegram/client.go` | Prepare for deletion or shrink to sender/error adapter |
|
||||
| Reference | `plans/reports/260626-1602-go-telegram-framework-research.md` | Prior library comparison |
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Run `go get github.com/go-telegram/bot@v1.21.0`.
|
||||
2. Confirm `go mod tidy` does not introduce unexpected third-party dependencies.
|
||||
3. Add local package aliases in planned files: `tgbot "github.com/go-telegram/bot"` and `tgmodels "github.com/go-telegram/bot/models"`.
|
||||
4. Define the target startup sequence in `main.go` before code movement:
|
||||
- load config/logger/context/health/Mongo;
|
||||
- build status client/store;
|
||||
- load stored Telegram offset if retained;
|
||||
- create framework bot with options;
|
||||
- delete webhook, set commands, resolve username;
|
||||
- create `internal/bot.App`;
|
||||
- register handlers;
|
||||
- create `internal/telegram.Sender` for poller notifications;
|
||||
- start status poller goroutine;
|
||||
- mark readiness;
|
||||
- call `tg.Start(ctx)`.
|
||||
5. Decide initial file boundaries:
|
||||
- `internal/bot/app.go` for app struct/registration;
|
||||
- `internal/bot/message_context.go` or helper functions for model conversion;
|
||||
- `internal/telegram/sender.go` for poller notification sending;
|
||||
- `internal/telegram/errors.go` for terminal error classification.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [x] `go.mod` contains `github.com/go-telegram/bot v1.21.0`.
|
||||
- [x] Plan-confirmed architecture avoids importing framework types into `internal/poller`.
|
||||
- [x] Startup sequence is documented in code comments only where non-obvious.
|
||||
- [x] No source file grows past 200 lines without a modularization check.
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
- Risk: package name collision between project `internal/bot` and framework `bot`.
|
||||
Mitigation: always alias framework import as `tgbot`.
|
||||
- Risk: framework debug/error logging leaks token-bearing URLs.
|
||||
Mitigation: do not enable `WithDebug`; route framework errors through a redacting slog handler.
|
||||
- Risk: migration becomes a broad rewrite.
|
||||
Mitigation: keep OpenAI status formatting, Mongo store, and poller event logic unchanged.
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
---
|
||||
phase: 2
|
||||
title: "Command handler migration"
|
||||
status: completed
|
||||
priority: P1
|
||||
dependencies: [1]
|
||||
---
|
||||
|
||||
# Phase 2: Command handler migration
|
||||
|
||||
## Overview
|
||||
|
||||
Move command processing from the custom `GetUpdates` loop to `go-telegram/bot` handlers while preserving command behavior and testability.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Functional: every existing command keeps output and storage behavior.
|
||||
- Functional: commands with another bot username remain ignored.
|
||||
- Functional: topic-specific subscriptions still use `message_thread_id`.
|
||||
- Non-functional: command logic remains unit-testable without live Telegram HTTP calls.
|
||||
|
||||
## Architecture
|
||||
|
||||
Recommended shape:
|
||||
|
||||
```go
|
||||
type App struct {
|
||||
sender ReplySender
|
||||
statusClient StatusClient
|
||||
store Store
|
||||
logger *slog.Logger
|
||||
username string
|
||||
}
|
||||
|
||||
func (a *App) RegisterHandlers(tg *tgbot.Bot)
|
||||
func (a *App) HandleUpdate(ctx context.Context, tg *tgbot.Bot, update *tgmodels.Update)
|
||||
```
|
||||
|
||||
Use one framework default handler or `RegisterHandlerMatchFunc` for message commands, then reuse `normalizeCommand` for consistent `/cmd@BotName` behavior. Direct framework `MatchTypeCommandStartOnly` alone does not normalize own-bot suffixes; if using per-command registration, explicitly test `/start@<username>`.
|
||||
|
||||
Use a small local message context instead of threading framework models everywhere:
|
||||
|
||||
```go
|
||||
type MessageContext struct {
|
||||
ChatID int64
|
||||
ThreadID *int
|
||||
Text string
|
||||
}
|
||||
```
|
||||
|
||||
Existing command methods can then migrate from `telegram.Message` to `MessageContext`.
|
||||
|
||||
## Related Code Files
|
||||
|
||||
| Action | File | Notes |
|
||||
|---|---|---|
|
||||
| Modify | `internal/bot/bot.go` | Remove manual `Run` loop; introduce app handler entrypoint |
|
||||
| Modify | `internal/bot/helpers.go` | Change reply helpers from custom message type to local context |
|
||||
| Modify | `internal/bot/commands.go` | Change command methods from custom message type to local context |
|
||||
| Modify | `internal/bot/menu_commands.go` | Return `[]tgmodels.BotCommand` or build menu in `main.go` |
|
||||
| Modify | `internal/bot/bot_test.go` | Replace fake `GetUpdates` tests with handler/dispatch tests |
|
||||
| Delete or shrink | `internal/telegram/types.go` | Custom `Update`, `Message`, `Chat`, `User`, `BotCommand` should disappear if unused |
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Create `App` and `MessageContext` types.
|
||||
2. Add helper conversion:
|
||||
- ignore nil update/message;
|
||||
- ignore empty/non-command text;
|
||||
- map `message.Chat.ID`;
|
||||
- map `message.MessageThreadID == 0` to `nil`, otherwise copy int pointer.
|
||||
3. Move `handleMessage` into `App.HandleUpdate` or split into:
|
||||
- `handleUpdate(ctx, update)`;
|
||||
- `handleCommand(ctx, msgCtx)`.
|
||||
4. Keep `normalizeCommand(text, username)` unchanged unless tests require framework model-specific adjustments.
|
||||
5. Convert `reply`, `subscribe`, `unsubscribe`, `replyStatus`, `replySubscribe`, `replyHistory`, `replyUptime`, and `replyInfo` to accept `MessageContext`.
|
||||
6. Register handlers in `main.go` after username is known:
|
||||
- safest path: `WithDefaultHandler(app.HandleUpdate)` or `RegisterHandlerMatchFunc(commandMessage, app.HandleUpdate)`;
|
||||
- use `WithNotAsyncHandlers` and `WithWorkers(1)` to preserve simple sequential command behavior.
|
||||
7. Remove `TelegramClient.GetUpdates` dependency from `internal/bot`.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [x] `internal/bot` no longer calls `GetUpdates`.
|
||||
- [x] `/start@OtherBot` remains ignored.
|
||||
- [x] `/start@OpenAIStatusBot` works when username is configured.
|
||||
- [x] Topic replies send to the original `message_thread_id`.
|
||||
- [x] Existing command tests still assert core behavior, but they use framework update models or local `MessageContext`.
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
- Risk: losing own-bot command suffix behavior.
|
||||
Mitigation: keep `normalizeCommand`; add tests for own-bot and other-bot suffixes.
|
||||
- Risk: framework async handler default introduces races.
|
||||
Mitigation: configure `WithNotAsyncHandlers()` and `WithWorkers(1)`.
|
||||
- Risk: tests become HTTP-heavy.
|
||||
Mitigation: keep command logic behind local sender interface; unit-test without framework network calls.
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
---
|
||||
phase: 3
|
||||
title: "Notification sender migration"
|
||||
status: completed
|
||||
priority: P1
|
||||
dependencies: [1]
|
||||
---
|
||||
|
||||
# Phase 3: Notification sender migration
|
||||
|
||||
## Overview
|
||||
|
||||
Replace custom notification sends with a `go-telegram/bot` backed sender while preserving poller retry isolation and terminal subscriber cleanup.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Functional: `poller.Runner` can still call `SendMessage(ctx, subscriber, text) error`.
|
||||
- Functional: HTML parse mode and disabled link previews remain.
|
||||
- Functional: `message_thread_id` remains for topic subscribers.
|
||||
- Functional: terminal send errors still remove unreachable subscribers.
|
||||
- Non-functional: `internal/poller` must stay independent from framework types.
|
||||
|
||||
## Architecture
|
||||
|
||||
Keep a local adapter package:
|
||||
|
||||
```go
|
||||
package telegram
|
||||
|
||||
type Sender struct {
|
||||
bot *tgbot.Bot
|
||||
}
|
||||
|
||||
func (s *Sender) SendMessage(ctx context.Context, sub mongostore.Subscriber, text string) error
|
||||
func IsTerminalSendError(err error) bool
|
||||
```
|
||||
|
||||
Map framework/API errors into a local `APIError` shape if needed. Preserve the existing semantic contract used by `internal/poller/delivery.go`: 403 is terminal; selected 400 descriptions are terminal; parse/entity errors are retryable.
|
||||
|
||||
## Related Code Files
|
||||
|
||||
| Action | File | Notes |
|
||||
|---|---|---|
|
||||
| Modify/create | `internal/telegram/sender.go` | Framework-backed poller sender |
|
||||
| Modify/create | `internal/telegram/errors.go` | API error mapping and terminal classification |
|
||||
| Modify | `internal/poller/delivery.go` | Should require little or no change |
|
||||
| Modify | `cmd/openai-status-bot/main.go` | Pass sender adapter to `poller.NewRunner` |
|
||||
| Modify | `internal/poller/poller_test.go` | Keep fake notifier tests working |
|
||||
| Modify/delete | `internal/telegram/client_test.go` | Replace HTTP-client tests with sender/error tests |
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Create `telegram.Sender` around `*tgbot.Bot`.
|
||||
2. Implement send params:
|
||||
- `ChatID: subscriber.ChatID`;
|
||||
- `Text: text`;
|
||||
- `ParseMode: models.ParseModeHTML`;
|
||||
- `LinkPreviewOptions: &models.LinkPreviewOptions{IsDisabled: true}`;
|
||||
- `MessageThreadID: *subscriber.ThreadID` when non-nil.
|
||||
3. Preserve a local `APIError` type if framework errors do not expose the exact fields poller needs.
|
||||
4. Implement error redaction for logs:
|
||||
- no debug request logging;
|
||||
- framework errors passed through `redactToken(err, token)` before logging where token may appear.
|
||||
5. Keep `telegram.IsTerminalSendError` signature unchanged so `poller` does not care about framework internals.
|
||||
6. Update `main.go` to instantiate one framework bot and one notification sender from it.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [x] Poller notification tests still prove per-subscriber retry behavior.
|
||||
- [x] Terminal 403 and terminal 400 messages still remove unreachable subscribers.
|
||||
- [x] Non-terminal 400 parse errors remain retryable.
|
||||
- [x] Topic notification sends include `message_thread_id`.
|
||||
- [x] `internal/poller` imports no `github.com/go-telegram/bot` package.
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
- Risk: framework error type does not include HTTP status and Telegram `error_code` in the same shape.
|
||||
Mitigation: inspect returned errors and map by `errors.As` or message fallback with tests.
|
||||
- Risk: link preview field changed from deprecated `disable_web_page_preview` to `link_preview_options`.
|
||||
Mitigation: test the framework params object, not raw JSON field, and verify Telegram docs support disabled previews.
|
||||
- Risk: bot token appears in transport errors.
|
||||
Mitigation: carry the token into adapter only for redaction; never log raw framework debug output.
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
---
|
||||
phase: 4
|
||||
title: "Offset policy and cleanup"
|
||||
status: completed
|
||||
priority: P2
|
||||
dependencies: [2, 3]
|
||||
---
|
||||
|
||||
# Phase 4: Offset policy and cleanup
|
||||
|
||||
## Overview
|
||||
|
||||
Adapt Mongo `telegramOffset` to the framework polling model and remove custom client leftovers after command and notification paths are migrated.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Functional: restarts should not intentionally replay old command updates when a stored offset exists.
|
||||
- Functional: the app must tolerate missing/zero offset on fresh databases.
|
||||
- Non-functional: document the weaker offset guarantee introduced by framework-owned polling.
|
||||
|
||||
## Architecture
|
||||
|
||||
`go-telegram/bot` stores `lastUpdateID` internally and calls `getUpdates` with `lastUpdateID + 1`. Current Mongo `telegramOffset` stores the next update ID to fetch. Therefore:
|
||||
|
||||
- if stored offset > 0, configure `WithInitialOffset(storedOffset - 1)`;
|
||||
- if stored offset == 0, omit `WithInitialOffset`;
|
||||
- after handling an update, optionally call `SaveTelegramOffset(update.ID + 1)` as a restart seed;
|
||||
- do not rely on Mongo offset as a strict Telegram confirmation boundary.
|
||||
|
||||
Use low-buffer sequential options to reduce the gap between framework receiving updates and app handling them:
|
||||
|
||||
```go
|
||||
tgbot.WithWorkers(1)
|
||||
tgbot.WithUpdatesChannelCap(1)
|
||||
tgbot.WithNotAsyncHandlers()
|
||||
```
|
||||
|
||||
## Related Code Files
|
||||
|
||||
| Action | File | Notes |
|
||||
|---|---|---|
|
||||
| Modify | `cmd/openai-status-bot/main.go` | Load offset and configure `WithInitialOffset` |
|
||||
| Modify | `internal/bot/app.go` or `internal/bot/bot.go` | Save restart seed after handling update |
|
||||
| Modify | `internal/mongostore/checkpoint.go` | Keep offset methods unless a later decision removes them |
|
||||
| Modify | `docs/system-architecture.md` | Update runtime/failure behavior |
|
||||
| Delete | `internal/telegram/client.go` | Remove custom `postJSON`, `GetUpdates`, `DeleteWebhook`, `SetMyCommands`, `GetMe` once unused |
|
||||
| Delete | `internal/telegram/types.go` | Remove custom Telegram API model types once unused |
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Load `store.TelegramOffset(ctx)` in `main.go` before creating framework bot.
|
||||
2. Convert stored next-offset to framework initial last-update:
|
||||
- `initialLastUpdateID := offset - 1` when offset > 0;
|
||||
- no option when offset <= 0.
|
||||
3. Add `SaveTelegramOffset(update.ID + 1)` after command handler completion.
|
||||
4. Log offset-save failures as warnings, same as current code.
|
||||
5. Remove old `Bot.Run` manual polling path and any unused fake `GetUpdates` interfaces.
|
||||
6. Remove `internal/telegram/client.go` and `types.go` only after sender/error replacements compile.
|
||||
7. Re-run `rg "GetUpdates|telegram.Update|telegram.Message|telegram.BotCommand|telegram.User" internal cmd` and remove stale references.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [x] Fresh DB starts without offset errors.
|
||||
- [x] Existing DB with `telegramOffset=N` starts framework polling from update `N`.
|
||||
- [x] Command handler saves `update.ID + 1` as restart seed after handling.
|
||||
- [x] No custom HTTP Telegram API client remains.
|
||||
- [x] Architecture docs describe framework-owned polling and restart-seed offset semantics.
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
- Risk: exact post-handler Telegram confirmation is no longer guaranteed.
|
||||
Mitigation: document trade-off; use sequential/low-buffer framework options; keep offset as restart seed.
|
||||
- Risk: off-by-one offset skip/replay.
|
||||
Mitigation: add tests for offset 0, offset 1, and offset N conversion to `WithInitialOffset`.
|
||||
- Risk: unused Mongo offset state becomes misleading.
|
||||
Mitigation: rename docs/comments to "restart seed"; consider later migration removing `telegramOffset` if not useful.
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
---
|
||||
phase: 5
|
||||
title: "Test and documentation update"
|
||||
status: completed
|
||||
priority: P1
|
||||
dependencies: [4]
|
||||
---
|
||||
|
||||
# Phase 5: Test and documentation update
|
||||
|
||||
## Overview
|
||||
|
||||
Lock the migration down with focused tests, full test execution, and documentation updates for the new runtime model.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Functional: existing bot commands and notification behavior remain equivalent.
|
||||
- Non-functional: tests should protect the new framework boundary without requiring a live Telegram token.
|
||||
|
||||
## Architecture
|
||||
|
||||
Test the app at three levels:
|
||||
|
||||
1. Command unit tests for `internal/bot` using local message contexts or framework `models.Update` with fake sender/store/status clients.
|
||||
2. Telegram sender/error tests for `internal/telegram` using framework params, fake API caller where possible, or local error mapping tests.
|
||||
3. Startup/compile verification through package tests and `go test ./...`.
|
||||
|
||||
Do not add live Telegram network tests.
|
||||
|
||||
## Related Code Files
|
||||
|
||||
| Action | File | Notes |
|
||||
|---|---|---|
|
||||
| Modify | `internal/bot/bot_test.go` | Replace manual polling tests with handler dispatch tests |
|
||||
| Modify | `internal/telegram/client_test.go` | Convert to sender/error/param tests or replace with new test files |
|
||||
| Modify | `internal/poller/poller_test.go` | Keep fake notifier behavior; update imports if error type moves |
|
||||
| Modify | `README.md` | Update dependency/runtime note only if user-visible setup changes |
|
||||
| Modify | `docs/system-architecture.md` | Required: framework polling, restart-seed offset |
|
||||
| Modify | `docs/setup-guide.md` | Update only if commands/setup text references custom long polling details |
|
||||
| Modify | `plans/reports/260626-1602-go-telegram-framework-research.md` | Optional: add note that user chose framework-style migration |
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Add command dispatch tests:
|
||||
- `/start` subscribes chat;
|
||||
- `/start` in topic subscribes `chatID:threadID`;
|
||||
- `/start@OtherBot` ignored;
|
||||
- `/start@OpenAIStatusBot` accepted;
|
||||
- unknown command replies with help hint;
|
||||
- non-command text ignored.
|
||||
2. Add reply/send tests:
|
||||
- HTML parse mode set;
|
||||
- link preview disabled;
|
||||
- topic reply includes `MessageThreadID`;
|
||||
- send errors are logged but do not panic command handlers.
|
||||
3. Add terminal error tests:
|
||||
- 403 terminal;
|
||||
- 400 chat/thread missing terminal;
|
||||
- 400 parse entities non-terminal.
|
||||
4. Add offset conversion tests:
|
||||
- no stored offset => no initial offset option;
|
||||
- stored offset 1 => initial last update 0;
|
||||
- stored offset N => initial last update N-1.
|
||||
5. Run focused tests:
|
||||
- `go test ./internal/bot ./internal/telegram ./internal/poller`
|
||||
6. Run broad tests:
|
||||
- `go test ./...`
|
||||
7. Run integration tests only when Docker is available and needed:
|
||||
- `go test -tags=integration ./internal/mongostore/...`
|
||||
8. Update docs after behavior is verified.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [x] Focused bot/telegram/poller tests pass.
|
||||
- [x] `go test ./...` passes.
|
||||
- [x] Docs mention `go-telegram/bot` runtime and offset restart-seed semantics.
|
||||
- [x] README remains accurate for local and Docker startup.
|
||||
- [x] No stale custom Telegram client symbols remain under `internal` or `cmd`.
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
- Risk: tests overfit framework internals.
|
||||
Mitigation: assert project-visible behavior and adapter params, not private framework state.
|
||||
- Risk: docs over-explain internals to end users.
|
||||
Mitigation: README stays user-focused; detailed offset semantics go in `docs/system-architecture.md`.
|
||||
- Risk: integration tests fail due Docker absence.
|
||||
Mitigation: keep Mongo integration tests behind existing build tag and report if not run.
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
---
|
||||
title: "Switch Telegram layer to go-telegram/bot"
|
||||
description: "Replace the custom Telegram HTTP client and command polling loop with the standard go-telegram/bot framework while preserving subscriptions, topic replies, notification delivery, and startup behavior."
|
||||
status: completed
|
||||
priority: P2
|
||||
branch: "develop"
|
||||
tags: [refactor, backend, telegram]
|
||||
blockedBy: []
|
||||
blocks: []
|
||||
created: "2026-06-26"
|
||||
createdBy: "ck:plan"
|
||||
source: skill
|
||||
---
|
||||
|
||||
# Switch Telegram layer to go-telegram/bot
|
||||
|
||||
## Overview
|
||||
|
||||
Migrate the Telegram integration from the custom `internal/telegram` HTTP client to `github.com/go-telegram/bot` (`v1.21.0`, Bot API 10.0, zero third-party deps). This is a framework-style refactor: `go-telegram/bot` owns long polling and update dispatch; app code owns command behavior, subscriber state, and notification delivery.
|
||||
|
||||
The accepted trade-off: Mongo `telegramOffset` stops being a strict post-handler confirmation checkpoint. It remains a restart seed when present, and command handlers can still save the latest handled update ID for replay reduction. This is acceptable because the user prioritized a good standard library and accepted larger changes.
|
||||
|
||||
## Scope Challenge
|
||||
|
||||
- Existing code: command parsing/replies are in `internal/bot`; delivery fan-out and terminal subscriber cleanup are in `internal/poller`; Telegram HTTP calls/errors are isolated in `internal/telegram`.
|
||||
- Minimum changes: add `go-telegram/bot`, refactor startup wiring, convert command handlers to framework update handling, replace notification sender, update tests/docs.
|
||||
- Complexity: expected >8 files touched. Justified because the current custom client, command loop, model types, tests, and architecture docs all encode Telegram transport assumptions.
|
||||
- Selected mode: HOLD SCOPE. No webhook migration, no interactive keyboards, no callback queries, no feature expansion.
|
||||
|
||||
## Architecture Decision
|
||||
|
||||
Use `go-telegram/bot` as the runtime and handler framework. Keep project-specific behavior behind local packages:
|
||||
|
||||
```text
|
||||
cmd/openai-status-bot/main.go
|
||||
-> creates *tgbot.Bot with options
|
||||
-> deleteWebhook, setMyCommands, getMe
|
||||
-> internal/bot.App registers command handlers
|
||||
-> internal/telegram.Sender sends poller notifications
|
||||
|
||||
internal/bot
|
||||
-> owns command dispatch and subscription business logic
|
||||
-> consumes go-telegram/bot models at the edge
|
||||
|
||||
internal/telegram
|
||||
-> owns poller notification sender and terminal-error classification
|
||||
-> wraps framework errors so poller logic stays stable
|
||||
|
||||
internal/poller
|
||||
-> keeps Notifier interface unchanged
|
||||
```
|
||||
|
||||
Runtime options should favor predictable command handling:
|
||||
|
||||
- `bot.WithAllowedUpdates(bot.AllowedUpdates{"message"})`
|
||||
- `bot.WithInitialOffset(storedOffset-1)` only when stored offset > 0
|
||||
- `bot.WithWorkers(1)`
|
||||
- `bot.WithUpdatesChannelCap(1)`
|
||||
- `bot.WithNotAsyncHandlers()`
|
||||
- custom `WithErrorsHandler` / no debug logging with token redaction
|
||||
|
||||
## Cross-Plan Dependencies
|
||||
|
||||
None. The prior Redis-to-Mongo plan is complete and does not block this work.
|
||||
|
||||
## Phases
|
||||
|
||||
| Phase | Name | Status |
|
||||
|-------|------|--------|
|
||||
| 1 | [Dependency and architecture setup](./phase-01-dependency-and-architecture-setup.md) | Completed |
|
||||
| 2 | [Command handler migration](./phase-02-command-handler-migration.md) | Completed |
|
||||
| 3 | [Notification sender migration](./phase-03-notification-sender-migration.md) | Completed |
|
||||
| 4 | [Offset policy and cleanup](./phase-04-offset-policy-and-cleanup.md) | Completed |
|
||||
| 5 | [Test and documentation update](./phase-05-test-and-documentation-update.md) | Completed |
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] Bot builds with `github.com/go-telegram/bot@v1.21.0`.
|
||||
- [x] Custom Telegram `getUpdates` loop is removed from app code.
|
||||
- [x] `/start`, `/stop`, `/status`, `/components`, `/subscribe`, `/history`, `/uptime`, `/info`, `/help`, and unknown-command behavior remain equivalent.
|
||||
- [x] Supergroup topic support via `message_thread_id` remains for replies and notifications.
|
||||
- [x] Poller delivery retry behavior and terminal subscriber cleanup remain equivalent.
|
||||
- [x] Startup still deletes webhook with `drop_pending_updates=false`, registers commands, starts health endpoint, starts OpenAI poller, then starts Telegram long polling.
|
||||
- [x] Tests cover framework handler dispatch, topic replies, notification sender params, terminal errors, offset seed behavior, and command menu registration.
|
||||
- [x] `go test ./...` passes; integration tests remain gated behind `-tags=integration`.
|
||||
|
||||
## Not In Scope
|
||||
|
||||
- Webhook mode.
|
||||
- New bot commands or UI features.
|
||||
- Inline keyboards, callback queries, payments, web apps.
|
||||
- Replacing MongoDB or changing subscription schema beyond optional offset cleanup.
|
||||
|
||||
## Research Inputs
|
||||
|
||||
- `plans/reports/260626-1602-go-telegram-framework-research.md`
|
||||
- `github.com/go-telegram/bot@v1.21.0` module metadata: released 2026-05-22, Go directive 1.18.
|
||||
- Local source confirmed support for `WithInitialOffset`, `WithAllowedUpdates`, `WithNotAsyncHandlers`, `SetMyCommands`, `DeleteWebhook`, `SendMessageParams.MessageThreadID`, and `LinkPreviewOptions`.
|
||||
|
||||
## Unresolved Questions
|
||||
|
||||
None.
|
||||
|
||||
@@ -1,286 +0,0 @@
|
||||
---
|
||||
type: research-report
|
||||
topic: go-telegram-framework-selection
|
||||
conducted_at: 2026-06-26 16:02 Asia/Saigon
|
||||
status: complete
|
||||
---
|
||||
|
||||
# Research Report: Go Telegram Framework For OpenAI Status Bot
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Recommendation: do not replace the current custom Telegram client unless we need faster Bot API feature coverage. Current code uses only five API calls and already has behavior this project cares about: explicit offset persistence after command handling, token redaction, typed API errors, request timeout control, `message_thread_id`, and narrow test fakes.
|
||||
|
||||
If we still apply a framework, pick `github.com/mymmrac/telego` behind this repo's existing `internal/telegram` interface. It exposes manual `GetUpdates(ctx, params)`, current Bot API types, `message_thread_id`, `setMyCommands`, `deleteWebhook`, and `sendMessage`. Avoid its `UpdatesViaLongPolling` helper because it advances offset internally before this app can persist offset after handling.
|
||||
|
||||
Do not pick `go-telegram-bot-api/v5` for this repo: latest module tag is from 2021 and its tagged source does not expose `message_thread_id`, so it regresses supergroup topic support. `go-telegram/bot` is current and zero-dependency, but its normal polling model owns offset progression internally, which mismatches this app's Mongo-backed offset contract. `telebot.v3` is handler-framework oriented and would force more rewrite than value. `gotgbot/v2` is capable but still release-candidate at latest.
|
||||
|
||||
## Research Methodology
|
||||
|
||||
- Sources consulted: 13 primary/local sources.
|
||||
- Date range: 2021-12-13 to 2026-06-14 module releases, checked on 2026-06-26.
|
||||
- Search terms: `Go Telegram Bot API framework`, `telego GetUpdates`, `go-telegram bot MessageThreadID`, `telegram-bot-api v5 message_thread_id`, `telebot.v3 polling`.
|
||||
- Source types: Telegram official Bot API docs, pkg.go.dev, Go module metadata via `go list`, downloaded module READMEs/source, current repo source.
|
||||
- Docs-seeker: checked; context7 had no docs for `go-telegram bot Telegram Bot API Go`, fallback used module sources.
|
||||
|
||||
## Table Of Contents
|
||||
|
||||
- [Project Fit](#project-fit)
|
||||
- [Key Findings](#key-findings)
|
||||
- [Comparative Analysis](#comparative-analysis)
|
||||
- [Recommendation](#recommendation)
|
||||
- [Implementation Notes](#implementation-notes)
|
||||
- [Resources](#resources)
|
||||
- [Unresolved Questions](#unresolved-questions)
|
||||
|
||||
## Project Fit
|
||||
|
||||
Current Telegram surface:
|
||||
|
||||
- `deleteWebhook(drop_pending_updates=false)`
|
||||
- `getMe`
|
||||
- `setMyCommands`
|
||||
- `getUpdates(offset, timeout, allowed_updates=["message"])`
|
||||
- `sendMessage(chat_id, text, parse_mode=HTML, disable preview, optional message_thread_id)`
|
||||
|
||||
Important local constraints:
|
||||
|
||||
- `internal/bot.Bot.Run` loads Telegram offset from MongoDB.
|
||||
- It handles each update, then saves `update_id + 1`.
|
||||
- This creates a conservative delivery contract: do not confirm future offset before app state is handled.
|
||||
- Existing client redacts bot token from transport errors.
|
||||
- `IsTerminalSendError` maps 403 and selected 400 descriptions to subscriber cleanup.
|
||||
|
||||
## Key Findings
|
||||
|
||||
### 1. Technology Overview
|
||||
|
||||
Telegram Bot API works fine with a thin HTTP wrapper. A framework adds value when the bot needs rich handlers, callback routing, media upload helpers, payments, inline mode, web apps, or faster coverage of new Bot API fields.
|
||||
|
||||
This project is not there yet. It is a polling status-notification bot with simple text commands. Most complexity is OpenAI-status dedupe, Mongo state, and delivery semantics, not Telegram routing.
|
||||
|
||||
### 2. Current State And Trends
|
||||
|
||||
Module metadata checked with `go list -m -json <module>@latest`:
|
||||
|
||||
| Module | Latest | Release time | Go directive | Fit |
|
||||
|---|---:|---|---:|---|
|
||||
| `github.com/mymmrac/telego` | `v1.10.0` | 2026-06-14 | `1.25.7` | Best external fit |
|
||||
| `github.com/go-telegram/bot` | `v1.21.0` | 2026-05-22 | `1.18` | Good library, poorer offset fit |
|
||||
| `github.com/PaulSonOfLars/gotgbot/v2` | `v2.0.0-rc.35` | 2026-05-25 | `1.24` | Current but pre-release |
|
||||
| `gopkg.in/telebot.v3` | `v3.3.8` stable, `v3.4.2-beta` exists | 2024-08-06 stable | `1.16` | Framework-heavy |
|
||||
| `github.com/go-telegram-bot-api/telegram-bot-api/v5` | `v5.5.1` | 2021-12-13 | `1.16` | Not suitable for topics |
|
||||
|
||||
`go-telegram/bot` README says it supports Bot API 10.0 and is zero-dependency. Good signal. But its exported API is handler/poller oriented.
|
||||
|
||||
`telego` is generated/current and exposes manual `GetUpdates`. Bad signal: dependency cost is materially larger (`fasthttp`, custom JSON libs, Sonic, etc.) and its module says Go `1.25.7`, while this repo says Go `1.25.0`.
|
||||
|
||||
### 3. Best Practices
|
||||
|
||||
- Keep this repo's `internal/telegram` boundary. Do not let a framework leak into `internal/bot` or `internal/poller`.
|
||||
- Preserve explicit offset persistence. Use manual `GetUpdates`, not framework-owned long polling.
|
||||
- Preserve token redaction tests.
|
||||
- Preserve terminal send error classification. If using telego, map `errors.As(err, *telegoapi.Error)` to existing `APIError` or update `IsTerminalSendError`.
|
||||
- Keep `allowed_updates=["message"]`.
|
||||
- Keep `deleteWebhook(drop_pending_updates=false)` on startup.
|
||||
- Use context deadlines per request. For long polling, deadline should be `timeoutSeconds + HTTP_TIMEOUT`, not just `HTTP_TIMEOUT`.
|
||||
|
||||
### 4. Security Considerations
|
||||
|
||||
- Bot token must never enter logs. Current client explicitly redacts URL-bearing transport errors.
|
||||
- Framework default loggers can log request failures. Disable or replace framework logger.
|
||||
- Do not enable debug logging around Telegram requests in production.
|
||||
- Keep command parsing defensive. Do not trust message text, chat type, or thread ID.
|
||||
- Avoid webhook mode unless deploying with HTTPS, secret token validation, and replay-safe handler.
|
||||
|
||||
### 5. Performance Insights
|
||||
|
||||
- Traffic is tiny. Performance should not drive selection.
|
||||
- `telego` and `gotgbot` are more complete than needed.
|
||||
- Long polling dominates latency. Any library using `getUpdates` is good enough.
|
||||
- Dependency count matters more than raw throughput for this app.
|
||||
|
||||
## Comparative Analysis
|
||||
|
||||
| Option | Pros | Cons | Verdict |
|
||||
|---|---|---|---|
|
||||
| Keep custom client | Exact behavior, no new deps, easy fakes, offset contract already right | Manual maintenance for new Bot API fields | Best default |
|
||||
| `mymmrac/telego` | Current, manual `GetUpdates`, topics, typed methods, error type | Heavier deps, Go `1.25.7`, migration still needed | Best if adopting |
|
||||
| `go-telegram/bot` | Current, zero deps, Bot API 10.0, nice handlers | Polling owns offset, handler model duplicates app command dispatch | Good only if accepting offset model change |
|
||||
| `gotgbot/v2` | Generated, current, zero third-party deps, manual methods | Latest is RC, generated API can be verbose | Watch, not first choice |
|
||||
| `telebot.v3` | Mature handler framework, topic support | Framework rewrite, stable tag older, unnecessary features | Not for this bot |
|
||||
| `telegram-bot-api/v5` | Simple wrapper, classic package | Latest tag old, no topic field in tagged source | Reject |
|
||||
|
||||
## Recommendation
|
||||
|
||||
### Decision
|
||||
|
||||
Keep the custom client now. If user explicitly wants a framework migration, use `telego` only inside `internal/telegram`.
|
||||
|
||||
### Why
|
||||
|
||||
The app's actual Telegram need is tiny. Migrating only pays off if maintaining Bot API models becomes painful. The current code is closer to the desired operational semantics than most frameworks.
|
||||
|
||||
### Acceptance Criteria For A Telego Migration
|
||||
|
||||
- `go test ./...` passes.
|
||||
- `internal/bot` and `internal/poller` interfaces do not import `telego`.
|
||||
- Existing Telegram client tests still cover:
|
||||
- `deleteWebhook` keeps pending updates.
|
||||
- `message_thread_id` included for topic subscriptions.
|
||||
- token redaction.
|
||||
- terminal send errors.
|
||||
- long poll request timeout is `telegram timeout + HTTP_TIMEOUT`.
|
||||
- Offset saved only after `handleMessage` completes.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### Quick Start
|
||||
|
||||
```bash
|
||||
go get github.com/mymmrac/telego@v1.10.0
|
||||
```
|
||||
|
||||
Then rewrite only `internal/telegram/client.go` and `internal/telegram/types.go` conversion helpers.
|
||||
|
||||
### Adapter Sketch
|
||||
|
||||
```go
|
||||
package telegram
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/mymmrac/telego"
|
||||
"github.com/mymmrac/telego/telegoapi"
|
||||
"github.com/mymmrac/telego/telegoutil"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
bot *telego.Bot
|
||||
requestTimeout time.Duration
|
||||
}
|
||||
|
||||
func NewClient(token string, timeout time.Duration) (*Client, error) {
|
||||
b, err := telego.NewBot(
|
||||
token,
|
||||
telego.WithHTTPClient(&http.Client{}),
|
||||
telego.WithDefaultLogger(false, false),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Client{bot: b, requestTimeout: timeout}, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetUpdates(ctx context.Context, offset int64, timeoutSeconds int) ([]Update, error) {
|
||||
reqCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second+c.requestTimeout)
|
||||
defer cancel()
|
||||
|
||||
updates, err := c.bot.GetUpdates(reqCtx, &telego.GetUpdatesParams{
|
||||
Offset: int(offset),
|
||||
Timeout: timeoutSeconds,
|
||||
AllowedUpdates: []string{telego.MessageUpdates},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, mapTelegoError(err)
|
||||
}
|
||||
return convertUpdates(updates), nil
|
||||
}
|
||||
|
||||
func (c *Client) SendText(ctx context.Context, chatID int64, threadID *int, text string) error {
|
||||
reqCtx, cancel := context.WithTimeout(ctx, c.requestTimeout)
|
||||
defer cancel()
|
||||
|
||||
params := &telego.SendMessageParams{
|
||||
ChatID: telegoutil.ID(chatID),
|
||||
Text: text,
|
||||
ParseMode: telego.ModeHTML,
|
||||
LinkPreviewOptions: &telego.LinkPreviewOptions{IsDisabled: true},
|
||||
}
|
||||
if threadID != nil {
|
||||
params.MessageThreadID = *threadID
|
||||
}
|
||||
|
||||
_, err := c.bot.SendMessage(reqCtx, params)
|
||||
return mapTelegoError(err)
|
||||
}
|
||||
|
||||
func mapTelegoError(err error) error {
|
||||
var apiErr *telegoapi.Error
|
||||
if errors.As(err, &apiErr) {
|
||||
return &APIError{ErrorCode: apiErr.ErrorCode, Description: apiErr.Description}
|
||||
}
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
This is intentionally a sketch. Validate exact conversion code in tests.
|
||||
|
||||
### Common Pitfalls
|
||||
|
||||
- Do not use `telego.UpdatesViaLongPolling`; it mutates offset internally before this repo persists offset.
|
||||
- Do not set `http.Client.Timeout` to `HTTP_TIMEOUT` globally; long polling needs longer.
|
||||
- Do not leak telego types outside `internal/telegram`.
|
||||
- Do not drop topic support while converting `MessageThreadID`.
|
||||
- Do not remove token-redaction coverage without replacing it.
|
||||
|
||||
## Resources
|
||||
|
||||
### Official Documentation
|
||||
|
||||
- Telegram Bot API: https://core.telegram.org/bots/api
|
||||
- Go package docs, telego: https://pkg.go.dev/github.com/mymmrac/telego
|
||||
- Go package docs, go-telegram/bot: https://pkg.go.dev/github.com/go-telegram/bot
|
||||
- Go package docs, gotgbot/v2: https://pkg.go.dev/github.com/PaulSonOfLars/gotgbot/v2
|
||||
- Go package docs, telebot.v3: https://pkg.go.dev/gopkg.in/telebot.v3
|
||||
- Go package docs, telegram-bot-api/v5: https://pkg.go.dev/github.com/go-telegram-bot-api/telegram-bot-api/v5
|
||||
|
||||
### Repository References
|
||||
|
||||
- telego: https://github.com/mymmrac/telego
|
||||
- go-telegram/bot: https://github.com/go-telegram/bot
|
||||
- gotgbot: https://github.com/PaulSonOfLars/gotgbot
|
||||
- telebot: https://github.com/tucnak/telebot
|
||||
- telegram-bot-api: https://github.com/go-telegram-bot-api/telegram-bot-api
|
||||
|
||||
### Local Evidence
|
||||
|
||||
- `internal/telegram/client.go`: current custom API wrapper.
|
||||
- `internal/bot/bot.go`: offset persistence after update handling.
|
||||
- `README.md`: topic support and long-polling startup behavior.
|
||||
- `go list -m -json <module>@latest`: current module versions above.
|
||||
|
||||
## Appendix A: Glossary
|
||||
|
||||
- Bot API: Telegram HTTPS API for bot accounts.
|
||||
- MTProto: Telegram client protocol. Not needed here.
|
||||
- Long polling: `getUpdates` request waits for new updates.
|
||||
- Offset: Telegram update checkpoint. Higher offset confirms older updates.
|
||||
- Topic: Supergroup forum thread, sent via `message_thread_id`.
|
||||
|
||||
## Appendix B: Version Compatibility Matrix
|
||||
|
||||
| Project constraint | Custom | telego | go-telegram/bot | gotgbot/v2 | telebot.v3 | telegram-bot-api/v5 |
|
||||
|---|---:|---:|---:|---:|---:|---:|
|
||||
| Go 1.25 project | yes | maybe needs 1.25.7 | yes | yes | yes | yes |
|
||||
| Manual `GetUpdates` | yes | yes | no public manual method found | yes | not primary path | yes |
|
||||
| Topic send support | yes | yes | yes | yes | yes | no in latest tag |
|
||||
| Low dependency footprint | yes | no | yes | yes | medium | yes |
|
||||
| Minimal migration | yes | medium | high | medium | high | medium |
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Keep current client unless a concrete Telegram API gap appears.
|
||||
2. If migrating, create a small telego adapter under `internal/telegram`.
|
||||
3. Run focused tests first: `go test ./internal/telegram ./internal/bot ./internal/poller`.
|
||||
4. Then run `go test ./...`.
|
||||
5. If Go toolchain upgrade appears due telego `go 1.25.7`, decide whether that is acceptable before merge.
|
||||
|
||||
## Unresolved Questions
|
||||
|
||||
- Is the user goal to reduce maintenance, or to add upcoming Telegram Bot API features?
|
||||
- Is Go toolchain auto-upgrade to 1.25.7 acceptable if telego requires it?
|
||||
@@ -1,31 +0,0 @@
|
||||
## Plan Complete: Switch Telegram layer to go-telegram/bot
|
||||
|
||||
### Summary
|
||||
- **Duration:** 2026-06-26 plan -> 2026-06-26 complete
|
||||
- **Phases:** 5/5 completed
|
||||
- **Status:** completed
|
||||
- **Branch:** develop
|
||||
- **Tests:** `go test ./...` pass
|
||||
|
||||
### Achievements
|
||||
- Replaced custom Telegram `getUpdates` loop with `github.com/go-telegram/bot@v1.21.0` runtime.
|
||||
- Kept command behavior in `internal/bot`; added framework update handler and `MessageContext` edge type.
|
||||
- Replaced custom Telegram HTTP client with `internal/telegram.Sender` adapter for replies and poller notifications.
|
||||
- Preserved topic replies via `message_thread_id`, HTML parse mode, disabled link preview, terminal subscriber cleanup.
|
||||
- Preserved startup flow: health endpoint, Mongo setup, offset seed, delete webhook, set commands, poller, Telegram long polling.
|
||||
- Updated `docs/system-architecture.md` for framework runtime and restart offset semantics.
|
||||
|
||||
### Validation
|
||||
| Check | Result |
|
||||
|-------|--------|
|
||||
| Full tests | pass: `go test ./...` |
|
||||
| Stale custom DTO search | pass: no `telegram.Update`, `telegram.Message`, `GetUpdates`, `NewClient` in app Telegram layer |
|
||||
| Plan sync | pass: all phases + acceptance criteria checked |
|
||||
| Docs impact | major: architecture doc updated |
|
||||
|
||||
### Known Limitations
|
||||
- No live Telegram token/network test added; framework integration covered by compile and adapter tests.
|
||||
- Webhook mode, keyboards, callbacks remain out of scope.
|
||||
|
||||
### Unresolved Questions
|
||||
None.
|
||||
Reference in new issue
Block a user