Files
2026-08-07 13:36:56 +08:00

152 lines
3.9 KiB
Go

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