PublicApiController.php 29 KB

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