package handler import ( "encoding/json" "fmt" "log" "net/http" "os" "strconv" "sync" "git.yeij.top/AskaEth/Cyrene/platform-bridge/internal/bridge" ) // BridgeHandler exposes the Platform Bridge REST API. type BridgeHandler struct { router *bridge.PlatformRouter internalToken string channelsMu sync.RWMutex cachedChannels []bridge.ChannelInfo } func NewBridgeHandler(router *bridge.PlatformRouter) *BridgeHandler { return &BridgeHandler{ router: router, internalToken: os.Getenv("INTERNAL_SERVICE_TOKEN"), } } func (h *BridgeHandler) RegisterRoutes(mux *http.ServeMux) { mux.HandleFunc("/health", h.health) mux.HandleFunc("/api/v1/platforms", h.listPlatforms) mux.HandleFunc("/api/v1/platforms/", h.platformInfo) mux.HandleFunc("/api/v1/identities", h.listIdentities) mux.HandleFunc("/api/v1/internal/send-proactive", h.sendProactive) mux.HandleFunc("/api/v1/channels", h.listChannels) mux.HandleFunc("/api/v1/webhook/telegram", h.telegramWebhook) mux.HandleFunc("/api/v1/webhook/", h.genericWebhook) } func (h *BridgeHandler) health(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]interface{}{ "status": "ok", "service": "platform-bridge", "platforms": h.router.ListAdapters(), }) } func (h *BridgeHandler) listPlatforms(w http.ResponseWriter, r *http.Request) { names := h.router.ListAdapters() type platformSummary struct { Name string `json:"name"` Connected bool `json:"connected"` Caps bridge.PlatformCapabilities `json:"capabilities"` } var platforms []platformSummary for _, name := range names { a, err := h.router.GetAdapter(name) if err != nil { continue } platforms = append(platforms, platformSummary{ Name: name, Connected: a.IsConnected(), Caps: a.Capabilities(), }) } writeJSON(w, http.StatusOK, map[string]interface{}{"platforms": platforms, "total": len(platforms)}) } func (h *BridgeHandler) platformInfo(w http.ResponseWriter, r *http.Request) { name := r.URL.Path[len("/api/v1/platforms/"):] a, err := h.router.GetAdapter(name) if err != nil { writeJSON(w, http.StatusNotFound, errResp(err.Error())) return } writeJSON(w, http.StatusOK, map[string]interface{}{ "name": name, "connected": a.IsConnected(), "capabilities": a.Capabilities(), }) } func (h *BridgeHandler) listIdentities(w http.ResponseWriter, r *http.Request) { all := h.router.ListAllIdentities() writeJSON(w, http.StatusOK, all) } // telegramWebhook receives updates from Telegram Bot API. func (h *BridgeHandler) telegramWebhook(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { writeJSON(w, http.StatusMethodNotAllowed, errResp("method not allowed")) return } // Parse into a generic map first to check for message presence. var raw map[string]interface{} if err := json.NewDecoder(r.Body).Decode(&raw); err != nil { writeJSON(w, http.StatusBadRequest, errResp("invalid telegram update")) return } if _, hasMsg := raw["message"]; !hasMsg { writeJSON(w, http.StatusOK, map[string]string{"status": "ignored"}) return } _, err := h.router.RouteMessage("telegram", raw) if err != nil { writeJSON(w, http.StatusInternalServerError, errResp(err.Error())) return } writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) } // genericWebhook receives standard webhook payloads. func (h *BridgeHandler) genericWebhook(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { writeJSON(w, http.StatusMethodNotAllowed, errResp("method not allowed")) return } // Extract platform name from path: /api/v1/webhook/{platform} platform := r.URL.Path[len("/api/v1/webhook/"):] if platform == "" || platform == "telegram" { writeJSON(w, http.StatusBadRequest, errResp("specify platform name in path")) return } var payload map[string]interface{} if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { writeJSON(w, http.StatusBadRequest, errResp("invalid JSON payload")) return } response, err := h.router.RouteMessage(platform, &payload) if err != nil { writeJSON(w, http.StatusInternalServerError, errResp(err.Error())) return } // Convert to platform-specific format. msgs, err := h.router.SendResponse(response) if err != nil { writeJSON(w, http.StatusInternalServerError, errResp(err.Error())) return } writeJSON(w, http.StatusOK, map[string]interface{}{ "messages": msgs, "reply_to": response.ReplyTo, }) } // sendProactive handles internal proactive message delivery to platform adapters. // POST /api/v1/internal/send-proactive func (h *BridgeHandler) sendProactive(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { writeJSON(w, http.StatusMethodNotAllowed, errResp("method not allowed")) return } // Validate internal token token := r.Header.Get("X-Internal-Token") if h.internalToken == "" || token != h.internalToken { writeJSON(w, http.StatusUnauthorized, errResp("unauthorized")) return } var req struct { Platform string `json:"platform"` ChatType string `json:"chat_type"` UserID string `json:"user_id"` GroupID string `json:"group_id"` AtUserID string `json:"at_user_id"` Content string `json:"content"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, errResp("invalid request body")) return } if req.Platform == "" || req.ChatType == "" || req.Content == "" { writeJSON(w, http.StatusBadRequest, errResp("platform, chat_type, and content are required")) return } // Map chat type to OBv11 message_type msgType := req.ChatType if msgType != "private" && msgType != "group" { writeJSON(w, http.StatusBadRequest, errResp("chat_type must be private or group")) return } userID := parseIntSafe(req.UserID) groupID := parseIntSafe(req.GroupID) // Prepend CQ @mention tag if at_user_id is specified content := req.Content if req.AtUserID != "" { content = fmt.Sprintf("[CQ:at,qq=%s] %s", req.AtUserID, content) } // Resolve adapter: try exact name first, then find by platform type. adapterName := req.Platform err := h.router.SendProactive(adapterName, msgType, userID, groupID, content) if err != nil { for _, name := range h.router.ListAdapters() { if a, aErr := h.router.GetAdapter(name); aErr == nil && a.PlatformName() == req.Platform && a.IsConnected() { adapterName = name err = h.router.SendProactive(name, msgType, userID, groupID, content) break } } } if err != nil { log.Printf("[send-proactive] 发送失败: adapter=%s err=%v", adapterName, err) writeJSON(w, http.StatusInternalServerError, errResp("send failed: "+err.Error())) return } log.Printf("[send-proactive] 已发送: adapter=%s chat=%s user=%d group=%d at=%s len=%d", adapterName, msgType, userID, groupID, req.AtUserID, len(content)) writeJSON(w, http.StatusOK, map[string]interface{}{ "success": true, "message": "消息已发送", }) } func parseIntSafe(s string) int64 { if s == "" { return 0 } n, err := strconv.ParseInt(s, 10, 64) if err != nil { return 0 } return n } func errResp(msg string) map[string]string { return map[string]string{"error": msg} } func writeJSON(w http.ResponseWriter, status int, data interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(data) }