friendship ended with social-app. php is my new best friend
1<?php 2 3error_reporting(E_ALL); 4ini_set('display_errors', 'On'); 5ini_set('log_errors_max_len', '0'); 6 7require_once('config.php'); 8require_once('vendor/autoload.php'); 9//if (DB_TYPE === 'sqlite') require_once('db.php'); 10if (DB_TYPE === 'mysql') require_once('maria-db.php'); 11 12use Matrix\Async; 13use React\Http\Browser; 14use React\Promise\Promise; 15use React\Async\Parallel; 16use React\Dns\Config\Config; 17use GuzzleHttp\Client; 18use GuzzleHttp\Query; 19use GuzzleHttp\Middleware; 20use GuzzleHttp\Handler\CurlHandler; 21use GuzzleHttp\HandlerStack; 22use GuzzleHttp\Exception\ConnectException; 23use GuzzleHttp\Psr7\Request; 24use GuzzleHttp\TransferStats; 25use Psr\Http\Message\RequestInterface; 26use Psr\Http\Message\ResponseInterface; 27use Fusonic\OpenGraph\Consumer; 28 29class BskyToucher { 30 private $bskyApiBase = 'https://public.api.bsky.app/xrpc/'; 31 private $slingshotBase = \SLINGSHOT_INSTANCE.'/xrpc/'; 32 private $constellationBase = \CONSTELLATION_INSTANCE; 33 private $plcDirectoryBase = \PLC_DIRECTORY; 34 private $publicApiBase = \PUBLIC_API.'/xrpc/'; 35 36 /* GENERAL QUERIES */ 37 38 function makeRequest(string $method, string $uri, array $query = [], array $headers = [], string $body = ''): Promise|Exception { 39 if ($method === 'GET') { 40 $headers = [ 41 "Content-Type" => "application/x-www-form-urlencoded", 42 "User-Agent" => \USER_AGENT_STR 43 ]; 44 } 45 $connector = new React\Socket\Connector([ 46 'dns' => '1.1.1.1' 47 ]); 48 $browser = new React\Http\Browser($connector); 49 $built_query = ""; 50 if (!empty($query)) { 51 $built_query = "?".http_build_query($query); 52 } 53 $ret = retry(fn () => $browser->request($method, $uri.$built_query, $headers, $body), 5) 54 ->then(function (Psr\Http\Message\ResponseInterface $response) { 55 return $response; 56 }, function(Exception $e) { 57 return $e; 58 }); 59 return $ret; 60 } 61 62 function getSlingshotData(string $repo, string $collection, ?string $rkey = 'self', ?array $params = []): object|bool { 63 $query = [ 64 'collection' => $collection, 65 'repo' => $repo 66 ]; 67 if ($rkey) { 68 $query['rkey'] = $rkey; 69 } 70 if ($params) { 71 $query = array_merge($query, $params); 72 } 73 $ret = await($this->makeRequest("GET", $this->slingshotBase."com.atproto.repo.getRecord", $query)); 74 if ($ret && method_exists($ret, 'getBody')) return json_decode($ret->getBody()); 75 return false; 76 } 77 78 function resolveHandle(string $handle): string|bool { 79 $cache = \requestUserCache($handle, 'handle'); 80 if ($cache) return $cache->did; 81 $ret = await($this->makeRequest("GET", $this->slingshotBase."com.atproto.identity.resolveHandle", ['handle' => $handle])); 82 if ($ret && method_exists($ret, 'getBody') && $ret->getReasonPhrase() === "OK") { 83 return json_decode($ret->getBody())->did; 84 } 85 } 86 87 function getSlingshotIdentityMiniDoc(string $did): object|bool { 88 $cache = \requestMinidocCache($did); 89 if ($cache) return $cache; 90 $ret = await(async(fn () => $this->makeRequest("GET", $this->slingshotBase."com.bad-example.identity.resolveMiniDoc", ['identifier' => $did]))); 91 92 if ($ret && method_exists($ret, 'getBody') && $ret->getReasonPhrase() === 'OK') { 93 $body = json_decode($ret->getBody()); 94 \updateMinidocCache($body->handle, $body->did, $body->pds, $body->signing_key); 95 return $body; 96 } 97 98 return false; 99 } 100 101 function getConstellationLinkData(string $target, string $collection, string $path, int $limit = 50, int $offset = 0, string $endpoint = '/xrpc/blue.microcosm.links.getBacklinks'): object|bool { 102 $query = [ 103 'offset' => $offset 104 ]; 105 if ($endpoint === '/xrpc/blue.microcosm.links.getBacklinks') { 106 $query['subject'] = $target; 107 $query['source'] = $collection.":".$path; 108 } else { 109 $query['target'] = $target; 110 $query['collection'] = $collection; 111 $query['path'] = ".".$path; 112 } 113 $ret = await($this->makeRequest("GET", $this->constellationBase.$endpoint, $query)); 114 if ($ret && method_exists($ret, 'getBody') && $ret->getReasonPhrase() === 'OK') { 115 return json_decode($ret->getBody()); 116 } 117 118 return false; 119 } 120 121 function getPublicApiData(string $endpoint, array $query): object|bool { 122 $ret = await($this->makeRequest("GET", $this->publicApiBase.$endpoint, $query)); 123 if ($ret && method_exists($ret, 'getBody') && $ret->getReasonPhrase() === 'OK') { 124 return json_decode($ret->getBody()); 125 } 126 127 return false; 128 } 129 130 function getPdsData(string $pds, string $endpoint, array $query, ?string $token = null): object|bool { 131 $headers = []; 132 if ($token) { 133 $headers['Authorization'] = 'Bearer: '.$token; 134 } 135 $ret = await($this->makeRequest("GET", $pds."/xrpc/".$endpoint, $query, $headers)); 136 137 if ($ret && method_exists($ret, 'getBody') && $ret->getReasonPhrase() === 'OK') { 138 return json_decode($ret->getBody()); 139 } 140 141 return false; 142 } 143 144 function getPlcInfoFromRecord(string $did):object|bool { 145 $cache = \requestPlcCache($did); 146 if ($cache) { 147 $doc = json_decode($cache->plcdoc); 148 return (object) [ 149 'pds' => $doc->service[0]->serviceEndpoint, 150 'did' => $did, 151 'handle' => str_replace("at://", "", $doc->alsoKnownAs[0]) 152 ]; 153 } 154 155 $plcdirBase = \PLC_DIRECTORY."/"; 156 $ret = await($this->makeRequest("GET", $plcdirBase.$did)); 157 if ($ret && method_exists($ret, 'getBody') && $ret->getReasonPhrase() === 'OK') { 158 $body = $ret->getBody(); 159 \updatePlcCache($did, (string) $body); 160 $resp = json_decode((string) $body); 161 162 if ($resp && property_exists($resp, 'service') && is_array($resp->service) && count($resp->service) > 0 && property_exists($resp->service[0], 'serviceEndpoint')) { 163 $handle = ""; 164 if (property_exists($resp, 'alsoKnownAs') && is_array($resp->alsoKnownAs) && count($resp->alsoKnownAs) > 0) { 165 $handle = $resp->alsoKnownAs[0]; 166 } 167 return (object) [ 168 'pds' => $resp->service[0]->serviceEndpoint, 169 'did' => $did, 170 'handle' => str_replace("at://", "", $handle) 171 ]; 172 } 173 } 174 175 return false; 176 } 177 178 function getFeedSkeleton(string $didweb, string $atUri, ?string $offset = null): object|bool { 179 $query = [ 180 'feed' => $atUri 181 ]; 182 if ($offset) { 183 $query['offset'] = $offset; 184 } 185 preg_match('/^did:web:([a-z0-9\.\-]+)$/', $didweb, $didWebBase); 186 if (!$didWebBase) return false; 187 $base = "https://".$didWebBase[1]; 188 $ret = await($this->makeRequest('GET', $base.'/xrpc/app.bsky.feed.getFeedSkeleton', ['feed' => $atUri])); 189 if (!$ret || $ret->getReasonPhrase() !== 'OK') return false; 190 $body = json_decode((string) $ret->getBody()); 191 if (!$body->feed || count($body->feed) === 0) return []; 192 return $body; 193 } 194 195 /* AUTH */ 196 197 /* HELPERS */ 198 199 function getMediaUrl(string $pds, string $did, string $cid): string { 200 return $pds."/xrpc/com.atproto.sync.getBlob?did=".$did."&cid=".$cid; 201 } 202 203 function splitAtUri(string $atUri): object|bool { 204 preg_match('/at:\/\/(did:plc:[a-z0-9]+)\/([a-zA-Z\.]+)\/([a-z0-9]+)/', $atUri, $uriComponents); 205 if (!$uriComponents) return false; 206 return (object) [ 207 'did' => $uriComponents[1], 208 'collection' => $uriComponents[2], 209 'rkey' => $uriComponents[3] 210 ]; 211 } 212 213 function applyFacets(string $text, array $facets): string { 214 $ret = $text; 215 $split_str = str_split($text); 216 $additions = 0; 217 foreach ($facets as $facet) { 218 if ($facet->type === "tag") { 219 $hashtagLink = '/h/'.$facet->tag; 220 $splicedpart = array_splice($split_str, $facet->start + $additions, 0, '<a href="'.$hashtagLink.'">'); 221 $additions++; 222 array_splice($split_str, $facet->end + $additions, 0, '</a>'); 223 $additions++; 224 } else if ($facet->type === "mention") { 225 $profileLink = '/u/'.$facet->handle; 226 $replaceString = '<a href="'.$profileLink.'">@'.$facet->handle.'</a>'; 227 array_splice($split_str, $facet->start + $additions, $facet->end - $facet->start, $replaceString); 228 $additions += 1 - ($facet->end - $facet->start); 229 } else if ($facet->type === "link") { 230 $pat = '/('.preg_quote($facet->link, '/').')/'; 231 array_splice($split_str, $facet->start + $additions, 0, '<a href="'.$facet->link.'" rel="external" target="_blank">'); 232 $additions++; 233 array_splice($split_str, $facet->end + $additions, 0, '</a>'); 234 $additions++; 235 } 236 // there might be more idk. like for bold or whatever 237 } 238 return join("", $split_str); 239 } 240 241 /* USER INFO */ 242 243 function getUserInfo(string $identifier, string $type = 'handle'): object|bool { 244 $cache = \requestUserCache($identifier, $type); 245 if ($cache) return $cache; 246 $id = $identifier; 247 $did = null; 248 $id = null; 249 $pds = null; 250 if ($type === 'did') { 251 $did = $identifier; 252 $userData = await(async(fn () => $this->getSlingshotIdentityMiniDoc($did))); 253 if (!$userData || !property_exists($userData, 'did')) return false; 254 $did = $userData->did; 255 $pds = $userData->pds; 256 } else { 257 $did = await(async(fn() => $this->resolveHandle($identifier))); 258 $userData = await(async(fn () => $this->getSlingshotIdentityMiniDoc($did))); 259 if (!$userData) return false; 260 $pds = $userData->pds; 261 } 262 $userInfo = await(async(fn () => $this->getSlingshotData($did, 'app.bsky.actor.profile', 'self'))); 263 if ($userInfo) { 264 $avatar = property_exists($userInfo->value, 'avatar') ? $this->getMediaUrl($pds, $did, $userInfo->value->avatar->ref->{'$link'}) : null; 265 $banner = property_exists($userInfo->value, 'banner') ? $this->getMediaUrl($pds, $did, $userInfo->value->banner->ref->{'$link'}) : null; 266 $pinned = property_exists($userInfo->value, 'pinnedPost') ? $userInfo->value->pinnedPost->uri : null; 267 $description = property_exists($userInfo->value, 'description') ? $userInfo->value->description : ""; 268 \updateUserCache($userData->handle, $did, $userInfo->value->displayName, $pds, $avatar, $banner, $description, $pinned); 269 return (object) [ 270 'handle' => $userData->handle, 271 'displayName' => $userInfo->value->displayName, 272 'did' => $did, 273 'pds' => $pds, 274 'description' => $description, 275 'avatar' => $avatar, 276 'banner' => $banner, 277 'pinnedPost' => $pinned 278 ]; 279 } 280 return false; 281 } 282 283 /* GETTING USER INFO */ 284 285 function getUserDidAndProvider(string $user):object|bool { 286 preg_match('/^did:plc:[a-z0-9]+$/', $user, $didpat); 287 // if they gave us a DID here just return it and grab the PDS 288 if ($didpat) { 289 return $this->getPlcInfoFromRecord($user); 290 } 291 292 // check DNS first 293 $dnsRecords = dns_get_record('_atproto.'.$user, DNS_TXT); 294 if ($dnsRecords) { 295 $dnsRecords = array_filter($dnsRecords, function($v) { 296 preg_match('/^did=(did:plc:[a-z0-9]+)$/', $v['txt'], $dnsmatch); 297 if ($dnsmatch) { 298 return true; 299 } 300 return false; 301 }); 302 preg_match('/^did=(did:plc:[a-z0-9]+)$/', $dnsRecords[0]['txt'], $dnsmatch); 303 $did = $dnsmatch[1]; 304 return $this->getPlcInfoFromRecord($did); 305 } 306 // try http verification via .well-known 307 $httpRecord = file_get_contents('https://'.$user.'/.well-known/atproto-did'); 308 if ($httpRecord) { 309 $did = $httpRecord; 310 return $this->getPlcInfoFromRecord($did); 311 } 312 return false; 313 } 314 315 /* POSTS */ 316 317 function getPost(string $identifier, string $rkey, bool $slingshot = false, bool $getReplies = true):object|bool { 318 $cache = \requestPostCache($rkey); 319 if ($cache && !empty($cache)) { 320 return $this->sanitizeCachedPost($cache, $getReplies); 321 } 322 323 $ret = $this->getSlingshotData($identifier, "app.bsky.feed.post", $rkey); 324 if (!$ret) return false; 325 $post = $this->sanitizePost($ret, true, false); 326 return $post; 327 } 328 329 function getLikes(string $post):object { 330 $ret = $this->getConstellationLinkData($post, "app.bsky.feed.like", "subject.uri"); 331 if ($ret && property_exists($ret, 'records') && $ret->total !== 0) { 332 $likeUsers = await(batch( 333 $ret->records, 334 function ($batch) { 335 return async(function () use ($batch) { 336 return array_map(fn ($user) => $this->getUserInfo($user->did, 'did'), $batch); 337 }); 338 }, 50, 2)); 339 if ($ret && property_exists($ret, 'total')) return (object) ['total' => $ret->total, 'records' => $likeUsers, 'cursor' => $ret->cursor]; 340 } 341 return (object) [ 342 'total' => 0, 343 'records' => [], 344 'cursor' => 0 345 ]; 346 } 347 348 function getReposts(string $post):object { 349 $ret = $this->getConstellationLinkData($post, "app.bsky.feed.repost", "subject.uri"); 350 if ($ret && property_exists($ret, 'records') && $ret->total !== 0) { 351 $repUsers = await(batch( 352 $ret->records, 353 function ($batch) { 354 return async(function() use ($batch) { 355 return array_map(fn ($user) => $this->getUserInfo($user->did, 'did'), $batch); 356 }); 357 }, 50, 2)); 358 if ($ret && property_exists($ret, 'total')) return (object) ['total' => $ret->total, 'records' => $repUsers, 'cursor' => $ret->cursor]; 359 } 360 return (object) [ 361 'total' => 0, 362 'records' => [], 363 'cursor' => 0 364 ]; 365 } 366 367 function getQuotes(string $post):object { 368 $ret = $this->getConstellationLinkData($post, "app.bsky.feed.post", "embed.record.record.uri"); 369 if ($ret && property_exists($ret, 'records') && $ret->total !== 0) { 370 $quoteRecords = await(batch( 371 $ret->records, 372 function($batch) { 373 return async(function () use ($batch) { 374 return array_map(function ($rec) { 375 return $this->getPost($rec->did, $rec->rkey, true, false); 376 }, $batch); 377 }); 378 }, 50, 2)); 379 if ($ret && property_exists($ret, 'total')) return (object) ['total' => $ret->total, 'records' => $quoteRecords, 'cursor' => $ret->cursor]; 380 } 381 return (object) [ 382 'total' => 0, 383 'records' => [], 384 'cursor' => 0 385 ]; 386 } 387 388 function getReplies(string $post, int $recurseLevel = 0):object { 389 $ret = $this->getConstellationLinkData($post, "app.bsky.feed.post", "reply.parent.uri"); 390 if ($ret && property_exists($ret, 'records') && is_array($ret->records) && $ret->total !== 0) { 391 $replyRecords = await(batch( 392 $ret->records, function ($batch) { 393 return async(function () use ($batch) { 394 return array_map(fn($rec) => $this->getPost($rec->did, $rec->rkey, true, false), $batch); 395 }); 396 }, 50, 2)); 397 if ($ret && property_exists($ret, 'total')) return (object) ['total' => $ret->total, 'records' => $replyRecords, 'cursor' => $ret->cursor]; 398 } 399 return (object) [ 400 'total' => 0, 401 'records' => [], 402 'cursor' => 0 403 ]; 404 } 405 406 function getUserPosts(string $did, ?string $cursor = null, bool $auth = false, bool $newer = false): object|bool { 407 $userData = $this->getUserInfo($did, 'did'); 408 if (!$userData) return false; 409 $postData = await(async(function () use ($did, $cursor, $userData) { 410 return $this->getPdsData($userData->pds, 'com.atproto.repo.listRecords', [ 411 'repo' => $did, 412 'collection' => 'app.bsky.feed.post', 413 'limit' => 20, 414 'cursor' => $cursor 415 ]); 416 })); 417 if (!$postData) return false; 418 return $postData; 419 } 420 421 function getSearchResults(string $query):array { 422 $ret = $this->getPublicApiData('app.bsky.feed.searchPosts', [ 423 'q' => $query, 424 'tag' => $query 425 ]); 426 return $this->sanitizePosts($ret->posts); 427 } 428 429 function getFeedInfo(string $atUri): object|bool { 430 $cache = \requestFeedCache($atUri); 431 if ($cache) { 432 $uriComponents = $this->splitAtUri($cache->atUri); 433 $authorInfo = $this->getUserInfo($cache->creatorDid, 'did'); 434 if (!$authorInfo) return false; 435 return (object) [ 436 'title' => $cache->title, 437 'url' => '/f/'.$cache->creatorDid.'/'.$uriComponents->rkey, 438 'description' => $cache->description, 439 'avatar' => $cache->avatar, 440 'creatorDisplay' => $authorInfo->displayName, 441 'creatorDid' => $uriComponents->did, 442 'creatorPds' => $authorInfo->pds, 443 'creatorHandle' => $authorInfo->handle 444 ]; 445 } 446 447 $uriComponents = $this->splitAtUri($atUri); 448 if (!$uriComponents) return false; 449 $feedData = $this->getSlingshotData($uriComponents->did, $uriComponents->collection, $uriComponents->rkey); 450 $authorInfo = $this->getUserInfo($uriComponents->did, 'did'); 451 if (!$authorInfo) return false; 452 \updateFeedCache($atUri, $feedData->value->displayName, $feedData->value->description, $this->getMediaUrl($authorInfo->pds, $uriComponents->did, $feedData->value->avatar->ref->{'$link'}), $uriComponents->did); 453 return (object) [ 454 'title' => $feedData->value->displayName, 455 'url' => '/f/'.$uriComponents->did.'/'.$uriComponents->rkey, 456 'description' => $feedData->value->description, 457 'avatar' => $this->getMediaUrl($authorInfo->pds, $uriComponents->did, $feedData->value->avatar->ref->{'$link'}), 458 'creatorDisplay' => $authorInfo->displayName, 459 'creatorDid' => $uriComponents->did, 460 'creatorPds' => $authorInfo->pds, 461 'creatorHandle' => $authorInfo->handle 462 ]; 463 return $feedData; 464 } 465 466 function getFeed(string $atUri, ?string $cursor = null, ?string $userAuth = null, ?bool $newer = false, int $limit = 15):object|bool { 467 preg_match('/^at:\/\/(did:plc:[a-z0-9\.]+)\/app.bsky.feed.generator\/([a-z0-9]+)$/', $atUri, $uriComponents); 468 $did = $uriComponents[1]; 469 $rkey = $uriComponents[2]; 470 $userInfo = $this->getUserInfo($did, 'did'); 471 472 if (!$userInfo) return false; 473 474 $feedInfo = await(async(function () use ($did, $rkey, $userInfo) { 475 return $this->getPdsData($userInfo->pds, 'com.atproto.repo.getRecord', [ 476 'repo' => $did, 477 'collection' => 'app.bsky.feed.generator', 478 'rkey' => $rkey 479 ]); 480 })); 481 482 if (!$feedInfo) return false; 483 484 if ($userAuth) { 485 // do something. i don't care rn tho 486 487 /*$feedData = $this->getPdsData($pds, 'app.bsky.feed.getFeed', [ 488 'feed' => FRONTPAGE_FEED, 489 'limit' => $limit, 490 'cursor' => $cursor 491 ]);*/ 492 return false; 493 } 494 495 $feedData = $this->getFeedSkeleton($feedInfo->value->did, $atUri, $cursor); 496 497 if ($feedData) { 498 return $feedData; 499 } 500 501 return false; 502 } 503 504 /* SANITIZATION */ 505 506 function sanitizePost(object $post, ?bool $slingshot = false, bool $getReplies = true):object|bool { 507 if ($slingshot) { 508 return $this->sanitizeSlingshotRecordPost($post, false); 509 } 510 return $this->sanitizePublicApiPost($post, false); 511 } 512 513 function sanitizeSlingshotRecordPost(object $post, ?string $cursor = null, $getReplies = true):object|bool { 514 preg_match('/at:\/\/(did:plc:[a-z0-9]+)\/[a-zA-Z\.]+\/([a-z0-9]+)/', $post->uri, $uriComponents); 515 if (!$uriComponents) return false; 516 $did = $uriComponents[1]; 517 $rkey = $uriComponents[2]; 518 $authorInfo = $this->getUserInfo($did, 'did'); 519 $facets = property_exists($post->value, 'facets') ? $this->sanitizeFacets($post->value->facets) : []; 520 $ret = (object) [ 521 'author' => (object) [ 522 'displayName' => $authorInfo->displayName, 523 'handle' => $authorInfo->handle, 524 'avatar' => $authorInfo->avatar, 525 'did' => $did, 526 'profileLink' => '/u/'.$authorInfo->handle, 527 ], 528 'uri' => $post->uri, 529 'pds' => $authorInfo->pds, 530 'postId' => $rkey, 531 'postLink' => '/u/'.$authorInfo->handle.'/'.$rkey, 532 'content' => property_exists($post->value, 'text') ? $this->applyFacets($post->value->text, $facets) : '', 533 'createdAt' => $post->value->createdAt, 534 'embedType' => property_exists($post->value, 'embed') ? $post->value->embed->{'$type'} : null, 535 'embeds' => property_exists($post->value, 'embed') ? $this->sanitizeEmbeds($post->value->embed, $authorInfo) : [], 536 'cursor' => $cursor 537 ]; 538 \updatePostCache($rkey, $did, $ret->content, $ret->embedType ? $ret->embedType : '', json_encode($ret->embeds), $post->value->createdAt); 539 return $ret; 540 } 541 542 function sanitizePublicApiPost(object $post, ?string $cursor = null, bool $getReplies = true):object|bool { 543 preg_match('/at:\/\/(did:plc:[a-z0-9]+)\/[a-zA-Z\.]+\/([a-z0-9]+)/', $post->uri, $rkeyMatch); 544 if (!$rkeyMatch) return false; 545 $authorData = $this->getUserInfo($post->author->did, 'did'); 546 $facets = property_exists($post->record, 'facets') ? $this->sanitizeFacets($post->record->facets) : []; 547 $processedText = $this->applyFacets($post->record->text, $facets); 548 $embedType = property_exists($post->record, 'embed') ? $post->record->embed->{'$type'} : ''; 549 $embeds = property_exists($post->record, 'embed') ? $this->sanitizeEmbeds($post->record->embed, $authorData) : []; 550 \updatePostCache($rkeyMatch[2], $authorData->did, $processedText, $embedType, json_encode($embeds), $post->record->createdAt); 551 return (object) [ 552 'author' => (object) [ 553 'displayName' => $authorData->displayName, 554 'handle' => $authorData->handle, 555 'did' => $authorData->did, 556 'avatar' => $authorData->avatar, 557 'profileLink' => '/u/'.$authorData->handle, 558 ], 559 'uri' => $post->uri, 560 'pds' => $authorData->pds, 561 'postId' => $rkeyMatch[2], 562 'postLink' => '/u/'.$authorData->handle.'/'.$rkeyMatch[2], 563 'createdAt' => $post->record->createdAt, 564 'embedType' => $embedType, 565 'embeds' => $embeds 566 ]; 567 } 568 569 function sanitizeCachedPost(object $post, bool $getReplies = true): object { 570 $uri = 'at://'.$post->did.'/app.bsky.feed.post/'.$post->rkey; 571 $authorData = $this->getUserInfo($post->did, 'did'); 572 return (object) [ 573 'author' => (object) [ 574 'displayName' => $authorData->displayName, 575 'handle' => $authorData->handle, 576 'did' => $authorData->did, 577 'avatar' => $authorData->avatar, 578 'profileLink' => '/u/'.$authorData->handle, 579 ], 580 'uri' => $uri, 581 'pds' => $authorData->pds, 582 'postId' => $post->rkey, 583 'postLink' => '/u/'.$authorData->handle.'/'.$post->rkey, 584 'content' => $post->text, 585 'createdAt' => $post->createdAt, 586 'embedType' => $post->embedType, 587 'embeds' => json_decode($post->embeds) 588 ]; 589 } 590 591 function sanitizeEmbeds(object $embeds, object $authorData):array|bool { 592 if ($embeds->{'$type'} === 'app.bsky.embed.images') { 593 return array_map(function ($im) use ($authorData) { 594 return (object) [ 595 'imgUrl' => $this->getMediaUrl($authorData->pds, $authorData->did, $im->image->ref->{'$link'}), 596 'alt' => $im->alt, 597 'width' => property_exists($im, 'aspectRatio') ? $im->aspectRatio->width : "auto", 598 'height' => property_exists($im, 'aspectRatio') ? $im->aspectRatio->height : "auto" 599 ]; 600 }, $embeds->images); 601 } else if ($embeds->{'$type'} === 'app.bsky.embed.video') { 602 return [ 603 (object) [ 604 'thumb' => property_exists($embeds->video, 'thumbnail') ? $embeds->video->thumbnail : '', 605 'videoUrl' => $this->getMediaUrl($authorData->pds, $authorData->did, $embeds->video->ref->{'$link'}), 606 'width' => property_exists($embeds, 'aspectRatio') ? $embeds->aspectRatio->width : "auto", 607 'height' => property_exists($embeds, 'aspectRatio') ? $embeds->aspectRatio->height : "auto" 608 ] 609 ]; 610 } else if ($embeds->{'$type'} === 'app.bsky.embed.record') { 611 $uriComponents = $this->splitAtUri($embeds->record->uri); 612 $post = $this->getSlingshotData($uriComponents->did, $uriComponents->collection, $uriComponents->rkey); 613 $sanitizedPost = $this->sanitizePost($post, true); 614 return [ 615 (object) [ 616 'post' => $sanitizedPost 617 ] 618 ]; 619 } else if ($embeds->{'$type'} === 'app.bsky.embed.external') { 620 return [ 621 (object) [ 622 'uri' => $embeds->external->uri, 623 'title' => $embeds->external->title, 624 'description' => $embeds->external->description, 625 'thumb' => property_exists($embeds->external, 'thumb') ? $this->getMediaUrl($authorData->pds, $authorData->did, $embeds->external->thumb->ref->{'$link'}) : null 626 ] 627 ]; 628 } 629 return false; 630 } 631 632 function sanitizeUserList(array $users):array { 633 $normalized = array_map(function ($rec) { 634 $hydratedRec = $rec; 635 return $this->getUserInfo($rec->did, 'did'); 636 }, $users); 637 $ret = array_values(array_filter($normalized)); 638 return $ret; 639 } 640 641 function sanitizeFacets(array $facets):array { 642 usort($facets, function($a, $b) { 643 if ($a->index->byteStart > $b->index->byteStart) { 644 return 1; 645 } 646 return -1; 647 }); 648 return array_map(function ($facet) { 649 if ($facet->features[0]->{'$type'} === "app.bsky.richtext.facet#mention") { 650 $did = $facet->features[0]->did; 651 $userInfo = $this->getPlcInfoFromRecord($did); 652 if ($userInfo) { 653 $handle = $userInfo->handle; 654 return (object) [ 655 'type' => 'mention', 656 'did' => $did, 657 'handle' => $handle, 658 'start' => $facet->index->byteStart, 659 'end' => $facet->index->byteEnd 660 ]; 661 } 662 return (object) []; 663 } else if ($facet->features[0]->{'$type'} === "app.bsky.richtext.facet#tag") { 664 return (object) [ 665 'type' => 'tag', 666 'tag' => $facet->features[0]->tag, 667 'start' => $facet->index->byteStart, 668 'end' => $facet->index->byteEnd 669 ]; 670 } else if ($facet->features[0]->{'$type'} === "app.bsky.richtext.facet#link") { 671 return (object) [ 672 'type' => 'link', 673 'link' => $facet->features[0]->uri, 674 'start' => $facet->index->byteStart, 675 'end' => $facet->index->byteEnd 676 ]; 677 } 678 return (object) []; 679 }, $facets); 680 } 681 682 /* App-Level Ban/Timeout Implementation */ 683 684 function userBanCheck(string $did): bool { 685 } 686 687 function filterRecords(array $records, string $userField):array { 688 } 689} 690 691?>