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