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...")