···
12
+
comatproto "github.com/bluesky-social/indigo/api/atproto"
13
+
"github.com/bluesky-social/indigo/atproto/syntax"
14
+
lexutil "github.com/bluesky-social/indigo/lex/util"
15
+
"github.com/go-chi/chi/v5"
17
+
"tangled.sh/tangled.sh/core/api/tangled"
18
+
"tangled.sh/tangled.sh/core/appview/config"
19
+
"tangled.sh/tangled.sh/core/appview/db"
20
+
"tangled.sh/tangled.sh/core/appview/middleware"
21
+
"tangled.sh/tangled.sh/core/appview/oauth"
22
+
"tangled.sh/tangled.sh/core/appview/pages"
23
+
"tangled.sh/tangled.sh/core/appview/reporesolver"
24
+
"tangled.sh/tangled.sh/core/appview/xrpcclient"
25
+
"tangled.sh/tangled.sh/core/eventconsumer"
26
+
"tangled.sh/tangled.sh/core/idresolver"
27
+
"tangled.sh/tangled.sh/core/log"
28
+
"tangled.sh/tangled.sh/core/rbac"
29
+
"tangled.sh/tangled.sh/core/tid"
32
+
type Labels struct {
33
+
repoResolver *reporesolver.RepoResolver
34
+
idResolver *idresolver.Resolver
43
+
repoResolver *reporesolver.RepoResolver,
45
+
spindlestream *eventconsumer.Consumer,
46
+
idResolver *idresolver.Resolver,
48
+
config *config.Config,
49
+
enforcer *rbac.Enforcer,
51
+
logger := log.New("labels")
55
+
repoResolver: repoResolver,
57
+
idResolver: idResolver,
63
+
func (l *Labels) Router(mw *middleware.Middleware) http.Handler {
64
+
r := chi.NewRouter()
66
+
r.With(middleware.AuthMiddleware(l.oauth)).Put("/perform", l.PerformLabelOp)
71
+
func (l *Labels) PerformLabelOp(w http.ResponseWriter, r *http.Request) {
72
+
user := l.oauth.GetUser(r)
74
+
if err := r.ParseForm(); err != nil {
75
+
l.logger.Error("failed to parse form data", "error", err)
76
+
http.Error(w, "Invalid form data", http.StatusBadRequest)
82
+
performedAt := time.Now()
83
+
indexedAt := time.Now()
84
+
repoAt := r.Form.Get("repo")
85
+
subjectUri := r.Form.Get("subject")
86
+
keys := r.Form["operand-key"]
87
+
vals := r.Form["operand-val"]
89
+
var labelOps []db.LabelOp
90
+
for i := range len(keys) {
91
+
op := r.FormValue(fmt.Sprintf("op-%d", i))
93
+
op = string(db.LabelOperationDel)
98
+
labelOps = append(labelOps, db.LabelOp{
101
+
Subject: syntax.ATURI(subjectUri),
102
+
Operation: db.LabelOperation(op),
105
+
PerformedAt: performedAt,
106
+
IndexedAt: indexedAt,
110
+
// TODO: validate the operations
112
+
// find all the labels that this repo subscribes to
113
+
repoLabels, err := db.GetRepoLabels(l.db, db.FilterEq("repo_at", repoAt))
115
+
http.Error(w, "Invalid form data", http.StatusBadRequest)
119
+
var labelAts []string
120
+
for _, rl := range repoLabels {
121
+
labelAts = append(labelAts, rl.LabelAt.String())
124
+
actx, err := db.NewLabelApplicationCtx(l.db, db.FilterIn("at_uri", labelAts))
126
+
http.Error(w, "Invalid form data", http.StatusBadRequest)
130
+
// calculate the start state by applying already known labels
131
+
existingOps, err := db.GetLabelOps(l.db, db.FilterEq("subject", subjectUri))
133
+
http.Error(w, "Invalid form data", http.StatusBadRequest)
137
+
labelState := db.NewLabelState()
138
+
actx.ApplyLabelOps(labelState, existingOps)
140
+
// next, apply all ops introduced in this request and filter out ones that are no-ops
141
+
validLabelOps := labelOps[:0]
142
+
for _, op := range labelOps {
143
+
if err = actx.ApplyLabelOp(labelState, op); err != db.LabelNoOpError {
144
+
validLabelOps = append(validLabelOps, op)
149
+
if len(validLabelOps) == 0 {
150
+
l.pages.HxRefresh(w)
154
+
// create an atproto record of valid ops
155
+
record := db.LabelOpsAsRecord(validLabelOps)
157
+
client, err := l.oauth.AuthorizedClient(r)
159
+
l.logger.Error("failed to create client", "error", err)
160
+
http.Error(w, "Invalid form data", http.StatusBadRequest)
164
+
resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
165
+
Collection: tangled.LabelOpNSID,
168
+
Record: &lexutil.LexiconTypeDecoder{
173
+
l.logger.Error("failed to write to PDS", "error", err)
174
+
http.Error(w, "failed to write to PDS", http.StatusInternalServerError)
179
+
tx, err := l.db.BeginTx(r.Context(), nil)
181
+
l.logger.Error("failed to start tx", "error", err)
185
+
rollback := func() {
186
+
err1 := tx.Rollback()
187
+
err2 := rollbackRecord(context.Background(), atUri, client)
189
+
// ignore txn complete errors, this is okay
190
+
if errors.Is(err1, sql.ErrTxDone) {
194
+
if errs := errors.Join(err1, err2); errs != nil {
200
+
for _, o := range validLabelOps {
201
+
if _, err := db.AddLabelOp(l.db, &o); err != nil {
202
+
l.logger.Error("failed to add op", "err", err)
206
+
l.logger.Info("performed label op", "did", o.Did, "rkey", o.Rkey, "kind", o.Operation, "subjcet", o.Subject, "key", o.OperandKey)
214
+
// clear aturi when everything is successful
217
+
l.pages.HxRefresh(w)
220
+
// this is used to rollback changes made to the PDS
222
+
// it is a no-op if the provided ATURI is empty
223
+
func rollbackRecord(ctx context.Context, aturi string, xrpcc *xrpcclient.Client) error {
228
+
parsed := syntax.ATURI(aturi)
230
+
collection := parsed.Collection().String()
231
+
repo := parsed.Authority().String()
232
+
rkey := parsed.RecordKey().String()
234
+
_, err := xrpcc.RepoDeleteRecord(ctx, &comatproto.RepoDeleteRecord_Input{
235
+
Collection: collection,