this repo has no description
1package appview
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "log/slog"
8 "strings"
9 "time"
10
11 "github.com/bluesky-social/indigo/atproto/syntax"
12 "github.com/bluesky-social/jetstream/pkg/models"
13 "github.com/go-git/go-git/v5/plumbing"
14 "github.com/ipfs/go-cid"
15 "tangled.sh/tangled.sh/core/api/tangled"
16 "tangled.sh/tangled.sh/core/appview/config"
17 "tangled.sh/tangled.sh/core/appview/db"
18 "tangled.sh/tangled.sh/core/appview/pages/markup"
19 "tangled.sh/tangled.sh/core/appview/serververify"
20 "tangled.sh/tangled.sh/core/idresolver"
21 "tangled.sh/tangled.sh/core/rbac"
22)
23
24type Ingester struct {
25 Db db.DbWrapper
26 Enforcer *rbac.Enforcer
27 IdResolver *idresolver.Resolver
28 Config *config.Config
29 Logger *slog.Logger
30}
31
32type processFunc func(ctx context.Context, e *models.Event) error
33
34func (i *Ingester) Ingest() processFunc {
35 return func(ctx context.Context, e *models.Event) error {
36 var err error
37 defer func() {
38 eventTime := e.TimeUS
39 lastTimeUs := eventTime + 1
40 if err := i.Db.SaveLastTimeUs(lastTimeUs); err != nil {
41 err = fmt.Errorf("(deferred) failed to save last time us: %w", err)
42 }
43 }()
44
45 l := i.Logger.With("kind", e.Kind)
46 switch e.Kind {
47 case models.EventKindAccount:
48 if !e.Account.Active && *e.Account.Status == "deactivated" {
49 err = i.IdResolver.InvalidateIdent(ctx, e.Account.Did)
50 }
51 case models.EventKindIdentity:
52 err = i.IdResolver.InvalidateIdent(ctx, e.Identity.Did)
53 case models.EventKindCommit:
54 switch e.Commit.Collection {
55 case tangled.GraphFollowNSID:
56 err = i.ingestFollow(e)
57 case tangled.FeedStarNSID:
58 err = i.ingestStar(e)
59 case tangled.PublicKeyNSID:
60 err = i.ingestPublicKey(e)
61 case tangled.RepoArtifactNSID:
62 err = i.ingestArtifact(e)
63 case tangled.ActorProfileNSID:
64 err = i.ingestProfile(e)
65 case tangled.SpindleMemberNSID:
66 err = i.ingestSpindleMember(ctx, e)
67 case tangled.SpindleNSID:
68 err = i.ingestSpindle(ctx, e)
69 case tangled.KnotMemberNSID:
70 err = i.ingestKnotMember(e)
71 case tangled.KnotNSID:
72 err = i.ingestKnot(e)
73 case tangled.StringNSID:
74 err = i.ingestString(e)
75 case tangled.RepoIssueNSID:
76 err = i.ingestIssue(ctx, e)
77 }
78 l = i.Logger.With("nsid", e.Commit.Collection)
79 }
80
81 if err != nil {
82 l.Debug("error ingesting record", "err", err)
83 }
84
85 return nil
86 }
87}
88
89func (i *Ingester) ingestStar(e *models.Event) error {
90 var err error
91 did := e.Did
92
93 l := i.Logger.With("handler", "ingestStar")
94 l = l.With("nsid", e.Commit.Collection)
95
96 switch e.Commit.Operation {
97 case models.CommitOperationCreate, models.CommitOperationUpdate:
98 var subjectUri syntax.ATURI
99
100 raw := json.RawMessage(e.Commit.Record)
101 record := tangled.FeedStar{}
102 err := json.Unmarshal(raw, &record)
103 if err != nil {
104 l.Error("invalid record", "err", err)
105 return err
106 }
107
108 subjectUri, err = syntax.ParseATURI(record.Subject)
109 if err != nil {
110 l.Error("invalid record", "err", err)
111 return err
112 }
113 err = db.AddStar(i.Db, &db.Star{
114 StarredByDid: did,
115 RepoAt: subjectUri,
116 Rkey: e.Commit.RKey,
117 })
118 case models.CommitOperationDelete:
119 err = db.DeleteStarByRkey(i.Db, did, e.Commit.RKey)
120 }
121
122 if err != nil {
123 return fmt.Errorf("failed to %s star record: %w", e.Commit.Operation, err)
124 }
125
126 return nil
127}
128
129func (i *Ingester) ingestFollow(e *models.Event) error {
130 var err error
131 did := e.Did
132
133 l := i.Logger.With("handler", "ingestFollow")
134 l = l.With("nsid", e.Commit.Collection)
135
136 switch e.Commit.Operation {
137 case models.CommitOperationCreate, models.CommitOperationUpdate:
138 raw := json.RawMessage(e.Commit.Record)
139 record := tangled.GraphFollow{}
140 err = json.Unmarshal(raw, &record)
141 if err != nil {
142 l.Error("invalid record", "err", err)
143 return err
144 }
145
146 err = db.AddFollow(i.Db, &db.Follow{
147 UserDid: did,
148 SubjectDid: record.Subject,
149 Rkey: e.Commit.RKey,
150 })
151 case models.CommitOperationDelete:
152 err = db.DeleteFollowByRkey(i.Db, did, e.Commit.RKey)
153 }
154
155 if err != nil {
156 return fmt.Errorf("failed to %s follow record: %w", e.Commit.Operation, err)
157 }
158
159 return nil
160}
161
162func (i *Ingester) ingestPublicKey(e *models.Event) error {
163 did := e.Did
164 var err error
165
166 l := i.Logger.With("handler", "ingestPublicKey")
167 l = l.With("nsid", e.Commit.Collection)
168
169 switch e.Commit.Operation {
170 case models.CommitOperationCreate, models.CommitOperationUpdate:
171 l.Debug("processing add of pubkey")
172 raw := json.RawMessage(e.Commit.Record)
173 record := tangled.PublicKey{}
174 err = json.Unmarshal(raw, &record)
175 if err != nil {
176 l.Error("invalid record", "err", err)
177 return err
178 }
179
180 name := record.Name
181 key := record.Key
182 err = db.AddPublicKey(i.Db, did, name, key, e.Commit.RKey)
183 case models.CommitOperationDelete:
184 l.Debug("processing delete of pubkey")
185 err = db.DeletePublicKeyByRkey(i.Db, did, e.Commit.RKey)
186 }
187
188 if err != nil {
189 return fmt.Errorf("failed to %s pubkey record: %w", e.Commit.Operation, err)
190 }
191
192 return nil
193}
194
195func (i *Ingester) ingestArtifact(e *models.Event) error {
196 did := e.Did
197 var err error
198
199 l := i.Logger.With("handler", "ingestArtifact")
200 l = l.With("nsid", e.Commit.Collection)
201
202 switch e.Commit.Operation {
203 case models.CommitOperationCreate, models.CommitOperationUpdate:
204 raw := json.RawMessage(e.Commit.Record)
205 record := tangled.RepoArtifact{}
206 err = json.Unmarshal(raw, &record)
207 if err != nil {
208 l.Error("invalid record", "err", err)
209 return err
210 }
211
212 repoAt, err := syntax.ParseATURI(record.Repo)
213 if err != nil {
214 return err
215 }
216
217 repo, err := db.GetRepoByAtUri(i.Db, repoAt.String())
218 if err != nil {
219 return err
220 }
221
222 ok, err := i.Enforcer.E.Enforce(did, repo.Knot, repo.DidSlashRepo(), "repo:push")
223 if err != nil || !ok {
224 return err
225 }
226
227 createdAt, err := time.Parse(time.RFC3339, record.CreatedAt)
228 if err != nil {
229 createdAt = time.Now()
230 }
231
232 artifact := db.Artifact{
233 Did: did,
234 Rkey: e.Commit.RKey,
235 RepoAt: repoAt,
236 Tag: plumbing.Hash(record.Tag),
237 CreatedAt: createdAt,
238 BlobCid: cid.Cid(record.Artifact.Ref),
239 Name: record.Name,
240 Size: uint64(record.Artifact.Size),
241 MimeType: record.Artifact.MimeType,
242 }
243
244 err = db.AddArtifact(i.Db, artifact)
245 case models.CommitOperationDelete:
246 err = db.DeleteArtifact(i.Db, db.FilterEq("did", did), db.FilterEq("rkey", e.Commit.RKey))
247 }
248
249 if err != nil {
250 return fmt.Errorf("failed to %s artifact record: %w", e.Commit.Operation, err)
251 }
252
253 return nil
254}
255
256func (i *Ingester) ingestProfile(e *models.Event) error {
257 did := e.Did
258 var err error
259
260 l := i.Logger.With("handler", "ingestProfile")
261 l = l.With("nsid", e.Commit.Collection)
262
263 if e.Commit.RKey != "self" {
264 return fmt.Errorf("ingestProfile only ingests `self` record")
265 }
266
267 switch e.Commit.Operation {
268 case models.CommitOperationCreate, models.CommitOperationUpdate:
269 raw := json.RawMessage(e.Commit.Record)
270 record := tangled.ActorProfile{}
271 err = json.Unmarshal(raw, &record)
272 if err != nil {
273 l.Error("invalid record", "err", err)
274 return err
275 }
276
277 description := ""
278 if record.Description != nil {
279 description = *record.Description
280 }
281
282 includeBluesky := record.Bluesky
283
284 location := ""
285 if record.Location != nil {
286 location = *record.Location
287 }
288
289 var links [5]string
290 for i, l := range record.Links {
291 if i < 5 {
292 links[i] = l
293 }
294 }
295
296 var stats [2]db.VanityStat
297 for i, s := range record.Stats {
298 if i < 2 {
299 stats[i].Kind = db.VanityStatKind(s)
300 }
301 }
302
303 var pinned [6]syntax.ATURI
304 for i, r := range record.PinnedRepositories {
305 if i < 6 {
306 pinned[i] = syntax.ATURI(r)
307 }
308 }
309
310 profile := db.Profile{
311 Did: did,
312 Description: description,
313 IncludeBluesky: includeBluesky,
314 Location: location,
315 Links: links,
316 Stats: stats,
317 PinnedRepos: pinned,
318 }
319
320 ddb, ok := i.Db.Execer.(*db.DB)
321 if !ok {
322 return fmt.Errorf("failed to index profile record, invalid db cast")
323 }
324
325 tx, err := ddb.Begin()
326 if err != nil {
327 return fmt.Errorf("failed to start transaction")
328 }
329
330 err = db.ValidateProfile(tx, &profile)
331 if err != nil {
332 return fmt.Errorf("invalid profile record")
333 }
334
335 err = db.UpsertProfile(tx, &profile)
336 case models.CommitOperationDelete:
337 err = db.DeleteArtifact(i.Db, db.FilterEq("did", did), db.FilterEq("rkey", e.Commit.RKey))
338 }
339
340 if err != nil {
341 return fmt.Errorf("failed to %s profile record: %w", e.Commit.Operation, err)
342 }
343
344 return nil
345}
346
347func (i *Ingester) ingestSpindleMember(ctx context.Context, e *models.Event) error {
348 did := e.Did
349 var err error
350
351 l := i.Logger.With("handler", "ingestSpindleMember")
352 l = l.With("nsid", e.Commit.Collection)
353
354 switch e.Commit.Operation {
355 case models.CommitOperationCreate:
356 raw := json.RawMessage(e.Commit.Record)
357 record := tangled.SpindleMember{}
358 err = json.Unmarshal(raw, &record)
359 if err != nil {
360 l.Error("invalid record", "err", err)
361 return err
362 }
363
364 // only spindle owner can invite to spindles
365 ok, err := i.Enforcer.IsSpindleInviteAllowed(did, record.Instance)
366 if err != nil || !ok {
367 return fmt.Errorf("failed to enforce permissions: %w", err)
368 }
369
370 memberId, err := i.IdResolver.ResolveIdent(ctx, record.Subject)
371 if err != nil {
372 return err
373 }
374
375 if memberId.Handle.IsInvalidHandle() {
376 return err
377 }
378
379 ddb, ok := i.Db.Execer.(*db.DB)
380 if !ok {
381 return fmt.Errorf("failed to index profile record, invalid db cast")
382 }
383
384 err = db.AddSpindleMember(ddb, db.SpindleMember{
385 Did: syntax.DID(did),
386 Rkey: e.Commit.RKey,
387 Instance: record.Instance,
388 Subject: memberId.DID,
389 })
390 if !ok {
391 return fmt.Errorf("failed to add to db: %w", err)
392 }
393
394 err = i.Enforcer.AddSpindleMember(record.Instance, memberId.DID.String())
395 if err != nil {
396 return fmt.Errorf("failed to update ACLs: %w", err)
397 }
398
399 l.Info("added spindle member")
400 case models.CommitOperationDelete:
401 rkey := e.Commit.RKey
402
403 ddb, ok := i.Db.Execer.(*db.DB)
404 if !ok {
405 return fmt.Errorf("failed to index profile record, invalid db cast")
406 }
407
408 // get record from db first
409 members, err := db.GetSpindleMembers(
410 ddb,
411 db.FilterEq("did", did),
412 db.FilterEq("rkey", rkey),
413 )
414 if err != nil || len(members) != 1 {
415 return fmt.Errorf("failed to get member: %w, len(members) = %d", err, len(members))
416 }
417 member := members[0]
418
419 tx, err := ddb.Begin()
420 if err != nil {
421 return fmt.Errorf("failed to start txn: %w", err)
422 }
423
424 // remove record by rkey && update enforcer
425 if err = db.RemoveSpindleMember(
426 tx,
427 db.FilterEq("did", did),
428 db.FilterEq("rkey", rkey),
429 ); err != nil {
430 return fmt.Errorf("failed to remove from db: %w", err)
431 }
432
433 // update enforcer
434 err = i.Enforcer.RemoveSpindleMember(member.Instance, member.Subject.String())
435 if err != nil {
436 return fmt.Errorf("failed to update ACLs: %w", err)
437 }
438
439 if err = tx.Commit(); err != nil {
440 return fmt.Errorf("failed to commit txn: %w", err)
441 }
442
443 if err = i.Enforcer.E.SavePolicy(); err != nil {
444 return fmt.Errorf("failed to save ACLs: %w", err)
445 }
446
447 l.Info("removed spindle member")
448 }
449
450 return nil
451}
452
453func (i *Ingester) ingestSpindle(ctx context.Context, e *models.Event) error {
454 did := e.Did
455 var err error
456
457 l := i.Logger.With("handler", "ingestSpindle")
458 l = l.With("nsid", e.Commit.Collection)
459
460 switch e.Commit.Operation {
461 case models.CommitOperationCreate:
462 raw := json.RawMessage(e.Commit.Record)
463 record := tangled.Spindle{}
464 err = json.Unmarshal(raw, &record)
465 if err != nil {
466 l.Error("invalid record", "err", err)
467 return err
468 }
469
470 instance := e.Commit.RKey
471
472 ddb, ok := i.Db.Execer.(*db.DB)
473 if !ok {
474 return fmt.Errorf("failed to index profile record, invalid db cast")
475 }
476
477 err := db.AddSpindle(ddb, db.Spindle{
478 Owner: syntax.DID(did),
479 Instance: instance,
480 })
481 if err != nil {
482 l.Error("failed to add spindle to db", "err", err, "instance", instance)
483 return err
484 }
485
486 err = serververify.RunVerification(ctx, instance, did, i.Config.Core.Dev)
487 if err != nil {
488 l.Error("failed to add spindle to db", "err", err, "instance", instance)
489 return err
490 }
491
492 _, err = serververify.MarkSpindleVerified(ddb, i.Enforcer, instance, did)
493 if err != nil {
494 return fmt.Errorf("failed to mark verified: %w", err)
495 }
496
497 return nil
498
499 case models.CommitOperationDelete:
500 instance := e.Commit.RKey
501
502 ddb, ok := i.Db.Execer.(*db.DB)
503 if !ok {
504 return fmt.Errorf("failed to index profile record, invalid db cast")
505 }
506
507 // get record from db first
508 spindles, err := db.GetSpindles(
509 ddb,
510 db.FilterEq("owner", did),
511 db.FilterEq("instance", instance),
512 )
513 if err != nil || len(spindles) != 1 {
514 return fmt.Errorf("failed to get spindles: %w, len(spindles) = %d", err, len(spindles))
515 }
516 spindle := spindles[0]
517
518 tx, err := ddb.Begin()
519 if err != nil {
520 return err
521 }
522 defer func() {
523 tx.Rollback()
524 i.Enforcer.E.LoadPolicy()
525 }()
526
527 // remove spindle members first
528 err = db.RemoveSpindleMember(
529 tx,
530 db.FilterEq("owner", did),
531 db.FilterEq("instance", instance),
532 )
533 if err != nil {
534 return err
535 }
536
537 err = db.DeleteSpindle(
538 tx,
539 db.FilterEq("owner", did),
540 db.FilterEq("instance", instance),
541 )
542 if err != nil {
543 return err
544 }
545
546 if spindle.Verified != nil {
547 err = i.Enforcer.RemoveSpindle(instance)
548 if err != nil {
549 return err
550 }
551 }
552
553 err = tx.Commit()
554 if err != nil {
555 return err
556 }
557
558 err = i.Enforcer.E.SavePolicy()
559 if err != nil {
560 return err
561 }
562 }
563
564 return nil
565}
566
567func (i *Ingester) ingestString(e *models.Event) error {
568 did := e.Did
569 rkey := e.Commit.RKey
570
571 var err error
572
573 l := i.Logger.With("handler", "ingestString", "nsid", e.Commit.Collection, "did", did, "rkey", rkey)
574 l.Info("ingesting record")
575
576 ddb, ok := i.Db.Execer.(*db.DB)
577 if !ok {
578 return fmt.Errorf("failed to index string record, invalid db cast")
579 }
580
581 switch e.Commit.Operation {
582 case models.CommitOperationCreate, models.CommitOperationUpdate:
583 raw := json.RawMessage(e.Commit.Record)
584 record := tangled.String{}
585 err = json.Unmarshal(raw, &record)
586 if err != nil {
587 l.Error("invalid record", "err", err)
588 return err
589 }
590
591 string := db.StringFromRecord(did, rkey, record)
592
593 if err = string.Validate(); err != nil {
594 l.Error("invalid record", "err", err)
595 return err
596 }
597
598 if err = db.AddString(ddb, string); err != nil {
599 l.Error("failed to add string", "err", err)
600 return err
601 }
602
603 return nil
604
605 case models.CommitOperationDelete:
606 if err := db.DeleteString(
607 ddb,
608 db.FilterEq("did", did),
609 db.FilterEq("rkey", rkey),
610 ); err != nil {
611 l.Error("failed to delete", "err", err)
612 return fmt.Errorf("failed to delete string record: %w", err)
613 }
614
615 return nil
616 }
617
618 return nil
619}
620
621func (i *Ingester) ingestKnotMember(e *models.Event) error {
622 did := e.Did
623 var err error
624
625 l := i.Logger.With("handler", "ingestKnotMember")
626 l = l.With("nsid", e.Commit.Collection)
627
628 switch e.Commit.Operation {
629 case models.CommitOperationCreate:
630 raw := json.RawMessage(e.Commit.Record)
631 record := tangled.KnotMember{}
632 err = json.Unmarshal(raw, &record)
633 if err != nil {
634 l.Error("invalid record", "err", err)
635 return err
636 }
637
638 // only knot owner can invite to knots
639 ok, err := i.Enforcer.IsKnotInviteAllowed(did, record.Domain)
640 if err != nil || !ok {
641 return fmt.Errorf("failed to enforce permissions: %w", err)
642 }
643
644 memberId, err := i.IdResolver.ResolveIdent(context.Background(), record.Subject)
645 if err != nil {
646 return err
647 }
648
649 if memberId.Handle.IsInvalidHandle() {
650 return err
651 }
652
653 err = i.Enforcer.AddKnotMember(record.Domain, memberId.DID.String())
654 if err != nil {
655 return fmt.Errorf("failed to update ACLs: %w", err)
656 }
657
658 l.Info("added knot member")
659 case models.CommitOperationDelete:
660 // we don't store knot members in a table (like we do for spindle)
661 // and we can't remove this just yet. possibly fixed if we switch
662 // to either:
663 // 1. a knot_members table like with spindle and store the rkey
664 // 2. use the knot host as the rkey
665 //
666 // TODO: implement member deletion
667 l.Info("skipping knot member delete", "did", did, "rkey", e.Commit.RKey)
668 }
669
670 return nil
671}
672
673func (i *Ingester) ingestKnot(e *models.Event) error {
674 did := e.Did
675 var err error
676
677 l := i.Logger.With("handler", "ingestKnot")
678 l = l.With("nsid", e.Commit.Collection)
679
680 switch e.Commit.Operation {
681 case models.CommitOperationCreate:
682 raw := json.RawMessage(e.Commit.Record)
683 record := tangled.Knot{}
684 err = json.Unmarshal(raw, &record)
685 if err != nil {
686 l.Error("invalid record", "err", err)
687 return err
688 }
689
690 domain := e.Commit.RKey
691
692 ddb, ok := i.Db.Execer.(*db.DB)
693 if !ok {
694 return fmt.Errorf("failed to index profile record, invalid db cast")
695 }
696
697 err := db.AddKnot(ddb, domain, did)
698 if err != nil {
699 l.Error("failed to add knot to db", "err", err, "domain", domain)
700 return err
701 }
702
703 err = serververify.RunVerification(context.Background(), domain, did, i.Config.Core.Dev)
704 if err != nil {
705 l.Error("failed to verify knot", "err", err, "domain", domain)
706 return err
707 }
708
709 err = serververify.MarkKnotVerified(ddb, i.Enforcer, domain, did)
710 if err != nil {
711 return fmt.Errorf("failed to mark verified: %w", err)
712 }
713
714 return nil
715
716 case models.CommitOperationDelete:
717 domain := e.Commit.RKey
718
719 ddb, ok := i.Db.Execer.(*db.DB)
720 if !ok {
721 return fmt.Errorf("failed to index knot record, invalid db cast")
722 }
723
724 // get record from db first
725 registrations, err := db.GetRegistrations(
726 ddb,
727 db.FilterEq("domain", domain),
728 db.FilterEq("did", did),
729 )
730 if err != nil {
731 return fmt.Errorf("failed to get registration: %w", err)
732 }
733 if len(registrations) != 1 {
734 return fmt.Errorf("got incorret number of registrations: %d, expected 1", len(registrations))
735 }
736 registration := registrations[0]
737
738 tx, err := ddb.Begin()
739 if err != nil {
740 return err
741 }
742 defer func() {
743 tx.Rollback()
744 i.Enforcer.E.LoadPolicy()
745 }()
746
747 err = db.DeleteKnot(
748 tx,
749 db.FilterEq("did", did),
750 db.FilterEq("domain", domain),
751 )
752 if err != nil {
753 return err
754 }
755
756 if registration.Registered != nil {
757 err = i.Enforcer.RemoveKnot(domain)
758 if err != nil {
759 return err
760 }
761 }
762
763 err = tx.Commit()
764 if err != nil {
765 return err
766 }
767
768 err = i.Enforcer.E.SavePolicy()
769 if err != nil {
770 return err
771 }
772 }
773
774 return nil
775}
776func (i *Ingester) ingestIssue(ctx context.Context, e *models.Event) error {
777 did := e.Did
778 rkey := e.Commit.RKey
779
780 var err error
781
782 l := i.Logger.With("handler", "ingestIssue", "nsid", e.Commit.Collection, "did", did, "rkey", rkey)
783 l.Info("ingesting record")
784
785 ddb, ok := i.Db.Execer.(*db.DB)
786 if !ok {
787 return fmt.Errorf("failed to index issue record, invalid db cast")
788 }
789
790 switch e.Commit.Operation {
791 case models.CommitOperationCreate:
792 raw := json.RawMessage(e.Commit.Record)
793 record := tangled.RepoIssue{}
794 err = json.Unmarshal(raw, &record)
795 if err != nil {
796 l.Error("invalid record", "err", err)
797 return err
798 }
799
800 issue := db.IssueFromRecord(did, rkey, record)
801
802 sanitizer := markup.NewSanitizer()
803 if st := strings.TrimSpace(sanitizer.SanitizeDescription(issue.Title)); st == "" {
804 return fmt.Errorf("title is empty after HTML sanitization")
805 }
806 if sb := strings.TrimSpace(sanitizer.SanitizeDefault(issue.Body)); sb == "" {
807 return fmt.Errorf("body is empty after HTML sanitization")
808 }
809
810 tx, err := ddb.BeginTx(ctx, nil)
811 if err != nil {
812 l.Error("failed to begin transaction", "err", err)
813 return err
814 }
815
816 err = db.NewIssue(tx, &issue)
817 if err != nil {
818 l.Error("failed to create issue", "err", err)
819 return err
820 }
821
822 return nil
823
824 case models.CommitOperationUpdate:
825 raw := json.RawMessage(e.Commit.Record)
826 record := tangled.RepoIssue{}
827 err = json.Unmarshal(raw, &record)
828 if err != nil {
829 l.Error("invalid record", "err", err)
830 return err
831 }
832
833 body := ""
834 if record.Body != nil {
835 body = *record.Body
836 }
837
838 sanitizer := markup.NewSanitizer()
839 if st := strings.TrimSpace(sanitizer.SanitizeDescription(record.Title)); st == "" {
840 return fmt.Errorf("title is empty after HTML sanitization")
841 }
842 if sb := strings.TrimSpace(sanitizer.SanitizeDefault(body)); sb == "" {
843 return fmt.Errorf("body is empty after HTML sanitization")
844 }
845
846 err = db.UpdateIssueByRkey(ddb, did, rkey, record.Title, body)
847 if err != nil {
848 l.Error("failed to update issue", "err", err)
849 return err
850 }
851
852 return nil
853
854 case models.CommitOperationDelete:
855 if err := db.DeleteIssueByRkey(ddb, did, rkey); err != nil {
856 l.Error("failed to delete", "err", err)
857 return fmt.Errorf("failed to delete issue record: %w", err)
858 }
859
860 return nil
861 }
862
863 return fmt.Errorf("unknown operation: %s", e.Commit.Operation)
864}