feat(handlers): /api/healthz, posts, comments, authors, channels

This commit is contained in:
Hermes 2026-06-23 01:31:05 -07:00
parent c93e236267
commit 4927eb6a11

158
handler.go Normal file
View file

@ -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)
}