1
0

PublicApiController.php 31 KB

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