Bluesky
view release on metacpan or search on metacpan
lib/Bluesky.pm view on Meta::CPAN
package Bluesky 1.01 {
use v5.40;
use Carp qw[carp];
use bytes;
use feature 'class';
no warnings 'experimental::class', 'experimental::try';
use At;
use Path::Tiny;
use HTTP::Tiny;
use URI;
use JSON::PP;
#
class Bluesky {
field $service : param //= 'https://bsky.social';
field $chat_service : param //= 'https://api.bsky.chat';
field $at : reader = At->new( host => $service );
#
method _at_for ($method) {
if ( $method =~ /^chat\.bsky\./ ) {
# Chat requests are proxied via the PDS.
# Service ID fragment (#bsky_chat) is required.
say '[DEBUG] [Bluesky] Proxying chat request...' if $ENV{DEBUG};
$self->at->http->at_protocol_proxy('did:web:api.bsky.chat#bsky_chat');
return $at;
}
# Ensure proxy is NOT used for standard repo/feed calls
$self->at->http->at_protocol_proxy(undef);
return $at;
}
method login ( $identifier, $password ) { $self->at->login( $identifier, $password ); }
method resume( $accessJwt, $refreshJwt, $token_type = 'Bearer', $dpop_key_jwk = (), $client_id = (), $handle = (), $pds = (), $scope = () ) {
$self->at->resume( $accessJwt, $refreshJwt, $token_type, $dpop_key_jwk, $client_id, $handle, $pds, $scope );
}
method oauth_start( $handle, $client_id, $redirect_uri, $scope = 'atproto' ) {
return $self->at->oauth_start( $handle, $client_id, $redirect_uri, $scope );
}
method oauth_callback( $code, $state ) { return $self->at->oauth_callback( $code, $state ); }
method oauth_helper (%args) {
my $handle = $args{handle} or die 'Handle is required for oauth_helper';
# client_id base MUST be localhost for the PDS to recognize it as metadata-less
my $client_id_base = $args{client_id} // 'http://localhost';
# RFC 8252 requires loopback IP (127.0.0.1) for the actual redirect
my $redirect_uri = $args{redirect} // 'http://127.0.0.1:8888/callback';
# Transitional scopes for chat access via PDS proxy
my $scope = $args{scope} // 'atproto transition:generic transition:chat.bsky';
my $on_success = $args{on_success} // sub { say 'Success! Session established.' };
# Build the magic localhost Client ID manually to ensure %20 encoding for spaces
require URI::Escape;
my $scope_encoded = URI::Escape::uri_escape($scope);
my $redirect_encoded = URI::Escape::uri_escape($redirect_uri);
my $client_id = "$client_id_base?scope=$scope_encoded&redirect_uri=$redirect_encoded";
say "Starting OAuth flow for $handle...";
my $auth_url = $self->oauth_start( $handle, $client_id, $redirect_uri, $scope );
say 'Please open the following URL in your browser:';
say "\n $auth_url\n";
if ( $args{listen} ) {
# Attempt to start a local server to catch the callback
try {
require Mojolicious;
my $app = Mojolicious->new();
my $port = 8888;
$port = $1 if $redirect_uri =~ /:(\d+)/;
my $path = URI->new($redirect_uri)->path || '/';
$app->routes->get($path)->to(
cb => sub ($c) {
my $code = $c->param('code');
my $state = $c->param('state');
if ( $code && $state ) {
say 'Exchanging code for tokens...';
try {
$self->oauth_callback( $code, $state );
( run in 1.705 second using v1.01-cache-2.11-cpan-364913b4093 )