//go:build simbot package main import ( "encoding/json" "flag" "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" channelID = "123456789" ) var ( minInterval = 500 * time.Millisecond maxInterval = 5 * time.Second ) type Handshake struct { Type string `json:"type"` Data json.RawMessage `json:"data"` } type BotHandshake struct { 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"` MsgID string `json:"msg_id"` Destination Destination `json:"destination,omitempty"` Author User `json:"author"` Content string `json:"content"` Meta map[string]interface{} `json:"meta,omitempty"` Ts time.Time `json:"ts,omitempty"` ReceivedAt time.Time `json:"-"` } 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) } func main() { rand.Seed(time.Now().UnixNano()) botID := flag.String("bot", "sim-bot-1", "bot id") sendAfter := flag.Duration("send-after", 0, "optional: send a bot->mod message after this delay (e.g. 2s)") sendMsg := flag.String("msg", "Hello from bot!", "optional bot->mod test message content") flag.Parse() 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() conn, _, err := websocket.DefaultDialer.Dial(u.String(), nil) if err != nil { log.Fatalf("Failed to connect: %v", err) } defer conn.Close() var writeMu sync.Mutex conn.SetPingHandler(func(appData string) error { writeMu.Lock() err := conn.WriteControl(websocket.PongMessage, []byte(appData), time.Now().Add(5*time.Second)) writeMu.Unlock() if err != nil { log.Printf("Failed to send pong: %v", err) return err } return nil }) bhs := BotHandshake{ChannelId: channelID} data, err := json.Marshal(bhs) if err != nil { _ = conn.Close() log.Fatalf("Failed to marshal bot handshake: %v", err) } hs := Handshake{Type: "bot", Data: data} writeMu.Lock() if err := conn.WriteJSON(hs); err != nil { writeMu.Unlock() _ = conn.Close() log.Fatalf("Failed to send handshake: %v", err) } writeMu.Unlock() conn.SetReadDeadline(time.Now().Add(10 * time.Second)) msgType, raw, err := conn.ReadMessage() if err != nil { _ = conn.Close() log.Fatalf("Failed to read handshake response: %v", err) } _ = conn.SetReadDeadline(time.Time{}) if msgType == websocket.TextMessage || msgType == websocket.BinaryMessage { log.Printf("Raw handshake reply: %s", string(raw)) var ack GatewayAck if err := json.Unmarshal(raw, &ack); err != nil { log.Printf("Handshake reply is not JSON or unmarshal failed: %v", err) } else { log.Printf("Parsed ack: status=%q, type=%q", ack.Status, ack.Type) } } else { log.Printf("Handshake reply type=%d", msgType) } done := make(chan struct{}) go func() { defer close(done) for { msgType, 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 or read error: %v", err) } return } if msgType == websocket.TextMessage || msgType == websocket.BinaryMessage { log.Printf("Received from gateway: %s", string(message)) } } }() var msgCounter uint64 = 1 if *sendAfter > 0 { go func() { time.Sleep(*sendAfter) msg := GatewayMessageIn{ MsgID: fmt.Sprintf("bot-msg-%06d", atomic.AddUint64(&msgCounter, 1)), ID: channelID, Author: User{ ID: *botID, Name: "SimBot", }, Content: *sendMsg, Ts: time.Now().UTC(), } writeMu.Lock() _ = conn.SetWriteDeadline(time.Now().Add(5 * time.Second)) if err := conn.WriteJSON(msg); err != nil { log.Printf("Failed to send bot->mod test message: %v", err) } else { log.Printf("Sent bot->mod test message (channel=%s)", channelID) } _ = conn.SetWriteDeadline(time.Time{}) writeMu.Unlock() }() } go func() { for { select { case <-done: return default: } d := randomDuration(minInterval, maxInterval) select { case <-done: return case <-time.After(d): msgNum := atomic.AddUint64(&msgCounter, 1) msg := GatewayMessageIn{ MsgID: fmt.Sprintf("sim-bot-msg-%06d", msgNum), ID: channelID, Author: User{ ID: fmt.Sprintf("%s-%d", *botID, msgNum%1000), Name: fmt.Sprintf("SimBot%d", msgNum%1000), }, Content: fmt.Sprintf("Automated bot message #%d (delay %s)", msgNum, d), Ts: time.Now().UTC(), } writeMu.Lock() _ = conn.SetWriteDeadline(time.Now().Add(5 * time.Second)) if err := conn.WriteJSON(msg); err != nil { writeMu.Unlock() log.Printf("Failed to send automated bot message: %v", err) return } _ = conn.SetWriteDeadline(time.Time{}) writeMu.Unlock() log.Printf("Sent automated bot message %s", msg.MsgID) } } }() for { select { case <-done: log.Println("Connection closed by server") _ = conn.Close() return case <-interrupt: log.Println("Interrupt received, closing connection...") writeMu.Lock() _ = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) writeMu.Unlock() select { case <-done: case <-time.After(time.Second): } _ = conn.Close() return } } }