package api import ( "context" "encoding/json" "fmt" "log" "net/http" "github.com/Wxw-Gu/WechatExplorer/services/wechat-connector/ilink" "github.com/Wxw-Gu/WechatExplorer/services/wechat-connector/messaging" ) // Server provides an HTTP API for sending messages. type Server struct { clients []*ilink.Client addr string } // NewServer creates an API server. func NewServer(clients []*ilink.Client, addr string) *Server { if addr == "" { addr = "127.0.0.1:18011" } return &Server{clients: clients, addr: addr} } // SendRequest is the JSON body for POST /api/send. type SendRequest struct { AccountID string `json:"account_id,omitempty"` To string `json:"to"` Text string `json:"text,omitempty"` MediaURL string `json:"media_url,omitempty"` // image/video/file URL } // Run starts the HTTP server. Blocks until ctx is cancelled. func (s *Server) Run(ctx context.Context) error { mux := http.NewServeMux() mux.HandleFunc("/api/send", s.handleSend) mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) fmt.Fprintln(w, "ok") }) srv := &http.Server{Addr: s.addr, Handler: mux} go func() { <-ctx.Done() srv.Shutdown(context.Background()) }() log.Printf("[api] listening on %s", s.addr) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { return err } return nil } func (s *Server) handleSend(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "POST only", http.StatusMethodNotAllowed) return } var req SendRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid JSON: "+err.Error(), http.StatusBadRequest) return } if req.To == "" { http.Error(w, `"to" is required`, http.StatusBadRequest) return } if req.Text == "" && req.MediaURL == "" { http.Error(w, `"text" or "media_url" is required`, http.StatusBadRequest) return } if len(s.clients) == 0 { http.Error(w, "no accounts configured", http.StatusServiceUnavailable) return } client := s.clientForAccount(req.AccountID) if client == nil { http.Error(w, "requested account is not available", http.StatusNotFound) return } ctx := r.Context() // Send text if provided if req.Text != "" { if err := messaging.SendTextReply(ctx, client, req.To, req.Text, "", ""); err != nil { log.Printf("[api] send text failed: %v", err) http.Error(w, "send text failed: "+err.Error(), http.StatusInternalServerError) return } log.Printf("[api] sent text to %s: %q", req.To, req.Text) // Extract and send any markdown images embedded in text for _, imgURL := range messaging.ExtractImageURLs(req.Text) { if err := messaging.SendMediaFromURL(ctx, client, req.To, imgURL, ""); err != nil { log.Printf("[api] send extracted image failed: %v", err) } else { log.Printf("[api] sent extracted image to %s: %s", req.To, imgURL) } } } // Send media if provided if req.MediaURL != "" { if err := messaging.SendMediaFromURL(ctx, client, req.To, req.MediaURL, ""); err != nil { log.Printf("[api] send media failed: %v", err) http.Error(w, "send media failed: "+err.Error(), http.StatusInternalServerError) return } log.Printf("[api] sent media to %s: %s", req.To, req.MediaURL) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) } func (s *Server) clientForAccount(accountID string) *ilink.Client { if accountID == "" { return s.clients[0] } for _, client := range s.clients { if client.BotID() == accountID { return client } } return nil }