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
+656 -269
Diff #4
+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.") ··· 127 128 return 128 129 } 129 130 130 - issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt) 131 + issue, err := db.GetIssue(rp.db, f.RepoAt(), issueIdInt) 131 132 if err != nil { 132 133 log.Println("failed to get issue", err) 133 134 rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") ··· 171 172 return 172 173 } 173 174 174 - err = db.CloseIssue(rp.db, f.RepoAt, issueIdInt) 175 + err = db.CloseIssue(rp.db, f.RepoAt(), issueIdInt) 175 176 if err != nil { 176 177 log.Println("failed to close issue", err) 177 178 rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") ··· 203 204 return 204 205 } 205 206 206 - issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt) 207 + issue, err := db.GetIssue(rp.db, f.RepoAt(), issueIdInt) 207 208 if err != nil { 208 209 log.Println("failed to get issue", err) 209 210 rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") ··· 220 221 isIssueOwner := user.Did == issue.OwnerDid 221 222 222 223 if isCollaborator || isIssueOwner { 223 - err := db.ReopenIssue(rp.db, f.RepoAt, issueIdInt) 224 + err := db.ReopenIssue(rp.db, f.RepoAt(), issueIdInt) 224 225 if err != nil { 225 226 log.Println("failed to reopen issue", err) 226 227 rp.pages.Notice(w, "issue-action", "Failed to reopen issue. Try again later.") ··· 256 257 257 258 258 259 260 + return 261 + } 259 262 260 - 261 - 262 - 263 - 264 - 265 - err := db.NewIssueComment(rp.db, &db.Comment{ 263 + comment := &db.Comment{ 266 264 OwnerDid: user.Did, 267 - RepoAt: f.RepoAt, 265 + RepoAt: f.RepoAt(), 268 266 Issue: issueIdInt, 269 - CommentId: commentId, 267 + CommentId: mathrand.IntN(1000000), 270 268 Body: body, 269 + Rkey: tid.TID(), 270 + } 271 271 272 + err := db.NewIssueComment(rp.db, comment) 273 + if err != nil { 274 + log.Println("failed to create comment", err) 275 + rp.pages.Notice(w, "issue-comment", "Failed to create comment.") 272 276 273 - 274 - 275 - 276 - 277 - 277 + } 278 278 279 279 createdAt := time.Now().Format(time.RFC3339) 280 - commentIdInt64 := int64(commentId) 281 - ownerDid := user.Did 282 - issueAt, err := db.GetIssueAt(rp.db, f.RepoAt, issueIdInt) 280 + commentIdInt64 := int64(comment.CommentId) 281 + issue, err := db.GetIssue(rp.db, f.RepoAt(), issueIdInt) 283 282 if err != nil { 284 - log.Println("failed to get issue at", err) 283 + log.Println("failed to get issue", err) 285 284 rp.pages.Notice(w, "issue-comment", "Failed to create comment.") 286 285 return 287 286 } 288 287 289 - atUri := f.RepoAt.String() 288 + atUri := comment.RepoAt.String() 290 289 client, err := rp.oauth.AuthorizedClient(r) 291 290 if err != nil { 292 291 log.Println("failed to get authorized client", err) 293 292 294 293 295 294 295 + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 296 + Collection: tangled.RepoIssueCommentNSID, 297 + Repo: user.Did, 298 + Rkey: comment.Rkey, 299 + Record: &lexutil.LexiconTypeDecoder{ 300 + Val: &tangled.RepoIssueComment{ 301 + Repo: &atUri, 302 + Issue: issue.IssueAt, 303 + CommentId: &commentIdInt64, 304 + Owner: &comment.OwnerDid, 305 + Body: body, 306 + CreatedAt: createdAt, 307 + }, 296 308 297 309 298 310 299 311 300 312 313 + return 314 + } 301 315 316 + mentions := markup.FindUserMentions(comment.Body) 302 317 318 + rp.notifier.NewIssueComment(r.Context(), &f.Repo, issue, comment, mentions) 303 319 304 - 305 - 306 - 307 - 308 - 309 - 310 - 311 - 312 - 313 - 314 - 315 - 316 - 317 - 318 - 319 - 320 - 320 + rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", f.OwnerSlashRepo(), comment.Issue, comment.CommentId)) 321 + return 322 + } 323 + } 321 324 322 325 323 326 ··· 343 346 return 344 347 } 345 348 346 - issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt) 349 + issue, err := db.GetIssue(rp.db, f.RepoAt(), issueIdInt) 347 350 if err != nil { 348 351 log.Println("failed to get issue", err) 349 352 rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 350 353 return 351 354 } 352 355 353 - comment, err := db.GetComment(rp.db, f.RepoAt, issueIdInt, commentIdInt) 356 + comment, err := db.GetComment(rp.db, f.RepoAt(), issueIdInt, commentIdInt) 354 357 if err != nil { 355 358 http.Error(w, "bad comment id", http.StatusBadRequest) 356 359 return ··· 388 391 return 389 392 } 390 393 391 - issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt) 394 + issue, err := db.GetIssue(rp.db, f.RepoAt(), issueIdInt) 392 395 if err != nil { 393 396 log.Println("failed to get issue", err) 394 397 rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 395 398 return 396 399 } 397 400 398 - comment, err := db.GetComment(rp.db, f.RepoAt, issueIdInt, commentIdInt) 401 + comment, err := db.GetComment(rp.db, f.RepoAt(), issueIdInt, commentIdInt) 399 402 if err != nil { 400 403 http.Error(w, "bad comment id", http.StatusBadRequest) 401 404 return ··· 506 509 return 507 510 } 508 511 509 - issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt) 512 + issue, err := db.GetIssue(rp.db, f.RepoAt(), issueIdInt) 510 513 if err != nil { 511 514 log.Println("failed to get issue", err) 512 515 rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.") ··· 521 524 return 522 525 } 523 526 524 - comment, err := db.GetComment(rp.db, f.RepoAt, issueIdInt, commentIdInt) 527 + comment, err := db.GetComment(rp.db, f.RepoAt(), issueIdInt, commentIdInt) 525 528 if err != nil { 526 529 http.Error(w, "bad comment id", http.StatusBadRequest) 527 530 return ··· 539 542 540 543 // optimistic deletion 541 544 deleted := time.Now() 542 - err = db.DeleteComment(rp.db, f.RepoAt, issueIdInt, commentIdInt) 545 + err = db.DeleteComment(rp.db, f.RepoAt(), issueIdInt, commentIdInt) 543 546 if err != nil { 544 547 log.Println("failed to delete comment") 545 548 rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment") ··· 603 606 return 604 607 } 605 608 606 - issues, err := db.GetIssues(rp.db, f.RepoAt, isOpen, page) 609 + issues, err := db.GetIssues(rp.db, f.RepoAt(), isOpen, page) 607 610 if err != nil { 608 611 log.Println("failed to get issues", err) 609 612 rp.pages.Notice(w, "issues", "Failed to load issues. Try again later.") ··· 650 653 } 651 654 652 655 issue := &db.Issue{ 653 - RepoAt: f.RepoAt, 656 + RepoAt: f.RepoAt(), 654 657 Title: title, 655 658 Body: body, 656 659 OwnerDid: user.Did, ··· 668 671 rp.pages.Notice(w, "issues", "Failed to create issue.") 669 672 return 670 673 } 671 - atUri := f.RepoAt.String() 674 + atUri := f.RepoAt().String() 672 675 resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 673 676 Collection: tangled.RepoIssueNSID, 674 677 Repo: user.Did, ··· 689 692 return 690 693 } 691 694 692 - err = db.SetIssueAt(rp.db, f.RepoAt, issue.IssueId, resp.Uri) 695 + err = db.SetIssueAt(rp.db, f.RepoAt(), issue.IssueId, resp.Uri) 693 696 if err != nil { 694 697 log.Println("failed to set issue at", err) 695 698 rp.pages.Notice(w, "issues", "Failed to create issue.")
+3 -8
appview/middleware/middleware.go
··· 9 9 "slices" 10 10 "strconv" 11 11 "strings" 12 - "time" 13 12 14 13 "github.com/bluesky-social/indigo/atproto/identity" 15 14 "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 ··· 246 246 patch = mergeable.CombinedPatch() 247 247 } 248 248 249 - resp, err := ksClient.MergeCheck([]byte(patch), f.OwnerDid(), f.RepoName, pull.TargetBranch) 249 + resp, err := ksClient.MergeCheck([]byte(patch), f.OwnerDid(), f.Name, pull.TargetBranch) 250 250 if err != nil { 251 251 log.Println("failed to check for mergeability:", err) 252 252 return types.MergeCheckResponse{ ··· 307 307 // pulls within the same repo 308 308 knot = f.Knot 309 309 ownerDid = f.OwnerDid() 310 - repoName = f.RepoName 310 + repoName = f.Name 311 311 } 312 312 313 313 us, err := knotclient.NewUnsignedClient(knot, s.config.Core.Dev) ··· 483 483 484 484 pulls, err := db.GetPulls( 485 485 s.db, 486 - db.FilterEq("repo_at", f.RepoAt), 486 + db.FilterEq("repo_at", f.RepoAt()), 487 487 db.FilterEq("state", state), 488 488 ) 489 489 if err != nil { ··· 610 610 createdAt := time.Now().Format(time.RFC3339) 611 611 ownerDid := user.Did 612 612 613 - pullAt, err := db.GetPullAt(s.db, f.RepoAt, pull.PullId) 613 + pullAt, err := db.GetPullAt(s.db, f.RepoAt(), pull.PullId) 614 614 if err != nil { 615 615 log.Println("failed to get pull at", err) 616 616 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 617 617 return 618 618 } 619 619 620 - atUri := f.RepoAt.String() 620 + atUri := f.RepoAt().String() 621 621 client, err := s.oauth.AuthorizedClient(r) 622 622 if err != nil { 623 623 log.Println("failed to get authorized client", err) ··· 646 646 647 647 comment := &db.PullComment{ 648 648 OwnerDid: user.Did, 649 - RepoAt: f.RepoAt.String(), 649 + RepoAt: f.RepoAt().String(), 650 650 PullId: pull.PullId, 651 651 Body: body, 652 652 CommentAt: atResp.Uri, ··· 665 665 666 666 667 667 668 + return 669 + } 668 670 671 + mentions := markup.FindUserMentions(comment.Body) 669 672 673 + s.notifier.NewPullComment(r.Context(), &f.Repo, pull, comment, mentions) 670 674 671 - 672 - 673 - 674 - 675 + s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", f.OwnerSlashRepo(), pull.PullId, commentId)) 676 + return 675 677 676 678 677 679 ··· 692 694 return 693 695 } 694 696 695 - result, err := us.Branches(f.OwnerDid(), f.RepoName) 697 + result, err := us.Branches(f.OwnerDid(), f.Name) 696 698 if err != nil { 697 699 log.Println("failed to fetch branches", err) 698 700 return ··· 816 818 return 817 819 } 818 820 819 - comparison, err := ksClient.Compare(f.OwnerDid(), f.RepoName, targetBranch, sourceBranch) 821 + comparison, err := ksClient.Compare(f.OwnerDid(), f.Name, targetBranch, sourceBranch) 820 822 if err != nil { 821 823 log.Println("failed to compare", err) 822 824 s.pages.Notice(w, "pull", err.Error()) ··· 915 917 916 918 917 919 920 + return 921 + } 918 922 923 + forkAtUri := fork.RepoAt() 924 + forkAtUriStr := forkAtUri.String() 919 925 926 + pullSource := &db.PullSource{ 927 + Branch: sourceBranch, 920 928 921 - 922 - 923 - 924 - 925 - 926 - 927 - 928 - 929 - 930 - 931 - 932 - 933 - 934 - 935 - 936 - 929 + } 930 + recordPullSource := &tangled.RepoPull_Source{ 931 + Branch: sourceBranch, 932 + Repo: &forkAtUriStr, 933 + Sha: sourceRev, 934 + } 937 935 938 936 939 937 ··· 1007 1005 Body: body, 1008 1006 TargetBranch: targetBranch, 1009 1007 OwnerDid: user.Did, 1010 - RepoAt: f.RepoAt, 1008 + RepoAt: f.RepoAt(), 1011 1009 Rkey: rkey, 1012 1010 Submissions: []*db.PullSubmission{ 1013 1011 &initialSubmission, ··· 1020 1018 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1021 1019 return 1022 1020 } 1023 - pullId, err := db.NextPullId(tx, f.RepoAt) 1021 + pullId, err := db.NextPullId(tx, f.RepoAt()) 1024 1022 if err != nil { 1025 1023 log.Println("failed to get pull id", err) 1026 1024 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") ··· 1035 1033 Val: &tangled.RepoPull{ 1036 1034 Title: title, 1037 1035 PullId: int64(pullId), 1038 - TargetRepo: string(f.RepoAt), 1036 + TargetRepo: string(f.RepoAt()), 1039 1037 TargetBranch: targetBranch, 1040 1038 Patch: patch, 1041 1039 Source: recordPullSource, ··· 1213 1211 return 1214 1212 } 1215 1213 1216 - result, err := us.Branches(f.OwnerDid(), f.RepoName) 1214 + result, err := us.Branches(f.OwnerDid(), f.Name) 1217 1215 if err != nil { 1218 1216 log.Println("failed to reach knotserver", err) 1219 1217 return ··· 1297 1295 return 1298 1296 } 1299 1297 1300 - targetResult, err := targetBranchesClient.Branches(f.OwnerDid(), f.RepoName) 1298 + targetResult, err := targetBranchesClient.Branches(f.OwnerDid(), f.Name) 1301 1299 if err != nil { 1302 1300 log.Println("failed to reach knotserver for target branches", err) 1303 1301 return ··· 1413 1411 return 1414 1412 } 1415 1413 1416 - comparison, err := ksClient.Compare(f.OwnerDid(), f.RepoName, pull.TargetBranch, pull.PullSource.Branch) 1414 + comparison, err := ksClient.Compare(f.OwnerDid(), f.Name, pull.TargetBranch, pull.PullSource.Branch) 1417 1415 if err != nil { 1418 1416 log.Printf("compare request failed: %s", err) 1419 1417 s.pages.Notice(w, "resubmit-error", err.Error()) ··· 1597 1595 Val: &tangled.RepoPull{ 1598 1596 Title: pull.Title, 1599 1597 PullId: int64(pull.PullId), 1600 - TargetRepo: string(f.RepoAt), 1598 + TargetRepo: string(f.RepoAt()), 1601 1599 TargetBranch: pull.TargetBranch, 1602 1600 Patch: patch, // new patch 1603 1601 Source: recordPullSource, ··· 1934 1932 } 1935 1933 1936 1934 // Merge the pull request 1937 - resp, err := ksClient.Merge([]byte(patch), f.OwnerDid(), f.RepoName, pull.TargetBranch, pull.Title, pull.Body, ident.Handle.String(), email.Address) 1935 + resp, err := ksClient.Merge([]byte(patch), f.OwnerDid(), f.Name, pull.TargetBranch, pull.Title, pull.Body, ident.Handle.String(), email.Address) 1938 1936 if err != nil { 1939 1937 log.Printf("failed to merge pull request: %s", err) 1940 1938 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") ··· 1956 1954 defer tx.Rollback() 1957 1955 1958 1956 for _, p := range pullsToMerge { 1959 - err := db.MergePull(tx, f.RepoAt, p.PullId) 1957 + err := db.MergePull(tx, f.RepoAt(), p.PullId) 1960 1958 if err != nil { 1961 1959 log.Printf("failed to update pull request status in database: %s", err) 1962 1960 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") ··· 1972 1970 return 1973 1971 } 1974 1972 1975 - s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, pull.PullId)) 1973 + s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.Name, pull.PullId)) 1976 1974 } 1977 1975 1978 1976 func (s *Pulls) ClosePull(w http.ResponseWriter, r *http.Request) { ··· 2024 2022 2025 2023 for _, p := range pullsToClose { 2026 2024 // Close the pull in the database 2027 - err = db.ClosePull(tx, f.RepoAt, p.PullId) 2025 + err = db.ClosePull(tx, f.RepoAt(), p.PullId) 2028 2026 if err != nil { 2029 2027 log.Println("failed to close pull", err) 2030 2028 s.pages.Notice(w, "pull-close", "Failed to close pull.") ··· 2092 2090 2093 2091 for _, p := range pullsToReopen { 2094 2092 // Close the pull in the database 2095 - err = db.ReopenPull(tx, f.RepoAt, p.PullId) 2093 + err = db.ReopenPull(tx, f.RepoAt(), p.PullId) 2096 2094 if err != nil { 2097 2095 log.Println("failed to close pull", err) 2098 2096 s.pages.Notice(w, "pull-close", "Failed to close pull.") ··· 2144 2142 Body: body, 2145 2143 TargetBranch: targetBranch, 2146 2144 OwnerDid: user.Did, 2147 - RepoAt: f.RepoAt, 2145 + RepoAt: f.RepoAt(), 2148 2146 Rkey: rkey, 2149 2147 Submissions: []*db.PullSubmission{ 2150 2148 &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
+50 -52
appview/repo/repo.go
··· 95 95 } else { 96 96 uri = "https" 97 97 } 98 - url := fmt.Sprintf("%s://%s/%s/%s/archive/%s.tar.gz", uri, f.Knot, f.OwnerDid(), f.RepoName, url.PathEscape(refParam)) 98 + url := fmt.Sprintf("%s://%s/%s/%s/archive/%s.tar.gz", uri, f.Knot, f.OwnerDid(), f.Name, url.PathEscape(refParam)) 99 99 100 100 http.Redirect(w, r, url, http.StatusFound) 101 101 } ··· 123 123 return 124 124 } 125 125 126 - repolog, err := us.Log(f.OwnerDid(), f.RepoName, ref, page) 126 + repolog, err := us.Log(f.OwnerDid(), f.Name, ref, page) 127 127 if err != nil { 128 128 log.Println("failed to reach knotserver", err) 129 129 return 130 130 } 131 131 132 - tagResult, err := us.Tags(f.OwnerDid(), f.RepoName) 132 + tagResult, err := us.Tags(f.OwnerDid(), f.Name) 133 133 if err != nil { 134 134 log.Println("failed to reach knotserver", err) 135 135 return ··· 144 144 tagMap[hash] = append(tagMap[hash], tag.Name) 145 145 } 146 146 147 - branchResult, err := us.Branches(f.OwnerDid(), f.RepoName) 147 + branchResult, err := us.Branches(f.OwnerDid(), f.Name) 148 148 if err != nil { 149 149 log.Println("failed to reach knotserver", err) 150 150 return ··· 212 212 return 213 213 } 214 214 215 - repoAt := f.RepoAt 215 + repoAt := f.RepoAt() 216 216 rkey := repoAt.RecordKey().String() 217 217 if rkey == "" { 218 218 log.Println("invalid aturi for repo", err) ··· 262 262 Record: &lexutil.LexiconTypeDecoder{ 263 263 Val: &tangled.Repo{ 264 264 Knot: f.Knot, 265 - Name: f.RepoName, 265 + Name: f.Name, 266 266 Owner: user.Did, 267 - CreatedAt: f.CreatedAt, 267 + CreatedAt: f.Created.Format(time.RFC3339), 268 268 Description: &newDescription, 269 269 Spindle: &f.Spindle, 270 270 }, ··· 310 310 return 311 311 } 312 312 313 - resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/commit/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref)) 313 + resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/commit/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref)) 314 314 if err != nil { 315 315 log.Println("failed to reach knotserver", err) 316 316 return ··· 375 375 if !rp.config.Core.Dev { 376 376 protocol = "https" 377 377 } 378 - resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tree/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, treePath)) 378 + resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tree/%s/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref, treePath)) 379 379 if err != nil { 380 380 log.Println("failed to reach knotserver", err) 381 381 return ··· 405 405 user := rp.oauth.GetUser(r) 406 406 407 407 var breadcrumbs [][]string 408 - breadcrumbs = append(breadcrumbs, []string{f.RepoName, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)}) 408 + breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)}) 409 409 if treePath != "" { 410 410 for idx, elem := range strings.Split(treePath, "/") { 411 411 breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)}) ··· 436 436 return 437 437 } 438 438 439 - result, err := us.Tags(f.OwnerDid(), f.RepoName) 439 + result, err := us.Tags(f.OwnerDid(), f.Name) 440 440 if err != nil { 441 441 log.Println("failed to reach knotserver", err) 442 442 return 443 443 } 444 444 445 - artifacts, err := db.GetArtifact(rp.db, db.FilterEq("repo_at", f.RepoAt)) 445 + artifacts, err := db.GetArtifact(rp.db, db.FilterEq("repo_at", f.RepoAt())) 446 446 if err != nil { 447 447 log.Println("failed grab artifacts", err) 448 448 return ··· 493 493 return 494 494 } 495 495 496 - result, err := us.Branches(f.OwnerDid(), f.RepoName) 496 + result, err := us.Branches(f.OwnerDid(), f.Name) 497 497 if err != nil { 498 498 log.Println("failed to reach knotserver", err) 499 499 return ··· 522 522 if !rp.config.Core.Dev { 523 523 protocol = "https" 524 524 } 525 - resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath)) 525 + resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref, filePath)) 526 526 if err != nil { 527 527 log.Println("failed to reach knotserver", err) 528 528 return ··· 542 542 } 543 543 544 544 var breadcrumbs [][]string 545 - breadcrumbs = append(breadcrumbs, []string{f.RepoName, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)}) 545 + breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)}) 546 546 if filePath != "" { 547 547 for idx, elem := range strings.Split(filePath, "/") { 548 548 breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)}) ··· 575 575 576 576 // fetch the actual binary content like in RepoBlobRaw 577 577 578 - blobURL := fmt.Sprintf("%s://%s/%s/%s/raw/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath) 578 + blobURL := fmt.Sprintf("%s://%s/%s/%s/raw/%s/%s", protocol, f.Knot, f.OwnerDid(), f.Name, ref, filePath) 579 579 contentSrc = blobURL 580 580 if !rp.config.Core.Dev { 581 581 contentSrc = markup.GenerateCamoURL(rp.config.Camo.Host, rp.config.Camo.SharedSecret, blobURL) ··· 612 612 if !rp.config.Core.Dev { 613 613 protocol = "https" 614 614 } 615 - blobURL := fmt.Sprintf("%s://%s/%s/%s/raw/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath) 615 + blobURL := fmt.Sprintf("%s://%s/%s/%s/raw/%s/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref, filePath) 616 616 resp, err := http.Get(blobURL) 617 617 if err != nil { 618 618 log.Println("failed to reach knotserver:", err) ··· 668 668 return 669 669 } 670 670 671 - repoAt := f.RepoAt 671 + repoAt := f.RepoAt() 672 672 rkey := repoAt.RecordKey().String() 673 673 if rkey == "" { 674 674 fail("Failed to resolve repo. Try again later", err) ··· 722 722 Record: &lexutil.LexiconTypeDecoder{ 723 723 Val: &tangled.Repo{ 724 724 Knot: f.Knot, 725 - Name: f.RepoName, 725 + Name: f.Name, 726 726 Owner: user.Did, 727 - CreatedAt: f.CreatedAt, 727 + CreatedAt: f.Created.Format(time.RFC3339), 728 728 Description: &f.Description, 729 729 Spindle: spindlePtr, 730 730 }, ··· 805 805 Record: &lexutil.LexiconTypeDecoder{ 806 806 Val: &tangled.RepoCollaborator{ 807 807 Subject: collaboratorIdent.DID.String(), 808 - Repo: string(f.RepoAt), 808 + Repo: string(f.RepoAt()), 809 809 CreatedAt: createdAt.Format(time.RFC3339), 810 810 }}, 811 811 }) ··· 830 830 return 831 831 } 832 832 833 - ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.RepoName, collaboratorIdent.DID.String()) 833 + ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.Name, collaboratorIdent.DID.String()) 834 834 if err != nil { 835 835 fail("Knot was unreachable.", err) 836 836 return ··· 864 864 Did: syntax.DID(currentUser.Did), 865 865 Rkey: rkey, 866 866 SubjectDid: collaboratorIdent.DID, 867 - RepoAt: f.RepoAt, 867 + RepoAt: f.RepoAt(), 868 868 Created: createdAt, 869 869 }) 870 870 if err != nil { ··· 902 902 log.Println("failed to get authorized client", err) 903 903 return 904 904 } 905 - repoRkey := f.RepoAt.RecordKey().String() 906 905 _, err = xrpcClient.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 907 906 Collection: tangled.RepoNSID, 908 907 Repo: user.Did, 909 - Rkey: repoRkey, 908 + Rkey: f.Rkey, 910 909 }) 911 910 if err != nil { 912 911 log.Printf("failed to delete record: %s", err) 913 912 rp.pages.Notice(w, "settings-delete", "Failed to delete repository from PDS.") 914 913 return 915 914 } 916 - log.Println("removed repo record ", f.RepoAt.String()) 915 + log.Println("removed repo record ", f.RepoAt().String()) 917 916 918 917 secret, err := db.GetRegistrationKey(rp.db, f.Knot) 919 918 if err != nil { ··· 927 926 return 928 927 } 929 928 930 - ksResp, err := ksClient.RemoveRepo(f.OwnerDid(), f.RepoName) 929 + ksResp, err := ksClient.RemoveRepo(f.OwnerDid(), f.Name) 931 930 if err != nil { 932 931 log.Printf("failed to make request to %s: %s", f.Knot, err) 933 932 return ··· 973 972 } 974 973 975 974 // remove repo from db 976 - err = db.RemoveRepo(tx, f.OwnerDid(), f.RepoName) 975 + err = db.RemoveRepo(tx, f.OwnerDid(), f.Name) 977 976 if err != nil { 978 977 rp.pages.Notice(w, "settings-delete", "Failed to update appview") 979 978 return ··· 1022 1021 return 1023 1022 } 1024 1023 1025 - ksResp, err := ksClient.SetDefaultBranch(f.OwnerDid(), f.RepoName, branch) 1024 + ksResp, err := ksClient.SetDefaultBranch(f.OwnerDid(), f.Name, branch) 1026 1025 if err != nil { 1027 1026 log.Printf("failed to make request to %s: %s", f.Knot, err) 1028 1027 return ··· 1090 1089 r.Context(), 1091 1090 spindleClient, 1092 1091 &tangled.RepoAddSecret_Input{ 1093 - Repo: f.RepoAt.String(), 1092 + Repo: f.RepoAt().String(), 1094 1093 Key: key, 1095 1094 Value: value, 1096 1095 }, ··· 1108 1107 r.Context(), 1109 1108 spindleClient, 1110 1109 &tangled.RepoRemoveSecret_Input{ 1111 - Repo: f.RepoAt.String(), 1110 + Repo: f.RepoAt().String(), 1112 1111 Key: key, 1113 1112 }, 1114 1113 ) ··· 1170 1169 // return 1171 1170 // } 1172 1171 1173 - // result, err := us.Branches(f.OwnerDid(), f.RepoName) 1172 + // result, err := us.Branches(f.OwnerDid(), f.Name) 1174 1173 // if err != nil { 1175 1174 // log.Println("failed to reach knotserver", err) 1176 1175 // return ··· 1192 1191 // oauth.WithDev(rp.config.Core.Dev), 1193 1192 // ); err != nil { 1194 1193 // log.Println("failed to create spindle client", err) 1195 - // } else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt.String()); err != nil { 1194 + // } else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt().String()); err != nil { 1196 1195 // log.Println("failed to fetch secrets", err) 1197 1196 // } else { 1198 1197 // secrets = resp.Secrets ··· 1221 1220 return 1222 1221 } 1223 1222 1224 - result, err := us.Branches(f.OwnerDid(), f.RepoName) 1223 + result, err := us.Branches(f.OwnerDid(), f.Name) 1225 1224 if err != nil { 1226 1225 log.Println("failed to reach knotserver", err) 1227 1226 return ··· 1275 1274 oauth.WithDev(rp.config.Core.Dev), 1276 1275 ); err != nil { 1277 1276 log.Println("failed to create spindle client", err) 1278 - } else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt.String()); err != nil { 1277 + } else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt().String()); err != nil { 1279 1278 log.Println("failed to fetch secrets", err) 1280 1279 } else { 1281 1280 secrets = resp.Secrets ··· 1343 1342 } else { 1344 1343 uri = "https" 1345 1344 } 1346 - forkName := fmt.Sprintf("%s", f.RepoName) 1347 - forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.RepoName) 1345 + forkName := fmt.Sprintf("%s", f.Name) 1346 + forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.Repo.Name) 1348 1347 1349 1348 _, err = client.SyncRepoFork(user.Did, forkSourceUrl, forkName, f.Ref) 1350 1349 if err != nil { ··· 1394 1393 return 1395 1394 } 1396 1395 1397 - forkName := fmt.Sprintf("%s", f.RepoName) 1396 + forkName := fmt.Sprintf("%s", f.Name) 1398 1397 1399 1398 // this check is *only* to see if the forked repo name already exists 1400 1399 // in the user's account. 1401 - existingRepo, err := db.GetRepo(rp.db, user.Did, f.RepoName) 1400 + existingRepo, err := db.GetRepo(rp.db, user.Did, f.Name) 1402 1401 if err != nil { 1403 1402 if errors.Is(err, sql.ErrNoRows) { 1404 1403 // no existing repo with this name found, we can use the name as is ··· 1429 1428 } else { 1430 1429 uri = "https" 1431 1430 } 1432 - forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.RepoName) 1433 - sourceAt := f.RepoAt.String() 1431 + forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.Repo.Name) 1432 + sourceAt := f.RepoAt().String() 1434 1433 1435 1434 rkey := tid.TID() 1436 1435 repo := &db.Repo{ ··· 1496 1495 1497 1496 1498 1497 1498 + } 1499 + log.Println("created repo record: ", atresp.Uri) 1499 1500 1500 - 1501 - 1502 - 1503 - 1504 - 1505 - 1501 + err = db.AddRepo(tx, repo) 1502 + if err != nil { 1503 + log.Println(err) 1506 1504 1507 1505 1508 1506 ··· 1550 1548 return 1551 1549 } 1552 1550 1553 - result, err := us.Branches(f.OwnerDid(), f.RepoName) 1551 + result, err := us.Branches(f.OwnerDid(), f.Name) 1554 1552 if err != nil { 1555 1553 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1556 1554 log.Println("failed to reach knotserver", err) ··· 1580 1578 head = queryHead 1581 1579 } 1582 1580 1583 - tags, err := us.Tags(f.OwnerDid(), f.RepoName) 1581 + tags, err := us.Tags(f.OwnerDid(), f.Name) 1584 1582 if err != nil { 1585 1583 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1586 1584 log.Println("failed to reach knotserver", err) ··· 1642 1640 return 1643 1641 } 1644 1642 1645 - branches, err := us.Branches(f.OwnerDid(), f.RepoName) 1643 + branches, err := us.Branches(f.OwnerDid(), f.Name) 1646 1644 if err != nil { 1647 1645 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1648 1646 log.Println("failed to reach knotserver", err) 1649 1647 return 1650 1648 } 1651 1649 1652 - tags, err := us.Tags(f.OwnerDid(), f.RepoName) 1650 + tags, err := us.Tags(f.OwnerDid(), f.Name) 1653 1651 if err != nil { 1654 1652 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1655 1653 log.Println("failed to reach knotserver", err) 1656 1654 return 1657 1655 } 1658 1656 1659 - formatPatch, err := us.Compare(f.OwnerDid(), f.RepoName, base, head) 1657 + formatPatch, err := us.Compare(f.OwnerDid(), f.Name, base, head) 1660 1658 if err != nil { 1661 1659 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1662 1660 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 ··· 394 396 // continue 395 397 } 396 398 397 - repo.AtUri = atresp.Uri 398 399 err = db.AddRepo(tx, repo) 399 400 if err != nil { 400 401 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 }
+7 -1
appview/pages/markup/markdown.go
··· 45 45 Sanitizer Sanitizer 46 46 } 47 47 48 - func (rctx *RenderContext) RenderMarkdown(source string) string { 48 + func NewMarkdown() goldmark.Markdown { 49 49 md := goldmark.New( 50 50 goldmark.WithExtensions( 51 51 extension.GFM, ··· 59 59 extension.NewFootnote( 60 60 extension.WithFootnoteIDPrefix([]byte("footnote")), 61 61 ), 62 + AtExt, 62 63 ), 63 64 goldmark.WithParserOptions( 64 65 parser.WithAutoHeadingID(), 65 66 ), 66 67 goldmark.WithRendererOptions(html.WithUnsafe()), 67 68 ) 69 + return md 70 + } 71 + 72 + func (rctx *RenderContext) RenderMarkdown(source string) string { 73 + md := NewMarkdown() 68 74 69 75 if rctx != nil { 70 76 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.