An atproto PDS written in Go
fork

Configure Feed

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

cleanup write records

authored by hailey.at and committed by

Tangled 0ed1a4e8 27d469e9

+92 -36
+1 -1
go.mod
··· 31 31 github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e 32 32 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b 33 33 golang.org/x/crypto v0.38.0 34 + gorm.io/driver/postgres v1.5.7 34 35 gorm.io/driver/sqlite v1.5.7 35 36 gorm.io/gorm v1.25.12 36 37 ) ··· 130 131 google.golang.org/protobuf v1.36.6 // indirect 131 132 gopkg.in/go-playground/assert.v1 v1.2.1 // indirect 132 133 gopkg.in/inf.v0 v0.9.1 // indirect 133 - gorm.io/driver/postgres v1.5.7 // indirect 134 134 lukechampine.com/blake3 v1.2.1 // indirect 135 135 )
+9 -7
server/handle_repo_apply_writes.go
··· 6 6 "github.com/labstack/echo/v4" 7 7 ) 8 8 9 - type ComAtprotoRepoApplyWritesRequest struct { 9 + type ComAtprotoRepoApplyWritesInput struct { 10 10 Repo string `json:"repo" validate:"required,atproto-did"` 11 11 Validate *bool `json:"bool,omitempty"` 12 12 Writes []ComAtprotoRepoApplyWritesItem `json:"writes"` ··· 20 20 Value *MarshalableMap `json:"value,omitempty"` 21 21 } 22 22 23 - type ComAtprotoRepoApplyWritesResponse struct { 23 + type ComAtprotoRepoApplyWritesOutput struct { 24 24 Commit RepoCommit `json:"commit"` 25 25 Results []ApplyWriteResult `json:"results"` 26 26 } 27 27 28 28 func (s *Server) handleApplyWrites(e echo.Context) error { 29 - repo := e.Get("repo").(*models.RepoActor) 29 + ctx := e.Request().Context() 30 30 31 - var req ComAtprotoRepoApplyWritesRequest 31 + var req ComAtprotoRepoApplyWritesInput 32 32 if err := e.Bind(&req); err != nil { 33 33 s.logger.Error("error binding", "error", err) 34 34 return helpers.ServerError(e, nil) ··· 39 39 return helpers.InputError(e, nil) 40 40 } 41 41 42 + repo := e.Get("repo").(*models.RepoActor) 43 + 42 44 if repo.Repo.Did != req.Repo { 43 45 s.logger.Warn("mismatched repo/auth") 44 46 return helpers.InputError(e, nil) 45 47 } 46 48 47 - ops := []Op{} 49 + ops := make([]Op, 0, len(req.Writes)) 48 50 for _, item := range req.Writes { 49 51 ops = append(ops, Op{ 50 52 Type: OpType(item.Type), ··· 54 56 }) 55 57 } 56 58 57 - results, err := s.repoman.applyWrites(repo.Repo, ops, req.SwapCommit) 59 + results, err := s.repoman.applyWrites(ctx, repo.Repo, ops, req.SwapCommit) 58 60 if err != nil { 59 61 s.logger.Error("error applying writes", "error", err) 60 62 return helpers.ServerError(e, nil) ··· 66 68 results[i].Commit = nil 67 69 } 68 70 69 - return e.JSON(200, ComAtprotoRepoApplyWritesResponse{ 71 + return e.JSON(200, ComAtprotoRepoApplyWritesOutput{ 70 72 Commit: commit, 71 73 Results: results, 72 74 })
+5 -3
server/handle_repo_create_record.go
··· 6 6 "github.com/labstack/echo/v4" 7 7 ) 8 8 9 - type ComAtprotoRepoCreateRecordRequest struct { 9 + type ComAtprotoRepoCreateRecordInput struct { 10 10 Repo string `json:"repo" validate:"required,atproto-did"` 11 11 Collection string `json:"collection" validate:"required,atproto-nsid"` 12 12 Rkey *string `json:"rkey,omitempty"` ··· 17 17 } 18 18 19 19 func (s *Server) handleCreateRecord(e echo.Context) error { 20 + ctx := e.Request().Context() 21 + 20 22 repo := e.Get("repo").(*models.RepoActor) 21 23 22 - var req ComAtprotoRepoCreateRecordRequest 24 + var req ComAtprotoRepoCreateRecordInput 23 25 if err := e.Bind(&req); err != nil { 24 26 s.logger.Error("error binding", "error", err) 25 27 return helpers.ServerError(e, nil) ··· 40 42 optype = OpTypeUpdate 41 43 } 42 44 43 - results, err := s.repoman.applyWrites(repo.Repo, []Op{ 45 + results, err := s.repoman.applyWrites(ctx, repo.Repo, []Op{ 44 46 { 45 47 Type: optype, 46 48 Collection: req.Collection,
+5 -3
server/handle_repo_delete_record.go
··· 6 6 "github.com/labstack/echo/v4" 7 7 ) 8 8 9 - type ComAtprotoRepoDeleteRecordRequest struct { 9 + type ComAtprotoRepoDeleteRecordInput struct { 10 10 Repo string `json:"repo" validate:"required,atproto-did"` 11 11 Collection string `json:"collection" validate:"required,atproto-nsid"` 12 12 Rkey string `json:"rkey" validate:"required,atproto-rkey"` ··· 15 15 } 16 16 17 17 func (s *Server) handleDeleteRecord(e echo.Context) error { 18 + ctx := e.Request().Context() 19 + 18 20 repo := e.Get("repo").(*models.RepoActor) 19 21 20 - var req ComAtprotoRepoDeleteRecordRequest 22 + var req ComAtprotoRepoDeleteRecordInput 21 23 if err := e.Bind(&req); err != nil { 22 24 s.logger.Error("error binding", "error", err) 23 25 return helpers.ServerError(e, nil) ··· 33 35 return helpers.InputError(e, nil) 34 36 } 35 37 36 - results, err := s.repoman.applyWrites(repo.Repo, []Op{ 38 + results, err := s.repoman.applyWrites(ctx, repo.Repo, []Op{ 37 39 { 38 40 Type: OpTypeDelete, 39 41 Collection: req.Collection,
+5 -3
server/handle_repo_put_record.go
··· 6 6 "github.com/labstack/echo/v4" 7 7 ) 8 8 9 - type ComAtprotoRepoPutRecordRequest struct { 9 + type ComAtprotoRepoPutRecordInput struct { 10 10 Repo string `json:"repo" validate:"required,atproto-did"` 11 11 Collection string `json:"collection" validate:"required,atproto-nsid"` 12 12 Rkey string `json:"rkey" validate:"required,atproto-rkey"` ··· 17 17 } 18 18 19 19 func (s *Server) handlePutRecord(e echo.Context) error { 20 + ctx := e.Request().Context() 21 + 20 22 repo := e.Get("repo").(*models.RepoActor) 21 23 22 - var req ComAtprotoRepoPutRecordRequest 24 + var req ComAtprotoRepoPutRecordInput 23 25 if err := e.Bind(&req); err != nil { 24 26 s.logger.Error("error binding", "error", err) 25 27 return helpers.ServerError(e, nil) ··· 40 42 optype = OpTypeUpdate 41 43 } 42 44 43 - results, err := s.repoman.applyWrites(repo.Repo, []Op{ 45 + results, err := s.repoman.applyWrites(ctx, repo.Repo, []Op{ 44 46 { 45 47 Type: optype, 46 48 Collection: req.Collection,
+3 -1
server/handle_sync_get_record.go
··· 13 13 ) 14 14 15 15 func (s *Server) handleSyncGetRecord(e echo.Context) error { 16 + ctx := e.Request().Context() 17 + 16 18 did := e.QueryParam("did") 17 19 collection := e.QueryParam("collection") 18 20 rkey := e.QueryParam("rkey") ··· 23 25 return helpers.ServerError(e, nil) 24 26 } 25 27 26 - root, blocks, err := s.repoman.getRecordProof(urepo, collection, rkey) 28 + root, blocks, err := s.repoman.getRecordProof(ctx, urepo, collection, rkey) 27 29 if err != nil { 28 30 return err 29 31 }
+64 -18
server/repo.go
··· 96 96 } 97 97 98 98 // TODO make use of swap commit 99 - func (rm *RepoMan) applyWrites(urepo models.Repo, writes []Op, swapCommit *string) ([]ApplyWriteResult, error) { 99 + func (rm *RepoMan) applyWrites(ctx context.Context, urepo models.Repo, writes []Op, swapCommit *string) ([]ApplyWriteResult, error) { 100 100 rootcid, err := cid.Cast(urepo.Root) 101 101 if err != nil { 102 102 return nil, err ··· 106 106 bs := recording_blockstore.New(dbs) 107 107 r, err := repo.OpenRepo(context.TODO(), bs, rootcid) 108 108 109 - entries := []models.Record{} 110 109 var results []ApplyWriteResult 111 110 111 + entries := make([]models.Record, 0, len(writes)) 112 112 for i, op := range writes { 113 + // updates or deletes must supply an rkey 113 114 if op.Type != OpTypeCreate && op.Rkey == nil { 114 115 return nil, fmt.Errorf("invalid rkey") 115 116 } else if op.Type == OpTypeCreate && op.Rkey != nil { 116 - _, _, err := r.GetRecord(context.TODO(), op.Collection+"/"+*op.Rkey) 117 + // we should conver this op to an update if the rkey already exists 118 + _, _, err := r.GetRecord(ctx, fmt.Sprintf("%s/%s", op.Collection, *op.Rkey)) 117 119 if err == nil { 118 120 op.Type = OpTypeUpdate 119 121 } 120 122 } else if op.Rkey == nil { 123 + // creates that don't supply an rkey will have one generated for them 121 124 op.Rkey = to.StringPtr(rm.clock.Next().String()) 122 125 writes[i].Rkey = op.Rkey 123 126 } 124 127 128 + // validate the record key is actually valid 125 129 _, err := syntax.ParseRecordKey(*op.Rkey) 126 130 if err != nil { 127 131 return nil, err ··· 129 133 130 134 switch op.Type { 131 135 case OpTypeCreate: 132 - j, err := json.Marshal(*op.Record) 136 + // HACK: this fixes some type conversions, mainly around integers 137 + // first we convert to json bytes 138 + b, err := json.Marshal(*op.Record) 133 139 if err != nil { 134 140 return nil, err 135 141 } 136 - out, err := atdata.UnmarshalJSON(j) 142 + // then we use atdata.UnmarshalJSON to convert it back to a map 143 + out, err := atdata.UnmarshalJSON(b) 137 144 if err != nil { 138 145 return nil, err 139 146 } 147 + // finally we can cast to a MarshalableMap 140 148 mm := MarshalableMap(out) 141 149 142 150 // HACK: if a record doesn't contain a $type, we can manually set it here based on the op's collection 151 + // i forget why this is actually necessary? 143 152 if mm["$type"] == "" { 144 153 mm["$type"] = op.Collection 145 154 } 146 155 147 - nc, err := r.PutRecord(context.TODO(), op.Collection+"/"+*op.Rkey, &mm) 156 + nc, err := r.PutRecord(ctx, fmt.Sprintf("%s/%s", op.Collection, *op.Rkey), &mm) 148 157 if err != nil { 149 158 return nil, err 150 159 } 160 + 151 161 d, err := atdata.MarshalCBOR(mm) 152 162 if err != nil { 153 163 return nil, err 154 164 } 165 + 155 166 entries = append(entries, models.Record{ 156 167 Did: urepo.Did, 157 168 CreatedAt: rm.clock.Next().String(), ··· 160 171 Cid: nc.String(), 161 172 Value: d, 162 173 }) 174 + 163 175 results = append(results, ApplyWriteResult{ 164 176 Type: to.StringPtr(OpTypeCreate.String()), 165 177 Uri: to.StringPtr("at://" + urepo.Did + "/" + op.Collection + "/" + *op.Rkey), ··· 167 179 ValidationStatus: to.StringPtr("valid"), // TODO: obviously this might not be true atm lol 168 180 }) 169 181 case OpTypeDelete: 182 + // try to find the old record in the database 170 183 var old models.Record 171 184 if err := rm.db.Raw("SELECT value FROM records WHERE did = ? AND nsid = ? AND rkey = ?", nil, urepo.Did, op.Collection, op.Rkey).Scan(&old).Error; err != nil { 172 185 return nil, err 173 186 } 187 + 188 + // TODO: this is really confusing, and looking at it i have no idea why i did this. below when we are doing deletes, we 189 + // check if `cid` here is nil to indicate if we should delete. that really doesn't make much sense and its super illogical 190 + // when reading this code. i dont feel like fixing right now though so 174 191 entries = append(entries, models.Record{ 175 192 Did: urepo.Did, 176 193 Nsid: op.Collection, 177 194 Rkey: *op.Rkey, 178 195 Value: old.Value, 179 196 }) 197 + 198 + // delete the record from the repo 180 199 err := r.DeleteRecord(context.TODO(), op.Collection+"/"+*op.Rkey) 181 200 if err != nil { 182 201 return nil, err 183 202 } 203 + 204 + // add a result for the delete 184 205 results = append(results, ApplyWriteResult{ 185 206 Type: to.StringPtr(OpTypeDelete.String()), 186 207 }) 187 208 case OpTypeUpdate: 188 - j, err := json.Marshal(*op.Record) 209 + // HACK: same hack as above for type fixes 210 + b, err := json.Marshal(*op.Record) 189 211 if err != nil { 190 212 return nil, err 191 213 } 192 - out, err := atdata.UnmarshalJSON(j) 214 + out, err := atdata.UnmarshalJSON(b) 193 215 if err != nil { 194 216 return nil, err 195 217 } 196 218 mm := MarshalableMap(out) 197 - nc, err := r.UpdateRecord(context.TODO(), op.Collection+"/"+*op.Rkey, &mm) 219 + 220 + nc, err := r.UpdateRecord(ctx, fmt.Sprintf("%s/%s", op.Collection, *op.Rkey), &mm) 198 221 if err != nil { 199 222 return nil, err 200 223 } 224 + 201 225 d, err := atdata.MarshalCBOR(mm) 202 226 if err != nil { 203 227 return nil, err 204 228 } 229 + 205 230 entries = append(entries, models.Record{ 206 231 Did: urepo.Did, 207 232 CreatedAt: rm.clock.Next().String(), ··· 210 235 Cid: nc.String(), 211 236 Value: d, 212 237 }) 238 + 213 239 results = append(results, ApplyWriteResult{ 214 240 Type: to.StringPtr(OpTypeUpdate.String()), 215 241 Uri: to.StringPtr("at://" + urepo.Did + "/" + op.Collection + "/" + *op.Rkey), ··· 219 245 } 220 246 } 221 247 222 - newroot, rev, err := r.Commit(context.TODO(), urepo.SignFor) 248 + // commit and get the new root 249 + newroot, rev, err := r.Commit(ctx, urepo.SignFor) 223 250 if err != nil { 224 251 return nil, err 225 252 } 226 253 254 + // create a buffer for dumping our new cbor into 227 255 buf := new(bytes.Buffer) 228 256 257 + // first write the car header to the buffer 229 258 hb, err := cbor.DumpObject(&car.CarHeader{ 230 259 Roots: []cid.Cid{newroot}, 231 260 Version: 1, 232 261 }) 233 - 234 262 if _, err := carstore.LdWrite(buf, hb); err != nil { 235 263 return nil, err 236 264 } 237 265 266 + // get a diff of the changes to the repo 238 267 diffops, err := r.DiffSince(context.TODO(), rootcid) 239 268 if err != nil { 240 269 return nil, err 241 270 } 242 271 272 + // create the repo ops for the given diff 243 273 ops := make([]*atproto.SyncSubscribeRepos_RepoOp, 0, len(diffops)) 244 - 245 274 for _, op := range diffops { 246 275 var c cid.Cid 247 276 switch op.Op { ··· 270 299 }) 271 300 } 272 301 273 - blk, err := dbs.Get(context.TODO(), c) 302 + blk, err := dbs.Get(ctx, c) 274 303 if err != nil { 275 304 return nil, err 276 305 } 277 306 307 + // write the block to the buffer 278 308 if _, err := carstore.LdWrite(buf, blk.Cid().Bytes(), blk.RawData()); err != nil { 279 309 return nil, err 280 310 } 281 311 } 282 312 313 + // write the writelog to the buffer 283 314 for _, op := range bs.GetWriteLog() { 284 315 if _, err := carstore.LdWrite(buf, op.Cid().Bytes(), op.RawData()); err != nil { 285 316 return nil, err 286 317 } 287 318 } 288 319 320 + // blob blob blob blob blob :3 289 321 var blobs []lexutil.LexLink 290 322 for _, entry := range entries { 291 323 var cids []cid.Cid 324 + // whenever there is cid present, we know it's a create (dumb) 292 325 if entry.Cid != "" { 293 326 if err := rm.s.db.Create(&entry, []clause.Expression{clause.OnConflict{ 294 327 Columns: []clause.Column{{Name: "did"}, {Name: "nsid"}, {Name: "rkey"}}, ··· 297 330 return nil, err 298 331 } 299 332 333 + // increment the given blob refs, yay 300 334 cids, err = rm.incrementBlobRefs(urepo, entry.Value) 301 335 if err != nil { 302 336 return nil, err 303 337 } 304 338 } else { 339 + // as i noted above this is dumb. but we delete whenever the cid is nil. it works solely becaue the pkey 340 + // is did + collection + rkey. i still really want to separate that out, or use a different type to make 341 + // this less confusing/easy to read. alas, its 2 am and yea no 305 342 if err := rm.s.db.Delete(&entry, nil).Error; err != nil { 306 343 return nil, err 307 344 } 345 + 346 + // TODO: 308 347 cids, err = rm.decrementBlobRefs(urepo, entry.Value) 309 348 if err != nil { 310 349 return nil, err 311 350 } 312 351 } 313 352 353 + // add all the relevant blobs to the blobs list of blobs. blob ^.^ 314 354 for _, c := range cids { 315 355 blobs = append(blobs, lexutil.LexLink(c)) 316 356 } 317 357 } 318 358 319 - rm.s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{ 359 + // NOTE: using the request ctx seems a bit suss here, so using a background context. i'm not sure if this 360 + // runs sync or not 361 + rm.s.evtman.AddEvent(context.Background(), &events.XRPCStreamEvent{ 320 362 RepoCommit: &atproto.SyncSubscribeRepos_Commit{ 321 363 Repo: urepo.Did, 322 364 Blocks: buf.Bytes(), ··· 330 372 }, 331 373 }) 332 374 333 - if err := rm.s.UpdateRepo(context.TODO(), urepo.Did, newroot, rev); err != nil { 375 + if err := rm.s.UpdateRepo(ctx, urepo.Did, newroot, rev); err != nil { 334 376 return nil, err 335 377 } 336 378 ··· 345 387 return results, nil 346 388 } 347 389 348 - func (rm *RepoMan) getRecordProof(urepo models.Repo, collection, rkey string) (cid.Cid, []blocks.Block, error) { 390 + // this is a fun little guy. to get a proof, we need to read the record out of the blockstore and record how we actually 391 + // got to the guy. we'll wrap a new blockstore in a recording blockstore, then return the log for proof 392 + func (rm *RepoMan) getRecordProof(ctx context.Context, urepo models.Repo, collection, rkey string) (cid.Cid, []blocks.Block, error) { 349 393 c, err := cid.Cast(urepo.Root) 350 394 if err != nil { 351 395 return cid.Undef, nil, err ··· 354 398 dbs := rm.s.getBlockstore(urepo.Did) 355 399 bs := recording_blockstore.New(dbs) 356 400 357 - r, err := repo.OpenRepo(context.TODO(), bs, c) 401 + r, err := repo.OpenRepo(ctx, bs, c) 358 402 if err != nil { 359 403 return cid.Undef, nil, err 360 404 } 361 405 362 - _, _, err = r.GetRecordBytes(context.TODO(), collection+"/"+rkey) 406 + _, _, err = r.GetRecordBytes(ctx, fmt.Sprintf("%s/%s", collection, rkey)) 363 407 if err != nil { 364 408 return cid.Undef, nil, err 365 409 } ··· 397 441 return nil, err 398 442 } 399 443 444 + // TODO: this does _not_ handle deletions of blobs that are on s3 storage!!!! we need to get the blob, see what 445 + // storage it is in, and clean up s3!!!! 400 446 if res.Count == 0 { 401 447 if err := rm.db.Exec("DELETE FROM blobs WHERE id = ?", nil, res.ID).Error; err != nil { 402 448 return nil, err