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