proof-of-concept worked. rolling into main
This commit is contained in:
64
WEBSOCKET_FINDINGS.md
Normal file
64
WEBSOCKET_FINDINGS.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# Kosmi WebSocket Protocol Findings
|
||||||
|
|
||||||
|
## Key Discoveries from Browser Monitoring
|
||||||
|
|
||||||
|
### Room ID Format
|
||||||
|
- Browser uses `"@hyperspaceout"` (WITH @ symbol) for all operations
|
||||||
|
- Our script was using `"hyperspaceout"` (WITHOUT @) - this is why `joinRoom` failed with `ROOM_NOT_FOUND`
|
||||||
|
|
||||||
|
### Page Load Sequence
|
||||||
|
The browser does NOT call `joinRoom` mutation. Instead, it:
|
||||||
|
|
||||||
|
1. **Connects to WebSocket** at `wss://engine.kosmi.io/gql-ws`
|
||||||
|
2. **Sends `connection_init`** with auth token
|
||||||
|
3. **Immediately starts subscribing** to various topics:
|
||||||
|
- `OnNewMessage(roomId: "@hyperspaceout")` - for chat messages
|
||||||
|
- `OnLinkedMembers(roomId: "@hyperspaceout")` - for room members
|
||||||
|
- `OnMediaPlayerUpdateState(roomId: "@hyperspaceout")` - for media player
|
||||||
|
- `OnMediaPlayerUpdateSubtitles(roomId: "@hyperspaceout")` - for subtitles
|
||||||
|
- `OnMediasoupUpdate(roomId: "@hyperspaceout")` - for WebRTC
|
||||||
|
- `OnRoomUpdate(roomId: "@hyperspaceout")` - for room state
|
||||||
|
- `OnMessageReadersUpdate(roomId: "@hyperspaceout")` - for read receipts
|
||||||
|
|
||||||
|
### Sending Messages
|
||||||
|
To send a message, the browser likely uses:
|
||||||
|
```graphql
|
||||||
|
mutation SendMessage($body: String!, $roomId: String!) {
|
||||||
|
sendMessage(body: $body, roomId: $roomId) {
|
||||||
|
ok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
With variables:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"body": "message text",
|
||||||
|
"roomId": "@hyperspaceout"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
The `connection_init` payload includes:
|
||||||
|
- `token`: JWT from `anonLogin` mutation
|
||||||
|
- `ua`: Base64-encoded User-Agent
|
||||||
|
- `v`: App version "4364"
|
||||||
|
- `r`: Empty string for anonymous users
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. ✅ Use `"@hyperspaceout"` (with @) instead of `"hyperspaceout"`
|
||||||
|
2. ✅ Skip the `joinRoom` mutation entirely
|
||||||
|
3. ✅ Just send the `sendMessage` mutation directly after `connection_ack`
|
||||||
|
4. Test if the message appears in chat
|
||||||
|
|
||||||
|
## Important Note
|
||||||
|
|
||||||
|
The `sendMessage` mutation returns `{ ok: true }` even when the message doesn't appear in chat. This suggests that:
|
||||||
|
- The mutation succeeds on the server side
|
||||||
|
- But the message might be filtered or not broadcast
|
||||||
|
- Possibly because we're not "subscribed" to the room's message feed
|
||||||
|
- Or because anonymous users have restrictions
|
||||||
|
|
||||||
|
We should subscribe to `OnNewMessage` to see if our sent messages come back through the subscription.
|
||||||
|
|
||||||
226
cmd/monitor-ws/main.go
Normal file
226
cmd/monitor-ws/main.go
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/playwright-community/playwright-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
roomURL = "https://app.kosmi.io/room/@hyperspaceout"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log.Println("🔍 Starting Kosmi WebSocket Monitor")
|
||||||
|
log.Printf("📡 Room URL: %s", roomURL)
|
||||||
|
log.Println("This will capture ALL WebSocket traffic from the browser...")
|
||||||
|
log.Println()
|
||||||
|
|
||||||
|
// Set up interrupt handler
|
||||||
|
interrupt := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(interrupt, os.Interrupt)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
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"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create context: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create page
|
||||||
|
page, err := context.NewPage()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create page: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject WebSocket monitoring script BEFORE navigation
|
||||||
|
log.Println("📝 Injecting WebSocket 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++;
|
||||||
|
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++;
|
||||||
|
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 = "ℹ️"
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("%s [BROWSER %s] %s", prefix, msgType, text)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Navigate to room
|
||||||
|
log.Printf("🌐 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()
|
||||||
|
|
||||||
|
// 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...")
|
||||||
|
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' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { success: false, error: 'No input found' };
|
||||||
|
})();
|
||||||
|
`)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("❌ Failed to send test message: %v", err)
|
||||||
|
} 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!")
|
||||||
|
} else {
|
||||||
|
log.Printf("❌ Failed to send: %v", resultMap["error"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep monitoring
|
||||||
|
log.Println("\n⏳ Continuing to monitor... Press Ctrl+C to stop")
|
||||||
|
|
||||||
|
// Wait for interrupt
|
||||||
|
<-interrupt
|
||||||
|
log.Println("\n👋 Stopping monitor...")
|
||||||
|
}
|
||||||
|
|
||||||
51
cmd/test-graphql-ws/README.md
Normal file
51
cmd/test-graphql-ws/README.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# GraphQL-WS Proof of Concept
|
||||||
|
|
||||||
|
This is a simple test script to verify we can send messages to Kosmi using the WebSocket GraphQL API directly, following the `graphql-ws` protocol.
|
||||||
|
|
||||||
|
## What This Tests
|
||||||
|
|
||||||
|
1. Connects to Kosmi's WebSocket endpoint (`wss://engine.kosmi.io/gql-ws`)
|
||||||
|
2. Follows the graphql-ws protocol:
|
||||||
|
- Sends `connection_init`
|
||||||
|
- Waits for `connection_ack`
|
||||||
|
- Sends a mutation using `subscribe` message type
|
||||||
|
3. Attempts to send "Hello World from GraphQL-WS! 🎉" to the chat
|
||||||
|
|
||||||
|
## Protocol Details
|
||||||
|
|
||||||
|
Kosmi uses the [graphql-ws protocol](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md), which is different from the older `subscriptions-transport-ws`.
|
||||||
|
|
||||||
|
Key differences:
|
||||||
|
- Uses `graphql-transport-ws` subprotocol
|
||||||
|
- Both queries/mutations AND subscriptions use `type: "subscribe"`
|
||||||
|
- Must send `connection_init` before any operations
|
||||||
|
|
||||||
|
## Running the Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From the project root
|
||||||
|
go run cmd/test-graphql-ws/main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
If successful, you should see:
|
||||||
|
1. ✅ WebSocket connected
|
||||||
|
2. ✅ Connection acknowledged by server
|
||||||
|
3. ✅ Message sent
|
||||||
|
4. 📨 Response from server (either success or error)
|
||||||
|
|
||||||
|
## What We're Testing
|
||||||
|
|
||||||
|
This will help us determine if:
|
||||||
|
- We can connect to Kosmi's WebSocket without browser automation
|
||||||
|
- The authentication works (or what auth is needed)
|
||||||
|
- We can send messages via GraphQL mutations over WebSocket
|
||||||
|
- We can replace the UI automation approach with direct WebSocket
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Change `roomID` in the code to match your Kosmi room
|
||||||
|
- The script will keep running until you press Ctrl+C
|
||||||
|
- All messages are logged for debugging
|
||||||
|
|
||||||
512
cmd/test-graphql-ws/main.go
Normal file
512
cmd/test-graphql-ws/main.go
Normal file
@@ -0,0 +1,512 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Kosmi WebSocket endpoint
|
||||||
|
kosmiWSURL = "wss://engine.kosmi.io/gql-ws"
|
||||||
|
|
||||||
|
// Your room ID (MUST include @ symbol!)
|
||||||
|
roomID = "@hyperspaceout"
|
||||||
|
|
||||||
|
// User agent and app version (from AUTH_FINDINGS.md)
|
||||||
|
userAgent = "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"
|
||||||
|
appVersion = "4364"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GraphQL-WS Protocol message types
|
||||||
|
const (
|
||||||
|
MessageTypeConnectionInit = "connection_init"
|
||||||
|
MessageTypeConnectionAck = "connection_ack"
|
||||||
|
MessageTypeSubscribe = "subscribe"
|
||||||
|
MessageTypeNext = "next"
|
||||||
|
MessageTypeError = "error"
|
||||||
|
MessageTypeComplete = "complete"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Message represents a graphql-ws protocol message
|
||||||
|
type Message struct {
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Payload interface{} `json:"payload,omitempty"` // Can be map or array
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log.Println("🚀 Starting Kosmi GraphQL-WS Proof of Concept")
|
||||||
|
log.Printf("📡 WebSocket URL: %s", kosmiWSURL)
|
||||||
|
log.Printf("🏠 Room ID: %s", roomID)
|
||||||
|
|
||||||
|
// Step 1: Get anonymous token
|
||||||
|
log.Println("\n📝 Step 1: Getting anonymous token...")
|
||||||
|
token, err := getAnonymousToken()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("❌ Failed to get token: %v", err)
|
||||||
|
}
|
||||||
|
log.Printf("✅ Got token: %s...", token[:50])
|
||||||
|
|
||||||
|
// Set up interrupt handler
|
||||||
|
interrupt := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(interrupt, os.Interrupt)
|
||||||
|
|
||||||
|
log.Println("\n🔌 Step 2: Connecting to WebSocket...")
|
||||||
|
|
||||||
|
// Create WebSocket dialer with headers
|
||||||
|
dialer := websocket.Dialer{
|
||||||
|
Subprotocols: []string{"graphql-transport-ws"},
|
||||||
|
HandshakeTimeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add headers that a browser would send
|
||||||
|
headers := http.Header{}
|
||||||
|
headers.Add("Origin", "https://app.kosmi.io")
|
||||||
|
headers.Add("User-Agent", userAgent)
|
||||||
|
|
||||||
|
// Connect to WebSocket
|
||||||
|
log.Println("🔌 Establishing WebSocket connection...")
|
||||||
|
conn, resp, err := dialer.Dial(kosmiWSURL, headers)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("❌ Failed to connect: %v (response: %+v)", err, resp)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
log.Println("✅ WebSocket connected!")
|
||||||
|
log.Printf("📋 Response status: %s", resp.Status)
|
||||||
|
log.Printf("📋 Subprotocol: %s", conn.Subprotocol())
|
||||||
|
|
||||||
|
// Channel for receiving messages
|
||||||
|
done := make(chan struct{})
|
||||||
|
|
||||||
|
// Start reading messages
|
||||||
|
go func() {
|
||||||
|
defer close(done)
|
||||||
|
for {
|
||||||
|
var msg Message
|
||||||
|
err := conn.ReadJSON(&msg)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("❌ Read error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("📨 Received: %s (id: %s)", msg.Type, msg.ID)
|
||||||
|
if msg.Payload != nil {
|
||||||
|
payloadJSON, _ := json.MarshalIndent(msg.Payload, "", " ")
|
||||||
|
log.Printf(" Payload: %s", string(payloadJSON))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle different message types
|
||||||
|
switch msg.Type {
|
||||||
|
case MessageTypeConnectionAck:
|
||||||
|
log.Println("✅ Connection acknowledged by server!")
|
||||||
|
// First, subscribe to messages (like the browser does)
|
||||||
|
go subscribeToMessages(conn)
|
||||||
|
|
||||||
|
case MessageTypeNext:
|
||||||
|
log.Println("✅ Received 'next' - operation successful!")
|
||||||
|
|
||||||
|
case MessageTypeError:
|
||||||
|
log.Println("❌ Received 'error' from server!")
|
||||||
|
|
||||||
|
case MessageTypeComplete:
|
||||||
|
log.Println("✅ Received 'complete' - operation finished!")
|
||||||
|
// After joining, send a message
|
||||||
|
if msg.ID == "join-room" {
|
||||||
|
log.Println("\n⏭️ Room joined, now sending message...")
|
||||||
|
go sendMessage(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Step 1: Send connection_init with required payload
|
||||||
|
log.Println("📤 Sending connection_init...")
|
||||||
|
|
||||||
|
// Base64 encode the user agent (required by Kosmi)
|
||||||
|
uaEncoded := base64.StdEncoding.EncodeToString([]byte(userAgent))
|
||||||
|
|
||||||
|
initMsg := Message{
|
||||||
|
Type: MessageTypeConnectionInit,
|
||||||
|
Payload: map[string]interface{}{
|
||||||
|
"token": token, // JWT token from anonLogin
|
||||||
|
"ua": uaEncoded, // Base64-encoded User-Agent
|
||||||
|
"v": appVersion, // App version "4364"
|
||||||
|
"r": "", // Room-related field (empty for anonymous)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := conn.WriteJSON(initMsg); err != nil {
|
||||||
|
log.Fatalf("❌ Failed to send connection_init: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println(" Payload: token, ua (base64), v=4364, r=\"\"")
|
||||||
|
|
||||||
|
// Wait for interrupt or done
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
log.Println("🛑 Connection closed")
|
||||||
|
case <-interrupt:
|
||||||
|
log.Println("🛑 Interrupt received, closing connection...")
|
||||||
|
|
||||||
|
// Send close message
|
||||||
|
err := conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("❌ Error sending close: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAnonymousToken() (string, error) {
|
||||||
|
// GraphQL mutation for anonymous login
|
||||||
|
mutation := map[string]interface{}{
|
||||||
|
"query": `mutation { anonLogin { token } }`,
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonBody, err := json.Marshal(mutation)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf(" Sending: %s", string(jsonBody))
|
||||||
|
|
||||||
|
// Create HTTP client
|
||||||
|
client := &http.Client{Timeout: 10 * time.Second}
|
||||||
|
|
||||||
|
// Create request with body
|
||||||
|
req, err := http.NewRequest("POST", "https://engine.kosmi.io/", nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set headers and body
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Referer", "https://app.kosmi.io/")
|
||||||
|
req.Header.Set("User-Agent", userAgent)
|
||||||
|
req.Body = io.NopCloser(bytes.NewReader(jsonBody))
|
||||||
|
req.ContentLength = int64(len(jsonBody))
|
||||||
|
|
||||||
|
// Send request
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
log.Printf(" Response status: %d", resp.StatusCode)
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
return "", fmt.Errorf("HTTP %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse response
|
||||||
|
var result map[string]interface{}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract token from response
|
||||||
|
if data, ok := result["data"].(map[string]interface{}); ok {
|
||||||
|
if anonLogin, ok := data["anonLogin"].(map[string]interface{}); ok {
|
||||||
|
if token, ok := anonLogin["token"].(string); ok {
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resultJSON, _ := json.MarshalIndent(result, "", " ")
|
||||||
|
return "", fmt.Errorf("no token in response: %s", string(resultJSON))
|
||||||
|
}
|
||||||
|
|
||||||
|
func subscribeToMessages(conn *websocket.Conn) {
|
||||||
|
// Wait a bit
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
|
log.Println("\n📨 Step 3: Subscribing to message feed (like browser does)...")
|
||||||
|
log.Printf(" Room ID: %s", roomID)
|
||||||
|
|
||||||
|
// Subscribe to new messages (copied from browser monitoring)
|
||||||
|
subscription := `subscription OnNewMessage($roomId: String!) {
|
||||||
|
newMessage(roomId: $roomId) {
|
||||||
|
id
|
||||||
|
body
|
||||||
|
time
|
||||||
|
user {
|
||||||
|
id
|
||||||
|
displayName
|
||||||
|
username
|
||||||
|
avatarUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
msg := Message{
|
||||||
|
ID: "subscribe-messages",
|
||||||
|
Type: MessageTypeSubscribe,
|
||||||
|
Payload: map[string]interface{}{
|
||||||
|
"query": subscription,
|
||||||
|
"variables": map[string]interface{}{
|
||||||
|
"roomId": roomID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
msgJSON, _ := json.MarshalIndent(msg, "", " ")
|
||||||
|
log.Printf("📋 Sending subscription:\n%s", string(msgJSON))
|
||||||
|
|
||||||
|
if err := conn.WriteJSON(msg); err != nil {
|
||||||
|
log.Printf("❌ Failed to subscribe: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("✅ Subscription sent!")
|
||||||
|
log.Println(" Note: Subscriptions stay open and don't send 'complete'")
|
||||||
|
log.Println(" Proceeding to next step...")
|
||||||
|
|
||||||
|
// Wait a bit, then proceed to join room
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
go joinRoomWithCorrectID(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func joinRoomWithCorrectID(conn *websocket.Conn) {
|
||||||
|
// Wait a bit
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
|
log.Println("\n🚪 Step 4: Joining room with correct ID format...")
|
||||||
|
log.Printf(" Room ID: %s", roomID)
|
||||||
|
|
||||||
|
// Join room mutation with correct ID format
|
||||||
|
mutation := `mutation JoinRoom($id: String!) {
|
||||||
|
joinRoom(id: $id) {
|
||||||
|
ok
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
msg := Message{
|
||||||
|
ID: "join-room",
|
||||||
|
Type: MessageTypeSubscribe,
|
||||||
|
Payload: map[string]interface{}{
|
||||||
|
"query": mutation,
|
||||||
|
"variables": map[string]interface{}{
|
||||||
|
"id": roomID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
msgJSON, _ := json.MarshalIndent(msg, "", " ")
|
||||||
|
log.Printf("📋 Sending joinRoom mutation:\n%s", string(msgJSON))
|
||||||
|
|
||||||
|
if err := conn.WriteJSON(msg); err != nil {
|
||||||
|
log.Printf("❌ Failed to join room: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("✅ Join room request sent! Waiting for response...")
|
||||||
|
}
|
||||||
|
|
||||||
|
func joinRoom(conn *websocket.Conn) {
|
||||||
|
// Wait a bit to ensure connection is fully established
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
|
log.Println("\n🚪 Step 3: Getting room info and joining...")
|
||||||
|
|
||||||
|
// First, query for the room to get its actual ID
|
||||||
|
query := `query GetRoom($name: String!) {
|
||||||
|
roomByName(name: $name) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
displayName
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
msg := Message{
|
||||||
|
ID: "get-room",
|
||||||
|
Type: MessageTypeSubscribe,
|
||||||
|
Payload: map[string]interface{}{
|
||||||
|
"query": query,
|
||||||
|
"variables": map[string]interface{}{
|
||||||
|
"name": roomID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
msgJSON, _ := json.MarshalIndent(msg, "", " ")
|
||||||
|
log.Printf("📋 Querying for room by name:\n%s", string(msgJSON))
|
||||||
|
|
||||||
|
if err := conn.WriteJSON(msg); err != nil {
|
||||||
|
log.Printf("❌ Failed to query room: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("✅ Room query sent! Waiting for response...")
|
||||||
|
}
|
||||||
|
|
||||||
|
func actuallyJoinRoom(conn *websocket.Conn) {
|
||||||
|
// Wait a bit
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
|
log.Println("\n🚪 Step 3b: Actually joining room with ID...")
|
||||||
|
|
||||||
|
// Join room mutation - we'll use the room name for now
|
||||||
|
// The server might accept the name directly
|
||||||
|
mutation := `mutation JoinRoom($id: String!) {
|
||||||
|
joinRoom(id: $id) {
|
||||||
|
ok
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
msg := Message{
|
||||||
|
ID: "join-room",
|
||||||
|
Type: MessageTypeSubscribe,
|
||||||
|
Payload: map[string]interface{}{
|
||||||
|
"query": mutation,
|
||||||
|
"variables": map[string]interface{}{
|
||||||
|
"id": roomID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
msgJSON, _ := json.MarshalIndent(msg, "", " ")
|
||||||
|
log.Printf("📋 Sending joinRoom mutation:\n%s", string(msgJSON))
|
||||||
|
|
||||||
|
if err := conn.WriteJSON(msg); err != nil {
|
||||||
|
log.Printf("❌ Failed to join room: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("✅ Join room request sent! Waiting for response...")
|
||||||
|
}
|
||||||
|
|
||||||
|
func introspectSendMessage(conn *websocket.Conn) {
|
||||||
|
// Wait a bit
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
|
log.Println("\n🔍 Step 4: Introspecting room and message mutations...")
|
||||||
|
|
||||||
|
// Targeted introspection for room/message related mutations only
|
||||||
|
introspectionQuery := `query IntrospectRoomMutations {
|
||||||
|
mutations: __type(name: "RootMutationType") {
|
||||||
|
fields {
|
||||||
|
name
|
||||||
|
args {
|
||||||
|
name
|
||||||
|
type {
|
||||||
|
name
|
||||||
|
kind
|
||||||
|
ofType {
|
||||||
|
name
|
||||||
|
kind
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type {
|
||||||
|
name
|
||||||
|
kind
|
||||||
|
fields {
|
||||||
|
name
|
||||||
|
type {
|
||||||
|
name
|
||||||
|
kind
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
queries: __type(name: "RootQueryType") {
|
||||||
|
fields {
|
||||||
|
name
|
||||||
|
args {
|
||||||
|
name
|
||||||
|
type {
|
||||||
|
name
|
||||||
|
kind
|
||||||
|
ofType {
|
||||||
|
name
|
||||||
|
kind
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type {
|
||||||
|
name
|
||||||
|
kind
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
msg := Message{
|
||||||
|
ID: "introspect-sendMessage",
|
||||||
|
Type: MessageTypeSubscribe,
|
||||||
|
Payload: map[string]interface{}{
|
||||||
|
"query": introspectionQuery,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
msgJSON, _ := json.MarshalIndent(msg, "", " ")
|
||||||
|
log.Printf("📋 Sending introspection query:\n%s", string(msgJSON))
|
||||||
|
|
||||||
|
if err := conn.WriteJSON(msg); err != nil {
|
||||||
|
log.Printf("❌ Failed to send introspection: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("✅ Introspection sent! Waiting for response...")
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendMessage(conn *websocket.Conn) {
|
||||||
|
// Wait a bit
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
|
log.Println("\n📤 Step 5: Attempting to send 'Hello World' message...")
|
||||||
|
log.Printf(" Using room ID: %s", roomID)
|
||||||
|
|
||||||
|
// Fixed mutation based on introspection and browser monitoring:
|
||||||
|
// - roomId is String!, not ID!
|
||||||
|
// - roomId MUST include @ symbol (e.g., "@hyperspaceout")
|
||||||
|
// - sendMessage returns Success type with field "ok: Boolean"
|
||||||
|
mutation := `mutation SendMessage($body: String!, $roomId: String!) {
|
||||||
|
sendMessage(body: $body, roomId: $roomId) {
|
||||||
|
ok
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
// Create subscribe message (yes, mutations use "subscribe" in graphql-ws!)
|
||||||
|
msg := Message{
|
||||||
|
ID: fmt.Sprintf("send-message-%d", time.Now().Unix()),
|
||||||
|
Type: MessageTypeSubscribe,
|
||||||
|
Payload: map[string]interface{}{
|
||||||
|
"query": mutation,
|
||||||
|
"variables": map[string]interface{}{
|
||||||
|
"body": "Hello World from GraphQL-WS! 🎉",
|
||||||
|
"roomId": roomID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
msgJSON, _ := json.MarshalIndent(msg, "", " ")
|
||||||
|
log.Printf("📋 Sending message:\n%s", string(msgJSON))
|
||||||
|
|
||||||
|
if err := conn.WriteJSON(msg); err != nil {
|
||||||
|
log.Printf("❌ Failed to send message: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("✅ Message sent! Waiting for response...")
|
||||||
|
log.Println(" Check the Kosmi chat to see if the message appeared!")
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user