ApiV1Dot1Controller.php 45 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258
  1. <?php
  2. namespace App\Http\Controllers\Api;
  3. use App\AccountLog;
  4. use App\EmailVerification;
  5. use App\Http\Controllers\Controller;
  6. use App\Http\Controllers\StatusController;
  7. use App\Http\Resources\StatusStateless;
  8. use App\Jobs\ImageOptimizePipeline\ImageOptimize;
  9. use App\Jobs\ReportPipeline\ReportNotifyAdminViaEmail;
  10. use App\Jobs\StatusPipeline\NewStatusPipeline;
  11. use App\Jobs\StatusPipeline\RemoteStatusDelete;
  12. use App\Jobs\StatusPipeline\StatusDelete;
  13. use App\Jobs\VideoPipeline\VideoThumbnail;
  14. use App\Mail\ConfirmAppEmail;
  15. use App\Mail\PasswordChange;
  16. use App\Media;
  17. use App\Place;
  18. use App\Profile;
  19. use App\Report;
  20. use App\Services\AccountService;
  21. use App\Services\BouncerService;
  22. use App\Services\EmailService;
  23. use App\Services\FollowerService;
  24. use App\Services\MediaBlocklistService;
  25. use App\Services\MediaPathService;
  26. use App\Services\NetworkTimelineService;
  27. use App\Services\ProfileStatusService;
  28. use App\Services\PublicTimelineService;
  29. use App\Services\StatusService;
  30. use App\Status;
  31. use App\StatusArchived;
  32. use App\User;
  33. use App\UserSetting;
  34. use App\Util\Lexer\Autolink;
  35. use App\Util\Lexer\RestrictedNames;
  36. use Cache;
  37. use DB;
  38. use Illuminate\Http\Request;
  39. use Illuminate\Support\Facades\Hash;
  40. use Illuminate\Support\Facades\RateLimiter;
  41. use Illuminate\Support\Str;
  42. use Jenssegers\Agent\Agent;
  43. use League\Fractal;
  44. use League\Fractal\Serializer\ArraySerializer;
  45. use Mail;
  46. use NotificationChannels\Expo\ExpoPushToken;
  47. class ApiV1Dot1Controller extends Controller
  48. {
  49. protected $fractal;
  50. public function __construct()
  51. {
  52. $this->fractal = new Fractal\Manager();
  53. $this->fractal->setSerializer(new ArraySerializer());
  54. }
  55. public function json($res, $code = 200, $headers = [])
  56. {
  57. return response()->json($res, $code, $headers, JSON_UNESCAPED_SLASHES);
  58. }
  59. public function error($msg, $code = 400, $extra = [], $headers = [])
  60. {
  61. $res = [
  62. 'msg' => $msg,
  63. 'code' => $code,
  64. ];
  65. return response()->json(array_merge($res, $extra), $code, $headers, JSON_UNESCAPED_SLASHES);
  66. }
  67. public function report(Request $request)
  68. {
  69. abort_if(! $request->user() || ! $request->user()->token(), 403);
  70. abort_unless($request->user()->tokenCan('write'), 403);
  71. $user = $request->user();
  72. abort_if($user->status != null, 403);
  73. if (config('pixelfed.bouncer.cloud_ips.ban_signups')) {
  74. abort_if(BouncerService::checkIp($request->ip()), 404);
  75. }
  76. $report_type = $request->input('report_type');
  77. $object_id = $request->input('object_id');
  78. $object_type = $request->input('object_type');
  79. $types = [
  80. 'spam',
  81. 'sensitive',
  82. 'abusive',
  83. 'underage',
  84. 'violence',
  85. 'copyright',
  86. 'impersonation',
  87. 'scam',
  88. 'terrorism',
  89. ];
  90. if (! $report_type || ! $object_id || ! $object_type) {
  91. return $this->error('Invalid or missing parameters', 400, ['error_code' => 'ERROR_INVALID_PARAMS']);
  92. }
  93. if (! in_array($report_type, $types)) {
  94. return $this->error('Invalid report type', 400, ['error_code' => 'ERROR_TYPE_INVALID']);
  95. }
  96. if ($object_type === 'user' && $object_id == $user->profile_id) {
  97. return $this->error('Cannot self report', 400, ['error_code' => 'ERROR_NO_SELF_REPORTS']);
  98. }
  99. $rpid = null;
  100. switch ($object_type) {
  101. case 'post':
  102. $object = Status::find($object_id);
  103. if (! $object) {
  104. return $this->error('Invalid object id', 400, ['error_code' => 'ERROR_INVALID_OBJECT_ID']);
  105. }
  106. $object_type = 'App\Status';
  107. $exists = Report::whereUserId($user->id)
  108. ->whereObjectId($object->id)
  109. ->whereObjectType('App\Status')
  110. ->count();
  111. $rpid = $object->profile_id;
  112. break;
  113. case 'user':
  114. $object = Profile::find($object_id);
  115. if (! $object) {
  116. return $this->error('Invalid object id', 400, ['error_code' => 'ERROR_INVALID_OBJECT_ID']);
  117. }
  118. $object_type = 'App\Profile';
  119. $exists = Report::whereUserId($user->id)
  120. ->whereObjectId($object->id)
  121. ->whereObjectType('App\Profile')
  122. ->count();
  123. $rpid = $object->id;
  124. break;
  125. default:
  126. return $this->error('Invalid report type', 400, ['error_code' => 'ERROR_REPORT_OBJECT_TYPE_INVALID']);
  127. break;
  128. }
  129. if ($exists !== 0) {
  130. return $this->error('Duplicate report', 400, ['error_code' => 'ERROR_REPORT_DUPLICATE']);
  131. }
  132. if ($object->profile_id == $user->profile_id) {
  133. return $this->error('Cannot self report', 400, ['error_code' => 'ERROR_NO_SELF_REPORTS']);
  134. }
  135. $report = new Report;
  136. $report->profile_id = $user->profile_id;
  137. $report->user_id = $user->id;
  138. $report->object_id = $object->id;
  139. $report->object_type = $object_type;
  140. $report->reported_profile_id = $rpid;
  141. $report->type = $report_type;
  142. $report->save();
  143. if (config('instance.reports.email.enabled')) {
  144. ReportNotifyAdminViaEmail::dispatch($report)->onQueue('default');
  145. }
  146. $res = [
  147. 'msg' => 'Successfully sent report',
  148. 'code' => 200,
  149. ];
  150. return $this->json($res);
  151. }
  152. /**
  153. * DELETE /api/v1.1/accounts/avatar
  154. *
  155. * @return \App\Transformer\Api\AccountTransformer
  156. */
  157. public function deleteAvatar(Request $request)
  158. {
  159. abort_if(! $request->user() || ! $request->user()->token(), 403);
  160. abort_unless($request->user()->tokenCan('write'), 403);
  161. $user = $request->user();
  162. abort_if($user->status != null, 403);
  163. if (config('pixelfed.bouncer.cloud_ips.ban_signups')) {
  164. abort_if(BouncerService::checkIp($request->ip()), 404);
  165. }
  166. $avatar = $user->profile->avatar;
  167. if ($avatar->media_path == 'public/avatars/default.png' ||
  168. $avatar->media_path == 'public/avatars/default.jpg'
  169. ) {
  170. return AccountService::get($user->profile_id);
  171. }
  172. if (is_file(storage_path('app/'.$avatar->media_path))) {
  173. @unlink(storage_path('app/'.$avatar->media_path));
  174. }
  175. $avatar->media_path = 'public/avatars/default.jpg';
  176. $avatar->change_count = $avatar->change_count + 1;
  177. $avatar->save();
  178. Cache::forget('avatar:'.$user->profile_id);
  179. Cache::forget("avatar:{$user->profile_id}");
  180. Cache::forget('user:account:id:'.$user->id);
  181. AccountService::del($user->profile_id);
  182. return AccountService::get($user->profile_id);
  183. }
  184. /**
  185. * GET /api/v1.1/accounts/{id}/posts
  186. *
  187. * @return \App\Transformer\Api\StatusTransformer
  188. */
  189. public function accountPosts(Request $request, $id)
  190. {
  191. abort_if(! $request->user() || ! $request->user()->token(), 403);
  192. abort_unless($request->user()->tokenCan('read'), 403);
  193. $user = $request->user();
  194. abort_if($user->status != null, 403);
  195. if (config('pixelfed.bouncer.cloud_ips.ban_signups')) {
  196. abort_if(BouncerService::checkIp($request->ip()), 404);
  197. }
  198. $account = AccountService::get($id);
  199. if (! $account || $account['username'] !== $request->input('username')) {
  200. return $this->json([]);
  201. }
  202. $posts = ProfileStatusService::get($id);
  203. if (! $posts) {
  204. return $this->json([]);
  205. }
  206. $res = collect($posts)
  207. ->map(function ($id) {
  208. return StatusService::get($id);
  209. })
  210. ->filter(function ($post) {
  211. return $post && isset($post['account']);
  212. })
  213. ->toArray();
  214. return $this->json($res);
  215. }
  216. /**
  217. * POST /api/v1.1/accounts/change-password
  218. *
  219. * @return \App\Transformer\Api\AccountTransformer
  220. */
  221. public function accountChangePassword(Request $request)
  222. {
  223. abort_if(! $request->user() || ! $request->user()->token(), 403);
  224. abort_unless($request->user()->tokenCan('write'), 403);
  225. $user = $request->user();
  226. abort_if($user->status != null, 403);
  227. if (config('pixelfed.bouncer.cloud_ips.ban_signups')) {
  228. abort_if(BouncerService::checkIp($request->ip()), 404);
  229. }
  230. $this->validate($request, [
  231. 'current_password' => 'bail|required|current_password',
  232. 'new_password' => 'required|min:'.config('pixelfed.min_password_length', 8),
  233. 'confirm_password' => 'required|same:new_password',
  234. ], [
  235. 'current_password' => 'The password you entered is incorrect',
  236. ]);
  237. $user->password = bcrypt($request->input('new_password'));
  238. $user->save();
  239. $log = new AccountLog;
  240. $log->user_id = $user->id;
  241. $log->item_id = $user->id;
  242. $log->item_type = 'App\User';
  243. $log->action = 'account.edit.password';
  244. $log->message = 'Password changed';
  245. $log->link = null;
  246. $log->ip_address = $request->ip();
  247. $log->user_agent = $request->userAgent();
  248. $log->save();
  249. Mail::to($request->user())->send(new PasswordChange($user));
  250. return $this->json(AccountService::get($user->profile_id));
  251. }
  252. /**
  253. * GET /api/v1.1/accounts/login-activity
  254. *
  255. * @return array
  256. */
  257. public function accountLoginActivity(Request $request)
  258. {
  259. abort_if(! $request->user() || ! $request->user()->token(), 403);
  260. abort_unless($request->user()->tokenCan('read'), 403);
  261. $user = $request->user();
  262. abort_if($user->status != null, 403);
  263. if (config('pixelfed.bouncer.cloud_ips.ban_signups')) {
  264. abort_if(BouncerService::checkIp($request->ip()), 404);
  265. }
  266. $agent = new Agent();
  267. $currentIp = $request->ip();
  268. $activity = AccountLog::whereUserId($user->id)
  269. ->whereAction('auth.login')
  270. ->orderBy('created_at', 'desc')
  271. ->groupBy('ip_address')
  272. ->limit(10)
  273. ->get()
  274. ->map(function ($item) use ($agent, $currentIp) {
  275. $agent->setUserAgent($item->user_agent);
  276. return [
  277. 'id' => $item->id,
  278. 'action' => $item->action,
  279. 'ip' => $item->ip_address,
  280. 'ip_current' => $item->ip_address === $currentIp,
  281. 'is_mobile' => $agent->isMobile(),
  282. 'device' => $agent->device(),
  283. 'browser' => $agent->browser(),
  284. 'platform' => $agent->platform(),
  285. 'created_at' => $item->created_at->format('c'),
  286. ];
  287. });
  288. return $this->json($activity);
  289. }
  290. /**
  291. * GET /api/v1.1/accounts/two-factor
  292. *
  293. * @return array
  294. */
  295. public function accountTwoFactor(Request $request)
  296. {
  297. abort_if(! $request->user() || ! $request->user()->token(), 403);
  298. abort_unless($request->user()->tokenCan('read'), 403);
  299. $user = $request->user();
  300. abort_if($user->status != null, 403);
  301. if (config('pixelfed.bouncer.cloud_ips.ban_signups')) {
  302. abort_if(BouncerService::checkIp($request->ip()), 404);
  303. }
  304. $res = [
  305. 'active' => (bool) $user->{'2fa_enabled'},
  306. 'setup_at' => $user->{'2fa_setup_at'},
  307. ];
  308. return $this->json($res);
  309. }
  310. /**
  311. * GET /api/v1.1/accounts/emails-from-pixelfed
  312. *
  313. * @return array
  314. */
  315. public function accountEmailsFromPixelfed(Request $request)
  316. {
  317. abort_if(! $request->user() || ! $request->user()->token(), 403);
  318. abort_unless($request->user()->tokenCan('read'), 403);
  319. $user = $request->user();
  320. abort_if($user->status != null, 403);
  321. if (config('pixelfed.bouncer.cloud_ips.ban_signups')) {
  322. abort_if(BouncerService::checkIp($request->ip()), 404);
  323. }
  324. $from = config('mail.from.address');
  325. $emailVerifications = EmailVerification::whereUserId($user->id)
  326. ->orderByDesc('id')
  327. ->where('created_at', '>', now()->subDays(14))
  328. ->limit(10)
  329. ->get()
  330. ->map(function ($mail) use ($user, $from) {
  331. return [
  332. 'type' => 'Email Verification',
  333. 'subject' => 'Confirm Email',
  334. 'to_address' => $user->email,
  335. 'from_address' => $from,
  336. 'created_at' => str_replace('@', 'at', $mail->created_at->format('M j, Y @ g:i:s A')),
  337. ];
  338. })
  339. ->toArray();
  340. $passwordResets = DB::table('password_resets')
  341. ->whereEmail($user->email)
  342. ->where('created_at', '>', now()->subDays(14))
  343. ->orderByDesc('created_at')
  344. ->limit(10)
  345. ->get()
  346. ->map(function ($mail) use ($user, $from) {
  347. return [
  348. 'type' => 'Password Reset',
  349. 'subject' => 'Reset Password Notification',
  350. 'to_address' => $user->email,
  351. 'from_address' => $from,
  352. 'created_at' => str_replace('@', 'at', now()->parse($mail->created_at)->format('M j, Y @ g:i:s A')),
  353. ];
  354. })
  355. ->toArray();
  356. $passwordChanges = AccountLog::whereUserId($user->id)
  357. ->whereAction('account.edit.password')
  358. ->where('created_at', '>', now()->subDays(14))
  359. ->orderByDesc('created_at')
  360. ->limit(10)
  361. ->get()
  362. ->map(function ($mail) use ($user, $from) {
  363. return [
  364. 'type' => 'Password Change',
  365. 'subject' => 'Password Change',
  366. 'to_address' => $user->email,
  367. 'from_address' => $from,
  368. 'created_at' => str_replace('@', 'at', now()->parse($mail->created_at)->format('M j, Y @ g:i:s A')),
  369. ];
  370. })
  371. ->toArray();
  372. $res = collect([])
  373. ->merge($emailVerifications)
  374. ->merge($passwordResets)
  375. ->merge($passwordChanges)
  376. ->sortByDesc('created_at')
  377. ->values();
  378. return $this->json($res);
  379. }
  380. /**
  381. * GET /api/v1.1/accounts/apps-and-applications
  382. *
  383. * @return array
  384. */
  385. public function accountApps(Request $request)
  386. {
  387. abort_if(! $request->user() || ! $request->user()->token(), 403);
  388. abort_unless($request->user()->tokenCan('read'), 403);
  389. $user = $request->user();
  390. abort_if($user->status != null, 403);
  391. if (config('pixelfed.bouncer.cloud_ips.ban_signups')) {
  392. abort_if(BouncerService::checkIp($request->ip()), 404);
  393. }
  394. $res = $user->tokens->sortByDesc('created_at')->take(10)->map(function ($token, $key) use ($request) {
  395. return [
  396. 'id' => $token->id,
  397. 'current_session' => $request->user()->token()->id == $token->id,
  398. 'name' => $token->client->name,
  399. 'scopes' => $token->scopes,
  400. 'revoked' => $token->revoked,
  401. 'created_at' => str_replace('@', 'at', now()->parse($token->created_at)->format('M j, Y @ g:i:s A')),
  402. 'expires_at' => str_replace('@', 'at', now()->parse($token->expires_at)->format('M j, Y @ g:i:s A')),
  403. ];
  404. });
  405. return $this->json($res);
  406. }
  407. public function inAppRegistrationPreFlightCheck(Request $request)
  408. {
  409. return [
  410. 'open' => (bool) config_cache('pixelfed.open_registration'),
  411. 'iara' => (bool) config_cache('pixelfed.allow_app_registration'),
  412. ];
  413. }
  414. public function inAppRegistration(Request $request)
  415. {
  416. abort_if($request->user(), 404);
  417. abort_unless((bool) config_cache('pixelfed.open_registration'), 404);
  418. abort_unless((bool) config_cache('pixelfed.allow_app_registration'), 404);
  419. abort_unless($request->hasHeader('X-PIXELFED-APP'), 403);
  420. if (config('pixelfed.bouncer.cloud_ips.ban_signups')) {
  421. abort_if(BouncerService::checkIp($request->ip()), 404);
  422. }
  423. $rl = RateLimiter::attempt('pf:apiv1.1:iar:'.$request->ip(), config('pixelfed.app_registration_rate_limit_attempts', 3), function () {}, config('pixelfed.app_registration_rate_limit_decay', 1800));
  424. abort_if(! $rl, 400, 'Too many requests');
  425. $this->validate($request, [
  426. 'email' => [
  427. 'required',
  428. 'string',
  429. 'email',
  430. 'max:255',
  431. 'unique:users',
  432. function ($attribute, $value, $fail) {
  433. $banned = EmailService::isBanned($value);
  434. if ($banned) {
  435. return $fail('Email is invalid.');
  436. }
  437. },
  438. ],
  439. 'username' => [
  440. 'required',
  441. 'min:2',
  442. 'max:15',
  443. 'unique:users',
  444. function ($attribute, $value, $fail) {
  445. $dash = substr_count($value, '-');
  446. $underscore = substr_count($value, '_');
  447. $period = substr_count($value, '.');
  448. if (ends_with($value, ['.php', '.js', '.css'])) {
  449. return $fail('Username is invalid.');
  450. }
  451. if (($dash + $underscore + $period) > 1) {
  452. return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).');
  453. }
  454. if (! ctype_alnum($value[0])) {
  455. return $fail('Username is invalid. Must start with a letter or number.');
  456. }
  457. if (! ctype_alnum($value[strlen($value) - 1])) {
  458. return $fail('Username is invalid. Must end with a letter or number.');
  459. }
  460. $val = str_replace(['_', '.', '-'], '', $value);
  461. if (! ctype_alnum($val)) {
  462. return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).');
  463. }
  464. $restricted = RestrictedNames::get();
  465. if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
  466. return $fail('Username cannot be used.');
  467. }
  468. },
  469. ],
  470. 'password' => 'required|string|min:8',
  471. ]);
  472. $email = $request->input('email');
  473. $username = $request->input('username');
  474. $password = $request->input('password');
  475. if (config('database.default') == 'pgsql') {
  476. $username = strtolower($username);
  477. $email = strtolower($email);
  478. }
  479. $user = new User;
  480. $user->name = $username;
  481. $user->username = $username;
  482. $user->email = $email;
  483. $user->password = Hash::make($password);
  484. $user->register_source = 'app';
  485. $user->app_register_ip = $request->ip();
  486. $user->app_register_token = Str::random(40);
  487. $user->save();
  488. $rtoken = Str::random(64);
  489. $verify = new EmailVerification();
  490. $verify->user_id = $user->id;
  491. $verify->email = $user->email;
  492. $verify->user_token = $user->app_register_token;
  493. $verify->random_token = $rtoken;
  494. $verify->save();
  495. $params = http_build_query([
  496. 'ut' => $user->app_register_token,
  497. 'rt' => $rtoken,
  498. 'ea' => base64_encode($user->email),
  499. ]);
  500. $appUrl = url('/api/v1.1/auth/iarer?'.$params);
  501. Mail::to($user->email)->send(new ConfirmAppEmail($verify, $appUrl));
  502. return response()->json([
  503. 'success' => true,
  504. ]);
  505. }
  506. public function inAppRegistrationEmailRedirect(Request $request)
  507. {
  508. $this->validate($request, [
  509. 'ut' => 'required',
  510. 'rt' => 'required',
  511. 'ea' => 'required',
  512. ]);
  513. $ut = $request->input('ut');
  514. $rt = $request->input('rt');
  515. $ea = $request->input('ea');
  516. $params = http_build_query([
  517. 'ut' => $ut,
  518. 'rt' => $rt,
  519. 'domain' => config('pixelfed.domain.app'),
  520. 'ea' => $ea,
  521. ]);
  522. $url = 'pixelfed://confirm-account/'.$ut.'?'.$params;
  523. return redirect()->away($url);
  524. }
  525. public function inAppRegistrationConfirm(Request $request)
  526. {
  527. abort_if($request->user(), 404);
  528. abort_unless((bool) config_cache('pixelfed.open_registration'), 404);
  529. abort_unless((bool) config_cache('pixelfed.allow_app_registration'), 404);
  530. abort_unless($request->hasHeader('X-PIXELFED-APP'), 403);
  531. if (config('pixelfed.bouncer.cloud_ips.ban_signups')) {
  532. abort_if(BouncerService::checkIp($request->ip()), 404);
  533. }
  534. $rl = RateLimiter::attempt('pf:apiv1.1:iarc:'.$request->ip(), config('pixelfed.app_registration_confirm_rate_limit_attempts', 20), function () {}, config('pixelfed.app_registration_confirm_rate_limit_decay', 1800));
  535. abort_if(! $rl, 429, 'Too many requests');
  536. $request->validate([
  537. 'user_token' => 'required',
  538. 'random_token' => 'required',
  539. 'email' => 'required',
  540. ]);
  541. $verify = EmailVerification::whereEmail($request->input('email'))
  542. ->whereUserToken($request->input('user_token'))
  543. ->whereRandomToken($request->input('random_token'))
  544. ->first();
  545. if (! $verify) {
  546. return response()->json(['error' => 'Invalid tokens'], 403);
  547. }
  548. if ($verify->created_at->lt(now()->subHours(24))) {
  549. $verify->delete();
  550. return response()->json(['error' => 'Invalid tokens'], 403);
  551. }
  552. $user = User::findOrFail($verify->user_id);
  553. $user->email_verified_at = now();
  554. $user->last_active_at = now();
  555. $user->save();
  556. $token = $user->createToken('Pixelfed', ['read', 'write', 'follow', 'admin:read', 'admin:write', 'push']);
  557. return response()->json([
  558. 'access_token' => $token->accessToken,
  559. ]);
  560. }
  561. public function archive(Request $request, $id)
  562. {
  563. abort_if(! $request->user() || ! $request->user()->token(), 403);
  564. abort_unless($request->user()->tokenCan('write'), 403);
  565. if (config('pixelfed.bouncer.cloud_ips.ban_signups')) {
  566. abort_if(BouncerService::checkIp($request->ip()), 404);
  567. }
  568. $status = Status::whereNull('in_reply_to_id')
  569. ->whereNull('reblog_of_id')
  570. ->whereProfileId($request->user()->profile_id)
  571. ->findOrFail($id);
  572. if ($status->scope === 'archived') {
  573. return [200];
  574. }
  575. $archive = new StatusArchived;
  576. $archive->status_id = $status->id;
  577. $archive->profile_id = $status->profile_id;
  578. $archive->original_scope = $status->scope;
  579. $archive->save();
  580. $status->scope = 'archived';
  581. $status->visibility = 'draft';
  582. $status->save();
  583. StatusService::del($status->id, true);
  584. AccountService::syncPostCount($status->profile_id);
  585. return [200];
  586. }
  587. public function unarchive(Request $request, $id)
  588. {
  589. abort_if(! $request->user() || ! $request->user()->token(), 403);
  590. abort_unless($request->user()->tokenCan('write'), 403);
  591. if (config('pixelfed.bouncer.cloud_ips.ban_signups')) {
  592. abort_if(BouncerService::checkIp($request->ip()), 404);
  593. }
  594. $status = Status::whereNull('in_reply_to_id')
  595. ->whereNull('reblog_of_id')
  596. ->whereProfileId($request->user()->profile_id)
  597. ->findOrFail($id);
  598. if ($status->scope !== 'archived') {
  599. return [200];
  600. }
  601. $archive = StatusArchived::whereStatusId($status->id)
  602. ->whereProfileId($status->profile_id)
  603. ->firstOrFail();
  604. $status->scope = $archive->original_scope;
  605. $status->visibility = $archive->original_scope;
  606. $status->save();
  607. $archive->delete();
  608. StatusService::del($status->id, true);
  609. AccountService::syncPostCount($status->profile_id);
  610. return [200];
  611. }
  612. public function archivedPosts(Request $request)
  613. {
  614. abort_if(! $request->user() || ! $request->user()->token(), 403);
  615. abort_unless($request->user()->tokenCan('read'), 403);
  616. if (config('pixelfed.bouncer.cloud_ips.ban_signups')) {
  617. abort_if(BouncerService::checkIp($request->ip()), 404);
  618. }
  619. $statuses = Status::whereProfileId($request->user()->profile_id)
  620. ->whereScope('archived')
  621. ->orderByDesc('id')
  622. ->cursorPaginate(10);
  623. return StatusStateless::collection($statuses);
  624. }
  625. public function placesById(Request $request, $id, $slug)
  626. {
  627. abort_if(! $request->user() || ! $request->user()->token(), 403);
  628. abort_unless($request->user()->tokenCan('read'), 403);
  629. if (config('pixelfed.bouncer.cloud_ips.ban_signups')) {
  630. abort_if(BouncerService::checkIp($request->ip()), 404);
  631. }
  632. $place = Place::whereSlug($slug)->findOrFail($id);
  633. $posts = Cache::remember('pf-api:v1.1:places-by-id:'.$place->id, 3600, function () use ($place) {
  634. return Status::wherePlaceId($place->id)
  635. ->whereNull('uri')
  636. ->whereScope('public')
  637. ->orderByDesc('created_at')
  638. ->limit(60)
  639. ->pluck('id');
  640. });
  641. $posts = $posts->map(function ($id) {
  642. return StatusService::get($id);
  643. })
  644. ->filter()
  645. ->values();
  646. return [
  647. 'place' => [
  648. 'id' => $place->id,
  649. 'name' => $place->name,
  650. 'slug' => $place->slug,
  651. 'country' => $place->country,
  652. 'lat' => $place->lat,
  653. 'long' => $place->long,
  654. ],
  655. 'posts' => $posts];
  656. }
  657. public function moderatePost(Request $request, $id)
  658. {
  659. abort_if(! $request->user() || ! $request->user()->token(), 403);
  660. abort_if($request->user()->is_admin != true, 403);
  661. abort_unless($request->user()->tokenCan('admin:write'), 403);
  662. if (config('pixelfed.bouncer.cloud_ips.ban_signups')) {
  663. abort_if(BouncerService::checkIp($request->ip()), 404);
  664. }
  665. $this->validate($request, [
  666. 'action' => 'required|in:cw,mark-public,mark-unlisted,mark-private,mark-spammer,delete',
  667. ]);
  668. $action = $request->input('action');
  669. $status = Status::find($id);
  670. if (! $status) {
  671. return response()->json(['error' => 'Cannot find status'], 400);
  672. }
  673. if ($status->uri == null) {
  674. if ($status->profile->user && $status->profile->user->is_admin) {
  675. return response()->json(['error' => 'Cannot moderate admin accounts'], 400);
  676. }
  677. }
  678. if ($action == 'mark-spammer') {
  679. $status->profile->update([
  680. 'unlisted' => true,
  681. 'cw' => true,
  682. 'no_autolink' => true,
  683. ]);
  684. Status::whereProfileId($status->profile_id)
  685. ->get()
  686. ->each(function ($s) {
  687. if (in_array($s->scope, ['public', 'unlisted'])) {
  688. $s->scope = 'private';
  689. $s->visibility = 'private';
  690. }
  691. $s->is_nsfw = true;
  692. $s->save();
  693. StatusService::del($s->id, true);
  694. });
  695. Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$status->profile_id);
  696. Cache::forget('pf:bouncer_v0:recent_by_pid:'.$status->profile_id);
  697. Cache::forget('admin-dash:reports:spam-count');
  698. } elseif ($action == 'cw') {
  699. $state = $status->is_nsfw;
  700. $status->is_nsfw = ! $state;
  701. $status->save();
  702. StatusService::del($status->id);
  703. } elseif ($action == 'mark-public') {
  704. $state = $status->scope;
  705. $status->scope = 'public';
  706. $status->visibility = 'public';
  707. $status->save();
  708. StatusService::del($status->id, true);
  709. if ($state !== 'public') {
  710. if ($status->uri) {
  711. if ($status->in_reply_to_id == null && $status->reblog_of_id == null) {
  712. NetworkTimelineService::add($status->id);
  713. }
  714. } else {
  715. if ($status->in_reply_to_id == null && $status->reblog_of_id == null) {
  716. PublicTimelineService::add($status->id);
  717. }
  718. }
  719. }
  720. } elseif ($action == 'mark-unlisted') {
  721. $state = $status->scope;
  722. $status->scope = 'unlisted';
  723. $status->visibility = 'unlisted';
  724. $status->save();
  725. StatusService::del($status->id);
  726. if ($state == 'public') {
  727. PublicTimelineService::del($status->id);
  728. NetworkTimelineService::del($status->id);
  729. }
  730. } elseif ($action == 'mark-private') {
  731. $state = $status->scope;
  732. $status->scope = 'private';
  733. $status->visibility = 'private';
  734. $status->save();
  735. StatusService::del($status->id);
  736. if ($state == 'public') {
  737. PublicTimelineService::del($status->id);
  738. NetworkTimelineService::del($status->id);
  739. }
  740. } elseif ($action == 'delete') {
  741. PublicTimelineService::del($status->id);
  742. NetworkTimelineService::del($status->id);
  743. Cache::forget('_api:statuses:recent_9:'.$status->profile_id);
  744. Cache::forget('profile:status_count:'.$status->profile_id);
  745. Cache::forget('profile:embed:'.$status->profile_id);
  746. StatusService::del($status->id, true);
  747. Cache::forget('profile:status_count:'.$status->profile_id);
  748. $status->uri ? RemoteStatusDelete::dispatch($status) : StatusDelete::dispatch($status);
  749. return [];
  750. }
  751. Cache::forget('_api:statuses:recent_9:'.$status->profile_id);
  752. return StatusService::get($status->id, false);
  753. }
  754. public function getWebSettings(Request $request)
  755. {
  756. abort_if(! $request->user() || ! $request->user()->token(), 403);
  757. abort_unless($request->user()->tokenCan('read'), 403);
  758. $uid = $request->user()->id;
  759. $settings = UserSetting::firstOrCreate([
  760. 'user_id' => $uid,
  761. ]);
  762. if (! $settings->other) {
  763. return [];
  764. }
  765. return $settings->other;
  766. }
  767. public function setWebSettings(Request $request)
  768. {
  769. abort_if(! $request->user() || ! $request->user()->token(), 403);
  770. abort_unless($request->user()->tokenCan('write'), 403);
  771. $this->validate($request, [
  772. 'field' => 'required|in:enable_reblogs,hide_reblog_banner',
  773. 'value' => 'required',
  774. ]);
  775. $field = $request->input('field');
  776. $value = $request->input('value');
  777. $settings = UserSetting::firstOrCreate([
  778. 'user_id' => $request->user()->id,
  779. ]);
  780. if (! $settings->other) {
  781. $other = [];
  782. } else {
  783. $other = $settings->other;
  784. }
  785. $other[$field] = $value;
  786. $settings->other = $other;
  787. $settings->save();
  788. return [200];
  789. }
  790. public function getMutualAccounts(Request $request, $id)
  791. {
  792. abort_if(! $request->user() || ! $request->user()->token(), 403);
  793. abort_unless($request->user()->tokenCan('follow'), 403);
  794. $account = AccountService::get($id, true);
  795. if (! $account || ! isset($account['id'])) {
  796. return [];
  797. }
  798. $res = collect(FollowerService::mutualAccounts($request->user()->profile_id, $id))
  799. ->map(function ($accountId) {
  800. return AccountService::get($accountId, true);
  801. })
  802. ->filter()
  803. ->take(24)
  804. ->values();
  805. return $this->json($res);
  806. }
  807. public function accountUsernameToId(Request $request, $username)
  808. {
  809. abort_if(! $request->user() || ! $request->user()->token() || ! $username, 403);
  810. abort_unless($request->user()->tokenCan('read'), 403);
  811. $username = trim($username);
  812. $rateLimiting = (bool) config_cache('api.rate-limits.v1Dot1.accounts.usernameToId.enabled');
  813. $ipRateLimiting = (bool) config_cache('api.rate-limits.v1Dot1.accounts.usernameToId.ip_enabled');
  814. if ($ipRateLimiting) {
  815. $userLimit = (int) config_cache('api.rate-limits.v1Dot1.accounts.usernameToId.ip_limit');
  816. $userDecay = (int) config_cache('api.rate-limits.v1Dot1.accounts.usernameToId.ip_decay');
  817. $userKey = 'pf:apiv1.1:acctU2ID:byIp:'.$request->ip();
  818. if (RateLimiter::tooManyAttempts($userKey, $userLimit)) {
  819. $limits = [
  820. 'X-Rate-Limit-Limit' => $userLimit,
  821. 'X-Rate-Limit-Remaining' => RateLimiter::remaining($userKey, $userLimit),
  822. 'X-Rate-Limit-Reset' => RateLimiter::availableIn($userKey),
  823. ];
  824. return $this->json(['error' => 'Too many attempts!'], 429, $limits);
  825. }
  826. RateLimiter::increment($userKey, $userDecay);
  827. $limits = [
  828. 'X-Rate-Limit-Limit' => $userLimit,
  829. 'X-Rate-Limit-Remaining' => RateLimiter::remaining($userKey, $userLimit),
  830. 'X-Rate-Limit-Reset' => RateLimiter::availableIn($userKey),
  831. ];
  832. }
  833. if ($rateLimiting) {
  834. $userLimit = (int) config_cache('api.rate-limits.v1Dot1.accounts.usernameToId.limit');
  835. $userDecay = (int) config_cache('api.rate-limits.v1Dot1.accounts.usernameToId.decay');
  836. $userKey = 'pf:apiv1.1:acctU2ID:byUid:'.$request->user()->id;
  837. if (RateLimiter::tooManyAttempts($userKey, $userLimit)) {
  838. $limits = [
  839. 'X-Rate-Limit-Limit' => $userLimit,
  840. 'X-Rate-Limit-Remaining' => RateLimiter::remaining($userKey, $userLimit),
  841. 'X-Rate-Limit-Reset' => RateLimiter::availableIn($userKey),
  842. ];
  843. return $this->json(['error' => 'Too many attempts!'], 429, $limits);
  844. }
  845. RateLimiter::increment($userKey, $userDecay);
  846. $limits = [
  847. 'X-Rate-Limit-Limit' => $userLimit,
  848. 'X-Rate-Limit-Remaining' => RateLimiter::remaining($userKey, $userLimit),
  849. 'X-Rate-Limit-Reset' => RateLimiter::availableIn($userKey),
  850. ];
  851. }
  852. if (str_ends_with($username, config_cache('pixelfed.domain.app'))) {
  853. $pre = str_starts_with($username, '@') ? substr($username, 1) : $username;
  854. $parts = explode('@', $pre);
  855. $username = $parts[0];
  856. }
  857. $accountId = AccountService::usernameToId($username, true);
  858. if (! $accountId) {
  859. return [];
  860. }
  861. $account = AccountService::get($accountId);
  862. return $this->json($account, 200, $rateLimiting ? $limits : []);
  863. }
  864. public function getExpoPushNotifications(Request $request)
  865. {
  866. abort_if(! $request->user() || ! $request->user()->token(), 403);
  867. abort_unless($request->user()->tokenCan('push'), 403);
  868. abort_unless(config('services.expo.access_token') && strlen(config('services.expo.access_token')) > 10, 404, 'Push notifications are not supported on this server.');
  869. $user = $request->user();
  870. $res = [
  871. 'expo_token' => (bool) $user->expo_token,
  872. 'notify_like' => (bool) $user->notify_like,
  873. 'notify_follow' => (bool) $user->notify_follow,
  874. 'notify_mention' => (bool) $user->notify_mention,
  875. 'notify_comment' => (bool) $user->notify_comment,
  876. ];
  877. return $this->json($res);
  878. }
  879. public function disableExpoPushNotifications(Request $request)
  880. {
  881. abort_if(! $request->user() || ! $request->user()->token(), 403);
  882. abort_unless($request->user()->tokenCan('push'), 403);
  883. abort_unless(config('services.expo.access_token') && strlen(config('services.expo.access_token')) > 10, 404, 'Push notifications are not supported on this server.');
  884. $request->user()->update([
  885. 'expo_token' => null,
  886. ]);
  887. return $this->json(['expo_token' => null]);
  888. }
  889. public function updateExpoPushNotifications(Request $request)
  890. {
  891. abort_if(! $request->user() || ! $request->user()->token(), 403);
  892. abort_unless($request->user()->tokenCan('push'), 403);
  893. abort_unless(config('services.expo.access_token') && strlen(config('services.expo.access_token')) > 10, 404, 'Push notifications are not supported on this server.');
  894. $this->validate($request, [
  895. 'expo_token' => ['required', ExpoPushToken::rule()],
  896. 'notify_like' => 'sometimes',
  897. 'notify_follow' => 'sometimes',
  898. 'notify_mention' => 'sometimes',
  899. 'notify_comment' => 'sometimes',
  900. ]);
  901. $user = $request->user()->update([
  902. 'expo_token' => $request->input('expo_token'),
  903. 'notify_like' => $request->has('notify_like') && $request->boolean('notify_like'),
  904. 'notify_follow' => $request->has('notify_follow') && $request->boolean('notify_follow'),
  905. 'notify_mention' => $request->has('notify_mention') && $request->boolean('notify_mention'),
  906. 'notify_comment' => $request->has('notify_comment') && $request->boolean('notify_comment'),
  907. ]);
  908. $res = [
  909. 'expo_token' => (bool) $request->user()->expo_token,
  910. 'notify_like' => (bool) $request->user()->notify_like,
  911. 'notify_follow' => (bool) $request->user()->notify_follow,
  912. 'notify_mention' => (bool) $request->user()->notify_mention,
  913. 'notify_comment' => (bool) $request->user()->notify_comment,
  914. ];
  915. return $this->json($res);
  916. }
  917. /**
  918. * POST /api/v1.1/status/create
  919. *
  920. *
  921. * @return StatusTransformer
  922. */
  923. public function statusCreate(Request $request)
  924. {
  925. abort_if(! $request->user() || ! $request->user()->token(), 403);
  926. abort_unless($request->user()->tokenCan('write'), 403);
  927. $this->validate($request, [
  928. 'status' => 'nullable|string|max:'.(int) config_cache('pixelfed.max_caption_length'),
  929. 'file' => [
  930. 'required',
  931. 'file',
  932. 'mimetypes:'.config_cache('pixelfed.media_types'),
  933. 'max:'.config_cache('pixelfed.max_photo_size'),
  934. function ($attribute, $value, $fail) {
  935. if (is_array($value) && count($value) > 1) {
  936. $fail('Only one file can be uploaded at a time.');
  937. }
  938. },
  939. ],
  940. 'sensitive' => 'nullable',
  941. 'visibility' => 'string|in:private,unlisted,public',
  942. 'spoiler_text' => 'sometimes|max:140',
  943. ]);
  944. if ($request->hasHeader('idempotency-key')) {
  945. $key = 'pf:api:v1:status:idempotency-key:'.$request->user()->id.':'.hash('sha1', $request->header('idempotency-key'));
  946. $exists = Cache::has($key);
  947. abort_if($exists, 400, 'Duplicate idempotency key.');
  948. Cache::put($key, 1, 3600);
  949. }
  950. if (config('costar.enabled') == true) {
  951. $blockedKeywords = config('costar.keyword.block');
  952. if ($blockedKeywords !== null && $request->status) {
  953. $keywords = config('costar.keyword.block');
  954. foreach ($keywords as $kw) {
  955. if (Str::contains($request->status, $kw) == true) {
  956. abort(400, 'Invalid object. Contains banned keyword.');
  957. }
  958. }
  959. }
  960. }
  961. $user = $request->user();
  962. $limitKey = 'compose:rate-limit:media-upload:'.$user->id;
  963. $limitTtl = now()->addMinutes(15);
  964. $limitReached = Cache::remember($limitKey, $limitTtl, function () use ($user) {
  965. $dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count();
  966. return $dailyLimit >= 1250;
  967. });
  968. abort_if($limitReached == true, 429);
  969. if ($user->has_roles) {
  970. abort_if(! UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action');
  971. }
  972. $profile = $user->profile;
  973. if (config_cache('pixelfed.enforce_account_limit') == true) {
  974. $size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function () use ($user) {
  975. return Media::whereUserId($user->id)->sum('size') / 1000;
  976. });
  977. $limit = (int) config_cache('pixelfed.max_account_size');
  978. if ($size >= $limit) {
  979. abort(403, 'Account size limit reached.');
  980. }
  981. }
  982. abort_if($limitReached == true, 429);
  983. $photo = $request->file('file');
  984. $mimes = explode(',', config_cache('pixelfed.media_types'));
  985. if (in_array($photo->getMimeType(), $mimes) == false) {
  986. abort(403, 'Invalid or unsupported mime type.');
  987. }
  988. $storagePath = MediaPathService::get($user, 2);
  989. $path = $photo->storePublicly($storagePath);
  990. $hash = \hash_file('sha256', $photo);
  991. $license = null;
  992. $mime = $photo->getMimeType();
  993. $settings = UserSetting::whereUserId($user->id)->first();
  994. if ($settings && ! empty($settings->compose_settings)) {
  995. $compose = $settings->compose_settings;
  996. if (isset($compose['default_license']) && $compose['default_license'] != 1) {
  997. $license = $compose['default_license'];
  998. }
  999. }
  1000. abort_if(MediaBlocklistService::exists($hash) == true, 451);
  1001. $visibility = $profile->is_private ? 'private' : (
  1002. $profile->unlisted == true &&
  1003. $request->input('visibility', 'public') == 'public' ?
  1004. 'unlisted' :
  1005. $request->input('visibility', 'public'));
  1006. if ($user->last_active_at == null) {
  1007. return [];
  1008. }
  1009. $content = strip_tags($request->input('status'));
  1010. $rendered = Autolink::create()->autolink($content);
  1011. $cw = $user->profile->cw == true ? true : $request->boolean('sensitive', false);
  1012. $spoilerText = $cw && $request->filled('spoiler_text') ? $request->input('spoiler_text') : null;
  1013. $status = new Status;
  1014. $status->caption = $content;
  1015. $status->rendered = $rendered;
  1016. $status->profile_id = $user->profile_id;
  1017. $status->is_nsfw = $cw;
  1018. $status->cw_summary = $spoilerText;
  1019. $status->scope = $visibility;
  1020. $status->visibility = $visibility;
  1021. $status->type = StatusController::mimeTypeCheck([$mime]);
  1022. $status->save();
  1023. if (! $status) {
  1024. abort(500, 'An error occured.');
  1025. }
  1026. $media = new Media();
  1027. $media->status_id = $status->id;
  1028. $media->profile_id = $profile->id;
  1029. $media->user_id = $user->id;
  1030. $media->media_path = $path;
  1031. $media->original_sha256 = $hash;
  1032. $media->size = $photo->getSize();
  1033. $media->mime = $mime;
  1034. $media->order = 1;
  1035. $media->caption = $request->input('description');
  1036. if ($license) {
  1037. $media->license = $license;
  1038. }
  1039. $media->save();
  1040. switch ($media->mime) {
  1041. case 'image/jpeg':
  1042. case 'image/png':
  1043. ImageOptimize::dispatch($media)->onQueue('mmo');
  1044. break;
  1045. case 'video/mp4':
  1046. VideoThumbnail::dispatch($media)->onQueue('mmo');
  1047. $preview_url = '/storage/no-preview.png';
  1048. $url = '/storage/no-preview.png';
  1049. break;
  1050. }
  1051. NewStatusPipeline::dispatch($status);
  1052. Cache::forget('user:account:id:'.$user->id);
  1053. Cache::forget('_api:statuses:recent_9:'.$user->profile_id);
  1054. Cache::forget('profile:status_count:'.$user->profile_id);
  1055. Cache::forget($user->storageUsedKey());
  1056. Cache::forget('profile:embed:'.$status->profile_id);
  1057. Cache::forget($limitKey);
  1058. $res = StatusService::getMastodon($status->id, false);
  1059. $res['favourited'] = false;
  1060. $res['language'] = 'en';
  1061. $res['bookmarked'] = false;
  1062. $res['card'] = null;
  1063. return $this->json($res);
  1064. }
  1065. }