Skip to content

Latest commit

 

History

History
558 lines (393 loc) · 14.2 KB

basic-client.md

File metadata and controls

558 lines (393 loc) · 14.2 KB

Basic Client

Authentication

Bluesky has two authentication methods: "App password" and "OAuth". "OAuth" is recommended from now on, so please also read the Socialite docs.

App password(Legacy)

You can easily log in with the identifier and password you set in .env.

// .env

BLUESKY_IDENTIFIER=
BLUESKY_APP_PASSWORD=
use Revolution\Bluesky\Facades\Bluesky;

$profile = Bluesky::login(identifier: config('bluesky.identifier'), password: config('bluesky.password'))
                  ->getProfile();

Resume LegacySession.

use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Session\LegacySession;

Bluesky::login(identifier: config('bluesky.identifier'), password: config('bluesky.password'));

cache()->put('bluesky_legacy_session', Bluesky::agent()->session()->toArray(), now()->addDay());

Bluesky::withToken(LegacySession::create(cache('bluesky_legacy_session', [])));

if (! Bluesky::check() {
    Bluesky::refreshSession();
}

OAuth

Specify the OAuthSession containing the token obtained from Socialite.

use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Session\OAuthSession;

$session = OAuthSession::create(session('bluesky_session'));

$timeline = Bluesky::withToken($session)->getTimeline();

Response

The API results are returned as an Illuminate\Http\Client\Response object, so you can use it freely just like you would with normal Laravel.

/** @var \Illuminate\Http\Client\Response $response */
$response->json();
$response->collect();

Structure

  • Bluesky Facade: Entrance
  • Agent: Authentication
  • Client: Send API request

Basic functions have a "ShortHand", so you can use it in three steps: Facade - Authentication - Send.

use Revolution\Bluesky\Facades\Bluesky;

$response = Bluesky::withToken()->post();

Functions not in ShortHand can be executed via Client.

use Revolution\Bluesky\Facades\Bluesky;

$response = Bluesky::withToken()->client(auth: true)->createRecord();

Finally, if you want to use an API not in Client, you can send anything with send().

use Revolution\Bluesky\Facades\Bluesky;
use Illuminate\Http\Client\PendingRequest;

$response = Bluesky::withToken()
                   ->send(
                         api: 'com.atproto.repo.createRecord',
                         method: 'post',
                         auth: true,
                         params: [],
                         callback: function (PendingRequest $http) {
                                $http->...;
                         },
                   );

Viewing my feed

Only my posts and reposts.

use Revolution\Bluesky\Facades\Bluesky;

/** @var \Illuminate\Http\Client\Response $response */
$response = Bluesky::withToken()->getAuthorFeed();

dump($response->collect('feed'));

Viewing my timeline

use Revolution\Bluesky\Facades\Bluesky;

/** @var \Illuminate\Http\Client\Response $response */
$response = Bluesky::withToken()->getTimeline();

dump($response->json());

Creating a post

If it's just simple text you can just pass the string, but no automatic links or tags will work.

use Revolution\Bluesky\Facades\Bluesky;

/** @var \Illuminate\Http\Client\Response $response */
$response = Bluesky::withToken()->post('test');

dump($response->json());

TextBuilder

Bluesky requires you to set up facets for links and tags to work. TextBuilder makes this easy.

use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Record\Post;
use Revolution\Bluesky\RichText\TextBuilder;

$builder = TextBuilder::make(text: 'test')
                      ->newLine()
                      ->mention('@***.bsky.social')
                      ->newLine()
                      ->link('https://')
                      ->newLine()
                      ->tag('#Laravel');

$post = Post::create(text: $builder->text, facets: $builder->facets);

/** @var \Illuminate\Http\Client\Response $response */
$response = Bluesky::withToken()->post($post);

dump($response->json());

You can create a Post object directly using toPost().

use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Record\Post;
use Revolution\Bluesky\RichText\TextBuilder;

$post = TextBuilder::make(text: 'test')
                   ->newLine()
                   ->link('https://')
                   ->newLine()
                   ->tag('#Laravel')
                   ->toPost();

/** @var \Illuminate\Http\Client\Response $response */
$response = Bluesky::withToken()->post($post);

dump($response->json());

Alternatively you can use Post::build(), use whichever you prefer.

use Revolution\Bluesky\Record\Post;
use Revolution\Bluesky\RichText\TextBuilder;

$post = Post::build(function (TextBuilder $builder) {
            $builder->text('test')
                    ->newLine()
                    ->link('https://')
                    ->newLine()
                    ->tag('#Laravel')
    });

Auto-detect

TextBuilder also has detectFacets(), but it is not clear whether it works perfectly, so it is safer to assemble it manually.

use Revolution\Bluesky\Record\Post;
use Revolution\Bluesky\RichText\TextBuilder;

$post = Post::build(function (TextBuilder $builder) {
            $builder->text('@alice.test')
                    ->newLine()
                    ->text('test')
                    ->newLine()
                    ->text('https://alice.test')
                    ->newLine()
                    ->text('#alice #🙃 #ゑ')
                    ->detectFacets();
    });

Reply

use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Record\Post;
use Revolution\Bluesky\Types\ReplyRef;
use Revolution\Bluesky\Types\StrongRef;

$reply = ReplyRef::to(root: StrongRef::to(uri: 'at://', cid: 'cid'), parent: StrongRef::to(uri: 'at://', cid: 'cid'));

$post = Post::create(text: 'test')
            ->reply($reply);

/** @var \Illuminate\Http\Client\Response $response */
$response = Bluesky::withToken()->post($post);

dump($response->json());

External link / Social Card

use Illuminate\Support\Facades\Storage;
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Record\Post;
use Revolution\Bluesky\Embed\External;

$external = External::create(
    title: 'Title', 
    description: 'test', 
    uri: 'http://', 
    thumb: fn() => Bluesky::uploadBlob(Storage::get('test.png'), Storage::mimeType('test.png'))->json('blob'),
);

$post = Post::create(text: 'test')
            ->embed($external);

/** @var \Illuminate\Http\Client\Response $response */
$response = Bluesky::withToken()->post($post);

dump($response->json());

Upload Images

Images are passed as an array of data called a blob object.

{
    "$type": "blob",
    "ref": {
        "$link": "..."
    },
    "mimeType": "image/png",
    "size": 10000
}

You can upload up to 4 images at a time.

use Illuminate\Support\Facades\Storage;
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Record\Post;
use Revolution\Bluesky\Embed\Images;

Bluesky::withToken();

$images = Images::create()
                ->add(alt: 'ALT TEXT', blob: function (): array {
                    return Bluesky::uploadBlob(Storage::get('test.png'), Storage::mimeType('test.png'))->json('blob');
                   })
                ->add(alt: 'image 2', blob: []);

$post = Post::create(text: 'test')
            ->embed($images);

/** @var \Illuminate\Http\Client\Response $response */
$response = Bluesky::post($post);

dump($response->json());

Upload video

There is no official documentation so this may change in the future.

// routes/web.php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Storage;
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Record\Post;
use Revolution\Bluesky\Embed\Video;

Route::post('upload_video', function (Request $request) {
    $upload = Bluesky::withToken()
                     ->uploadVideo(
                         data: $request->file('video')->get(),
                         type: $request->file('video')->getMimeType(),
                     );

    // If the upload doesn't work, check the error message.
    info('upload', $upload->json());
    // successful
    // ['did' => 'did:plc:***', 'jobId' => '***', 'status' => 'JOB_STATE_CREATED']
    // fails
    // ['did' => '', 'error' => '***', 'jobId' => '', 'message' => '***', 'status' => '']

    $jobId = $upload->json('jobId');

    // Bluesky::uploadVideo() returns a jobId, then you can use Bluesky::getJobStatus() to check if the upload is complete and retrieve the blob.

    $status = Bluesky::getJobStatus($jobId);

    info('status', $status->json());

    // Wait until state becomes JOB_STATE_COMPLETED.
    if($status->json('jobStatus.state') === 'JOB_STATE_COMPLETED') {
         $blob = $status->json('jobStatus.blob');
    }

    $video = Video::create(video: $blob);

    $post = Post::create(text: 'Upload video')->embed($video);

    $response = Bluesky::post($post);

    dump($response->json());
})

uploadVideo() also accepts StreamInterface.

// UploadedFile

use GuzzleHttp\Psr7\Utils;

$upload = Bluesky::withToken()
                 ->uploadVideo(
                     data: Utils::streamFor(Utils::tryFopen($request->file('video')->getPathname(), 'rb')),
                     type: $request->file('video')->getMimeType(),
                 );
// Upload from Storage

use Illuminate\Support\Facades\Storage;
use GuzzleHttp\Psr7\Utils;

$upload = Bluesky::withToken()
                 ->uploadVideo(
                     data: Utils::streamFor(Storage::readStream('video.mp4')),
                     type: Storage::mimeType('video.mp4'),
                 );

Quote post

use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Record\Post;
use Revolution\Bluesky\Embed\QuoteRecord;
use Revolution\Bluesky\Types\StrongRef;

$quote = QuoteRecord::create(StrongRef::to(uri: 'at://', cid: 'cid'));

$post = Post::create(text: 'test')
            ->embed($quote);

/** @var \Illuminate\Http\Client\Response $response */
$response = Bluesky::withToken()->post($post);

dump($response->json());

Quote post with media

Supported media: One of Images Video External

use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Record\Post;
use Revolution\Bluesky\Embed\External;
use Revolution\Bluesky\Embed\QuoteRecordWithMedia;
use Revolution\Bluesky\Types\StrongRef;

$external = External::create(
    title: 'Title', 
    description: '', 
    uri: 'https://', 
);

$quote = QuoteRecordWithMedia::create(StrongRef::to(uri: 'at://', cid: 'cid'), media: $external);

$post = Post::create(text: 'test')
            ->embed($quote);

/** @var \Illuminate\Http\Client\Response $response */
$response = Bluesky::withToken()->post($post);

dump($response->json());

Following a user

use Revolution\Bluesky\Facades\Bluesky;

/** @var \Illuminate\Http\Client\Response $response */
$response = Bluesky::withToken()->follow(did: 'did:plc:...');

dump($response->json());

Like

use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Types\StrongRef;

/** @var \Illuminate\Http\Client\Response $response */
$response = Bluesky::withToken()->like(StrongRef::to(uri: 'at://', cid: 'cid'));

dump($response->json());

Repost

use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Record\Repost;
use Revolution\Bluesky\Types\StrongRef;

$repost = Repost::create(StrongRef::to(uri: 'at://', cid: 'cid'));

/** @var \Illuminate\Http\Client\Response $response */
$response = Bluesky::withToken()->repost($repost);

dump($response->json());

Editing profiles

Use a Closure to update an existing profile.

use Illuminate\Support\Facades\Storage;
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Record\Profile;
use Revolution\Bluesky\Types\StrongRef;

/** @var \Illuminate\Http\Client\Response $response */
$response = Bluesky::withToken()->upsertProfile(function(Profile $profile) {
    $profile->displayName('new name')
            ->description('new description');

    $profile->avatar(function(): array {
        return Bluesky::uploadBlob(Storage::get('test.png'), Storage::mimeType('test.png'))->json('blob');
    });

    $profile->pinnedPost(StrongRef::to(uri: 'at://', cid: ''));
})

dump($response->json());

Public API

In fact, many of Bluesky's APIs can be used without authentication.

use Revolution\Bluesky\Facades\Bluesky;

$profile = Bluesky::getProfile(actor: 'did')->json();

$feed = Bluesky::getAuthorFeed(actor: 'did')->json('feed');

For app.bsky.* APIs, use Bluesky::public() to explicitly specify a public endpoint.

use Revolution\Bluesky\Facades\Bluesky;

$profile = Bluesky::public()->getProfile(actor: 'did')->json();

$feed = Bluesky::public()->getAuthorFeed(actor: 'did')->json('feed');

For com.atproto.* APIs, client(auth: false) or logout() will ensure that the public endpoint is used. However, most APIs require authentication.

use Revolution\Bluesky\Facades\Bluesky;

$res = Bluesky::getRecord();

$res = Bluesky::client(auth: false)->getRecord();

$res = Bluesky::logout()->getRecord();
API Endpoint Docs
app.bsky.* https://public.api.bsky.app API Hosts and Auth
com.atproto.* https://bsky.social PDS Entryway

Macroable

// AppServiceProvider

use Revolution\Bluesky\Facades\Bluesky;
use Revolution\AtProto\Lexicon\Contracts\App\Bsky\Feed;
use Illuminate\Http\Client\Response;

    public function boot(): void
    {
        Bluesky::macro('timeline', function (?int $limit = 50, ?string $cursor = null): array {
            /** @var Bluesky $this */
            return $this->getTimeline(limit: $limit, cursor: $cursor)->throw()->json('feed');
        });
    }
use Revolution\Bluesky\Facades\Bluesky;

$feed = Bluesky::timeline();