Mirror of @tangled.org/core. Running on a Raspberry Pi Zero 2 (Please be gentle).

appview: lookup emails to dids/handles in commits

+209 -117
+45 -1
appview/db/email.go
··· 1 1 package db 2 2 3 - import "time" 3 + import ( 4 + "strings" 5 + "time" 6 + ) 4 7 5 8 type Email struct { 6 9 ID int64 ··· 65 62 return "", err 66 63 } 67 64 return did, nil 65 + } 66 + 67 + func GetDidsForEmails(e Execer, ems []string) ([]string, error) { 68 + if len(ems) == 0 { 69 + return []string{}, nil 70 + } 71 + 72 + // Create placeholders for the IN clause 73 + placeholders := make([]string, len(ems)) 74 + args := make([]interface{}, len(ems)) 75 + for i, em := range ems { 76 + placeholders[i] = "?" 77 + args[i] = em 78 + } 79 + 80 + query := ` 81 + select did 82 + from emails 83 + where email in (` + strings.Join(placeholders, ",") + `) 84 + ` 85 + 86 + rows, err := e.Query(query, args...) 87 + if err != nil { 88 + return nil, err 89 + } 90 + defer rows.Close() 91 + 92 + var dids []string 93 + for rows.Next() { 94 + var did string 95 + if err := rows.Scan(&did); err != nil { 96 + return nil, err 97 + } 98 + dids = append(dids, did) 99 + } 100 + 101 + if err := rows.Err(); err != nil { 102 + return nil, err 103 + } 104 + 105 + return dids, nil 68 106 } 69 107 70 108 func GetVerificationCodeForEmail(e Execer, did string, email string) (string, error) {
+5 -2
appview/pages/pages.go
··· 295 295 Active string 296 296 TagMap map[string][]string 297 297 types.RepoIndexResponse 298 - HTMLReadme template.HTML 299 - Raw bool 298 + HTMLReadme template.HTML 299 + Raw bool 300 + EmailToDidOrHandle map[string]string 300 301 } 301 302 302 303 func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error { ··· 329 328 RepoInfo RepoInfo 330 329 types.RepoLogResponse 331 330 Active string 331 + EmailToDidOrHandle map[string]string 332 332 } 333 333 334 334 func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error { ··· 342 340 RepoInfo RepoInfo 343 341 Active string 344 342 types.RepoCommitResponse 343 + EmailToDidOrHandle map[string]string 345 344 } 346 345 347 346 func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error {
+6 -1
appview/pages/templates/repo/commit.html
··· 20 20 21 21 <div class="flex items-center"> 22 22 <p class="text-sm text-gray-500"> 23 - <a href="mailto:{{ $commit.Author.Email }}" class="no-underline hover:underline text-gray-500">{{ $commit.Author.Name }}</a> 23 + {{ if index .EmailToDidOrHandle $commit.Author.Email }} 24 + {{ $handle := index .EmailToDidOrHandle $commit.Author.Email }} 25 + <a href="/@{{ $handle }}" class="no-underline hover:underline text-gray-500">@{{ $handle }}</a> 26 + {{ else }} 27 + <a href="mailto:{{ $commit.Author.Email }}" class="no-underline hover:underline text-gray-500">{{ $commit.Author.Name }}</a> 28 + {{ end }} 24 29 <span class="px-1 select-none before:content-['\00B7']"></span> 25 30 {{ timeFmt $commit.Author.When }} 26 31 <span class="px-1 select-none before:content-['\00B7']"></span>
+3 -2
appview/pages/templates/repo/index.html
··· 164 164 class="mx-2 before:content-['·'] before:select-none" 165 165 ></span> 166 166 <span> 167 + {{ $handle := index $.EmailToDidOrHandle .Author.Email }} 167 168 <a 168 - href="mailto:{{ .Author.Email }}" 169 + href="{{ if $handle }}/@{{ $handle }}{{ else }}mailto:{{ .Author.Email }}{{ end }}" 169 170 class="text-gray-500 no-underline hover:underline" 170 - >{{ .Author.Name }}</a 171 + >{{ if $handle }}@{{ $handle }}{{ else }}{{ .Author.Name }}{{ end }}</a 171 172 > 172 173 </span> 173 174 <div
+18
appview/pages/templates/repo/log.html
··· 25 25 </span> 26 26 <span class="mx-2 before:content-['·'] before:select-none"></span> 27 27 <span> 28 + {{ $handle := index $.EmailToDidOrHandle $commit.Author.Email }} 29 + {{ if $handle }} 30 + <a 31 + href="/@{{ $handle }}" 32 + class="text-gray-500 no-underline hover:underline" 33 + >@{{ $handle }}</a 34 + > 35 + {{ else }} 28 36 <a 29 37 href="mailto:{{ $commit.Author.Email }}" 30 38 class="text-gray-500 no-underline hover:underline" 31 39 >{{ $commit.Author.Name }}</a 32 40 > 41 + {{ end }} 33 42 </span> 34 43 <div 35 44 class="inline-block px-1 select-none after:content-['·']" ··· 105 96 class="mx-2 before:content-['·'] before:select-none" 106 97 ></span> 107 98 <span> 99 + {{ $handle := index $.EmailToDidOrHandle .Author.Email }} 100 + {{ if $handle }} 101 + <a 102 + href="/@{{ $handle }}" 103 + class="text-gray-500 no-underline hover:underline" 104 + >@{{ $handle }}</a 105 + > 106 + {{ else }} 108 107 <a 109 108 href="mailto:{{ .Author.Email }}" 110 109 class="text-gray-500 no-underline hover:underline" 111 110 >{{ .Author.Name }}</a 112 111 > 112 + {{ end }} 113 113 </span> 114 114 <div 115 115 class="inline-block px-1 select-none after:content-['·']"
+4 -10
appview/pages/templates/settings.html
··· 31 31 {{ define "keys" }} 32 32 <h2 class="text-sm font-bold py-2 px-6 uppercase">ssh keys</h2> 33 33 <section class="rounded bg-white drop-shadow-sm px-6 py-4 mb-6 w-full lg:w-fit"> 34 + <p class="mb-8">SSH public keys added here will be broadcasted to knots that you are a member of, <br> allowing you to push to repositories there.</p> 34 35 <div id="key-list" class="flex flex-col gap-6 mb-8"> 35 36 {{ range $index, $key := .PubKeys }} 36 37 <div class="flex justify-between items-center gap-4"> ··· 52 51 </button> 53 52 </div> 54 53 {{ end }} 55 - {{ if .PubKeys }} 56 - <hr class="mb-4" /> 57 - {{ end }} 58 54 </div> 59 - <p class="mb-2">add an ssh key</p> 60 55 <form 61 56 hx-put="/settings/keys" 62 57 hx-swap="none" ··· 73 76 required 74 77 class="w-full"/> 75 78 76 - <button class="btn w-full" type="submit">add key</button> 79 + <button class="btn" type="submit">add key</button> 77 80 78 81 <div id="settings-keys" class="error"></div> 79 82 </form> ··· 83 86 {{ define "emails" }} 84 87 <h2 class="text-sm font-bold py-2 px-6 uppercase">email addresses</h2> 85 88 <section class="rounded bg-white drop-shadow-sm px-6 py-4 mb-6 w-full lg:w-fit"> 89 + <p class="mb-8">Commits authored using emails listed here will be associated with your Tangled profile.</p> 86 90 <div id="email-list" class="flex flex-col gap-6 mb-8"> 87 91 {{ range $index, $email := .Emails }} 88 92 <div class="flex justify-between items-center gap-4"> ··· 127 129 </div> 128 130 </div> 129 131 {{ end }} 130 - {{ if .Emails }} 131 - <hr class="mb-4" /> 132 - {{ end }} 133 132 </div> 134 - <p class="mb-2">add an email address</p> 135 133 <form 136 134 hx-put="/settings/emails" 137 135 hx-swap="none" ··· 141 147 required 142 148 class="w-full"/> 143 149 144 - <button class="btn w-full" type="submit">add email</button> 150 + <button class="btn" type="submit">add email</button> 145 151 146 152 <div id="settings-emails-error" class="error"></div> 147 153 <div id="settings-emails-success" class="success"></div>
+12 -56
appview/state/repo.go
··· 75 75 tagMap[hash] = append(tagMap[hash], branch.Name) 76 76 } 77 77 78 + emails := uniqueEmails(result.Commits) 79 + 78 80 user := s.auth.GetUser(r) 79 81 s.pages.RepoIndexPage(w, pages.RepoIndexParams{ 80 - LoggedInUser: user, 81 - RepoInfo: f.RepoInfo(s, user), 82 - TagMap: tagMap, 83 - RepoIndexResponse: result, 82 + LoggedInUser: user, 83 + RepoInfo: f.RepoInfo(s, user), 84 + TagMap: tagMap, 85 + RepoIndexResponse: result, 86 + EmailToDidOrHandle: EmailToDidOrHandle(s, emails), 84 87 }) 85 - 86 88 return 87 89 } 88 90 ··· 131 129 132 130 user := s.auth.GetUser(r) 133 131 s.pages.RepoLog(w, pages.RepoLogParams{ 134 - LoggedInUser: user, 135 - RepoInfo: f.RepoInfo(s, user), 136 - RepoLogResponse: repolog, 132 + LoggedInUser: user, 133 + RepoInfo: f.RepoInfo(s, user), 134 + RepoLogResponse: repolog, 135 + EmailToDidOrHandle: EmailToDidOrHandle(s, uniqueEmails(repolog.Commits)), 137 136 }) 138 137 return 139 138 } ··· 268 265 LoggedInUser: user, 269 266 RepoInfo: f.RepoInfo(s, user), 270 267 RepoCommitResponse: result, 268 + EmailToDidOrHandle: EmailToDidOrHandle(s, []string{result.Diff.Commit.Author.Email}), 271 269 }) 272 270 return 273 271 } ··· 1071 1067 1072 1068 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueId)) 1073 1069 return 1074 - } 1075 - } 1076 - 1077 - func fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) { 1078 - repoName := chi.URLParam(r, "repo") 1079 - knot, ok := r.Context().Value("knot").(string) 1080 - if !ok { 1081 - log.Println("malformed middleware") 1082 - return nil, fmt.Errorf("malformed middleware") 1083 - } 1084 - id, ok := r.Context().Value("resolvedId").(identity.Identity) 1085 - if !ok { 1086 - log.Println("malformed middleware") 1087 - return nil, fmt.Errorf("malformed middleware") 1088 - } 1089 - 1090 - repoAt, ok := r.Context().Value("repoAt").(string) 1091 - if !ok { 1092 - log.Println("malformed middleware") 1093 - return nil, fmt.Errorf("malformed middleware") 1094 - } 1095 - 1096 - parsedRepoAt, err := syntax.ParseATURI(repoAt) 1097 - if err != nil { 1098 - log.Println("malformed repo at-uri") 1099 - return nil, fmt.Errorf("malformed middleware") 1100 - } 1101 - 1102 - // pass through values from the middleware 1103 - description, ok := r.Context().Value("repoDescription").(string) 1104 - addedAt, ok := r.Context().Value("repoAddedAt").(string) 1105 - 1106 - return &FullyResolvedRepo{ 1107 - Knot: knot, 1108 - OwnerId: id, 1109 - RepoName: repoName, 1110 - RepoAt: parsedRepoAt, 1111 - Description: description, 1112 - AddedAt: addedAt, 1113 - }, nil 1114 - } 1115 - 1116 - func RolesInRepo(s *State, u *auth.User, f *FullyResolvedRepo) pages.RolesInRepo { 1117 - if u != nil { 1118 - r := s.enforcer.GetPermissionsInRepo(u.Did, f.Knot, f.OwnerSlashRepo()) 1119 - return pages.RolesInRepo{r} 1120 - } else { 1121 - return pages.RolesInRepo{} 1122 1070 } 1123 1071 }
+113
appview/state/repo_util.go
··· 1 + package state 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "log" 7 + "net/http" 8 + 9 + "github.com/bluesky-social/indigo/atproto/identity" 10 + "github.com/bluesky-social/indigo/atproto/syntax" 11 + "github.com/go-chi/chi/v5" 12 + "github.com/go-git/go-git/v5/plumbing/object" 13 + "github.com/sotangled/tangled/appview/auth" 14 + "github.com/sotangled/tangled/appview/db" 15 + "github.com/sotangled/tangled/appview/pages" 16 + ) 17 + 18 + func fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) { 19 + repoName := chi.URLParam(r, "repo") 20 + knot, ok := r.Context().Value("knot").(string) 21 + if !ok { 22 + log.Println("malformed middleware") 23 + return nil, fmt.Errorf("malformed middleware") 24 + } 25 + id, ok := r.Context().Value("resolvedId").(identity.Identity) 26 + if !ok { 27 + log.Println("malformed middleware") 28 + return nil, fmt.Errorf("malformed middleware") 29 + } 30 + 31 + repoAt, ok := r.Context().Value("repoAt").(string) 32 + if !ok { 33 + log.Println("malformed middleware") 34 + return nil, fmt.Errorf("malformed middleware") 35 + } 36 + 37 + parsedRepoAt, err := syntax.ParseATURI(repoAt) 38 + if err != nil { 39 + log.Println("malformed repo at-uri") 40 + return nil, fmt.Errorf("malformed middleware") 41 + } 42 + 43 + // pass through values from the middleware 44 + description, ok := r.Context().Value("repoDescription").(string) 45 + addedAt, ok := r.Context().Value("repoAddedAt").(string) 46 + 47 + return &FullyResolvedRepo{ 48 + Knot: knot, 49 + OwnerId: id, 50 + RepoName: repoName, 51 + RepoAt: parsedRepoAt, 52 + Description: description, 53 + AddedAt: addedAt, 54 + }, nil 55 + } 56 + 57 + func RolesInRepo(s *State, u *auth.User, f *FullyResolvedRepo) pages.RolesInRepo { 58 + if u != nil { 59 + r := s.enforcer.GetPermissionsInRepo(u.Did, f.Knot, f.OwnerSlashRepo()) 60 + return pages.RolesInRepo{r} 61 + } else { 62 + return pages.RolesInRepo{} 63 + } 64 + } 65 + 66 + func uniqueEmails(commits []*object.Commit) []string { 67 + emails := make(map[string]struct{}) 68 + for _, commit := range commits { 69 + if commit.Author.Email != "" { 70 + emails[commit.Author.Email] = struct{}{} 71 + } 72 + if commit.Committer.Email != "" { 73 + emails[commit.Committer.Email] = struct{}{} 74 + } 75 + } 76 + var uniqueEmails []string 77 + for email := range emails { 78 + uniqueEmails = append(uniqueEmails, email) 79 + } 80 + return uniqueEmails 81 + } 82 + 83 + func EmailToDidOrHandle(s *State, emails []string) map[string]string { 84 + dids, err := db.GetDidsForEmails(s.db, emails) 85 + if err != nil { 86 + log.Printf("error fetching dids for emails: %v", err) 87 + return nil 88 + } 89 + 90 + didHandleMap := make(map[string]string) 91 + emailToDid := make(map[string]string) 92 + resolvedIdents := s.resolver.ResolveIdents(context.Background(), dids) 93 + for i, resolved := range resolvedIdents { 94 + if resolved != nil { 95 + didHandleMap[dids[i]] = resolved.Handle.String() 96 + if i < len(emails) { 97 + emailToDid[emails[i]] = dids[i] 98 + } 99 + } 100 + } 101 + 102 + // Create map of email to didOrHandle for commit display 103 + emailToDidOrHandle := make(map[string]string) 104 + for email, did := range emailToDid { 105 + if handle, ok := didHandleMap[did]; ok { 106 + emailToDidOrHandle[email] = handle 107 + } else { 108 + emailToDidOrHandle[email] = did 109 + } 110 + } 111 + 112 + return emailToDidOrHandle 113 + }
+3 -45
appview/state/state.go
··· 5 5 "crypto/hmac" 6 6 "crypto/sha256" 7 7 "encoding/hex" 8 - "encoding/json" 9 8 "fmt" 10 9 "log" 11 10 "log/slog" ··· 763 764 followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, ident.DID.String()) 764 765 } 765 766 766 - profileAvatarUri, err := GetAvatarUri(ident.DID.String(), ident.PDSEndpoint()) 767 + profileAvatarUri, err := GetAvatarUri(ident.Handle.String()) 767 768 if err != nil { 768 769 log.Println("failed to fetch bsky avatar", err) 769 770 } ··· 784 785 }) 785 786 } 786 787 787 - func GetAvatarUri(did string, pds string) (string, error) { 788 - recordURL := fmt.Sprintf("%s/xrpc/com.atproto.repo.getRecord?repo=%s&collection=app.bsky.actor.profile&rkey=self", pds, did) 789 - 790 - recordResp, err := http.Get(recordURL) 791 - if err != nil { 792 - return "", err 793 - } 794 - defer recordResp.Body.Close() 795 - 796 - if recordResp.StatusCode != http.StatusOK { 797 - return "", fmt.Errorf("getRecord API returned status code %d", recordResp.StatusCode) 798 - } 799 - 800 - var profileResp map[string]any 801 - if err := json.NewDecoder(recordResp.Body).Decode(&profileResp); err != nil { 802 - return "", err 803 - } 804 - 805 - value, ok := profileResp["value"].(map[string]any) 806 - if !ok { 807 - log.Println(profileResp) 808 - return "", fmt.Errorf("no value found for handle %s", did) 809 - } 810 - 811 - avatar, ok := value["avatar"].(map[string]any) 812 - if !ok { 813 - log.Println(profileResp) 814 - return "", fmt.Errorf("no avatar found for handle %s", did) 815 - } 816 - 817 - blobRef, ok := avatar["ref"].(map[string]any) 818 - if !ok { 819 - log.Println(profileResp) 820 - return "", fmt.Errorf("no ref found for handle %s", did) 821 - } 822 - 823 - link, ok := blobRef["$link"].(string) 824 - if !ok { 825 - log.Println(profileResp) 826 - return "", fmt.Errorf("no link found for handle %s", did) 827 - } 828 - 829 - return fmt.Sprintf("%s/xrpc/com.atproto.sync.getBlob?did=%s&cid=%s", pds, did, link), nil 788 + func GetAvatarUri(handle string) (string, error) { 789 + return fmt.Sprintf("https://avatars.dog/%s@webp", handle), nil 830 790 }