PublicApiController.php 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741
  1. <?php
  2. namespace App\Http\Controllers;
  3. use Illuminate\Http\Request;
  4. use App\{
  5. Hashtag,
  6. Follower,
  7. Like,
  8. Media,
  9. Notification,
  10. Profile,
  11. StatusHashtag,
  12. Status,
  13. StatusView,
  14. UserFilter
  15. };
  16. use Auth, Cache;
  17. use Illuminate\Support\Facades\Redis;
  18. use Carbon\Carbon;
  19. use League\Fractal;
  20. use App\Transformer\Api\{
  21. AccountTransformer,
  22. RelationshipTransformer,
  23. StatusTransformer,
  24. StatusStatelessTransformer
  25. };
  26. use App\Services\{
  27. AccountService,
  28. LikeService,
  29. PublicTimelineService,
  30. ProfileService,
  31. StatusService,
  32. SnowflakeService,
  33. UserFilterService
  34. };
  35. use App\Jobs\StatusPipeline\NewStatusPipeline;
  36. use League\Fractal\Serializer\ArraySerializer;
  37. use League\Fractal\Pagination\IlluminatePaginatorAdapter;
  38. class PublicApiController extends Controller
  39. {
  40. protected $fractal;
  41. public function __construct()
  42. {
  43. $this->fractal = new Fractal\Manager();
  44. $this->fractal->setSerializer(new ArraySerializer());
  45. }
  46. protected function getUserData($user)
  47. {
  48. if(!$user) {
  49. return [];
  50. } else {
  51. return AccountService::get($user->profile_id);
  52. }
  53. }
  54. protected function getLikes($status)
  55. {
  56. if(false == Auth::check()) {
  57. return [];
  58. } else {
  59. $profile = Auth::user()->profile;
  60. if($profile->status) {
  61. return [];
  62. }
  63. $likes = $status->likedBy()->orderBy('created_at','desc')->paginate(10);
  64. $collection = new Fractal\Resource\Collection($likes, new AccountTransformer());
  65. return $this->fractal->createData($collection)->toArray();
  66. }
  67. }
  68. protected function getShares($status)
  69. {
  70. if(false == Auth::check()) {
  71. return [];
  72. } else {
  73. $profile = Auth::user()->profile;
  74. if($profile->status) {
  75. return [];
  76. }
  77. $shares = $status->sharedBy()->orderBy('created_at','desc')->paginate(10);
  78. $collection = new Fractal\Resource\Collection($shares, new AccountTransformer());
  79. return $this->fractal->createData($collection)->toArray();
  80. }
  81. }
  82. public function status(Request $request, $username, int $postid)
  83. {
  84. $profile = Profile::whereUsername($username)->whereNull('status')->firstOrFail();
  85. $status = Status::whereProfileId($profile->id)->findOrFail($postid);
  86. $this->scopeCheck($profile, $status);
  87. if(!Auth::check()) {
  88. $res = Cache::remember('wapi:v1:status:stateless_byid:' . $status->id, now()->addMinutes(30), function() use($status) {
  89. $item = new Fractal\Resource\Item($status, new StatusStatelessTransformer());
  90. $res = [
  91. 'status' => $this->fractal->createData($item)->toArray(),
  92. ];
  93. return $res;
  94. });
  95. return response()->json($res);
  96. }
  97. $item = new Fractal\Resource\Item($status, new StatusStatelessTransformer());
  98. $res = [
  99. 'status' => $this->fractal->createData($item)->toArray(),
  100. ];
  101. return response()->json($res);
  102. }
  103. public function statusState(Request $request, $username, int $postid)
  104. {
  105. $profile = Profile::whereUsername($username)->whereNull('status')->firstOrFail();
  106. $status = Status::whereProfileId($profile->id)->findOrFail($postid);
  107. $this->scopeCheck($profile, $status);
  108. if(!Auth::check()) {
  109. $res = [
  110. 'user' => [],
  111. 'likes' => [],
  112. 'shares' => [],
  113. 'reactions' => [
  114. 'liked' => false,
  115. 'shared' => false,
  116. 'bookmarked' => false,
  117. ],
  118. ];
  119. return response()->json($res);
  120. }
  121. $res = [
  122. 'user' => $this->getUserData($request->user()),
  123. 'likes' => [],
  124. 'shares' => [],
  125. 'reactions' => [
  126. 'liked' => (bool) $status->liked(),
  127. 'shared' => (bool) $status->shared(),
  128. 'bookmarked' => (bool) $status->bookmarked(),
  129. ],
  130. ];
  131. return response()->json($res);
  132. }
  133. public function statusComments(Request $request, $username, int $postId)
  134. {
  135. $this->validate($request, [
  136. 'min_id' => 'nullable|integer|min:1',
  137. 'max_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX,
  138. 'limit' => 'nullable|integer|min:5|max:50'
  139. ]);
  140. $limit = $request->limit ?? 10;
  141. $profile = Profile::whereNull('status')->findOrFail($username);
  142. $status = Status::whereProfileId($profile->id)->whereCommentsDisabled(false)->findOrFail($postId);
  143. $this->scopeCheck($profile, $status);
  144. if(Auth::check()) {
  145. $p = Auth::user()->profile;
  146. $filtered = UserFilter::whereUserId($p->id)
  147. ->whereFilterableType('App\Profile')
  148. ->whereIn('filter_type', ['mute', 'block'])
  149. ->pluck('filterable_id')->toArray();
  150. $scope = $p->id == $status->profile_id ? ['public', 'private', 'unlisted'] : ['public','unlisted'];
  151. } else {
  152. $filtered = [];
  153. $scope = ['public', 'unlisted'];
  154. }
  155. if($request->filled('min_id') || $request->filled('max_id')) {
  156. if($request->filled('min_id')) {
  157. $replies = $status->comments()
  158. ->whereNull('reblog_of_id')
  159. ->whereIn('scope', $scope)
  160. ->whereNotIn('profile_id', $filtered)
  161. ->select('id', 'caption', 'local', 'visibility', 'scope', 'is_nsfw', 'rendered', 'profile_id', 'in_reply_to_id', 'type', 'reply_count', 'created_at')
  162. ->where('id', '>=', $request->min_id)
  163. ->orderBy('id', 'desc')
  164. ->paginate($limit);
  165. }
  166. if($request->filled('max_id')) {
  167. $replies = $status->comments()
  168. ->whereNull('reblog_of_id')
  169. ->whereIn('scope', $scope)
  170. ->whereNotIn('profile_id', $filtered)
  171. ->select('id', 'caption', 'local', 'visibility', 'scope', 'is_nsfw', 'rendered', 'profile_id', 'in_reply_to_id', 'type', 'reply_count', 'created_at')
  172. ->where('id', '<=', $request->max_id)
  173. ->orderBy('id', 'desc')
  174. ->paginate($limit);
  175. }
  176. } else {
  177. $replies = $status->comments()
  178. ->whereNull('reblog_of_id')
  179. ->whereIn('scope', $scope)
  180. ->whereNotIn('profile_id', $filtered)
  181. ->select('id', 'caption', 'local', 'visibility', 'scope', 'is_nsfw', 'rendered', 'profile_id', 'in_reply_to_id', 'type', 'reply_count', 'created_at')
  182. ->orderBy('id', 'desc')
  183. ->paginate($limit);
  184. }
  185. $resource = new Fractal\Resource\Collection($replies, new StatusTransformer(), 'data');
  186. $resource->setPaginator(new IlluminatePaginatorAdapter($replies));
  187. $res = $this->fractal->createData($resource)->toArray();
  188. return response()->json($res, 200, [], JSON_PRETTY_PRINT);
  189. }
  190. public function statusLikes(Request $request, $username, $id)
  191. {
  192. abort_if(!$request->user(), 404);
  193. $status = Status::findOrFail($id);
  194. $this->scopeCheck($status->profile, $status);
  195. $page = $request->input('page');
  196. if($page && $page >= 3 && $request->user()->profile_id != $status->profile_id) {
  197. return response()->json([
  198. 'data' => []
  199. ]);
  200. }
  201. $likes = $this->getLikes($status);
  202. return response()->json([
  203. 'data' => $likes
  204. ]);
  205. }
  206. public function statusShares(Request $request, $username, $id)
  207. {
  208. abort_if(!$request->user(), 404);
  209. $profile = Profile::whereUsername($username)->whereNull('status')->firstOrFail();
  210. $status = Status::whereProfileId($profile->id)->findOrFail($id);
  211. $this->scopeCheck($profile, $status);
  212. $page = $request->input('page');
  213. if($page && $page >= 3 && $request->user()->profile_id != $status->profile_id) {
  214. return response()->json([
  215. 'data' => []
  216. ]);
  217. }
  218. $shares = $this->getShares($status);
  219. return response()->json([
  220. 'data' => $shares
  221. ]);
  222. }
  223. protected function scopeCheck(Profile $profile, Status $status)
  224. {
  225. if($profile->is_private == true && Auth::check() == false) {
  226. abort(404);
  227. }
  228. switch ($status->scope) {
  229. case 'public':
  230. case 'unlisted':
  231. break;
  232. case 'private':
  233. $user = Auth::check() ? Auth::user() : false;
  234. if(!$user) {
  235. abort(403);
  236. } else {
  237. $follows = $profile->followedBy($user->profile);
  238. if($follows == false && $profile->id !== $user->profile->id && $user->is_admin == false) {
  239. abort(404);
  240. }
  241. }
  242. break;
  243. case 'direct':
  244. abort(404);
  245. break;
  246. case 'draft':
  247. abort(404);
  248. break;
  249. default:
  250. abort(404);
  251. break;
  252. }
  253. }
  254. public function publicTimelineApi(Request $request)
  255. {
  256. $this->validate($request,[
  257. 'page' => 'nullable|integer|max:40',
  258. 'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
  259. 'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
  260. 'limit' => 'nullable|integer|max:30'
  261. ]);
  262. if(config('instance.timeline.local.is_public') == false && !Auth::check()) {
  263. abort(403, 'Authentication required.');
  264. }
  265. $page = $request->input('page');
  266. $min = $request->input('min_id');
  267. $max = $request->input('max_id');
  268. $limit = $request->input('limit') ?? 3;
  269. $user = $request->user();
  270. $filtered = $user ? UserFilterService::filters($user->profile_id) : [];
  271. if($min || $max) {
  272. $dir = $min ? '>' : '<';
  273. $id = $min ?? $max;
  274. $timeline = Status::select(
  275. 'id',
  276. 'profile_id',
  277. 'type',
  278. 'scope',
  279. 'local'
  280. )
  281. ->where('id', $dir, $id)
  282. ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
  283. ->whereNotIn('profile_id', $filtered)
  284. ->whereLocal(true)
  285. ->whereScope('public')
  286. ->orderBy('id', 'desc')
  287. ->limit($limit)
  288. ->get()
  289. ->map(function($s) use ($user) {
  290. $status = StatusService::get($s->id);
  291. $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id);
  292. return $status;
  293. });
  294. $res = $timeline->toArray();
  295. } else {
  296. $timeline = Status::select(
  297. 'id',
  298. 'uri',
  299. 'caption',
  300. 'rendered',
  301. 'profile_id',
  302. 'type',
  303. 'in_reply_to_id',
  304. 'reblog_of_id',
  305. 'is_nsfw',
  306. 'scope',
  307. 'local',
  308. 'reply_count',
  309. 'comments_disabled',
  310. 'created_at',
  311. 'place_id',
  312. 'likes_count',
  313. 'reblogs_count',
  314. 'updated_at'
  315. )
  316. ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
  317. ->whereNotIn('profile_id', $filtered)
  318. ->with('profile', 'hashtags', 'mentions')
  319. ->whereLocal(true)
  320. ->whereScope('public')
  321. ->orderBy('id', 'desc')
  322. ->simplePaginate($limit);
  323. $fractal = new Fractal\Resource\Collection($timeline, new StatusTransformer());
  324. $res = $this->fractal->createData($fractal)->toArray();
  325. }
  326. return response()->json($res);
  327. }
  328. public function homeTimelineApi(Request $request)
  329. {
  330. if(!Auth::check()) {
  331. return abort(403);
  332. }
  333. $this->validate($request,[
  334. 'page' => 'nullable|integer|max:40',
  335. 'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
  336. 'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
  337. 'limit' => 'nullable|integer|max:40',
  338. 'recent_feed' => 'nullable',
  339. 'recent_min' => 'nullable|integer'
  340. ]);
  341. $recentFeed = $request->input('recent_feed') == 'true';
  342. $recentFeedMin = $request->input('recent_min');
  343. $page = $request->input('page');
  344. $min = $request->input('min_id');
  345. $max = $request->input('max_id');
  346. $limit = $request->input('limit') ?? 3;
  347. $user = $request->user();
  348. $key = 'user:last_active_at:id:'.$user->id;
  349. $ttl = now()->addMinutes(20);
  350. Cache::remember($key, $ttl, function() use($user) {
  351. $user->last_active_at = now();
  352. $user->save();
  353. return;
  354. });
  355. $pid = $user->profile_id;
  356. $following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function() use($pid) {
  357. $following = Follower::whereProfileId($pid)->pluck('following_id');
  358. return $following->push($pid)->toArray();
  359. });
  360. if($recentFeed == true) {
  361. $key = 'profile:home-timeline-cursor:'.$user->id;
  362. $ttl = now()->addMinutes(30);
  363. $min = Cache::remember($key, $ttl, function() use($pid) {
  364. $res = StatusView::whereProfileId($pid)->orderByDesc('status_id')->first();
  365. return $res ? $res->status_id : null;
  366. });
  367. }
  368. $filtered = $user ? UserFilterService::filters($user->profile_id) : [];
  369. $textOnlyPosts = (bool) Redis::zscore('pf:tl:top', $pid);
  370. $textOnlyReplies = (bool) Redis::zscore('pf:tl:replies', $pid);
  371. $types = $textOnlyPosts ?
  372. ['text', 'photo', 'photo:album', 'video', 'video:album', 'photo:video:album'] :
  373. ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'];
  374. if($min || $max) {
  375. $dir = $min ? '>' : '<';
  376. $id = $min ?? $max;
  377. $timeline = Status::select(
  378. 'id',
  379. 'uri',
  380. 'caption',
  381. 'rendered',
  382. 'profile_id',
  383. 'type',
  384. 'in_reply_to_id',
  385. 'reblog_of_id',
  386. 'is_nsfw',
  387. 'scope',
  388. 'local',
  389. 'reply_count',
  390. 'comments_disabled',
  391. 'place_id',
  392. 'likes_count',
  393. 'reblogs_count',
  394. 'created_at',
  395. 'updated_at'
  396. )
  397. ->whereIn('type', $types)
  398. ->when(!$textOnlyReplies, function($q, $textOnlyReplies) {
  399. return $q->whereNull('in_reply_to_id');
  400. })
  401. ->with('profile', 'hashtags', 'mentions')
  402. ->where('id', $dir, $id)
  403. ->whereIn('profile_id', $following)
  404. ->whereNotIn('profile_id', $filtered)
  405. ->whereIn('visibility',['public', 'unlisted', 'private'])
  406. ->orderBy('created_at', 'desc')
  407. ->limit($limit)
  408. ->get();
  409. } else {
  410. $timeline = Status::select(
  411. 'id',
  412. 'uri',
  413. 'caption',
  414. 'rendered',
  415. 'profile_id',
  416. 'type',
  417. 'in_reply_to_id',
  418. 'reblog_of_id',
  419. 'is_nsfw',
  420. 'scope',
  421. 'local',
  422. 'reply_count',
  423. 'comments_disabled',
  424. 'place_id',
  425. 'likes_count',
  426. 'reblogs_count',
  427. 'created_at',
  428. 'updated_at'
  429. )
  430. ->whereIn('type', $types)
  431. ->when(!$textOnlyReplies, function($q, $textOnlyReplies) {
  432. return $q->whereNull('in_reply_to_id');
  433. })
  434. ->with('profile', 'hashtags', 'mentions')
  435. ->whereIn('profile_id', $following)
  436. ->whereNotIn('profile_id', $filtered)
  437. ->whereIn('visibility',['public', 'unlisted', 'private'])
  438. ->orderBy('created_at', 'desc')
  439. ->simplePaginate($limit);
  440. }
  441. $fractal = new Fractal\Resource\Collection($timeline, new StatusTransformer());
  442. $res = $this->fractal->createData($fractal)->toArray();
  443. return response()->json($res);
  444. }
  445. public function networkTimelineApi(Request $request)
  446. {
  447. abort_if(!Auth::check(), 403);
  448. abort_if(config('federation.network_timeline') == false, 404);
  449. $this->validate($request,[
  450. 'page' => 'nullable|integer|max:40',
  451. 'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
  452. 'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
  453. 'limit' => 'nullable|integer|max:30'
  454. ]);
  455. $page = $request->input('page');
  456. $min = $request->input('min_id');
  457. $max = $request->input('max_id');
  458. $limit = $request->input('limit') ?? 3;
  459. $user = $request->user();
  460. $amin = SnowflakeService::byDate(now()->subDays(90));
  461. $key = 'user:last_active_at:id:'.$user->id;
  462. $ttl = now()->addMinutes(5);
  463. Cache::remember($key, $ttl, function() use($user) {
  464. $user->last_active_at = now();
  465. $user->save();
  466. return;
  467. });
  468. $filtered = $user ? UserFilterService::filters($user->profile_id) : [];
  469. if($min || $max) {
  470. $dir = $min ? '>' : '<';
  471. $id = $min ?? $max;
  472. $timeline = Status::select(
  473. 'id',
  474. 'uri',
  475. 'type',
  476. 'scope',
  477. 'created_at',
  478. )
  479. ->where('id', $dir, $id)
  480. ->whereNotIn('profile_id', $filtered)
  481. ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
  482. ->whereNotNull('uri')
  483. ->whereScope('public')
  484. ->where('id', '>', $amin)
  485. ->orderBy('created_at', 'desc')
  486. ->limit($limit)
  487. ->get()
  488. ->map(function($s) use ($user) {
  489. $status = StatusService::get($s->id);
  490. $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id);
  491. return $status;
  492. });
  493. $res = $timeline->toArray();
  494. } else {
  495. $timeline = Status::select(
  496. 'id',
  497. 'uri',
  498. 'type',
  499. 'scope',
  500. 'created_at',
  501. )
  502. ->whereNotIn('profile_id', $filtered)
  503. ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
  504. ->whereNotNull('uri')
  505. ->whereScope('public')
  506. ->where('id', '>', $amin)
  507. ->orderBy('created_at', 'desc')
  508. ->limit($limit)
  509. ->get()
  510. ->map(function($s) use ($user) {
  511. $status = StatusService::get($s->id);
  512. $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id);
  513. return $status;
  514. });
  515. $res = $timeline->toArray();
  516. }
  517. return response()->json($res);
  518. }
  519. public function relationships(Request $request)
  520. {
  521. if(!Auth::check()) {
  522. return response()->json([]);
  523. }
  524. $this->validate($request, [
  525. 'id' => 'required|array|min:1|max:20',
  526. 'id.*' => 'required|integer'
  527. ]);
  528. $ids = collect($request->input('id'));
  529. $filtered = $ids->filter(function($v) {
  530. return $v != Auth::user()->profile->id;
  531. });
  532. $relations = Profile::whereNull('status')->findOrFail($filtered->all());
  533. $fractal = new Fractal\Resource\Collection($relations, new RelationshipTransformer());
  534. $res = $this->fractal->createData($fractal)->toArray();
  535. return response()->json($res);
  536. }
  537. public function account(Request $request, $id)
  538. {
  539. $res = AccountService::get($id);
  540. return response()->json($res);
  541. }
  542. public function accountFollowers(Request $request, $id)
  543. {
  544. abort_unless(Auth::check(), 403);
  545. $profile = Profile::with('user')->whereNull('status')->findOrFail($id);
  546. $owner = Auth::id() == $profile->user_id;
  547. if(Auth::id() != $profile->user_id && $profile->is_private) {
  548. return response()->json([]);
  549. }
  550. if(!$profile->domain && !$profile->user->settings->show_profile_followers) {
  551. return response()->json([]);
  552. }
  553. if(!$owner && $request->page > 5) {
  554. return [];
  555. }
  556. $res = Follower::select('id', 'profile_id', 'following_id')
  557. ->whereFollowingId($profile->id)
  558. ->orderByDesc('id')
  559. ->simplePaginate(10)
  560. ->map(function($follower) {
  561. return ProfileService::get($follower['profile_id']);
  562. })
  563. ->toArray();
  564. return response()->json($res);
  565. }
  566. public function accountFollowing(Request $request, $id)
  567. {
  568. abort_unless(Auth::check(), 403);
  569. $profile = Profile::with('user')
  570. ->whereNull('status')
  571. ->findOrFail($id);
  572. // filter by username
  573. $search = $request->input('fbu');
  574. $owner = Auth::id() == $profile->user_id;
  575. $filter = ($owner == true) && ($search != null);
  576. abort_if($owner == false && $profile->is_private == true && !$profile->followedBy(Auth::user()->profile), 404);
  577. if(!$profile->domain) {
  578. abort_if($profile->user->settings->show_profile_following == false && $owner == false, 404);
  579. }
  580. if(!$owner && $request->page > 5) {
  581. return [];
  582. }
  583. if($search) {
  584. abort_if(!$owner, 404);
  585. $following = $profile->following()
  586. ->where('profiles.username', 'like', '%'.$search.'%')
  587. ->orderByDesc('followers.created_at')
  588. ->paginate(10);
  589. } else {
  590. $following = $profile->following()
  591. ->orderByDesc('followers.created_at')
  592. ->paginate(10);
  593. }
  594. $resource = new Fractal\Resource\Collection($following, new AccountTransformer());
  595. $res = $this->fractal->createData($resource)->toArray();
  596. return response()->json($res);
  597. }
  598. public function accountStatuses(Request $request, $id)
  599. {
  600. $this->validate($request, [
  601. 'only_media' => 'nullable',
  602. 'pinned' => 'nullable',
  603. 'exclude_replies' => 'nullable',
  604. 'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
  605. 'since_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
  606. 'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
  607. 'limit' => 'nullable|integer|min:1|max:24'
  608. ]);
  609. $user = $request->user();
  610. $profile = Profile::whereNull('status')->findOrFail($id);
  611. $limit = $request->limit ?? 9;
  612. $max_id = $request->max_id;
  613. $min_id = $request->min_id;
  614. $scope = ['photo', 'photo:album', 'video', 'video:album'];
  615. if($profile->is_private) {
  616. if(!$user) {
  617. return response()->json([]);
  618. }
  619. $pid = $user->profile_id;
  620. $following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function() use($pid) {
  621. $following = Follower::whereProfileId($pid)->pluck('following_id');
  622. return $following->push($pid)->toArray();
  623. });
  624. $visibility = true == in_array($profile->id, $following) ? ['public', 'unlisted', 'private'] : [];
  625. } else {
  626. if($user) {
  627. $pid = $user->profile_id;
  628. $following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function() use($pid) {
  629. $following = Follower::whereProfileId($pid)->pluck('following_id');
  630. return $following->push($pid)->toArray();
  631. });
  632. $visibility = true == in_array($profile->id, $following) ? ['public', 'unlisted', 'private'] : ['public', 'unlisted'];
  633. } else {
  634. $visibility = ['public', 'unlisted'];
  635. }
  636. }
  637. $dir = $min_id ? '>' : '<';
  638. $id = $min_id ?? $max_id;
  639. $res = Status::select(
  640. 'id',
  641. 'profile_id',
  642. 'type',
  643. 'scope',
  644. 'local',
  645. 'created_at'
  646. )
  647. ->whereProfileId($profile->id)
  648. ->whereNull('in_reply_to_id')
  649. ->whereNull('reblog_of_id')
  650. ->whereIn('type', $scope)
  651. ->where('id', $dir, $id)
  652. ->whereIn('scope', $visibility)
  653. ->limit($limit)
  654. ->orderByDesc('id')
  655. ->get()
  656. ->map(function($s) use($user) {
  657. try {
  658. $status = StatusService::get($s->id, false);
  659. } catch (\Exception $e) {
  660. $status = false;
  661. }
  662. if($user && $status) {
  663. $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id);
  664. }
  665. return $status;
  666. })
  667. ->filter(function($s) {
  668. return $s;
  669. })
  670. ->values();
  671. return response()->json($res);
  672. }
  673. }