Mirror of @tangled.org/core. Running on a Raspberry Pi Zero 2 (Please be gentle).

appview: refactor ingester, ingest spindle records

Signed-off-by: oppiliappan <me@oppi.li>

oppi.li 460e1816 f1a8e659

verified
+221 -100
+209 -99
appview/ingester.go
··· 3 3 import ( 4 4 "context" 5 5 "encoding/json" 6 - "errors" 7 6 "fmt" 8 - "io" 9 - "log" 10 - "net/http" 11 - "strings" 7 + "log/slog" 12 8 "time" 13 9 14 10 "github.com/bluesky-social/indigo/atproto/syntax" ··· 12 16 "github.com/go-git/go-git/v5/plumbing" 13 17 "github.com/ipfs/go-cid" 14 18 "tangled.sh/tangled.sh/core/api/tangled" 19 + "tangled.sh/tangled.sh/core/appview/config" 15 20 "tangled.sh/tangled.sh/core/appview/db" 21 + "tangled.sh/tangled.sh/core/appview/idresolver" 22 + "tangled.sh/tangled.sh/core/appview/spindleverify" 16 23 "tangled.sh/tangled.sh/core/rbac" 17 24 ) 18 25 19 - type Ingester func(ctx context.Context, e *models.Event) error 26 + type Ingester struct { 27 + Db db.DbWrapper 28 + Enforcer *rbac.Enforcer 29 + IdResolver *idresolver.Resolver 30 + Config *config.Config 31 + Logger *slog.Logger 32 + } 20 33 21 - func Ingest(d db.DbWrapper, enforcer *rbac.Enforcer) Ingester { 34 + type processFunc func(ctx context.Context, e *models.Event) error 35 + 36 + func (i *Ingester) Ingest() processFunc { 22 37 return func(ctx context.Context, e *models.Event) error { 23 38 var err error 24 39 defer func() { 25 40 eventTime := e.TimeUS 26 41 lastTimeUs := eventTime + 1 27 - if err := d.SaveLastTimeUs(lastTimeUs); err != nil { 42 + if err := i.Db.SaveLastTimeUs(lastTimeUs); err != nil { 28 43 err = fmt.Errorf("(deferred) failed to save last time us: %w", err) 29 44 } 30 45 }() ··· 46 39 47 40 switch e.Commit.Collection { 48 41 case tangled.GraphFollowNSID: 49 - ingestFollow(&d, e) 42 + err = i.ingestFollow(e) 50 43 case tangled.FeedStarNSID: 51 - ingestStar(&d, e) 44 + err = i.ingestStar(e) 52 45 case tangled.PublicKeyNSID: 53 - ingestPublicKey(&d, e) 46 + err = i.ingestPublicKey(e) 54 47 case tangled.RepoArtifactNSID: 55 - ingestArtifact(&d, e, enforcer) 48 + err = i.ingestArtifact(e) 56 49 case tangled.ActorProfileNSID: 57 - ingestProfile(&d, e) 50 + err = i.ingestProfile(e) 58 51 case tangled.SpindleMemberNSID: 59 - ingestSpindleMember(&d, e, enforcer) 52 + err = i.ingestSpindleMember(e) 60 53 case tangled.SpindleNSID: 61 - ingestSpindle(&d, e, true) // TODO: change this to dynamic 54 + err = i.ingestSpindle(e) 55 + } 56 + 57 + if err != nil { 58 + l := i.Logger.With("nsid", e.Commit.Collection) 59 + l.Error("error ingesting record", "err", err) 62 60 } 63 61 64 62 return err 65 63 } 66 64 } 67 65 68 - func ingestStar(d *db.DbWrapper, e *models.Event) error { 66 + func (i *Ingester) ingestStar(e *models.Event) error { 69 67 var err error 70 68 did := e.Did 69 + 70 + l := i.Logger.With("handler", "ingestStar") 71 + l = l.With("nsid", e.Commit.Collection) 71 72 72 73 switch e.Commit.Operation { 73 74 case models.CommitOperationCreate, models.CommitOperationUpdate: ··· 85 70 record := tangled.FeedStar{} 86 71 err := json.Unmarshal(raw, &record) 87 72 if err != nil { 88 - log.Println("invalid record") 73 + l.Error("invalid record", "err", err) 89 74 return err 90 75 } 91 76 92 77 subjectUri, err = syntax.ParseATURI(record.Subject) 93 78 if err != nil { 94 - log.Println("invalid record") 79 + l.Error("invalid record", "err", err) 95 80 return err 96 81 } 97 - err = db.AddStar(d, did, subjectUri, e.Commit.RKey) 82 + err = db.AddStar(i.Db, did, subjectUri, e.Commit.RKey) 98 83 case models.CommitOperationDelete: 99 - err = db.DeleteStarByRkey(d, did, e.Commit.RKey) 84 + err = db.DeleteStarByRkey(i.Db, did, e.Commit.RKey) 100 85 } 101 86 102 87 if err != nil { ··· 106 91 return nil 107 92 } 108 93 109 - func ingestFollow(d *db.DbWrapper, e *models.Event) error { 94 + func (i *Ingester) ingestFollow(e *models.Event) error { 110 95 var err error 111 96 did := e.Did 97 + 98 + l := i.Logger.With("handler", "ingestFollow") 99 + l = l.With("nsid", e.Commit.Collection) 112 100 113 101 switch e.Commit.Operation { 114 102 case models.CommitOperationCreate, models.CommitOperationUpdate: ··· 119 101 record := tangled.GraphFollow{} 120 102 err = json.Unmarshal(raw, &record) 121 103 if err != nil { 122 - log.Println("invalid record") 104 + l.Error("invalid record", "err", err) 123 105 return err 124 106 } 125 107 126 108 subjectDid := record.Subject 127 - err = db.AddFollow(d, did, subjectDid, e.Commit.RKey) 109 + err = db.AddFollow(i.Db, did, subjectDid, e.Commit.RKey) 128 110 case models.CommitOperationDelete: 129 - err = db.DeleteFollowByRkey(d, did, e.Commit.RKey) 111 + err = db.DeleteFollowByRkey(i.Db, did, e.Commit.RKey) 130 112 } 131 113 132 114 if err != nil { ··· 136 118 return nil 137 119 } 138 120 139 - func ingestPublicKey(d *db.DbWrapper, e *models.Event) error { 121 + func (i *Ingester) ingestPublicKey(e *models.Event) error { 140 122 did := e.Did 141 123 var err error 142 124 125 + l := i.Logger.With("handler", "ingestPublicKey") 126 + l = l.With("nsid", e.Commit.Collection) 127 + 143 128 switch e.Commit.Operation { 144 129 case models.CommitOperationCreate, models.CommitOperationUpdate: 145 - log.Println("processing add of pubkey") 130 + l.Debug("processing add of pubkey") 146 131 raw := json.RawMessage(e.Commit.Record) 147 132 record := tangled.PublicKey{} 148 133 err = json.Unmarshal(raw, &record) 149 134 if err != nil { 150 - log.Printf("invalid record: %s", err) 135 + l.Error("invalid record", "err", err) 151 136 return err 152 137 } 153 138 154 139 name := record.Name 155 140 key := record.Key 156 - err = db.AddPublicKey(d, did, name, key, e.Commit.RKey) 141 + err = db.AddPublicKey(i.Db, did, name, key, e.Commit.RKey) 157 142 case models.CommitOperationDelete: 158 - log.Println("processing delete of pubkey") 159 - err = db.DeletePublicKeyByRkey(d, did, e.Commit.RKey) 143 + l.Debug("processing delete of pubkey") 144 + err = db.DeletePublicKeyByRkey(i.Db, did, e.Commit.RKey) 160 145 } 161 146 162 147 if err != nil { ··· 169 148 return nil 170 149 } 171 150 172 - func ingestArtifact(d *db.DbWrapper, e *models.Event, enforcer *rbac.Enforcer) error { 151 + func (i *Ingester) ingestArtifact(e *models.Event) error { 173 152 did := e.Did 174 153 var err error 154 + 155 + l := i.Logger.With("handler", "ingestArtifact") 156 + l = l.With("nsid", e.Commit.Collection) 175 157 176 158 switch e.Commit.Operation { 177 159 case models.CommitOperationCreate, models.CommitOperationUpdate: ··· 182 158 record := tangled.RepoArtifact{} 183 159 err = json.Unmarshal(raw, &record) 184 160 if err != nil { 185 - log.Printf("invalid record: %s", err) 161 + l.Error("invalid record", "err", err) 186 162 return err 187 163 } 188 164 ··· 191 167 return err 192 168 } 193 169 194 - repo, err := db.GetRepoByAtUri(d, repoAt.String()) 170 + repo, err := db.GetRepoByAtUri(i.Db, repoAt.String()) 195 171 if err != nil { 196 172 return err 197 173 } 198 174 199 - ok, err := enforcer.E.Enforce(did, repo.Knot, repo.DidSlashRepo(), "repo:push") 175 + ok, err := i.Enforcer.E.Enforce(did, repo.Knot, repo.DidSlashRepo(), "repo:push") 200 176 if err != nil || !ok { 201 177 return err 202 178 } ··· 218 194 MimeType: record.Artifact.MimeType, 219 195 } 220 196 221 - err = db.AddArtifact(d, artifact) 197 + err = db.AddArtifact(i.Db, artifact) 222 198 case models.CommitOperationDelete: 223 - err = db.DeleteArtifact(d, db.FilterEq("did", did), db.FilterEq("rkey", e.Commit.RKey)) 199 + err = db.DeleteArtifact(i.Db, db.FilterEq("did", did), db.FilterEq("rkey", e.Commit.RKey)) 224 200 } 225 201 226 202 if err != nil { ··· 230 206 return nil 231 207 } 232 208 233 - func ingestProfile(d *db.DbWrapper, e *models.Event) error { 209 + func (i *Ingester) ingestProfile(e *models.Event) error { 234 210 did := e.Did 235 211 var err error 212 + 213 + l := i.Logger.With("handler", "ingestProfile") 214 + l = l.With("nsid", e.Commit.Collection) 236 215 237 216 if e.Commit.RKey != "self" { 238 217 return fmt.Errorf("ingestProfile only ingests `self` record") ··· 247 220 record := tangled.ActorProfile{} 248 221 err = json.Unmarshal(raw, &record) 249 222 if err != nil { 250 - log.Printf("invalid record: %s", err) 223 + l.Error("invalid record", "err", err) 251 224 return err 252 225 } 253 226 ··· 294 267 PinnedRepos: pinned, 295 268 } 296 269 297 - ddb, ok := d.Execer.(*db.DB) 270 + ddb, ok := i.Db.Execer.(*db.DB) 298 271 if !ok { 299 272 return fmt.Errorf("failed to index profile record, invalid db cast") 300 273 } ··· 311 284 312 285 err = db.UpsertProfile(tx, &profile) 313 286 case models.CommitOperationDelete: 314 - err = db.DeleteArtifact(d, db.FilterEq("did", did), db.FilterEq("rkey", e.Commit.RKey)) 287 + err = db.DeleteArtifact(i.Db, db.FilterEq("did", did), db.FilterEq("rkey", e.Commit.RKey)) 315 288 } 316 289 317 290 if err != nil { ··· 321 294 return nil 322 295 } 323 296 324 - func ingestSpindleMember(_ *db.DbWrapper, e *models.Event, enforcer *rbac.Enforcer) error { 297 + func (i *Ingester) ingestSpindleMember(e *models.Event) error { 325 298 did := e.Did 326 299 var err error 300 + 301 + l := i.Logger.With("handler", "ingestSpindleMember") 302 + l = l.With("nsid", e.Commit.Collection) 327 303 328 304 switch e.Commit.Operation { 329 305 case models.CommitOperationCreate: ··· 334 304 record := tangled.SpindleMember{} 335 305 err = json.Unmarshal(raw, &record) 336 306 if err != nil { 337 - log.Printf("invalid record: %s", err) 307 + l.Error("invalid record", "err", err) 338 308 return err 339 309 } 340 310 341 311 // only spindle owner can invite to spindles 342 - ok, err := enforcer.IsSpindleInviteAllowed(did, record.Instance) 312 + ok, err := i.Enforcer.IsSpindleInviteAllowed(did, record.Instance) 343 313 if err != nil || !ok { 344 314 return fmt.Errorf("failed to enforce permissions: %w", err) 345 315 } 346 316 347 - err = enforcer.AddSpindleMember(record.Instance, record.Subject) 317 + memberId, err := i.IdResolver.ResolveIdent(context.Background(), record.Subject) 348 318 if err != nil { 349 - return fmt.Errorf("failed to add member: %w", err) 319 + return err 320 + } 321 + 322 + if memberId.Handle.IsInvalidHandle() { 323 + return err 324 + } 325 + 326 + ddb, ok := i.Db.Execer.(*db.DB) 327 + if !ok { 328 + return fmt.Errorf("failed to index profile record, invalid db cast") 329 + } 330 + 331 + err = db.AddSpindleMember(ddb, db.SpindleMember{ 332 + Did: syntax.DID(did), 333 + Rkey: e.Commit.RKey, 334 + Instance: record.Instance, 335 + Subject: memberId.DID, 336 + }) 337 + if !ok { 338 + return fmt.Errorf("failed to add to db: %w", err) 339 + } 340 + 341 + err = i.Enforcer.AddSpindleMember(record.Instance, memberId.DID.String()) 342 + if err != nil { 343 + return fmt.Errorf("failed to update ACLs: %w", err) 344 + } 345 + case models.CommitOperationDelete: 346 + rkey := e.Commit.RKey 347 + 348 + ddb, ok := i.Db.Execer.(*db.DB) 349 + if !ok { 350 + return fmt.Errorf("failed to index profile record, invalid db cast") 351 + } 352 + 353 + // get record from db first 354 + members, err := db.GetSpindleMembers( 355 + ddb, 356 + db.FilterEq("did", did), 357 + db.FilterEq("rkey", rkey), 358 + ) 359 + if err != nil || len(members) != 1 { 360 + return fmt.Errorf("failed to get member: %w, len(members) = %d", err, len(members)) 361 + } 362 + member := members[0] 363 + 364 + tx, err := ddb.Begin() 365 + if err != nil { 366 + return fmt.Errorf("failed to start txn: %w", err) 367 + } 368 + 369 + // remove record by rkey && update enforcer 370 + if err = db.RemoveSpindleMember( 371 + tx, 372 + db.FilterEq("did", did), 373 + db.FilterEq("rkey", rkey), 374 + ); err != nil { 375 + return fmt.Errorf("failed to remove from db: %w", err) 376 + } 377 + 378 + // update enforcer 379 + err = i.Enforcer.RemoveSpindleMember(member.Instance, member.Subject.String()) 380 + if err != nil { 381 + return fmt.Errorf("failed to update ACLs: %w", err) 382 + } 383 + 384 + if err = tx.Commit(); err != nil { 385 + return fmt.Errorf("failed to commit txn: %w", err) 386 + } 387 + 388 + if err = i.Enforcer.E.SavePolicy(); err != nil { 389 + return fmt.Errorf("failed to save ACLs: %w", err) 350 390 } 351 391 } 352 392 353 393 return nil 354 394 } 355 395 356 - func ingestSpindle(d *db.DbWrapper, e *models.Event, dev bool) error { 396 + func (i *Ingester) ingestSpindle(e *models.Event) error { 357 397 did := e.Did 358 398 var err error 399 + 400 + l := i.Logger.With("handler", "ingestSpindle") 401 + l = l.With("nsid", e.Commit.Collection) 359 402 360 403 switch e.Commit.Operation { 361 404 case models.CommitOperationCreate: ··· 436 333 record := tangled.Spindle{} 437 334 err = json.Unmarshal(raw, &record) 438 335 if err != nil { 439 - log.Printf("invalid record: %s", err) 336 + l.Error("invalid record", "err", err) 440 337 return err 441 338 } 442 339 443 - // this is a special record whose rkey is the instance of the spindle itself 444 340 instance := e.Commit.RKey 445 341 446 - owner, err := fetchOwner(context.TODO(), instance, dev) 447 - if err != nil { 448 - log.Printf("failed to verify owner of %s: %s", instance, err) 449 - return err 450 - } 451 - 452 - // verify that the spindle owner points back to this did 453 - if owner != did { 454 - log.Printf("incorrect owner for domain: %s, %s != %s", instance, owner, did) 455 - return err 456 - } 457 - 458 - // mark this spindle as registered 459 - ddb, ok := d.Execer.(*db.DB) 342 + ddb, ok := i.Db.Execer.(*db.DB) 460 343 if !ok { 461 344 return fmt.Errorf("failed to index profile record, invalid db cast") 462 345 } 463 346 464 - _, err = db.VerifySpindle( 465 - ddb, 347 + err := db.AddSpindle(ddb, db.Spindle{ 348 + Owner: syntax.DID(did), 349 + Instance: instance, 350 + }) 351 + if err != nil { 352 + l.Error("failed to add spindle to db", "err", err, "instance", instance) 353 + return err 354 + } 355 + 356 + err = spindleverify.RunVerification(context.Background(), instance, did, i.Config.Core.Dev) 357 + if err != nil { 358 + l.Error("failed to add spindle to db", "err", err, "instance", instance) 359 + return err 360 + } 361 + 362 + _, err = spindleverify.MarkVerified(ddb, i.Enforcer, instance, did) 363 + if err != nil { 364 + return fmt.Errorf("failed to mark verified: %w", err) 365 + } 366 + 367 + return nil 368 + 369 + case models.CommitOperationDelete: 370 + instance := e.Commit.RKey 371 + 372 + ddb, ok := i.Db.Execer.(*db.DB) 373 + if !ok { 374 + return fmt.Errorf("failed to index profile record, invalid db cast") 375 + } 376 + 377 + tx, err := ddb.Begin() 378 + if err != nil { 379 + return err 380 + } 381 + defer func() { 382 + tx.Rollback() 383 + i.Enforcer.E.LoadPolicy() 384 + }() 385 + 386 + err = db.DeleteSpindle( 387 + tx, 466 388 db.FilterEq("owner", did), 467 389 db.FilterEq("instance", instance), 468 390 ) 391 + if err != nil { 392 + return err 393 + } 469 394 470 - return err 395 + err = i.Enforcer.RemoveSpindle(instance) 396 + if err != nil { 397 + return err 398 + } 399 + 400 + err = tx.Commit() 401 + if err != nil { 402 + return err 403 + } 404 + 405 + err = i.Enforcer.E.SavePolicy() 406 + if err != nil { 407 + return err 408 + } 471 409 } 472 410 473 411 return nil 474 - } 475 - 476 - func fetchOwner(ctx context.Context, domain string, dev bool) (string, error) { 477 - scheme := "https" 478 - if dev { 479 - scheme = "http" 480 - } 481 - 482 - url := fmt.Sprintf("%s://%s/owner", scheme, domain) 483 - req, err := http.NewRequest("GET", url, nil) 484 - if err != nil { 485 - return "", err 486 - } 487 - 488 - client := &http.Client{ 489 - Timeout: 1 * time.Second, 490 - } 491 - 492 - resp, err := client.Do(req.WithContext(ctx)) 493 - if err != nil || resp.StatusCode != 200 { 494 - return "", errors.New("failed to fetch /owner") 495 - } 496 - 497 - body, err := io.ReadAll(io.LimitReader(resp.Body, 1024)) // read atmost 1kb of data 498 - if err != nil { 499 - return "", fmt.Errorf("failed to read /owner response: %w", err) 500 - } 501 - 502 - did := strings.TrimSpace(string(body)) 503 - if did == "" { 504 - return "", errors.New("empty DID in /owner response") 505 - } 506 - 507 - return did, nil 508 412 }
+12 -1
appview/state/state.go
··· 31 31 "tangled.sh/tangled.sh/core/eventconsumer" 32 32 "tangled.sh/tangled.sh/core/jetstream" 33 33 "tangled.sh/tangled.sh/core/knotclient" 34 + tlog "tangled.sh/tangled.sh/core/log" 34 35 "tangled.sh/tangled.sh/core/rbac" 35 36 ) 36 37 ··· 94 93 tangled.PublicKeyNSID, 95 94 tangled.RepoArtifactNSID, 96 95 tangled.ActorProfileNSID, 96 + tangled.SpindleMemberNSID, 97 + tangled.SpindleNSID, 97 98 }, 98 99 nil, 99 100 slog.Default(), ··· 109 106 if err != nil { 110 107 return nil, fmt.Errorf("failed to create jetstream client: %w", err) 111 108 } 112 - err = jc.StartJetstream(ctx, appview.Ingest(wrapper, enforcer)) 109 + 110 + ingester := appview.Ingester{ 111 + Db: wrapper, 112 + Enforcer: enforcer, 113 + IdResolver: res, 114 + Config: config, 115 + Logger: tlog.New("ingester"), 116 + } 117 + err = jc.StartJetstream(ctx, ingester.Ingest()) 113 118 if err != nil { 114 119 return nil, fmt.Errorf("failed to start jetstream watcher: %w", err) 115 120 }