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.
|
||||
|
||||
@@ -169,12 +169,12 @@ func (b *Bkosmi) handleIncomingMessage(payload *NewMessagePayload) {
|
||||
|
||||
// Send to Matterbridge
|
||||
b.Log.Debugf("Forwarding to Matterbridge channel=%s account=%s: %s", rmsg.Channel, rmsg.Account, rmsg.Text)
|
||||
|
||||
|
||||
if b.Remote == nil {
|
||||
b.Log.Error("Remote channel is nil! Cannot forward message")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
b.Remote <- rmsg
|
||||
}
|
||||
|
||||
|
||||
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