App-bsky
view release on metacpan or search on metacpan
lib/App/bsky.pm view on Meta::CPAN
}
else {
$self->err('unsure what to do with this banner; does not seem to be a URL or local file');
}
if ($banner) {
$self->say( 'uploaded banner... %d bytes', $banner->{size} );
}
else {
$self->say('failed to upload banner');
}
}
my $res = $bsky->at->put_record(
'app.bsky.actor.profile',
'self',
{ defined $displayName ? ( displayName => $displayName ) : (),
defined $description ? ( description => $description ) : (),
defined $avatar ? ( avatar => $avatar ) : (),
defined $banner ? ( banner => $banner ) : ()
}
);
defined $res->{uri} ? $self->say( $res->{uri}->as_string ) : $self->err( $res->{message} );
}
method cmd_oauth ( $handle, @args ) {
my $cli = $self;
GetOptionsFromArray( \@args, 'redirect=s' => \my $redirect );
$bsky->oauth_helper(
handle => $handle,
listen => 1,
defined $redirect ? ( redirect => $redirect ) : (),
on_success => sub ($bsky_obj) {
$config->{resume} = $bsky_obj->session->_raw;
$config->{session} = $bsky_obj->session->_raw;
$cli->put_config;
$cli->say( "Authenticated as " . $bsky_obj->did );
}
);
}
method cmd_showsession (@args) {
GetOptionsFromArray( \@args, 'json!' => \my $json );
my $session = $bsky->session;
unless ($session) {
return $self->err("No active session. Run 'bsky oauth <handle>' or 'bsky login' first.");
}
if ($json) {
$self->say( JSON::Tiny::to_json( $session->_raw ) );
}
else {
$self->say( 'DID: ' . $session->did );
$self->say( 'Handle: ' . $session->handle );
$self->say( 'Email: ' . ( $session->email // 'N/A' ) );
$self->say( 'Type: ' . $session->token_type );
$self->say( 'Scopes: ' . ( $session->scope // 'N/A' ) );
}
return 1;
}
method _dump_post ( $depth, $post ) {
if ( builtin::blessed $post ) {
if ( $post->isa('At::Lexicon::app::bsky::feed::threadViewPost') && builtin::blessed $post->parent ) {
$self->_dump_post( $depth++, $post->parent );
$post = $post->post;
}
elsif ( $post->isa('At::Lexicon::app::bsky::feed::threadViewPost') ) {
$self->_dump_post( $depth++, $post->post );
my $replies = $post->replies // [];
$self->_dump_post( $depth + 2, $_->post ) for @$replies;
return;
}
}
#~ warn ref $post;
#~ use Data::Dump;
#~ ddx $post;
# TODO: Support image embeds as raw links
$self->say(
'%s%s%s%s%s (%s)',
' ' x ( $depth * 4 ),
color('red'), $post->{author}{handle},
color('reset'),
defined $post->{author}{displayName} ? ' [' . $post->{author}{displayName} . ']' : '',
$post->{record}{createdAt}
);
if ( $post->{embed} && defined $post->{embed}{images} ) { # TODO: Check $post->embed->$type to match 'app.bsky.embed.images#view'
$self->say( '%s%s', ' ' x ( $depth * 4 ), $_->{fullsize} ) for @{ $post->{embed}{images} };
}
$self->say( '%s%s', ' ' x ( $depth * 4 ), $post->{record}{text} );
$self->say(
'%s â¤ï¸ %d ð¬ %d ð %d %s',
' ' x ( $depth * 4 ),
$post->{likeCount}, $post->{replyCount}, $post->{repostCount},
( builtin::blessed $post->{uri} ? $post->{uri}->as_string : $post->{uri} )
);
$self->say( '%s', ' ' x ( $depth * 4 ) );
}
method cmd_timeline (@args) {
GetOptionsFromArray( \@args, 'json!' => \my $json );
my $tl = $bsky->getTimeline();
if ( builtin::blessed $tl && $tl->isa('At::Error') ) {
return $self->err( "Error fetching timeline: " . $tl->message );
}
unless ( $tl && $tl->{feed} ) {
return $self->say("Timeline is empty.");
}
if ($json) {
$self->say( JSON::Tiny::to_json( $tl->{feed} ) );
}
else {
for my $item ( @{ $tl->{feed} } ) {
my $depth = 0;
if ( $item->{reply} && $item->{reply}{parent} ) {
$self->_dump_post( $depth, $item->{reply}{parent} );
$depth = 1;
}
$self->_dump_post( $depth, $item->{post} );
}
}
return scalar @{ $tl->{feed} };
}
method cmd_tl (@args) { $self->cmd_timeline(@args); }
method cmd_stream(@args) {
GetOptionsFromArray( \@args, 'json|j' => \my $json );
lib/App/bsky.pm view on Meta::CPAN
return;
}
# Only process commit events for now
unless ( defined $header->{t} && $header->{t} eq '#commit' ) {
return;
}
for my $op ( @{ $body->{ops} } ) {
next unless $op->{action} eq 'create';
next unless $op->{path} =~ /^app\.bsky\.feed\.post\//;
try {
# Decode the blocks to find the record
require Archive::CAR::v1;
my $car = Archive::CAR::v1->new();
open my $cfh, '<:raw', \$body->{blocks};
my %blocks = map { $_->{cid}->to_string => $_->{data} } $car->read($cfh)->blocks->@*;
require Archive::CAR::CID; # Ensure it's loaded for conversion
my $cid_raw = $op->{cid};
if ( ref $cid_raw eq 'HASH' && exists $cid_raw->{cid_raw} ) {
$cid_raw = $cid_raw->{cid_raw};
}
my $target_cid_obj = Archive::CAR::CID->from_raw($cid_raw);
my $record_bytes = $blocks{ $target_cid_obj->to_string };
next unless $record_bytes;
require Codec::CBOR;
my $codec = Codec::CBOR->new();
my $record = $codec->decode($record_bytes);
next unless $record;
my $repo = $body->{repo};
my $ts = $record->{createdAt} // '';
$ts =~ s/T/ /;
$ts =~ s/\..*Z//;
# Queue for later rendering
push @post_queue, { repo => $repo, record => $record, ts => $ts };
$dids_to_resolve{$repo} = 1 unless exists $profile_cache{$repo};
if ( $record->{reply} && $record->{reply}{parent} ) {
my $parent_uri = $record->{reply}{parent}{uri};
if ( $parent_uri =~ m[^at://(did:[^/]+)] ) {
my $parent_did = $1;
$dids_to_resolve{$parent_did} = 1 unless exists $profile_cache{$parent_did};
}
}
}
catch ($e) {
warn "CAR/CBOR decoding error for op on repo " . $body->{repo} . ": $e";
}
}
}
catch ($e) {
warn "Error processing firehose event: $e";
}
}
);
$fh->start();
};
$start_stream->();
Mojo::IOLoop->start unless Mojo::IOLoop->is_running;
}
method cmd_thread (@args) {
GetOptionsFromArray( \@args, 'json!' => \my $json, 'n=i' => \my $number );
$number //= ();
my ($id) = @args;
$id // return $self->cmd_help('thread');
my $res = $bsky->getPostThread( uri => $id, depth => $number, parentHeight => $number ); # $uri, depth, $parentHeight
return unless $res->{thread};
return $self->say( JSON::Tiny::to_json $res->{thread} ) if $json;
$self->_dump_post( 0, $res->{thread} );
}
method cmd_post ($text) {
my $res = $bsky->createPost( text => $text );
defined $res ? $self->say( $res->{uri} ) : 0;
}
method cmd_delete ($uri) {
$uri = At::Protocol::URI->new($uri) unless builtin::blessed $uri;
$bsky->at->delete_record( $uri->collection, $uri->rkey );
}
# TODO
method cmd_like ( $uri, @args ) { # can take the post uri
GetOptionsFromArray( \@args, 'json!' => \my $json, 'cid=s' => \my $cid );
my $res = $bsky->like( $uri, $cid );
$res || $res->throw;
$self->say( $json ? JSON::Tiny::to_json($res) : sprintf 'Liked! [id:%s]', $res->{uri}->as_string );
}
# TODO
method cmd_unlike ( $uri, @args ) { # can take the post uri or the like uri
GetOptionsFromArray( \@args, 'json!' => \my $json, 'cid=s' => \my $cid );
my $res = $bsky->deleteLike($uri);
$res || $res->throw;
$self->say( $json ? JSON::Tiny::to_json($res) : sprintf 'Removed like!' );
}
# TODO
method cmd_likes ( $uri, @args ) {
GetOptionsFromArray( \@args, 'json!' => \my $json );
my @likes;
my $cursor = ();
do {
my $likes = $bsky->at->get( 'app.bsky.feed.getLikes', { uri => $uri, limit => 100, cursor => $cursor } );
push @likes, @{ $likes->{likes} };
$cursor = $likes->{cursor};
} while ($cursor);
if ($json) {
$self->say( JSON::Tiny::to_json \@likes );
}
else {
$self->say(
'%s%s%s%s (%s)',
color('red'), $_->{actor}{handle},
color('reset'), defined $_->{actor}{displayName} ? ' [' . $_->{actor}{displayName} . ']' : '',
$_->{createdAt}
) for @likes;
}
scalar @likes;
}
# TODO
method cmd_repost ($uri) {
my $res = $bsky->repost($uri);
$res // return;
$self->say( $res->{uri}->as_string );
}
# TODO
( run in 0.608 second using v1.01-cache-2.11-cpan-39bf76dae61 )