Laravel AT Protocol Client (alpha & unstable)
3
fork

Configure Feed

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

Add Bluesky documentation comments and fix `uploadBlob()` implementation

+239 -28
+6 -5
composer.json
··· 19 19 ], 20 20 "require": { 21 21 "php": "^8.2", 22 - "illuminate/support": "^11.0|^12.0", 23 - "illuminate/http": "^11.0|^12.0", 22 + "ext-fileinfo": "*", 23 + "firebase/php-jwt": "^6.0", 24 24 "guzzlehttp/guzzle": "^7.0", 25 + "illuminate/http": "^11.0|^12.0", 26 + "illuminate/support": "^11.0|^12.0", 25 27 "phpseclib/phpseclib": "^3.0", 26 - "firebase/php-jwt": "^6.0", 27 - "socialdept/atp-schema": "^0.2", 28 - "socialdept/atp-resolver": "^1.0" 28 + "socialdept/atp-resolver": "^1.0", 29 + "socialdept/atp-schema": "^0.2" 29 30 }, 30 31 "require-dev": { 31 32 "orchestra/testbench": "^9.0",
+4
src/Client/Requests/Atproto/IdentityRequestClient.php
··· 9 9 { 10 10 /** 11 11 * Resolve handle to DID 12 + * 13 + * @see https://docs.bsky.app/docs/api/com-atproto-identity-resolve-handle 12 14 */ 13 15 public function resolveHandle(string $handle): Response 14 16 { ··· 20 22 21 23 /** 22 24 * Update handle 25 + * 26 + * @see https://docs.bsky.app/docs/api/com-atproto-identity-update-handle 23 27 */ 24 28 public function updateHandle(string $handle): Response 25 29 {
+42 -4
src/Client/Requests/Atproto/RepoRequestClient.php
··· 2 2 3 3 namespace SocialDept\AtpClient\Client\Requests\Atproto; 4 4 5 + use Illuminate\Http\UploadedFile; 6 + use InvalidArgumentException; 5 7 use SocialDept\AtpClient\Client\Requests\Request; 6 8 use SocialDept\AtpClient\Http\Response; 9 + use SplFileInfo; 10 + use Throwable; 7 11 8 12 class RepoRequestClient extends Request 9 13 { 10 14 /** 11 15 * Create a record 16 + * 17 + * @see https://docs.bsky.app/docs/api/com-atproto-repo-create-record 12 18 */ 13 19 public function createRecord( 14 20 string $repo, ··· 29 35 30 36 /** 31 37 * Delete a record 38 + * 39 + * @see https://docs.bsky.app/docs/api/com-atproto-repo-delete-record 32 40 */ 33 41 public function deleteRecord( 34 42 string $repo, ··· 48 56 49 57 /** 50 58 * Put (upsert) a record 59 + * 60 + * @see https://docs.bsky.app/docs/api/com-atproto-repo-put-record 51 61 */ 52 62 public function putRecord( 53 63 string $repo, ··· 69 79 70 80 /** 71 81 * Get a record 82 + * 83 + * @see https://docs.bsky.app/docs/api/com-atproto-repo-get-record 72 84 */ 73 85 public function getRecord( 74 86 string $repo, ··· 84 96 85 97 /** 86 98 * List records in a collection 99 + * 100 + * @see https://docs.bsky.app/docs/api/com-atproto-repo-list-records 87 101 */ 88 102 public function listRecords( 89 103 string $repo, ··· 99 113 } 100 114 101 115 /** 102 - * Upload a blob 116 + * Upload a new blob, to be referenced from a repository record 117 + * 118 + * The blob will be deleted if it is not referenced within a time window. 119 + * 120 + * @param UploadedFile|SplFileInfo|string $file The file to upload 121 + * @param string|null $mimeType MIME type (required for string input, auto-detected for file objects) 122 + * 123 + * @throws InvalidArgumentException|Throwable When $file is a string and $mimeType is not provided 124 + * 125 + * @see https://docs.bsky.app/docs/api/com-atproto-repo-upload-blob 103 126 */ 104 - public function uploadBlob(string $data, string $mimeType): Response 127 + public function uploadBlob(UploadedFile|SplFileInfo|string $file, ?string $mimeType = null): Response 105 128 { 106 - return $this->atp->client->post( 129 + // Handle different input types 130 + if ($file instanceof UploadedFile) { 131 + $data = $file->getContent(); 132 + $mimeType ??= $file->getMimeType(); 133 + } elseif ($file instanceof SplFileInfo) { 134 + $data = file_get_contents($file->getRealPath()); 135 + $mimeType ??= mime_content_type($file->getRealPath()) ?: 'application/octet-stream'; 136 + } else { 137 + throw_if($mimeType === null, new InvalidArgumentException('The $mimeType parameter is required when $file is a string.')); 138 + $data = $file; 139 + } 140 + 141 + return $this->atp->client->postBlob( 107 142 endpoint: 'com.atproto.repo.uploadBlob', 108 - body: ['blob' => $data, 'mimeType' => $mimeType] 143 + data: $data, 144 + mimeType: $mimeType 109 145 ); 110 146 } 111 147 112 148 /** 113 149 * Describe the repository 150 + * 151 + * @see https://docs.bsky.app/docs/api/com-atproto-repo-describe-repo 114 152 */ 115 153 public function describeRepo(string $repo): Response 116 154 {
+4
src/Client/Requests/Atproto/ServerRequestClient.php
··· 9 9 { 10 10 /** 11 11 * Get current session 12 + * 13 + * @see https://docs.bsky.app/docs/api/com-atproto-server-get-session 12 14 */ 13 15 public function getSession(): Response 14 16 { ··· 19 21 20 22 /** 21 23 * Describe server 24 + * 25 + * @see https://docs.bsky.app/docs/api/com-atproto-server-describe-server 22 26 */ 23 27 public function describeServer(): Response 24 28 {
+68 -18
src/Client/Requests/Atproto/SyncRequestClient.php
··· 8 8 class SyncRequestClient extends Request 9 9 { 10 10 /** 11 - * Get blob from sync 11 + * Get a blob associated with a given account 12 + * 13 + * @see https://docs.bsky.app/docs/api/com-atproto-sync-get-blob 12 14 */ 13 15 public function getBlob(string $did, string $cid): Response 14 16 { ··· 19 21 } 20 22 21 23 /** 22 - * Get checkout from sync 24 + * Download a repository export as CAR file 25 + * 26 + * @see https://docs.bsky.app/docs/api/com-atproto-sync-get-repo 27 + */ 28 + public function getRepo(string $did, ?string $since = null): Response 29 + { 30 + return $this->atp->client->get( 31 + endpoint: 'com.atproto.sync.getRepo', 32 + params: compact('did', 'since') 33 + ); 34 + } 35 + 36 + /** 37 + * Enumerates all the DID, rev, and commit CID for all repos hosted by this service 38 + * 39 + * @see https://docs.bsky.app/docs/api/com-atproto-sync-list-repos 40 + */ 41 + public function listRepos(int $limit = 500, ?string $cursor = null): Response 42 + { 43 + return $this->atp->client->get( 44 + endpoint: 'com.atproto.sync.listRepos', 45 + params: compact('limit', 'cursor') 46 + ); 47 + } 48 + 49 + /** 50 + * Get the current commit CID & revision of the specified repo 51 + * 52 + * @see https://docs.bsky.app/docs/api/com-atproto-sync-get-latest-commit 23 53 */ 24 - public function getCheckout(string $did): Response 54 + public function getLatestCommit(string $did): Response 25 55 { 26 56 return $this->atp->client->get( 27 - endpoint: 'com.atproto.sync.getCheckout', 57 + endpoint: 'com.atproto.sync.getLatestCommit', 28 58 params: compact('did') 29 59 ); 30 60 } 31 61 32 62 /** 33 - * Get commit path from sync 63 + * Get data blocks needed to prove the existence or non-existence of record 64 + * 65 + * @see https://docs.bsky.app/docs/api/com-atproto-sync-get-record 34 66 */ 35 - public function getCommitPath( 67 + public function getRecord(string $did, string $collection, string $rkey): Response 68 + { 69 + return $this->atp->client->get( 70 + endpoint: 'com.atproto.sync.getRecord', 71 + params: compact('did', 'collection', 'rkey') 72 + ); 73 + } 74 + 75 + /** 76 + * List blob CIDs for an account, since some repo revision 77 + * 78 + * @see https://docs.bsky.app/docs/api/com-atproto-sync-list-blobs 79 + */ 80 + public function listBlobs( 36 81 string $did, 37 - ?string $latest = null, 38 - ?string $earliest = null 82 + ?string $since = null, 83 + int $limit = 500, 84 + ?string $cursor = null 39 85 ): Response { 40 86 return $this->atp->client->get( 41 - endpoint: 'com.atproto.sync.getCommitPath', 42 - params: compact('did', 'latest', 'earliest') 87 + endpoint: 'com.atproto.sync.listBlobs', 88 + params: compact('did', 'since', 'limit', 'cursor') 43 89 ); 44 90 } 45 91 46 92 /** 47 - * Get repo from sync 93 + * Get data blocks from a given repo, by CID 94 + * 95 + * @see https://docs.bsky.app/docs/api/com-atproto-sync-get-blocks 48 96 */ 49 - public function getRepo(string $did, ?string $since = null): Response 97 + public function getBlocks(string $did, array $cids): Response 50 98 { 51 99 return $this->atp->client->get( 52 - endpoint: 'com.atproto.sync.getRepo', 53 - params: compact('did', 'since') 100 + endpoint: 'com.atproto.sync.getBlocks', 101 + params: compact('did', 'cids') 54 102 ); 55 103 } 56 104 57 105 /** 58 - * List repos from sync 106 + * Get the hosting status for a repository, on this server 107 + * 108 + * @see https://docs.bsky.app/docs/api/com-atproto-sync-get-repo-status 59 109 */ 60 - public function listRepos(int $limit = 500, ?string $cursor = null): Response 110 + public function getRepoStatus(string $did): Response 61 111 { 62 112 return $this->atp->client->get( 63 - endpoint: 'com.atproto.sync.listRepos', 64 - params: compact('limit', 'cursor') 113 + endpoint: 'com.atproto.sync.getRepoStatus', 114 + params: compact('did') 65 115 ); 66 116 } 67 117 }
+2
src/Client/Requests/Bsky/ActorRequestClient.php
··· 9 9 { 10 10 /** 11 11 * Get actor profile 12 + * 13 + * @see https://docs.bsky.app/docs/api/app-bsky-actor-get-profile 12 14 */ 13 15 public function getProfile(string $actor): Response 14 16 {
+12
src/Client/Requests/Bsky/FeedRequestClient.php
··· 9 9 { 10 10 /** 11 11 * Get timeline feed 12 + * 13 + * @see https://docs.bsky.app/docs/api/app-bsky-feed-get-timeline 12 14 */ 13 15 public function getTimeline(int $limit = 50, ?string $cursor = null): Response 14 16 { ··· 20 22 21 23 /** 22 24 * Get author feed 25 + * 26 + * @see https://docs.bsky.app/docs/api/app-bsky-feed-get-author-feed 23 27 */ 24 28 public function getAuthorFeed( 25 29 string $actor, ··· 34 38 35 39 /** 36 40 * Get post thread 41 + * 42 + * @see https://docs.bsky.app/docs/api/app-bsky-feed-get-post-thread 37 43 */ 38 44 public function getPostThread(string $uri, int $depth = 6): Response 39 45 { ··· 45 51 46 52 /** 47 53 * Search posts 54 + * 55 + * @see https://docs.bsky.app/docs/api/app-bsky-feed-search-posts 48 56 */ 49 57 public function searchPosts( 50 58 string $q, ··· 59 67 60 68 /** 61 69 * Get likes for a post 70 + * 71 + * @see https://docs.bsky.app/docs/api/app-bsky-feed-get-likes 62 72 */ 63 73 public function getLikes( 64 74 string $uri, ··· 73 83 74 84 /** 75 85 * Get reposts for a post 86 + * 87 + * @see https://docs.bsky.app/docs/api/app-bsky-feed-get-reposted-by 76 88 */ 77 89 public function getRepostedBy( 78 90 string $uri,
+6
src/Client/Requests/Chat/ActorRequestClient.php
··· 9 9 { 10 10 /** 11 11 * Get actor metadata 12 + * 13 + * @see https://docs.bsky.app/docs/api/chat-bsky-actor-export-account-data 12 14 */ 13 15 public function getActorMetadata(): Response 14 16 { ··· 19 21 20 22 /** 21 23 * Export account data 24 + * 25 + * @see https://docs.bsky.app/docs/api/chat-bsky-actor-export-account-data 22 26 */ 23 27 public function exportAccountData(): Response 24 28 { ··· 29 33 30 34 /** 31 35 * Delete account 36 + * 37 + * @see https://docs.bsky.app/docs/api/chat-bsky-actor-delete-account 32 38 */ 33 39 public function deleteAccount(): Response 34 40 {
+25 -1
src/Client/Requests/Chat/ConvoRequestClient.php
··· 9 9 { 10 10 /** 11 11 * Get conversation 12 + * 13 + * @see https://docs.bsky.app/docs/api/chat-bsky-convo-get-convo 12 14 */ 13 15 public function getConvo(string $convoId): Response 14 16 { ··· 20 22 21 23 /** 22 24 * Get conversation for members 25 + * 26 + * @see https://docs.bsky.app/docs/api/chat-bsky-convo-get-convo-for-members 23 27 */ 24 28 public function getConvoForMembers(array $members): Response 25 29 { ··· 31 35 32 36 /** 33 37 * List conversations 38 + * 39 + * @see https://docs.bsky.app/docs/api/chat-bsky-convo-list-convos 34 40 */ 35 41 public function listConvos(int $limit = 50, ?string $cursor = null): Response 36 42 { ··· 42 48 43 49 /** 44 50 * Get messages 51 + * 52 + * @see https://docs.bsky.app/docs/api/chat-bsky-convo-get-messages 45 53 */ 46 54 public function getMessages( 47 55 string $convoId, ··· 56 64 57 65 /** 58 66 * Send message 67 + * 68 + * @see https://docs.bsky.app/docs/api/chat-bsky-convo-send-message 59 69 */ 60 70 public function sendMessage(string $convoId, array $message): Response 61 71 { ··· 67 77 68 78 /** 69 79 * Send message batch 80 + * 81 + * @see https://docs.bsky.app/docs/api/chat-bsky-convo-send-message-batch 70 82 */ 71 83 public function sendMessageBatch(array $items): Response 72 84 { ··· 77 89 } 78 90 79 91 /** 80 - * Delete message 92 + * Delete message for self 93 + * 94 + * @see https://docs.bsky.app/docs/api/chat-bsky-convo-delete-message-for-self 81 95 */ 82 96 public function deleteMessageForSelf(string $convoId, string $messageId): Response 83 97 { ··· 89 103 90 104 /** 91 105 * Update read status 106 + * 107 + * @see https://docs.bsky.app/docs/api/chat-bsky-convo-update-read 92 108 */ 93 109 public function updateRead(string $convoId, ?string $messageId = null): Response 94 110 { ··· 100 116 101 117 /** 102 118 * Mute conversation 119 + * 120 + * @see https://docs.bsky.app/docs/api/chat-bsky-convo-mute-convo 103 121 */ 104 122 public function muteConvo(string $convoId): Response 105 123 { ··· 111 129 112 130 /** 113 131 * Unmute conversation 132 + * 133 + * @see https://docs.bsky.app/docs/api/chat-bsky-convo-unmute-convo 114 134 */ 115 135 public function unmuteConvo(string $convoId): Response 116 136 { ··· 122 142 123 143 /** 124 144 * Leave conversation 145 + * 146 + * @see https://docs.bsky.app/docs/api/chat-bsky-convo-leave-convo 125 147 */ 126 148 public function leaveConvo(string $convoId): Response 127 149 { ··· 133 155 134 156 /** 135 157 * Get log 158 + * 159 + * @see https://docs.bsky.app/docs/api/chat-bsky-convo-get-log 136 160 */ 137 161 public function getLog(?string $cursor = null): Response 138 162 {
+16
src/Client/Requests/Ozone/ModerationRequestClient.php
··· 9 9 { 10 10 /** 11 11 * Get moderation event 12 + * 13 + * @see https://docs.bsky.app/docs/api/tools-ozone-moderation-get-event 12 14 */ 13 15 public function getModerationEvent(int $id): Response 14 16 { ··· 20 22 21 23 /** 22 24 * Get moderation events 25 + * 26 + * @see https://docs.bsky.app/docs/api/tools-ozone-moderation-query-events 23 27 */ 24 28 public function getModerationEvents( 25 29 ?string $subject = null, ··· 39 43 40 44 /** 41 45 * Get record 46 + * 47 + * @see https://docs.bsky.app/docs/api/tools-ozone-moderation-get-record 42 48 */ 43 49 public function getRecord(string $uri, ?string $cid = null): Response 44 50 { ··· 50 56 51 57 /** 52 58 * Get repo 59 + * 60 + * @see https://docs.bsky.app/docs/api/tools-ozone-moderation-get-repo 53 61 */ 54 62 public function getRepo(string $did): Response 55 63 { ··· 61 69 62 70 /** 63 71 * Query events 72 + * 73 + * @see https://docs.bsky.app/docs/api/tools-ozone-moderation-query-events 64 74 */ 65 75 public function queryEvents( 66 76 ?array $types = null, ··· 81 91 82 92 /** 83 93 * Query statuses 94 + * 95 + * @see https://docs.bsky.app/docs/api/tools-ozone-moderation-query-statuses 84 96 */ 85 97 public function queryStatuses( 86 98 ?string $subject = null, ··· 100 112 101 113 /** 102 114 * Search repos 115 + * 116 + * @see https://docs.bsky.app/docs/api/tools-ozone-moderation-search-repos 103 117 */ 104 118 public function searchRepos( 105 119 ?string $term = null, ··· 118 132 119 133 /** 120 134 * Emit moderation event 135 + * 136 + * @see https://docs.bsky.app/docs/api/tools-ozone-moderation-emit-event 121 137 */ 122 138 public function emitEvent( 123 139 array $event,
+4
src/Client/Requests/Ozone/ServerRequestClient.php
··· 9 9 { 10 10 /** 11 11 * Get blob 12 + * 13 + * @see https://docs.bsky.app/docs/api/tools-ozone-server-get-config 12 14 */ 13 15 public function getBlob(string $did, string $cid): Response 14 16 { ··· 20 22 21 23 /** 22 24 * Get config 25 + * 26 + * @see https://docs.bsky.app/docs/api/tools-ozone-server-get-config 23 27 */ 24 28 public function getConfig(): Response 25 29 {
+10
src/Client/Requests/Ozone/TeamRequestClient.php
··· 9 9 { 10 10 /** 11 11 * Get team member 12 + * 13 + * @see https://docs.bsky.app/docs/api/tools-ozone-team-list-members 12 14 */ 13 15 public function getTeamMember(string $did): Response 14 16 { ··· 20 22 21 23 /** 22 24 * List team members 25 + * 26 + * @see https://docs.bsky.app/docs/api/tools-ozone-team-list-members 23 27 */ 24 28 public function listTeamMembers(int $limit = 50, ?string $cursor = null): Response 25 29 { ··· 31 35 32 36 /** 33 37 * Add team member 38 + * 39 + * @see https://docs.bsky.app/docs/api/tools-ozone-team-add-member 34 40 */ 35 41 public function addTeamMember(string $did, string $role): Response 36 42 { ··· 42 48 43 49 /** 44 50 * Update team member 51 + * 52 + * @see https://docs.bsky.app/docs/api/tools-ozone-team-update-member 45 53 */ 46 54 public function updateTeamMember( 47 55 string $did, ··· 59 67 60 68 /** 61 69 * Delete team member 70 + * 71 + * @see https://docs.bsky.app/docs/api/tools-ozone-team-delete-member 62 72 */ 63 73 public function deleteTeamMember(string $did): Response 64 74 {
+40
src/Http/HasHttp.php
··· 119 119 { 120 120 return $this->call($endpoint, 'DELETE', $params); 121 121 } 122 + 123 + /** 124 + * Make POST request with raw binary body (for blob uploads) 125 + */ 126 + protected function postBlob(string $endpoint, string $data, string $mimeType): Response 127 + { 128 + // Ensure session is valid (auto-refresh) 129 + $session = $this->sessions->ensureValid($this->identifier); 130 + 131 + // Build URL 132 + $url = rtrim($session->pdsEndpoint(), '/').'/xrpc/'.$endpoint; 133 + 134 + // Get DPoP nonce 135 + $nonce = $this->nonceManager->getNonce($session->pdsEndpoint()); 136 + 137 + // Create DPoP proof using DPoPKeyManager 138 + $dpopProof = app(DPoPKeyManager::class)->createProof( 139 + key: $session->dpopKey(), 140 + method: 'POST', 141 + url: $url, 142 + nonce: $nonce, 143 + accessToken: $session->accessToken(), 144 + ); 145 + 146 + // Build and send request with raw binary body 147 + $response = $this->http 148 + ->withHeaders([ 149 + 'Authorization' => 'Bearer '.$session->accessToken(), 150 + 'DPoP' => $dpopProof, 151 + ]) 152 + ->withBody($data, $mimeType) 153 + ->post($url); 154 + 155 + // Store nonce from response if present 156 + if ($newNonce = $response->header('DPoP-Nonce')) { 157 + $this->nonceManager->storeNonce($session->pdsEndpoint(), $newNonce); 158 + } 159 + 160 + return new Response($response); 161 + } 122 162 }