ApiV1Controller.php 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131
  1. <?php
  2. namespace App\Http\Controllers\Api;
  3. use Illuminate\Http\Request;
  4. use App\Http\Controllers\Controller;
  5. use Illuminate\Support\Str;
  6. use App\Util\ActivityPub\Helpers;
  7. use Laravel\Passport\Passport;
  8. use Auth, Cache, DB, URL;
  9. use App\{
  10. Follower,
  11. FollowRequest,
  12. Like,
  13. Media,
  14. Notification,
  15. Profile,
  16. Status,
  17. UserFilter,
  18. };
  19. use League\Fractal;
  20. use App\Transformer\Api\{
  21. AccountTransformer,
  22. MediaTransformer,
  23. RelationshipTransformer,
  24. StatusTransformer,
  25. };
  26. use App\Http\Controllers\FollowerController;
  27. use League\Fractal\Serializer\ArraySerializer;
  28. use League\Fractal\Pagination\IlluminatePaginatorAdapter;
  29. use App\Jobs\LikePipeline\LikePipeline;
  30. use App\Jobs\StatusPipeline\StatusDelete;
  31. use App\Jobs\FollowPipeline\FollowPipeline;
  32. use App\Jobs\ImageOptimizePipeline\ImageOptimize;
  33. use App\Jobs\VideoPipeline\{
  34. VideoOptimize,
  35. VideoPostProcess,
  36. VideoThumbnail
  37. };
  38. use App\Services\NotificationService;
  39. class ApiV1Controller extends Controller
  40. {
  41. protected $fractal;
  42. public function __construct()
  43. {
  44. $this->fractal = new Fractal\Manager();
  45. $this->fractal->setSerializer(new ArraySerializer());
  46. }
  47. public function apps(Request $request)
  48. {
  49. abort_if(!config('pixelfed.oauth_enabled'), 404);
  50. $this->validate($request, [
  51. 'client_name' => 'required',
  52. 'redirect_uris' => 'required',
  53. 'scopes' => 'nullable',
  54. 'website' => 'nullable'
  55. ]);
  56. $client = Passport::client()->forceFill([
  57. 'user_id' => null,
  58. 'name' => e($request->client_name),
  59. 'secret' => Str::random(40),
  60. 'redirect' => $request->redirect_uris,
  61. 'personal_access_client' => false,
  62. 'password_client' => false,
  63. 'revoked' => false,
  64. ]);
  65. $client->save();
  66. $res = [
  67. 'id' => $client->id,
  68. 'name' => $client->name,
  69. 'website' => null,
  70. 'redirect_uri' => $client->redirect,
  71. 'client_id' => $client->id,
  72. 'client_secret' => $client->secret,
  73. 'vapid_key' => null
  74. ];
  75. return $res;
  76. }
  77. /**
  78. * GET /api/v1/accounts/{id}
  79. *
  80. * @param integer $id
  81. *
  82. * @return \App\Transformer\Api\AccountTransformer
  83. */
  84. public function accountById(Request $request, $id)
  85. {
  86. $profile = Profile::whereNull('status')->findOrFail($id);
  87. $resource = new Fractal\Resource\Item($profile, new AccountTransformer());
  88. $res = $this->fractal->createData($resource)->toArray();
  89. return response()->json($res);
  90. }
  91. /**
  92. * PATCH /api/v1/accounts/update_credentials
  93. *
  94. * @return \App\Transformer\Api\AccountTransformer
  95. */
  96. public function accountUpdateCredentials(Request $request)
  97. {
  98. abort_if(!$request->user(), 403);
  99. $this->validate($request, [
  100. 'display_name' => 'nullable|string',
  101. 'note' => 'nullable|string',
  102. 'locked' => 'nullable|boolean',
  103. // 'source.privacy' => 'nullable|in:unlisted,public,private',
  104. // 'source.sensitive' => 'nullable|boolean'
  105. ]);
  106. $user = $request->user();
  107. $profile = $user->profile;
  108. $displayName = $request->input('display_name');
  109. $note = $request->input('note');
  110. $locked = $request->input('locked');
  111. // $privacy = $request->input('source.privacy');
  112. // $sensitive = $request->input('source.sensitive');
  113. $changes = false;
  114. if($displayName !== $user->name) {
  115. $user->name = $displayName;
  116. $profile->name = $displayName;
  117. $changes = true;
  118. }
  119. if($note !== $profile->bio) {
  120. $profile->bio = e($note);
  121. $changes = true;
  122. }
  123. if(!is_null($locked)) {
  124. $profile->is_private = $locked;
  125. $changes = true;
  126. }
  127. if($changes) {
  128. $user->save();
  129. $profile->save();
  130. }
  131. $resource = new Fractal\Resource\Item($profile, new AccountTransformer());
  132. $res = $this->fractal->createData($resource)->toArray();
  133. return response()->json($res);
  134. }
  135. /**
  136. * GET /api/v1/accounts/{id}/followers
  137. *
  138. * @param integer $id
  139. *
  140. * @return \App\Transformer\Api\AccountTransformer
  141. */
  142. public function accountFollowersById(Request $request, $id)
  143. {
  144. abort_if(!$request->user(), 403);
  145. $profile = Profile::whereNull('status')->findOrFail($id);
  146. $settings = $profile->user->settings;
  147. if($settings->show_profile_followers == true) {
  148. $limit = $request->input('limit') ?? 40;
  149. $followers = $profile->followers()->paginate($limit);
  150. $resource = new Fractal\Resource\Collection($followers, new AccountTransformer());
  151. $res = $this->fractal->createData($resource)->toArray();
  152. } else {
  153. $res = [];
  154. }
  155. return response()->json($res);
  156. }
  157. /**
  158. * GET /api/v1/accounts/{id}/following
  159. *
  160. * @param integer $id
  161. *
  162. * @return \App\Transformer\Api\AccountTransformer
  163. */
  164. public function accountFollowingById(Request $request, $id)
  165. {
  166. abort_if(!$request->user(), 403);
  167. $profile = Profile::whereNull('status')->findOrFail($id);
  168. $settings = $profile->user->settings;
  169. if($settings->show_profile_following == true) {
  170. $limit = $request->input('limit') ?? 40;
  171. $following = $profile->following()->paginate($limit);
  172. $resource = new Fractal\Resource\Collection($following, new AccountTransformer());
  173. $res = $this->fractal->createData($resource)->toArray();
  174. } else {
  175. $res = [];
  176. }
  177. return response()->json($res);
  178. }
  179. /**
  180. * GET /api/v1/accounts/{id}/statuses
  181. *
  182. * @param integer $id
  183. *
  184. * @return \App\Transformer\Api\StatusTransformer
  185. */
  186. public function accountStatusesById(Request $request, $id)
  187. {
  188. abort_if(!$request->user(), 403);
  189. $this->validate($request, [
  190. 'only_media' => 'nullable',
  191. 'pinned' => 'nullable',
  192. 'exclude_replies' => 'nullable',
  193. 'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
  194. 'since_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
  195. 'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
  196. 'limit' => 'nullable|integer|min:1|max:40'
  197. ]);
  198. $profile = Profile::whereNull('status')->findOrFail($id);
  199. $limit = $request->limit ?? 20;
  200. $max_id = $request->max_id;
  201. $min_id = $request->min_id;
  202. $pid = $request->user()->profile_id;
  203. $scope = $request->only_media == true ?
  204. ['photo', 'photo:album', 'video', 'video:album'] :
  205. ['photo', 'photo:album', 'video', 'video:album', 'share', 'reply'];
  206. if($pid == $profile->id) {
  207. $visibility = ['public', 'unlisted', 'private'];
  208. } else if($profile->is_private) {
  209. $following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function() use($pid) {
  210. $following = Follower::whereProfileId($pid)->pluck('following_id');
  211. return $following->push($pid)->toArray();
  212. });
  213. $visibility = true == in_array($profile->id, $following) ? ['public', 'unlisted', 'private'] : [];
  214. } else {
  215. $following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function() use($pid) {
  216. $following = Follower::whereProfileId($pid)->pluck('following_id');
  217. return $following->push($pid)->toArray();
  218. });
  219. $visibility = true == in_array($profile->id, $following) ? ['public', 'unlisted', 'private'] : ['public', 'unlisted'];
  220. }
  221. if($min_id || $max_id) {
  222. $dir = $min_id ? '>' : '<';
  223. $id = $min_id ?? $max_id;
  224. $timeline = Status::select(
  225. 'id',
  226. 'uri',
  227. 'caption',
  228. 'rendered',
  229. 'profile_id',
  230. 'type',
  231. 'in_reply_to_id',
  232. 'reblog_of_id',
  233. 'is_nsfw',
  234. 'scope',
  235. 'local',
  236. 'place_id',
  237. 'created_at',
  238. 'updated_at'
  239. )->whereProfileId($profile->id)
  240. ->whereIn('type', $scope)
  241. ->where('id', $dir, $id)
  242. ->whereIn('visibility', $visibility)
  243. ->latest()
  244. ->limit($limit)
  245. ->get();
  246. } else {
  247. $timeline = Status::select(
  248. 'id',
  249. 'uri',
  250. 'caption',
  251. 'rendered',
  252. 'profile_id',
  253. 'type',
  254. 'in_reply_to_id',
  255. 'reblog_of_id',
  256. 'is_nsfw',
  257. 'scope',
  258. 'local',
  259. 'place_id',
  260. 'created_at',
  261. 'updated_at'
  262. )->whereProfileId($profile->id)
  263. ->whereIn('type', $scope)
  264. ->whereIn('visibility', $visibility)
  265. ->latest()
  266. ->limit($limit)
  267. ->get();
  268. }
  269. $resource = new Fractal\Resource\Collection($timeline, new StatusTransformer());
  270. $res = $this->fractal->createData($resource)->toArray();
  271. return response()->json($res);
  272. }
  273. /**
  274. * POST /api/v1/accounts/{id}/follow
  275. *
  276. * @param integer $id
  277. *
  278. * @return \App\Transformer\Api\RelationshipTransformer
  279. */
  280. public function accountFollowById(Request $request, $id)
  281. {
  282. abort_if(!$request->user(), 403);
  283. $user = $request->user();
  284. $target = Profile::where('id', '!=', $user->id)
  285. ->whereNull('status')
  286. ->findOrFail($item);
  287. $private = (bool) $target->is_private;
  288. $remote = (bool) $target->domain;
  289. $blocked = UserFilter::whereUserId($target->id)
  290. ->whereFilterType('block')
  291. ->whereFilterableId($user->id)
  292. ->whereFilterableType('App\Profile')
  293. ->exists();
  294. if($blocked == true) {
  295. abort(400, 'You cannot follow this user.');
  296. }
  297. $isFollowing = Follower::whereProfileId($user->id)
  298. ->whereFollowingId($target->id)
  299. ->exists();
  300. // Following already, return empty relationship
  301. if($isFollowing == true) {
  302. $resource = new Fractal\Resource\Item($target, new RelationshipTransformer());
  303. $res = $this->fractal->createData($resource)->toArray();
  304. return response()->json($res);
  305. }
  306. // Rate limits, max 7500 followers per account
  307. if($user->following()->count() >= Follower::MAX_FOLLOWING) {
  308. abort(400, 'You cannot follow more than ' . Follower::MAX_FOLLOWING . ' accounts');
  309. }
  310. // Rate limits, follow 30 accounts per hour max
  311. if($user->following()->where('followers.created_at', '>', now()->subHour())->count() >= Follower::FOLLOW_PER_HOUR) {
  312. abort(400, 'You can only follow ' . Follower::FOLLOW_PER_HOUR . ' users per hour');
  313. }
  314. if($private == true) {
  315. $follow = FollowRequest::firstOrCreate([
  316. 'follower_id' => $user->id,
  317. 'following_id' => $target->id
  318. ]);
  319. if($remote == true && config('federation.activitypub.remoteFollow') == true) {
  320. (new FollowerController())->sendFollow($user, $target);
  321. }
  322. } else {
  323. $follower = new Follower();
  324. $follower->profile_id = $user->id;
  325. $follower->following_id = $target->id;
  326. $follower->save();
  327. if($remote == true && config('federation.activitypub.remoteFollow') == true) {
  328. (new FollowerController())->sendFollow($user, $target);
  329. }
  330. FollowPipeline::dispatch($follower);
  331. }
  332. Cache::forget('profile:following:'.$target->id);
  333. Cache::forget('profile:followers:'.$target->id);
  334. Cache::forget('profile:following:'.$user->id);
  335. Cache::forget('profile:followers:'.$user->id);
  336. Cache::forget('api:local:exp:rec:'.$user->id);
  337. Cache::forget('user:account:id:'.$target->user_id);
  338. Cache::forget('user:account:id:'.$user->user_id);
  339. $resource = new Fractal\Resource\Item($target, new RelationshipTransformer());
  340. $res = $this->fractal->createData($resource)->toArray();
  341. return response()->json($res);
  342. }
  343. /**
  344. * POST /api/v1/accounts/{id}/unfollow
  345. *
  346. * @param integer $id
  347. *
  348. * @return \App\Transformer\Api\RelationshipTransformer
  349. */
  350. public function accountUnfollowById(Request $request, $id)
  351. {
  352. abort_if(!$request->user(), 403);
  353. $user = $request->user();
  354. $target = Profile::where('id', '!=', $user->id)
  355. ->whereNull('status')
  356. ->findOrFail($item);
  357. $private = (bool) $target->is_private;
  358. $remote = (bool) $target->domain;
  359. $isFollowing = Follower::whereProfileId($user->id)
  360. ->whereFollowingId($target->id)
  361. ->exists();
  362. if($isFollowing == false) {
  363. $resource = new Fractal\Resource\Item($target, new RelationshipTransformer());
  364. $res = $this->fractal->createData($resource)->toArray();
  365. return response()->json($res);
  366. }
  367. // Rate limits, follow 30 accounts per hour max
  368. if($user->following()->where('followers.updated_at', '>', now()->subHour())->count() >= Follower::FOLLOW_PER_HOUR) {
  369. abort(400, 'You can only follow or unfollow ' . Follower::FOLLOW_PER_HOUR . ' users per hour');
  370. }
  371. FollowRequest::whereFollowerId($user->id)
  372. ->whereFollowingId($target->id)
  373. ->delete();
  374. Follower::whereProfileId($user->id)
  375. ->whereFollowingId($target->id)
  376. ->delete();
  377. if($remote == true && config('federation.activitypub.remoteFollow') == true) {
  378. (new FollowerController())->sendUndoFollow($user, $target);
  379. }
  380. Cache::forget('profile:following:'.$target->id);
  381. Cache::forget('profile:followers:'.$target->id);
  382. Cache::forget('profile:following:'.$user->id);
  383. Cache::forget('profile:followers:'.$user->id);
  384. Cache::forget('api:local:exp:rec:'.$user->id);
  385. Cache::forget('user:account:id:'.$target->user_id);
  386. Cache::forget('user:account:id:'.$user->user_id);
  387. $resource = new Fractal\Resource\Item($target, new RelationshipTransformer());
  388. $res = $this->fractal->createData($resource)->toArray();
  389. return response()->json($res);
  390. }
  391. /**
  392. * GET /api/v1/accounts/relationships
  393. *
  394. * @param array|integer $id
  395. *
  396. * @return \App\Transformer\Api\RelationshipTransformer
  397. */
  398. public function accountRelationshipsById(Request $request)
  399. {
  400. abort_if(!$request->user(), 403);
  401. $this->validate($request, [
  402. 'id' => 'required|array|min:1|max:20',
  403. 'id.*' => 'required|integer|min:1|max:' . PHP_INT_MAX
  404. ]);
  405. $pid = $request->user()->profile_id ?? $request->user()->profile->id;
  406. $ids = collect($request->input('id'));
  407. $filtered = $ids->filter(function($v) use($pid) {
  408. return $v != $pid;
  409. });
  410. $relations = Profile::whereNull('status')->findOrFail($filtered->values());
  411. $fractal = new Fractal\Resource\Collection($relations, new RelationshipTransformer());
  412. $res = $this->fractal->createData($fractal)->toArray();
  413. return response()->json($res);
  414. }
  415. /**
  416. * GET /api/v1/accounts/search
  417. *
  418. *
  419. *
  420. * @return \App\Transformer\Api\AccountTransformer
  421. */
  422. public function accountSearch(Request $request)
  423. {
  424. abort_if(!$request->user(), 403);
  425. $this->validate($request, [
  426. 'q' => 'required|string|min:1|max:255',
  427. 'limit' => 'nullable|integer|min:1|max:40',
  428. 'resolve' => 'nullable'
  429. ]);
  430. $user = $request->user();
  431. $query = $request->input('q');
  432. $limit = $request->input('limit') ?? 20;
  433. $resolve = (bool) $request->input('resolve', false);
  434. $q = '%' . $query . '%';
  435. $profiles = Profile::whereNull('status')
  436. ->where('username', 'like', $q)
  437. ->orWhere('name', 'like', $q)
  438. ->limit($limit)
  439. ->get();
  440. $resource = new Fractal\Resource\Collection($profiles, new AccountTransformer());
  441. $res = $this->fractal->createData($resource)->toArray();
  442. return response()->json($res);
  443. }
  444. /**
  445. * GET /api/v1/blocks
  446. *
  447. *
  448. *
  449. * @return \App\Transformer\Api\AccountTransformer
  450. */
  451. public function accountBlocks(Request $request)
  452. {
  453. abort_if(!$request->user(), 403);
  454. $this->validate($request, [
  455. 'limit' => 'nullable|integer|min:1|max:40',
  456. 'page' => 'nullable|integer|min:1|max:10'
  457. ]);
  458. $user = $request->user();
  459. $limit = $request->input('limit') ?? 40;
  460. $blocked = UserFilter::select('filterable_id','filterable_type','filter_type','user_id')
  461. ->whereUserId($user->profile_id)
  462. ->whereFilterableType('App\Profile')
  463. ->whereFilterType('block')
  464. ->simplePaginate($limit)
  465. ->pluck('filterable_id');
  466. $profiles = Profile::findOrFail($blocked);
  467. $resource = new Fractal\Resource\Collection($profiles, new AccountTransformer());
  468. $res = $this->fractal->createData($resource)->toArray();
  469. return response()->json($res);
  470. }
  471. /**
  472. * POST /api/v1/accounts/{id}/block
  473. *
  474. * @param integer $id
  475. *
  476. * @return \App\Transformer\Api\RelationshipTransformer
  477. */
  478. public function accountBlockById(Request $request, $id)
  479. {
  480. abort_if(!$request->user(), 403);
  481. $user = $request->user();
  482. $pid = $user->profile_id ?? $user->profile->id;
  483. if($id == $pid) {
  484. abort(400, 'You cannot block yourself');
  485. }
  486. $profile = Profile::findOrFail($id);
  487. Follower::whereProfileId($profile->id)->whereFollowingId($pid)->delete();
  488. Follower::whereProfileId($pid)->whereFollowingId($profile->id)->delete();
  489. Notification::whereProfileId($pid)->whereActorId($profile->id)->delete();
  490. $filter = UserFilter::firstOrCreate([
  491. 'user_id' => $pid,
  492. 'filterable_id' => $profile->id,
  493. 'filterable_type' => 'App\Profile',
  494. 'filter_type' => 'block',
  495. ]);
  496. Cache::forget("user:filter:list:$pid");
  497. Cache::forget("api:local:exp:rec:$pid");
  498. $resource = new Fractal\Resource\Item($profile, new RelationshipTransformer());
  499. $res = $this->fractal->createData($resource)->toArray();
  500. return response()->json($res);
  501. }
  502. /**
  503. * POST /api/v1/accounts/{id}/unblock
  504. *
  505. * @param integer $id
  506. *
  507. * @return \App\Transformer\Api\RelationshipTransformer
  508. */
  509. public function accountUnblockById(Request $request, $id)
  510. {
  511. abort_if(!$request->user(), 403);
  512. $user = $request->user();
  513. $pid = $user->profile_id ?? $user->profile->id;
  514. if($id == $pid) {
  515. abort(400, 'You cannot unblock yourself');
  516. }
  517. $profile = Profile::findOrFail($id);
  518. UserFilter::whereUserId($pid)
  519. ->whereFilterableId($profile->id)
  520. ->whereFilterableType('App\Profile')
  521. ->whereFilterType('block')
  522. ->delete();
  523. Cache::forget("user:filter:list:$pid");
  524. Cache::forget("api:local:exp:rec:$pid");
  525. $resource = new Fractal\Resource\Item($profile, new RelationshipTransformer());
  526. $res = $this->fractal->createData($resource)->toArray();
  527. return response()->json($res);
  528. }
  529. /**
  530. * GET /api/v1/custom_emojis
  531. *
  532. * Return empty array, we don't support custom emoji
  533. *
  534. * @return array
  535. */
  536. public function customEmojis()
  537. {
  538. return response()->json([]);
  539. }
  540. /**
  541. * GET /api/v1/domain_blocks
  542. *
  543. * Return empty array
  544. *
  545. * @return array
  546. */
  547. public function accountDomainBlocks(Request $request)
  548. {
  549. abort_if(!$request->user(), 403);
  550. return response()->json([]);
  551. }
  552. /**
  553. * GET /api/v1/endorsements
  554. *
  555. * Return empty array
  556. *
  557. * @return array
  558. */
  559. public function accountEndorsements(Request $request)
  560. {
  561. abort_if(!$request->user(), 403);
  562. return response()->json([]);
  563. }
  564. /**
  565. * GET /api/v1/favourites
  566. *
  567. * Returns collection of liked statuses
  568. *
  569. * @return \App\Transformer\Api\StatusTransformer
  570. */
  571. public function accountFavourites(Request $request)
  572. {
  573. abort_if(!$request->user(), 403);
  574. $user = $request->user();
  575. $limit = $request->input('limit') ?? 20;
  576. $favourites = Like::whereProfileId($user->profile_id)
  577. ->latest()
  578. ->simplePaginate($limit)
  579. ->pluck('status_id');
  580. $statuses = Status::findOrFail($favourites);
  581. $resource = new Fractal\Resource\Collection($statuses, new StatusTransformer());
  582. $res = $this->fractal->createData($resource)->toArray();
  583. return response()->json($res);
  584. }
  585. /**
  586. * POST /api/v1/statuses/{id}/favourite
  587. *
  588. * @param integer $id
  589. *
  590. * @return \App\Transformer\Api\StatusTransformer
  591. */
  592. public function statusFavouriteById(Request $request, $id)
  593. {
  594. abort_if(!$request->user(), 403);
  595. $user = $request->user();
  596. $status = Status::findOrFail($id);
  597. $like = Like::firstOrCreate([
  598. 'profile_id' => $user->profile_id,
  599. 'status_id' => $status->id
  600. ]);
  601. if($like->wasRecentlyCreated == true) {
  602. LikePipeline::dispatch($like);
  603. }
  604. $resource = new Fractal\Resource\Item($status, new StatusTransformer());
  605. $res = $this->fractal->createData($resource)->toArray();
  606. return response()->json($res);
  607. }
  608. /**
  609. * POST /api/v1/statuses/{id}/unfavourite
  610. *
  611. * @param integer $id
  612. *
  613. * @return \App\Transformer\Api\StatusTransformer
  614. */
  615. public function statusUnfavouriteById(Request $request, $id)
  616. {
  617. abort_if(!$request->user(), 403);
  618. $user = $request->user();
  619. $status = Status::findOrFail($id);
  620. $like = Like::whereProfileId($user->profile_id)
  621. ->whereStatusId($status->id)
  622. ->first();
  623. if($like) {
  624. $like->delete();
  625. }
  626. $resource = new Fractal\Resource\Item($status, new StatusTransformer());
  627. $res = $this->fractal->createData($resource)->toArray();
  628. return response()->json($res);
  629. }
  630. /**
  631. * GET /api/v1/filters
  632. *
  633. * Return empty response since we filter server side
  634. *
  635. * @return array
  636. */
  637. public function accountFilters(Request $request)
  638. {
  639. abort_if(!$request->user(), 403);
  640. return response()->json([]);
  641. }
  642. /**
  643. * GET /api/v1/follow_requests
  644. *
  645. * Return array of Accounts that have sent follow requests
  646. *
  647. * @return \App\Transformer\Api\AccountTransformer
  648. */
  649. public function accountFollowRequests(Request $request)
  650. {
  651. abort_if(!$request->user(), 403);
  652. $user = $request->user();
  653. $followRequests = FollowRequest::whereFollowingId($user->profile->id)->pluck('follower_id');
  654. $profiles = Profile::find($followRequests);
  655. $resource = new Fractal\Resource\Collection($profiles, new AccountTransformer());
  656. $res = $this->fractal->createData($resource)->toArray();
  657. return response()->json($res);
  658. }
  659. /**
  660. * POST /api/v1/follow_requests/{id}/authorize
  661. *
  662. * @param integer $id
  663. *
  664. * @return null
  665. */
  666. public function accountFollowRequestAccept(Request $request, $id)
  667. {
  668. abort_if(!$request->user(), 403);
  669. // todo
  670. return response()->json([]);
  671. }
  672. /**
  673. * POST /api/v1/follow_requests/{id}/reject
  674. *
  675. * @param integer $id
  676. *
  677. * @return null
  678. */
  679. public function accountFollowRequestReject(Request $request, $id)
  680. {
  681. abort_if(!$request->user(), 403);
  682. // todo
  683. return response()->json([]);
  684. }
  685. /**
  686. * GET /api/v1/suggestions
  687. *
  688. * Return empty array as we don't support suggestions
  689. *
  690. * @return null
  691. */
  692. public function accountSuggestions(Request $request)
  693. {
  694. abort_if(!$request->user(), 403);
  695. // todo
  696. return response()->json([]);
  697. }
  698. /**
  699. * GET /api/v1/instance
  700. *
  701. * Information about the server.
  702. *
  703. * @return Instance
  704. */
  705. public function instance(Request $request)
  706. {
  707. $res = [
  708. 'description' => 'Pixelfed - Photo sharing for everyone',
  709. 'email' => config('instance.email'),
  710. 'languages' => ['en'],
  711. 'max_toot_chars' => config('pixelfed.max_caption_length'),
  712. 'registrations' => config('pixelfed.open_registration'),
  713. 'stats' => [
  714. 'user_count' => 0,
  715. 'status_count' => 0,
  716. 'domain_count' => 0
  717. ],
  718. 'thumbnail' => config('app.url') . '/img/pixelfed-icon-color.png',
  719. 'title' => 'Pixelfed (' . config('pixelfed.domain.app') . ')',
  720. 'uri' => config('app.url'),
  721. 'urls' => [],
  722. 'version' => '2.7.2 (compatible; Pixelfed ' . config('pixelfed.version') . ')'
  723. ];
  724. return response()->json($res, 200, [], JSON_PRETTY_PRINT);
  725. }
  726. /**
  727. * GET /api/v1/lists
  728. *
  729. * Return empty array as we don't support lists
  730. *
  731. * @return null
  732. */
  733. public function accountLists(Request $request)
  734. {
  735. abort_if(!$request->user(), 403);
  736. return response()->json([]);
  737. }
  738. /**
  739. * GET /api/v1/accounts/{id}/lists
  740. *
  741. * @param integer $id
  742. *
  743. * @return null
  744. */
  745. public function accountListsById(Request $request, $id)
  746. {
  747. abort_if(!$request->user(), 403);
  748. return response()->json([]);
  749. }
  750. /**
  751. * POST /api/v1/media
  752. *
  753. *
  754. * @return App\Transformer\Api\MediaTransformer
  755. */
  756. public function mediaUpload(Request $request)
  757. {
  758. abort_if(!$request->user(), 403);
  759. $this->validate($request, [
  760. 'file.*' => function() {
  761. return [
  762. 'required',
  763. 'mimes:' . config('pixelfed.media_types'),
  764. 'max:' . config('pixelfed.max_photo_size'),
  765. ];
  766. },
  767. 'filter_name' => 'nullable|string|max:24',
  768. 'filter_class' => 'nullable|alpha_dash|max:24',
  769. 'description' => 'nullable|string|max:420'
  770. ]);
  771. $user = $request->user();
  772. $profile = $user->profile;
  773. if(config('pixelfed.enforce_account_limit') == true) {
  774. $size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) {
  775. return Media::whereUserId($user->id)->sum('size') / 1000;
  776. });
  777. $limit = (int) config('pixelfed.max_account_size');
  778. if ($size >= $limit) {
  779. abort(403, 'Account size limit reached.');
  780. }
  781. }
  782. $monthHash = hash('sha1', date('Y').date('m'));
  783. $userHash = hash('sha1', $user->id . (string) $user->created_at);
  784. $photo = $request->file('file');
  785. $mimes = explode(',', config('pixelfed.media_types'));
  786. if(in_array($photo->getMimeType(), $mimes) == false) {
  787. abort(403, 'Invalid or unsupported mime type.');
  788. }
  789. $storagePath = "public/m/{$monthHash}/{$userHash}";
  790. $path = $photo->store($storagePath);
  791. $hash = \hash_file('sha256', $photo);
  792. $media = new Media();
  793. $media->status_id = null;
  794. $media->profile_id = $profile->id;
  795. $media->user_id = $user->id;
  796. $media->media_path = $path;
  797. $media->original_sha256 = $hash;
  798. $media->size = $photo->getSize();
  799. $media->mime = $photo->getMimeType();
  800. $media->caption = $request->input('description');
  801. $media->filter_class = $request->input('filter_class');
  802. $media->filter_name = $request->input('filter_name');
  803. $media->save();
  804. switch ($media->mime) {
  805. case 'image/jpeg':
  806. case 'image/png':
  807. ImageOptimize::dispatch($media);
  808. break;
  809. case 'video/mp4':
  810. VideoThumbnail::dispatch($media);
  811. $preview_url = '/storage/no-preview.png';
  812. $url = '/storage/no-preview.png';
  813. break;
  814. }
  815. $resource = new Fractal\Resource\Item($media, new MediaTransformer());
  816. $res = $this->fractal->createData($resource)->toArray();
  817. $res['preview_url'] = url('/storage/no-preview.png');
  818. $res['url'] = url('/storage/no-preview.png');
  819. return response()->json($res);
  820. }
  821. /**
  822. * PUT /api/v1/media/{id}
  823. *
  824. * @param integer $id
  825. *
  826. * @return App\Transformer\Api\MediaTransformer
  827. */
  828. public function mediaUpdate(Request $request, $id)
  829. {
  830. abort_if(!$request->user(), 403);
  831. $this->validate($request, [
  832. 'description' => 'nullable|string|max:420'
  833. ]);
  834. $user = $request->user();
  835. $media = Media::whereUserId($user->id)
  836. ->whereNull('status_id')
  837. ->findOrFail($id);
  838. $media->caption = $request->input('description');
  839. $media->save();
  840. $resource = new Fractal\Resource\Item($media, new MediaTransformer());
  841. $res = $this->fractal->createData($resource)->toArray();
  842. $res['preview_url'] = url('/storage/no-preview.png');
  843. $res['url'] = url('/storage/no-preview.png');
  844. return response()->json($res);
  845. }
  846. /**
  847. * GET /api/v1/mutes
  848. *
  849. *
  850. * @return App\Transformer\Api\AccountTransformer
  851. */
  852. public function accountMutes(Request $request)
  853. {
  854. abort_if(!$request->user(), 403);
  855. $this->validate($request, [
  856. 'limit' => 'nullable|integer|min:1|max:40'
  857. ]);
  858. $user = $request->user();
  859. $limit = $request->input('limit') ?? 40;
  860. $mutes = UserFilter::whereUserId($user->profile_id)
  861. ->whereFilterableType('App\Profile')
  862. ->whereFilterType('mute')
  863. ->simplePaginate($limit)
  864. ->pluck('filterable_id');
  865. $accounts = Profile::find($mutes);
  866. $resource = new Fractal\Resource\Collection($accounts, new AccountTransformer());
  867. $res = $this->fractal->createData($resource)->toArray();
  868. return response()->json($res);
  869. }
  870. /**
  871. * POST /api/v1/accounts/{id}/mute
  872. *
  873. * @param integer $id
  874. *
  875. * @return App\Transformer\Api\RelationshipTransformer
  876. */
  877. public function accountMuteById(Request $request, $id)
  878. {
  879. abort_if(!$request->user(), 403);
  880. $user = $request->user();
  881. $pid = $user->profile_id;
  882. $account = Profile::findOrFail($id);
  883. $filter = UserFilter::firstOrCreate([
  884. 'user_id' => $pid,
  885. 'filterable_id' => $account->id,
  886. 'filterable_type' => 'App\Profile',
  887. 'filter_type' => 'mute',
  888. ]);
  889. Cache::forget("user:filter:list:$pid");
  890. Cache::forget("feature:discover:posts:$pid");
  891. Cache::forget("api:local:exp:rec:$pid");
  892. $resource = new Fractal\Resource\Item($account, new RelationshipTransformer());
  893. $res = $this->fractal->createData($resource)->toArray();
  894. return response()->json($res);
  895. }
  896. /**
  897. * POST /api/v1/accounts/{id}/unmute
  898. *
  899. * @param integer $id
  900. *
  901. * @return App\Transformer\Api\RelationshipTransformer
  902. */
  903. public function accountUnmuteById(Request $request, $id)
  904. {
  905. abort_if(!$request->user(), 403);
  906. $user = $request->user();
  907. $pid = $user->profile_id;
  908. $account = Profile::findOrFail($id);
  909. $filter = UserFilter::whereUserId($pid)
  910. ->whereFilterableId($account->id)
  911. ->whereFilterableType('App\Profile')
  912. ->whereFilterType('mute')
  913. ->first();
  914. if($filter) {
  915. $filter->delete();
  916. Cache::forget("user:filter:list:$pid");
  917. Cache::forget("feature:discover:posts:$pid");
  918. Cache::forget("api:local:exp:rec:$pid");
  919. }
  920. $resource = new Fractal\Resource\Item($account, new RelationshipTransformer());
  921. $res = $this->fractal->createData($resource)->toArray();
  922. return response()->json($res);
  923. }
  924. public function statusById(Request $request, $id)
  925. {
  926. $status = Status::whereVisibility('public')->findOrFail($id);
  927. $resource = new Fractal\Resource\Item($status, new StatusTransformer());
  928. $res = $this->fractal->createData($resource)->toArray();
  929. return response()->json($res);
  930. }
  931. public function context(Request $request)
  932. {
  933. // todo
  934. $res = [
  935. 'ancestors' => [],
  936. 'descendants' => []
  937. ];
  938. return response()->json($res);
  939. }
  940. public function createStatus(Request $request)
  941. {
  942. abort_if(!$request->user(), 403);
  943. $this->validate($request, [
  944. 'status' => 'string',
  945. 'media_ids' => 'array',
  946. 'media_ids.*' => 'integer|min:1',
  947. 'sensitive' => 'nullable|boolean',
  948. 'visibility' => 'string|in:private,unlisted,public',
  949. 'in_reply_to_id' => 'integer'
  950. ]);
  951. if(!$request->filled('media_ids') && !$request->filled('in_reply_to_id')) {
  952. abort(403, 'Empty statuses are not allowed');
  953. }
  954. }
  955. }