Monorepo for Tangled tangled.org
11

Configure Feed

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

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 pulls []*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 perCidForms := parseStackLabelForms(form) 114 115 applyAll := form.Get("applyLabelsToAll") == "on" 116 var firstStackForm url.Values 117 if applyAll && len(pulls) > 0 && len(pulls[0].Submissions) > 0 { 118 if firstCid := pulls[0].Submissions[0].ChangeId(); firstCid != "" { 119 if f, ok := perCidForms[firstCid]; ok { 120 firstStackForm = f 121 } 122 } 123 } 124 125 performedAt := time.Now() 126 for _, pull := range pulls { 127 labelForm := form 128 if firstStackForm != nil { 129 labelForm = firstStackForm 130 } else if len(perCidForms) > 0 && len(pull.Submissions) > 0 { 131 if cid := pull.Submissions[0].ChangeId(); cid != "" { 132 if perForm, ok := perCidForms[cid]; ok { 133 labelForm = perForm 134 } 135 } 136 } 137 rkey := tid.TID() 138 raw := buildCreationLabelOps(userDid, pull.AtUri(), rkey, labelForm, defs, performedAt) 139 140 valid := make([]models.LabelOp, 0, len(raw)) 141 for _, op := range raw { 142 def := defs[op.OperandKey] 143 144 // validate permissions: only collaborators can apply labels currently 145 // 146 // TODO: introduce a repo:triage permission 147 ok, err := s.acl.HasRepoPermissionErr(ctx, repo, op.Did, "repo:push") 148 if err != nil { 149 l.Warn("invalid label op", "err", err, "subject", op.Subject, "key", op.OperandKey) 150 continue 151 } 152 if !ok { 153 l.Warn("forbidden label op", "subject", op.Subject, "key", op.OperandKey) 154 continue 155 } 156 157 // resolve Handle to DID 158 if def.ValueType.IsString() && def.ValueType.IsDidFormat() { 159 val := syntax.AtIdentifier(op.OperandValue) 160 if val.IsHandle() { 161 ident, err := s.idResolver.Directory().Lookup(ctx, val) 162 if err != nil { 163 l.Warn("failed to resolve handle", "err", err, "subject", op.Subject, "key", op.OperandKey) 164 } 165 op.OperandValue = ident.DID.String() 166 } 167 } 168 169 if err := def.ValidateOperandValue(&op); err != nil { 170 l.Warn("invalid label op", "err", err, "subject", op.Subject, "key", op.OperandKey) 171 continue 172 } 173 valid = append(valid, op) 174 } 175 if len(valid) == 0 { 176 continue 177 } 178 179 record := models.LabelOpsAsRecord(valid) 180 if _, err := comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{ 181 Collection: tangled.LabelOpNSID, 182 Repo: userDid.String(), 183 Rkey: rkey, 184 Record: &lexutil.LexiconTypeDecoder{Val: &record}, 185 }); err != nil { 186 l.Warn("failed to write label ops to PDS", "err", err, "subject", pull.AtUri()) 187 continue 188 } 189 190 if err := s.indexLabelOps(ctx, valid); err != nil { 191 l.Warn("failed to index label ops", "err", err, "subject", pull.AtUri()) 192 if _, err := comatproto.RepoDeleteRecord(context.Background(), client, &comatproto.RepoDeleteRecord_Input{ 193 Collection: tangled.LabelOpNSID, 194 Repo: userDid.String(), 195 Rkey: rkey, 196 }); err != nil { 197 l.Warn("failed to rollback label ops record from PDS", "err", err, "subject", pull.AtUri()) 198 } 199 continue 200 } 201 202 s.notifier.NewPullLabelOp(ctx, userDid, pull, valid) 203 } 204} 205 206func (s *Pulls) indexLabelOps(ctx context.Context, ops []models.LabelOp) error { 207 tx, err := s.db.BeginTx(ctx, nil) 208 if err != nil { 209 return err 210 } 211 defer tx.Rollback() 212 for _, op := range ops { 213 if _, err := db.AddLabelOp(tx, &op); err != nil { 214 return err 215 } 216 } 217 return tx.Commit() 218}