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 )