1
0

ApiV1Controller.php 21 KB

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