···172 if err := rows.Err(); err != nil {
173 return nil, err
174 }
0175176 // collect references from each comments
177 commentAts := slices.Collect(maps.Keys(commentMap))
···172 if err := rows.Err(); err != nil {
173 return nil, err
174 }
175+ defer rows.Close()
176177 // collect references from each comments
178 commentAts := slices.Collect(maps.Keys(commentMap))
+6-186
appview/db/issues.go
···100}
101102func GetIssuesPaginated(e Execer, page pagination.Page, filters ...orm.Filter) ([]models.Issue, error) {
103- issueMap := make(map[string]*models.Issue) // at-uri -> issue
104105 var conditions []string
106 var args []any
···196 }
197 }
198199- atUri := issue.AtUri().String()
200- issueMap[atUri] = &issue
201 }
202203 // collect reverse repos
···229 // collect comments
230 issueAts := slices.Collect(maps.Keys(issueMap))
231232- 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 }
···349 }
350351 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- defer rows.Close()
456-457- for rows.Next() {
458- var comment models.IssueComment
459- var created string
460- var rkey, edited, deleted, replyTo sql.Null[string]
461- err := rows.Scan(
462- &comment.Id,
463- &comment.Did,
464- &rkey,
465- &comment.IssueAt,
466- &replyTo,
467- &comment.Body,
468- &created,
469- &edited,
470- &deleted,
471- )
472- if err != nil {
473- return nil, err
474- }
475-476- // this is a remnant from old times, newer comments always have rkey
477- if rkey.Valid {
478- comment.Rkey = rkey.V
479- }
480-481- if t, err := time.Parse(time.RFC3339, created); err == nil {
482- comment.Created = t
483- }
484-485- if edited.Valid {
486- if t, err := time.Parse(time.RFC3339, edited.V); err == nil {
487- comment.Edited = &t
488- }
489- }
490-491- if deleted.Valid {
492- if t, err := time.Parse(time.RFC3339, deleted.V); err == nil {
493- comment.Deleted = &t
494- }
495- }
496-497- if replyTo.Valid {
498- comment.ReplyTo = &replyTo.V
499- }
500-501- atUri := comment.AtUri().String()
502- commentMap[atUri] = &comment
503- }
504-505- if err = rows.Err(); err != nil {
506- return nil, err
507- }
508-509- // collect references for each comments
510- commentAts := slices.Collect(maps.Keys(commentMap))
511- allReferencs, err := GetReferencesAll(e, orm.FilterIn("from_at", commentAts))
512- if err != nil {
513- return nil, fmt.Errorf("failed to query reference_links: %w", err)
514- }
515- for commentAt, references := range allReferencs {
516- if comment, ok := commentMap[commentAt.String()]; ok {
517- comment.References = references
518- }
519- }
520-521- var comments []models.IssueComment
522- for _, c := range commentMap {
523- comments = append(comments, *c)
524- }
525-526- sort.Slice(comments, func(i, j int) bool {
527- return comments[i].Created.After(comments[j].Created)
528- })
529-530- return comments, nil
531}
532533func DeleteIssues(tx *sql.Tx, did, rkey string) error {
···100}
101102func GetIssuesPaginated(e Execer, page pagination.Page, filters ...orm.Filter) ([]models.Issue, error) {
103+ issueMap := make(map[syntax.ATURI]*models.Issue) // at-uri -> issue
104105 var conditions []string
106 var args []any
···196 }
197 }
198199+ issueMap[issue.AtUri()] = &issue
0200 }
201202 // collect reverse repos
···228 // collect comments
229 issueAts := slices.Collect(maps.Keys(issueMap))
230231+ 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 }
···348 }
349350 return ids, nil
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000351}
352353func DeleteIssues(tx *sql.Tx, did, rkey string) error {
+13-24
appview/db/reference.go
···11 "tangled.org/core/orm"
12)
1314-// ValidateReferenceLinks resolves refLinks to Issue/PR/IssueComment/PullComment ATURIs.
15// It will ignore missing refLinks.
16func 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, ","),
···7980 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)
1314+// ValidateReferenceLinks resolves refLinks to Issue/PR/Comment ATURIs.
15// It will ignore missing refLinks.
16func ValidateReferenceLinks(e Execer, refLinks []models.ReferenceLink) ([]syntax.ATURI, error) {
17 var (
···53 values %s
54 )
55 select
56+ i.at_uri, c.at_uri
057 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, ","),
···7879 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)
0000089 } else {
90+ uri = syntax.ATURI(issueUri)
0000091 }
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
···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}
870871-func (i *Ingester) ingestIssueComment(e *jmodels.Event) error {
872 did := e.Did
873 rkey := e.Commit.RKey
874875 var err error
876877- l := i.Logger.With("handler", "ingestIssueComment", "nsid", e.Commit.Collection, "did", did, "rkey", rkey)
878 l.Info("ingesting record")
879880 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 }
893894- comment, err := models.IssueCommentFromRecord(did, rkey, record)
895 if err != nil {
896 return fmt.Errorf("failed to parse comment from record: %w", err)
897 }
898899- if err := i.Validator.ValidateIssueComment(comment); err != nil {
00000000900 return fmt.Errorf("failed to validate comment: %w", err)
901 }
902···906 }
907 defer tx.Rollback()
908909- _, err = db.AddIssueComment(tx, *comment)
910 if err != nil {
911- return fmt.Errorf("failed to create issue comment: %w", err)
912 }
913914 return tx.Commit()
915916 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 }
924925 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}
870871+func (i *Ingester) ingestComment(e *jmodels.Event) error {
872 did := e.Did
873 rkey := e.Commit.RKey
874875 var err error
876877+ l := i.Logger.With("handler", "ingestComment", "nsid", e.Commit.Collection, "did", did, "rkey", rkey)
878 l.Info("ingesting record")
879880 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 }
893894+ comment, err := models.CommentFromRecord(did, rkey, record)
895 if err != nil {
896 return fmt.Errorf("failed to parse comment from record: %w", err)
897 }
898899+ // 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()
916917+ err = db.PutComment(tx, comment)
918 if err != nil {
919+ return fmt.Errorf("failed to create comment: %w", err)
920 }
921922 return tx.Commit()
923924 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 }
932933 return nil
+30-28
appview/issues/issues.go
···403404 body := r.FormValue("body")
405 if body == "" {
406- rp.pages.Notice(w, "issue", "Body is required")
407 return
408 }
409410- replyToUri := r.FormValue("reply-to")
411- var replyTo *string
412- if replyToUri != "" {
413- replyTo = &replyToUri
00000414 }
415416 mentions, references := rp.mentionsResolver.Resolve(r.Context(), body)
417418- 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
···441442 // 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()
470471- 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 = ""
486487- // notify about the new comment
488- comment.Id = commentId
489-490 rp.notifier.NewIssueComment(r.Context(), &comment, mentions)
491492 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
493- rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", ownerSlashRepo, issue.IssueId, commentId))
494}
495496func (rp *Issues) IssueComment(w http.ResponseWriter, r *http.Request) {
···505 }
506507 commentId := chi.URLParam(r, "commentId")
508- comments, err := db.GetIssueComments(
509 rp.db,
510 orm.FilterEq("id", commentId),
511 )
···541 }
542543 commentId := chi.URLParam(r, "commentId")
544- comments, err := db.GetIssueComments(
545 rp.db,
546 orm.FilterEq("id", commentId),
547 )
···557 }
558 comment := comments[0]
559560- 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()
599600- _, 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 }
617618 _, 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 }
652653 commentId := chi.URLParam(r, "commentId")
654- comments, err := db.GetIssueComments(
655 rp.db,
656 orm.FilterEq("id", commentId),
657 )
···687 }
688689 commentId := chi.URLParam(r, "commentId")
690- comments, err := db.GetIssueComments(
691 rp.db,
692 orm.FilterEq("id", commentId),
693 )
···723 }
724725 commentId := chi.URLParam(r, "commentId")
726- comments, err := db.GetIssueComments(
727 rp.db,
728 orm.FilterEq("id", commentId),
729 )
···739 }
740 comment := comments[0]
741742- 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
···752753 // 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 })
···403404 body := r.FormValue("body")
405 if body == "" {
406+ rp.pages.Notice(w, "issue-comment", "Body is required")
407 return
408 }
409410+ 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 }
420421 mentions, references := rp.mentionsResolver.Resolve(r.Context(), body)
422423+ 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
···446447 // 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()
475476+ 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 = ""
491000492 rp.notifier.NewIssueComment(r.Context(), &comment, mentions)
493494 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
495+ rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", ownerSlashRepo, issue.IssueId, comment.Id))
496}
497498func (rp *Issues) IssueComment(w http.ResponseWriter, r *http.Request) {
···507 }
508509 commentId := chi.URLParam(r, "commentId")
510+ comments, err := db.GetComments(
511 rp.db,
512 orm.FilterEq("id", commentId),
513 )
···543 }
544545 commentId := chi.URLParam(r, "commentId")
546+ comments, err := db.GetComments(
547 rp.db,
548 orm.FilterEq("id", commentId),
549 )
···559 }
560 comment := comments[0]
561562+ 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()
601602+ 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 }
619620 _, 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 }
654655 commentId := chi.URLParam(r, "commentId")
656+ comments, err := db.GetComments(
657 rp.db,
658 orm.FilterEq("id", commentId),
659 )
···689 }
690691 commentId := chi.URLParam(r, "commentId")
692+ comments, err := db.GetComments(
693 rp.db,
694 orm.FilterEq("id", commentId),
695 )
···725 }
726727 commentId := chi.URLParam(r, "commentId")
728+ comments, err := db.GetComments(
729 rp.db,
730 orm.FilterEq("id", commentId),
731 )
···741 }
742 comment := comments[0]
743744+ 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
···754755 // 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
···2627 // 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}
6364type CommentListItem struct {
65- Self *IssueComment
66- Replies []*IssueComment
67}
6869func (it *CommentListItem) Participants() []syntax.DID {
···8889func (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
9394 // 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 }
116117 // 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)
145146 for _, c := range i.Comments {
147- addParticipant(c.Did)
148 }
149150 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-}
···2627 // 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}
6364type CommentListItem struct {
65+ Self *Comment
66+ Replies []*Comment
67}
6869func (it *CommentListItem) Participants() []syntax.DID {
···8889func (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
9394 // 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 }
116117 // 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)
145146 for _, c := range i.Comments {
147+ addParticipant(c.Did.String())
148 }
149150 return participants
···171 Open: true, // new issues are open by default
172 }
173}
000000000000000000000000000000000000000000000000000000000000000000000000000000000
+4-4
appview/notify/db/db.go
···122 )
123}
124125-func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) {
126- issues, err := db.GetIssues(n.db, orm.FilterEq("at_uri", comment.IssueAt))
127 if err != nil {
128 log.Printf("NewIssueComment: failed to get issues: %v", err)
129 return
130 }
131 if len(issues) == 0 {
132- log.Printf("NewIssueComment: no issue found for %s", comment.IssueAt)
133 return
134 }
135 issue := issues[0]
···147148 // find the parent thread, and add all DIDs from here to the recipient list
149 for _, t := range issue.CommentList() {
150- if t.Self.AtUri().String() == parentAtUri {
151 for _, p := range t.Participants() {
152 recipients.Insert(p)
153 }
···122 )
123}
124125+func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID) {
126+ issues, err := db.GetIssues(n.db, orm.FilterEq("at_uri", comment.Subject))
127 if err != nil {
128 log.Printf("NewIssueComment: failed to get issues: %v", err)
129 return
130 }
131 if len(issues) == 0 {
132+ log.Printf("NewIssueComment: no issue found for %s", comment.Subject)
133 return
134 }
135 issue := issues[0]
···147148 // find the parent thread, and add all DIDs from here to the recipient list
149 for _, t := range issue.CommentList() {
150+ if t.Self.AtUri() == parentAtUri {
151 for _, p := range t.Participants() {
152 recipients.Insert(p)
153 }