Signed-off-by: oppiliappan me@oppi.li
+378
-39
Diff
round #2
+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.String }}" 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.String }}" class="font-medium hover:underline dark:text-white truncate">{{ resolve .Did.String }}</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.String }}" class="font-medium hover:underline">{{ resolve $v.Did.String }}</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.String }}" class="font-medium hover:underline">{{ resolve $v.SubjectDid.String }}</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"),
+156
appview/state/vouch.go
+156
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
+
"github.com/bluesky-social/indigo/atproto/syntax"
9
+
lexutil "github.com/bluesky-social/indigo/lex/util"
10
+
"github.com/ipfs/go-cid"
11
+
"tangled.org/core/api/tangled"
12
+
"tangled.org/core/appview/db"
13
+
"tangled.org/core/appview/models"
14
+
"tangled.org/core/log"
15
+
)
16
+
17
+
func (s *State) Vouch(w http.ResponseWriter, r *http.Request) {
18
+
l := log.SubLogger(s.logger, "vouch")
19
+
l = s.logger.With("handler", "Vouch")
20
+
currentUser := s.oauth.GetMultiAccountUser(r)
21
+
22
+
var subject string
23
+
24
+
subject = r.FormValue("subject")
25
+
26
+
if subject == "" {
27
+
l.Warn("invalid form: missing subject")
28
+
s.pages.Notice(w, "error", "Missing subject user.")
29
+
return
30
+
}
31
+
32
+
subjectIdent, err := s.idResolver.ResolveIdent(r.Context(), subject)
33
+
if err != nil {
34
+
l.Error("failed to vouch, invalid did", "subject", subject, "err", err)
35
+
s.pages.Notice(w, "error", "Could not find that user.")
36
+
return
37
+
}
38
+
39
+
if currentUser.Did == subjectIdent.DID.String() {
40
+
l.Warn("cant vouch or denounce yourself")
41
+
s.pages.Notice(w, "error", "You cannot vouch for yourself.")
42
+
return
43
+
}
44
+
45
+
client, err := s.oauth.AuthorizedClient(r)
46
+
if err != nil {
47
+
l.Error("failed to authorize client", "err", err)
48
+
s.pages.Notice(w, "error", "Authentication required.")
49
+
return
50
+
}
51
+
52
+
if err := r.ParseForm(); err != nil {
53
+
l.Warn("failed to parse form", "err", err)
54
+
s.pages.Notice(w, "error", "Invalid form data.")
55
+
return
56
+
}
57
+
58
+
subjectDid := subjectIdent.DID.String()
59
+
60
+
// handle "none" by deleting any existing vouch
61
+
if r.FormValue("kind") == "none" {
62
+
_, err := db.GetVouch(s.db, currentUser.Did, subjectDid)
63
+
if err != nil {
64
+
l.Info("no existing vouch to delete")
65
+
s.pages.HxRefresh(w)
66
+
return
67
+
}
68
+
69
+
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
70
+
Collection: tangled.GraphVouchNSID,
71
+
Repo: currentUser.Did,
72
+
Rkey: subjectDid,
73
+
})
74
+
75
+
if err != nil {
76
+
l.Error("failed to delete vouch record", "err", err)
77
+
s.pages.Notice(w, "error", "Failed to delete vouch record.")
78
+
return
79
+
}
80
+
81
+
err = db.DeleteVouch(s.db, currentUser.Did, subjectDid)
82
+
if err != nil {
83
+
l.Warn("failed to delete vouch from DB", "err", err)
84
+
}
85
+
86
+
l.Info("deleted vouch record")
87
+
s.pages.HxRefresh(w)
88
+
return
89
+
}
90
+
91
+
kind, err := models.ParseVouchKind(r.FormValue("kind"))
92
+
if err != nil {
93
+
l.Warn("failed to parse vouch kind", "err", err)
94
+
s.pages.Notice(w, "error", "Invalid action type.")
95
+
return
96
+
}
97
+
98
+
reason := r.FormValue("reason")
99
+
createdAt := time.Now().Format(time.RFC3339)
100
+
101
+
var reasonPtr *string
102
+
if reason != "" {
103
+
reasonPtr = &reason
104
+
}
105
+
106
+
var swapCid *string
107
+
existingVouch, err := db.GetVouch(s.db, currentUser.Did, subjectDid)
108
+
if err == nil {
109
+
cidStr := existingVouch.Cid.String()
110
+
swapCid = &cidStr
111
+
}
112
+
113
+
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
114
+
Collection: tangled.GraphVouchNSID,
115
+
Repo: currentUser.Did,
116
+
Rkey: subjectDid,
117
+
SwapRecord: swapCid,
118
+
Record: &lexutil.LexiconTypeDecoder{
119
+
Val: &tangled.GraphVouch{
120
+
Kind: string(kind),
121
+
Reason: reasonPtr,
122
+
CreatedAt: createdAt,
123
+
}},
124
+
})
125
+
if err != nil {
126
+
l.Error("failed to create atproto record", "err", err)
127
+
s.pages.Notice(w, "error", "Failed to create vouch record.")
128
+
return
129
+
}
130
+
131
+
l.Info("created atproto record", "uri", resp.Uri, "kind", kind)
132
+
133
+
newCid, err := cid.Parse(resp.Cid)
134
+
if err != nil {
135
+
l.Error("failed to parse returned cid", "err", err)
136
+
s.pages.Notice(w, "error", "Failed to save vouch.")
137
+
return
138
+
}
139
+
140
+
vouch := &models.Vouch{
141
+
Did: syntax.DID(currentUser.Did),
142
+
SubjectDid: subjectIdent.DID,
143
+
Cid: newCid,
144
+
Kind: kind,
145
+
Reason: reasonPtr,
146
+
}
147
+
148
+
err = db.AddVouch(s.db, vouch)
149
+
if err != nil {
150
+
l.Error("failed to add vouch to db", "err", err)
151
+
s.pages.Notice(w, "error", "Failed to save vouch.")
152
+
return
153
+
}
154
+
155
+
s.pages.HxRefresh(w)
156
+
}