package handlers import ( "encoding/json" "net/http" "strconv" "time" "unicode/utf8" "github.com/gin-gonic/gin" "booklib/internal/store" ) const maxNoteRunes = 500 func bookmarkJSON(b store.Bookmark) gin.H { return gin.H{ "id": b.ID, "library_id": b.LibraryID, "path": b.BookPath, "locator": json.RawMessage(b.Locator), "percent": b.Percent, "note": b.Note, "created_at": b.CreatedAt.Format(time.RFC3339), } } func (h *H) ListBookmarks(c *gin.Context) { b, ok := h.bookFromParam(c) if !ok { return } rows, e := h.bookmarks.ListBookmarks(c, uid(c), b.LibraryID, b.Path) if e != nil { dbErr(c, e) return } out := make([]gin.H, 0, len(rows)) for _, r := range rows { out = append(out, bookmarkJSON(r)) } c.JSON(http.StatusOK, out) } func (h *H) CreateBookmark(c *gin.Context) { b, ok := h.bookFromParam(c) if !ok { return } var req struct { Locator json.RawMessage `json:"locator"` Percent float64 `json:"percent"` Note string `json:"note"` } if e := c.ShouldBindJSON(&req); e != nil { err(c, http.StatusBadRequest, "bad_request", "json body required") return } if len(req.Locator) == 0 || string(req.Locator) == "null" || !json.Valid(req.Locator) { err(c, http.StatusBadRequest, "bad_request", "locator must be valid json") return } if req.Percent < 0 || req.Percent > 1 { err(c, http.StatusBadRequest, "bad_request", "percent must be in [0,1]") return } if utf8.RuneCountInString(req.Note) > maxNoteRunes { err(c, http.StatusBadRequest, "bad_request", "note too long (max 500 characters)") return } id, e := h.bookmarks.InsertBookmark(c, uid(c), b.LibraryID, b.Path, req.Locator, req.Percent, req.Note) if e != nil { dbErr(c, e) return } c.JSON(http.StatusCreated, bookmarkJSON(store.Bookmark{ ID: id, LibraryID: b.LibraryID, BookPath: b.Path, Locator: req.Locator, Percent: req.Percent, Note: req.Note, CreatedAt: time.Now(), })) } func bookmarkID(c *gin.Context) (int64, bool) { id, e := strconv.ParseInt(c.Param("id"), 10, 64) if e != nil { err(c, http.StatusBadRequest, "bad_request", "bad id") return 0, false } return id, true } func (h *H) PatchBookmark(c *gin.Context) { id, ok := bookmarkID(c) if !ok { return } var req struct { Note string `json:"note"` } if e := c.ShouldBindJSON(&req); e != nil { err(c, http.StatusBadRequest, "bad_request", "json body required") return } if utf8.RuneCountInString(req.Note) > maxNoteRunes { err(c, http.StatusBadRequest, "bad_request", "note too long (max 500 characters)") return } updated, e := h.bookmarks.UpdateBookmarkNote(c, uid(c), id, req.Note) if e != nil { dbErr(c, e) return } if !updated { err(c, http.StatusNotFound, "not_found", "no such bookmark") return } c.JSON(http.StatusOK, gin.H{"id": id, "note": req.Note}) } func (h *H) DeleteBookmark(c *gin.Context) { id, ok := bookmarkID(c) if !ok { return } deleted, e := h.bookmarks.DeleteBookmark(c, uid(c), id) if e != nil { dbErr(c, e) return } if !deleted { err(c, http.StatusNotFound, "not_found", "no such bookmark") return } c.Status(http.StatusNoContent) }