//go:build sim package main import ( "encoding/json" "fmt" "log" "math/rand" "net/url" "os" "os/signal" "sync" "sync/atomic" "syscall" "time" "github.com/gorilla/websocket" ) const ( gatewayURL = "ws://localhost:3333/sync" apiKey = "gateway" serverID = "test-server-001" // THE CHANNEL ID the mod says it serves. Must match gateway expectation. channelID = "1444253682777587804" ) // send interval range (random between minInterval and maxInterval) var ( minInterval = 500 * time.Millisecond maxInterval = 5 * time.Second ) type Handshake struct { Type string `json:"type"` Data json.RawMessage `json:"data"` } // ModHandshake now includes ChannelID type ModHandshake struct { ServerID string `json:"server_id"` ChannelID string `json:"channel_id"` } type GatewayAck struct { Status string `json:"status"` Type string `json:"type"` } type User struct { ID string `json:"id"` Name string `json:"name"` } type Destination struct { ID string `json:"channel_id,omitempty"` } type GatewayMessageIn struct { ID string `json:"id"` // where am I from (channel_id or server_id) MsgID string `json:"msg_id"` // msg id Destination Destination `json:"destination,omitempty"` // where do I wanna go (channel_id or empty if from Bot) Author User `json:"author"` // who sent the message Content string `json:"content"` // message content Meta map[string]interface{} `json:"meta,omitempty"` // additional metadata Ts time.Time `json:"ts,omitempty"` // timestamp ReceivedAt time.Time `json:"-"` // ReceivedAt is populated by gateway (not from mod) } func main() { rand.Seed(time.Now().UnixNano()) interrupt := make(chan os.Signal, 1) signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM) u, err := url.Parse(gatewayURL) if err != nil { log.Fatalf("Failed to parse URL: %v", err) } q := u.Query() q.Set("api_key", apiKey) u.RawQuery = q.Encode() log.Printf("Connecting to %s", u.String()) conn, _, err := websocket.DefaultDialer.Dial(u.String(), nil) if err != nil { log.Fatalf("Failed to connect: %v", err) } defer conn.Close() log.Println("Connected to gateway") var writeMu sync.Mutex conn.SetPingHandler(func(appData string) error { log.Println("Received ping from server, sending pong") writeMu.Lock() defer writeMu.Unlock() if err := conn.WriteControl(websocket.PongMessage, []byte(appData), time.Now().Add(5*time.Second)); err != nil { log.Printf("Failed to send pong: %v", err) return err } return nil }) modHS := ModHandshake{ ServerID: serverID, ChannelID: channelID, } modHSData, err := json.Marshal(modHS) if err != nil { log.Fatalf("Failed to marshal mod handshake: %v", err) } handshake := Handshake{ Type: "mod", Data: modHSData, } writeMu.Lock() if err := conn.WriteJSON(handshake); err != nil { writeMu.Unlock() log.Fatalf("Failed to send handshake: %v", err) } writeMu.Unlock() log.Println("Handshake sent") var ack GatewayAck if err := conn.ReadJSON(&ack); err != nil { log.Fatalf("Failed to read acknowledgment: %v", err) } log.Printf("Received acknowledgment: status=%q, type=%q", ack.Status, ack.Type) done := make(chan struct{}) go func() { defer close(done) for { messageType, message, err := conn.ReadMessage() if err != nil { if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure) { log.Printf("WebSocket error: %v", err) } else { log.Printf("Connection closed/read error: %v", err) } return } switch messageType { case websocket.TextMessage, websocket.BinaryMessage: log.Printf("Received from server: %s", string(message)) default: } } }() var msgCounter uint64 = 1 func() { testMsg := GatewayMessageIn{ MsgID: fmt.Sprintf("test-msg-%06d", atomic.AddUint64(&msgCounter, 1)), ID: serverID, Destination: Destination{ ID: channelID, }, Author: User{ ID: "player-uuid-123", Name: "TestPlayer", }, Content: "Hello from simulated mod!", Ts: time.Now().UTC(), } writeMu.Lock() if err := conn.WriteJSON(testMsg); err != nil { log.Printf("Failed to send test message: %v", err) } else { log.Println("Sent initial test message to gateway") } writeMu.Unlock() }() go func() { for { d := randomDuration(minInterval, maxInterval) select { case <-done: return case <-time.After(d): // build message msgNum := atomic.AddUint64(&msgCounter, 1) msg := GatewayMessageIn{ MsgID: fmt.Sprintf("sim-msg-%06d", msgNum), ID: serverID, Destination: Destination{ ID: channelID, }, Author: User{ ID: fmt.Sprintf("sim-user-%d", msgNum%1000), Name: fmt.Sprintf("SimUser%d", msgNum%1000), }, Content: fmt.Sprintf("Random interval message #%d (delay %s)", msgNum, d), Ts: time.Now().UTC(), } writeMu.Lock() if err := conn.WriteJSON(msg); err != nil { log.Printf("Failed to send simulated message: %v", err) writeMu.Unlock() return } writeMu.Unlock() log.Printf("Sent simulated message %s (next wait up to %s)", msg.MsgID, maxInterval) } } }() log.Println("Connection established. Sending simulated messages at random intervals. Press Ctrl+C to disconnect.") for { select { case <-done: log.Println("Connection read loop closed, exiting") return case <-interrupt: log.Println("Interrupt received, closing connection...") writeMu.Lock() err := conn.WriteMessage( websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), ) writeMu.Unlock() if err != nil { log.Printf("Write close error: %v", err) } select { case <-done: case <-time.After(1 * time.Second): } return } } } func randomDuration(min, max time.Duration) time.Duration { if max <= min { return min } diff := int64(max - min) n := rand.Int63n(diff) return min + time.Duration(n) }