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 )