Monorepo for Tangled tangled.org

appview: replace `PullComment` to `Comment`

Including db migration to migrate `issue_comments` and `pull_comments`
to unified `comments` table.

Signed-off-by: Seongmin Lee <git@boltless.me>

boltless.me afa51a0c 744dc125

verified
+556 -199
+202
appview/db/comments.go
··· 1 + package db 2 + 3 + import ( 4 + "database/sql" 5 + "fmt" 6 + "maps" 7 + "slices" 8 + "sort" 9 + "strings" 10 + "time" 11 + 12 + "github.com/bluesky-social/indigo/atproto/syntax" 13 + "tangled.org/core/api/tangled" 14 + "tangled.org/core/appview/models" 15 + "tangled.org/core/orm" 16 + ) 17 + 18 + func PutComment(tx *sql.Tx, c *models.Comment) error { 19 + if c.Collection == "" { 20 + c.Collection = tangled.CommentNSID 21 + } 22 + result, err := tx.Exec( 23 + `insert into comments ( 24 + did, 25 + collection, 26 + rkey, 27 + subject_at, 28 + reply_to, 29 + body, 30 + pull_submission_id, 31 + created 32 + ) 33 + values (?, ?, ?, ?, ?, ?, ?, ?) 34 + on conflict(did, collection, rkey) do update set 35 + subject_at = excluded.subject_at, 36 + reply_to = excluded.reply_to, 37 + body = excluded.body, 38 + edited = case 39 + when 40 + comments.subject_at != excluded.subject_at 41 + or comments.body != excluded.body 42 + or comments.reply_to != excluded.reply_to 43 + then ? 44 + else comments.edited 45 + end`, 46 + c.Did, 47 + c.Collection, 48 + c.Rkey, 49 + c.Subject, 50 + c.ReplyTo, 51 + c.Body, 52 + c.PullSubmissionId, 53 + c.Created.Format(time.RFC3339), 54 + time.Now().Format(time.RFC3339), 55 + ) 56 + if err != nil { 57 + return err 58 + } 59 + 60 + c.Id, err = result.LastInsertId() 61 + if err != nil { 62 + return err 63 + } 64 + 65 + if err := putReferences(tx, c.AtUri(), c.References); err != nil { 66 + return fmt.Errorf("put reference_links: %w", err) 67 + } 68 + 69 + return nil 70 + } 71 + 72 + func DeleteComments(e Execer, filters ...orm.Filter) error { 73 + var conditions []string 74 + var args []any 75 + for _, filter := range filters { 76 + conditions = append(conditions, filter.Condition()) 77 + args = append(args, filter.Arg()...) 78 + } 79 + 80 + whereClause := "" 81 + if conditions != nil { 82 + whereClause = " where " + strings.Join(conditions, " and ") 83 + } 84 + 85 + query := fmt.Sprintf(`update comments set body = "", deleted = strftime('%%Y-%%m-%%dT%%H:%%M:%%SZ', 'now') %s`, whereClause) 86 + 87 + _, err := e.Exec(query, args...) 88 + return err 89 + } 90 + 91 + func GetComments(e Execer, filters ...orm.Filter) ([]models.Comment, error) { 92 + commentMap := make(map[string]*models.Comment) 93 + 94 + var conditions []string 95 + var args []any 96 + for _, filter := range filters { 97 + conditions = append(conditions, filter.Condition()) 98 + args = append(args, filter.Arg()...) 99 + } 100 + 101 + whereClause := "" 102 + if conditions != nil { 103 + whereClause = " where " + strings.Join(conditions, " and ") 104 + } 105 + 106 + query := fmt.Sprintf(` 107 + select 108 + id, 109 + did, 110 + collection, 111 + rkey, 112 + subject_at, 113 + reply_to, 114 + body, 115 + pull_submission_id, 116 + created, 117 + edited, 118 + deleted 119 + from 120 + comments 121 + %s 122 + `, whereClause) 123 + 124 + rows, err := e.Query(query, args...) 125 + if err != nil { 126 + return nil, err 127 + } 128 + 129 + for rows.Next() { 130 + var comment models.Comment 131 + var created string 132 + var edited, deleted, replyTo sql.Null[string] 133 + err := rows.Scan( 134 + &comment.Id, 135 + &comment.Did, 136 + &comment.Collection, 137 + &comment.Rkey, 138 + &comment.Subject, 139 + &replyTo, 140 + &comment.Body, 141 + &comment.PullSubmissionId, 142 + &created, 143 + &edited, 144 + &deleted, 145 + ) 146 + if err != nil { 147 + return nil, err 148 + } 149 + 150 + if t, err := time.Parse(time.RFC3339, created); err == nil { 151 + comment.Created = t 152 + } 153 + 154 + if edited.Valid { 155 + if t, err := time.Parse(time.RFC3339, edited.V); err == nil { 156 + comment.Edited = &t 157 + } 158 + } 159 + 160 + if deleted.Valid { 161 + if t, err := time.Parse(time.RFC3339, deleted.V); err == nil { 162 + comment.Deleted = &t 163 + } 164 + } 165 + 166 + if replyTo.Valid { 167 + rt := syntax.ATURI(replyTo.V) 168 + comment.ReplyTo = &rt 169 + } 170 + 171 + atUri := comment.AtUri().String() 172 + commentMap[atUri] = &comment 173 + } 174 + 175 + if err := rows.Err(); err != nil { 176 + return nil, err 177 + } 178 + defer rows.Close() 179 + 180 + // collect references from each comments 181 + commentAts := slices.Collect(maps.Keys(commentMap)) 182 + allReferencs, err := GetReferencesAll(e, orm.FilterIn("from_at", commentAts)) 183 + if err != nil { 184 + return nil, fmt.Errorf("failed to query reference_links: %w", err) 185 + } 186 + for commentAt, references := range allReferencs { 187 + if comment, ok := commentMap[commentAt.String()]; ok { 188 + comment.References = references 189 + } 190 + } 191 + 192 + var comments []models.Comment 193 + for _, c := range commentMap { 194 + comments = append(comments, *c) 195 + } 196 + 197 + sort.Slice(comments, func(i, j int) bool { 198 + return comments[i].Created.Before(comments[j].Created) 199 + }) 200 + 201 + return comments, nil 202 + }
+81
appview/db/db.go
··· 1255 1255 return err 1256 1256 }) 1257 1257 1258 + orm.RunMigration(conn, logger, "add-comments-table", func(tx *sql.Tx) error { 1259 + _, err := tx.Exec(` 1260 + drop table if exists comments; 1261 + 1262 + create table comments ( 1263 + -- identifiers 1264 + id integer primary key autoincrement, 1265 + did text not null, 1266 + collection text not null default 'sh.tangled.comment', 1267 + rkey text not null, 1268 + at_uri text generated always as ('at://' || did || '/' || collection || '/' || rkey) stored, 1269 + 1270 + -- at identifiers 1271 + subject_at text not null, 1272 + reply_to text, -- at_uri of parent comment 1273 + 1274 + pull_submission_id integer, -- dirty fix until we atprotate the pull-rounds 1275 + 1276 + -- content 1277 + body text not null, 1278 + created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1279 + edited text, 1280 + deleted text, 1281 + 1282 + -- constraints 1283 + unique(did, collection, rkey) 1284 + ); 1285 + 1286 + insert into comments ( 1287 + did, 1288 + collection, 1289 + rkey, 1290 + subject_at, 1291 + reply_to, 1292 + body, 1293 + created, 1294 + edited, 1295 + deleted 1296 + ) 1297 + select 1298 + did, 1299 + 'sh.tangled.repo.issue.comment', 1300 + rkey, 1301 + issue_at, 1302 + reply_to, 1303 + body, 1304 + created, 1305 + edited, 1306 + deleted 1307 + from issue_comments 1308 + where rkey is not null; 1309 + 1310 + insert into comments ( 1311 + did, 1312 + collection, 1313 + rkey, 1314 + subject_at, 1315 + pull_submission_id, 1316 + body, 1317 + created 1318 + ) 1319 + select 1320 + c.owner_did, 1321 + 'sh.tangled.repo.pull.comment', 1322 + substr( 1323 + substr(c.comment_at, 6 + instr(substr(c.comment_at, 6), '/')), -- nsid/rkey 1324 + instr( 1325 + substr(c.comment_at, 6 + instr(substr(c.comment_at, 6), '/')), -- nsid/rkey 1326 + '/' 1327 + ) + 1 1328 + ), -- rkey 1329 + p.at_uri, 1330 + c.submission_id, 1331 + c.body, 1332 + c.created 1333 + from pull_comments c 1334 + join pulls p on c.repo_at = p.repo_at and c.pull_id = p.pull_id; 1335 + `) 1336 + return err 1337 + }) 1338 + 1258 1339 return &DB{ 1259 1340 db, 1260 1341 logger,
+6 -121
appview/db/pulls.go
··· 391 391 return nil, err 392 392 } 393 393 394 - // Get comments for all submissions using GetPullComments 394 + // Get comments for all submissions using GetComments 395 395 submissionIds := slices.Collect(maps.Keys(submissionMap)) 396 - comments, err := GetPullComments(e, orm.FilterIn("submission_id", submissionIds)) 396 + comments, err := GetComments(e, orm.FilterIn("pull_submission_id", submissionIds)) 397 397 if err != nil { 398 398 return nil, fmt.Errorf("failed to get pull comments: %w", err) 399 399 } 400 400 for _, comment := range comments { 401 - if submission, ok := submissionMap[comment.SubmissionId]; ok { 402 - submission.Comments = append(submission.Comments, comment) 401 + if comment.PullSubmissionId != nil { 402 + if submission, ok := submissionMap[*comment.PullSubmissionId]; ok { 403 + submission.Comments = append(submission.Comments, comment) 404 + } 403 405 } 404 406 } 405 407 ··· 419 421 return m, nil 420 422 } 421 423 422 - func GetPullComments(e Execer, filters ...orm.Filter) ([]models.PullComment, error) { 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 - pull_id, 439 - submission_id, 440 - repo_at, 441 - owner_did, 442 - comment_at, 443 - body, 444 - created 445 - from 446 - pull_comments 447 - %s 448 - order by 449 - created asc 450 - `, whereClause) 451 - 452 - rows, err := e.Query(query, args...) 453 - if err != nil { 454 - return nil, err 455 - } 456 - defer rows.Close() 457 - 458 - commentMap := make(map[string]*models.PullComment) 459 - for rows.Next() { 460 - var comment models.PullComment 461 - var createdAt string 462 - err := rows.Scan( 463 - &comment.ID, 464 - &comment.PullId, 465 - &comment.SubmissionId, 466 - &comment.RepoAt, 467 - &comment.OwnerDid, 468 - &comment.CommentAt, 469 - &comment.Body, 470 - &createdAt, 471 - ) 472 - if err != nil { 473 - return nil, err 474 - } 475 - 476 - if t, err := time.Parse(time.RFC3339, createdAt); err == nil { 477 - comment.Created = t 478 - } 479 - 480 - atUri := comment.AtUri().String() 481 - commentMap[atUri] = &comment 482 - } 483 - 484 - if err := rows.Err(); err != nil { 485 - return nil, err 486 - } 487 - 488 - // collect references for each comments 489 - commentAts := slices.Collect(maps.Keys(commentMap)) 490 - allReferencs, err := GetReferencesAll(e, orm.FilterIn("from_at", commentAts)) 491 - if err != nil { 492 - return nil, fmt.Errorf("failed to query reference_links: %w", err) 493 - } 494 - for commentAt, references := range allReferencs { 495 - if comment, ok := commentMap[commentAt.String()]; ok { 496 - comment.References = references 497 - } 498 - } 499 - 500 - var comments []models.PullComment 501 - for _, c := range commentMap { 502 - comments = append(comments, *c) 503 - } 504 - 505 - sort.Slice(comments, func(i, j int) bool { 506 - return comments[i].Created.Before(comments[j].Created) 507 - }) 508 - 509 - return comments, nil 510 - } 511 - 512 424 // timeframe here is directly passed into the sql query filter, and any 513 425 // timeframe in the past should be negative; e.g.: "-3 months" 514 426 func GetPullsByOwnerDid(e Execer, did, timeframe string) ([]models.Pull, error) { ··· 583 495 } 584 496 585 497 return pulls, nil 586 - } 587 - 588 - func NewPullComment(tx *sql.Tx, comment *models.PullComment) (int64, error) { 589 - query := `insert into pull_comments (owner_did, repo_at, submission_id, comment_at, pull_id, body) values (?, ?, ?, ?, ?, ?)` 590 - res, err := tx.Exec( 591 - query, 592 - comment.OwnerDid, 593 - comment.RepoAt, 594 - comment.SubmissionId, 595 - comment.CommentAt, 596 - comment.PullId, 597 - comment.Body, 598 - ) 599 - if err != nil { 600 - return 0, err 601 - } 602 - 603 - i, err := res.LastInsertId() 604 - if err != nil { 605 - return 0, err 606 - } 607 - 608 - if err := putReferences(tx, comment.AtUri(), comment.References); err != nil { 609 - return 0, fmt.Errorf("put reference_links: %w", err) 610 - } 611 - 612 - return i, nil 613 498 } 614 499 615 500 func SetPullState(e Execer, repoAt syntax.ATURI, pullId int, pullState models.PullState) error {
+7 -8
appview/db/reference.go
··· 124 124 values %s 125 125 ) 126 126 select 127 - p.owner_did, p.rkey, 128 - c.comment_at 127 + p.owner_did, p.rkey, c.at_uri 129 128 from input inp 130 129 join repos r 131 130 on r.did = inp.owner_did ··· 133 132 join pulls p 134 133 on p.repo_at = r.at_uri 135 134 and p.pull_id = inp.pull_id 136 - left join pull_comments c 135 + left join comments c 137 136 on inp.comment_id is not null 138 - and c.repo_at = r.at_uri and c.pull_id = p.pull_id 137 + and c.subject_at = ('at://' || p.owner_did || '/' || 'sh.tangled.repo.pull' || '/' || p.rkey) 139 138 and c.id = inp.comment_id 140 139 `, 141 140 strings.Join(vals, ","), ··· 293 292 return nil, fmt.Errorf("get pull backlinks: %w", err) 294 293 } 295 294 backlinks = append(backlinks, ls...) 296 - ls, err = getPullCommentBacklinks(e, backlinksMap[tangled.RepoPullCommentNSID]) 295 + ls, err = getPullCommentBacklinks(e, backlinksMap[tangled.CommentNSID]) 297 296 if err != nil { 298 297 return nil, fmt.Errorf("get pull_comment backlinks: %w", err) 299 298 } ··· 428 427 if len(aturis) == 0 { 429 428 return nil, nil 430 429 } 431 - filter := orm.FilterIn("c.comment_at", aturis) 430 + filter := orm.FilterIn("c.at_uri", aturis) 432 431 rows, err := e.Query( 433 432 fmt.Sprintf( 434 433 `select r.did, r.name, p.pull_id, c.id, p.title, p.state 435 434 from repos r 436 435 join pulls p 437 436 on r.at_uri = p.repo_at 438 - join pull_comments c 439 - on r.at_uri = c.repo_at and p.pull_id = c.pull_id 437 + join comments c 438 + on ('at://' || p.owner_did || '/' || 'sh.tangled.repo.pull' || '/' || p.rkey) = c.subject_at 440 439 where %s`, 441 440 filter.Condition(), 442 441 ),
+71
appview/ingester.go
··· 79 79 err = i.ingestString(e) 80 80 case tangled.RepoIssueNSID: 81 81 err = i.ingestIssue(ctx, e) 82 + case tangled.CommentNSID: 83 + err = i.ingestComment(e) 82 84 case tangled.RepoIssueCommentNSID: 83 85 err = i.ingestIssueComment(e) 84 86 case tangled.LabelDefinitionNSID: ··· 926 928 orm.FilterEq("rkey", rkey), 927 929 ); err != nil { 928 930 return fmt.Errorf("failed to delete issue comment record: %w", err) 931 + } 932 + 933 + return nil 934 + } 935 + 936 + return nil 937 + } 938 + 939 + func (i *Ingester) ingestComment(e *jmodels.Event) error { 940 + did := e.Did 941 + rkey := e.Commit.RKey 942 + 943 + var err error 944 + 945 + l := i.Logger.With("handler", "ingestComment", "nsid", e.Commit.Collection, "did", did, "rkey", rkey) 946 + l.Info("ingesting record") 947 + 948 + ddb, ok := i.Db.Execer.(*db.DB) 949 + if !ok { 950 + return fmt.Errorf("failed to index issue comment record, invalid db cast") 951 + } 952 + 953 + switch e.Commit.Operation { 954 + case jmodels.CommitOperationCreate, jmodels.CommitOperationUpdate: 955 + raw := json.RawMessage(e.Commit.Record) 956 + record := tangled.Comment{} 957 + err = json.Unmarshal(raw, &record) 958 + if err != nil { 959 + return fmt.Errorf("invalid record: %w", err) 960 + } 961 + 962 + comment, err := models.CommentFromRecord(syntax.DID(did), syntax.RecordKey(rkey), record) 963 + if err != nil { 964 + return fmt.Errorf("failed to parse comment from record: %w", err) 965 + } 966 + 967 + // TODO: ingest pull comments 968 + // we aren't ingesting pull comments yet because pull itself isn't fully atprotated. 969 + // so we cannot know which round this comment is pointing to 970 + if comment.Subject.Collection().String() == tangled.RepoPullNSID { 971 + l.Info("skip ingesting pull comments") 972 + return nil 973 + } 974 + 975 + if err := comment.Validate(); err != nil { 976 + return fmt.Errorf("failed to validate comment: %w", err) 977 + } 978 + 979 + tx, err := ddb.Begin() 980 + if err != nil { 981 + return fmt.Errorf("failed to start transaction: %w", err) 982 + } 983 + defer tx.Rollback() 984 + 985 + err = db.PutComment(tx, comment) 986 + if err != nil { 987 + return fmt.Errorf("failed to create comment: %w", err) 988 + } 989 + 990 + return tx.Commit() 991 + 992 + case jmodels.CommitOperationDelete: 993 + if err := db.DeleteComments( 994 + ddb, 995 + orm.FilterEq("did", did), 996 + orm.FilterEq("collection", e.Commit.Collection), 997 + orm.FilterEq("rkey", rkey), 998 + ); err != nil { 999 + return fmt.Errorf("failed to delete comment record: %w", err) 929 1000 } 930 1001 931 1002 return nil
+138
appview/models/comment.go
··· 1 + package models 2 + 3 + import ( 4 + "fmt" 5 + "strings" 6 + "time" 7 + 8 + "github.com/bluesky-social/indigo/atproto/syntax" 9 + "github.com/whyrusleeping/cbor-gen" 10 + "tangled.org/core/api/tangled" 11 + ) 12 + 13 + type Comment struct { 14 + Id int64 15 + Did syntax.DID 16 + Collection syntax.NSID 17 + Rkey string 18 + Subject syntax.ATURI 19 + ReplyTo *syntax.ATURI 20 + Body string 21 + Created time.Time 22 + Edited *time.Time 23 + Deleted *time.Time 24 + Mentions []syntax.DID 25 + References []syntax.ATURI 26 + PullSubmissionId *int 27 + } 28 + 29 + func (c *Comment) AtUri() syntax.ATURI { 30 + return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", c.Did, c.Collection, c.Rkey)) 31 + } 32 + 33 + func (c *Comment) AsRecord() typegen.CBORMarshaler { 34 + mentions := make([]string, len(c.Mentions)) 35 + for i, did := range c.Mentions { 36 + mentions[i] = string(did) 37 + } 38 + references := make([]string, len(c.References)) 39 + for i, uri := range c.References { 40 + references[i] = string(uri) 41 + } 42 + var replyTo *string 43 + if c.ReplyTo != nil { 44 + replyToStr := c.ReplyTo.String() 45 + replyTo = &replyToStr 46 + } 47 + switch c.Collection { 48 + case tangled.RepoIssueCommentNSID: 49 + return &tangled.RepoIssueComment{ 50 + Issue: c.Subject.String(), 51 + Body: c.Body, 52 + CreatedAt: c.Created.Format(time.RFC3339), 53 + ReplyTo: replyTo, 54 + Mentions: mentions, 55 + References: references, 56 + } 57 + case tangled.RepoPullCommentNSID: 58 + return &tangled.RepoPullComment{ 59 + Pull: c.Subject.String(), 60 + Body: c.Body, 61 + CreatedAt: c.Created.Format(time.RFC3339), 62 + Mentions: mentions, 63 + References: references, 64 + } 65 + default: // default to CommentNSID 66 + return &tangled.Comment{ 67 + Subject: c.Subject.String(), 68 + Body: c.Body, 69 + CreatedAt: c.Created.Format(time.RFC3339), 70 + ReplyTo: replyTo, 71 + Mentions: mentions, 72 + References: references, 73 + } 74 + } 75 + } 76 + 77 + func (c *Comment) IsTopLevel() bool { 78 + return c.ReplyTo == nil 79 + } 80 + 81 + func (c *Comment) IsReply() bool { 82 + return c.ReplyTo != nil 83 + } 84 + 85 + func (c *Comment) Validate() error { 86 + // TODO: sanitize the body and then trim space 87 + if sb := strings.TrimSpace(c.Body); sb == "" { 88 + return fmt.Errorf("body is empty after HTML sanitization") 89 + } 90 + 91 + // if it's for PR, PullSubmissionId should not be nil 92 + if c.Subject.Collection().String() == tangled.RepoPullNSID { 93 + if c.PullSubmissionId == nil { 94 + return fmt.Errorf("PullSubmissionId should not be nil") 95 + } 96 + } 97 + return nil 98 + } 99 + 100 + func CommentFromRecord(did syntax.DID, rkey syntax.RecordKey, record tangled.Comment) (*Comment, error) { 101 + created, err := time.Parse(time.RFC3339, record.CreatedAt) 102 + if err != nil { 103 + created = time.Now() 104 + } 105 + 106 + if _, err = syntax.ParseATURI(record.Subject); err != nil { 107 + return nil, err 108 + } 109 + 110 + i := record 111 + mentions := make([]syntax.DID, len(record.Mentions)) 112 + for i, did := range record.Mentions { 113 + mentions[i] = syntax.DID(did) 114 + } 115 + references := make([]syntax.ATURI, len(record.References)) 116 + for i, uri := range i.References { 117 + references[i] = syntax.ATURI(uri) 118 + } 119 + var replyTo *syntax.ATURI 120 + if record.ReplyTo != nil { 121 + replyToAtUri := syntax.ATURI(*record.ReplyTo) 122 + replyTo = &replyToAtUri 123 + } 124 + 125 + comment := Comment{ 126 + Did: did, 127 + Collection: tangled.CommentNSID, 128 + Rkey: rkey.String(), 129 + Body: record.Body, 130 + Subject: syntax.ATURI(record.Subject), 131 + ReplyTo: replyTo, 132 + Created: created, 133 + Mentions: mentions, 134 + References: references, 135 + } 136 + 137 + return &comment, nil 138 + }
+2 -28
appview/models/pull.go
··· 138 138 RoundNumber int 139 139 Patch string 140 140 Combined string 141 - Comments []PullComment 141 + Comments []Comment 142 142 SourceRev string // include the rev that was used to create this submission: only for branch/fork PRs 143 143 144 144 // meta 145 145 Created time.Time 146 - } 147 - 148 - type PullComment struct { 149 - // ids 150 - ID int 151 - PullId int 152 - SubmissionId int 153 - 154 - // at ids 155 - RepoAt string 156 - OwnerDid string 157 - CommentAt string 158 - 159 - // content 160 - Body string 161 - 162 - // meta 163 - Mentions []syntax.DID 164 - References []syntax.ATURI 165 - 166 - // meta 167 - Created time.Time 168 - } 169 - 170 - func (p *PullComment) AtUri() syntax.ATURI { 171 - return syntax.ATURI(p.CommentAt) 172 146 } 173 147 174 148 func (p *Pull) TotalComments() int { ··· 279 253 addParticipant(s.PullAt.Authority().String()) 280 254 281 255 for _, c := range s.Comments { 282 - addParticipant(c.OwnerDid) 256 + addParticipant(c.Did.String()) 283 257 } 284 258 285 259 return participants
+12 -7
appview/notify/db/db.go
··· 278 278 ) 279 279 } 280 280 281 - func (n *databaseNotifier) NewPullComment(ctx context.Context, comment *models.PullComment, mentions []syntax.DID) { 281 + func (n *databaseNotifier) NewPullComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID) { 282 282 l := log.FromContext(ctx) 283 283 284 - pull, err := db.GetPull(n.db, 285 - syntax.ATURI(comment.RepoAt), 286 - comment.PullId, 284 + pulls, err := db.GetPulls(n.db, 285 + orm.FilterEq("owner_did", comment.Subject.Authority()), 286 + orm.FilterEq("rkey", comment.Subject.RecordKey()), 287 287 ) 288 288 if err != nil { 289 - l.Error("failed to get pulls", "err", err) 289 + l.Error("failed to get pull", "err", err) 290 + return 291 + } 292 + if len(pulls) == 0 { 293 + l.Error("NewPullComment: no pull found", "aturi", comment.Subject) 290 294 return 291 295 } 296 + pull := pulls[0] 292 297 293 - repo, err := db.GetRepo(n.db, orm.FilterEq("at_uri", comment.RepoAt)) 298 + repo, err := db.GetRepo(n.db, orm.FilterEq("at_uri", pull.RepoAt)) 294 299 if err != nil { 295 300 l.Error("failed to get repos", "err", err) 296 301 return ··· 308 313 recipients.Remove(m) 309 314 } 310 315 311 - actorDid := syntax.DID(comment.OwnerDid) 316 + actorDid := comment.Did 312 317 eventType := models.NotificationTypePullCommented 313 318 entityType := "pull" 314 319 entityId := pull.AtUri().String()
+1 -1
appview/notify/merged_notifier.go
··· 78 78 m.fanout(func(n Notifier) { n.NewPull(ctx, pull) }) 79 79 } 80 80 81 - func (m *mergedNotifier) NewPullComment(ctx context.Context, comment *models.PullComment, mentions []syntax.DID) { 81 + func (m *mergedNotifier) NewPullComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID) { 82 82 m.fanout(func(n Notifier) { n.NewPullComment(ctx, comment, mentions) }) 83 83 } 84 84
+2 -2
appview/notify/notifier.go
··· 22 22 DeleteFollow(ctx context.Context, follow *models.Follow) 23 23 24 24 NewPull(ctx context.Context, pull *models.Pull) 25 - NewPullComment(ctx context.Context, comment *models.PullComment, mentions []syntax.DID) 25 + NewPullComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID) 26 26 NewPullState(ctx context.Context, actor syntax.DID, pull *models.Pull) 27 27 28 28 NewIssueLabelOp(ctx context.Context, issue *models.Issue) ··· 60 60 func (m *BaseNotifier) DeleteFollow(ctx context.Context, follow *models.Follow) {} 61 61 62 62 func (m *BaseNotifier) NewPull(ctx context.Context, pull *models.Pull) {} 63 - func (m *BaseNotifier) NewPullComment(ctx context.Context, models *models.PullComment, mentions []syntax.DID) { 63 + func (m *BaseNotifier) NewPullComment(ctx context.Context, models *models.Comment, mentions []syntax.DID) { 64 64 } 65 65 func (m *BaseNotifier) NewPullState(ctx context.Context, actor syntax.DID, pull *models.Pull) {} 66 66
+3 -4
appview/notify/posthog/notifier.go
··· 86 86 } 87 87 } 88 88 89 - func (n *posthogNotifier) NewPullComment(ctx context.Context, comment *models.PullComment, mentions []syntax.DID) { 89 + func (n *posthogNotifier) NewPullComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID) { 90 90 err := n.client.Enqueue(posthog.Capture{ 91 - DistinctId: comment.OwnerDid, 91 + DistinctId: comment.Did.String(), 92 92 Event: "new_pull_comment", 93 93 Properties: posthog.Properties{ 94 - "repo_at": comment.RepoAt, 95 - "pull_id": comment.PullId, 94 + "pull_at": comment.Subject, 96 95 "mentions": mentions, 97 96 }, 98 97 })
+4 -4
appview/pages/templates/repo/pulls/pull.html
··· 578 578 {{ end }} 579 579 580 580 {{ define "submissionComment" }} 581 - <div id="comment-{{.ID}}" class="flex gap-2 -ml-4 py-4 w-full mx-auto"> 581 + <div id="comment-{{.Id}}" class="flex gap-2 -ml-4 py-4 w-full mx-auto"> 582 582 <!-- left column: profile picture --> 583 583 <div class="flex-shrink-0 h-fit relative"> 584 - {{ template "user/fragments/picLink" (list .OwnerDid "size-8") }} 584 + {{ template "user/fragments/picLink" (list .Did.String "size-8") }} 585 585 </div> 586 586 <!-- right column: name and body in two rows --> 587 587 <div class="flex-1 min-w-0"> 588 588 <!-- Row 1: Author and timestamp --> 589 589 <div class="text-sm text-gray-500 dark:text-gray-400 flex items-center gap-1"> 590 - {{ $handle := resolve .OwnerDid }} 590 + {{ $handle := resolve .Did.String }} 591 591 <a class="text-gray-500 dark:text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" href="/{{ $handle }}">{{ $handle }}</a> 592 592 <span class="before:content-['·']"></span> 593 - <a class="text-gray-500 dark:text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" href="#comment-{{.ID}}"> 593 + <a class="text-gray-500 dark:text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" href="#comment-{{.Id}}"> 594 594 {{ template "repo/fragments/shortTime" .Created }} 595 595 </a> 596 596 </div>
+26 -24
appview/pulls/pulls.go
··· 834 834 } 835 835 defer tx.Rollback() 836 836 837 - createdAt := time.Now().Format(time.RFC3339) 837 + comment := models.Comment{ 838 + Did: syntax.DID(user.Active.Did), 839 + Collection: tangled.CommentNSID, 840 + Rkey: tid.TID(), 841 + Subject: pull.AtUri(), 842 + ReplyTo: nil, 843 + Body: body, 844 + Created: time.Now(), 845 + Mentions: mentions, 846 + References: references, 847 + PullSubmissionId: &pull.Submissions[roundNumber].ID, 848 + } 849 + if err = comment.Validate(); err != nil { 850 + log.Println("failed to validate comment", err) 851 + s.pages.Notice(w, "pull-comment", "Failed to create comment.") 852 + return 853 + } 838 854 839 855 client, err := s.oauth.AuthorizedClient(r) 840 856 if err != nil { ··· 842 858 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 843 859 return 844 860 } 845 - atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 846 - Collection: tangled.RepoPullCommentNSID, 847 - Repo: user.Active.Did, 848 - Rkey: tid.TID(), 861 + 862 + _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 863 + Collection: comment.Collection.String(), 864 + Repo: comment.Did.String(), 865 + Rkey: comment.Rkey, 849 866 Record: &lexutil.LexiconTypeDecoder{ 850 - Val: &tangled.RepoPullComment{ 851 - Pull: pull.AtUri().String(), 852 - Body: body, 853 - CreatedAt: createdAt, 854 - }, 867 + Val: comment.AsRecord(), 855 868 }, 856 869 }) 857 870 if err != nil { ··· 860 873 return 861 874 } 862 875 863 - comment := &models.PullComment{ 864 - OwnerDid: user.Active.Did, 865 - RepoAt: f.RepoAt().String(), 866 - PullId: pull.PullId, 867 - Body: body, 868 - CommentAt: atResp.Uri, 869 - SubmissionId: pull.Submissions[roundNumber].ID, 870 - Mentions: mentions, 871 - References: references, 872 - } 873 - 874 876 // Create the pull comment in the database with the commentAt field 875 - commentId, err := db.NewPullComment(tx, comment) 877 + err = db.PutComment(tx, &comment) 876 878 if err != nil { 877 879 log.Println("failed to create pull comment", err) 878 880 s.pages.Notice(w, "pull-comment", "Failed to create comment.") ··· 886 888 return 887 889 } 888 890 889 - s.notifier.NewPullComment(r.Context(), comment, mentions) 891 + s.notifier.NewPullComment(r.Context(), &comment, mentions) 890 892 891 893 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) 892 - s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", ownerSlashRepo, pull.PullId, commentId)) 894 + s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", ownerSlashRepo, pull.PullId, comment.Id)) 893 895 return 894 896 } 895 897 }
+1
appview/state/state.go
··· 122 122 tangled.StringNSID, 123 123 tangled.RepoIssueNSID, 124 124 tangled.RepoIssueCommentNSID, 125 + tangled.CommentNSID, 125 126 tangled.LabelDefinitionNSID, 126 127 tangled.LabelOpNSID, 127 128 },