gimucco/bluesky-php
Composer 安装命令:
composer require gimucco/bluesky-php
包简介
Bluesky / AT Protocol API client for PHP — a typed wrapper over the lexicon API, built on gimucco/atproto-php for OAuth.
README 文档
README
A typed PHP client for the Bluesky / AT Protocol API — built on gimucco/atproto-php for OAuth 2.1 / DPoP authentication.
- 93 typed API methods + 28 auto-paginators generated from official Bluesky lexicons
- 250 generated value-object types with
fromArray()/toArray() - Convenience methods for posts, threads, all 5 embed types, engagement, social graph, moderation
- PHPStan level 10 with strict-rules — fully statically typed
- OAuth 2.1 + DPoP — no app-password flow (deprecated by Bluesky)
At a glance
use Gimucco\Bluesky\Client; use Gimucco\Bluesky\EmbeddedImage; $client = new Client($session); // session restored from atproto-php OAuth flow $client->post('Hello world'); $client->post('With image', images: [new EmbeddedImage($blob, alt: 'A sunset')]); $client->postVideo('Watch this', $videoBytes, alt: 'A clip'); // upload + await + post $client->thread('First', 'Second', 'Third'); $client->reply($parentUri, $parentCid, 'Great post!'); $client->like($postUri, $postCid); $client->follow('alice.bsky.social'); // handle auto-resolved to DID $client->block('did:plc:troll'); $me = $client->myProfile(); $post = $client->getPost($uri);
Cheat sheet
| Domain | Methods |
|---|---|
| Posting | post(), reply(), thread(), deletePost() |
| Embeds | EmbeddedImage, EmbeddedVideo, EmbeddedExternal, EmbeddedRecord, EmbeddedRecordWithMedia |
| Engagement | like() / unlike(), repost() / unrepost() |
| Social graph | follow() / unfollow(), block() / unblock(), mute() / unmute() |
| Reading | myProfile(), getPost() |
| Media uploads | uploadImage(), uploadVideo(), postVideo(), awaitVideo() |
| Pagination | feed->paginateTimeline(), graph->paginateFollowers(), … (28 auto-generated) + Pager::iterate() for custom shapes |
| Lexicon API | $client->actor, $client->feed, $client->graph, $client->notification, $client->repo, $client->identity, $client->server, $client->label, $client->video, $client->bookmark, $client->labeler |
| Identifiers | Did, Handle, AtUri (validated, Stringable) |
| Refs | PostRef, FollowRef, LikeRef, RepostRef, BlockRef (all Stringable to their uri) |
See examples/ for runnable code per use case.
Installation
composer require gimucco/bluesky-php
composer require guzzlehttp/guzzle # recommended HTTP client
Requirements: PHP 8.2+, ext-json, ext-fileinfo, ext-curl, ext-openssl, ext-sodium (curl/openssl/sodium come from gimucco/atproto-php).
Authentication: OAuth only
This library performs Bluesky/AT Protocol API calls. It does not handle authentication — that's gimucco/atproto-php, which implements the AT Protocol's mandatory OAuth 2.1 + DPoP + PAR profile.
There is no app-password / identifier+password flow. App passwords are deprecated; OAuth 2.1 is the only path. To use this library you set up the OAuth flow once (login redirect → callback → stored session), then restore the session by DID for subsequent API calls.
For long-running automation: log in once interactively, then reuse the persisted session indefinitely (tokens auto-refresh).
Quick start
The shortest possible example, assuming you already have a stored session:
use Gimucco\Bluesky\Client; $client = new Client($session); // see "OAuth setup" below for how to get $session $client->post('Hello from bluesky-php!');
OAuth setup (one-time)
- Generate an ES256 key
openssl ecparam -genkey -name prime256v1 -noout -out private.pem
- Host two static JSON files at HTTPS URLs (
client-metadata.jsonandjwks.json). Usebin/generate-metadatafromvendor/gimucco/atproto-phpto produce them. - Configure & build the OAuth client — see
examples/login.phpandexamples/callback.phpfor the complete browser flow, andexamples/bootstrap.phpfor the runtime restore pattern.
Restoring a session at runtime
use Gimucco\Atproto\ClientConfig; use Gimucco\Atproto\OAuthClient; use Gimucco\Atproto\Storage\FileSessionStore; use Gimucco\Atproto\Storage\FileStateStore; use Gimucco\Bluesky\Client; use GuzzleHttp\Client as GuzzleClient; use GuzzleHttp\Psr7\HttpFactory; $factory = new HttpFactory(); $oauth = new OAuthClient( config: new ClientConfig( clientId: 'https://your-app.com/atproto/client-metadata.json', redirectUri: 'https://your-app.com/atproto/callback', scope: 'atproto transition:generic', clientName: 'My Bluesky App', jwksUri: 'https://your-app.com/atproto/jwks.json', privateKey: file_get_contents('/secure/private.pem'), encryptionPassphrase: getenv('ATPROTO_PASSPHRASE'), ), sessionStore: new FileSessionStore('/var/app/sessions', getenv('ATPROTO_PASSPHRASE')), stateStore: new FileStateStore('/var/app/states', getenv('ATPROTO_PASSPHRASE')), httpClient: new GuzzleClient(['timeout' => 30]), requestFactory: $factory, streamFactory: $factory, ); $session = $oauth->restoreSession('did:plc:abc123'); if ($session === null) { throw new RuntimeException('No stored session — user must complete OAuth flow first'); } $client = new Client($session);
Posting
Plain text
$ref = $client->post('Hello world'); echo $ref->uri;
Reply
$ref = $client->reply( parentUri: $parentUri, parentCid: $parentCid, text: 'Great take!', );
For nested replies in a thread, also pass rootUri and rootCid of the thread root.
Thread
$refs = $client->thread( 'First post in the thread 🧵', 'Second post (auto-replies to the first)', 'Third post (auto-replies to the second, root remains the first)', );
For long threads, throttle to avoid burst rate limits:
$client->setDefaultThreadDelay(2)->thread('First', 'Second', ...);
Note: thread() is not transactional — if a mid-thread post fails, prior posts remain published. See the method's docblock for partial-failure semantics.
All 5 embed types
A post can carry at most one embed (the EmbeddedRecordWithMedia type combines a quote with images/video). Mismatch throws InvalidArgumentException.
use Gimucco\Bluesky\{ EmbeddedImage, EmbeddedVideo, EmbeddedExternal, EmbeddedRecord, EmbeddedRecordWithMedia, }; // Images (up to 4 — alt text strongly recommended for accessibility) $client->post('caption', images: [ new EmbeddedImage($blob, alt: 'A sunset over the ocean'), ]); // Video (after upload + awaitVideo) $client->post('Watch this', video: new EmbeddedVideo($videoBlob, alt: '...')); // Link card (with optional thumbnail blob) $client->post('Worth a read:', external: new EmbeddedExternal( uri: 'https://example.com/article', title: 'Article title', description: 'Card description', thumb: $thumbnailBlob, // optional )); // Quote post $client->post('Look at this 👇', quoting: new EmbeddedRecord($postUri, $postCid)); // Quote with media (images OR video) $client->post('My take, with proof:', quoting: new EmbeddedRecordWithMedia( record: new EmbeddedRecord($postUri, $postCid), images: [new EmbeddedImage($blob, alt: 'screenshot')], ));
EmbeddedExternal rejects non-http(s) URIs at construction time.
Media
Images
$blob = $client->uploadImage(file_get_contents('photo.jpg')); // MIME auto-detected $blob = $client->uploadImage($bytes, 'image/webp'); // explicit MIME
Empty bytes or empty MIME string throws InvalidArgumentException.
Video
Bluesky processes videos asynchronously, but the convenience methods hide the polling. Three levels of API, in order of decreasing convenience:
// One-shot: upload + await + post. $ref = $client->postVideo('Watch this', $bytes, alt: 'A clip'); // Returns a BlobRef — for reuse (same video on multiple posts, retries, recordWithMedia, etc.) $blob = $client->uploadVideo($bytes); $client->post('Watch this', video: new EmbeddedVideo($blob, alt: '...')); $client->reply($parent, $cid, 'see this', video: new EmbeddedVideo($blob)); // Lowest level: drive the upload + poll loop yourself. $job = $client->video->uploadVideo($bytes); $blob = $client->awaitVideo($job->jobStatus->jobId, timeoutSeconds: 60);
All three block the calling thread — fine for CLI / cron / queue, not appropriate inside a synchronous web request. The default await timeout is 120 s (exponential backoff capped at 10 s/poll); pass a smaller timeoutSeconds from a request with a max execution budget.
Video calls do not go to the user's PDS — Bluesky operates a dedicated video processing service at https://video.bsky.app. The library handles routing transparently: each call mints a short-lived service-auth JWT via the user's PDS (com.atproto.server.getServiceAuth) with the audience set to the user's PDS DID (did:web:<pdsHost>, derived from the live session) and sends it as a plain bearer token to the video service, which validates the issuer chain back to the user's PDS. Uploads use lxm=com.atproto.repo.uploadBlob (the video service treats them as generic blob uploads); status / limits use their lexicon-defined lxm values. Re-uploading the exact same bytes (content-hash dedupe) is recognized server-side and returned as a successful job — no 409 thrown. The HTTP path uses curl with explicit timeouts (10 s connect, 120 s total upload, 30 s status) and SSL verification on. To target a different video service host, pass videoServiceUrl: 'https://your-video-service' to new Client(...).
Engagement
$client->like($postUri, $postCid); // returns LikeRef $client->unlike($likeRef); // accepts string|AtUri|LikeRef (Stringable) $client->repost($postUri, $postCid); $client->unrepost($repostRef);
Social graph
follow() and block() accept handle, DID, Did, or Handle. Handles are auto-resolved to DIDs (one extra API call). mute() accepts both directly (no resolution needed).
$client->follow('alice.bsky.social'); // resolves handle → DID, then follows $client->follow('did:plc:abc'); // direct, no resolution call $client->follow(new Did('did:plc:abc')); // typed $client->unfollow($followRef); $client->block('did:plc:troll'); // returns BlockRef $client->unblock($blockRef); $client->mute('spammer.bsky.social'); $client->unmute('spammer.bsky.social');
Reading
$me = $client->myProfile(); // ProfileViewDetailed $them = $client->actor->getProfile('alice.bsky.social'); $post = $client->getPost('at://did:plc:.../app.bsky.feed.post/abc'); // throws NotFoundException if missing // Iterate the timeline (auto-pages) foreach ($client->feed->paginateTimeline(limit: 50, maxItems: 200) as $item) { echo $item->post->author->handle.': '.$item->post->record['text']."\n"; }
The full generated API surface is on $client->actor, $client->feed, $client->graph, etc. See examples/profile-and-fetch.php, examples/feed-walker.php, examples/notifications.php.
Pagination
34 of the 93 generated methods accept cursor and return {cursor, items}. Each gets a typed paginate* companion:
foreach ($client->feed->paginateTimeline() as $item) { /* ... */ } foreach ($client->graph->paginateFollowers('alice.bsky.social') as $follower) { /* ... */ } foreach ($client->notification->paginateNotifications() as $notif) { /* ... */ }
For custom shapes or non-generated endpoints, use Pager::iterate():
use Gimucco\Bluesky\Pager; $items = Pager::iterate( fetch: fn(?string $cursor) => [ ($r = $client->feed->getTimeline(cursor: $cursor))->feed, $r->cursor, ], maxItems: 500, );
Typed identifiers
use Gimucco\Bluesky\{Did, Handle, AtUri}; $did = new Did('did:plc:abc123'); $client->follow($did); $uri = new AtUri('at://did:plc:alice/app.bsky.feed.post/abc'); echo $uri->authority; // did:plc:alice echo $uri->collection; // app.bsky.feed.post echo $uri->rkey; // abc $h = new Handle('@alice.bsky.social'); // strips leading @ echo $h->value; // alice.bsky.social
All five Ref classes (PostRef, FollowRef, LikeRef, RepostRef, BlockRef) are Stringable to their uri — you can pass them directly to delete methods (unfollow($followRef), etc.).
Logging
use Monolog\Logger; use Monolog\Handler\StreamHandler; $logger = new Logger('bluesky'); $logger->pushHandler(new StreamHandler('php://stderr')); $client = new Client($session, $logger);
Convenience methods emit debug-level events on post/follow/like/etc. Heads up: log context includes subject DIDs and post URIs, so a shared log file becomes a who-blocks-whom audit trail. Configure your logger accordingly.
Error handling
use Gimucco\Bluesky\Exception\{ BlueskyException, // base class ApiException, // catch-all for HTTP errors NotFoundException, // 404 RateLimitException, // 429 — exposes ->retryAfter (DateTimeImmutable|null) ValidationException, // 400 from API AuthException, // 401, 403 ServerException, // 5xx LexiconException, // malformed response from server InvalidArgumentException, // bad input to this library (distinct from API 400) }; try { $client->getPost($uri); } catch (NotFoundException $e) { // handle "not found" } catch (RateLimitException $e) { sleep((int) ($e->retryAfter?->getTimestamp() - time() ?? 60)); // retry... } catch (ApiException $e) { // any other HTTP error error_log("Bluesky {$e->status}: {$e->error} — {$e->getMessage()}"); }
Code generation
The src/Generated/ directory is produced from AT Protocol lexicon JSONs (which are not committed — fetched on demand via composer sync-lexicons).
composer sync-lexicons # download latest lexicons from upstream composer generate # regenerate PHP from lexicons composer generate-check # verify committed output is current (used in CI)
Skipped namespaces (out of scope for this library): com.atproto.sync.*, chat.bsky.*, tools.ozone.*, com.atproto.admin.*, com.atproto.temp.*, com.atproto.moderation.*, app.bsky.unspecced.*.
Development
composer install composer test # PHPUnit composer phpstan # level 10 + strict-rules composer cs-check # PER-CS 2.0 composer cs-fix
For real-API smoke tests against your test account, see tests/manual/.
Project structure
src/
├── Client.php # Main facade
├── RichText.php # Facet parser (links, mentions, hashtags)
├── Pager.php # Closure-based pagination helper
├── RefTrait.php # Shared body for the 5 Ref classes
├── Did.php # Validated DID value object
├── Handle.php # Validated handle value object
├── AtUri.php # Parsed at-uri value object
├── BlobRef.php # Blob reference (uploaded blob)
├── PostRef.php, FollowRef.php, LikeRef.php, RepostRef.php, BlockRef.php
├── EmbeddedImage.php, EmbeddedVideo.php, EmbeddedExternal.php
├── EmbeddedRecord.php, EmbeddedRecordWithMedia.php
├── Exception/ # 9 typed exceptions
├── Internal/Cast.php # JSON narrowing helpers (used by generated code)
└── Generated/ # Auto-generated (do not edit)
├── Methods/ # 14 method classes (93 methods + 28 paginators)
└── Types/ # 250 value-object types
bin/
├── generate-lexicons # Code generator
├── sync-lexicons # Lexicon downloader
└── lib/filter.php # Shared scope/skip rules
examples/ # 13 runnable example scripts
tests/ # PHPUnit (Unit + Integration) + manual harness
License
GPL-2.0-or-later. See LICENSE.
gimucco/bluesky-php 适用场景与选型建议
gimucco/bluesky-php 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 8 次下载、GitHub Stars 达 1, 最近一次更新时间为 2026 年 05 月 01 日, 在 PHP 生态内属于活跃度较高的组件。
它主要适用于以下技术方向: 「social」 「API-Client」 「atproto」 「bluesky」 「at-protocol」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。
我们在过去多个企业项目中使用过 gimucco/bluesky-php 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。
基于 gimucco/bluesky-php 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。
线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。
承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。
与 gimucco/bluesky-php 相关的其它包
同方向 / 同关键字的高下载量 PHP Composer 包推荐,方便对比选型:
An small PHP library for posting messages to the bluesky social network using the AT Protocol.
Auto generated pure PHP interface and enum
Bluesky(AT Protocol) for Laravel
Simple Sharing generates social media share links within CP entry pages, allowing you to quickly & easily share entries.
Skolkovo API Client
LinkedIn integration for Social
统计信息
- 总下载量: 8
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 1
- 点击次数: 45
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: GPL-2.0-or-later
- 更新时间: 2026-05-01