// Package qq provides an adapter for QQ (OBv11 protocol) integration. package qq import ( "encoding/json" "fmt" "sync" "time" "github.com/gorilla/websocket" "git.yeij.top/AskaEth/Cyrene_v2/services/platform-bridge" ) // Config holds QQ adapter configuration. type Config struct { Name string // Adapter name (e.g. "qq-main") WSURL string // OBv11 WebSocket URL ("ws://localhost:3001") AccessToken string // OBv11 access token BotQQ string // Bot's own QQ number } // Adapter implements bridge.Adapter for QQ OBv11. type Adapter struct { cfg Config conn *websocket.Conn incoming chan *bridge.UnifiedMessage mu sync.Mutex done chan struct{} } // New creates a QQ adapter. func New(cfg Config) *Adapter { return &Adapter{ cfg: cfg, incoming: make(chan *bridge.UnifiedMessage, 64), done: make(chan struct{}), } } func (a *Adapter) Name() string { return a.cfg.Name } func (a *Adapter) Platform() string { return "qq" } func (a *Adapter) Start() error { conn, _, err := websocket.DefaultDialer.Dial(a.cfg.WSURL, nil) if err != nil { return fmt.Errorf("qq ws connect: %w", err) } a.conn = conn // Send auth authMsg := map[string]any{ "type": "meta::connect", "access_token": a.cfg.AccessToken, } conn.WriteJSON(authMsg) // Read loop go func() { defer close(a.incoming) for { _, raw, err := conn.ReadMessage() if err != nil { return } var event map[string]any if err := json.Unmarshal(raw, &event); err != nil { continue } msg := a.parseMessage(event) if msg != nil { a.incoming <- msg } } }() return nil } func (a *Adapter) Stop() error { a.mu.Lock() defer a.mu.Unlock() if a.conn != nil { a.conn.Close() } close(a.done) return nil } func (a *Adapter) Incoming() <-chan *bridge.UnifiedMessage { return a.incoming } func (a *Adapter) IsConnected() bool { a.mu.Lock() defer a.mu.Unlock() return a.conn != nil } func (a *Adapter) Send(target string, content string, opts bridge.SendOptions) error { a.mu.Lock() defer a.mu.Unlock() if a.conn == nil { return fmt.Errorf("not connected") } // Apply length limit if opts.MaxLength > 0 && len([]rune(content)) > opts.MaxLength { runes := []rune(content) content = string(runes[:opts.MaxLength]) + "..." } msg := map[string]any{ "action": "send_msg", "params": map[string]any{ "user_id": target, "message": content, "auto_escape": false, }, } return a.conn.WriteJSON(msg) } func (a *Adapter) parseMessage(event map[string]any) *bridge.UnifiedMessage { postType, _ := event["post_type"].(string) if postType != "message" { return nil } msgType, _ := event["message_type"].(string) sender := event["sender"].(map[string]any) rawMsg, _ := event["raw_message"].(string) senderID := fmt.Sprintf("%v", sender["user_id"]) senderName, _ := sender["nickname"].(string) isDM := msgType == "private" groupID := "" if !isDM { groupID = fmt.Sprintf("%v", event["group_id"]) } // Check if bot was @mentioned atBot := false if atList, ok := event["at_list"].([]any); ok { for _, at := range atList { if fmt.Sprintf("%v", at) == a.cfg.BotQQ { atBot = true break } } } return &bridge.UnifiedMessage{ ID: fmt.Sprintf("%v", event["message_id"]), Platform: "qq", Source: groupID, SenderID: senderID, SenderName: senderName, Content: rawMsg, IsAtBot: atBot, IsDM: isDM, RawData: event, Timestamp: time.Unix(int64(event["time"].(float64)), 0), } }