ApiV1Controller.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691
  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 App\Jobs\StatusPipeline\StatusDelete;
  8. use App\Jobs\FollowPipeline\FollowPipeline;
  9. use Laravel\Passport\Passport;
  10. use Auth, Cache, DB;
  11. use App\{
  12. Follower,
  13. FollowRequest,
  14. Like,
  15. Media,
  16. Notification,
  17. Profile,
  18. Status,
  19. UserFilter,
  20. };
  21. use League\Fractal;
  22. use App\Transformer\Api\{
  23. AccountTransformer,
  24. RelationshipTransformer,
  25. StatusTransformer,
  26. };
  27. use App\Http\Controllers\FollowerController;
  28. use League\Fractal\Serializer\ArraySerializer;
  29. use League\Fractal\Pagination\IlluminatePaginatorAdapter;
  30. use App\Services\NotificationService;
  31. class ApiV1Controller extends Controller
  32. {
  33. protected $fractal;
  34. public function __construct()
  35. {
  36. $this->fractal = new Fractal\Manager();
  37. $this->fractal->setSerializer(new ArraySerializer());
  38. }
  39. public function apps(Request $request)
  40. {
  41. abort_if(!config('pixelfed.oauth_enabled'), 404);
  42. $this->validate($request, [
  43. 'client_name' => 'required',
  44. 'redirect_uris' => 'required',
  45. 'scopes' => 'nullable',
  46. 'website' => 'nullable'
  47. ]);
  48. $client = Passport::client()->forceFill([
  49. 'user_id' => null,
  50. 'name' => e($request->client_name),
  51. 'secret' => Str::random(40),
  52. 'redirect' => $request->redirect_uris,
  53. 'personal_access_client' => false,
  54. 'password_client' => false,
  55. 'revoked' => false,
  56. ]);
  57. $client->save();
  58. $res = [
  59. 'id' => $client->id,
  60. 'name' => $client->name,
  61. 'website' => null,
  62. 'redirect_uri' => $client->redirect,
  63. 'client_id' => $client->id,
  64. 'client_secret' => $client->secret,
  65. 'vapid_key' => null
  66. ];
  67. return $res;
  68. }
  69. /**
  70. * GET /api/v1/accounts/{id}
  71. *
  72. * @param integer $id
  73. *
  74. * @return \App\Transformer\Api\AccountTransformer
  75. */
  76. public function accountById(Request $request, $id)
  77. {
  78. $profile = Profile::whereNull('status')->findOrFail($id);
  79. $resource = new Fractal\Resource\Item($profile, new AccountTransformer());
  80. $res = $this->fractal->createData($resource)->toArray();
  81. return response()->json($res);
  82. }
  83. /**
  84. * PATCH /api/v1/accounts/update_credentials
  85. *
  86. * @return \App\Transformer\Api\AccountTransformer
  87. */
  88. public function accountUpdateCredentials(Request $request)
  89. {
  90. abort_if(!$request->user(), 403);
  91. $this->validate($request, [
  92. 'display_name' => 'nullable|string',
  93. 'note' => 'nullable|string',
  94. 'locked' => 'nullable|boolean',
  95. // 'source.privacy' => 'nullable|in:unlisted,public,private',
  96. // 'source.sensitive' => 'nullable|boolean'
  97. ]);
  98. $user = $request->user();
  99. $profile = $user->profile;
  100. $displayName = $request->input('display_name');
  101. $note = $request->input('note');
  102. $locked = $request->input('locked');
  103. // $privacy = $request->input('source.privacy');
  104. // $sensitive = $request->input('source.sensitive');
  105. $changes = false;
  106. if($displayName !== $user->name) {
  107. $user->name = $displayName;
  108. $profile->name = $displayName;
  109. $changes = true;
  110. }
  111. if($note !== $profile->bio) {
  112. $profile->bio = e($note);
  113. $changes = true;
  114. }
  115. if(!is_null($locked)) {
  116. $profile->is_private = $locked;
  117. $changes = true;
  118. }
  119. if($changes) {
  120. $user->save();
  121. $profile->save();
  122. }
  123. $resource = new Fractal\Resource\Item($profile, new AccountTransformer());
  124. $res = $this->fractal->createData($resource)->toArray();
  125. return response()->json($res);
  126. }
  127. /**
  128. * GET /api/v1/accounts/{id}/followers
  129. *
  130. * @param integer $id
  131. *
  132. * @return \App\Transformer\Api\AccountTransformer
  133. */
  134. public function accountFollowersById(Request $request, $id)
  135. {
  136. abort_if(!$request->user(), 403);
  137. $profile = Profile::whereNull('status')->findOrFail($id);
  138. $settings = $profile->user->settings;
  139. if($settings->show_profile_followers == true) {
  140. $limit = $request->input('limit') ?? 40;
  141. $followers = $profile->followers()->paginate($limit);
  142. $resource = new Fractal\Resource\Collection($followers, new AccountTransformer());
  143. $res = $this->fractal->createData($resource)->toArray();
  144. } else {
  145. $res = [];
  146. }
  147. return response()->json($res);
  148. }
  149. /**
  150. * GET /api/v1/accounts/{id}/following
  151. *
  152. * @param integer $id
  153. *
  154. * @return \App\Transformer\Api\AccountTransformer
  155. */
  156. public function accountFollowingById(Request $request, $id)
  157. {
  158. abort_if(!$request->user(), 403);
  159. $profile = Profile::whereNull('status')->findOrFail($id);
  160. $settings = $profile->user->settings;
  161. if($settings->show_profile_following == true) {
  162. $limit = $request->input('limit') ?? 40;
  163. $following = $profile->following()->paginate($limit);
  164. $resource = new Fractal\Resource\Collection($following, new AccountTransformer());
  165. $res = $this->fractal->createData($resource)->toArray();
  166. } else {
  167. $res = [];
  168. }
  169. return response()->json($res);
  170. }
  171. /**
  172. * GET /api/v1/accounts/{id}/statuses
  173. *
  174. * @param integer $id
  175. *
  176. * @return \App\Transformer\Api\StatusTransformer
  177. */
  178. public function accountStatusesById(Request $request, $id)
  179. {
  180. abort_if(!$request->user(), 403);
  181. $this->validate($request, [
  182. 'only_media' => 'nullable',
  183. 'pinned' => 'nullable',
  184. 'exclude_replies' => 'nullable',
  185. 'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
  186. 'since_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
  187. 'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
  188. 'limit' => 'nullable|integer|min:1|max:40'
  189. ]);
  190. $profile = Profile::whereNull('status')->findOrFail($id);
  191. $limit = $request->limit ?? 20;
  192. $max_id = $request->max_id;
  193. $min_id = $request->min_id;
  194. $pid = $request->user()->profile_id;
  195. $scope = $request->only_media == true ?
  196. ['photo', 'photo:album', 'video', 'video:album'] :
  197. ['photo', 'photo:album', 'video', 'video:album', 'share', 'reply'];
  198. if($pid == $profile->id) {
  199. $visibility = ['public', 'unlisted', 'private'];
  200. } else if($profile->is_private) {
  201. $following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function() use($pid) {
  202. $following = Follower::whereProfileId($pid)->pluck('following_id');
  203. return $following->push($pid)->toArray();
  204. });
  205. $visibility = true == in_array($profile->id, $following) ? ['public', 'unlisted', 'private'] : [];
  206. } else {
  207. $following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function() use($pid) {
  208. $following = Follower::whereProfileId($pid)->pluck('following_id');
  209. return $following->push($pid)->toArray();
  210. });
  211. $visibility = true == in_array($profile->id, $following) ? ['public', 'unlisted', 'private'] : ['public', 'unlisted'];
  212. }
  213. if($min_id || $max_id) {
  214. $dir = $min_id ? '>' : '<';
  215. $id = $min_id ?? $max_id;
  216. $timeline = Status::select(
  217. 'id',
  218. 'uri',
  219. 'caption',
  220. 'rendered',
  221. 'profile_id',
  222. 'type',
  223. 'in_reply_to_id',
  224. 'reblog_of_id',
  225. 'is_nsfw',
  226. 'scope',
  227. 'local',
  228. 'place_id',
  229. 'created_at',
  230. 'updated_at'
  231. )->whereProfileId($profile->id)
  232. ->whereIn('type', $scope)
  233. ->where('id', $dir, $id)
  234. ->whereIn('visibility', $visibility)
  235. ->latest()
  236. ->limit($limit)
  237. ->get();
  238. } else {
  239. $timeline = Status::select(
  240. 'id',
  241. 'uri',
  242. 'caption',
  243. 'rendered',
  244. 'profile_id',
  245. 'type',
  246. 'in_reply_to_id',
  247. 'reblog_of_id',
  248. 'is_nsfw',
  249. 'scope',
  250. 'local',
  251. 'place_id',
  252. 'created_at',
  253. 'updated_at'
  254. )->whereProfileId($profile->id)
  255. ->whereIn('type', $scope)
  256. ->whereIn('visibility', $visibility)
  257. ->latest()
  258. ->limit($limit)
  259. ->get();
  260. }
  261. $resource = new Fractal\Resource\Collection($timeline, new StatusTransformer());
  262. $res = $this->fractal->createData($resource)->toArray();
  263. return response()->json($res);
  264. }
  265. /**
  266. * POST /api/v1/accounts/{id}/follow
  267. *
  268. * @param integer $id
  269. *
  270. * @return \App\Transformer\Api\RelationshipTransformer
  271. */
  272. public function accountFollowById(Request $request, $id)
  273. {
  274. abort_if(!$request->user(), 403);
  275. $user = $request->user();
  276. $target = Profile::where('id', '!=', $user->id)
  277. ->whereNull('status')
  278. ->findOrFail($item);
  279. $private = (bool) $target->is_private;
  280. $remote = (bool) $target->domain;
  281. $blocked = UserFilter::whereUserId($target->id)
  282. ->whereFilterType('block')
  283. ->whereFilterableId($user->id)
  284. ->whereFilterableType('App\Profile')
  285. ->exists();
  286. if($blocked == true) {
  287. abort(400, 'You cannot follow this user.');
  288. }
  289. $isFollowing = Follower::whereProfileId($user->id)
  290. ->whereFollowingId($target->id)
  291. ->exists();
  292. // Following already, return empty relationship
  293. if($isFollowing == true) {
  294. $resource = new Fractal\Resource\Item($target, new RelationshipTransformer());
  295. $res = $this->fractal->createData($resource)->toArray();
  296. return response()->json($res);
  297. }
  298. // Rate limits, max 7500 followers per account
  299. if($user->following()->count() >= Follower::MAX_FOLLOWING) {
  300. abort(400, 'You cannot follow more than ' . Follower::MAX_FOLLOWING . ' accounts');
  301. }
  302. // Rate limits, follow 30 accounts per hour max
  303. if($user->following()->where('followers.created_at', '>', now()->subHour())->count() >= Follower::FOLLOW_PER_HOUR) {
  304. abort(400, 'You can only follow ' . Follower::FOLLOW_PER_HOUR . ' users per hour');
  305. }
  306. if($private == true) {
  307. $follow = FollowRequest::firstOrCreate([
  308. 'follower_id' => $user->id,
  309. 'following_id' => $target->id
  310. ]);
  311. if($remote == true && config('federation.activitypub.remoteFollow') == true) {
  312. (new FollowerController())->sendFollow($user, $target);
  313. }
  314. } else {
  315. $follower = new Follower();
  316. $follower->profile_id = $user->id;
  317. $follower->following_id = $target->id;
  318. $follower->save();
  319. if($remote == true && config('federation.activitypub.remoteFollow') == true) {
  320. (new FollowerController())->sendFollow($user, $target);
  321. }
  322. FollowPipeline::dispatch($follower);
  323. }
  324. Cache::forget('profile:following:'.$target->id);
  325. Cache::forget('profile:followers:'.$target->id);
  326. Cache::forget('profile:following:'.$user->id);
  327. Cache::forget('profile:followers:'.$user->id);
  328. Cache::forget('api:local:exp:rec:'.$user->id);
  329. Cache::forget('user:account:id:'.$target->user_id);
  330. Cache::forget('user:account:id:'.$user->user_id);
  331. $resource = new Fractal\Resource\Item($target, new RelationshipTransformer());
  332. $res = $this->fractal->createData($resource)->toArray();
  333. return response()->json($res);
  334. }
  335. /**
  336. * POST /api/v1/accounts/{id}/unfollow
  337. *
  338. * @param integer $id
  339. *
  340. * @return \App\Transformer\Api\RelationshipTransformer
  341. */
  342. public function accountUnfollowById(Request $request, $id)
  343. {
  344. abort_if(!$request->user(), 403);
  345. $user = $request->user();
  346. $target = Profile::where('id', '!=', $user->id)
  347. ->whereNull('status')
  348. ->findOrFail($item);
  349. $private = (bool) $target->is_private;
  350. $remote = (bool) $target->domain;
  351. $isFollowing = Follower::whereProfileId($user->id)
  352. ->whereFollowingId($target->id)
  353. ->exists();
  354. if($isFollowing == false) {
  355. $resource = new Fractal\Resource\Item($target, new RelationshipTransformer());
  356. $res = $this->fractal->createData($resource)->toArray();
  357. return response()->json($res);
  358. }
  359. // Rate limits, follow 30 accounts per hour max
  360. if($user->following()->where('followers.updated_at', '>', now()->subHour())->count() >= Follower::FOLLOW_PER_HOUR) {
  361. abort(400, 'You can only follow or unfollow ' . Follower::FOLLOW_PER_HOUR . ' users per hour');
  362. }
  363. FollowRequest::whereFollowerId($user->id)
  364. ->whereFollowingId($target->id)
  365. ->delete();
  366. Follower::whereProfileId($user->id)
  367. ->whereFollowingId($target->id)
  368. ->delete();
  369. if($remote == true && config('federation.activitypub.remoteFollow') == true) {
  370. (new FollowerController())->sendUndoFollow($user, $target);
  371. }
  372. Cache::forget('profile:following:'.$target->id);
  373. Cache::forget('profile:followers:'.$target->id);
  374. Cache::forget('profile:following:'.$user->id);
  375. Cache::forget('profile:followers:'.$user->id);
  376. Cache::forget('api:local:exp:rec:'.$user->id);
  377. Cache::forget('user:account:id:'.$target->user_id);
  378. Cache::forget('user:account:id:'.$user->user_id);
  379. $resource = new Fractal\Resource\Item($target, new RelationshipTransformer());
  380. $res = $this->fractal->createData($resource)->toArray();
  381. return response()->json($res);
  382. }
  383. /**
  384. * GET /api/v1/accounts/relationships
  385. *
  386. * @param array|integer $id
  387. *
  388. * @return \App\Transformer\Api\RelationshipTransformer
  389. */
  390. public function accountRelationshipsById(Request $request)
  391. {
  392. abort_if(!$request->user(), 403);
  393. $this->validate($request, [
  394. 'id' => 'required|array|min:1|max:20',
  395. 'id.*' => 'required|integer|min:1|max:' . PHP_INT_MAX
  396. ]);
  397. $pid = $request->user()->profile_id ?? $request->user()->profile->id;
  398. $ids = collect($request->input('id'));
  399. $filtered = $ids->filter(function($v) use($pid) {
  400. return $v != $pid;
  401. });
  402. $relations = Profile::whereNull('status')->findOrFail($filtered->values());
  403. $fractal = new Fractal\Resource\Collection($relations, new RelationshipTransformer());
  404. $res = $this->fractal->createData($fractal)->toArray();
  405. return response()->json($res);
  406. }
  407. /**
  408. * GET /api/v1/accounts/search
  409. *
  410. *
  411. *
  412. * @return \App\Transformer\Api\AccountTransformer
  413. */
  414. public function accountSearch(Request $request)
  415. {
  416. abort_if(!$request->user(), 403);
  417. $this->validate($request, [
  418. 'q' => 'required|string|min:1|max:255',
  419. 'limit' => 'nullable|integer|min:1|max:40',
  420. 'resolve' => 'nullable'
  421. ]);
  422. $user = $request->user();
  423. $query = $request->input('q');
  424. $limit = $request->input('limit') ?? 20;
  425. $resolve = (bool) $request->input('resolve', false);
  426. $q = '%' . $query . '%';
  427. $profiles = Profile::whereNull('status')
  428. ->where('username', 'like', $q)
  429. ->orWhere('name', 'like', $q)
  430. ->limit($limit)
  431. ->get();
  432. $resource = new Fractal\Resource\Collection($profiles, new AccountTransformer());
  433. $res = $this->fractal->createData($resource)->toArray();
  434. return response()->json($res);
  435. }
  436. /**
  437. * GET /api/v1/blocks
  438. *
  439. *
  440. *
  441. * @return \App\Transformer\Api\AccountTransformer
  442. */
  443. public function accountBlocks(Request $request)
  444. {
  445. abort_if(!$request->user(), 403);
  446. $this->validate($request, [
  447. 'limit' => 'nullable|integer|min:1|max:40',
  448. 'page' => 'nullable|integer|min:1|max:10'
  449. ]);
  450. $user = $request->user();
  451. $limit = $request->input('limit') ?? 40;
  452. $blocked = UserFilter::select('filterable_id','filterable_type','filter_type','user_id')
  453. ->whereUserId($user->profile_id)
  454. ->whereFilterableType('App\Profile')
  455. ->whereFilterType('block')
  456. ->simplePaginate($limit)
  457. ->pluck('filterable_id');
  458. $profiles = Profile::findOrFail($blocked);
  459. $resource = new Fractal\Resource\Collection($profiles, new AccountTransformer());
  460. $res = $this->fractal->createData($resource)->toArray();
  461. return response()->json($res);
  462. }
  463. /**
  464. * POST /api/v1/accounts/{id}/block
  465. *
  466. * @param integer $id
  467. *
  468. * @return \App\Transformer\Api\RelationshipTransformer
  469. */
  470. public function accountBlockById(Request $request, $id)
  471. {
  472. abort_if(!$request->user(), 403);
  473. $user = $request->user();
  474. $pid = $user->profile_id ?? $user->profile->id;
  475. if($id == $pid) {
  476. abort(400, 'You cannot block yourself');
  477. }
  478. $profile = Profile::findOrFail($id);
  479. Follower::whereProfileId($profile->id)->whereFollowingId($pid)->delete();
  480. Follower::whereProfileId($pid)->whereFollowingId($profile->id)->delete();
  481. Notification::whereProfileId($pid)->whereActorId($profile->id)->delete();
  482. $filter = UserFilter::firstOrCreate([
  483. 'user_id' => $pid,
  484. 'filterable_id' => $profile->id,
  485. 'filterable_type' => 'App\Profile',
  486. 'filter_type' => 'block',
  487. ]);
  488. Cache::forget("user:filter:list:$pid");
  489. Cache::forget("api:local:exp:rec:$pid");
  490. $resource = new Fractal\Resource\Item($profile, new RelationshipTransformer());
  491. $res = $this->fractal->createData($resource)->toArray();
  492. return response()->json($res);
  493. }
  494. /**
  495. * POST /api/v1/accounts/{id}/unblock
  496. *
  497. * @param integer $id
  498. *
  499. * @return \App\Transformer\Api\RelationshipTransformer
  500. */
  501. public function accountUnblockById(Request $request, $id)
  502. {
  503. abort_if(!$request->user(), 403);
  504. $user = $request->user();
  505. $pid = $user->profile_id ?? $user->profile->id;
  506. if($id == $pid) {
  507. abort(400, 'You cannot unblock yourself');
  508. }
  509. $profile = Profile::findOrFail($id);
  510. UserFilter::whereUserId($pid)
  511. ->whereFilterableId($profile->id)
  512. ->whereFilterableType('App\Profile')
  513. ->whereFilterType('block')
  514. ->delete();
  515. Cache::forget("user:filter:list:$pid");
  516. Cache::forget("api:local:exp:rec:$pid");
  517. $resource = new Fractal\Resource\Item($profile, new RelationshipTransformer());
  518. $res = $this->fractal->createData($resource)->toArray();
  519. return response()->json($res);
  520. }
  521. /**
  522. * GET /api/v1/custom_emojis
  523. *
  524. * Return empty array, we don't support custom emoji
  525. *
  526. * @return array
  527. */
  528. public function customEmojis()
  529. {
  530. return response()->json([]);
  531. }
  532. public function statusById(Request $request, $id)
  533. {
  534. $status = Status::whereVisibility('public')->findOrFail($id);
  535. $resource = new Fractal\Resource\Item($status, new StatusTransformer());
  536. $res = $this->fractal->createData($resource)->toArray();
  537. return response()->json($res);
  538. }
  539. public function instance(Request $request)
  540. {
  541. $res = [
  542. 'description' => 'Pixelfed - Photo sharing for everyone',
  543. 'email' => config('instance.email'),
  544. 'languages' => ['en'],
  545. 'max_toot_chars' => config('pixelfed.max_caption_length'),
  546. 'registrations' => config('pixelfed.open_registration'),
  547. 'stats' => [
  548. 'user_count' => 0,
  549. 'status_count' => 0,
  550. 'domain_count' => 0
  551. ],
  552. 'thumbnail' => config('app.url') . '/img/pixelfed-icon-color.png',
  553. 'title' => 'Pixelfed (' . config('pixelfed.domain.app') . ')',
  554. 'uri' => config('app.url'),
  555. 'urls' => [],
  556. 'version' => '2.7.2 (compatible; Pixelfed ' . config('pixelfed.version') . ')'
  557. ];
  558. return response()->json($res, 200, [], JSON_PRETTY_PRINT);
  559. }
  560. public function filters(Request $request)
  561. {
  562. // Pixelfed does not yet support keyword filters
  563. return response()->json([]);
  564. }
  565. public function context(Request $request)
  566. {
  567. // todo
  568. $res = [
  569. 'ancestors' => [],
  570. 'descendants' => []
  571. ];
  572. return response()->json($res);
  573. }
  574. public function createStatus(Request $request)
  575. {
  576. abort_if(!$request->user(), 403);
  577. $this->validate($request, [
  578. 'status' => 'string',
  579. 'media_ids' => 'array',
  580. 'media_ids.*' => 'integer|min:1',
  581. 'sensitive' => 'nullable|boolean',
  582. 'visibility' => 'string|in:private,unlisted,public',
  583. 'in_reply_to_id' => 'integer'
  584. ]);
  585. if(!$request->filled('media_ids') && !$request->filled('in_reply_to_id')) {
  586. abort(403, 'Empty statuses are not allowed');
  587. }
  588. }
  589. }