diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5dbf4f6..8a17809 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,7 +48,7 @@ jobs: govulncheck ./... # No DB emulator step: MongoDB integration tests skip gracefully when - # MONGODB_TEST_URL is unset. Run them locally via `make test-mongo`. + # MONGODB_TEST_URL is unset. See README.md for the local Docker command. - name: go test env: # Quiet test logs so real failures stand out. diff --git a/AGENTS.md b/AGENTS.md index d6131a3..51de163 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,7 +25,6 @@ deleting commands, update all related surfaces: - module command registration in `internal/modules//` - handler usage text and user-facing error text -- `telegram-commands.json` - tests for registration, handlers, and command menu behavior - README/docs when behavior changes are user-visible diff --git a/Makefile b/Makefile deleted file mode 100644 index d60380f..0000000 --- a/Makefile +++ /dev/null @@ -1,87 +0,0 @@ -.PHONY: help test test-mongo mongo-local mongo-local-stop vet build run telegram-commands telegram-commands-info telegram-deletewebhook telegram-webhook-info clean - -# Short git SHA baked into local binaries. Coolify sets SOURCE_COMMIT at -# runtime; this ldflags value is the fallback for local builds. -GIT_SHA := $(shell git rev-parse --short HEAD 2>/dev/null) -LDFLAGS := -s -w -X main.gitSHA=$(GIT_SHA) - -TELEGRAM_COMMANDS_FILE ?= telegram-commands.json - -help: ## Show this help - @grep -hE '^[a-zA-Z0-9_-]+:.*?## ' $(MAKEFILE_LIST) | awk 'BEGIN {FS=":.*?## "}; {printf " \033[36m%-22s\033[0m %s\n", $$1, $$2}' - -# ---- Test ------------------------------------------------------------------ - -test: ## Unit tests (no emulator required) - go test -race -count=1 ./... - -# Run MongoDB integration tests against a local Mongo container. -# Override MONGO_PORT if 27017 is taken on your host. -MONGO_PORT ?= 27017 -test-mongo: mongo-local ## Run MongoDB tests against a local Mongo container - MONGODB_TEST_URL=mongodb://127.0.0.1:$(MONGO_PORT) LOG_LEVEL=error \ - go test -race -count=1 ./internal/storage/... ./internal/modules/lol/... ./internal/modules/stats/... - -# ---- Lint / Vet ------------------------------------------------------------ - -vet: ## go vet - go vet ./... - -# ---- Build ----------------------------------------------------------------- - -build: ## Build the local server binary (host arch) - CGO_ENABLED=0 go build -ldflags="$(LDFLAGS)" -o ./bin/server ./cmd/server - -# ---- Run ------------------------------------------------------------------- - -run: ## Run locally (in-memory storage unless MONGO_URL is set) - go run ./cmd/server - -# ---- MongoDB local for tests ------------------------------------------------ - -mongo-local: ## Start MongoDB container on :$(MONGO_PORT) (idempotent) - @if ! docker ps --format '{{.Names}}' | grep -q '^miti99bot-mongo$$'; then \ - docker run -d --rm --name miti99bot-mongo -p $(MONGO_PORT):27017 mongo:7; \ - echo "MongoDB started on :$(MONGO_PORT)"; \ - sleep 2; \ - else \ - echo "MongoDB already running"; \ - fi - -mongo-local-stop: ## Stop local MongoDB - -docker stop miti99bot-mongo - -# ---- Telegram operations ---------------------------------------------------- - -telegram-commands: ## Register command menu using TELEGRAM_BOT_TOKEN env - @set -eu; \ - : "$${TELEGRAM_BOT_TOKEN:?set TELEGRAM_BOT_TOKEN}"; \ - echo "Registering Telegram commands from $(TELEGRAM_COMMANDS_FILE)"; \ - curl -sS -X POST "https://api.telegram.org/bot$${TELEGRAM_BOT_TOKEN}/setMyCommands" \ - -H 'Content-Type: application/json' \ - --data-binary "@$(TELEGRAM_COMMANDS_FILE)"; \ - echo - -telegram-commands-info: ## Show Telegram getMyCommands using TELEGRAM_BOT_TOKEN env - @set -eu; \ - : "$${TELEGRAM_BOT_TOKEN:?set TELEGRAM_BOT_TOKEN}"; \ - curl -sS "https://api.telegram.org/bot$${TELEGRAM_BOT_TOKEN}/getMyCommands"; \ - echo - -telegram-deletewebhook: ## Delete webhook so the poller can run - @set -eu; \ - : "$${TELEGRAM_BOT_TOKEN:?set TELEGRAM_BOT_TOKEN}"; \ - curl -sS -X POST "https://api.telegram.org/bot$${TELEGRAM_BOT_TOKEN}/deleteWebhook" \ - -d 'drop_pending_updates=false'; \ - echo - -telegram-webhook-info: ## Show Telegram getWebhookInfo using TELEGRAM_BOT_TOKEN env - @set -eu; \ - : "$${TELEGRAM_BOT_TOKEN:?set TELEGRAM_BOT_TOKEN}"; \ - curl -sS "https://api.telegram.org/bot$${TELEGRAM_BOT_TOKEN}/getWebhookInfo"; \ - echo - -# ---- Clean ----------------------------------------------------------------- - -clean: ## Remove local build artifacts - rm -rf build/ bin/ cov.out diff --git a/README.md b/README.md index a214eff..4c529ca 100644 --- a/README.md +++ b/README.md @@ -44,46 +44,69 @@ internal/modules/ Module framework, registry, dispatchers, modules internal/storage/ typed DocStore[T] (Provider + Typed); mongodb runtime + memory (tests). Values persist as flattened native BSON root documents internal/systemstate/ shared `system` collection helper for future startup migrations compose.yml Coolify self-host stack (single bot service) -telegram-commands.json Manual Telegram command menu source docs/deploy-coolify-selfhosted.md Self-host deploy and operations guide ``` ## Run locally -In-memory storage (no database required): +In-memory storage requires no database. Set the environment variables for your +shell, then run the server with Go: -```sh -TELEGRAM_BOT_TOKEN=… \ -MODULES= \ +```powershell +# PowerShell +$env:TELEGRAM_BOT_TOKEN = "…" +$env:MODULES = "" go run ./cmd/server ``` -The bot uses long polling, so a local run talks to Telegram directly — no `ngrok`, no public URL. Ensure the bot's webhook is unset (the server clears it on startup) or `getUpdates` 409s. The dev bot is created manually; token injected via env vars only. +```sh +# POSIX shells (Linux/macOS) +export TELEGRAM_BOT_TOKEN="…" +export MODULES="" +go run ./cmd/server +``` + +The bot uses long polling, so a local run talks to Telegram directly — no +`ngrok` or public URL. The server clears any existing webhook on startup. The +dev bot is created manually; its token is injected through the environment. Persistent MongoDB locally (auto-selected when `MONGO_URL` is set): ```sh -make mongo-local -TELEGRAM_BOT_TOKEN=… \ -MONGO_URL=mongodb://127.0.0.1:27017 \ -MONGO_DATABASE=miti99bot_dev \ -go run ./cmd/server +docker run -d --rm --name miti99bot-mongo -p 27017:27017 mongo:7 +``` + +Then set `MONGO_URL=mongodb://127.0.0.1:27017` and +`MONGO_DATABASE=miti99bot_dev` using the shell syntax above before running +`go run ./cmd/server`. Stop the local database with +`docker stop miti99bot-mongo`. + +For MongoDB integration tests, start that container and set +`MONGODB_TEST_URL=mongodb://127.0.0.1:27017`: + +```powershell +# PowerShell +$env:MONGODB_TEST_URL = "mongodb://127.0.0.1:27017" +$env:LOG_LEVEL = "error" +go test -count=1 ./internal/storage/... ./internal/modules/lol/... ./internal/modules/stats/... ``` -For integration tests (each skips when its emulator env var is unset): ```sh -make mongo-local # docker run mongo:7 on :27017 -make test-mongo # MongoDB integration tests against local MongoDB +# POSIX shells (Linux/macOS) +MONGODB_TEST_URL=mongodb://127.0.0.1:27017 LOG_LEVEL=error \ + go test -count=1 ./internal/storage/... ./internal/modules/lol/... ./internal/modules/stats/... ``` ## Test ```sh -make vet # go vet -make test # full unit suite (no emulator) -make test-mongo # MongoDB integration tests against local Mongo (requires Docker) +go vet ./... +go test -count=1 ./... +go build ./... ``` +CI additionally runs the test suite with Go's race detector. + ## Deploy [`docs/deploy-coolify-selfhosted.md`](docs/deploy-coolify-selfhosted.md) covers diff --git a/cmd/server/main.go b/cmd/server/main.go index 5c7cf23..df09e1e 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -7,6 +7,7 @@ import ( "net/http" "os" "os/signal" + "runtime/debug" "strconv" "strings" "syscall" @@ -32,15 +33,35 @@ import ( "github.com/tiennm99/miti99bot/internal/telegram" ) -// gitSHA is the local-build fallback, populated via `-ldflags "-X -// main.gitSHA="` (see Makefile). On Coolify the commit comes from the -// SOURCE_COMMIT runtime env instead (resolveCommitSHA prefers it). Empty from -// both sources means deploynotify stays silent. -var gitSHA string +// gitSHA is the local-build fallback read from the VCS metadata that go build +// embeds automatically. On Coolify the commit comes from SOURCE_COMMIT instead +// (resolveCommitSHA prefers it). +var gitSHA = buildCommitSHA() + +func buildCommitSHA() string { + info, ok := debug.ReadBuildInfo() + if !ok { + return "" + } + for _, setting := range info.Settings { + if setting.Key == "vcs.revision" { + return shortCommitSHA(setting.Value) + } + } + return "" +} + +func shortCommitSHA(value string) string { + value = strings.TrimSpace(value) + if len(value) > 7 { + return value[:7] + } + return value +} // resolveCommitSHA returns the commit identifier for the deploy notification. // Coolify injects SOURCE_COMMIT into the container environment at runtime, so -// prefer it; fall back to the ldflags-baked gitSHA for local builds; and when +// prefer it; fall back to Go's embedded VCS revision for local builds; and when // neither is set, report "unknown" so the owner still gets the startup DM. func resolveCommitSHA(envSourceCommit string) string { if s := strings.TrimSpace(envSourceCommit); s != "" { diff --git a/cmd/server/main_test.go b/cmd/server/main_test.go index 68bdaea..6bff6fd 100644 --- a/cmd/server/main_test.go +++ b/cmd/server/main_test.go @@ -20,7 +20,7 @@ func TestResolveCommitSHA(t *testing.T) { t.Errorf("env present: got %q, want trimmed runtime-sha", got) } - // Empty env falls back to the ldflags-baked value. + // Empty env falls back to the VCS revision embedded by go build. if got := resolveCommitSHA(""); got != "baked" { t.Errorf("env empty: got %q, want baked fallback", got) } @@ -32,6 +32,15 @@ func TestResolveCommitSHA(t *testing.T) { } } +func TestShortCommitSHA(t *testing.T) { + if got := shortCommitSHA(" 0123456789abcdef "); got != "0123456" { + t.Errorf("full revision: got %q, want %q", got, "0123456") + } + if got := shortCommitSHA("abc123"); got != "abc123" { + t.Errorf("short revision: got %q, want %q", got, "abc123") + } +} + func TestComposeDoesNotOverrideSourceCommit(t *testing.T) { b, err := os.ReadFile(filepath.Join("..", "..", "compose.yml")) if err != nil { diff --git a/docs/deploy-coolify-selfhosted.md b/docs/deploy-coolify-selfhosted.md index 2fbf07a..57af27f 100644 --- a/docs/deploy-coolify-selfhosted.md +++ b/docs/deploy-coolify-selfhosted.md @@ -135,11 +135,8 @@ Successful GIF replies include the result behind Telegram spoiler formatting. ## 3. Command menu The bot registers its Telegram command menu from loaded public modules on -startup. The manual target remains useful for repairs or local experiments: - -```sh -TELEGRAM_BOT_TOKEN=… make telegram-commands -``` +every startup. The Go module registry is the single source of truth; no separate +command-menu file or manual registration step is required. ## Operations @@ -147,23 +144,44 @@ The live deployment is the Coolify container and MongoDB is the sole system of record. Keep exactly one replica running. To confirm Telegram is in polling mode: ```sh -TELEGRAM_BOT_TOKEN=… make telegram-webhook-info +# POSIX shells (Linux/macOS) +curl "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getWebhookInfo" +``` + +```powershell +# PowerShell +Invoke-RestMethod "https://api.telegram.org/bot$env:TELEGRAM_BOT_TOKEN/getWebhookInfo" ``` `url` should be empty. If needed, clear the webhook explicitly: ```sh -TELEGRAM_BOT_TOKEN=… make telegram-deletewebhook +# POSIX shells (Linux/macOS) +curl -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/deleteWebhook" \ + --data "drop_pending_updates=false" +``` + +```powershell +# PowerShell +Invoke-RestMethod -Method Post -Uri "https://api.telegram.org/bot$env:TELEGRAM_BOT_TOKEN/deleteWebhook" -Body @{ drop_pending_updates = "false" } ``` ## Local smoke test +```powershell +# PowerShell +Copy-Item .env.example .env # fill TELEGRAM_BOT_TOKEN, MONGO_URL, MONGO_DATABASE +docker compose up --build +``` + ```sh -cp .env.example .env # fill TELEGRAM_BOT_TOKEN, MONGO_URL, MONGO_DATABASE +# POSIX shells (Linux/macOS) +cp .env.example .env # fill TELEGRAM_BOT_TOKEN, MONGO_URL, MONGO_DATABASE docker compose up --build ``` Boot logs should show `storage backend backend=mongodb database=…` (no connection string), `cron scheduler started`, and `telegram long polling -started`. `curl localhost:8080/` returns `miti99bot ok`. The bot's webhook must -be unset (the container clears it on startup) or `getUpdates` 409s. +started`. A request to `http://localhost:8080/` returns `miti99bot ok` (use +`Invoke-WebRequest` in PowerShell or `curl` in a POSIX shell). The bot's webhook +must be unset (the container clears it on startup) or `getUpdates` 409s. diff --git a/docs/journals/260721-0857-cross-platform-go-workflow.md b/docs/journals/260721-0857-cross-platform-go-workflow.md new file mode 100644 index 0000000..9867ac9 --- /dev/null +++ b/docs/journals/260721-0857-cross-platform-go-workflow.md @@ -0,0 +1,47 @@ +# Cross-Platform Go Workflow Journal + +## Context + +Removed Unix-oriented Make targets so Windows, macOS, and Linux contributors +can use the same standard Go workflow with platform-specific environment syntax. + +## What Changed + +- Deleted `Makefile` and replaced its development shortcuts with documented + `go test`, `go vet`, `go build`, `go run`, and direct Docker commands. +- Documented Telegram webhook inspection and cleanup with PowerShell + `Invoke-RestMethod` and POSIX `curl` examples. +- Deleted `telegram-commands.json`; the runtime module registry already builds + and registers the command menu on every startup, so the JSON duplicated the + authoritative Go definitions. +- Replaced Makefile linker flags with Go's embedded VCS build metadata and kept + the existing seven-character commit SHA behavior for deploy notifications. +- Updated CI and MongoDB test guidance to point contributors to the portable + README workflow. + +## Reflection + +Removing the task wrapper makes the repository less convenient for habitual +`make` users, but avoids maintaining shell-specific orchestration and duplicate +command-menu data. Standard Go, Docker, and HTTP tools keep each operation +explicit and work across supported development platforms. + +## Decisions + +- The Go module registry is the single source of truth for Telegram commands. +- Local binaries obtain their short revision from Go build information; the + deployment-provided `SOURCE_COMMIT` remains preferred at runtime. +- Platform differences are documented only where shell syntax or HTTP tooling + differs. + +## Verification + +- Passed: `go test ./...` +- Passed: `go vet ./...` +- Passed: `go build ./...` +- Unavailable: `golangci-lint run` because the binary is not installed. + +## Next Steps + +- Run the lint gate when `golangci-lint` is available. +- Keep command registration tests aligned with future public command changes. diff --git a/internal/storage/mongo_doc_store_test.go b/internal/storage/mongo_doc_store_test.go index c71c81a..408b03b 100644 --- a/internal/storage/mongo_doc_store_test.go +++ b/internal/storage/mongo_doc_store_test.go @@ -20,7 +20,7 @@ func mongoLocalSetup(t *testing.T) (*mongo.Database, func()) { t.Helper() uri := os.Getenv("MONGODB_TEST_URL") if uri == "" { - t.Skip("MONGODB_TEST_URL not set; skipping MongoDB integration test (run `make mongo-local` to start the local container)") + t.Skip("MONGODB_TEST_URL not set; skipping MongoDB integration test (see README.md for local MongoDB setup)") } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) diff --git a/telegram-commands.json b/telegram-commands.json deleted file mode 100644 index b90f320..0000000 --- a/telegram-commands.json +++ /dev/null @@ -1,164 +0,0 @@ -{ - "commands": [ - { - "command": "help", - "description": "Show all available commands" - }, - { - "command": "ping", - "description": "Health check; replies pong" - }, - { - "command": "random", - "description": "Pick one random comma-separated option" - }, - { - "command": "wheelofnames", - "description": "Pick one option with wheel GIF when configured" - }, - { - "command": "trongtruonghop", - "description": "Phát biểu disclaimer mặc định" - }, - { - "command": "tth", - "description": "Alias for /trongtruonghop" - }, - { - "command": "trongtruonghopvng", - "description": "Phát biểu disclaimer VNG mặc định" - }, - { - "command": "tthvng", - "description": "Alias for /trongtruonghopvng" - }, - { - "command": "wordle", - "description": "Classic wordle; guess the 5-letter word" - }, - { - "command": "wordle_new", - "description": "Start a new wordle round; active round counts as give-up" - }, - { - "command": "wordle_giveup", - "description": "Reveal the current wordle answer" - }, - { - "command": "wordle_stats", - "description": "Show your wordle stats" - }, - { - "command": "loldle", - "description": "Classic loldle; guess the champion" - }, - { - "command": "loldle_giveup", - "description": "Reveal the current loldle answer" - }, - { - "command": "loldle_stats", - "description": "Show your loldle stats" - }, - { - "command": "lol", - "description": "LoL matches for a date; supports dd, dd-mm, dd/mm, ddmm" - }, - { - "command": "lol_tomorrow", - "description": "LoL matches for tomorrow" - }, - { - "command": "lol_this_week", - "description": "LoL matches for this week" - }, - { - "command": "lol_next_week", - "description": "LoL matches for next week" - }, - { - "command": "lol_subscribe", - "description": "Get the daily LoL schedule digest" - }, - { - "command": "lol_unsubscribe", - "description": "Stop the daily LoL schedule digest" - }, - { - "command": "stock_price", - "description": "Show current VN stock price" - }, - { - "command": "stock_topup", - "description": "Top up VND to your stock account" - }, - { - "command": "stock_buy", - "description": "Buy VN stock at market price" - }, - { - "command": "stock_sell", - "description": "Sell VN stock back to VND" - }, - { - "command": "stock_cash_dividend", - "description": "Record cash dividend (VND/share TICKER)" - }, - { - "command": "stock_share_dividend", - "description": "Record share dividend (owned:new TICKER)" - }, - { - "command": "stock_dividend", - "description": "Record cash and share dividend" - }, - { - "command": "stock_portfolio", - "description": "Show stock portfolio with P&L" - }, - { - "command": "gold_price", - "description": "Show current SJC gold buy/sell price" - }, - { - "command": "gold_topup", - "description": "Top up VND to your gold account" - }, - { - "command": "gold_buy", - "description": "Buy gold at SJC sell price (luong)" - }, - { - "command": "gold_sell", - "description": "Sell gold at SJC buy price (luong)" - }, - { - "command": "gold_portfolio", - "description": "Show gold portfolio with P&L" - }, - { - "command": "coin_price", - "description": "Show current crypto price in USD" - }, - { - "command": "coin_topup", - "description": "Top up USD to your coin account" - }, - { - "command": "coin_buy", - "description": "Spend a USD amount to buy coin" - }, - { - "command": "coin_sell", - "description": "Sell enough coin to receive a USD amount" - }, - { - "command": "coin_portfolio", - "description": "Show coin portfolio with P&L" - }, - { - "command": "stats", - "description": "Stats. Try: /stats users, /stats user , /stats cmd " - } - ] -}