Laravel AT Protocol Client (alpha & unstable)
3
fork

Configure Feed

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

Add record clients for posts, likes, follows, and profiles

+512 -4
+32 -4
src/Client/BskyClient.php
··· 3 3 namespace SocialDept\AtpClient\Client; 4 4 5 5 use SocialDept\AtpClient\AtpClient; 6 + use SocialDept\AtpClient\Client\Records\FollowRecordClient; 7 + use SocialDept\AtpClient\Client\Records\LikeRecordClient; 8 + use SocialDept\AtpClient\Client\Records\PostRecordClient; 9 + use SocialDept\AtpClient\Client\Records\ProfileRecordClient; 6 10 use SocialDept\AtpClient\Client\Requests\Bsky; 7 11 8 12 class BskyClient ··· 15 19 /** 16 20 * Feed operations (app.bsky.feed.*) 17 21 */ 18 - public Bsky\Feed $feed; 22 + public Bsky\FeedRequestClient $feed; 19 23 20 24 /** 21 25 * Actor operations (app.bsky.actor.*) 22 26 */ 23 - public Bsky\Actor $actor; 27 + public Bsky\ActorRequestClient $actor; 28 + 29 + /** 30 + * Post record client 31 + */ 32 + public PostRecordClient $post; 33 + 34 + /** 35 + * Profile record client 36 + */ 37 + public ProfileRecordClient $profile; 38 + 39 + /** 40 + * Like record client 41 + */ 42 + public LikeRecordClient $like; 43 + 44 + /** 45 + * Follow record client 46 + */ 47 + public FollowRecordClient $follow; 24 48 25 49 public function __construct(AtpClient $parent) 26 50 { 27 51 $this->atp = $parent; 28 - $this->feed = new Bsky\Feed($this); 29 - $this->actor = new Bsky\Actor($this); 52 + $this->feed = new Bsky\FeedRequestClient($this); 53 + $this->actor = new Bsky\ActorRequestClient($this); 54 + $this->post = new PostRecordClient($this); 55 + $this->profile = new ProfileRecordClient($this); 56 + $this->like = new LikeRecordClient($this); 57 + $this->follow = new FollowRecordClient($this); 30 58 } 31 59 }
+68
src/Client/Records/FollowRecordClient.php
··· 1 + <?php 2 + 3 + namespace SocialDept\AtpClient\Client\Records; 4 + 5 + use DateTimeInterface; 6 + use SocialDept\AtpClient\Client\Requests\Request; 7 + use SocialDept\AtpClient\Data\StrongRef; 8 + 9 + class FollowRecordClient extends Request 10 + { 11 + /** 12 + * Follow a user 13 + */ 14 + public function create( 15 + string $subject, 16 + ?DateTimeInterface $createdAt = null 17 + ): StrongRef { 18 + $record = [ 19 + '$type' => 'app.bsky.graph.follow', 20 + 'subject' => $subject, // DID 21 + 'createdAt' => ($createdAt ?? now())->format('c'), 22 + ]; 23 + 24 + $response = $this->atp->client->post( 25 + endpoint: 'com.atproto.repo.createRecord', 26 + body: [ 27 + 'repo' => $this->atp->client->sessions->session($this->atp->client->identifier)->did(), 28 + 'collection' => 'app.bsky.graph.follow', 29 + 'record' => $record, 30 + ] 31 + ); 32 + 33 + return StrongRef::fromResponse($response->json()); 34 + } 35 + 36 + /** 37 + * Unfollow a user (delete follow record) 38 + */ 39 + public function delete(string $rkey): void 40 + { 41 + $this->atp->client->post( 42 + endpoint: 'com.atproto.repo.deleteRecord', 43 + body: [ 44 + 'repo' => $this->atp->client->sessions->session($this->atp->client->identifier)->did(), 45 + 'collection' => 'app.bsky.graph.follow', 46 + 'rkey' => $rkey, 47 + ] 48 + ); 49 + } 50 + 51 + /** 52 + * Get a follow record 53 + */ 54 + public function get(string $rkey, ?string $cid = null): array 55 + { 56 + $response = $this->atp->client->get( 57 + endpoint: 'com.atproto.repo.getRecord', 58 + params: array_filter([ 59 + 'repo' => $this->atp->client->sessions->session($this->atp->client->identifier)->did(), 60 + 'collection' => 'app.bsky.graph.follow', 61 + 'rkey' => $rkey, 62 + 'cid' => $cid, 63 + ]) 64 + ); 65 + 66 + return $response->json('value'); 67 + } 68 + }
+68
src/Client/Records/LikeRecordClient.php
··· 1 + <?php 2 + 3 + namespace SocialDept\AtpClient\Client\Records; 4 + 5 + use DateTimeInterface; 6 + use SocialDept\AtpClient\Client\Requests\Request; 7 + use SocialDept\AtpClient\Data\StrongRef; 8 + 9 + class LikeRecordClient extends Request 10 + { 11 + /** 12 + * Like a post 13 + */ 14 + public function create( 15 + StrongRef $subject, 16 + ?DateTimeInterface $createdAt = null 17 + ): StrongRef { 18 + $record = [ 19 + '$type' => 'app.bsky.feed.like', 20 + 'subject' => $subject->toArray(), 21 + 'createdAt' => ($createdAt ?? now())->format('c'), 22 + ]; 23 + 24 + $response = $this->atp->client->post( 25 + endpoint: 'com.atproto.repo.createRecord', 26 + body: [ 27 + 'repo' => $this->atp->client->sessions->session($this->atp->client->identifier)->did(), 28 + 'collection' => 'app.bsky.feed.like', 29 + 'record' => $record, 30 + ] 31 + ); 32 + 33 + return StrongRef::fromResponse($response->json()); 34 + } 35 + 36 + /** 37 + * Unlike a post (delete like record) 38 + */ 39 + public function delete(string $rkey): void 40 + { 41 + $this->atp->client->post( 42 + endpoint: 'com.atproto.repo.deleteRecord', 43 + body: [ 44 + 'repo' => $this->atp->client->sessions->session($this->atp->client->identifier)->did(), 45 + 'collection' => 'app.bsky.feed.like', 46 + 'rkey' => $rkey, 47 + ] 48 + ); 49 + } 50 + 51 + /** 52 + * Get a like record 53 + */ 54 + public function get(string $rkey, ?string $cid = null): array 55 + { 56 + $response = $this->atp->client->get( 57 + endpoint: 'com.atproto.repo.getRecord', 58 + params: array_filter([ 59 + 'repo' => $this->atp->client->sessions->session($this->atp->client->identifier)->did(), 60 + 'collection' => 'app.bsky.feed.like', 61 + 'rkey' => $rkey, 62 + 'cid' => $cid, 63 + ]) 64 + ); 65 + 66 + return $response->json('value'); 67 + } 68 + }
+236
src/Client/Records/PostRecordClient.php
··· 1 + <?php 2 + 3 + namespace SocialDept\AtpClient\Client\Records; 4 + 5 + use DateTimeInterface; 6 + use SocialDept\AtpClient\Client\Requests\Request; 7 + use SocialDept\AtpClient\Contracts\Recordable; 8 + use SocialDept\AtpClient\Data\StrongRef; 9 + use SocialDept\AtpClient\Http\Response; 10 + use SocialDept\AtpClient\RichText\TextBuilder; 11 + 12 + class PostRecordClient extends Request 13 + { 14 + /** 15 + * Create a post 16 + */ 17 + public function create( 18 + string|array|Recordable $content, 19 + ?array $facets = null, 20 + ?array $embed = null, 21 + ?array $reply = null, 22 + ?array $langs = null, 23 + ?DateTimeInterface $createdAt = null 24 + ): StrongRef { 25 + // Handle different input types 26 + if (is_string($content)) { 27 + $record = [ 28 + 'text' => $content, 29 + 'facets' => $facets ?? TextBuilder::parse($content)['facets'], 30 + ]; 31 + } elseif ($content instanceof Recordable) { 32 + $record = $content->toArray(); 33 + } else { 34 + $record = $content; 35 + } 36 + 37 + // Add optional fields 38 + if ($embed) { 39 + $record['embed'] = $embed; 40 + } 41 + if ($reply) { 42 + $record['reply'] = $reply; 43 + } 44 + if ($langs) { 45 + $record['langs'] = $langs; 46 + } 47 + if (! isset($record['createdAt'])) { 48 + $record['createdAt'] = ($createdAt ?? now())->format('c'); 49 + } 50 + 51 + // Ensure $type is set 52 + if (! isset($record['$type'])) { 53 + $record['$type'] = 'app.bsky.feed.post'; 54 + } 55 + 56 + // Create record via XRPC 57 + $response = $this->atp->client->post( 58 + endpoint: 'com.atproto.repo.createRecord', 59 + body: [ 60 + 'repo' => $this->atp->client->sessions->session($this->atp->client->identifier)->did(), 61 + 'collection' => 'app.bsky.feed.post', 62 + 'record' => $record, 63 + ] 64 + ); 65 + 66 + return StrongRef::fromResponse($response->json()); 67 + } 68 + 69 + /** 70 + * Update a post 71 + */ 72 + public function update(string $rkey, array $record): StrongRef 73 + { 74 + // Ensure $type is set 75 + if (! isset($record['$type'])) { 76 + $record['$type'] = 'app.bsky.feed.post'; 77 + } 78 + 79 + $response = $this->atp->client->post( 80 + endpoint: 'com.atproto.repo.putRecord', 81 + body: [ 82 + 'repo' => $this->atp->client->sessions->session($this->atp->client->identifier)->did(), 83 + 'collection' => 'app.bsky.feed.post', 84 + 'rkey' => $rkey, 85 + 'record' => $record, 86 + ] 87 + ); 88 + 89 + return StrongRef::fromResponse($response->json()); 90 + } 91 + 92 + /** 93 + * Delete a post 94 + */ 95 + public function delete(string $rkey): void 96 + { 97 + $this->atp->client->post( 98 + endpoint: 'com.atproto.repo.deleteRecord', 99 + body: [ 100 + 'repo' => $this->atp->client->sessions->session($this->atp->client->identifier)->did(), 101 + 'collection' => 'app.bsky.feed.post', 102 + 'rkey' => $rkey, 103 + ] 104 + ); 105 + } 106 + 107 + /** 108 + * Get a post 109 + */ 110 + public function get(string $rkey, ?string $cid = null): array 111 + { 112 + $response = $this->atp->client->get( 113 + endpoint: 'com.atproto.repo.getRecord', 114 + params: array_filter([ 115 + 'repo' => $this->atp->client->sessions->session($this->atp->client->identifier)->did(), 116 + 'collection' => 'app.bsky.feed.post', 117 + 'rkey' => $rkey, 118 + 'cid' => $cid, 119 + ]) 120 + ); 121 + 122 + return $response->json('value'); 123 + } 124 + 125 + /** 126 + * Create a reply to another post 127 + */ 128 + public function reply( 129 + StrongRef $parent, 130 + StrongRef $root, 131 + string|array|Recordable $content, 132 + ?array $facets = null, 133 + ?array $embed = null, 134 + ?array $langs = null, 135 + ?DateTimeInterface $createdAt = null 136 + ): StrongRef { 137 + $reply = [ 138 + 'parent' => $parent->toArray(), 139 + 'root' => $root->toArray(), 140 + ]; 141 + 142 + return $this->create( 143 + content: $content, 144 + facets: $facets, 145 + embed: $embed, 146 + reply: $reply, 147 + langs: $langs, 148 + createdAt: $createdAt 149 + ); 150 + } 151 + 152 + /** 153 + * Create a quote post (post with embedded post) 154 + */ 155 + public function quote( 156 + StrongRef $quotedPost, 157 + string|array|Recordable $content, 158 + ?array $facets = null, 159 + ?array $langs = null, 160 + ?DateTimeInterface $createdAt = null 161 + ): StrongRef { 162 + $embed = [ 163 + '$type' => 'app.bsky.embed.record', 164 + 'record' => $quotedPost->toArray(), 165 + ]; 166 + 167 + return $this->create( 168 + content: $content, 169 + facets: $facets, 170 + embed: $embed, 171 + langs: $langs, 172 + createdAt: $createdAt 173 + ); 174 + } 175 + 176 + /** 177 + * Create a post with images 178 + */ 179 + public function withImages( 180 + string|array|Recordable $content, 181 + array $images, 182 + ?array $facets = null, 183 + ?array $langs = null, 184 + ?DateTimeInterface $createdAt = null 185 + ): StrongRef { 186 + $embed = [ 187 + '$type' => 'app.bsky.embed.images', 188 + 'images' => $images, 189 + ]; 190 + 191 + return $this->create( 192 + content: $content, 193 + facets: $facets, 194 + embed: $embed, 195 + langs: $langs, 196 + createdAt: $createdAt 197 + ); 198 + } 199 + 200 + /** 201 + * Create a post with external link embed 202 + */ 203 + public function withLink( 204 + string|array|Recordable $content, 205 + string $uri, 206 + string $title, 207 + string $description, 208 + ?string $thumbBlob = null, 209 + ?array $facets = null, 210 + ?array $langs = null, 211 + ?DateTimeInterface $createdAt = null 212 + ): StrongRef { 213 + $external = [ 214 + 'uri' => $uri, 215 + 'title' => $title, 216 + 'description' => $description, 217 + ]; 218 + 219 + if ($thumbBlob) { 220 + $external['thumb'] = $thumbBlob; 221 + } 222 + 223 + $embed = [ 224 + '$type' => 'app.bsky.embed.external', 225 + 'external' => $external, 226 + ]; 227 + 228 + return $this->create( 229 + content: $content, 230 + facets: $facets, 231 + embed: $embed, 232 + langs: $langs, 233 + createdAt: $createdAt 234 + ); 235 + } 236 + }
+108
src/Client/Records/ProfileRecordClient.php
··· 1 + <?php 2 + 3 + namespace SocialDept\AtpClient\Client\Records; 4 + 5 + use SocialDept\AtpClient\Client\Requests\Request; 6 + use SocialDept\AtpClient\Data\StrongRef; 7 + 8 + class ProfileRecordClient extends Request 9 + { 10 + /** 11 + * Update profile 12 + */ 13 + public function update(array $profile): StrongRef 14 + { 15 + // Ensure $type is set 16 + if (! isset($profile['$type'])) { 17 + $profile['$type'] = 'app.bsky.actor.profile'; 18 + } 19 + 20 + $response = $this->atp->client->post( 21 + endpoint: 'com.atproto.repo.putRecord', 22 + body: [ 23 + 'repo' => $this->atp->client->sessions->session($this->atp->client->identifier)->did(), 24 + 'collection' => 'app.bsky.actor.profile', 25 + 'rkey' => 'self', // Profile records always use 'self' as rkey 26 + 'record' => $profile, 27 + ] 28 + ); 29 + 30 + return StrongRef::fromResponse($response->json()); 31 + } 32 + 33 + /** 34 + * Get current profile 35 + */ 36 + public function get(): array 37 + { 38 + $response = $this->atp->client->get( 39 + endpoint: 'com.atproto.repo.getRecord', 40 + params: [ 41 + 'repo' => $this->atp->client->sessions->session($this->atp->client->identifier)->did(), 42 + 'collection' => 'app.bsky.actor.profile', 43 + 'rkey' => 'self', 44 + ] 45 + ); 46 + 47 + return $response->json('value'); 48 + } 49 + 50 + /** 51 + * Update display name 52 + */ 53 + public function updateDisplayName(string $displayName): StrongRef 54 + { 55 + $profile = $this->getOrCreateProfile(); 56 + $profile['displayName'] = $displayName; 57 + 58 + return $this->update($profile); 59 + } 60 + 61 + /** 62 + * Update description/bio 63 + */ 64 + public function updateDescription(string $description): StrongRef 65 + { 66 + $profile = $this->getOrCreateProfile(); 67 + $profile['description'] = $description; 68 + 69 + return $this->update($profile); 70 + } 71 + 72 + /** 73 + * Update avatar 74 + */ 75 + public function updateAvatar(array $avatarBlob): StrongRef 76 + { 77 + $profile = $this->getOrCreateProfile(); 78 + $profile['avatar'] = $avatarBlob; 79 + 80 + return $this->update($profile); 81 + } 82 + 83 + /** 84 + * Update banner 85 + */ 86 + public function updateBanner(array $bannerBlob): StrongRef 87 + { 88 + $profile = $this->getOrCreateProfile(); 89 + $profile['banner'] = $bannerBlob; 90 + 91 + return $this->update($profile); 92 + } 93 + 94 + /** 95 + * Get profile or create empty one if doesn't exist 96 + */ 97 + protected function getOrCreateProfile(): array 98 + { 99 + try { 100 + return $this->get(); 101 + } catch (\Exception $e) { 102 + // Profile doesn't exist, return empty structure 103 + return [ 104 + '$type' => 'app.bsky.actor.profile', 105 + ]; 106 + } 107 + } 108 + }