forked from tangled.org/core
Monorepo for Tangled — https://tangled.org

appview: lookup emails to dids/handles in commits

Changed files
+209 -117
appview
+45 -1
appview/db/email.go
···
package db
-
import "time"
+
import (
+
"strings"
+
"time"
+
)
type Email struct {
ID int64
···
return "", err
}
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) {
+5 -2
appview/pages/pages.go
···
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 {
···
RepoInfo RepoInfo
types.RepoLogResponse
Active string
+
EmailToDidOrHandle map[string]string
}
func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error {
···
RepoInfo RepoInfo
Active string
types.RepoCommitResponse
+
EmailToDidOrHandle map[string]string
}
func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error {
+6 -1
appview/pages/templates/repo/commit.html
···
<div class="flex items-center">
<p class="text-sm text-gray-500">
-
<a href="mailto:{{ $commit.Author.Email }}" class="no-underline hover:underline text-gray-500">{{ $commit.Author.Name }}</a>
+
{{ if index .EmailToDidOrHandle $commit.Author.Email }}
+
{{ $handle := index .EmailToDidOrHandle $commit.Author.Email }}
+
<a href="/@{{ $handle }}" class="no-underline hover:underline text-gray-500">@{{ $handle }}</a>
+
{{ else }}
+
<a href="mailto:{{ $commit.Author.Email }}" class="no-underline hover:underline text-gray-500">{{ $commit.Author.Name }}</a>
+
{{ end }}
<span class="px-1 select-none before:content-['\00B7']"></span>
{{ timeFmt $commit.Author.When }}
<span class="px-1 select-none before:content-['\00B7']"></span>
+3 -2
appview/pages/templates/repo/index.html
···
class="mx-2 before:content-['·'] before:select-none"
></span>
<span>
+
{{ $handle := index $.EmailToDidOrHandle .Author.Email }}
<a
-
href="mailto:{{ .Author.Email }}"
+
href="{{ if $handle }}/@{{ $handle }}{{ else }}mailto:{{ .Author.Email }}{{ end }}"
class="text-gray-500 no-underline hover:underline"
-
>{{ .Author.Name }}</a
+
>{{ if $handle }}@{{ $handle }}{{ else }}{{ .Author.Name }}{{ end }}</a
>
</span>
<div
+18
appview/pages/templates/repo/log.html
···
</span>
<span class="mx-2 before:content-['·'] before:select-none"></span>
<span>
+
{{ $handle := index $.EmailToDidOrHandle $commit.Author.Email }}
+
{{ if $handle }}
+
<a
+
href="/@{{ $handle }}"
+
class="text-gray-500 no-underline hover:underline"
+
>@{{ $handle }}</a
+
>
+
{{ else }}
<a
href="mailto:{{ $commit.Author.Email }}"
class="text-gray-500 no-underline hover:underline"
>{{ $commit.Author.Name }}</a
>
+
{{ end }}
</span>
<div
class="inline-block px-1 select-none after:content-['·']"
···
class="mx-2 before:content-['·'] before:select-none"
></span>
<span>
+
{{ $handle := index $.EmailToDidOrHandle .Author.Email }}
+
{{ if $handle }}
+
<a
+
href="/@{{ $handle }}"
+
class="text-gray-500 no-underline hover:underline"
+
>@{{ $handle }}</a
+
>
+
{{ else }}
<a
href="mailto:{{ .Author.Email }}"
class="text-gray-500 no-underline hover:underline"
>{{ .Author.Name }}</a
>
+
{{ end }}
</span>
<div
class="inline-block px-1 select-none after:content-['·']"
+4 -10
appview/pages/templates/settings.html
···
{{ define "keys" }}
<h2 class="text-sm font-bold py-2 px-6 uppercase">ssh keys</h2>
<section class="rounded bg-white drop-shadow-sm px-6 py-4 mb-6 w-full lg:w-fit">
+
<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>
<div id="key-list" class="flex flex-col gap-6 mb-8">
{{ range $index, $key := .PubKeys }}
<div class="flex justify-between items-center gap-4">
···
<i class="w-5 h-5" data-lucide="trash-2"></i>
</button>
</div>
-
{{ end }}
-
{{ if .PubKeys }}
-
<hr class="mb-4" />
{{ end }}
</div>
-
<p class="mb-2">add an ssh key</p>
<form
hx-put="/settings/keys"
hx-swap="none"
···
required
class="w-full"/>
-
<button class="btn w-full" type="submit">add key</button>
+
<button class="btn" type="submit">add key</button>
<div id="settings-keys" class="error"></div>
</form>
···
{{ define "emails" }}
<h2 class="text-sm font-bold py-2 px-6 uppercase">email addresses</h2>
<section class="rounded bg-white drop-shadow-sm px-6 py-4 mb-6 w-full lg:w-fit">
+
<p class="mb-8">Commits authored using emails listed here will be associated with your Tangled profile.</p>
<div id="email-list" class="flex flex-col gap-6 mb-8">
{{ range $index, $email := .Emails }}
<div class="flex justify-between items-center gap-4">
···
</div>
</div>
{{ end }}
-
{{ if .Emails }}
-
<hr class="mb-4" />
-
{{ end }}
</div>
-
<p class="mb-2">add an email address</p>
<form
hx-put="/settings/emails"
hx-swap="none"
···
required
class="w-full"/>
-
<button class="btn w-full" type="submit">add email</button>
+
<button class="btn" type="submit">add email</button>
<div id="settings-emails-error" class="error"></div>
<div id="settings-emails-success" class="success"></div>
+12 -56
appview/state/repo.go
···
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
}
···
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
}
···
LoggedInUser: user,
RepoInfo: f.RepoInfo(s, user),
RepoCommitResponse: result,
+
EmailToDidOrHandle: EmailToDidOrHandle(s, []string{result.Diff.Commit.Author.Email}),
})
return
}
···
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{}
-
}
-
}
+113
appview/state/repo_util.go
···
+
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
+
}
+3 -45
appview/state/state.go
···
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
-
"encoding/json"
"fmt"
"log"
"log/slog"
···
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)
}
···
})
}
-
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
}