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