A vibe coded tangled fork which supports pijul.
at 1edcd7707b74263a60a244da3cbb982d5016a0ae 283 lines 6.7 kB view raw
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}