Signed-off-by: Seongmin Lee git@boltless.me
+6
-185
appview/db/issues.go
+6
-185
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
}
···
351
return ids, nil
352
}
353
354
-
func AddIssueComment(tx *sql.Tx, c models.IssueComment) (int64, error) {
355
-
result, err := tx.Exec(
356
-
`insert into issue_comments (
357
-
did,
358
-
rkey,
359
-
issue_at,
360
-
body,
361
-
reply_to,
362
-
created,
363
-
edited
364
-
)
365
-
values (?, ?, ?, ?, ?, ?, null)
366
-
on conflict(did, rkey) do update set
367
-
issue_at = excluded.issue_at,
368
-
body = excluded.body,
369
-
edited = case
370
-
when
371
-
issue_comments.issue_at != excluded.issue_at
372
-
or issue_comments.body != excluded.body
373
-
or issue_comments.reply_to != excluded.reply_to
374
-
then ?
375
-
else issue_comments.edited
376
-
end`,
377
-
c.Did,
378
-
c.Rkey,
379
-
c.IssueAt,
380
-
c.Body,
381
-
c.ReplyTo,
382
-
c.Created.Format(time.RFC3339),
383
-
time.Now().Format(time.RFC3339),
384
-
)
385
-
if err != nil {
386
-
return 0, err
387
-
}
388
-
389
-
id, err := result.LastInsertId()
390
-
if err != nil {
391
-
return 0, err
392
-
}
393
-
394
-
if err := putReferences(tx, c.AtUri(), c.References); err != nil {
395
-
return 0, fmt.Errorf("put reference_links: %w", err)
396
-
}
397
-
398
-
return id, nil
399
-
}
400
-
401
-
func DeleteIssueComments(e Execer, filters ...orm.Filter) error {
402
-
var conditions []string
403
-
var args []any
404
-
for _, filter := range filters {
405
-
conditions = append(conditions, filter.Condition())
406
-
args = append(args, filter.Arg()...)
407
-
}
408
-
409
-
whereClause := ""
410
-
if conditions != nil {
411
-
whereClause = " where " + strings.Join(conditions, " and ")
412
-
}
413
-
414
-
query := fmt.Sprintf(`update issue_comments set body = "", deleted = strftime('%%Y-%%m-%%dT%%H:%%M:%%SZ', 'now') %s`, whereClause)
415
-
416
-
_, err := e.Exec(query, args...)
417
-
return err
418
-
}
419
-
420
-
func GetIssueComments(e Execer, filters ...orm.Filter) ([]models.IssueComment, error) {
421
-
commentMap := make(map[string]*models.IssueComment)
422
-
423
-
var conditions []string
424
-
var args []any
425
-
for _, filter := range filters {
426
-
conditions = append(conditions, filter.Condition())
427
-
args = append(args, filter.Arg()...)
428
-
}
429
-
430
-
whereClause := ""
431
-
if conditions != nil {
432
-
whereClause = " where " + strings.Join(conditions, " and ")
433
-
}
434
-
435
-
query := fmt.Sprintf(`
436
-
select
437
-
id,
438
-
did,
439
-
rkey,
440
-
issue_at,
441
-
reply_to,
442
-
body,
443
-
created,
444
-
edited,
445
-
deleted
446
-
from
447
-
issue_comments
448
-
%s
449
-
`, whereClause)
450
-
451
-
rows, err := e.Query(query, args...)
452
-
if err != nil {
453
-
return nil, err
454
-
}
455
-
456
-
for rows.Next() {
457
-
var comment models.IssueComment
458
-
var created string
459
-
var rkey, edited, deleted, replyTo sql.Null[string]
460
-
err := rows.Scan(
461
-
&comment.Id,
462
-
&comment.Did,
463
-
&rkey,
464
-
&comment.IssueAt,
465
-
&replyTo,
466
-
&comment.Body,
467
-
&created,
468
-
&edited,
469
-
&deleted,
470
-
)
471
-
if err != nil {
472
-
return nil, err
473
-
}
474
-
475
-
// this is a remnant from old times, newer comments always have rkey
476
-
if rkey.Valid {
477
-
comment.Rkey = rkey.V
478
-
}
479
-
480
-
if t, err := time.Parse(time.RFC3339, created); err == nil {
481
-
comment.Created = t
482
-
}
483
-
484
-
if edited.Valid {
485
-
if t, err := time.Parse(time.RFC3339, edited.V); err == nil {
486
-
comment.Edited = &t
487
-
}
488
-
}
489
-
490
-
if deleted.Valid {
491
-
if t, err := time.Parse(time.RFC3339, deleted.V); err == nil {
492
-
comment.Deleted = &t
493
-
}
494
-
}
495
-
496
-
if replyTo.Valid {
497
-
comment.ReplyTo = &replyTo.V
498
-
}
499
-
500
-
atUri := comment.AtUri().String()
501
-
commentMap[atUri] = &comment
502
-
}
503
-
504
-
if err = rows.Err(); err != nil {
505
-
return nil, err
506
-
}
507
-
508
-
// collect references for each comments
509
-
commentAts := slices.Collect(maps.Keys(commentMap))
510
-
allReferencs, err := GetReferencesAll(e, orm.FilterIn("from_at", commentAts))
511
-
if err != nil {
512
-
return nil, fmt.Errorf("failed to query reference_links: %w", err)
513
-
}
514
-
for commentAt, references := range allReferencs {
515
-
if comment, ok := commentMap[commentAt.String()]; ok {
516
-
comment.References = references
517
-
}
518
-
}
519
-
520
-
var comments []models.IssueComment
521
-
for _, c := range commentMap {
522
-
comments = append(comments, *c)
523
-
}
524
-
525
-
sort.Slice(comments, func(i, j int) bool {
526
-
return comments[i].Created.After(comments[j].Created)
527
-
})
528
-
529
-
return comments, nil
530
-
}
531
-
532
func DeleteIssues(tx *sql.Tx, did, rkey string) error {
533
_, err := tx.Exec(
534
`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
}
···
350
return ids, nil
351
}
352
353
func DeleteIssues(tx *sql.Tx, did, rkey string) error {
354
_, err := tx.Exec(
355
`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`,
+19
-11
appview/ingester.go
+19
-11
appview/ingester.go
···
79
err = i.ingestString(e)
80
case tangled.RepoIssueNSID:
81
err = i.ingestIssue(ctx, e)
82
-
case tangled.RepoIssueCommentNSID:
83
-
err = i.ingestIssueComment(e)
84
case tangled.LabelDefinitionNSID:
85
err = i.ingestLabelDefinition(e)
86
case tangled.LabelOpNSID:
···
868
return nil
869
}
870
871
-
func (i *Ingester) ingestIssueComment(e *jmodels.Event) error {
872
did := e.Did
873
rkey := e.Commit.RKey
874
875
var err error
876
877
-
l := i.Logger.With("handler", "ingestIssueComment", "nsid", e.Commit.Collection, "did", did, "rkey", rkey)
878
l.Info("ingesting record")
879
880
ddb, ok := i.Db.Execer.(*db.DB)
···
885
switch e.Commit.Operation {
886
case jmodels.CommitOperationCreate, jmodels.CommitOperationUpdate:
887
raw := json.RawMessage(e.Commit.Record)
888
-
record := tangled.RepoIssueComment{}
889
err = json.Unmarshal(raw, &record)
890
if err != nil {
891
return fmt.Errorf("invalid record: %w", err)
892
}
893
894
-
comment, err := models.IssueCommentFromRecord(did, rkey, record)
895
if err != nil {
896
return fmt.Errorf("failed to parse comment from record: %w", err)
897
}
898
899
-
if err := i.Validator.ValidateIssueComment(comment); err != nil {
900
return fmt.Errorf("failed to validate comment: %w", err)
901
}
902
···
906
}
907
defer tx.Rollback()
908
909
-
_, err = db.AddIssueComment(tx, *comment)
910
if err != nil {
911
-
return fmt.Errorf("failed to create issue comment: %w", err)
912
}
913
914
return tx.Commit()
915
916
case jmodels.CommitOperationDelete:
917
-
if err := db.DeleteIssueComments(
918
ddb,
919
orm.FilterEq("did", did),
920
orm.FilterEq("rkey", rkey),
921
); err != nil {
922
-
return fmt.Errorf("failed to delete issue comment record: %w", err)
923
}
924
925
return nil
···
79
err = i.ingestString(e)
80
case tangled.RepoIssueNSID:
81
err = i.ingestIssue(ctx, e)
82
+
case tangled.CommentNSID:
83
+
err = i.ingestComment(e)
84
case tangled.LabelDefinitionNSID:
85
err = i.ingestLabelDefinition(e)
86
case tangled.LabelOpNSID:
···
868
return nil
869
}
870
871
+
func (i *Ingester) ingestComment(e *jmodels.Event) error {
872
did := e.Did
873
rkey := e.Commit.RKey
874
875
var err error
876
877
+
l := i.Logger.With("handler", "ingestComment", "nsid", e.Commit.Collection, "did", did, "rkey", rkey)
878
l.Info("ingesting record")
879
880
ddb, ok := i.Db.Execer.(*db.DB)
···
885
switch e.Commit.Operation {
886
case jmodels.CommitOperationCreate, jmodels.CommitOperationUpdate:
887
raw := json.RawMessage(e.Commit.Record)
888
+
record := tangled.Comment{}
889
err = json.Unmarshal(raw, &record)
890
if err != nil {
891
return fmt.Errorf("invalid record: %w", err)
892
}
893
894
+
comment, err := models.CommentFromRecord(did, rkey, record)
895
if err != nil {
896
return fmt.Errorf("failed to parse comment from record: %w", err)
897
}
898
899
+
// TODO: ingest pull comments
900
+
// we aren't ingesting pull comments yet because pull itself isn't fully atprotated.
901
+
// so we cannot know which round this comment is pointing to
902
+
if comment.Subject.Collection().String() == tangled.RepoPullNSID {
903
+
l.Info("skip ingesting pull comments")
904
+
return nil
905
+
}
906
+
907
+
if err := comment.Validate(); err != nil {
908
return fmt.Errorf("failed to validate comment: %w", err)
909
}
910
···
914
}
915
defer tx.Rollback()
916
917
+
err = db.PutComment(tx, comment)
918
if err != nil {
919
+
return fmt.Errorf("failed to create comment: %w", err)
920
}
921
922
return tx.Commit()
923
924
case jmodels.CommitOperationDelete:
925
+
if err := db.DeleteComments(
926
ddb,
927
orm.FilterEq("did", did),
928
orm.FilterEq("rkey", rkey),
929
); err != nil {
930
+
return fmt.Errorf("failed to delete comment record: %w", err)
931
}
932
933
return nil
+30
-28
appview/issues/issues.go
+30
-28
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.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
···
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,
···
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.Did {
561
l.Error("unauthorized comment edit", "expectedDid", comment.Did, "gotDid", user.Did)
562
http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
563
return
···
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.")
···
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.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.")
···
616
}
617
618
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
619
-
Collection: tangled.RepoIssueCommentNSID,
620
Repo: user.Did,
621
Rkey: newComment.Rkey,
622
SwapRecord: ex.Cid,
···
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.Did {
743
l.Error("unauthorized action", "expectedDid", comment.Did, "gotDid", user.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.Did,
773
Rkey: comment.Rkey,
774
})
···
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(r.FormValue("reply-to"))
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.Did),
425
Rkey: tid.TID(),
426
+
Subject: issue.AtUri(),
427
ReplyTo: replyTo,
428
Body: body,
429
Created: time.Now(),
430
Mentions: mentions,
431
References: references,
432
}
433
+
if err = comment.Validate(); err != nil {
434
l.Error("failed to validate comment", "err", err)
435
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
436
return
···
446
447
// create a record first
448
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
449
+
Collection: tangled.CommentNSID,
450
+
Repo: user.Did,
451
Rkey: comment.Rkey,
452
Record: &lexutil.LexiconTypeDecoder{
453
Val: &record,
···
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.Did {
563
l.Error("unauthorized comment edit", "expectedDid", comment.Did, "gotDid", user.Did)
564
http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
565
return
···
599
}
600
defer tx.Rollback()
601
602
+
err = db.PutComment(tx, &newComment)
603
if err != nil {
604
l.Error("failed to perferom update-description query", "err", err)
605
rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.")
···
610
// rkey is optional, it was introduced later
611
if newComment.Rkey != "" {
612
// update the record on pds
613
+
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.CommentNSID, user.Did, comment.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 description, no record found on PDS.")
···
618
}
619
620
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
621
+
Collection: tangled.CommentNSID,
622
Repo: user.Did,
623
Rkey: newComment.Rkey,
624
SwapRecord: ex.Cid,
···
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.Did {
745
l.Error("unauthorized action", "expectedDid", comment.Did, "gotDid", user.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: tangled.CommentNSID,
774
Repo: user.Did,
775
Rkey: comment.Rkey,
776
})
+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
···
119
)
120
}
121
122
-
func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) {
123
-
issues, err := db.GetIssues(n.db, orm.FilterEq("at_uri", comment.IssueAt))
124
if err != nil {
125
log.Printf("NewIssueComment: failed to get issues: %v", err)
126
return
127
}
128
if len(issues) == 0 {
129
-
log.Printf("NewIssueComment: no issue found for %s", comment.IssueAt)
130
return
131
}
132
issue := issues[0]
···
141
142
// find the parent thread, and add all DIDs from here to the recipient list
143
for _, t := range allThreads {
144
-
if t.Self.AtUri().String() == parentAtUri {
145
recipients = append(recipients, t.Participants()...)
146
}
147
}
···
119
)
120
}
121
122
+
func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID) {
123
+
issues, err := db.GetIssues(n.db, orm.FilterEq("at_uri", comment.Subject))
124
if err != nil {
125
log.Printf("NewIssueComment: failed to get issues: %v", err)
126
return
127
}
128
if len(issues) == 0 {
129
+
log.Printf("NewIssueComment: no issue found for %s", comment.Subject)
130
return
131
}
132
issue := issues[0]
···
141
142
// find the parent thread, and add all DIDs from here to the recipient list
143
for _, t := range allThreads {
144
+
if t.Self.AtUri() == parentAtUri {
145
recipients = append(recipients, t.Participants()...)
146
}
147
}
+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
···
988
LoggedInUser *oauth.User
989
RepoInfo repoinfo.RepoInfo
990
Issue *models.Issue
991
-
Comment *models.IssueComment
992
}
993
994
func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error {
···
999
LoggedInUser *oauth.User
1000
RepoInfo repoinfo.RepoInfo
1001
Issue *models.Issue
1002
-
Comment *models.IssueComment
1003
}
1004
1005
func (p *Pages) ReplyIssueCommentPlaceholderFragment(w io.Writer, params ReplyIssueCommentPlaceholderParams) error {
···
1010
LoggedInUser *oauth.User
1011
RepoInfo repoinfo.RepoInfo
1012
Issue *models.Issue
1013
-
Comment *models.IssueComment
1014
}
1015
1016
func (p *Pages) ReplyIssueCommentFragment(w io.Writer, params ReplyIssueCommentParams) error {
···
1021
LoggedInUser *oauth.User
1022
RepoInfo repoinfo.RepoInfo
1023
Issue *models.Issue
1024
-
Comment *models.IssueComment
1025
}
1026
1027
func (p *Pages) IssueCommentBodyFragment(w io.Writer, params IssueCommentBodyParams) error {
···
988
LoggedInUser *oauth.User
989
RepoInfo repoinfo.RepoInfo
990
Issue *models.Issue
991
+
Comment *models.Comment
992
}
993
994
func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error {
···
999
LoggedInUser *oauth.User
1000
RepoInfo repoinfo.RepoInfo
1001
Issue *models.Issue
1002
+
Comment *models.Comment
1003
}
1004
1005
func (p *Pages) ReplyIssueCommentPlaceholderFragment(w io.Writer, params ReplyIssueCommentPlaceholderParams) error {
···
1010
LoggedInUser *oauth.User
1011
RepoInfo repoinfo.RepoInfo
1012
Issue *models.Issue
1013
+
Comment *models.Comment
1014
}
1015
1016
func (p *Pages) ReplyIssueCommentFragment(w io.Writer, params ReplyIssueCommentParams) error {
···
1021
LoggedInUser *oauth.User
1022
RepoInfo repoinfo.RepoInfo
1023
Issue *models.Issue
1024
+
Comment *models.Comment
1025
}
1026
1027
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
-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.