Signed-off-by: oppiliappan me@oppi.li
+377
-39
Diff
round #1
+19
-18
appview/oauth/scopes.go
+19
-18
appview/oauth/scopes.go
···
3
3
var TangledScopes = []string{
4
4
"atproto",
5
5
6
+
"repo:sh.tangled.actor.profile",
7
+
"repo:sh.tangled.feed.reaction",
8
+
"repo:sh.tangled.feed.star",
9
+
"repo:sh.tangled.graph.follow",
10
+
"repo:sh.tangled.graph.vouch",
11
+
"repo:sh.tangled.knot",
12
+
"repo:sh.tangled.knot.member",
13
+
"repo:sh.tangled.label.definition",
14
+
"repo:sh.tangled.label.op",
6
15
"repo:sh.tangled.publicKey",
7
16
"repo:sh.tangled.repo",
8
-
"repo:sh.tangled.repo.pull",
9
-
"repo:sh.tangled.repo.pull.comment",
10
17
"repo:sh.tangled.repo.artifact",
18
+
"repo:sh.tangled.repo.collaborator",
11
19
"repo:sh.tangled.repo.issue",
12
20
"repo:sh.tangled.repo.issue.comment",
13
-
"repo:sh.tangled.repo.collaborator",
14
-
"repo:sh.tangled.knot",
15
-
"repo:sh.tangled.knot.member",
21
+
"repo:sh.tangled.repo.pull",
22
+
"repo:sh.tangled.repo.pull.comment",
16
23
"repo:sh.tangled.spindle",
17
24
"repo:sh.tangled.spindle.member",
18
-
"repo:sh.tangled.graph.follow",
19
-
"repo:sh.tangled.feed.star",
20
-
"repo:sh.tangled.feed.reaction",
21
-
"repo:sh.tangled.label.definition",
22
-
"repo:sh.tangled.label.op",
23
25
"repo:sh.tangled.string",
24
-
"repo:sh.tangled.actor.profile",
25
26
26
27
"blob:*/*",
27
28
29
+
"rpc:sh.tangled.pipeline.cancelPipeline?aud=*",
30
+
"rpc:sh.tangled.repo.addSecret?aud=*",
28
31
"rpc:sh.tangled.repo.create?aud=*",
29
32
"rpc:sh.tangled.repo.delete?aud=*",
30
-
"rpc:sh.tangled.repo.merge?aud=*",
31
-
"rpc:sh.tangled.repo.hiddenRef?aud=*",
32
33
"rpc:sh.tangled.repo.deleteBranch?aud=*",
33
-
"rpc:sh.tangled.repo.setDefaultBranch?aud=*",
34
-
"rpc:sh.tangled.repo.forkSync?aud=*",
35
34
"rpc:sh.tangled.repo.forkStatus?aud=*",
35
+
"rpc:sh.tangled.repo.forkSync?aud=*",
36
+
"rpc:sh.tangled.repo.hiddenRef?aud=*",
37
+
"rpc:sh.tangled.repo.listSecrets?aud=*",
38
+
"rpc:sh.tangled.repo.merge?aud=*",
36
39
"rpc:sh.tangled.repo.mergeCheck?aud=*",
37
-
"rpc:sh.tangled.pipeline.cancelPipeline?aud=*",
38
-
"rpc:sh.tangled.repo.addSecret?aud=*",
39
40
"rpc:sh.tangled.repo.removeSecret?aud=*",
40
-
"rpc:sh.tangled.repo.listSecrets?aud=*",
41
+
"rpc:sh.tangled.repo.setDefaultBranch?aud=*",
41
42
}
+49
-12
appview/pages/templates/user/fragments/profileCard.html
+49
-12
appview/pages/templates/user/fragments/profileCard.html
···
39
39
</div>
40
40
</div>
41
41
<div class="col-span-3 md:col-span-full">
42
-
<div id="profile-bio" class="text-sm">
42
+
<div id="profile-bio" class="text-sm space-y-2">
43
43
{{ $profile := .Profile }}
44
44
{{ with .Profile }}
45
45
···
87
87
</div>
88
88
{{ end }}
89
89
90
-
<div class="flex mt-2 items-center gap-2">
90
+
<div class="flex my-2 items-center gap-2">
91
91
{{ if ne .FollowStatus.String "IsSelf" }}
92
92
{{ template "user/fragments/follow" . }}
93
93
{{ else }}
···
108
108
</a>
109
109
</div>
110
110
111
+
{{ if ne .FollowStatus.String "IsSelf" }}
112
+
{{ template "user/fragments/vouch" . }}
113
+
114
+
{{ if .VouchRelationship }}
115
+
{{ if .VouchRelationship.IndirectVouches }}
116
+
<div class="flex flex-col gap-2">
117
+
{{ template "user/fragments/networkVouches" .VouchRelationship }}
118
+
</div>
119
+
{{ end }}
120
+
{{ end }}
121
+
{{ end }}
122
+
111
123
</div>
112
124
<div id="update-profile" class="text-red-400 dark:text-red-500"></div>
113
125
</div>
···
115
127
{{ end }}
116
128
117
129
{{ define "followerFollowing" }}
118
-
{{ $root := index . 0 }}
119
-
{{ $userIdent := index . 1 }}
120
-
{{ with $root }}
121
-
<div class="flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full text-sm">
122
-
<span class="flex-shrink-0">{{ i "users" "size-4" }}</span>
123
-
<span id="followers" data-followers-did="{{ .UserDid }}"><a href="/{{ $userIdent }}?tab=followers">{{
124
-
.Stats.FollowersCount }} followers</a></span>
125
-
<span class="select-none after:content-['路']"></span>
126
-
<span id="following"><a href="/{{ $userIdent }}?tab=following">{{ .Stats.FollowingCount }} following</a></span>
127
-
</div>
130
+
{{ $root := index . 0 }}
131
+
{{ $userIdent := index . 1 }}
132
+
{{ with $root }}
133
+
<div class="flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full text-sm">
134
+
<span class="flex-shrink-0">{{ i "users" "size-4" }}</span>
135
+
<span id="followers" data-followers-did="{{ .UserDid }}"><a href="/{{ $userIdent }}?tab=followers">{{
136
+
.Stats.FollowersCount }} followers</a></span>
137
+
<span class="select-none after:content-['路']"></span>
138
+
<span id="following"><a href="/{{ $userIdent }}?tab=following">{{ .Stats.FollowingCount }} following</a></span>
139
+
</div>
140
+
{{ end }}
128
141
{{ end }}
142
+
143
+
144
+
{{ define "networkVouchItem" }}
145
+
<div class="flex items-center gap-2 p-2 group/item">
146
+
<div class="flex-shrink-0 h-fit relative">
147
+
{{ template "user/fragments/picLink" (list .Did "size-8") }}
148
+
</div>
149
+
<div class="flex-1 min-w-0 flex items-center gap-2">
150
+
<div class="text-sm font-medium flex-shrink-0 text-green-900 dark:text-green-100">
151
+
{{ resolve .Did }}
152
+
</div>
153
+
<div class="flex-1 min-w-0 text-sm text-green-700 dark:text-green-300 truncate">
154
+
{{ .Reason | description }}
155
+
</div>
156
+
</div>
157
+
<div class="flex-shrink-0 relative min-w-12 text-center">
158
+
<div class="text-xs text-green-600 dark:text-green-400 group-hover/item:opacity-0 transition-opacity">
159
+
{{ template "repo/fragments/shortTime" .CreatedAt }}
160
+
</div>
161
+
<div class="absolute inset-0 flex items-center gap-1 justify-center opacity-0 group-hover/item:opacity-100 transition-opacity text-xs font-medium text-green-900 dark:text-green-100">
162
+
view {{ i "arrow-right" "size-3" }}
163
+
</div>
164
+
</div>
165
+
</div>
129
166
{{ end }}
+6
appview/pages/templates/user/fragments/vouch.html
+6
appview/pages/templates/user/fragments/vouch.html
+25
appview/pages/templates/user/fragments/vouchTooltip.html
+25
appview/pages/templates/user/fragments/vouchTooltip.html
···
1
+
{{ define "user/fragments/vouchTooltip" }}
2
+
{{- $vr := . -}}
3
+
{{- if not $vr.IsEmpty -}}
4
+
{{- $networkPart := "" -}}
5
+
{{- if and (gt $vr.VouchStrength 0) (eq $vr.DenounceStrength 0) -}}
6
+
{{- $networkPart = printf "%s from your network vouched for this user." (plural $vr.VouchStrength "person" "people") -}}
7
+
{{- else if and (gt $vr.DenounceStrength 0) (eq $vr.VouchStrength 0) -}}
8
+
{{- $networkPart = printf "%s from your network denounced this user." (plural $vr.DenounceStrength "person" "people") -}}
9
+
{{- else if and (eq $vr.DenounceStrength 0) (eq $vr.VouchStrength 0) -}}
10
+
{{- $networkPart = "" -}}
11
+
{{- else -}}
12
+
{{- $networkPart = printf "%s and %s from your network." (plural $vr.VouchStrength "vouch" "vouches") (plural $vr.DenounceStrength "denounce" "") -}}
13
+
{{- end -}}
14
+
{{- $direct := $vr.GetDirectVouch -}}
15
+
{{- if $direct -}}
16
+
{{- if $direct.IsVouch -}}
17
+
You vouched for {{ resolve $vr.SubjectDid }} {{ relTimeFmt $direct.CreatedAt }}. {{ $networkPart }}
18
+
{{- else -}}
19
+
You denounced {{ resolve $vr.SubjectDid }} {{ relTimeFmt $direct.CreatedAt }}. {{ $networkPart }}
20
+
{{- end -}}
21
+
{{- else -}}
22
+
{{- $networkPart -}}.
23
+
{{- end -}}
24
+
{{- end -}}
25
+
{{ end }}
+109
appview/pages/templates/user/vouches.html
+109
appview/pages/templates/user/vouches.html
···
1
+
{{ define "title" }}{{ resolve .Card.UserDid }} 路 vouches {{ end }}
2
+
3
+
{{ define "profileContent" }}
4
+
{{ $isSelf := and .LoggedInUser (eq .LoggedInUser.Did .Card.UserDid) }}
5
+
<div id="all-vouches" class="md:col-span-8 order-2 md:order-2 flex flex-col gap-6">
6
+
{{ if $isSelf }}{{ template "vouchSuggestions" . }}{{ end }}
7
+
{{ block "vouchTimeline" . }}{{ end }}
8
+
{{ if ge (len .Vouches) .Page.Limit }}
9
+
{{ $handle := resolve .Card.UserDid }}
10
+
{{ template "fragments/pagination" (dict
11
+
"Page" .Page
12
+
"TotalCount" (add (len .Vouches) .Page.Offset)
13
+
"BasePath" (printf "/%s" $handle)
14
+
"QueryParams" (queryParams "tab" "vouches")
15
+
) }}
16
+
{{ end }}
17
+
</div>
18
+
{{ end }}
19
+
20
+
{{ define "vouchTimeline" }}
21
+
{{ $isSelf := and .LoggedInUser (eq .LoggedInUser.Did .Card.UserDid) }}
22
+
{{ if not .LoggedInUser }}
23
+
<div class="text-base text-gray-500 flex items-center justify-center italic p-12 border border-gray-200 dark:border-gray-700 rounded">
24
+
<span>You need to <a href="/login" class="underline">log in</a> to view vouches.</span>
25
+
</div>
26
+
{{ else if not .Vouches }}
27
+
{{ if not (and $isSelf .Suggestions) }}
28
+
<div class="text-base text-gray-500 flex items-center justify-center italic p-12 border border-gray-200 dark:border-gray-700 rounded">
29
+
<span>No vouches in your network yet.</span>
30
+
</div>
31
+
{{ end }}
32
+
{{ else }}
33
+
<div class="relative flex flex-col gap-6 ml-5 px-4 border border-transparent">
34
+
<div class="absolute top-8 bottom-8 w-0.5 bg-gray-200 dark:bg-gray-700 z-0"></div>
35
+
{{ range .Vouches }}
36
+
{{ template "vouchTimelineItem" (list $.Card.UserDid $.LoggedInUser.Did .) }}
37
+
{{ end }}
38
+
</div>
39
+
{{ end }}
40
+
{{ end }}
41
+
42
+
{{ define "vouchSuggestions" }}
43
+
{{ if .Suggestions }}
44
+
<div class="flex flex-col gap-2">
45
+
{{ range .Suggestions }}
46
+
<div class="flex items-center gap-3 border border-gray-200 dark:border-gray-700 rounded-sm p-4 bg-white dark:bg-gray-800">
47
+
<img src="{{ tinyAvatar .Did }}" alt="" class="rounded-full size-10 border border-gray-300 dark:border-gray-700 shrink-0" />
48
+
<div class="flex flex-col gap-0.5 min-w-0 flex-1">
49
+
<a href="/{{ resolve .Did }}" class="font-medium hover:underline dark:text-white truncate">{{ resolve .Did }}</a>
50
+
<span class="text-sm text-gray-500 dark:text-gray-400">{{ .Reason }}</span>
51
+
</div>
52
+
<div class="shrink-0 w-32">
53
+
{{ template "user/fragments/vouch" . }}
54
+
</div>
55
+
</div>
56
+
{{ end }}
57
+
</div>
58
+
{{ end }}
59
+
{{ end }}
60
+
61
+
{{ define "vouchTimelineItem" }}
62
+
{{ $profileDid := index . 0 }}
63
+
{{ $viewerDid := index . 1 }}
64
+
{{ $v := index . 2 }}
65
+
{{ $isOutgoing := eq $v.Did $profileDid }}
66
+
{{ $isVouch := $v.IsVouch }}
67
+
{{ $isSelf := eq $profileDid $viewerDid }}
68
+
69
+
<div class="-ml-5 flex items-center gap-3 relative z-10">
70
+
<div class="flex-shrink-0 flex items-center justify-center size-10 rounded-full
71
+
{{ if $isVouch }}bg-green-200 dark:bg-green-800{{ else }}bg-red-200 dark:bg-red-800{{ end }}">
72
+
{{ if $isVouch }}
73
+
{{ if $isOutgoing }}
74
+
{{ i "arrow-up-right" "size-5 text-green-500 dark:text-green-400" }}
75
+
{{ else }}
76
+
{{ i "arrow-down-left" "size-5 text-green-500 dark:text-green-400" }}
77
+
{{ end }}
78
+
{{ else }}
79
+
{{ if $isOutgoing }}
80
+
{{ i "arrow-up-right" "size-5 text-red-500 dark:text-red-400" }}
81
+
{{ else }}
82
+
{{ i "arrow-down-left" "size-5 text-red-500 dark:text-red-400" }}
83
+
{{ end }}
84
+
{{ end }}
85
+
</div>
86
+
87
+
<div class="flex flex-col gap-1">
88
+
<div class="flex flex-wrap items-center gap-1 text-sm dark:text-white">
89
+
{{ if and $isSelf $isOutgoing }}
90
+
<span class="font-medium text-gray-700 dark:text-gray-300">you</span>
91
+
{{ else }}
92
+
<a href="/{{ resolve $v.Did }}" class="font-medium hover:underline">{{ resolve $v.Did }}</a>
93
+
{{ end }}
94
+
<span class="text-gray-500 dark:text-gray-400">
95
+
{{ if $isVouch }}vouched for{{ else }}denounced{{ end }}
96
+
</span>
97
+
{{ if and $isSelf (not $isOutgoing) }}
98
+
<span class="font-medium text-gray-700 dark:text-gray-300">you</span>
99
+
{{ else }}
100
+
<a href="/{{ resolve $v.SubjectDid }}" class="font-medium hover:underline">{{ resolve $v.SubjectDid }}</a>
101
+
{{ end }}
102
+
<span class="text-gray-400 dark:text-gray-500 text-xs">{{ relTimeFmt $v.CreatedAt }}</span>
103
+
</div>
104
+
{{ with $v.Reason }}
105
+
<p class="text-gray-500 dark:text-gray-400">{{ . }}</p>
106
+
{{ end }}
107
+
</div>
108
+
</div>
109
+
{{ end }}
+4
appview/state/router.go
+4
appview/state/router.go
···
181
181
r.Delete("/", s.Follow)
182
182
})
183
183
184
+
r.With(middleware.AuthMiddleware(s.oauth)).Route("/vouch", func(r chi.Router) {
185
+
r.Post("/", s.Vouch)
186
+
})
187
+
184
188
r.With(middleware.AuthMiddleware(s.oauth)).Route("/star", func(r chi.Router) {
185
189
r.Post("/", s.Star)
186
190
r.Delete("/", s.Star)
+10
-9
appview/state/state.go
+10
-9
appview/state/state.go
···
123
123
config.Jetstream.Endpoint,
124
124
"appview",
125
125
[]string{
126
-
tangled.GraphFollowNSID,
126
+
tangled.ActorProfileNSID,
127
127
tangled.FeedStarNSID,
128
+
tangled.GraphFollowNSID,
129
+
tangled.GraphVouchNSID,
130
+
tangled.KnotMemberNSID,
131
+
tangled.KnotNSID,
132
+
tangled.LabelDefinitionNSID,
133
+
tangled.LabelOpNSID,
128
134
tangled.PublicKeyNSID,
129
135
tangled.RepoArtifactNSID,
130
-
tangled.ActorProfileNSID,
131
-
tangled.KnotMemberNSID,
136
+
tangled.RepoIssueCommentNSID,
137
+
tangled.RepoIssueNSID,
138
+
tangled.RepoPullNSID,
132
139
tangled.SpindleMemberNSID,
133
140
tangled.SpindleNSID,
134
-
tangled.KnotNSID,
135
141
tangled.StringNSID,
136
-
tangled.RepoPullNSID,
137
-
tangled.RepoIssueNSID,
138
-
tangled.RepoIssueCommentNSID,
139
-
tangled.LabelDefinitionNSID,
140
-
tangled.LabelOpNSID,
141
142
},
142
143
nil,
143
144
tlog.SubLogger(logger, "jetstream"),
+155
appview/state/vouch.go
+155
appview/state/vouch.go
···
1
+
package state
2
+
3
+
import (
4
+
"net/http"
5
+
"time"
6
+
7
+
comatproto "github.com/bluesky-social/indigo/api/atproto"
8
+
lexutil "github.com/bluesky-social/indigo/lex/util"
9
+
"github.com/ipfs/go-cid"
10
+
"tangled.org/core/api/tangled"
11
+
"tangled.org/core/appview/db"
12
+
"tangled.org/core/appview/models"
13
+
"tangled.org/core/log"
14
+
)
15
+
16
+
func (s *State) Vouch(w http.ResponseWriter, r *http.Request) {
17
+
l := log.SubLogger(s.logger, "vouch")
18
+
l = s.logger.With("handler", "Vouch")
19
+
currentUser := s.oauth.GetMultiAccountUser(r)
20
+
21
+
var subject string
22
+
23
+
subject = r.FormValue("subject")
24
+
25
+
if subject == "" {
26
+
l.Warn("invalid form: missing subject")
27
+
s.pages.Notice(w, "error", "Missing subject user.")
28
+
return
29
+
}
30
+
31
+
subjectIdent, err := s.idResolver.ResolveIdent(r.Context(), subject)
32
+
if err != nil {
33
+
l.Error("failed to vouch, invalid did", "subject", subject, "err", err)
34
+
s.pages.Notice(w, "error", "Could not find that user.")
35
+
return
36
+
}
37
+
38
+
if currentUser.Did == subjectIdent.DID.String() {
39
+
l.Warn("cant vouch or denounce yourself")
40
+
s.pages.Notice(w, "error", "You cannot vouch for yourself.")
41
+
return
42
+
}
43
+
44
+
client, err := s.oauth.AuthorizedClient(r)
45
+
if err != nil {
46
+
l.Error("failed to authorize client", "err", err)
47
+
s.pages.Notice(w, "error", "Authentication required.")
48
+
return
49
+
}
50
+
51
+
if err := r.ParseForm(); err != nil {
52
+
l.Warn("failed to parse form", "err", err)
53
+
s.pages.Notice(w, "error", "Invalid form data.")
54
+
return
55
+
}
56
+
57
+
subjectDid := subjectIdent.DID.String()
58
+
59
+
// handle "none" by deleting any existing vouch
60
+
if r.FormValue("kind") == "none" {
61
+
_, err := db.GetVouch(s.db, currentUser.Did, subjectDid)
62
+
if err != nil {
63
+
l.Info("no existing vouch to delete")
64
+
s.pages.HxRefresh(w)
65
+
return
66
+
}
67
+
68
+
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
69
+
Collection: tangled.GraphVouchNSID,
70
+
Repo: currentUser.Did,
71
+
Rkey: subjectDid,
72
+
})
73
+
74
+
if err != nil {
75
+
l.Error("failed to delete vouch record", "err", err)
76
+
s.pages.Notice(w, "error", "Failed to delete vouch record.")
77
+
return
78
+
}
79
+
80
+
err = db.DeleteVouch(s.db, currentUser.Did, subjectDid)
81
+
if err != nil {
82
+
l.Warn("failed to delete vouch from DB", "err", err)
83
+
}
84
+
85
+
l.Info("deleted vouch record")
86
+
s.pages.HxRefresh(w)
87
+
return
88
+
}
89
+
90
+
kind, err := models.ParseVouchKind(r.FormValue("kind"))
91
+
if err != nil {
92
+
l.Warn("failed to parse vouch kind", "err", err)
93
+
s.pages.Notice(w, "error", "Invalid action type.")
94
+
return
95
+
}
96
+
97
+
reason := r.FormValue("reason")
98
+
createdAt := time.Now().Format(time.RFC3339)
99
+
100
+
var reasonPtr *string
101
+
if reason != "" {
102
+
reasonPtr = &reason
103
+
}
104
+
105
+
var swapCid *string
106
+
existingVouch, err := db.GetVouch(s.db, currentUser.Did, subjectDid)
107
+
if err == nil {
108
+
cidStr := existingVouch.Cid.String()
109
+
swapCid = &cidStr
110
+
}
111
+
112
+
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
113
+
Collection: tangled.GraphVouchNSID,
114
+
Repo: currentUser.Did,
115
+
Rkey: subjectDid,
116
+
SwapRecord: swapCid,
117
+
Record: &lexutil.LexiconTypeDecoder{
118
+
Val: &tangled.GraphVouch{
119
+
Kind: string(kind),
120
+
Reason: reasonPtr,
121
+
CreatedAt: createdAt,
122
+
}},
123
+
})
124
+
if err != nil {
125
+
l.Error("failed to create atproto record", "err", err)
126
+
s.pages.Notice(w, "error", "Failed to create vouch record.")
127
+
return
128
+
}
129
+
130
+
l.Info("created atproto record", "uri", resp.Uri, "kind", kind)
131
+
132
+
newCid, err := cid.Parse(resp.Cid)
133
+
if err != nil {
134
+
l.Error("failed to parse returned cid", "err", err)
135
+
s.pages.Notice(w, "error", "Failed to save vouch.")
136
+
return
137
+
}
138
+
139
+
vouch := &models.Vouch{
140
+
Did: currentUser.Did,
141
+
SubjectDid: subjectDid,
142
+
Cid: newCid,
143
+
Kind: kind,
144
+
Reason: reasonPtr,
145
+
}
146
+
147
+
err = db.AddVouch(s.db, vouch)
148
+
if err != nil {
149
+
l.Error("failed to add vouch to db", "err", err)
150
+
s.pages.Notice(w, "error", "Failed to save vouch.")
151
+
return
152
+
}
153
+
154
+
s.pages.HxRefresh(w)
155
+
}