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