package bsky import ( "encoding/json" "errors" "fmt" "net/http" "net/url" "os" "strings" "time" "github.com/dghubble/sling" ) const ( didWebPrefixLen = len("did:web:") atPrefixLen = len("at://") httpClientTimeout = 3 * time.Second ) type BSky struct { Bluesky *Bluesky } func NewBSky() *BSky { return &BSky{ Bluesky: &Bluesky{ Cfg: &BlueskyConfig{}, HttpClient: &http.Client{}, sling: sling.New().Client(&http.Client{Timeout: httpClientTimeout}), publicSling: sling.New().Base("https://public.api.bsky.app/").Client(&http.Client{Timeout: httpClientTimeout}), }, } } func (b *BSky) ResolveHandle(handle string) (string, error) { resp := new(BSkySessionResponse) errResp := &struct { Message string `json:"message"` Error string `json:"error"` }{} params := struct { Handle string `url:"handle"` }{ Handle: handle, } b.Bluesky.publicSling.New().Get("/xrpc/com.atproto.identity.resolveHandle").QueryStruct(params). Receive(resp, errResp) if errResp.Error != "" { return "", errors.New(errResp.Message) } return resp.DID, nil } func parseDIDURL(did string) (*url.URL, error) { if strings.HasPrefix(did, "did:web:") { return url.Parse("https://" + did[didWebPrefixLen:] + "/.well-known/did.json") } else if strings.HasPrefix(did, "did:plc:") { return url.Parse("https://plc.directory/" + did) } return nil, errors.New("DID is not supported") } func (b *BSky) getPDS() error { did, _ := b.ResolveHandle(b.Bluesky.Cfg.Handle) didURL, err := parseDIDURL(did) if err != nil { return err } didResp := new(DIDResponse) baseURL := fmt.Sprintf("%s://%s", didURL.Scheme, didURL.Host) sling.New().Base(baseURL).Get(didURL.Path).ReceiveSuccess(didResp) if didResp.ID == "" { return errors.New("unable to resolve DID") } b.Bluesky.Cfg.DID = didResp.ID if len(didResp.Service) == 0 { return errors.New("DID response has no services") } pdsURL := didResp.Service[0].ServiceEndpoint if pdsURL == "" { return errors.New("service endpoint is empty") } b.Bluesky.Cfg.PDSURL = pdsURL b.Bluesky.sling.Base(pdsURL) return nil } func (b *BSky) GetHandleFromDID(did string) (handle string, err error) { didURL, err := parseDIDURL(did) if err != nil { return "", err } didResp := new(DIDResponse) baseURL := fmt.Sprintf("%s://%s", didURL.Scheme, didURL.Host) sling.New().Base(baseURL).Get(didURL.Path).ReceiveSuccess(didResp) if didResp.ID == "" { return "", errors.New("unable to resolve DID") } return didResp.AlsoKnownAs[0][atPrefixLen:], nil } func (b *BSky) GetPDS() 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.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 err = json.Unmarshal(fBytes, &auth) if err != nil { return nil, fmt.Errorf("failed to parse auth file: %w", err) } return auth, nil }