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
+93 -380
Diff #0
+6 -185
appview/db/issues.go
··· 99 } 100 101 func GetIssuesPaginated(e Execer, page pagination.Page, filters ...filter) ([]models.Issue, error) { 102 - issueMap := make(map[string]*models.Issue) // at-uri -> issue 103 104 var conditions []string 105 var args []any ··· 195 } 196 } 197 198 - atUri := issue.AtUri().String() 199 - issueMap[atUri] = &issue 200 } 201 202 // collect reverse repos ··· 228 // collect comments 229 issueAts := slices.Collect(maps.Keys(issueMap)) 230 231 - comments, err := GetIssueComments(e, FilterIn("issue_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].IssueAt 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.String()]; 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.String()]; ok { 260 issue.References = references 261 } 262 } ··· 350 return ids, nil 351 } 352 353 - func AddIssueComment(tx *sql.Tx, c models.IssueComment) (int64, error) { 354 - result, err := tx.Exec( 355 - `insert into issue_comments ( 356 - did, 357 - rkey, 358 - issue_at, 359 - body, 360 - reply_to, 361 - created, 362 - edited 363 - ) 364 - values (?, ?, ?, ?, ?, ?, null) 365 - on conflict(did, rkey) do update set 366 - issue_at = excluded.issue_at, 367 - body = excluded.body, 368 - edited = case 369 - when 370 - issue_comments.issue_at != excluded.issue_at 371 - or issue_comments.body != excluded.body 372 - or issue_comments.reply_to != excluded.reply_to 373 - then ? 374 - else issue_comments.edited 375 - end`, 376 - c.Did, 377 - c.Rkey, 378 - c.IssueAt, 379 - c.Body, 380 - c.ReplyTo, 381 - c.Created.Format(time.RFC3339), 382 - time.Now().Format(time.RFC3339), 383 - ) 384 - if err != nil { 385 - return 0, err 386 - } 387 - 388 - id, err := result.LastInsertId() 389 - if err != nil { 390 - return 0, err 391 - } 392 - 393 - if err := putReferences(tx, c.AtUri(), c.References); err != nil { 394 - return 0, fmt.Errorf("put reference_links: %w", err) 395 - } 396 - 397 - return id, nil 398 - } 399 - 400 - func DeleteIssueComments(e Execer, filters ...filter) error { 401 - var conditions []string 402 - var args []any 403 - for _, filter := range filters { 404 - conditions = append(conditions, filter.Condition()) 405 - args = append(args, filter.Arg()...) 406 - } 407 - 408 - whereClause := "" 409 - if conditions != nil { 410 - whereClause = " where " + strings.Join(conditions, " and ") 411 - } 412 - 413 - query := fmt.Sprintf(`update issue_comments set body = "", deleted = strftime('%%Y-%%m-%%dT%%H:%%M:%%SZ', 'now') %s`, whereClause) 414 - 415 - _, err := e.Exec(query, args...) 416 - return err 417 - } 418 - 419 - func GetIssueComments(e Execer, filters ...filter) ([]models.IssueComment, error) { 420 - commentMap := make(map[string]*models.IssueComment) 421 - 422 - var conditions []string 423 - var args []any 424 - for _, filter := range filters { 425 - conditions = append(conditions, filter.Condition()) 426 - args = append(args, filter.Arg()...) 427 - } 428 - 429 - whereClause := "" 430 - if conditions != nil { 431 - whereClause = " where " + strings.Join(conditions, " and ") 432 - } 433 - 434 - query := fmt.Sprintf(` 435 - select 436 - id, 437 - did, 438 - rkey, 439 - issue_at, 440 - reply_to, 441 - body, 442 - created, 443 - edited, 444 - deleted 445 - from 446 - issue_comments 447 - %s 448 - `, whereClause) 449 - 450 - rows, err := e.Query(query, args...) 451 - if err != nil { 452 - return nil, err 453 - } 454 - 455 - for rows.Next() { 456 - var comment models.IssueComment 457 - var created string 458 - var rkey, edited, deleted, replyTo sql.Null[string] 459 - err := rows.Scan( 460 - &comment.Id, 461 - &comment.Did, 462 - &rkey, 463 - &comment.IssueAt, 464 - &replyTo, 465 - &comment.Body, 466 - &created, 467 - &edited, 468 - &deleted, 469 - ) 470 - if err != nil { 471 - return nil, err 472 - } 473 - 474 - // this is a remnant from old times, newer comments always have rkey 475 - if rkey.Valid { 476 - comment.Rkey = rkey.V 477 - } 478 - 479 - if t, err := time.Parse(time.RFC3339, created); err == nil { 480 - comment.Created = t 481 - } 482 - 483 - if edited.Valid { 484 - if t, err := time.Parse(time.RFC3339, edited.V); err == nil { 485 - comment.Edited = &t 486 - } 487 - } 488 - 489 - if deleted.Valid { 490 - if t, err := time.Parse(time.RFC3339, deleted.V); err == nil { 491 - comment.Deleted = &t 492 - } 493 - } 494 - 495 - if replyTo.Valid { 496 - comment.ReplyTo = &replyTo.V 497 - } 498 - 499 - atUri := comment.AtUri().String() 500 - commentMap[atUri] = &comment 501 - } 502 - 503 - if err = rows.Err(); err != nil { 504 - return nil, err 505 - } 506 - 507 - // collect references for each comments 508 - commentAts := slices.Collect(maps.Keys(commentMap)) 509 - allReferencs, err := GetReferencesAll(e, FilterIn("from_at", commentAts)) 510 - if err != nil { 511 - return nil, fmt.Errorf("failed to query reference_links: %w", err) 512 - } 513 - for commentAt, references := range allReferencs { 514 - if comment, ok := commentMap[commentAt.String()]; ok { 515 - comment.References = references 516 - } 517 - } 518 - 519 - var comments []models.IssueComment 520 - for _, c := range commentMap { 521 - comments = append(comments, *c) 522 - } 523 - 524 - sort.Slice(comments, func(i, j int) bool { 525 - return comments[i].Created.After(comments[j].Created) 526 - }) 527 - 528 - return comments, nil 529 - } 530 - 531 func DeleteIssues(tx *sql.Tx, did, rkey string) error { 532 _, err := tx.Exec( 533 `delete from issues
··· 99 } 100 101 func GetIssuesPaginated(e Execer, page pagination.Page, filters ...filter) ([]models.Issue, error) { 102 + issueMap := make(map[syntax.ATURI]*models.Issue) // at-uri -> issue 103 104 var conditions []string 105 var args []any ··· 195 } 196 } 197 198 + issueMap[issue.AtUri()] = &issue 199 } 200 201 // collect reverse repos ··· 227 // collect comments 228 issueAts := slices.Collect(maps.Keys(issueMap)) 229 230 + comments, err := GetComments(e, FilterIn("subject_at", issueAts)) 231 if err != nil { 232 return nil, fmt.Errorf("failed to query comments: %w", err) 233 } 234 for i := range comments { 235 + issueAt := comments[i].Subject 236 if issue, ok := issueMap[issueAt]; ok { 237 issue.Comments = append(issue.Comments, comments[i]) 238 } ··· 244 return nil, fmt.Errorf("failed to query labels: %w", err) 245 } 246 for issueAt, labels := range allLabels { 247 + if issue, ok := issueMap[issueAt]; ok { 248 issue.Labels = labels 249 } 250 } ··· 255 return nil, fmt.Errorf("failed to query reference_links: %w", err) 256 } 257 for issueAt, references := range allReferencs { 258 + if issue, ok := issueMap[issueAt]; ok { 259 issue.References = references 260 } 261 } ··· 349 return ids, nil 350 } 351 352 func DeleteIssues(tx *sql.Tx, did, rkey string) error { 353 _, err := tx.Exec( 354 `delete from issues
+13 -24
appview/db/reference.go
··· 10 "tangled.org/core/appview/models" 11 ) 12 13 - // ValidateReferenceLinks resolves refLinks to Issue/PR/IssueComment/PullComment ATURIs. 14 // It will ignore missing refLinks. 15 func ValidateReferenceLinks(e Execer, refLinks []models.ReferenceLink) ([]syntax.ATURI, error) { 16 var ( ··· 52 values %s 53 ) 54 select 55 - i.did, i.rkey, 56 - c.did, c.rkey 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 issue_comments c 65 on inp.comment_id is not null 66 - and c.issue_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 issueOwner, issueRkey string 82 - var commentOwner, commentRkey sql.NullString 83 var uri syntax.ATURI 84 - if err := rows.Scan(&issueOwner, &issueRkey, &commentOwner, &commentRkey); err != nil { 85 return nil, err 86 } 87 - if commentOwner.Valid && commentRkey.Valid { 88 - uri = syntax.ATURI(fmt.Sprintf( 89 - "at://%s/%s/%s", 90 - commentOwner.String, 91 - tangled.RepoIssueCommentNSID, 92 - commentRkey.String, 93 - )) 94 } else { 95 - uri = syntax.ATURI(fmt.Sprintf( 96 - "at://%s/%s/%s", 97 - issueOwner, 98 - tangled.RepoIssueNSID, 99 - issueRkey, 100 - )) 101 } 102 uris = append(uris, uri) 103 } ··· 281 return nil, fmt.Errorf("get issue backlinks: %w", err) 282 } 283 backlinks = append(backlinks, ls...) 284 - ls, err = getIssueCommentBacklinks(e, backlinksMap[tangled.RepoIssueCommentNSID]) 285 if err != nil { 286 return nil, fmt.Errorf("get issue_comment backlinks: %w", err) 287 } ··· 350 rows, err := e.Query( 351 fmt.Sprintf( 352 `select r.did, r.name, i.issue_id, c.id, i.title, i.open 353 - from issue_comments c 354 join issues i 355 - on i.at_uri = c.issue_at 356 join repos r 357 on r.at_uri = i.repo_at 358 where %s`,
··· 10 "tangled.org/core/appview/models" 11 ) 12 13 + // ValidateReferenceLinks resolves refLinks to Issue/PR/Comment ATURIs. 14 // It will ignore missing refLinks. 15 func ValidateReferenceLinks(e Execer, refLinks []models.ReferenceLink) ([]syntax.ATURI, error) { 16 var ( ··· 52 values %s 53 ) 54 select 55 + i.at_uri, c.at_uri 56 from input inp 57 join repos r 58 on r.did = inp.owner_did ··· 60 join issues i 61 on i.repo_at = r.at_uri 62 and i.issue_id = inp.issue_id 63 + left join comments c 64 on inp.comment_id is not null 65 + and c.subject_at = i.at_uri 66 and c.id = inp.comment_id 67 `, 68 strings.Join(vals, ","), ··· 77 78 for rows.Next() { 79 // Scan rows 80 + var issueUri string 81 + var commentUri sql.NullString 82 var uri syntax.ATURI 83 + if err := rows.Scan(&issueUri, &commentUri); err != nil { 84 return nil, err 85 } 86 + if commentUri.Valid { 87 + uri = syntax.ATURI(commentUri.String) 88 } else { 89 + uri = syntax.ATURI(issueUri) 90 } 91 uris = append(uris, uri) 92 } ··· 270 return nil, fmt.Errorf("get issue backlinks: %w", err) 271 } 272 backlinks = append(backlinks, ls...) 273 + ls, err = getIssueCommentBacklinks(e, backlinksMap[tangled.CommentNSID]) 274 if err != nil { 275 return nil, fmt.Errorf("get issue_comment backlinks: %w", err) 276 } ··· 339 rows, err := e.Query( 340 fmt.Sprintf( 341 `select r.did, r.name, i.issue_id, c.id, i.title, i.open 342 + from comments c 343 join issues i 344 + on i.at_uri = c.subject_at 345 join repos r 346 on r.at_uri = i.repo_at 347 where %s`,
+19 -11
appview/ingester.go
··· 78 err = i.ingestString(e) 79 case tangled.RepoIssueNSID: 80 err = i.ingestIssue(ctx, e) 81 - case tangled.RepoIssueCommentNSID: 82 - err = i.ingestIssueComment(e) 83 case tangled.LabelDefinitionNSID: 84 err = i.ingestLabelDefinition(e) 85 case tangled.LabelOpNSID: ··· 867 return nil 868 } 869 870 - func (i *Ingester) ingestIssueComment(e *jmodels.Event) error { 871 did := e.Did 872 rkey := e.Commit.RKey 873 874 var err error 875 876 - l := i.Logger.With("handler", "ingestIssueComment", "nsid", e.Commit.Collection, "did", did, "rkey", rkey) 877 l.Info("ingesting record") 878 879 ddb, ok := i.Db.Execer.(*db.DB) ··· 884 switch e.Commit.Operation { 885 case jmodels.CommitOperationCreate, jmodels.CommitOperationUpdate: 886 raw := json.RawMessage(e.Commit.Record) 887 - record := tangled.RepoIssueComment{} 888 err = json.Unmarshal(raw, &record) 889 if err != nil { 890 return fmt.Errorf("invalid record: %w", err) 891 } 892 893 - comment, err := models.IssueCommentFromRecord(did, rkey, record) 894 if err != nil { 895 return fmt.Errorf("failed to parse comment from record: %w", err) 896 } 897 898 - if err := i.Validator.ValidateIssueComment(comment); err != nil { 899 return fmt.Errorf("failed to validate comment: %w", err) 900 } 901 ··· 905 } 906 defer tx.Rollback() 907 908 - _, err = db.AddIssueComment(tx, *comment) 909 if err != nil { 910 - return fmt.Errorf("failed to create issue comment: %w", err) 911 } 912 913 return tx.Commit() 914 915 case jmodels.CommitOperationDelete: 916 - if err := db.DeleteIssueComments( 917 ddb, 918 db.FilterEq("did", did), 919 db.FilterEq("rkey", rkey), 920 ); err != nil { 921 - return fmt.Errorf("failed to delete issue comment record: %w", err) 922 } 923 924 return nil
··· 78 err = i.ingestString(e) 79 case tangled.RepoIssueNSID: 80 err = i.ingestIssue(ctx, e) 81 + case tangled.CommentNSID: 82 + err = i.ingestComment(e) 83 case tangled.LabelDefinitionNSID: 84 err = i.ingestLabelDefinition(e) 85 case tangled.LabelOpNSID: ··· 867 return nil 868 } 869 870 + func (i *Ingester) ingestComment(e *jmodels.Event) error { 871 did := e.Did 872 rkey := e.Commit.RKey 873 874 var err error 875 876 + l := i.Logger.With("handler", "ingestComment", "nsid", e.Commit.Collection, "did", did, "rkey", rkey) 877 l.Info("ingesting record") 878 879 ddb, ok := i.Db.Execer.(*db.DB) ··· 884 switch e.Commit.Operation { 885 case jmodels.CommitOperationCreate, jmodels.CommitOperationUpdate: 886 raw := json.RawMessage(e.Commit.Record) 887 + record := tangled.Comment{} 888 err = json.Unmarshal(raw, &record) 889 if err != nil { 890 return fmt.Errorf("invalid record: %w", err) 891 } 892 893 + comment, err := models.CommentFromRecord(did, rkey, record) 894 if err != nil { 895 return fmt.Errorf("failed to parse comment from record: %w", err) 896 } 897 898 + // TODO: ingest pull comments 899 + // we aren't ingesting pull comments yet because pull itself isn't fully atprotated. 900 + // so we cannot know which round this comment is pointing to 901 + if comment.Subject.Collection().String() == tangled.RepoPullNSID { 902 + l.Info("skip ingesting pull comments") 903 + return nil 904 + } 905 + 906 + if err := comment.Validate(); err != nil { 907 return fmt.Errorf("failed to validate comment: %w", err) 908 } 909 ··· 913 } 914 defer tx.Rollback() 915 916 + err = db.PutComment(tx, comment) 917 if err != nil { 918 + return fmt.Errorf("failed to create comment: %w", err) 919 } 920 921 return tx.Commit() 922 923 case jmodels.CommitOperationDelete: 924 + if err := db.DeleteComments( 925 ddb, 926 db.FilterEq("did", did), 927 db.FilterEq("rkey", rkey), 928 ); err != nil { 929 + return fmt.Errorf("failed to delete comment record: %w", err) 930 } 931 932 return nil
+30 -28
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.refResolver.Resolve(r.Context(), body) 416 417 - comment := models.IssueComment{ 418 - Did: user.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 ··· 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, ··· 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 db.FilterEq("id", commentId), 510 ) ··· 540 } 541 542 commentId := chi.URLParam(r, "commentId") 543 - comments, err := db.GetIssueComments( 544 rp.db, 545 db.FilterEq("id", commentId), 546 ) ··· 556 } 557 comment := comments[0] 558 559 - if comment.Did != user.Did { 560 l.Error("unauthorized comment edit", "expectedDid", comment.Did, "gotDid", user.Did) 561 http.Error(w, "you are not the author of this comment", http.StatusUnauthorized) 562 return ··· 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.") ··· 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.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.") ··· 615 } 616 617 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 618 - Collection: tangled.RepoIssueCommentNSID, 619 Repo: user.Did, 620 Rkey: newComment.Rkey, 621 SwapRecord: ex.Cid, ··· 650 } 651 652 commentId := chi.URLParam(r, "commentId") 653 - comments, err := db.GetIssueComments( 654 rp.db, 655 db.FilterEq("id", commentId), 656 ) ··· 686 } 687 688 commentId := chi.URLParam(r, "commentId") 689 - comments, err := db.GetIssueComments( 690 rp.db, 691 db.FilterEq("id", commentId), 692 ) ··· 722 } 723 724 commentId := chi.URLParam(r, "commentId") 725 - comments, err := db.GetIssueComments( 726 rp.db, 727 db.FilterEq("id", commentId), 728 ) ··· 738 } 739 comment := comments[0] 740 741 - if comment.Did != user.Did { 742 l.Error("unauthorized action", "expectedDid", comment.Did, "gotDid", user.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, db.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.Did, 772 Rkey: comment.Rkey, 773 })
··· 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(r.FormValue("reply-to")) 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.refResolver.Resolve(r.Context(), body) 421 422 + comment := models.Comment{ 423 + Did: syntax.DID(user.Did), 424 Rkey: tid.TID(), 425 + Subject: issue.AtUri(), 426 ReplyTo: replyTo, 427 Body: body, 428 Created: time.Now(), 429 Mentions: mentions, 430 References: references, 431 } 432 + if err = comment.Validate(); err != nil { 433 l.Error("failed to validate comment", "err", err) 434 rp.pages.Notice(w, "issue-comment", "Failed to create comment.") 435 return ··· 445 446 // create a record first 447 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 448 + Collection: tangled.CommentNSID, 449 + Repo: user.Did, 450 Rkey: comment.Rkey, 451 Record: &lexutil.LexiconTypeDecoder{ 452 Val: &record, ··· 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 db.FilterEq("id", commentId), 512 ) ··· 542 } 543 544 commentId := chi.URLParam(r, "commentId") 545 + comments, err := db.GetComments( 546 rp.db, 547 db.FilterEq("id", commentId), 548 ) ··· 558 } 559 comment := comments[0] 560 561 + if comment.Did.String() != user.Did { 562 l.Error("unauthorized comment edit", "expectedDid", comment.Did, "gotDid", user.Did) 563 http.Error(w, "you are not the author of this comment", http.StatusUnauthorized) 564 return ··· 598 } 599 defer tx.Rollback() 600 601 + err = db.PutComment(tx, &newComment) 602 if err != nil { 603 l.Error("failed to perferom update-description query", "err", err) 604 rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.") ··· 609 // rkey is optional, it was introduced later 610 if newComment.Rkey != "" { 611 // update the record on pds 612 + ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.CommentNSID, user.Did, comment.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 description, no record found on PDS.") ··· 617 } 618 619 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 620 + Collection: tangled.CommentNSID, 621 Repo: user.Did, 622 Rkey: newComment.Rkey, 623 SwapRecord: ex.Cid, ··· 652 } 653 654 commentId := chi.URLParam(r, "commentId") 655 + comments, err := db.GetComments( 656 rp.db, 657 db.FilterEq("id", commentId), 658 ) ··· 688 } 689 690 commentId := chi.URLParam(r, "commentId") 691 + comments, err := db.GetComments( 692 rp.db, 693 db.FilterEq("id", commentId), 694 ) ··· 724 } 725 726 commentId := chi.URLParam(r, "commentId") 727 + comments, err := db.GetComments( 728 rp.db, 729 db.FilterEq("id", commentId), 730 ) ··· 740 } 741 comment := comments[0] 742 743 + if comment.Did.String() != user.Did { 744 l.Error("unauthorized action", "expectedDid", comment.Did, "gotDid", user.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, db.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: tangled.CommentNSID, 773 Repo: user.Did, 774 Rkey: comment.Rkey, 775 })
+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
··· 118 ) 119 } 120 121 - func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) { 122 - issues, err := db.GetIssues(n.db, db.FilterEq("at_uri", comment.IssueAt)) 123 if err != nil { 124 log.Printf("NewIssueComment: failed to get issues: %v", err) 125 return 126 } 127 if len(issues) == 0 { 128 - log.Printf("NewIssueComment: no issue found for %s", comment.IssueAt) 129 return 130 } 131 issue := issues[0] ··· 140 141 // find the parent thread, and add all DIDs from here to the recipient list 142 for _, t := range allThreads { 143 - if t.Self.AtUri().String() == parentAtUri { 144 recipients = append(recipients, t.Participants()...) 145 } 146 }
··· 118 ) 119 } 120 121 + func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID) { 122 + issues, err := db.GetIssues(n.db, db.FilterEq("at_uri", comment.Subject)) 123 if err != nil { 124 log.Printf("NewIssueComment: failed to get issues: %v", err) 125 return 126 } 127 if len(issues) == 0 { 128 + log.Printf("NewIssueComment: no issue found for %s", comment.Subject) 129 return 130 } 131 issue := issues[0] ··· 140 141 // find the parent thread, and add all DIDs from here to the recipient list 142 for _, t := range allThreads { 143 + if t.Self.AtUri() == parentAtUri { 144 recipients = append(recipients, t.Participants()...) 145 } 146 }
+1 -1
appview/notify/merged_notifier.go
··· 58 m.fanout("NewIssue", ctx, issue, mentions) 59 } 60 61 - func (m *mergedNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) { 62 m.fanout("NewIssueComment", ctx, comment, mentions) 63 } 64
··· 58 m.fanout("NewIssue", ctx, issue, mentions) 59 } 60 61 + func (m *mergedNotifier) NewIssueComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID) { 62 m.fanout("NewIssueComment", ctx, comment, mentions) 63 } 64
+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
··· 989 LoggedInUser *oauth.User 990 RepoInfo repoinfo.RepoInfo 991 Issue *models.Issue 992 - Comment *models.IssueComment 993 } 994 995 func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error { ··· 1000 LoggedInUser *oauth.User 1001 RepoInfo repoinfo.RepoInfo 1002 Issue *models.Issue 1003 - Comment *models.IssueComment 1004 } 1005 1006 func (p *Pages) ReplyIssueCommentPlaceholderFragment(w io.Writer, params ReplyIssueCommentPlaceholderParams) error { ··· 1011 LoggedInUser *oauth.User 1012 RepoInfo repoinfo.RepoInfo 1013 Issue *models.Issue 1014 - Comment *models.IssueComment 1015 } 1016 1017 func (p *Pages) ReplyIssueCommentFragment(w io.Writer, params ReplyIssueCommentParams) error { ··· 1022 LoggedInUser *oauth.User 1023 RepoInfo repoinfo.RepoInfo 1024 Issue *models.Issue 1025 - Comment *models.IssueComment 1026 } 1027 1028 func (p *Pages) IssueCommentBodyFragment(w io.Writer, params IssueCommentBodyParams) error {
··· 989 LoggedInUser *oauth.User 990 RepoInfo repoinfo.RepoInfo 991 Issue *models.Issue 992 + Comment *models.Comment 993 } 994 995 func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error { ··· 1000 LoggedInUser *oauth.User 1001 RepoInfo repoinfo.RepoInfo 1002 Issue *models.Issue 1003 + Comment *models.Comment 1004 } 1005 1006 func (p *Pages) ReplyIssueCommentPlaceholderFragment(w io.Writer, params ReplyIssueCommentPlaceholderParams) error { ··· 1011 LoggedInUser *oauth.User 1012 RepoInfo repoinfo.RepoInfo 1013 Issue *models.Issue 1014 + Comment *models.Comment 1015 } 1016 1017 func (p *Pages) ReplyIssueCommentFragment(w io.Writer, params ReplyIssueCommentParams) error { ··· 1022 LoggedInUser *oauth.User 1023 RepoInfo repoinfo.RepoInfo 1024 Issue *models.Issue 1025 + Comment *models.Comment 1026 } 1027 1028 func (p *Pages) IssueCommentBodyFragment(w io.Writer, params IssueCommentBodyParams) error {
+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 - {{ template "user/fragments/picHandleLink" .Comment.Did }} 4 {{ template "hats" $ }} 5 {{ template "timestamp" . }} 6 - {{ $isCommentOwner := and .LoggedInUser (eq .LoggedInUser.Did .Comment.Did) }} 7 {{ if and $isCommentOwner (not .Comment.Deleted) }} 8 {{ template "editIssueComment" . }} 9 {{ 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 + {{ template "user/fragments/picHandleLink" .Comment.Did.String }} 4 {{ template "hats" $ }} 5 {{ template "timestamp" . }} 6 + {{ $isCommentOwner := and .LoggedInUser (eq .LoggedInUser.Did .Comment.Did.String) }} 7 {{ if and $isCommentOwner (not .Comment.Deleted) }} 8 {{ template "editIssueComment" . }} 9 {{ template "deleteIssueComment" . }}
+1 -1
appview/state/state.go
··· 116 tangled.SpindleNSID, 117 tangled.StringNSID, 118 tangled.RepoIssueNSID, 119 - tangled.RepoIssueCommentNSID, 120 tangled.LabelDefinitionNSID, 121 tangled.LabelOpNSID, 122 },
··· 116 tangled.SpindleNSID, 117 tangled.StringNSID, 118 tangled.RepoIssueNSID, 119 + tangled.CommentNSID, 120 tangled.LabelDefinitionNSID, 121 tangled.LabelOpNSID, 122 },
-26
appview/validator/issue.go
··· 4 "fmt" 5 "strings" 6 7 - "tangled.org/core/appview/db" 8 "tangled.org/core/appview/models" 9 ) 10 11 - func (v *Validator) ValidateIssueComment(comment *models.IssueComment) error { 12 - // if comments have parents, only ingest ones that are 1 level deep 13 - if comment.ReplyTo != nil { 14 - parents, err := db.GetIssueComments(v.db, db.FilterEq("at_uri", *comment.ReplyTo)) 15 - if err != nil { 16 - return fmt.Errorf("failed to fetch parent comment: %w", err) 17 - } 18 - if len(parents) != 1 { 19 - return fmt.Errorf("incorrect number of parent comments returned: %d", len(parents)) 20 - } 21 - 22 - // depth check 23 - parent := parents[0] 24 - if parent.ReplyTo != nil { 25 - return fmt.Errorf("incorrect depth, this comment is replying at depth >1") 26 - } 27 - } 28 - 29 - if sb := strings.TrimSpace(v.sanitizer.SanitizeDefault(comment.Body)); sb == "" { 30 - return fmt.Errorf("body is empty after HTML sanitization") 31 - } 32 - 33 - return nil 34 - } 35 - 36 func (v *Validator) ValidateIssue(issue *models.Issue) error { 37 if issue.Title == "" { 38 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/db/reference.go:293
  • 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.

boltless.me submitted #0
1 commit
expand
appview: replace IssueComment to Comment
3/3 success
expand
expand 0 comments