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>
90 lines
2.7 KiB
Go
90 lines
2.7 KiB
Go
// 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
|
|
}
|