This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / appview / pulls / labels.go
5.0 kB 193 lines
1package pulls 2 3import ( 4 "context" 5 "iter" 6 "net/url" 7 "slices" 8 "time" 9 10 "tangled.org/core/api/tangled" 11 "tangled.org/core/appview/db" 12 "tangled.org/core/appview/models" 13 "tangled.org/core/orm" 14 "tangled.org/core/tid" 15 16 comatproto "github.com/bluesky-social/indigo/api/atproto" 17 "github.com/bluesky-social/indigo/atproto/atclient" 18 "github.com/bluesky-social/indigo/atproto/syntax" 19 lexutil "github.com/bluesky-social/indigo/lex/util" 20) 21 22func (s *Pulls) pullLabelDefs(repo *models.Repo) (map[string]*models.LabelDefinition, error) { 23 defs, err := db.GetLabelDefinitions( 24 s.db, 25 orm.FilterIn("at_uri", repo.Labels), 26 orm.FilterContains("scope", tangled.RepoPullNSID), 27 ) 28 if err != nil { 29 return nil, err 30 } 31 32 out := make(map[string]*models.LabelDefinition, len(defs)) 33 for i := range defs { 34 d := defs[i] 35 if !slices.Contains(d.Scope, tangled.RepoPullNSID) { 36 continue 37 } 38 out[d.AtUri().String()] = &d 39 } 40 return out, nil 41} 42 43func formLabelEntries(form url.Values, defs map[string]*models.LabelDefinition) iter.Seq2[string, string] { 44 return func(yield func(string, string) bool) { 45 for key := range defs { 46 for _, v := range form[key] { 47 if v == "" { 48 continue 49 } 50 if !yield(key, v) { 51 return 52 } 53 } 54 } 55 } 56} 57 58func labelStateFromForm(form url.Values, defs map[string]*models.LabelDefinition) models.LabelState { 59 state := models.NewLabelState() 60 actx := &models.LabelApplicationCtx{Defs: defs} 61 for key, val := range formLabelEntries(form, defs) { 62 _ = actx.ApplyLabelOp(state, models.LabelOp{ 63 Operation: models.LabelOperationAdd, 64 OperandKey: key, 65 OperandValue: val, 66 }) 67 } 68 return state 69} 70 71func buildCreationLabelOps( 72 userDid syntax.DID, 73 subject syntax.ATURI, 74 rkey string, 75 form url.Values, 76 defs map[string]*models.LabelDefinition, 77 performedAt time.Time, 78) []models.LabelOp { 79 var ops []models.LabelOp 80 for key, val := range formLabelEntries(form, defs) { 81 ops = append(ops, models.LabelOp{ 82 Did: userDid.String(), 83 Rkey: rkey, 84 Subject: subject, 85 Operation: models.LabelOperationAdd, 86 OperandKey: key, 87 OperandValue: val, 88 PerformedAt: performedAt, 89 }) 90 } 91 return ops 92} 93 94func (s *Pulls) applyCreationLabels( 95 ctx context.Context, 96 client *atclient.APIClient, 97 userDid syntax.DID, 98 pull *models.Pull, 99 form url.Values, 100 repo *models.Repo, 101) { 102 l := s.logger.With("handler", "applyCreationLabels", "user", userDid) 103 104 defs, err := s.pullLabelDefs(repo) 105 if err != nil { 106 l.Warn("failed to fetch label defs", "err", err) 107 return 108 } 109 if len(defs) == 0 { 110 return 111 } 112 113 rkey := tid.TID() 114 raw := buildCreationLabelOps(userDid, pull.AtUri(), rkey, form, defs, time.Now()) 115 116 valid := make([]models.LabelOp, 0, len(raw)) 117 for _, op := range raw { 118 def := defs[op.OperandKey] 119 120 // validate permissions: only collaborators can apply labels currently 121 // 122 // TODO: introduce a repo:triage permission 123 ok, err := s.acl.HasRepoPermissionErr(ctx, repo, op.Did, "repo:push") 124 if err != nil { 125 l.Warn("invalid label op", "err", err, "subject", op.Subject, "key", op.OperandKey) 126 continue 127 } 128 if !ok { 129 l.Warn("forbidden label op", "subject", op.Subject, "key", op.OperandKey) 130 continue 131 } 132 133 // resolve Handle to DID 134 if def.ValueType.IsString() && def.ValueType.IsDidFormat() { 135 val := syntax.AtIdentifier(op.OperandValue) 136 if val.IsHandle() { 137 ident, err := s.idResolver.Directory().Lookup(ctx, val) 138 if err != nil { 139 l.Warn("failed to resolve handle", "err", err, "subject", op.Subject, "key", op.OperandKey) 140 } 141 op.OperandValue = ident.DID.String() 142 } 143 } 144 145 if err := def.ValidateOperandValue(&op); err != nil { 146 l.Warn("invalid label op", "err", err, "subject", op.Subject, "key", op.OperandKey) 147 continue 148 } 149 valid = append(valid, op) 150 } 151 if len(valid) == 0 { 152 return 153 } 154 155 record := models.LabelOpsAsRecord(valid) 156 if _, err := comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{ 157 Collection: tangled.LabelOpNSID, 158 Repo: userDid.String(), 159 Rkey: rkey, 160 Record: &lexutil.LexiconTypeDecoder{Val: &record}, 161 }); err != nil { 162 l.Warn("failed to write label ops to PDS", "err", err, "subject", pull.AtUri()) 163 return 164 } 165 166 if err := s.indexLabelOps(ctx, valid); err != nil { 167 l.Warn("failed to index label ops", "err", err, "subject", pull.AtUri()) 168 if _, err := comatproto.RepoDeleteRecord(context.Background(), client, &comatproto.RepoDeleteRecord_Input{ 169 Collection: tangled.LabelOpNSID, 170 Repo: userDid.String(), 171 Rkey: rkey, 172 }); err != nil { 173 l.Warn("failed to rollback label ops record from PDS", "err", err, "subject", pull.AtUri()) 174 } 175 return 176 } 177 178 s.notifier.NewPullLabelOp(ctx, userDid, pull, valid) 179} 180 181func (s *Pulls) indexLabelOps(ctx context.Context, ops []models.LabelOp) error { 182 tx, err := s.db.BeginTx(ctx, nil) 183 if err != nil { 184 return err 185 } 186 defer tx.Rollback() 187 for _, op := range ops { 188 if _, err := db.AddLabelOp(tx, &op); err != nil { 189 return err 190 } 191 } 192 return tx.Commit() 193}