From 90679d26070907e7ae62cade4c337219b69376a0 Mon Sep 17 00:00:00 2001 From: Seongmin Lee Date: Wed, 30 Jul 2025 01:03:48 +0900 Subject: [PATCH] appview: reporesolver: pass entire `Repo` object through context Change-Id: ovmluwsrpullzyxvrzxmsxtzslnztptr There is no reason for `ResolvedRepo` to not directly inherit `Repo` Signed-off-by: Seongmin Lee --- appview/issues/issues.go | 38 +++++++-------- appview/middleware/middleware.go | 11 ++--- appview/pulls/pulls.go | 42 ++++++++-------- appview/repo/artifact.go | 12 ++--- appview/repo/index.go | 10 ++-- appview/repo/repo.go | 81 +++++++++++++++---------------- appview/reporesolver/resolver.go | 83 ++++++++++---------------------- 7 files changed, 119 insertions(+), 158 deletions(-) diff --git a/appview/issues/issues.go b/appview/issues/issues.go index 3e2d058..35c37f1 100644 --- a/appview/issues/issues.go +++ b/appview/issues/issues.go @@ -73,7 +73,7 @@ func (rp *Issues) RepoSingleIssue(w http.ResponseWriter, r *http.Request) { return } - issue, comments, err := db.GetIssueWithComments(rp.db, f.RepoAt, issueIdInt) + issue, comments, err := db.GetIssueWithComments(rp.db, f.RepoAt(), issueIdInt) if err != nil { log.Println("failed to get issue and comments", err) rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.") @@ -142,7 +142,7 @@ func (rp *Issues) CloseIssue(w http.ResponseWriter, r *http.Request) { return } - issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt) + issue, err := db.GetIssue(rp.db, f.RepoAt(), issueIdInt) if err != nil { log.Println("failed to get issue", err) rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") @@ -186,7 +186,7 @@ func (rp *Issues) CloseIssue(w http.ResponseWriter, r *http.Request) { return } - err = db.CloseIssue(rp.db, f.RepoAt, issueIdInt) + err = db.CloseIssue(rp.db, f.RepoAt(), issueIdInt) if err != nil { log.Println("failed to close issue", err) rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") @@ -218,7 +218,7 @@ func (rp *Issues) ReopenIssue(w http.ResponseWriter, r *http.Request) { return } - issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt) + issue, err := db.GetIssue(rp.db, f.RepoAt(), issueIdInt) if err != nil { log.Println("failed to get issue", err) rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") @@ -235,7 +235,7 @@ func (rp *Issues) ReopenIssue(w http.ResponseWriter, r *http.Request) { isIssueOwner := user.Did == issue.OwnerDid if isCollaborator || isIssueOwner { - err := db.ReopenIssue(rp.db, f.RepoAt, issueIdInt) + err := db.ReopenIssue(rp.db, f.RepoAt(), issueIdInt) if err != nil { log.Println("failed to reopen issue", err) rp.pages.Notice(w, "issue-action", "Failed to reopen issue. Try again later.") @@ -279,7 +279,7 @@ func (rp *Issues) NewIssueComment(w http.ResponseWriter, r *http.Request) { err := db.NewIssueComment(rp.db, &db.Comment{ OwnerDid: user.Did, - RepoAt: f.RepoAt, + RepoAt: f.RepoAt(), Issue: issueIdInt, CommentId: commentId, Body: body, @@ -294,14 +294,14 @@ func (rp *Issues) NewIssueComment(w http.ResponseWriter, r *http.Request) { createdAt := time.Now().Format(time.RFC3339) commentIdInt64 := int64(commentId) ownerDid := user.Did - issueAt, err := db.GetIssueAt(rp.db, f.RepoAt, issueIdInt) + issueAt, err := db.GetIssueAt(rp.db, f.RepoAt(), issueIdInt) if err != nil { log.Println("failed to get issue at", err) rp.pages.Notice(w, "issue-comment", "Failed to create comment.") return } - atUri := f.RepoAt.String() + atUri := f.RepoAt().String() client, err := rp.oauth.AuthorizedClient(r) if err != nil { log.Println("failed to get authorized client", err) @@ -358,14 +358,14 @@ func (rp *Issues) IssueComment(w http.ResponseWriter, r *http.Request) { return } - issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt) + issue, err := db.GetIssue(rp.db, f.RepoAt(), issueIdInt) if err != nil { log.Println("failed to get issue", err) rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.") return } - comment, err := db.GetComment(rp.db, f.RepoAt, issueIdInt, commentIdInt) + comment, err := db.GetComment(rp.db, f.RepoAt(), issueIdInt, commentIdInt) if err != nil { http.Error(w, "bad comment id", http.StatusBadRequest) return @@ -417,14 +417,14 @@ func (rp *Issues) EditIssueComment(w http.ResponseWriter, r *http.Request) { return } - issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt) + issue, err := db.GetIssue(rp.db, f.RepoAt(), issueIdInt) if err != nil { log.Println("failed to get issue", err) rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.") return } - comment, err := db.GetComment(rp.db, f.RepoAt, issueIdInt, commentIdInt) + comment, err := db.GetComment(rp.db, f.RepoAt(), issueIdInt, commentIdInt) if err != nil { http.Error(w, "bad comment id", http.StatusBadRequest) return @@ -539,7 +539,7 @@ func (rp *Issues) DeleteIssueComment(w http.ResponseWriter, r *http.Request) { return } - issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt) + issue, err := db.GetIssue(rp.db, f.RepoAt(), issueIdInt) if err != nil { log.Println("failed to get issue", err) rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.") @@ -554,7 +554,7 @@ func (rp *Issues) DeleteIssueComment(w http.ResponseWriter, r *http.Request) { return } - comment, err := db.GetComment(rp.db, f.RepoAt, issueIdInt, commentIdInt) + comment, err := db.GetComment(rp.db, f.RepoAt(), issueIdInt, commentIdInt) if err != nil { http.Error(w, "bad comment id", http.StatusBadRequest) return @@ -572,7 +572,7 @@ func (rp *Issues) DeleteIssueComment(w http.ResponseWriter, r *http.Request) { // optimistic deletion deleted := time.Now() - err = db.DeleteComment(rp.db, f.RepoAt, issueIdInt, commentIdInt) + err = db.DeleteComment(rp.db, f.RepoAt(), issueIdInt, commentIdInt) if err != nil { log.Println("failed to delete comment") rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment") @@ -641,7 +641,7 @@ func (rp *Issues) RepoIssues(w http.ResponseWriter, r *http.Request) { return } - issues, err := db.GetIssues(rp.db, f.RepoAt, isOpen, page) + issues, err := db.GetIssues(rp.db, f.RepoAt(), isOpen, page) if err != nil { log.Println("failed to get issues", err) rp.pages.Notice(w, "issues", "Failed to load issues. Try again later.") @@ -704,7 +704,7 @@ func (rp *Issues) NewIssue(w http.ResponseWriter, r *http.Request) { } issue := &db.Issue{ - RepoAt: f.RepoAt, + RepoAt: f.RepoAt(), Title: title, Body: body, OwnerDid: user.Did, @@ -722,7 +722,7 @@ func (rp *Issues) NewIssue(w http.ResponseWriter, r *http.Request) { rp.pages.Notice(w, "issues", "Failed to create issue.") return } - atUri := f.RepoAt.String() + atUri := f.RepoAt().String() resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ Collection: tangled.RepoIssueNSID, Repo: user.Did, @@ -743,7 +743,7 @@ func (rp *Issues) NewIssue(w http.ResponseWriter, r *http.Request) { return } - err = db.SetIssueAt(rp.db, f.RepoAt, issue.IssueId, resp.Uri) + err = db.SetIssueAt(rp.db, f.RepoAt(), issue.IssueId, resp.Uri) if err != nil { log.Println("failed to set issue at", err) rp.pages.Notice(w, "issues", "Failed to create issue.") diff --git a/appview/middleware/middleware.go b/appview/middleware/middleware.go index e427eb2..1a615ca 100644 --- a/appview/middleware/middleware.go +++ b/appview/middleware/middleware.go @@ -8,7 +8,6 @@ import ( "slices" "strconv" "strings" - "time" "github.com/bluesky-social/indigo/atproto/identity" "github.com/go-chi/chi/v5" @@ -222,11 +221,7 @@ func (mw Middleware) ResolveRepo() middlewareFunc { return } - ctx := context.WithValue(req.Context(), "knot", repo.Knot) - ctx = context.WithValue(ctx, "repoAt", repo.AtUri) - ctx = context.WithValue(ctx, "repoDescription", repo.Description) - ctx = context.WithValue(ctx, "repoSpindle", repo.Spindle) - ctx = context.WithValue(ctx, "repoAddedAt", repo.Created.Format(time.RFC3339)) + ctx := context.WithValue(req.Context(), "repo", repo) next.ServeHTTP(w, req.WithContext(ctx)) }) } @@ -251,7 +246,7 @@ func (mw Middleware) ResolvePull() middlewareFunc { return } - pr, err := db.GetPull(mw.db, f.RepoAt, prIdInt) + pr, err := db.GetPull(mw.db, f.RepoAt(), prIdInt) if err != nil { log.Println("failed to get pull and comments", err) return @@ -292,7 +287,7 @@ func (mw Middleware) GoImport() middlewareFunc { return } - fullName := f.OwnerHandle() + "/" + f.RepoName + fullName := f.OwnerHandle() + "/" + f.Name if r.Header.Get("User-Agent") == "Go-http-client/1.1" { if r.URL.Query().Get("go-get") == "1" { diff --git a/appview/pulls/pulls.go b/appview/pulls/pulls.go index 824f702..39ca6fb 100644 --- a/appview/pulls/pulls.go +++ b/appview/pulls/pulls.go @@ -257,7 +257,7 @@ func (s *Pulls) mergeCheck(f *reporesolver.ResolvedRepo, pull *db.Pull, stack db patch = mergeable.CombinedPatch() } - resp, err := ksClient.MergeCheck([]byte(patch), f.OwnerDid(), f.RepoName, pull.TargetBranch) + resp, err := ksClient.MergeCheck([]byte(patch), f.OwnerDid(), f.Name, pull.TargetBranch) if err != nil { log.Println("failed to check for mergeability:", err) return types.MergeCheckResponse{ @@ -318,7 +318,7 @@ func (s *Pulls) resubmitCheck(f *reporesolver.ResolvedRepo, pull *db.Pull, stack // pulls within the same repo knot = f.Knot ownerDid = f.OwnerDid() - repoName = f.RepoName + repoName = f.Name } us, err := knotclient.NewUnsignedClient(knot, s.config.Core.Dev) @@ -529,7 +529,7 @@ func (s *Pulls) RepoPulls(w http.ResponseWriter, r *http.Request) { pulls, err := db.GetPulls( s.db, - db.FilterEq("repo_at", f.RepoAt), + db.FilterEq("repo_at", f.RepoAt()), db.FilterEq("state", state), ) if err != nil { @@ -650,14 +650,14 @@ func (s *Pulls) PullComment(w http.ResponseWriter, r *http.Request) { createdAt := time.Now().Format(time.RFC3339) ownerDid := user.Did - pullAt, err := db.GetPullAt(s.db, f.RepoAt, pull.PullId) + pullAt, err := db.GetPullAt(s.db, f.RepoAt(), pull.PullId) if err != nil { log.Println("failed to get pull at", err) s.pages.Notice(w, "pull-comment", "Failed to create comment.") return } - atUri := f.RepoAt.String() + atUri := f.RepoAt().String() client, err := s.oauth.AuthorizedClient(r) if err != nil { log.Println("failed to get authorized client", err) @@ -686,7 +686,7 @@ func (s *Pulls) PullComment(w http.ResponseWriter, r *http.Request) { comment := &db.PullComment{ OwnerDid: user.Did, - RepoAt: f.RepoAt.String(), + RepoAt: f.RepoAt().String(), PullId: pull.PullId, Body: body, CommentAt: atResp.Uri, @@ -732,7 +732,7 @@ func (s *Pulls) NewPull(w http.ResponseWriter, r *http.Request) { return } - result, err := us.Branches(f.OwnerDid(), f.RepoName) + result, err := us.Branches(f.OwnerDid(), f.Name) if err != nil { log.Println("failed to fetch branches", err) return @@ -856,7 +856,7 @@ func (s *Pulls) handleBranchBasedPull( return } - comparison, err := ksClient.Compare(f.OwnerDid(), f.RepoName, targetBranch, sourceBranch) + comparison, err := ksClient.Compare(f.OwnerDid(), f.Name, targetBranch, sourceBranch) if err != nil { log.Println("failed to compare", err) s.pages.Notice(w, "pull", err.Error()) @@ -1047,7 +1047,7 @@ func (s *Pulls) createPullRequest( Body: body, TargetBranch: targetBranch, OwnerDid: user.Did, - RepoAt: f.RepoAt, + RepoAt: f.RepoAt(), Rkey: rkey, Submissions: []*db.PullSubmission{ &initialSubmission, @@ -1060,7 +1060,7 @@ func (s *Pulls) createPullRequest( s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") return } - pullId, err := db.NextPullId(tx, f.RepoAt) + pullId, err := db.NextPullId(tx, f.RepoAt()) if err != nil { log.Println("failed to get pull id", err) s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") @@ -1075,7 +1075,7 @@ func (s *Pulls) createPullRequest( Val: &tangled.RepoPull{ Title: title, PullId: int64(pullId), - TargetRepo: string(f.RepoAt), + TargetRepo: string(f.RepoAt()), TargetBranch: targetBranch, Patch: patch, Source: recordPullSource, @@ -1253,7 +1253,7 @@ func (s *Pulls) CompareBranchesFragment(w http.ResponseWriter, r *http.Request) return } - result, err := us.Branches(f.OwnerDid(), f.RepoName) + result, err := us.Branches(f.OwnerDid(), f.Name) if err != nil { log.Println("failed to reach knotserver", err) return @@ -1337,7 +1337,7 @@ func (s *Pulls) CompareForksBranchesFragment(w http.ResponseWriter, r *http.Requ return } - targetResult, err := targetBranchesClient.Branches(f.OwnerDid(), f.RepoName) + targetResult, err := targetBranchesClient.Branches(f.OwnerDid(), f.Name) if err != nil { log.Println("failed to reach knotserver for target branches", err) return @@ -1453,7 +1453,7 @@ func (s *Pulls) resubmitBranch(w http.ResponseWriter, r *http.Request) { return } - comparison, err := ksClient.Compare(f.OwnerDid(), f.RepoName, pull.TargetBranch, pull.PullSource.Branch) + comparison, err := ksClient.Compare(f.OwnerDid(), f.Name, pull.TargetBranch, pull.PullSource.Branch) if err != nil { log.Printf("compare request failed: %s", err) s.pages.Notice(w, "resubmit-error", err.Error()) @@ -1637,7 +1637,7 @@ func (s *Pulls) resubmitPullHelper( Val: &tangled.RepoPull{ Title: pull.Title, PullId: int64(pull.PullId), - TargetRepo: string(f.RepoAt), + TargetRepo: string(f.RepoAt()), TargetBranch: pull.TargetBranch, Patch: patch, // new patch Source: recordPullSource, @@ -1964,7 +1964,7 @@ func (s *Pulls) MergePull(w http.ResponseWriter, r *http.Request) { } // Merge the pull request - resp, err := ksClient.Merge([]byte(patch), f.OwnerDid(), f.RepoName, pull.TargetBranch, pull.Title, pull.Body, ident.Handle.String(), email.Address) + resp, err := ksClient.Merge([]byte(patch), f.OwnerDid(), f.Name, pull.TargetBranch, pull.Title, pull.Body, ident.Handle.String(), email.Address) if err != nil { log.Printf("failed to merge pull request: %s", err) s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") @@ -1986,7 +1986,7 @@ func (s *Pulls) MergePull(w http.ResponseWriter, r *http.Request) { defer tx.Rollback() for _, p := range pullsToMerge { - err := db.MergePull(tx, f.RepoAt, p.PullId) + err := db.MergePull(tx, f.RepoAt(), p.PullId) if err != nil { log.Printf("failed to update pull request status in database: %s", err) s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") @@ -2002,7 +2002,7 @@ func (s *Pulls) MergePull(w http.ResponseWriter, r *http.Request) { return } - s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, pull.PullId)) + s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.Name, pull.PullId)) } func (s *Pulls) ClosePull(w http.ResponseWriter, r *http.Request) { @@ -2054,7 +2054,7 @@ func (s *Pulls) ClosePull(w http.ResponseWriter, r *http.Request) { for _, p := range pullsToClose { // Close the pull in the database - err = db.ClosePull(tx, f.RepoAt, p.PullId) + err = db.ClosePull(tx, f.RepoAt(), p.PullId) if err != nil { log.Println("failed to close pull", err) s.pages.Notice(w, "pull-close", "Failed to close pull.") @@ -2122,7 +2122,7 @@ func (s *Pulls) ReopenPull(w http.ResponseWriter, r *http.Request) { for _, p := range pullsToReopen { // Close the pull in the database - err = db.ReopenPull(tx, f.RepoAt, p.PullId) + err = db.ReopenPull(tx, f.RepoAt(), p.PullId) if err != nil { log.Println("failed to close pull", err) s.pages.Notice(w, "pull-close", "Failed to close pull.") @@ -2174,7 +2174,7 @@ func newStack(f *reporesolver.ResolvedRepo, user *oauth.User, targetBranch, patc Body: body, TargetBranch: targetBranch, OwnerDid: user.Did, - RepoAt: f.RepoAt, + RepoAt: f.RepoAt(), Rkey: rkey, Submissions: []*db.PullSubmission{ &initialSubmission, diff --git a/appview/repo/artifact.go b/appview/repo/artifact.go index e9124da..151fd91 100644 --- a/appview/repo/artifact.go +++ b/appview/repo/artifact.go @@ -76,7 +76,7 @@ func (rp *Repo) AttachArtifact(w http.ResponseWriter, r *http.Request) { Artifact: uploadBlobResp.Blob, CreatedAt: createdAt.Format(time.RFC3339), Name: handler.Filename, - Repo: f.RepoAt.String(), + Repo: f.RepoAt().String(), Tag: tag.Tag.Hash[:], }, }, @@ -100,7 +100,7 @@ func (rp *Repo) AttachArtifact(w http.ResponseWriter, r *http.Request) { artifact := db.Artifact{ Did: user.Did, Rkey: rkey, - RepoAt: f.RepoAt, + RepoAt: f.RepoAt(), Tag: tag.Tag.Hash, CreatedAt: createdAt, BlobCid: cid.Cid(uploadBlobResp.Blob.Ref), @@ -155,7 +155,7 @@ func (rp *Repo) DownloadArtifact(w http.ResponseWriter, r *http.Request) { artifacts, err := db.GetArtifact( rp.db, - db.FilterEq("repo_at", f.RepoAt), + db.FilterEq("repo_at", f.RepoAt()), db.FilterEq("tag", tag.Tag.Hash[:]), db.FilterEq("name", filename), ) @@ -197,7 +197,7 @@ func (rp *Repo) DeleteArtifact(w http.ResponseWriter, r *http.Request) { artifacts, err := db.GetArtifact( rp.db, - db.FilterEq("repo_at", f.RepoAt), + db.FilterEq("repo_at", f.RepoAt()), db.FilterEq("tag", tag[:]), db.FilterEq("name", filename), ) @@ -239,7 +239,7 @@ func (rp *Repo) DeleteArtifact(w http.ResponseWriter, r *http.Request) { defer tx.Rollback() err = db.DeleteArtifact(tx, - db.FilterEq("repo_at", f.RepoAt), + db.FilterEq("repo_at", f.RepoAt()), db.FilterEq("tag", artifact.Tag[:]), db.FilterEq("name", filename), ) @@ -270,7 +270,7 @@ func (rp *Repo) resolveTag(f *reporesolver.ResolvedRepo, tagParam string) (*type return nil, err } - result, err := us.Tags(f.OwnerDid(), f.RepoName) + result, err := us.Tags(f.OwnerDid(), f.Name) if err != nil { log.Println("failed to reach knotserver", err) return nil, err diff --git a/appview/repo/index.go b/appview/repo/index.go index 2661325..3325f08 100644 --- a/appview/repo/index.go +++ b/appview/repo/index.go @@ -37,7 +37,7 @@ func (rp *Repo) RepoIndex(w http.ResponseWriter, r *http.Request) { return } - result, err := us.Index(f.OwnerDid(), f.RepoName, ref) + result, err := us.Index(f.OwnerDid(), f.Name, ref) if err != nil { rp.pages.Error503(w) log.Println("failed to reach knotserver", err) @@ -166,13 +166,13 @@ func (rp *Repo) getLanguageInfo( // first attempt to fetch from db langs, err := db.GetRepoLanguages( rp.db, - db.FilterEq("repo_at", f.RepoAt), + db.FilterEq("repo_at", f.RepoAt()), db.FilterEq("ref", f.Ref), ) if err != nil || langs == nil { // non-fatal, fetch langs from ks - ls, err := signedClient.RepoLanguages(f.OwnerDid(), f.RepoName, f.Ref) + ls, err := signedClient.RepoLanguages(f.OwnerDid(), f.Name, f.Ref) if err != nil { return nil, err } @@ -182,7 +182,7 @@ func (rp *Repo) getLanguageInfo( for l, s := range ls.Languages { langs = append(langs, db.RepoLanguage{ - RepoAt: f.RepoAt, + RepoAt: f.RepoAt(), Ref: f.Ref, IsDefaultRef: isDefaultRef, Language: l, @@ -279,7 +279,7 @@ func getForkInfo( hiddenRef := fmt.Sprintf("hidden/%s/%s", f.Ref, f.Ref) var status types.AncestorCheckResponse - forkSyncableResp, err := signedClient.RepoForkAheadBehind(user.Did, string(f.RepoAt), repoInfo.Name, f.Ref, hiddenRef) + forkSyncableResp, err := signedClient.RepoForkAheadBehind(user.Did, string(f.RepoAt()), repoInfo.Name, f.Ref, hiddenRef) if err != nil { log.Printf("failed to check if fork is ahead/behind: %s", err) return nil, err diff --git a/appview/repo/repo.go b/appview/repo/repo.go index 3cf93b8..3ad67c3 100644 --- a/appview/repo/repo.go +++ b/appview/repo/repo.go @@ -98,13 +98,13 @@ func (rp *Repo) RepoLog(w http.ResponseWriter, r *http.Request) { return } - repolog, err := us.Log(f.OwnerDid(), f.RepoName, ref, page) + repolog, err := us.Log(f.OwnerDid(), f.Name, ref, page) if err != nil { log.Println("failed to reach knotserver", err) return } - tagResult, err := us.Tags(f.OwnerDid(), f.RepoName) + tagResult, err := us.Tags(f.OwnerDid(), f.Name) if err != nil { log.Println("failed to reach knotserver", err) return @@ -119,7 +119,7 @@ func (rp *Repo) RepoLog(w http.ResponseWriter, r *http.Request) { tagMap[hash] = append(tagMap[hash], tag.Name) } - branchResult, err := us.Branches(f.OwnerDid(), f.RepoName) + branchResult, err := us.Branches(f.OwnerDid(), f.Name) if err != nil { log.Println("failed to reach knotserver", err) return @@ -187,7 +187,7 @@ func (rp *Repo) RepoDescription(w http.ResponseWriter, r *http.Request) { return } - repoAt := f.RepoAt + repoAt := f.RepoAt() rkey := repoAt.RecordKey().String() if rkey == "" { log.Println("invalid aturi for repo", err) @@ -237,9 +237,9 @@ func (rp *Repo) RepoDescription(w http.ResponseWriter, r *http.Request) { Record: &lexutil.LexiconTypeDecoder{ Val: &tangled.Repo{ Knot: f.Knot, - Name: f.RepoName, + Name: f.Name, Owner: user.Did, - CreatedAt: f.CreatedAt, + CreatedAt: f.Created.Format(time.RFC3339), Description: &newDescription, Spindle: &f.Spindle, }, @@ -285,7 +285,7 @@ func (rp *Repo) RepoCommit(w http.ResponseWriter, r *http.Request) { return } - resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/commit/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref)) + resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/commit/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref)) if err != nil { log.Println("failed to reach knotserver", err) return @@ -350,7 +350,7 @@ func (rp *Repo) RepoTree(w http.ResponseWriter, r *http.Request) { if !rp.config.Core.Dev { protocol = "https" } - resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tree/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, treePath)) + resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tree/%s/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref, treePath)) if err != nil { log.Println("failed to reach knotserver", err) return @@ -380,7 +380,7 @@ func (rp *Repo) RepoTree(w http.ResponseWriter, r *http.Request) { user := rp.oauth.GetUser(r) var breadcrumbs [][]string - breadcrumbs = append(breadcrumbs, []string{f.RepoName, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)}) + breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)}) if treePath != "" { for idx, elem := range strings.Split(treePath, "/") { breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)}) @@ -411,13 +411,13 @@ func (rp *Repo) RepoTags(w http.ResponseWriter, r *http.Request) { return } - result, err := us.Tags(f.OwnerDid(), f.RepoName) + result, err := us.Tags(f.OwnerDid(), f.Name) if err != nil { log.Println("failed to reach knotserver", err) return } - artifacts, err := db.GetArtifact(rp.db, db.FilterEq("repo_at", f.RepoAt)) + artifacts, err := db.GetArtifact(rp.db, db.FilterEq("repo_at", f.RepoAt())) if err != nil { log.Println("failed grab artifacts", err) return @@ -468,7 +468,7 @@ func (rp *Repo) RepoBranches(w http.ResponseWriter, r *http.Request) { return } - result, err := us.Branches(f.OwnerDid(), f.RepoName) + result, err := us.Branches(f.OwnerDid(), f.Name) if err != nil { log.Println("failed to reach knotserver", err) return @@ -497,7 +497,7 @@ func (rp *Repo) RepoBlob(w http.ResponseWriter, r *http.Request) { if !rp.config.Core.Dev { protocol = "https" } - resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath)) + resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref, filePath)) if err != nil { log.Println("failed to reach knotserver", err) return @@ -517,7 +517,7 @@ func (rp *Repo) RepoBlob(w http.ResponseWriter, r *http.Request) { } var breadcrumbs [][]string - breadcrumbs = append(breadcrumbs, []string{f.RepoName, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)}) + breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)}) if filePath != "" { for idx, elem := range strings.Split(filePath, "/") { breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)}) @@ -557,7 +557,7 @@ func (rp *Repo) RepoBlobRaw(w http.ResponseWriter, r *http.Request) { if !rp.config.Core.Dev { protocol = "https" } - resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath)) + resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref, filePath)) if err != nil { log.Println("failed to reach knotserver", err) return @@ -595,7 +595,7 @@ func (rp *Repo) EditSpindle(w http.ResponseWriter, r *http.Request) { return } - repoAt := f.RepoAt + repoAt := f.RepoAt() rkey := repoAt.RecordKey().String() if rkey == "" { log.Println("invalid aturi for repo", err) @@ -649,9 +649,9 @@ func (rp *Repo) EditSpindle(w http.ResponseWriter, r *http.Request) { Record: &lexutil.LexiconTypeDecoder{ Val: &tangled.Repo{ Knot: f.Knot, - Name: f.RepoName, + Name: f.Name, Owner: user.Did, - CreatedAt: f.CreatedAt, + CreatedAt: f.Created.Format(time.RFC3339), Description: &f.Description, Spindle: &newSpindle, }, @@ -708,7 +708,7 @@ func (rp *Repo) AddCollaborator(w http.ResponseWriter, r *http.Request) { return } - ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.RepoName, collaboratorIdent.DID.String()) + ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.Name, collaboratorIdent.DID.String()) if err != nil { log.Printf("failed to make request to %s: %s", f.Knot, err) return @@ -739,7 +739,7 @@ func (rp *Repo) AddCollaborator(w http.ResponseWriter, r *http.Request) { return } - err = db.AddCollaborator(rp.db, collaboratorIdent.DID.String(), f.OwnerDid(), f.RepoName, f.Knot) + err = db.AddCollaborator(rp.db, collaboratorIdent.DID.String(), f.OwnerDid(), f.Name, f.Repo.Knot) if err != nil { w.Write(fmt.Append(nil, "failed to add collaborator: ", err)) return @@ -778,18 +778,17 @@ func (rp *Repo) DeleteRepo(w http.ResponseWriter, r *http.Request) { log.Println("failed to get authorized client", err) return } - repoRkey := f.RepoAt.RecordKey().String() _, err = xrpcClient.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ Collection: tangled.RepoNSID, Repo: user.Did, - Rkey: repoRkey, + Rkey: f.Rkey, }) if err != nil { log.Printf("failed to delete record: %s", err) rp.pages.Notice(w, "settings-delete", "Failed to delete repository from PDS.") return } - log.Println("removed repo record ", f.RepoAt.String()) + log.Println("removed repo record ", f.RepoAt().String()) secret, err := db.GetRegistrationKey(rp.db, f.Knot) if err != nil { @@ -803,7 +802,7 @@ func (rp *Repo) DeleteRepo(w http.ResponseWriter, r *http.Request) { return } - ksResp, err := ksClient.RemoveRepo(f.OwnerDid(), f.RepoName) + ksResp, err := ksClient.RemoveRepo(f.OwnerDid(), f.Name) if err != nil { log.Printf("failed to make request to %s: %s", f.Knot, err) return @@ -849,7 +848,7 @@ func (rp *Repo) DeleteRepo(w http.ResponseWriter, r *http.Request) { } // remove repo from db - err = db.RemoveRepo(tx, f.OwnerDid(), f.RepoName) + err = db.RemoveRepo(tx, f.OwnerDid(), f.Name) if err != nil { rp.pages.Notice(w, "settings-delete", "Failed to update appview") return @@ -898,7 +897,7 @@ func (rp *Repo) SetDefaultBranch(w http.ResponseWriter, r *http.Request) { return } - ksResp, err := ksClient.SetDefaultBranch(f.OwnerDid(), f.RepoName, branch) + ksResp, err := ksClient.SetDefaultBranch(f.OwnerDid(), f.Name, branch) if err != nil { log.Printf("failed to make request to %s: %s", f.Knot, err) return @@ -958,7 +957,7 @@ func (rp *Repo) Secrets(w http.ResponseWriter, r *http.Request) { r.Context(), spindleClient, &tangled.RepoAddSecret_Input{ - Repo: f.RepoAt.String(), + Repo: f.RepoAt().String(), Key: key, Value: value, }, @@ -973,7 +972,7 @@ func (rp *Repo) Secrets(w http.ResponseWriter, r *http.Request) { r.Context(), spindleClient, &tangled.RepoRemoveSecret_Input{ - Repo: f.RepoAt.String(), + Repo: f.RepoAt().String(), Key: key, }, ) @@ -1014,7 +1013,7 @@ func (rp *Repo) RepoSettings(w http.ResponseWriter, r *http.Request) { return } - result, err := us.Branches(f.OwnerDid(), f.RepoName) + result, err := us.Branches(f.OwnerDid(), f.Name) if err != nil { log.Println("failed to reach knotserver", err) return @@ -1036,7 +1035,7 @@ func (rp *Repo) RepoSettings(w http.ResponseWriter, r *http.Request) { oauth.WithDev(rp.config.Core.Dev), ); err != nil { log.Println("failed to create spindle client", err) - } else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt.String()); err != nil { + } else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt().String()); err != nil { log.Println("failed to fetch secrets", err) } else { secrets = resp.Secrets @@ -1084,8 +1083,8 @@ func (rp *Repo) SyncRepoFork(w http.ResponseWriter, r *http.Request) { } else { uri = "https" } - forkName := fmt.Sprintf("%s", f.RepoName) - forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.RepoName) + forkName := fmt.Sprintf("%s", f.Name) + forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.Repo.Name) _, err = client.SyncRepoFork(user.Did, forkSourceUrl, forkName, f.Ref) if err != nil { @@ -1135,11 +1134,11 @@ func (rp *Repo) ForkRepo(w http.ResponseWriter, r *http.Request) { return } - forkName := fmt.Sprintf("%s", f.RepoName) + forkName := fmt.Sprintf("%s", f.Name) // this check is *only* to see if the forked repo name already exists // in the user's account. - existingRepo, err := db.GetRepo(rp.db, user.Did, f.RepoName) + existingRepo, err := db.GetRepo(rp.db, user.Did, f.Name) if err != nil { if errors.Is(err, sql.ErrNoRows) { // no existing repo with this name found, we can use the name as is @@ -1170,8 +1169,8 @@ func (rp *Repo) ForkRepo(w http.ResponseWriter, r *http.Request) { } else { uri = "https" } - forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.RepoName) - sourceAt := f.RepoAt.String() + forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.Repo.Name) + sourceAt := f.RepoAt().String() rkey := tid.TID() repo := &db.Repo{ @@ -1291,7 +1290,7 @@ func (rp *Repo) RepoCompareNew(w http.ResponseWriter, r *http.Request) { return } - result, err := us.Branches(f.OwnerDid(), f.RepoName) + result, err := us.Branches(f.OwnerDid(), f.Name) if err != nil { rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") log.Println("failed to reach knotserver", err) @@ -1321,7 +1320,7 @@ func (rp *Repo) RepoCompareNew(w http.ResponseWriter, r *http.Request) { head = queryHead } - tags, err := us.Tags(f.OwnerDid(), f.RepoName) + tags, err := us.Tags(f.OwnerDid(), f.Name) if err != nil { rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") log.Println("failed to reach knotserver", err) @@ -1383,21 +1382,21 @@ func (rp *Repo) RepoCompare(w http.ResponseWriter, r *http.Request) { return } - branches, err := us.Branches(f.OwnerDid(), f.RepoName) + branches, err := us.Branches(f.OwnerDid(), f.Name) if err != nil { rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") log.Println("failed to reach knotserver", err) return } - tags, err := us.Tags(f.OwnerDid(), f.RepoName) + tags, err := us.Tags(f.OwnerDid(), f.Name) if err != nil { rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") log.Println("failed to reach knotserver", err) return } - formatPatch, err := us.Compare(f.OwnerDid(), f.RepoName, base, head) + formatPatch, err := us.Compare(f.OwnerDid(), f.Name, base, head) if err != nil { rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") log.Println("failed to compare", err) diff --git a/appview/reporesolver/resolver.go b/appview/reporesolver/resolver.go index ad2fdd9..a3928b5 100644 --- a/appview/reporesolver/resolver.go +++ b/appview/reporesolver/resolver.go @@ -12,7 +12,6 @@ import ( "strings" "github.com/bluesky-social/indigo/atproto/identity" - "github.com/bluesky-social/indigo/atproto/syntax" securejoin "github.com/cyphar/filepath-securejoin" "github.com/go-chi/chi/v5" "tangled.sh/tangled.sh/core/appview/config" @@ -26,13 +25,8 @@ import ( ) type ResolvedRepo struct { - Knot string + db.Repo OwnerId identity.Identity - RepoName string - RepoAt syntax.ATURI - Description string - Spindle string - CreatedAt string Ref string CurrentDir string @@ -51,10 +45,9 @@ func New(config *config.Config, enforcer *rbac.Enforcer, resolver *idresolver.Re } func (rr *RepoResolver) Resolve(r *http.Request) (*ResolvedRepo, error) { - repoName := chi.URLParam(r, "repo") - knot, ok := r.Context().Value("knot").(string) + repo, ok := r.Context().Value("repo").(*db.Repo) if !ok { - log.Println("malformed middleware") + log.Println("malformed middleware: `repo` not exist in context") return nil, fmt.Errorf("malformed middleware") } id, ok := r.Context().Value("resolvedId").(identity.Identity) @@ -63,27 +56,15 @@ func (rr *RepoResolver) Resolve(r *http.Request) (*ResolvedRepo, error) { return nil, fmt.Errorf("malformed middleware") } - repoAt, ok := r.Context().Value("repoAt").(string) - if !ok { - log.Println("malformed middleware") - return nil, fmt.Errorf("malformed middleware") - } - - parsedRepoAt, err := syntax.ParseATURI(repoAt) - if err != nil { - log.Println("malformed repo at-uri") - return nil, fmt.Errorf("malformed middleware") - } - ref := chi.URLParam(r, "ref") if ref == "" { - us, err := knotclient.NewUnsignedClient(knot, rr.config.Core.Dev) + us, err := knotclient.NewUnsignedClient(repo.Knot, rr.config.Core.Dev) if err != nil { return nil, err } - defaultBranch, err := us.DefaultBranch(id.DID.String(), repoName) + defaultBranch, err := us.DefaultBranch(id.DID.String(), repo.Name) if err != nil { return nil, err } @@ -93,21 +74,11 @@ func (rr *RepoResolver) Resolve(r *http.Request) (*ResolvedRepo, error) { currentDir := path.Dir(extractPathAfterRef(r.URL.EscapedPath(), ref)) - // pass through values from the middleware - description, ok := r.Context().Value("repoDescription").(string) - addedAt, ok := r.Context().Value("repoAddedAt").(string) - spindle, ok := r.Context().Value("repoSpindle").(string) - return &ResolvedRepo{ - Knot: knot, - OwnerId: id, - RepoName: repoName, - RepoAt: parsedRepoAt, - Description: description, - CreatedAt: addedAt, - Ref: ref, - CurrentDir: currentDir, - Spindle: spindle, + Repo: *repo, + OwnerId: id, + Ref: ref, + CurrentDir: currentDir, rr: rr, }, nil @@ -126,19 +97,14 @@ func (f *ResolvedRepo) OwnerSlashRepo() string { var p string if handle != "" && !handle.IsInvalidHandle() { - p, _ = securejoin.SecureJoin(fmt.Sprintf("@%s", handle), f.RepoName) + p, _ = securejoin.SecureJoin(fmt.Sprintf("@%s", handle), f.Name) } else { - p, _ = securejoin.SecureJoin(f.OwnerDid(), f.RepoName) + p, _ = securejoin.SecureJoin(f.OwnerDid(), f.Name) } return p } -func (f *ResolvedRepo) DidSlashRepo() string { - p, _ := securejoin.SecureJoin(f.OwnerDid(), f.RepoName) - return p -} - func (f *ResolvedRepo) Collaborators(ctx context.Context) ([]pages.Collaborator, error) { repoCollaborators, err := f.rr.enforcer.E.GetImplicitUsersForResourceByDomain(f.DidSlashRepo(), f.Knot) if err != nil { @@ -186,28 +152,29 @@ func (f *ResolvedRepo) Collaborators(ctx context.Context) ([]pages.Collaborator, // this function is a bit weird since it now returns RepoInfo from an entirely different // package. we should refactor this or get rid of RepoInfo entirely. func (f *ResolvedRepo) RepoInfo(user *oauth.User) repoinfo.RepoInfo { + repoAt := f.RepoAt() isStarred := false if user != nil { - isStarred = db.GetStarStatus(f.rr.execer, user.Did, syntax.ATURI(f.RepoAt)) + isStarred = db.GetStarStatus(f.rr.execer, user.Did, repoAt) } - starCount, err := db.GetStarCount(f.rr.execer, f.RepoAt) + starCount, err := db.GetStarCount(f.rr.execer, repoAt) if err != nil { - log.Println("failed to get star count for ", f.RepoAt) + log.Println("failed to get star count for ", repoAt) } - issueCount, err := db.GetIssueCount(f.rr.execer, f.RepoAt) + issueCount, err := db.GetIssueCount(f.rr.execer, repoAt) if err != nil { - log.Println("failed to get issue count for ", f.RepoAt) + log.Println("failed to get issue count for ", repoAt) } - pullCount, err := db.GetPullCount(f.rr.execer, f.RepoAt) + pullCount, err := db.GetPullCount(f.rr.execer, repoAt) if err != nil { - log.Println("failed to get issue count for ", f.RepoAt) + log.Println("failed to get issue count for ", repoAt) } - source, err := db.GetRepoSource(f.rr.execer, f.RepoAt) + source, err := db.GetRepoSource(f.rr.execer, repoAt) if errors.Is(err, sql.ErrNoRows) { source = "" } else if err != nil { - log.Println("failed to get repo source for ", f.RepoAt, err) + log.Println("failed to get repo source for ", repoAt, err) } var sourceRepo *db.Repo @@ -232,9 +199,9 @@ func (f *ResolvedRepo) RepoInfo(user *oauth.User) repoinfo.RepoInfo { if err != nil { log.Printf("failed to create unsigned client for %s: %v", knot, err) } else { - result, err := us.Branches(f.OwnerDid(), f.RepoName) + result, err := us.Branches(f.OwnerDid(), f.Name) if err != nil { - log.Printf("failed to get branches for %s/%s: %v", f.OwnerDid(), f.RepoName, err) + log.Printf("failed to get branches for %s/%s: %v", f.OwnerDid(), f.Name, err) } if len(result.Branches) == 0 { @@ -245,8 +212,8 @@ func (f *ResolvedRepo) RepoInfo(user *oauth.User) repoinfo.RepoInfo { repoInfo := repoinfo.RepoInfo{ OwnerDid: f.OwnerDid(), OwnerHandle: f.OwnerHandle(), - Name: f.RepoName, - RepoAt: f.RepoAt, + Name: f.Name, + RepoAt: repoAt, Description: f.Description, Ref: f.Ref, IsStarred: isStarred, -- 2.43.0 From 3fbb4f0b58b5e17f2525fa110ab3230a23558937 Mon Sep 17 00:00:00 2001 From: Seongmin Lee Date: Fri, 1 Aug 2025 22:52:43 +0900 Subject: [PATCH] apview: replace all use of `db.Repo.AtUri` with `db.Repo.RepoAt()` Change-Id: ystmvoqyowpusnnuunrztmppvpwrnvno Signed-off-by: Seongmin Lee --- appview/db/repos.go | 11 +++++++---- appview/pulls/pulls.go | 11 +++-------- appview/repo/repo.go | 1 - appview/state/state.go | 1 - 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/appview/db/repos.go b/appview/db/repos.go index 0d91f64..208d4c2 100644 --- a/appview/db/repos.go +++ b/appview/db/repos.go @@ -391,7 +391,7 @@ func GetRepo(e Execer, did, name string) (*Repo, error) { var description, spindle sql.NullString row := e.QueryRow(` - select did, name, knot, created, at_uri, description, spindle + select did, name, knot, created, at_uri, description, spindle, rkey from repos where did = ? and name = ? `, @@ -400,7 +400,7 @@ func GetRepo(e Execer, did, name string) (*Repo, error) { ) var createdAt string - if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &description, &spindle); err != nil { + if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &description, &spindle, &repo.Rkey); err != nil { return nil, err } createdAtTime, _ := time.Parse(time.RFC3339, createdAt) @@ -421,10 +421,10 @@ func GetRepoByAtUri(e Execer, atUri string) (*Repo, error) { var repo Repo var nullableDescription sql.NullString - row := e.QueryRow(`select did, name, knot, created, at_uri, description from repos where at_uri = ?`, atUri) + row := e.QueryRow(`select did, name, knot, created, at_uri, rkey, description from repos where at_uri = ?`, atUri) var createdAt string - if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &nullableDescription); err != nil { + if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &repo.Rkey, &nullableDescription); err != nil { return nil, err } createdAtTime, _ := time.Parse(time.RFC3339, createdAt) @@ -440,6 +440,9 @@ func GetRepoByAtUri(e Execer, atUri string) (*Repo, error) { } func AddRepo(e Execer, repo *Repo) error { + if repo.AtUri == "" { + repo.AtUri = repo.RepoAt().String() + } _, err := e.Exec( `insert into repos (did, name, knot, rkey, at_uri, description, source) diff --git a/appview/pulls/pulls.go b/appview/pulls/pulls.go index 39ca6fb..30dc697 100644 --- a/appview/pulls/pulls.go +++ b/appview/pulls/pulls.go @@ -28,7 +28,6 @@ import ( "github.com/bluekeyes/go-gitdiff/gitdiff" comatproto "github.com/bluesky-social/indigo/api/atproto" - "github.com/bluesky-social/indigo/atproto/syntax" lexutil "github.com/bluesky-social/indigo/lex/util" "github.com/go-chi/chi/v5" "github.com/google/uuid" @@ -958,12 +957,8 @@ func (s *Pulls) handleForkBasedPull(w http.ResponseWriter, r *http.Request, f *r return } - forkAtUri, err := syntax.ParseATURI(fork.AtUri) - if err != nil { - log.Println("failed to parse fork AT URI", err) - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") - return - } + forkAtUri := fork.RepoAt() + forkAtUriStr := forkAtUri.String() pullSource := &db.PullSource{ Branch: sourceBranch, @@ -971,7 +966,7 @@ func (s *Pulls) handleForkBasedPull(w http.ResponseWriter, r *http.Request, f *r } recordPullSource := &tangled.RepoPull_Source{ Branch: sourceBranch, - Repo: &fork.AtUri, + Repo: &forkAtUriStr, Sha: sourceRev, } diff --git a/appview/repo/repo.go b/appview/repo/repo.go index 3ad67c3..3d26225 100644 --- a/appview/repo/repo.go +++ b/appview/repo/repo.go @@ -1239,7 +1239,6 @@ func (rp *Repo) ForkRepo(w http.ResponseWriter, r *http.Request) { } log.Println("created repo record: ", atresp.Uri) - repo.AtUri = atresp.Uri err = db.AddRepo(tx, repo) if err != nil { log.Println(err) diff --git a/appview/state/state.go b/appview/state/state.go index 8945552..abaea3c 100644 --- a/appview/state/state.go +++ b/appview/state/state.go @@ -395,7 +395,6 @@ func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) { // continue } - repo.AtUri = atresp.Uri err = db.AddRepo(tx, repo) if err != nil { log.Println(err) -- 2.43.0 From 1b6628ab2473ff9f8013859c630671ddd8e5b403 Mon Sep 17 00:00:00 2001 From: Seongmin Lee Date: Fri, 25 Jul 2025 00:27:46 +0900 Subject: [PATCH] appview/pages: markup: add `@` user-mention parsing in markdown Change-Id: ulyruprmrqxnopnnuuovqqzuzonqrpyl Signed-off-by: Seongmin Lee --- appview/pages/markup/markdown.go | 12 +- appview/pages/markup/markdown_at_extension.go | 134 ++++++++++++++++++ 2 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 appview/pages/markup/markdown_at_extension.go diff --git a/appview/pages/markup/markdown.go b/appview/pages/markup/markdown.go index df4afa5..5cdc038 100644 --- a/appview/pages/markup/markdown.go +++ b/appview/pages/markup/markdown.go @@ -42,14 +42,22 @@ type RenderContext struct { RendererType RendererType } -func (rctx *RenderContext) RenderMarkdown(source string) string { +func NewMarkdown() goldmark.Markdown { md := goldmark.New( - goldmark.WithExtensions(extension.GFM), + goldmark.WithExtensions( + extension.GFM, + AtExt, + ), goldmark.WithParserOptions( parser.WithAutoHeadingID(), ), goldmark.WithRendererOptions(html.WithUnsafe()), ) + return md +} + +func (rctx *RenderContext) RenderMarkdown(source string) string { + md := NewMarkdown() if rctx != nil { var transformers []util.PrioritizedValue diff --git a/appview/pages/markup/markdown_at_extension.go b/appview/pages/markup/markdown_at_extension.go new file mode 100644 index 0000000..1bd63e8 --- /dev/null +++ b/appview/pages/markup/markdown_at_extension.go @@ -0,0 +1,134 @@ +// heavily inspired by: https://github.com/kaleocheng/goldmark-extensions + +package markup + +import ( + "regexp" + + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/renderer" + "github.com/yuin/goldmark/renderer/html" + "github.com/yuin/goldmark/text" + "github.com/yuin/goldmark/util" +) + +// An AtNode struct represents an AtNode +type AtNode struct { + handle string + ast.BaseInline +} + +var _ ast.Node = &AtNode{} + +// Dump implements Node.Dump. +func (n *AtNode) Dump(source []byte, level int) { + ast.DumpHelper(n, source, level, nil, nil) +} + +// KindAt is a NodeKind of the At node. +var KindAt = ast.NewNodeKind("At") + +// Kind implements Node.Kind. +func (n *AtNode) Kind() ast.NodeKind { + return KindAt +} + +var atRegexp = regexp.MustCompile(`(^|\s|\()(@)([a-zA-Z0-9.-]+)(\b)`) + +type atParser struct{} + +// NewAtParser return a new InlineParser that parses +// at expressions. +func NewAtParser() parser.InlineParser { + return &atParser{} +} + +func (s *atParser) Trigger() []byte { + return []byte{'@'} +} + +func (s *atParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node { + line, segment := block.PeekLine() + m := atRegexp.FindSubmatchIndex(line) + if m == nil { + return nil + } + block.Advance(m[1]) + node := &AtNode{} + node.AppendChild(node, ast.NewTextSegment(text.NewSegment(segment.Start, segment.Start+m[1]))) + node.handle = string(node.Text(block.Source())[1:]) + return node +} + +// atHtmlRenderer is a renderer.NodeRenderer implementation that +// renders At nodes. +type atHtmlRenderer struct { + html.Config +} + +// NewAtHTMLRenderer returns a new AtHTMLRenderer. +func NewAtHTMLRenderer(opts ...html.Option) renderer.NodeRenderer { + r := &atHtmlRenderer{ + Config: html.NewConfig(), + } + for _, opt := range opts { + opt.SetHTMLOption(&r.Config) + } + return r +} + +// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs. +func (r *atHtmlRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { + reg.Register(KindAt, r.renderAt) +} + +func (r *atHtmlRenderer) renderAt(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) { + if entering { + w.WriteString(``) + } else { + w.WriteString("") + } + return ast.WalkContinue, nil +} + +type atExt struct{} + +// At is an extension that allow you to use at expression like '@user.bsky.social' . +var AtExt = &atExt{} + +func (e *atExt) Extend(m goldmark.Markdown) { + m.Parser().AddOptions(parser.WithInlineParsers( + util.Prioritized(NewAtParser(), 500), + )) + m.Renderer().AddOptions(renderer.WithNodeRenderers( + util.Prioritized(NewAtHTMLRenderer(), 500), + )) +} + +// FindUserMentions returns Set of user handles from given markup soruce. +// It doesn't guarntee unique DIDs +func FindUserMentions(source string) []string { + var ( + mentions []string + mentionsSet = make(map[string]struct{}) + md = NewMarkdown() + sourceBytes = []byte(source) + root = md.Parser().Parse(text.NewReader(sourceBytes)) + ) + ast.Walk(root, func(n ast.Node, entering bool) (ast.WalkStatus, error) { + if entering && n.Kind() == KindAt { + handle := n.(*AtNode).handle + mentionsSet[handle] = struct{}{} + return ast.WalkSkipChildren, nil + } + return ast.WalkContinue, nil + }) + for handle := range mentionsSet { + mentions = append(mentions, handle) + } + return mentions +} -- 2.43.0 From e1287c9563a2539aef7f4de93b56d97bc78bd0a3 Mon Sep 17 00:00:00 2001 From: Seongmin Lee Date: Sat, 26 Jul 2025 10:59:02 +0900 Subject: [PATCH] appview: add `NewIssueComment` event Change-Id: knkwrvolorspoxoqttxurpplqqxvoyxo Signed-off-by: Seongmin Lee --- appview/issues/issues.go | 31 +++++++++++++++++-------------- appview/notify/merged_notifier.go | 6 ++++++ appview/notify/notifier.go | 2 ++ appview/posthog/notifier.go | 14 ++++++++++++++ 4 files changed, 39 insertions(+), 14 deletions(-) diff --git a/appview/issues/issues.go b/appview/issues/issues.go index 35c37f1..fc98ade 100644 --- a/appview/issues/issues.go +++ b/appview/issues/issues.go @@ -21,6 +21,7 @@ import ( "tangled.sh/tangled.sh/core/appview/notify" "tangled.sh/tangled.sh/core/appview/oauth" "tangled.sh/tangled.sh/core/appview/pages" + "tangled.sh/tangled.sh/core/appview/pages/markup" "tangled.sh/tangled.sh/core/appview/pagination" "tangled.sh/tangled.sh/core/appview/reporesolver" "tangled.sh/tangled.sh/core/idresolver" @@ -274,17 +275,16 @@ func (rp *Issues) NewIssueComment(w http.ResponseWriter, r *http.Request) { return } - commentId := mathrand.IntN(1000000) - rkey := tid.TID() - - err := db.NewIssueComment(rp.db, &db.Comment{ + comment := &db.Comment{ OwnerDid: user.Did, RepoAt: f.RepoAt(), Issue: issueIdInt, - CommentId: commentId, + CommentId: mathrand.IntN(1000000), Body: body, - Rkey: rkey, - }) + Rkey: tid.TID(), + } + + err := db.NewIssueComment(rp.db, comment) if err != nil { log.Println("failed to create comment", err) rp.pages.Notice(w, "issue-comment", "Failed to create comment.") @@ -292,16 +292,15 @@ func (rp *Issues) NewIssueComment(w http.ResponseWriter, r *http.Request) { } createdAt := time.Now().Format(time.RFC3339) - commentIdInt64 := int64(commentId) - ownerDid := user.Did - issueAt, err := db.GetIssueAt(rp.db, f.RepoAt(), issueIdInt) + commentIdInt64 := int64(comment.CommentId) + issueAt, err := db.GetIssueAt(rp.db, f.RepoAt(), issueIdInt) if err != nil { log.Println("failed to get issue at", err) rp.pages.Notice(w, "issue-comment", "Failed to create comment.") return } - atUri := f.RepoAt().String() + atUri := comment.RepoAt.String() client, err := rp.oauth.AuthorizedClient(r) if err != nil { log.Println("failed to get authorized client", err) @@ -311,13 +310,13 @@ func (rp *Issues) NewIssueComment(w http.ResponseWriter, r *http.Request) { _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ Collection: tangled.RepoIssueCommentNSID, Repo: user.Did, - Rkey: rkey, + Rkey: comment.Rkey, Record: &lexutil.LexiconTypeDecoder{ Val: &tangled.RepoIssueComment{ Repo: &atUri, Issue: issueAt, CommentId: &commentIdInt64, - Owner: &ownerDid, + Owner: &comment.OwnerDid, Body: body, CreatedAt: createdAt, }, @@ -329,7 +328,11 @@ func (rp *Issues) NewIssueComment(w http.ResponseWriter, r *http.Request) { return } - rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", f.OwnerSlashRepo(), issueIdInt, commentId)) + mentions := markup.FindUserMentions(comment.Body) + + rp.notifier.NewIssueComment(r.Context(), comment, mentions) + + rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", f.OwnerSlashRepo(), comment.Issue, comment.CommentId)) return } } diff --git a/appview/notify/merged_notifier.go b/appview/notify/merged_notifier.go index faf0a11..4034c6a 100644 --- a/appview/notify/merged_notifier.go +++ b/appview/notify/merged_notifier.go @@ -39,6 +39,12 @@ func (m *mergedNotifier) NewIssue(ctx context.Context, issue *db.Issue) { } } +func (m *mergedNotifier) NewIssueComment(ctx context.Context, comment *db.Comment, mentions []string) { + for _, notifier := range m.notifiers { + notifier.NewIssueComment(ctx, comment, mentions) + } +} + func (m *mergedNotifier) NewFollow(ctx context.Context, follow *db.Follow) { for _, notifier := range m.notifiers { notifier.NewFollow(ctx, follow) diff --git a/appview/notify/notifier.go b/appview/notify/notifier.go index 89f8121..4d2813b 100644 --- a/appview/notify/notifier.go +++ b/appview/notify/notifier.go @@ -13,6 +13,7 @@ type Notifier interface { DeleteStar(ctx context.Context, star *db.Star) NewIssue(ctx context.Context, issue *db.Issue) + NewIssueComment(ctx context.Context, comment *db.Comment, mentions []string) NewFollow(ctx context.Context, follow *db.Follow) DeleteFollow(ctx context.Context, follow *db.Follow) @@ -34,6 +35,7 @@ func (m *BaseNotifier) NewStar(ctx context.Context, star *db.Star) {} func (m *BaseNotifier) DeleteStar(ctx context.Context, star *db.Star) {} func (m *BaseNotifier) NewIssue(ctx context.Context, issue *db.Issue) {} +func (m *BaseNotifier) NewIssueComment(ctx context.Context, comment *db.Comment, mentions []string) {} func (m *BaseNotifier) NewFollow(ctx context.Context, follow *db.Follow) {} func (m *BaseNotifier) DeleteFollow(ctx context.Context, follow *db.Follow) {} diff --git a/appview/posthog/notifier.go b/appview/posthog/notifier.go index 8dbd198..47638da 100644 --- a/appview/posthog/notifier.go +++ b/appview/posthog/notifier.go @@ -70,6 +70,20 @@ func (n *posthogNotifier) NewIssue(ctx context.Context, issue *db.Issue) { } } +func (n *posthogNotifier) NewIssueComment(ctx context.Context, comment *db.Comment, mentions []string) { + err := n.client.Enqueue(posthog.Capture{ + DistinctId: comment.OwnerDid, + Event: "new_issue", + Properties: posthog.Properties{ + "repo_at": comment.RepoAt.String(), + "issue_id": comment.Issue, + }, + }) + if err != nil { + log.Println("failed to enqueue posthog event:", err) + } +} + func (n *posthogNotifier) NewPull(ctx context.Context, pull *db.Pull) { err := n.client.Enqueue(posthog.Capture{ DistinctId: pull.OwnerDid, -- 2.43.0 From 18b248e57efc0f9c9a3ccd40dd64a32335cda208 Mon Sep 17 00:00:00 2001 From: Seongmin Lee Date: Tue, 29 Jul 2025 00:27:16 +0900 Subject: [PATCH] appview: email: send email notification on mention Change-Id: vkmkzoplqkxytqyktuqxxnlkyqlxwwnp Signed-off-by: Seongmin Lee --- appview/email/notifier.go | 95 +++++++++++++++++++++++++++++++++++++++ appview/state/state.go | 2 + 2 files changed, 97 insertions(+) create mode 100644 appview/email/notifier.go diff --git a/appview/email/notifier.go b/appview/email/notifier.go new file mode 100644 index 0000000..cf9edea --- /dev/null +++ b/appview/email/notifier.go @@ -0,0 +1,95 @@ +package email + +import ( + "context" + "fmt" + "log" + + securejoin "github.com/cyphar/filepath-securejoin" + "tangled.sh/tangled.sh/core/appview/config" + "tangled.sh/tangled.sh/core/appview/db" + "tangled.sh/tangled.sh/core/appview/notify" + "tangled.sh/tangled.sh/core/idresolver" +) + +type EmailNotifier struct { + db *db.DB + idResolver *idresolver.Resolver + Config *config.Config + notify.BaseNotifier +} + +func NewEmailNotifier( + db *db.DB, + idResolver *idresolver.Resolver, + config *config.Config, +) notify.Notifier { + return &EmailNotifier{ + db, + idResolver, + config, + notify.BaseNotifier{}, + } +} + +var _ notify.Notifier = &EmailNotifier{} + +func (n *EmailNotifier) buildIssueEmail(ctx context.Context, comment *db.Comment, did string) (Email, error) { + issue, err := db.GetIssue(n.db, comment.RepoAt, comment.Issue) + if err != nil { + return Email{}, fmt.Errorf("db.GetIssue: %w", err) + } + repo, err := db.GetRepoByAtUri(n.db, comment.RepoAt.String()) + if err != nil { + return Email{}, fmt.Errorf("db.GetRepoByAtUri: %w", err) + } + // TODO: check email preferences + email, err := db.GetPrimaryEmail(n.db, did) + if err != nil { + return Email{}, fmt.Errorf("db.GetPrimaryEmail: %w", err) + } + commentOwner, err := n.idResolver.ResolveIdent(ctx, comment.OwnerDid) + if err != nil || commentOwner.Handle.IsInvalidHandle() { + return Email{}, fmt.Errorf("resolve comment owner did: %w", err) + } + repoOwnerID, err := n.idResolver.ResolveIdent(ctx, repo.Did) + if err != nil || repoOwnerID.Handle.IsInvalidHandle() { + return Email{}, fmt.Errorf("resolve repo owner did: %w", err) + } + repoOwnerHandle := repoOwnerID.Handle + var repoOwnerSlashName string + if repoOwnerHandle != "" && !repoOwnerHandle.IsInvalidHandle() { + repoOwnerSlashName, _ = securejoin.SecureJoin(fmt.Sprintf("@%s", repoOwnerHandle), repo.Name) + } else { + repoOwnerSlashName = repo.DidSlashRepo() + } + return Email{ + APIKey: n.Config.Resend.ApiKey, + From: n.Config.Resend.SentFrom, + To: email.Address, + Subject: fmt.Sprintf("[%s] %s (issue#%d)", repoOwnerSlashName, issue.Title, issue.IssueId), + Html: fmt.Sprintf("@%s mentioned you", commentOwner.Handle.String()), + }, nil +} + +func (n *EmailNotifier) NewIssueComment(ctx context.Context, comment *db.Comment, mentions []string) { + resolvedIds := n.idResolver.ResolveIdents(ctx, mentions) + handleDidMap := make(map[string]string) + for _, identity := range resolvedIds { + if !identity.Handle.IsInvalidHandle() { + handleDidMap[identity.Handle.String()] = identity.DID.String() + } + } + for _, handle := range mentions { + id, err := n.idResolver.ResolveIdent(ctx, handle) + email, err := n.buildIssueEmail(ctx, comment, id.DID.String()) + if err != nil { + log.Println("failed to create issue-email:", err) + } + SendEmail(email) + } +} + +// func (n *EmailNotifier) NewPullComment(ctx context.Context, comment *db.PullComment, []string) { +// n.usersMentioned(ctx, mentions) +// } diff --git a/appview/state/state.go b/appview/state/state.go index abaea3c..b226329 100644 --- a/appview/state/state.go +++ b/appview/state/state.go @@ -20,6 +20,7 @@ import ( "tangled.sh/tangled.sh/core/appview/cache/session" "tangled.sh/tangled.sh/core/appview/config" "tangled.sh/tangled.sh/core/appview/db" + "tangled.sh/tangled.sh/core/appview/email" "tangled.sh/tangled.sh/core/appview/notify" "tangled.sh/tangled.sh/core/appview/oauth" "tangled.sh/tangled.sh/core/appview/pages" @@ -132,6 +133,7 @@ func Make(ctx context.Context, config *config.Config) (*State, error) { spindlestream.Start(ctx) var notifiers []notify.Notifier + notifiers = append(notifiers, email.NewEmailNotifier(d, res, config)) if !config.Core.Dev { notifiers = append(notifiers, posthog_service.NewPosthogNotifier(posthog)) } -- 2.43.0 From 9eebc110a4e2a9b77c718cb366d5733e337b4530 Mon Sep 17 00:00:00 2001 From: Seongmin Lee Date: Wed, 30 Jul 2025 01:03:48 +0900 Subject: [PATCH] appview: email: add source to email notification Change-Id: llokmwxvknnuzvuptzwpqtyuurrsplyo Signed-off-by: Seongmin Lee --- appview/email/notifier.go | 19 +++++++------------ appview/issues/issues.go | 8 ++++---- appview/notify/merged_notifier.go | 4 ++-- appview/notify/notifier.go | 4 ++-- appview/posthog/notifier.go | 2 +- 5 files changed, 16 insertions(+), 21 deletions(-) diff --git a/appview/email/notifier.go b/appview/email/notifier.go index cf9edea..fecab44 100644 --- a/appview/email/notifier.go +++ b/appview/email/notifier.go @@ -34,15 +34,7 @@ func NewEmailNotifier( var _ notify.Notifier = &EmailNotifier{} -func (n *EmailNotifier) buildIssueEmail(ctx context.Context, comment *db.Comment, did string) (Email, error) { - issue, err := db.GetIssue(n.db, comment.RepoAt, comment.Issue) - if err != nil { - return Email{}, fmt.Errorf("db.GetIssue: %w", err) - } - repo, err := db.GetRepoByAtUri(n.db, comment.RepoAt.String()) - if err != nil { - return Email{}, fmt.Errorf("db.GetRepoByAtUri: %w", err) - } +func (n *EmailNotifier) buildIssueEmail(ctx context.Context, repo *db.Repo, issue *db.Issue, comment *db.Comment, did string) (Email, error) { // TODO: check email preferences email, err := db.GetPrimaryEmail(n.db, did) if err != nil { @@ -63,16 +55,19 @@ func (n *EmailNotifier) buildIssueEmail(ctx context.Context, comment *db.Comment } else { repoOwnerSlashName = repo.DidSlashRepo() } + // TODO: make this configurable + baseUrl := "https://tangled.sh" + url := fmt.Sprintf("%s/%s/issues/%d#comment-%d", baseUrl, repoOwnerSlashName, comment.Issue, comment.CommentId) return Email{ APIKey: n.Config.Resend.ApiKey, From: n.Config.Resend.SentFrom, To: email.Address, Subject: fmt.Sprintf("[%s] %s (issue#%d)", repoOwnerSlashName, issue.Title, issue.IssueId), - Html: fmt.Sprintf("@%s mentioned you", commentOwner.Handle.String()), + Html: fmt.Sprintf(`

@%s mentioned you:

View it on tangled.sh.`, commentOwner.Handle.String(), url), }, nil } -func (n *EmailNotifier) NewIssueComment(ctx context.Context, comment *db.Comment, mentions []string) { +func (n *EmailNotifier) NewIssueComment(ctx context.Context, repo *db.Repo, issue *db.Issue, comment *db.Comment, mentions []string) { resolvedIds := n.idResolver.ResolveIdents(ctx, mentions) handleDidMap := make(map[string]string) for _, identity := range resolvedIds { @@ -82,7 +77,7 @@ func (n *EmailNotifier) NewIssueComment(ctx context.Context, comment *db.Comment } for _, handle := range mentions { id, err := n.idResolver.ResolveIdent(ctx, handle) - email, err := n.buildIssueEmail(ctx, comment, id.DID.String()) + email, err := n.buildIssueEmail(ctx, repo, issue, comment, id.DID.String()) if err != nil { log.Println("failed to create issue-email:", err) } diff --git a/appview/issues/issues.go b/appview/issues/issues.go index fc98ade..15121f0 100644 --- a/appview/issues/issues.go +++ b/appview/issues/issues.go @@ -293,9 +293,9 @@ func (rp *Issues) NewIssueComment(w http.ResponseWriter, r *http.Request) { createdAt := time.Now().Format(time.RFC3339) commentIdInt64 := int64(comment.CommentId) - issueAt, err := db.GetIssueAt(rp.db, f.RepoAt(), issueIdInt) + issue, err := db.GetIssue(rp.db, f.RepoAt(), issueIdInt) if err != nil { - log.Println("failed to get issue at", err) + log.Println("failed to get issue", err) rp.pages.Notice(w, "issue-comment", "Failed to create comment.") return } @@ -314,7 +314,7 @@ func (rp *Issues) NewIssueComment(w http.ResponseWriter, r *http.Request) { Record: &lexutil.LexiconTypeDecoder{ Val: &tangled.RepoIssueComment{ Repo: &atUri, - Issue: issueAt, + Issue: issue.IssueAt, CommentId: &commentIdInt64, Owner: &comment.OwnerDid, Body: body, @@ -330,7 +330,7 @@ func (rp *Issues) NewIssueComment(w http.ResponseWriter, r *http.Request) { mentions := markup.FindUserMentions(comment.Body) - rp.notifier.NewIssueComment(r.Context(), comment, mentions) + rp.notifier.NewIssueComment(r.Context(), &f.Repo, issue, comment, mentions) rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", f.OwnerSlashRepo(), comment.Issue, comment.CommentId)) return diff --git a/appview/notify/merged_notifier.go b/appview/notify/merged_notifier.go index 4034c6a..b08a0e0 100644 --- a/appview/notify/merged_notifier.go +++ b/appview/notify/merged_notifier.go @@ -39,9 +39,9 @@ func (m *mergedNotifier) NewIssue(ctx context.Context, issue *db.Issue) { } } -func (m *mergedNotifier) NewIssueComment(ctx context.Context, comment *db.Comment, mentions []string) { +func (m *mergedNotifier) NewIssueComment(ctx context.Context, repo *db.Repo, issue *db.Issue, comment *db.Comment, mentions []string) { for _, notifier := range m.notifiers { - notifier.NewIssueComment(ctx, comment, mentions) + notifier.NewIssueComment(ctx, repo, issue, comment, mentions) } } diff --git a/appview/notify/notifier.go b/appview/notify/notifier.go index 4d2813b..01a265c 100644 --- a/appview/notify/notifier.go +++ b/appview/notify/notifier.go @@ -13,7 +13,7 @@ type Notifier interface { DeleteStar(ctx context.Context, star *db.Star) NewIssue(ctx context.Context, issue *db.Issue) - NewIssueComment(ctx context.Context, comment *db.Comment, mentions []string) + NewIssueComment(ctx context.Context, repo *db.Repo, issue *db.Issue, comment *db.Comment, mentions []string) NewFollow(ctx context.Context, follow *db.Follow) DeleteFollow(ctx context.Context, follow *db.Follow) @@ -35,7 +35,7 @@ func (m *BaseNotifier) NewStar(ctx context.Context, star *db.Star) {} func (m *BaseNotifier) DeleteStar(ctx context.Context, star *db.Star) {} func (m *BaseNotifier) NewIssue(ctx context.Context, issue *db.Issue) {} -func (m *BaseNotifier) NewIssueComment(ctx context.Context, comment *db.Comment, mentions []string) {} +func (m *BaseNotifier) NewIssueComment(ctx context.Context, repo *db.Repo, issue *db.Issue, comment *db.Comment, mentions []string) {} func (m *BaseNotifier) NewFollow(ctx context.Context, follow *db.Follow) {} func (m *BaseNotifier) DeleteFollow(ctx context.Context, follow *db.Follow) {} diff --git a/appview/posthog/notifier.go b/appview/posthog/notifier.go index 47638da..298524f 100644 --- a/appview/posthog/notifier.go +++ b/appview/posthog/notifier.go @@ -70,7 +70,7 @@ func (n *posthogNotifier) NewIssue(ctx context.Context, issue *db.Issue) { } } -func (n *posthogNotifier) NewIssueComment(ctx context.Context, comment *db.Comment, mentions []string) { +func (n *posthogNotifier) NewIssueComment(ctx context.Context, repo *db.Repo, issue *db.Issue, comment *db.Comment, mentions []string) { err := n.client.Enqueue(posthog.Capture{ DistinctId: comment.OwnerDid, Event: "new_issue", -- 2.43.0 From dfa7134838d5af5281fbfb52e00e55a882919f0f Mon Sep 17 00:00:00 2001 From: Seongmin Lee Date: Fri, 1 Aug 2025 22:15:52 +0900 Subject: [PATCH] appview: email: notify mentioned users on pull-comments Change-Id: ruullntsvqlyylorxwnnrxxszpkulypz Signed-off-by: Seongmin Lee --- appview/email/notifier.go | 77 +++++++++++++++++++++++++------ appview/notify/merged_notifier.go | 4 +- appview/notify/notifier.go | 4 +- appview/posthog/notifier.go | 2 +- appview/pulls/pulls.go | 5 +- 5 files changed, 72 insertions(+), 20 deletions(-) diff --git a/appview/email/notifier.go b/appview/email/notifier.go index fecab44..f9cbd59 100644 --- a/appview/email/notifier.go +++ b/appview/email/notifier.go @@ -34,19 +34,11 @@ func NewEmailNotifier( var _ notify.Notifier = &EmailNotifier{} -func (n *EmailNotifier) buildIssueEmail(ctx context.Context, repo *db.Repo, issue *db.Issue, comment *db.Comment, did string) (Email, error) { - // TODO: check email preferences - email, err := db.GetPrimaryEmail(n.db, did) - if err != nil { - return Email{}, fmt.Errorf("db.GetPrimaryEmail: %w", err) - } - commentOwner, err := n.idResolver.ResolveIdent(ctx, comment.OwnerDid) - if err != nil || commentOwner.Handle.IsInvalidHandle() { - return Email{}, fmt.Errorf("resolve comment owner did: %w", err) - } +// TODO: yeah this is just bad design. should be moved under idResolver ore include repoResolver at first place +func (n *EmailNotifier) repoOwnerSlashName(ctx context.Context, repo *db.Repo) (string, error) { repoOwnerID, err := n.idResolver.ResolveIdent(ctx, repo.Did) if err != nil || repoOwnerID.Handle.IsInvalidHandle() { - return Email{}, fmt.Errorf("resolve repo owner did: %w", err) + return "", fmt.Errorf("resolve comment owner did: %w", err) } repoOwnerHandle := repoOwnerID.Handle var repoOwnerSlashName string @@ -55,8 +47,25 @@ func (n *EmailNotifier) buildIssueEmail(ctx context.Context, repo *db.Repo, issu } else { repoOwnerSlashName = repo.DidSlashRepo() } + return repoOwnerSlashName, nil +} + +func (n *EmailNotifier) buildIssueEmail(ctx context.Context, repo *db.Repo, issue *db.Issue, comment *db.Comment, did string) (Email, error) { + // TODO: check email preferences + email, err := db.GetPrimaryEmail(n.db, did) + if err != nil { + return Email{}, fmt.Errorf("db.GetPrimaryEmail: %w", err) + } + commentOwner, err := n.idResolver.ResolveIdent(ctx, comment.OwnerDid) + if err != nil || commentOwner.Handle.IsInvalidHandle() { + return Email{}, fmt.Errorf("resolve comment owner did: %w", err) + } // TODO: make this configurable baseUrl := "https://tangled.sh" + repoOwnerSlashName, err := n.repoOwnerSlashName(ctx, repo) + if err != nil { + return Email{}, nil + } url := fmt.Sprintf("%s/%s/issues/%d#comment-%d", baseUrl, repoOwnerSlashName, comment.Issue, comment.CommentId) return Email{ APIKey: n.Config.Resend.ApiKey, @@ -67,6 +76,32 @@ func (n *EmailNotifier) buildIssueEmail(ctx context.Context, repo *db.Repo, issu }, nil } +func (n *EmailNotifier) buildPullEmail(ctx context.Context, repo *db.Repo, pull *db.Pull, comment *db.PullComment, did string) (Email, error) { + // TODO: check email preferences + email, err := db.GetPrimaryEmail(n.db, did) + if err != nil { + return Email{}, fmt.Errorf("db.GetPrimaryEmail: %w", err) + } + commentOwner, err := n.idResolver.ResolveIdent(ctx, comment.OwnerDid) + if err != nil || commentOwner.Handle.IsInvalidHandle() { + return Email{}, fmt.Errorf("resolve comment owner did: %w", err) + } + repoOwnerSlashName, err := n.repoOwnerSlashName(ctx, repo) + if err != nil { + return Email{}, nil + } + // TODO: make this configurable + baseUrl := "https://tangled.sh" + url := fmt.Sprintf("%s/%s/pulls/%d#comment-%d", baseUrl, repoOwnerSlashName, comment.PullId, comment.ID) + return Email{ + APIKey: n.Config.Resend.ApiKey, + From: n.Config.Resend.SentFrom, + To: email.Address, + Subject: fmt.Sprintf("[%s] %s (pr#%d)", repoOwnerSlashName, pull.Title, pull.PullId), + Html: fmt.Sprintf(`

@%s mentioned you:

View it on tangled.sh.`, commentOwner.Handle.String(), url), + }, nil +} + func (n *EmailNotifier) NewIssueComment(ctx context.Context, repo *db.Repo, issue *db.Issue, comment *db.Comment, mentions []string) { resolvedIds := n.idResolver.ResolveIdents(ctx, mentions) handleDidMap := make(map[string]string) @@ -85,6 +120,20 @@ func (n *EmailNotifier) NewIssueComment(ctx context.Context, repo *db.Repo, issu } } -// func (n *EmailNotifier) NewPullComment(ctx context.Context, comment *db.PullComment, []string) { -// n.usersMentioned(ctx, mentions) -// } +func (n *EmailNotifier) NewPullComment(ctx context.Context, repo *db.Repo, pull *db.Pull, comment *db.PullComment, mentions []string) { + resolvedIds := n.idResolver.ResolveIdents(ctx, mentions) + handleDidMap := make(map[string]string) + for _, identity := range resolvedIds { + if !identity.Handle.IsInvalidHandle() { + handleDidMap[identity.Handle.String()] = identity.DID.String() + } + } + for _, handle := range mentions { + id, err := n.idResolver.ResolveIdent(ctx, handle) + email, err := n.buildPullEmail(ctx, repo, pull, comment, id.DID.String()) + if err != nil { + log.Println("failed to create issue-email:", err) + } + SendEmail(email) + } +} diff --git a/appview/notify/merged_notifier.go b/appview/notify/merged_notifier.go index b08a0e0..715be13 100644 --- a/appview/notify/merged_notifier.go +++ b/appview/notify/merged_notifier.go @@ -61,9 +61,9 @@ func (m *mergedNotifier) NewPull(ctx context.Context, pull *db.Pull) { notifier.NewPull(ctx, pull) } } -func (m *mergedNotifier) NewPullComment(ctx context.Context, comment *db.PullComment) { +func (m *mergedNotifier) NewPullComment(ctx context.Context, repo *db.Repo, pull *db.Pull, comment *db.PullComment, mentions []string) { for _, notifier := range m.notifiers { - notifier.NewPullComment(ctx, comment) + notifier.NewPullComment(ctx, repo, pull, comment, mentions) } } diff --git a/appview/notify/notifier.go b/appview/notify/notifier.go index 01a265c..2725bb4 100644 --- a/appview/notify/notifier.go +++ b/appview/notify/notifier.go @@ -19,7 +19,7 @@ type Notifier interface { DeleteFollow(ctx context.Context, follow *db.Follow) NewPull(ctx context.Context, pull *db.Pull) - NewPullComment(ctx context.Context, comment *db.PullComment) + NewPullComment(ctx context.Context, repo *db.Repo, pull *db.Pull, comment *db.PullComment, mentions []string) UpdateProfile(ctx context.Context, profile *db.Profile) } @@ -41,6 +41,6 @@ func (m *BaseNotifier) NewFollow(ctx context.Context, follow *db.Follow) {} func (m *BaseNotifier) DeleteFollow(ctx context.Context, follow *db.Follow) {} func (m *BaseNotifier) NewPull(ctx context.Context, pull *db.Pull) {} -func (m *BaseNotifier) NewPullComment(ctx context.Context, comment *db.PullComment) {} +func (m *BaseNotifier) NewPullComment(ctx context.Context, repo *db.Repo, pull *db.Pull, comment *db.PullComment, mentions []string) {} func (m *BaseNotifier) UpdateProfile(ctx context.Context, profile *db.Profile) {} diff --git a/appview/posthog/notifier.go b/appview/posthog/notifier.go index 298524f..b3b1b07 100644 --- a/appview/posthog/notifier.go +++ b/appview/posthog/notifier.go @@ -98,7 +98,7 @@ func (n *posthogNotifier) NewPull(ctx context.Context, pull *db.Pull) { } } -func (n *posthogNotifier) NewPullComment(ctx context.Context, comment *db.PullComment) { +func (n *posthogNotifier) NewPullComment(ctx context.Context, repo *db.Repo, pull *db.Pull, comment *db.PullComment, mentions []string) { err := n.client.Enqueue(posthog.Capture{ DistinctId: comment.OwnerDid, Event: "new_pull_comment", diff --git a/appview/pulls/pulls.go b/appview/pulls/pulls.go index 30dc697..2e64368 100644 --- a/appview/pulls/pulls.go +++ b/appview/pulls/pulls.go @@ -19,6 +19,7 @@ import ( "tangled.sh/tangled.sh/core/appview/notify" "tangled.sh/tangled.sh/core/appview/oauth" "tangled.sh/tangled.sh/core/appview/pages" + "tangled.sh/tangled.sh/core/appview/pages/markup" "tangled.sh/tangled.sh/core/appview/reporesolver" "tangled.sh/tangled.sh/core/idresolver" "tangled.sh/tangled.sh/core/knotclient" @@ -707,7 +708,9 @@ func (s *Pulls) PullComment(w http.ResponseWriter, r *http.Request) { return } - s.notifier.NewPullComment(r.Context(), comment) + mentions := markup.FindUserMentions(comment.Body) + + s.notifier.NewPullComment(r.Context(), &f.Repo, pull, comment, mentions) s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", f.OwnerSlashRepo(), pull.PullId, commentId)) return -- 2.43.0