From 1df0d5ddebc3a6e6ce29f95829751a351866cc35 Mon Sep 17 00:00:00 2001 From: Seongmin Lee Date: Thu, 07 May 2026 15:30:46 +0000 Subject: [PATCH] appview: be more compatible with legacy comment records Signed-off-by: Seongmin Lee --- appview/db/comments.go | 5 ----- appview/migration/migrate_use_feed_comment.go | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------- appview/models/comment.go | 17 ++++++++++++----- appview/state/comment.go | 55 +++++++++++++++++++++++++++++++++++++++++++++++-------- appview/pages/templates/fragments/comment/commentList.html | 8 +------- 5 file(s) changed, 118 insertion(s)(+), 42 deletion(s)(-) diff --git a/appview/db/comments.go b/appview/db/comments.go --- a/appview/db/comments.go +++ b/appview/db/comments.go @@ -11,16 +11,11 @@ "github.com/bluesky-social/indigo/api/atproto" "github.com/bluesky-social/indigo/atproto/syntax" - "tangled.org/core/api/tangled" "tangled.org/core/appview/models" "tangled.org/core/orm" ) func PutComment(tx *sql.Tx, c *models.Comment, references []syntax.ATURI) error { - if c.Collection == "" { - c.Collection = tangled.FeedCommentNSID - } - var bodyBlobs, replyToUri, replyToCid *string if len(c.Body.Blobs) > 0 { encoded, err := json.Marshal(c.Body.Blobs) diff --git a/appview/migration/migrate_use_feed_comment.go b/appview/migration/migrate_use_feed_comment.go --- a/appview/migration/migrate_use_feed_comment.go +++ b/appview/migration/migrate_use_feed_comment.go @@ -1,6 +1,7 @@ package migration import ( + "bytes" "context" "fmt" @@ -10,8 +11,11 @@ "github.com/bluesky-social/indigo/atproto/syntax" "github.com/bluesky-social/indigo/lex/util" "github.com/bluesky-social/indigo/xrpc" + "github.com/ipfs/go-cid" + "github.com/multiformats/go-multihash" "tangled.org/core/api/tangled" "tangled.org/core/appview/db" + "tangled.org/core/appview/models" "tangled.org/core/orm" ) @@ -26,10 +30,15 @@ return fmt.Errorf("unexpected collection: '%s'", record.Collection()) } - comment, err := db.GetComment(s.db, orm.FilterEq("at_uri", record)) + comments, err := db.GetComments(s.db, orm.FilterEq("at_uri", record)) if err != nil { return fmt.Errorf("db: %w", err) } + if len(comments) < 1 { + l.Info("can't found legacy record from db. skipping migration") + return nil + } + comment := comments[0] comment.Collection = tangled.FeedCommentNSID @@ -58,9 +67,9 @@ // fail if it isn't ready uri = syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", uri.Authority(), tangled.FeedCommentNSID, uri.RecordKey())) - cid, err := s.getRecordCid(ctx, uri) + cid, err := s.guessParentCommentCid(uri, &comment) if err != nil { - return fmt.Errorf("pds: getRecordCid for replyTo.uri: %w", err) + return fmt.Errorf("cbor: guessParentCommentCid for replyTo.uri: %w", err) } comment.ReplyTo.Uri = uri.String() comment.ReplyTo.Cid = cid.String() @@ -69,21 +78,27 @@ // use same rkey for new record rkey := record.RecordKey().String() - if _, err := comatproto.RepoApplyWrites(ctx, client, &comatproto.RepoApplyWrites_Input{ - Repo: did.String(), - Writes: []*comatproto.RepoApplyWrites_Input_Writes_Elem{ - {RepoApplyWrites_Delete: &comatproto.RepoApplyWrites_Delete{ - Collection: record.Collection().String(), - Rkey: rkey, - }}, - {RepoApplyWrites_Create: &comatproto.RepoApplyWrites_Create{ - Collection: tangled.FeedCommentNSID, - Rkey: &rkey, - Value: &util.LexiconTypeDecoder{Val: comment.AsRecord()}, - }}, - }, + // ensure new record is missing in PDS + if _, err := agnostic.RepoGetRecord(ctx, client, "", tangled.FeedCommentNSID, did.String(), rkey); err == nil { + l.Info("New comment record already exists") + } else { + // insert new record + if _, err := comatproto.RepoCreateRecord(ctx, client, &comatproto.RepoCreateRecord_Input{ + Repo: did.String(), + Collection: tangled.FeedCommentNSID, + Rkey: &rkey, + Record: &util.LexiconTypeDecoder{Val: comment.AsRecord()}, + }); err != nil { + return fmt.Errorf("pds: putRecord: %w", err) + } + } + + if _, err := comatproto.RepoDeleteRecord(ctx, client, &comatproto.RepoDeleteRecord_Input{ + Repo: did.String(), + Collection: record.Collection().String(), + Rkey: rkey, }); err != nil { - return fmt.Errorf("pds: applyWrites: %w", err) + l.Info("Failed to cleanup old record. Proceeding migration...", "err", err) } return nil @@ -110,4 +125,30 @@ } return cid, nil +} + +func (s *Migration) guessParentCommentCid(uri syntax.ATURI, comment *models.Comment) (syntax.CID, error) { + parent, err := db.GetComment(s.db, orm.FilterEq("did", uri.Authority()), orm.FilterEq("rkey", uri.RecordKey())) + if err != nil { + return "", fmt.Errorf("db: failed to queyr subject comment: %w", err) + } + if parent.Deleted != nil { + // leave cid empty. reply comment won't pass the schema validation. + return "", nil + } + + // since parent comment is also migrating, parent comments subject.cid might be empty + if parent.Subject.Cid == "" { + parent.Subject.Cid = comment.Subject.Cid + } + + buf := new(bytes.Buffer) + if err := parent.AsRecord().MarshalCBOR(buf); err != nil { + return "", fmt.Errorf("MarshalCBOR: %w", err) + } + c, err := cid.NewPrefixV1(cid.DagCBOR, multihash.SHA2_256).Sum(buf.Bytes()) + if err != nil { + return "", fmt.Errorf("cid: sum: %w", err) + } + return syntax.CID(c.String()), nil } diff --git a/appview/models/comment.go b/appview/models/comment.go --- a/appview/models/comment.go +++ b/appview/models/comment.go @@ -44,10 +44,6 @@ } func (c Comment) AsRecord() typegen.CBORMarshaler { - // can't convert to record for legacy types - if c.Collection != tangled.FeedCommentNSID { - return nil - } var pullRoundIdx *int64 if c.PullRoundIdx != nil { pullRoundIdx = new(int64) @@ -204,8 +200,19 @@ if r.ReplyTo == nil { continue } - if parent, exists := toplevel[syntax.ATURI(r.ReplyTo.Uri)]; exists { + uri := syntax.ATURI(r.ReplyTo.Uri) + if parent, exists := toplevel[uri]; exists { parent.Replies = append(parent.Replies, r) + continue + } + // HACK: fallback to legacy comment collections + if parent, exists := toplevel[syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", uri.Authority(), tangled.RepoIssueCommentNSID, uri.RecordKey()))]; exists { + parent.Replies = append(parent.Replies, r) + continue + } + if parent, exists := toplevel[syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", uri.Authority(), tangled.RepoPullCommentNSID, uri.RecordKey()))]; exists { + parent.Replies = append(parent.Replies, r) + continue } } diff --git a/appview/state/comment.go b/appview/state/comment.go --- a/appview/state/comment.go +++ b/appview/state/comment.go @@ -1,6 +1,7 @@ package state import ( + "bytes" "fmt" "net/http" "strconv" @@ -10,6 +11,8 @@ "github.com/bluesky-social/indigo/atproto/syntax" lexutil "github.com/bluesky-social/indigo/lex/util" indigoxrpc "github.com/bluesky-social/indigo/xrpc" + "github.com/ipfs/go-cid" + "github.com/multiformats/go-multihash" "tangled.org/core/api/tangled" "tangled.org/core/appview/db" @@ -176,20 +179,56 @@ 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 replyToUriRaw != "" { + replyToUri, err := syntax.ParseATURI(replyToUriRaw) if err != nil { s.pages.Notice(w, noticeId, "reply-to-uri should be valid AT-URI") return } - cid, err := syntax.ParseCID(replyToCidRaw) - if err != nil { - s.pages.Notice(w, noticeId, "reply-to-cid should be valid CID") - return + // force replyTo.uri to `sh.tangled.feed.comment` collection, even when they aren't. + // we are expecting parent comment will be migrated later. + replyToUri = syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", replyToUri.Authority(), tangled.FeedCommentNSID, replyToUri.RecordKey())) + + var replyToCid syntax.CID + if replyToCidRaw != "" { + replyToCid, err = syntax.ParseCID(replyToCidRaw) + if err != nil { + s.pages.Notice(w, noticeId, "reply-to-cid should be valid CID") + return + } + } else { + // guess parent comment cid + subjectComment, err := db.GetComment(s.db, orm.FilterEq("did", replyToUri.Authority()), orm.FilterEq("rkey", replyToUri.RecordKey())) + if err != nil { + l.Warn("db: failed to query subject comment", "err", err) + s.pages.Notice(w, noticeId, "Subject record is unknown.") + return + } + if subjectComment.Deleted != nil { + // leave cid empty. reply comment won't pass the schema validation. + } else { + // guess cid from content + c, err := func() (cid.Cid, error) { + buf := new(bytes.Buffer) + if subjectComment.Subject.Cid == "" { + subjectComment.Subject.Cid = subject.Cid + } + if err := subjectComment.AsRecord().MarshalCBOR(buf); err != nil { + return cid.Undef, fmt.Errorf("MarshalCBOR: %w", err) + } + return cid.NewPrefixV1(cid.DagCBOR, multihash.SHA2_256).Sum(buf.Bytes()) + }() + if err != nil { + l.Warn("cbor: failed to guess parent comment cid", "err", err) + s.pages.Notice(w, noticeId, "Parent comment is invalid.") + return + } + replyToCid = syntax.CID(c.String()) + } } replyTo = &comatproto.RepoStrongRef{ - Uri: uri.String(), - Cid: cid.String(), + Uri: replyToUri.String(), + Cid: replyToCid.String(), } } diff --git a/appview/pages/templates/fragments/comment/commentList.html b/appview/pages/templates/fragments/comment/commentList.html --- a/appview/pages/templates/fragments/comment/commentList.html +++ b/appview/pages/templates/fragments/comment/commentList.html @@ -38,13 +38,7 @@ - {{ if $item.Self.IsLegacy }} -
- Can't reply to legacy comment. -
- {{ else }} - {{ template "fragments/comment/replyPlaceholder" (dict "LoggedInUser" $root.LoggedInUser) }} - {{ end }} + {{ template "fragments/comment/replyPlaceholder" (dict "LoggedInUser" $root.LoggedInUser) }} {{ end }} -- tangled.sh