Laravel AT Protocol Client (alpha & unstable)
at dev 6.3 kB view raw
1<?php 2 3namespace SocialDept\AtpClient\Builders; 4 5use Closure; 6use DateTimeInterface; 7use SocialDept\AtpClient\Builders\Concerns\BuildsRichText; 8use SocialDept\AtpClient\Builders\Embeds\ImagesBuilder; 9use SocialDept\AtpClient\Client\Records\PostRecordClient; 10use SocialDept\AtpClient\Contracts\Recordable; 11use SocialDept\AtpClient\Data\StrongRef; 12use SocialDept\AtpClient\Enums\Nsid\BskyFeed; 13 14class PostBuilder implements Recordable 15{ 16 use BuildsRichText; 17 18 protected ?array $embed = null; 19 20 protected ?array $reply = null; 21 22 protected ?array $langs = null; 23 24 protected ?DateTimeInterface $createdAt = null; 25 26 protected ?PostRecordClient $client = null; 27 28 /** 29 * Create a new post builder instance 30 */ 31 public static function make(): self 32 { 33 return new self; 34 } 35 36 /** 37 * Add images embed 38 * 39 * @param Closure|array $images Closure receiving ImagesBuilder, or array of image data 40 */ 41 public function images(Closure|array $images): self 42 { 43 if ($images instanceof Closure) { 44 $builder = ImagesBuilder::make(); 45 $images($builder); 46 $this->embed = $builder->toArray(); 47 } else { 48 $this->embed = [ 49 '$type' => 'app.bsky.embed.images', 50 'images' => array_map(fn ($img) => $this->normalizeImageData($img), $images), 51 ]; 52 } 53 54 return $this; 55 } 56 57 /** 58 * Add external link embed (link card) 59 * 60 * @param string $uri URL of the external content 61 * @param string $title Title of the link card 62 * @param string $description Description text 63 * @param mixed|null $thumb Optional thumbnail blob 64 */ 65 public function external(string $uri, string $title, string $description, mixed $thumb = null): self 66 { 67 $external = [ 68 'uri' => $uri, 69 'title' => $title, 70 'description' => $description, 71 ]; 72 73 if ($thumb !== null) { 74 $external['thumb'] = $this->normalizeBlob($thumb); 75 } 76 77 $this->embed = [ 78 '$type' => 'app.bsky.embed.external', 79 'external' => $external, 80 ]; 81 82 return $this; 83 } 84 85 /** 86 * Add video embed 87 * 88 * @param mixed $blob Video blob reference 89 * @param string|null $alt Alt text for the video 90 * @param array|null $captions Optional captions array 91 */ 92 public function video(mixed $blob, ?string $alt = null, ?array $captions = null): self 93 { 94 $video = [ 95 '$type' => 'app.bsky.embed.video', 96 'video' => $this->normalizeBlob($blob), 97 ]; 98 99 if ($alt !== null) { 100 $video['alt'] = $alt; 101 } 102 103 if ($captions !== null) { 104 $video['captions'] = $captions; 105 } 106 107 $this->embed = $video; 108 109 return $this; 110 } 111 112 /** 113 * Add quote embed (embed another post) 114 */ 115 public function quote(StrongRef $post): self 116 { 117 $this->embed = [ 118 '$type' => 'app.bsky.embed.record', 119 'record' => $post->toArray(), 120 ]; 121 122 return $this; 123 } 124 125 /** 126 * Set as a reply to another post 127 * 128 * @param StrongRef $parent The post being replied to 129 * @param StrongRef|null $root The root post of the thread (defaults to parent if not provided) 130 */ 131 public function replyTo(StrongRef $parent, ?StrongRef $root = null): self 132 { 133 $this->reply = [ 134 'parent' => $parent->toArray(), 135 'root' => ($root ?? $parent)->toArray(), 136 ]; 137 138 return $this; 139 } 140 141 /** 142 * Set the post languages 143 * 144 * @param array $langs Array of BCP-47 language codes 145 */ 146 public function langs(array $langs): self 147 { 148 $this->langs = $langs; 149 150 return $this; 151 } 152 153 /** 154 * Set the creation timestamp 155 */ 156 public function createdAt(DateTimeInterface $date): self 157 { 158 $this->createdAt = $date; 159 160 return $this; 161 } 162 163 /** 164 * Bind to a PostRecordClient for creating the post 165 */ 166 public function for(PostRecordClient $client): self 167 { 168 $this->client = $client; 169 170 return $this; 171 } 172 173 /** 174 * Create the post (requires client binding via for() or build()) 175 * 176 * @throws \RuntimeException If no client is bound 177 */ 178 public function create(): StrongRef 179 { 180 if ($this->client === null) { 181 throw new \RuntimeException( 182 'No client bound. Use ->for($client) or create via $client->bsky->post->build()' 183 ); 184 } 185 186 return $this->client->create($this); 187 } 188 189 /** 190 * Convert to array for XRPC (implements Recordable) 191 */ 192 public function toArray(): array 193 { 194 $record = $this->getTextAndFacets(); 195 196 if ($this->embed !== null) { 197 $record['embed'] = $this->embed; 198 } 199 200 if ($this->reply !== null) { 201 $record['reply'] = $this->reply; 202 } 203 204 if ($this->langs !== null) { 205 $record['langs'] = $this->langs; 206 } 207 208 $record['createdAt'] = ($this->createdAt ?? now())->format('c'); 209 $record['$type'] = $this->getType(); 210 211 return $record; 212 } 213 214 /** 215 * Get the record type (implements Recordable) 216 */ 217 public function getType(): string 218 { 219 return BskyFeed::Post->value; 220 } 221 222 /** 223 * Normalize image data from array format 224 */ 225 protected function normalizeImageData(array $data): array 226 { 227 $image = [ 228 'image' => $this->normalizeBlob($data['blob'] ?? $data['image']), 229 'alt' => $data['alt'] ?? '', 230 ]; 231 232 if (isset($data['aspectRatio'])) { 233 $ratio = $data['aspectRatio']; 234 $image['aspectRatio'] = is_array($ratio) && isset($ratio['width']) 235 ? $ratio 236 : ['width' => $ratio[0], 'height' => $ratio[1]]; 237 } 238 239 return $image; 240 } 241 242 /** 243 * Normalize blob to array format 244 */ 245 protected function normalizeBlob(mixed $blob): array 246 { 247 if (is_array($blob)) { 248 return $blob; 249 } 250 251 if (method_exists($blob, 'toArray')) { 252 return $blob->toArray(); 253 } 254 255 return (array) $blob; 256 } 257}