Signed-off-by: Seongmin Lee git@boltless.me
+98
-386
Diff
round #5
+6
-186
appview/db/issues.go
+6
-186
appview/db/issues.go
···
100
}
101
102
func GetIssuesPaginated(e Execer, page pagination.Page, filters ...orm.Filter) ([]models.Issue, error) {
103
-
issueMap := make(map[string]*models.Issue) // at-uri -> issue
104
105
var conditions []string
106
var args []any
···
196
}
197
}
198
199
-
atUri := issue.AtUri().String()
200
-
issueMap[atUri] = &issue
201
}
202
203
// collect reverse repos
···
229
// collect comments
230
issueAts := slices.Collect(maps.Keys(issueMap))
231
232
-
comments, err := GetIssueComments(e, orm.FilterIn("issue_at", issueAts))
233
if err != nil {
234
return nil, fmt.Errorf("failed to query comments: %w", err)
235
}
236
for i := range comments {
237
-
issueAt := comments[i].IssueAt
238
if issue, ok := issueMap[issueAt]; ok {
239
issue.Comments = append(issue.Comments, comments[i])
240
}
···
246
return nil, fmt.Errorf("failed to query labels: %w", err)
247
}
248
for issueAt, labels := range allLabels {
249
-
if issue, ok := issueMap[issueAt.String()]; ok {
250
issue.Labels = labels
251
}
252
}
···
257
return nil, fmt.Errorf("failed to query reference_links: %w", err)
258
}
259
for issueAt, references := range allReferencs {
260
-
if issue, ok := issueMap[issueAt.String()]; ok {
261
issue.References = references
262
}
263
}
···
295
return GetIssuesPaginated(e, pagination.Page{}, filters...)
296
}
297
298
-
func AddIssueComment(tx *sql.Tx, c models.IssueComment) (int64, error) {
299
-
result, err := tx.Exec(
300
-
`insert into issue_comments (
301
-
did,
302
-
rkey,
303
-
issue_at,
304
-
body,
305
-
reply_to,
306
-
created,
307
-
edited
308
-
)
309
-
values (?, ?, ?, ?, ?, ?, null)
310
-
on conflict(did, rkey) do update set
311
-
issue_at = excluded.issue_at,
312
-
body = excluded.body,
313
-
edited = case
314
-
when
315
-
issue_comments.issue_at != excluded.issue_at
316
-
or issue_comments.body != excluded.body
317
-
or issue_comments.reply_to != excluded.reply_to
318
-
then ?
319
-
else issue_comments.edited
320
-
end`,
321
-
c.Did,
322
-
c.Rkey,
323
-
c.IssueAt,
324
-
c.Body,
325
-
c.ReplyTo,
326
-
c.Created.Format(time.RFC3339),
327
-
time.Now().Format(time.RFC3339),
328
-
)
329
-
if err != nil {
330
-
return 0, err
331
-
}
332
-
333
-
id, err := result.LastInsertId()
334
-
if err != nil {
335
-
return 0, err
336
-
}
337
-
338
-
if err := putReferences(tx, c.AtUri(), c.References); err != nil {
339
-
return 0, fmt.Errorf("put reference_links: %w", err)
340
-
}
341
-
342
-
return id, nil
343
-
}
344
-
345
-
func DeleteIssueComments(e Execer, filters ...orm.Filter) error {
346
-
var conditions []string
347
-
var args []any
348
-
for _, filter := range filters {
349
-
conditions = append(conditions, filter.Condition())
350
-
args = append(args, filter.Arg()...)
351
-
}
352
-
353
-
whereClause := ""
354
-
if conditions != nil {
355
-
whereClause = " where " + strings.Join(conditions, " and ")
356
-
}
357
-
358
-
query := fmt.Sprintf(`update issue_comments set body = "", deleted = strftime('%%Y-%%m-%%dT%%H:%%M:%%SZ', 'now') %s`, whereClause)
359
-
360
-
_, err := e.Exec(query, args...)
361
-
return err
362
-
}
363
-
364
-
func GetIssueComments(e Execer, filters ...orm.Filter) ([]models.IssueComment, error) {
365
-
commentMap := make(map[string]*models.IssueComment)
366
-
367
-
var conditions []string
368
-
var args []any
369
-
for _, filter := range filters {
370
-
conditions = append(conditions, filter.Condition())
371
-
args = append(args, filter.Arg()...)
372
-
}
373
-
374
-
whereClause := ""
375
-
if conditions != nil {
376
-
whereClause = " where " + strings.Join(conditions, " and ")
377
-
}
378
-
379
-
query := fmt.Sprintf(`
380
-
select
381
-
id,
382
-
did,
383
-
rkey,
384
-
issue_at,
385
-
reply_to,
386
-
body,
387
-
created,
388
-
edited,
389
-
deleted
390
-
from
391
-
issue_comments
392
-
%s
393
-
`, whereClause)
394
-
395
-
rows, err := e.Query(query, args...)
396
-
if err != nil {
397
-
return nil, err
398
-
}
399
-
defer rows.Close()
400
-
401
-
for rows.Next() {
402
-
var comment models.IssueComment
403
-
var created string
404
-
var rkey, edited, deleted, replyTo sql.Null[string]
405
-
err := rows.Scan(
406
-
&comment.Id,
407
-
&comment.Did,
408
-
&rkey,
409
-
&comment.IssueAt,
410
-
&replyTo,
411
-
&comment.Body,
412
-
&created,
413
-
&edited,
414
-
&deleted,
415
-
)
416
-
if err != nil {
417
-
return nil, err
418
-
}
419
-
420
-
// this is a remnant from old times, newer comments always have rkey
421
-
if rkey.Valid {
422
-
comment.Rkey = rkey.V
423
-
}
424
-
425
-
if t, err := time.Parse(time.RFC3339, created); err == nil {
426
-
comment.Created = t
427
-
}
428
-
429
-
if edited.Valid {
430
-
if t, err := time.Parse(time.RFC3339, edited.V); err == nil {
431
-
comment.Edited = &t
432
-
}
433
-
}
434
-
435
-
if deleted.Valid {
436
-
if t, err := time.Parse(time.RFC3339, deleted.V); err == nil {
437
-
comment.Deleted = &t
438
-
}
439
-
}
440
-
441
-
if replyTo.Valid {
442
-
comment.ReplyTo = &replyTo.V
443
-
}
444
-
445
-
atUri := comment.AtUri().String()
446
-
commentMap[atUri] = &comment
447
-
}
448
-
449
-
if err = rows.Err(); err != nil {
450
-
return nil, err
451
-
}
452
-
453
-
// collect references for each comments
454
-
commentAts := slices.Collect(maps.Keys(commentMap))
455
-
allReferencs, err := GetReferencesAll(e, orm.FilterIn("from_at", commentAts))
456
-
if err != nil {
457
-
return nil, fmt.Errorf("failed to query reference_links: %w", err)
458
-
}
459
-
for commentAt, references := range allReferencs {
460
-
if comment, ok := commentMap[commentAt.String()]; ok {
461
-
comment.References = references
462
-
}
463
-
}
464
-
465
-
var comments []models.IssueComment
466
-
for _, c := range commentMap {
467
-
comments = append(comments, *c)
468
-
}
469
-
470
-
sort.Slice(comments, func(i, j int) bool {
471
-
return comments[i].Created.After(comments[j].Created)
472
-
})
473
-
474
-
return comments, nil
475
-
}
476
-
477
func DeleteIssues(tx *sql.Tx, did, rkey string) error {
478
_, err := tx.Exec(
479
`delete from issues
···
100
}
101
102
func GetIssuesPaginated(e Execer, page pagination.Page, filters ...orm.Filter) ([]models.Issue, error) {
103
+
issueMap := make(map[syntax.ATURI]*models.Issue) // at-uri -> issue
104
105
var conditions []string
106
var args []any
···
196
}
197
}
198
199
+
issueMap[issue.AtUri()] = &issue
200
}
201
202
// collect reverse repos
···
228
// collect comments
229
issueAts := slices.Collect(maps.Keys(issueMap))
230
231
+
comments, err := GetComments(e, orm.FilterIn("subject_at", issueAts))
232
if err != nil {
233
return nil, fmt.Errorf("failed to query comments: %w", err)
234
}
235
for i := range comments {
236
+
issueAt := comments[i].Subject
237
if issue, ok := issueMap[issueAt]; ok {
238
issue.Comments = append(issue.Comments, comments[i])
239
}
···
245
return nil, fmt.Errorf("failed to query labels: %w", err)
246
}
247
for issueAt, labels := range allLabels {
248
+
if issue, ok := issueMap[issueAt]; ok {
249
issue.Labels = labels
250
}
251
}
···
256
return nil, fmt.Errorf("failed to query reference_links: %w", err)
257
}
258
for issueAt, references := range allReferencs {
259
+
if issue, ok := issueMap[issueAt]; ok {
260
issue.References = references
261
}
262
}
···
294
return GetIssuesPaginated(e, pagination.Page{}, filters...)
295
}
296
297
func DeleteIssues(tx *sql.Tx, did, rkey string) error {
298
_, err := tx.Exec(
299
`delete from issues
+13
-24
appview/db/reference.go
+13
-24
appview/db/reference.go
···
11
"tangled.org/core/orm"
12
)
13
14
-
// ValidateReferenceLinks resolves refLinks to Issue/PR/IssueComment/PullComment ATURIs.
15
// It will ignore missing refLinks.
16
func ValidateReferenceLinks(e Execer, refLinks []models.ReferenceLink) ([]syntax.ATURI, error) {
17
var (
···
53
values %s
54
)
55
select
56
-
i.did, i.rkey,
57
-
c.did, c.rkey
58
from input inp
59
join repos r
60
on r.did = inp.owner_did
···
62
join issues i
63
on i.repo_at = r.at_uri
64
and i.issue_id = inp.issue_id
65
-
left join issue_comments c
66
on inp.comment_id is not null
67
-
and c.issue_at = i.at_uri
68
and c.id = inp.comment_id
69
`,
70
strings.Join(vals, ","),
···
79
80
for rows.Next() {
81
// Scan rows
82
-
var issueOwner, issueRkey string
83
-
var commentOwner, commentRkey sql.NullString
84
var uri syntax.ATURI
85
-
if err := rows.Scan(&issueOwner, &issueRkey, &commentOwner, &commentRkey); err != nil {
86
return nil, err
87
}
88
-
if commentOwner.Valid && commentRkey.Valid {
89
-
uri = syntax.ATURI(fmt.Sprintf(
90
-
"at://%s/%s/%s",
91
-
commentOwner.String,
92
-
tangled.RepoIssueCommentNSID,
93
-
commentRkey.String,
94
-
))
95
} else {
96
-
uri = syntax.ATURI(fmt.Sprintf(
97
-
"at://%s/%s/%s",
98
-
issueOwner,
99
-
tangled.RepoIssueNSID,
100
-
issueRkey,
101
-
))
102
}
103
uris = append(uris, uri)
104
}
···
282
return nil, fmt.Errorf("get issue backlinks: %w", err)
283
}
284
backlinks = append(backlinks, ls...)
285
-
ls, err = getIssueCommentBacklinks(e, backlinksMap[tangled.RepoIssueCommentNSID])
286
if err != nil {
287
return nil, fmt.Errorf("get issue_comment backlinks: %w", err)
288
}
···
351
rows, err := e.Query(
352
fmt.Sprintf(
353
`select r.did, r.name, i.issue_id, c.id, i.title, i.open
354
-
from issue_comments c
355
join issues i
356
-
on i.at_uri = c.issue_at
357
join repos r
358
on r.at_uri = i.repo_at
359
where %s`,
···
11
"tangled.org/core/orm"
12
)
13
14
+
// ValidateReferenceLinks resolves refLinks to Issue/PR/Comment ATURIs.
15
// It will ignore missing refLinks.
16
func ValidateReferenceLinks(e Execer, refLinks []models.ReferenceLink) ([]syntax.ATURI, error) {
17
var (
···
53
values %s
54
)
55
select
56
+
i.at_uri, c.at_uri
57
from input inp
58
join repos r
59
on r.did = inp.owner_did
···
61
join issues i
62
on i.repo_at = r.at_uri
63
and i.issue_id = inp.issue_id
64
+
left join comments c
65
on inp.comment_id is not null
66
+
and c.subject_at = i.at_uri
67
and c.id = inp.comment_id
68
`,
69
strings.Join(vals, ","),
···
78
79
for rows.Next() {
80
// Scan rows
81
+
var issueUri string
82
+
var commentUri sql.NullString
83
var uri syntax.ATURI
84
+
if err := rows.Scan(&issueUri, &commentUri); err != nil {
85
return nil, err
86
}
87
+
if commentUri.Valid {
88
+
uri = syntax.ATURI(commentUri.String)
89
} else {
90
+
uri = syntax.ATURI(issueUri)
91
}
92
uris = append(uris, uri)
93
}
···
271
return nil, fmt.Errorf("get issue backlinks: %w", err)
272
}
273
backlinks = append(backlinks, ls...)
274
+
ls, err = getIssueCommentBacklinks(e, backlinksMap[tangled.CommentNSID])
275
if err != nil {
276
return nil, fmt.Errorf("get issue_comment backlinks: %w", err)
277
}
···
340
rows, err := e.Query(
341
fmt.Sprintf(
342
`select r.did, r.name, i.issue_id, c.id, i.title, i.open
343
+
from comments c
344
join issues i
345
+
on i.at_uri = c.subject_at
346
join repos r
347
on r.at_uri = i.repo_at
348
where %s`,
+15
-6
appview/ingester.go
+15
-6
appview/ingester.go
···
885
}
886
887
switch e.Commit.Operation {
888
-
case jmodels.CommitOperationCreate, jmodels.CommitOperationUpdate:
889
raw := json.RawMessage(e.Commit.Record)
890
record := tangled.RepoIssueComment{}
891
err = json.Unmarshal(raw, &record)
···
893
return fmt.Errorf("invalid record: %w", err)
894
}
895
896
-
comment, err := models.IssueCommentFromRecord(did, rkey, record)
897
if err != nil {
898
return fmt.Errorf("failed to parse comment from record: %w", err)
899
}
900
901
-
if err := i.Validator.ValidateIssueComment(comment); err != nil {
902
return fmt.Errorf("failed to validate comment: %w", err)
903
}
904
···
908
}
909
defer tx.Rollback()
910
911
-
_, err = db.AddIssueComment(tx, *comment)
912
if err != nil {
913
-
return fmt.Errorf("failed to create issue comment: %w", err)
914
}
915
916
return tx.Commit()
917
918
case jmodels.CommitOperationDelete:
919
-
if err := db.DeleteIssueComments(
920
ddb,
921
orm.FilterEq("did", did),
922
orm.FilterEq("rkey", rkey),
923
); err != nil {
924
return fmt.Errorf("failed to delete issue comment record: %w", err)
···
885
}
886
887
switch e.Commit.Operation {
888
+
case jmodels.CommitOperationUpdate:
889
raw := json.RawMessage(e.Commit.Record)
890
record := tangled.RepoIssueComment{}
891
err = json.Unmarshal(raw, &record)
···
893
return fmt.Errorf("invalid record: %w", err)
894
}
895
896
+
// convert 'sh.tangled.repo.issue.comment' to 'sh.tangled.comment'
897
+
comment, err := models.CommentFromRecord(syntax.DID(did), syntax.RecordKey(rkey), tangled.Comment{
898
+
Body: record.Body,
899
+
CreatedAt: record.CreatedAt,
900
+
Mentions: record.Mentions,
901
+
References: record.References,
902
+
ReplyTo: record.ReplyTo,
903
+
Subject: record.Issue,
904
+
})
905
if err != nil {
906
return fmt.Errorf("failed to parse comment from record: %w", err)
907
}
908
909
+
if err := comment.Validate(); err != nil {
910
return fmt.Errorf("failed to validate comment: %w", err)
911
}
912
···
916
}
917
defer tx.Rollback()
918
919
+
err = db.PutComment(tx, comment)
920
if err != nil {
921
+
return fmt.Errorf("failed to create comment: %w", err)
922
}
923
924
return tx.Commit()
925
926
case jmodels.CommitOperationDelete:
927
+
if err := db.DeleteComments(
928
ddb,
929
orm.FilterEq("did", did),
930
+
orm.FilterEq("collection", e.Commit.Collection),
931
orm.FilterEq("rkey", rkey),
932
); err != nil {
933
return fmt.Errorf("failed to delete issue comment record: %w", err)
+38
-36
appview/issues/issues.go
+38
-36
appview/issues/issues.go
···
403
404
body := r.FormValue("body")
405
if body == "" {
406
-
rp.pages.Notice(w, "issue", "Body is required")
407
return
408
}
409
410
-
replyToUri := r.FormValue("reply-to")
411
-
var replyTo *string
412
-
if replyToUri != "" {
413
-
replyTo = &replyToUri
414
}
415
416
mentions, references := rp.mentionsResolver.Resolve(r.Context(), body)
417
418
-
comment := models.IssueComment{
419
-
Did: user.Active.Did,
420
Rkey: tid.TID(),
421
-
IssueAt: issue.AtUri().String(),
422
ReplyTo: replyTo,
423
Body: body,
424
Created: time.Now(),
425
Mentions: mentions,
426
References: references,
427
}
428
-
if err = rp.validator.ValidateIssueComment(&comment); err != nil {
429
l.Error("failed to validate comment", "err", err)
430
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
431
return
432
}
433
-
record := comment.AsRecord()
434
435
client, err := rp.oauth.AuthorizedClient(r)
436
if err != nil {
···
441
442
// create a record first
443
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
444
-
Collection: tangled.RepoIssueCommentNSID,
445
-
Repo: comment.Did,
446
Rkey: comment.Rkey,
447
Record: &lexutil.LexiconTypeDecoder{
448
-
Val: &record,
449
},
450
})
451
if err != nil {
···
468
}
469
defer tx.Rollback()
470
471
-
commentId, err := db.AddIssueComment(tx, comment)
472
if err != nil {
473
l.Error("failed to create comment", "err", err)
474
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
···
484
// reset atUri to make rollback a no-op
485
atUri = ""
486
487
-
// notify about the new comment
488
-
comment.Id = commentId
489
-
490
rp.notifier.NewIssueComment(r.Context(), &comment, mentions)
491
492
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
493
-
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", ownerSlashRepo, issue.IssueId, commentId))
494
}
495
496
func (rp *Issues) IssueComment(w http.ResponseWriter, r *http.Request) {
···
505
}
506
507
commentId := chi.URLParam(r, "commentId")
508
-
comments, err := db.GetIssueComments(
509
rp.db,
510
orm.FilterEq("id", commentId),
511
)
···
541
}
542
543
commentId := chi.URLParam(r, "commentId")
544
-
comments, err := db.GetIssueComments(
545
rp.db,
546
orm.FilterEq("id", commentId),
547
)
···
557
}
558
comment := comments[0]
559
560
-
if comment.Did != user.Active.Did {
561
l.Error("unauthorized comment edit", "expectedDid", comment.Did, "gotDid", user.Active.Did)
562
http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
563
return
···
587
newComment.Edited = &now
588
newComment.Mentions, newComment.References = rp.mentionsResolver.Resolve(r.Context(), newBody)
589
590
-
record := newComment.AsRecord()
591
-
592
tx, err := rp.db.Begin()
593
if err != nil {
594
l.Error("failed to start transaction", "err", err)
···
597
}
598
defer tx.Rollback()
599
600
-
_, err = db.AddIssueComment(tx, newComment)
601
if err != nil {
602
l.Error("failed to perferom update-description query", "err", err)
603
rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.")
···
607
608
// rkey is optional, it was introduced later
609
if newComment.Rkey != "" {
610
// update the record on pds
611
-
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueCommentNSID, user.Active.Did, comment.Rkey)
612
if err != nil {
613
l.Error("failed to get record", "err", err, "did", newComment.Did, "rkey", newComment.Rkey)
614
-
rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.")
615
return
616
}
617
618
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
619
-
Collection: tangled.RepoIssueCommentNSID,
620
-
Repo: user.Active.Did,
621
Rkey: newComment.Rkey,
622
SwapRecord: ex.Cid,
623
Record: &lexutil.LexiconTypeDecoder{
624
-
Val: &record,
625
},
626
})
627
if err != nil {
···
651
}
652
653
commentId := chi.URLParam(r, "commentId")
654
-
comments, err := db.GetIssueComments(
655
rp.db,
656
orm.FilterEq("id", commentId),
657
)
···
687
}
688
689
commentId := chi.URLParam(r, "commentId")
690
-
comments, err := db.GetIssueComments(
691
rp.db,
692
orm.FilterEq("id", commentId),
693
)
···
723
}
724
725
commentId := chi.URLParam(r, "commentId")
726
-
comments, err := db.GetIssueComments(
727
rp.db,
728
orm.FilterEq("id", commentId),
729
)
···
739
}
740
comment := comments[0]
741
742
-
if comment.Did != user.Active.Did {
743
l.Error("unauthorized action", "expectedDid", comment.Did, "gotDid", user.Active.Did)
744
http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
745
return
···
752
753
// optimistic deletion
754
deleted := time.Now()
755
-
err = db.DeleteIssueComments(rp.db, orm.FilterEq("id", comment.Id))
756
if err != nil {
757
l.Error("failed to delete comment", "err", err)
758
rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment")
···
768
return
769
}
770
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
771
-
Collection: tangled.RepoIssueCommentNSID,
772
-
Repo: user.Active.Did,
773
Rkey: comment.Rkey,
774
})
775
if err != nil {
···
403
404
body := r.FormValue("body")
405
if body == "" {
406
+
rp.pages.Notice(w, "issue-comment", "Body is required")
407
return
408
}
409
410
+
var replyTo *syntax.ATURI
411
+
replyToRaw := r.FormValue("reply-to")
412
+
if replyToRaw != "" {
413
+
aturi, err := syntax.ParseATURI(replyToRaw)
414
+
if err != nil {
415
+
rp.pages.Notice(w, "issue-comment", "reply-to should be valid AT-URI")
416
+
return
417
+
}
418
+
replyTo = &aturi
419
}
420
421
mentions, references := rp.mentionsResolver.Resolve(r.Context(), body)
422
423
+
comment := models.Comment{
424
+
Did: syntax.DID(user.Active.Did),
425
+
Collection: tangled.CommentNSID,
426
Rkey: tid.TID(),
427
+
Subject: issue.AtUri(),
428
ReplyTo: replyTo,
429
Body: body,
430
Created: time.Now(),
431
Mentions: mentions,
432
References: references,
433
}
434
+
if err = comment.Validate(); err != nil {
435
l.Error("failed to validate comment", "err", err)
436
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
437
return
438
}
439
440
client, err := rp.oauth.AuthorizedClient(r)
441
if err != nil {
···
446
447
// create a record first
448
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
449
+
Collection: comment.Collection.String(),
450
+
Repo: comment.Did.String(),
451
Rkey: comment.Rkey,
452
Record: &lexutil.LexiconTypeDecoder{
453
+
Val: comment.AsRecord(),
454
},
455
})
456
if err != nil {
···
473
}
474
defer tx.Rollback()
475
476
+
err = db.PutComment(tx, &comment)
477
if err != nil {
478
l.Error("failed to create comment", "err", err)
479
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
···
489
// reset atUri to make rollback a no-op
490
atUri = ""
491
492
rp.notifier.NewIssueComment(r.Context(), &comment, mentions)
493
494
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
495
+
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", ownerSlashRepo, issue.IssueId, comment.Id))
496
}
497
498
func (rp *Issues) IssueComment(w http.ResponseWriter, r *http.Request) {
···
507
}
508
509
commentId := chi.URLParam(r, "commentId")
510
+
comments, err := db.GetComments(
511
rp.db,
512
orm.FilterEq("id", commentId),
513
)
···
543
}
544
545
commentId := chi.URLParam(r, "commentId")
546
+
comments, err := db.GetComments(
547
rp.db,
548
orm.FilterEq("id", commentId),
549
)
···
559
}
560
comment := comments[0]
561
562
+
if comment.Did.String() != user.Active.Did {
563
l.Error("unauthorized comment edit", "expectedDid", comment.Did, "gotDid", user.Active.Did)
564
http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
565
return
···
589
newComment.Edited = &now
590
newComment.Mentions, newComment.References = rp.mentionsResolver.Resolve(r.Context(), newBody)
591
592
tx, err := rp.db.Begin()
593
if err != nil {
594
l.Error("failed to start transaction", "err", err)
···
597
}
598
defer tx.Rollback()
599
600
+
err = db.PutComment(tx, &newComment)
601
if err != nil {
602
l.Error("failed to perferom update-description query", "err", err)
603
rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.")
···
607
608
// rkey is optional, it was introduced later
609
if newComment.Rkey != "" {
610
+
// TODO: update correct comment
611
+
612
// update the record on pds
613
+
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", newComment.Collection.String(), newComment.Did.String(), newComment.Rkey)
614
if err != nil {
615
l.Error("failed to get record", "err", err, "did", newComment.Did, "rkey", newComment.Rkey)
616
+
rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update comment, no record found on PDS.")
617
return
618
}
619
620
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
621
+
Collection: newComment.Collection.String(),
622
+
Repo: newComment.Did.String(),
623
Rkey: newComment.Rkey,
624
SwapRecord: ex.Cid,
625
Record: &lexutil.LexiconTypeDecoder{
626
+
Val: newComment.AsRecord(),
627
},
628
})
629
if err != nil {
···
653
}
654
655
commentId := chi.URLParam(r, "commentId")
656
+
comments, err := db.GetComments(
657
rp.db,
658
orm.FilterEq("id", commentId),
659
)
···
689
}
690
691
commentId := chi.URLParam(r, "commentId")
692
+
comments, err := db.GetComments(
693
rp.db,
694
orm.FilterEq("id", commentId),
695
)
···
725
}
726
727
commentId := chi.URLParam(r, "commentId")
728
+
comments, err := db.GetComments(
729
rp.db,
730
orm.FilterEq("id", commentId),
731
)
···
741
}
742
comment := comments[0]
743
744
+
if comment.Did.String() != user.Active.Did {
745
l.Error("unauthorized action", "expectedDid", comment.Did, "gotDid", user.Active.Did)
746
http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
747
return
···
754
755
// optimistic deletion
756
deleted := time.Now()
757
+
err = db.DeleteComments(rp.db, orm.FilterEq("id", comment.Id))
758
if err != nil {
759
l.Error("failed to delete comment", "err", err)
760
rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment")
···
770
return
771
}
772
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
773
+
Collection: comment.Collection.String(),
774
+
Repo: comment.Did.String(),
775
Rkey: comment.Rkey,
776
})
777
if err != nil {
+8
-89
appview/models/issue.go
+8
-89
appview/models/issue.go
···
26
27
// optionally, populate this when querying for reverse mappings
28
// like comment counts, parent repo etc.
29
-
Comments []IssueComment
30
Labels LabelState
31
Repo *Repo
32
}
···
62
}
63
64
type CommentListItem struct {
65
-
Self *IssueComment
66
-
Replies []*IssueComment
67
}
68
69
func (it *CommentListItem) Participants() []syntax.DID {
···
88
89
func (i *Issue) CommentList() []CommentListItem {
90
// Create a map to quickly find comments by their aturi
91
-
toplevel := make(map[string]*CommentListItem)
92
-
var replies []*IssueComment
93
94
// collect top level comments into the map
95
for _, comment := range i.Comments {
96
if comment.IsTopLevel() {
97
-
toplevel[comment.AtUri().String()] = &CommentListItem{
98
Self: &comment,
99
}
100
} else {
···
115
}
116
117
// sort everything
118
-
sortFunc := func(a, b *IssueComment) bool {
119
return a.Created.Before(b.Created)
120
}
121
sort.Slice(listing, func(i, j int) bool {
···
144
addParticipant(i.Did)
145
146
for _, c := range i.Comments {
147
-
addParticipant(c.Did)
148
}
149
150
return participants
···
171
Open: true, // new issues are open by default
172
}
173
}
174
-
175
-
type IssueComment struct {
176
-
Id int64
177
-
Did string
178
-
Rkey string
179
-
IssueAt string
180
-
ReplyTo *string
181
-
Body string
182
-
Created time.Time
183
-
Edited *time.Time
184
-
Deleted *time.Time
185
-
Mentions []syntax.DID
186
-
References []syntax.ATURI
187
-
}
188
-
189
-
func (i *IssueComment) AtUri() syntax.ATURI {
190
-
return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", i.Did, tangled.RepoIssueCommentNSID, i.Rkey))
191
-
}
192
-
193
-
func (i *IssueComment) AsRecord() tangled.RepoIssueComment {
194
-
mentions := make([]string, len(i.Mentions))
195
-
for i, did := range i.Mentions {
196
-
mentions[i] = string(did)
197
-
}
198
-
references := make([]string, len(i.References))
199
-
for i, uri := range i.References {
200
-
references[i] = string(uri)
201
-
}
202
-
return tangled.RepoIssueComment{
203
-
Body: i.Body,
204
-
Issue: i.IssueAt,
205
-
CreatedAt: i.Created.Format(time.RFC3339),
206
-
ReplyTo: i.ReplyTo,
207
-
Mentions: mentions,
208
-
References: references,
209
-
}
210
-
}
211
-
212
-
func (i *IssueComment) IsTopLevel() bool {
213
-
return i.ReplyTo == nil
214
-
}
215
-
216
-
func (i *IssueComment) IsReply() bool {
217
-
return i.ReplyTo != nil
218
-
}
219
-
220
-
func IssueCommentFromRecord(did, rkey string, record tangled.RepoIssueComment) (*IssueComment, error) {
221
-
created, err := time.Parse(time.RFC3339, record.CreatedAt)
222
-
if err != nil {
223
-
created = time.Now()
224
-
}
225
-
226
-
ownerDid := did
227
-
228
-
if _, err = syntax.ParseATURI(record.Issue); err != nil {
229
-
return nil, err
230
-
}
231
-
232
-
i := record
233
-
mentions := make([]syntax.DID, len(record.Mentions))
234
-
for i, did := range record.Mentions {
235
-
mentions[i] = syntax.DID(did)
236
-
}
237
-
references := make([]syntax.ATURI, len(record.References))
238
-
for i, uri := range i.References {
239
-
references[i] = syntax.ATURI(uri)
240
-
}
241
-
242
-
comment := IssueComment{
243
-
Did: ownerDid,
244
-
Rkey: rkey,
245
-
Body: record.Body,
246
-
IssueAt: record.Issue,
247
-
ReplyTo: record.ReplyTo,
248
-
Created: created,
249
-
Mentions: mentions,
250
-
References: references,
251
-
}
252
-
253
-
return &comment, nil
254
-
}
···
26
27
// optionally, populate this when querying for reverse mappings
28
// like comment counts, parent repo etc.
29
+
Comments []Comment
30
Labels LabelState
31
Repo *Repo
32
}
···
62
}
63
64
type CommentListItem struct {
65
+
Self *Comment
66
+
Replies []*Comment
67
}
68
69
func (it *CommentListItem) Participants() []syntax.DID {
···
88
89
func (i *Issue) CommentList() []CommentListItem {
90
// Create a map to quickly find comments by their aturi
91
+
toplevel := make(map[syntax.ATURI]*CommentListItem)
92
+
var replies []*Comment
93
94
// collect top level comments into the map
95
for _, comment := range i.Comments {
96
if comment.IsTopLevel() {
97
+
toplevel[comment.AtUri()] = &CommentListItem{
98
Self: &comment,
99
}
100
} else {
···
115
}
116
117
// sort everything
118
+
sortFunc := func(a, b *Comment) bool {
119
return a.Created.Before(b.Created)
120
}
121
sort.Slice(listing, func(i, j int) bool {
···
144
addParticipant(i.Did)
145
146
for _, c := range i.Comments {
147
+
addParticipant(c.Did.String())
148
}
149
150
return participants
···
171
Open: true, // new issues are open by default
172
}
173
}
+4
-4
appview/notify/db/db.go
+4
-4
appview/notify/db/db.go
···
122
)
123
}
124
125
-
func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) {
126
-
issues, err := db.GetIssues(n.db, orm.FilterEq("at_uri", comment.IssueAt))
127
if err != nil {
128
log.Printf("NewIssueComment: failed to get issues: %v", err)
129
return
130
}
131
if len(issues) == 0 {
132
-
log.Printf("NewIssueComment: no issue found for %s", comment.IssueAt)
133
return
134
}
135
issue := issues[0]
···
147
148
// find the parent thread, and add all DIDs from here to the recipient list
149
for _, t := range issue.CommentList() {
150
-
if t.Self.AtUri().String() == parentAtUri {
151
for _, p := range t.Participants() {
152
recipients.Insert(p)
153
}
···
122
)
123
}
124
125
+
func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID) {
126
+
issues, err := db.GetIssues(n.db, orm.FilterEq("at_uri", comment.Subject))
127
if err != nil {
128
log.Printf("NewIssueComment: failed to get issues: %v", err)
129
return
130
}
131
if len(issues) == 0 {
132
+
log.Printf("NewIssueComment: no issue found for %s", comment.Subject)
133
return
134
}
135
issue := issues[0]
···
147
148
// find the parent thread, and add all DIDs from here to the recipient list
149
for _, t := range issue.CommentList() {
150
+
if t.Self.AtUri() == parentAtUri {
151
for _, p := range t.Participants() {
152
recipients.Insert(p)
153
}
+1
-1
appview/notify/merged_notifier.go
+1
-1
appview/notify/merged_notifier.go
+2
-2
appview/notify/notifier.go
+2
-2
appview/notify/notifier.go
···
14
DeleteStar(ctx context.Context, star *models.Star)
15
16
NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID)
17
-
NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID)
18
NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue)
19
DeleteIssue(ctx context.Context, issue *models.Issue)
20
···
43
func (m *BaseNotifier) DeleteStar(ctx context.Context, star *models.Star) {}
44
45
func (m *BaseNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) {}
46
-
func (m *BaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) {
47
}
48
func (m *BaseNotifier) NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) {}
49
func (m *BaseNotifier) DeleteIssue(ctx context.Context, issue *models.Issue) {}
···
14
DeleteStar(ctx context.Context, star *models.Star)
15
16
NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID)
17
+
NewIssueComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID)
18
NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue)
19
DeleteIssue(ctx context.Context, issue *models.Issue)
20
···
43
func (m *BaseNotifier) DeleteStar(ctx context.Context, star *models.Star) {}
44
45
func (m *BaseNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) {}
46
+
func (m *BaseNotifier) NewIssueComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID) {
47
}
48
func (m *BaseNotifier) NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) {}
49
func (m *BaseNotifier) DeleteIssue(ctx context.Context, issue *models.Issue) {}
+3
-3
appview/notify/posthog/notifier.go
+3
-3
appview/notify/posthog/notifier.go
···
179
}
180
}
181
182
-
func (n *posthogNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) {
183
err := n.client.Enqueue(posthog.Capture{
184
-
DistinctId: comment.Did,
185
Event: "new_issue_comment",
186
Properties: posthog.Properties{
187
-
"issue_at": comment.IssueAt,
188
"mentions": mentions,
189
},
190
})
···
179
}
180
}
181
182
+
func (n *posthogNotifier) NewIssueComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID) {
183
err := n.client.Enqueue(posthog.Capture{
184
+
DistinctId: comment.Did.String(),
185
Event: "new_issue_comment",
186
Properties: posthog.Properties{
187
+
"issue_at": comment.Subject,
188
"mentions": mentions,
189
},
190
})
+4
-4
appview/pages/pages.go
+4
-4
appview/pages/pages.go
···
1004
LoggedInUser *oauth.MultiAccountUser
1005
RepoInfo repoinfo.RepoInfo
1006
Issue *models.Issue
1007
-
Comment *models.IssueComment
1008
}
1009
1010
func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error {
···
1015
LoggedInUser *oauth.MultiAccountUser
1016
RepoInfo repoinfo.RepoInfo
1017
Issue *models.Issue
1018
-
Comment *models.IssueComment
1019
}
1020
1021
func (p *Pages) ReplyIssueCommentPlaceholderFragment(w io.Writer, params ReplyIssueCommentPlaceholderParams) error {
···
1026
LoggedInUser *oauth.MultiAccountUser
1027
RepoInfo repoinfo.RepoInfo
1028
Issue *models.Issue
1029
-
Comment *models.IssueComment
1030
}
1031
1032
func (p *Pages) ReplyIssueCommentFragment(w io.Writer, params ReplyIssueCommentParams) error {
···
1037
LoggedInUser *oauth.MultiAccountUser
1038
RepoInfo repoinfo.RepoInfo
1039
Issue *models.Issue
1040
-
Comment *models.IssueComment
1041
}
1042
1043
func (p *Pages) IssueCommentBodyFragment(w io.Writer, params IssueCommentBodyParams) error {
···
1004
LoggedInUser *oauth.MultiAccountUser
1005
RepoInfo repoinfo.RepoInfo
1006
Issue *models.Issue
1007
+
Comment *models.Comment
1008
}
1009
1010
func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error {
···
1015
LoggedInUser *oauth.MultiAccountUser
1016
RepoInfo repoinfo.RepoInfo
1017
Issue *models.Issue
1018
+
Comment *models.Comment
1019
}
1020
1021
func (p *Pages) ReplyIssueCommentPlaceholderFragment(w io.Writer, params ReplyIssueCommentPlaceholderParams) error {
···
1026
LoggedInUser *oauth.MultiAccountUser
1027
RepoInfo repoinfo.RepoInfo
1028
Issue *models.Issue
1029
+
Comment *models.Comment
1030
}
1031
1032
func (p *Pages) ReplyIssueCommentFragment(w io.Writer, params ReplyIssueCommentParams) error {
···
1037
LoggedInUser *oauth.MultiAccountUser
1038
RepoInfo repoinfo.RepoInfo
1039
Issue *models.Issue
1040
+
Comment *models.Comment
1041
}
1042
1043
func (p *Pages) IssueCommentBodyFragment(w io.Writer, params IssueCommentBodyParams) error {
+2
-2
appview/pages/templates/repo/issues/fragments/commentList.html
+2
-2
appview/pages/templates/repo/issues/fragments/commentList.html
···
42
<div class="rounded px-6 py-4 bg-white dark:bg-gray-800 flex gap-2 ">
43
<div class="flex-shrink-0">
44
<img
45
-
src="{{ tinyAvatar .Comment.Did }}"
46
alt=""
47
class="rounded-full size-8 mr-1 border-2 border-gray-100 dark:border-gray-900"
48
/>
···
58
<div class="py-4 pr-4 w-full mx-auto overflow-hidden flex gap-2 ">
59
<div class="flex-shrink-0">
60
<img
61
-
src="{{ tinyAvatar .Comment.Did }}"
62
alt=""
63
class="rounded-full size-8 mr-1 border-2 border-gray-100 dark:border-gray-900"
64
/>
···
42
<div class="rounded px-6 py-4 bg-white dark:bg-gray-800 flex gap-2 ">
43
<div class="flex-shrink-0">
44
<img
45
+
src="{{ tinyAvatar .Comment.Did.String }}"
46
alt=""
47
class="rounded-full size-8 mr-1 border-2 border-gray-100 dark:border-gray-900"
48
/>
···
58
<div class="py-4 pr-4 w-full mx-auto overflow-hidden flex gap-2 ">
59
<div class="flex-shrink-0">
60
<img
61
+
src="{{ tinyAvatar .Comment.Did.String }}"
62
alt=""
63
class="rounded-full size-8 mr-1 border-2 border-gray-100 dark:border-gray-900"
64
/>
+2
-2
appview/pages/templates/repo/issues/fragments/issueCommentHeader.html
+2
-2
appview/pages/templates/repo/issues/fragments/issueCommentHeader.html
···
1
{{ define "repo/issues/fragments/issueCommentHeader" }}
2
<div class="flex flex-wrap items-center gap-2 text-sm text-gray-500 dark:text-gray-400 ">
3
-
{{ resolve .Comment.Did }}
4
{{ template "hats" $ }}
5
<span class="before:content-['路']"></span>
6
{{ template "timestamp" . }}
7
-
{{ $isCommentOwner := and .LoggedInUser (eq .LoggedInUser.Did .Comment.Did) }}
8
{{ if and $isCommentOwner (not .Comment.Deleted) }}
9
{{ template "editIssueComment" . }}
10
{{ template "deleteIssueComment" . }}
···
1
{{ define "repo/issues/fragments/issueCommentHeader" }}
2
<div class="flex flex-wrap items-center gap-2 text-sm text-gray-500 dark:text-gray-400 ">
3
+
{{ resolve .Comment.Did.String }}
4
{{ template "hats" $ }}
5
<span class="before:content-['路']"></span>
6
{{ template "timestamp" . }}
7
+
{{ $isCommentOwner := and .LoggedInUser (eq .LoggedInUser.Did .Comment.Did.String) }}
8
{{ if and $isCommentOwner (not .Comment.Deleted) }}
9
{{ template "editIssueComment" . }}
10
{{ template "deleteIssueComment" . }}
-27
appview/validator/issue.go
-27
appview/validator/issue.go
···
4
"fmt"
5
"strings"
6
7
-
"tangled.org/core/appview/db"
8
"tangled.org/core/appview/models"
9
-
"tangled.org/core/orm"
10
)
11
12
-
func (v *Validator) ValidateIssueComment(comment *models.IssueComment) error {
13
-
// if comments have parents, only ingest ones that are 1 level deep
14
-
if comment.ReplyTo != nil {
15
-
parents, err := db.GetIssueComments(v.db, orm.FilterEq("at_uri", *comment.ReplyTo))
16
-
if err != nil {
17
-
return fmt.Errorf("failed to fetch parent comment: %w", err)
18
-
}
19
-
if len(parents) != 1 {
20
-
return fmt.Errorf("incorrect number of parent comments returned: %d", len(parents))
21
-
}
22
-
23
-
// depth check
24
-
parent := parents[0]
25
-
if parent.ReplyTo != nil {
26
-
return fmt.Errorf("incorrect depth, this comment is replying at depth >1")
27
-
}
28
-
}
29
-
30
-
if sb := strings.TrimSpace(v.sanitizer.SanitizeDefault(comment.Body)); sb == "" {
31
-
return fmt.Errorf("body is empty after HTML sanitization")
32
-
}
33
-
34
-
return nil
35
-
}
36
-
37
func (v *Validator) ValidateIssue(issue *models.Issue) error {
38
if issue.Title == "" {
39
return fmt.Errorf("issue title is empty")
History
8 rounds
1 comment
boltless.me
submitted
#7
1 commit
expand
collapse
appview: replace
IssueComment to Comment
Signed-off-by: Seongmin Lee <git@boltless.me>
2/3 failed, 1/3 success
expand
collapse
merge conflicts detected
expand
collapse
expand
collapse
- appview/notify/db/db.go:260
- appview/notify/merged_notifier.go:81
- appview/notify/notifier.go:22
- appview/pulls/opengraph.go:277
expand 0 comments
boltless.me
submitted
#6
1 commit
expand
collapse
appview: replace
IssueComment to Comment
Signed-off-by: Seongmin Lee <git@boltless.me>
2/3 failed, 1/3 success
expand
collapse
expand 0 comments
boltless.me
submitted
#5
1 commit
expand
collapse
appview: replace
IssueComment to Comment
Signed-off-by: Seongmin Lee <git@boltless.me>
3/3 success
expand
collapse
expand 0 comments
boltless.me
submitted
#4
1 commit
expand
collapse
appview: replace
IssueComment to Comment
Signed-off-by: Seongmin Lee <git@boltless.me>
3/3 success
expand
collapse
expand 0 comments
boltless.me
submitted
#3
1 commit
expand
collapse
appview: replace
IssueComment to Comment
Signed-off-by: Seongmin Lee <git@boltless.me>
1/3 failed, 2/3 success
expand
collapse
expand 0 comments
boltless.me
submitted
#2
1 commit
expand
collapse
appview: replace
IssueComment to Comment
Signed-off-by: Seongmin Lee <git@boltless.me>
1/3 failed, 2/3 success
expand
collapse
expand 0 comments
boltless.me
submitted
#1
1 commit
expand
collapse
appview: replace
IssueComment to Comment
Signed-off-by: Seongmin Lee <git@boltless.me>
3/3 success
expand
collapse
expand 1 comment
boltless.me
submitted
#0
1 commit
expand
collapse
appview: replace
IssueComment to Comment
Signed-off-by: Seongmin Lee <git@boltless.me>
imo: we should still continue to ingest
sh.tangled.issue.comment(and pull comments), but just try to convert to the regular lexicon. this would only be used for backfill purposes, newer appviews should ideally not create the old NSID anymore.the way i see it, the new lexicon is just an alias for the old ones, so the ingesters should be backwards compatible.
i will do a deeper dive into the code itself shortly.