a love letter to tangled (android, iOS, and a search API)
at main 162 lines 4.0 kB view raw
1package backfill 2 3import ( 4 "context" 5 "errors" 6 "fmt" 7 8 "tangled.org/desertthunder.dev/twister/internal/normalize" 9 "tangled.org/desertthunder.dev/twister/internal/store" 10 "tangled.org/desertthunder.dev/twister/internal/xrpc" 11) 12 13const profileCollection = "sh.tangled.actor.profile" 14const repoCollection = "sh.tangled.repo" 15 16// ProfileRecord holds the fetched profile data and resolved handle. 17type ProfileRecord struct { 18 Record map[string]any 19 CID string 20 Handle string 21} 22 23type profileFetcher interface { 24 FetchProfile(ctx context.Context, did string) (*ProfileRecord, error) 25} 26 27type RepoRecord struct { 28 RKey string 29 CID string 30 Record map[string]any 31} 32 33type repoFetcher interface { 34 ListRepos(ctx context.Context, did string) ([]RepoRecord, error) 35} 36 37// XRPCProfileFetcher fetches sh.tangled.actor.profile records via xrpc.Client 38// and resolves handles from the DID document. 39type XRPCProfileFetcher struct { 40 client *xrpc.Client 41} 42 43func NewXRPCProfileFetcher(client *xrpc.Client) *XRPCProfileFetcher { 44 return &XRPCProfileFetcher{client: client} 45} 46 47func (f *XRPCProfileFetcher) FetchProfile(ctx context.Context, did string) (*ProfileRecord, error) { 48 info, err := f.client.ResolveIdentity(ctx, did) 49 if err != nil { 50 return nil, fmt.Errorf("resolve identity: %w", err) 51 } 52 53 rec, err := f.client.GetRecord(ctx, info.PDS, did, profileCollection, "self") 54 if err != nil { 55 var nfe *xrpc.NotFoundError 56 if errors.As(err, &nfe) { 57 return &ProfileRecord{Handle: info.Handle}, nil 58 } 59 return nil, fmt.Errorf("getRecord: %w", err) 60 } 61 62 return &ProfileRecord{ 63 Record: rec.Value, 64 CID: rec.CID, 65 Handle: info.Handle, 66 }, nil 67} 68 69type XRPCRepoFetcher struct { 70 client *xrpc.Client 71} 72 73func NewXRPCRepoFetcher(client *xrpc.Client) *XRPCRepoFetcher { 74 return &XRPCRepoFetcher{client: client} 75} 76 77func (f *XRPCRepoFetcher) ListRepos(ctx context.Context, did string) ([]RepoRecord, error) { 78 info, err := f.client.ResolveIdentity(ctx, did) 79 if err != nil { 80 return nil, fmt.Errorf("resolve identity: %w", err) 81 } 82 83 records, err := f.client.ListAllRecords(ctx, info.PDS, did, repoCollection) 84 if err != nil { 85 return nil, fmt.Errorf("list repos: %w", err) 86 } 87 88 repos := make([]RepoRecord, 0, len(records)) 89 for _, rec := range records { 90 _, _, rkey, err := normalize.ParseATURI(rec.URI) 91 if err != nil { 92 continue 93 } 94 repos = append(repos, RepoRecord{ 95 RKey: rkey, 96 CID: rec.CID, 97 Record: rec.Value, 98 }) 99 } 100 return repos, nil 101} 102 103func bootstrapProfileDocument(did string, profile *ProfileRecord, handle string) *store.Document { 104 if profile == nil || profile.Record == nil { 105 return nil 106 } 107 108 description, _ := profile.Record["description"].(string) 109 location, _ := profile.Record["location"].(string) 110 summary := description 111 if location != "" { 112 if summary != "" { 113 summary = summary + " · " + location 114 } else { 115 summary = location 116 } 117 } 118 if len(summary) > 200 { 119 summary = summary[:200] 120 } 121 122 return &store.Document{ 123 ID: fmt.Sprintf("%s|%s|self", did, profileCollection), 124 DID: did, 125 Collection: profileCollection, 126 RKey: "self", 127 ATURI: fmt.Sprintf("at://%s/%s/self", did, profileCollection), 128 CID: profile.CID, 129 RecordType: "profile", 130 Title: handle, 131 Body: description, 132 Summary: summary, 133 AuthorHandle: handle, 134 TagsJSON: "[]", 135 } 136} 137 138func bootstrapRepoDocument(did, handle string, repo RepoRecord) *store.Document { 139 adapter := &normalize.RepoAdapter{} 140 if !adapter.Searchable(repo.Record) { 141 return nil 142 } 143 144 event := normalize.TapRecordEvent{ 145 Type: "record", 146 Record: &normalize.TapRecord{ 147 DID: did, 148 Collection: repoCollection, 149 RKey: repo.RKey, 150 CID: repo.CID, 151 Record: repo.Record, 152 }, 153 } 154 155 doc, err := adapter.Normalize(event) 156 if err != nil { 157 return nil 158 } 159 doc.AuthorHandle = handle 160 doc.WebURL = xrpc.BuildWebURL(handle, doc.RepoName, doc.RecordType, doc.RKey) 161 return doc 162}