This repository has no description
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}