aboutsummaryrefslogtreecommitdiff
path: root/general
diff options
context:
space:
mode:
authorGuillermo Serrahima <dev@vylion.com>2026-04-11 07:40:30 +0200
committerGuillermo Serrahima <dev@vylion.com>2026-04-11 23:39:19 +0200
commit9f0a9a6779f00786e4728ad5c3fad36814a293b0 (patch)
tree4b16d535c5d41ef46980608a88b7581be3c2bfd0 /general
parentf6d60077b7cfc0d589fa7961e2bb59aea9f993a3 (diff)
downloadllostre-9f0a9a6779f00786e4728ad5c3fad36814a293b0.tar.xz
llostre-9f0a9a6779f00786e4728ad5c3fad36814a293b0.zip
project restructure
Diffstat (limited to 'general')
-rw-r--r--general/records.go148
-rw-r--r--general/utils.go35
2 files changed, 183 insertions, 0 deletions
diff --git a/general/records.go b/general/records.go
new file mode 100644
index 0000000..12825bd
--- /dev/null
+++ b/general/records.go
@@ -0,0 +1,148 @@
+package general
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "llostre/auth"
+ "log/slog"
+ "net/http"
+)
+
+type RawRecord struct {
+ URI string `json:"uri"`
+ Value json.RawMessage `json:"value"`
+}
+
+func FetchRawRecordCollection(logger *slog.Logger, did, collection, baseURL string, session *auth.OAuthState) ([]RawRecord, error) {
+ url := fmt.Sprintf("%s/xrpc/com.atproto.repo.listRecords?repo=%s&collection=%s", baseURL, did, collection)
+
+ logger.Debug("fetching raw record collection", "did", did, "endpoint", baseURL, "collection", collection)
+
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ // only add auth if provided
+ if session != nil {
+ dpopProof, err := session.KeyPair.GenerateDPoPProof("GET", url, "")
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Set("Authorization", "Bearer "+session.AccessToken)
+ req.Header.Set("DPoP", dpopProof)
+ }
+
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != 200 {
+ body, _ := io.ReadAll(resp.Body)
+ return nil, fmt.Errorf("server returned %d: %s", resp.StatusCode, string(body))
+ }
+
+ var result struct {
+ Records []RawRecord `json:"records"`
+ }
+ if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
+ return nil, err
+ }
+
+ return result.Records, nil
+}
+
+func FetchRawRecordCollectionWithCursor(logger *slog.Logger, did, collection, baseURL string, session *auth.OAuthState, cursor string, limit int) ([]RawRecord, string, error) {
+ url := fmt.Sprintf("%s/xrpc/com.atproto.repo.listRecords?repo=%s&collection=%s", baseURL, did, collection)
+ if cursor != "" {
+ url += "&cursor=" + cursor
+ }
+ if limit > 0 {
+ url += fmt.Sprintf("&limit=%d", limit)
+ }
+
+ logger.Debug("fetching raw record collection", "did", did, "endpoint", baseURL, "collection", collection, "cursor", cursor)
+
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return nil, "", err
+ }
+
+ if session != nil {
+ dpopProof, err := session.KeyPair.GenerateDPoPProof("GET", url, "")
+ if err != nil {
+ return nil, "", err
+ }
+ req.Header.Set("Authorization", "Bearer "+session.AccessToken)
+ req.Header.Set("DPoP", dpopProof)
+ }
+
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return nil, "", err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != 200 {
+ body, _ := io.ReadAll(resp.Body)
+ return nil, "", fmt.Errorf("server returned %d: %s", resp.StatusCode, string(body))
+ }
+
+ var result struct {
+ Records []RawRecord `json:"records"`
+ Cursor string `json:"cursor"`
+ }
+ if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
+ return nil, "", err
+ }
+
+ return result.Records, result.Cursor, nil
+}
+
+func FetchRawRecordByURI(logger *slog.Logger, uri, baseURL string, session *auth.OAuthState) (*RawRecord, error) {
+ repo, collection, rkey, err := ParseURI(uri)
+ if err != nil {
+ return nil, err
+ }
+
+ url := fmt.Sprintf("%s/xrpc/com.atproto.repo.getRecord?repo=%s&collection=%s&rkey=%s",
+ baseURL, repo, collection, rkey)
+
+ logger.Debug("fetching raw record by URI", "uri", uri, "endpoint", baseURL)
+
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ // only add auth if provided
+ if session != nil {
+ dpopProof, err := session.KeyPair.GenerateDPoPProof("GET", url, "")
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Set("Authorization", "Bearer "+session.AccessToken)
+ req.Header.Set("DPoP", dpopProof)
+ }
+
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != 200 {
+ body, _ := io.ReadAll(resp.Body)
+ return nil, fmt.Errorf("endpoint returned %d: %s", resp.StatusCode, string(body))
+ }
+
+ var result RawRecord
+ if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
+ return nil, err
+ }
+
+ return &result, nil
+}
diff --git a/general/utils.go b/general/utils.go
new file mode 100644
index 0000000..ac27443
--- /dev/null
+++ b/general/utils.go
@@ -0,0 +1,35 @@
+package general
+
+import (
+ "fmt"
+ "strings"
+ "sync"
+ "time"
+)
+
+var (
+ slingshotBackoff time.Time
+ slingshotBackoffMu sync.Mutex
+)
+
+func ParseURI(uri string) (repo, collection, rkey string, err error) {
+ // URI format: 'at://DID/collection/recordkey'
+ parts := strings.Split(strings.TrimPrefix(uri, "at://"), "/")
+ if len(parts) != 3 {
+ return "", "", "", fmt.Errorf("invalid URI format: %s", uri)
+ }
+
+ return parts[0], parts[1], parts[2], nil
+}
+
+func SlingshotAvailable() bool {
+ slingshotBackoffMu.Lock()
+ defer slingshotBackoffMu.Unlock()
+ return time.Now().After(slingshotBackoff)
+}
+
+func SetSlingshotBackoff(d time.Duration) {
+ slingshotBackoffMu.Lock()
+ defer slingshotBackoffMu.Unlock()
+ slingshotBackoff = time.Now().Add(d)
+}