friendship ended with social-app. php is my new best friend

fix alegreya font to not be smallcaps, add examples of appview-level ban data done jankily through json files

+1
.gitignore
··· 22 22 .env.production.local 23 23 .env.local 24 24 jwks.json 25 + data/*.json 25 26 26 27 # caches 27 28 .eslintcache
+2
config.php
··· 3 3 define('FRONTPAGE_FEED', "at://did:plc:ffkgesg3jsv2j7aagkzrtcvt/app.bsky.feed.generator/aaagllxbcbsje"); 4 4 define('SLINGSHOT_INSTANCE', "https://slingshot.microcosm.blue"); 5 5 define('CONSTELLATION_INSTANCE', "https://constellation.microcosm.blue"); 6 + define('DEFAULT_PDS', "https://bsky.social"); 6 7 define('PLC_DIRECTORY', "https://plc.directory"); 7 8 define('FAVORITE_FEEDS', [ // list of feeds you want to display in the sidebar to users not logged in 9 + "at://did:plc:ffkgesg3jsv2j7aagkzrtcvt/app.bsky.feed.generator/aaagllxbcbsje", 8 10 "at://did:plc:xdtvlh4k3tntzp2vinmgehsz/app.bsky.feed.generator/aaaeb7hxb53jg", 9 11 "at://did:plc:abv47bjgzjgoh3yrygwoi36x/app.bsky.feed.generator/aaai4f7awwzkc", 10 12 "at://did:plc:dltmiiweocm22l656iffwn5a/app.bsky.feed.generator/aaagnt6bwkkb2",
+4 -4
css/_partials/_fonts.scss
··· 2 2 font-family: 'Alegreya'; 3 3 font-weight: normal; 4 4 font-style: normal; 5 - src: url(@fonts/alegreyasc-regular-webfont.woff2) format('woff2'); 5 + src: url(@fonts/Alegreya-Regular.woff2) format('woff2'); 6 6 } 7 7 8 8 @font-face { 9 9 font-family: 'Alegreya'; 10 10 font-weight: bold; 11 11 font-style: normal; 12 - src: url(@fonts/alegreyasc-bold-webfont.woff2) format('woff2'); 12 + src: url(@fonts/Alegreya-Bold.woff2) format('woff2'); 13 13 } 14 14 15 15 @font-face { 16 16 font-family: 'Alegreya'; 17 17 font-weight: normal; 18 18 font-style: italic; 19 - src: url(@fonts/alegreyasc-italic-webfont.woff2) format('woff2'); 19 + src: url(@fonts/Alegreya-Italic.woff2) format('woff2'); 20 20 } 21 21 22 22 @font-face { 23 23 font-family: 'Alegreya'; 24 24 font-weight: bold; 25 25 font-style: italic; 26 - src: url(@fonts/alegreyasc-bolditalic-webfont.woff2) format('woff2'); 26 + src: url(@fonts/Alegreya-BoldItalic.woff2) format('woff2'); 27 27 } 28 28 29 29 @font-face {
+6
css/_partials/_globals.scss
··· 14 14 color: var(--link-color); 15 15 } 16 16 17 + h2 { 18 + font-size: 1em; 19 + text-align: center; 20 + margin: 0; 21 + } 22 + 17 23 label { 18 24 display: block; 19 25 font-weight: bold;
+15
css/_partials/_layout.scss
··· 51 51 main { 52 52 border-right: 1px var(--border-color) solid; 53 53 grid-area: main; 54 + max-width: 660px; 55 + width: 100%; 54 56 55 57 &.about, 56 58 &.terms, ··· 66 68 padding: 10px; 67 69 border-bottom: 1px var(--border-color) solid; 68 70 grid-area: aside; 71 + } 72 + 73 + #homefeeds { 74 + list-style: none; 75 + margin: 0; 76 + padding: 0; 77 + 78 + li { 79 + a { 80 + display: block; 81 + padding: 8px 0; 82 + } 83 + } 69 84 } 70 85 71 86 footer {
+13 -5
css/_partials/_post.scss
··· 1 1 .post { 2 2 border-bottom: 1px var(--border-color) solid; 3 3 padding: 10px; 4 + width: 100%; 4 5 5 6 .postHeader { 6 7 display: grid; ··· 62 63 gap: 10px; 63 64 margin: 10px 0; 64 65 65 - video { 66 - max-width: 100%; 67 - height: auto; 68 - } 69 - 70 66 img { 71 67 max-width: 100%; 72 68 height: 100%; ··· 77 73 .btn-invis { 78 74 width: 100%; 79 75 height: 100%; 76 + } 77 + 78 + .video { 79 + width: 100%; 80 + } 81 + 82 + video { 83 + max-width: 100%; 84 + width: 100%; 85 + height: auto; 86 + margin: 1em 0; 87 + border: 1px var(--border-color) solid; 80 88 } 81 89 82 90 &.img-2 {
+7
data/babyjail.json.example
··· 1 + [ 2 + { 3 + "did": "did:plc:whateverabckdfdkjf", 4 + "reason": "being a jerk on main", 5 + "timeoutUntil": "2025-10-15T00:00:00Z" 6 + } 7 + ]
+6
data/bans.json.example
··· 1 + [ 2 + { 3 + "did": "did:plc:232eprew2nyn75ensoah2lg2", 4 + "reason": "jesse singal" 5 + } 6 + ]
+3
data/index.php
··· 1 + <?php 2 + echo 'nope'; 3 + ?>
+6
data/pds-bans.json.example
··· 1 + [ 2 + { 3 + "serverUrl": "https://badsite.domain", 4 + "reason": "full of jerks" 5 + } 6 + ]
fonts/Alegreya-Bold.woff2

This is a binary file and will not be displayed.

fonts/Alegreya-BoldItalic.woff2

This is a binary file and will not be displayed.

fonts/Alegreya-Italic.woff2

This is a binary file and will not be displayed.

fonts/Alegreya-Regular.woff2

This is a binary file and will not be displayed.

fonts/alegreyasc-bold-webfont.woff

This is a binary file and will not be displayed.

fonts/alegreyasc-bold-webfont.woff2

This is a binary file and will not be displayed.

fonts/alegreyasc-bolditalic-webfont.woff

This is a binary file and will not be displayed.

fonts/alegreyasc-bolditalic-webfont.woff2

This is a binary file and will not be displayed.

fonts/alegreyasc-italic-webfont.woff

This is a binary file and will not be displayed.

fonts/alegreyasc-italic-webfont.woff2

This is a binary file and will not be displayed.

fonts/alegreyasc-regular-webfont.woff

This is a binary file and will not be displayed.

fonts/alegreyasc-regular-webfont.woff2

This is a binary file and will not be displayed.

+19 -17
index.php
··· 3 3 require_once('config.php'); 4 4 require_once('lib/helpers.php'); 5 5 require_once('lib/bskyToucher.php'); 6 + require_once('lib/bskyRenderer.php'); 6 7 7 - use Smallnest\Helpers\getTheme; 8 - use Smallnest\Helpers\getUrl; 9 - use Smallnest\Helpers\renderLogo; 10 8 use Smallnest\Bsky\BskyToucher; 11 9 12 10 $dotenv = Dotenv\Dotenv::createImmutable(__DIR__); 13 11 $dotenv->load(); 14 12 15 - $provider = new BskyToucher(); 13 + $bskyToucher = new BskyToucher(); 16 14 17 15 $mainClass = array_key_exists('p', $_GET) ? " class='".$_GET['p']."'" : ""; 18 16 $pageName = array_key_exists('p', $_GET) ? $_GET['p'] : null; ··· 47 45 } ?> 48 46 </main> 49 47 <aside> 48 + <h2>Recommended Feeds</h2> 49 + <ul id="homefeeds"> 50 50 <?php 51 - foreach (FAVORITE_FEEDS as $feed) { 52 - preg_match('/at:\/\/(did:[a-z]+:[a-z0-9]+)\/app.bsky.feed.generator/([a-zA-Z0-9]+)', $feed, $feedParts); 53 - 54 - if ($feedParts) { 55 - $info = $provider->getSlingshotData($feedParts[1], 'app.bsky.feed.generator', $feedParts[2]); 56 - $url = Smallnest\Helpers\getUrl('/index.php', PRETTY_URLS, [ 57 - 'p' => 'feed', 58 - 'did' => $feedParts[1], 59 - 'rkey' => $feedParts[2] 60 - ]); 61 - echo "<a href='".$url."'></a>"; 51 + if (FAVORITE_FEEDS) { 52 + foreach (FAVORITE_FEEDS as $feed) { 53 + preg_match('/at:\/\/(did:plc:[a-z0-9]+)\/app.bsky.feed.generator\/([a-z0-9]+)/', $feed, $feedParts); 54 + if ($feedParts) { 55 + $info = $bskyToucher->getSlingshotData($feedParts[1], 'app.bsky.feed.generator', $feedParts[2]); 56 + $url = Smallnest\Helpers\getUrl('', PRETTY_URLS, [ 57 + 'p' => 'feed', 58 + 'did' => $feedParts[1], 59 + 'rkey' => $feedParts[2] 60 + ]); 61 + echo "<li><a href='".$url."'>".$info->value->displayName."</a></li>"; 62 + } 62 63 } 63 64 } 64 65 ?> 66 + </ul> 65 67 </aside> 66 68 <footer> 67 69 <div id="themes"> ··· 82 84 <li><a href="#" data-font="alegreya">alegreya</a></li> 83 85 </ul> 84 86 </div> 85 - <a href="<?= Smallnest\Helpers\getUrl('terms', PRETTY_URLS); ?>">terms</a> &bull; <a href="<?= Smallnest\Helpers\getUrl('privacy', PRETTY_URLS); ?>">privacy</a> &bull; <a href="<?= Smallnest\Helpers\getUrl('about', PRETTY_URLS); ?>">about</a><br /><br /> 86 - <a href="https://tangled.org/@veryroundbird.house/smallbird-social" target="_blank" rel="external">code</a> / <a href="https://bsky.app/profile/smallbirdsocial.on.computer" target="_blank" rel="external"> / <a href="https://ko-fi.com/veryroundbird" target="_blank" rel="external">ko-fi</a><br /><br /> 87 + <a href="<?= Smallnest\Helpers\getUrl('', PRETTY_URLS, ['p' => 'terms']); ?>">terms</a> &bull; <a href="<?= Smallnest\Helpers\getUrl('', PRETTY_URLS, ['p' => 'privacy']); ?>">privacy</a> &bull; <a href="<?= Smallnest\Helpers\getUrl('', PRETTY_URLS, ['p' => 'about']); ?>">about</a><br /><br /> 88 + <a href="https://tangled.org/@veryroundbird.house/smallbird-social" target="_blank" rel="external">code</a> / <a href="https://bsky.app/profile/smallbirdsocial.on.computer" target="_blank" rel="external">bsky</a> / <a href="https://ko-fi.com/veryroundbird" target="_blank" rel="external">ko-fi</a><br /><br /> 87 89 a <a href="https://veryroundbird.house" target="_blank" rel="external">veryroundbird.house</a> project 88 90 </footer> 89 91 </div>
+152
lib/bskyRenderer.php
··· 1 + <?php 2 + namespace Smallnest\Bsky; 3 + 4 + require_once('config.php'); 5 + 6 + class BskyRenderer { 7 + public function renderProfile(object $user, ?bool $auth = false):string|bool { 8 + return false; 9 + } 10 + 11 + public function renderMiniProfile(object $user):string|bool { 12 + $profileUrl = getUrl('', PRETTY_URLS, [ 13 + 'p' => 'profile', 14 + 'handle' => $user->handle 15 + ]); 16 + $ret = <<<EOD 17 + <div class="userMini"> 18 + <a href="$profileUrl"> 19 + <div class="avatar"> 20 + <img src="$user->avatar" alt="$user->displayName" /> 21 + </div> 22 + <div class="name"> 23 + <span class="displayName">$user->displayName</span> 24 + <span class="did">$user->did</span> 25 + </div> 26 + </a> 27 + </div> 28 + EOD; 29 + return false; 30 + } 31 + 32 + public function renderPost(object $post, ?bool $prettyUrls = false):void { 33 + $author = $post->author; 34 + $profileLink = $prettyUrls ? "/profile/".$author->handle : "/?p=profile&handle=".$author->handle; 35 + preg_match('/([a-zA-Z0-9]+)$/', $post->uri, $uriParts); 36 + $postId = $uriParts ? $uriParts[1] : ''; 37 + $postLink = $prettyUrls ? $profileLink."/post/".$postId : $profileLink."&post=".$postId; 38 + $postCopyUrl = "https://".getEnv('SERVER_URL').$postLink; 39 + $embeds = $this->renderEmbeds($post); 40 + $imageEmbedClass = $post->embedType === "images" ? " img-".count($post->embeds) : ""; 41 + echo <<<EOD 42 + <div class="post" data-uri="$post->uri"> 43 + <div class="postHeader"> 44 + <div class="avatar"> 45 + <a href="$profileLink"><img src="$author->avatar" /></a> 46 + </div> 47 + <div class="displayName"><a href="$profileLink">$author->displayName</a></div> 48 + <div class="handle"><a href="$profileLink">$author->handle</a></div> 49 + <div class="timeAgo"><a href="$postLink">$post->createdAt</a></div> 50 + </div> 51 + <div class="postContent">$post->content</div> 52 + $embeds 53 + <div class="postFooter"> 54 + <div class="postInteraction"> 55 + <a href="#like">like ($post->likeCount)</a> 56 + <a href="#reply">reply ($post->replyCount)</a> 57 + <a href="#quote">quote ($post->quoteCount)</a> 58 + <a href="#repost">repost ($post->repostCount)</a> 59 + </div> 60 + <div class="postSharing"> 61 + <a href="#" data-share="$postCopyUrl">copy link</a> 62 + </div> 63 + </div> 64 + </div> 65 + EOD; 66 + } 67 + 68 + public function renderEmbeds(object $post):string|bool { 69 + $embeds = $post->embeds; 70 + $type = $post->embedType; 71 + $ret = ""; 72 + if ($embeds) { 73 + if ($type === 'app.bsky.embed.images') { 74 + $imageCount = count($embeds); 75 + $ret .= <<<EOD 76 + <div class="postEmbeds img-$imageCount"> 77 + EOD; 78 + foreach ($embeds as $i=>$embed) { 79 + $altSanitized = addslashes(htmlspecialchars($embed->alt)); 80 + $pds = $post->pds; 81 + $did = $post->author->did; 82 + $cid = $embed->cid; 83 + $idx = $i+1; 84 + $ret .= <<<EOD 85 + <div class="img" id="img-$post->postId-$idx-small"> 86 + <button type="button" class="btn-invis" popovertarget="img-full-$post->postId-$idx"><img src="$pds/xrpc/com.atproto.sync.getBlob?did=$did&cid=$cid" alt="$altSanitized" /></button> 87 + </div> 88 + <div id="img-full-$post->postId-$idx" popover> 89 + <img src="$pds/xrpc/com.atproto.sync.getBlob?did=$did&cid=$cid" alt="$altSanitized" /> 90 + <div class="alt">$embed->alt</div> 91 + </div> 92 + EOD; 93 + } 94 + $ret .= <<<EOD 95 + </div> 96 + EOD; 97 + } else if ($type === 'app.bsky.embed.video') { 98 + $thumb = $embeds[0]->thumb; 99 + $vid = $embeds[0]->videoCid; 100 + $author = $post->author->did; 101 + $pds = $post->pds; 102 + $ret .= <<<EOD 103 + <div class="postEmbeds"> 104 + <div class="video"> 105 + <video 106 + src="$pds/xrpc/com.atproto.sync.getBlob?did=$author&cid=$vid" 107 + controls 108 + ></video> 109 + </div> 110 + </div> 111 + EOD; 112 + } else if ($type === 'app.bsky.embed.external') { 113 + $title = $embeds[0]->title; 114 + $uri = $embeds[0]->uri; 115 + $thumb = $embeds[0]->thumb; 116 + $desc = $embeds[0]->description; 117 + $ret .= <<<EOD 118 + <div class="postEmbeds"> 119 + <div class="externalLink"> 120 + <a href="$uri" rel="external" target="_blank"> 121 + <div class="externalLinkImage"> 122 + <img src="$thumb" alt="$title" /> 123 + </div> 124 + <div class="externalLinkInfo"> 125 + <div class="externalLinkTitle">$title</div> 126 + <div class="externalLinkDesc">$desc</div> 127 + <div class="externalLinkUri">$uri</div> 128 + </div> 129 + </a> 130 + </div> 131 + </div> 132 + EOD; 133 + } else if ($type === 'app.bsky.embed.record') { 134 + /*preg_match('/at:\/\/(did:plc:[a-z0-9]+)\/([a-zA-Z0-9\.])\/([a-z0-9]+)/', $embeds[0], $recComponents); 135 + $recDid = $recComponents[1]; 136 + $recRkey = $recComponents[2]; 137 + $uri = getEnv('SERVER_URL').'/?p=post&handle='.$recDid.'&postId='.$recRkey; 138 + $rec = $this->getSlingshotData($recDid, 'app.bsky.feed.post', $recRkey); 139 + $ret .= <<<EOD 140 + <div class="embeds"> 141 + <div class="record"> 142 + <a href="$uri"> 143 + idk 144 + </a> 145 + </div> 146 + </div> 147 + EOD;*/ 148 + } 149 + } 150 + return $ret; 151 + } 152 + }
+95 -205
lib/bskyToucher.php
··· 2 2 namespace Smallnest\Bsky; 3 3 4 4 require_once('config.php'); 5 - require_once('helpers.php'); 6 5 require_once('vendor/autoload.php'); 7 6 require_once('bskyProvider.php'); 8 7 9 8 use Smallnest\Bsky\BskyProvider; 10 - use Smallnest\Helpers; 9 + use Smallnest\Bsky\BskyRenderer; 11 10 use GuzzleHttp\Client; 12 11 use GuzzleHttp\Query; 13 12 use Firebase\JWT\JWT; ··· 276 275 return $ret; 277 276 } 278 277 279 - function renderPost($post, $prettyUrls = false) { 280 - $author = $post->author; 281 - $profileLink = $prettyUrls ? "/profile/".$author->handle : "/?p=profile&handle=".$author->handle; 282 - preg_match('/([a-zA-Z0-9]+)$/', $post->uri, $uriParts); 283 - $postId = $uriParts[1]; 284 - $postLink = $prettyUrls ? $profileLink."/post/".$postId : $profileLink."&post=".$postId; 285 - $postCopyUrl = "https://".getEnv('SERVER_URL').$postLink; 286 - $embeds = $this->renderEmbeds($post->embedType, $post->embeds, $post); 287 - $imageEmbedClass = $post->embedType === "images" ? " img-".count($post->embeds) : ""; 288 - echo <<<EOD 289 - <div class="post" data-uri="$post->uri"> 290 - <div class="postHeader"> 291 - <div class="avatar"> 292 - <a href="$profileLink"><img src="$author->avatar" /></a> 293 - </div> 294 - <div class="displayName"><a href="$profileLink">$author->displayName</a></div> 295 - <div class="handle"><a href="$profileLink">$author->handle</a></div> 296 - <div class="timeAgo"><a href="$postLink">$post->createdAt</a></div> 297 - </div> 298 - <div class="postContent">$post->text</div> 299 - $embeds 300 - <div class="postFooter"> 301 - <div class="postInteraction"> 302 - <a href="#like">like ($post->likeCount)</a> 303 - <a href="#reply">reply ($post->replyCount)</a> 304 - <a href="#quote">quote ($post->quoteCount)</a> 305 - <a href="#repost">repost ($post->repostCount)</a> 306 - </div> 307 - <div class="postSharing"> 308 - <a href="#" data-share="$postCopyUrl">copy link</a> 309 - </div> 310 - </div> 311 - </div> 312 - EOD; 313 - } 314 - 315 - function renderEmbeds($type, $embeds, $context) { 316 - $ret = ""; 317 - if ($type === 'images') { 318 - $imageCount = count($embeds); 319 - foreach ($embeds as $embed) { 320 - $altSanitized = addslashes(htmlspecialchars($embed->alt)); 321 - $ret .= <<<EOD 322 - <div class="postEmbeds img-$post->embeds"> 323 - <div class="img" id="img-$context->postId-small"> 324 - <button type="button" class="btn-invis" popovertarget="img-full-$context->postId"><img src="$embed->thumb" alt="$altSanitized" /></button> 325 - <div id="img-full-$context->postId" popover> 326 - <img src="$embed->fullsize" alt="$altSanitized" /> 327 - <div class="alt">$embed->alt</div> 328 - </div> 329 - </div> 330 - </div> 331 - EOD; 332 - } 333 - } else if ($type === 'video') { 334 - $thumb = $embeds[0]->thumb; 335 - $vid = $embeds[0]->video; 336 - $author = $context->did; 337 - $pds = DEFAULT_PDS; 338 - $ret .= <<<EOD 339 - <div class="embeds"> 340 - <div class="video"> 341 - <video 342 - src="$pds/xrpc/com.atproto.sync.getBlob?did=$author&cid=$vid" 343 - controls 344 - ></video> 345 - </div> 346 - </div> 347 - EOD; 348 - } else if ($type === 'external') { 349 - $title = $embeds[0]->title; 350 - $uri = $embeds[0]->uri; 351 - $thumb = $embeds[0]->thumb; 352 - $desc = $embeds[0]->description; 353 - $ret .= <<<EOD 354 - <div class="embeds"> 355 - <div class="externalLink"> 356 - <a href="$uri" rel="external" target="_blank"> 357 - <div class="externalLinkImage"> 358 - <img src="$thumb" alt="$title" /> 359 - </div> 360 - <div class="externalLinkInfo"> 361 - <div class="externalLinkTitle">$title</div> 362 - <div class="externalLinkDesc">$desc</div> 363 - <div class="externalLinkUri">$uri</div> 364 - </div> 365 - </a> 366 - </div> 367 - </div> 368 - EOD; 369 - } else if ($type === 'record') { 370 - preg_match('/at:\/\/(did:plc:[a-z0-9]+)\/([a-zA-Z0-9\.])\/([a-z0-9]+)/', $embeds[0], $recComponents); 371 - print_r($recComponents); 372 - $recDid = $recComponents[1]; 373 - $recRkey = $recComponents[2]; 374 - $uri = SERVER_URL.'/?p=post&handle='.$recDid.'&postId='.$recRkey; 375 - $rec = $this->getSlingshotData($recDid, 'app.bsky.feed.post', $recRkey); 376 - print_r($rec); 377 - $ret .= <<<EOD 378 - <div class="embeds"> 379 - <div class="record"> 380 - <a href="$uri"> 381 - idk 382 - </a> 383 - </div> 384 - </div> 385 - EOD; 386 - } 387 - return $ret; 388 - } 389 - 390 278 /* POSTS */ 391 279 392 280 function getPost(string $did, string $rkey, $userAuth = null) { 393 281 if ($userAuth) { 394 282 395 283 } 396 - $authorInfo = $this->getUserInfo($did); 397 284 $ret = $this->getSlingshotData($did, "app.bsky.feed.post", $rkey); 285 + $userInfo = $this->getSlingshotData($did, "app.bsky.actor.profile", "self"); 286 + $author = (object) [ 287 + 'displayName' => $userInfo->value->displayName, 288 + 'handle' => $userInfo->value->handle, 289 + 'did' => $did, 290 + 'avatar' => getImage($userInfo->value->avatar->ref->{'$link'}) 291 + ]; 292 + $ret['author'] = $author; 398 293 if ($ret) { 399 - return $this->tidyPost($ret, $authorInfo); 294 + return $this->sanitizePost($ret); 400 295 } 401 296 return false; 402 297 } ··· 465 360 return []; 466 361 } 467 362 468 - function tidyPost($ret, $authorInfo) { 469 - $uri = $ret->uri; 470 - $post = $ret->value; 471 - $embeds = []; 472 - $embedType = null; 473 - $replyTo = null; 474 - 475 - if (property_exists($post, 'reply') && property_exists($post->reply, 'parent')) { 476 - preg_match('/^at:\/\/(did:plc:[a-z0-9]+)\/app.bsky.feed.post\/([a-z0-9]+)$/', $post->reply->parent->uri, $atUriSplit); 477 - $replyUserInfo = $this->getUserInfo($atUriSplit[1]); 478 - $replyTo = $post->reply->parent->uri;/*(object) [ 479 - 'uri' => \Smallnest\Helpers\getUrl(SERVER_URL, PRETTY_URLS, [ 480 - 'p' => $replyUserInfo->handle, 481 - 'post' => $atUriSplit[2] 482 - ]), 483 - 'author' => $this->getUserInfo($atUriSplit[1]) 484 - ];*/ 485 - } 486 - 487 - if (property_exists($post, 'embed') && property_exists($post->embed, '$type')) { 488 - if ($post->embed->{'$type'} === 'app.bsky.embed.images#view') { 489 - $embeds = $post->embed->images; 490 - $embedType = 'images'; 491 - } elseif ($post->embed->{'$type'} === 'app.bsky.embed.video#view') { 492 - $embeds = [(object) [ 493 - "video" => $post->embed->cid, 494 - "thumb" => $post->embed->thumbnail 495 - ]]; 496 - $embedType = 'video'; 497 - } elseif ($post->embed->{'$type'} === 'app.bsky.embed.external#view') { 498 - $embeds = [$post->embed->external]; 499 - $embedtype = 'external'; 500 - } elseif ($post->embed->{'$type'} === 'app.bsky.embed.record') { 501 - $embedtype = 'record'; 502 - $embeds = [$post->embed->record->uri]; 503 - } 504 - } 505 - 506 - return (object) [ 507 - 'uri' => $ret->uri, 508 - "author" => $authorInfo, 509 - "createdAt" => $post->createdAt, 510 - "text" => $post->text, 511 - 'replyCount' => $this->getReplies($uri), 512 - 'repostCount' => $this->getReposts($uri), 513 - 'likeCount' => $this->getLikes($uri), 514 - 'quoteCount' => $this->getQuotes($uri), 515 - 'embedType' => $embedType, 516 - 'embeds' => $embeds, 517 - 'reply' => $replyTo 518 - ]; 519 - } 520 - 521 363 function getUserPosts($user, $cursor = null, $newer = false) { 522 364 [$provider, $did, $handle] = $this->getUserDidAndProvider($user); 523 365 $didInfo = $this->getUserInfo($did); ··· 531 373 532 374 $newCursor = $pdsData->cursor; 533 375 $feed = array_map(function($post) use ($user) { 534 - $authorInfo = $this->getUserInfo($user); 535 - return $this->tidyPost($post, $authorInfo); 376 + return $this->sanitizePost($post); 536 377 }, $pdsData->records); 537 378 //$feed = $pdsData->records; 538 379 return $feed; 539 380 //$this->sanitizeFeed($feed, $newCursor, $cursor, $newer); 540 381 } 541 382 542 - function getFeed(string $atUri, ?string $cursor = null, ?string $userAuth = null, ?bool $newer = false, int $limit = 100):array|bool { 383 + function getFeed(string $atUri, ?string $cursor = null, ?string $userAuth = null, ?bool $newer = false, int $limit = 15):array|bool { 543 384 $httpClient = new Client([ 544 385 'timeout' => 2.0, 545 386 ]); ··· 573 414 return false; 574 415 } 575 416 417 + function sanitizePost(object $post, ?bool $auth = null):object { 418 + if ($auth) { 419 + return $this->sanitizeAuthedPost($post); 420 + } 421 + return $this->sanitizePublicApiPost($post); 422 + } 423 + 424 + function sanitizeAuthedPost(object $post):object { 425 + return (object) []; 426 + } 427 + 428 + function sanitizePublicApiPost(object $post):object { 429 + preg_match('/at:\/\/(did:plc:[a-z0-9]+)\/[a-zA-Z\.]+\/([a-z0-9]+)/', $post->uri, $rkeyMatch); 430 + if (!$rkeyMatch) return null; 431 + print_r($post); 432 + $authorInfo = $this->getSlingshotIdentityMiniDoc($post->author->did); 433 + return (object) [ 434 + 'author' => $post->author, 435 + 'uri' => $post->uri, 436 + 'pds' => $authorInfo->pds, 437 + 'postId' => $rkeyMatch[2], 438 + 'content' => $post->record->text, 439 + 'facets' => property_exists($post->record, 'facets') ? $post->record->facets : [], 440 + 'replyCount' => $post->replyCount, 441 + 'repostCount' => $post->repostCount, 442 + 'likeCount' => $post->likeCount, 443 + 'quoteCount' => $post->quoteCount, 444 + 'createdAt' => $post->record->createdAt, 445 + 'embedType' => property_exists($post->record, 'embed') ? $post->record->embed->{'$type'} : null, 446 + 'embeds' => property_exists($post->record, 'embed') ? $this->sanitizeEmbeds($post->record->embed) : [], 447 + 'cursor' => $newCursor 448 + ]; 449 + } 450 + 576 451 function sanitizePublicApiPosts(array $posts, string $newCursor, ?string $cursor = null, ?bool $newer = false):array { 577 452 $feed = array_filter(array_map(function($p) use ($newCursor, $cursor) { 578 453 $post = $p->post; 579 - return (object) [ 580 - 'author' => $post->author, 581 - 'postId' => $pid, 582 - 'content' => $post->record->text, 583 - 'facets' => $post->record->facets, 584 - 'replyCount' => $post->replyCount, 585 - 'repostCount' => $post->repostCount, 586 - 'likeCount' => $post->likeCount, 587 - 'quoteCount' => $post->quoteCount, 588 - 'createdAt' => $post->record->createdAt, 589 - 'embedType' => $post->record->embed->{'$type'}, 590 - 'embeds' => '', 591 - 'cursor' => $newCursor 592 - ]; 454 + return $this->sanitizePublicApiPost($post); 593 455 }, $posts), function ($p) use ($newer, $newCursor, $cursor) { 456 + if (!$p) return false; 594 457 if (!$newer) return true; 595 458 return strtotime($feedItem->createdAt) > strtotime($cursor); 596 459 }); ··· 620 483 ]; 621 484 }, $post->record->facets) : []; 622 485 623 - if (count($facetsList) > 0) { 486 + if ($facetsList && count($facetsList) > 0) { 624 487 $processedText = $this->applyFacets($post->record->text, $facetsList); 625 488 } 626 - $embeds = []; 627 - $embedType = null; 628 - //print_r($post); 629 - if (property_exists($post, 'embed') && property_exists($post->embed, '$type')) { 630 - if ($post->embed->{'$type'} === 'app.bsky.embed.images#view') { 631 - $embeds = $post->embed->images; 632 - $embedType = 'images'; 633 - } elseif ($post->embed->{'$type'} === 'app.bsky.embed.video#view') { 634 - $embeds = [(object) [ 635 - "video" => $post->embed->cid, 636 - "thumb" => $post->embed->thumbnail 637 - ]]; 638 - $embedType = 'video'; 639 - } elseif ($post->embed->{'$type'} === 'app.bsky.embed.external#view') { 640 - $embeds = [$post->embed->external]; 641 - $embedtype = 'external'; 642 - } 643 - } 489 + $embeds = property_exists($post, 'embed') ? $this->sanitizeEmbeds($post->embed) : []; 490 + $embedType = property_exists($post, 'embed') ? $post->embed->{'$type'} : null; 644 491 645 492 return (object) [ 646 493 'author' => $this->getUserInfo($post->author->handle), ··· 656 503 'embeds' => $embeds, 657 504 'cursor' => $newCursor 658 505 ]; 659 - }, $posts), function($feedItem) use ($newer, $cursor) { 506 + }, $posts), function (object $feedItem) use ($newer, $cursor) { 660 507 if (!$newer) return true; 661 508 return strtotime($feedItem->createdAt) > strtotime($cursor); 662 509 }); 663 510 return $feed; 511 + } 512 + 513 + function sanitizeEmbeds(object $embeds):array|bool { 514 + if ($embeds->{'$type'} === 'app.bsky.embed.images') { 515 + return array_map(function ($im) { 516 + return (object) [ 517 + 'cid' => $im->image->ref->{'$link'}, 518 + 'alt' => $im->alt, 519 + 'width' => $im->aspectRatio->width, 520 + 'height' => $im->aspectRatio->height 521 + ]; 522 + }, $embeds->images); 523 + } else if ($embeds->{'$type'} === 'app.bsky.embed.video') { 524 + return [ 525 + (object) [ 526 + 'thumb' => $embeds->video->thumbnail, 527 + 'videoCid' => $embeds->video->ref->{'$link'}, 528 + 'width' => $embeds->aspectRatio->width, 529 + 'height' => $embeds->aspectRatio->height 530 + ] 531 + ]; 532 + } else if ($embeds->{'$type'} === 'app.bsky.embed.record') { 533 + return [ 534 + (object) [ 535 + 'uri' => $embeds->record->uri/*, 536 + 'author' => (object) [ 537 + 'displayName' => $embeds->record->author->displayName, 538 + 'handle' => $embeds->record->author->handle, 539 + 'did' => $embeds->record->author->did, 540 + 'avatar' => $embeds->record->author->avatar 541 + ], 542 + 'post' => (object) [ 543 + 'text' => $embeds->record->value->text, 544 + 'createdAt' => $embeds->record->value->createdAt, 545 + 'facets' => $embeds->record->value->facets, 546 + 'embeds' => $this->sanitizeEmbeds($embeds->record->value->$embeds) 547 + ]*/ 548 + ] 549 + ]; 550 + } else if ($embeds->{'$type'} === 'app.bsky.something.external') { 551 + 552 + } 553 + return false; 664 554 } 665 555 } 666 556
+2 -2
lib/helpers.php
··· 2 2 namespace Smallnest\Helpers; 3 3 4 4 function getUrl($base, $prettyUrls = false, $params = []) { 5 - $paramString = join(($prettyUrls ? "/" : "&"), array_map(function ($k, $p) { 5 + $paramString = join(($prettyUrls ? "/" : "&"), array_map(function ($k, $p) use ($prettyUrls) { 6 6 return $prettyUrls ? $k."/".$p : $k."=".$p; 7 7 }, array_keys($params), array_values($params))); 8 - return $prettyUrls ? "/".$base.(count($params) > 0 ? "/".$paramString : "") : "/?p=".$base.(count($params) > 0 ? "&".$paramString : ""); 8 + return $prettyUrls ? "/".$base.(count($params) > 0 ? "/".$paramString : "") : $base.(count($params) > 0 ? "?".$paramString : ""); 9 9 } 10 10 11 11 function renderLogo() {
+3 -1
pages/home.php
··· 1 1 <?php 2 2 require_once('config.php'); 3 3 use Smallnest\Bsky\BskyToucher; 4 + use Smallnest\Bsky\BskyRenderer; 4 5 5 6 $bskyToucher = new BskyToucher(); 7 + $bskyRenderer = new BskyRenderer(); 6 8 $defaultFeed = "at://did:plc:ffkgesg3jsv2j7aagkzrtcvt/app.bsky.feed.generator/aaagllxbcbsje"; 7 9 $feedContents = $bskyToucher->getFeed(FRONTPAGE_FEED ? FRONTPAGE_FEED : $defaultFeed); 8 10 echo <<<EOF 9 11 <a href="#" class="refreshBar" data-feed="$defaultFeed">check for new posts</a> 10 12 EOF; 11 13 foreach($feedContents as $post) { 12 - $bskyToucher->renderPost($post, PRETTY_URLS); 14 + $bskyRenderer->renderPost($post, PRETTY_URLS); 13 15 } 14 16 ?>
+17 -7
pages/post.php
··· 1 1 <?php 2 2 require_once('config.php'); 3 3 require_once('lib/bskyToucher.php'); 4 + require_once('lib/bskyRenderer.php'); 4 5 5 6 use Smallnest\Bsky\BskyToucher; 7 + use Smallnest\Bsky\BskyRenderer; 6 8 7 9 $bskyToucher = new BskyToucher(); 10 + $bskyRenderer = new BskyRenderer(); 8 11 [$provider, $did, $handle] = $bskyToucher->getUserDidAndProvider($_GET['handle']); 9 12 $post = $bskyToucher->getPost($did, $_GET['postId']); 10 - $bskyToucher->renderPost($post); 13 + $sanitizedPost = $bskyToucher->sanitizePost($post); 14 + $bskyRenderer->renderPost($sanitizedPost); 11 15 $replies = $bskyToucher->getReplyRecords($post); 12 16 $quotes = $bskyToucher->getQuoteRecords($post); 13 17 $likes = $bskyToucher->getLikeUsers($post); 14 18 $reposts = $bskyToucher->getRepostUsers($post); 15 19 ?> 16 20 <div id="replies"> 17 - 21 + <h2>Replies</h2> 22 + <?php 23 + foreach ($replies as $reply) { 24 + $bskyRenderer->renderPost($reply); 25 + } 26 + ?> 18 27 </div> 19 28 <div id="quotes" popover> 20 29 21 30 </div> 22 31 <div id="likes" popover> 23 - <?php if ($likes) : ?> 24 - <?php foreach($likes as $like) : ?> 25 - 26 - <?php endforeach; ?> 27 - <?php endif; ?> 32 + <?php if ($likes) { 33 + foreach($likes as $like) { 34 + //echo $bskyRenderer->renderMiniPost($); 35 + } 36 + } 37 + ?> 28 38 </div> 29 39 <div id="reposts" popover> 30 40
+1
vite.config.js
··· 13 13 usePHP({ 14 14 entry: [ 15 15 'index.php', 16 + '.env', 16 17 'jwks.json', 17 18 'client-metadata.json', 18 19 'config.php',