Monorepo for Tangled
tangled.org
1package state
2
3import (
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
17func (s *State) SkipVouchSuggestion(w http.ResponseWriter, r *http.Request) {
18 l := log.SubLogger(s.logger, "skipVouchSuggestion")
19 currentUser := s.oauth.GetMultiAccountUser(r)
20
21 subject := r.FormValue("subject")
22 if subject == "" {
23 l.Warn("missing subject")
24 s.pages.Notice(w, "error", "Missing subject user.")
25 return
26 }
27
28 subjectIdent, err := s.idResolver.ResolveIdent(r.Context(), subject)
29 if err != nil {
30 l.Error("failed to resolve subject", "subject", subject, "err", err)
31 s.pages.Notice(w, "error", "Could not find that user.")
32 return
33 }
34
35 if currentUser.Did == subjectIdent.DID.String() {
36 l.Warn("cannot skip yourself")
37 s.pages.Notice(w, "error", "You cannot skip yourself.")
38 return
39 }
40
41 if err := db.SkipVouchSuggestion(s.db, currentUser.Did, subjectIdent.DID.String()); err != nil {
42 l.Error("failed to skip vouch suggestion", "err", err)
43 s.pages.Notice(w, "error", "Failed to skip suggestion.")
44 return
45 }
46
47 s.pages.HxRefresh(w)
48}
49
50func (s *State) Vouch(w http.ResponseWriter, r *http.Request) {
51 l := log.SubLogger(s.logger, "vouch")
52 l = s.logger.With("handler", "Vouch")
53 currentUser := s.oauth.GetMultiAccountUser(r)
54
55 var subject string
56
57 subject = r.FormValue("subject")
58
59 if subject == "" {
60 l.Warn("invalid form: missing subject")
61 s.pages.Notice(w, "error", "Missing subject user.")
62 return
63 }
64
65 subjectIdent, err := s.idResolver.ResolveIdent(r.Context(), subject)
66 if err != nil {
67 l.Error("failed to vouch, invalid did", "subject", subject, "err", err)
68 s.pages.Notice(w, "error", "Could not find that user.")
69 return
70 }
71
72 if currentUser.Did == subjectIdent.DID.String() {
73 l.Warn("cant vouch or denounce yourself")
74 s.pages.Notice(w, "error", "You cannot vouch for yourself.")
75 return
76 }
77
78 client, err := s.oauth.AuthorizedClient(r)
79 if err != nil {
80 l.Error("failed to authorize client", "err", err)
81 s.pages.Notice(w, "error", "Authentication required.")
82 return
83 }
84
85 if err := r.ParseForm(); err != nil {
86 l.Warn("failed to parse form", "err", err)
87 s.pages.Notice(w, "error", "Invalid form data.")
88 return
89 }
90
91 subjectDid := subjectIdent.DID.String()
92
93 // handle "none" by deleting any existing vouch
94 if r.FormValue("kind") == "none" {
95 _, err := db.GetVouch(s.db, currentUser.Did, subjectDid)
96 if err != nil {
97 l.Info("no existing vouch to delete")
98 s.pages.HxRefresh(w)
99 return
100 }
101
102 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
103 Collection: tangled.GraphVouchNSID,
104 Repo: currentUser.Did,
105 Rkey: subjectDid,
106 })
107
108 if err != nil {
109 l.Error("failed to delete vouch record", "err", err)
110 s.pages.Notice(w, "error", "Failed to delete vouch record.")
111 return
112 }
113
114 err = db.DeleteVouch(s.db, currentUser.Did, subjectDid)
115 if err != nil {
116 l.Warn("failed to delete vouch from DB", "err", err)
117 }
118
119 l.Info("deleted vouch record")
120 s.pages.HxRefresh(w)
121 return
122 }
123
124 kind, err := models.ParseVouchKind(r.FormValue("kind"))
125 if err != nil {
126 l.Warn("failed to parse vouch kind", "err", err)
127 s.pages.Notice(w, "error", "Invalid action type.")
128 return
129 }
130
131 reason := r.FormValue("reason")
132 createdAt := time.Now().Format(time.RFC3339)
133
134 var reasonPtr *string
135 if reason != "" {
136 reasonPtr = &reason
137 }
138
139 var evidences []syntax.ATURI
140 for _, raw := range r.Form["evidences"] {
141 uri, err := syntax.ParseATURI(raw)
142 if err != nil {
143 l.Warn("invalid evidence AT-URI, skipping", "uri", raw, "err", err)
144 continue
145 }
146 evidences = append(evidences, uri)
147 }
148
149 var swapCid *string
150 existingVouch, err := db.GetVouch(s.db, currentUser.Did, subjectDid)
151 if err == nil {
152 cidStr := existingVouch.Cid.String()
153 swapCid = &cidStr
154 }
155
156 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
157 Collection: tangled.GraphVouchNSID,
158 Repo: currentUser.Did,
159 Rkey: subjectDid,
160 SwapRecord: swapCid,
161 Record: &lexutil.LexiconTypeDecoder{
162 Val: &tangled.GraphVouch{
163 Kind: string(kind),
164 Reason: reasonPtr,
165 CreatedAt: createdAt,
166 Evidences: func() []string {
167 ss := make([]string, len(evidences))
168 for i, e := range evidences {
169 ss[i] = e.String()
170 }
171 return ss
172 }(),
173 }},
174 })
175 if err != nil {
176 l.Error("failed to create atproto record", "err", err)
177 s.pages.Notice(w, "error", "Failed to create vouch record.")
178 return
179 }
180
181 l.Info("created atproto record", "uri", resp.Uri, "kind", kind)
182
183 newCid, err := cid.Parse(resp.Cid)
184 if err != nil {
185 l.Error("failed to parse returned cid", "err", err)
186 s.pages.Notice(w, "error", "Failed to save vouch.")
187 return
188 }
189
190 vouch := &models.Vouch{
191 Did: syntax.DID(currentUser.Did),
192 SubjectDid: subjectIdent.DID,
193 Cid: newCid,
194 Kind: kind,
195 Reason: reasonPtr,
196 Evidences: evidences,
197 }
198
199 tx, err := s.db.Begin()
200 if err != nil {
201 l.Error("failed to start transaction", "err", err)
202 s.pages.Notice(w, "error", "Failed to save vouch.")
203 return
204 }
205 defer tx.Rollback()
206
207 err = db.AddVouch(tx, vouch)
208 if err != nil {
209 l.Error("failed to add vouch to db", "err", err)
210 s.pages.Notice(w, "error", "Failed to save vouch.")
211 return
212 }
213
214 if err = tx.Commit(); err != nil {
215 l.Error("failed to commit vouch transaction", "err", err)
216 s.pages.Notice(w, "error", "Failed to save vouch.")
217 return
218 }
219
220 s.pages.HxRefresh(w)
221}