friendship ended with social-app. php is my new best friend
1<?php 2error_reporting(E_ALL); 3ini_set('display_errors', 'On'); 4ini_set('log_errors_max_len', '0'); 5 6require_once('vendor/autoload.php'); 7require_once('config.php'); 8require_once('lib/bskyToucher.php'); 9require_once('lib/bskyProvider.php'); 10 11use flight\Engine; 12use League\CommonMark\CommonMarkConverter; 13use React\Promise\Deferred; 14use React\EventLoop\Loop; 15use React\Promise\Promise; 16use GuzzleHttp\Client; 17use Tracy\Debugger; 18use Tracy\OutputDebugger; 19use Matrix\Async; 20use GuzzleHttp\Psr7\HttpFactory; 21use chillerlan\OAuth\OAuthOptions; 22use chillerlan\OAuth\Storage\SessionStorage; 23use DateTimeImmutable; 24use Lcobucci\JWT\Token\Builder; 25use Lcobucci\JWT\Encoding\ChainedFormatter; 26use Lcobucci\JWT\Encoding\JoseEncoder; 27use Lcobucci\JWT\Signer\Key\InMemory; 28use Lcobucci\JWT\Signer\Ecdsa\Sha256; 29use Smallnest\Bsky\BskyProvider; 30 31$bskyToucher = new BskyToucher(); 32 33$favoriteFeeds = array_map(function ($feed) use ($bskyToucher) { 34 return $bskyToucher->getFeedInfo($feed); 35}, FAVORITE_FEEDS); 36 37function getPostOgImage(object $post): ?string { 38 if (!property_exists($post, 'embedType') || !$post->embedType) return null; 39 40 if ($post->embedType === 'app.bsky.embed.images') { 41 return $post->embeds[0]->imgUrl; 42 } else if ($post->embedType === 'app.bsky.embed.external' || $post->embedType === 'app.bsky.embed.video') { 43 return $post->embeds[0]->thumb; 44 } else if ($post->embedType === 'app.bsky.embed.record') { 45 return getPostOgImage($post->embeds[0]->post); 46 } 47 return null; 48} 49 50/*Debugger::enable(); 51// This where errors and exceptions will be logged. Make sure this directory exists and is writable. 52Debugger::$logDirectory = __DIR__ . '/../log/'; 53//Debugger::$strictMode = true; // display all errors 54// Debugger::$strictMode = E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED; // all errors except deprecated notices 55if (Debugger::$showBar) { 56 // This is specific to the Tracy Extension for Flight if you've included that 57 // otherwise comment this out. 58 //new TracyExtensionLoader($app); 59}*/ 60 61Flight::set('frontpageFeed', FRONTPAGE_FEED); 62Flight::set('slingshotInstance', SLINGSHOT_INSTANCE); 63Flight::set('constellationInstance', CONSTELLATION_INSTANCE); 64Flight::set('plcDirectory', PLC_DIRECTORY); 65Flight::set('defaultPds', DEFAULT_PDS); 66Flight::set('publicApi', PUBLIC_API); 67Flight::set('frontpageFeed', FRONTPAGE_FEED); 68Flight::set('defaultRelay', DEFAULT_RELAY); 69Flight::set('userAuth', $_SESSION['sbs_'.SITE_DOMAIN]); 70Flight::set('flight.log_errors', false); 71Flight::set('flight.handle_errors', false); 72Flight::set('flight.content_length', false); 73 74Flight::set('standardParams', [ 75 'siteTitle' => SITE_TITLE, 76 'themes' => THEMES, 77 'fonts' => FONTS, 78 'setTheme' => array_key_exists('sbs_theme', $_COOKIE) ? $_COOKIE['sbs_theme'] : DEFAULT_THEME, 79 'setFont' => array_key_exists('sbs_font', $_COOKIE) ? $_COOKIE['sbs_font'] : DEFAULT_FONT, 80 'userAuth' => Flight::get('userAuth'), 81 'favFeeds' => $favoriteFeeds, 82 'pages' => PAGES, 83 'links' => LINKS, 84 'ogdomain' => 'https://'.SITE_DOMAIN 85]); 86 87Flight::route('/', function () { 88 $bskyToucher = new BskyToucher(); 89 $posts = $bskyToucher->getFeed(Flight::get('frontpageFeed')); 90 $feedInfo = $bskyToucher->getFeedInfo(Flight::get('frontpageFeed')); 91 $latte = new Latte\Engine; 92 $latte->render('./templates/home.latte', array_merge(Flight::get('standardParams'), [ 93 'mainClass' => 'home feed', 94 'feedInfo' => $feedInfo, 95 'posts' => array_map(function ($p) { return $p->post; }, $posts->feed), 96 'cursor' => $posts->cursor, 97 'feedAtUri' => FRONTPAGE_FEED, 98 'ogtitle' => SITE_TITLE, 99 'ogdesc' => SITE_DESC, 100 'ogimage' => '', 101 'ogurl' => 'https://'.SITE_DOMAIN.'/' 102 ])); 103}); 104 105Flight::route('/u/@handle:[a-z0-9\.]+/@rkey:[a-z0-9]+', function (string $handle, string $rkey): void { 106 $bskyToucher = new BskyToucher(); 107 $did = $bskyToucher->resolveHandle($handle); 108 $userInfo = $bskyToucher->getUserInfo($handle); 109 $atUri = 'at://'.$did.'/app.bsky.feed.post/'.$rkey; 110 $latte = new Latte\Engine; 111 $latte->render('./templates/single.latte', array_merge(Flight::get('standardParams'), [ 112 'mainClass' => 'post', 113 'post' => $atUri, 114 'displayName' => $userInfo->displayName, 115 'handle' => $handle, 116 'ogtitle' => SITE_TITLE." | ".$userInfo->displayName." (@".$handle.")", 117 'ogdesc' => '', //$post->content, 118 'ogimage' => '', //getPostOgImage($post), 119 'ogurl' => 'https://'.SITE_DOMAIN.'/u/'.$handle.'/'.$rkey 120 ])); 121}); 122 123Flight::route('/u/@handle:[a-z0-9\.]+(/@tab:[a-z]+)', function (string $handle, ?string $tab): void { 124 $bskyToucher = new BskyToucher(); 125 $user = $bskyToucher->getUserInfo($handle, 'handle'); 126 $posts = $bskyToucher->getUserPosts($user->did); 127 $latte = new Latte\Engine; 128 $latte->render('./templates/profile.latte', array_merge(Flight::get('standardParams'), [ 129 'mainClass' => 'profile', 130 'handle' => $handle, 131 'posts' => array_map(function ($p) { return $p->uri; }, $posts->records), 132 'cursor' => $posts->cursor, 133 'user' => $user, 134 'ogtitle' => SITE_TITLE." | ".$user->displayName." (@".$user->handle.")", 135 'ogdesc' => $user->description, 136 'ogimage' => $user->avatar, 137 'ogurl' => 'https://'.SITE_DOMAIN.'/u/'.$user->handle.'/' 138 ])); 139}); 140 141Flight::route('/f/@did:did:plc:[0-9a-z]+/@name:[a-z0-9\-\_]+', function (string $did, string $name): void { 142 $bskyToucher = new BskyToucher(); 143 $feedUrl = "at://".$did."/app.bsky.feed.generator/".$name; 144 $feedInfo = $bskyToucher->getFeedInfo($feedUrl); 145 $creatorInfo = $bskyToucher->getUserInfo($feedInfo->creatorDid, 'did'); 146 $posts = $bskyToucher->getFeed($feedUrl); 147 $latte = new Latte\Engine; 148 $latte->render('./templates/feed.latte', array_merge(Flight::get('standardParams'), [ 149 'mainClass' => 'feed', 150 'posts' => array_map(function ($p) { return $p->post; }, $posts->feed), 151 'cursor' => '', 152 'feedName' => $feedInfo->title, 153 'feedAvatar' => $feedInfo->avatar, 154 'feedDescription' => $feedInfo->description, 155 'feedAtUri' => $feedUrl, 156 'feedAuthorName' => $creatorInfo->displayName, 157 'feedAuthorHandle' => $creatorInfo->handle, 158 'feedAuthorDid' => $creatorInfo->did, 159 'feedAuthorPds' => $creatorInfo->pds, 160 'ogtitle' => SITE_TITLE." | ".$feedInfo->title, 161 'ogdesc' => $feedInfo->description, 162 'ogimage' => $feedInfo->avatar, 163 'ogurl' => 'https://'.SITE_DOMAIN.'/f/'.$did.'/'.$name 164 ])); 165}); 166 167Flight::route('/s', function (): void { 168 $latte = new Latte\Engine; 169 $latte->render('./templates/search.latte', array_merge(Flight::get('standardParams'), [ 170 'mainClass' => 'search', 171 'params' => $_GET, 172 'ogtitle' => SITE_TITLE." | search".(array_key_exists('s', $_GET) ? ': '.$_GET['s'] : ''), 173 'ogdesc' => SITE_DESC, 174 'ogimage' => '', 175 'ogurl' => 'https://'.SITE_DOMAIN.'/u/'.$handle.'/'.$rkey 176 ])); 177}); 178 179Flight::route('/login', function(): void { 180 if (!array_key_exists('username', $_GET)) { 181 $latte = new Latte\Engine; 182 $latte->render('./templates/login.latte', array_merge(Flight::get('standardParams'), [ 183 'mainClass' => 'form', 184 'ogtitle' => SITE_TITLE." | login", 185 'ogdesc' => SITE_DESC, 186 'ogimage' => '', 187 'ogurl' => 'https://'.SITE_DOMAIN.'/login' 188 ])); 189 die(1); 190 } 191 $username = $_GET['username']; 192 $bskyToucher = new BskyToucher(); 193 $userInfo = $bskyToucher->getUserInfo($username); 194 if (!$userInfo) die(1); 195 $pds = $userInfo->pds; 196 $options = new OAuthOptions([ 197 'key' => 'https://'.SITE_DOMAIN.CLIENT_ID, 198 'secret' => CLIENT_SECRET, 199 'callbackURL' => 'https://'.SITE_DOMAIN.'/login', 200 'sessionStart' => true, 201 'sessionStorageVar' => 'sbs_'.SITE_DOMAIN 202 ]); 203 $connector = new React\Socket\Connector([ 204 'dns' => '1.1.1.1' 205 ]); 206 $http = new React\Http\Browser($connector); 207 $httpFactory = new HttpFactory(); 208 $token_builder = Builder::new(new JoseEncoder(), ChainedFormatter::default()); 209 $algorithm = new Sha256(); 210 $signing_key = InMemory::file(CERT_PATH); 211 $now = new DateTimeImmutable(); 212 $token = $token_builder 213 ->withHeader('alg', 'ES256') 214 ->withHeader('typ', 'JWT') 215 ->withHeader('kid', 'ocwgKj_O7H9at1sL6yWf9ZZ82NOM7D0xlN8HGIyWH6M') 216 ->issuedBy('https://'.SITE_DOMAIN.CLIENT_ID) 217 ->identifiedBy(uniqid()) 218 ->relatedTo('https://'.SITE_DOMAIN.CLIENT_ID) 219 ->permittedFor($pds) 220 ->issuedAt($now->modify('-5 seconds')) 221 ->getToken($algorithm, $signing_key); 222 $client = new GuzzleHttp\Client([ 223 'verify' => true, 224 'headers' => [ 225 'User-Agent' => USER_AGENT_STR, 226 'Authorization' => 'Bearer: '.$token->toString() 227 ] 228 ]); 229 $provider = new BskyProvider($options, $client, $httpFactory, $httpFactory, $httpFactory); 230 $provider->setPds($pds); 231 $name = $provider->getName(); 232 233 if (isset($_GET['login']) && $_GET['login'] === $name) { 234 $auth_url = $provider->getAuthorizationUrl([ 235 'client_assertion_type' => 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', 236 'client_assertion' => $token->toString() 237 ]); 238 header('Location: '.$auth_url); 239 die(1); 240 } else if (isset($_GET['code'], $_GET['state'])) { 241 $token = $provider->getAccessToken($_GET['code'], $_GET['state']); 242 $storage = new SessionStorage($options); 243 $storage->storeAccessToken($token, $name); 244 header('Location: ?granted='.$name); 245 die(1); 246 } else if (isset($_GET['granted']) && $_GET['granted'] === $name) { 247 $tokenJSON = $provider->getAccessTokenFromStorage()->toJSON(); 248 header('Location: https://'.SITE_DOMAIN); 249 die(1); 250 } else if (isset($_GET['error'])) { 251 die(1); 252 } 253 $latte = new Latte\Engine; 254 $latte->render('./templates/login.latte', array_merge(Flight::get('standardParams'), [ 255 'mainClass' => 'form', 256 'ogtitle' => SITE_TITLE." | login", 257 'ogdesc' => SITE_DESC, 258 'ogimage' => '', 259 'ogurl' => 'https://'.SITE_DOMAIN.'/login' 260 ])); 261}); 262 263Flight::route('/createaccount', function(): void { 264 $latte = new Latte\Engine; 265 $latte->render('./templates/create.latte', array_merge(Flight::get('standardParams'), [ 266 'mainClass' => 'form', 267 'ogtitle' => SITE_TITLE." | create account", 268 'ogdesc' => SITE_DESC, 269 'ogimage' => '', 270 'ogurl' => SITE_DOMAIN.'/createaccount' 271 ])); 272}); 273 274Flight::route('/api/post/@did/@rkey', function (string $did, string $rkey): void { 275 $bskyToucher = new BskyToucher(); 276 $ret = $bskyToucher->getPost($did, $rkey, true); 277 if ($ret) { 278 $latte = new Latte\Engine; 279 $latte->render('./templates/_partials/post.latte', array_merge(Flight::get('standardParams'), [ 280 'post' => $ret 281 ])); 282 die(1); 283 } 284 Flight::json(['error' => 'malformed response or bad vibes']); 285}); 286 287Flight::route('/api/likes/@did/@rkey', function (string $did, string $rkey): void { 288 $bskyToucher = new BskyToucher(); 289 $ret = $bskyToucher->getLikes('at://'.$did.'/app.bsky.feed.post/'.$rkey); 290 if ($ret) { 291 $latte = new Latte\Engine; 292 $output = $latte->renderToString('./templates/_partials/interaction_list.latte', array_merge(Flight::get('standardParams'), [ 293 'interactions' => $ret->records 294 ])); 295 $ret->rendered = $output; 296 Flight::json($ret); 297 } 298}); 299 300Flight::route('/api/reposts/@did/@rkey', function (string $did, string $rkey): void { 301 $bskyToucher = new BskyToucher(); 302 $ret = $bskyToucher->getReposts('at://'.$did.'/app.bsky.feed.post/'.$rkey); 303 if ($ret) { 304 $latte = new Latte\Engine; 305 $output = $latte->renderToString('./templates/_partials/interaction_list.latte', array_merge(Flight::get('standardParams'), [ 306 'interactions' => $ret->records 307 ])); 308 $ret->rendered = $output; 309 Flight::json($ret); 310 } 311}); 312 313Flight::route('/api/replies/@did/@rkey', function (string $did, string $rkey): void { 314 $bskyToucher = new BskyToucher(); 315 $ret = $bskyToucher->getReplies('at://'.$did.'/app.bsky.feed.post/'.$rkey); 316 if ($ret) { 317 $latte = new Latte\Engine; 318 $output = $latte->renderToString('./templates/_partials/feedPosts.latte', array_merge(Flight::get('standardParams'), [ 319 'total' => $ret->total, 320 'cursor' => $ret->cursor, 321 'posts' => array_map(function($p) { return $p->uri; }, $ret->records) 322 ])); 323 $ret->rendered = $output; 324 Flight::json($ret); 325 } 326}); 327 328Flight::route('/api/quotes/@did/@rkey', function (string $did, string $rkey): void { 329 $bskyToucher = new BskyToucher(); 330 $ret = $bskyToucher->getLikes('at://'.$did.'/app.bsky.feed.post/'.$rkey); 331 if ($ret) { 332 $latte = new Latte\Engine; 333 $output = $latte->renderToString('./templates/_partials/interaction_list.latte', array_merge(Flight::get('standardParams'), [ 334 'interactions' => $ret->records 335 ])); 336 $ret->rendered = $output; 337 Flight::json($ret); 338 } 339}); 340 341Flight::route('/api/user/@handle', function (string $handle): void { 342 $bskyToucher = new BskyToucher(); 343 $ret = $bskyToucher->getUserInfo($handle); 344 if ($ret) { 345 Flight::json($ret); 346 die(1); 347 } 348 Flight::json(['error' => 'malformed response or bad vibes']); 349}); 350 351Flight::route('/api/feed/@did/@rkey', function (string $did, string $rkey): void { 352 $bskyToucher = new BskyToucher(); 353}); 354 355Flight::route('/@page', function (string $page): void { 356 $latte = new Latte\Engine; 357 $converter = new CommonMarkConverter(); 358 $md = $converter->convert(file_get_contents('./pages/'.$page.'.md')); 359 $latte->render('./templates/page.latte', array_merge(Flight::get('standardParams'), [ 360 'mainClass' => 'page', 361 'content' => $md, 362 'ogtitle' => SITE_TITLE." | ".$page, 363 'ogdesc' => SITE_DESC, 364 'ogimage' => '', 365 'ogurl' => SITE_DOMAIN.'/'.$page 366 ])); 367}); 368 369Flight::start(); 370 371?>