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