249 lines
7.3 KiB
Go
249 lines
7.3 KiB
Go
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)
|
|
}
|