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 -381
Diff #1
+6 -185
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 } ··· 351 return ids, nil 352 } 353 354 - func AddIssueComment(tx *sql.Tx, c models.IssueComment) (int64, error) { 355 - result, err := tx.Exec( 356 - `insert into issue_comments ( 357 - did, 358 - rkey, 359 - issue_at, 360 - body, 361 - reply_to, 362 - created, 363 - edited 364 - ) 365 - values (?, ?, ?, ?, ?, ?, null) 366 - on conflict(did, rkey) do update set 367 - issue_at = excluded.issue_at, 368 - body = excluded.body, 369 - edited = case 370 - when 371 - issue_comments.issue_at != excluded.issue_at 372 - or issue_comments.body != excluded.body 373 - or issue_comments.reply_to != excluded.reply_to 374 - then ? 375 - else issue_comments.edited 376 - end`, 377 - c.Did, 378 - c.Rkey, 379 - c.IssueAt, 380 - c.Body, 381 - c.ReplyTo, 382 - c.Created.Format(time.RFC3339), 383 - time.Now().Format(time.RFC3339), 384 - ) 385 - if err != nil { 386 - return 0, err 387 - } 388 - 389 - id, err := result.LastInsertId() 390 - if err != nil { 391 - return 0, err 392 - } 393 - 394 - if err := putReferences(tx, c.AtUri(), c.References); err != nil { 395 - return 0, fmt.Errorf("put reference_links: %w", err) 396 - } 397 - 398 - return id, nil 399 - } 400 - 401 - func DeleteIssueComments(e Execer, filters ...orm.Filter) error { 402 - var conditions []string 403 - var args []any 404 - for _, filter := range filters { 405 - conditions = append(conditions, filter.Condition()) 406 - args = append(args, filter.Arg()...) 407 - } 408 - 409 - whereClause := "" 410 - if conditions != nil { 411 - whereClause = " where " + strings.Join(conditions, " and ") 412 - } 413 - 414 - query := fmt.Sprintf(`update issue_comments set body = "", deleted = strftime('%%Y-%%m-%%dT%%H:%%M:%%SZ', 'now') %s`, whereClause) 415 - 416 - _, err := e.Exec(query, args...) 417 - return err 418 - } 419 - 420 - func GetIssueComments(e Execer, filters ...orm.Filter) ([]models.IssueComment, error) { 421 - commentMap := make(map[string]*models.IssueComment) 422 - 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 - did, 439 - rkey, 440 - issue_at, 441 - reply_to, 442 - body, 443 - created, 444 - edited, 445 - deleted 446 - from 447 - issue_comments 448 - %s 449 - `, whereClause) 450 - 451 - rows, err := e.Query(query, args...) 452 - if err != nil { 453 - return nil, err 454 - } 455 - 456 - for rows.Next() { 457 - var comment models.IssueComment 458 - var created string 459 - var rkey, edited, deleted, replyTo sql.Null[string] 460 - err := rows.Scan( 461 - &comment.Id, 462 - &comment.Did, 463 - &rkey, 464 - &comment.IssueAt, 465 - &replyTo, 466 - &comment.Body, 467 - &created, 468 - &edited, 469 - &deleted, 470 - ) 471 - if err != nil { 472 - return nil, err 473 - } 474 - 475 - // this is a remnant from old times, newer comments always have rkey 476 - if rkey.Valid { 477 - comment.Rkey = rkey.V 478 - } 479 - 480 - if t, err := time.Parse(time.RFC3339, created); err == nil { 481 - comment.Created = t 482 - } 483 - 484 - if edited.Valid { 485 - if t, err := time.Parse(time.RFC3339, edited.V); err == nil { 486 - comment.Edited = &t 487 - } 488 - } 489 - 490 - if deleted.Valid { 491 - if t, err := time.Parse(time.RFC3339, deleted.V); err == nil { 492 - comment.Deleted = &t 493 - } 494 - } 495 - 496 - if replyTo.Valid { 497 - comment.ReplyTo = &replyTo.V 498 - } 499 - 500 - atUri := comment.AtUri().String() 501 - commentMap[atUri] = &comment 502 - } 503 - 504 - if err = rows.Err(); err != nil { 505 - return nil, err 506 - } 507 - 508 - // collect references for each comments 509 - commentAts := slices.Collect(maps.Keys(commentMap)) 510 - allReferencs, err := GetReferencesAll(e, orm.FilterIn("from_at", commentAts)) 511 - if err != nil { 512 - return nil, fmt.Errorf("failed to query reference_links: %w", err) 513 - } 514 - for commentAt, references := range allReferencs { 515 - if comment, ok := commentMap[commentAt.String()]; ok { 516 - comment.References = references 517 - } 518 - } 519 - 520 - var comments []models.IssueComment 521 - for _, c := range commentMap { 522 - comments = append(comments, *c) 523 - } 524 - 525 - sort.Slice(comments, func(i, j int) bool { 526 - return comments[i].Created.After(comments[j].Created) 527 - }) 528 - 529 - return comments, nil 530 - } 531 - 532 func DeleteIssues(tx *sql.Tx, did, rkey string) error { 533 _, err := tx.Exec( 534 `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 } ··· 350 return ids, nil 351 } 352 353 func DeleteIssues(tx *sql.Tx, did, rkey string) error { 354 _, err := tx.Exec( 355 `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`,
+19 -11
appview/ingester.go
··· 79 err = i.ingestString(e) 80 case tangled.RepoIssueNSID: 81 err = i.ingestIssue(ctx, e) 82 - case tangled.RepoIssueCommentNSID: 83 - err = i.ingestIssueComment(e) 84 case tangled.LabelDefinitionNSID: 85 err = i.ingestLabelDefinition(e) 86 case tangled.LabelOpNSID: ··· 868 return nil 869 } 870 871 - func (i *Ingester) ingestIssueComment(e *jmodels.Event) error { 872 did := e.Did 873 rkey := e.Commit.RKey 874 875 var err error 876 877 - l := i.Logger.With("handler", "ingestIssueComment", "nsid", e.Commit.Collection, "did", did, "rkey", rkey) 878 l.Info("ingesting record") 879 880 ddb, ok := i.Db.Execer.(*db.DB) ··· 885 switch e.Commit.Operation { 886 case jmodels.CommitOperationCreate, jmodels.CommitOperationUpdate: 887 raw := json.RawMessage(e.Commit.Record) 888 - record := tangled.RepoIssueComment{} 889 err = json.Unmarshal(raw, &record) 890 if err != nil { 891 return fmt.Errorf("invalid record: %w", err) 892 } 893 894 - comment, err := models.IssueCommentFromRecord(did, rkey, record) 895 if err != nil { 896 return fmt.Errorf("failed to parse comment from record: %w", err) 897 } 898 899 - if err := i.Validator.ValidateIssueComment(comment); err != nil { 900 return fmt.Errorf("failed to validate comment: %w", err) 901 } 902 ··· 906 } 907 defer tx.Rollback() 908 909 - _, err = db.AddIssueComment(tx, *comment) 910 if err != nil { 911 - return fmt.Errorf("failed to create issue comment: %w", err) 912 } 913 914 return tx.Commit() 915 916 case jmodels.CommitOperationDelete: 917 - if err := db.DeleteIssueComments( 918 ddb, 919 orm.FilterEq("did", did), 920 orm.FilterEq("rkey", rkey), 921 ); err != nil { 922 - return fmt.Errorf("failed to delete issue comment record: %w", err) 923 } 924 925 return nil
··· 79 err = i.ingestString(e) 80 case tangled.RepoIssueNSID: 81 err = i.ingestIssue(ctx, e) 82 + case tangled.CommentNSID: 83 + err = i.ingestComment(e) 84 case tangled.LabelDefinitionNSID: 85 err = i.ingestLabelDefinition(e) 86 case tangled.LabelOpNSID: ··· 868 return nil 869 } 870 871 + func (i *Ingester) ingestComment(e *jmodels.Event) error { 872 did := e.Did 873 rkey := e.Commit.RKey 874 875 var err error 876 877 + l := i.Logger.With("handler", "ingestComment", "nsid", e.Commit.Collection, "did", did, "rkey", rkey) 878 l.Info("ingesting record") 879 880 ddb, ok := i.Db.Execer.(*db.DB) ··· 885 switch e.Commit.Operation { 886 case jmodels.CommitOperationCreate, jmodels.CommitOperationUpdate: 887 raw := json.RawMessage(e.Commit.Record) 888 + record := tangled.Comment{} 889 err = json.Unmarshal(raw, &record) 890 if err != nil { 891 return fmt.Errorf("invalid record: %w", err) 892 } 893 894 + comment, err := models.CommentFromRecord(did, rkey, record) 895 if err != nil { 896 return fmt.Errorf("failed to parse comment from record: %w", err) 897 } 898 899 + // TODO: ingest pull comments 900 + // we aren't ingesting pull comments yet because pull itself isn't fully atprotated. 901 + // so we cannot know which round this comment is pointing to 902 + if comment.Subject.Collection().String() == tangled.RepoPullNSID { 903 + l.Info("skip ingesting pull comments") 904 + return nil 905 + } 906 + 907 + if err := comment.Validate(); err != nil { 908 return fmt.Errorf("failed to validate comment: %w", err) 909 } 910 ··· 914 } 915 defer tx.Rollback() 916 917 + err = db.PutComment(tx, comment) 918 if err != nil { 919 + return fmt.Errorf("failed to create comment: %w", err) 920 } 921 922 return tx.Commit() 923 924 case jmodels.CommitOperationDelete: 925 + if err := db.DeleteComments( 926 ddb, 927 orm.FilterEq("did", did), 928 orm.FilterEq("rkey", rkey), 929 ); err != nil { 930 + return fmt.Errorf("failed to delete comment record: %w", err) 931 } 932 933 return nil
+30 -28
appview/issues/issues.go
··· 403 404 body := r.FormValue("body") 405 if body == "" { 406 - rp.pages.Notice(w, "issue", "Body is required") 407 return 408 } 409 410 - replyToUri := r.FormValue("reply-to") 411 - var replyTo *string 412 - if replyToUri != "" { 413 - replyTo = &replyToUri 414 } 415 416 mentions, references := rp.mentionsResolver.Resolve(r.Context(), body) 417 418 - comment := models.IssueComment{ 419 - Did: user.Did, 420 Rkey: tid.TID(), 421 - IssueAt: issue.AtUri().String(), 422 ReplyTo: replyTo, 423 Body: body, 424 Created: time.Now(), 425 Mentions: mentions, 426 References: references, 427 } 428 - if err = rp.validator.ValidateIssueComment(&comment); err != nil { 429 l.Error("failed to validate comment", "err", err) 430 rp.pages.Notice(w, "issue-comment", "Failed to create comment.") 431 return ··· 441 442 // create a record first 443 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 444 - Collection: tangled.RepoIssueCommentNSID, 445 - Repo: comment.Did, 446 Rkey: comment.Rkey, 447 Record: &lexutil.LexiconTypeDecoder{ 448 Val: &record, ··· 468 } 469 defer tx.Rollback() 470 471 - commentId, err := db.AddIssueComment(tx, comment) 472 if err != nil { 473 l.Error("failed to create comment", "err", err) 474 rp.pages.Notice(w, "issue-comment", "Failed to create comment.") ··· 484 // reset atUri to make rollback a no-op 485 atUri = "" 486 487 - // notify about the new comment 488 - comment.Id = commentId 489 - 490 rp.notifier.NewIssueComment(r.Context(), &comment, mentions) 491 492 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) 493 - rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", ownerSlashRepo, issue.IssueId, commentId)) 494 } 495 496 func (rp *Issues) IssueComment(w http.ResponseWriter, r *http.Request) { ··· 505 } 506 507 commentId := chi.URLParam(r, "commentId") 508 - comments, err := db.GetIssueComments( 509 rp.db, 510 orm.FilterEq("id", commentId), 511 ) ··· 541 } 542 543 commentId := chi.URLParam(r, "commentId") 544 - comments, err := db.GetIssueComments( 545 rp.db, 546 orm.FilterEq("id", commentId), 547 ) ··· 557 } 558 comment := comments[0] 559 560 - if comment.Did != user.Did { 561 l.Error("unauthorized comment edit", "expectedDid", comment.Did, "gotDid", user.Did) 562 http.Error(w, "you are not the author of this comment", http.StatusUnauthorized) 563 return ··· 597 } 598 defer tx.Rollback() 599 600 - _, err = db.AddIssueComment(tx, newComment) 601 if err != nil { 602 l.Error("failed to perferom update-description query", "err", err) 603 rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.") ··· 608 // rkey is optional, it was introduced later 609 if newComment.Rkey != "" { 610 // update the record on pds 611 - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueCommentNSID, user.Did, comment.Rkey) 612 if err != nil { 613 l.Error("failed to get record", "err", err, "did", newComment.Did, "rkey", newComment.Rkey) 614 rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.") ··· 616 } 617 618 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 619 - Collection: tangled.RepoIssueCommentNSID, 620 Repo: user.Did, 621 Rkey: newComment.Rkey, 622 SwapRecord: ex.Cid, ··· 651 } 652 653 commentId := chi.URLParam(r, "commentId") 654 - comments, err := db.GetIssueComments( 655 rp.db, 656 orm.FilterEq("id", commentId), 657 ) ··· 687 } 688 689 commentId := chi.URLParam(r, "commentId") 690 - comments, err := db.GetIssueComments( 691 rp.db, 692 orm.FilterEq("id", commentId), 693 ) ··· 723 } 724 725 commentId := chi.URLParam(r, "commentId") 726 - comments, err := db.GetIssueComments( 727 rp.db, 728 orm.FilterEq("id", commentId), 729 ) ··· 739 } 740 comment := comments[0] 741 742 - if comment.Did != user.Did { 743 l.Error("unauthorized action", "expectedDid", comment.Did, "gotDid", user.Did) 744 http.Error(w, "you are not the author of this comment", http.StatusUnauthorized) 745 return ··· 752 753 // optimistic deletion 754 deleted := time.Now() 755 - err = db.DeleteIssueComments(rp.db, orm.FilterEq("id", comment.Id)) 756 if err != nil { 757 l.Error("failed to delete comment", "err", err) 758 rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment") ··· 768 return 769 } 770 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 771 - Collection: tangled.RepoIssueCommentNSID, 772 Repo: user.Did, 773 Rkey: comment.Rkey, 774 })
··· 403 404 body := r.FormValue("body") 405 if body == "" { 406 + rp.pages.Notice(w, "issue-comment", "Body is required") 407 return 408 } 409 410 + var replyTo *syntax.ATURI 411 + replyToRaw := r.FormValue("reply-to") 412 + if replyToRaw != "" { 413 + aturi, err := syntax.ParseATURI(r.FormValue("reply-to")) 414 + if err != nil { 415 + rp.pages.Notice(w, "issue-comment", "reply-to should be valid AT-URI") 416 + return 417 + } 418 + replyTo = &aturi 419 } 420 421 mentions, references := rp.mentionsResolver.Resolve(r.Context(), body) 422 423 + comment := models.Comment{ 424 + Did: syntax.DID(user.Did), 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 ··· 446 447 // create a record first 448 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 449 + Collection: tangled.CommentNSID, 450 + Repo: user.Did, 451 Rkey: comment.Rkey, 452 Record: &lexutil.LexiconTypeDecoder{ 453 Val: &record, ··· 473 } 474 defer tx.Rollback() 475 476 + err = db.PutComment(tx, &comment) 477 if err != nil { 478 l.Error("failed to create comment", "err", err) 479 rp.pages.Notice(w, "issue-comment", "Failed to create comment.") ··· 489 // reset atUri to make rollback a no-op 490 atUri = "" 491 492 rp.notifier.NewIssueComment(r.Context(), &comment, mentions) 493 494 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) 495 + rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", ownerSlashRepo, issue.IssueId, comment.Id)) 496 } 497 498 func (rp *Issues) IssueComment(w http.ResponseWriter, r *http.Request) { ··· 507 } 508 509 commentId := chi.URLParam(r, "commentId") 510 + comments, err := db.GetComments( 511 rp.db, 512 orm.FilterEq("id", commentId), 513 ) ··· 543 } 544 545 commentId := chi.URLParam(r, "commentId") 546 + comments, err := db.GetComments( 547 rp.db, 548 orm.FilterEq("id", commentId), 549 ) ··· 559 } 560 comment := comments[0] 561 562 + if comment.Did.String() != user.Did { 563 l.Error("unauthorized comment edit", "expectedDid", comment.Did, "gotDid", user.Did) 564 http.Error(w, "you are not the author of this comment", http.StatusUnauthorized) 565 return ··· 599 } 600 defer tx.Rollback() 601 602 + err = db.PutComment(tx, &newComment) 603 if err != nil { 604 l.Error("failed to perferom update-description query", "err", err) 605 rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.") ··· 610 // rkey is optional, it was introduced later 611 if newComment.Rkey != "" { 612 // update the record on pds 613 + ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.CommentNSID, user.Did, comment.Rkey) 614 if err != nil { 615 l.Error("failed to get record", "err", err, "did", newComment.Did, "rkey", newComment.Rkey) 616 rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.") ··· 618 } 619 620 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 621 + Collection: tangled.CommentNSID, 622 Repo: user.Did, 623 Rkey: newComment.Rkey, 624 SwapRecord: ex.Cid, ··· 653 } 654 655 commentId := chi.URLParam(r, "commentId") 656 + comments, err := db.GetComments( 657 rp.db, 658 orm.FilterEq("id", commentId), 659 ) ··· 689 } 690 691 commentId := chi.URLParam(r, "commentId") 692 + comments, err := db.GetComments( 693 rp.db, 694 orm.FilterEq("id", commentId), 695 ) ··· 725 } 726 727 commentId := chi.URLParam(r, "commentId") 728 + comments, err := db.GetComments( 729 rp.db, 730 orm.FilterEq("id", commentId), 731 ) ··· 741 } 742 comment := comments[0] 743 744 + if comment.Did.String() != user.Did { 745 l.Error("unauthorized action", "expectedDid", comment.Did, "gotDid", user.Did) 746 http.Error(w, "you are not the author of this comment", http.StatusUnauthorized) 747 return ··· 754 755 // optimistic deletion 756 deleted := time.Now() 757 + err = db.DeleteComments(rp.db, orm.FilterEq("id", comment.Id)) 758 if err != nil { 759 l.Error("failed to delete comment", "err", err) 760 rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment") ··· 770 return 771 } 772 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 773 + Collection: tangled.CommentNSID, 774 Repo: user.Did, 775 Rkey: comment.Rkey, 776 })
+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
··· 119 ) 120 } 121 122 - func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) { 123 - issues, err := db.GetIssues(n.db, orm.FilterEq("at_uri", comment.IssueAt)) 124 if err != nil { 125 log.Printf("NewIssueComment: failed to get issues: %v", err) 126 return 127 } 128 if len(issues) == 0 { 129 - log.Printf("NewIssueComment: no issue found for %s", comment.IssueAt) 130 return 131 } 132 issue := issues[0] ··· 141 142 // find the parent thread, and add all DIDs from here to the recipient list 143 for _, t := range allThreads { 144 - if t.Self.AtUri().String() == parentAtUri { 145 recipients = append(recipients, t.Participants()...) 146 } 147 }
··· 119 ) 120 } 121 122 + func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID) { 123 + issues, err := db.GetIssues(n.db, orm.FilterEq("at_uri", comment.Subject)) 124 if err != nil { 125 log.Printf("NewIssueComment: failed to get issues: %v", err) 126 return 127 } 128 if len(issues) == 0 { 129 + log.Printf("NewIssueComment: no issue found for %s", comment.Subject) 130 return 131 } 132 issue := issues[0] ··· 141 142 // find the parent thread, and add all DIDs from here to the recipient list 143 for _, t := range allThreads { 144 + if t.Self.AtUri() == parentAtUri { 145 recipients = append(recipients, t.Participants()...) 146 } 147 }
+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
··· 988 LoggedInUser *oauth.User 989 RepoInfo repoinfo.RepoInfo 990 Issue *models.Issue 991 - Comment *models.IssueComment 992 } 993 994 func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error { ··· 999 LoggedInUser *oauth.User 1000 RepoInfo repoinfo.RepoInfo 1001 Issue *models.Issue 1002 - Comment *models.IssueComment 1003 } 1004 1005 func (p *Pages) ReplyIssueCommentPlaceholderFragment(w io.Writer, params ReplyIssueCommentPlaceholderParams) error { ··· 1010 LoggedInUser *oauth.User 1011 RepoInfo repoinfo.RepoInfo 1012 Issue *models.Issue 1013 - Comment *models.IssueComment 1014 } 1015 1016 func (p *Pages) ReplyIssueCommentFragment(w io.Writer, params ReplyIssueCommentParams) error { ··· 1021 LoggedInUser *oauth.User 1022 RepoInfo repoinfo.RepoInfo 1023 Issue *models.Issue 1024 - Comment *models.IssueComment 1025 } 1026 1027 func (p *Pages) IssueCommentBodyFragment(w io.Writer, params IssueCommentBodyParams) error {
··· 988 LoggedInUser *oauth.User 989 RepoInfo repoinfo.RepoInfo 990 Issue *models.Issue 991 + Comment *models.Comment 992 } 993 994 func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error { ··· 999 LoggedInUser *oauth.User 1000 RepoInfo repoinfo.RepoInfo 1001 Issue *models.Issue 1002 + Comment *models.Comment 1003 } 1004 1005 func (p *Pages) ReplyIssueCommentPlaceholderFragment(w io.Writer, params ReplyIssueCommentPlaceholderParams) error { ··· 1010 LoggedInUser *oauth.User 1011 RepoInfo repoinfo.RepoInfo 1012 Issue *models.Issue 1013 + Comment *models.Comment 1014 } 1015 1016 func (p *Pages) ReplyIssueCommentFragment(w io.Writer, params ReplyIssueCommentParams) error { ··· 1021 LoggedInUser *oauth.User 1022 RepoInfo repoinfo.RepoInfo 1023 Issue *models.Issue 1024 + Comment *models.Comment 1025 } 1026 1027 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
··· 117 tangled.SpindleNSID, 118 tangled.StringNSID, 119 tangled.RepoIssueNSID, 120 - tangled.RepoIssueCommentNSID, 121 tangled.LabelDefinitionNSID, 122 tangled.LabelOpNSID, 123 },
··· 117 tangled.SpindleNSID, 118 tangled.StringNSID, 119 tangled.RepoIssueNSID, 120 + tangled.CommentNSID, 121 tangled.LabelDefinitionNSID, 122 tangled.LabelOpNSID, 123 },
-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