package modules import ( "encoding/base64" "encoding/json" "fmt" "io" "net/http" "github.com/gin-gonic/gin" "github.com/lineup/app-server/internal" ) // WebhookModule receives WuKongIM 3.0 HTTP webhook callbacks. type WebhookModule struct { hub *internal.Hub } // NewWebhookModule creates a new WebhookModule. func NewWebhookModule(hub *internal.Hub) *WebhookModule { return &WebhookModule{hub: hub} } // RegisterRoutes registers webhook routes. func (m *WebhookModule) RegisterRoutes(r *gin.Engine) { r.POST("/v1/webhook", m.handleWebhook) } // wkMsgResp mirrors WuKongIM 3.0's msg.notify message format. type wkMsgResp struct { Header wkMsgHeader `json:"header"` Setting uint8 `json:"setting"` MessageID uint64 `json:"message_id"` MessageSeq uint64 `json:"message_seq"` FromUID string `json:"from_uid"` ChannelID string `json:"channel_id"` ChannelType uint8 `json:"channel_type"` Timestamp int32 `json:"timestamp"` Payload string `json:"payload"` // base64-encoded } type wkMsgHeader struct { NoPersist int `json:"no_persist"` RedDot int `json:"red_dot"` SyncOnce int `json:"sync_once"` } // wkOfflineResp mirrors WuKongIM 3.0's msg.offline format. type wkOfflineResp struct { wkMsgResp ToUIDs []string `json:"to_uids"` Compress string `json:"compress"` CompressToUIDs string `json:"compress_to_uids"` SourceID int64 `json:"source_id"` } func (m *WebhookModule) handleWebhook(c *gin.Context) { event := c.Query("event") body, err := io.ReadAll(c.Request.Body) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "读取请求体失败"}) return } switch event { case "msg.notify": m.handleMsgNotify(body) case "msg.offline": m.handleMsgOffline(body) case "user.onlinestatus": m.handleOnlineStatus(body) default: // Unknown event, still return 200 per WK webhook contract } c.JSON(http.StatusOK, gin.H{"status": "ok"}) } func (m *WebhookModule) handleMsgNotify(data []byte) { var msgs []wkMsgResp if err := json.Unmarshal(data, &msgs); err != nil { return } for _, msg := range msgs { m.pushToClient(msg.ChannelID, msg) } } func (m *WebhookModule) handleMsgOffline(data []byte) { var msg wkOfflineResp if err := json.Unmarshal(data, &msg); err != nil { return } // Push to each offline recipient for _, uid := range msg.ToUIDs { m.pushToClient(uid, msg.wkMsgResp) } // Handle gzip-compressed UIDs if msg.Compress == "gzip" && len(msg.CompressToUIDs) > 0 { decoded, err := base64.StdEncoding.DecodeString(msg.CompressToUIDs) if err == nil { var uids []string if json.Unmarshal(decoded, &uids) == nil { for _, uid := range uids { m.pushToClient(uid, msg.wkMsgResp) } } } } } func (m *WebhookModule) handleOnlineStatus(data []byte) { // Online status format: ["uid-flag-status", ...] var statuses []string if err := json.Unmarshal(data, &statuses); err != nil { return } // For now, just log. Could be used to notify clients. _ = statuses } // pushToClient decodes the payload and pushes to the connected client. func (m *WebhookModule) pushToClient(uid string, msg wkMsgResp) { if m.hub == nil { return } decoded, _ := base64.StdEncoding.DecodeString(msg.Payload) pushMsg := map[string]interface{}{ "event": "message", "message_id": msg.MessageID, "message_seq": msg.MessageSeq, "from_uid": msg.FromUID, "channel_id": msg.ChannelID, "channel_type": msg.ChannelType, "payload": string(decoded), "timestamp": msg.Timestamp, } pushData, _ := json.Marshal(pushMsg) if err := m.hub.PushToClient(uid, pushData); err != nil { // Client not connected, that's OK _ = err } // Also push to the other participant if it's a person channel // This ensures both sides get the message if msg.ChannelType == 1 && msg.FromUID != uid { _ = m.hub.PushToClient(msg.FromUID, pushData) } } // Ensure fmt is used (for potential future logging) var _ = fmt.Sprintf