1
0

AdminApiController.php 20 KB

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