From e33a63e748e520cf54a2b6289d4c895d9810dc38 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 | 91 ++++++++++++++++---------------- appview/reporesolver/resolver.go | 83 +++++++++-------------------- 7 files changed, 124 insertions(+), 163 deletions(-) diff --git a/appview/issues/issues.go b/appview/issues/issues.go index 7693078..62ee292 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.") @@ -127,7 +127,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.") @@ -171,7 +171,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.") @@ -203,7 +203,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.") @@ -220,7 +220,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.") @@ -264,7 +264,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, @@ -279,14 +279,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) @@ -343,14 +343,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 @@ -388,14 +388,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 @@ -506,7 +506,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.") @@ -521,7 +521,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 @@ -539,7 +539,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") @@ -603,7 +603,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.") @@ -650,7 +650,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, @@ -668,7 +668,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, @@ -689,7 +689,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 b5532e3..64175bb 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" @@ -214,11 +213,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)) }) } @@ -243,7 +238,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 @@ -284,7 +279,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 ddff2e6..15e4962 100644 --- a/appview/pulls/pulls.go +++ b/appview/pulls/pulls.go @@ -246,7 +246,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{ @@ -307,7 +307,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) @@ -483,7 +483,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 { @@ -610,14 +610,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) @@ -646,7 +646,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, @@ -692,7 +692,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 @@ -816,7 +816,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()) @@ -1007,7 +1007,7 @@ func (s *Pulls) createPullRequest( Body: body, TargetBranch: targetBranch, OwnerDid: user.Did, - RepoAt: f.RepoAt, + RepoAt: f.RepoAt(), Rkey: rkey, Submissions: []*db.PullSubmission{ &initialSubmission, @@ -1020,7 +1020,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.") @@ -1035,7 +1035,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, @@ -1213,7 +1213,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 @@ -1297,7 +1297,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 @@ -1413,7 +1413,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()) @@ -1597,7 +1597,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, @@ -1924,7 +1924,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.") @@ -1946,7 +1946,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.") @@ -1962,7 +1962,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) { @@ -2014,7 +2014,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.") @@ -2082,7 +2082,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.") @@ -2134,7 +2134,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 cd6475a..a68ab5d 100644 --- a/appview/repo/repo.go +++ b/appview/repo/repo.go @@ -95,7 +95,7 @@ func (rp *Repo) DownloadArchive(w http.ResponseWriter, r *http.Request) { } else { uri = "https" } - url := fmt.Sprintf("%s://%s/%s/%s/archive/%s.tar.gz", uri, f.Knot, f.OwnerDid(), f.RepoName, url.PathEscape(refParam)) + url := fmt.Sprintf("%s://%s/%s/%s/archive/%s.tar.gz", uri, f.Knot, f.OwnerDid(), f.Name, url.PathEscape(refParam)) http.Redirect(w, r, url, http.StatusFound) } @@ -123,13 +123,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 @@ -144,7 +144,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 @@ -212,7 +212,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) @@ -262,9 +262,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, }, @@ -310,7 +310,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 @@ -375,7 +375,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 @@ -405,7 +405,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)}) @@ -436,13 +436,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 @@ -493,7 +493,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 @@ -522,7 +522,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 @@ -542,7 +542,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)}) @@ -575,7 +575,7 @@ func (rp *Repo) RepoBlob(w http.ResponseWriter, r *http.Request) { // fetch the actual binary content like in RepoBlobRaw - blobURL := fmt.Sprintf("%s://%s/%s/%s/raw/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath) + blobURL := fmt.Sprintf("%s://%s/%s/%s/raw/%s/%s", protocol, f.Knot, f.OwnerDid(), f.Name, ref, filePath) contentSrc = blobURL if !rp.config.Core.Dev { contentSrc = markup.GenerateCamoURL(rp.config.Camo.Host, rp.config.Camo.SharedSecret, blobURL) @@ -612,7 +612,7 @@ func (rp *Repo) RepoBlobRaw(w http.ResponseWriter, r *http.Request) { if !rp.config.Core.Dev { protocol = "https" } - blobURL := fmt.Sprintf("%s://%s/%s/%s/raw/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath) + blobURL := fmt.Sprintf("%s://%s/%s/%s/raw/%s/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref, filePath) resp, err := http.Get(blobURL) if err != nil { log.Println("failed to reach knotserver:", err) @@ -668,7 +668,7 @@ func (rp *Repo) EditSpindle(w http.ResponseWriter, r *http.Request) { return } - repoAt := f.RepoAt + repoAt := f.RepoAt() rkey := repoAt.RecordKey().String() if rkey == "" { fail("Failed to resolve repo. Try again later", err) @@ -722,9 +722,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: spindlePtr, }, @@ -805,7 +805,7 @@ func (rp *Repo) AddCollaborator(w http.ResponseWriter, r *http.Request) { Record: &lexutil.LexiconTypeDecoder{ Val: &tangled.RepoCollaborator{ Subject: collaboratorIdent.DID.String(), - Repo: string(f.RepoAt), + Repo: string(f.RepoAt()), CreatedAt: createdAt.Format(time.RFC3339), }}, }) @@ -830,7 +830,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 { fail("Knot was unreachable.", err) return @@ -864,7 +864,7 @@ func (rp *Repo) AddCollaborator(w http.ResponseWriter, r *http.Request) { Did: syntax.DID(currentUser.Did), Rkey: rkey, SubjectDid: collaboratorIdent.DID, - RepoAt: f.RepoAt, + RepoAt: f.RepoAt(), Created: createdAt, }) if err != nil { @@ -902,18 +902,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 { @@ -927,7 +926,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 @@ -973,7 +972,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 @@ -1022,7 +1021,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 @@ -1089,7 +1088,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, }, @@ -1107,7 +1106,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, }, ) @@ -1169,7 +1168,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 @@ -1191,7 +1190,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 @@ -1220,7 +1219,7 @@ func (rp *Repo) generalSettings(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 @@ -1273,7 +1272,7 @@ func (rp *Repo) pipelineSettings(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 @@ -1341,8 +1340,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 { @@ -1392,11 +1391,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 @@ -1427,8 +1426,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{ @@ -1548,7 +1547,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) @@ -1578,7 +1577,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) @@ -1640,21 +1639,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 c622c29..bf8b8bb 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 { @@ -187,28 +153,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 @@ -233,9 +200,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 { @@ -246,8 +213,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 26adbe09fe970f81056552b78c5672b0902a2a3b 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 61e5029..9c82b20 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 15e4962..804baa0 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" @@ -918,12 +917,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, @@ -931,7 +926,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 a68ab5d..40c530a 100644 --- a/appview/repo/repo.go +++ b/appview/repo/repo.go @@ -1496,7 +1496,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 e2df9ee..03f70fd 100644 --- a/appview/state/state.go +++ b/appview/state/state.go @@ -381,7 +381,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 0a65a3f9c387c4396f8ba46c27c25e721626ce55 Mon Sep 17 00:00:00 2001 From: Seongmin Lee Date: Mon, 11 Aug 2025 22:43:25 +0900 Subject: [PATCH] appview: db/repos: remove `AtUri` from `Repo` Change-Id: vmuzrnrlmtrytntmzukwrxmzspmylouw sqlite db still has `at_uri` column to easily use as reference Signed-off-by: Seongmin Lee --- appview/db/repos.go | 22 +++++++++------------- appview/db/star.go | 4 +--- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/appview/db/repos.go b/appview/db/repos.go index 9c82b20..dfd0c25 100644 --- a/appview/db/repos.go +++ b/appview/db/repos.go @@ -19,7 +19,6 @@ type Repo struct { Knot string Rkey string Created time.Time - AtUri string Description string Spindle string @@ -391,7 +390,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, rkey + select did, name, knot, created, description, spindle, rkey from repos where did = ? and name = ? `, @@ -400,7 +399,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, &repo.Rkey); err != nil { + if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &description, &spindle, &repo.Rkey); err != nil { return nil, err } createdAtTime, _ := time.Parse(time.RFC3339, createdAt) @@ -421,10 +420,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, rkey, description from repos where at_uri = ?`, atUri) + row := e.QueryRow(`select did, name, knot, created, rkey, description from repos where at_uri = ?`, atUri) var createdAt string - if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &repo.Rkey, &nullableDescription); err != nil { + if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.Rkey, &nullableDescription); err != nil { return nil, err } createdAtTime, _ := time.Parse(time.RFC3339, createdAt) @@ -440,14 +439,11 @@ 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) values (?, ?, ?, ?, ?, ?, ?)`, - repo.Did, repo.Name, repo.Knot, repo.Rkey, repo.AtUri, repo.Description, repo.Source, + repo.Did, repo.Name, repo.Knot, repo.Rkey, repo.RepoAt().String(), repo.Description, repo.Source, ) return err } @@ -470,7 +466,7 @@ func GetForksByDid(e Execer, did string) ([]Repo, error) { var repos []Repo rows, err := e.Query( - `select did, name, knot, rkey, description, created, at_uri, source + `select did, name, knot, rkey, description, created, source from repos where did = ? and source is not null and source != '' order by created desc`, @@ -487,7 +483,7 @@ func GetForksByDid(e Execer, did string) ([]Repo, error) { var nullableDescription sql.NullString var nullableSource sql.NullString - err := rows.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repo.AtUri, &nullableSource) + err := rows.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &nullableSource) if err != nil { return nil, err } @@ -524,13 +520,13 @@ func GetForkByDid(e Execer, did string, name string) (*Repo, error) { var nullableSource sql.NullString row := e.QueryRow( - `select did, name, knot, rkey, description, created, at_uri, source + `select did, name, knot, rkey, description, created, source from repos where did = ? and name = ? and source is not null and source != ''`, did, name, ) - err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repo.AtUri, &nullableSource) + err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &nullableSource) if err != nil { return nil, err } diff --git a/appview/db/star.go b/appview/db/star.go index 1b23943..7a005f3 100644 --- a/appview/db/star.go +++ b/appview/db/star.go @@ -196,8 +196,7 @@ func GetAllStars(e Execer, limit int) ([]Star, error) { r.name, r.knot, r.rkey, - r.created, - r.at_uri + r.created from stars s join repos r on s.repo_at = r.at_uri `) @@ -222,7 +221,6 @@ func GetAllStars(e Execer, limit int) ([]Star, error) { &repo.Knot, &repo.Rkey, &repoCreatedAt, - &repo.AtUri, ); err != nil { return nil, err } -- 2.43.0 From c247aea0397627925421c82bdadf7a2cdec8a6d0 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 | 8 +- appview/pages/markup/markdown_at_extension.go | 134 ++++++++++++++++++ 2 files changed, 141 insertions(+), 1 deletion(-) 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 223f860..e0bca18 100644 --- a/appview/pages/markup/markdown.go +++ b/appview/pages/markup/markdown.go @@ -54,7 +54,7 @@ type Sanitizer struct { defaultPolicy *bluemonday.Policy } -func (rctx *RenderContext) RenderMarkdown(source string) string { +func NewMarkdown() goldmark.Markdown { md := goldmark.New( goldmark.WithExtensions( extension.GFM, @@ -65,12 +65,18 @@ func (rctx *RenderContext) RenderMarkdown(source string) string { ), highlighting.WithCustomStyle(styles.Get("catppuccin-latte")), ), + 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 869804c1bed6c71a039fc93b3a8a1e8a1a0b3b07 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 | 35 +++++++++++++++++-------------- appview/notify/merged_notifier.go | 6 ++++++ appview/notify/notifier.go | 2 ++ appview/posthog/notifier.go | 14 +++++++++++++ 4 files changed, 41 insertions(+), 16 deletions(-) diff --git a/appview/issues/issues.go b/appview/issues/issues.go index 62ee292..d8a40a8 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" @@ -259,17 +260,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.") @@ -277,16 +277,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) + 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 } - 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) @@ -296,13 +295,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, + Issue: issue.IssueAt, CommentId: &commentIdInt64, - Owner: &ownerDid, + Owner: &comment.OwnerDid, Body: body, CreatedAt: createdAt, }, @@ -314,7 +313,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(), &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 faf0a11..b08a0e0 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, repo *db.Repo, issue *db.Issue, comment *db.Comment, mentions []string) { + for _, notifier := range m.notifiers { + notifier.NewIssueComment(ctx, repo, issue, 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..01a265c 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, 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) @@ -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, 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 8dbd198..298524f 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, repo *db.Repo, issue *db.Issue, 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 1b57d9e61235d1d9e2e83aa05b8bd3e876462b32 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 | 99 +++++++++++++++++++++++++++++++++++++++ appview/state/state.go | 2 + 2 files changed, 101 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..f4bd5a1 --- /dev/null +++ b/appview/email/notifier.go @@ -0,0 +1,99 @@ +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{} + +// 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 "", fmt.Errorf("resolve comment 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 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, + 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:

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) + 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, repo, issue, 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 03f70fd..a5082a4 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" @@ -133,6 +134,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, posthogService.NewPosthogNotifier(posthog)) } -- 2.43.0 From c154972c51c02748a8d6361b1d45f99c229d0dd4 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 | 46 +++++++++++++++++++++++++++++-- appview/notify/merged_notifier.go | 4 +-- appview/notify/notifier.go | 4 +-- appview/posthog/notifier.go | 2 +- appview/pulls/pulls.go | 5 +++- 5 files changed, 52 insertions(+), 9 deletions(-) diff --git a/appview/email/notifier.go b/appview/email/notifier.go index f4bd5a1..f9cbd59 100644 --- a/appview/email/notifier.go +++ b/appview/email/notifier.go @@ -76,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) @@ -94,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 804baa0..998b401 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" @@ -667,7 +668,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