ApiV1Dot1Controller.php 49 KB

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