From 4927eb6a11b5e17f331a27492c7cf97a58250d6a Mon Sep 17 00:00:00 2001 From: Hermes Date: Tue, 23 Jun 2026 01:31:05 -0700 Subject: [PATCH] feat(handlers): /api/healthz, posts, comments, authors, channels --- handler.go | 158 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 handler.go diff --git a/handler.go b/handler.go new file mode 100644 index 0000000..c6db37b --- /dev/null +++ b/handler.go @@ -0,0 +1,158 @@ +package main + +import ( + "encoding/json" + "net/http" + "strings" +) + +func writeJSON(w http.ResponseWriter, code int, v any) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + _ = json.NewEncoder(w).Encode(v) +} + +func jsonError(w http.ResponseWriter, code int, msg string) { + writeJSON(w, code, map[string]string{"error": msg}) +} + +func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) { + count, errors := s.store.FlushStats() + mem := s.store.MemUsage() + writeJSON(w, 200, map[string]any{ + "status": "ok", + "flushes": count, + "flush_errors": errors, + "mem": mem, + }) +} + +func (s *Server) handleListPosts(w http.ResponseWriter, r *http.Request) { + channel := r.URL.Query().Get("channel") + posts := s.store.ListPosts(channel, 20) + if posts == nil { + posts = []*Post{} + } + writeJSON(w, 200, map[string]any{"posts": posts}) +} + +func (s *Server) handleGetPost(w http.ResponseWriter, r *http.Request) { + id := strings.TrimPrefix(r.URL.Path, "/api/posts/") + if id == "" || strings.Contains(id, "/") { + jsonError(w, 400, "bad id") + return + } + post, ok := s.store.GetPost(id) + if !ok { + jsonError(w, 404, "not found") + return + } + comments := s.store.ListComments(id) + if comments == nil { + comments = []*Comment{} + } + writeJSON(w, 200, PostWithComments{Post: *post, Comments: comments}) +} + +func (s *Server) handleCreatePost(w http.ResponseWriter, r *http.Request) { + author := r.Header.Get("X-Posts-Author") + if author == "" { + jsonError(w, 401, "X-Posts-Author required") + return + } + var req CreatePostRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + jsonError(w, 400, "bad json") + return + } + m := &CreatePost{ + AuthorID: author, + Title: req.Title, + Body: req.Body, + Channels: req.Channels, + } + if err := Submit(r.Context(), s.store, m); err != nil { + jsonError(w, 400, err.Error()) + return + } + post, _ := s.store.GetPost(m.ResolvedPostID()) + writeJSON(w, 201, post) +} + +func (s *Server) handleCreateComment(w http.ResponseWriter, r *http.Request) { + author := r.Header.Get("X-Posts-Author") + if author == "" { + jsonError(w, 401, "X-Posts-Author required") + return + } + parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/") + if len(parts) != 4 || parts[0] != "api" || parts[1] != "posts" || parts[3] != "comments" { + jsonError(w, 400, "bad path") + return + } + postID := parts[2] + var req CreateCommentRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + jsonError(w, 400, "bad json") + return + } + m := &AddComment{ + PostID: postID, + ParentID: req.ParentID, + AuthorID: author, + Body: req.Body, + } + if err := Submit(r.Context(), s.store, m); err != nil { + jsonError(w, 400, err.Error()) + return + } + comments := s.store.ListComments(postID) + var created *Comment + for _, c := range comments { + if c.ID == m.ResolvedCommentID() { + created = c + break + } + } + writeJSON(w, 201, created) +} + + +func (s *Server) handleUpsertAuthor(w http.ResponseWriter, r *http.Request) { + var req struct { + ID string `json:"id"` + Kind string `json:"kind"` + DisplayName string `json:"display_name"` + AvatarURL string `json:"avatar_url,omitempty"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + jsonError(w, 400, "bad json") + return + } + m := &UpsertAuthor{ID: req.ID, AuthorKind: req.Kind, DisplayName: req.DisplayName, AvatarURL: req.AvatarURL} + if err := Submit(r.Context(), s.store, m); err != nil { + jsonError(w, 400, err.Error()) + return + } + a, _ := s.store.GetAuthor(m.ID) + writeJSON(w, 200, a) +} + +func (s *Server) handleUpsertChannel(w http.ResponseWriter, r *http.Request) { + var req struct { + Slug string `json:"slug"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + jsonError(w, 400, "bad json") + return + } + m := &UpsertChannel{Slug: req.Slug, Name: req.Name, Description: req.Description} + if err := Submit(r.Context(), s.store, m); err != nil { + jsonError(w, 400, err.Error()) + return + } + c, _ := s.store.GetChannel(m.Slug) + writeJSON(w, 200, c) +}