mirror of
https://github.com/tiennm99/tiennm99bot.git
synced 2026-10-17 06:15:18 +00:00
docs(plans): remove plan artifacts
This commit is contained in:
1 parent
aa74379f81
commit
8e748c3153
12 files changed
-1273
No files matched your search
@@ -1,86 +0,0 @@
|
||||
---
|
||||
phase: 1
|
||||
title: Implement command
|
||||
status: completed
|
||||
priority: P2
|
||||
dependencies: []
|
||||
---
|
||||
|
||||
# Phase 1: Implement command
|
||||
|
||||
## Overview
|
||||
|
||||
Add the `/wheelofnames` command inside `internal/modules/misc/misc.go`, using the current misc module pattern: small command factory returning `modules.Command`, `chathelper.ArgAfterCommand` for parsing, and `chathelper.Reply` for plain text response.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Functional: parse text after `/wheelofnames` as comma-separated options.
|
||||
- Functional: trim whitespace and drop empty options.
|
||||
- Functional: reply usage when no valid options exist.
|
||||
- Functional: randomly pick one option from valid entries; preserve duplicates as weighting.
|
||||
- Non-functional: keep implementation local to misc; no storage dependency; no new module.
|
||||
|
||||
## Architecture
|
||||
|
||||
Flow:
|
||||
|
||||
```text
|
||||
Telegram update
|
||||
-> dispatcher matches wheelofnames
|
||||
-> wheelOfNamesCommand handler
|
||||
-> chathelper.ArgAfterCommand
|
||||
-> splitWheelOptions
|
||||
-> math/rand/v2 rand.N(len(options))
|
||||
-> chathelper.Reply
|
||||
```
|
||||
|
||||
Use a tiny pure helper for parsing:
|
||||
|
||||
```go
|
||||
func splitWheelOptions(arg string) []string {
|
||||
parts := strings.Split(arg, ",")
|
||||
out := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
if s := strings.TrimSpace(part); s != "" {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
```
|
||||
|
||||
No crypto-grade randomness required; this is a casual selection command, not auth, payment, or security behavior.
|
||||
|
||||
## Related Code Files
|
||||
|
||||
- Modify: `/config/workspace/tiennm99/miti99bot/internal/modules/misc/misc.go`
|
||||
- Create: none
|
||||
- Delete: none
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Add `math/rand/v2` import, unless existing Go version/toolchain rejects it; fallback to standard `math/rand` only if needed.
|
||||
2. Update package comment to include `/wheelofnames`.
|
||||
3. Add `wheelOfNamesUsage` constant, e.g. `Usage: /wheelofnames <option1>, <option2>, ...`.
|
||||
4. Add `wheelOfNamesCommand()` returning public `modules.Command`.
|
||||
5. Add command to `New()` after other public misc commands or near `/ping`.
|
||||
6. Add `splitWheelOptions(arg string) []string` helper near the command.
|
||||
7. Handler:
|
||||
- return nil for nil message
|
||||
- parse args with `chathelper.ArgAfterCommand(update.Message.Text)`
|
||||
- reply usage if parsed options length is zero
|
||||
- pick `options[rand.N(len(options))]`
|
||||
- reply selected option as plain text
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [x] `misc.New` registers `wheelofnames` as public command.
|
||||
- [x] Handler replies usage for missing/empty option list.
|
||||
- [x] Handler never returns an empty option.
|
||||
- [x] Handler uses plain text reply, preserving forum topic behavior through `chathelper.Reply`.
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
Main risk: flaky tests if randomness is asserted too strictly. Mitigate by testing deterministic one-option input and set membership for multi-option input.
|
||||
|
||||
Security: no HTML parse mode; user-supplied option text is plain text, so no HTML escaping needed.
|
||||
@@ -1,54 +0,0 @@
|
||||
---
|
||||
phase: 2
|
||||
title: Update command surfaces
|
||||
status: completed
|
||||
priority: P2
|
||||
dependencies:
|
||||
- 1
|
||||
---
|
||||
|
||||
# Phase 2: Update command surfaces
|
||||
|
||||
## Overview
|
||||
|
||||
Update all user-facing command surfaces required by `AGENTS.md` for a new Telegram command. This is an additive command, so no stats migration is needed.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Functional: command appears in `/help` because it is public and registered in the misc module.
|
||||
- Functional: command appears in `telegram-commands.json` for Telegram command menu setup.
|
||||
- Functional: README misc module summary reflects the new command.
|
||||
- Non-functional: no command rename/delete, so preserve stats behavior by doing nothing special.
|
||||
|
||||
## Architecture
|
||||
|
||||
The source of runtime behavior is `internal/modules/misc/misc.go`. `telegram-commands.json` is a manual Telegram command menu source, and `README.md` is the human-facing module overview.
|
||||
|
||||
Stats compatibility note:
|
||||
- Adding `/wheelofnames` creates new command usage rows naturally when the stats hook records invocations.
|
||||
- No existing stats rows need migration because no command is renamed or deleted.
|
||||
|
||||
## Related Code Files
|
||||
|
||||
- Modify: `/config/workspace/tiennm99/miti99bot/telegram-commands.json`
|
||||
- Modify: `/config/workspace/tiennm99/miti99bot/README.md`
|
||||
- Create: none
|
||||
- Delete: none
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Add command entry to `telegram-commands.json` near other misc commands:
|
||||
- `command`: `wheelofnames`
|
||||
- `description`: concise public description, e.g. `Pick one random comma-separated option`
|
||||
2. Update README Modules table for `misc` to include `/wheelofnames`.
|
||||
3. Do not update stats migrations or system state; this is add-only behavior.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [x] `telegram-commands.json` remains valid JSON.
|
||||
- [x] README `misc` command list includes `/wheelofnames`.
|
||||
- [x] No migration or legacy stats marker added.
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
Risk: runtime registration and manual command menu source drift. Mitigate by updating both `misc.go` and `telegram-commands.json` in same implementation.
|
||||
@@ -1,68 +0,0 @@
|
||||
---
|
||||
phase: 3
|
||||
title: Validate behavior
|
||||
status: completed
|
||||
priority: P2
|
||||
dependencies:
|
||||
- 1
|
||||
- 2
|
||||
---
|
||||
|
||||
# Phase 3: Validate behavior
|
||||
|
||||
## Overview
|
||||
|
||||
Add focused tests for the new handler and run command-surface validation required for command changes.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Functional: tests cover registration, usage reply, one-option selection, trimming, and empty segment handling.
|
||||
- Non-functional: run focused tests first, then full suite and vet because command/menu/shared behavior changed.
|
||||
|
||||
## Architecture
|
||||
|
||||
Use existing misc test harness:
|
||||
- `installMisc` in `internal/modules/misc/handlers_test.go` wires the misc module to `testutil.RecordingBot`.
|
||||
- `TestNew_RegistersExpectedCommands` in `internal/modules/misc/misc_test.go` locks command names and visibility.
|
||||
|
||||
Avoid brittle randomness assertions:
|
||||
- Single-option input should return that option exactly.
|
||||
- Multi-option input should assert the reply is one of the non-empty trimmed options.
|
||||
|
||||
## Related Code Files
|
||||
|
||||
- Modify: `/config/workspace/tiennm99/miti99bot/internal/modules/misc/misc_test.go`
|
||||
- Modify: `/config/workspace/tiennm99/miti99bot/internal/modules/misc/handlers_test.go`
|
||||
- Create: none
|
||||
- Delete: none
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Update `TestNew_RegistersExpectedCommands` expected map:
|
||||
- add `wheelofnames: modules.VisibilityPublic`
|
||||
2. Add handler tests:
|
||||
- `/wheelofnames` returns usage.
|
||||
- `/wheelofnames , ,` returns usage.
|
||||
- `/wheelofnames Alice` returns `Alice`.
|
||||
- `/wheelofnames Alice, Bob, Carol` returns one of those exact strings.
|
||||
- `/wheelofnames , Alice , , Bob ,` returns `Alice` or `Bob`, never empty/spaced.
|
||||
3. Run formatting and focused tests:
|
||||
- `gofmt -w internal/modules/misc/misc.go internal/modules/misc/misc_test.go internal/modules/misc/handlers_test.go`
|
||||
- `go test ./internal/modules/misc`
|
||||
4. Run broader validation because command menu/user-facing contract changes:
|
||||
- `go test ./cmd/server ./internal/modules/misc`
|
||||
- `go test ./...`
|
||||
- `go vet ./...`
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [x] Focused misc tests pass.
|
||||
- [x] Command menu/server tests pass.
|
||||
- [x] Full `go test ./...` passes.
|
||||
- [x] Full `go vet ./...` passes.
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
Risk: random selection makes tests nondeterministic. Mitigate by avoiding exact expected value for multi-option tests.
|
||||
|
||||
Rollback: remove `wheelOfNamesCommand`, helper, tests, README entry, and `telegram-commands.json` entry.
|
||||
@@ -1,63 +0,0 @@
|
||||
---
|
||||
title: Wheel of Names Misc Command
|
||||
description: >-
|
||||
Add public /wheelofnames command to pick one random comma-separated option in
|
||||
the misc module.
|
||||
status: completed
|
||||
priority: P2
|
||||
branch: main
|
||||
tags:
|
||||
- feature
|
||||
- backend
|
||||
blockedBy: []
|
||||
blocks: []
|
||||
created: '2026-07-03T04:04:35.848Z'
|
||||
createdBy: 'ck:plan'
|
||||
source: skill
|
||||
---
|
||||
|
||||
# Wheel of Names Misc Command
|
||||
|
||||
## Overview
|
||||
|
||||
Add `/wheelofnames` to the existing `misc` module. The command accepts comma-separated options after the command text, trims whitespace, ignores empty entries, then replies with one randomly selected option. No storage, migrations, auth changes, or new module needed.
|
||||
|
||||
Assumptions:
|
||||
- Public command, visible in `/help` and Telegram command menu.
|
||||
- Usage reply when no valid comma-separated entries remain.
|
||||
- One valid entry is allowed and returns that entry.
|
||||
- Duplicate entries stay duplicated, so users can weight an option intentionally.
|
||||
|
||||
## Phases
|
||||
|
||||
| Phase | Name | Status |
|
||||
|-------|------|--------|
|
||||
| 1 | [Implement command](./phase-01-implement-command.md) | Completed |
|
||||
| 2 | [Update command surfaces](./phase-02-update-command-surfaces.md) | Completed |
|
||||
| 3 | [Validate behavior](./phase-03-validate-behavior.md) | Completed |
|
||||
|
||||
## Cross-Plan Dependencies
|
||||
|
||||
None. No unfinished project plans found under `plans/`.
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Action |
|
||||
|------|--------|
|
||||
| `internal/modules/misc/misc.go` | Add command registration, parser/helper, handler |
|
||||
| `internal/modules/misc/misc_test.go` | Update registration expectations |
|
||||
| `internal/modules/misc/handlers_test.go` | Add handler behavior tests |
|
||||
| `telegram-commands.json` | Add public Telegram command menu entry |
|
||||
| `README.md` | Update misc module command list |
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- `/wheelofnames a, b, c` replies with exactly one trimmed item from `a`, `b`, `c`.
|
||||
- `/wheelofnames` and `/wheelofnames , ,` reply with usage text.
|
||||
- Empty comma segments do not appear as choices.
|
||||
- Command is public and included in module registration, `/help`, and `telegram-commands.json`.
|
||||
- Focused tests pass, then full `go test ./...` and `go vet ./...` pass.
|
||||
|
||||
## Unresolved Questions
|
||||
|
||||
None.
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
# Plan Complete: Wheel of Names Misc Command
|
||||
|
||||
## Summary
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Plan | `plans/260703-0404-wheelofnames-misc-command/plan.md` |
|
||||
| Status | completed |
|
||||
| Phases | 3/3 |
|
||||
| Files changed | 5 implementation/docs files + plan files |
|
||||
| Tests | `go test ./internal/modules/misc`, `go test ./cmd/server ./internal/modules/misc`, `go test ./...`, `go vet ./...` |
|
||||
|
||||
## Work Completed
|
||||
|
||||
- [x] Added public `/wheelofnames` command in `misc`.
|
||||
- [x] Parses comma-separated options, trims whitespace, ignores empty entries.
|
||||
- [x] Replies usage when no valid option exists.
|
||||
- [x] Picks one valid option with casual randomness.
|
||||
- [x] Updated README and `telegram-commands.json`.
|
||||
- [x] Added focused handler and registration tests.
|
||||
|
||||
## Documentation Updates
|
||||
|
||||
- README module command list updated.
|
||||
- No `docs/` update needed; no architecture, setup, security, or deploy behavior changed.
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- Randomness is non-cryptographic by design.
|
||||
- Duplicate options are preserved as weighting.
|
||||
|
||||
## Unresolved Questions
|
||||
|
||||
None.
|
||||
-114
@@ -1,114 +0,0 @@
|
||||
---
|
||||
phase: 1
|
||||
title: API Client And Env Config
|
||||
status: completed
|
||||
priority: P2
|
||||
dependencies: []
|
||||
---
|
||||
|
||||
# Phase 1: API Client And Env Config
|
||||
|
||||
## Overview
|
||||
|
||||
Create a small misc-local HTTP client for the wheelofnames API and load its
|
||||
endpoint/token from system env. This phase adds the remote render capability
|
||||
without changing command behavior yet.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Functional: Read `WHEELOFNAMES_API_URL` and `WHEELOFNAMES_API_TOKEN` from env.
|
||||
- Functional: POST JSON to the configured full endpoint URL.
|
||||
- Functional: Include Bearer auth only when token is non-empty.
|
||||
- Functional: Return GIF bytes and metadata needed by the command.
|
||||
- Non-functional: Use a bounded `http.Client` timeout, around 30 seconds.
|
||||
- Non-functional: Never log or expose `WHEELOFNAMES_API_TOKEN`.
|
||||
- Non-functional: Allow `http://` for private Coolify/Docker networks and
|
||||
`https://` for public deployments; reject other schemes.
|
||||
|
||||
## Architecture
|
||||
|
||||
Add `wheelofnames_beta_api_client.go` in package `misc`.
|
||||
|
||||
Suggested types:
|
||||
|
||||
```go
|
||||
type wheelBetaAPIClient struct {
|
||||
HTTP *http.Client
|
||||
URL string
|
||||
Token string
|
||||
}
|
||||
|
||||
type wheelBetaAPIRequest struct {
|
||||
Options []string `json:"options"`
|
||||
WinnerIndex int `json:"winnerIndex"`
|
||||
DurationMs int `json:"durationMs"`
|
||||
HoldMs int `json:"holdMs"`
|
||||
FPS int `json:"fps"`
|
||||
Size int `json:"size"`
|
||||
Theme string `json:"theme"`
|
||||
}
|
||||
```
|
||||
|
||||
Use a method like:
|
||||
|
||||
```go
|
||||
func (c *wheelBetaAPIClient) Render(ctx context.Context, options []string, winner int) ([]byte, error)
|
||||
```
|
||||
|
||||
Return errors for:
|
||||
- client not configured
|
||||
- invalid URL scheme or malformed URL
|
||||
- request build/transport failure
|
||||
- non-2xx response
|
||||
- non-`image/gif` content type
|
||||
- empty or oversized body
|
||||
|
||||
Keep body reads bounded. Use a conservative max such as 12 MiB because the bot
|
||||
default remote render is now 512px, 20fps, and 7 seconds total.
|
||||
|
||||
## Related Code Files
|
||||
|
||||
- Create: `/config/workspace/tiennm99/miti99bot/internal/modules/misc/wheelofnames_beta_api_client.go`
|
||||
- Create: `/config/workspace/tiennm99/miti99bot/internal/modules/misc/wheelofnames_beta_api_client_test.go`
|
||||
- Modify: none in command path yet
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Add env constants:
|
||||
- `WHEELOFNAMES_API_URL`
|
||||
- `WHEELOFNAMES_API_TOKEN`
|
||||
2. Add `newWheelBetaAPIClientFromEnv()` that trims env strings.
|
||||
3. Implement URL validation and request construction.
|
||||
4. Set request headers:
|
||||
- `Content-Type: application/json`
|
||||
- `Accept: image/gif`
|
||||
- `Authorization: Bearer <token>` when token is non-empty
|
||||
5. Encode request with fixed render options:
|
||||
- `durationMs: 6000`
|
||||
- `holdMs: 1000`
|
||||
- `fps: 20`
|
||||
- `size: 512`
|
||||
- `theme: "classic"`
|
||||
6. Read response body through `io.LimitReader`.
|
||||
7. Add focused client tests using `httptest.Server`.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] URL unset returns a typed/configuration error that command can treat as fallback.
|
||||
- [ ] Valid request test confirms JSON body and winner index.
|
||||
- [ ] Valid request test confirms default render fields:
|
||||
`durationMs=6000`, `holdMs=1000`, `fps=20`, `size=512`, `theme=classic`.
|
||||
- [ ] Token test confirms Bearer auth header.
|
||||
- [ ] HTTP 500 and 401 return errors without leaking response body as user text.
|
||||
- [ ] Non-GIF content type returns error.
|
||||
- [ ] Oversized response returns error.
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
- Risk: Env URL points at service base URL instead of `/api/gif`.
|
||||
Mitigation: Plan/docs explicitly define `WHEELOFNAMES_API_URL` as full endpoint.
|
||||
- Risk: Remote service token appears in logs.
|
||||
Mitigation: Never log request headers or token value; log only high-level status.
|
||||
- Risk: 512px/20fps render takes longer than older 384px smoke path.
|
||||
Mitigation: 30-second HTTP timeout and local fallback; tune only after
|
||||
production timing data.
|
||||
-105
@@ -1,105 +0,0 @@
|
||||
---
|
||||
phase: 2
|
||||
title: Command Integration And Fallback
|
||||
status: completed
|
||||
priority: P2
|
||||
dependencies:
|
||||
- 1
|
||||
---
|
||||
|
||||
# Phase 2: Command Integration And Fallback
|
||||
|
||||
## Overview
|
||||
|
||||
Wire `/wheelofnamesbeta` to try the remote API first when configured, while
|
||||
preserving the existing local GIF renderer as fallback and preserving Telegram
|
||||
upload behavior.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Functional: Keep `splitWheelOptions` and `pickWheelOption` as the only winner
|
||||
selection source in the bot.
|
||||
- Functional: Use remote GIF only when API call succeeds with valid GIF bytes.
|
||||
- Functional: Fall back to `renderWheelOfNamesBetaGIF(options, winner)` on any
|
||||
remote error.
|
||||
- Functional: Preserve `sendAnimation`, `MessageThreadID`, filename, and
|
||||
non-spoiler caption.
|
||||
- Non-functional: Remote failure should log enough to diagnose status/error but
|
||||
should not reply with stack traces or token-bearing details.
|
||||
- Non-functional: Existing command behavior remains unchanged when env is unset.
|
||||
|
||||
## Architecture
|
||||
|
||||
Introduce a small orchestration helper in package `misc`:
|
||||
|
||||
```go
|
||||
func renderWheelOfNamesBetaAnimation(ctx context.Context, options []string, winner int) ([]byte, int, int, int, error)
|
||||
```
|
||||
|
||||
Return bytes plus Telegram metadata (`duration`, `width`, `height`). Suggested
|
||||
metadata:
|
||||
- Remote success: duration `7`, width/height `512`.
|
||||
- Local fallback: existing `wheelBetaDuration`, `wheelBetaSize`,
|
||||
`wheelBetaSize`.
|
||||
|
||||
Alternative: return a tiny struct:
|
||||
|
||||
```go
|
||||
type wheelBetaAnimation struct {
|
||||
Data []byte
|
||||
Duration int
|
||||
Width int
|
||||
Height int
|
||||
}
|
||||
```
|
||||
|
||||
The command handler should stay simple:
|
||||
1. parse options
|
||||
2. pick winner
|
||||
3. call `renderWheelOfNamesBetaAnimation`
|
||||
4. send animation
|
||||
5. on final render/upload failure, text fallback remains `options[winner]`
|
||||
|
||||
## Related Code Files
|
||||
|
||||
- Modify: `/config/workspace/tiennm99/miti99bot/internal/modules/misc/wheelofnames_beta_command.go`
|
||||
- Modify: `/config/workspace/tiennm99/miti99bot/internal/modules/misc/wheelofnames_beta.go` only if helper placement requires it
|
||||
- Modify: `/config/workspace/tiennm99/miti99bot/internal/modules/misc/handlers_test.go`
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Add the orchestration helper that creates the env client and attempts remote
|
||||
render.
|
||||
2. If client is unconfigured, skip remote without warning noise.
|
||||
3. If client is configured but fails, log warning with safe fields:
|
||||
- command: `wheelofnamesbeta`
|
||||
- status/error category
|
||||
- never token or request body
|
||||
4. On remote success, return remote bytes and remote metadata.
|
||||
5. On remote failure, call current local `renderWheelOfNamesBetaGIF`.
|
||||
6. Update `wheelOfNamesBetaCommand` to use returned metadata in
|
||||
`SendAnimationParams`.
|
||||
7. Keep caption exactly `"Spinning..."`.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] URL unset path still sends local GIF and existing tests pass.
|
||||
- [ ] URL configured path calls remote service and sends remote GIF.
|
||||
- [ ] Remote success path uses Telegram metadata `duration=7`,
|
||||
`width=512`, `height=512`.
|
||||
- [ ] Remote failure path sends local GIF, not text, when local renderer works.
|
||||
- [ ] Telegram upload failure still replies with the selected winner text.
|
||||
- [ ] Caption does not contain the winner.
|
||||
- [ ] Message thread forwarding remains covered.
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
- Risk: Remote and local metadata diverge.
|
||||
Mitigation: use explicit metadata struct; tests assert remote width/height and
|
||||
local compatibility.
|
||||
- Risk: Remote API chooses a different winner.
|
||||
Mitigation: always pass `winnerIndex`; ignore remote winner headers for bot
|
||||
caption/source of truth.
|
||||
- Risk: A configured but down service makes the command slower.
|
||||
Mitigation: timeout and fallback; consider lowering timeout later only after
|
||||
production timing data.
|
||||
-88
@@ -1,88 +0,0 @@
|
||||
---
|
||||
phase: 3
|
||||
title: Deployment Docs And Env Surfaces
|
||||
status: completed
|
||||
priority: P2
|
||||
dependencies:
|
||||
- 1
|
||||
- 2
|
||||
---
|
||||
|
||||
# Phase 3: Deployment Docs And Env Surfaces
|
||||
|
||||
## Overview
|
||||
|
||||
Document the new optional environment variables across local and Coolify
|
||||
deployment surfaces so the bot can find the wheelofnames service without code
|
||||
changes.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Functional: Document `WHEELOFNAMES_API_URL` as the full `/api/gif` endpoint.
|
||||
- Functional: Document `WHEELOFNAMES_API_TOKEN` as the Bearer token matching the
|
||||
wheelofnames service `API_TOKEN`.
|
||||
- Non-functional: `.env.example` must use placeholders, not real secrets.
|
||||
- Non-functional: Deploy docs must explain private network HTTP URL vs public
|
||||
HTTPS URL options.
|
||||
- Non-functional: Compose comments should not force the feature on by default
|
||||
if the service is not running.
|
||||
|
||||
## Architecture
|
||||
|
||||
The bot remains a single long-polling service. The wheelofnames service is a
|
||||
separate container or external service. The bot uses outbound HTTP only.
|
||||
The current wheelofnames service `compose.yml` forwards its own env from the
|
||||
shell with `${VAR:-default}` fallbacks, including `PORT`, `API_TOKEN`,
|
||||
`MAX_CONCURRENT_RENDERS`, and render limits.
|
||||
|
||||
Coolify/private-network example:
|
||||
|
||||
```env
|
||||
WHEELOFNAMES_API_URL=http://wheelofnames:3000/api/gif
|
||||
WHEELOFNAMES_API_TOKEN=<same value as wheelofnames API_TOKEN>
|
||||
```
|
||||
|
||||
Public URL example:
|
||||
|
||||
```env
|
||||
WHEELOFNAMES_API_URL=https://wheelofnames.example.com/api/gif
|
||||
WHEELOFNAMES_API_TOKEN=<same value as wheelofnames API_TOKEN>
|
||||
```
|
||||
|
||||
## Related Code Files
|
||||
|
||||
- Modify: `/config/workspace/tiennm99/miti99bot/.env.example`
|
||||
- Modify: `/config/workspace/tiennm99/miti99bot/compose.yml`
|
||||
- Modify: `/config/workspace/tiennm99/miti99bot/docs/deploy-coolify-selfhosted.md`
|
||||
- Optional modify: `/config/workspace/tiennm99/miti99bot/README.md` only if the
|
||||
command description needs to mention remote rendering.
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Add optional env entries to `.env.example` under operational settings.
|
||||
2. Add commented or placeholder entries in `compose.yml` near other operational
|
||||
env vars.
|
||||
3. Update deploy docs required/optional env table.
|
||||
4. Add a short deployment note:
|
||||
- URL unset = local renderer fallback.
|
||||
- URL set + token = remote Remotion GIF rendering.
|
||||
- Token should match wheelofnames service `API_TOKEN`.
|
||||
- Bot default remote render request is 512px, 20fps, 7 seconds total.
|
||||
5. Avoid suggesting multiple bot replicas; existing single-replica rule still
|
||||
applies.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] `.env.example` includes both variables with safe placeholder values.
|
||||
- [ ] `compose.yml` includes the env names without embedding secrets.
|
||||
- [ ] Deploy docs tell operators how to wire both services.
|
||||
- [ ] Docs state fallback behavior when env is absent or remote fails.
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
- Risk: Docs imply a public bot ingress is required.
|
||||
Mitigation: explicitly state this is outbound HTTP from the bot to
|
||||
wheelofnames; Telegram transport stays long polling.
|
||||
- Risk: Operators put a service base URL without `/api/gif`.
|
||||
Mitigation: examples show full endpoint and implementation returns clear
|
||||
safe error/fallback.
|
||||
-84
@@ -1,84 +0,0 @@
|
||||
---
|
||||
phase: 4
|
||||
title: Validation And Regression Tests
|
||||
status: completed
|
||||
priority: P2
|
||||
dependencies:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
---
|
||||
|
||||
# Phase 4: Validation And Regression Tests
|
||||
|
||||
## Overview
|
||||
|
||||
Add focused regression tests and run the repo quality gates required for a
|
||||
command integration touching HTTP, env config, and Telegram upload behavior.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Functional: Test remote success, remote failure fallback, token header, and
|
||||
malformed/non-GIF responses.
|
||||
- Functional: Existing local renderer tests remain valid.
|
||||
- Non-functional: Run lint/test gates required by `AGENTS.md`.
|
||||
- Non-functional: Do not require a live wheelofnames service for unit tests.
|
||||
|
||||
## Architecture
|
||||
|
||||
Use `httptest.Server` for the wheelofnames API client. Keep tests in package
|
||||
`misc` so they can use unexported helpers and existing test utilities.
|
||||
|
||||
Test matrix:
|
||||
|
||||
| Area | Scenario | Expected |
|
||||
|---|---|---|
|
||||
| Client | valid `200 image/gif` | returns GIF bytes |
|
||||
| Client | default request payload | sends 6000ms spin, 1000ms hold, 20fps, 512px, classic |
|
||||
| Client | token configured | sends `Authorization: Bearer ...` |
|
||||
| Client | `401`/`500` | returns error |
|
||||
| Client | `text/plain` 200 | returns error |
|
||||
| Client | bad URL scheme | returns error |
|
||||
| Command | env unset | local GIF path still sends animation |
|
||||
| Command | remote success | upstream receives request; bot sends animation |
|
||||
| Command | remote failure | bot sends local animation |
|
||||
| Command | remote success in thread | message thread id preserved |
|
||||
|
||||
## Related Code Files
|
||||
|
||||
- Create: `/config/workspace/tiennm99/miti99bot/internal/modules/misc/wheelofnames_beta_api_client_test.go`
|
||||
- Modify: `/config/workspace/tiennm99/miti99bot/internal/modules/misc/handlers_test.go`
|
||||
- Verify: `/config/workspace/tiennm99/miti99bot/internal/modules/misc/misc_test.go`
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Write client tests before wiring command behavior where practical.
|
||||
2. Add command tests with `t.Setenv` and `httptest.Server`.
|
||||
3. Confirm tests do not depend on real network.
|
||||
4. Run focused package tests:
|
||||
```sh
|
||||
go test ./internal/modules/misc
|
||||
```
|
||||
5. Run full gates:
|
||||
```sh
|
||||
go test ./...
|
||||
go vet ./...
|
||||
```
|
||||
6. If docs changed only, no Telegram command registration update is needed.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] `go test ./internal/modules/misc` passes.
|
||||
- [ ] `go test ./...` passes.
|
||||
- [ ] `go vet ./...` passes.
|
||||
- [ ] Tests prove winner index is sent to remote API.
|
||||
- [ ] Tests prove default render config is sent to remote API.
|
||||
- [ ] Tests prove fallback keeps user-visible behavior stable.
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
- Risk: Tests become flaky due package-level env.
|
||||
Mitigation: use `t.Setenv`, avoid `t.Parallel` in env-sensitive tests.
|
||||
- Risk: Recording bot cannot inspect uploaded file bytes.
|
||||
Mitigation: assert upstream request count/body plus `sendAnimation` metadata;
|
||||
client tests validate returned bytes.
|
||||
@@ -1,161 +0,0 @@
|
||||
---
|
||||
title: Wheelofnamesbeta API Integration
|
||||
description: >-
|
||||
Route /wheelofnamesbeta GIF rendering through the self-hosted wheelofnames API
|
||||
when configured, with the current local renderer as fallback.
|
||||
status: completed
|
||||
priority: P2
|
||||
branch: main
|
||||
tags:
|
||||
- feature
|
||||
- backend
|
||||
- api
|
||||
blockedBy: []
|
||||
blocks: []
|
||||
created: '2026-07-06T15:20:57.412Z'
|
||||
createdBy: 'ck:plan'
|
||||
source: skill
|
||||
---
|
||||
|
||||
# Wheelofnamesbeta API Integration
|
||||
|
||||
## Overview
|
||||
|
||||
Integrate the existing Go `/wheelofnamesbeta` command with the new
|
||||
`wheelofnames` Remotion service by reading the API endpoint from system env.
|
||||
When `WHEELOFNAMES_API_URL` is set, the command sends the parsed options and
|
||||
locally selected `winnerIndex` to that endpoint and uploads the returned GIF to
|
||||
Telegram. When the env is unset, invalid, timed out, unauthorized, or the
|
||||
service returns non-GIF/non-2xx, the bot keeps using the current local GIF
|
||||
renderer.
|
||||
|
||||
Checked service repo state on 2026-07-06:
|
||||
- `/config/workspaces/tiennm99/wheelofnames` is clean.
|
||||
- Latest commit: `4cf72ea fix: align wheel labels and compose env`.
|
||||
- Service now has dedicated radial label layout logic in
|
||||
`src/remotion/wheel-label-layout.js`, tests for label layout, and
|
||||
`compose.yml` env defaults via `${VAR:-default}`.
|
||||
- API schema still accepts `durationMs`, `holdMs`, `fps`, `size`, and `theme`.
|
||||
- Service composition duration is `durationMs + holdMs`; the requested bot
|
||||
payload below renders a 7-second GIF.
|
||||
|
||||
Scope Challenge:
|
||||
- Existing code: `/wheelofnamesbeta` already parses options, picks the winner,
|
||||
renders local GIF bytes, sends `sendAnimation`, preserves message threads,
|
||||
and falls back to text on local render/upload failure.
|
||||
- Minimum changes: add a small API client, wire command render path through
|
||||
remote-or-local selection, add env docs, and test remote success/failure.
|
||||
- Complexity: expected 5-7 touched files, one new client file, four focused
|
||||
phases. No command rename, stats migration, persistent jobs, or async flow.
|
||||
- Selected mode: SCOPE REDUCTION / fast plan. The API service already exists;
|
||||
this plan only integrates the command.
|
||||
|
||||
## Architecture
|
||||
|
||||
```text
|
||||
/wheelofnamesbeta message
|
||||
|
|
||||
| splitWheelOptions + pickWheelOption (existing)
|
||||
v
|
||||
renderWheelOfNamesBetaAnimation(ctx, options, winner)
|
||||
|
|
||||
| if WHEELOFNAMES_API_URL unset
|
||||
| -> existing renderWheelOfNamesBetaGIF fallback
|
||||
|
|
||||
| POST WHEELOFNAMES_API_URL
|
||||
| Authorization: Bearer $WHEELOFNAMES_API_TOKEN when set
|
||||
| JSON: { options, winnerIndex, durationMs, holdMs, fps, size, theme }
|
||||
v
|
||||
remote GIF bytes -> Telegram sendAnimation
|
||||
|
|
||||
| on any remote error
|
||||
v
|
||||
existing local GIF renderer -> Telegram sendAnimation
|
||||
```
|
||||
|
||||
Environment contract:
|
||||
- `WHEELOFNAMES_API_URL`: optional full endpoint URL, expected to include
|
||||
`/api/gif`, for example `http://wheelofnames:3000/api/gif`.
|
||||
- `WHEELOFNAMES_API_TOKEN`: optional Bearer token. Required for production
|
||||
`wheelofnames` service deployments because that service refuses production
|
||||
startup without `API_TOKEN`.
|
||||
|
||||
Remote render request values should be fixed in code for now:
|
||||
|
||||
```json
|
||||
{
|
||||
"options": ["alice", "bob", "carol"],
|
||||
"winnerIndex": 1,
|
||||
"durationMs": 6000,
|
||||
"holdMs": 1000,
|
||||
"fps": 20,
|
||||
"size": 512,
|
||||
"theme": "classic"
|
||||
}
|
||||
```
|
||||
|
||||
Do not add user-facing flags, persistent storage, or dynamic theme selection in
|
||||
this integration pass.
|
||||
|
||||
## Phases
|
||||
|
||||
| Phase | Name | Status |
|
||||
|-------|------|--------|
|
||||
| 1 | [API Client And Env Config](./phase-01-api-client-and-env-config.md) | Completed |
|
||||
| 2 | [Command Integration And Fallback](./phase-02-command-integration-and-fallback.md) | Completed |
|
||||
| 3 | [Deployment Docs And Env Surfaces](./phase-03-deployment-docs-and-env-surfaces.md) | Completed |
|
||||
| 4 | [Validation And Regression Tests](./phase-04-validation-and-regression-tests.md) | Completed |
|
||||
|
||||
## Dependencies
|
||||
|
||||
## Cross-Plan Dependencies
|
||||
|
||||
| Relationship | Plan | Status |
|
||||
|---|---|---|
|
||||
| Uses output from | `plans/260706-1441-wheelofnames-remotion-api/plan.md` | completed |
|
||||
|
||||
No unfinished project plans overlap this scope.
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Action |
|
||||
|---|---|
|
||||
| `/config/workspace/tiennm99/miti99bot/internal/modules/misc/wheelofnames_beta_api_client.go` | Create remote API client and env loader |
|
||||
| `/config/workspace/tiennm99/miti99bot/internal/modules/misc/wheelofnames_beta_command.go` | Route command through remote-or-local renderer |
|
||||
| `/config/workspace/tiennm99/miti99bot/internal/modules/misc/wheelofnames_beta.go` | Keep local renderer unchanged as fallback |
|
||||
| `/config/workspace/tiennm99/miti99bot/internal/modules/misc/handlers_test.go` | Add command-level remote success/fallback tests |
|
||||
| `/config/workspace/tiennm99/miti99bot/internal/modules/misc/wheelofnames_beta_api_client_test.go` | Create focused client tests |
|
||||
| `/config/workspace/tiennm99/miti99bot/.env.example` | Document optional API env vars |
|
||||
| `/config/workspace/tiennm99/miti99bot/compose.yml` | Add commented Coolify/private-network env hints |
|
||||
| `/config/workspace/tiennm99/miti99bot/docs/deploy-coolify-selfhosted.md` | Document deployment wiring with wheelofnames service |
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- With `WHEELOFNAMES_API_URL` unset, `/wheelofnamesbeta` behaves like today.
|
||||
- With `WHEELOFNAMES_API_URL` set and remote returns `200 image/gif`, the bot
|
||||
uploads those GIF bytes via `sendAnimation`.
|
||||
- The remote request uses `durationMs=6000`, `holdMs=1000`, `fps=20`,
|
||||
`size=512`, and `theme=classic` by default.
|
||||
- The request body contains the original parsed options and the locally chosen
|
||||
`winnerIndex`; the service must not be the source of truth for the winner.
|
||||
- If `WHEELOFNAMES_API_TOKEN` is set, request includes
|
||||
`Authorization: Bearer <token>` and tests verify it.
|
||||
- Remote failures fall back to the existing local GIF renderer and do not reveal
|
||||
the winner in the caption.
|
||||
- Timeout is bounded with an HTTP client timeout; no goroutine can hang on a
|
||||
wedged render service.
|
||||
- Docs and env examples explain that URL is optional and token is required for
|
||||
production wheelofnames service auth.
|
||||
- Focused tests pass, then `go test ./...` and `go vet ./...` pass.
|
||||
|
||||
## Out Of Scope
|
||||
|
||||
- Replacing `/wheelofnames`; only `/wheelofnamesbeta` changes.
|
||||
- Removing the local Go GIF renderer.
|
||||
- Async job polling, object storage, or cached GIF URLs.
|
||||
- User-selectable themes, sizes, fps, or command syntax changes.
|
||||
- Stats migrations or Telegram command menu changes.
|
||||
|
||||
## Unresolved Questions
|
||||
|
||||
None.
|
||||
@@ -1,265 +0,0 @@
|
||||
---
|
||||
type: research
|
||||
topic: mongodb-stats-design
|
||||
created_at: 2026-07-01T03:00:06Z
|
||||
---
|
||||
|
||||
# Research Report: MongoDB Stats Design
|
||||
|
||||
## Executive Summary
|
||||
|
||||
For this bot's `/stats` feature, best primary model is one aggregate document per
|
||||
`(command, user)` pair, plus one anonymous command bucket when sender has no
|
||||
username. This keeps documents bounded, uses MongoDB atomic `$inc`, and makes all
|
||||
current views queryable without dynamic object keys.
|
||||
|
||||
Do not store stats as "one user document with commands map" or "one command
|
||||
document with users map" as primary shape. Those designs make one query easy and
|
||||
the opposite query expensive, create hot growing documents, and push us toward
|
||||
dynamic field names. Pair docs are boring and better.
|
||||
|
||||
Recommended document:
|
||||
|
||||
```js
|
||||
{ _id: "ping:42", cmd: "ping", uid: 42, user: "alice", n: 3 }
|
||||
{ _id: "ping", cmd: "ping", n: 2 } // no public username
|
||||
```
|
||||
|
||||
## Methodology
|
||||
|
||||
- Sources consulted: MongoDB official docs, Go driver docs, local codebase.
|
||||
- Date: 2026-07-01.
|
||||
- Key terms: MongoDB schema design, unbounded arrays, `$inc`, atomicity,
|
||||
aggregation `$group`, `$sort`, compound indexes, ESR guideline.
|
||||
- Scope: stats module only. Bot persistence remains one collection per module.
|
||||
|
||||
## Codebase Context
|
||||
|
||||
- Repo: Go Telegram bot.
|
||||
- Storage: `internal/storage` has one MongoDB collection per module.
|
||||
- Current stats: `count:*`, `user:*`, `pair:*` key families in one collection.
|
||||
- Current views:
|
||||
- `/stats`: top commands.
|
||||
- `/stats users`: top users.
|
||||
- `/stats user <name>`: commands by user.
|
||||
- `/stats cmd <name>`: users by command.
|
||||
- Mongo docs already store flattened BSON fields, so stats fields can be queried.
|
||||
|
||||
## Key Findings
|
||||
|
||||
### 1. Avoid Growing Nested User/Command Maps
|
||||
|
||||
MongoDB docs warn against unbounded arrays because growing documents hurt
|
||||
performance and risk document size limits. The same practical concern applies to
|
||||
ever-growing embedded maps like `commands` under a user or `users` under a
|
||||
command: one document grows forever and becomes a write hotspot.
|
||||
|
||||
Source: MongoDB "Avoid Unbounded Arrays":
|
||||
https://www.mongodb.com/docs/manual/data-modeling/design-antipatterns/unbounded-arrays/
|
||||
|
||||
### 2. Use Atomic `$inc` For Counts
|
||||
|
||||
MongoDB `$inc` creates missing numeric fields, increments by the given amount,
|
||||
and is atomic within one document. This fits per-pair counter documents exactly.
|
||||
|
||||
Source: MongoDB `$inc` operator:
|
||||
https://www.mongodb.com/docs/manual/reference/operator/update/inc/
|
||||
|
||||
MongoDB write operations are atomic at single-document level. Multi-document
|
||||
updates are not atomic as a whole, so each counter increment should update one
|
||||
aggregate document.
|
||||
|
||||
Source: MongoDB atomicity:
|
||||
https://www.mongodb.com/docs/manual/core/write-operations-atomicity/
|
||||
|
||||
### 3. Use Aggregation For Cross-Dimension Totals
|
||||
|
||||
MongoDB `$group` combines documents by a group key. That fits:
|
||||
|
||||
```js
|
||||
// top commands
|
||||
[{ $group: { _id: "$cmd", n: { $sum: "$n" } } }]
|
||||
|
||||
// top users
|
||||
[{ $match: { uid: { $gt: 0 } } },
|
||||
{ $group: { _id: "$uid", user: { $first: "$user" }, n: { $sum: "$n" } } }]
|
||||
```
|
||||
|
||||
Source: MongoDB `$group`:
|
||||
https://www.mongodb.com/docs/manual/reference/operator/aggregation/group/
|
||||
|
||||
Use `$sort` then `$limit` for top K. MongoDB can coalesce adjacent sort/limit so
|
||||
only top N must be kept in memory.
|
||||
|
||||
Source: MongoDB `$sort` optimization:
|
||||
https://www.mongodb.com/docs/manual/reference/operator/aggregation/sort/
|
||||
|
||||
### 4. Index From Query Shape, Not Guesswork
|
||||
|
||||
MongoDB aggregation can use indexes, especially early `$match` and `$sort`
|
||||
stages. The ESR guideline says equality fields first, then sort, then range for
|
||||
compound indexes.
|
||||
|
||||
Sources:
|
||||
- Aggregation pipeline optimization:
|
||||
https://www.mongodb.com/docs/manual/core/aggregation-pipeline-optimization/
|
||||
- ESR guideline:
|
||||
https://www.mongodb.com/docs/manual/tutorial/equality-sort-range-guideline/
|
||||
- Indexing strategies:
|
||||
https://www.mongodb.com/docs/manual/applications/indexes/
|
||||
|
||||
For this module, start with no extra indexes or a tiny set. Traffic likely low.
|
||||
Add indexes once data grows or `/stats` gets slow.
|
||||
|
||||
Useful indexes if needed:
|
||||
|
||||
```js
|
||||
db.stats.createIndex({ cmd: 1, n: -1 }) // /stats cmd <name>
|
||||
db.stats.createIndex({ uid: 1, n: -1 }) // /stats user <name>
|
||||
db.stats.createIndex({ user: 1, updatedAt: -1 }) // resolve username
|
||||
```
|
||||
|
||||
Do not add many indexes early. Each index costs memory/disk and write work.
|
||||
|
||||
## Comparative Analysis
|
||||
|
||||
| Shape | Example | Good | Bad | Verdict |
|
||||
|---|---|---|---|---|
|
||||
| User-rooted | `{ uid, user, commands: { ping: 3 } }` | `/stats user` easy | `/stats cmd` scans users, dynamic keys, growing doc | Reject |
|
||||
| Command-rooted | `{ cmd, users: { "42": { n: 3 } } }` | `/stats cmd` easy | `/stats user` scans commands, hot command docs, dynamic keys | Reject |
|
||||
| Pair-rooted | `{ cmd, uid, user, n }` | All 4 views queryable, bounded docs, atomic `$inc` | Aggregation needed for totals | Recommend |
|
||||
| Event log | `{ cmd, uid, user, at }` per invocation | Full history | Too much data, needs rollups | Overkill |
|
||||
|
||||
## Implementation Recommendations
|
||||
|
||||
### Primary Model
|
||||
|
||||
Use one document per aggregate:
|
||||
|
||||
```go
|
||||
type usageEntry struct {
|
||||
Cmd string `bson:"cmd" json:"cmd"`
|
||||
UserID int64 `bson:"uid,omitempty" json:"uid,omitempty"`
|
||||
Username string `bson:"user,omitempty" json:"user,omitempty"`
|
||||
N int64 `bson:"n" json:"n"`
|
||||
}
|
||||
```
|
||||
|
||||
Keys:
|
||||
|
||||
```text
|
||||
<cmd> anonymous/no-public-username bucket
|
||||
<cmd>:<uid> named user pair bucket
|
||||
```
|
||||
|
||||
### Write Path
|
||||
|
||||
Use MongoDB `UpdateOne` with `upsert: true`:
|
||||
|
||||
```js
|
||||
db.stats.updateOne(
|
||||
{ _id: "ping:42" },
|
||||
{
|
||||
$set: { cmd: "ping", uid: 42, user: "alice" },
|
||||
$inc: { n: 1 },
|
||||
$currentDate: { updatedAt: true }
|
||||
},
|
||||
{ upsert: true }
|
||||
)
|
||||
```
|
||||
|
||||
This avoids read-modify-write races in the current generic `DocStore` loop.
|
||||
|
||||
### Read Path
|
||||
|
||||
Use Mongo queries/aggregation when backend is MongoDB. Keep in-memory fallback
|
||||
for tests and no-database local run.
|
||||
|
||||
Queries:
|
||||
|
||||
```js
|
||||
// /stats
|
||||
db.stats.aggregate([
|
||||
{ $group: { _id: "$cmd", n: { $sum: "$n" } } },
|
||||
{ $sort: { n: -1, _id: 1 } },
|
||||
{ $limit: 20 }
|
||||
])
|
||||
|
||||
// /stats users
|
||||
db.stats.aggregate([
|
||||
{ $match: { uid: { $gt: 0 }, user: { $type: "string", $ne: "" } } },
|
||||
{ $group: { _id: "$uid", user: { $first: "$user" }, n: { $sum: "$n" } } },
|
||||
{ $sort: { n: -1, user: 1 } },
|
||||
{ $limit: 20 }
|
||||
])
|
||||
|
||||
// /stats user alice
|
||||
db.stats.find({ uid: 42 }).sort({ n: -1, cmd: 1 }).limit(20)
|
||||
|
||||
// /stats cmd ping
|
||||
db.stats.find({ cmd: "ping", uid: { $gt: 0 } }).sort({ n: -1, user: 1 }).limit(20)
|
||||
```
|
||||
|
||||
### Username Rename
|
||||
|
||||
When a user calls a command with a new username, refresh all docs for that `uid`
|
||||
with `UpdateMany({ uid }, { $set: { user } })`. It is not atomic as a whole, but
|
||||
stats are best-effort and eventual consistency is acceptable.
|
||||
|
||||
### Migration
|
||||
|
||||
Existing prod data likely in old key prefixes. Options:
|
||||
|
||||
1. No migration: simplest, stats reset after deploy.
|
||||
2. Read old + new for one release: more code, little value.
|
||||
3. One-off migration script: only if historical stats matter.
|
||||
|
||||
Recommendation: no migration unless user explicitly wants historical stats.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Do not use dynamic fields like `commands.ping` or `users.42` as primary model.
|
||||
- Do not grow one document forever.
|
||||
- Do not use generic get-then-put for hot counters on MongoDB.
|
||||
- Do not over-index small collections.
|
||||
- Do not use username as identity. Telegram username can change; use `uid`.
|
||||
|
||||
## Decision
|
||||
|
||||
Recommended: pair-rooted aggregate docs, Mongo atomic upsert increments, Mongo
|
||||
aggregation for display views, memory fallback for tests.
|
||||
|
||||
## References
|
||||
|
||||
- MongoDB Avoid Unbounded Arrays:
|
||||
https://www.mongodb.com/docs/manual/data-modeling/design-antipatterns/unbounded-arrays/
|
||||
- MongoDB `$inc`:
|
||||
https://www.mongodb.com/docs/manual/reference/operator/update/inc/
|
||||
- MongoDB Atomicity:
|
||||
https://www.mongodb.com/docs/manual/core/write-operations-atomicity/
|
||||
- MongoDB `$group`:
|
||||
https://www.mongodb.com/docs/manual/reference/operator/aggregation/group/
|
||||
- MongoDB `$sort`:
|
||||
https://www.mongodb.com/docs/manual/reference/operator/aggregation/sort/
|
||||
- MongoDB Aggregation Optimization:
|
||||
https://www.mongodb.com/docs/manual/core/aggregation-pipeline-optimization/
|
||||
- MongoDB ESR Guideline:
|
||||
https://www.mongodb.com/docs/manual/tutorial/equality-sort-range-guideline/
|
||||
- MongoDB Indexing Strategies:
|
||||
https://www.mongodb.com/docs/manual/applications/indexes/
|
||||
- MongoDB Go Driver Update:
|
||||
https://www.mongodb.com/docs/drivers/go/current/crud/update/
|
||||
- MongoDB Go Driver Indexes:
|
||||
https://www.mongodb.com/docs/drivers/go/current/indexes/
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Confirm pair-rooted aggregate docs.
|
||||
2. Finish stats implementation.
|
||||
3. Run `go test ./internal/modules/stats ./internal/storage`.
|
||||
4. Optional later: add Mongo integration test if local Mongo available.
|
||||
|
||||
## Unresolved Questions
|
||||
|
||||
- Keep old stats by migration, or accept reset?
|
||||
- Add indexes now, or wait until data size/latency proves need?
|
||||
@@ -1,151 +0,0 @@
|
||||
---
|
||||
type: research
|
||||
date: 2026-07-06T11:34:00Z
|
||||
topic: wheelofnamesbeta gif generation and Telegram delivery
|
||||
skills:
|
||||
- ck:research
|
||||
- ck:project-organization
|
||||
---
|
||||
|
||||
# Research Report: Wheelofnamesbeta Gif Generation
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Use Go stdlib `image/gif` for GIF encoding. It supports multi-frame GIFs via
|
||||
`EncodeAll`, frame delay in 1/100s, and loop control. Add only
|
||||
`golang.org/x/image/font/basicfont` for self-contained bitmap text if labels or
|
||||
winner text must appear inside frames. Avoid ffmpeg/cgo/runtime binaries.
|
||||
|
||||
Send the generated GIF with Telegram `sendAnimation`, not `sendPhoto`.
|
||||
Telegram Bot API defines `sendAnimation` for GIF or silent H.264/MPEG-4
|
||||
animation files, max 50 MB at current docs. Generate in memory, upload with
|
||||
`models.InputFileUpload`, and set caption to the winner.
|
||||
|
||||
Recommended `/wheelofnamesbeta` behavior: parse same comma input as
|
||||
`/wheelofnames`, choose winner once, render 6s spin plus 1s hold. Keep canvas
|
||||
small, frame count low, and palette fixed to avoid CPU/file-size spikes.
|
||||
|
||||
## Research Methodology
|
||||
|
||||
- Sources consulted: 4
|
||||
- Date: 2026-07-06
|
||||
- Gemini: disabled by `.ck.json`; used WebSearch
|
||||
- Key terms: Go animated GIF EncodeAll, golang.org/x/image font basicfont,
|
||||
Telegram Bot API sendAnimation, GIF optimization frame count palette
|
||||
|
||||
## Key Findings
|
||||
|
||||
### 1. Technology Overview
|
||||
|
||||
`image/gif` is in Go stdlib and implements GIF decoding/encoding. `EncodeAll`
|
||||
writes multiple paletted images as one GIF. The `GIF` struct stores successive
|
||||
frames, delays in 1/100s, and loop count. This maps directly to "6s spin + 1s
|
||||
hold": e.g. 30 frames at 20/100s each, then 1 final frame at 100/100s.
|
||||
|
||||
`golang.org/x/image/font` provides text drawing interfaces. `basicfont` provides
|
||||
fixed-size font faces. `Face7x13` is self-contained, uses printable ASCII, and
|
||||
needs no font files.
|
||||
|
||||
Telegram Bot API `sendAnimation` sends GIF or silent H.264/MPEG-4 animation
|
||||
files and returns a sent message. Current limit: 50 MB. It supports normal
|
||||
`chat_id` and `message_thread_id`, so it works for groups/topics unlike
|
||||
`sendMessageDraft`.
|
||||
|
||||
### 2. Current State And Trends
|
||||
|
||||
- GIF is simple but inefficient compared to MP4. For this bot, low-res generated
|
||||
vector-ish frames are small enough.
|
||||
- Go stdlib avoids external binary dependencies and is deploy-safe on Coolify.
|
||||
- `x/image/basicfont` is enough for readable short text; no custom TTF asset.
|
||||
|
||||
### 3. Best Practices
|
||||
|
||||
- Render fixed small canvas, e.g. 320x320.
|
||||
- Use fixed palette and `image.NewPaletted`.
|
||||
- Keep frame count low: 30 spin frames + 1 hold frame.
|
||||
- Delay: spin frames 20/100s = 6s total; final hold 100/100s = 1s.
|
||||
- Set `LoopCount: -1` so the GIF plays once. Telegram clients may still expose
|
||||
replay behavior, but file intent is one pass.
|
||||
- Put exact winner in caption too, because GIF autoplay/download can vary by
|
||||
client settings.
|
||||
|
||||
### 4. Security Considerations
|
||||
|
||||
- No user-provided image decoding; generated pixels only, so image bomb risk low.
|
||||
- Bound input names and rendered text length to avoid huge memory/caption work.
|
||||
- Never write temp files; generate in memory.
|
||||
- Avoid shelling to ffmpeg from command handler.
|
||||
|
||||
### 5. Performance Insights
|
||||
|
||||
GIF frame memory is width * height * frame count. At 320x320 and 31 frames, raw
|
||||
paletted data is about 3.2 MB before encoding plus overhead. Acceptable for one
|
||||
long-polling handler.
|
||||
|
||||
Drawing simple wedges/lines/text in Go is CPU-cheap. Avoid antialiasing and
|
||||
large canvases. 20/100s frame delay keeps total frames modest.
|
||||
|
||||
## Comparative Analysis
|
||||
|
||||
| Option | Pros | Cons | Verdict |
|
||||
|---|---|---|---|
|
||||
| stdlib `image/gif` | No new heavy dependency, pure Go, direct timing control | Basic drawing only | Use |
|
||||
| `x/image/font/basicfont` | Small official Go subrepo, no font files | ASCII-ish visual, small font | Use for labels |
|
||||
| `gg` | Easier antialiased drawing/text | Adds third-party dependency | Reject for beta |
|
||||
| ffmpeg MP4/GIF | Better compression/quality | External binary, shell/runtime complexity | Reject |
|
||||
|
||||
## Implementation Recommendations
|
||||
|
||||
### Quick Start Guide
|
||||
|
||||
1. Add `/wheelofnamesbeta` command in `misc`.
|
||||
2. Reuse `splitWheelOptions` and `pickWheelOption`.
|
||||
3. Generate GIF bytes in memory.
|
||||
4. Send using `b.SendAnimation` with `models.InputFileUpload`.
|
||||
5. Caption final winner.
|
||||
6. Add tests that assert `sendAnimation` and encoded GIF timing.
|
||||
|
||||
### Code Sketch
|
||||
|
||||
```go
|
||||
buf := bytes.Buffer{}
|
||||
err := gif.EncodeAll(&buf, &gif.GIF{
|
||||
Image: frames,
|
||||
Delay: delays, // 30 x 20, then 100
|
||||
LoopCount: -1,
|
||||
})
|
||||
_, err = b.SendAnimation(ctx, &bot.SendAnimationParams{
|
||||
ChatID: msg.Chat.ID,
|
||||
MessageThreadID: msg.MessageThreadID,
|
||||
Animation: &models.InputFileUpload{
|
||||
Filename: "wheelofnamesbeta.gif",
|
||||
Data: bytes.NewReader(buf.Bytes()),
|
||||
},
|
||||
Duration: 7,
|
||||
Caption: "Winner: " + winner,
|
||||
})
|
||||
```
|
||||
|
||||
### Common Pitfalls
|
||||
|
||||
- `sendPhoto` can flatten GIF to a static image. Use `sendAnimation`.
|
||||
- GIF delays are centiseconds, not milliseconds.
|
||||
- Large canvases or many frames can bloat files fast.
|
||||
- Telegram client autoplay is user-controlled; caption must contain winner.
|
||||
|
||||
## Resources And References
|
||||
|
||||
- Go `image/gif` docs: https://pkg.go.dev/image/gif
|
||||
- Go `golang.org/x/image/font` docs: https://pkg.go.dev/golang.org/x/image/font
|
||||
- Go `basicfont` docs: https://pkg.go.dev/golang.org/x/image/font/basicfont
|
||||
- Telegram `sendAnimation`: https://core.telegram.org/bots/api#sendanimation
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Implement pure-Go GIF renderer in `internal/modules/misc`.
|
||||
2. Add `/wheelofnamesbeta` command surfaces and tests.
|
||||
3. Run focused misc tests, full `go test ./...`, and `go vet ./...`.
|
||||
|
||||
## Unresolved Questions
|
||||
|
||||
- None.
|
||||
Reference in new issue
Block a user