From 98eeb2e2ba3421f09efaec9df55bba8c65a8a1cc Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 2 Aug 2026 00:04:53 +0000 Subject: [PATCH] Add standalone wsdebug tool for inspecting the raw Sneedchat WebSocket stream Loads the same .env credentials as the bridge, connects to the Sneedchat WebSocket independent of Discord, and prints every raw frame to stdout while also appending it to a log file. Adds an OnRaw hook to sneed.Client so the full, untruncated frame is available to callers. --- .gitignore | 1 + README.md | 25 ++++++++++++ cmd/wsdebug/main.go | 93 +++++++++++++++++++++++++++++++++++++++++++++ sneed/client.go | 9 +++++ 4 files changed, 128 insertions(+) create mode 100644 cmd/wsdebug/main.go diff --git a/.gitignore b/.gitignore index 1b433ec..5ed6fbd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ Sneedchat-Discord-Bridge file.log +wsdebug.log diff --git a/README.md b/README.md index 72d5472..a716513 100644 --- a/README.md +++ b/README.md @@ -245,6 +245,31 @@ sudo systemctl status sneedchat-bridge --- +## Debugging Tools + +### wsdebug — standalone WebSocket inspector + +`wsdebug` is a separate binary that logs into Kiwi Farms using the same `.env` file as the bridge, connects directly to the Sneedchat WebSocket, and prints every raw frame it receives to stdout while also writing it to a log file. It doesn't touch Discord at all — use it to see exactly what Sneedchat is sending, independent of the bridge. + +Build it: + +```bash +go build -o wsdebug ./cmd/wsdebug +``` + +Run it: + +```bash +./wsdebug --env .env --log wsdebug.log --room 1 +``` + +Flags: +- `--env` - path to the `.env` file to load (default `.env`) +- `--log` - path to the log file to append output to (default `wsdebug.log`) +- `--room` - override `SNEEDCHAT_ROOM_ID` from the `.env` file (optional) + +Press `Ctrl+C` to stop. Every raw WebSocket frame, plus connect/disconnect/message/edit/delete events, are printed to the terminal and appended to the log file simultaneously. + ## Troubleshooting ### Cookie Issues diff --git a/cmd/wsdebug/main.go b/cmd/wsdebug/main.go new file mode 100644 index 0000000..f349803 --- /dev/null +++ b/cmd/wsdebug/main.go @@ -0,0 +1,93 @@ +// Command wsdebug is a standalone debugging tool that logs into Kiwi Farms +// using the same .env credentials as the bridge, opens the Sneedchat +// WebSocket connection, and prints/logs every raw frame received. It does +// not touch Discord at all — it exists purely to inspect what the +// WebSocket is sending. +package main + +import ( + "context" + "flag" + "fmt" + "io" + "log" + "os" + "os/signal" + "syscall" + "time" + + "local/sneedchatbridge/config" + "local/sneedchatbridge/cookie" + "local/sneedchatbridge/sneed" +) + +func main() { + envFile := flag.String("env", ".env", "path to .env file") + logFile := flag.String("log", "wsdebug.log", "path to log file") + room := flag.Int("room", 0, "override SNEEDCHAT_ROOM_ID from .env") + flag.Parse() + + cfg, err := config.Load(*envFile) + if err != nil { + log.Fatalf("Failed to load config: %v", err) + } + if *room != 0 { + cfg.SneedchatRoomID = *room + } + + f, err := os.OpenFile(*logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + log.Fatalf("Failed to open log file %s: %v", *logFile, err) + } + defer f.Close() + + logger := log.New(io.MultiWriter(os.Stdout, f), "", log.LstdFlags|log.Lmicroseconds) + log.SetOutput(io.MultiWriter(os.Stdout, f)) + log.SetFlags(log.LstdFlags | log.Lmicroseconds) + + logger.Printf("wsdebug starting — env=%s log=%s room=%d", *envFile, *logFile, cfg.SneedchatRoomID) + + ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer cancel() + + session, err := cookie.NewSessionService(ctx, "kiwifarms.st", cfg.BridgeUsername, cfg.BridgePassword) + if err != nil { + log.Fatalf("Failed to establish session: %v", err) + } + defer session.Close() + + client := sneed.NewClient(cfg.SneedchatRoomID, session, true) + client.SetBridgeIdentity(cfg.BridgeUserID, cfg.BridgeUsername) + + client.OnRaw = func(raw string) { + logger.Printf("RECV: %s", raw) + } + client.OnConnect = func() { + logger.Printf("=== CONNECTED (room %d) ===", cfg.SneedchatRoomID) + } + client.OnDisconnect = func() { + logger.Printf("=== DISCONNECTED ===") + } + client.OnMessage = func(m map[string]interface{}) { + logger.Printf("MESSAGE: user=%v content=%q uuid=%v", m["username"], m["content"], m["message_uuid"]) + } + client.OnEdit = func(uuid, content string) { + logger.Printf("EDIT: uuid=%s content=%q", uuid, content) + } + client.OnDelete = func(uuid string) { + logger.Printf("DELETE: uuid=%s", uuid) + } + + if err := client.Connect(ctx); err != nil { + log.Fatalf("Initial connect failed: %v", err) + } + + logger.Println("wsdebug running — press Ctrl+C to stop") + + <-ctx.Done() + stopTime := time.Now().Format(time.RFC3339) + fmt.Fprintf(f, "--- shutdown requested at %s ---\n", stopTime) + logger.Println("Shutdown signal received, disconnecting...") + client.Disconnect() + logger.Println("wsdebug stopped") +} diff --git a/sneed/client.go b/sneed/client.go index 6d4710d..a52591e 100644 --- a/sneed/client.go +++ b/sneed/client.go @@ -63,6 +63,11 @@ type Client struct { OnConnect func() OnDisconnect func() + // OnRaw, if set, is called with every raw WebSocket frame exactly as + // received, before any parsing or truncation. Intended for debugging + // tools that need to observe the unfiltered stream. + OnRaw func(string) + recentOutboundIter func() []map[string]interface{} mapDiscordSneed func(string, int, string) @@ -197,6 +202,10 @@ func (c *Client) readLoop(ctx context.Context) { raw := string(message) + if c.OnRaw != nil { + c.OnRaw(raw) + } + // Server sends plaintext "cannot join" when session has expired. if isCannotJoin(raw) { log.Println("⚠️ 'cannot join' received — refreshing session and reconnecting...")