tangled
alpha
login
or
join now
tjh.dev
/
test
forked from
tangled.org/core
0
fork
atom
Mirror of @tangled.org/core. Running on a Raspberry Pi Zero 2 (Please be gentle).
0
fork
atom
overview
issues
pulls
pipelines
appview: lookup emails to dids/handles in commits
anirudh.fi
1 year 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
reviewed
···
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
···
65
62
return "", err
66
63
}
67
64
return did, nil
65
65
+
}
66
66
+
67
67
+
func GetDidsForEmails(e Execer, ems []string) ([]string, error) {
68
68
+
if len(ems) == 0 {
69
69
+
return []string{}, nil
70
70
+
}
71
71
+
72
72
+
// Create placeholders for the IN clause
73
73
+
placeholders := make([]string, len(ems))
74
74
+
args := make([]interface{}, len(ems))
75
75
+
for i, em := range ems {
76
76
+
placeholders[i] = "?"
77
77
+
args[i] = em
78
78
+
}
79
79
+
80
80
+
query := `
81
81
+
select did
82
82
+
from emails
83
83
+
where email in (` + strings.Join(placeholders, ",") + `)
84
84
+
`
85
85
+
86
86
+
rows, err := e.Query(query, args...)
87
87
+
if err != nil {
88
88
+
return nil, err
89
89
+
}
90
90
+
defer rows.Close()
91
91
+
92
92
+
var dids []string
93
93
+
for rows.Next() {
94
94
+
var did string
95
95
+
if err := rows.Scan(&did); err != nil {
96
96
+
return nil, err
97
97
+
}
98
98
+
dids = append(dids, did)
99
99
+
}
100
100
+
101
101
+
if err := rows.Err(); err != nil {
102
102
+
return nil, err
103
103
+
}
104
104
+
105
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
reviewed
···
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 {
···
329
328
RepoInfo RepoInfo
330
329
types.RepoLogResponse
331
330
Active string
331
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
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
reviewed
···
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
reviewed
···
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
reviewed
···
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-['·']"
···
105
96
class="mx-2 before:content-['·'] before:select-none"
106
97
></span>
107
98
<span>
99
99
+
{{ $handle := index $.EmailToDidOrHandle .Author.Email }}
100
100
+
{{ if $handle }}
101
101
+
<a
102
102
+
href="/@{{ $handle }}"
103
103
+
class="text-gray-500 no-underline hover:underline"
104
104
+
>@{{ $handle }}</a
105
105
+
>
106
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
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
reviewed
···
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">
···
52
51
</button>
53
52
</div>
54
53
{{ end }}
55
55
-
{{ if .PubKeys }}
56
56
-
<hr class="mb-4" />
57
57
-
{{ end }}
58
54
</div>
59
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
76
-
<button class="btn w-full" type="submit">add key</button>
79
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
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
130
-
{{ if .Emails }}
131
131
-
<hr class="mb-4" />
132
132
-
{{ end }}
133
132
</div>
134
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
144
-
<button class="btn w-full" type="submit">add email</button>
150
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
reviewed
···
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
···
131
129
132
130
user := s.auth.GetUser(r)
133
131
s.pages.RepoLog(w, pages.RepoLogParams{
134
134
-
LoggedInUser: user,
135
135
-
RepoInfo: f.RepoInfo(s, user),
136
136
-
RepoLogResponse: repolog,
132
132
+
LoggedInUser: user,
133
133
+
RepoInfo: f.RepoInfo(s, user),
134
134
+
RepoLogResponse: repolog,
135
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
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
1074
-
}
1075
1075
-
}
1076
1076
-
1077
1077
-
func fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) {
1078
1078
-
repoName := chi.URLParam(r, "repo")
1079
1079
-
knot, ok := r.Context().Value("knot").(string)
1080
1080
-
if !ok {
1081
1081
-
log.Println("malformed middleware")
1082
1082
-
return nil, fmt.Errorf("malformed middleware")
1083
1083
-
}
1084
1084
-
id, ok := r.Context().Value("resolvedId").(identity.Identity)
1085
1085
-
if !ok {
1086
1086
-
log.Println("malformed middleware")
1087
1087
-
return nil, fmt.Errorf("malformed middleware")
1088
1088
-
}
1089
1089
-
1090
1090
-
repoAt, ok := r.Context().Value("repoAt").(string)
1091
1091
-
if !ok {
1092
1092
-
log.Println("malformed middleware")
1093
1093
-
return nil, fmt.Errorf("malformed middleware")
1094
1094
-
}
1095
1095
-
1096
1096
-
parsedRepoAt, err := syntax.ParseATURI(repoAt)
1097
1097
-
if err != nil {
1098
1098
-
log.Println("malformed repo at-uri")
1099
1099
-
return nil, fmt.Errorf("malformed middleware")
1100
1100
-
}
1101
1101
-
1102
1102
-
// pass through values from the middleware
1103
1103
-
description, ok := r.Context().Value("repoDescription").(string)
1104
1104
-
addedAt, ok := r.Context().Value("repoAddedAt").(string)
1105
1105
-
1106
1106
-
return &FullyResolvedRepo{
1107
1107
-
Knot: knot,
1108
1108
-
OwnerId: id,
1109
1109
-
RepoName: repoName,
1110
1110
-
RepoAt: parsedRepoAt,
1111
1111
-
Description: description,
1112
1112
-
AddedAt: addedAt,
1113
1113
-
}, nil
1114
1114
-
}
1115
1115
-
1116
1116
-
func RolesInRepo(s *State, u *auth.User, f *FullyResolvedRepo) pages.RolesInRepo {
1117
1117
-
if u != nil {
1118
1118
-
r := s.enforcer.GetPermissionsInRepo(u.Did, f.Knot, f.OwnerSlashRepo())
1119
1119
-
return pages.RolesInRepo{r}
1120
1120
-
} else {
1121
1121
-
return pages.RolesInRepo{}
1122
1070
}
1123
1071
}
+113
appview/state/repo_util.go
reviewed
···
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
reviewed
···
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"
···
763
764
followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, ident.DID.String())
764
765
}
765
766
766
766
-
profileAvatarUri, err := GetAvatarUri(ident.DID.String(), ident.PDSEndpoint())
767
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
787
-
func GetAvatarUri(did string, pds string) (string, error) {
788
788
-
recordURL := fmt.Sprintf("%s/xrpc/com.atproto.repo.getRecord?repo=%s&collection=app.bsky.actor.profile&rkey=self", pds, did)
789
789
-
790
790
-
recordResp, err := http.Get(recordURL)
791
791
-
if err != nil {
792
792
-
return "", err
793
793
-
}
794
794
-
defer recordResp.Body.Close()
795
795
-
796
796
-
if recordResp.StatusCode != http.StatusOK {
797
797
-
return "", fmt.Errorf("getRecord API returned status code %d", recordResp.StatusCode)
798
798
-
}
799
799
-
800
800
-
var profileResp map[string]any
801
801
-
if err := json.NewDecoder(recordResp.Body).Decode(&profileResp); err != nil {
802
802
-
return "", err
803
803
-
}
804
804
-
805
805
-
value, ok := profileResp["value"].(map[string]any)
806
806
-
if !ok {
807
807
-
log.Println(profileResp)
808
808
-
return "", fmt.Errorf("no value found for handle %s", did)
809
809
-
}
810
810
-
811
811
-
avatar, ok := value["avatar"].(map[string]any)
812
812
-
if !ok {
813
813
-
log.Println(profileResp)
814
814
-
return "", fmt.Errorf("no avatar found for handle %s", did)
815
815
-
}
816
816
-
817
817
-
blobRef, ok := avatar["ref"].(map[string]any)
818
818
-
if !ok {
819
819
-
log.Println(profileResp)
820
820
-
return "", fmt.Errorf("no ref found for handle %s", did)
821
821
-
}
822
822
-
823
823
-
link, ok := blobRef["$link"].(string)
824
824
-
if !ok {
825
825
-
log.Println(profileResp)
826
826
-
return "", fmt.Errorf("no link found for handle %s", did)
827
827
-
}
828
828
-
829
829
-
return fmt.Sprintf("%s/xrpc/com.atproto.sync.getBlob?did=%s&cid=%s", pds, did, link), nil
788
788
+
func GetAvatarUri(handle string) (string, error) {
789
789
+
return fmt.Sprintf("https://avatars.dog/%s@webp", handle), nil
830
790
}