package output import ( "bytes" "context" "errors" "fmt" "strings" "time" "github.com/bluesky-social/indigo/api/atproto" "github.com/bluesky-social/indigo/api/bsky" lexutil "github.com/bluesky-social/indigo/lex/util" "github.com/bluesky-social/indigo/util" "github.com/bluesky-social/indigo/xrpc" ) const blueskyCollection = "app.bsky.feed.post" type blueskyoutput struct { identifier string password string } func BlueskyOutput(identifier string, password string) *blueskyoutput { blueskyoutput := &blueskyoutput{ identifier: identifier, password: password, } return blueskyoutput } // session logs in and returns a client authenticated as the configured account. func (bo *blueskyoutput) session() (*xrpc.Client, error) { cli := &xrpc.Client{ Host: "https://bsky.social", } input := &atproto.ServerCreateSession_Input{ Identifier: bo.identifier, Password: bo.password, } output, err := atproto.ServerCreateSession(context.TODO(), cli, input) if err != nil { return nil, err } cli.Auth = &xrpc.AuthInfo{ AccessJwt: output.AccessJwt, RefreshJwt: output.RefreshJwt, Handle: output.Handle, Did: output.Did, } return cli, nil } func (bo *blueskyoutput) Write(post Post) (Ref, error) { cli, err := bo.session() if err != nil { return Ref{}, err } feedpost := &bsky.FeedPost{ Text: post.Text, CreatedAt: time.Now().Format(util.ISO8601), Langs: []string{"ja"}, } if len(post.Images) > 0 { embedimages := make([]*bsky.EmbedImages_Image, 0, len(post.Images)) for _, img := range post.Images { blob, err := atproto.RepoUploadBlob(context.TODO(), cli, bytes.NewReader(img.Data)) if err != nil { return Ref{}, fmt.Errorf("uploading %s: %w", img.Filename, err) } embedimages = append(embedimages, &bsky.EmbedImages_Image{ Alt: img.Filename, Image: blob.Blob, }) } feedpost.Embed = &bsky.FeedPost_Embed{ EmbedImages: &bsky.EmbedImages{ Images: embedimages, }, } } Recordinput := &atproto.RepoCreateRecord_Input{ Collection: blueskyCollection, Repo: cli.Auth.Did, // "matope.bsky.social" のDID Record: &lexutil.LexiconTypeDecoder{Val: feedpost}, } record, recerr := atproto.RepoCreateRecord(context.TODO(), cli, Recordinput) if recerr != nil { return Ref{}, recerr } return Ref{URI: record.Uri, CID: record.Cid}, nil } func (bo *blueskyoutput) Delete(ref Ref) error { rkey, err := recordKey(ref.URI) if err != nil { return err } cli, err := bo.session() if err != nil { return err } _, err = atproto.RepoDeleteRecord(context.TODO(), cli, &atproto.RepoDeleteRecord_Input{ Collection: blueskyCollection, Repo: cli.Auth.Did, Rkey: rkey, }) return err } // recordKey pulls the rkey out of an at://did/collection/rkey URI. func recordKey(uri string) (string, error) { if uri == "" { return "", errors.New("no record URI to delete") } idx := strings.LastIndex(uri, "/") if idx < 0 || idx == len(uri)-1 { return "", fmt.Errorf("malformed record URI %q", uri) } return uri[idx+1:], nil } func (bo *blueskyoutput) GetName() string { return "bluesky" }