Signed-off-by: Seongmin Lee git@boltless.me
+6
-185
appview/db/issues.go
+6
-185
appview/db/issues.go
···
99
99
}
100
100
101
101
func GetIssuesPaginated(e Execer, page pagination.Page, filters ...filter) ([]models.Issue, error) {
102
-
issueMap := make(map[string]*models.Issue) // at-uri -> issue
102
+
issueMap := make(map[syntax.ATURI]*models.Issue) // at-uri -> issue
103
103
104
104
var conditions []string
105
105
var args []any
···
195
195
}
196
196
}
197
197
198
-
atUri := issue.AtUri().String()
199
-
issueMap[atUri] = &issue
198
+
issueMap[issue.AtUri()] = &issue
200
199
}
201
200
202
201
// collect reverse repos
···
228
227
// collect comments
229
228
issueAts := slices.Collect(maps.Keys(issueMap))
230
229
231
-
comments, err := GetIssueComments(e, FilterIn("issue_at", issueAts))
230
+
comments, err := GetComments(e, FilterIn("subject_at", issueAts))
232
231
if err != nil {
233
232
return nil, fmt.Errorf("failed to query comments: %w", err)
234
233
}
235
234
for i := range comments {
236
-
issueAt := comments[i].IssueAt
235
+
issueAt := comments[i].Subject
237
236
if issue, ok := issueMap[issueAt]; ok {
238
237
issue.Comments = append(issue.Comments, comments[i])
239
238
}
···
245
244
return nil, fmt.Errorf("failed to query labels: %w", err)
246
245
}
247
246
for issueAt, labels := range allLabels {
248
-
if issue, ok := issueMap[issueAt.String()]; ok {
247
+
if issue, ok := issueMap[issueAt]; ok {
249
248
issue.Labels = labels
250
249
}
251
250
}
···
256
255
return nil, fmt.Errorf("failed to query reference_links: %w", err)
257
256
}
258
257
for issueAt, references := range allReferencs {
259
-
if issue, ok := issueMap[issueAt.String()]; ok {
258
+
if issue, ok := issueMap[issueAt]; ok {
260
259
issue.References = references
261
260
}
262
261
}
···
350
349
return ids, nil
351
350
}
352
351
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
352
func DeleteIssues(tx *sql.Tx, did, rkey string) error {
532
353
_, err := tx.Exec(
533
354
`delete from issues
+13
-24
appview/db/reference.go
+13
-24
appview/db/reference.go
···
10
10
"tangled.org/core/appview/models"
11
11
)
12
12
13
-
// ValidateReferenceLinks resolves refLinks to Issue/PR/IssueComment/PullComment ATURIs.
13
+
// ValidateReferenceLinks resolves refLinks to Issue/PR/Comment ATURIs.
14
14
// It will ignore missing refLinks.
15
15
func ValidateReferenceLinks(e Execer, refLinks []models.ReferenceLink) ([]syntax.ATURI, error) {
16
16
var (
···
52
52
values %s
53
53
)
54
54
select
55
-
i.did, i.rkey,
56
-
c.did, c.rkey
55
+
i.at_uri, c.at_uri
57
56
from input inp
58
57
join repos r
59
58
on r.did = inp.owner_did
···
61
60
join issues i
62
61
on i.repo_at = r.at_uri
63
62
and i.issue_id = inp.issue_id
64
-
left join issue_comments c
63
+
left join comments c
65
64
on inp.comment_id is not null
66
-
and c.issue_at = i.at_uri
65
+
and c.subject_at = i.at_uri
67
66
and c.id = inp.comment_id
68
67
`,
69
68
strings.Join(vals, ","),
···
78
77
79
78
for rows.Next() {
80
79
// Scan rows
81
-
var issueOwner, issueRkey string
82
-
var commentOwner, commentRkey sql.NullString
80
+
var issueUri string
81
+
var commentUri sql.NullString
83
82
var uri syntax.ATURI
84
-
if err := rows.Scan(&issueOwner, &issueRkey, &commentOwner, &commentRkey); err != nil {
83
+
if err := rows.Scan(&issueUri, &commentUri); err != nil {
85
84
return nil, err
86
85
}
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
-
))
86
+
if commentUri.Valid {
87
+
uri = syntax.ATURI(commentUri.String)
94
88
} else {
95
-
uri = syntax.ATURI(fmt.Sprintf(
96
-
"at://%s/%s/%s",
97
-
issueOwner,
98
-
tangled.RepoIssueNSID,
99
-
issueRkey,
100
-
))
89
+
uri = syntax.ATURI(issueUri)
101
90
}
102
91
uris = append(uris, uri)
103
92
}
···
281
270
return nil, fmt.Errorf("get issue backlinks: %w", err)
282
271
}
283
272
backlinks = append(backlinks, ls...)
284
-
ls, err = getIssueCommentBacklinks(e, backlinksMap[tangled.RepoIssueCommentNSID])
273
+
ls, err = getIssueCommentBacklinks(e, backlinksMap[tangled.CommentNSID])
285
274
if err != nil {
286
275
return nil, fmt.Errorf("get issue_comment backlinks: %w", err)
287
276
}
···
350
339
rows, err := e.Query(
351
340
fmt.Sprintf(
352
341
`select r.did, r.name, i.issue_id, c.id, i.title, i.open
353
-
from issue_comments c
342
+
from comments c
354
343
join issues i
355
-
on i.at_uri = c.issue_at
344
+
on i.at_uri = c.subject_at
356
345
join repos r
357
346
on r.at_uri = i.repo_at
358
347
where %s`,
+19
-11
appview/ingester.go
+19
-11
appview/ingester.go
···
78
78
err = i.ingestString(e)
79
79
case tangled.RepoIssueNSID:
80
80
err = i.ingestIssue(ctx, e)
81
-
case tangled.RepoIssueCommentNSID:
82
-
err = i.ingestIssueComment(e)
81
+
case tangled.CommentNSID:
82
+
err = i.ingestComment(e)
83
83
case tangled.LabelDefinitionNSID:
84
84
err = i.ingestLabelDefinition(e)
85
85
case tangled.LabelOpNSID:
···
867
867
return nil
868
868
}
869
869
870
-
func (i *Ingester) ingestIssueComment(e *jmodels.Event) error {
870
+
func (i *Ingester) ingestComment(e *jmodels.Event) error {
871
871
did := e.Did
872
872
rkey := e.Commit.RKey
873
873
874
874
var err error
875
875
876
-
l := i.Logger.With("handler", "ingestIssueComment", "nsid", e.Commit.Collection, "did", did, "rkey", rkey)
876
+
l := i.Logger.With("handler", "ingestComment", "nsid", e.Commit.Collection, "did", did, "rkey", rkey)
877
877
l.Info("ingesting record")
878
878
879
879
ddb, ok := i.Db.Execer.(*db.DB)
···
884
884
switch e.Commit.Operation {
885
885
case jmodels.CommitOperationCreate, jmodels.CommitOperationUpdate:
886
886
raw := json.RawMessage(e.Commit.Record)
887
-
record := tangled.RepoIssueComment{}
887
+
record := tangled.Comment{}
888
888
err = json.Unmarshal(raw, &record)
889
889
if err != nil {
890
890
return fmt.Errorf("invalid record: %w", err)
891
891
}
892
892
893
-
comment, err := models.IssueCommentFromRecord(did, rkey, record)
893
+
comment, err := models.CommentFromRecord(did, rkey, record)
894
894
if err != nil {
895
895
return fmt.Errorf("failed to parse comment from record: %w", err)
896
896
}
897
897
898
-
if err := i.Validator.ValidateIssueComment(comment); err != nil {
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 {
899
907
return fmt.Errorf("failed to validate comment: %w", err)
900
908
}
901
909
···
905
913
}
906
914
defer tx.Rollback()
907
915
908
-
_, err = db.AddIssueComment(tx, *comment)
916
+
err = db.PutComment(tx, comment)
909
917
if err != nil {
910
-
return fmt.Errorf("failed to create issue comment: %w", err)
918
+
return fmt.Errorf("failed to create comment: %w", err)
911
919
}
912
920
913
921
return tx.Commit()
914
922
915
923
case jmodels.CommitOperationDelete:
916
-
if err := db.DeleteIssueComments(
924
+
if err := db.DeleteComments(
917
925
ddb,
918
926
db.FilterEq("did", did),
919
927
db.FilterEq("rkey", rkey),
920
928
); err != nil {
921
-
return fmt.Errorf("failed to delete issue comment record: %w", err)
929
+
return fmt.Errorf("failed to delete comment record: %w", err)
922
930
}
923
931
924
932
return nil
+30
-28
appview/issues/issues.go
+30
-28
appview/issues/issues.go
···
402
402
403
403
body := r.FormValue("body")
404
404
if body == "" {
405
-
rp.pages.Notice(w, "issue", "Body is required")
405
+
rp.pages.Notice(w, "issue-comment", "Body is required")
406
406
return
407
407
}
408
408
409
-
replyToUri := r.FormValue("reply-to")
410
-
var replyTo *string
411
-
if replyToUri != "" {
412
-
replyTo = &replyToUri
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
413
418
}
414
419
415
420
mentions, references := rp.refResolver.Resolve(r.Context(), body)
416
421
417
-
comment := models.IssueComment{
418
-
Did: user.Did,
422
+
comment := models.Comment{
423
+
Did: syntax.DID(user.Did),
419
424
Rkey: tid.TID(),
420
-
IssueAt: issue.AtUri().String(),
425
+
Subject: issue.AtUri(),
421
426
ReplyTo: replyTo,
422
427
Body: body,
423
428
Created: time.Now(),
424
429
Mentions: mentions,
425
430
References: references,
426
431
}
427
-
if err = rp.validator.ValidateIssueComment(&comment); err != nil {
432
+
if err = comment.Validate(); err != nil {
428
433
l.Error("failed to validate comment", "err", err)
429
434
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
430
435
return
···
440
445
441
446
// create a record first
442
447
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
443
-
Collection: tangled.RepoIssueCommentNSID,
444
-
Repo: comment.Did,
448
+
Collection: tangled.CommentNSID,
449
+
Repo: user.Did,
445
450
Rkey: comment.Rkey,
446
451
Record: &lexutil.LexiconTypeDecoder{
447
452
Val: &record,
···
467
472
}
468
473
defer tx.Rollback()
469
474
470
-
commentId, err := db.AddIssueComment(tx, comment)
475
+
err = db.PutComment(tx, &comment)
471
476
if err != nil {
472
477
l.Error("failed to create comment", "err", err)
473
478
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
···
483
488
// reset atUri to make rollback a no-op
484
489
atUri = ""
485
490
486
-
// notify about the new comment
487
-
comment.Id = commentId
488
-
489
491
rp.notifier.NewIssueComment(r.Context(), &comment, mentions)
490
492
491
493
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
492
-
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", ownerSlashRepo, issue.IssueId, commentId))
494
+
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", ownerSlashRepo, issue.IssueId, comment.Id))
493
495
}
494
496
495
497
func (rp *Issues) IssueComment(w http.ResponseWriter, r *http.Request) {
···
504
506
}
505
507
506
508
commentId := chi.URLParam(r, "commentId")
507
-
comments, err := db.GetIssueComments(
509
+
comments, err := db.GetComments(
508
510
rp.db,
509
511
db.FilterEq("id", commentId),
510
512
)
···
540
542
}
541
543
542
544
commentId := chi.URLParam(r, "commentId")
543
-
comments, err := db.GetIssueComments(
545
+
comments, err := db.GetComments(
544
546
rp.db,
545
547
db.FilterEq("id", commentId),
546
548
)
···
556
558
}
557
559
comment := comments[0]
558
560
559
-
if comment.Did != user.Did {
561
+
if comment.Did.String() != user.Did {
560
562
l.Error("unauthorized comment edit", "expectedDid", comment.Did, "gotDid", user.Did)
561
563
http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
562
564
return
···
596
598
}
597
599
defer tx.Rollback()
598
600
599
-
_, err = db.AddIssueComment(tx, newComment)
601
+
err = db.PutComment(tx, &newComment)
600
602
if err != nil {
601
603
l.Error("failed to perferom update-description query", "err", err)
602
604
rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.")
···
607
609
// rkey is optional, it was introduced later
608
610
if newComment.Rkey != "" {
609
611
// update the record on pds
610
-
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueCommentNSID, user.Did, comment.Rkey)
612
+
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.CommentNSID, user.Did, comment.Rkey)
611
613
if err != nil {
612
614
l.Error("failed to get record", "err", err, "did", newComment.Did, "rkey", newComment.Rkey)
613
615
rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.")
···
615
617
}
616
618
617
619
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
618
-
Collection: tangled.RepoIssueCommentNSID,
620
+
Collection: tangled.CommentNSID,
619
621
Repo: user.Did,
620
622
Rkey: newComment.Rkey,
621
623
SwapRecord: ex.Cid,
···
650
652
}
651
653
652
654
commentId := chi.URLParam(r, "commentId")
653
-
comments, err := db.GetIssueComments(
655
+
comments, err := db.GetComments(
654
656
rp.db,
655
657
db.FilterEq("id", commentId),
656
658
)
···
686
688
}
687
689
688
690
commentId := chi.URLParam(r, "commentId")
689
-
comments, err := db.GetIssueComments(
691
+
comments, err := db.GetComments(
690
692
rp.db,
691
693
db.FilterEq("id", commentId),
692
694
)
···
722
724
}
723
725
724
726
commentId := chi.URLParam(r, "commentId")
725
-
comments, err := db.GetIssueComments(
727
+
comments, err := db.GetComments(
726
728
rp.db,
727
729
db.FilterEq("id", commentId),
728
730
)
···
738
740
}
739
741
comment := comments[0]
740
742
741
-
if comment.Did != user.Did {
743
+
if comment.Did.String() != user.Did {
742
744
l.Error("unauthorized action", "expectedDid", comment.Did, "gotDid", user.Did)
743
745
http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
744
746
return
···
751
753
752
754
// optimistic deletion
753
755
deleted := time.Now()
754
-
err = db.DeleteIssueComments(rp.db, db.FilterEq("id", comment.Id))
756
+
err = db.DeleteComments(rp.db, db.FilterEq("id", comment.Id))
755
757
if err != nil {
756
758
l.Error("failed to delete comment", "err", err)
757
759
rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment")
···
767
769
return
768
770
}
769
771
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
770
-
Collection: tangled.RepoIssueCommentNSID,
772
+
Collection: tangled.CommentNSID,
771
773
Repo: user.Did,
772
774
Rkey: comment.Rkey,
773
775
})
+8
-89
appview/models/issue.go
+8
-89
appview/models/issue.go
···
26
26
27
27
// optionally, populate this when querying for reverse mappings
28
28
// like comment counts, parent repo etc.
29
-
Comments []IssueComment
29
+
Comments []Comment
30
30
Labels LabelState
31
31
Repo *Repo
32
32
}
···
62
62
}
63
63
64
64
type CommentListItem struct {
65
-
Self *IssueComment
66
-
Replies []*IssueComment
65
+
Self *Comment
66
+
Replies []*Comment
67
67
}
68
68
69
69
func (it *CommentListItem) Participants() []syntax.DID {
···
88
88
89
89
func (i *Issue) CommentList() []CommentListItem {
90
90
// Create a map to quickly find comments by their aturi
91
-
toplevel := make(map[string]*CommentListItem)
92
-
var replies []*IssueComment
91
+
toplevel := make(map[syntax.ATURI]*CommentListItem)
92
+
var replies []*Comment
93
93
94
94
// collect top level comments into the map
95
95
for _, comment := range i.Comments {
96
96
if comment.IsTopLevel() {
97
-
toplevel[comment.AtUri().String()] = &CommentListItem{
97
+
toplevel[comment.AtUri()] = &CommentListItem{
98
98
Self: &comment,
99
99
}
100
100
} else {
···
115
115
}
116
116
117
117
// sort everything
118
-
sortFunc := func(a, b *IssueComment) bool {
118
+
sortFunc := func(a, b *Comment) bool {
119
119
return a.Created.Before(b.Created)
120
120
}
121
121
sort.Slice(listing, func(i, j int) bool {
···
144
144
addParticipant(i.Did)
145
145
146
146
for _, c := range i.Comments {
147
-
addParticipant(c.Did)
147
+
addParticipant(c.Did.String())
148
148
}
149
149
150
150
return participants
···
171
171
Open: true, // new issues are open by default
172
172
}
173
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
-
}
+4
-4
appview/notify/db/db.go
+4
-4
appview/notify/db/db.go
···
118
118
)
119
119
}
120
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))
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
123
if err != nil {
124
124
log.Printf("NewIssueComment: failed to get issues: %v", err)
125
125
return
126
126
}
127
127
if len(issues) == 0 {
128
-
log.Printf("NewIssueComment: no issue found for %s", comment.IssueAt)
128
+
log.Printf("NewIssueComment: no issue found for %s", comment.Subject)
129
129
return
130
130
}
131
131
issue := issues[0]
···
140
140
141
141
// find the parent thread, and add all DIDs from here to the recipient list
142
142
for _, t := range allThreads {
143
-
if t.Self.AtUri().String() == parentAtUri {
143
+
if t.Self.AtUri() == parentAtUri {
144
144
recipients = append(recipients, t.Participants()...)
145
145
}
146
146
}
+1
-1
appview/notify/merged_notifier.go
+1
-1
appview/notify/merged_notifier.go
···
58
58
m.fanout("NewIssue", ctx, issue, mentions)
59
59
}
60
60
61
-
func (m *mergedNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) {
61
+
func (m *mergedNotifier) NewIssueComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID) {
62
62
m.fanout("NewIssueComment", ctx, comment, mentions)
63
63
}
64
64
+2
-2
appview/notify/notifier.go
+2
-2
appview/notify/notifier.go
···
14
14
DeleteStar(ctx context.Context, star *models.Star)
15
15
16
16
NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID)
17
-
NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID)
17
+
NewIssueComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID)
18
18
NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue)
19
19
DeleteIssue(ctx context.Context, issue *models.Issue)
20
20
···
43
43
func (m *BaseNotifier) DeleteStar(ctx context.Context, star *models.Star) {}
44
44
45
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) {
46
+
func (m *BaseNotifier) NewIssueComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID) {
47
47
}
48
48
func (m *BaseNotifier) NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) {}
49
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
179
}
180
180
}
181
181
182
-
func (n *posthogNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) {
182
+
func (n *posthogNotifier) NewIssueComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID) {
183
183
err := n.client.Enqueue(posthog.Capture{
184
-
DistinctId: comment.Did,
184
+
DistinctId: comment.Did.String(),
185
185
Event: "new_issue_comment",
186
186
Properties: posthog.Properties{
187
-
"issue_at": comment.IssueAt,
187
+
"issue_at": comment.Subject,
188
188
"mentions": mentions,
189
189
},
190
190
})
+4
-4
appview/pages/pages.go
+4
-4
appview/pages/pages.go
···
989
989
LoggedInUser *oauth.User
990
990
RepoInfo repoinfo.RepoInfo
991
991
Issue *models.Issue
992
-
Comment *models.IssueComment
992
+
Comment *models.Comment
993
993
}
994
994
995
995
func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error {
···
1000
1000
LoggedInUser *oauth.User
1001
1001
RepoInfo repoinfo.RepoInfo
1002
1002
Issue *models.Issue
1003
-
Comment *models.IssueComment
1003
+
Comment *models.Comment
1004
1004
}
1005
1005
1006
1006
func (p *Pages) ReplyIssueCommentPlaceholderFragment(w io.Writer, params ReplyIssueCommentPlaceholderParams) error {
···
1011
1011
LoggedInUser *oauth.User
1012
1012
RepoInfo repoinfo.RepoInfo
1013
1013
Issue *models.Issue
1014
-
Comment *models.IssueComment
1014
+
Comment *models.Comment
1015
1015
}
1016
1016
1017
1017
func (p *Pages) ReplyIssueCommentFragment(w io.Writer, params ReplyIssueCommentParams) error {
···
1022
1022
LoggedInUser *oauth.User
1023
1023
RepoInfo repoinfo.RepoInfo
1024
1024
Issue *models.Issue
1025
-
Comment *models.IssueComment
1025
+
Comment *models.Comment
1026
1026
}
1027
1027
1028
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
1
{{ define "repo/issues/fragments/issueCommentHeader" }}
2
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 }}
3
+
{{ template "user/fragments/picHandleLink" .Comment.Did.String }}
4
4
{{ template "hats" $ }}
5
5
{{ template "timestamp" . }}
6
-
{{ $isCommentOwner := and .LoggedInUser (eq .LoggedInUser.Did .Comment.Did) }}
6
+
{{ $isCommentOwner := and .LoggedInUser (eq .LoggedInUser.Did .Comment.Did.String) }}
7
7
{{ if and $isCommentOwner (not .Comment.Deleted) }}
8
8
{{ template "editIssueComment" . }}
9
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
4
"fmt"
5
5
"strings"
6
6
7
-
"tangled.org/core/appview/db"
8
7
"tangled.org/core/appview/models"
9
8
)
10
9
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
10
func (v *Validator) ValidateIssue(issue *models.Issue) error {
37
11
if issue.Title == "" {
38
12
return fmt.Errorf("issue title is empty")