ApiV1Controller.php 16 KB

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