Bluesky has two authentication methods: "App password" and "OAuth". "OAuth" is recommended from now on, so please also read the Socialite docs.
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();
}
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();
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();
- 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->...;
},
);
Only my posts and reposts.
use Revolution\Bluesky\Facades\Bluesky;
/** @var \Illuminate\Http\Client\Response $response */
$response = Bluesky::withToken()->getAuthorFeed();
dump($response->collect('feed'));
use Revolution\Bluesky\Facades\Bluesky;
/** @var \Illuminate\Http\Client\Response $response */
$response = Bluesky::withToken()->getTimeline();
dump($response->json());
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());
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')
});
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();
});
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());
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());
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());
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'),
);
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());
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());
use Revolution\Bluesky\Facades\Bluesky;
/** @var \Illuminate\Http\Client\Response $response */
$response = Bluesky::withToken()->follow(did: 'did:plc:...');
dump($response->json());
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());
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());
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());
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 |
// 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();