Monorepo for Tangled tangled.org

appview: replace IssueComment to Comment #866

open opened by boltless.me targeting master from sl/wnrvrwyvrlzo
Labels

None yet.

assignee

None yet.

Participants 2
AT URI
at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.pull/3m7iohv2yb622
+98 -386
Diff #6
+6 -186
appview/db/issues.go
··· 100 } 101 102 func GetIssuesPaginated(e Execer, page pagination.Page, filters ...orm.Filter) ([]models.Issue, error) { 103 - issueMap := make(map[string]*models.Issue) // at-uri -> issue 104 105 var conditions []string 106 var args []any ··· 196 } 197 } 198 199 - atUri := issue.AtUri().String() 200 - issueMap[atUri] = &issue 201 } 202 203 // collect reverse repos ··· 229 // collect comments 230 issueAts := slices.Collect(maps.Keys(issueMap)) 231 232 - 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 } ··· 295 return GetIssuesPaginated(e, pagination.Page{}, filters...) 296 } 297 298 - func AddIssueComment(tx *sql.Tx, c models.IssueComment) (int64, error) { 299 - result, err := tx.Exec( 300 - `insert into issue_comments ( 301 - did, 302 - rkey, 303 - issue_at, 304 - body, 305 - reply_to, 306 - created, 307 - edited 308 - ) 309 - values (?, ?, ?, ?, ?, ?, null) 310 - on conflict(did, rkey) do update set 311 - issue_at = excluded.issue_at, 312 - body = excluded.body, 313 - edited = case 314 - when 315 - issue_comments.issue_at != excluded.issue_at 316 - or issue_comments.body != excluded.body 317 - or issue_comments.reply_to != excluded.reply_to 318 - then ? 319 - else issue_comments.edited 320 - end`, 321 - c.Did, 322 - c.Rkey, 323 - c.IssueAt, 324 - c.Body, 325 - c.ReplyTo, 326 - c.Created.Format(time.RFC3339), 327 - time.Now().Format(time.RFC3339), 328 - ) 329 - if err != nil { 330 - return 0, err 331 - } 332 - 333 - id, err := result.LastInsertId() 334 - if err != nil { 335 - return 0, err 336 - } 337 - 338 - if err := putReferences(tx, c.AtUri(), c.References); err != nil { 339 - return 0, fmt.Errorf("put reference_links: %w", err) 340 - } 341 - 342 - return id, nil 343 - } 344 - 345 - func DeleteIssueComments(e Execer, filters ...orm.Filter) error { 346 - var conditions []string 347 - var args []any 348 - for _, filter := range filters { 349 - conditions = append(conditions, filter.Condition()) 350 - args = append(args, filter.Arg()...) 351 - } 352 - 353 - whereClause := "" 354 - if conditions != nil { 355 - whereClause = " where " + strings.Join(conditions, " and ") 356 - } 357 - 358 - query := fmt.Sprintf(`update issue_comments set body = "", deleted = strftime('%%Y-%%m-%%dT%%H:%%M:%%SZ', 'now') %s`, whereClause) 359 - 360 - _, err := e.Exec(query, args...) 361 - return err 362 - } 363 - 364 - func GetIssueComments(e Execer, filters ...orm.Filter) ([]models.IssueComment, error) { 365 - commentMap := make(map[string]*models.IssueComment) 366 - 367 - var conditions []string 368 - var args []any 369 - for _, filter := range filters { 370 - conditions = append(conditions, filter.Condition()) 371 - args = append(args, filter.Arg()...) 372 - } 373 - 374 - whereClause := "" 375 - if conditions != nil { 376 - whereClause = " where " + strings.Join(conditions, " and ") 377 - } 378 - 379 - query := fmt.Sprintf(` 380 - select 381 - id, 382 - did, 383 - rkey, 384 - issue_at, 385 - reply_to, 386 - body, 387 - created, 388 - edited, 389 - deleted 390 - from 391 - issue_comments 392 - %s 393 - `, whereClause) 394 - 395 - rows, err := e.Query(query, args...) 396 - if err != nil { 397 - return nil, err 398 - } 399 - defer rows.Close() 400 - 401 - for rows.Next() { 402 - var comment models.IssueComment 403 - var created string 404 - var rkey, edited, deleted, replyTo sql.Null[string] 405 - err := rows.Scan( 406 - &comment.Id, 407 - &comment.Did, 408 - &rkey, 409 - &comment.IssueAt, 410 - &replyTo, 411 - &comment.Body, 412 - &created, 413 - &edited, 414 - &deleted, 415 - ) 416 - if err != nil { 417 - return nil, err 418 - } 419 - 420 - // this is a remnant from old times, newer comments always have rkey 421 - if rkey.Valid { 422 - comment.Rkey = rkey.V 423 - } 424 - 425 - if t, err := time.Parse(time.RFC3339, created); err == nil { 426 - comment.Created = t 427 - } 428 - 429 - if edited.Valid { 430 - if t, err := time.Parse(time.RFC3339, edited.V); err == nil { 431 - comment.Edited = &t 432 - } 433 - } 434 - 435 - if deleted.Valid { 436 - if t, err := time.Parse(time.RFC3339, deleted.V); err == nil { 437 - comment.Deleted = &t 438 - } 439 - } 440 - 441 - if replyTo.Valid { 442 - comment.ReplyTo = &replyTo.V 443 - } 444 - 445 - atUri := comment.AtUri().String() 446 - commentMap[atUri] = &comment 447 - } 448 - 449 - if err = rows.Err(); err != nil { 450 - return nil, err 451 - } 452 - 453 - // collect references for each comments 454 - commentAts := slices.Collect(maps.Keys(commentMap)) 455 - allReferencs, err := GetReferencesAll(e, orm.FilterIn("from_at", commentAts)) 456 - if err != nil { 457 - return nil, fmt.Errorf("failed to query reference_links: %w", err) 458 - } 459 - for commentAt, references := range allReferencs { 460 - if comment, ok := commentMap[commentAt.String()]; ok { 461 - comment.References = references 462 - } 463 - } 464 - 465 - var comments []models.IssueComment 466 - for _, c := range commentMap { 467 - comments = append(comments, *c) 468 - } 469 - 470 - sort.Slice(comments, func(i, j int) bool { 471 - return comments[i].Created.After(comments[j].Created) 472 - }) 473 - 474 - return comments, nil 475 - } 476 - 477 func DeleteIssues(tx *sql.Tx, did, rkey string) error { 478 _, err := tx.Exec( 479 `delete from issues
··· 100 } 101 102 func GetIssuesPaginated(e Execer, page pagination.Page, filters ...orm.Filter) ([]models.Issue, error) { 103 + issueMap := make(map[syntax.ATURI]*models.Issue) // at-uri -> issue 104 105 var conditions []string 106 var args []any ··· 196 } 197 } 198 199 + issueMap[issue.AtUri()] = &issue 200 } 201 202 // collect reverse repos ··· 228 // collect comments 229 issueAts := slices.Collect(maps.Keys(issueMap)) 230 231 + 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 } ··· 294 return GetIssuesPaginated(e, pagination.Page{}, filters...) 295 } 296 297 func DeleteIssues(tx *sql.Tx, did, rkey string) error { 298 _, err := tx.Exec( 299 `delete from issues
+13 -24
appview/db/reference.go
··· 11 "tangled.org/core/orm" 12 ) 13 14 - // ValidateReferenceLinks resolves refLinks to Issue/PR/IssueComment/PullComment ATURIs. 15 // It will ignore missing refLinks. 16 func 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, ","), ··· 79 80 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 ) 13 14 + // ValidateReferenceLinks resolves refLinks to Issue/PR/Comment ATURIs. 15 // It will ignore missing refLinks. 16 func ValidateReferenceLinks(e Execer, refLinks []models.ReferenceLink) ([]syntax.ATURI, error) { 17 var ( ··· 53 values %s 54 ) 55 select 56 + i.at_uri, c.at_uri 57 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, ","), ··· 78 79 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) 89 } else { 90 + uri = syntax.ATURI(issueUri) 91 } 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`,
+15 -6
appview/ingester.go
··· 891 } 892 893 switch e.Commit.Operation { 894 - case jmodels.CommitOperationCreate, jmodels.CommitOperationUpdate: 895 raw := json.RawMessage(e.Commit.Record) 896 record := tangled.RepoIssueComment{} 897 err = json.Unmarshal(raw, &record) ··· 899 return fmt.Errorf("invalid record: %w", err) 900 } 901 902 - comment, err := models.IssueCommentFromRecord(did, rkey, record) 903 if err != nil { 904 return fmt.Errorf("failed to parse comment from record: %w", err) 905 } 906 907 - if err := i.Validator.ValidateIssueComment(comment); err != nil { 908 return fmt.Errorf("failed to validate comment: %w", err) 909 } 910 ··· 914 } 915 defer tx.Rollback() 916 917 - _, err = db.AddIssueComment(tx, *comment) 918 if err != nil { 919 - return fmt.Errorf("failed to create issue comment: %w", err) 920 } 921 922 return tx.Commit() 923 924 case jmodels.CommitOperationDelete: 925 - if err := db.DeleteIssueComments( 926 ddb, 927 orm.FilterEq("did", did), 928 orm.FilterEq("rkey", rkey), 929 ); err != nil { 930 return fmt.Errorf("failed to delete issue comment record: %w", err)
··· 891 } 892 893 switch e.Commit.Operation { 894 + case jmodels.CommitOperationUpdate: 895 raw := json.RawMessage(e.Commit.Record) 896 record := tangled.RepoIssueComment{} 897 err = json.Unmarshal(raw, &record) ··· 899 return fmt.Errorf("invalid record: %w", err) 900 } 901 902 + // convert 'sh.tangled.repo.issue.comment' to 'sh.tangled.comment' 903 + comment, err := models.CommentFromRecord(syntax.DID(did), syntax.RecordKey(rkey), tangled.Comment{ 904 + Body: record.Body, 905 + CreatedAt: record.CreatedAt, 906 + Mentions: record.Mentions, 907 + References: record.References, 908 + ReplyTo: record.ReplyTo, 909 + Subject: record.Issue, 910 + }) 911 if err != nil { 912 return fmt.Errorf("failed to parse comment from record: %w", err) 913 } 914 915 + if err := comment.Validate(); err != nil { 916 return fmt.Errorf("failed to validate comment: %w", err) 917 } 918 ··· 922 } 923 defer tx.Rollback() 924 925 + err = db.PutComment(tx, comment) 926 if err != nil { 927 + return fmt.Errorf("failed to create comment: %w", err) 928 } 929 930 return tx.Commit() 931 932 case jmodels.CommitOperationDelete: 933 + if err := db.DeleteComments( 934 ddb, 935 orm.FilterEq("did", did), 936 + orm.FilterEq("collection", e.Commit.Collection), 937 orm.FilterEq("rkey", rkey), 938 ); err != nil { 939 return fmt.Errorf("failed to delete issue comment record: %w", err)
+38 -36
appview/issues/issues.go
··· 402 403 body := r.FormValue("body") 404 if body == "" { 405 - rp.pages.Notice(w, "issue", "Body is required") 406 return 407 } 408 409 - replyToUri := r.FormValue("reply-to") 410 - var replyTo *string 411 - if replyToUri != "" { 412 - replyTo = &replyToUri 413 } 414 415 mentions, references := rp.mentionsResolver.Resolve(r.Context(), body) 416 417 - comment := models.IssueComment{ 418 - Did: user.Active.Did, 419 Rkey: tid.TID(), 420 - IssueAt: issue.AtUri().String(), 421 ReplyTo: replyTo, 422 Body: body, 423 Created: time.Now(), 424 Mentions: mentions, 425 References: references, 426 } 427 - if err = rp.validator.ValidateIssueComment(&comment); err != nil { 428 l.Error("failed to validate comment", "err", err) 429 rp.pages.Notice(w, "issue-comment", "Failed to create comment.") 430 return 431 } 432 - record := comment.AsRecord() 433 434 client, err := rp.oauth.AuthorizedClient(r) 435 if err != nil { ··· 440 441 // create a record first 442 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 443 - Collection: tangled.RepoIssueCommentNSID, 444 - Repo: comment.Did, 445 Rkey: comment.Rkey, 446 Record: &lexutil.LexiconTypeDecoder{ 447 - Val: &record, 448 }, 449 }) 450 if err != nil { ··· 467 } 468 defer tx.Rollback() 469 470 - commentId, err := db.AddIssueComment(tx, comment) 471 if err != nil { 472 l.Error("failed to create comment", "err", err) 473 rp.pages.Notice(w, "issue-comment", "Failed to create comment.") ··· 483 // reset atUri to make rollback a no-op 484 atUri = "" 485 486 - // notify about the new comment 487 - comment.Id = commentId 488 - 489 rp.notifier.NewIssueComment(r.Context(), &comment, mentions) 490 491 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) 492 - rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", ownerSlashRepo, issue.IssueId, commentId)) 493 } 494 495 func (rp *Issues) IssueComment(w http.ResponseWriter, r *http.Request) { ··· 504 } 505 506 commentId := chi.URLParam(r, "commentId") 507 - comments, err := db.GetIssueComments( 508 rp.db, 509 orm.FilterEq("id", commentId), 510 ) ··· 540 } 541 542 commentId := chi.URLParam(r, "commentId") 543 - comments, err := db.GetIssueComments( 544 rp.db, 545 orm.FilterEq("id", commentId), 546 ) ··· 556 } 557 comment := comments[0] 558 559 - if comment.Did != user.Active.Did { 560 l.Error("unauthorized comment edit", "expectedDid", comment.Did, "gotDid", user.Active.Did) 561 http.Error(w, "you are not the author of this comment", http.StatusUnauthorized) 562 return ··· 586 newComment.Edited = &now 587 newComment.Mentions, newComment.References = rp.mentionsResolver.Resolve(r.Context(), newBody) 588 589 - record := newComment.AsRecord() 590 - 591 tx, err := rp.db.Begin() 592 if err != nil { 593 l.Error("failed to start transaction", "err", err) ··· 596 } 597 defer tx.Rollback() 598 599 - _, err = db.AddIssueComment(tx, newComment) 600 if err != nil { 601 l.Error("failed to perferom update-description query", "err", err) 602 rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.") ··· 606 607 // rkey is optional, it was introduced later 608 if newComment.Rkey != "" { 609 // update the record on pds 610 - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueCommentNSID, user.Active.Did, comment.Rkey) 611 if err != nil { 612 l.Error("failed to get record", "err", err, "did", newComment.Did, "rkey", newComment.Rkey) 613 - rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.") 614 return 615 } 616 617 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 618 - Collection: tangled.RepoIssueCommentNSID, 619 - Repo: user.Active.Did, 620 Rkey: newComment.Rkey, 621 SwapRecord: ex.Cid, 622 Record: &lexutil.LexiconTypeDecoder{ 623 - Val: &record, 624 }, 625 }) 626 if err != nil { ··· 650 } 651 652 commentId := chi.URLParam(r, "commentId") 653 - comments, err := db.GetIssueComments( 654 rp.db, 655 orm.FilterEq("id", commentId), 656 ) ··· 686 } 687 688 commentId := chi.URLParam(r, "commentId") 689 - comments, err := db.GetIssueComments( 690 rp.db, 691 orm.FilterEq("id", commentId), 692 ) ··· 722 } 723 724 commentId := chi.URLParam(r, "commentId") 725 - comments, err := db.GetIssueComments( 726 rp.db, 727 orm.FilterEq("id", commentId), 728 ) ··· 738 } 739 comment := comments[0] 740 741 - if comment.Did != user.Active.Did { 742 l.Error("unauthorized action", "expectedDid", comment.Did, "gotDid", user.Active.Did) 743 http.Error(w, "you are not the author of this comment", http.StatusUnauthorized) 744 return ··· 751 752 // optimistic deletion 753 deleted := time.Now() 754 - err = db.DeleteIssueComments(rp.db, orm.FilterEq("id", comment.Id)) 755 if err != nil { 756 l.Error("failed to delete comment", "err", err) 757 rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment") ··· 767 return 768 } 769 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 770 - Collection: tangled.RepoIssueCommentNSID, 771 - Repo: user.Active.Did, 772 Rkey: comment.Rkey, 773 }) 774 if err != nil {
··· 402 403 body := r.FormValue("body") 404 if body == "" { 405 + rp.pages.Notice(w, "issue-comment", "Body is required") 406 return 407 } 408 409 + var replyTo *syntax.ATURI 410 + replyToRaw := r.FormValue("reply-to") 411 + if replyToRaw != "" { 412 + aturi, err := syntax.ParseATURI(replyToRaw) 413 + if err != nil { 414 + rp.pages.Notice(w, "issue-comment", "reply-to should be valid AT-URI") 415 + return 416 + } 417 + replyTo = &aturi 418 } 419 420 mentions, references := rp.mentionsResolver.Resolve(r.Context(), body) 421 422 + comment := models.Comment{ 423 + Did: syntax.DID(user.Active.Did), 424 + Collection: tangled.CommentNSID, 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 437 } 438 439 client, err := rp.oauth.AuthorizedClient(r) 440 if err != nil { ··· 445 446 // create a record first 447 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 448 + Collection: comment.Collection.String(), 449 + Repo: comment.Did.String(), 450 Rkey: comment.Rkey, 451 Record: &lexutil.LexiconTypeDecoder{ 452 + Val: comment.AsRecord(), 453 }, 454 }) 455 if err != nil { ··· 472 } 473 defer tx.Rollback() 474 475 + err = db.PutComment(tx, &comment) 476 if err != nil { 477 l.Error("failed to create comment", "err", err) 478 rp.pages.Notice(w, "issue-comment", "Failed to create comment.") ··· 488 // reset atUri to make rollback a no-op 489 atUri = "" 490 491 rp.notifier.NewIssueComment(r.Context(), &comment, mentions) 492 493 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) 494 + rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", ownerSlashRepo, issue.IssueId, comment.Id)) 495 } 496 497 func (rp *Issues) IssueComment(w http.ResponseWriter, r *http.Request) { ··· 506 } 507 508 commentId := chi.URLParam(r, "commentId") 509 + comments, err := db.GetComments( 510 rp.db, 511 orm.FilterEq("id", commentId), 512 ) ··· 542 } 543 544 commentId := chi.URLParam(r, "commentId") 545 + comments, err := db.GetComments( 546 rp.db, 547 orm.FilterEq("id", commentId), 548 ) ··· 558 } 559 comment := comments[0] 560 561 + if comment.Did.String() != user.Active.Did { 562 l.Error("unauthorized comment edit", "expectedDid", comment.Did, "gotDid", user.Active.Did) 563 http.Error(w, "you are not the author of this comment", http.StatusUnauthorized) 564 return ··· 588 newComment.Edited = &now 589 newComment.Mentions, newComment.References = rp.mentionsResolver.Resolve(r.Context(), newBody) 590 591 tx, err := rp.db.Begin() 592 if err != nil { 593 l.Error("failed to start transaction", "err", err) ··· 596 } 597 defer tx.Rollback() 598 599 + err = db.PutComment(tx, &newComment) 600 if err != nil { 601 l.Error("failed to perferom update-description query", "err", err) 602 rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.") ··· 606 607 // rkey is optional, it was introduced later 608 if newComment.Rkey != "" { 609 + // TODO: update correct comment 610 + 611 // update the record on pds 612 + ex, err := comatproto.RepoGetRecord(r.Context(), client, "", newComment.Collection.String(), newComment.Did.String(), newComment.Rkey) 613 if err != nil { 614 l.Error("failed to get record", "err", err, "did", newComment.Did, "rkey", newComment.Rkey) 615 + rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update comment, no record found on PDS.") 616 return 617 } 618 619 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 620 + Collection: newComment.Collection.String(), 621 + Repo: newComment.Did.String(), 622 Rkey: newComment.Rkey, 623 SwapRecord: ex.Cid, 624 Record: &lexutil.LexiconTypeDecoder{ 625 + Val: newComment.AsRecord(), 626 }, 627 }) 628 if err != nil { ··· 652 } 653 654 commentId := chi.URLParam(r, "commentId") 655 + comments, err := db.GetComments( 656 rp.db, 657 orm.FilterEq("id", commentId), 658 ) ··· 688 } 689 690 commentId := chi.URLParam(r, "commentId") 691 + comments, err := db.GetComments( 692 rp.db, 693 orm.FilterEq("id", commentId), 694 ) ··· 724 } 725 726 commentId := chi.URLParam(r, "commentId") 727 + comments, err := db.GetComments( 728 rp.db, 729 orm.FilterEq("id", commentId), 730 ) ··· 740 } 741 comment := comments[0] 742 743 + if comment.Did.String() != user.Active.Did { 744 l.Error("unauthorized action", "expectedDid", comment.Did, "gotDid", user.Active.Did) 745 http.Error(w, "you are not the author of this comment", http.StatusUnauthorized) 746 return ··· 753 754 // optimistic deletion 755 deleted := time.Now() 756 + err = db.DeleteComments(rp.db, orm.FilterEq("id", comment.Id)) 757 if err != nil { 758 l.Error("failed to delete comment", "err", err) 759 rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment") ··· 769 return 770 } 771 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 772 + Collection: comment.Collection.String(), 773 + Repo: comment.Did.String(), 774 Rkey: comment.Rkey, 775 }) 776 if err != nil {
+8 -89
appview/models/issue.go
··· 26 27 // 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 } 63 64 type CommentListItem struct { 65 - Self *IssueComment 66 - Replies []*IssueComment 67 } 68 69 func (it *CommentListItem) Participants() []syntax.DID { ··· 88 89 func (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 93 94 // 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 } 116 117 // 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) 145 146 for _, c := range i.Comments { 147 - addParticipant(c.Did) 148 } 149 150 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 - }
··· 26 27 // 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 } 63 64 type CommentListItem struct { 65 + Self *Comment 66 + Replies []*Comment 67 } 68 69 func (it *CommentListItem) Participants() []syntax.DID { ··· 88 89 func (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 93 94 // 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 } 116 117 // 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) 145 146 for _, c := range i.Comments { 147 + addParticipant(c.Did.String()) 148 } 149 150 return participants ··· 171 Open: true, // new issues are open by default 172 } 173 }
+4 -4
appview/notify/db/db.go
··· 122 ) 123 } 124 125 - 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] ··· 147 148 // 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 } 124 125 + 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] ··· 147 148 // 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 }
+1 -1
appview/notify/merged_notifier.go
··· 57 m.fanout("NewIssue", ctx, issue, mentions) 58 } 59 60 - func (m *mergedNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) { 61 m.fanout("NewIssueComment", ctx, comment, mentions) 62 } 63
··· 57 m.fanout("NewIssue", ctx, issue, mentions) 58 } 59 60 + func (m *mergedNotifier) NewIssueComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID) { 61 m.fanout("NewIssueComment", ctx, comment, mentions) 62 } 63
+2 -2
appview/notify/notifier.go
··· 14 DeleteStar(ctx context.Context, star *models.Star) 15 16 NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) 17 - NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) 18 NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) 19 DeleteIssue(ctx context.Context, issue *models.Issue) 20 ··· 43 func (m *BaseNotifier) DeleteStar(ctx context.Context, star *models.Star) {} 44 45 func (m *BaseNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) {} 46 - func (m *BaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) { 47 } 48 func (m *BaseNotifier) NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) {} 49 func (m *BaseNotifier) DeleteIssue(ctx context.Context, issue *models.Issue) {}
··· 14 DeleteStar(ctx context.Context, star *models.Star) 15 16 NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) 17 + NewIssueComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID) 18 NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) 19 DeleteIssue(ctx context.Context, issue *models.Issue) 20 ··· 43 func (m *BaseNotifier) DeleteStar(ctx context.Context, star *models.Star) {} 44 45 func (m *BaseNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) {} 46 + func (m *BaseNotifier) NewIssueComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID) { 47 } 48 func (m *BaseNotifier) NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) {} 49 func (m *BaseNotifier) DeleteIssue(ctx context.Context, issue *models.Issue) {}
+3 -3
appview/notify/posthog/notifier.go
··· 179 } 180 } 181 182 - func (n *posthogNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) { 183 err := n.client.Enqueue(posthog.Capture{ 184 - DistinctId: comment.Did, 185 Event: "new_issue_comment", 186 Properties: posthog.Properties{ 187 - "issue_at": comment.IssueAt, 188 "mentions": mentions, 189 }, 190 })
··· 179 } 180 } 181 182 + func (n *posthogNotifier) NewIssueComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID) { 183 err := n.client.Enqueue(posthog.Capture{ 184 + DistinctId: comment.Did.String(), 185 Event: "new_issue_comment", 186 Properties: posthog.Properties{ 187 + "issue_at": comment.Subject, 188 "mentions": mentions, 189 }, 190 })
+4 -4
appview/pages/pages.go
··· 1003 LoggedInUser *oauth.MultiAccountUser 1004 RepoInfo repoinfo.RepoInfo 1005 Issue *models.Issue 1006 - Comment *models.IssueComment 1007 } 1008 1009 func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error { ··· 1014 LoggedInUser *oauth.MultiAccountUser 1015 RepoInfo repoinfo.RepoInfo 1016 Issue *models.Issue 1017 - Comment *models.IssueComment 1018 } 1019 1020 func (p *Pages) ReplyIssueCommentPlaceholderFragment(w io.Writer, params ReplyIssueCommentPlaceholderParams) error { ··· 1025 LoggedInUser *oauth.MultiAccountUser 1026 RepoInfo repoinfo.RepoInfo 1027 Issue *models.Issue 1028 - Comment *models.IssueComment 1029 } 1030 1031 func (p *Pages) ReplyIssueCommentFragment(w io.Writer, params ReplyIssueCommentParams) error { ··· 1036 LoggedInUser *oauth.MultiAccountUser 1037 RepoInfo repoinfo.RepoInfo 1038 Issue *models.Issue 1039 - Comment *models.IssueComment 1040 } 1041 1042 func (p *Pages) IssueCommentBodyFragment(w io.Writer, params IssueCommentBodyParams) error {
··· 1003 LoggedInUser *oauth.MultiAccountUser 1004 RepoInfo repoinfo.RepoInfo 1005 Issue *models.Issue 1006 + Comment *models.Comment 1007 } 1008 1009 func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error { ··· 1014 LoggedInUser *oauth.MultiAccountUser 1015 RepoInfo repoinfo.RepoInfo 1016 Issue *models.Issue 1017 + Comment *models.Comment 1018 } 1019 1020 func (p *Pages) ReplyIssueCommentPlaceholderFragment(w io.Writer, params ReplyIssueCommentPlaceholderParams) error { ··· 1025 LoggedInUser *oauth.MultiAccountUser 1026 RepoInfo repoinfo.RepoInfo 1027 Issue *models.Issue 1028 + Comment *models.Comment 1029 } 1030 1031 func (p *Pages) ReplyIssueCommentFragment(w io.Writer, params ReplyIssueCommentParams) error { ··· 1036 LoggedInUser *oauth.MultiAccountUser 1037 RepoInfo repoinfo.RepoInfo 1038 Issue *models.Issue 1039 + Comment *models.Comment 1040 } 1041 1042 func (p *Pages) IssueCommentBodyFragment(w io.Writer, params IssueCommentBodyParams) error {
+2 -2
appview/pages/templates/repo/issues/fragments/commentList.html
··· 42 <div class="rounded px-6 py-4 bg-white dark:bg-gray-800 flex gap-2 "> 43 <div class="flex-shrink-0"> 44 <img 45 - src="{{ tinyAvatar .Comment.Did }}" 46 alt="" 47 class="rounded-full size-8 mr-1 border-2 border-gray-100 dark:border-gray-900" 48 /> ··· 58 <div class="py-4 pr-4 w-full mx-auto overflow-hidden flex gap-2 "> 59 <div class="flex-shrink-0"> 60 <img 61 - src="{{ tinyAvatar .Comment.Did }}" 62 alt="" 63 class="rounded-full size-8 mr-1 border-2 border-gray-100 dark:border-gray-900" 64 />
··· 42 <div class="rounded px-6 py-4 bg-white dark:bg-gray-800 flex gap-2 "> 43 <div class="flex-shrink-0"> 44 <img 45 + src="{{ tinyAvatar .Comment.Did.String }}" 46 alt="" 47 class="rounded-full size-8 mr-1 border-2 border-gray-100 dark:border-gray-900" 48 /> ··· 58 <div class="py-4 pr-4 w-full mx-auto overflow-hidden flex gap-2 "> 59 <div class="flex-shrink-0"> 60 <img 61 + src="{{ tinyAvatar .Comment.Did.String }}" 62 alt="" 63 class="rounded-full size-8 mr-1 border-2 border-gray-100 dark:border-gray-900" 64 />
+2 -2
appview/pages/templates/repo/issues/fragments/issueCommentHeader.html
··· 1 {{ define "repo/issues/fragments/issueCommentHeader" }} 2 <div class="flex flex-wrap items-center gap-2 text-sm text-gray-500 dark:text-gray-400 "> 3 - {{ $handle := resolve .Comment.Did }} 4 <a class="text-gray-500 dark:text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" href="/{{ $handle }}">{{ $handle }}</a> 5 {{ template "hats" $ }} 6 <span class="before:content-['路']"></span> 7 {{ template "timestamp" . }} 8 - {{ $isCommentOwner := and .LoggedInUser (eq .LoggedInUser.Did .Comment.Did) }} 9 {{ if and $isCommentOwner (not .Comment.Deleted) }} 10 {{ template "editIssueComment" . }} 11 {{ template "deleteIssueComment" . }}
··· 1 {{ define "repo/issues/fragments/issueCommentHeader" }} 2 <div class="flex flex-wrap items-center gap-2 text-sm text-gray-500 dark:text-gray-400 "> 3 + {{ $handle := resolve .Comment.Did.String }} 4 <a class="text-gray-500 dark:text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" href="/{{ $handle }}">{{ $handle }}</a> 5 {{ template "hats" $ }} 6 <span class="before:content-['路']"></span> 7 {{ template "timestamp" . }} 8 + {{ $isCommentOwner := and .LoggedInUser (eq .LoggedInUser.Did .Comment.Did.String) }} 9 {{ if and $isCommentOwner (not .Comment.Deleted) }} 10 {{ template "editIssueComment" . }} 11 {{ template "deleteIssueComment" . }}
-27
appview/validator/issue.go
··· 4 "fmt" 5 "strings" 6 7 - "tangled.org/core/appview/db" 8 "tangled.org/core/appview/models" 9 - "tangled.org/core/orm" 10 ) 11 12 - func (v *Validator) ValidateIssueComment(comment *models.IssueComment) error { 13 - // if comments have parents, only ingest ones that are 1 level deep 14 - if comment.ReplyTo != nil { 15 - parents, err := db.GetIssueComments(v.db, orm.FilterEq("at_uri", *comment.ReplyTo)) 16 - if err != nil { 17 - return fmt.Errorf("failed to fetch parent comment: %w", err) 18 - } 19 - if len(parents) != 1 { 20 - return fmt.Errorf("incorrect number of parent comments returned: %d", len(parents)) 21 - } 22 - 23 - // depth check 24 - parent := parents[0] 25 - if parent.ReplyTo != nil { 26 - return fmt.Errorf("incorrect depth, this comment is replying at depth >1") 27 - } 28 - } 29 - 30 - if sb := strings.TrimSpace(v.sanitizer.SanitizeDefault(comment.Body)); sb == "" { 31 - return fmt.Errorf("body is empty after HTML sanitization") 32 - } 33 - 34 - return nil 35 - } 36 - 37 func (v *Validator) ValidateIssue(issue *models.Issue) error { 38 if issue.Title == "" { 39 return fmt.Errorf("issue title is empty")
··· 4 "fmt" 5 "strings" 6 7 "tangled.org/core/appview/models" 8 ) 9 10 func (v *Validator) ValidateIssue(issue *models.Issue) error { 11 if issue.Title == "" { 12 return fmt.Errorf("issue title is empty")

History

8 rounds 1 comment
sign up or login to add to the discussion
1 commit
expand
appview: replace IssueComment to Comment
2/3 failed, 1/3 success
expand
merge conflicts detected
expand
  • appview/notify/db/db.go:260
  • appview/notify/merged_notifier.go:81
  • appview/notify/notifier.go:22
  • appview/pulls/opengraph.go:277
expand 0 comments
1 commit
expand
appview: replace IssueComment to Comment
2/3 failed, 1/3 success
expand
expand 0 comments
1 commit
expand
appview: replace IssueComment to Comment
3/3 success
expand
expand 0 comments
1 commit
expand
appview: replace IssueComment to Comment
3/3 success
expand
expand 0 comments
1 commit
expand
appview: replace IssueComment to Comment
1/3 failed, 2/3 success
expand
expand 0 comments
1 commit
expand
appview: replace IssueComment to Comment
1/3 failed, 2/3 success
expand
expand 0 comments
1 commit
expand
appview: replace IssueComment to Comment
3/3 success
expand
expand 1 comment

imo: we should still continue to ingest sh.tangled.issue.comment (and pull comments), but just try to convert to the regular lexicon. this would only be used for backfill purposes, newer appviews should ideally not create the old NSID anymore.

the way i see it, the new lexicon is just an alias for the old ones, so the ingesters should be backwards compatible.

i will do a deeper dive into the code itself shortly.

1 commit
expand
appview: replace IssueComment to Comment
3/3 success
expand
expand 0 comments