feat(go-services): discord-go — Go port of discord-rare-monitor
Consumes the Python tracker's /ws/live firehose (subscribed to rare+chat), classifies rares common/great, posts embeds + relays allegiance chat. - classify.go: the 74-name common-rares set, extracted verbatim from the Python COMMON_RARES_PATTERN (not hand-transcribed). go test runs at build time; a server-side dump-rares vs the Python regex confirms the sets are IDENTICAL. - poster.go: a `poster` interface with a real discordgo impl (REST sends by channel id; gold/blue embeds, location/time fields, icon attachment) and a dry-run log impl. - ws.go: coder/websocket client to /ws/live with subscribe, ping keepalive, exponential-backoff reconnect; rare/chat dispatch incl. vortex-warning + the MONITOR_CHARACTER filter. - SAFE BY DEFAULT: dry-run unless a token AND DRY_RUN=0 are set, so it can never double-post to production. Deployed via the compose override (discord-rare-monitor-go), running dry-run against the same live firehose. Validated on the server: connects, subscribes, relays a real chat in dry-run; classifier parity 74/74 vs the Python regex. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
426fe025d3
commit
8d4c6ff68f
8 changed files with 593 additions and 0 deletions
90
go-services/discord-go/main.go
Normal file
90
go-services/discord-go/main.go
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
// Command discord-go is a Go port of discord-rare-monitor: it consumes the
|
||||
// tracker's /ws/live firehose (subscribed to rare+chat), classifies rares
|
||||
// common/great, posts embeds to Discord, and relays allegiance chat.
|
||||
//
|
||||
// SAFETY: it runs in dry-run (log only, no Discord posts) by default. Going live
|
||||
// requires BOTH a bot token AND DRY_RUN=0 — so it can never accidentally
|
||||
// double-post to the production channels during the parallel run. For a parallel
|
||||
// test, set a TEST token + TEST channel IDs + DRY_RUN=0.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sort"
|
||||
"syscall"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// `discord-go dump-rares` prints the common-rares set (for parity diffing
|
||||
// against the Python regex). No network, no token.
|
||||
if len(os.Args) > 1 && os.Args[1] == "dump-rares" {
|
||||
names := make([]string, 0, len(commonRares))
|
||||
for n := range commonRares {
|
||||
names = append(names, n)
|
||||
}
|
||||
sort.Strings(names)
|
||||
for _, n := range names {
|
||||
fmt.Println(n)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
|
||||
slog.SetDefault(logger)
|
||||
|
||||
token := os.Getenv("DISCORD_RARE_BOT_TOKEN")
|
||||
// Dry-run unless a token is present AND DRY_RUN is explicitly "0".
|
||||
dryRun := token == "" || os.Getenv("DRY_RUN") != "0"
|
||||
|
||||
wsURL := envOr("DERETH_TRACKER_WS_URL", "ws://dereth-tracker:8765/ws/live")
|
||||
monitorChar := envOr("MONITOR_CHARACTER", "Dunking Rares")
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
var out poster
|
||||
if dryRun {
|
||||
reason := "no DISCORD_RARE_BOT_TOKEN"
|
||||
if token != "" {
|
||||
reason = "DRY_RUN != 0"
|
||||
}
|
||||
logger.Info("starting in DRY-RUN — classifying but NOT posting to Discord", "reason", reason, "ws", wsURL, "monitor", monitorChar)
|
||||
out = &logPoster{log: logger}
|
||||
} else {
|
||||
dg, err := discordgo.New("Bot " + token)
|
||||
if err != nil {
|
||||
logger.Error("discord session init failed", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
// REST-only: we send by channel ID, so no gateway Open()/intents needed.
|
||||
logger.Info("starting LIVE — posting to Discord", "ws", wsURL, "monitor", monitorChar)
|
||||
out = &discordPoster{
|
||||
dg: dg,
|
||||
common: envOr("COMMON_RARE_CHANNEL_ID", "1355328792184226014"),
|
||||
great: envOr("GREAT_RARE_CHANNEL_ID", "1353676584334131211"),
|
||||
aclog: envOr("ACLOG_CHANNEL_ID", "1349649482786275328"),
|
||||
sawato: envOr("SAWATOLIFE_CHANNEL_ID", "1387323032271327423"),
|
||||
iconsDir: envOr("ICONS_DIR", "icons"),
|
||||
log: logger,
|
||||
}
|
||||
}
|
||||
|
||||
b := &bot{wsURL: wsURL, monitorChar: monitorChar, out: out, log: logger}
|
||||
go b.run(ctx)
|
||||
|
||||
<-ctx.Done()
|
||||
logger.Info("shutdown signal received")
|
||||
}
|
||||
|
||||
func envOr(key, def string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue