wow that took awhile

This commit is contained in:
cottongin
2025-11-01 10:40:53 -04:00
parent 9143a0bc60
commit bd9513b86c
44 changed files with 4484 additions and 76 deletions

111
cmd/monitor-ws/README.md Normal file
View File

@@ -0,0 +1,111 @@
# Kosmi WebSocket Monitor - Image Upload Capture
This tool captures ALL WebSocket and HTTP traffic from a Kosmi chat session, specifically designed to reverse-engineer the image upload protocol.
## ⚠️ Known Issue
The Playwright browser may have restrictions that prevent file uploads. If uploads don't work in the automated browser, **use the manual capture method instead** (see `CAPTURE_UPLOAD_MANUALLY.md`).
## Prerequisites
- `blurt.jpg` must exist in the project root directory (test image for upload)
- Playwright must be installed: `go run github.com/playwright-community/playwright-go/cmd/playwright@latest install`
## Running the Monitor
```bash
cd /Users/erikfredericks/dev-ai/HSO/irc-kosmi-relay
./monitor-ws
```
**Note:** Press Ctrl+C to stop (now properly handles cleanup).
## What It Does
1. Opens a browser window to https://app.kosmi.io/room/@hyperspaceout
2. Captures all WebSocket messages (text and binary)
3. Captures all HTTP requests and responses
4. Logs everything to console AND `image-upload-capture.log`
5. Attempts to trigger file upload dialog automatically
6. If auto-trigger fails, waits for you to manually upload an image
## Manual Steps
1. Run the monitor
2. Wait for the browser to open and load Kosmi
3. Look for the attachment/upload button in the chat interface (usually a paperclip or + icon)
4. Click it and select `blurt.jpg` from the project root
5. Wait for the upload to complete
6. Press Ctrl+C to stop monitoring
## What to Look For
### In the Console/Log File:
**HTTP Requests:**
- Look for POST or PUT requests to upload endpoints
- Common patterns: `/upload`, `/media`, `/file`, `/attachment`, `/image`
- Note the URL, headers, and request body format
**HTTP Responses:**
- Look for JSON responses containing image URLs
- Note the URL structure (CDN, S3, etc.)
**WebSocket Messages:**
- Look for GraphQL mutations related to file upload
- Mutation names might include: `uploadFile`, `uploadImage`, `sendMedia`, `attachFile`
- Note the mutation structure, variables, and response format
- Check for binary WebSocket frames (image data sent directly)
**Example patterns to look for:**
```graphql
mutation UploadFile($file: Upload!, $roomId: String!) {
uploadFile(file: $file, roomId: $roomId) {
url
id
}
}
```
Or HTTP multipart form-data:
```
POST /api/upload
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary...
Authorization: Bearer <token>
------WebKitFormBoundary...
Content-Disposition: form-data; name="file"; filename="blurt.jpg"
Content-Type: image/jpeg
<binary data>
------WebKitFormBoundary...--
```
## Output
All traffic is logged to:
- Console (real-time)
- `image-upload-capture.log` (persistent)
The log file includes timestamps and is easier to search through after the capture is complete.
## Alternative: Manual Capture
If the automated browser doesn't allow uploads, use your normal browser instead:
**See `CAPTURE_UPLOAD_MANUALLY.md` for detailed instructions.**
Quick summary:
1. Open Chrome DevTools (F12)
2. Go to Network tab, check "Preserve log"
3. Upload an image in Kosmi
4. Find the upload request
5. Copy request/response details
## Next Steps
After capturing the upload flow:
1. Analyze the log file (or manual capture) to identify the upload method
2. Document findings in `KOSMI_IMAGE_UPLOAD.md`
3. Implement the upload client in Go based on the findings

View File

@@ -1,9 +1,12 @@
package main
import (
"encoding/json"
"fmt"
"log"
"os"
"os/signal"
"path/filepath"
"time"
"github.com/playwright-community/playwright-go"
@@ -11,17 +14,33 @@ import (
const (
roomURL = "https://app.kosmi.io/room/@hyperspaceout"
logFile = "image-upload-capture.log"
)
func main() {
log.Println("🔍 Starting Kosmi WebSocket Monitor")
log.Println("🔍 Starting Kosmi WebSocket Monitor (Image Upload Capture)")
log.Printf("📡 Room URL: %s", roomURL)
log.Println("This will capture ALL WebSocket traffic from the browser...")
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()
@@ -39,13 +58,51 @@ func main() {
}
defer browser.Close()
// Create context
// 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()
@@ -54,7 +111,7 @@ func main() {
}
// Inject WebSocket monitoring script BEFORE navigation
log.Println("📝 Injecting WebSocket monitoring script...")
logBoth("📝 Injecting WebSocket and file upload monitoring script...")
if err := page.AddInitScript(playwright.Script{
Content: playwright.String(`
(function() {
@@ -81,15 +138,22 @@ func main() {
const originalSend = socket.send;
socket.send = function(data) {
messageCount++;
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));
// 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
}
} catch (e) {
// Not JSON
}
return originalSend.call(this, data);
};
@@ -97,15 +161,22 @@ func main() {
// Intercept incoming messages
socket.addEventListener('message', (event) => {
messageCount++;
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));
// 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
}
} catch (e) {
// Not JSON
}
});
@@ -142,85 +213,112 @@ func main() {
prefix = ""
}
log.Printf("%s [BROWSER %s] %s", prefix, msgType, text)
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
log.Printf("🌐 Navigating to %s...", roomURL)
logBoth("🌐 Navigating to %s...", roomURL)
if _, err := page.Goto(roomURL, playwright.PageGotoOptions{
WaitUntil: playwright.WaitUntilStateDomcontentloaded,
}); err != nil {
log.Fatalf("Failed to navigate: %v", err)
}
log.Println("✅ Page loaded! Monitoring WebSocket traffic...")
log.Println("Press Ctrl+C to stop monitoring")
log.Println()
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 send a test message
log.Println("\n📝 Attempting to send a test message via UI...")
// Try to trigger file upload programmatically (fallback to manual)
logBoth("\n📎 Attempting to trigger file upload dialog...")
result, err := page.Evaluate(`
(async function() {
// Find the chat input
const textareas = document.querySelectorAll('textarea');
for (let ta of textareas) {
if (ta.offsetParent !== null) {
ta.value = 'Test message from monitor script 🔍';
ta.dispatchEvent(new Event('input', { bubbles: true }));
ta.dispatchEvent(new Event('change', { bubbles: true }));
ta.focus();
// Wait a bit
await new Promise(resolve => setTimeout(resolve, 100));
// Try to find and click send button
const buttons = document.querySelectorAll('button');
for (let btn of buttons) {
if (btn.textContent.toLowerCase().includes('send') ||
btn.getAttribute('aria-label')?.toLowerCase().includes('send')) {
btn.click();
return { success: true, method: 'button' };
}
}
// If no button, press Enter
const enterEvent = new KeyboardEvent('keydown', {
key: 'Enter',
code: 'Enter',
keyCode: 13,
which: 13,
bubbles: true,
cancelable: true
});
ta.dispatchEvent(enterEvent);
return { success: true, method: 'enter' };
// 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 input found' };
return { success: false, error: 'No upload button found' };
})();
`)
if err != nil {
log.Printf("❌ Failed to send test message: %v", err)
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)
log.Printf("✅ Test message sent via %s", method)
log.Println("📊 Check the console output above to see the WebSocket traffic!")
logBoth("✅ Triggered upload via %s", method)
} else {
log.Printf("❌ Failed to send: %v", resultMap["error"])
logBoth("❌ Could not find upload button: %v", resultMap["error"])
logBoth("⚠️ FALLBACK: Please manually click the upload/attachment button in Kosmi")
}
}
// Keep monitoring
log.Println("\n⏳ Continuing to monitor... Press Ctrl+C to stop")
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
// Wait for interrupt with proper cleanup
<-interrupt
log.Println("\n👋 Stopping monitor...")
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)
}

View File

@@ -0,0 +1,38 @@
package main
import (
"fmt"
"log"
bkosmi "github.com/42wim/matterbridge/bridge/kosmi"
"github.com/42wim/matterbridge/bridge/jackbox"
)
func main() {
fmt.Println("=== Kosmi Image Upload Test ===\n")
// Test 1: Generate a room code image
fmt.Println("1. Generating room code image for 'TEST'...")
imageData, err := jackbox.GenerateRoomCodeImage("TEST")
if err != nil {
log.Fatalf("Failed to generate image: %v", err)
}
fmt.Printf(" ✓ Generated image (%d bytes)\n\n", len(imageData))
// Test 2: Upload the image to Kosmi CDN
fmt.Println("2. Uploading image to Kosmi CDN (https://img.kosmi.io/)...")
imageURL, err := bkosmi.UploadImage(imageData, "roomcode_TEST.png")
if err != nil {
log.Fatalf("Failed to upload image: %v", err)
}
fmt.Printf(" ✓ Upload successful!\n\n")
// Test 3: Display the result
fmt.Println("=== RESULT ===")
fmt.Printf("Image URL: %s\n\n", imageURL)
fmt.Println("Next steps:")
fmt.Println("1. Open the URL in your browser to verify the image")
fmt.Println("2. Send this URL to Kosmi chat via WebSocket")
fmt.Println("3. Kosmi will display it as a thumbnail")
}

View File

@@ -0,0 +1,41 @@
package main
import (
"fmt"
"log"
"os"
"github.com/42wim/matterbridge/bridge/jackbox"
)
func main() {
testCases := []struct {
gameTitle string
roomCode string
}{
{"Quiplash 3", "ABCD"},
{"Drawful 2", "XYZ123"},
{"Fibbage XL", "TEST"},
{"Trivia Murder Party", "ROOM42"},
}
for _, tc := range testCases {
log.Printf("Generating image for: %s - %s", tc.gameTitle, tc.roomCode)
imageData, err := jackbox.GenerateRoomCodeImage(tc.roomCode, tc.gameTitle)
if err != nil {
log.Fatalf("Failed to generate image for %s: %v", tc.roomCode, err)
}
filename := fmt.Sprintf("roomcode_%s.gif", tc.roomCode)
if err := os.WriteFile(filename, imageData, 0644); err != nil {
log.Fatalf("Failed to write image file %s: %v", filename, err)
}
log.Printf("✅ Generated %s (%d bytes)", filename, len(imageData))
}
log.Println("\n🎉 All room code images generated successfully!")
log.Println("Check the current directory for roomcode_*.gif files")
}

41
cmd/test-upload/main.go Normal file
View File

@@ -0,0 +1,41 @@
package main
import (
"fmt"
"log"
"os"
"github.com/42wim/matterbridge/bridge/jackbox"
"github.com/sirupsen/logrus"
)
func main() {
// Enable debug logging
logrus.SetLevel(logrus.DebugLevel)
// Generate a test image
log.Println("Generating test room code image...")
imageData, err := jackbox.GenerateRoomCodeImage("TEST", "Quiplash 3")
if err != nil {
log.Fatalf("Failed to generate image: %v", err)
}
log.Printf("Generated %d bytes of GIF data", len(imageData))
// Save locally for verification
if err := os.WriteFile("test_upload.gif", imageData, 0644); err != nil {
log.Fatalf("Failed to save test file: %v", err)
}
log.Println("Saved test_upload.gif locally")
// Upload to Kosmi
log.Println("Uploading to Kosmi CDN...")
url, err := jackbox.UploadImageToKosmi(imageData, "test_roomcode.gif")
if err != nil {
log.Fatalf("Upload failed: %v", err)
}
fmt.Printf("\n✅ SUCCESS! Uploaded to: %s\n", url)
fmt.Println("\nPlease check the URL in your browser to verify it's animated!")
}