AdminApiController.php 24 KB

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