Monorepo for Tangled
tangled.org
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}