Monorepo for Tangled tangled.org

appview: introduce email notifications for @ mentions on issue/pr comments #393

closed opened by boltless.me targeting master from boltless.me/core: feat/mentions

Stacked on top of #392

Yes, I know we have stacked PRs, but I want to explicitly separate two sets of commits and review both on different places

This is MVC implementation of email notification.

Still lot of parts are missing, but this is a PR with most basic features.

Labels

None yet.

assignee

None yet.

Participants 2
AT URI
at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.pull/3lvdu42ftpn22
+658 -269
Diff #2
+60 -57
appview/issues/issues.go
··· 18 18 19 19 20 20 21 - 22 - 23 - 24 - 25 - 26 - 21 + "tangled.sh/tangled.sh/core/appview/notify" 22 + "tangled.sh/tangled.sh/core/appview/oauth" 23 + "tangled.sh/tangled.sh/core/appview/pages" 24 + "tangled.sh/tangled.sh/core/appview/pages/markup" 25 + "tangled.sh/tangled.sh/core/appview/pagination" 26 + "tangled.sh/tangled.sh/core/appview/reporesolver" 27 + "tangled.sh/tangled.sh/core/idresolver" 27 28 28 29 29 30 ··· 73 74 return 74 75 } 75 76 76 - issue, comments, err := db.GetIssueWithComments(rp.db, f.RepoAt, issueIdInt) 77 + issue, comments, err := db.GetIssueWithComments(rp.db, f.RepoAt(), issueIdInt) 77 78 if err != nil { 78 79 log.Println("failed to get issue and comments", err) 79 80 rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.") ··· 142 143 return 143 144 } 144 145 145 - issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt) 146 + issue, err := db.GetIssue(rp.db, f.RepoAt(), issueIdInt) 146 147 if err != nil { 147 148 log.Println("failed to get issue", err) 148 149 rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") ··· 186 187 return 187 188 } 188 189 189 - err = db.CloseIssue(rp.db, f.RepoAt, issueIdInt) 190 + err = db.CloseIssue(rp.db, f.RepoAt(), issueIdInt) 190 191 if err != nil { 191 192 log.Println("failed to close issue", err) 192 193 rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") ··· 218 219 return 219 220 } 220 221 221 - issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt) 222 + issue, err := db.GetIssue(rp.db, f.RepoAt(), issueIdInt) 222 223 if err != nil { 223 224 log.Println("failed to get issue", err) 224 225 rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") ··· 235 236 isIssueOwner := user.Did == issue.OwnerDid 236 237 237 238 if isCollaborator || isIssueOwner { 238 - err := db.ReopenIssue(rp.db, f.RepoAt, issueIdInt) 239 + err := db.ReopenIssue(rp.db, f.RepoAt(), issueIdInt) 239 240 if err != nil { 240 241 log.Println("failed to reopen issue", err) 241 242 rp.pages.Notice(w, "issue-action", "Failed to reopen issue. Try again later.") ··· 271 272 272 273 273 274 275 + return 276 + } 274 277 275 - 276 - 277 - 278 - 279 - 280 - err := db.NewIssueComment(rp.db, &db.Comment{ 278 + comment := &db.Comment{ 281 279 OwnerDid: user.Did, 282 - RepoAt: f.RepoAt, 280 + RepoAt: f.RepoAt(), 283 281 Issue: issueIdInt, 284 - CommentId: commentId, 282 + CommentId: mathrand.IntN(1000000), 285 283 Body: body, 284 + Rkey: tid.TID(), 285 + } 286 286 287 + err := db.NewIssueComment(rp.db, comment) 288 + if err != nil { 289 + log.Println("failed to create comment", err) 290 + rp.pages.Notice(w, "issue-comment", "Failed to create comment.") 287 291 288 - 289 - 290 - 291 - 292 - 292 + } 293 293 294 294 createdAt := time.Now().Format(time.RFC3339) 295 - commentIdInt64 := int64(commentId) 296 - ownerDid := user.Did 297 - issueAt, err := db.GetIssueAt(rp.db, f.RepoAt, issueIdInt) 295 + commentIdInt64 := int64(comment.CommentId) 296 + issue, err := db.GetIssue(rp.db, f.RepoAt(), issueIdInt) 298 297 if err != nil { 299 - log.Println("failed to get issue at", err) 298 + log.Println("failed to get issue", err) 300 299 rp.pages.Notice(w, "issue-comment", "Failed to create comment.") 301 300 return 302 301 } 303 302 304 - atUri := f.RepoAt.String() 303 + atUri := comment.RepoAt.String() 305 304 client, err := rp.oauth.AuthorizedClient(r) 306 305 if err != nil { 307 306 log.Println("failed to get authorized client", err) 308 307 309 308 310 309 310 + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 311 + Collection: tangled.RepoIssueCommentNSID, 312 + Repo: user.Did, 313 + Rkey: comment.Rkey, 314 + Record: &lexutil.LexiconTypeDecoder{ 315 + Val: &tangled.RepoIssueComment{ 316 + Repo: &atUri, 317 + Issue: issue.IssueAt, 318 + CommentId: &commentIdInt64, 319 + Owner: &comment.OwnerDid, 320 + Body: body, 321 + CreatedAt: createdAt, 322 + }, 311 323 312 324 313 325 314 326 315 327 328 + return 329 + } 316 330 331 + mentions := markup.FindUserMentions(comment.Body) 317 332 333 + rp.notifier.NewIssueComment(r.Context(), &f.Repo, issue, comment, mentions) 318 334 319 - 320 - 321 - 322 - 323 - 324 - 325 - 326 - 327 - 328 - 329 - 330 - 331 - 332 - 333 - 334 - 335 - 335 + rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", f.OwnerSlashRepo(), comment.Issue, comment.CommentId)) 336 + return 337 + } 338 + } 336 339 337 340 338 341 ··· 358 361 return 359 362 } 360 363 361 - issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt) 364 + issue, err := db.GetIssue(rp.db, f.RepoAt(), issueIdInt) 362 365 if err != nil { 363 366 log.Println("failed to get issue", err) 364 367 rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 365 368 return 366 369 } 367 370 368 - comment, err := db.GetComment(rp.db, f.RepoAt, issueIdInt, commentIdInt) 371 + comment, err := db.GetComment(rp.db, f.RepoAt(), issueIdInt, commentIdInt) 369 372 if err != nil { 370 373 http.Error(w, "bad comment id", http.StatusBadRequest) 371 374 return ··· 417 420 return 418 421 } 419 422 420 - issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt) 423 + issue, err := db.GetIssue(rp.db, f.RepoAt(), issueIdInt) 421 424 if err != nil { 422 425 log.Println("failed to get issue", err) 423 426 rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 424 427 return 425 428 } 426 429 427 - comment, err := db.GetComment(rp.db, f.RepoAt, issueIdInt, commentIdInt) 430 + comment, err := db.GetComment(rp.db, f.RepoAt(), issueIdInt, commentIdInt) 428 431 if err != nil { 429 432 http.Error(w, "bad comment id", http.StatusBadRequest) 430 433 return ··· 539 542 return 540 543 } 541 544 542 - issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt) 545 + issue, err := db.GetIssue(rp.db, f.RepoAt(), issueIdInt) 543 546 if err != nil { 544 547 log.Println("failed to get issue", err) 545 548 rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.") ··· 554 557 return 555 558 } 556 559 557 - comment, err := db.GetComment(rp.db, f.RepoAt, issueIdInt, commentIdInt) 560 + comment, err := db.GetComment(rp.db, f.RepoAt(), issueIdInt, commentIdInt) 558 561 if err != nil { 559 562 http.Error(w, "bad comment id", http.StatusBadRequest) 560 563 return ··· 572 575 573 576 // optimistic deletion 574 577 deleted := time.Now() 575 - err = db.DeleteComment(rp.db, f.RepoAt, issueIdInt, commentIdInt) 578 + err = db.DeleteComment(rp.db, f.RepoAt(), issueIdInt, commentIdInt) 576 579 if err != nil { 577 580 log.Println("failed to delete comment") 578 581 rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment") ··· 641 644 return 642 645 } 643 646 644 - issues, err := db.GetIssues(rp.db, f.RepoAt, isOpen, page) 647 + issues, err := db.GetIssues(rp.db, f.RepoAt(), isOpen, page) 645 648 if err != nil { 646 649 log.Println("failed to get issues", err) 647 650 rp.pages.Notice(w, "issues", "Failed to load issues. Try again later.") ··· 704 707 } 705 708 706 709 issue := &db.Issue{ 707 - RepoAt: f.RepoAt, 710 + RepoAt: f.RepoAt(), 708 711 Title: title, 709 712 Body: body, 710 713 OwnerDid: user.Did, ··· 722 725 rp.pages.Notice(w, "issues", "Failed to create issue.") 723 726 return 724 727 } 725 - atUri := f.RepoAt.String() 728 + atUri := f.RepoAt().String() 726 729 resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 727 730 Collection: tangled.RepoIssueNSID, 728 731 Repo: user.Did, ··· 743 746 return 744 747 } 745 748 746 - err = db.SetIssueAt(rp.db, f.RepoAt, issue.IssueId, resp.Uri) 749 + err = db.SetIssueAt(rp.db, f.RepoAt(), issue.IssueId, resp.Uri) 747 750 if err != nil { 748 751 log.Println("failed to set issue at", err) 749 752 rp.pages.Notice(w, "issues", "Failed to create issue.")
+3 -8
appview/middleware/middleware.go
··· 8 8 "slices" 9 9 "strconv" 10 10 "strings" 11 - "time" 12 11 13 12 "github.com/bluesky-social/indigo/atproto/identity" 14 13 "github.com/go-chi/chi/v5" ··· 214 213 return 215 214 } 216 215 217 - ctx := context.WithValue(req.Context(), "knot", repo.Knot) 218 - ctx = context.WithValue(ctx, "repoAt", repo.AtUri) 219 - ctx = context.WithValue(ctx, "repoDescription", repo.Description) 220 - ctx = context.WithValue(ctx, "repoSpindle", repo.Spindle) 221 - ctx = context.WithValue(ctx, "repoAddedAt", repo.Created.Format(time.RFC3339)) 216 + ctx := context.WithValue(req.Context(), "repo", repo) 222 217 next.ServeHTTP(w, req.WithContext(ctx)) 223 218 }) 224 219 } ··· 243 238 return 244 239 } 245 240 246 - pr, err := db.GetPull(mw.db, f.RepoAt, prIdInt) 241 + pr, err := db.GetPull(mw.db, f.RepoAt(), prIdInt) 247 242 if err != nil { 248 243 log.Println("failed to get pull and comments", err) 249 244 return ··· 284 279 return 285 280 } 286 281 287 - fullName := f.OwnerHandle() + "/" + f.RepoName 282 + fullName := f.OwnerHandle() + "/" + f.Name 288 283 289 284 if r.Header.Get("User-Agent") == "Go-http-client/1.1" { 290 285 if r.URL.Query().Get("go-get") == "1" {
+51 -53
appview/pulls/pulls.go
··· 16 16 17 17 18 18 19 + "tangled.sh/tangled.sh/core/appview/notify" 20 + "tangled.sh/tangled.sh/core/appview/oauth" 21 + "tangled.sh/tangled.sh/core/appview/pages" 22 + "tangled.sh/tangled.sh/core/appview/pages/markup" 23 + "tangled.sh/tangled.sh/core/appview/reporesolver" 24 + "tangled.sh/tangled.sh/core/idresolver" 25 + "tangled.sh/tangled.sh/core/knotclient" 19 26 20 27 21 28 22 29 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - 32 - 33 - 34 - 30 + "github.com/bluekeyes/go-gitdiff/gitdiff" 31 + comatproto "github.com/bluesky-social/indigo/api/atproto" 32 + lexutil "github.com/bluesky-social/indigo/lex/util" 33 + "github.com/go-chi/chi/v5" 34 + "github.com/google/uuid" 35 35 36 36 37 37 ··· 257 257 patch = mergeable.CombinedPatch() 258 258 } 259 259 260 - resp, err := ksClient.MergeCheck([]byte(patch), f.OwnerDid(), f.RepoName, pull.TargetBranch) 260 + resp, err := ksClient.MergeCheck([]byte(patch), f.OwnerDid(), f.Name, pull.TargetBranch) 261 261 if err != nil { 262 262 log.Println("failed to check for mergeability:", err) 263 263 return types.MergeCheckResponse{ ··· 318 318 // pulls within the same repo 319 319 knot = f.Knot 320 320 ownerDid = f.OwnerDid() 321 - repoName = f.RepoName 321 + repoName = f.Name 322 322 } 323 323 324 324 us, err := knotclient.NewUnsignedClient(knot, s.config.Core.Dev) ··· 529 529 530 530 pulls, err := db.GetPulls( 531 531 s.db, 532 - db.FilterEq("repo_at", f.RepoAt), 532 + db.FilterEq("repo_at", f.RepoAt()), 533 533 db.FilterEq("state", state), 534 534 ) 535 535 if err != nil { ··· 671 671 createdAt := time.Now().Format(time.RFC3339) 672 672 ownerDid := user.Did 673 673 674 - pullAt, err := db.GetPullAt(s.db, f.RepoAt, pull.PullId) 674 + pullAt, err := db.GetPullAt(s.db, f.RepoAt(), pull.PullId) 675 675 if err != nil { 676 676 log.Println("failed to get pull at", err) 677 677 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 678 678 return 679 679 } 680 680 681 - atUri := f.RepoAt.String() 681 + atUri := f.RepoAt().String() 682 682 client, err := s.oauth.AuthorizedClient(r) 683 683 if err != nil { 684 684 log.Println("failed to get authorized client", err) ··· 707 707 708 708 comment := &db.PullComment{ 709 709 OwnerDid: user.Did, 710 - RepoAt: f.RepoAt.String(), 710 + RepoAt: f.RepoAt().String(), 711 711 PullId: pull.PullId, 712 712 Body: body, 713 713 CommentAt: atResp.Uri, ··· 726 726 727 727 728 728 729 + return 730 + } 729 731 732 + mentions := markup.FindUserMentions(comment.Body) 730 733 734 + s.notifier.NewPullComment(r.Context(), &f.Repo, pull, comment, mentions) 731 735 732 - 733 - 734 - 735 - 736 + s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", f.OwnerSlashRepo(), pull.PullId, commentId)) 737 + return 736 738 737 739 738 740 ··· 753 755 return 754 756 } 755 757 756 - result, err := us.Branches(f.OwnerDid(), f.RepoName) 758 + result, err := us.Branches(f.OwnerDid(), f.Name) 757 759 if err != nil { 758 760 log.Println("failed to fetch branches", err) 759 761 return ··· 877 879 return 878 880 } 879 881 880 - comparison, err := ksClient.Compare(f.OwnerDid(), f.RepoName, targetBranch, sourceBranch) 882 + comparison, err := ksClient.Compare(f.OwnerDid(), f.Name, targetBranch, sourceBranch) 881 883 if err != nil { 882 884 log.Println("failed to compare", err) 883 885 s.pages.Notice(w, "pull", err.Error()) ··· 976 978 977 979 978 980 981 + return 982 + } 979 983 984 + forkAtUri := fork.RepoAt() 985 + forkAtUriStr := forkAtUri.String() 980 986 987 + pullSource := &db.PullSource{ 988 + Branch: sourceBranch, 981 989 982 - 983 - 984 - 985 - 986 - 987 - 988 - 989 - 990 - 991 - 992 - 993 - 994 - 995 - 996 - 997 - 990 + } 991 + recordPullSource := &tangled.RepoPull_Source{ 992 + Branch: sourceBranch, 993 + Repo: &forkAtUriStr, 994 + Sha: sourceRev, 995 + } 998 996 999 997 1000 998 ··· 1068 1066 Body: body, 1069 1067 TargetBranch: targetBranch, 1070 1068 OwnerDid: user.Did, 1071 - RepoAt: f.RepoAt, 1069 + RepoAt: f.RepoAt(), 1072 1070 Rkey: rkey, 1073 1071 Submissions: []*db.PullSubmission{ 1074 1072 &initialSubmission, ··· 1081 1079 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1082 1080 return 1083 1081 } 1084 - pullId, err := db.NextPullId(tx, f.RepoAt) 1082 + pullId, err := db.NextPullId(tx, f.RepoAt()) 1085 1083 if err != nil { 1086 1084 log.Println("failed to get pull id", err) 1087 1085 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") ··· 1096 1094 Val: &tangled.RepoPull{ 1097 1095 Title: title, 1098 1096 PullId: int64(pullId), 1099 - TargetRepo: string(f.RepoAt), 1097 + TargetRepo: string(f.RepoAt()), 1100 1098 TargetBranch: targetBranch, 1101 1099 Patch: patch, 1102 1100 Source: recordPullSource, ··· 1274 1272 return 1275 1273 } 1276 1274 1277 - result, err := us.Branches(f.OwnerDid(), f.RepoName) 1275 + result, err := us.Branches(f.OwnerDid(), f.Name) 1278 1276 if err != nil { 1279 1277 log.Println("failed to reach knotserver", err) 1280 1278 return ··· 1358 1356 return 1359 1357 } 1360 1358 1361 - targetResult, err := targetBranchesClient.Branches(f.OwnerDid(), f.RepoName) 1359 + targetResult, err := targetBranchesClient.Branches(f.OwnerDid(), f.Name) 1362 1360 if err != nil { 1363 1361 log.Println("failed to reach knotserver for target branches", err) 1364 1362 return ··· 1474 1472 return 1475 1473 } 1476 1474 1477 - comparison, err := ksClient.Compare(f.OwnerDid(), f.RepoName, pull.TargetBranch, pull.PullSource.Branch) 1475 + comparison, err := ksClient.Compare(f.OwnerDid(), f.Name, pull.TargetBranch, pull.PullSource.Branch) 1478 1476 if err != nil { 1479 1477 log.Printf("compare request failed: %s", err) 1480 1478 s.pages.Notice(w, "resubmit-error", err.Error()) ··· 1658 1656 Val: &tangled.RepoPull{ 1659 1657 Title: pull.Title, 1660 1658 PullId: int64(pull.PullId), 1661 - TargetRepo: string(f.RepoAt), 1659 + TargetRepo: string(f.RepoAt()), 1662 1660 TargetBranch: pull.TargetBranch, 1663 1661 Patch: patch, // new patch 1664 1662 Source: recordPullSource, ··· 1985 1983 } 1986 1984 1987 1985 // Merge the pull request 1988 - resp, err := ksClient.Merge([]byte(patch), f.OwnerDid(), f.RepoName, pull.TargetBranch, pull.Title, pull.Body, ident.Handle.String(), email.Address) 1986 + resp, err := ksClient.Merge([]byte(patch), f.OwnerDid(), f.Name, pull.TargetBranch, pull.Title, pull.Body, ident.Handle.String(), email.Address) 1989 1987 if err != nil { 1990 1988 log.Printf("failed to merge pull request: %s", err) 1991 1989 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") ··· 2007 2005 defer tx.Rollback() 2008 2006 2009 2007 for _, p := range pullsToMerge { 2010 - err := db.MergePull(tx, f.RepoAt, p.PullId) 2008 + err := db.MergePull(tx, f.RepoAt(), p.PullId) 2011 2009 if err != nil { 2012 2010 log.Printf("failed to update pull request status in database: %s", err) 2013 2011 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") ··· 2023 2021 return 2024 2022 } 2025 2023 2026 - s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, pull.PullId)) 2024 + s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.Name, pull.PullId)) 2027 2025 } 2028 2026 2029 2027 func (s *Pulls) ClosePull(w http.ResponseWriter, r *http.Request) { ··· 2075 2073 2076 2074 for _, p := range pullsToClose { 2077 2075 // Close the pull in the database 2078 - err = db.ClosePull(tx, f.RepoAt, p.PullId) 2076 + err = db.ClosePull(tx, f.RepoAt(), p.PullId) 2079 2077 if err != nil { 2080 2078 log.Println("failed to close pull", err) 2081 2079 s.pages.Notice(w, "pull-close", "Failed to close pull.") ··· 2143 2141 2144 2142 for _, p := range pullsToReopen { 2145 2143 // Close the pull in the database 2146 - err = db.ReopenPull(tx, f.RepoAt, p.PullId) 2144 + err = db.ReopenPull(tx, f.RepoAt(), p.PullId) 2147 2145 if err != nil { 2148 2146 log.Println("failed to close pull", err) 2149 2147 s.pages.Notice(w, "pull-close", "Failed to close pull.") ··· 2195 2193 Body: body, 2196 2194 TargetBranch: targetBranch, 2197 2195 OwnerDid: user.Did, 2198 - RepoAt: f.RepoAt, 2196 + RepoAt: f.RepoAt(), 2199 2197 Rkey: rkey, 2200 2198 Submissions: []*db.PullSubmission{ 2201 2199 &initialSubmission,
+6 -6
appview/repo/artifact.go
··· 76 76 Artifact: uploadBlobResp.Blob, 77 77 CreatedAt: createdAt.Format(time.RFC3339), 78 78 Name: handler.Filename, 79 - Repo: f.RepoAt.String(), 79 + Repo: f.RepoAt().String(), 80 80 Tag: tag.Tag.Hash[:], 81 81 }, 82 82 }, ··· 100 100 artifact := db.Artifact{ 101 101 Did: user.Did, 102 102 Rkey: rkey, 103 - RepoAt: f.RepoAt, 103 + RepoAt: f.RepoAt(), 104 104 Tag: tag.Tag.Hash, 105 105 CreatedAt: createdAt, 106 106 BlobCid: cid.Cid(uploadBlobResp.Blob.Ref), ··· 155 155 156 156 artifacts, err := db.GetArtifact( 157 157 rp.db, 158 - db.FilterEq("repo_at", f.RepoAt), 158 + db.FilterEq("repo_at", f.RepoAt()), 159 159 db.FilterEq("tag", tag.Tag.Hash[:]), 160 160 db.FilterEq("name", filename), 161 161 ) ··· 197 197 198 198 artifacts, err := db.GetArtifact( 199 199 rp.db, 200 - db.FilterEq("repo_at", f.RepoAt), 200 + db.FilterEq("repo_at", f.RepoAt()), 201 201 db.FilterEq("tag", tag[:]), 202 202 db.FilterEq("name", filename), 203 203 ) ··· 239 239 defer tx.Rollback() 240 240 241 241 err = db.DeleteArtifact(tx, 242 - db.FilterEq("repo_at", f.RepoAt), 242 + db.FilterEq("repo_at", f.RepoAt()), 243 243 db.FilterEq("tag", artifact.Tag[:]), 244 244 db.FilterEq("name", filename), 245 245 ) ··· 270 270 return nil, err 271 271 } 272 272 273 - result, err := us.Tags(f.OwnerDid(), f.RepoName) 273 + result, err := us.Tags(f.OwnerDid(), f.Name) 274 274 if err != nil { 275 275 log.Println("failed to reach knotserver", err) 276 276 return nil, err
+5 -5
appview/repo/index.go
··· 37 37 return 38 38 } 39 39 40 - result, err := us.Index(f.OwnerDid(), f.RepoName, ref) 40 + result, err := us.Index(f.OwnerDid(), f.Name, ref) 41 41 if err != nil { 42 42 rp.pages.Error503(w) 43 43 log.Println("failed to reach knotserver", err) ··· 166 166 // first attempt to fetch from db 167 167 langs, err := db.GetRepoLanguages( 168 168 rp.db, 169 - db.FilterEq("repo_at", f.RepoAt), 169 + db.FilterEq("repo_at", f.RepoAt()), 170 170 db.FilterEq("ref", f.Ref), 171 171 ) 172 172 173 173 if err != nil || langs == nil { 174 174 // non-fatal, fetch langs from ks 175 - ls, err := signedClient.RepoLanguages(f.OwnerDid(), f.RepoName, f.Ref) 175 + ls, err := signedClient.RepoLanguages(f.OwnerDid(), f.Name, f.Ref) 176 176 if err != nil { 177 177 return nil, err 178 178 } ··· 182 182 183 183 for l, s := range ls.Languages { 184 184 langs = append(langs, db.RepoLanguage{ 185 - RepoAt: f.RepoAt, 185 + RepoAt: f.RepoAt(), 186 186 Ref: f.Ref, 187 187 IsDefaultRef: isDefaultRef, 188 188 Language: l, ··· 279 279 hiddenRef := fmt.Sprintf("hidden/%s/%s", f.Ref, f.Ref) 280 280 281 281 var status types.AncestorCheckResponse 282 - forkSyncableResp, err := signedClient.RepoForkAheadBehind(user.Did, string(f.RepoAt), repoInfo.Name, f.Ref, hiddenRef) 282 + forkSyncableResp, err := signedClient.RepoForkAheadBehind(user.Did, string(f.RepoAt()), repoInfo.Name, f.Ref, hiddenRef) 283 283 if err != nil { 284 284 log.Printf("failed to check if fork is ahead/behind: %s", err) 285 285 return nil, err
+49 -51
appview/repo/repo.go
··· 104 104 return 105 105 } 106 106 107 - repolog, err := us.Log(f.OwnerDid(), f.RepoName, ref, page) 107 + repolog, err := us.Log(f.OwnerDid(), f.Name, ref, page) 108 108 if err != nil { 109 109 log.Println("failed to reach knotserver", err) 110 110 return 111 111 } 112 112 113 - tagResult, err := us.Tags(f.OwnerDid(), f.RepoName) 113 + tagResult, err := us.Tags(f.OwnerDid(), f.Name) 114 114 if err != nil { 115 115 log.Println("failed to reach knotserver", err) 116 116 return ··· 125 125 tagMap[hash] = append(tagMap[hash], tag.Name) 126 126 } 127 127 128 - branchResult, err := us.Branches(f.OwnerDid(), f.RepoName) 128 + branchResult, err := us.Branches(f.OwnerDid(), f.Name) 129 129 if err != nil { 130 130 log.Println("failed to reach knotserver", err) 131 131 return ··· 193 193 return 194 194 } 195 195 196 - repoAt := f.RepoAt 196 + repoAt := f.RepoAt() 197 197 rkey := repoAt.RecordKey().String() 198 198 if rkey == "" { 199 199 log.Println("invalid aturi for repo", err) ··· 243 243 Record: &lexutil.LexiconTypeDecoder{ 244 244 Val: &tangled.Repo{ 245 245 Knot: f.Knot, 246 - Name: f.RepoName, 246 + Name: f.Name, 247 247 Owner: user.Did, 248 - CreatedAt: f.CreatedAt, 248 + CreatedAt: f.Created.Format(time.RFC3339), 249 249 Description: &newDescription, 250 250 Spindle: &f.Spindle, 251 251 }, ··· 291 291 return 292 292 } 293 293 294 - resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/commit/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref)) 294 + resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/commit/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref)) 295 295 if err != nil { 296 296 log.Println("failed to reach knotserver", err) 297 297 return ··· 356 356 if !rp.config.Core.Dev { 357 357 protocol = "https" 358 358 } 359 - resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tree/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, treePath)) 359 + resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tree/%s/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref, treePath)) 360 360 if err != nil { 361 361 log.Println("failed to reach knotserver", err) 362 362 return ··· 386 386 user := rp.oauth.GetUser(r) 387 387 388 388 var breadcrumbs [][]string 389 - breadcrumbs = append(breadcrumbs, []string{f.RepoName, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)}) 389 + breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)}) 390 390 if treePath != "" { 391 391 for idx, elem := range strings.Split(treePath, "/") { 392 392 breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)}) ··· 417 417 return 418 418 } 419 419 420 - result, err := us.Tags(f.OwnerDid(), f.RepoName) 420 + result, err := us.Tags(f.OwnerDid(), f.Name) 421 421 if err != nil { 422 422 log.Println("failed to reach knotserver", err) 423 423 return 424 424 } 425 425 426 - artifacts, err := db.GetArtifact(rp.db, db.FilterEq("repo_at", f.RepoAt)) 426 + artifacts, err := db.GetArtifact(rp.db, db.FilterEq("repo_at", f.RepoAt())) 427 427 if err != nil { 428 428 log.Println("failed grab artifacts", err) 429 429 return ··· 474 474 return 475 475 } 476 476 477 - result, err := us.Branches(f.OwnerDid(), f.RepoName) 477 + result, err := us.Branches(f.OwnerDid(), f.Name) 478 478 if err != nil { 479 479 log.Println("failed to reach knotserver", err) 480 480 return ··· 503 503 if !rp.config.Core.Dev { 504 504 protocol = "https" 505 505 } 506 - resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath)) 506 + resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref, filePath)) 507 507 if err != nil { 508 508 log.Println("failed to reach knotserver", err) 509 509 return ··· 523 523 } 524 524 525 525 var breadcrumbs [][]string 526 - breadcrumbs = append(breadcrumbs, []string{f.RepoName, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)}) 526 + breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)}) 527 527 if filePath != "" { 528 528 for idx, elem := range strings.Split(filePath, "/") { 529 529 breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)}) ··· 556 556 557 557 // fetch the actual binary content like in RepoBlobRaw 558 558 559 - blobURL := fmt.Sprintf("%s://%s/%s/%s/raw/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath) 559 + blobURL := fmt.Sprintf("%s://%s/%s/%s/raw/%s/%s", protocol, f.Knot, f.OwnerDid(), f.Name, ref, filePath) 560 560 contentSrc = blobURL 561 561 if !rp.config.Core.Dev { 562 562 contentSrc = markup.GenerateCamoURL(rp.config.Camo.Host, rp.config.Camo.SharedSecret, blobURL) ··· 593 593 if !rp.config.Core.Dev { 594 594 protocol = "https" 595 595 } 596 - blobURL := fmt.Sprintf("%s://%s/%s/%s/raw/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath) 596 + blobURL := fmt.Sprintf("%s://%s/%s/%s/raw/%s/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref, filePath) 597 597 resp, err := http.Get(blobURL) 598 598 if err != nil { 599 599 log.Println("failed to reach knotserver:", err) ··· 649 649 return 650 650 } 651 651 652 - repoAt := f.RepoAt 652 + repoAt := f.RepoAt() 653 653 rkey := repoAt.RecordKey().String() 654 654 if rkey == "" { 655 655 fail("Failed to resolve repo. Try again later", err) ··· 703 703 Record: &lexutil.LexiconTypeDecoder{ 704 704 Val: &tangled.Repo{ 705 705 Knot: f.Knot, 706 - Name: f.RepoName, 706 + Name: f.Name, 707 707 Owner: user.Did, 708 - CreatedAt: f.CreatedAt, 708 + CreatedAt: f.Created.Format(time.RFC3339), 709 709 Description: &f.Description, 710 710 Spindle: spindlePtr, 711 711 }, ··· 786 786 Record: &lexutil.LexiconTypeDecoder{ 787 787 Val: &tangled.RepoCollaborator{ 788 788 Subject: collaboratorIdent.DID.String(), 789 - Repo: string(f.RepoAt), 789 + Repo: string(f.RepoAt()), 790 790 CreatedAt: createdAt.Format(time.RFC3339), 791 791 }}, 792 792 }) ··· 811 811 return 812 812 } 813 813 814 - ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.RepoName, collaboratorIdent.DID.String()) 814 + ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.Name, collaboratorIdent.DID.String()) 815 815 if err != nil { 816 816 fail("Knot was unreachable.", err) 817 817 return ··· 845 845 Did: syntax.DID(currentUser.Did), 846 846 Rkey: rkey, 847 847 SubjectDid: collaboratorIdent.DID, 848 - RepoAt: f.RepoAt, 848 + RepoAt: f.RepoAt(), 849 849 Created: createdAt, 850 850 }) 851 851 if err != nil { ··· 883 883 log.Println("failed to get authorized client", err) 884 884 return 885 885 } 886 - repoRkey := f.RepoAt.RecordKey().String() 887 886 _, err = xrpcClient.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 888 887 Collection: tangled.RepoNSID, 889 888 Repo: user.Did, 890 - Rkey: repoRkey, 889 + Rkey: f.Rkey, 891 890 }) 892 891 if err != nil { 893 892 log.Printf("failed to delete record: %s", err) 894 893 rp.pages.Notice(w, "settings-delete", "Failed to delete repository from PDS.") 895 894 return 896 895 } 897 - log.Println("removed repo record ", f.RepoAt.String()) 896 + log.Println("removed repo record ", f.RepoAt().String()) 898 897 899 898 secret, err := db.GetRegistrationKey(rp.db, f.Knot) 900 899 if err != nil { ··· 908 907 return 909 908 } 910 909 911 - ksResp, err := ksClient.RemoveRepo(f.OwnerDid(), f.RepoName) 910 + ksResp, err := ksClient.RemoveRepo(f.OwnerDid(), f.Name) 912 911 if err != nil { 913 912 log.Printf("failed to make request to %s: %s", f.Knot, err) 914 913 return ··· 954 953 } 955 954 956 955 // remove repo from db 957 - err = db.RemoveRepo(tx, f.OwnerDid(), f.RepoName) 956 + err = db.RemoveRepo(tx, f.OwnerDid(), f.Name) 958 957 if err != nil { 959 958 rp.pages.Notice(w, "settings-delete", "Failed to update appview") 960 959 return ··· 1003 1002 return 1004 1003 } 1005 1004 1006 - ksResp, err := ksClient.SetDefaultBranch(f.OwnerDid(), f.RepoName, branch) 1005 + ksResp, err := ksClient.SetDefaultBranch(f.OwnerDid(), f.Name, branch) 1007 1006 if err != nil { 1008 1007 log.Printf("failed to make request to %s: %s", f.Knot, err) 1009 1008 return ··· 1070 1069 r.Context(), 1071 1070 spindleClient, 1072 1071 &tangled.RepoAddSecret_Input{ 1073 - Repo: f.RepoAt.String(), 1072 + Repo: f.RepoAt().String(), 1074 1073 Key: key, 1075 1074 Value: value, 1076 1075 }, ··· 1088 1087 r.Context(), 1089 1088 spindleClient, 1090 1089 &tangled.RepoRemoveSecret_Input{ 1091 - Repo: f.RepoAt.String(), 1090 + Repo: f.RepoAt().String(), 1092 1091 Key: key, 1093 1092 }, 1094 1093 ) ··· 1150 1149 // return 1151 1150 // } 1152 1151 1153 - // result, err := us.Branches(f.OwnerDid(), f.RepoName) 1152 + // result, err := us.Branches(f.OwnerDid(), f.Name) 1154 1153 // if err != nil { 1155 1154 // log.Println("failed to reach knotserver", err) 1156 1155 // return ··· 1172 1171 // oauth.WithDev(rp.config.Core.Dev), 1173 1172 // ); err != nil { 1174 1173 // log.Println("failed to create spindle client", err) 1175 - // } else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt.String()); err != nil { 1174 + // } else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt().String()); err != nil { 1176 1175 // log.Println("failed to fetch secrets", err) 1177 1176 // } else { 1178 1177 // secrets = resp.Secrets ··· 1201 1200 return 1202 1201 } 1203 1202 1204 - result, err := us.Branches(f.OwnerDid(), f.RepoName) 1203 + result, err := us.Branches(f.OwnerDid(), f.Name) 1205 1204 if err != nil { 1206 1205 log.Println("failed to reach knotserver", err) 1207 1206 return ··· 1254 1253 oauth.WithDev(rp.config.Core.Dev), 1255 1254 ); err != nil { 1256 1255 log.Println("failed to create spindle client", err) 1257 - } else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt.String()); err != nil { 1256 + } else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt().String()); err != nil { 1258 1257 log.Println("failed to fetch secrets", err) 1259 1258 } else { 1260 1259 secrets = resp.Secrets ··· 1322 1321 } else { 1323 1322 uri = "https" 1324 1323 } 1325 - forkName := fmt.Sprintf("%s", f.RepoName) 1326 - forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.RepoName) 1324 + forkName := fmt.Sprintf("%s", f.Name) 1325 + forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.Repo.Name) 1327 1326 1328 1327 _, err = client.SyncRepoFork(user.Did, forkSourceUrl, forkName, f.Ref) 1329 1328 if err != nil { ··· 1373 1372 return 1374 1373 } 1375 1374 1376 - forkName := fmt.Sprintf("%s", f.RepoName) 1375 + forkName := fmt.Sprintf("%s", f.Name) 1377 1376 1378 1377 // this check is *only* to see if the forked repo name already exists 1379 1378 // in the user's account. 1380 - existingRepo, err := db.GetRepo(rp.db, user.Did, f.RepoName) 1379 + existingRepo, err := db.GetRepo(rp.db, user.Did, f.Name) 1381 1380 if err != nil { 1382 1381 if errors.Is(err, sql.ErrNoRows) { 1383 1382 // no existing repo with this name found, we can use the name as is ··· 1408 1407 } else { 1409 1408 uri = "https" 1410 1409 } 1411 - forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.RepoName) 1412 - sourceAt := f.RepoAt.String() 1410 + forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.Repo.Name) 1411 + sourceAt := f.RepoAt().String() 1413 1412 1414 1413 rkey := tid.TID() 1415 1414 repo := &db.Repo{ ··· 1475 1474 1476 1475 1477 1476 1477 + } 1478 + log.Println("created repo record: ", atresp.Uri) 1478 1479 1479 - 1480 - 1481 - 1482 - 1483 - 1484 - 1480 + err = db.AddRepo(tx, repo) 1481 + if err != nil { 1482 + log.Println(err) 1485 1483 1486 1484 1487 1485 ··· 1529 1527 return 1530 1528 } 1531 1529 1532 - result, err := us.Branches(f.OwnerDid(), f.RepoName) 1530 + result, err := us.Branches(f.OwnerDid(), f.Name) 1533 1531 if err != nil { 1534 1532 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1535 1533 log.Println("failed to reach knotserver", err) ··· 1559 1557 head = queryHead 1560 1558 } 1561 1559 1562 - tags, err := us.Tags(f.OwnerDid(), f.RepoName) 1560 + tags, err := us.Tags(f.OwnerDid(), f.Name) 1563 1561 if err != nil { 1564 1562 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1565 1563 log.Println("failed to reach knotserver", err) ··· 1621 1619 return 1622 1620 } 1623 1621 1624 - branches, err := us.Branches(f.OwnerDid(), f.RepoName) 1622 + branches, err := us.Branches(f.OwnerDid(), f.Name) 1625 1623 if err != nil { 1626 1624 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1627 1625 log.Println("failed to reach knotserver", err) 1628 1626 return 1629 1627 } 1630 1628 1631 - tags, err := us.Tags(f.OwnerDid(), f.RepoName) 1629 + tags, err := us.Tags(f.OwnerDid(), f.Name) 1632 1630 if err != nil { 1633 1631 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1634 1632 log.Println("failed to reach knotserver", err) 1635 1633 return 1636 1634 } 1637 1635 1638 - formatPatch, err := us.Compare(f.OwnerDid(), f.RepoName, base, head) 1636 + formatPatch, err := us.Compare(f.OwnerDid(), f.Name, base, head) 1639 1637 if err != nil { 1640 1638 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1641 1639 log.Println("failed to compare", err)
+25 -58
appview/reporesolver/resolver.go
··· 12 12 "strings" 13 13 14 14 "github.com/bluesky-social/indigo/atproto/identity" 15 - "github.com/bluesky-social/indigo/atproto/syntax" 16 15 securejoin "github.com/cyphar/filepath-securejoin" 17 16 "github.com/go-chi/chi/v5" 18 17 "tangled.sh/tangled.sh/core/appview/config" ··· 26 25 ) 27 26 28 27 type ResolvedRepo struct { 29 - Knot string 28 + db.Repo 30 29 OwnerId identity.Identity 31 - RepoName string 32 - RepoAt syntax.ATURI 33 - Description string 34 - Spindle string 35 - CreatedAt string 36 30 Ref string 37 31 CurrentDir string 38 32 ··· 51 45 } 52 46 53 47 func (rr *RepoResolver) Resolve(r *http.Request) (*ResolvedRepo, error) { 54 - repoName := chi.URLParam(r, "repo") 55 - knot, ok := r.Context().Value("knot").(string) 48 + repo, ok := r.Context().Value("repo").(*db.Repo) 56 49 if !ok { 57 - log.Println("malformed middleware") 50 + log.Println("malformed middleware: `repo` not exist in context") 58 51 return nil, fmt.Errorf("malformed middleware") 59 52 } 60 53 id, ok := r.Context().Value("resolvedId").(identity.Identity) ··· 63 56 return nil, fmt.Errorf("malformed middleware") 64 57 } 65 58 66 - repoAt, ok := r.Context().Value("repoAt").(string) 67 - if !ok { 68 - log.Println("malformed middleware") 69 - return nil, fmt.Errorf("malformed middleware") 70 - } 71 - 72 - parsedRepoAt, err := syntax.ParseATURI(repoAt) 73 - if err != nil { 74 - log.Println("malformed repo at-uri") 75 - return nil, fmt.Errorf("malformed middleware") 76 - } 77 - 78 59 ref := chi.URLParam(r, "ref") 79 60 80 61 if ref == "" { 81 - us, err := knotclient.NewUnsignedClient(knot, rr.config.Core.Dev) 62 + us, err := knotclient.NewUnsignedClient(repo.Knot, rr.config.Core.Dev) 82 63 if err != nil { 83 64 return nil, err 84 65 } 85 66 86 - defaultBranch, err := us.DefaultBranch(id.DID.String(), repoName) 67 + defaultBranch, err := us.DefaultBranch(id.DID.String(), repo.Name) 87 68 if err != nil { 88 69 return nil, err 89 70 } ··· 93 74 94 75 currentDir := path.Dir(extractPathAfterRef(r.URL.EscapedPath(), ref)) 95 76 96 - // pass through values from the middleware 97 - description, ok := r.Context().Value("repoDescription").(string) 98 - addedAt, ok := r.Context().Value("repoAddedAt").(string) 99 - spindle, ok := r.Context().Value("repoSpindle").(string) 100 - 101 77 return &ResolvedRepo{ 102 - Knot: knot, 103 - OwnerId: id, 104 - RepoName: repoName, 105 - RepoAt: parsedRepoAt, 106 - Description: description, 107 - CreatedAt: addedAt, 108 - Ref: ref, 109 - CurrentDir: currentDir, 110 - Spindle: spindle, 78 + Repo: *repo, 79 + OwnerId: id, 80 + Ref: ref, 81 + CurrentDir: currentDir, 111 82 112 83 rr: rr, 113 84 }, nil ··· 126 97 127 98 var p string 128 99 if handle != "" && !handle.IsInvalidHandle() { 129 - p, _ = securejoin.SecureJoin(fmt.Sprintf("@%s", handle), f.RepoName) 100 + p, _ = securejoin.SecureJoin(fmt.Sprintf("@%s", handle), f.Name) 130 101 } else { 131 - p, _ = securejoin.SecureJoin(f.OwnerDid(), f.RepoName) 102 + p, _ = securejoin.SecureJoin(f.OwnerDid(), f.Name) 132 103 } 133 104 134 105 return p 135 106 } 136 107 137 - func (f *ResolvedRepo) DidSlashRepo() string { 138 - p, _ := securejoin.SecureJoin(f.OwnerDid(), f.RepoName) 139 - return p 140 - } 141 - 142 108 func (f *ResolvedRepo) Collaborators(ctx context.Context) ([]pages.Collaborator, error) { 143 109 repoCollaborators, err := f.rr.enforcer.E.GetImplicitUsersForResourceByDomain(f.DidSlashRepo(), f.Knot) 144 110 if err != nil { ··· 187 153 // this function is a bit weird since it now returns RepoInfo from an entirely different 188 154 // package. we should refactor this or get rid of RepoInfo entirely. 189 155 func (f *ResolvedRepo) RepoInfo(user *oauth.User) repoinfo.RepoInfo { 156 + repoAt := f.RepoAt() 190 157 isStarred := false 191 158 if user != nil { 192 - isStarred = db.GetStarStatus(f.rr.execer, user.Did, syntax.ATURI(f.RepoAt)) 159 + isStarred = db.GetStarStatus(f.rr.execer, user.Did, repoAt) 193 160 } 194 161 195 - starCount, err := db.GetStarCount(f.rr.execer, f.RepoAt) 162 + starCount, err := db.GetStarCount(f.rr.execer, repoAt) 196 163 if err != nil { 197 - log.Println("failed to get star count for ", f.RepoAt) 164 + log.Println("failed to get star count for ", repoAt) 198 165 } 199 - issueCount, err := db.GetIssueCount(f.rr.execer, f.RepoAt) 166 + issueCount, err := db.GetIssueCount(f.rr.execer, repoAt) 200 167 if err != nil { 201 - log.Println("failed to get issue count for ", f.RepoAt) 168 + log.Println("failed to get issue count for ", repoAt) 202 169 } 203 - pullCount, err := db.GetPullCount(f.rr.execer, f.RepoAt) 170 + pullCount, err := db.GetPullCount(f.rr.execer, repoAt) 204 171 if err != nil { 205 - log.Println("failed to get issue count for ", f.RepoAt) 172 + log.Println("failed to get issue count for ", repoAt) 206 173 } 207 - source, err := db.GetRepoSource(f.rr.execer, f.RepoAt) 174 + source, err := db.GetRepoSource(f.rr.execer, repoAt) 208 175 if errors.Is(err, sql.ErrNoRows) { 209 176 source = "" 210 177 } else if err != nil { 211 - log.Println("failed to get repo source for ", f.RepoAt, err) 178 + log.Println("failed to get repo source for ", repoAt, err) 212 179 } 213 180 214 181 var sourceRepo *db.Repo ··· 233 200 if err != nil { 234 201 log.Printf("failed to create unsigned client for %s: %v", knot, err) 235 202 } else { 236 - result, err := us.Branches(f.OwnerDid(), f.RepoName) 203 + result, err := us.Branches(f.OwnerDid(), f.Name) 237 204 if err != nil { 238 - log.Printf("failed to get branches for %s/%s: %v", f.OwnerDid(), f.RepoName, err) 205 + log.Printf("failed to get branches for %s/%s: %v", f.OwnerDid(), f.Name, err) 239 206 } 240 207 241 208 if len(result.Branches) == 0 { ··· 246 213 repoInfo := repoinfo.RepoInfo{ 247 214 OwnerDid: f.OwnerDid(), 248 215 OwnerHandle: f.OwnerHandle(), 249 - Name: f.RepoName, 250 - RepoAt: f.RepoAt, 216 + Name: f.Name, 217 + RepoAt: repoAt, 251 218 Description: f.Description, 252 219 Ref: f.Ref, 253 220 IsStarred: isStarred,
+97 -10
appview/db/repos.go
··· 16 16 17 17 18 18 19 - 20 - 21 - 22 - 23 - 24 - 19 + Knot string 20 + Rkey string 21 + Created time.Time 22 + Description string 23 + Spindle string 25 24 26 25 27 26 ··· 391 390 var description, spindle sql.NullString 392 391 393 392 row := e.QueryRow(` 394 - select did, name, knot, created, at_uri, description, spindle 393 + select did, name, knot, created, description, spindle, rkey 395 394 from repos 396 395 where did = ? and name = ? 397 396 `, ··· 400 399 ) 401 400 402 401 var createdAt string 403 - if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &description, &spindle); err != nil { 402 + if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &description, &spindle, &repo.Rkey); err != nil { 404 403 return nil, err 405 404 } 406 405 createdAtTime, _ := time.Parse(time.RFC3339, createdAt) ··· 421 420 var repo Repo 422 421 var nullableDescription sql.NullString 423 422 424 - row := e.QueryRow(`select did, name, knot, created, at_uri, description from repos where at_uri = ?`, atUri) 423 + row := e.QueryRow(`select did, name, knot, created, rkey, description from repos where at_uri = ?`, atUri) 425 424 426 425 var createdAt string 427 - if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &nullableDescription); err != nil { 426 + if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.Rkey, &nullableDescription); err != nil { 428 427 return nil, err 429 428 } 430 429 createdAtTime, _ := time.Parse(time.RFC3339, createdAt) ··· 443 442 _, err := e.Exec( 444 443 `insert into repos 445 444 (did, name, knot, rkey, at_uri, description, source) 445 + values (?, ?, ?, ?, ?, ?, ?)`, 446 + repo.Did, repo.Name, repo.Knot, repo.Rkey, repo.RepoAt().String(), repo.Description, repo.Source, 447 + ) 448 + return err 449 + } 450 + 451 + 452 + 453 + 454 + 455 + 456 + 457 + 458 + 459 + 460 + 461 + 462 + 463 + 464 + 465 + 466 + var repos []Repo 467 + 468 + rows, err := e.Query( 469 + `select did, name, knot, rkey, description, created, source 470 + from repos 471 + where did = ? and source is not null and source != '' 472 + order by created desc`, 473 + 474 + 475 + 476 + 477 + 478 + 479 + 480 + 481 + 482 + 483 + var nullableDescription sql.NullString 484 + var nullableSource sql.NullString 485 + 486 + err := rows.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &nullableSource) 487 + if err != nil { 488 + return nil, err 489 + } 490 + 491 + 492 + 493 + 494 + 495 + 496 + 497 + 498 + 499 + 500 + 501 + 502 + 503 + 504 + 505 + 506 + 507 + 508 + 509 + 510 + 511 + 512 + 513 + 514 + 515 + 516 + 517 + 518 + 519 + 520 + var nullableSource sql.NullString 521 + 522 + row := e.QueryRow( 523 + `select did, name, knot, rkey, description, created, source 524 + from repos 525 + where did = ? and name = ? and source is not null and source != ''`, 526 + did, name, 527 + ) 528 + 529 + err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &nullableSource) 530 + if err != nil { 531 + return nil, err 532 + }
+13 -12
appview/state/state.go
··· 17 17 18 18 19 19 20 + "tangled.sh/tangled.sh/core/appview/cache/session" 21 + "tangled.sh/tangled.sh/core/appview/config" 22 + "tangled.sh/tangled.sh/core/appview/db" 23 + "tangled.sh/tangled.sh/core/appview/email" 24 + "tangled.sh/tangled.sh/core/appview/notify" 25 + "tangled.sh/tangled.sh/core/appview/oauth" 26 + "tangled.sh/tangled.sh/core/appview/pages" 20 27 21 28 22 29 ··· 124 131 125 132 126 133 134 + spindlestream.Start(ctx) 127 135 128 - 129 - 130 - 131 - 132 - 133 - 134 - 135 - 136 - 137 - 138 - 136 + var notifiers []notify.Notifier 137 + notifiers = append(notifiers, email.NewEmailNotifier(d, res, config)) 138 + if !config.Core.Dev { 139 + notifiers = append(notifiers, posthogService.NewPosthogNotifier(posthog)) 140 + } 139 141 140 142 141 143 ··· 410 412 // continue 411 413 } 412 414 413 - repo.AtUri = atresp.Uri 414 415 err = db.AddRepo(tx, repo) 415 416 if err != nil { 416 417 log.Println(err)
+1 -3
appview/db/star.go
··· 196 196 r.name, 197 197 r.knot, 198 198 r.rkey, 199 - r.created, 200 - r.at_uri 199 + r.created 201 200 from stars s 202 201 join repos r on s.repo_at = r.at_uri 203 202 `) ··· 222 221 &repo.Knot, 223 222 &repo.Rkey, 224 223 &repoCreatedAt, 225 - &repo.AtUri, 226 224 ); err != nil { 227 225 return nil, err 228 226 }
+10 -2
appview/pages/markup/markdown.go
··· 42 42 RendererType RendererType 43 43 } 44 44 45 - func (rctx *RenderContext) RenderMarkdown(source string) string { 45 + func NewMarkdown() goldmark.Markdown { 46 46 md := goldmark.New( 47 - goldmark.WithExtensions(extension.GFM), 47 + goldmark.WithExtensions( 48 + extension.GFM, 49 + AtExt, 50 + ), 48 51 goldmark.WithParserOptions( 49 52 parser.WithAutoHeadingID(), 50 53 ), 51 54 goldmark.WithRendererOptions(html.WithUnsafe()), 52 55 ) 56 + return md 57 + } 58 + 59 + func (rctx *RenderContext) RenderMarkdown(source string) string { 60 + md := NewMarkdown() 53 61 54 62 if rctx != nil { 55 63 var transformers []util.PrioritizedValue
+134
appview/pages/markup/markdown_at_extension.go
··· 1 + // heavily inspired by: https://github.com/kaleocheng/goldmark-extensions 2 + 3 + package markup 4 + 5 + import ( 6 + "regexp" 7 + 8 + "github.com/yuin/goldmark" 9 + "github.com/yuin/goldmark/ast" 10 + "github.com/yuin/goldmark/parser" 11 + "github.com/yuin/goldmark/renderer" 12 + "github.com/yuin/goldmark/renderer/html" 13 + "github.com/yuin/goldmark/text" 14 + "github.com/yuin/goldmark/util" 15 + ) 16 + 17 + // An AtNode struct represents an AtNode 18 + type AtNode struct { 19 + handle string 20 + ast.BaseInline 21 + } 22 + 23 + var _ ast.Node = &AtNode{} 24 + 25 + // Dump implements Node.Dump. 26 + func (n *AtNode) Dump(source []byte, level int) { 27 + ast.DumpHelper(n, source, level, nil, nil) 28 + } 29 + 30 + // KindAt is a NodeKind of the At node. 31 + var KindAt = ast.NewNodeKind("At") 32 + 33 + // Kind implements Node.Kind. 34 + func (n *AtNode) Kind() ast.NodeKind { 35 + return KindAt 36 + } 37 + 38 + var atRegexp = regexp.MustCompile(`(^|\s|\()(@)([a-zA-Z0-9.-]+)(\b)`) 39 + 40 + type atParser struct{} 41 + 42 + // NewAtParser return a new InlineParser that parses 43 + // at expressions. 44 + func NewAtParser() parser.InlineParser { 45 + return &atParser{} 46 + } 47 + 48 + func (s *atParser) Trigger() []byte { 49 + return []byte{'@'} 50 + } 51 + 52 + func (s *atParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node { 53 + line, segment := block.PeekLine() 54 + m := atRegexp.FindSubmatchIndex(line) 55 + if m == nil { 56 + return nil 57 + } 58 + block.Advance(m[1]) 59 + node := &AtNode{} 60 + node.AppendChild(node, ast.NewTextSegment(text.NewSegment(segment.Start, segment.Start+m[1]))) 61 + node.handle = string(node.Text(block.Source())[1:]) 62 + return node 63 + } 64 + 65 + // atHtmlRenderer is a renderer.NodeRenderer implementation that 66 + // renders At nodes. 67 + type atHtmlRenderer struct { 68 + html.Config 69 + } 70 + 71 + // NewAtHTMLRenderer returns a new AtHTMLRenderer. 72 + func NewAtHTMLRenderer(opts ...html.Option) renderer.NodeRenderer { 73 + r := &atHtmlRenderer{ 74 + Config: html.NewConfig(), 75 + } 76 + for _, opt := range opts { 77 + opt.SetHTMLOption(&r.Config) 78 + } 79 + return r 80 + } 81 + 82 + // RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs. 83 + func (r *atHtmlRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { 84 + reg.Register(KindAt, r.renderAt) 85 + } 86 + 87 + func (r *atHtmlRenderer) renderAt(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) { 88 + if entering { 89 + w.WriteString(`<a href="/@`) 90 + w.WriteString(n.(*AtNode).handle) 91 + w.WriteString(`" class="text-red-500">`) 92 + } else { 93 + w.WriteString("</a>") 94 + } 95 + return ast.WalkContinue, nil 96 + } 97 + 98 + type atExt struct{} 99 + 100 + // At is an extension that allow you to use at expression like '@user.bsky.social' . 101 + var AtExt = &atExt{} 102 + 103 + func (e *atExt) Extend(m goldmark.Markdown) { 104 + m.Parser().AddOptions(parser.WithInlineParsers( 105 + util.Prioritized(NewAtParser(), 500), 106 + )) 107 + m.Renderer().AddOptions(renderer.WithNodeRenderers( 108 + util.Prioritized(NewAtHTMLRenderer(), 500), 109 + )) 110 + } 111 + 112 + // FindUserMentions returns Set of user handles from given markup soruce. 113 + // It doesn't guarntee unique DIDs 114 + func FindUserMentions(source string) []string { 115 + var ( 116 + mentions []string 117 + mentionsSet = make(map[string]struct{}) 118 + md = NewMarkdown() 119 + sourceBytes = []byte(source) 120 + root = md.Parser().Parse(text.NewReader(sourceBytes)) 121 + ) 122 + ast.Walk(root, func(n ast.Node, entering bool) (ast.WalkStatus, error) { 123 + if entering && n.Kind() == KindAt { 124 + handle := n.(*AtNode).handle 125 + mentionsSet[handle] = struct{}{} 126 + return ast.WalkSkipChildren, nil 127 + } 128 + return ast.WalkContinue, nil 129 + }) 130 + for handle := range mentionsSet { 131 + mentions = append(mentions, handle) 132 + } 133 + return mentions 134 + }
+25
appview/notify/merged_notifier.go
··· 39 39 } 40 40 } 41 41 42 + func (m *mergedNotifier) NewIssueComment(ctx context.Context, repo *db.Repo, issue *db.Issue, comment *db.Comment, mentions []string) { 43 + for _, notifier := range m.notifiers { 44 + notifier.NewIssueComment(ctx, repo, issue, comment, mentions) 45 + } 46 + } 47 + 42 48 func (m *mergedNotifier) NewFollow(ctx context.Context, follow *db.Follow) { 43 49 for _, notifier := range m.notifiers { 44 50 notifier.NewFollow(ctx, follow) 51 + 52 + 53 + 54 + 55 + 56 + 57 + 58 + 59 + 60 + 61 + notifier.NewPull(ctx, pull) 62 + } 63 + } 64 + func (m *mergedNotifier) NewPullComment(ctx context.Context, repo *db.Repo, pull *db.Pull, comment *db.PullComment, mentions []string) { 65 + for _, notifier := range m.notifiers { 66 + notifier.NewPullComment(ctx, repo, pull, comment, mentions) 67 + } 68 + } 69 +
+11 -4
appview/notify/notifier.go
··· 13 13 DeleteStar(ctx context.Context, star *db.Star) 14 14 15 15 NewIssue(ctx context.Context, issue *db.Issue) 16 + NewIssueComment(ctx context.Context, repo *db.Repo, issue *db.Issue, comment *db.Comment, mentions []string) 16 17 17 18 NewFollow(ctx context.Context, follow *db.Follow) 18 19 DeleteFollow(ctx context.Context, follow *db.Follow) 19 20 21 + NewPull(ctx context.Context, pull *db.Pull) 22 + NewPullComment(ctx context.Context, repo *db.Repo, pull *db.Pull, comment *db.PullComment, mentions []string) 20 23 21 - 22 - 23 - 24 - 24 + UpdateProfile(ctx context.Context, profile *db.Profile) 25 + } 25 26 26 27 27 28 ··· 34 35 func (m *BaseNotifier) DeleteStar(ctx context.Context, star *db.Star) {} 35 36 36 37 func (m *BaseNotifier) NewIssue(ctx context.Context, issue *db.Issue) {} 38 + func (m *BaseNotifier) NewIssueComment(ctx context.Context, repo *db.Repo, issue *db.Issue, comment *db.Comment, mentions []string) {} 37 39 38 40 func (m *BaseNotifier) NewFollow(ctx context.Context, follow *db.Follow) {} 39 41 func (m *BaseNotifier) DeleteFollow(ctx context.Context, follow *db.Follow) {} 42 + 43 + func (m *BaseNotifier) NewPull(ctx context.Context, pull *db.Pull) {} 44 + func (m *BaseNotifier) NewPullComment(ctx context.Context, repo *db.Repo, pull *db.Pull, comment *db.PullComment, mentions []string) {} 45 + 46 + func (m *BaseNotifier) UpdateProfile(ctx context.Context, profile *db.Profile) {}
+29
appview/posthog/notifier.go
··· 70 70 } 71 71 } 72 72 73 + func (n *posthogNotifier) NewIssueComment(ctx context.Context, repo *db.Repo, issue *db.Issue, comment *db.Comment, mentions []string) { 74 + err := n.client.Enqueue(posthog.Capture{ 75 + DistinctId: comment.OwnerDid, 76 + Event: "new_issue", 77 + Properties: posthog.Properties{ 78 + "repo_at": comment.RepoAt.String(), 79 + "issue_id": comment.Issue, 80 + }, 81 + }) 82 + if err != nil { 83 + log.Println("failed to enqueue posthog event:", err) 84 + } 85 + } 86 + 73 87 func (n *posthogNotifier) NewPull(ctx context.Context, pull *db.Pull) { 74 88 err := n.client.Enqueue(posthog.Capture{ 75 89 DistinctId: pull.OwnerDid, 90 + 91 + 92 + 93 + 94 + 95 + 96 + 97 + 98 + } 99 + } 100 + 101 + func (n *posthogNotifier) NewPullComment(ctx context.Context, repo *db.Repo, pull *db.Pull, comment *db.PullComment, mentions []string) { 102 + err := n.client.Enqueue(posthog.Capture{ 103 + DistinctId: comment.OwnerDid, 104 + Event: "new_pull_comment",
+139
appview/email/notifier.go
··· 1 + package email 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "log" 7 + 8 + securejoin "github.com/cyphar/filepath-securejoin" 9 + "tangled.sh/tangled.sh/core/appview/config" 10 + "tangled.sh/tangled.sh/core/appview/db" 11 + "tangled.sh/tangled.sh/core/appview/notify" 12 + "tangled.sh/tangled.sh/core/idresolver" 13 + ) 14 + 15 + type EmailNotifier struct { 16 + db *db.DB 17 + idResolver *idresolver.Resolver 18 + Config *config.Config 19 + notify.BaseNotifier 20 + } 21 + 22 + func NewEmailNotifier( 23 + db *db.DB, 24 + idResolver *idresolver.Resolver, 25 + config *config.Config, 26 + ) notify.Notifier { 27 + return &EmailNotifier{ 28 + db, 29 + idResolver, 30 + config, 31 + notify.BaseNotifier{}, 32 + } 33 + } 34 + 35 + var _ notify.Notifier = &EmailNotifier{} 36 + 37 + // TODO: yeah this is just bad design. should be moved under idResolver ore include repoResolver at first place 38 + func (n *EmailNotifier) repoOwnerSlashName(ctx context.Context, repo *db.Repo) (string, error) { 39 + repoOwnerID, err := n.idResolver.ResolveIdent(ctx, repo.Did) 40 + if err != nil || repoOwnerID.Handle.IsInvalidHandle() { 41 + return "", fmt.Errorf("resolve comment owner did: %w", err) 42 + } 43 + repoOwnerHandle := repoOwnerID.Handle 44 + var repoOwnerSlashName string 45 + if repoOwnerHandle != "" && !repoOwnerHandle.IsInvalidHandle() { 46 + repoOwnerSlashName, _ = securejoin.SecureJoin(fmt.Sprintf("@%s", repoOwnerHandle), repo.Name) 47 + } else { 48 + repoOwnerSlashName = repo.DidSlashRepo() 49 + } 50 + return repoOwnerSlashName, nil 51 + } 52 + 53 + func (n *EmailNotifier) buildIssueEmail(ctx context.Context, repo *db.Repo, issue *db.Issue, comment *db.Comment, did string) (Email, error) { 54 + // TODO: check email preferences 55 + email, err := db.GetPrimaryEmail(n.db, did) 56 + if err != nil { 57 + return Email{}, fmt.Errorf("db.GetPrimaryEmail: %w", err) 58 + } 59 + commentOwner, err := n.idResolver.ResolveIdent(ctx, comment.OwnerDid) 60 + if err != nil || commentOwner.Handle.IsInvalidHandle() { 61 + return Email{}, fmt.Errorf("resolve comment owner did: %w", err) 62 + } 63 + // TODO: make this configurable 64 + baseUrl := "https://tangled.sh" 65 + repoOwnerSlashName, err := n.repoOwnerSlashName(ctx, repo) 66 + if err != nil { 67 + return Email{}, nil 68 + } 69 + url := fmt.Sprintf("%s/%s/issues/%d#comment-%d", baseUrl, repoOwnerSlashName, comment.Issue, comment.CommentId) 70 + return Email{ 71 + APIKey: n.Config.Resend.ApiKey, 72 + From: n.Config.Resend.SentFrom, 73 + To: email.Address, 74 + Subject: fmt.Sprintf("[%s] %s (issue#%d)", repoOwnerSlashName, issue.Title, issue.IssueId), 75 + Html: fmt.Sprintf(`<p><b>@%s</b> mentioned you:</p><a href="%s">View it on tangled.sh</a>.`, commentOwner.Handle.String(), url), 76 + }, nil 77 + } 78 + 79 + func (n *EmailNotifier) buildPullEmail(ctx context.Context, repo *db.Repo, pull *db.Pull, comment *db.PullComment, did string) (Email, error) { 80 + // TODO: check email preferences 81 + email, err := db.GetPrimaryEmail(n.db, did) 82 + if err != nil { 83 + return Email{}, fmt.Errorf("db.GetPrimaryEmail: %w", err) 84 + } 85 + commentOwner, err := n.idResolver.ResolveIdent(ctx, comment.OwnerDid) 86 + if err != nil || commentOwner.Handle.IsInvalidHandle() { 87 + return Email{}, fmt.Errorf("resolve comment owner did: %w", err) 88 + } 89 + repoOwnerSlashName, err := n.repoOwnerSlashName(ctx, repo) 90 + if err != nil { 91 + return Email{}, nil 92 + } 93 + // TODO: make this configurable 94 + baseUrl := "https://tangled.sh" 95 + url := fmt.Sprintf("%s/%s/pulls/%d#comment-%d", baseUrl, repoOwnerSlashName, comment.PullId, comment.ID) 96 + return Email{ 97 + APIKey: n.Config.Resend.ApiKey, 98 + From: n.Config.Resend.SentFrom, 99 + To: email.Address, 100 + Subject: fmt.Sprintf("[%s] %s (pr#%d)", repoOwnerSlashName, pull.Title, pull.PullId), 101 + Html: fmt.Sprintf(`<p><b>@%s</b> mentioned you:</p><a href="%s">View it on tangled.sh</a>.`, commentOwner.Handle.String(), url), 102 + }, nil 103 + } 104 + 105 + func (n *EmailNotifier) NewIssueComment(ctx context.Context, repo *db.Repo, issue *db.Issue, comment *db.Comment, mentions []string) { 106 + resolvedIds := n.idResolver.ResolveIdents(ctx, mentions) 107 + handleDidMap := make(map[string]string) 108 + for _, identity := range resolvedIds { 109 + if !identity.Handle.IsInvalidHandle() { 110 + handleDidMap[identity.Handle.String()] = identity.DID.String() 111 + } 112 + } 113 + for _, handle := range mentions { 114 + id, err := n.idResolver.ResolveIdent(ctx, handle) 115 + email, err := n.buildIssueEmail(ctx, repo, issue, comment, id.DID.String()) 116 + if err != nil { 117 + log.Println("failed to create issue-email:", err) 118 + } 119 + SendEmail(email) 120 + } 121 + } 122 + 123 + func (n *EmailNotifier) NewPullComment(ctx context.Context, repo *db.Repo, pull *db.Pull, comment *db.PullComment, mentions []string) { 124 + resolvedIds := n.idResolver.ResolveIdents(ctx, mentions) 125 + handleDidMap := make(map[string]string) 126 + for _, identity := range resolvedIds { 127 + if !identity.Handle.IsInvalidHandle() { 128 + handleDidMap[identity.Handle.String()] = identity.DID.String() 129 + } 130 + } 131 + for _, handle := range mentions { 132 + id, err := n.idResolver.ResolveIdent(ctx, handle) 133 + email, err := n.buildPullEmail(ctx, repo, pull, comment, id.DID.String()) 134 + if err != nil { 135 + log.Println("failed to create issue-email:", err) 136 + } 137 + SendEmail(email) 138 + } 139 + }

History

7 rounds 4 comments
sign up or login to add to the discussion
7 commits
expand
appview/pages: markup: add @ user-mention parsing in markdown
appview: add NewIssueComment event
appview: db/issues: set IssueId on GetIssue
appview: email: support multiple recipients on SendEmail
appview: email: send email notification on mention
appview: settings: add email preference setting
appview: email: notify mentioned users on pull-comments
expand 1 comment
closed without merging
7 commits
expand
appview/pages: markup: add @ user-mention parsing in markdown
appview: add NewIssueComment event
appview: db/issues: set IssueId on GetIssue
appview: email: support multiple recipients on SendEmail
appview: email: send email notification on mention
appview: settings: add email preference setting
appview: email: notify mentioned users on pull-comments
expand 0 comments
7 commits
expand
appview: reporesolver: pass entire Repo object through context
apview: replace all use of db.Repo.AtUri with db.Repo.RepoAt()
appview: db/repos: remove AtUri from Repo
appview/pages: markup: add @ user-mention parsing in markdown
appview: add NewIssueComment event
appview: email: send email notification on mention
appview: email: notify mentioned users on pull-comments
expand 0 comments
7 commits
expand
appview: reporesolver: pass entire Repo object through context
apview: replace all use of db.Repo.AtUri with db.Repo.RepoAt()
appview: db/repos: remove AtUri from Repo
appview/pages: markup: add @ user-mention parsing in markdown
appview: add NewIssueComment event
appview: email: send email notification on mention
appview: email: notify mentioned users on pull-comments
expand 0 comments
7 commits
expand
appview: reporesolver: pass entire Repo object through context
apview: replace all use of db.Repo.AtUri with db.Repo.RepoAt()
appview: db/repos: remove AtUri from Repo
appview/pages: markup: add @ user-mention parsing in markdown
appview: add NewIssueComment event
appview: email: send email notification on mention
appview: email: notify mentioned users on pull-comments
expand 0 comments
6 commits
expand
appview: reporesolver: pass entire Repo object through context
apview: replace all use of db.Repo.AtUri with db.Repo.RepoAt()
appview/pages: markup: add @ user-mention parsing in markdown
appview: add NewIssueComment event
appview: email: send email notification on mention
appview: email: notify mentioned users on pull-comments
expand 1 comment

@oppi.li I addressed your last two points. Thank you again for the detailed review.

for regex part, I'm not sure if we can share part of regex and modify by our needs.

7 commits
expand
appview: reporesolver: pass entire Repo object through context
apview: replace all use of db.Repo.AtUri with db.Repo.RepoAt()
appview/pages: markup: add @ user-mention parsing in markdown
appview: add NewIssueComment event
appview: email: send email notification on mention
appview: email: add source to email notification
appview: email: notify mentioned users on pull-comments
expand 2 comments

i'd prefer if this was stacked! it would make it slightly easier to review:

  • in 1b6628ab: can we use the handle regex from the userutil module and customize it a bit for atRegex?
  • 18b248e5 and 9eebc110 seem to be modifying the same file, it would be nice to merge the two changes (running jj absorb when editing the second change should automatically solve it!).
  • similarly, dfa71348 seems to update the logic for issue-comment emails. would be nice to collapse or clean up the diffs.

this was only a first pass of reviews, but ill go through and have a closer look at the logic in the code, and give this a go locally. thanks again for working on this, its looking pretty cool already!

@oppi.li Thank you for the review!

and yeah I agree that I should have made this PR stacked.

Tell me if you think complete resubmission in form of stacked PR will be better.