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?>