AdminApiController.php 19 KB

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