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