PublicApiController.php 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807
  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. 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. $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id);
  300. return $status;
  301. })
  302. ->filter(function($s) use($filtered) {
  303. return $s && isset($s['account']) && in_array($s['account']['id'], $filtered) == false;
  304. })
  305. ->values();
  306. $res = $timeline->toArray();
  307. } else {
  308. $timeline = Status::select(
  309. 'id',
  310. 'uri',
  311. 'caption',
  312. 'rendered',
  313. 'profile_id',
  314. 'type',
  315. 'in_reply_to_id',
  316. 'reblog_of_id',
  317. 'is_nsfw',
  318. 'scope',
  319. 'local',
  320. 'reply_count',
  321. 'comments_disabled',
  322. 'created_at',
  323. 'place_id',
  324. 'likes_count',
  325. 'reblogs_count',
  326. 'updated_at'
  327. )
  328. ->whereNull(['in_reply_to_id', 'reblog_of_id'])
  329. ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
  330. ->with('profile', 'hashtags', 'mentions')
  331. ->whereLocal(true)
  332. ->whereScope('public')
  333. ->orderBy('id', 'desc')
  334. ->limit($limit)
  335. ->get()
  336. ->map(function($s) use ($user) {
  337. $status = StatusService::getFull($s->id, $user->profile_id);
  338. $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id);
  339. return $status;
  340. })
  341. ->filter(function($s) use($filtered) {
  342. return $s && in_array($s['account']['id'], $filtered) == false;
  343. })
  344. ->values();
  345. $res = $timeline->toArray();
  346. }
  347. } else {
  348. Cache::remember('api:v1:timelines:public:cache_check', 10368000, function() {
  349. if(PublicTimelineService::count() == 0) {
  350. PublicTimelineService::warmCache(true, 400);
  351. }
  352. });
  353. if ($max) {
  354. $feed = PublicTimelineService::getRankedMaxId($max, $limit);
  355. } else if ($min) {
  356. $feed = PublicTimelineService::getRankedMinId($min, $limit);
  357. } else {
  358. $feed = PublicTimelineService::get(0, $limit);
  359. }
  360. $res = collect($feed)
  361. ->map(function($k) use($user) {
  362. $status = StatusService::get($k);
  363. if($user) {
  364. $status['favourited'] = (bool) LikeService::liked($user->profile_id, $k);
  365. $status['relationship'] = RelationshipService::get($user->profile_id, $status['account']['id']);
  366. }
  367. return $status;
  368. })
  369. ->filter(function($s) use($filtered) {
  370. return isset($s['account']) && in_array($s['account']['id'], $filtered) == false;
  371. })
  372. ->values()
  373. ->toArray();
  374. }
  375. return response()->json($res);
  376. }
  377. public function homeTimelineApi(Request $request)
  378. {
  379. if(!Auth::check()) {
  380. return abort(403);
  381. }
  382. $this->validate($request,[
  383. 'page' => 'nullable|integer|max:40',
  384. 'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
  385. 'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
  386. 'limit' => 'nullable|integer|max:40',
  387. 'recent_feed' => 'nullable',
  388. 'recent_min' => 'nullable|integer'
  389. ]);
  390. $recentFeed = $request->input('recent_feed') == 'true';
  391. $recentFeedMin = $request->input('recent_min');
  392. $page = $request->input('page');
  393. $min = $request->input('min_id');
  394. $max = $request->input('max_id');
  395. $limit = $request->input('limit') ?? 3;
  396. $user = $request->user();
  397. $key = 'user:last_active_at:id:'.$user->id;
  398. $ttl = now()->addMinutes(20);
  399. Cache::remember($key, $ttl, function() use($user) {
  400. $user->last_active_at = now();
  401. $user->save();
  402. return;
  403. });
  404. $pid = $user->profile_id;
  405. $following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function() use($pid) {
  406. $following = Follower::whereProfileId($pid)->pluck('following_id');
  407. return $following->push($pid)->toArray();
  408. });
  409. if($recentFeed == true) {
  410. $key = 'profile:home-timeline-cursor:'.$user->id;
  411. $ttl = now()->addMinutes(30);
  412. $min = Cache::remember($key, $ttl, function() use($pid) {
  413. $res = StatusView::whereProfileId($pid)->orderByDesc('status_id')->first();
  414. return $res ? $res->status_id : null;
  415. });
  416. }
  417. $filtered = $user ? UserFilterService::filters($user->profile_id) : [];
  418. $types = ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'];
  419. // $types = ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album', 'text'];
  420. $textOnlyReplies = false;
  421. if(config('exp.top')) {
  422. $textOnlyPosts = (bool) Redis::zscore('pf:tl:top', $pid);
  423. $textOnlyReplies = (bool) Redis::zscore('pf:tl:replies', $pid);
  424. if($textOnlyPosts) {
  425. array_push($types, 'text');
  426. }
  427. }
  428. if(config('exp.polls') == true) {
  429. array_push($types, 'poll');
  430. }
  431. if($min || $max) {
  432. $dir = $min ? '>' : '<';
  433. $id = $min ?? $max;
  434. $timeline = Status::select(
  435. 'id',
  436. 'uri',
  437. 'caption',
  438. 'rendered',
  439. 'profile_id',
  440. 'type',
  441. 'in_reply_to_id',
  442. 'reblog_of_id',
  443. 'is_nsfw',
  444. 'scope',
  445. 'local',
  446. 'reply_count',
  447. 'comments_disabled',
  448. 'place_id',
  449. 'likes_count',
  450. 'reblogs_count',
  451. 'created_at',
  452. 'updated_at'
  453. )
  454. ->whereIn('type', $types)
  455. ->when($textOnlyReplies != true, function($q, $textOnlyReplies) {
  456. return $q->whereNull('in_reply_to_id');
  457. })
  458. ->with('profile', 'hashtags', 'mentions')
  459. ->where('id', $dir, $id)
  460. ->whereIn('profile_id', $following)
  461. ->whereNotIn('profile_id', $filtered)
  462. ->whereIn('visibility',['public', 'unlisted', 'private'])
  463. ->orderBy('created_at', 'desc')
  464. ->limit($limit)
  465. ->get();
  466. } else {
  467. $timeline = Status::select(
  468. 'id',
  469. 'uri',
  470. 'caption',
  471. 'rendered',
  472. 'profile_id',
  473. 'type',
  474. 'in_reply_to_id',
  475. 'reblog_of_id',
  476. 'is_nsfw',
  477. 'scope',
  478. 'local',
  479. 'reply_count',
  480. 'comments_disabled',
  481. 'place_id',
  482. 'likes_count',
  483. 'reblogs_count',
  484. 'created_at',
  485. 'updated_at'
  486. )
  487. ->whereIn('type', $types)
  488. ->when(!$textOnlyReplies, function($q, $textOnlyReplies) {
  489. return $q->whereNull('in_reply_to_id');
  490. })
  491. ->with('profile', 'hashtags', 'mentions')
  492. ->whereIn('profile_id', $following)
  493. ->whereNotIn('profile_id', $filtered)
  494. ->whereIn('visibility',['public', 'unlisted', 'private'])
  495. ->orderBy('created_at', 'desc')
  496. ->simplePaginate($limit);
  497. }
  498. $fractal = new Fractal\Resource\Collection($timeline, new StatusTransformer());
  499. $res = $this->fractal->createData($fractal)->toArray();
  500. return response()->json($res);
  501. }
  502. public function networkTimelineApi(Request $request)
  503. {
  504. abort_if(!Auth::check(), 403);
  505. abort_if(config('federation.network_timeline') == false, 404);
  506. $this->validate($request,[
  507. 'page' => 'nullable|integer|max:40',
  508. 'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
  509. 'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
  510. 'limit' => 'nullable|integer|max:30'
  511. ]);
  512. $page = $request->input('page');
  513. $min = $request->input('min_id');
  514. $max = $request->input('max_id');
  515. $limit = $request->input('limit') ?? 3;
  516. $user = $request->user();
  517. $amin = SnowflakeService::byDate(now()->subDays(490));
  518. $filtered = $user ? UserFilterService::filters($user->profile_id) : [];
  519. if($min || $max) {
  520. $dir = $min ? '>' : '<';
  521. $id = $min ?? $max;
  522. $timeline = Status::select(
  523. 'id',
  524. 'uri',
  525. 'type',
  526. 'scope',
  527. 'created_at',
  528. )
  529. ->where('id', $dir, $id)
  530. ->whereNull(['in_reply_to_id', 'reblog_of_id'])
  531. ->whereNotIn('profile_id', $filtered)
  532. ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
  533. ->whereNotNull('uri')
  534. ->whereScope('public')
  535. ->where('id', '>', $amin)
  536. ->orderBy('created_at', 'desc')
  537. ->limit($limit)
  538. ->get()
  539. ->map(function($s) use ($user) {
  540. $status = StatusService::get($s->id);
  541. $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id);
  542. return $status;
  543. });
  544. $res = $timeline->toArray();
  545. } else {
  546. $timeline = Status::select(
  547. 'id',
  548. 'uri',
  549. 'type',
  550. 'scope',
  551. 'created_at',
  552. )
  553. ->whereNull(['in_reply_to_id', 'reblog_of_id'])
  554. ->whereNotIn('profile_id', $filtered)
  555. ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
  556. ->whereNotNull('uri')
  557. ->whereScope('public')
  558. ->where('id', '>', $amin)
  559. ->orderBy('created_at', 'desc')
  560. ->limit($limit)
  561. ->get()
  562. ->map(function($s) use ($user) {
  563. $status = StatusService::get($s->id);
  564. $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id);
  565. return $status;
  566. });
  567. $res = $timeline->toArray();
  568. }
  569. return response()->json($res);
  570. }
  571. public function relationships(Request $request)
  572. {
  573. if(!Auth::check()) {
  574. return response()->json([]);
  575. }
  576. $pid = $request->user()->profile_id;
  577. $this->validate($request, [
  578. 'id' => 'required|array|min:1|max:20',
  579. 'id.*' => 'required|integer'
  580. ]);
  581. $ids = collect($request->input('id'));
  582. $res = $ids->filter(function($v) use($pid) {
  583. return $v != $pid;
  584. })
  585. ->map(function($id) use($pid) {
  586. return RelationshipService::get($pid, $id);
  587. });
  588. return response()->json($res);
  589. }
  590. public function account(Request $request, $id)
  591. {
  592. $res = AccountService::get($id);
  593. return response()->json($res);
  594. }
  595. public function accountFollowers(Request $request, $id)
  596. {
  597. abort_unless(Auth::check(), 403);
  598. $profile = Profile::with('user')->whereNull('status')->findOrFail($id);
  599. $owner = Auth::id() == $profile->user_id;
  600. if(Auth::id() != $profile->user_id && $profile->is_private) {
  601. return response()->json([]);
  602. }
  603. if(!$profile->domain && !$profile->user->settings->show_profile_followers) {
  604. return response()->json([]);
  605. }
  606. if(!$owner && $request->page > 5) {
  607. return [];
  608. }
  609. $res = Follower::select('id', 'profile_id', 'following_id')
  610. ->whereFollowingId($profile->id)
  611. ->orderByDesc('id')
  612. ->simplePaginate(10)
  613. ->map(function($follower) {
  614. return ProfileService::get($follower['profile_id']);
  615. })
  616. ->toArray();
  617. return response()->json($res);
  618. }
  619. public function accountFollowing(Request $request, $id)
  620. {
  621. abort_unless(Auth::check(), 403);
  622. $profile = Profile::with('user')
  623. ->whereNull('status')
  624. ->findOrFail($id);
  625. // filter by username
  626. $search = $request->input('fbu');
  627. $owner = Auth::id() == $profile->user_id;
  628. $filter = ($owner == true) && ($search != null);
  629. abort_if($owner == false && $profile->is_private == true && !$profile->followedBy(Auth::user()->profile), 404);
  630. if(!$profile->domain) {
  631. abort_if($profile->user->settings->show_profile_following == false && $owner == false, 404);
  632. }
  633. if(!$owner && $request->page > 5) {
  634. return [];
  635. }
  636. if($search) {
  637. abort_if(!$owner, 404);
  638. $following = $profile->following()
  639. ->where('profiles.username', 'like', '%'.$search.'%')
  640. ->orderByDesc('followers.created_at')
  641. ->paginate(10);
  642. } else {
  643. $following = $profile->following()
  644. ->orderByDesc('followers.created_at')
  645. ->paginate(10);
  646. }
  647. $resource = new Fractal\Resource\Collection($following, new AccountTransformer());
  648. $res = $this->fractal->createData($resource)->toArray();
  649. return response()->json($res);
  650. }
  651. public function accountStatuses(Request $request, $id)
  652. {
  653. $this->validate($request, [
  654. 'only_media' => 'nullable',
  655. 'pinned' => 'nullable',
  656. 'exclude_replies' => 'nullable',
  657. 'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
  658. 'since_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
  659. 'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
  660. 'limit' => 'nullable|integer|min:1|max:24'
  661. ]);
  662. $user = $request->user();
  663. $profile = AccountService::get($id);
  664. abort_if(!$profile, 404);
  665. $limit = $request->limit ?? 9;
  666. $max_id = $request->max_id;
  667. $min_id = $request->min_id;
  668. $scope = ['photo', 'photo:album', 'video', 'video:album'];
  669. $onlyMedia = $request->input('only_media', true);
  670. if(!$min_id && !$max_id) {
  671. $min_id = 1;
  672. }
  673. if($profile['locked']) {
  674. if(!$user) {
  675. return response()->json([]);
  676. }
  677. $pid = $user->profile_id;
  678. $following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function() use($pid) {
  679. $following = Follower::whereProfileId($pid)->pluck('following_id');
  680. return $following->push($pid)->toArray();
  681. });
  682. $visibility = true == in_array($profile['id'], $following) ? ['public', 'unlisted', 'private'] : [];
  683. } else {
  684. if($user) {
  685. $pid = $user->profile_id;
  686. $following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function() use($pid) {
  687. $following = Follower::whereProfileId($pid)->pluck('following_id');
  688. return $following->push($pid)->toArray();
  689. });
  690. $visibility = true == in_array($profile['id'], $following) ? ['public', 'unlisted', 'private'] : ['public', 'unlisted'];
  691. } else {
  692. $visibility = ['public', 'unlisted'];
  693. }
  694. }
  695. $dir = $min_id ? '>' : '<';
  696. $id = $min_id ?? $max_id;
  697. $res = Status::whereProfileId($profile['id'])
  698. ->whereNull('in_reply_to_id')
  699. ->whereNull('reblog_of_id')
  700. ->whereIn('type', $scope)
  701. ->where('id', $dir, $id)
  702. ->whereIn('scope', $visibility)
  703. ->limit($limit)
  704. ->orderByDesc('id')
  705. ->get()
  706. ->map(function($s) use($user) {
  707. try {
  708. $status = StatusService::get($s->id, false);
  709. } catch (\Exception $e) {
  710. $status = false;
  711. }
  712. if($user && $status) {
  713. $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id);
  714. }
  715. return $status;
  716. })
  717. ->filter(function($s) use($onlyMedia) {
  718. if($onlyMedia) {
  719. if(
  720. !isset($s['media_attachments']) ||
  721. !is_array($s['media_attachments']) ||
  722. empty($s['media_attachments'])
  723. ) {
  724. return false;
  725. }
  726. }
  727. return $s;
  728. })
  729. ->values();
  730. return response()->json($res);
  731. }
  732. }