A vibe coded tangled fork which supports pijul.
1package labels
2
3import (
4 "context"
5 "database/sql"
6 "errors"
7 "fmt"
8 "log/slog"
9 "net/http"
10 "time"
11
12 "tangled.org/core/api/tangled"
13 "tangled.org/core/appview/db"
14 "tangled.org/core/appview/middleware"
15 "tangled.org/core/appview/models"
16 "tangled.org/core/appview/oauth"
17 "tangled.org/core/appview/pages"
18 "tangled.org/core/orm"
19 "tangled.org/core/rbac"
20 "tangled.org/core/tid"
21
22 comatproto "github.com/bluesky-social/indigo/api/atproto"
23 atpclient "github.com/bluesky-social/indigo/atproto/client"
24 "github.com/bluesky-social/indigo/atproto/syntax"
25 lexutil "github.com/bluesky-social/indigo/lex/util"
26 "github.com/go-chi/chi/v5"
27)
28
29type Labels struct {
30 oauth *oauth.OAuth
31 pages *pages.Pages
32 db *db.DB
33 logger *slog.Logger
34 enforcer *rbac.Enforcer
35}
36
37func New(
38 oauth *oauth.OAuth,
39 pages *pages.Pages,
40 db *db.DB,
41 enforcer *rbac.Enforcer,
42 logger *slog.Logger,
43) *Labels {
44 return &Labels{
45 oauth: oauth,
46 pages: pages,
47 db: db,
48 logger: logger,
49 enforcer: enforcer,
50 }
51}
52
53func (l *Labels) Router() http.Handler {
54 r := chi.NewRouter()
55
56 r.Use(middleware.AuthMiddleware(l.oauth))
57 r.Put("/perform", l.PerformLabelOp)
58
59 return r
60}
61
62// this is a tricky handler implementation:
63// - the user selects the new state of all the labels in the label panel and hits save
64// - this handler should calculate the diff in order to create the labelop record
65// - we need the diff in order to maintain a "history" of operations performed by users
66func (l *Labels) PerformLabelOp(w http.ResponseWriter, r *http.Request) {
67 user := l.oauth.GetMultiAccountUser(r)
68
69 noticeId := "add-label-error"
70
71 fail := func(msg string, err error) {
72 l.logger.Error("failed to add label", "err", err)
73 l.pages.Notice(w, noticeId, msg)
74 }
75
76 if err := r.ParseForm(); err != nil {
77 fail("Invalid form.", err)
78 return
79 }
80
81 did := user.Active.Did
82 rkey := tid.TID()
83 performedAt := time.Now()
84 indexedAt := time.Now()
85 repoAt := r.Form.Get("repo")
86 subjectUri := r.Form.Get("subject")
87
88 repo, err := db.GetRepo(l.db, orm.FilterEq("at_uri", repoAt))
89 if err != nil {
90 fail("Failed to get repository.", err)
91 return
92 }
93
94 // find all the labels that this repo subscribes to
95 repoLabels, err := db.GetRepoLabels(l.db, orm.FilterEq("repo_at", repoAt))
96 if err != nil {
97 fail("Failed to get labels for this repository.", err)
98 return
99 }
100
101 var labelAts []string
102 for _, rl := range repoLabels {
103 labelAts = append(labelAts, rl.LabelAt.String())
104 }
105
106 actx, err := db.NewLabelApplicationCtx(l.db, orm.FilterIn("at_uri", labelAts))
107 if err != nil {
108 fail("Invalid form data.", err)
109 return
110 }
111
112 // calculate the start state by applying already known labels
113 existingOps, err := db.GetLabelOps(l.db, orm.FilterEq("subject", subjectUri))
114 if err != nil {
115 fail("Invalid form data.", err)
116 return
117 }
118
119 labelState := models.NewLabelState()
120 actx.ApplyLabelOps(labelState, existingOps)
121
122 var labelOps []models.LabelOp
123
124 // first delete all existing state
125 for key, vals := range labelState.Inner() {
126 for val := range vals {
127 labelOps = append(labelOps, models.LabelOp{
128 Did: did,
129 Rkey: rkey,
130 Subject: syntax.ATURI(subjectUri),
131 Operation: models.LabelOperationDel,
132 OperandKey: key,
133 OperandValue: val,
134 PerformedAt: performedAt,
135 IndexedAt: indexedAt,
136 })
137 }
138 }
139
140 // add all the new state the user specified
141 for key, vals := range r.Form {
142 if _, ok := actx.Defs[key]; !ok {
143 continue
144 }
145
146 for _, val := range vals {
147 labelOps = append(labelOps, models.LabelOp{
148 Did: did,
149 Rkey: rkey,
150 Subject: syntax.ATURI(subjectUri),
151 Operation: models.LabelOperationAdd,
152 OperandKey: key,
153 OperandValue: val,
154 PerformedAt: performedAt,
155 IndexedAt: indexedAt,
156 })
157 }
158 }
159
160 for i := range labelOps {
161 def := actx.Defs[labelOps[i].OperandKey]
162 op := labelOps[i]
163
164 // validate permissions: only collaborators can apply labels currently
165 //
166 // TODO: introduce a repo:triage permission
167 ok, err := l.enforcer.IsPushAllowed(op.Did, repo.Knot, repo.DidSlashRepo())
168 if err != nil {
169 fail("Failed to enforce permissions. Please try again later", fmt.Errorf("enforcing permission: %w", err))
170 return
171 }
172 if !ok {
173 fail("Unauthorized label operation", fmt.Errorf("unauthorized label operation"))
174 return
175 }
176
177 if err := def.ValidateOperandValue(&op); err != nil {
178 fail(fmt.Sprintf("Invalid form data: %s", err), err)
179 return
180 }
181 labelOps[i] = op
182 }
183
184 // reduce the opset
185 labelOps = models.ReduceLabelOps(labelOps)
186
187 // next, apply all ops introduced in this request and filter out ones that are no-ops
188 validLabelOps := labelOps[:0]
189 for _, op := range labelOps {
190 if err = actx.ApplyLabelOp(labelState, op); err != models.LabelNoOpError {
191 validLabelOps = append(validLabelOps, op)
192 }
193 }
194
195 // nothing to do
196 if len(validLabelOps) == 0 {
197 l.pages.HxRefresh(w)
198 return
199 }
200
201 // create an atproto record of valid ops
202 record := models.LabelOpsAsRecord(validLabelOps)
203
204 client, err := l.oauth.AuthorizedClient(r)
205 if err != nil {
206 fail("Failed to authorize user.", err)
207 return
208 }
209
210 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
211 Collection: tangled.LabelOpNSID,
212 Repo: did,
213 Rkey: rkey,
214 Record: &lexutil.LexiconTypeDecoder{
215 Val: &record,
216 },
217 })
218 if err != nil {
219 fail("Failed to create record on PDS for user.", err)
220 return
221 }
222 atUri := resp.Uri
223
224 tx, err := l.db.BeginTx(r.Context(), nil)
225 if err != nil {
226 fail("Failed to update labels. Try again later.", err)
227 return
228 }
229
230 rollback := func() {
231 err1 := tx.Rollback()
232 err2 := rollbackRecord(context.Background(), atUri, client)
233
234 // ignore txn complete errors, this is okay
235 if errors.Is(err1, sql.ErrTxDone) {
236 err1 = nil
237 }
238
239 if errs := errors.Join(err1, err2); errs != nil {
240 return
241 }
242 }
243 defer rollback()
244
245 for _, o := range validLabelOps {
246 if _, err := db.AddLabelOp(l.db, &o); err != nil {
247 fail("Failed to update labels. Try again later.", err)
248 return
249 }
250 }
251
252 err = tx.Commit()
253 if err != nil {
254 return
255 }
256
257 // clear aturi when everything is successful
258 atUri = ""
259
260 l.pages.HxRefresh(w)
261}
262
263// this is used to rollback changes made to the PDS
264//
265// it is a no-op if the provided ATURI is empty
266func rollbackRecord(ctx context.Context, aturi string, client *atpclient.APIClient) error {
267 if aturi == "" {
268 return nil
269 }
270
271 parsed := syntax.ATURI(aturi)
272
273 collection := parsed.Collection().String()
274 repo := parsed.Authority().String()
275 rkey := parsed.RecordKey().String()
276
277 _, err := comatproto.RepoDeleteRecord(ctx, client, &comatproto.RepoDeleteRecord_Input{
278 Collection: collection,
279 Repo: repo,
280 Rkey: rkey,
281 })
282 return err
283}