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