package httpserver import ( "context" "encoding/json" "errors" "log/slog" "net/http" "strings" "time" "gitea.unprism.ru/KRBL/mila/protocol" ) type Handler struct { cfg protocol.Config store protocol.Store logger *slog.Logger onSet func(protocol.ContentState) } func NewHandler(cfg protocol.Config, store protocol.Store, opts ...Option) *Handler { if store == nil { store = nilStore{} } h := &Handler{ cfg: protocol.NormalizeConfig(cfg), store: store, logger: slog.Default(), } for _, opt := range opts { opt(h) } return h } func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { start := time.Now() status := http.StatusOK defer func() { h.logger.Info("mila request", slog.String("method", r.Method), slog.String("path", r.URL.Path), slog.String("remote_addr", r.RemoteAddr), slog.Int("status", status), slog.Duration("duration", time.Since(start)), ) }() if r.Method != http.MethodGet { status = http.StatusMethodNotAllowed w.Header().Set("Allow", http.MethodGet) writeError(w, status, "method not allowed") return } switch cleanPath(r.URL.Path) { case "/set-content": status = h.handleSetContent(w, r) case "/get-content": status = h.handleGetContent(w, r) default: status = http.StatusNotFound writeError(w, status, "path not found") } } func (h *Handler) handleSetContent(w http.ResponseWriter, r *http.Request) int { q := r.URL.Query() state := protocol.ContentState{ Route: strings.TrimSpace(q.Get("route")), Text: q.Get("str"), Template: strings.TrimSpace(q.Get("tpl_name")), UpdatedAt: time.Now().UTC(), } if err := protocol.Validate(h.cfg, state); err != nil { if errors.Is(err, protocol.ErrInvalidData) { writeError(w, http.StatusBadRequest, err.Error()) return http.StatusBadRequest } writeError(w, http.StatusInternalServerError, err.Error()) return http.StatusInternalServerError } if err := h.store.Set(r.Context(), state); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return http.StatusInternalServerError } if h.onSet != nil { h.onSet(state) } writeJSON(w, http.StatusOK, map[string]string{"status": "OK"}) return http.StatusOK } func (h *Handler) handleGetContent(w http.ResponseWriter, r *http.Request) int { state, err := h.store.Get(r.Context()) if err != nil { if errors.Is(err, protocol.ErrNotReady) { writeError(w, http.StatusServiceUnavailable, "content state is not ready") return http.StatusServiceUnavailable } if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { writeError(w, http.StatusServiceUnavailable, err.Error()) return http.StatusServiceUnavailable } writeError(w, http.StatusInternalServerError, err.Error()) return http.StatusInternalServerError } writeJSON(w, http.StatusOK, protocolContentResponse{ Route: state.Route, Text: state.Text, Template: state.Template, }) return http.StatusOK } type protocolContentResponse struct { Route string `json:"route"` Text string `json:"str"` Template string `json:"tpl_name"` } type nilStore struct{} func (nilStore) Get(context.Context) (protocol.ContentState, error) { return protocol.ContentState{}, protocol.ErrNotReady } func (nilStore) Set(context.Context, protocol.ContentState) error { return errors.New("mila: store is not configured") } func cleanPath(path string) string { if path == "" { return "/" } if len(path) > 1 { path = strings.TrimRight(path, "/") } return path } 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) } func writeError(w http.ResponseWriter, status int, msg string) { writeJSON(w, status, map[string]string{"error": msg}) }