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
+563 -256
Diff #1
+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" ··· 222 221 return 223 222 } 224 223 225 - ctx := context.WithValue(req.Context(), "knot", repo.Knot) 226 - ctx = context.WithValue(ctx, "repoAt", repo.AtUri) 227 - ctx = context.WithValue(ctx, "repoDescription", repo.Description) 228 - ctx = context.WithValue(ctx, "repoSpindle", repo.Spindle) 229 - ctx = context.WithValue(ctx, "repoAddedAt", repo.Created.Format(time.RFC3339)) 224 + ctx := context.WithValue(req.Context(), "repo", repo) 230 225 next.ServeHTTP(w, req.WithContext(ctx)) 231 226 }) 232 227 } ··· 251 246 return 252 247 } 253 248 254 - pr, err := db.GetPull(mw.db, f.RepoAt, prIdInt) 249 + pr, err := db.GetPull(mw.db, f.RepoAt(), prIdInt) 255 250 if err != nil { 256 251 log.Println("failed to get pull and comments", err) 257 252 return ··· 292 287 return 293 288 } 294 289 295 - fullName := f.OwnerHandle() + "/" + f.RepoName 290 + fullName := f.OwnerHandle() + "/" + f.Name 296 291 297 292 if r.Header.Get("User-Agent") == "Go-http-client/1.1" { 298 293 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 { ··· 650 650 createdAt := time.Now().Format(time.RFC3339) 651 651 ownerDid := user.Did 652 652 653 - pullAt, err := db.GetPullAt(s.db, f.RepoAt, pull.PullId) 653 + pullAt, err := db.GetPullAt(s.db, f.RepoAt(), pull.PullId) 654 654 if err != nil { 655 655 log.Println("failed to get pull at", err) 656 656 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 657 657 return 658 658 } 659 659 660 - atUri := f.RepoAt.String() 660 + atUri := f.RepoAt().String() 661 661 client, err := s.oauth.AuthorizedClient(r) 662 662 if err != nil { 663 663 log.Println("failed to get authorized client", err) ··· 686 686 687 687 comment := &db.PullComment{ 688 688 OwnerDid: user.Did, 689 - RepoAt: f.RepoAt.String(), 689 + RepoAt: f.RepoAt().String(), 690 690 PullId: pull.PullId, 691 691 Body: body, 692 692 CommentAt: atResp.Uri, ··· 705 705 706 706 707 707 708 + return 709 + } 708 710 711 + mentions := markup.FindUserMentions(comment.Body) 709 712 713 + s.notifier.NewPullComment(r.Context(), &f.Repo, pull, comment, mentions) 710 714 711 - 712 - 713 - 714 - 715 + s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", f.OwnerSlashRepo(), pull.PullId, commentId)) 716 + return 715 717 716 718 717 719 ··· 732 734 return 733 735 } 734 736 735 - result, err := us.Branches(f.OwnerDid(), f.RepoName) 737 + result, err := us.Branches(f.OwnerDid(), f.Name) 736 738 if err != nil { 737 739 log.Println("failed to fetch branches", err) 738 740 return ··· 856 858 return 857 859 } 858 860 859 - comparison, err := ksClient.Compare(f.OwnerDid(), f.RepoName, targetBranch, sourceBranch) 861 + comparison, err := ksClient.Compare(f.OwnerDid(), f.Name, targetBranch, sourceBranch) 860 862 if err != nil { 861 863 log.Println("failed to compare", err) 862 864 s.pages.Notice(w, "pull", err.Error()) ··· 955 957 956 958 957 959 960 + return 961 + } 958 962 963 + forkAtUri := fork.RepoAt() 964 + forkAtUriStr := forkAtUri.String() 959 965 966 + pullSource := &db.PullSource{ 967 + Branch: sourceBranch, 960 968 961 - 962 - 963 - 964 - 965 - 966 - 967 - 968 - 969 - 970 - 971 - 972 - 973 - 974 - 975 - 976 - 969 + } 970 + recordPullSource := &tangled.RepoPull_Source{ 971 + Branch: sourceBranch, 972 + Repo: &forkAtUriStr, 973 + Sha: sourceRev, 974 + } 977 975 978 976 979 977 ··· 1047 1045 Body: body, 1048 1046 TargetBranch: targetBranch, 1049 1047 OwnerDid: user.Did, 1050 - RepoAt: f.RepoAt, 1048 + RepoAt: f.RepoAt(), 1051 1049 Rkey: rkey, 1052 1050 Submissions: []*db.PullSubmission{ 1053 1051 &initialSubmission, ··· 1060 1058 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1061 1059 return 1062 1060 } 1063 - pullId, err := db.NextPullId(tx, f.RepoAt) 1061 + pullId, err := db.NextPullId(tx, f.RepoAt()) 1064 1062 if err != nil { 1065 1063 log.Println("failed to get pull id", err) 1066 1064 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") ··· 1075 1073 Val: &tangled.RepoPull{ 1076 1074 Title: title, 1077 1075 PullId: int64(pullId), 1078 - TargetRepo: string(f.RepoAt), 1076 + TargetRepo: string(f.RepoAt()), 1079 1077 TargetBranch: targetBranch, 1080 1078 Patch: patch, 1081 1079 Source: recordPullSource, ··· 1253 1251 return 1254 1252 } 1255 1253 1256 - result, err := us.Branches(f.OwnerDid(), f.RepoName) 1254 + result, err := us.Branches(f.OwnerDid(), f.Name) 1257 1255 if err != nil { 1258 1256 log.Println("failed to reach knotserver", err) 1259 1257 return ··· 1337 1335 return 1338 1336 } 1339 1337 1340 - targetResult, err := targetBranchesClient.Branches(f.OwnerDid(), f.RepoName) 1338 + targetResult, err := targetBranchesClient.Branches(f.OwnerDid(), f.Name) 1341 1339 if err != nil { 1342 1340 log.Println("failed to reach knotserver for target branches", err) 1343 1341 return ··· 1453 1451 return 1454 1452 } 1455 1453 1456 - comparison, err := ksClient.Compare(f.OwnerDid(), f.RepoName, pull.TargetBranch, pull.PullSource.Branch) 1454 + comparison, err := ksClient.Compare(f.OwnerDid(), f.Name, pull.TargetBranch, pull.PullSource.Branch) 1457 1455 if err != nil { 1458 1456 log.Printf("compare request failed: %s", err) 1459 1457 s.pages.Notice(w, "resubmit-error", err.Error()) ··· 1637 1635 Val: &tangled.RepoPull{ 1638 1636 Title: pull.Title, 1639 1637 PullId: int64(pull.PullId), 1640 - TargetRepo: string(f.RepoAt), 1638 + TargetRepo: string(f.RepoAt()), 1641 1639 TargetBranch: pull.TargetBranch, 1642 1640 Patch: patch, // new patch 1643 1641 Source: recordPullSource, ··· 1964 1962 } 1965 1963 1966 1964 // Merge the pull request 1967 - resp, err := ksClient.Merge([]byte(patch), f.OwnerDid(), f.RepoName, pull.TargetBranch, pull.Title, pull.Body, ident.Handle.String(), email.Address) 1965 + resp, err := ksClient.Merge([]byte(patch), f.OwnerDid(), f.Name, pull.TargetBranch, pull.Title, pull.Body, ident.Handle.String(), email.Address) 1968 1966 if err != nil { 1969 1967 log.Printf("failed to merge pull request: %s", err) 1970 1968 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") ··· 1986 1984 defer tx.Rollback() 1987 1985 1988 1986 for _, p := range pullsToMerge { 1989 - err := db.MergePull(tx, f.RepoAt, p.PullId) 1987 + err := db.MergePull(tx, f.RepoAt(), p.PullId) 1990 1988 if err != nil { 1991 1989 log.Printf("failed to update pull request status in database: %s", err) 1992 1990 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") ··· 2002 2000 return 2003 2001 } 2004 2002 2005 - s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, pull.PullId)) 2003 + s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.Name, pull.PullId)) 2006 2004 } 2007 2005 2008 2006 func (s *Pulls) ClosePull(w http.ResponseWriter, r *http.Request) { ··· 2054 2052 2055 2053 for _, p := range pullsToClose { 2056 2054 // Close the pull in the database 2057 - err = db.ClosePull(tx, f.RepoAt, p.PullId) 2055 + err = db.ClosePull(tx, f.RepoAt(), p.PullId) 2058 2056 if err != nil { 2059 2057 log.Println("failed to close pull", err) 2060 2058 s.pages.Notice(w, "pull-close", "Failed to close pull.") ··· 2122 2120 2123 2121 for _, p := range pullsToReopen { 2124 2122 // Close the pull in the database 2125 - err = db.ReopenPull(tx, f.RepoAt, p.PullId) 2123 + err = db.ReopenPull(tx, f.RepoAt(), p.PullId) 2126 2124 if err != nil { 2127 2125 log.Println("failed to close pull", err) 2128 2126 s.pages.Notice(w, "pull-close", "Failed to close pull.") ··· 2174 2172 Body: body, 2175 2173 TargetBranch: targetBranch, 2176 2174 OwnerDid: user.Did, 2177 - RepoAt: f.RepoAt, 2175 + RepoAt: f.RepoAt(), 2178 2176 Rkey: rkey, 2179 2177 Submissions: []*db.PullSubmission{ 2180 2178 &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
+45 -47
appview/repo/repo.go
··· 98 98 return 99 99 } 100 100 101 - repolog, err := us.Log(f.OwnerDid(), f.RepoName, ref, page) 101 + repolog, err := us.Log(f.OwnerDid(), f.Name, ref, page) 102 102 if err != nil { 103 103 log.Println("failed to reach knotserver", err) 104 104 return 105 105 } 106 106 107 - tagResult, err := us.Tags(f.OwnerDid(), f.RepoName) 107 + tagResult, err := us.Tags(f.OwnerDid(), f.Name) 108 108 if err != nil { 109 109 log.Println("failed to reach knotserver", err) 110 110 return ··· 119 119 tagMap[hash] = append(tagMap[hash], tag.Name) 120 120 } 121 121 122 - branchResult, err := us.Branches(f.OwnerDid(), f.RepoName) 122 + branchResult, err := us.Branches(f.OwnerDid(), f.Name) 123 123 if err != nil { 124 124 log.Println("failed to reach knotserver", err) 125 125 return ··· 187 187 return 188 188 } 189 189 190 - repoAt := f.RepoAt 190 + repoAt := f.RepoAt() 191 191 rkey := repoAt.RecordKey().String() 192 192 if rkey == "" { 193 193 log.Println("invalid aturi for repo", err) ··· 237 237 Record: &lexutil.LexiconTypeDecoder{ 238 238 Val: &tangled.Repo{ 239 239 Knot: f.Knot, 240 - Name: f.RepoName, 240 + Name: f.Name, 241 241 Owner: user.Did, 242 - CreatedAt: f.CreatedAt, 242 + CreatedAt: f.Created.Format(time.RFC3339), 243 243 Description: &newDescription, 244 244 Spindle: &f.Spindle, 245 245 }, ··· 285 285 return 286 286 } 287 287 288 - resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/commit/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref)) 288 + resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/commit/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref)) 289 289 if err != nil { 290 290 log.Println("failed to reach knotserver", err) 291 291 return ··· 350 350 if !rp.config.Core.Dev { 351 351 protocol = "https" 352 352 } 353 - resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tree/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, treePath)) 353 + resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tree/%s/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref, treePath)) 354 354 if err != nil { 355 355 log.Println("failed to reach knotserver", err) 356 356 return ··· 380 380 user := rp.oauth.GetUser(r) 381 381 382 382 var breadcrumbs [][]string 383 - breadcrumbs = append(breadcrumbs, []string{f.RepoName, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)}) 383 + breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)}) 384 384 if treePath != "" { 385 385 for idx, elem := range strings.Split(treePath, "/") { 386 386 breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)}) ··· 411 411 return 412 412 } 413 413 414 - result, err := us.Tags(f.OwnerDid(), f.RepoName) 414 + result, err := us.Tags(f.OwnerDid(), f.Name) 415 415 if err != nil { 416 416 log.Println("failed to reach knotserver", err) 417 417 return 418 418 } 419 419 420 - artifacts, err := db.GetArtifact(rp.db, db.FilterEq("repo_at", f.RepoAt)) 420 + artifacts, err := db.GetArtifact(rp.db, db.FilterEq("repo_at", f.RepoAt())) 421 421 if err != nil { 422 422 log.Println("failed grab artifacts", err) 423 423 return ··· 468 468 return 469 469 } 470 470 471 - result, err := us.Branches(f.OwnerDid(), f.RepoName) 471 + result, err := us.Branches(f.OwnerDid(), f.Name) 472 472 if err != nil { 473 473 log.Println("failed to reach knotserver", err) 474 474 return ··· 497 497 if !rp.config.Core.Dev { 498 498 protocol = "https" 499 499 } 500 - resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath)) 500 + resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref, filePath)) 501 501 if err != nil { 502 502 log.Println("failed to reach knotserver", err) 503 503 return ··· 517 517 } 518 518 519 519 var breadcrumbs [][]string 520 - breadcrumbs = append(breadcrumbs, []string{f.RepoName, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)}) 520 + breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)}) 521 521 if filePath != "" { 522 522 for idx, elem := range strings.Split(filePath, "/") { 523 523 breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)}) ··· 557 557 if !rp.config.Core.Dev { 558 558 protocol = "https" 559 559 } 560 - resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath)) 560 + resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref, filePath)) 561 561 if err != nil { 562 562 log.Println("failed to reach knotserver", err) 563 563 return ··· 595 595 return 596 596 } 597 597 598 - repoAt := f.RepoAt 598 + repoAt := f.RepoAt() 599 599 rkey := repoAt.RecordKey().String() 600 600 if rkey == "" { 601 601 log.Println("invalid aturi for repo", err) ··· 649 649 Record: &lexutil.LexiconTypeDecoder{ 650 650 Val: &tangled.Repo{ 651 651 Knot: f.Knot, 652 - Name: f.RepoName, 652 + Name: f.Name, 653 653 Owner: user.Did, 654 - CreatedAt: f.CreatedAt, 654 + CreatedAt: f.Created.Format(time.RFC3339), 655 655 Description: &f.Description, 656 656 Spindle: &newSpindle, 657 657 }, ··· 708 708 return 709 709 } 710 710 711 - ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.RepoName, collaboratorIdent.DID.String()) 711 + ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.Name, collaboratorIdent.DID.String()) 712 712 if err != nil { 713 713 log.Printf("failed to make request to %s: %s", f.Knot, err) 714 714 return ··· 739 739 return 740 740 } 741 741 742 - err = db.AddCollaborator(rp.db, collaboratorIdent.DID.String(), f.OwnerDid(), f.RepoName, f.Knot) 742 + err = db.AddCollaborator(rp.db, collaboratorIdent.DID.String(), f.OwnerDid(), f.Name, f.Repo.Knot) 743 743 if err != nil { 744 744 w.Write(fmt.Append(nil, "failed to add collaborator: ", err)) 745 745 return ··· 778 778 log.Println("failed to get authorized client", err) 779 779 return 780 780 } 781 - repoRkey := f.RepoAt.RecordKey().String() 782 781 _, err = xrpcClient.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 783 782 Collection: tangled.RepoNSID, 784 783 Repo: user.Did, 785 - Rkey: repoRkey, 784 + Rkey: f.Rkey, 786 785 }) 787 786 if err != nil { 788 787 log.Printf("failed to delete record: %s", err) 789 788 rp.pages.Notice(w, "settings-delete", "Failed to delete repository from PDS.") 790 789 return 791 790 } 792 - log.Println("removed repo record ", f.RepoAt.String()) 791 + log.Println("removed repo record ", f.RepoAt().String()) 793 792 794 793 secret, err := db.GetRegistrationKey(rp.db, f.Knot) 795 794 if err != nil { ··· 803 802 return 804 803 } 805 804 806 - ksResp, err := ksClient.RemoveRepo(f.OwnerDid(), f.RepoName) 805 + ksResp, err := ksClient.RemoveRepo(f.OwnerDid(), f.Name) 807 806 if err != nil { 808 807 log.Printf("failed to make request to %s: %s", f.Knot, err) 809 808 return ··· 849 848 } 850 849 851 850 // remove repo from db 852 - err = db.RemoveRepo(tx, f.OwnerDid(), f.RepoName) 851 + err = db.RemoveRepo(tx, f.OwnerDid(), f.Name) 853 852 if err != nil { 854 853 rp.pages.Notice(w, "settings-delete", "Failed to update appview") 855 854 return ··· 898 897 return 899 898 } 900 899 901 - ksResp, err := ksClient.SetDefaultBranch(f.OwnerDid(), f.RepoName, branch) 900 + ksResp, err := ksClient.SetDefaultBranch(f.OwnerDid(), f.Name, branch) 902 901 if err != nil { 903 902 log.Printf("failed to make request to %s: %s", f.Knot, err) 904 903 return ··· 958 957 r.Context(), 959 958 spindleClient, 960 959 &tangled.RepoAddSecret_Input{ 961 - Repo: f.RepoAt.String(), 960 + Repo: f.RepoAt().String(), 962 961 Key: key, 963 962 Value: value, 964 963 }, ··· 973 972 r.Context(), 974 973 spindleClient, 975 974 &tangled.RepoRemoveSecret_Input{ 976 - Repo: f.RepoAt.String(), 975 + Repo: f.RepoAt().String(), 977 976 Key: key, 978 977 }, 979 978 ) ··· 1014 1013 return 1015 1014 } 1016 1015 1017 - result, err := us.Branches(f.OwnerDid(), f.RepoName) 1016 + result, err := us.Branches(f.OwnerDid(), f.Name) 1018 1017 if err != nil { 1019 1018 log.Println("failed to reach knotserver", err) 1020 1019 return ··· 1036 1035 oauth.WithDev(rp.config.Core.Dev), 1037 1036 ); err != nil { 1038 1037 log.Println("failed to create spindle client", err) 1039 - } else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt.String()); err != nil { 1038 + } else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt().String()); err != nil { 1040 1039 log.Println("failed to fetch secrets", err) 1041 1040 } else { 1042 1041 secrets = resp.Secrets ··· 1084 1083 } else { 1085 1084 uri = "https" 1086 1085 } 1087 - forkName := fmt.Sprintf("%s", f.RepoName) 1088 - forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.RepoName) 1086 + forkName := fmt.Sprintf("%s", f.Name) 1087 + forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.Repo.Name) 1089 1088 1090 1089 _, err = client.SyncRepoFork(user.Did, forkSourceUrl, forkName, f.Ref) 1091 1090 if err != nil { ··· 1135 1134 return 1136 1135 } 1137 1136 1138 - forkName := fmt.Sprintf("%s", f.RepoName) 1137 + forkName := fmt.Sprintf("%s", f.Name) 1139 1138 1140 1139 // this check is *only* to see if the forked repo name already exists 1141 1140 // in the user's account. 1142 - existingRepo, err := db.GetRepo(rp.db, user.Did, f.RepoName) 1141 + existingRepo, err := db.GetRepo(rp.db, user.Did, f.Name) 1143 1142 if err != nil { 1144 1143 if errors.Is(err, sql.ErrNoRows) { 1145 1144 // no existing repo with this name found, we can use the name as is ··· 1170 1169 } else { 1171 1170 uri = "https" 1172 1171 } 1173 - forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.RepoName) 1174 - sourceAt := f.RepoAt.String() 1172 + forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.Repo.Name) 1173 + sourceAt := f.RepoAt().String() 1175 1174 1176 1175 rkey := tid.TID() 1177 1176 repo := &db.Repo{ ··· 1237 1236 1238 1237 1239 1238 1239 + } 1240 + log.Println("created repo record: ", atresp.Uri) 1240 1241 1241 - 1242 - 1243 - 1244 - 1245 - 1246 - 1242 + err = db.AddRepo(tx, repo) 1243 + if err != nil { 1244 + log.Println(err) 1247 1245 1248 1246 1249 1247 ··· 1291 1289 return 1292 1290 } 1293 1291 1294 - result, err := us.Branches(f.OwnerDid(), f.RepoName) 1292 + result, err := us.Branches(f.OwnerDid(), f.Name) 1295 1293 if err != nil { 1296 1294 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1297 1295 log.Println("failed to reach knotserver", err) ··· 1321 1319 head = queryHead 1322 1320 } 1323 1321 1324 - tags, err := us.Tags(f.OwnerDid(), f.RepoName) 1322 + tags, err := us.Tags(f.OwnerDid(), f.Name) 1325 1323 if err != nil { 1326 1324 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1327 1325 log.Println("failed to reach knotserver", err) ··· 1383 1381 return 1384 1382 } 1385 1383 1386 - branches, err := us.Branches(f.OwnerDid(), f.RepoName) 1384 + branches, err := us.Branches(f.OwnerDid(), f.Name) 1387 1385 if err != nil { 1388 1386 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1389 1387 log.Println("failed to reach knotserver", err) 1390 1388 return 1391 1389 } 1392 1390 1393 - tags, err := us.Tags(f.OwnerDid(), f.RepoName) 1391 + tags, err := us.Tags(f.OwnerDid(), f.Name) 1394 1392 if err != nil { 1395 1393 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1396 1394 log.Println("failed to reach knotserver", err) 1397 1395 return 1398 1396 } 1399 1397 1400 - formatPatch, err := us.Compare(f.OwnerDid(), f.RepoName, base, head) 1398 + formatPatch, err := us.Compare(f.OwnerDid(), f.Name, base, head) 1401 1399 if err != nil { 1402 1400 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1403 1401 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 { ··· 186 152 // this function is a bit weird since it now returns RepoInfo from an entirely different 187 153 // package. we should refactor this or get rid of RepoInfo entirely. 188 154 func (f *ResolvedRepo) RepoInfo(user *oauth.User) repoinfo.RepoInfo { 155 + repoAt := f.RepoAt() 189 156 isStarred := false 190 157 if user != nil { 191 - isStarred = db.GetStarStatus(f.rr.execer, user.Did, syntax.ATURI(f.RepoAt)) 158 + isStarred = db.GetStarStatus(f.rr.execer, user.Did, repoAt) 192 159 } 193 160 194 - starCount, err := db.GetStarCount(f.rr.execer, f.RepoAt) 161 + starCount, err := db.GetStarCount(f.rr.execer, repoAt) 195 162 if err != nil { 196 - log.Println("failed to get star count for ", f.RepoAt) 163 + log.Println("failed to get star count for ", repoAt) 197 164 } 198 - issueCount, err := db.GetIssueCount(f.rr.execer, f.RepoAt) 165 + issueCount, err := db.GetIssueCount(f.rr.execer, repoAt) 199 166 if err != nil { 200 - log.Println("failed to get issue count for ", f.RepoAt) 167 + log.Println("failed to get issue count for ", repoAt) 201 168 } 202 - pullCount, err := db.GetPullCount(f.rr.execer, f.RepoAt) 169 + pullCount, err := db.GetPullCount(f.rr.execer, repoAt) 203 170 if err != nil { 204 - log.Println("failed to get issue count for ", f.RepoAt) 171 + log.Println("failed to get issue count for ", repoAt) 205 172 } 206 - source, err := db.GetRepoSource(f.rr.execer, f.RepoAt) 173 + source, err := db.GetRepoSource(f.rr.execer, repoAt) 207 174 if errors.Is(err, sql.ErrNoRows) { 208 175 source = "" 209 176 } else if err != nil { 210 - log.Println("failed to get repo source for ", f.RepoAt, err) 177 + log.Println("failed to get repo source for ", repoAt, err) 211 178 } 212 179 213 180 var sourceRepo *db.Repo ··· 232 199 if err != nil { 233 200 log.Printf("failed to create unsigned client for %s: %v", knot, err) 234 201 } else { 235 - result, err := us.Branches(f.OwnerDid(), f.RepoName) 202 + result, err := us.Branches(f.OwnerDid(), f.Name) 236 203 if err != nil { 237 - log.Printf("failed to get branches for %s/%s: %v", f.OwnerDid(), f.RepoName, err) 204 + log.Printf("failed to get branches for %s/%s: %v", f.OwnerDid(), f.Name, err) 238 205 } 239 206 240 207 if len(result.Branches) == 0 { ··· 245 212 repoInfo := repoinfo.RepoInfo{ 246 213 OwnerDid: f.OwnerDid(), 247 214 OwnerHandle: f.OwnerHandle(), 248 - Name: f.RepoName, 249 - RepoAt: f.RepoAt, 215 + Name: f.Name, 216 + RepoAt: repoAt, 250 217 Description: f.Description, 251 218 Ref: f.Ref, 252 219 IsStarred: isStarred,
+7 -4
appview/db/repos.go
··· 391 391 var description, spindle sql.NullString 392 392 393 393 row := e.QueryRow(` 394 - select did, name, knot, created, at_uri, description, spindle 394 + select did, name, knot, created, at_uri, description, spindle, rkey 395 395 from repos 396 396 where did = ? and name = ? 397 397 `, ··· 400 400 ) 401 401 402 402 var createdAt string 403 - if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &description, &spindle); err != nil { 403 + if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &description, &spindle, &repo.Rkey); err != nil { 404 404 return nil, err 405 405 } 406 406 createdAtTime, _ := time.Parse(time.RFC3339, createdAt) ··· 421 421 var repo Repo 422 422 var nullableDescription sql.NullString 423 423 424 - row := e.QueryRow(`select did, name, knot, created, at_uri, description from repos where at_uri = ?`, atUri) 424 + row := e.QueryRow(`select did, name, knot, created, at_uri, rkey, description from repos where at_uri = ?`, atUri) 425 425 426 426 var createdAt string 427 - if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &nullableDescription); err != nil { 427 + if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &repo.Rkey, &nullableDescription); err != nil { 428 428 return nil, err 429 429 } 430 430 createdAtTime, _ := time.Parse(time.RFC3339, createdAt) ··· 440 440 } 441 441 442 442 func AddRepo(e Execer, repo *Repo) error { 443 + if repo.AtUri == "" { 444 + repo.AtUri = repo.RepoAt().String() 445 + } 443 446 _, err := e.Exec( 444 447 `insert into repos 445 448 (did, name, knot, rkey, at_uri, description, source)
+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 ··· 123 130 124 131 125 132 133 + spindlestream.Start(ctx) 126 134 127 - 128 - 129 - 130 - 131 - 132 - 133 - 134 - 135 - 136 - 137 - 135 + var notifiers []notify.Notifier 136 + notifiers = append(notifiers, email.NewEmailNotifier(d, res, config)) 137 + if !config.Core.Dev { 138 + notifiers = append(notifiers, posthog_service.NewPosthogNotifier(posthog)) 139 + } 138 140 139 141 140 142 ··· 395 397 // continue 396 398 } 397 399 398 - repo.AtUri = atresp.Uri 399 400 err = db.AddRepo(tx, repo) 400 401 if err != nil { 401 402 log.Println(err)
+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.