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
John Luther
3 weeks ago
db911784
ad49f775
+143
-1
4 changed files
expand all
collapse all
unified
split
cmd
server
main.go
internal
atproto
xrpc
client.go
handler
handler.go
model
models.go
+2
cmd/server/main.go
reviewed
···
100
100
mux.HandleFunc("POST /api/docs/{rkey}/save", h.APIDocumentSave)
101
101
mux.HandleFunc("PUT /api/docs/{rkey}/autosave", h.APIDocumentAutoSave)
102
102
mux.HandleFunc("DELETE /api/docs/{rkey}", h.APIDocumentDelete)
103
103
+
mux.HandleFunc("POST /api/docs/{rkey}/comments", h.CommentCreate)
104
104
+
mux.HandleFunc("GET /api/docs/{rkey}/comments", h.CommentList)
103
105
104
106
// Middleware stack
105
107
stack := middleware.Logger(
+40
internal/atproto/xrpc/client.go
reviewed
···
16
16
"github.com/limeleaf/diffdown/internal/model"
17
17
)
18
18
19
19
+
const collectionComment = "com.diffdown.comment"
20
20
+
19
21
type Client struct {
20
22
db *db.DB
21
23
userID string
···
291
293
}
292
294
return nil
293
295
}
296
296
+
297
297
+
// CreateComment creates a new comment record.
298
298
+
func (c *Client) CreateComment(comment *model.Comment) (string, error) {
299
299
+
now := time.Now().UTC().Format(time.RFC3339)
300
300
+
record := map[string]interface{}{
301
301
+
"$type": "com.diffdown.comment",
302
302
+
"documentUri": comment.DocumentURI,
303
303
+
"paragraphId": comment.ParagraphID,
304
304
+
"text": comment.Text,
305
305
+
"authorDid": comment.AuthorDID,
306
306
+
"createdAt": now,
307
307
+
}
308
308
+
309
309
+
uri, _, err := c.CreateRecord(collectionComment, record)
310
310
+
if err != nil {
311
311
+
return "", err
312
312
+
}
313
313
+
return uri, nil
314
314
+
}
315
315
+
316
316
+
// ListComments lists all comments for a document.
317
317
+
func (c *Client) ListComments(rkey string) ([]model.Comment, error) {
318
318
+
records, _, err := c.ListRecords(c.session.DID, collectionComment, 100, "")
319
319
+
if err != nil {
320
320
+
return nil, err
321
321
+
}
322
322
+
323
323
+
var comments []model.Comment
324
324
+
for _, rec := range records {
325
325
+
var comment model.Comment
326
326
+
if err := json.Unmarshal(rec.Value, &comment); err != nil {
327
327
+
continue
328
328
+
}
329
329
+
comment.URI = rec.URI
330
330
+
comments = append(comments, comment)
331
331
+
}
332
332
+
return comments, nil
333
333
+
}
+92
internal/handler/handler.go
reviewed
···
18
18
)
19
19
20
20
const collectionDocument = "com.diffdown.document"
21
21
+
const collectionComment = "com.diffdown.comment"
21
22
22
23
type Handler struct {
23
24
DB *db.DB
···
419
420
}
420
421
421
422
h.jsonResponse(w, map[string]string{"status": "ok"})
423
423
+
}
424
424
+
425
425
+
// --- API: Comments ---
426
426
+
427
427
+
func (h *Handler) CommentCreate(w http.ResponseWriter, r *http.Request) {
428
428
+
user := h.currentUser(r)
429
429
+
if user == nil {
430
430
+
http.Error(w, "Unauthorized", http.StatusUnauthorized)
431
431
+
return
432
432
+
}
433
433
+
434
434
+
rKey := r.PathValue("rkey")
435
435
+
if rKey == "" {
436
436
+
http.Error(w, "Invalid document", http.StatusBadRequest)
437
437
+
return
438
438
+
}
439
439
+
440
440
+
var req struct {
441
441
+
ParagraphID string `json:"paragraphId"`
442
442
+
Text string `json:"text"`
443
443
+
}
444
444
+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
445
445
+
http.Error(w, "Invalid request", http.StatusBadRequest)
446
446
+
return
447
447
+
}
448
448
+
449
449
+
if req.Text == "" {
450
450
+
http.Error(w, "Comment text required", http.StatusBadRequest)
451
451
+
return
452
452
+
}
453
453
+
454
454
+
client, err := h.xrpcClient(user.ID)
455
455
+
if err != nil {
456
456
+
log.Printf("CommentCreate: xrpc client: %v", err)
457
457
+
http.Error(w, "Not authenticated with ATProto", http.StatusUnauthorized)
458
458
+
return
459
459
+
}
460
460
+
461
461
+
session, err := h.DB.GetATProtoSession(user.ID)
462
462
+
if err != nil || session == nil {
463
463
+
http.Error(w, "Not authenticated with ATProto", http.StatusUnauthorized)
464
464
+
return
465
465
+
}
466
466
+
467
467
+
comment := &model.Comment{
468
468
+
DocumentURI: fmt.Sprintf("at://%s/com.diffdown.document/%s", session.DID, rKey),
469
469
+
ParagraphID: req.ParagraphID,
470
470
+
Text: req.Text,
471
471
+
AuthorDID: session.DID,
472
472
+
}
473
473
+
474
474
+
uri, err := client.CreateComment(comment)
475
475
+
if err != nil {
476
476
+
log.Printf("CommentCreate: %v", err)
477
477
+
http.Error(w, "Failed to create comment", http.StatusInternalServerError)
478
478
+
return
479
479
+
}
480
480
+
481
481
+
w.WriteHeader(http.StatusCreated)
482
482
+
h.jsonResponse(w, map[string]string{"uri": uri})
483
483
+
}
484
484
+
485
485
+
func (h *Handler) CommentList(w http.ResponseWriter, r *http.Request) {
486
486
+
rKey := r.PathValue("rkey")
487
487
+
if rKey == "" {
488
488
+
http.Error(w, "Invalid document", http.StatusBadRequest)
489
489
+
return
490
490
+
}
491
491
+
492
492
+
user := h.currentUser(r)
493
493
+
if user == nil {
494
494
+
http.Error(w, "Unauthorized", http.StatusUnauthorized)
495
495
+
return
496
496
+
}
497
497
+
498
498
+
client, err := h.xrpcClient(user.ID)
499
499
+
if err != nil {
500
500
+
log.Printf("CommentList: xrpc client: %v", err)
501
501
+
http.Error(w, "Not authenticated with ATProto", http.StatusUnauthorized)
502
502
+
return
503
503
+
}
504
504
+
505
505
+
comments, err := client.ListComments(rKey)
506
506
+
if err != nil {
507
507
+
log.Printf("CommentList: %v", err)
508
508
+
http.Error(w, "Failed to list comments", http.StatusInternalServerError)
509
509
+
return
510
510
+
}
511
511
+
512
512
+
w.WriteHeader(http.StatusOK)
513
513
+
h.jsonResponse(w, comments)
422
514
}
423
515
424
516
// --- API: Render markdown ---
+9
-1
internal/model/models.go
reviewed
···
56
56
RawMarkdown string `json:"rawMarkdown"`
57
57
}
58
58
59
59
+
type Comment struct {
60
60
+
URI string `json:"uri,omitempty"`
61
61
+
DocumentURI string `json:"documentUri"`
62
62
+
ParagraphID string `json:"paragraphId"`
63
63
+
Text string `json:"text"`
64
64
+
AuthorDID string `json:"authorDid"`
65
65
+
CreatedAt string `json:"createdAt,omitempty"`
66
66
+
}
67
67
+
59
68
// RKeyFromURI extracts the rkey (last path segment) from an at:// URI.
60
69
func RKeyFromURI(uri string) string {
61
70
parts := strings.Split(uri, "/")
···
64
73
}
65
74
return ""
66
75
}
67
67
-