mirror of
https://github.com/sky22333/qqbot.git
synced 2026-07-18 22:31:28 +08:00
216 lines
5.4 KiB
Go
216 lines
5.4 KiB
Go
package httpserver
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"log/slog"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/sky22333/qqbot/config"
|
|
"github.com/sky22333/qqbot/internal/notifier"
|
|
"github.com/sky22333/qqbot/internal/targets"
|
|
"github.com/sky22333/qqbot/message"
|
|
)
|
|
|
|
type Server struct {
|
|
cfg config.Config
|
|
logger *slog.Logger
|
|
notifier *notifier.Notifier
|
|
targets *targets.Store
|
|
srv *http.Server
|
|
}
|
|
|
|
func New(cfg config.Config, logger *slog.Logger, n *notifier.Notifier, t *targets.Store) (*Server, error) {
|
|
if logger == nil {
|
|
logger = slog.Default()
|
|
}
|
|
readTimeout, err := cfg.ReadTimeout()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
writeTimeout, err := cfg.WriteTimeout()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
handler := &Server{
|
|
cfg: cfg,
|
|
logger: logger,
|
|
notifier: n,
|
|
targets: t,
|
|
}
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/healthz", handler.healthz)
|
|
mux.HandleFunc("/readyz", handler.readyz)
|
|
mux.HandleFunc("/api/v1/messages/send", handler.sendSync)
|
|
mux.HandleFunc("/api/v1/messages", handler.enqueue)
|
|
mux.HandleFunc("/api/v1/messages/", handler.queryStatus)
|
|
mux.HandleFunc("/api/v1/targets", handler.listTargets)
|
|
|
|
handler.srv = &http.Server{
|
|
Addr: cfg.Server.ListenAddr,
|
|
Handler: mux,
|
|
ReadTimeout: readTimeout,
|
|
WriteTimeout: writeTimeout,
|
|
}
|
|
return handler, nil
|
|
}
|
|
|
|
func (s *Server) Start() error {
|
|
s.logger.Info("HTTP 服务启动", "addr", s.cfg.Server.ListenAddr)
|
|
err := s.srv.ListenAndServe()
|
|
if errors.Is(err, http.ErrServerClosed) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (s *Server) Shutdown(ctx context.Context) error {
|
|
return s.srv.Shutdown(ctx)
|
|
}
|
|
|
|
func (s *Server) healthz(w http.ResponseWriter, _ *http.Request) {
|
|
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "time": time.Now()})
|
|
}
|
|
|
|
func (s *Server) readyz(w http.ResponseWriter, _ *http.Request) {
|
|
writeJSON(w, http.StatusOK, map[string]any{"ready": true, "time": time.Now()})
|
|
}
|
|
|
|
func (s *Server) sendSync(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
|
|
return
|
|
}
|
|
if !s.authorized(r) {
|
|
writeError(w, http.StatusUnauthorized, "unauthorized")
|
|
return
|
|
}
|
|
req, err := s.decodePushRequest(w, r)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
result, err := s.notifier.Send(r.Context(), req)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadGateway, err.Error())
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"code": 0,
|
|
"message": "ok",
|
|
"data": result,
|
|
})
|
|
}
|
|
|
|
func (s *Server) enqueue(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
|
|
return
|
|
}
|
|
if !s.authorized(r) {
|
|
writeError(w, http.StatusUnauthorized, "unauthorized")
|
|
return
|
|
}
|
|
req, err := s.decodePushRequest(w, r)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
requestID, err := s.notifier.Enqueue(r.Context(), req)
|
|
if err != nil {
|
|
writeError(w, http.StatusServiceUnavailable, err.Error())
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusAccepted, map[string]any{
|
|
"code": 0,
|
|
"message": "accepted",
|
|
"data": map[string]any{
|
|
"request_id": requestID,
|
|
},
|
|
})
|
|
}
|
|
|
|
func (s *Server) queryStatus(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
|
|
return
|
|
}
|
|
if !s.authorized(r) {
|
|
writeError(w, http.StatusUnauthorized, "unauthorized")
|
|
return
|
|
}
|
|
requestID := strings.TrimPrefix(r.URL.Path, "/api/v1/messages/")
|
|
if requestID == "" {
|
|
writeError(w, http.StatusBadRequest, "request id 不能为空")
|
|
return
|
|
}
|
|
status, ok := s.notifier.GetStatus(requestID)
|
|
if !ok {
|
|
writeError(w, http.StatusNotFound, "not found")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"code": 0,
|
|
"message": "ok",
|
|
"data": status,
|
|
})
|
|
}
|
|
|
|
func (s *Server) listTargets(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
|
|
return
|
|
}
|
|
if !s.authorized(r) {
|
|
writeError(w, http.StatusUnauthorized, "unauthorized")
|
|
return
|
|
}
|
|
targetType := strings.TrimSpace(r.URL.Query().Get("target_type"))
|
|
items := s.targets.List(200, targetType)
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"code": 0,
|
|
"message": "ok",
|
|
"data": items,
|
|
})
|
|
}
|
|
|
|
func (s *Server) decodePushRequest(w http.ResponseWriter, r *http.Request) (message.PushRequest, error) {
|
|
defer r.Body.Close()
|
|
r.Body = http.MaxBytesReader(w, r.Body, s.cfg.Server.MaxBodyBytes)
|
|
decoder := json.NewDecoder(r.Body)
|
|
decoder.DisallowUnknownFields()
|
|
req := message.PushRequest{}
|
|
if err := decoder.Decode(&req); err != nil {
|
|
return message.PushRequest{}, err
|
|
}
|
|
return req, nil
|
|
}
|
|
|
|
func (s *Server) authorized(r *http.Request) bool {
|
|
if strings.TrimSpace(s.cfg.Server.APIToken) == "" {
|
|
return true
|
|
}
|
|
auth := r.Header.Get("Authorization")
|
|
if !strings.HasPrefix(auth, "Bearer ") {
|
|
return false
|
|
}
|
|
token := strings.TrimPrefix(auth, "Bearer ")
|
|
return token == s.cfg.Server.APIToken
|
|
}
|
|
|
|
func writeError(w http.ResponseWriter, status int, msg string) {
|
|
writeJSON(w, status, map[string]any{
|
|
"code": status,
|
|
"message": msg,
|
|
})
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, status int, payload any) {
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
w.WriteHeader(status)
|
|
_ = json.NewEncoder(w).Encode(payload)
|
|
}
|