Files
cairnquire/apps/server/internal/httpserver/collab_handlers.go
2026-05-31 23:08:53 -04:00

263 lines
8.8 KiB
Go

package httpserver
import (
"encoding/json"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/tim/cairnquire/apps/server/internal/collaboration"
)
// Comments
func (s *Server) handleListComments(w http.ResponseWriter, r *http.Request) {
documentID := r.URL.Query().Get("document_id")
if documentID == "" {
writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": "document_id is required"})
return
}
anchorHash := r.URL.Query().Get("anchor_hash")
var comments []collaboration.Comment
var err error
if anchorHash != "" {
comments, err = s.collaboration.ListCommentsByAnchor(r.Context(), documentID, anchorHash)
} else {
comments, err = s.collaboration.ListComments(r.Context(), documentID)
}
if err != nil {
s.logger.Warn("list comments", "error", err)
writeJSONWithStatus(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, map[string]any{"comments": comments})
}
func (s *Server) handleCreateComment(w http.ResponseWriter, r *http.Request) {
principal, ok := requirePrincipal(r)
if !ok {
writeJSONWithStatus(w, http.StatusUnauthorized, map[string]string{"error": "authentication required"})
return
}
var input collaboration.CreateCommentInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"})
return
}
comment, err := s.collaboration.CreateComment(r.Context(), principal, input)
if err != nil {
s.logger.Warn("create comment", "error", err)
writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
writeJSONWithStatus(w, http.StatusCreated, comment)
}
func (s *Server) handleResolveComment(w http.ResponseWriter, r *http.Request) {
principal, ok := requirePrincipal(r)
if !ok {
writeJSONWithStatus(w, http.StatusUnauthorized, map[string]string{"error": "authentication required"})
return
}
commentID := chi.URLParam(r, "id")
if err := s.collaboration.ResolveComment(r.Context(), principal, commentID); err != nil {
s.logger.Warn("resolve comment", "error", err)
writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "resolved"})
}
// Notifications
func (s *Server) handleListNotifications(w http.ResponseWriter, r *http.Request) {
principal, ok := requirePrincipal(r)
if !ok {
writeJSONWithStatus(w, http.StatusUnauthorized, map[string]string{"error": "authentication required"})
return
}
unreadOnly := r.URL.Query().Get("unread") == "true"
limit := 50
if r.URL.Query().Get("limit") != "" {
// Parse limit if needed, but for now just use default
}
notifications, err := s.collaboration.ListNotifications(r.Context(), principal, unreadOnly, limit)
if err != nil {
s.logger.Warn("list notifications", "error", err)
writeJSONWithStatus(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, map[string]any{"notifications": notifications})
}
func (s *Server) handleMarkNotificationRead(w http.ResponseWriter, r *http.Request) {
principal, ok := requirePrincipal(r)
if !ok {
writeJSONWithStatus(w, http.StatusUnauthorized, map[string]string{"error": "authentication required"})
return
}
notificationID := chi.URLParam(r, "id")
if err := s.collaboration.MarkNotificationRead(r.Context(), principal, notificationID); err != nil {
s.logger.Warn("mark notification read", "error", err)
writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
func (s *Server) handleMarkAllNotificationsRead(w http.ResponseWriter, r *http.Request) {
principal, ok := requirePrincipal(r)
if !ok {
writeJSONWithStatus(w, http.StatusUnauthorized, map[string]string{"error": "authentication required"})
return
}
if err := s.collaboration.MarkAllNotificationsRead(r.Context(), principal); err != nil {
s.logger.Warn("mark all notifications read", "error", err)
writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
func (s *Server) handleNotificationBell(w http.ResponseWriter, r *http.Request) {
principal, ok := requirePrincipal(r)
if !ok {
writeJSON(w, http.StatusOK, collaboration.NotificationBell{UnreadCount: 0, Items: []collaboration.Notification{}})
return
}
bell, err := s.collaboration.GetBellStatus(r.Context(), principal)
if err != nil {
s.logger.Warn("notification bell", "error", err)
writeJSONWithStatus(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, bell)
}
// Watchers
func (s *Server) handleWatchDocument(w http.ResponseWriter, r *http.Request) {
principal, ok := requirePrincipal(r)
if !ok {
writeJSONWithStatus(w, http.StatusUnauthorized, map[string]string{"error": "authentication required"})
return
}
var req struct {
DocumentID string `json:"documentId"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"})
return
}
if err := s.collaboration.WatchDocument(r.Context(), principal, req.DocumentID); err != nil {
s.logger.Warn("watch document", "error", err)
writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "watching"})
}
func (s *Server) handleUnwatchDocument(w http.ResponseWriter, r *http.Request) {
principal, ok := requirePrincipal(r)
if !ok {
writeJSONWithStatus(w, http.StatusUnauthorized, map[string]string{"error": "authentication required"})
return
}
var req struct {
DocumentID string `json:"documentId"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"})
return
}
if err := s.collaboration.UnwatchDocument(r.Context(), principal, req.DocumentID); err != nil {
s.logger.Warn("unwatch document", "error", err)
writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "unwatched"})
}
func (s *Server) handleWatchFolder(w http.ResponseWriter, r *http.Request) {
principal, ok := requirePrincipal(r)
if !ok {
writeJSONWithStatus(w, http.StatusUnauthorized, map[string]string{"error": "authentication required"})
return
}
var req struct {
FolderPath string `json:"folderPath"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"})
return
}
if err := s.collaboration.WatchFolder(r.Context(), principal, req.FolderPath); err != nil {
s.logger.Warn("watch folder", "error", err)
writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "watching"})
}
func (s *Server) handleUnwatchFolder(w http.ResponseWriter, r *http.Request) {
principal, ok := requirePrincipal(r)
if !ok {
writeJSONWithStatus(w, http.StatusUnauthorized, map[string]string{"error": "authentication required"})
return
}
var req struct {
FolderPath string `json:"folderPath"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"})
return
}
if err := s.collaboration.UnwatchFolder(r.Context(), principal, req.FolderPath); err != nil {
s.logger.Warn("unwatch folder", "error", err)
writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "unwatched"})
}
func (s *Server) handleIsWatching(w http.ResponseWriter, r *http.Request) {
principal, ok := requirePrincipal(r)
if !ok {
writeJSON(w, http.StatusOK, map[string]bool{"watching": false})
return
}
documentID := r.URL.Query().Get("document_id")
watching, err := s.collaboration.IsWatching(r.Context(), principal, documentID)
if err != nil {
s.logger.Warn("is watching", "error", err)
writeJSONWithStatus(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, map[string]bool{"watching": watching})
}
// Document page with comments
func (s *Server) renderDocumentPageWithComments(w http.ResponseWriter, r *http.Request, pagePath string) {
// This is a helper that could be used to inject comments into the template
// For now, comments are fetched client-side via JS
s.renderDocumentPage(w, r, pagePath)
}