tangled
alpha
login
or
join now
diffdown.com
/
diffdown-app
0
fork
atom
Diffdown is a real-time collaborative Markdown editor/previewer built on the AT Protocol
diffdown.com
0
fork
atom
overview
issues
10
pulls
pipelines
feat: add comment handlers and xrpc methods
John Luther
3 weeks ago
d222fc8b
8eb3d482
+132
3 changed files
expand all
collapse all
unified
split
cmd
server
main.go
internal
atproto
xrpc
client.go
handler
handler.go
+2
cmd/server/main.go
reviewed
···
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
109
+
mux.HandleFunc("POST /api/docs/{rkey}/comments", h.CommentCreate)
110
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
reviewed
···
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
316
+
317
317
+
const collectionComment = "com.diffdown.comment"
318
318
+
319
319
+
// CreateComment creates a new comment record.
320
320
+
func (c *Client) CreateComment(comment *model.Comment) (string, error) {
321
321
+
record := map[string]interface{}{
322
322
+
"$type": "com.diffdown.comment",
323
323
+
"documentURI": comment.DocumentURI,
324
324
+
"paragraphId": comment.ParagraphID,
325
325
+
"text": comment.Text,
326
326
+
"author": comment.AuthorDID,
327
327
+
}
328
328
+
uri, _, err := c.CreateRecord(collectionComment, record)
329
329
+
if err != nil {
330
330
+
return "", err
331
331
+
}
332
332
+
return uri, nil
333
333
+
}
334
334
+
335
335
+
// ListComments fetches all comments for a document.
336
336
+
func (c *Client) ListComments(documentRKey string) ([]model.Comment, error) {
337
337
+
records, _, err := c.ListRecords(c.session.DID, collectionComment, 100, "")
338
338
+
if err != nil {
339
339
+
return nil, err
340
340
+
}
341
341
+
342
342
+
var comments []model.Comment
343
343
+
for _, r := range records {
344
344
+
var comment model.Comment
345
345
+
if err := json.Unmarshal(r.Value, &comment); err != nil {
346
346
+
continue
347
347
+
}
348
348
+
comment.URI = r.URI
349
349
+
comments = append(comments, comment)
350
350
+
}
351
351
+
return comments, nil
352
352
+
}
+93
internal/handler/handler.go
reviewed
···
550
550
http.Redirect(w, r, "/docs/"+rKey, http.StatusSeeOther)
551
551
}
552
552
553
553
+
// --- API: Comments ---
554
554
+
555
555
+
func (h *Handler) CommentCreate(w http.ResponseWriter, r *http.Request) {
556
556
+
user := h.currentUser(r)
557
557
+
if user == nil {
558
558
+
http.Error(w, "Unauthorized", http.StatusUnauthorized)
559
559
+
return
560
560
+
}
561
561
+
562
562
+
rKey := r.PathValue("rkey")
563
563
+
if rKey == "" {
564
564
+
http.Error(w, "Invalid document", http.StatusBadRequest)
565
565
+
return
566
566
+
}
567
567
+
568
568
+
var req struct {
569
569
+
ParagraphID string `json:"paragraphId"`
570
570
+
Text string `json:"text"`
571
571
+
}
572
572
+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
573
573
+
http.Error(w, "Invalid request", http.StatusBadRequest)
574
574
+
return
575
575
+
}
576
576
+
577
577
+
if req.Text == "" {
578
578
+
http.Error(w, "Comment text required", http.StatusBadRequest)
579
579
+
return
580
580
+
}
581
581
+
582
582
+
session, err := h.DB.GetATProtoSession(user.ID)
583
583
+
if err != nil || session == nil {
584
584
+
http.Error(w, "Not authenticated with ATProto", http.StatusUnauthorized)
585
585
+
return
586
586
+
}
587
587
+
588
588
+
client, err := h.xrpcClient(user.ID)
589
589
+
if err != nil {
590
590
+
http.Error(w, "Failed to connect to ATProto", http.StatusInternalServerError)
591
591
+
return
592
592
+
}
593
593
+
594
594
+
comment := &model.Comment{
595
595
+
DocumentURI: fmt.Sprintf("at://%s/com.diffdown.document/%s", session.DID, rKey),
596
596
+
ParagraphID: req.ParagraphID,
597
597
+
Text: req.Text,
598
598
+
AuthorDID: session.DID,
599
599
+
}
600
600
+
601
601
+
uri, err := client.CreateComment(comment)
602
602
+
if err != nil {
603
603
+
log.Printf("CommentCreate: %v", err)
604
604
+
http.Error(w, "Failed to create comment", http.StatusInternalServerError)
605
605
+
return
606
606
+
}
607
607
+
608
608
+
h.jsonResponse(w, map[string]string{"uri": uri}, http.StatusCreated)
609
609
+
}
610
610
+
611
611
+
func (h *Handler) CommentList(w http.ResponseWriter, r *http.Request) {
612
612
+
rKey := r.PathValue("rkey")
613
613
+
if rKey == "" {
614
614
+
http.Error(w, "Invalid document", http.StatusBadRequest)
615
615
+
return
616
616
+
}
617
617
+
618
618
+
user := h.currentUser(r)
619
619
+
if user == nil {
620
620
+
http.Error(w, "Unauthorized", http.StatusUnauthorized)
621
621
+
return
622
622
+
}
623
623
+
624
624
+
session, err := h.DB.GetATProtoSession(user.ID)
625
625
+
if err != nil || session == nil {
626
626
+
http.Error(w, "Not authenticated with ATProto", http.StatusUnauthorized)
627
627
+
return
628
628
+
}
629
629
+
630
630
+
client, err := h.xrpcClient(user.ID)
631
631
+
if err != nil {
632
632
+
http.Error(w, "Failed to connect to ATProto", http.StatusInternalServerError)
633
633
+
return
634
634
+
}
635
635
+
636
636
+
comments, err := client.ListComments(rKey)
637
637
+
if err != nil {
638
638
+
log.Printf("CommentList: %v", err)
639
639
+
http.Error(w, "Failed to list comments", http.StatusInternalServerError)
640
640
+
return
641
641
+
}
642
642
+
643
643
+
h.jsonResponse(w, comments, http.StatusOK)
644
644
+
}
645
645
+
553
646
// --- API: Render markdown ---
554
647
555
648
func (h *Handler) APIRender(w http.ResponseWriter, r *http.Request) {