diff --git a/appview/db/comments.go b/appview/db/comments.go index 9e1ba2ac..07053748 100644 --- a/appview/db/comments.go +++ b/appview/db/comments.go @@ -4,6 +4,7 @@ import ( "database/sql" "encoding/json" "fmt" + "log" "sort" "strings" "time" @@ -95,11 +96,14 @@ func PutComment(tx *sql.Tx, c *models.Comment, references []syntax.ATURI) error return err } - if affected > 0 { - // update references when comment is updated - if err := putReferences(tx, c.AtUri(), references); err != nil { - return fmt.Errorf("put reference_links: %w", err) - } + if affected < 1 { + log.Println("record is already stored. skipping operation") + return nil + } + + // update references when comment is updated + if err := putReferences(tx, c.AtUri(), references); err != nil { + return fmt.Errorf("put reference_links: %w", err) } return nil diff --git a/appview/db/issues.go b/appview/db/issues.go index cd909ea3..23f1ce80 100644 --- a/appview/db/issues.go +++ b/appview/db/issues.go @@ -100,7 +100,7 @@ func updateIssue(tx *sql.Tx, issue *models.Issue) error { } func GetIssuesPaginated(e Execer, page pagination.Page, filters ...orm.Filter) ([]models.Issue, error) { - issueMap := make(map[string]*models.Issue) // at-uri -> issue + issueMap := make(map[syntax.ATURI]*models.Issue) // at-uri -> issue var conditions []string var args []any @@ -196,8 +196,7 @@ func GetIssuesPaginated(e Execer, page pagination.Page, filters ...orm.Filter) ( } } - atUri := issue.AtUri().String() - issueMap[atUri] = &issue + issueMap[issue.AtUri()] = &issue } // collect reverse repos @@ -229,12 +228,12 @@ func GetIssuesPaginated(e Execer, page pagination.Page, filters ...orm.Filter) ( // collect comments issueAts := slices.Collect(maps.Keys(issueMap)) - comments, err := GetIssueComments(e, orm.FilterIn("issue_at", issueAts)) + comments, err := GetComments(e, orm.FilterIn("subject_uri", issueAts)) if err != nil { return nil, fmt.Errorf("failed to query comments: %w", err) } for i := range comments { - issueAt := comments[i].IssueAt + issueAt := syntax.ATURI(comments[i].Subject.Uri) if issue, ok := issueMap[issueAt]; ok { issue.Comments = append(issue.Comments, comments[i]) } @@ -246,7 +245,7 @@ func GetIssuesPaginated(e Execer, page pagination.Page, filters ...orm.Filter) ( return nil, fmt.Errorf("failed to query labels: %w", err) } for issueAt, labels := range allLabels { - if issue, ok := issueMap[issueAt.String()]; ok { + if issue, ok := issueMap[issueAt]; ok { issue.Labels = labels } } @@ -257,7 +256,7 @@ func GetIssuesPaginated(e Execer, page pagination.Page, filters ...orm.Filter) ( return nil, fmt.Errorf("failed to query reference_links: %w", err) } for issueAt, references := range allReferences { - if issue, ok := issueMap[issueAt.String()]; ok { + if issue, ok := issueMap[issueAt]; ok { issue.References = references } } @@ -295,185 +294,6 @@ func GetIssues(e Execer, filters ...orm.Filter) ([]models.Issue, error) { return GetIssuesPaginated(e, pagination.Page{}, filters...) } -func AddIssueComment(tx *sql.Tx, c models.IssueComment) (int64, error) { - result, err := tx.Exec( - `insert into issue_comments ( - did, - rkey, - issue_at, - body, - reply_to, - created, - edited - ) - values (?, ?, ?, ?, ?, ?, null) - on conflict(did, rkey) do update set - issue_at = excluded.issue_at, - body = excluded.body, - edited = case - when - issue_comments.issue_at != excluded.issue_at - or issue_comments.body != excluded.body - or issue_comments.reply_to != excluded.reply_to - then ? - else issue_comments.edited - end`, - c.Did, - c.Rkey, - c.IssueAt, - c.Body, - c.ReplyTo, - c.Created.Format(time.RFC3339), - time.Now().Format(time.RFC3339), - ) - if err != nil { - return 0, err - } - - id, err := result.LastInsertId() - if err != nil { - return 0, err - } - - if err := putReferences(tx, c.AtUri(), c.References); err != nil { - return 0, fmt.Errorf("put reference_links: %w", err) - } - - return id, nil -} - -func DeleteIssueComments(e Execer, filters ...orm.Filter) error { - var conditions []string - var args []any - for _, filter := range filters { - conditions = append(conditions, filter.Condition()) - args = append(args, filter.Arg()...) - } - - whereClause := "" - if conditions != nil { - whereClause = " where " + strings.Join(conditions, " and ") - } - - query := fmt.Sprintf(`update issue_comments set body = "", deleted = strftime('%%Y-%%m-%%dT%%H:%%M:%%SZ', 'now') %s`, whereClause) - - _, err := e.Exec(query, args...) - return err -} - -func GetIssueComments(e Execer, filters ...orm.Filter) ([]models.IssueComment, error) { - commentMap := make(map[string]*models.IssueComment) - - var conditions []string - var args []any - for _, filter := range filters { - conditions = append(conditions, filter.Condition()) - args = append(args, filter.Arg()...) - } - - whereClause := "" - if conditions != nil { - whereClause = " where " + strings.Join(conditions, " and ") - } - - query := fmt.Sprintf(` - select - id, - did, - rkey, - issue_at, - reply_to, - body, - created, - edited, - deleted - from - issue_comments - %s - `, whereClause) - - rows, err := e.Query(query, args...) - if err != nil { - return nil, err - } - defer rows.Close() - - for rows.Next() { - var comment models.IssueComment - var created string - var rkey, edited, deleted, replyTo sql.Null[string] - err := rows.Scan( - &comment.Id, - &comment.Did, - &rkey, - &comment.IssueAt, - &replyTo, - &comment.Body, - &created, - &edited, - &deleted, - ) - if err != nil { - return nil, err - } - - // this is a remnant from old times, newer comments always have rkey - if rkey.Valid { - comment.Rkey = rkey.V - } - - if t, err := time.Parse(time.RFC3339, created); err == nil { - comment.Created = t - } - - if edited.Valid { - if t, err := time.Parse(time.RFC3339, edited.V); err == nil { - comment.Edited = &t - } - } - - if deleted.Valid { - if t, err := time.Parse(time.RFC3339, deleted.V); err == nil { - comment.Deleted = &t - } - } - - if replyTo.Valid { - comment.ReplyTo = &replyTo.V - } - - atUri := comment.AtUri().String() - commentMap[atUri] = &comment - } - - if err = rows.Err(); err != nil { - return nil, err - } - - // collect references for each comments - commentAts := slices.Collect(maps.Keys(commentMap)) - allReferences, err := GetReferencesAll(e, orm.FilterIn("from_at", commentAts)) - if err != nil { - return nil, fmt.Errorf("failed to query reference_links: %w", err) - } - for commentAt, references := range allReferences { - if comment, ok := commentMap[commentAt.String()]; ok { - comment.References = references - } - } - - var comments []models.IssueComment - for _, c := range commentMap { - comments = append(comments, *c) - } - - sort.Slice(comments, func(i, j int) bool { - return comments[i].Created.After(comments[j].Created) - }) - - return comments, nil -} - func DeleteIssues(tx *sql.Tx, did, rkey string) error { _, err := tx.Exec( `delete from issues diff --git a/appview/db/reference.go b/appview/db/reference.go index 1628b691..2281eb6e 100644 --- a/appview/db/reference.go +++ b/appview/db/reference.go @@ -11,7 +11,7 @@ import ( "tangled.org/core/orm" ) -// ValidateReferenceLinks resolves refLinks to Issue/PR/IssueComment/PullComment ATURIs. +// ValidateReferenceLinks resolves refLinks to Issue/PR/Comment ATURIs. // It will ignore missing refLinks. func ValidateReferenceLinks(e Execer, refLinks []models.ReferenceLink) ([]syntax.ATURI, error) { var ( @@ -53,8 +53,7 @@ func findIssueReferences(e Execer, refLinks []models.ReferenceLink) ([]syntax.AT values %s ) select - i.did, i.rkey, - c.did, c.rkey + i.at_uri, c.at_uri from input inp join repos r on r.did = inp.owner_did @@ -62,9 +61,9 @@ func findIssueReferences(e Execer, refLinks []models.ReferenceLink) ([]syntax.AT join issues i on i.repo_did = r.repo_did and i.issue_id = inp.issue_id - left join issue_comments c + left join comments c on inp.comment_id is not null - and c.issue_at = i.at_uri + and c.subject_uri = i.at_uri and c.id = inp.comment_id `, strings.Join(vals, ","), @@ -79,26 +78,16 @@ func findIssueReferences(e Execer, refLinks []models.ReferenceLink) ([]syntax.AT for rows.Next() { // Scan rows - var issueOwner, issueRkey string - var commentOwner, commentRkey sql.NullString + var issueUri string + var commentUri sql.NullString var uri syntax.ATURI - if err := rows.Scan(&issueOwner, &issueRkey, &commentOwner, &commentRkey); err != nil { + if err := rows.Scan(&issueUri, &commentUri); err != nil { return nil, err } - if commentOwner.Valid && commentRkey.Valid { - uri = syntax.ATURI(fmt.Sprintf( - "at://%s/%s/%s", - commentOwner.String, - tangled.RepoIssueCommentNSID, - commentRkey.String, - )) + if commentUri.Valid { + uri = syntax.ATURI(commentUri.String) } else { - uri = syntax.ATURI(fmt.Sprintf( - "at://%s/%s/%s", - issueOwner, - tangled.RepoIssueNSID, - issueRkey, - )) + uri = syntax.ATURI(issueUri) } uris = append(uris, uri) } @@ -282,7 +271,7 @@ func GetBacklinks(e Execer, target syntax.ATURI) ([]models.RichReferenceLink, er return nil, fmt.Errorf("get issue backlinks: %w", err) } backlinks = append(backlinks, ls...) - ls, err = getIssueCommentBacklinks(e, target, backlinksMap[tangled.RepoIssueCommentNSID]) + ls, err = getIssueCommentBacklinks(e, target, backlinksMap[tangled.FeedCommentNSID]) if err != nil { return nil, fmt.Errorf("get issue_comment backlinks: %w", err) } @@ -352,9 +341,9 @@ func getIssueCommentBacklinks(e Execer, target syntax.ATURI, aturis []syntax.ATU rows, err := e.Query( fmt.Sprintf( `select r.did, r.name, i.issue_id, c.id, i.title, i.open - from issue_comments c + from comments c join issues i - on i.at_uri = c.issue_at + on i.at_uri = c.subject_uri join repos r on r.repo_did = i.repo_did where %s and %s`, diff --git a/appview/ingester.go b/appview/ingester.go index fa457166..991a26d5 100644 --- a/appview/ingester.go +++ b/appview/ingester.go @@ -1545,56 +1545,24 @@ func (i *Ingester) ingestPull(ctx context.Context, e *jmodels.Event) error { return nil } +// ingestIssueComment ingests legacy sh.tangled.repo.issue.comment deletions func (i *Ingester) ingestIssueComment(e *jmodels.Event) error { - did := e.Did - rkey := e.Commit.RKey - - var err error - - l := i.Logger.With("handler", "ingestIssueComment", "nsid", e.Commit.Collection, "did", did, "rkey", rkey) + l := i.Logger.With("handler", "ingestIssueComment", "nsid", e.Commit.Collection, "did", e.Did, "rkey", e.Commit.RKey) l.Info("ingesting record") switch e.Commit.Operation { case jmodels.CommitOperationCreate, jmodels.CommitOperationUpdate: - raw := json.RawMessage(e.Commit.Record) - record := tangled.RepoIssueComment{} - err = json.Unmarshal(raw, &record) - if err != nil { - return fmt.Errorf("invalid record: %w", err) - } - - comment, err := models.IssueCommentFromRecord(did, rkey, record) - if err != nil { - return fmt.Errorf("failed to parse comment from record: %w", err) - } - - if err := i.Validator.ValidateIssueComment(comment); err != nil { - return fmt.Errorf("failed to validate comment: %w", err) - } - - tx, err := i.Db.Begin() - if err != nil { - return fmt.Errorf("failed to start transaction: %w", err) - } - defer tx.Rollback() - - _, err = db.AddIssueComment(tx, *comment) - if err != nil { - return fmt.Errorf("failed to create issue comment: %w", err) - } - - return tx.Commit() + // no-op. sh.tangled.repo.issue.comment is deprecated case jmodels.CommitOperationDelete: - if err := db.DeleteIssueComments( + if err := db.PurgeComments( i.Db, - orm.FilterEq("did", did), - orm.FilterEq("rkey", rkey), + orm.FilterEq("did", e.Did), + orm.FilterEq("collection", e.Commit.Collection), + orm.FilterEq("rkey", e.Commit.RKey), ); err != nil { - return fmt.Errorf("failed to delete issue comment record: %w", err) + return fmt.Errorf("failed to delete comment record: %w", err) } - - return nil } return nil diff --git a/appview/issues/issues.go b/appview/issues/issues.go index 3c167893..320d23f3 100644 --- a/appview/issues/issues.go +++ b/appview/issues/issues.go @@ -14,6 +14,7 @@ import ( "github.com/bluesky-social/indigo/atproto/atclient" "github.com/bluesky-social/indigo/atproto/syntax" lexutil "github.com/bluesky-social/indigo/lex/util" + indigoxrpc "github.com/bluesky-social/indigo/xrpc" "github.com/go-chi/chi/v5" "tangled.org/core/api/tangled" @@ -420,34 +421,91 @@ func (rp *Issues) NewIssueComment(w http.ResponseWriter, r *http.Request) { body := r.FormValue("body") if body == "" { - rp.pages.Notice(w, "issue", "Body is required") + rp.pages.Notice(w, "issue-comment", "Body is required") return } - replyToUri := r.FormValue("reply-to") - var replyTo *string - if replyToUri != "" { - replyTo = &replyToUri + // TODO(boltless): normalize markdown body + normalizedBody := body + _, references := rp.mentionsResolver.Resolve(r.Context(), body) + + markdownBody := tangled.MarkupMarkdown{ + Text: normalizedBody, + Original: &body, + Blobs: nil, + } + + // ingest CID of issue record on-demand. + // TODO(boltless): appview should ingest CID of atproto records + cid, err := func() (syntax.CID, error) { + ident, err := rp.idResolver.ResolveIdent(r.Context(), issue.Did) + if err != nil { + return "", err + } + + xrpcc := indigoxrpc.Client{Host: ident.PDSEndpoint()} + out, err := comatproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoIssueNSID, issue.Did, issue.Rkey) + if err != nil { + return "", err + } + if out.Cid == nil { + return "", fmt.Errorf("record CID is empty") + } + + cid, err := syntax.ParseCID(*out.Cid) + if err != nil { + return "", err + } + + return cid, nil + }() + if err != nil { + rp.logger.Error("failed to backfill subject PR record", "err", err) + rp.pages.Notice(w, "issue-comment", "failed to backfill subject record") + return + } + issueStrongRef := comatproto.RepoStrongRef{ + Uri: issue.AtUri().String(), + Cid: cid.String(), + } + + var replyTo *comatproto.RepoStrongRef + replyToUriRaw := r.FormValue("reply-to-uri") + replyToCidRaw := r.FormValue("reply-to-cid") + if replyToUriRaw != "" && replyToCidRaw != "" { + uri, err := syntax.ParseATURI(replyToUriRaw) + if err != nil { + rp.pages.Notice(w, "issue-comment", "reply-to-uri should be valid AT-URI") + return + } + cid, err := syntax.ParseCID(replyToCidRaw) + if err != nil { + rp.pages.Notice(w, "issue-comment", "reply-to-cid should be valid CID") + return + } + replyTo = &comatproto.RepoStrongRef{ + Uri: uri.String(), + Cid: cid.String(), + } } mentions, references := rp.mentionsResolver.Resolve(r.Context(), body) - comment := models.IssueComment{ - Did: user.Did, - Rkey: tid.TID(), - IssueAt: issue.AtUri().String(), - ReplyTo: replyTo, - Body: body, - Created: time.Now(), - Mentions: mentions, - References: references, - } - if err = rp.validator.ValidateIssueComment(&comment); err != nil { + comment := models.Comment{ + Did: syntax.DID(user.Did), + Collection: tangled.FeedCommentNSID, + Rkey: syntax.RecordKey(tid.TID()), + + Subject: issueStrongRef, + Body: markdownBody, + Created: time.Now(), + ReplyTo: replyTo, + } + if err = comment.Validate(); err != nil { l.Error("failed to validate comment", "err", err) rp.pages.Notice(w, "issue-comment", "Failed to create comment.") return } - record := comment.AsRecord() client, err := rp.oauth.AuthorizedClient(r) if err != nil { @@ -457,25 +515,19 @@ func (rp *Issues) NewIssueComment(w http.ResponseWriter, r *http.Request) { } // create a record first - resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ - Collection: tangled.RepoIssueCommentNSID, - Repo: comment.Did, - Rkey: comment.Rkey, - Record: &lexutil.LexiconTypeDecoder{ - Val: &record, - }, + out, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ + Collection: comment.Collection.String(), + Repo: comment.Did.String(), + Rkey: comment.Rkey.String(), + Record: &lexutil.LexiconTypeDecoder{Val: comment.AsRecord()}, }) if err != nil { l.Error("failed to create comment", "err", err) rp.pages.Notice(w, "issue-comment", "Failed to create comment.") return } - atUri := resp.Uri - defer func() { - if err := rollbackRecord(context.Background(), atUri, client); err != nil { - l.Error("rollback failed", "err", err) - } - }() + + comment.Cid = syntax.CID(out.Cid) tx, err := rp.db.Begin() if err != nil { @@ -485,12 +537,13 @@ func (rp *Issues) NewIssueComment(w http.ResponseWriter, r *http.Request) { } defer tx.Rollback() - commentId, err := db.AddIssueComment(tx, comment) + err = db.PutComment(tx, &comment, references) if err != nil { l.Error("failed to create comment", "err", err) rp.pages.Notice(w, "issue-comment", "Failed to create comment.") return } + err = tx.Commit() if err != nil { l.Error("failed to commit transaction", "err", err) @@ -498,16 +551,10 @@ func (rp *Issues) NewIssueComment(w http.ResponseWriter, r *http.Request) { return } - // reset atUri to make rollback a no-op - atUri = "" - - // notify about the new comment - comment.Id = commentId - rp.notifier.NewIssueComment(r.Context(), &comment, mentions) ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) - rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", ownerSlashRepo, issue.IssueId, commentId)) + rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", ownerSlashRepo, issue.IssueId, comment.Id)) } func (rp *Issues) IssueComment(w http.ResponseWriter, r *http.Request) { @@ -522,7 +569,7 @@ func (rp *Issues) IssueComment(w http.ResponseWriter, r *http.Request) { } commentId := chi.URLParam(r, "commentId") - comments, err := db.GetIssueComments( + comments, err := db.GetComments( rp.db, orm.FilterEq("id", commentId), ) @@ -558,7 +605,7 @@ func (rp *Issues) EditIssueComment(w http.ResponseWriter, r *http.Request) { } commentId := chi.URLParam(r, "commentId") - comments, err := db.GetIssueComments( + comments, err := db.GetComments( rp.db, orm.FilterEq("id", commentId), ) @@ -574,7 +621,7 @@ func (rp *Issues) EditIssueComment(w http.ResponseWriter, r *http.Request) { } comment := comments[0] - if comment.Did != user.Did { + if comment.Did.String() != user.Did { l.Error("unauthorized comment edit", "expectedDid", comment.Did, "gotDid", user.Did) http.Error(w, "you are not the author of this comment", http.StatusUnauthorized) return @@ -590,7 +637,25 @@ func (rp *Issues) EditIssueComment(w http.ResponseWriter, r *http.Request) { }) case http.MethodPost: // extract form value - newBody := r.FormValue("body") + body := r.FormValue("body") + if body == "" { + rp.pages.Notice(w, "issue-comment", "Body is required") + return + } + + // TODO(boltless): normalize markdown body + normalizedBody := body + _, references := rp.mentionsResolver.Resolve(r.Context(), body) + + now := time.Now() + newComment := comment + newComment.Body = tangled.MarkupMarkdown{ + Text: normalizedBody, + Original: &body, + Blobs: nil, + } + newComment.Edited = &now + client, err := rp.oauth.AuthorizedClient(r) if err != nil { l.Error("failed to get authorized client", "err", err) @@ -598,13 +663,24 @@ func (rp *Issues) EditIssueComment(w http.ResponseWriter, r *http.Request) { return } - now := time.Now() - newComment := comment - newComment.Body = newBody - newComment.Edited = &now - newComment.Mentions, newComment.References = rp.mentionsResolver.Resolve(r.Context(), newBody) + // update a record first + exCid := comment.Cid.String() + resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ + Collection: newComment.Collection.String(), + Repo: newComment.Did.String(), + Rkey: newComment.Rkey.String(), + SwapRecord: &exCid, + Record: &lexutil.LexiconTypeDecoder{ + Val: newComment.AsRecord(), + }, + }) + if err != nil { + l.Error("failed to update comment", "err", err) + rp.pages.Notice(w, "issue-comment", "Failed to update comment, try again later.") + return + } - record := newComment.AsRecord() + newComment.Cid = syntax.CID(resp.Cid) tx, err := rp.db.Begin() if err != nil { @@ -614,36 +690,17 @@ func (rp *Issues) EditIssueComment(w http.ResponseWriter, r *http.Request) { } defer tx.Rollback() - _, err = db.AddIssueComment(tx, newComment) + err = db.PutComment(tx, &newComment, references) if err != nil { l.Error("failed to perform update-description query", "err", err) rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.") return } - tx.Commit() - - // rkey is optional, it was introduced later - if newComment.Rkey != "" { - // update the record on pds - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueCommentNSID, user.Did, comment.Rkey) - if err != nil { - l.Error("failed to get record", "err", err, "did", newComment.Did, "rkey", newComment.Rkey) - rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.") - return - } - - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ - Collection: tangled.RepoIssueCommentNSID, - Repo: user.Did, - Rkey: newComment.Rkey, - SwapRecord: ex.Cid, - Record: &lexutil.LexiconTypeDecoder{ - Val: &record, - }, - }) - if err != nil { - l.Error("failed to update record on PDS", "err", err) - } + err = tx.Commit() + if err != nil { + l.Error("failed to commit transaction", "err", err) + rp.pages.Notice(w, "issue-comment", "Failed to update comment, try again later.") + return } // return new comment body with htmx @@ -668,7 +725,7 @@ func (rp *Issues) ReplyIssueCommentPlaceholder(w http.ResponseWriter, r *http.Re } commentId := chi.URLParam(r, "commentId") - comments, err := db.GetIssueComments( + comments, err := db.GetComments( rp.db, orm.FilterEq("id", commentId), ) @@ -704,7 +761,7 @@ func (rp *Issues) ReplyIssueComment(w http.ResponseWriter, r *http.Request) { } commentId := chi.URLParam(r, "commentId") - comments, err := db.GetIssueComments( + comments, err := db.GetComments( rp.db, orm.FilterEq("id", commentId), ) @@ -740,7 +797,7 @@ func (rp *Issues) DeleteIssueComment(w http.ResponseWriter, r *http.Request) { } commentId := chi.URLParam(r, "commentId") - comments, err := db.GetIssueComments( + comments, err := db.GetComments( rp.db, orm.FilterEq("id", commentId), ) @@ -756,7 +813,7 @@ func (rp *Issues) DeleteIssueComment(w http.ResponseWriter, r *http.Request) { } comment := comments[0] - if comment.Did != user.Did { + if comment.Did.String() != user.Did { l.Error("unauthorized action", "expectedDid", comment.Did, "gotDid", user.Did) http.Error(w, "you are not the author of this comment", http.StatusUnauthorized) return @@ -769,7 +826,7 @@ func (rp *Issues) DeleteIssueComment(w http.ResponseWriter, r *http.Request) { // optimistic deletion deleted := time.Now() - err = db.DeleteIssueComments(rp.db, orm.FilterEq("id", comment.Id)) + err = db.DeleteComments(rp.db, orm.FilterEq("id", comment.Id)) if err != nil { l.Error("failed to delete comment", "err", err) rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment") @@ -785,9 +842,9 @@ func (rp *Issues) DeleteIssueComment(w http.ResponseWriter, r *http.Request) { return } _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ - Collection: tangled.RepoIssueCommentNSID, - Repo: user.Did, - Rkey: comment.Rkey, + Collection: comment.Collection.String(), + Repo: comment.Did.String(), + Rkey: comment.Rkey.String(), }) if err != nil { l.Error("failed to delete from PDS", "err", err) @@ -795,7 +852,7 @@ func (rp *Issues) DeleteIssueComment(w http.ResponseWriter, r *http.Request) { } // optimistic update for htmx - comment.Body = "" + comment.Body = tangled.MarkupMarkdown{} comment.Deleted = &deleted // htmx fragment of comment after deletion diff --git a/appview/models/comment.go b/appview/models/comment.go index 61ae01c7..60db6315 100644 --- a/appview/models/comment.go +++ b/appview/models/comment.go @@ -61,6 +61,17 @@ func (c *Comment) AsRecord() typegen.CBORMarshaler { } } +func (c *Comment) EditableBody() string { + if c.Body.Original != nil { + return *c.Body.Original + } + return c.Body.Text +} + +func (c *Comment) IsLegacy() bool { + return c.Collection != tangled.FeedCommentNSID +} + func (c *Comment) IsTopLevel() bool { return c.ReplyTo == nil } diff --git a/appview/models/issue.go b/appview/models/issue.go index 5d0c74cc..da630a38 100644 --- a/appview/models/issue.go +++ b/appview/models/issue.go @@ -26,7 +26,7 @@ type Issue struct { // optionally, populate this when querying for reverse mappings // like comment counts, parent repo etc. - Comments []IssueComment + Comments []Comment Labels LabelState Repo *Repo } @@ -63,8 +63,8 @@ func (i *Issue) State() string { } type CommentListItem struct { - Self *IssueComment - Replies []*IssueComment + Self *Comment + Replies []*Comment } func (it *CommentListItem) Participants() []syntax.DID { @@ -89,13 +89,13 @@ func (it *CommentListItem) Participants() []syntax.DID { func (i *Issue) CommentList() []CommentListItem { // Create a map to quickly find comments by their aturi - toplevel := make(map[string]*CommentListItem) - var replies []*IssueComment + toplevel := make(map[syntax.ATURI]*CommentListItem) + var replies []*Comment // collect top level comments into the map for _, comment := range i.Comments { if comment.IsTopLevel() { - toplevel[comment.AtUri().String()] = &CommentListItem{ + toplevel[comment.AtUri()] = &CommentListItem{ Self: &comment, } } else { @@ -104,8 +104,10 @@ func (i *Issue) CommentList() []CommentListItem { } for _, r := range replies { - parentAt := *r.ReplyTo - if parent, exists := toplevel[parentAt]; exists { + if r.ReplyTo == nil { + continue + } + if parent, exists := toplevel[syntax.ATURI(r.ReplyTo.Uri)]; exists { parent.Replies = append(parent.Replies, r) } } @@ -116,7 +118,7 @@ func (i *Issue) CommentList() []CommentListItem { } // sort everything - sortFunc := func(a, b *IssueComment) bool { + sortFunc := func(a, b *Comment) bool { return a.Created.Before(b.Created) } sort.Slice(listing, func(i, j int) bool { @@ -145,7 +147,7 @@ func (i *Issue) Participants() []syntax.DID { addParticipant(syntax.DID(i.Did)) for _, c := range i.Comments { - addParticipant(syntax.DID(c.Did)) + addParticipant(c.Did) } return participants @@ -172,84 +174,3 @@ func IssueFromRecord(did, rkey string, record tangled.RepoIssue) Issue { Open: true, // new issues are open by default } } - -type IssueComment struct { - Id int64 - Did string - Rkey string - IssueAt string - ReplyTo *string - Body string - Created time.Time - Edited *time.Time - Deleted *time.Time - Mentions []syntax.DID - References []syntax.ATURI -} - -func (i *IssueComment) AtUri() syntax.ATURI { - return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", i.Did, tangled.RepoIssueCommentNSID, i.Rkey)) -} - -func (i *IssueComment) AsRecord() tangled.RepoIssueComment { - mentions := make([]string, len(i.Mentions)) - for i, did := range i.Mentions { - mentions[i] = string(did) - } - references := make([]string, len(i.References)) - for i, uri := range i.References { - references[i] = string(uri) - } - return tangled.RepoIssueComment{ - Body: i.Body, - Issue: i.IssueAt, - CreatedAt: i.Created.Format(time.RFC3339), - ReplyTo: i.ReplyTo, - Mentions: mentions, - References: references, - } -} - -func (i *IssueComment) IsTopLevel() bool { - return i.ReplyTo == nil -} - -func (i *IssueComment) IsReply() bool { - return i.ReplyTo != nil -} - -func IssueCommentFromRecord(did, rkey string, record tangled.RepoIssueComment) (*IssueComment, error) { - created, err := time.Parse(time.RFC3339, record.CreatedAt) - if err != nil { - created = time.Now() - } - - ownerDid := did - - if _, err = syntax.ParseATURI(record.Issue); err != nil { - return nil, err - } - - i := record - mentions := make([]syntax.DID, len(record.Mentions)) - for i, did := range record.Mentions { - mentions[i] = syntax.DID(did) - } - references := make([]syntax.ATURI, len(record.References)) - for i, uri := range i.References { - references[i] = syntax.ATURI(uri) - } - - comment := IssueComment{ - Did: ownerDid, - Rkey: rkey, - Body: record.Body, - IssueAt: record.Issue, - ReplyTo: record.ReplyTo, - Created: created, - Mentions: mentions, - References: references, - } - - return &comment, nil -} diff --git a/appview/notify/db/db.go b/appview/notify/db/db.go index 871be8e8..93e09291 100644 --- a/appview/notify/db/db.go +++ b/appview/notify/db/db.go @@ -133,16 +133,16 @@ func (n *databaseNotifier) NewIssue(ctx context.Context, issue *models.Issue, me ) } -func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) { +func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID) { l := log.FromContext(ctx) - issues, err := db.GetIssues(n.db, orm.FilterEq("at_uri", comment.IssueAt)) + issues, err := db.GetIssues(n.db, orm.FilterEq("at_uri", comment.Subject)) if err != nil { l.Error("failed to get issues", "err", err) return } if len(issues) == 0 { - l.Error("no issue found for", "err", comment.IssueAt) + l.Error("no issue found for", "err", comment.Subject) return } issue := issues[0] @@ -156,11 +156,11 @@ func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models. if comment.IsReply() { // if this comment is a reply, then notify everybody in that thread - parentAtUri := *comment.ReplyTo + parent := *comment.ReplyTo // find the parent thread, and add all DIDs from here to the recipient list for _, t := range issue.CommentList() { - if t.Self.AtUri().String() == parentAtUri { + if t.Self.AtUri() == syntax.ATURI(parent.Uri) { for _, p := range t.Participants() { recipients.Insert(p) } diff --git a/appview/notify/logging/notifier.go b/appview/notify/logging/notifier.go index 9335c833..985fd7ca 100644 --- a/appview/notify/logging/notifier.go +++ b/appview/notify/logging/notifier.go @@ -51,7 +51,7 @@ func (l *loggingNotifier) NewIssue(ctx context.Context, issue *models.Issue, men l.inner.NewIssue(ctx, issue, mentions) } -func (l *loggingNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) { +func (l *loggingNotifier) NewIssueComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID) { ctx = tlog.IntoContext(ctx, tlog.SubLogger(l.logger, "NewIssueComment")) l.inner.NewIssueComment(ctx, comment, mentions) } diff --git a/appview/notify/merged_notifier.go b/appview/notify/merged_notifier.go index b21d1fe2..ebf28fc7 100644 --- a/appview/notify/merged_notifier.go +++ b/appview/notify/merged_notifier.go @@ -54,7 +54,7 @@ func (m *mergedNotifier) NewIssue(ctx context.Context, issue *models.Issue, ment m.fanout(func(n Notifier) { n.NewIssue(ctx, issue, mentions) }) } -func (m *mergedNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) { +func (m *mergedNotifier) NewIssueComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID) { m.fanout(func(n Notifier) { n.NewIssueComment(ctx, comment, mentions) }) } diff --git a/appview/notify/notifier.go b/appview/notify/notifier.go index 35956538..23182fa8 100644 --- a/appview/notify/notifier.go +++ b/appview/notify/notifier.go @@ -16,7 +16,7 @@ type Notifier interface { DeleteStar(ctx context.Context, star *models.Star) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) - NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) + NewIssueComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID) NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) DeleteIssue(ctx context.Context, issue *models.Issue) @@ -55,7 +55,7 @@ func (m *BaseNotifier) NewStar(ctx context.Context, star *models.Star) {} func (m *BaseNotifier) DeleteStar(ctx context.Context, star *models.Star) {} func (m *BaseNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) {} -func (m *BaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) { +func (m *BaseNotifier) NewIssueComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID) { } func (m *BaseNotifier) NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) {} func (m *BaseNotifier) DeleteIssue(ctx context.Context, issue *models.Issue) {} diff --git a/appview/notify/posthog/notifier.go b/appview/notify/posthog/notifier.go index 86bfec19..38bd0e82 100644 --- a/appview/notify/posthog/notifier.go +++ b/appview/notify/posthog/notifier.go @@ -212,12 +212,12 @@ func (n *posthogNotifier) Clone(ctx context.Context, repo *models.Repo) { } } -func (n *posthogNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) { +func (n *posthogNotifier) NewIssueComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID) { err := n.client.Enqueue(posthog.Capture{ - DistinctId: comment.Did, + DistinctId: comment.Did.String(), Event: "new_issue_comment", Properties: posthog.Properties{ - "issue_at": comment.IssueAt, + "issue_at": comment.Subject.Uri, "mentions": mentions, }, }) diff --git a/appview/pages/pages.go b/appview/pages/pages.go index 2c7038a9..a886ee25 100644 --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -1242,7 +1242,7 @@ type EditIssueCommentParams struct { LoggedInUser *oauth.MultiAccountUser RepoInfo repoinfo.RepoInfo Issue *models.Issue - Comment *models.IssueComment + Comment *models.Comment } func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error { @@ -1253,7 +1253,7 @@ type ReplyIssueCommentPlaceholderParams struct { LoggedInUser *oauth.MultiAccountUser RepoInfo repoinfo.RepoInfo Issue *models.Issue - Comment *models.IssueComment + Comment *models.Comment } func (p *Pages) ReplyIssueCommentPlaceholderFragment(w io.Writer, params ReplyIssueCommentPlaceholderParams) error { @@ -1264,7 +1264,7 @@ type ReplyIssueCommentParams struct { LoggedInUser *oauth.MultiAccountUser RepoInfo repoinfo.RepoInfo Issue *models.Issue - Comment *models.IssueComment + Comment *models.Comment } func (p *Pages) ReplyIssueCommentFragment(w io.Writer, params ReplyIssueCommentParams) error { @@ -1275,7 +1275,7 @@ type IssueCommentBodyParams struct { LoggedInUser *oauth.MultiAccountUser RepoInfo repoinfo.RepoInfo Issue *models.Issue - Comment *models.IssueComment + Comment *models.Comment } func (p *Pages) IssueCommentBodyFragment(w io.Writer, params IssueCommentBodyParams) error { diff --git a/appview/pages/templates/repo/issues/fragments/commentList.html b/appview/pages/templates/repo/issues/fragments/commentList.html index 6a09db8a..fa75146f 100644 --- a/appview/pages/templates/repo/issues/fragments/commentList.html +++ b/appview/pages/templates/repo/issues/fragments/commentList.html @@ -15,7 +15,7 @@ "LoggedInUser" $root.LoggedInUser "Issue" $root.Issue "Comment" $comment.Self - "VouchRelationship" (index $root.VouchRelationships (did $comment.Self.Did)) + "VouchRelationship" (index $root.VouchRelationships $comment.Self.Did) ) }}
@@ -31,7 +31,7 @@ "LoggedInUser" $root.LoggedInUser "Issue" $root.Issue "Comment" $reply - "VouchRelationship" (index $root.VouchRelationships (did $reply.Did)) + "VouchRelationship" (index $root.VouchRelationships $reply.Did) ) }}
{{ end }} @@ -44,7 +44,7 @@ {{ define "topLevelComment" }}
- {{ template "user/fragments/picLink" (list .Comment.Did "size-8 mr-1" .VouchRelationship) }} + {{ template "user/fragments/picLink" (list .Comment.Did.String "size-8 mr-1" .VouchRelationship) }}
{{ template "repo/issues/fragments/issueCommentHeader" . }} @@ -56,7 +56,7 @@ {{ define "replyComment" }}
- {{ template "user/fragments/picLink" (list .Comment.Did "size-8 mr-1" .VouchRelationship) }} + {{ template "user/fragments/picLink" (list .Comment.Did.String "size-8 mr-1" .VouchRelationship) }}
{{ template "repo/issues/fragments/issueCommentHeader" . }} diff --git a/appview/pages/templates/repo/issues/fragments/editIssueComment.html b/appview/pages/templates/repo/issues/fragments/editIssueComment.html index 32be9b06..ca7354b5 100644 --- a/appview/pages/templates/repo/issues/fragments/editIssueComment.html +++ b/appview/pages/templates/repo/issues/fragments/editIssueComment.html @@ -5,7 +5,7 @@ name="body" class="w-full p-2 rounded border border-gray-200 dark:border-gray-700" rows="5" - autofocus>{{ .Comment.Body }} + autofocus>{{ .Comment.EditableBody }} {{ template "editActions" $ }}
diff --git a/appview/pages/templates/repo/issues/fragments/issueCommentBody.html b/appview/pages/templates/repo/issues/fragments/issueCommentBody.html index 0dbababe..093eb903 100644 --- a/appview/pages/templates/repo/issues/fragments/issueCommentBody.html +++ b/appview/pages/templates/repo/issues/fragments/issueCommentBody.html @@ -1,7 +1,7 @@ {{ define "repo/issues/fragments/issueCommentBody" }}
{{ if not .Comment.Deleted }} -
{{ .Comment.Body | markdown }}
+
{{ .Comment.Body.Text | markdown }}
{{ else }}
[deleted by author]
{{ end }} diff --git a/appview/pages/templates/repo/issues/fragments/issueCommentHeader.html b/appview/pages/templates/repo/issues/fragments/issueCommentHeader.html index 538b0765..be137904 100644 --- a/appview/pages/templates/repo/issues/fragments/issueCommentHeader.html +++ b/appview/pages/templates/repo/issues/fragments/issueCommentHeader.html @@ -1,13 +1,15 @@ {{ define "repo/issues/fragments/issueCommentHeader" }}
- {{ $handle := resolve .Comment.Did }} + {{ $handle := resolve .Comment.Did.String }} {{ $handle }} {{ template "hats" $ }} {{ template "timestamp" . }} - {{ $isCommentOwner := and .LoggedInUser (eq .LoggedInUser.Did .Comment.Did) }} + {{ $isCommentOwner := and .LoggedInUser (eq .LoggedInUser.Did .Comment.Did.String) }} {{ if and $isCommentOwner (not .Comment.Deleted) }} - {{ template "editIssueComment" . }} + {{ if not .Comment.IsLegacy }} + {{ template "editIssueComment" . }} + {{ end }} {{ template "deleteIssueComment" . }} {{ end }}
diff --git a/appview/pages/templates/repo/issues/fragments/replyComment.html b/appview/pages/templates/repo/issues/fragments/replyComment.html index fc87ccc9..e80b0cfe 100644 --- a/appview/pages/templates/repo/issues/fragments/replyComment.html +++ b/appview/pages/templates/repo/issues/fragments/replyComment.html @@ -18,12 +18,20 @@ + {{ template "replyActions" . }} {{ end }} diff --git a/appview/pages/templates/repo/issues/fragments/replyIssueCommentPlaceholder.html b/appview/pages/templates/repo/issues/fragments/replyIssueCommentPlaceholder.html index 2f812d6c..549748ee 100644 --- a/appview/pages/templates/repo/issues/fragments/replyIssueCommentPlaceholder.html +++ b/appview/pages/templates/repo/issues/fragments/replyIssueCommentPlaceholder.html @@ -1,5 +1,10 @@ {{ define "repo/issues/fragments/replyIssueCommentPlaceholder" }}
+ {{ if .Comment.IsLegacy }} + {{ if .LoggedInUser }} + Can't reply to legacy comment. + {{ end }} + {{ else }} {{ if .LoggedInUser }} {{ template "user/fragments/pic" (list .LoggedInUser.Did "size-8 mr-1") }} {{ end }} @@ -12,5 +17,6 @@ hx-swap="outerHTML" > + {{ end }}
{{ end }} diff --git a/appview/validator/issue.go b/appview/validator/issue.go index b199f513..9d0edcbd 100644 --- a/appview/validator/issue.go +++ b/appview/validator/issue.go @@ -4,36 +4,9 @@ import ( "fmt" "strings" - "tangled.org/core/appview/db" "tangled.org/core/appview/models" - "tangled.org/core/orm" ) -func (v *Validator) ValidateIssueComment(comment *models.IssueComment) error { - // if comments have parents, only ingest ones that are 1 level deep - if comment.ReplyTo != nil { - parents, err := db.GetIssueComments(v.db, orm.FilterEq("at_uri", *comment.ReplyTo)) - if err != nil { - return fmt.Errorf("failed to fetch parent comment: %w", err) - } - if len(parents) != 1 { - return fmt.Errorf("incorrect number of parent comments returned: %d", len(parents)) - } - - // depth check - parent := parents[0] - if parent.ReplyTo != nil { - return fmt.Errorf("incorrect depth, this comment is replying at depth >1") - } - } - - if sb := strings.TrimSpace(v.sanitizer.SanitizeDefault(comment.Body)); sb == "" { - return fmt.Errorf("body is empty after HTML sanitization") - } - - return nil -} - func (v *Validator) ValidateIssue(issue *models.Issue) error { if issue.Title == "" { return fmt.Errorf("issue title is empty")