Laravel AT Protocol Client (alpha & unstable)
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

Rename `readme.md` to `README.md` and add banner

+608 -57
+608
README.md
··· 1 + [![Resolver Header](./header.png)](https://github.com/socialdept/atp-signals) 2 + 3 + <h3 align="center"> 4 + Type-safe AT Protocol HTTP client with OAuth 2.0 support for Laravel. 5 + </h3> 6 + 7 + <p align="center"> 8 + <br> 9 + <a href="https://packagist.org/packages/socialdept/atp-client" title="Latest Version on Packagist"><img src="https://img.shields.io/packagist/v/socialdept/atp-client.svg?style=flat-square"></a> 10 + <a href="https://packagist.org/packages/socialdept/atp-client" title="Total Downloads"><img src="https://img.shields.io/packagist/dt/socialdept/atp-client.svg?style=flat-square"></a> 11 + <a href="https://github.com/socialdept/atp-client/actions/workflows/tests.yml" title="GitHub Tests Action Status"><img src="https://img.shields.io/github/actions/workflow/status/socialdept/atp-client/tests.yml?branch=main&label=tests&style=flat-square"></a> 12 + <a href="LICENSE" title="Software License"><img src="https://img.shields.io/github/license/socialdept/atp-client?style=flat-square"></a> 13 + </p> 14 + 15 + --- 16 + 17 + ## What is AtpClient? 18 + 19 + **AtpClient** is a Laravel package for interacting with Bluesky and the AT Protocol. It provides a fluent, type-safe API for authentication, posting, profiles, follows, likes, and feeds. Supports both OAuth 2.0 (with PKCE, PAR, and DPoP) and app passwords. 20 + 21 + Think of it as Laravel's HTTP client, but for the decentralized social web. 22 + 23 + ## Why use AtpClient? 24 + 25 + - **Laravel-style code** - Familiar patterns you already know 26 + - **OAuth 2.0 support** - Full PKCE, PAR, and DPoP implementation 27 + - **App password support** - Simple authentication for scripts and bots 28 + - **Automatic token refresh** - Sessions stay alive without manual intervention 29 + - **Type-safe API** - Method chaining with IDE autocompletion 30 + - **Rich text builder** - Fluent API for mentions, links, and hashtags 31 + - **Full Bluesky coverage** - Posts, profiles, follows, likes, and feeds 32 + - **AT Protocol operations** - Low-level repository access when needed 33 + 34 + ## Quick Example 35 + 36 + ```php 37 + use SocialDept\AtpClient\Facades\Atp; 38 + 39 + // Login with app password 40 + $client = Atp::login('yourhandle.bsky.social', 'your-app-password'); 41 + 42 + // Create a post 43 + $post = $client->bsky->post->create('Hello from Laravel!'); 44 + 45 + // Get your timeline 46 + $timeline = $client->bsky->feed->getTimeline(limit: 50); 47 + ``` 48 + 49 + ## Installation 50 + 51 + ```bash 52 + composer require socialdept/atp-client 53 + ``` 54 + 55 + Optionally publish the configuration: 56 + 57 + ```bash 58 + php artisan vendor:publish --tag=atp-client-config 59 + ``` 60 + 61 + ## Getting Started 62 + 63 + Once installed, you're three steps away from using the AT Protocol: 64 + 65 + ### 1. Choose Your Authentication Method 66 + 67 + **App Password** (recommended for bots/scripts): 68 + ```php 69 + $client = Atp::login('yourhandle.bsky.social', 'your-app-password'); 70 + ``` 71 + 72 + **OAuth 2.0** (recommended for user-facing apps): 73 + ```php 74 + $auth = Atp::oauth()->authorize('user@bsky.social'); 75 + return redirect($auth->url); 76 + ``` 77 + 78 + ### 2. Make API Calls 79 + 80 + ```php 81 + // Create posts 82 + $client->bsky->post->create('Hello world!'); 83 + 84 + // Get profiles 85 + $client->bsky->actor->getProfile('someone.bsky.social'); 86 + 87 + // Browse feeds 88 + $client->bsky->feed->getTimeline(); 89 + ``` 90 + 91 + ### 3. Store Credentials (OAuth only) 92 + 93 + Implement the `CredentialProvider` interface to persist tokens between requests. 94 + 95 + ## What can you build? 96 + 97 + - **Bluesky integrations** - Connect your app to the AT Protocol 98 + - **Social media management** - Post and manage content programmatically 99 + - **Automated posting** - Schedule and automate content delivery 100 + - **Analytics dashboards** - Track engagement and activity 101 + - **Moderation tools** - Build bots for community moderation 102 + - **Cross-platform syndication** - Mirror content across networks 103 + 104 + ## Authentication 105 + 106 + ### App Password Flow 107 + 108 + The simplest way to authenticate. Generate an app password in your Bluesky settings. 109 + 110 + ```php 111 + use SocialDept\AtpClient\Facades\Atp; 112 + 113 + $client = Atp::login('yourhandle.bsky.social', 'your-app-password'); 114 + 115 + // Client is now authenticated and ready to use 116 + $profile = $client->bsky->actor->getProfile('yourhandle.bsky.social'); 117 + ``` 118 + 119 + ### OAuth 2.0 Flow 120 + 121 + For user-facing applications where users authenticate with their own accounts. 122 + 123 + **Step 1: Initiate authorization** 124 + ```php 125 + use SocialDept\AtpClient\Facades\Atp; 126 + 127 + public function redirect() 128 + { 129 + $auth = Atp::oauth()->authorize('user@bsky.social'); 130 + 131 + // Store auth request in session for callback 132 + session(['atp_auth' => $auth]); 133 + 134 + return redirect($auth->url); 135 + } 136 + ``` 137 + 138 + **Step 2: Handle callback** 139 + ```php 140 + public function callback(Request $request) 141 + { 142 + $auth = session('atp_auth'); 143 + 144 + $token = Atp::oauth()->callback( 145 + code: $request->get('code'), 146 + state: $request->get('state'), 147 + request: $auth 148 + ); 149 + 150 + // Store credentials using your CredentialProvider 151 + // $token contains: accessJwt, refreshJwt, did, handle, expiresAt 152 + } 153 + ``` 154 + 155 + **Step 3: Use stored credentials** 156 + ```php 157 + // After storing credentials, use them with Atp::as() 158 + $client = Atp::as('user@bsky.social'); 159 + ``` 160 + 161 + ### Token Refresh 162 + 163 + Sessions automatically refresh when tokens are about to expire (default: 5 minutes before expiration). Listen to events if you need to persist refreshed tokens: 164 + 165 + ```php 166 + use SocialDept\AtpClient\Events\TokenRefreshed; 167 + 168 + Event::listen(TokenRefreshed::class, function ($event) { 169 + // $event->identifier - the user identifier 170 + // $event->token - the new AccessToken 171 + // Update your credential storage here 172 + }); 173 + ``` 174 + 175 + ## Working with Posts 176 + 177 + ### Create a Simple Post 178 + 179 + ```php 180 + $post = $client->bsky->post->create('Hello, Bluesky!'); 181 + 182 + // Returns StrongRef with uri and cid 183 + echo $post->uri; // at://did:plc:.../app.bsky.feed.post/... 184 + echo $post->cid; // bafyre... 185 + ``` 186 + 187 + ### Rich Text with Mentions, Links, and Hashtags 188 + 189 + Use the `TextBuilder` for posts with rich text formatting: 190 + 191 + ```php 192 + use SocialDept\AtpClient\RichText\TextBuilder; 193 + 194 + $content = TextBuilder::make() 195 + ->text('Check out ') 196 + ->mention('someone.bsky.social') 197 + ->text(' and visit ') 198 + ->link('our website', 'https://example.com') 199 + ->text(' ') 200 + ->tag('Laravel') 201 + ->toArray(); 202 + 203 + $post = $client->bsky->post->create($content); 204 + ``` 205 + 206 + Or use auto-detection on plain text: 207 + 208 + ```php 209 + // Facets are automatically detected 210 + $post = $client->bsky->post->create( 211 + 'Hello @someone.bsky.social! Check out https://example.com #Bluesky' 212 + ); 213 + ``` 214 + 215 + ### Reply to a Post 216 + 217 + ```php 218 + $parent = new StrongRef(uri: 'at://...', cid: 'bafyre...'); 219 + $root = $parent; // Same as parent for direct replies 220 + 221 + $reply = $client->bsky->post->reply( 222 + parent: $parent, 223 + root: $root, 224 + content: 'This is a reply!' 225 + ); 226 + ``` 227 + 228 + ### Quote Post 229 + 230 + ```php 231 + $quotedPost = new StrongRef(uri: 'at://...', cid: 'bafyre...'); 232 + 233 + $quote = $client->bsky->post->quote( 234 + quotedPost: $quotedPost, 235 + content: 'Interesting take!' 236 + ); 237 + ``` 238 + 239 + ### Post with Images 240 + 241 + ```php 242 + // Upload from a Laravel request 243 + $blob = $client->atproto->repo->uploadBlob($request->file('image')); 244 + 245 + // Or from a file path 246 + $blob = $client->atproto->repo->uploadBlob(new SplFileInfo('/path/to/image.jpg')); 247 + 248 + // Or from raw binary data (mimeType required) 249 + $blob = $client->atproto->repo->uploadBlob( 250 + file: file_get_contents('/path/to/image.jpg'), 251 + mimeType: 'image/jpeg' 252 + ); 253 + 254 + $post = $client->bsky->post->withImages( 255 + content: 'Check out this photo!', 256 + images: [ 257 + [ 258 + 'image' => $blob->json('blob'), 259 + 'alt' => 'Description of the image', 260 + ], 261 + ] 262 + ); 263 + ``` 264 + 265 + ### Post with External Link Card 266 + 267 + ```php 268 + $post = $client->bsky->post->withLink( 269 + content: 'Great article about Laravel', 270 + uri: 'https://example.com/article', 271 + title: 'Article Title', 272 + description: 'A brief description of the article...' 273 + ); 274 + ``` 275 + 276 + ### Delete a Post 277 + 278 + ```php 279 + // Extract rkey from the post URI 280 + $rkey = basename($post->uri); 281 + 282 + $client->bsky->post->delete($rkey); 283 + ``` 284 + 285 + ## Working with Profiles 286 + 287 + ### Get a Profile 288 + 289 + ```php 290 + $profile = $client->bsky->actor->getProfile('someone.bsky.social'); 291 + 292 + echo $profile->json('displayName'); 293 + echo $profile->json('description'); 294 + echo $profile->json('followersCount'); 295 + ``` 296 + 297 + ### Update Your Profile 298 + 299 + ```php 300 + // Update display name 301 + $client->bsky->profile->updateDisplayName('New Name'); 302 + 303 + // Update bio/description 304 + $client->bsky->profile->updateDescription('Laravel developer building on AT Protocol'); 305 + 306 + // Update multiple fields at once 307 + $client->bsky->profile->update([ 308 + 'displayName' => 'New Name', 309 + 'description' => 'New bio here', 310 + ]); 311 + ``` 312 + 313 + ### Update Avatar 314 + 315 + ```php 316 + $blob = $client->atproto->repo->uploadBlob(new SplFileInfo('/path/to/avatar.jpg')); 317 + 318 + $client->bsky->profile->updateAvatar($blob->json('blob')); 319 + ``` 320 + 321 + ## Social Graph 322 + 323 + ### Follow a User 324 + 325 + ```php 326 + // Follow requires the user's DID 327 + $follow = $client->bsky->follow->create('did:plc:...'); 328 + ``` 329 + 330 + ### Unfollow a User 331 + 332 + ```php 333 + // Get the rkey from the follow record URI 334 + $client->bsky->follow->delete($rkey); 335 + ``` 336 + 337 + ### Like a Post 338 + 339 + ```php 340 + $postRef = new StrongRef(uri: 'at://...', cid: 'bafyre...'); 341 + 342 + $like = $client->bsky->like->create($postRef); 343 + ``` 344 + 345 + ### Unlike a Post 346 + 347 + ```php 348 + $client->bsky->like->delete($rkey); 349 + ``` 350 + 351 + ## Feed Operations 352 + 353 + ### Get Your Timeline 354 + 355 + ```php 356 + $timeline = $client->bsky->feed->getTimeline(limit: 50); 357 + 358 + foreach ($timeline->json('feed') as $item) { 359 + $post = $item['post']; 360 + echo $post['author']['handle'] . ': ' . $post['record']['text']; 361 + } 362 + ``` 363 + 364 + ### Pagination with Cursors 365 + 366 + ```php 367 + $cursor = null; 368 + 369 + do { 370 + $timeline = $client->bsky->feed->getTimeline(limit: 100, cursor: $cursor); 371 + 372 + foreach ($timeline->json('feed') as $item) { 373 + // Process posts 374 + } 375 + 376 + $cursor = $timeline->json('cursor'); 377 + } while ($cursor); 378 + ``` 379 + 380 + ### Get Author Feed 381 + 382 + ```php 383 + $feed = $client->bsky->feed->getAuthorFeed( 384 + actor: 'someone.bsky.social', 385 + limit: 50 386 + ); 387 + ``` 388 + 389 + ### Search Posts 390 + 391 + ```php 392 + $results = $client->bsky->feed->searchPosts( 393 + q: 'laravel php', 394 + limit: 25 395 + ); 396 + ``` 397 + 398 + ### Get Post Thread 399 + 400 + ```php 401 + $thread = $client->bsky->feed->getPostThread( 402 + uri: 'at://did:plc:.../app.bsky.feed.post/...', 403 + depth: 6 404 + ); 405 + ``` 406 + 407 + ### Get Likes on a Post 408 + 409 + ```php 410 + $likes = $client->bsky->feed->getLikes(uri: 'at://...'); 411 + ``` 412 + 413 + ### Get Reposts 414 + 415 + ```php 416 + $reposts = $client->bsky->feed->getRepostedBy(uri: 'at://...'); 417 + ``` 418 + 419 + ## Configuration 420 + 421 + After publishing the config file, you can customize these options: 422 + 423 + ```php 424 + // config/client.php 425 + 426 + return [ 427 + // OAuth client metadata 428 + 'client' => [ 429 + 'name' => env('ATP_CLIENT_NAME', config('app.name')), 430 + 'url' => env('ATP_CLIENT_URL', config('app.url')), 431 + 'redirect_uris' => [ 432 + env('ATP_CLIENT_REDIRECT_URI', config('app.url').'/auth/atp/callback'), 433 + ], 434 + 'scopes' => ['atproto', 'transition:generic'], 435 + ], 436 + 437 + // Credential storage provider 438 + 'credential_provider' => \SocialDept\AtpClient\Providers\ArrayCredentialProvider::class, 439 + 440 + // Session behavior 441 + 'session' => [ 442 + 'refresh_threshold' => 300, // Refresh if expires within 5 minutes 443 + 'dpop_key_rotation' => 86400, // Rotate DPoP keys after 24 hours 444 + ], 445 + 446 + // OAuth settings 447 + 'oauth' => [ 448 + 'disabled' => false, 449 + 'prefix' => '/atp/oauth/', 450 + 'private_key' => env('ATP_OAUTH_PRIVATE_KEY'), 451 + ], 452 + 453 + // HTTP client settings 454 + 'http' => [ 455 + 'timeout' => 30, 456 + 'retry' => [ 457 + 'times' => 3, 458 + 'sleep' => 100, 459 + ], 460 + ], 461 + ]; 462 + ``` 463 + 464 + ### Environment Variables 465 + 466 + ```env 467 + ATP_CLIENT_NAME="My App" 468 + ATP_CLIENT_URL="https://myapp.com" 469 + ATP_CLIENT_REDIRECT_URI="https://myapp.com/auth/atp/callback" 470 + ATP_OAUTH_PRIVATE_KEY="base64-encoded-private-key" 471 + ATP_REFRESH_THRESHOLD=300 472 + ATP_HTTP_TIMEOUT=30 473 + ``` 474 + 475 + ## Credential Storage 476 + 477 + The package uses a `CredentialProvider` interface for token storage. The default `ArrayCredentialProvider` stores credentials in memory (lost on request end). 478 + 479 + ### Implementing Custom Storage 480 + 481 + ```php 482 + use SocialDept\AtpClient\Contracts\CredentialProvider; 483 + use SocialDept\AtpClient\Data\AccessToken; 484 + use SocialDept\AtpClient\Data\Credentials; 485 + 486 + class DatabaseCredentialProvider implements CredentialProvider 487 + { 488 + public function getCredentials(string $identifier): ?Credentials 489 + { 490 + $record = AtpCredential::where('identifier', $identifier)->first(); 491 + 492 + if (!$record) { 493 + return null; 494 + } 495 + 496 + return new Credentials( 497 + identifier: $record->identifier, 498 + did: $record->did, 499 + accessToken: $record->access_token, 500 + refreshToken: $record->refresh_token, 501 + expiresAt: $record->expires_at, 502 + ); 503 + } 504 + 505 + public function storeCredentials(string $identifier, AccessToken $token): void 506 + { 507 + AtpCredential::create([ 508 + 'identifier' => $identifier, 509 + 'did' => $token->did, 510 + 'access_token' => $token->accessJwt, 511 + 'refresh_token' => $token->refreshJwt, 512 + 'expires_at' => $token->expiresAt, 513 + ]); 514 + } 515 + 516 + public function updateCredentials(string $identifier, AccessToken $token): void 517 + { 518 + AtpCredential::where('identifier', $identifier)->update([ 519 + 'access_token' => $token->accessJwt, 520 + 'refresh_token' => $token->refreshJwt, 521 + 'expires_at' => $token->expiresAt, 522 + ]); 523 + } 524 + 525 + public function removeCredentials(string $identifier): void 526 + { 527 + AtpCredential::where('identifier', $identifier)->delete(); 528 + } 529 + } 530 + ``` 531 + 532 + Register your provider in the config: 533 + 534 + ```php 535 + 'credential_provider' => App\Providers\DatabaseCredentialProvider::class, 536 + ``` 537 + 538 + ## Events 539 + 540 + The package dispatches events you can listen to: 541 + 542 + ```php 543 + use SocialDept\AtpClient\Events\TokenRefreshing; 544 + use SocialDept\AtpClient\Events\TokenRefreshed; 545 + 546 + // Before token refresh 547 + Event::listen(TokenRefreshing::class, function ($event) { 548 + Log::info('Refreshing token for: ' . $event->identifier); 549 + }); 550 + 551 + // After token refresh 552 + Event::listen(TokenRefreshed::class, function ($event) { 553 + // Update your stored credentials 554 + $this->credentialProvider->updateCredentials( 555 + $event->identifier, 556 + $event->token 557 + ); 558 + }); 559 + ``` 560 + 561 + ## Available Commands 562 + 563 + ```bash 564 + # Generate OAuth private key 565 + php artisan atp-client:generate-key 566 + ``` 567 + 568 + ## Requirements 569 + 570 + - PHP 8.2+ 571 + - Laravel 11 or 12 572 + - [socialdept/atp-schema](https://github.com/socialdept/atp-schema) ^0.2 573 + - [socialdept/atp-resolver](https://github.com/socialdept/atp-resolver) ^1.0 574 + 575 + ## Testing 576 + 577 + ```bash 578 + composer test 579 + ``` 580 + 581 + ## Resources 582 + 583 + - [AT Protocol Documentation](https://atproto.com/) 584 + - [Bluesky API Docs](https://docs.bsky.app/) 585 + - [CRYPTO.md](CRYPTO.md) - Cryptographic implementation details 586 + 587 + ## Support & Contributing 588 + 589 + Found a bug or have a feature request? [Open an issue](https://github.com/socialdept/atp-client/issues). 590 + 591 + Want to contribute? Check out the [contribution guidelines](contributing.md). 592 + 593 + ## Changelog 594 + 595 + Please see [changelog](changelog.md) for recent changes. 596 + 597 + ## Credits 598 + 599 + - [Miguel Batres](https://batres.co) - founder & lead maintainer 600 + - [All contributors](https://github.com/socialdept/atp-client/graphs/contributors) 601 + 602 + ## License 603 + 604 + AtpClient is open-source software licensed under the [MIT license](license.md). 605 + 606 + --- 607 + 608 + **Built for the Federation** - By Social Dept.
header.png

This is a binary file and will not be displayed.

-57
readme.md
··· 1 - # AtpClient 2 - 3 - [![Latest Version on Packagist][ico-version]][link-packagist] 4 - [![Total Downloads][ico-downloads]][link-downloads] 5 - [![Build Status][ico-travis]][link-travis] 6 - [![StyleCI][ico-styleci]][link-styleci] 7 - 8 - This is where your description should go. Take a look at [contributing.md](contributing.md) to see a to do list. 9 - 10 - ## Installation 11 - 12 - Via Composer 13 - 14 - ```bash 15 - composer require social-dept/atp-client 16 - ``` 17 - 18 - ## Usage 19 - 20 - ## Change log 21 - 22 - Please see the [changelog](changelog.md) for more information on what has changed recently. 23 - 24 - ## Testing 25 - 26 - ```bash 27 - composer test 28 - ``` 29 - 30 - ## Contributing 31 - 32 - Please see [contributing.md](contributing.md) for details and a todolist. 33 - 34 - ## Security 35 - 36 - If you discover any security related issues, please email author@email.com instead of using the issue tracker. 37 - 38 - ## Credits 39 - 40 - - [Author Name][link-author] 41 - - [All Contributors][link-contributors] 42 - 43 - ## License 44 - 45 - MIT. Please see the [license file](license.md) for more information. 46 - 47 - [ico-version]: https://img.shields.io/packagist/v/social-dept/atp-client.svg?style=flat-square 48 - [ico-downloads]: https://img.shields.io/packagist/dt/social-dept/atp-client.svg?style=flat-square 49 - [ico-travis]: https://img.shields.io/travis/social-dept/atp-client/master.svg?style=flat-square 50 - [ico-styleci]: https://styleci.io/repos/12345678/shield 51 - 52 - [link-packagist]: https://packagist.org/packages/social-dept/atp-client 53 - [link-downloads]: https://packagist.org/packages/social-dept/atp-client 54 - [link-travis]: https://travis-ci.org/social-dept/atp-client 55 - [link-styleci]: https://styleci.io/repos/12345678 56 - [link-author]: https://github.com/social-dept 57 - [link-contributors]: ../../contributors