package blueskyclient import ( "encoding/json" "errors" "fmt" "net/http" "net/url" "os" "strings" "time" "github.com/dghubble/sling" ) type BSky struct { Bluesky *Bluesky DID string } func NewBSky() *BSky { return &BSky{ Bluesky: &Bluesky{ Cfg: &BlueskyConfig{}, HttpClient: &http.Client{}, sling: sling.New().Client(&http.Client{Timeout: time.Second * 3}), }, } } func (b *BSky) ResolveHandle(handle string) (string, error) { httpClient := &http.Client{Timeout: 3 * time.Second} resp := new(BSkySessionResponse) errResp := &struct { Message string `json:"message"` Error string `json:"error"` }{} params := struct { Handle string `url:"handle"` }{ Handle: handle, } sling.New().Base("https://public.api.bsky.app/").Client(httpClient). Get("/xrpc/com.atproto.identity.resolveHandle").QueryStruct(params). Receive(resp, errResp) if errResp.Error != "" { return "", errors.New(errResp.Message) } return resp.DID, nil } func (b *BSky) getPDS() error { did, _ := b.ResolveHandle(b.Bluesky.Cfg.Handle) var didURL url.URL if strings.HasPrefix(did, "did:web:") { didURL.Host = "https://" + did[8:] didURL.Path = "/.well-known/did.json" } else if strings.HasPrefix(did, "did:plc:") { didURL.Host = "https://plc.directory" didURL.Path = "/" + did } else { return errors.New("DID is not supported") } didResp := new(DIDResponse) sling.New().Base(didURL.Host).Get(didURL.Path).ReceiveSuccess(didResp) if didResp.ID == "" { return errors.New("unable to resolve DID") } b.Bluesky.Cfg.DID = didResp.ID b.Bluesky.Cfg.PDSURL = didResp.Service[0].ServiceEndpoint b.Bluesky.sling.Base(didResp.Service[0].ServiceEndpoint) return nil } func (b *BSky) GetHandleFromDID(did string) (handle string, err error) { var didURL url.URL if strings.HasPrefix(did, "did:web:") { didURL.Host = "https://" + did[8:] didURL.Path = "/.well-known/did.json" } else if strings.HasPrefix(did, "did:plc:") { didURL.Host = "https://plc.directory" didURL.Path = "/" + did } else { return "", errors.New("DID is not supported") } didResp := new(DIDResponse) sling.New().Base(didURL.Host).Get(didURL.Path).ReceiveSuccess(didResp) if didResp.ID == "" { return "", errors.New("unable to resolve DID") } return didResp.AlsoKnownAs[0][5:], nil } func (b *BSky) GetPDS(handle string) string { return b.Bluesky.Cfg.PDSURL } func (b *BSky) Auth(authData []string) error { b.Bluesky.Cfg.Handle = authData[0] b.getPDS() auth, err := loadAuth() if err != nil { // no auth session found b.Bluesky.Cfg.AppPassword = authData[1] err = b.Bluesky.CreateSession(b.Bluesky.Cfg) if err != nil { return fmt.Errorf("unable to auth: %s", err) } b.Bluesky.Cfg.AppPassword = "" // we don't need to save this PersistAuthSession(b.Bluesky.Cfg) } else { b.Bluesky.Cfg.Cursor = auth.Cursor b.Bluesky.Cfg.AccessJWT = auth.AccessJWT b.Bluesky.Cfg.RefreshJWT = auth.RefreshJWT // b.RefreshSession() b.Bluesky.CheckSessionValid() } return nil } func PersistAuthSession(sess *BlueskyConfig) error { f, err := os.OpenFile("auth-session.json", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { return err } defer f.Close() authBytes, err := json.MarshalIndent(sess, "", " ") if err != nil { return err } _, err = f.Write(authBytes) return err } func loadAuth() (*BlueskyConfig, error) { fBytes, err := os.ReadFile("auth-session.json") if err != nil { return nil, err } if len(fBytes) == 0 { return nil, errors.New("no auth file found") } var auth *BlueskyConfig json.Unmarshal(fBytes, &auth) return auth, nil }