Diffdown is a real-time collaborative Markdown editor/previewer built on the AT Protocol diffdown.com

feat: add comment handlers and xrpc methods

+132
+2
cmd/server/main.go
··· 106 106 mux.HandleFunc("DELETE /api/docs/{rkey}", h.APIDocumentDelete) 107 107 mux.HandleFunc("POST /api/docs/{rkey}/invite", h.DocumentInvite) 108 108 mux.HandleFunc("GET /docs/{rkey}/accept", h.AcceptInvite) 109 + mux.HandleFunc("POST /api/docs/{rkey}/comments", h.CommentCreate) 110 + mux.HandleFunc("GET /api/docs/{rkey}/comments", h.CommentList) 109 111 110 112 // Middleware stack 111 113 stack := middleware.Logger(
+37
internal/atproto/xrpc/client.go
··· 313 313 func (c *Client) PutDocument(rkey string, doc *model.Document) (string, string, error) { 314 314 return c.PutRecord(collectionDocument, rkey, doc) 315 315 } 316 + 317 + const collectionComment = "com.diffdown.comment" 318 + 319 + // CreateComment creates a new comment record. 320 + func (c *Client) CreateComment(comment *model.Comment) (string, error) { 321 + record := map[string]interface{}{ 322 + "$type": "com.diffdown.comment", 323 + "documentURI": comment.DocumentURI, 324 + "paragraphId": comment.ParagraphID, 325 + "text": comment.Text, 326 + "author": comment.AuthorDID, 327 + } 328 + uri, _, err := c.CreateRecord(collectionComment, record) 329 + if err != nil { 330 + return "", err 331 + } 332 + return uri, nil 333 + } 334 + 335 + // ListComments fetches all comments for a document. 336 + func (c *Client) ListComments(documentRKey string) ([]model.Comment, error) { 337 + records, _, err := c.ListRecords(c.session.DID, collectionComment, 100, "") 338 + if err != nil { 339 + return nil, err 340 + } 341 + 342 + var comments []model.Comment 343 + for _, r := range records { 344 + var comment model.Comment 345 + if err := json.Unmarshal(r.Value, &comment); err != nil { 346 + continue 347 + } 348 + comment.URI = r.URI 349 + comments = append(comments, comment) 350 + } 351 + return comments, nil 352 + }
+93
internal/handler/handler.go
··· 550 550 http.Redirect(w, r, "/docs/"+rKey, http.StatusSeeOther) 551 551 } 552 552 553 + // --- API: Comments --- 554 + 555 + func (h *Handler) CommentCreate(w http.ResponseWriter, r *http.Request) { 556 + user := h.currentUser(r) 557 + if user == nil { 558 + http.Error(w, "Unauthorized", http.StatusUnauthorized) 559 + return 560 + } 561 + 562 + rKey := r.PathValue("rkey") 563 + if rKey == "" { 564 + http.Error(w, "Invalid document", http.StatusBadRequest) 565 + return 566 + } 567 + 568 + var req struct { 569 + ParagraphID string `json:"paragraphId"` 570 + Text string `json:"text"` 571 + } 572 + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 573 + http.Error(w, "Invalid request", http.StatusBadRequest) 574 + return 575 + } 576 + 577 + if req.Text == "" { 578 + http.Error(w, "Comment text required", http.StatusBadRequest) 579 + return 580 + } 581 + 582 + session, err := h.DB.GetATProtoSession(user.ID) 583 + if err != nil || session == nil { 584 + http.Error(w, "Not authenticated with ATProto", http.StatusUnauthorized) 585 + return 586 + } 587 + 588 + client, err := h.xrpcClient(user.ID) 589 + if err != nil { 590 + http.Error(w, "Failed to connect to ATProto", http.StatusInternalServerError) 591 + return 592 + } 593 + 594 + comment := &model.Comment{ 595 + DocumentURI: fmt.Sprintf("at://%s/com.diffdown.document/%s", session.DID, rKey), 596 + ParagraphID: req.ParagraphID, 597 + Text: req.Text, 598 + AuthorDID: session.DID, 599 + } 600 + 601 + uri, err := client.CreateComment(comment) 602 + if err != nil { 603 + log.Printf("CommentCreate: %v", err) 604 + http.Error(w, "Failed to create comment", http.StatusInternalServerError) 605 + return 606 + } 607 + 608 + h.jsonResponse(w, map[string]string{"uri": uri}, http.StatusCreated) 609 + } 610 + 611 + func (h *Handler) CommentList(w http.ResponseWriter, r *http.Request) { 612 + rKey := r.PathValue("rkey") 613 + if rKey == "" { 614 + http.Error(w, "Invalid document", http.StatusBadRequest) 615 + return 616 + } 617 + 618 + user := h.currentUser(r) 619 + if user == nil { 620 + http.Error(w, "Unauthorized", http.StatusUnauthorized) 621 + return 622 + } 623 + 624 + session, err := h.DB.GetATProtoSession(user.ID) 625 + if err != nil || session == nil { 626 + http.Error(w, "Not authenticated with ATProto", http.StatusUnauthorized) 627 + return 628 + } 629 + 630 + client, err := h.xrpcClient(user.ID) 631 + if err != nil { 632 + http.Error(w, "Failed to connect to ATProto", http.StatusInternalServerError) 633 + return 634 + } 635 + 636 + comments, err := client.ListComments(rKey) 637 + if err != nil { 638 + log.Printf("CommentList: %v", err) 639 + http.Error(w, "Failed to list comments", http.StatusInternalServerError) 640 + return 641 + } 642 + 643 + h.jsonResponse(w, comments, http.StatusOK) 644 + } 645 + 553 646 // --- API: Render markdown --- 554 647 555 648 func (h *Handler) APIRender(w http.ResponseWriter, r *http.Request) {