Monorepo for Tangled tangled.org
2

Configure Feed

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

appview: ingest cid of `sh.tangled.string` records

the `cid` column is nullable for now, missing ones will be filled later.

Signed-off-by: Seongmin Lee <git@boltless.me>

author
Seongmin Lee
date (Jun 16, 2026, 11:33 PM +0900) commit 5129d588 parent 2359fa0c change-id wrvnywpv
+77 -40
+7
appview/db/db.go
··· 2218 2218 return err 2219 2219 }) 2220 2220 2221 + orm.RunMigration(conn, logger, "add-cid-to-strings", func(tx *sql.Tx) error { 2222 + _, err := tx.Exec(` 2223 + alter table strings add column cid text; 2224 + `) 2225 + return err 2226 + }) 2227 + 2221 2228 return &DB{ 2222 2229 db, 2223 2230 logger,
+18 -12
appview/db/strings.go
··· 7 7 "strings" 8 8 "time" 9 9 10 + "github.com/bluesky-social/indigo/atproto/syntax" 10 11 "tangled.org/core/appview/models" 11 12 "tangled.org/core/orm" 12 13 ) ··· 16 17 `insert into strings ( 17 18 did, 18 19 rkey, 20 + cid, 19 21 filename, 20 22 description, 21 23 content, 22 24 created, 23 25 edited 24 26 ) 25 - values (?, ?, ?, ?, ?, ?, null) 27 + values (?, ?, ?, ?, ?, ?, ?, null) 26 28 on conflict(did, rkey) do update set 27 - filename = excluded.filename, 29 + cid = excluded.cid, 30 + filename = excluded.filename, 28 31 description = excluded.description, 29 - content = excluded.content, 30 - edited = case 31 - when 32 - strings.content != excluded.content 33 - or strings.filename != excluded.filename 34 - or strings.description != excluded.description then ? 35 - else strings.edited 36 - end`, 32 + content = excluded.content, 33 + edited = case when strings.cid is not null then ? else strings.edited end 34 + where strings.cid is not excluded.cid`, 37 35 s.Did, 38 36 s.Rkey, 37 + s.Cid, 39 38 s.Filename, 40 39 s.Description, 41 40 s.Contents, ··· 68 67 query := fmt.Sprintf(`select 69 68 did, 70 69 rkey, 70 + cid, 71 71 filename, 72 72 description, 73 73 content, ··· 91 91 for rows.Next() { 92 92 var s models.String 93 93 var createdAt string 94 - var editedAt sql.NullString 94 + var cid, editedAt sql.Null[string] 95 95 96 96 if err := rows.Scan( 97 97 &s.Did, 98 98 &s.Rkey, 99 + &cid, 99 100 &s.Filename, 100 101 &s.Description, 101 102 &s.Contents, ··· 105 106 return nil, err 106 107 } 107 108 109 + if cid.Valid { 110 + s.Cid = new(syntax.CID) 111 + *s.Cid = syntax.CID(cid.V) 112 + } 113 + 108 114 s.Created, err = time.Parse(time.RFC3339, createdAt) 109 115 if err != nil { 110 116 s.Created = time.Now() 111 117 } 112 118 113 119 if editedAt.Valid { 114 - e, err := time.Parse(time.RFC3339, editedAt.String) 120 + e, err := time.Parse(time.RFC3339, editedAt.V) 115 121 if err != nil { 116 122 e = time.Now() 117 123 }
+2 -1
appview/ingester.go
··· 78 78 "nsid", e.Commit.Collection, 79 79 "did", e.Did, 80 80 "rkey", e.Commit.RKey, 81 + "cid", e.Commit.CID, 81 82 "op", e.Commit.Operation, 82 83 ) 83 84 switch e.Commit.Collection { ··· 930 931 return err 931 932 } 932 933 933 - string := models.StringFromRecord(did, rkey, record) 934 + string := models.StringFromRecord(syntax.DID(did), syntax.RecordKey(rkey), syntax.CID(e.Commit.CID), record) 934 935 935 936 if err = i.Validator.ValidateString(&string); err != nil { 936 937 l.Error("invalid record", "err", err)
+18
appview/ingester_string_test.go
··· 1 1 package appview 2 2 3 3 import ( 4 + "bytes" 4 5 "encoding/json" 5 6 "log/slog" 6 7 "path/filepath" ··· 9 10 "time" 10 11 11 12 jmodels "github.com/bluesky-social/jetstream/pkg/models" 13 + "github.com/ipfs/go-cid" 14 + "github.com/multiformats/go-multihash" 15 + cbg "github.com/whyrusleeping/cbor-gen" 12 16 "tangled.org/core/api/tangled" 13 17 "tangled.org/core/appview/db" 14 18 "tangled.org/core/appview/models" ··· 45 49 Collection: tangled.StringNSID, 46 50 RKey: rkey, 47 51 Record: raw, 52 + CID: makeCID(t, &record), 48 53 }, 49 54 } 55 + } 56 + 57 + func makeCID(t *testing.T, record cbg.CBORMarshaler) string { 58 + buf := new(bytes.Buffer) 59 + if err := record.MarshalCBOR(buf); err != nil { 60 + t.Fatalf("cbor marshal record: %v", err) 61 + } 62 + c, err := cid.NewPrefixV1(cid.DagCBOR, multihash.SHA2_256).Sum(buf.Bytes()) 63 + if err != nil { 64 + t.Fatalf("cid: sum: %v", err) 65 + } 66 + return c.String() 50 67 } 51 68 52 69 func loadString(t *testing.T, ing *Ingester, did, rkey string) (models.String, bool) { ··· 178 195 Operation: jmodels.CommitOperationDelete, 179 196 Collection: tangled.StringNSID, 180 197 RKey: "rk1", 198 + CID: makeCID(t, &rec), 181 199 }, 182 200 } 183 201 if err := ing.ingestString(del, ing.Logger); err != nil {
+7 -5
appview/models/string.go
··· 13 13 14 14 type String struct { 15 15 Did syntax.DID 16 - Rkey string 16 + Rkey syntax.RecordKey 17 + Cid *syntax.CID 17 18 18 19 Filename string 19 20 Description string ··· 26 27 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", s.Did, tangled.StringNSID, s.Rkey)) 27 28 } 28 29 29 - func (s *String) AsRecord() tangled.String { 30 - return tangled.String{ 30 + func (s *String) AsRecord() *tangled.String { 31 + return &tangled.String{ 31 32 Filename: s.Filename, 32 33 Description: s.Description, 33 34 Contents: s.Contents, ··· 35 36 } 36 37 } 37 38 38 - func StringFromRecord(did, rkey string, record tangled.String) String { 39 + func StringFromRecord(did syntax.DID, rkey syntax.RecordKey, cid syntax.CID, record tangled.String) String { 39 40 created, err := time.Parse(time.RFC3339, record.CreatedAt) 40 41 if err != nil { 41 42 created = time.Now() 42 43 } 43 44 return String{ 44 - Did: syntax.DID(did), 45 + Did: did, 45 46 Rkey: rkey, 47 + Cid: &cid, 46 48 Filename: record.Filename, 47 49 Description: record.Description, 48 50 Contents: record.Contents,
+1
appview/pages/templates/strings/string.html
··· 121 121 class="group/form" 122 122 > 123 123 <input name="subject-uri" type="hidden" value="{{ .String.AtUri }}"> 124 + <input name="subject-cid" type="hidden" value="{{ with .String.Cid }}{{ . }}{{ end }}"> 124 125 <div class="bg-white dark:bg-gray-800 rounded drop-shadow-sm py-4 px-4 relative w-full"> 125 126 <div class="text-sm pb-2 text-gray-500 dark:text-gray-400"> 126 127 {{ template "user/fragments/picHandleLink" .LoggedInUser.Did }}
+1 -1
appview/pages/url.go
··· 109 109 if err == nil && !ownerIdentity.Handle.IsInvalidHandle() { 110 110 owner = ownerIdentity.Handle.String() 111 111 } 112 - return path.Join("/strings", owner, string_.Rkey), nil 112 + return path.Join("/strings", owner, string_.Rkey.String()), nil 113 113 }
+23 -21
appview/strings/strings.go
··· 297 297 Created: first.Created, 298 298 } 299 299 300 - record := entry.AsRecord() 301 - 302 300 client, err := s.OAuth.AuthorizedClient(r) 303 301 if err != nil { 304 302 fail("Failed to create record.", err) ··· 306 304 } 307 305 308 306 // first replace the existing record in the PDS 309 - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.StringNSID, entry.Did.String(), entry.Rkey) 310 - if err != nil { 311 - fail("Failed to updated existing record.", err) 312 - return 307 + var exCid string 308 + if entry.Cid != nil { 309 + exCid = entry.Cid.String() 310 + } else { 311 + ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.StringNSID, entry.Did.String(), entry.Rkey.String()) 312 + if err != nil { 313 + fail("Failed to get existing record.", err) 314 + return 315 + } 316 + if ex.Cid == nil { 317 + fail("Failed to get existing record.", err) 318 + return 319 + } 320 + exCid = *ex.Cid 313 321 } 314 322 resp, err := comatproto.RepoPutRecord(r.Context(), client, &atproto.RepoPutRecord_Input{ 315 323 Collection: tangled.StringNSID, 316 324 Repo: entry.Did.String(), 317 - Rkey: entry.Rkey, 318 - SwapRecord: ex.Cid, 319 - Record: &lexutil.LexiconTypeDecoder{ 320 - Val: &record, 321 - }, 325 + Rkey: entry.Rkey.String(), 326 + SwapRecord: &exCid, 327 + Record: &lexutil.LexiconTypeDecoder{Val: entry.AsRecord()}, 322 328 }) 323 329 if err != nil { 324 330 fail("Failed to updated existing record.", err) ··· 336 342 s.Notifier.EditString(r.Context(), &entry) 337 343 338 344 // if that went okay, redir to the string 339 - s.Pages.HxRedirect(w, "/strings/"+user.Did+"/"+entry.Rkey) 345 + s.Pages.HxRedirect(w, fmt.Sprintf("/strings/%s/%s", entry.Did, entry.Rkey)) 340 346 } 341 347 342 348 } ··· 373 379 374 380 string := models.String{ 375 381 Did: syntax.DID(user.Did), 376 - Rkey: tid.TID(), 382 + Rkey: syntax.RecordKey(tid.TID()), 377 383 Filename: filename, 378 384 Description: description, 379 385 Contents: content, 380 386 Created: time.Now(), 381 387 } 382 388 383 - record := string.AsRecord() 384 - 385 389 client, err := s.OAuth.AuthorizedClient(r) 386 390 if err != nil { 387 391 fail("Failed to create record.", err) ··· 390 394 391 395 resp, err := comatproto.RepoPutRecord(r.Context(), client, &atproto.RepoPutRecord_Input{ 392 396 Collection: tangled.StringNSID, 393 - Repo: user.Did, 394 - Rkey: string.Rkey, 395 - Record: &lexutil.LexiconTypeDecoder{ 396 - Val: &record, 397 - }, 397 + Repo: string.Did.String(), 398 + Rkey: string.Rkey.String(), 399 + Record: &lexutil.LexiconTypeDecoder{Val: string.AsRecord()}, 398 400 }) 399 401 if err != nil { 400 402 fail("Failed to create record.", err) ··· 412 414 s.Notifier.NewString(r.Context(), &string) 413 415 414 416 // successful 415 - s.Pages.HxRedirect(w, "/strings/"+user.Did+"/"+string.Rkey) 417 + s.Pages.HxRedirect(w, fmt.Sprintf("/strings/%s/%s", string.Did, string.Rkey)) 416 418 } 417 419 } 418 420