diff --git a/cli/cmd/publish.go b/cli/cmd/publish.go index e522438..dfde7ce 100644 --- a/cli/cmd/publish.go +++ b/cli/cmd/publish.go @@ -69,21 +69,36 @@ func publishBookRecords(session atproto.Session, books []store.Book, storePath s var failed []string - // TODO: // call ListBookRecords and build an index: - // existing, err := atproto.ListBookRecords(session) - // - // then, build a map[string]book of olWorkId and isbn13 values already on the pds + existingRecords, err := atproto.ListBookRecords(session) + if err != nil { + return fmt.Errorf("listing book records: %w", err) + } + + // then, build a map[string]record of olWorkId and isbn13 values already on the pds + index := make(map[string]atproto.RepoRecord) + for _, record := range existingRecords { + if record.Value.OLWorkID != "" { + index[record.Value.OLWorkID] = record + } else if record.Value.ISBN13 != "" { + index[record.Value.ISBN13] = record + } + } // **Loop over unpublished books** — for each, build a `BookRecord` from the store book (`OLWorkID`, `ISBN` → `ISBN13`, `DateAdded` → `CreatedAt`) for i, book := range books { if book.ATURI != "" { continue } - // TODO: add a guard for book already indexed - // if existingIndex[book.OLWorkID] || existingIndex[book.ISBN] {continue} - } + // guard for book already indexed + _, ok := index[book.OLWorkID] + if !ok { + _, ok = index[book.ISBN] + } + if ok { + continue + } record := atproto.BookRecord{ OLWorkID: book.OLWorkID, diff --git a/cli/internal/atproto/records.go b/cli/internal/atproto/records.go index 6b2e429..d8cdaf7 100644 --- a/cli/internal/atproto/records.go +++ b/cli/internal/atproto/records.go @@ -5,22 +5,9 @@ import ( "encoding/json" "fmt" "net/http" + "net/url" ) -// TODO: -// type ListRecordsResponse struct { -// Records []struct { -// URI string `json:"uri"` -// CID string `json:"cid"` -// Value BookRecord `json:"value"` -// } `json:"records"` -// Cursor string `json:"cursor"` -// } -// -// TODO: -// function to fetch existing book records from PDS, paginated via cursor -// func ListBookRecords(session Session) (ListRecordsResponse, error) - type BookRecord struct { OLWorkID string `json:"olWorkId,omitempty"` ISBN13 string `json:"isbn13,omitempty"` @@ -50,6 +37,75 @@ type CreateRecordResponse struct { CID string `json:"cid"` } +type ListRecordsResponse struct { + Records []RepoRecord `json:"records"` + Cursor string `json:"cursor"` +} + +type RepoRecord struct { + URI string `json:"uri"` + CID string `json:"cid"` + Value BookRecord `json:"value"` +} + +// fetch existing book records from PDS, paginated via cursor +func ListBookRecords(session Session) ([]RepoRecord, error) { + + cursor := "" + var allRecords []RepoRecord + + for { + params := url.Values{} + params.Set("repo", session.DID) + params.Set("collection", "com.hectorsector.book.book") + params.Set("limit", "100") + + if cursor != "" { + params.Set("cursor", cursor) + } + + // GET from com.atproto.repo.listRecords, pass in params: + url := session.PDSHost + "/xrpc/com.atproto.repo.listRecords?" + params.Encode() + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, fmt.Errorf("creating request: %w", err) + } + req.Header.Set("Authorization", "Bearer "+session.AccessJwt) + + // Call with no cursor, get records and maybe a cursor + response, err := http.DefaultClient.Do(req) + if err != nil { + return nil, fmt.Errorf("listing records for pds: %w", err) + } + + if response.StatusCode == 401 { + return nil, fmt.Errorf("Invalid creds when calling PDS ", response.Status) + } + + if response.StatusCode != 200 { + return nil, fmt.Errorf("Got non-200 response from calling PDS ", response.Status) + } + + var listRecordResponse ListRecordsResponse + err = json.NewDecoder(response.Body).Decode(&listRecordResponse) + if err != nil { + return nil, fmt.Errorf("decoding response from pds: %w", err) + } + response.Body.Close() + + allRecords = append(allRecords, listRecordResponse.Records...) + + // while cursor: + // call again and the cursor + if listRecordResponse.Cursor == "" { + break + } + cursor = listRecordResponse.Cursor + } + + return allRecords, nil +} + func CreateRecord(session Session, collection string, record any) (CreateRecordResponse, error) { reqBody, err := json.Marshal(CreateRecordRequest{ Repo: session.DID,