AdminApiController.php 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661
  1. <?php
  2. namespace App\Http\Controllers\Api;
  3. use Illuminate\Http\Request;
  4. use App\Http\Controllers\Controller;
  5. use App\Jobs\StatusPipeline\StatusDelete;
  6. use Auth, Cache, DB;
  7. use Carbon\Carbon;
  8. use App\{
  9. AccountInterstitial,
  10. Instance,
  11. Like,
  12. Notification,
  13. Media,
  14. Profile,
  15. Report,
  16. Status,
  17. User
  18. };
  19. use App\Services\AccountService;
  20. use App\Services\AdminStatsService;
  21. use App\Services\ConfigCacheService;
  22. use App\Services\InstanceService;
  23. use App\Services\ModLogService;
  24. use App\Services\SnowflakeService;
  25. use App\Services\StatusService;
  26. use App\Services\NetworkTimelineService;
  27. use App\Services\NotificationService;
  28. use App\Http\Resources\AdminInstance;
  29. use App\Http\Resources\AdminUser;
  30. class AdminApiController extends Controller
  31. {
  32. public function supported(Request $request)
  33. {
  34. abort_if(!$request->user(), 404);
  35. abort_unless($request->user()->is_admin == 1, 404);
  36. return response()->json(['supported' => true]);
  37. }
  38. public function getStats(Request $request)
  39. {
  40. abort_if(!$request->user(), 404);
  41. abort_unless($request->user()->is_admin == 1, 404);
  42. $res = AdminStatsService::summary();
  43. $res['autospam_count'] = AccountInterstitial::whereType('post.autospam')
  44. ->whereNull('appeal_handled_at')
  45. ->count();
  46. return $res;
  47. }
  48. public function autospam(Request $request)
  49. {
  50. abort_if(!$request->user(), 404);
  51. abort_unless($request->user()->is_admin == 1, 404);
  52. $appeals = AccountInterstitial::whereType('post.autospam')
  53. ->whereNull('appeal_handled_at')
  54. ->latest()
  55. ->simplePaginate(6)
  56. ->map(function($report) {
  57. $r = [
  58. 'id' => $report->id,
  59. 'type' => $report->type,
  60. 'item_id' => $report->item_id,
  61. 'item_type' => $report->item_type,
  62. 'created_at' => $report->created_at
  63. ];
  64. if($report->item_type === 'App\\Status') {
  65. $status = StatusService::get($report->item_id, false);
  66. if(!$status) {
  67. return;
  68. }
  69. $r['status'] = $status;
  70. if($status['in_reply_to_id']) {
  71. $r['parent'] = StatusService::get($status['in_reply_to_id'], false);
  72. }
  73. }
  74. return $r;
  75. });
  76. return $appeals;
  77. }
  78. public function autospamHandle(Request $request)
  79. {
  80. abort_if(!$request->user(), 404);
  81. abort_unless($request->user()->is_admin == 1, 404);
  82. $this->validate($request, [
  83. 'action' => 'required|in:dismiss,approve,dismiss-all,approve-all',
  84. 'id' => 'required'
  85. ]);
  86. $action = $request->input('action');
  87. $id = $request->input('id');
  88. $appeal = AccountInterstitial::whereType('post.autospam')
  89. ->whereNull('appeal_handled_at')
  90. ->findOrFail($id);
  91. $now = now();
  92. $res = ['status' => 'success'];
  93. $meta = json_decode($appeal->meta);
  94. if($action == 'dismiss') {
  95. $appeal->is_spam = true;
  96. $appeal->appeal_handled_at = $now;
  97. $appeal->save();
  98. Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id);
  99. Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id);
  100. Cache::forget('admin-dash:reports:spam-count');
  101. return $res;
  102. }
  103. if($action == 'dismiss-all') {
  104. AccountInterstitial::whereType('post.autospam')
  105. ->whereItemType('App\Status')
  106. ->whereNull('appeal_handled_at')
  107. ->whereUserId($appeal->user_id)
  108. ->update(['appeal_handled_at' => $now, 'is_spam' => true]);
  109. Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id);
  110. Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id);
  111. Cache::forget('admin-dash:reports:spam-count');
  112. return $res;
  113. }
  114. if($action == 'approve') {
  115. $status = $appeal->status;
  116. $status->is_nsfw = $meta->is_nsfw;
  117. $status->scope = 'public';
  118. $status->visibility = 'public';
  119. $status->save();
  120. $appeal->is_spam = false;
  121. $appeal->appeal_handled_at = now();
  122. $appeal->save();
  123. StatusService::del($status->id);
  124. Notification::whereAction('autospam.warning')
  125. ->whereProfileId($appeal->user->profile_id)
  126. ->get()
  127. ->each(function($n) use($appeal) {
  128. NotificationService::del($appeal->user->profile_id, $n->id);
  129. $n->forceDelete();
  130. });
  131. Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id);
  132. Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id);
  133. Cache::forget('admin-dash:reports:spam-count');
  134. return $res;
  135. }
  136. if($action == 'approve-all') {
  137. AccountInterstitial::whereType('post.autospam')
  138. ->whereItemType('App\Status')
  139. ->whereNull('appeal_handled_at')
  140. ->whereUserId($appeal->user_id)
  141. ->get()
  142. ->each(function($report) use($meta) {
  143. $report->is_spam = false;
  144. $report->appeal_handled_at = now();
  145. $report->save();
  146. $status = Status::find($report->item_id);
  147. if($status) {
  148. $status->is_nsfw = $meta->is_nsfw;
  149. $status->scope = 'public';
  150. $status->visibility = 'public';
  151. $status->save();
  152. StatusService::del($status->id, true);
  153. }
  154. Notification::whereAction('autospam.warning')
  155. ->whereProfileId($report->user->profile_id)
  156. ->get()
  157. ->each(function($n) use($report) {
  158. NotificationService::del($report->user->profile_id, $n->id);
  159. $n->forceDelete();
  160. });
  161. });
  162. Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id);
  163. Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id);
  164. Cache::forget('admin-dash:reports:spam-count');
  165. return $res;
  166. }
  167. return $res;
  168. }
  169. public function modReports(Request $request)
  170. {
  171. abort_if(!$request->user(), 404);
  172. abort_unless($request->user()->is_admin == 1, 404);
  173. $reports = Report::whereNull('admin_seen')
  174. ->orderBy('created_at','desc')
  175. ->paginate(6)
  176. ->map(function($report) {
  177. $r = [
  178. 'id' => $report->id,
  179. 'type' => $report->type,
  180. 'message' => $report->message,
  181. 'object_id' => $report->object_id,
  182. 'object_type' => $report->object_type,
  183. 'created_at' => $report->created_at
  184. ];
  185. if($report->profile_id) {
  186. $r['reported_by_account'] = AccountService::get($report->profile_id, true);
  187. }
  188. if($report->object_type === 'App\\Status') {
  189. $status = StatusService::get($report->object_id, false);
  190. if(!$status) {
  191. return;
  192. }
  193. $r['status'] = $status;
  194. if($status['in_reply_to_id']) {
  195. $r['parent'] = StatusService::get($status['in_reply_to_id'], false);
  196. }
  197. }
  198. if($report->object_type === 'App\\Profile') {
  199. $r['account'] = AccountService::get($report->object_id, false);
  200. }
  201. return $r;
  202. })
  203. ->filter()
  204. ->values();
  205. return $reports;
  206. }
  207. public function modReportHandle(Request $request)
  208. {
  209. abort_if(!$request->user(), 404);
  210. abort_unless($request->user()->is_admin == 1, 404);
  211. $this->validate($request, [
  212. 'action' => 'required|string',
  213. 'id' => 'required'
  214. ]);
  215. $action = $request->input('action');
  216. $id = $request->input('id');
  217. $actions = [
  218. 'ignore',
  219. 'cw',
  220. 'unlist'
  221. ];
  222. if (!in_array($action, $actions)) {
  223. return abort(403);
  224. }
  225. $report = Report::findOrFail($id);
  226. $item = $report->reported();
  227. $report->admin_seen = now();
  228. switch ($action) {
  229. case 'ignore':
  230. $report->not_interested = true;
  231. break;
  232. case 'cw':
  233. Cache::forget('status:thumb:'.$item->id);
  234. $item->is_nsfw = true;
  235. $item->save();
  236. $report->nsfw = true;
  237. StatusService::del($item->id, true);
  238. break;
  239. case 'unlist':
  240. $item->visibility = 'unlisted';
  241. $item->save();
  242. StatusService::del($item->id, true);
  243. break;
  244. default:
  245. $report->admin_seen = null;
  246. break;
  247. }
  248. $report->save();
  249. Cache::forget('admin-dash:reports:list-cache');
  250. Cache::forget('admin:dashboard:home:data:v0:15min');
  251. return ['success' => true];
  252. }
  253. public function getConfiguration(Request $request)
  254. {
  255. abort_if(!$request->user(), 404);
  256. abort_unless($request->user()->is_admin == 1, 404);
  257. abort_unless(config('instance.enable_cc'), 400);
  258. return collect([
  259. [
  260. 'name' => 'ActivityPub Federation',
  261. 'description' => 'Enable activitypub federation support, compatible with Pixelfed, Mastodon and other platforms.',
  262. 'key' => 'federation.activitypub.enabled'
  263. ],
  264. [
  265. 'name' => 'Open Registration',
  266. 'description' => 'Allow new account registrations.',
  267. 'key' => 'pixelfed.open_registration'
  268. ],
  269. [
  270. 'name' => 'Stories',
  271. 'description' => 'Enable the ephemeral Stories feature.',
  272. 'key' => 'instance.stories.enabled'
  273. ],
  274. [
  275. 'name' => 'Require Email Verification',
  276. 'description' => 'Require new accounts to verify their email address.',
  277. 'key' => 'pixelfed.enforce_email_verification'
  278. ],
  279. [
  280. 'name' => 'AutoSpam Detection',
  281. 'description' => 'Detect and remove spam from public timelines.',
  282. 'key' => 'pixelfed.bouncer.enabled'
  283. ],
  284. ])
  285. ->map(function($s) {
  286. $s['state'] = (bool) config_cache($s['key']);
  287. return $s;
  288. });
  289. }
  290. public function updateConfiguration(Request $request)
  291. {
  292. abort_if(!$request->user(), 404);
  293. abort_unless($request->user()->is_admin == 1, 404);
  294. abort_unless(config('instance.enable_cc'), 400);
  295. $this->validate($request, [
  296. 'key' => 'required',
  297. 'value' => 'required'
  298. ]);
  299. $allowedKeys = [
  300. 'federation.activitypub.enabled',
  301. 'pixelfed.open_registration',
  302. 'instance.stories.enabled',
  303. 'pixelfed.enforce_email_verification',
  304. 'pixelfed.bouncer.enabled',
  305. ];
  306. $key = $request->input('key');
  307. $value = (bool) filter_var($request->input('value'), FILTER_VALIDATE_BOOLEAN);
  308. abort_if(!in_array($key, $allowedKeys), 400, 'Invalid cache key.');
  309. ConfigCacheService::put($key, $value);
  310. return collect([
  311. [
  312. 'name' => 'ActivityPub Federation',
  313. 'description' => 'Enable activitypub federation support, compatible with Pixelfed, Mastodon and other platforms.',
  314. 'key' => 'federation.activitypub.enabled'
  315. ],
  316. [
  317. 'name' => 'Open Registration',
  318. 'description' => 'Allow new account registrations.',
  319. 'key' => 'pixelfed.open_registration'
  320. ],
  321. [
  322. 'name' => 'Stories',
  323. 'description' => 'Enable the ephemeral Stories feature.',
  324. 'key' => 'instance.stories.enabled'
  325. ],
  326. [
  327. 'name' => 'Require Email Verification',
  328. 'description' => 'Require new accounts to verify their email address.',
  329. 'key' => 'pixelfed.enforce_email_verification'
  330. ],
  331. [
  332. 'name' => 'AutoSpam Detection',
  333. 'description' => 'Detect and remove spam from public timelines.',
  334. 'key' => 'pixelfed.bouncer.enabled'
  335. ],
  336. ])
  337. ->map(function($s) {
  338. $s['state'] = (bool) config_cache($s['key']);
  339. return $s;
  340. });
  341. }
  342. public function getUsers(Request $request)
  343. {
  344. abort_if(!$request->user(), 404);
  345. abort_unless($request->user()->is_admin == 1, 404);
  346. $q = $request->input('q');
  347. $sort = $request->input('sort', 'desc') === 'asc' ? 'asc' : 'desc';
  348. $res = User::whereNull('status')
  349. ->when($q, function($query, $q) {
  350. return $query->where('username', 'like', '%' . $q . '%');
  351. })
  352. ->orderBy('id', $sort)
  353. ->cursorPaginate(10);
  354. return AdminUser::collection($res);
  355. }
  356. public function getUser(Request $request)
  357. {
  358. abort_if(!$request->user(), 404);
  359. abort_unless($request->user()->is_admin == 1, 404);
  360. $id = $request->input('user_id');
  361. $user = User::findOrFail($id);
  362. $profile = $user->profile;
  363. $account = AccountService::get($user->profile_id, true);
  364. return (new AdminUser($user))->additional(['meta' => [
  365. 'account' => $account,
  366. 'moderation' => [
  367. 'unlisted' => (bool) $profile->unlisted,
  368. 'cw' => (bool) $profile->cw,
  369. 'no_autolink' => (bool) $profile->no_autolink
  370. ]
  371. ]]);
  372. }
  373. public function userAdminAction(Request $request)
  374. {
  375. abort_if(!$request->user(), 404);
  376. abort_unless($request->user()->is_admin == 1, 404);
  377. $this->validate($request, [
  378. 'id' => 'required',
  379. 'action' => 'required|in:unlisted,cw,no_autolink,refresh_stats,verify_email',
  380. 'value' => 'sometimes'
  381. ]);
  382. $id = $request->input('id');
  383. $user = User::findOrFail($id);
  384. $profile = Profile::findOrFail($user->profile_id);
  385. $action = $request->input('action');
  386. abort_if($user->is_admin == true && $action !== 'refresh_stats', 400, 'Cannot moderate admin accounts');
  387. if($action === 'refresh_stats') {
  388. $profile->following_count = DB::table('followers')->whereProfileId($user->profile_id)->count();
  389. $profile->followers_count = DB::table('followers')->whereFollowingId($user->profile_id)->count();
  390. $statusCount = Status::whereProfileId($user->profile_id)
  391. ->whereNull('in_reply_to_id')
  392. ->whereNull('reblog_of_id')
  393. ->whereIn('scope', ['public', 'unlisted', 'private'])
  394. ->count();
  395. $profile->status_count = $statusCount;
  396. $profile->save();
  397. } else if($action === 'verify_email') {
  398. $user->email_verified_at = now();
  399. $user->save();
  400. ModLogService::boot()
  401. ->objectUid($user->id)
  402. ->objectId($user->id)
  403. ->objectType('App\User::class')
  404. ->user($request->user())
  405. ->action('admin.user.moderate')
  406. ->metadata([
  407. 'action' => 'Manually verified email address',
  408. 'message' => 'Success!'
  409. ])
  410. ->accessLevel('admin')
  411. ->save();
  412. } else {
  413. $profile->{$action} = filter_var($request->input('value'), FILTER_VALIDATE_BOOLEAN);
  414. $profile->save();
  415. ModLogService::boot()
  416. ->objectUid($user->id)
  417. ->objectId($user->id)
  418. ->objectType('App\User::class')
  419. ->user($request->user())
  420. ->action('admin.user.moderate')
  421. ->metadata([
  422. 'action' => $action,
  423. 'message' => 'Success!'
  424. ])
  425. ->accessLevel('admin')
  426. ->save();
  427. }
  428. AccountService::del($user->profile_id);
  429. $account = AccountService::get($user->profile_id, true);
  430. return (new AdminUser($user))->additional(['meta' => [
  431. 'account' => $account,
  432. 'moderation' => [
  433. 'unlisted' => (bool) $profile->unlisted,
  434. 'cw' => (bool) $profile->cw,
  435. 'no_autolink' => (bool) $profile->no_autolink
  436. ]
  437. ]]);
  438. }
  439. public function instances(Request $request)
  440. {
  441. abort_if(!$request->user(), 404);
  442. abort_unless($request->user()->is_admin == 1, 404);
  443. $this->validate($request, [
  444. 'q' => 'sometimes',
  445. 'sort' => 'sometimes|in:asc,desc',
  446. 'sort_by' => 'sometimes|in:id,status_count,user_count,domain',
  447. 'filter' => 'sometimes|in:all,unlisted,auto_cw,banned',
  448. ]);
  449. $q = $request->input('q');
  450. $sort = $request->input('sort', 'desc') === 'asc' ? 'asc' : 'desc';
  451. $sortBy = $request->input('sort_by', 'id');
  452. $filter = $request->input('filter');
  453. $res = Instance::when($q, function($query, $q) {
  454. return $query->where('domain', 'like', '%' . $q . '%');
  455. })
  456. ->when($filter, function($query, $filter) {
  457. if($filter === 'all') {
  458. return $query;
  459. } else {
  460. return $query->where($filter, true);
  461. }
  462. })
  463. ->when($sortBy, function($query, $sortBy) use($sort) {
  464. return $query->orderBy($sortBy, $sort);
  465. }, function($query) {
  466. return $query->orderBy('id', 'desc');
  467. })
  468. ->cursorPaginate(10)
  469. ->withQueryString();
  470. return AdminInstance::collection($res);
  471. }
  472. public function getInstance(Request $request)
  473. {
  474. abort_if(!$request->user(), 404);
  475. abort_unless($request->user()->is_admin == 1, 404);
  476. $id = $request->input('id');
  477. $res = Instance::findOrFail($id);
  478. return new AdminInstance($res);
  479. }
  480. public function moderateInstance(Request $request)
  481. {
  482. abort_if(!$request->user(), 404);
  483. abort_unless($request->user()->is_admin == 1, 404);
  484. $this->validate($request, [
  485. 'id' => 'required',
  486. 'key' => 'required|in:unlisted,auto_cw,banned',
  487. 'value' => 'required'
  488. ]);
  489. $id = $request->input('id');
  490. $key = $request->input('key');
  491. $value = (bool) filter_var($request->input('value'), FILTER_VALIDATE_BOOLEAN);
  492. $res = Instance::findOrFail($id);
  493. $res->{$key} = $value;
  494. $res->save();
  495. InstanceService::refresh();
  496. NetworkTimelineService::warmCache(true);
  497. return new AdminInstance($res);
  498. }
  499. public function refreshInstanceStats(Request $request)
  500. {
  501. abort_if(!$request->user(), 404);
  502. abort_unless($request->user()->is_admin == 1, 404);
  503. $this->validate($request, [
  504. 'id' => 'required',
  505. ]);
  506. $id = $request->input('id');
  507. $instance = Instance::findOrFail($id);
  508. $instance->user_count = Profile::whereDomain($instance->domain)->count();
  509. $instance->status_count = Profile::whereDomain($instance->domain)->leftJoin('statuses', 'profiles.id', '=', 'statuses.profile_id')->count();
  510. $instance->save();
  511. return new AdminInstance($instance);
  512. }
  513. public function getAllStats(Request $request)
  514. {
  515. abort_if(!$request->user(), 404);
  516. abort_unless($request->user()->is_admin === 1, 404);
  517. if($request->has('refresh')) {
  518. Cache::forget('admin-api:instance-all-stats_v1');
  519. }
  520. return Cache::remember('admin-api:instance-all-stats-v1', 1209600, function() {
  521. $days = range(0, 6);
  522. $res = [
  523. 'cached_at' => now()->format('c'),
  524. ];
  525. $minStatusId = SnowflakeService::byDate(now()->subDays(8));
  526. foreach($days as $day) {
  527. $label = now()->subDays($day)->format('D');
  528. $labelShort = substr($label, 0, 1);
  529. $res['users']['days'][] = [
  530. 'date' => now()->subDays($day)->format('M j Y'),
  531. 'label_full' => $label,
  532. 'label' => $labelShort,
  533. 'count' => User::whereDate('created_at', now()->subDays($day))->count()
  534. ];
  535. $res['posts']['days'][] = [
  536. 'date' => now()->subDays($day)->format('M j Y'),
  537. 'label_full' => $label,
  538. 'label' => $labelShort,
  539. 'count' => Status::where('id', '>', $minStatusId)->whereNull('uri')->whereDate('created_at', now()->subDays($day))->count()
  540. ];
  541. $res['instances']['days'][] = [
  542. 'date' => now()->subDays($day)->format('M j Y'),
  543. 'label_full' => $label,
  544. 'label' => $labelShort,
  545. 'count' => Instance::whereDate('created_at', now()->subDays($day))->count()
  546. ];
  547. }
  548. $res['users']['total'] = DB::table('users')->count();
  549. $res['users']['min'] = collect($res['users']['days'])->min('count');
  550. $res['users']['max'] = $res['users']['total'];
  551. $res['users']['change'] = $res['users']['total'] - $res['users']['min'];
  552. $res['posts']['total'] = DB::table('statuses')->whereNull('uri')->count();
  553. $res['posts']['min'] = collect($res['posts']['days'])->min('count');
  554. $res['posts']['max'] = collect($res['posts']['days'])->max('count');
  555. $res['posts']['change'] = $res['posts']['total'] - $res['posts']['min'];
  556. $res['instances']['total'] = DB::table('instances')->count();
  557. $res['instances']['min'] = collect($res['instances']['days'])->min('count');
  558. $res['instances']['max'] = collect($res['instances']['days'])->max('count');
  559. $res['instances']['change'] = $res['instances']['total'] - $res['instances']['min'];
  560. return $res;
  561. });
  562. }
  563. }