Signed-off-by: Seongmin Lee git@boltless.me
+93
-380
Diff
round #0
+6
-185
appview/db/issues.go
+6
-185
appview/db/issues.go
···
99
}
100
101
func GetIssuesPaginated(e Execer, page pagination.Page, filters ...filter) ([]models.Issue, error) {
102
-
issueMap := make(map[string]*models.Issue) // at-uri -> issue
103
104
var conditions []string
105
var args []any
···
195
}
196
}
197
198
-
atUri := issue.AtUri().String()
199
-
issueMap[atUri] = &issue
200
}
201
202
// collect reverse repos
···
228
// collect comments
229
issueAts := slices.Collect(maps.Keys(issueMap))
230
231
-
comments, err := GetIssueComments(e, FilterIn("issue_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].IssueAt
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.String()]; 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.String()]; ok {
260
issue.References = references
261
}
262
}
···
350
return ids, nil
351
}
352
353
-
func AddIssueComment(tx *sql.Tx, c models.IssueComment) (int64, error) {
354
-
result, err := tx.Exec(
355
-
`insert into issue_comments (
356
-
did,
357
-
rkey,
358
-
issue_at,
359
-
body,
360
-
reply_to,
361
-
created,
362
-
edited
363
-
)
364
-
values (?, ?, ?, ?, ?, ?, null)
365
-
on conflict(did, rkey) do update set
366
-
issue_at = excluded.issue_at,
367
-
body = excluded.body,
368
-
edited = case
369
-
when
370
-
issue_comments.issue_at != excluded.issue_at
371
-
or issue_comments.body != excluded.body
372
-
or issue_comments.reply_to != excluded.reply_to
373
-
then ?
374
-
else issue_comments.edited
375
-
end`,
376
-
c.Did,
377
-
c.Rkey,
378
-
c.IssueAt,
379
-
c.Body,
380
-
c.ReplyTo,
381
-
c.Created.Format(time.RFC3339),
382
-
time.Now().Format(time.RFC3339),
383
-
)
384
-
if err != nil {
385
-
return 0, err
386
-
}
387
-
388
-
id, err := result.LastInsertId()
389
-
if err != nil {
390
-
return 0, err
391
-
}
392
-
393
-
if err := putReferences(tx, c.AtUri(), c.References); err != nil {
394
-
return 0, fmt.Errorf("put reference_links: %w", err)
395
-
}
396
-
397
-
return id, nil
398
-
}
399
-
400
-
func DeleteIssueComments(e Execer, filters ...filter) error {
401
-
var conditions []string
402
-
var args []any
403
-
for _, filter := range filters {
404
-
conditions = append(conditions, filter.Condition())
405
-
args = append(args, filter.Arg()...)
406
-
}
407
-
408
-
whereClause := ""
409
-
if conditions != nil {
410
-
whereClause = " where " + strings.Join(conditions, " and ")
411
-
}
412
-
413
-
query := fmt.Sprintf(`update issue_comments set body = "", deleted = strftime('%%Y-%%m-%%dT%%H:%%M:%%SZ', 'now') %s`, whereClause)
414
-
415
-
_, err := e.Exec(query, args...)
416
-
return err
417
-
}
418
-
419
-
func GetIssueComments(e Execer, filters ...filter) ([]models.IssueComment, error) {
420
-
commentMap := make(map[string]*models.IssueComment)
421
-
422
-
var conditions []string
423
-
var args []any
424
-
for _, filter := range filters {
425
-
conditions = append(conditions, filter.Condition())
426
-
args = append(args, filter.Arg()...)
427
-
}
428
-
429
-
whereClause := ""
430
-
if conditions != nil {
431
-
whereClause = " where " + strings.Join(conditions, " and ")
432
-
}
433
-
434
-
query := fmt.Sprintf(`
435
-
select
436
-
id,
437
-
did,
438
-
rkey,
439
-
issue_at,
440
-
reply_to,
441
-
body,
442
-
created,
443
-
edited,
444
-
deleted
445
-
from
446
-
issue_comments
447
-
%s
448
-
`, whereClause)
449
-
450
-
rows, err := e.Query(query, args...)
451
-
if err != nil {
452
-
return nil, err
453
-
}
454
-
455
-
for rows.Next() {
456
-
var comment models.IssueComment
457
-
var created string
458
-
var rkey, edited, deleted, replyTo sql.Null[string]
459
-
err := rows.Scan(
460
-
&comment.Id,
461
-
&comment.Did,
462
-
&rkey,
463
-
&comment.IssueAt,
464
-
&replyTo,
465
-
&comment.Body,
466
-
&created,
467
-
&edited,
468
-
&deleted,
469
-
)
470
-
if err != nil {
471
-
return nil, err
472
-
}
473
-
474
-
// this is a remnant from old times, newer comments always have rkey
475
-
if rkey.Valid {
476
-
comment.Rkey = rkey.V
477
-
}
478
-
479
-
if t, err := time.Parse(time.RFC3339, created); err == nil {
480
-
comment.Created = t
481
-
}
482
-
483
-
if edited.Valid {
484
-
if t, err := time.Parse(time.RFC3339, edited.V); err == nil {
485
-
comment.Edited = &t
486
-
}
487
-
}
488
-
489
-
if deleted.Valid {
490
-
if t, err := time.Parse(time.RFC3339, deleted.V); err == nil {
491
-
comment.Deleted = &t
492
-
}
493
-
}
494
-
495
-
if replyTo.Valid {
496
-
comment.ReplyTo = &replyTo.V
497
-
}
498
-
499
-
atUri := comment.AtUri().String()
500
-
commentMap[atUri] = &comment
501
-
}
502
-
503
-
if err = rows.Err(); err != nil {
504
-
return nil, err
505
-
}
506
-
507
-
// collect references for each comments
508
-
commentAts := slices.Collect(maps.Keys(commentMap))
509
-
allReferencs, err := GetReferencesAll(e, FilterIn("from_at", commentAts))
510
-
if err != nil {
511
-
return nil, fmt.Errorf("failed to query reference_links: %w", err)
512
-
}
513
-
for commentAt, references := range allReferencs {
514
-
if comment, ok := commentMap[commentAt.String()]; ok {
515
-
comment.References = references
516
-
}
517
-
}
518
-
519
-
var comments []models.IssueComment
520
-
for _, c := range commentMap {
521
-
comments = append(comments, *c)
522
-
}
523
-
524
-
sort.Slice(comments, func(i, j int) bool {
525
-
return comments[i].Created.After(comments[j].Created)
526
-
})
527
-
528
-
return comments, nil
529
-
}
530
-
531
func DeleteIssues(tx *sql.Tx, did, rkey string) error {
532
_, err := tx.Exec(
533
`delete from issues
···
99
}
100
101
func GetIssuesPaginated(e Execer, page pagination.Page, filters ...filter) ([]models.Issue, error) {
102
+
issueMap := make(map[syntax.ATURI]*models.Issue) // at-uri -> issue
103
104
var conditions []string
105
var args []any
···
195
}
196
}
197
198
+
issueMap[issue.AtUri()] = &issue
199
}
200
201
// collect reverse repos
···
227
// collect comments
228
issueAts := slices.Collect(maps.Keys(issueMap))
229
230
+
comments, err := GetComments(e, FilterIn("subject_at", issueAts))
231
if err != nil {
232
return nil, fmt.Errorf("failed to query comments: %w", err)
233
}
234
for i := range comments {
235
+
issueAt := comments[i].Subject
236
if issue, ok := issueMap[issueAt]; ok {
237
issue.Comments = append(issue.Comments, comments[i])
238
}
···
244
return nil, fmt.Errorf("failed to query labels: %w", err)
245
}
246
for issueAt, labels := range allLabels {
247
+
if issue, ok := issueMap[issueAt]; ok {
248
issue.Labels = labels
249
}
250
}
···
255
return nil, fmt.Errorf("failed to query reference_links: %w", err)
256
}
257
for issueAt, references := range allReferencs {
258
+
if issue, ok := issueMap[issueAt]; ok {
259
issue.References = references
260
}
261
}
···
349
return ids, nil
350
}
351
352
func DeleteIssues(tx *sql.Tx, did, rkey string) error {
353
_, err := tx.Exec(
354
`delete from issues
+13
-24
appview/db/reference.go
+13
-24
appview/db/reference.go
···
10
"tangled.org/core/appview/models"
11
)
12
13
-
// ValidateReferenceLinks resolves refLinks to Issue/PR/IssueComment/PullComment ATURIs.
14
// It will ignore missing refLinks.
15
func ValidateReferenceLinks(e Execer, refLinks []models.ReferenceLink) ([]syntax.ATURI, error) {
16
var (
···
52
values %s
53
)
54
select
55
-
i.did, i.rkey,
56
-
c.did, c.rkey
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 issue_comments c
65
on inp.comment_id is not null
66
-
and c.issue_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 issueOwner, issueRkey string
82
-
var commentOwner, commentRkey sql.NullString
83
var uri syntax.ATURI
84
-
if err := rows.Scan(&issueOwner, &issueRkey, &commentOwner, &commentRkey); err != nil {
85
return nil, err
86
}
87
-
if commentOwner.Valid && commentRkey.Valid {
88
-
uri = syntax.ATURI(fmt.Sprintf(
89
-
"at://%s/%s/%s",
90
-
commentOwner.String,
91
-
tangled.RepoIssueCommentNSID,
92
-
commentRkey.String,
93
-
))
94
} else {
95
-
uri = syntax.ATURI(fmt.Sprintf(
96
-
"at://%s/%s/%s",
97
-
issueOwner,
98
-
tangled.RepoIssueNSID,
99
-
issueRkey,
100
-
))
101
}
102
uris = append(uris, uri)
103
}
···
281
return nil, fmt.Errorf("get issue backlinks: %w", err)
282
}
283
backlinks = append(backlinks, ls...)
284
-
ls, err = getIssueCommentBacklinks(e, backlinksMap[tangled.RepoIssueCommentNSID])
285
if err != nil {
286
return nil, fmt.Errorf("get issue_comment backlinks: %w", err)
287
}
···
350
rows, err := e.Query(
351
fmt.Sprintf(
352
`select r.did, r.name, i.issue_id, c.id, i.title, i.open
353
-
from issue_comments c
354
join issues i
355
-
on i.at_uri = c.issue_at
356
join repos r
357
on r.at_uri = i.repo_at
358
where %s`,
···
10
"tangled.org/core/appview/models"
11
)
12
13
+
// ValidateReferenceLinks resolves refLinks to Issue/PR/Comment ATURIs.
14
// It will ignore missing refLinks.
15
func ValidateReferenceLinks(e Execer, refLinks []models.ReferenceLink) ([]syntax.ATURI, error) {
16
var (
···
52
values %s
53
)
54
select
55
+
i.at_uri, c.at_uri
56
from input inp
57
join repos r
58
on r.did = inp.owner_did
···
60
join issues i
61
on i.repo_at = r.at_uri
62
and i.issue_id = inp.issue_id
63
+
left join comments c
64
on inp.comment_id is not null
65
+
and c.subject_at = i.at_uri
66
and c.id = inp.comment_id
67
`,
68
strings.Join(vals, ","),
···
77
78
for rows.Next() {
79
// Scan rows
80
+
var issueUri string
81
+
var commentUri sql.NullString
82
var uri syntax.ATURI
83
+
if err := rows.Scan(&issueUri, &commentUri); err != nil {
84
return nil, err
85
}
86
+
if commentUri.Valid {
87
+
uri = syntax.ATURI(commentUri.String)
88
} else {
89
+
uri = syntax.ATURI(issueUri)
90
}
91
uris = append(uris, uri)
92
}
···
270
return nil, fmt.Errorf("get issue backlinks: %w", err)
271
}
272
backlinks = append(backlinks, ls...)
273
+
ls, err = getIssueCommentBacklinks(e, backlinksMap[tangled.CommentNSID])
274
if err != nil {
275
return nil, fmt.Errorf("get issue_comment backlinks: %w", err)
276
}
···
339
rows, err := e.Query(
340
fmt.Sprintf(
341
`select r.did, r.name, i.issue_id, c.id, i.title, i.open
342
+
from comments c
343
join issues i
344
+
on i.at_uri = c.subject_at
345
join repos r
346
on r.at_uri = i.repo_at
347
where %s`,
+19
-11
appview/ingester.go
+19
-11
appview/ingester.go
···
78
err = i.ingestString(e)
79
case tangled.RepoIssueNSID:
80
err = i.ingestIssue(ctx, e)
81
-
case tangled.RepoIssueCommentNSID:
82
-
err = i.ingestIssueComment(e)
83
case tangled.LabelDefinitionNSID:
84
err = i.ingestLabelDefinition(e)
85
case tangled.LabelOpNSID:
···
867
return nil
868
}
869
870
-
func (i *Ingester) ingestIssueComment(e *jmodels.Event) error {
871
did := e.Did
872
rkey := e.Commit.RKey
873
874
var err error
875
876
-
l := i.Logger.With("handler", "ingestIssueComment", "nsid", e.Commit.Collection, "did", did, "rkey", rkey)
877
l.Info("ingesting record")
878
879
ddb, ok := i.Db.Execer.(*db.DB)
···
884
switch e.Commit.Operation {
885
case jmodels.CommitOperationCreate, jmodels.CommitOperationUpdate:
886
raw := json.RawMessage(e.Commit.Record)
887
-
record := tangled.RepoIssueComment{}
888
err = json.Unmarshal(raw, &record)
889
if err != nil {
890
return fmt.Errorf("invalid record: %w", err)
891
}
892
893
-
comment, err := models.IssueCommentFromRecord(did, rkey, record)
894
if err != nil {
895
return fmt.Errorf("failed to parse comment from record: %w", err)
896
}
897
898
-
if err := i.Validator.ValidateIssueComment(comment); err != nil {
899
return fmt.Errorf("failed to validate comment: %w", err)
900
}
901
···
905
}
906
defer tx.Rollback()
907
908
-
_, err = db.AddIssueComment(tx, *comment)
909
if err != nil {
910
-
return fmt.Errorf("failed to create issue comment: %w", err)
911
}
912
913
return tx.Commit()
914
915
case jmodels.CommitOperationDelete:
916
-
if err := db.DeleteIssueComments(
917
ddb,
918
db.FilterEq("did", did),
919
db.FilterEq("rkey", rkey),
920
); err != nil {
921
-
return fmt.Errorf("failed to delete issue comment record: %w", err)
922
}
923
924
return nil
···
78
err = i.ingestString(e)
79
case tangled.RepoIssueNSID:
80
err = i.ingestIssue(ctx, e)
81
+
case tangled.CommentNSID:
82
+
err = i.ingestComment(e)
83
case tangled.LabelDefinitionNSID:
84
err = i.ingestLabelDefinition(e)
85
case tangled.LabelOpNSID:
···
867
return nil
868
}
869
870
+
func (i *Ingester) ingestComment(e *jmodels.Event) error {
871
did := e.Did
872
rkey := e.Commit.RKey
873
874
var err error
875
876
+
l := i.Logger.With("handler", "ingestComment", "nsid", e.Commit.Collection, "did", did, "rkey", rkey)
877
l.Info("ingesting record")
878
879
ddb, ok := i.Db.Execer.(*db.DB)
···
884
switch e.Commit.Operation {
885
case jmodels.CommitOperationCreate, jmodels.CommitOperationUpdate:
886
raw := json.RawMessage(e.Commit.Record)
887
+
record := tangled.Comment{}
888
err = json.Unmarshal(raw, &record)
889
if err != nil {
890
return fmt.Errorf("invalid record: %w", err)
891
}
892
893
+
comment, err := models.CommentFromRecord(did, rkey, record)
894
if err != nil {
895
return fmt.Errorf("failed to parse comment from record: %w", err)
896
}
897
898
+
// TODO: ingest pull comments
899
+
// we aren't ingesting pull comments yet because pull itself isn't fully atprotated.
900
+
// so we cannot know which round this comment is pointing to
901
+
if comment.Subject.Collection().String() == tangled.RepoPullNSID {
902
+
l.Info("skip ingesting pull comments")
903
+
return nil
904
+
}
905
+
906
+
if err := comment.Validate(); err != nil {
907
return fmt.Errorf("failed to validate comment: %w", err)
908
}
909
···
913
}
914
defer tx.Rollback()
915
916
+
err = db.PutComment(tx, comment)
917
if err != nil {
918
+
return fmt.Errorf("failed to create comment: %w", err)
919
}
920
921
return tx.Commit()
922
923
case jmodels.CommitOperationDelete:
924
+
if err := db.DeleteComments(
925
ddb,
926
db.FilterEq("did", did),
927
db.FilterEq("rkey", rkey),
928
); err != nil {
929
+
return fmt.Errorf("failed to delete comment record: %w", err)
930
}
931
932
return nil
+30
-28
appview/issues/issues.go
+30
-28
appview/issues/issues.go
···
402
403
body := r.FormValue("body")
404
if body == "" {
405
-
rp.pages.Notice(w, "issue", "Body is required")
406
return
407
}
408
409
-
replyToUri := r.FormValue("reply-to")
410
-
var replyTo *string
411
-
if replyToUri != "" {
412
-
replyTo = &replyToUri
413
}
414
415
mentions, references := rp.refResolver.Resolve(r.Context(), body)
416
417
-
comment := models.IssueComment{
418
-
Did: user.Did,
419
Rkey: tid.TID(),
420
-
IssueAt: issue.AtUri().String(),
421
ReplyTo: replyTo,
422
Body: body,
423
Created: time.Now(),
424
Mentions: mentions,
425
References: references,
426
}
427
-
if err = rp.validator.ValidateIssueComment(&comment); err != nil {
428
l.Error("failed to validate comment", "err", err)
429
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
430
return
···
440
441
// create a record first
442
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
443
-
Collection: tangled.RepoIssueCommentNSID,
444
-
Repo: comment.Did,
445
Rkey: comment.Rkey,
446
Record: &lexutil.LexiconTypeDecoder{
447
Val: &record,
···
467
}
468
defer tx.Rollback()
469
470
-
commentId, err := db.AddIssueComment(tx, comment)
471
if err != nil {
472
l.Error("failed to create comment", "err", err)
473
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
···
483
// reset atUri to make rollback a no-op
484
atUri = ""
485
486
-
// notify about the new comment
487
-
comment.Id = commentId
488
-
489
rp.notifier.NewIssueComment(r.Context(), &comment, mentions)
490
491
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
492
-
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", ownerSlashRepo, issue.IssueId, commentId))
493
}
494
495
func (rp *Issues) IssueComment(w http.ResponseWriter, r *http.Request) {
···
504
}
505
506
commentId := chi.URLParam(r, "commentId")
507
-
comments, err := db.GetIssueComments(
508
rp.db,
509
db.FilterEq("id", commentId),
510
)
···
540
}
541
542
commentId := chi.URLParam(r, "commentId")
543
-
comments, err := db.GetIssueComments(
544
rp.db,
545
db.FilterEq("id", commentId),
546
)
···
556
}
557
comment := comments[0]
558
559
-
if comment.Did != user.Did {
560
l.Error("unauthorized comment edit", "expectedDid", comment.Did, "gotDid", user.Did)
561
http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
562
return
···
596
}
597
defer tx.Rollback()
598
599
-
_, err = db.AddIssueComment(tx, newComment)
600
if err != nil {
601
l.Error("failed to perferom update-description query", "err", err)
602
rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.")
···
607
// rkey is optional, it was introduced later
608
if newComment.Rkey != "" {
609
// update the record on pds
610
-
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueCommentNSID, user.Did, comment.Rkey)
611
if err != nil {
612
l.Error("failed to get record", "err", err, "did", newComment.Did, "rkey", newComment.Rkey)
613
rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.")
···
615
}
616
617
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
618
-
Collection: tangled.RepoIssueCommentNSID,
619
Repo: user.Did,
620
Rkey: newComment.Rkey,
621
SwapRecord: ex.Cid,
···
650
}
651
652
commentId := chi.URLParam(r, "commentId")
653
-
comments, err := db.GetIssueComments(
654
rp.db,
655
db.FilterEq("id", commentId),
656
)
···
686
}
687
688
commentId := chi.URLParam(r, "commentId")
689
-
comments, err := db.GetIssueComments(
690
rp.db,
691
db.FilterEq("id", commentId),
692
)
···
722
}
723
724
commentId := chi.URLParam(r, "commentId")
725
-
comments, err := db.GetIssueComments(
726
rp.db,
727
db.FilterEq("id", commentId),
728
)
···
738
}
739
comment := comments[0]
740
741
-
if comment.Did != user.Did {
742
l.Error("unauthorized action", "expectedDid", comment.Did, "gotDid", user.Did)
743
http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
744
return
···
751
752
// optimistic deletion
753
deleted := time.Now()
754
-
err = db.DeleteIssueComments(rp.db, db.FilterEq("id", comment.Id))
755
if err != nil {
756
l.Error("failed to delete comment", "err", err)
757
rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment")
···
767
return
768
}
769
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
770
-
Collection: tangled.RepoIssueCommentNSID,
771
Repo: user.Did,
772
Rkey: comment.Rkey,
773
})
···
402
403
body := r.FormValue("body")
404
if body == "" {
405
+
rp.pages.Notice(w, "issue-comment", "Body is required")
406
return
407
}
408
409
+
var replyTo *syntax.ATURI
410
+
replyToRaw := r.FormValue("reply-to")
411
+
if replyToRaw != "" {
412
+
aturi, err := syntax.ParseATURI(r.FormValue("reply-to"))
413
+
if err != nil {
414
+
rp.pages.Notice(w, "issue-comment", "reply-to should be valid AT-URI")
415
+
return
416
+
}
417
+
replyTo = &aturi
418
}
419
420
mentions, references := rp.refResolver.Resolve(r.Context(), body)
421
422
+
comment := models.Comment{
423
+
Did: syntax.DID(user.Did),
424
Rkey: tid.TID(),
425
+
Subject: issue.AtUri(),
426
ReplyTo: replyTo,
427
Body: body,
428
Created: time.Now(),
429
Mentions: mentions,
430
References: references,
431
}
432
+
if err = comment.Validate(); err != nil {
433
l.Error("failed to validate comment", "err", err)
434
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
435
return
···
445
446
// create a record first
447
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
448
+
Collection: tangled.CommentNSID,
449
+
Repo: user.Did,
450
Rkey: comment.Rkey,
451
Record: &lexutil.LexiconTypeDecoder{
452
Val: &record,
···
472
}
473
defer tx.Rollback()
474
475
+
err = db.PutComment(tx, &comment)
476
if err != nil {
477
l.Error("failed to create comment", "err", err)
478
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
···
488
// reset atUri to make rollback a no-op
489
atUri = ""
490
491
rp.notifier.NewIssueComment(r.Context(), &comment, mentions)
492
493
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
494
+
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", ownerSlashRepo, issue.IssueId, comment.Id))
495
}
496
497
func (rp *Issues) IssueComment(w http.ResponseWriter, r *http.Request) {
···
506
}
507
508
commentId := chi.URLParam(r, "commentId")
509
+
comments, err := db.GetComments(
510
rp.db,
511
db.FilterEq("id", commentId),
512
)
···
542
}
543
544
commentId := chi.URLParam(r, "commentId")
545
+
comments, err := db.GetComments(
546
rp.db,
547
db.FilterEq("id", commentId),
548
)
···
558
}
559
comment := comments[0]
560
561
+
if comment.Did.String() != user.Did {
562
l.Error("unauthorized comment edit", "expectedDid", comment.Did, "gotDid", user.Did)
563
http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
564
return
···
598
}
599
defer tx.Rollback()
600
601
+
err = db.PutComment(tx, &newComment)
602
if err != nil {
603
l.Error("failed to perferom update-description query", "err", err)
604
rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.")
···
609
// rkey is optional, it was introduced later
610
if newComment.Rkey != "" {
611
// update the record on pds
612
+
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.CommentNSID, user.Did, comment.Rkey)
613
if err != nil {
614
l.Error("failed to get record", "err", err, "did", newComment.Did, "rkey", newComment.Rkey)
615
rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.")
···
617
}
618
619
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
620
+
Collection: tangled.CommentNSID,
621
Repo: user.Did,
622
Rkey: newComment.Rkey,
623
SwapRecord: ex.Cid,
···
652
}
653
654
commentId := chi.URLParam(r, "commentId")
655
+
comments, err := db.GetComments(
656
rp.db,
657
db.FilterEq("id", commentId),
658
)
···
688
}
689
690
commentId := chi.URLParam(r, "commentId")
691
+
comments, err := db.GetComments(
692
rp.db,
693
db.FilterEq("id", commentId),
694
)
···
724
}
725
726
commentId := chi.URLParam(r, "commentId")
727
+
comments, err := db.GetComments(
728
rp.db,
729
db.FilterEq("id", commentId),
730
)
···
740
}
741
comment := comments[0]
742
743
+
if comment.Did.String() != user.Did {
744
l.Error("unauthorized action", "expectedDid", comment.Did, "gotDid", user.Did)
745
http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
746
return
···
753
754
// optimistic deletion
755
deleted := time.Now()
756
+
err = db.DeleteComments(rp.db, db.FilterEq("id", comment.Id))
757
if err != nil {
758
l.Error("failed to delete comment", "err", err)
759
rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment")
···
769
return
770
}
771
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
772
+
Collection: tangled.CommentNSID,
773
Repo: user.Did,
774
Rkey: comment.Rkey,
775
})
+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
···
118
)
119
}
120
121
-
func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) {
122
-
issues, err := db.GetIssues(n.db, db.FilterEq("at_uri", comment.IssueAt))
123
if err != nil {
124
log.Printf("NewIssueComment: failed to get issues: %v", err)
125
return
126
}
127
if len(issues) == 0 {
128
-
log.Printf("NewIssueComment: no issue found for %s", comment.IssueAt)
129
return
130
}
131
issue := issues[0]
···
140
141
// find the parent thread, and add all DIDs from here to the recipient list
142
for _, t := range allThreads {
143
-
if t.Self.AtUri().String() == parentAtUri {
144
recipients = append(recipients, t.Participants()...)
145
}
146
}
···
118
)
119
}
120
121
+
func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID) {
122
+
issues, err := db.GetIssues(n.db, db.FilterEq("at_uri", comment.Subject))
123
if err != nil {
124
log.Printf("NewIssueComment: failed to get issues: %v", err)
125
return
126
}
127
if len(issues) == 0 {
128
+
log.Printf("NewIssueComment: no issue found for %s", comment.Subject)
129
return
130
}
131
issue := issues[0]
···
140
141
// find the parent thread, and add all DIDs from here to the recipient list
142
for _, t := range allThreads {
143
+
if t.Self.AtUri() == parentAtUri {
144
recipients = append(recipients, t.Participants()...)
145
}
146
}
+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
···
989
LoggedInUser *oauth.User
990
RepoInfo repoinfo.RepoInfo
991
Issue *models.Issue
992
-
Comment *models.IssueComment
993
}
994
995
func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error {
···
1000
LoggedInUser *oauth.User
1001
RepoInfo repoinfo.RepoInfo
1002
Issue *models.Issue
1003
-
Comment *models.IssueComment
1004
}
1005
1006
func (p *Pages) ReplyIssueCommentPlaceholderFragment(w io.Writer, params ReplyIssueCommentPlaceholderParams) error {
···
1011
LoggedInUser *oauth.User
1012
RepoInfo repoinfo.RepoInfo
1013
Issue *models.Issue
1014
-
Comment *models.IssueComment
1015
}
1016
1017
func (p *Pages) ReplyIssueCommentFragment(w io.Writer, params ReplyIssueCommentParams) error {
···
1022
LoggedInUser *oauth.User
1023
RepoInfo repoinfo.RepoInfo
1024
Issue *models.Issue
1025
-
Comment *models.IssueComment
1026
}
1027
1028
func (p *Pages) IssueCommentBodyFragment(w io.Writer, params IssueCommentBodyParams) error {
···
989
LoggedInUser *oauth.User
990
RepoInfo repoinfo.RepoInfo
991
Issue *models.Issue
992
+
Comment *models.Comment
993
}
994
995
func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error {
···
1000
LoggedInUser *oauth.User
1001
RepoInfo repoinfo.RepoInfo
1002
Issue *models.Issue
1003
+
Comment *models.Comment
1004
}
1005
1006
func (p *Pages) ReplyIssueCommentPlaceholderFragment(w io.Writer, params ReplyIssueCommentPlaceholderParams) error {
···
1011
LoggedInUser *oauth.User
1012
RepoInfo repoinfo.RepoInfo
1013
Issue *models.Issue
1014
+
Comment *models.Comment
1015
}
1016
1017
func (p *Pages) ReplyIssueCommentFragment(w io.Writer, params ReplyIssueCommentParams) error {
···
1022
LoggedInUser *oauth.User
1023
RepoInfo repoinfo.RepoInfo
1024
Issue *models.Issue
1025
+
Comment *models.Comment
1026
}
1027
1028
func (p *Pages) IssueCommentBodyFragment(w io.Writer, params IssueCommentBodyParams) error {
+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
-
{{ template "user/fragments/picHandleLink" .Comment.Did }}
4
{{ template "hats" $ }}
5
{{ template "timestamp" . }}
6
-
{{ $isCommentOwner := and .LoggedInUser (eq .LoggedInUser.Did .Comment.Did) }}
7
{{ if and $isCommentOwner (not .Comment.Deleted) }}
8
{{ template "editIssueComment" . }}
9
{{ 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
+
{{ template "user/fragments/picHandleLink" .Comment.Did.String }}
4
{{ template "hats" $ }}
5
{{ template "timestamp" . }}
6
+
{{ $isCommentOwner := and .LoggedInUser (eq .LoggedInUser.Did .Comment.Did.String) }}
7
{{ if and $isCommentOwner (not .Comment.Deleted) }}
8
{{ template "editIssueComment" . }}
9
{{ template "deleteIssueComment" . }}
+1
-1
appview/state/state.go
+1
-1
appview/state/state.go
-26
appview/validator/issue.go
-26
appview/validator/issue.go
···
4
"fmt"
5
"strings"
6
7
-
"tangled.org/core/appview/db"
8
"tangled.org/core/appview/models"
9
)
10
11
-
func (v *Validator) ValidateIssueComment(comment *models.IssueComment) error {
12
-
// if comments have parents, only ingest ones that are 1 level deep
13
-
if comment.ReplyTo != nil {
14
-
parents, err := db.GetIssueComments(v.db, db.FilterEq("at_uri", *comment.ReplyTo))
15
-
if err != nil {
16
-
return fmt.Errorf("failed to fetch parent comment: %w", err)
17
-
}
18
-
if len(parents) != 1 {
19
-
return fmt.Errorf("incorrect number of parent comments returned: %d", len(parents))
20
-
}
21
-
22
-
// depth check
23
-
parent := parents[0]
24
-
if parent.ReplyTo != nil {
25
-
return fmt.Errorf("incorrect depth, this comment is replying at depth >1")
26
-
}
27
-
}
28
-
29
-
if sb := strings.TrimSpace(v.sanitizer.SanitizeDefault(comment.Body)); sb == "" {
30
-
return fmt.Errorf("body is empty after HTML sanitization")
31
-
}
32
-
33
-
return nil
34
-
}
35
-
36
func (v *Validator) ValidateIssue(issue *models.Issue) error {
37
if issue.Title == "" {
38
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/db/reference.go:293
- 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.