ApiV1Dot1Controller.php 38 KB

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