Monorepo for Tangled tangled.org
2

Configure Feed

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

1package migration 2 3import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "strings" 8 "time" 9 10 comatproto "github.com/bluesky-social/indigo/api/atproto" 11 "github.com/bluesky-social/indigo/atproto/atclient" 12 "github.com/bluesky-social/indigo/atproto/syntax" 13 lexutil "github.com/bluesky-social/indigo/lex/util" 14 "tangled.org/core/api/tangled" 15 "tangled.org/core/appview/db" 16) 17 18func (s *Migration) ensureCreatedAt(ctx context.Context, client *atclient.APIClient, did syntax.DID, record syntax.ATURI, current string) (string, error) { 19 if t, err := time.Parse(time.RFC3339, current); err == nil { 20 return t.UTC().Format(time.RFC3339), nil 21 } 22 var raw struct { 23 Value struct { 24 CreatedAt *string `json:"createdAt,omitempty"` 25 AddedAt *string `json:"addedAt,omitempty"` 26 } `json:"value"` 27 } 28 params := map[string]any{ 29 "collection": record.Collection().String(), 30 "repo": did.String(), 31 "rkey": record.RecordKey().String(), 32 } 33 if err := client.LexDo(ctx, lexutil.Query, "", "com.atproto.repo.getRecord", params, nil, &raw); err != nil { 34 return "", fmt.Errorf("ensure createdAt: get record: %w", err) 35 } 36 for _, cand := range []*string{raw.Value.CreatedAt, raw.Value.AddedAt} { 37 if cand == nil || *cand == "" { 38 continue 39 } 40 if t, err := time.Parse(time.RFC3339, *cand); err == nil { 41 return t.UTC().Format(time.RFC3339), nil 42 } 43 } 44 s.logger.Warn("createdAt unparseable, defaulting to now", "record", record.String()) 45 return time.Now().UTC().Format(time.RFC3339), nil 46} 47 48func (s *Migration) migrateAddRepoDid(ctx context.Context, client *atclient.APIClient, did syntax.DID, record syntax.ATURI) error { 49 if record.Collection().String() == tangled.FeedStarNSID { 50 return s.migrateAddRepoDidStar(ctx, client, did, record) 51 } 52 53 ex, err := comatproto.RepoGetRecord(ctx, client, "", record.Collection().String(), did.String(), record.RecordKey().String()) 54 if err != nil { 55 return fmt.Errorf("pds: %w", err) 56 } 57 58 val := ex.Value.Val 59 60 switch record.Collection().String() { 61 case tangled.RepoNSID: 62 rec, ok := val.(*tangled.Repo) 63 if !ok { 64 return fmt.Errorf("unexpected type for repo record") 65 } 66 repo, err := db.GetRepoByAtUri(s.db, record.String()) 67 if err != nil { 68 return fmt.Errorf("db: failed to query repo: %w", err) 69 } 70 rec.RepoDid = &repo.RepoDid 71 rec.CreatedAt, err = s.ensureCreatedAt(ctx, client, did, record, rec.CreatedAt) 72 if err != nil { 73 return err 74 } 75 76 case tangled.RepoIssueNSID: 77 rec, ok := val.(*tangled.RepoIssue) 78 if !ok { 79 return fmt.Errorf("unexpected type for issue record") 80 } 81 if strings.HasPrefix(rec.Repo, "did:") { 82 return nil 83 } 84 repo, err := db.GetRepoByAtUri(s.db, rec.Repo) 85 if err != nil { 86 return fmt.Errorf("db: failed to query repo by at_uri %q: %w", rec.Repo, err) 87 } 88 rec.Repo = repo.RepoDid 89 rec.CreatedAt, err = s.ensureCreatedAt(ctx, client, did, record, rec.CreatedAt) 90 if err != nil { 91 return err 92 } 93 94 case tangled.RepoPullNSID: 95 rec, ok := val.(*tangled.RepoPull) 96 if !ok { 97 return fmt.Errorf("unexpected type for pull record") 98 } 99 if rec.Target == nil { 100 return fmt.Errorf("pull record has nil target") 101 } 102 if !strings.HasPrefix(rec.Target.Repo, "did:") { 103 repo, err := db.GetRepoByAtUri(s.db, rec.Target.Repo) 104 if err != nil { 105 return fmt.Errorf("db: failed to query target repo by at_uri %q: %w", rec.Target.Repo, err) 106 } 107 rec.Target.Repo = repo.RepoDid 108 } 109 if rec.Source != nil && rec.Source.Repo != nil && !strings.HasPrefix(*rec.Source.Repo, "did:") { 110 sourceRepo, srcErr := db.GetRepoByAtUri(s.db, *rec.Source.Repo) 111 if srcErr == nil && sourceRepo.RepoDid != "" { 112 rec.Source.Repo = &sourceRepo.RepoDid 113 } 114 } 115 rec.CreatedAt, err = s.ensureCreatedAt(ctx, client, did, record, rec.CreatedAt) 116 if err != nil { 117 return err 118 } 119 120 case tangled.RepoCollaboratorNSID: 121 rec, ok := val.(*tangled.RepoCollaborator) 122 if !ok { 123 return fmt.Errorf("unexpected type for collaborator record") 124 } 125 if strings.HasPrefix(rec.Repo, "did:") { 126 return nil 127 } 128 repo, err := db.GetRepoByAtUri(s.db, rec.Repo) 129 if err != nil { 130 return fmt.Errorf("db: failed to query repo by at_uri %q: %w", rec.Repo, err) 131 } 132 rec.Repo = repo.RepoDid 133 rec.CreatedAt, err = s.ensureCreatedAt(ctx, client, did, record, rec.CreatedAt) 134 if err != nil { 135 return err 136 } 137 138 case tangled.RepoArtifactNSID: 139 rec, ok := val.(*tangled.RepoArtifact) 140 if !ok { 141 return fmt.Errorf("unexpected type for artifact record") 142 } 143 if rec.Repo != nil { 144 repo, err := db.GetRepoByAtUri(s.db, *rec.Repo) 145 if err != nil { 146 return fmt.Errorf("db: failed to query repo by at_uri %q: %w", *rec.Repo, err) 147 } 148 rec.RepoDid = &repo.RepoDid 149 } 150 rec.CreatedAt, err = s.ensureCreatedAt(ctx, client, did, record, rec.CreatedAt) 151 if err != nil { 152 return err 153 } 154 155 case tangled.ActorProfileNSID: 156 rec, ok := val.(*tangled.ActorProfile) 157 if !ok { 158 return fmt.Errorf("unexpected type for profile record") 159 } 160 rewritten := make([]string, 0, len(rec.PinnedRepositories)) 161 for _, pin := range rec.PinnedRepositories { 162 if strings.HasPrefix(pin, "did:") { 163 rewritten = append(rewritten, pin) 164 continue 165 } 166 repo, repoErr := db.GetRepoByAtUri(s.db, pin) 167 if repoErr != nil || repo.RepoDid == "" { 168 rewritten = append(rewritten, pin) 169 continue 170 } 171 rewritten = append(rewritten, repo.RepoDid) 172 } 173 rec.PinnedRepositories = rewritten 174 175 default: 176 return fmt.Errorf("unexpected collection: '%s'", record.Collection()) 177 } 178 179 _, err = comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{ 180 Repo: did.String(), 181 Collection: record.Collection().String(), 182 Rkey: record.RecordKey().String(), 183 SwapRecord: ex.Cid, 184 Record: &lexutil.LexiconTypeDecoder{Val: val}, 185 }) 186 if err != nil { 187 return fmt.Errorf("put record: %w", err) 188 } 189 190 return nil 191} 192 193func (s *Migration) migrateAddRepoDidStar(ctx context.Context, client *atclient.APIClient, did syntax.DID, record syntax.ATURI) error { 194 var raw struct { 195 Cid *string `json:"cid,omitempty"` 196 Uri string `json:"uri"` 197 Value json.RawMessage `json:"value"` 198 } 199 params := map[string]any{ 200 "collection": record.Collection().String(), 201 "repo": did.String(), 202 "rkey": record.RecordKey().String(), 203 } 204 if err := client.LexDo(ctx, lexutil.Query, "", "com.atproto.repo.getRecord", params, nil, &raw); err != nil { 205 return fmt.Errorf("get record: %w", err) 206 } 207 208 var legacy struct { 209 CreatedAt string `json:"createdAt"` 210 AddedAt string `json:"addedAt"` 211 Subject *string `json:"subject,omitempty"` 212 } 213 if err := json.Unmarshal(raw.Value, &legacy); err != nil { 214 return fmt.Errorf("decode old star fields: %w", err) 215 } 216 if legacy.Subject == nil { 217 return fmt.Errorf("star record has no subject field") 218 } 219 220 repo, err := db.GetRepoByAtUri(s.db, *legacy.Subject) 221 if err != nil { 222 return fmt.Errorf("db: failed to query repo by at_uri %q: %w", *legacy.Subject, err) 223 } 224 if repo.RepoDid == "" { 225 return fmt.Errorf("repo has no repoDid: %s", *legacy.Subject) 226 } 227 228 createdAt := legacy.CreatedAt 229 if t, err := time.Parse(time.RFC3339, createdAt); err == nil { 230 createdAt = t.UTC().Format(time.RFC3339) 231 } else if t, err := time.Parse(time.RFC3339, legacy.AddedAt); err == nil { 232 createdAt = t.UTC().Format(time.RFC3339) 233 } else { 234 s.logger.Warn("star createdAt unparseable, defaulting to now", "record", record.String()) 235 createdAt = time.Now().UTC().Format(time.RFC3339) 236 } 237 238 newRecord := &tangled.FeedStar{ 239 CreatedAt: createdAt, 240 Subject: &tangled.FeedStar_Subject{ 241 FeedStar_Repo: &tangled.FeedStar_Repo{Did: repo.RepoDid}, 242 }, 243 } 244 245 _, err = comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{ 246 Repo: did.String(), 247 Collection: record.Collection().String(), 248 Rkey: record.RecordKey().String(), 249 SwapRecord: raw.Cid, 250 Record: &lexutil.LexiconTypeDecoder{Val: newRecord}, 251 }) 252 if err != nil { 253 return fmt.Errorf("put record: %w", err) 254 } 255 return nil 256}