package mila import ( "context" "encoding/json" "errors" "log/slog" "net/http" "strings" "time" ) type Handler struct { cfg Config store Store logger *slog.Logger onSet func(ContentState) } func NewHandler(cfg Config, store Store, opts ...Option) *Handler { if store == nil { store = NewMemoryStore() } h := &Handler{ cfg: 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 := ContentState{ Route: strings.TrimSpace(q.Get("route")), Text: q.Get("str"), Template: strings.TrimSpace(q.Get("tpl_name")), UpdatedAt: time.Now().UTC(), } if err := Validate(h.cfg, state); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return http.StatusBadRequest } 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, 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"` } 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}) }