mirror of
https://github.com/tiennm99/miti99bot.git
synced 2026-09-04 14:20:39 +00:00
refactor: adopt cross-platform Go development workflow
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -25,7 +25,6 @@ deleting commands, update all related surfaces:
|
||||
|
||||
- module command registration in `internal/modules/<module>/`
|
||||
- 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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
+27
-6
@@ -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=<sha>"` (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 != "" {
|
||||
|
||||
+10
-1
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
|
||||
@@ -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 <username>, /stats cmd <command>"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user