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