go-bluesky-client/bluesky.go
2025-07-13 18:09:52 +01:00

181 lines
4.8 KiB
Go

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) Post {
resp := &struct {
Posts []Post `json:"posts"`
}{}
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(&params).Receive(resp, resp)
return resp.Posts[0]
}