From 1e7b398cff93b67d880db93e10cb4f2429726648 Mon Sep 17 00:00:00 2001 From: Anirudh Oppiliappan Date: Fri, 21 Mar 2025 14:22:52 +0200 Subject: [PATCH] appview: lookup emails to dids/handles in commits --- appview/db/email.go | 46 ++++++++- appview/pages/pages.go | 7 +- appview/pages/templates/repo/commit.html | 7 +- appview/pages/templates/repo/index.html | 5 +- appview/pages/templates/repo/log.html | 18 ++++ appview/pages/templates/settings.html | 14 +-- appview/state/repo.go | 68 +++----------- appview/state/repo_util.go | 113 +++++++++++++++++++++++ appview/state/state.go | 48 +--------- 9 files changed, 209 insertions(+), 117 deletions(-) create mode 100644 appview/state/repo_util.go diff --git a/appview/db/email.go b/appview/db/email.go index 5dbbfe96..1d7fba63 100644 --- a/appview/db/email.go +++ b/appview/db/email.go @@ -1,6 +1,9 @@ package db -import "time" +import ( + "strings" + "time" +) type Email struct { ID int64 @@ -64,6 +67,47 @@ func GetDidForEmail(e Execer, em string) (string, error) { return did, nil } +func GetDidsForEmails(e Execer, ems []string) ([]string, error) { + if len(ems) == 0 { + return []string{}, nil + } + + // Create placeholders for the IN clause + placeholders := make([]string, len(ems)) + args := make([]interface{}, len(ems)) + for i, em := range ems { + placeholders[i] = "?" + args[i] = em + } + + query := ` + select did + from emails + where email in (` + strings.Join(placeholders, ",") + `) + ` + + rows, err := e.Query(query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + var dids []string + for rows.Next() { + var did string + if err := rows.Scan(&did); err != nil { + return nil, err + } + dids = append(dids, did) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return dids, nil +} + func GetVerificationCodeForEmail(e Execer, did string, email string) (string, error) { query := ` select verification_code diff --git a/appview/pages/pages.go b/appview/pages/pages.go index d7b9fee8..b5542e0c 100644 --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -295,8 +295,9 @@ type RepoIndexParams struct { Active string TagMap map[string][]string types.RepoIndexResponse - HTMLReadme template.HTML - Raw bool + HTMLReadme template.HTML + Raw bool + EmailToDidOrHandle map[string]string } func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error { @@ -328,6 +329,7 @@ type RepoLogParams struct { RepoInfo RepoInfo types.RepoLogResponse Active string + EmailToDidOrHandle map[string]string } func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error { @@ -340,6 +342,7 @@ type RepoCommitParams struct { RepoInfo RepoInfo Active string types.RepoCommitResponse + EmailToDidOrHandle map[string]string } func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error { diff --git a/appview/pages/templates/repo/commit.html b/appview/pages/templates/repo/commit.html index 1dfed308..56a2009a 100644 --- a/appview/pages/templates/repo/commit.html +++ b/appview/pages/templates/repo/commit.html @@ -20,7 +20,12 @@

- {{ $commit.Author.Name }} + {{ if index .EmailToDidOrHandle $commit.Author.Email }} + {{ $handle := index .EmailToDidOrHandle $commit.Author.Email }} + @{{ $handle }} + {{ else }} + {{ $commit.Author.Name }} + {{ end }} {{ timeFmt $commit.Author.When }} diff --git a/appview/pages/templates/repo/index.html b/appview/pages/templates/repo/index.html index dfcbb1a0..95eb4aee 100644 --- a/appview/pages/templates/repo/index.html +++ b/appview/pages/templates/repo/index.html @@ -164,10 +164,11 @@ class="mx-2 before:content-['·'] before:select-none" > + {{ $handle := index $.EmailToDidOrHandle .Author.Email }} {{ .Author.Name }}{{ if $handle }}@{{ $handle }}{{ else }}{{ .Author.Name }}{{ end }}

+ {{ $handle := index $.EmailToDidOrHandle $commit.Author.Email }} + {{ if $handle }} + @{{ $handle }} + {{ else }} {{ $commit.Author.Name }} + {{ end }}
+ {{ $handle := index $.EmailToDidOrHandle .Author.Email }} + {{ if $handle }} + @{{ $handle }} + {{ else }} {{ .Author.Name }} + {{ end }}
ssh keys
+

SSH public keys added here will be broadcasted to knots that you are a member of,
allowing you to push to repositories there.

{{ range $index, $key := .PubKeys }}
@@ -51,11 +52,7 @@
{{ end }} - {{ if .PubKeys }} -
- {{ end }}
-

add an ssh key

- +
@@ -86,6 +83,7 @@ {{ define "emails" }}

email addresses

+

Commits authored using emails listed here will be associated with your Tangled profile.

{{ range $index, $email := .Emails }}
@@ -129,11 +127,7 @@
{{ end }} - {{ if .Emails }} -
- {{ end }}
-

add an email address

- +
diff --git a/appview/state/repo.go b/appview/state/repo.go index 1a6e3bf8..419794e1 100644 --- a/appview/state/repo.go +++ b/appview/state/repo.go @@ -75,14 +75,16 @@ func (s *State) RepoIndex(w http.ResponseWriter, r *http.Request) { tagMap[hash] = append(tagMap[hash], branch.Name) } + emails := uniqueEmails(result.Commits) + user := s.auth.GetUser(r) s.pages.RepoIndexPage(w, pages.RepoIndexParams{ - LoggedInUser: user, - RepoInfo: f.RepoInfo(s, user), - TagMap: tagMap, - RepoIndexResponse: result, + LoggedInUser: user, + RepoInfo: f.RepoInfo(s, user), + TagMap: tagMap, + RepoIndexResponse: result, + EmailToDidOrHandle: EmailToDidOrHandle(s, emails), }) - return } @@ -129,9 +131,10 @@ func (s *State) RepoLog(w http.ResponseWriter, r *http.Request) { user := s.auth.GetUser(r) s.pages.RepoLog(w, pages.RepoLogParams{ - LoggedInUser: user, - RepoInfo: f.RepoInfo(s, user), - RepoLogResponse: repolog, + LoggedInUser: user, + RepoInfo: f.RepoInfo(s, user), + RepoLogResponse: repolog, + EmailToDidOrHandle: EmailToDidOrHandle(s, uniqueEmails(repolog.Commits)), }) return } @@ -265,6 +268,7 @@ func (s *State) RepoCommit(w http.ResponseWriter, r *http.Request) { LoggedInUser: user, RepoInfo: f.RepoInfo(s, user), RepoCommitResponse: result, + EmailToDidOrHandle: EmailToDidOrHandle(s, []string{result.Diff.Commit.Author.Email}), }) return } @@ -1069,51 +1073,3 @@ func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) { return } } - -func fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) { - repoName := chi.URLParam(r, "repo") - knot, ok := r.Context().Value("knot").(string) - if !ok { - log.Println("malformed middleware") - return nil, fmt.Errorf("malformed middleware") - } - id, ok := r.Context().Value("resolvedId").(identity.Identity) - if !ok { - log.Println("malformed middleware") - 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") - } - - // pass through values from the middleware - description, ok := r.Context().Value("repoDescription").(string) - addedAt, ok := r.Context().Value("repoAddedAt").(string) - - return &FullyResolvedRepo{ - Knot: knot, - OwnerId: id, - RepoName: repoName, - RepoAt: parsedRepoAt, - Description: description, - AddedAt: addedAt, - }, nil -} - -func RolesInRepo(s *State, u *auth.User, f *FullyResolvedRepo) pages.RolesInRepo { - if u != nil { - r := s.enforcer.GetPermissionsInRepo(u.Did, f.Knot, f.OwnerSlashRepo()) - return pages.RolesInRepo{r} - } else { - return pages.RolesInRepo{} - } -} diff --git a/appview/state/repo_util.go b/appview/state/repo_util.go new file mode 100644 index 00000000..0a280873 --- /dev/null +++ b/appview/state/repo_util.go @@ -0,0 +1,113 @@ +package state + +import ( + "context" + "fmt" + "log" + "net/http" + + "github.com/bluesky-social/indigo/atproto/identity" + "github.com/bluesky-social/indigo/atproto/syntax" + "github.com/go-chi/chi/v5" + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/sotangled/tangled/appview/auth" + "github.com/sotangled/tangled/appview/db" + "github.com/sotangled/tangled/appview/pages" +) + +func fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) { + repoName := chi.URLParam(r, "repo") + knot, ok := r.Context().Value("knot").(string) + if !ok { + log.Println("malformed middleware") + return nil, fmt.Errorf("malformed middleware") + } + id, ok := r.Context().Value("resolvedId").(identity.Identity) + if !ok { + log.Println("malformed middleware") + 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") + } + + // pass through values from the middleware + description, ok := r.Context().Value("repoDescription").(string) + addedAt, ok := r.Context().Value("repoAddedAt").(string) + + return &FullyResolvedRepo{ + Knot: knot, + OwnerId: id, + RepoName: repoName, + RepoAt: parsedRepoAt, + Description: description, + AddedAt: addedAt, + }, nil +} + +func RolesInRepo(s *State, u *auth.User, f *FullyResolvedRepo) pages.RolesInRepo { + if u != nil { + r := s.enforcer.GetPermissionsInRepo(u.Did, f.Knot, f.OwnerSlashRepo()) + return pages.RolesInRepo{r} + } else { + return pages.RolesInRepo{} + } +} + +func uniqueEmails(commits []*object.Commit) []string { + emails := make(map[string]struct{}) + for _, commit := range commits { + if commit.Author.Email != "" { + emails[commit.Author.Email] = struct{}{} + } + if commit.Committer.Email != "" { + emails[commit.Committer.Email] = struct{}{} + } + } + var uniqueEmails []string + for email := range emails { + uniqueEmails = append(uniqueEmails, email) + } + return uniqueEmails +} + +func EmailToDidOrHandle(s *State, emails []string) map[string]string { + dids, err := db.GetDidsForEmails(s.db, emails) + if err != nil { + log.Printf("error fetching dids for emails: %v", err) + return nil + } + + didHandleMap := make(map[string]string) + emailToDid := make(map[string]string) + resolvedIdents := s.resolver.ResolveIdents(context.Background(), dids) + for i, resolved := range resolvedIdents { + if resolved != nil { + didHandleMap[dids[i]] = resolved.Handle.String() + if i < len(emails) { + emailToDid[emails[i]] = dids[i] + } + } + } + + // Create map of email to didOrHandle for commit display + emailToDidOrHandle := make(map[string]string) + for email, did := range emailToDid { + if handle, ok := didHandleMap[did]; ok { + emailToDidOrHandle[email] = handle + } else { + emailToDidOrHandle[email] = did + } + } + + return emailToDidOrHandle +} diff --git a/appview/state/state.go b/appview/state/state.go index 99253aa0..3cd606ee 100644 --- a/appview/state/state.go +++ b/appview/state/state.go @@ -5,7 +5,6 @@ import ( "crypto/hmac" "crypto/sha256" "encoding/hex" - "encoding/json" "fmt" "log" "log/slog" @@ -764,7 +763,7 @@ func (s *State) ProfilePage(w http.ResponseWriter, r *http.Request) { followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, ident.DID.String()) } - profileAvatarUri, err := GetAvatarUri(ident.DID.String(), ident.PDSEndpoint()) + profileAvatarUri, err := GetAvatarUri(ident.Handle.String()) if err != nil { log.Println("failed to fetch bsky avatar", err) } @@ -785,47 +784,6 @@ func (s *State) ProfilePage(w http.ResponseWriter, r *http.Request) { }) } -func GetAvatarUri(did string, pds string) (string, error) { - recordURL := fmt.Sprintf("%s/xrpc/com.atproto.repo.getRecord?repo=%s&collection=app.bsky.actor.profile&rkey=self", pds, did) - - recordResp, err := http.Get(recordURL) - if err != nil { - return "", err - } - defer recordResp.Body.Close() - - if recordResp.StatusCode != http.StatusOK { - return "", fmt.Errorf("getRecord API returned status code %d", recordResp.StatusCode) - } - - var profileResp map[string]any - if err := json.NewDecoder(recordResp.Body).Decode(&profileResp); err != nil { - return "", err - } - - value, ok := profileResp["value"].(map[string]any) - if !ok { - log.Println(profileResp) - return "", fmt.Errorf("no value found for handle %s", did) - } - - avatar, ok := value["avatar"].(map[string]any) - if !ok { - log.Println(profileResp) - return "", fmt.Errorf("no avatar found for handle %s", did) - } - - blobRef, ok := avatar["ref"].(map[string]any) - if !ok { - log.Println(profileResp) - return "", fmt.Errorf("no ref found for handle %s", did) - } - - link, ok := blobRef["$link"].(string) - if !ok { - log.Println(profileResp) - return "", fmt.Errorf("no link found for handle %s", did) - } - - return fmt.Sprintf("%s/xrpc/com.atproto.sync.getBlob?did=%s&cid=%s", pds, did, link), nil +func GetAvatarUri(handle string) (string, error) { + return fmt.Sprintf("https://avatars.dog/%s@webp", handle), nil } -- 2.54.0