···5// 4. Extract documents and metadata
6// 5. Return as []DocumentWithMeta
7//
8-// TODO: Implement document pulling
9-// 1. GET {pdsURL}/xrpc/com.atproto.sync.getRepo?did={session.DID}
10-// 2. Parse CAR stream using github.com/bluesky-social/indigo/repo
11-// 3. Iterate over records, filter by collection == "pub.leaflet.document"
12-// 4. Parse each record as public.Document
13-// 5. Collect metadata (rkey, cid, uri)
14-//
15// TODO: Implement publication listing:
16// 1. Query records with collection: pub.leaflet.publication
17// 2. Parse as public.Publication
···21package services
2223import (
024 "context"
025 "fmt"
026 "time"
2728 "github.com/bluesky-social/indigo/api/atproto"
029 "github.com/bluesky-social/indigo/xrpc"
030 "github.com/stormlightlabs/noteleaf/internal/public"
31)
32···192 return nil, fmt.Errorf("not authenticated")
193 }
194195- return nil, fmt.Errorf("document pulling not yet implemented - TODO: implement com.atproto.sync.getRepo with CAR parsing")
0000000000000000000000000000000000000000000000000000196}
197198// ListPublications fetches available publications for the authenticated user
···5// 4. Extract documents and metadata
6// 5. Return as []DocumentWithMeta
7//
00000008// TODO: Implement publication listing:
9// 1. Query records with collection: pub.leaflet.publication
10// 2. Parse as public.Publication
···14package services
1516import (
17+ "bytes"
18 "context"
19+ "encoding/json"
20 "fmt"
21+ "strings"
22 "time"
2324 "github.com/bluesky-social/indigo/api/atproto"
25+ "github.com/bluesky-social/indigo/repo"
26 "github.com/bluesky-social/indigo/xrpc"
27+ "github.com/ipfs/go-cid"
28 "github.com/stormlightlabs/noteleaf/internal/public"
29)
30···190 return nil, fmt.Errorf("not authenticated")
191 }
192193+ carBytes, err := atproto.SyncGetRepo(ctx, s.client, s.session.DID, "")
194+ if err != nil {
195+ return nil, fmt.Errorf("failed to fetch repository: %w", err)
196+ }
197+198+ r, err := repo.ReadRepoFromCar(ctx, bytes.NewReader(carBytes))
199+ if err != nil {
200+ return nil, fmt.Errorf("failed to parse CAR file: %w", err)
201+ }
202+203+ var documents []DocumentWithMeta
204+ prefix := public.TypeDocument
205+206+ err = r.ForEach(ctx, prefix, func(k string, v cid.Cid) error {
207+ _, recordBytes, err := r.GetRecordBytes(ctx, k)
208+ if err != nil {
209+ return fmt.Errorf("failed to get record bytes for %s: %w", k, err)
210+ }
211+212+ var doc public.Document
213+ if err := json.Unmarshal(*recordBytes, &doc); err != nil {
214+ return fmt.Errorf("failed to unmarshal document %s: %w", k, err)
215+ }
216+217+ parts := strings.Split(k, "/")
218+ rkey := ""
219+ if len(parts) > 0 {
220+ rkey = parts[len(parts)-1]
221+ }
222+223+ uri := fmt.Sprintf("at://%s/%s", s.session.DID, k)
224+225+ meta := public.DocumentMeta{
226+ RKey: rkey,
227+ CID: v.String(),
228+ URI: uri,
229+ IsDraft: false,
230+ FetchedAt: time.Now(),
231+ }
232+233+ documents = append(documents, DocumentWithMeta{
234+ Document: doc,
235+ Meta: meta,
236+ })
237+238+ return nil
239+ })
240+241+ if err != nil {
242+ return nil, fmt.Errorf("failed to iterate over documents: %w", err)
243+ }
244+245+ return documents, nil
246}
247248// ListPublications fetches available publications for the authenticated user
+213
internal/services/atproto_test.go
···312 t.Error("Expected session to be nil after close")
313 }
314 })
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000315 })
316}