package blueskyclient import ( "errors" "fmt" "net/http" "strings" "github.com/charmbracelet/log" "github.com/dghubble/sling" ) type BlueskyConfig struct { PDSURL string `json:"pds-url"` Repo string `json:"repo"` Handle string `json:"handle"` DID string `json:"did"` AppPassword string `json:"app-password"` AccessJWT string `json:"access-jwt"` RefreshJWT string `json:"refresh-jwt"` Cursor int64 `json:"cursor"` } type DIDResponse struct { Context []string `json:"@context"` ID string `json:"id"` AlsoKnownAs []string `json:"alsoKnownAs"` VerificationMethod []struct { ID string `json:"id"` Type string `json:"type"` Controller string `json:"controller"` PublicKeyMultibase string `json:"publicKeyMultibase"` } `json:"verificationMethod"` Service []struct { ID string `json:"id"` Type string `json:"type"` ServiceEndpoint string `json:"serviceEndpoint"` } `json:"service"` } type BSkySessionResponse struct { AccessJWT string `json:"accessJwt,omitempty"` RefreshJWT string `json:"refreshJwt,omitempty"` Handle string `json:"handle"` DID string `json:"did"` Error string `json:"error,omitempty"` Message string `json:"message,omitempty"` } type CommitResponse struct { URI string `json:"uri"` Cid string `json:"cid"` Commit struct { Cid string `json:"cid"` Rev string `json:"rev"` } `json:"commit"` Error string `json:"error"` Message string `json:"message"` } func (c *CommitResponse) GetRKey() string { s := strings.SplitN(c.URI, "/", 5) if len(s) == 5 { return s[4] } return "" } type Link struct { Cid string `json:"cid"` URI string `json:"uri"` } type Bluesky struct { Cfg *BlueskyConfig HttpClient *http.Client Logger *log.Logger sling *sling.Sling } func (bluesky *Bluesky) CreateSession(cfg *BlueskyConfig) error { body := struct { Identifier string `json:"identifier"` Password string `json:"password"` }{ Identifier: cfg.Handle, Password: cfg.AppPassword, } resp := new(BSkySessionResponse) bluesky.sling.New().Post("/xrpc/com.atproto.server.createSession").BodyJSON(body).ReceiveSuccess(resp) if resp.AccessJWT != "" { cfg.AccessJWT = resp.AccessJWT cfg.RefreshJWT = resp.RefreshJWT return nil } bluesky.sling.New().Set("Authorization", fmt.Sprintf("Bearer %s", bluesky.Cfg.AccessJWT)) return errors.New("unable to authenticate, check handle/password") } func (bluesky *Bluesky) RefreshSession() error { resp := new(BSkySessionResponse) bluesky.sling.New().Set("Authorization", fmt.Sprintf("Bearer %s", bluesky.Cfg.RefreshJWT)). Post("/xrpc/com.atproto.server.refreshSession").Receive(resp, resp) if resp.AccessJWT != "" { bluesky.Cfg.AccessJWT = resp.AccessJWT bluesky.Cfg.RefreshJWT = resp.RefreshJWT PersistAuthSession(bluesky.Cfg) bluesky.sling.Set("Authorization", fmt.Sprintf("Bearer %s", bluesky.Cfg.AccessJWT)) return nil } return bluesky.CreateSession(bluesky.Cfg) } func (bluesky *Bluesky) CheckSessionValid() { resp := new(BSkySessionResponse) bluesky.sling.New().Set("Authorization", fmt.Sprintf("Bearer %s", bluesky.Cfg.AccessJWT)). Get("/xrpc/app.bsky.actor.getProfile").Receive(resp, resp) if resp.Error == "ExpiredToken" { bluesky.RefreshSession() } } func (bluesky *Bluesky) GetPost(uri string) *Post { bluesky.CheckSessionValid() var post = struct { URI string `json:"uri"` CID string `json:"cid"` Value *Post `json:"value"` }{} args := strings.SplitN(uri, "/", 5) params := struct { RKey string `url:"rkey"` Repo string `url:"repo"` Collection string `url:"collection"` }{ RKey: args[4], Repo: args[2], Collection: args[3], } bluesky.sling.New().Get("/xrpc/com.atproto.repo.getRecord").QueryStruct(params).ReceiveSuccess(&post) return post.Value } func (bluesky *Bluesky) DeleteRecord(args []string) *CommitResponse { bluesky.CheckSessionValid() resp := new(CommitResponse) params := struct { RKey string `json:"rkey"` Repo string `json:"repo"` Collection string `json:"collection"` }{ RKey: args[0], Repo: args[1], Collection: args[2], } bluesky.sling.New().Set("Authorization", fmt.Sprintf("Bearer %s", bluesky.Cfg.AccessJWT)). Post("/xrpc/com.atproto.repo.deleteRecord").BodyJSON(params).Receive(resp, resp) return resp } func (bluesky *Bluesky) FetchPost(did string, rkey string) FetchedPost { resp := FetchedPost{} params := struct { URIs string `url:"uris"` }{ URIs: fmt.Sprintf("at://%s/app.bsky.feed.post/%s", did, rkey), } bluesky.sling.New().Base("https://public.api.bsky.app"). Get("/xrpc/app.bsky.feed.getPosts").QueryStruct(¶ms).Receive(resp, resp) return resp }