package main import ( "encoding/json" "fmt" "log" "os" "os/signal" "path/filepath" "time" "github.com/playwright-community/playwright-go" ) const ( roomURL = "https://app.kosmi.io/room/@hyperspaceout" logFile = "image-upload-capture.log" ) func main() { log.Println("šŸ” Starting Kosmi WebSocket Monitor (Image Upload Capture)") log.Printf("šŸ“” Room URL: %s", roomURL) log.Printf("šŸ“ Log file: %s", logFile) log.Println("This will capture ALL WebSocket traffic and HTTP requests...") log.Println() // Create log file f, err := os.Create(logFile) if err != nil { log.Fatalf("Failed to create log file: %v", err) } defer f.Close() // Set up interrupt handler interrupt := make(chan os.Signal, 1) signal.Notify(interrupt, os.Interrupt) // Helper to log to both console and file logBoth := func(format string, args ...interface{}) { msg := fmt.Sprintf(format, args...) log.Println(msg) fmt.Fprintf(f, "%s %s\n", time.Now().Format("15:04:05.000"), msg) } // Launch Playwright pw, err := playwright.Run() if err != nil { log.Fatalf("Failed to start Playwright: %v", err) } defer pw.Stop() // Launch browser browser, err := pw.Chromium.Launch(playwright.BrowserTypeLaunchOptions{ Headless: playwright.Bool(false), // Keep visible so we can see what's happening }) if err != nil { log.Fatalf("Failed to launch browser: %v", err) } defer browser.Close() // Create context with permissions context, err := browser.NewContext(playwright.BrowserNewContextOptions{ UserAgent: playwright.String("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"), AcceptDownloads: playwright.Bool(true), // Grant all permissions to avoid upload blocking Permissions: []string{"clipboard-read", "clipboard-write"}, }) if err != nil { log.Fatalf("Failed to create context: %v", err) } // Grant permissions for the Kosmi domain if err := context.GrantPermissions([]string{"clipboard-read", "clipboard-write"}, playwright.BrowserContextGrantPermissionsOptions{ Origin: playwright.String("https://app.kosmi.io"), }); err != nil { logBoth("āš ļø Warning: Failed to grant permissions: %v", err) } // Listen to all network requests context.On("request", func(request playwright.Request) { logBoth("🌐 [HTTP REQUEST] %s %s", request.Method(), request.URL()) postData, err := request.PostData() if err == nil && postData != "" { logBoth(" POST Data: %s", postData) } }) context.On("response", func(response playwright.Response) { status := response.Status() url := response.URL() logBoth("šŸ“Ø [HTTP RESPONSE] %d %s", status, url) // Try to get response body for file uploads if status >= 200 && status < 300 { body, err := response.Body() if err == nil && len(body) > 0 && len(body) < 10000 { // Log if it looks like JSON var jsonData interface{} if json.Unmarshal(body, &jsonData) == nil { prettyJSON, _ := json.MarshalIndent(jsonData, " ", " ") logBoth(" Response Body: %s", string(prettyJSON)) } } } }) // Create page page, err := context.NewPage() if err != nil { log.Fatalf("Failed to create page: %v", err) } // Inject WebSocket monitoring script BEFORE navigation logBoth("šŸ“ Injecting WebSocket and file upload monitoring script...") if err := page.AddInitScript(playwright.Script{ Content: playwright.String(` (function() { const OriginalWebSocket = window.WebSocket; let messageCount = 0; window.WebSocket = function(url, protocols) { console.log('šŸ”Œ [WS MONITOR] WebSocket created:', url, 'protocols:', protocols); const socket = new OriginalWebSocket(url, protocols); socket.addEventListener('open', (event) => { console.log('āœ… [WS MONITOR] WebSocket OPENED'); }); socket.addEventListener('close', (event) => { console.log('šŸ”“ [WS MONITOR] WebSocket CLOSED:', event.code, event.reason); }); socket.addEventListener('error', (event) => { console.error('āŒ [WS MONITOR] WebSocket ERROR:', event); }); // Intercept outgoing messages const originalSend = socket.send; socket.send = function(data) { messageCount++; // Check if binary data if (data instanceof ArrayBuffer || data instanceof Blob) { console.log('šŸ“¤ [WS MONITOR] SEND #' + messageCount + ': [BINARY DATA]', 'Type:', data.constructor.name, 'Size:', data.byteLength || data.size); } else { console.log('šŸ“¤ [WS MONITOR] SEND #' + messageCount + ':', data); try { const parsed = JSON.parse(data); console.log(' Type:', parsed.type, 'ID:', parsed.id); if (parsed.payload) { console.log(' Payload:', JSON.stringify(parsed.payload, null, 2)); } } catch (e) { // Not JSON } } return originalSend.call(this, data); }; // Intercept incoming messages socket.addEventListener('message', (event) => { messageCount++; // Check if binary data if (event.data instanceof ArrayBuffer || event.data instanceof Blob) { console.log('šŸ“„ [WS MONITOR] RECEIVE #' + messageCount + ': [BINARY DATA]', 'Type:', event.data.constructor.name, 'Size:', event.data.byteLength || event.data.size); } else { console.log('šŸ“„ [WS MONITOR] RECEIVE #' + messageCount + ':', event.data); try { const parsed = JSON.parse(event.data); console.log(' Type:', parsed.type, 'ID:', parsed.id); if (parsed.payload) { console.log(' Payload:', JSON.stringify(parsed.payload, null, 2)); } } catch (e) { // Not JSON } } }); return socket; }; // Preserve WebSocket properties window.WebSocket.prototype = OriginalWebSocket.prototype; window.WebSocket.CONNECTING = OriginalWebSocket.CONNECTING; window.WebSocket.OPEN = OriginalWebSocket.OPEN; window.WebSocket.CLOSING = OriginalWebSocket.CLOSING; window.WebSocket.CLOSED = OriginalWebSocket.CLOSED; })(); `), }); err != nil { log.Fatalf("Failed to inject script: %v", err) } // Listen to console messages page.On("console", func(msg playwright.ConsoleMessage) { text := msg.Text() msgType := msg.Type() // Format the output nicely prefix := "šŸ’¬" switch msgType { case "log": prefix = "šŸ“‹" case "error": prefix = "āŒ" case "warning": prefix = "āš ļø" case "info": prefix = "ā„¹ļø" } logBoth("%s [BROWSER %s] %s", prefix, msgType, text) }) // Listen to file chooser events page.On("filechooser", func(fileChooser playwright.FileChooser) { logBoth("šŸ“ [FILE CHOOSER] File dialog opened!") logBoth(" Multiple: %v", fileChooser.IsMultiple()) // Try to set the test image absPath, err := filepath.Abs("blurt.jpg") if err != nil { logBoth(" āŒ Failed to get absolute path: %v", err) return } logBoth(" šŸ“Ž Setting file: %s", absPath) if err := fileChooser.SetFiles(absPath, playwright.FileChooserSetFilesOptions{}); err != nil { logBoth(" āŒ Failed to set file: %v", err) } else { logBoth(" āœ… File set successfully!") } }) // Navigate to room logBoth("🌐 Navigating to %s...", roomURL) if _, err := page.Goto(roomURL, playwright.PageGotoOptions{ WaitUntil: playwright.WaitUntilStateDomcontentloaded, }); err != nil { log.Fatalf("Failed to navigate: %v", err) } logBoth("āœ… Page loaded! Monitoring WebSocket traffic and HTTP requests...") logBoth("šŸ“ø Please manually upload the test image (blurt.jpg) in the Kosmi chat") logBoth(" Look for the attachment/upload button in the chat interface") logBoth("Press Ctrl+C when done to stop monitoring") logBoth("") // Wait for a bit to see initial traffic time.Sleep(5 * time.Second) // Try to trigger file upload programmatically (fallback to manual) logBoth("\nšŸ“Ž Attempting to trigger file upload dialog...") result, err := page.Evaluate(` (async function() { // Try to find file input or upload button const fileInputs = document.querySelectorAll('input[type="file"]'); if (fileInputs.length > 0) { console.log('Found', fileInputs.length, 'file inputs'); fileInputs[0].click(); return { success: true, method: 'file-input' }; } // Try to find buttons with upload-related text/icons const buttons = document.querySelectorAll('button, [role="button"]'); for (let btn of buttons) { const text = btn.textContent.toLowerCase(); const ariaLabel = btn.getAttribute('aria-label')?.toLowerCase() || ''; if (text.includes('upload') || text.includes('attach') || text.includes('file') || ariaLabel.includes('upload') || ariaLabel.includes('attach') || ariaLabel.includes('file')) { console.log('Found upload button:', btn.textContent, ariaLabel); btn.click(); return { success: true, method: 'upload-button' }; } } return { success: false, error: 'No upload button found' }; })(); `) if err != nil { logBoth("āŒ Failed to trigger upload: %v", err) logBoth("āš ļø FALLBACK: Please manually click the upload/attachment button in Kosmi") } else { resultMap := result.(map[string]interface{}) if success, ok := resultMap["success"].(bool); ok && success { method := resultMap["method"].(string) logBoth("āœ… Triggered upload via %s", method) } else { logBoth("āŒ Could not find upload button: %v", resultMap["error"]) logBoth("āš ļø FALLBACK: Please manually click the upload/attachment button in Kosmi") } } // Keep monitoring logBoth("\nā³ Monitoring all traffic... Upload an image in Kosmi, then press Ctrl+C to stop") logBoth("šŸ’” TIP: If upload doesn't work in this browser, just note what you see in a normal browser") logBoth(" We mainly need to see the HTTP requests and WebSocket messages") // Wait for interrupt with proper cleanup <-interrupt logBoth("\nšŸ‘‹ Stopping monitor...") // Close browser and context to stop all goroutines if page != nil { page.Close() } if context != nil { context.Close() } if browser != nil { browser.Close() } // Give goroutines time to finish time.Sleep(500 * time.Millisecond) logBoth("āœ… Monitor stopped. Check %s for captured traffic", logFile) }