ApiV1Dot1Controller.php 49 KB

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