Bluesky

 view release on metacpan or  search on metacpan

lib/Bluesky.pm  view on Meta::CPAN

        return $content unless ref $content eq 'HASH';
        $content->{jobStatus} // $content;
    }

    method getVideoUploadLimits () {
        my $token = $self->_service_token( 'did:web:video.bsky.app', 'app.bsky.video.getUploadLimits' );
        return $token if builtin::blessed $token;
        my $res = $self->_video_http->get( $self->_video_url('app.bsky.video.getUploadLimits'),
            { headers => { Authorization => "Bearer $token", Accept => 'application/json' } } );
        my $content = $res->{content};
        $content = decode_json($content) if defined $content && !ref $content && ( $res->{headers}{'content-type'} // '' ) =~ m[json];
        $res->{success} ? $self->_unwrap_job($content) : $self->_video_error( $res, $content );
    }

    method getVideoJobStatus ( $jobId, %opts ) {
        my $token = $opts{token} // $self->_service_token( 'did:web:video.bsky.app', 'app.bsky.video.getJobStatus', $opts{exp} );
        return $token if builtin::blessed $token;
        my $res = $self->_video_http->get(
            $self->_video_url( 'app.bsky.video.getJobStatus', jobId => $jobId ),
            { headers => { Authorization => "Bearer $token", Accept => 'application/json' } }
        );
        my $content = $res->{content};
        $content = decode_json($content) if defined $content && !ref $content && ( $res->{headers}{'content-type'} // '' ) =~ m[json];
        return $self->_video_error( $res, $content ) unless $res->{success};
        $self->_unwrap_job($content);
    }

    method uploadVideoJob ( $bytes, $mime_type = 'video/mp4', %opts ) {
        if    ( builtin::blessed $bytes ) { $bytes = $bytes->slurp_raw }
        elsif ( !ref $bytes && $bytes !~ /[\x00-\x1f]/ && -e $bytes ) {
            $bytes = path($bytes)->slurp_raw;
        }
        $opts{did} //= $self->did;

lib/Bluesky.pm  view on Meta::CPAN

        my $query = join( '&', map { "$_=" . URI::Escape::uri_escape( $opts{$_} ) } grep { defined $opts{$_} } qw[did name] );
        my $url   = "$base/xrpc/app.bsky.video.uploadVideo" . ( $query ? "?$query" : '' );

        # uploadVideo writes into the PDS on our behalf, so the token audience
        # is the PDS and the lexicon is uploadBlob, not uploadVideo.
        my $token = $opts{token} // $self->_service_token( $opts{aud} // $self->_pds_did, 'com.atproto.repo.uploadBlob', $opts{exp} );
        return $token if builtin::blessed $token;
        my $res
            = $self->_video_http->post( $url, { content => $bytes, headers => { Authorization => "Bearer $token", 'Content-Type' => $mime_type } } );
        my $content = $res->{content};
        $content = decode_json($content) if defined $content && !ref $content && ( $res->{headers}{'content-type'} // '' ) =~ m[json];

        # 409 Conflict means the video was already processed; the body still
        # carries a usable jobStatus (including the finished blob).
        return $self->_video_error( $res, $content ) unless $res->{success} || $res->{status} == 409;
        $self->_unwrap_job($content);
    }

    # Drafts (private post drafts via stash)
    method getDrafts(%args) {
        my $res = $self->_at_for('app.bsky.draft.getDrafts')->get( 'app.bsky.draft.getDrafts' => \%args );

lib/Bluesky.pm  view on Meta::CPAN

                    $at_http->dpop_nonce($nonce) if defined $nonce && $nonce ne '';
                    if ( $at_http && $at_http->can('_generate_dpop_proof') ) {
                        $headers{DPoP} = $at_http->_generate_dpop_proof( $url, 'POST' );
                    }
                    $res     = $http->post( $url, { content => $bytes, headers => \%headers } );
                    $content = $res->{content};
                }
            }
        }
        if ( $res->{success} ) {
            $content = decode_json($content) if $content && ( $res->{headers}{'content-type'} // '' ) =~ m[json];
            return $content->{blob};
        }
        my $msg = $res->{reason} // 'Unknown error';
        if ( $content && ( $res->{headers}{'content-type'} // '' ) =~ m[json] ) {
            my $json = decode_json($content);
            $msg .= ': ' . $json->{message} if $json->{message};
        }
        return At::Error->new( message => $msg, fatal => 1 );
    }

    # Upload a single image (path, bytes, handle, or Path::Tiny object),
    # enforcing the 1 MB legacy image guard, and return its blob. Shared by
    # the images and gallery embeds.
    method _upload_image ( $img, $mime = undef ) {

lib/Bluesky.pm  view on Meta::CPAN

        if ( $res->{success} ) {
            ( $card{title} )       = $res->{content} =~ m[<title>(.*?)</title>.*</head>]is;
            ( $card{description} ) = ( $res->{content} =~ m[<meta name="description" content="(.*?)".+</meta>.*</head>]is ) // '';
            my ($image) = $res->{content} =~ m[<img.*?src="([^"]*)"[^>]*>(?:</img>)?]isp;
            if ( defined $image ) {
                if ( $image =~ /^data:/ ) {
                    $card{thumb} = $self->uploadFile($image);
                }
                else {
                    $res = $http->get( URI->new_abs( $image, $url ) );
                    $card{thumb} = $res->{success} ? $self->uploadFile( $res->{content}, $res->{headers}{'content-type'} ) : ();
                }
            }
        }
        { '$type' => 'app.bsky.embed.external', external => \%card };
    }
};
#
1;

t/000_compile.t  view on Meta::CPAN

                field validationStatus => D();
                end;
                }, 'reply to text post';
            my $img = HTTP::Tiny->new->get('https://picsum.photos/500/300');

            #~ use Data::Dump;
            #~ ddx $img;
            skip 1 unless $img->{success};
            is my $image_reply = $bsky->createPost(
                reply_to => $post->{uri},
                embed    => { images => [ { image => $img->{content}, alt => 'Lorem Picsum', mime => $img->{headers}{'content-type'} } ] },
                text     => <<'END' ),
Yet another test.

But with an image this time!
END
                hash {
                field cid    => D();
                field commit => hash {
                    field cid => D();
                    field rev => D();

t/003_endpoints.t  view on Meta::CPAN

    ]
);

# The video service talks to video.bsky.app over its own HTTP::Tiny client.
my $video_http = mock 'HTTP::Tiny' => (
    override => [
        new => sub { bless {}, shift },
        get => sub ( $self, $url, $opts ) {
            my $content = $url =~ /getJobStatus/ ? '{"jobStatus":{"jobId":"job1","state":"JOB_STATE_COMPLETED"}}' :
                '{"canUpload":1,"remainingDailyVideos":9}';
            return { success => 1, status => 200, headers => { 'content-type' => 'application/json' }, content => $content };
        },
    ]
);
my $bsky = Bluesky->new();
subtest 'Feed additions' => sub {
    my $feed = $bsky->getListFeed('at://did:plc:x/app.bsky.graph.list/1');
    is ref $feed,                 'ARRAY',                                'getListFeed returns arrayref';
    is $last_get->{method},       'app.bsky.feed.getListFeed',            'getListFeed routes correctly';
    is $last_get->{params}{list}, 'at://did:plc:x/app.bsky.graph.list/1', 'getListFeed passes list';
    $bsky->sendInteractions( interactions => [ { item => 'at://did:plc:x/app.bsky.feed.post/1', event => 'app.bsky.feed.defs#interactionSeen' } ] );



( run in 1.561 second using v1.01-cache-2.11-cpan-99fa8243a42 )