PublicApiController.php 31 KB

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