PublicApiController.php 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883
  1. <?php
  2. namespace App\Http\Controllers;
  3. use App\Follower;
  4. use App\Profile;
  5. use App\Services\AccountService;
  6. use App\Services\BookmarkService;
  7. use App\Services\FollowerService;
  8. use App\Services\InstanceService;
  9. use App\Services\LikeService;
  10. use App\Services\NetworkTimelineService;
  11. use App\Services\PublicTimelineService;
  12. use App\Services\ReblogService;
  13. use App\Services\RelationshipService;
  14. use App\Services\SnowflakeService;
  15. use App\Services\StatusService;
  16. use App\Services\UserFilterService;
  17. use App\Status;
  18. use App\Transformer\Api\StatusStatelessTransformer;
  19. use Auth;
  20. use Cache;
  21. use Illuminate\Http\Request;
  22. use League\Fractal;
  23. use League\Fractal\Pagination\IlluminatePaginatorAdapter;
  24. use League\Fractal\Serializer\ArraySerializer;
  25. class PublicApiController extends Controller
  26. {
  27. protected $fractal;
  28. public function __construct()
  29. {
  30. $this->fractal = new Fractal\Manager;
  31. $this->fractal->setSerializer(new ArraySerializer);
  32. }
  33. public function json($res, $code = 200, $headers = [])
  34. {
  35. return response()->json($res, $code, $headers, JSON_UNESCAPED_SLASHES);
  36. }
  37. protected function getUserData($user)
  38. {
  39. if (! $user) {
  40. return [];
  41. } else {
  42. return AccountService::get($user->profile_id);
  43. }
  44. }
  45. public function getStatus(Request $request, $id)
  46. {
  47. abort_if(! $request->user(), 403);
  48. $status = StatusService::get($id, false);
  49. abort_if(! $status, 404);
  50. if (in_array($status['visibility'], ['public', 'unlisted'])) {
  51. return $status;
  52. }
  53. $pid = $request->user()->profile_id;
  54. if ($status['account']['id'] == $pid) {
  55. return $status;
  56. }
  57. if ($status['visibility'] == 'private') {
  58. if (FollowerService::follows($pid, $status['account']['id'])) {
  59. return $status;
  60. }
  61. }
  62. abort(404);
  63. }
  64. public function status(Request $request, $username, int $postid)
  65. {
  66. $profile = Profile::whereUsername($username)->whereNull('status')->firstOrFail();
  67. $status = Status::whereProfileId($profile->id)->findOrFail($postid);
  68. $this->scopeCheck($profile, $status);
  69. if (! $request->user()) {
  70. $cached = StatusService::get($status->id, false);
  71. abort_if(! in_array($cached['visibility'], ['public', 'unlisted']), 403);
  72. $res = ['status' => $cached];
  73. } else {
  74. $item = new Fractal\Resource\Item($status, new StatusStatelessTransformer);
  75. $res = [
  76. 'status' => $this->fractal->createData($item)->toArray(),
  77. ];
  78. }
  79. return response()->json($res);
  80. }
  81. public function statusState(Request $request, $username, int $postid)
  82. {
  83. $profile = Profile::whereUsername($username)->whereNull('status')->firstOrFail();
  84. $status = Status::whereProfileId($profile->id)->findOrFail($postid);
  85. $this->scopeCheck($profile, $status);
  86. if (! Auth::check()) {
  87. $res = [
  88. 'user' => [],
  89. 'likes' => [],
  90. 'shares' => [],
  91. 'reactions' => [
  92. 'liked' => false,
  93. 'shared' => false,
  94. 'bookmarked' => false,
  95. ],
  96. ];
  97. return response()->json($res);
  98. }
  99. $res = [
  100. 'user' => $this->getUserData($request->user()),
  101. 'likes' => [],
  102. 'shares' => [],
  103. 'reactions' => [
  104. 'liked' => (bool) $status->liked(),
  105. 'shared' => (bool) $status->shared(),
  106. 'bookmarked' => (bool) $status->bookmarked(),
  107. ],
  108. ];
  109. return response()->json($res);
  110. }
  111. public function statusComments(Request $request, $username, int $postId)
  112. {
  113. $this->validate($request, [
  114. 'min_id' => 'nullable|integer|min:1',
  115. 'max_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX,
  116. 'limit' => 'nullable|integer|min:5|max:50',
  117. ]);
  118. $limit = $request->limit ?? 10;
  119. $profile = Profile::whereNull('status')->findOrFail($username);
  120. $status = Status::whereProfileId($profile->id)->whereCommentsDisabled(false)->findOrFail($postId);
  121. $this->scopeCheck($profile, $status);
  122. if (Auth::check()) {
  123. $p = Auth::user()->profile;
  124. $scope = $p->id == $status->profile_id || FollowerService::follows($p->id, $profile->id) ? ['public', 'private', 'unlisted'] : ['public', 'unlisted'];
  125. } else {
  126. $scope = ['public', 'unlisted'];
  127. }
  128. if ($request->filled('min_id') || $request->filled('max_id')) {
  129. if ($request->filled('min_id')) {
  130. $replies = $status->comments()
  131. ->whereNull('reblog_of_id')
  132. ->whereIn('scope', $scope)
  133. ->select('id', 'caption', 'local', 'visibility', 'scope', 'is_nsfw', 'profile_id', 'in_reply_to_id', 'type', 'reply_count', 'created_at')
  134. ->where('id', '>=', $request->min_id)
  135. ->orderBy('id', 'desc')
  136. ->paginate($limit);
  137. }
  138. if ($request->filled('max_id')) {
  139. $replies = $status->comments()
  140. ->whereNull('reblog_of_id')
  141. ->whereIn('scope', $scope)
  142. ->select('id', 'caption', 'local', 'visibility', 'scope', 'is_nsfw', 'profile_id', 'in_reply_to_id', 'type', 'reply_count', 'created_at')
  143. ->where('id', '<=', $request->max_id)
  144. ->orderBy('id', 'desc')
  145. ->paginate($limit);
  146. }
  147. } else {
  148. $replies = Status::whereInReplyToId($status->id)
  149. ->whereNull('reblog_of_id')
  150. ->whereIn('scope', $scope)
  151. ->select('id', 'caption', 'local', 'visibility', 'scope', 'is_nsfw', 'profile_id', 'in_reply_to_id', 'type', 'reply_count', 'created_at')
  152. ->orderBy('id', 'desc')
  153. ->paginate($limit);
  154. }
  155. $resource = new Fractal\Resource\Collection($replies, new StatusStatelessTransformer, 'data');
  156. $resource->setPaginator(new IlluminatePaginatorAdapter($replies));
  157. $res = $this->fractal->createData($resource)->toArray();
  158. return response()->json($res, 200, [], JSON_PRETTY_PRINT);
  159. }
  160. protected function scopeCheck(Profile $profile, Status $status)
  161. {
  162. if ($profile->is_private == true && Auth::check() == false) {
  163. abort(404);
  164. }
  165. switch ($status->scope) {
  166. case 'public':
  167. case 'unlisted':
  168. break;
  169. case 'private':
  170. $user = Auth::check() ? Auth::user() : false;
  171. if (! $user) {
  172. abort(403);
  173. } else {
  174. $follows = FollowerService::follows($profile->id, $user->profile_id);
  175. if ($follows == false && $profile->id !== $user->profile_id && $user->is_admin == false) {
  176. abort(404);
  177. }
  178. }
  179. break;
  180. case 'direct':
  181. abort(404);
  182. break;
  183. case 'draft':
  184. abort(404);
  185. break;
  186. default:
  187. abort(404);
  188. break;
  189. }
  190. }
  191. public function publicTimelineApi(Request $request)
  192. {
  193. $this->validate($request, [
  194. 'page' => 'nullable|integer|max:40',
  195. 'min_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX,
  196. 'max_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX,
  197. 'limit' => 'nullable|integer|max:30',
  198. ]);
  199. if (! $request->user()) {
  200. return response('', 403);
  201. }
  202. $page = $request->input('page');
  203. $min = $request->input('min_id');
  204. $max = $request->input('max_id');
  205. $limit = $request->input('limit') ?? 3;
  206. $user = $request->user();
  207. $filtered = $user ? UserFilterService::filters($user->profile_id) : [];
  208. $hideNsfw = config('instance.hide_nsfw_on_public_feeds');
  209. if (config('exp.cached_public_timeline') == false) {
  210. if ($min || $max) {
  211. $dir = $min ? '>' : '<';
  212. $id = $min ?? $max;
  213. $timeline = Status::select(
  214. 'id',
  215. 'profile_id',
  216. 'type',
  217. 'scope',
  218. 'local'
  219. )
  220. ->where('id', $dir, $id)
  221. ->whereNull(['in_reply_to_id', 'reblog_of_id'])
  222. ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
  223. ->whereLocal(true)
  224. ->when($hideNsfw, function ($q, $hideNsfw) {
  225. return $q->where('is_nsfw', false);
  226. })
  227. ->whereScope('public')
  228. ->orderBy('id', 'desc')
  229. ->limit($limit)
  230. ->get()
  231. ->map(function ($s) use ($user) {
  232. $status = StatusService::getFull($s->id, $user->profile_id);
  233. if (! $status) {
  234. return false;
  235. }
  236. $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id);
  237. $status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $s->id);
  238. $status['reblogged'] = (bool) ReblogService::get($user->profile_id, $s->id);
  239. return $status;
  240. })
  241. ->filter(function ($s) use ($filtered) {
  242. return $s && isset($s['account']) && in_array($s['account']['id'], $filtered) == false;
  243. })
  244. ->values();
  245. $res = $timeline->toArray();
  246. } else {
  247. $timeline = Status::select(
  248. 'id',
  249. 'uri',
  250. 'caption',
  251. 'profile_id',
  252. 'type',
  253. 'in_reply_to_id',
  254. 'reblog_of_id',
  255. 'is_nsfw',
  256. 'scope',
  257. 'local',
  258. 'reply_count',
  259. 'comments_disabled',
  260. 'created_at',
  261. 'place_id',
  262. 'likes_count',
  263. 'reblogs_count',
  264. 'updated_at'
  265. )
  266. ->whereNull(['in_reply_to_id', 'reblog_of_id'])
  267. ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
  268. ->whereLocal(true)
  269. ->when($hideNsfw, function ($q, $hideNsfw) {
  270. return $q->where('is_nsfw', false);
  271. })
  272. ->whereScope('public')
  273. ->orderBy('id', 'desc')
  274. ->limit($limit)
  275. ->get()
  276. ->map(function ($s) use ($user) {
  277. $status = StatusService::getFull($s->id, $user->profile_id);
  278. if (! $status) {
  279. return false;
  280. }
  281. $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id);
  282. $status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $s->id);
  283. $status['reblogged'] = (bool) ReblogService::get($user->profile_id, $s->id);
  284. return $status;
  285. })
  286. ->filter(function ($s) use ($filtered) {
  287. return $s && isset($s['account']) && in_array($s['account']['id'], $filtered) == false;
  288. })
  289. ->values();
  290. $res = $timeline->toArray();
  291. }
  292. } else {
  293. Cache::remember('api:v1:timelines:public:cache_check', 10368000, function () {
  294. if (PublicTimelineService::count() == 0) {
  295. PublicTimelineService::warmCache(true, 400);
  296. }
  297. });
  298. if ($max) {
  299. $feed = PublicTimelineService::getRankedMaxId($max, $limit);
  300. } elseif ($min) {
  301. $feed = PublicTimelineService::getRankedMinId($min, $limit);
  302. } else {
  303. $feed = PublicTimelineService::get(0, $limit);
  304. }
  305. $res = collect($feed)
  306. ->take($limit)
  307. ->map(function ($k) use ($user) {
  308. $status = StatusService::get($k);
  309. if ($status && isset($status['account']) && $user) {
  310. $status['favourited'] = (bool) LikeService::liked($user->profile_id, $k);
  311. $status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $k);
  312. $status['reblogged'] = (bool) ReblogService::get($user->profile_id, $k);
  313. $status['relationship'] = RelationshipService::get($user->profile_id, $status['account']['id']);
  314. }
  315. return $status;
  316. })
  317. ->filter(function ($s) use ($filtered) {
  318. return $s && isset($s['account']) && in_array($s['account']['id'], $filtered) == false;
  319. })
  320. ->values()
  321. ->toArray();
  322. }
  323. return response()->json($res);
  324. }
  325. public function homeTimelineApi(Request $request)
  326. {
  327. if (! $request->user()) {
  328. return response('', 403);
  329. }
  330. $this->validate($request, [
  331. 'page' => 'nullable|integer|max:40',
  332. 'min_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX,
  333. 'max_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX,
  334. 'limit' => 'nullable|integer|max:40',
  335. 'recent_feed' => 'nullable',
  336. 'recent_min' => 'nullable|integer',
  337. ]);
  338. $recentFeed = $request->input('recent_feed') == 'true';
  339. $recentFeedMin = $request->input('recent_min');
  340. $page = $request->input('page');
  341. $min = $request->input('min_id');
  342. $max = $request->input('max_id');
  343. $limit = $request->input('limit') ?? 3;
  344. $user = $request->user();
  345. $key = 'user:last_active_at:id:'.$user->id;
  346. if (Cache::get($key) == null) {
  347. $user->last_active_at = now();
  348. $user->save();
  349. Cache::put($key, true, 43200);
  350. }
  351. $pid = $user->profile_id;
  352. $following = Cache::remember('profile:following:'.$pid, 1209600, function () use ($pid) {
  353. $following = Follower::whereProfileId($pid)->pluck('following_id');
  354. return $following->push($pid)->toArray();
  355. });
  356. $filtered = $user ? UserFilterService::filters($user->profile_id) : [];
  357. $types = ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'];
  358. // $types = ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album', 'text'];
  359. $textOnlyReplies = false;
  360. if ($min || $max) {
  361. $dir = $min ? '>' : '<';
  362. $id = $min ?? $max;
  363. return Status::select(
  364. 'id',
  365. 'uri',
  366. 'caption',
  367. 'profile_id',
  368. 'type',
  369. 'in_reply_to_id',
  370. 'reblog_of_id',
  371. 'is_nsfw',
  372. 'scope',
  373. 'local',
  374. 'reply_count',
  375. 'comments_disabled',
  376. 'place_id',
  377. 'likes_count',
  378. 'reblogs_count',
  379. 'created_at',
  380. 'updated_at'
  381. )
  382. ->whereIn('type', $types)
  383. ->when(! $textOnlyReplies, function ($q, $textOnlyReplies) {
  384. return $q->whereNull('in_reply_to_id');
  385. })
  386. ->where('id', $dir, $id)
  387. ->whereIn('profile_id', $following)
  388. ->whereIn('visibility', ['public', 'unlisted', 'private'])
  389. ->orderBy('created_at', 'desc')
  390. ->limit($limit)
  391. ->get()
  392. ->map(function ($s) use ($user) {
  393. try {
  394. $status = StatusService::get($s->id, false);
  395. if (! $status) {
  396. return false;
  397. }
  398. } catch (\Exception $e) {
  399. return false;
  400. }
  401. $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id);
  402. $status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $s->id);
  403. $status['reblogged'] = (bool) ReblogService::get($user->profile_id, $s->id);
  404. return $status;
  405. })
  406. ->filter(function ($s) use ($filtered) {
  407. return $s && in_array($s['account']['id'], $filtered) == false;
  408. })
  409. ->values()
  410. ->toArray();
  411. } else {
  412. return Status::select(
  413. 'id',
  414. 'uri',
  415. 'caption',
  416. 'profile_id',
  417. 'type',
  418. 'in_reply_to_id',
  419. 'reblog_of_id',
  420. 'is_nsfw',
  421. 'scope',
  422. 'local',
  423. 'reply_count',
  424. 'comments_disabled',
  425. 'place_id',
  426. 'likes_count',
  427. 'reblogs_count',
  428. 'created_at',
  429. 'updated_at'
  430. )
  431. ->whereIn('type', $types)
  432. ->when(! $textOnlyReplies, function ($q, $textOnlyReplies) {
  433. return $q->whereNull('in_reply_to_id');
  434. })
  435. ->whereIn('profile_id', $following)
  436. ->whereIn('visibility', ['public', 'unlisted', 'private'])
  437. ->orderBy('created_at', 'desc')
  438. ->limit($limit)
  439. ->get()
  440. ->map(function ($s) use ($user) {
  441. try {
  442. $status = StatusService::get($s->id, false);
  443. if (! $status) {
  444. return false;
  445. }
  446. } catch (\Exception $e) {
  447. return false;
  448. }
  449. $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id);
  450. $status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $s->id);
  451. $status['reblogged'] = (bool) ReblogService::get($user->profile_id, $s->id);
  452. return $status;
  453. })
  454. ->filter(function ($s) use ($filtered) {
  455. return $s && in_array($s['account']['id'], $filtered) == false;
  456. })
  457. ->values()
  458. ->toArray();
  459. }
  460. }
  461. public function networkTimelineApi(Request $request)
  462. {
  463. if (! $request->user()) {
  464. return response('', 403);
  465. }
  466. abort_if(config('federation.network_timeline') == false, 404);
  467. $this->validate($request, [
  468. 'page' => 'nullable|integer|max:40',
  469. 'min_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX,
  470. 'max_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX,
  471. 'limit' => 'nullable|integer|max:30',
  472. ]);
  473. $page = $request->input('page');
  474. $min = $request->input('min_id');
  475. $max = $request->input('max_id');
  476. $limit = $request->input('limit') ?? 3;
  477. $user = $request->user();
  478. $amin = SnowflakeService::byDate(now()->subDays(config('federation.network_timeline_days_falloff')));
  479. $filtered = $user ? UserFilterService::filters($user->profile_id) : [];
  480. $hideNsfw = config('instance.hide_nsfw_on_public_feeds');
  481. if (config('instance.timeline.network.cached') == false) {
  482. if ($min || $max) {
  483. $dir = $min ? '>' : '<';
  484. $id = $min ?? $max;
  485. $timeline = Status::select(
  486. 'id',
  487. 'uri',
  488. 'type',
  489. 'scope',
  490. 'created_at',
  491. )
  492. ->where('id', $dir, $id)
  493. ->when($hideNsfw, function ($q, $hideNsfw) {
  494. return $q->where('is_nsfw', false);
  495. })
  496. ->whereNull(['in_reply_to_id', 'reblog_of_id'])
  497. ->whereNotIn('profile_id', $filtered)
  498. ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
  499. ->whereNotNull('uri')
  500. ->whereScope('public')
  501. ->where('id', '>', $amin)
  502. ->orderBy('created_at', 'desc')
  503. ->limit($limit)
  504. ->get()
  505. ->map(function ($s) use ($user) {
  506. $status = StatusService::get($s->id);
  507. $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id);
  508. $status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $s->id);
  509. $status['reblogged'] = (bool) ReblogService::get($user->profile_id, $s->id);
  510. return $status;
  511. });
  512. $res = $timeline->toArray();
  513. } else {
  514. $timeline = Status::select(
  515. 'id',
  516. 'uri',
  517. 'type',
  518. 'scope',
  519. 'created_at',
  520. )
  521. ->whereNull(['in_reply_to_id', 'reblog_of_id'])
  522. ->whereNotIn('profile_id', $filtered)
  523. ->when($hideNsfw, function ($q, $hideNsfw) {
  524. return $q->where('is_nsfw', false);
  525. })
  526. ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
  527. ->whereNotNull('uri')
  528. ->whereScope('public')
  529. ->where('id', '>', $amin)
  530. ->orderBy('created_at', 'desc')
  531. ->limit($limit)
  532. ->get()
  533. ->map(function ($s) use ($user) {
  534. $status = StatusService::get($s->id);
  535. $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id);
  536. $status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $s->id);
  537. $status['reblogged'] = (bool) ReblogService::get($user->profile_id, $s->id);
  538. return $status;
  539. });
  540. $res = $timeline->toArray();
  541. }
  542. } else {
  543. Cache::remember('api:v1:timelines:network:cache_check', 10368000, function () {
  544. if (NetworkTimelineService::count() == 0) {
  545. NetworkTimelineService::warmCache(true, 400);
  546. }
  547. });
  548. if ($max) {
  549. $feed = NetworkTimelineService::getRankedMaxId($max, $limit);
  550. } elseif ($min) {
  551. $feed = NetworkTimelineService::getRankedMinId($min, $limit);
  552. } else {
  553. $feed = NetworkTimelineService::get(0, $limit);
  554. }
  555. $res = collect($feed)
  556. ->take($limit)
  557. ->map(function ($k) use ($user) {
  558. $status = StatusService::get($k);
  559. if ($status && isset($status['account']) && $user) {
  560. $status['favourited'] = (bool) LikeService::liked($user->profile_id, $k);
  561. $status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $k);
  562. $status['reblogged'] = (bool) ReblogService::get($user->profile_id, $k);
  563. $status['relationship'] = RelationshipService::get($user->profile_id, $status['account']['id']);
  564. }
  565. return $status;
  566. })
  567. ->filter(function ($s) use ($filtered) {
  568. return $s && isset($s['account']) && in_array($s['account']['id'], $filtered) == false;
  569. })
  570. ->values()
  571. ->toArray();
  572. }
  573. return response()->json($res);
  574. }
  575. public function relationships(Request $request)
  576. {
  577. if (! Auth::check()) {
  578. return response()->json([]);
  579. }
  580. $pid = $request->user()->profile_id;
  581. $this->validate($request, [
  582. 'id' => 'required|array|min:1|max:20',
  583. 'id.*' => 'required|integer',
  584. ]);
  585. $ids = collect($request->input('id'));
  586. $res = $ids->filter(function ($v) use ($pid) {
  587. return $v != $pid;
  588. })
  589. ->map(function ($id) use ($pid) {
  590. return RelationshipService::get($pid, $id);
  591. });
  592. return response()->json($res);
  593. }
  594. public function account(Request $request, $id)
  595. {
  596. $res = AccountService::get($id);
  597. if ($res && isset($res['local'], $res['url']) && ! $res['local']) {
  598. $domain = parse_url($res['url'], PHP_URL_HOST);
  599. abort_if(in_array($domain, InstanceService::getBannedDomains()), 404);
  600. }
  601. return response()->json($res);
  602. }
  603. public function accountStatuses(Request $request, $id)
  604. {
  605. $this->validate($request, [
  606. 'only_media' => 'nullable',
  607. 'pinned' => 'nullable',
  608. 'exclude_replies' => 'nullable',
  609. 'limit' => 'nullable|integer|min:1|max:24',
  610. 'cursor' => 'nullable',
  611. ]);
  612. $user = $request->user();
  613. $profile = AccountService::get($id);
  614. abort_if(! $profile, 404);
  615. if ($profile && isset($profile['local'], $profile['url']) && ! $profile['local']) {
  616. $domain = parse_url($profile['url'], PHP_URL_HOST);
  617. abort_if(in_array($domain, InstanceService::getBannedDomains()), 404);
  618. }
  619. $limit = $request->limit ?? 9;
  620. $scope = ['photo', 'photo:album', 'video', 'video:album'];
  621. $onlyMedia = $request->input('only_media', true);
  622. $pinned = $request->filled('pinned') && $request->boolean('pinned') == true;
  623. $hasCursor = $request->filled('cursor');
  624. $visibility = $this->determineVisibility($profile, $user);
  625. if (empty($visibility)) {
  626. return response()->json([]);
  627. }
  628. $result = collect();
  629. $remainingLimit = $limit;
  630. if ($pinned && ! $hasCursor) {
  631. $pinnedStatuses = Status::whereProfileId($profile['id'])
  632. ->whereNotNull('pinned_order')
  633. ->orderBy('pinned_order')
  634. ->get();
  635. $pinnedResult = $this->processStatuses($pinnedStatuses, $user, $onlyMedia);
  636. $result = $pinnedResult;
  637. $remainingLimit = max(1, $limit - $pinnedResult->count());
  638. }
  639. $paginator = Status::whereProfileId($profile['id'])
  640. ->whereNull('in_reply_to_id')
  641. ->whereNull('reblog_of_id')
  642. ->when($pinned, function ($query) {
  643. return $query->whereNull('pinned_order');
  644. })
  645. ->whereIn('type', $scope)
  646. ->whereIn('scope', $visibility)
  647. ->orderByDesc('id')
  648. ->cursorPaginate($remainingLimit)
  649. ->withQueryString();
  650. $headers = $this->generatePaginationHeaders($paginator);
  651. $regularStatuses = $this->processStatuses($paginator->items(), $user, $onlyMedia);
  652. $result = $result->concat($regularStatuses);
  653. return response()->json($result, 200, $headers);
  654. }
  655. /**
  656. * GET /api/pixelfed/v1/statuses/{id}/pin
  657. */
  658. public function statusPin(Request $request, $id)
  659. {
  660. abort_if(! $request->user(), 403);
  661. $user = $request->user();
  662. $status = Status::whereScope('public')->find($id);
  663. if (! $status) {
  664. return $this->json(['error' => 'Record not found'], 404);
  665. }
  666. if ($status->profile_id != $user->profile_id) {
  667. return $this->json(['error' => "Validation failed: Someone else's post cannot be pinned"], 422);
  668. }
  669. $res = StatusService::markPin($status->id);
  670. if (! $res['success']) {
  671. return $this->json([
  672. 'error' => $res['error'],
  673. ], 422);
  674. }
  675. $statusRes = StatusService::get($status->id, true, true);
  676. $status['pinned'] = true;
  677. return $this->json($statusRes);
  678. }
  679. /**
  680. * GET /api/pixelfed/v1/statuses/{id}/unpin
  681. */
  682. public function statusUnpin(Request $request, $id)
  683. {
  684. abort_if(! $request->user(), 403);
  685. $status = Status::whereScope('public')->findOrFail($id);
  686. $user = $request->user();
  687. if ($status->profile_id != $user->profile_id) {
  688. return $this->json(['error' => 'Record not found'], 404);
  689. }
  690. $res = StatusService::unmarkPin($status->id);
  691. if (! $res) {
  692. return $this->json($res, 422);
  693. }
  694. $status = StatusService::get($status->id, true, true);
  695. $status['pinned'] = false;
  696. return $this->json($status);
  697. }
  698. private function determineVisibility($profile, $user)
  699. {
  700. if (! $user || ! isset($user->profile_id)) {
  701. return [];
  702. }
  703. if (! $profile || ! isset($profile['id'])) {
  704. return [];
  705. }
  706. if ($profile['id'] == $user->profile_id) {
  707. return ['public', 'unlisted', 'private'];
  708. }
  709. if ($profile['locked']) {
  710. if (! $user) {
  711. return [];
  712. }
  713. $pid = $user->profile_id;
  714. $isFollowing = FollowerService::follows($pid, $profile['id']);
  715. return $isFollowing ? ['public', 'unlisted', 'private'] : ['public'];
  716. } else {
  717. if ($user) {
  718. $pid = $user->profile_id;
  719. $isFollowing = FollowerService::follows($pid, $profile['id']);
  720. return $isFollowing ? ['public', 'unlisted', 'private'] : ['public', 'unlisted'];
  721. } else {
  722. return ['public', 'unlisted'];
  723. }
  724. }
  725. }
  726. private function processStatuses($statuses, $user, $onlyMedia)
  727. {
  728. return collect($statuses)->map(function ($status) use ($user) {
  729. try {
  730. $mastodonStatus = StatusService::get($status->id, false);
  731. if (! $mastodonStatus) {
  732. return null;
  733. }
  734. if ($user) {
  735. $mastodonStatus['favourited'] = (bool) LikeService::liked($user->profile_id, $status->id);
  736. $mastodonStatus['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $status->id);
  737. $mastodonStatus['reblogged'] = (bool) StatusService::isShared($status->id, $user->profile_id);
  738. }
  739. return $mastodonStatus;
  740. } catch (\Exception $e) {
  741. return null;
  742. }
  743. })
  744. ->filter(function ($status) use ($onlyMedia) {
  745. if (! $status) {
  746. return false;
  747. }
  748. if ($onlyMedia) {
  749. return isset($status['media_attachments']) &&
  750. is_array($status['media_attachments']) &&
  751. ! empty($status['media_attachments']);
  752. }
  753. return true;
  754. })
  755. ->values();
  756. }
  757. /**
  758. * Generate pagination link headers from paginator
  759. */
  760. private function generatePaginationHeaders($paginator)
  761. {
  762. $link = null;
  763. if ($paginator->onFirstPage()) {
  764. if ($paginator->hasMorePages()) {
  765. $link = '<'.$paginator->nextPageUrl().'>; rel="prev"';
  766. }
  767. } else {
  768. if ($paginator->previousPageUrl()) {
  769. $link = '<'.$paginator->previousPageUrl().'>; rel="next"';
  770. }
  771. if ($paginator->hasMorePages()) {
  772. $link .= ($link ? ', ' : '').'<'.$paginator->nextPageUrl().'>; rel="prev"';
  773. }
  774. }
  775. return isset($link) ? ['Link' => $link] : [];
  776. }
  777. }