AdminReportController.php 45 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330
  1. <?php
  2. namespace App\Http\Controllers\Admin;
  3. use App\AccountInterstitial;
  4. use App\Http\Resources\AdminReport;
  5. use App\Http\Resources\AdminSpamReport;
  6. use App\Jobs\DeletePipeline\DeleteAccountPipeline;
  7. use App\Jobs\DeletePipeline\DeleteRemoteProfilePipeline;
  8. use App\Jobs\StatusPipeline\RemoteStatusDelete;
  9. use App\Jobs\StatusPipeline\StatusDelete;
  10. use App\Jobs\StoryPipeline\StoryDelete;
  11. use App\Notification;
  12. use App\Profile;
  13. use App\Report;
  14. use App\Services\AccountService;
  15. use App\Services\ModLogService;
  16. use App\Services\NetworkTimelineService;
  17. use App\Services\NotificationService;
  18. use App\Services\PublicTimelineService;
  19. use App\Services\StatusService;
  20. use App\Status;
  21. use App\Story;
  22. use App\User;
  23. use Cache;
  24. use Carbon\Carbon;
  25. use Illuminate\Http\Request;
  26. use Illuminate\Support\Facades\DB;
  27. use Illuminate\Support\Facades\Redis;
  28. trait AdminReportController
  29. {
  30. public function reports(Request $request)
  31. {
  32. $filter = $request->input('filter') == 'closed' ? 'closed' : 'open';
  33. $page = $request->input('page') ?? 1;
  34. $ai = Cache::remember('admin-dash:reports:ai-count', 3600, function () {
  35. return AccountInterstitial::whereNotNull('appeal_requested_at')->whereNull('appeal_handled_at')->count();
  36. });
  37. $spam = Cache::remember('admin-dash:reports:spam-count', 3600, function () {
  38. return AccountInterstitial::whereType('post.autospam')->whereNull('appeal_handled_at')->count();
  39. });
  40. $mailVerifications = Redis::scard('email:manual');
  41. if ($filter == 'open' && $page == 1) {
  42. $reports = Cache::remember('admin-dash:reports:list-cache', 300, function () use ($filter) {
  43. return Report::whereHas('status')
  44. ->whereHas('reportedUser')
  45. ->whereHas('reporter')
  46. ->orderBy('created_at', 'desc')
  47. ->when($filter, function ($q, $filter) {
  48. return $filter == 'open' ?
  49. $q->whereNull('admin_seen') :
  50. $q->whereNotNull('admin_seen');
  51. })
  52. ->paginate(6);
  53. });
  54. } else {
  55. $reports = Report::whereHas('status')
  56. ->whereHas('reportedUser')
  57. ->whereHas('reporter')
  58. ->orderBy('created_at', 'desc')
  59. ->when($filter, function ($q, $filter) {
  60. return $filter == 'open' ?
  61. $q->whereNull('admin_seen') :
  62. $q->whereNotNull('admin_seen');
  63. })
  64. ->paginate(6);
  65. }
  66. return view('admin.reports.home', compact('reports', 'ai', 'spam', 'mailVerifications'));
  67. }
  68. public function showReport(Request $request, $id)
  69. {
  70. $report = Report::with('status')->findOrFail($id);
  71. if ($request->has('ref') && $request->input('ref') == 'email') {
  72. return redirect('/i/admin/reports?tab=report&id='.$report->id);
  73. }
  74. return view('admin.reports.show', compact('report'));
  75. }
  76. public function appeals(Request $request)
  77. {
  78. $appeals = AccountInterstitial::whereNotNull('appeal_requested_at')
  79. ->whereNull('appeal_handled_at')
  80. ->latest()
  81. ->paginate(6);
  82. return view('admin.reports.appeals', compact('appeals'));
  83. }
  84. public function showAppeal(Request $request, $id)
  85. {
  86. $appeal = AccountInterstitial::whereNotNull('appeal_requested_at')
  87. ->whereNull('appeal_handled_at')
  88. ->findOrFail($id);
  89. $meta = json_decode($appeal->meta);
  90. return view('admin.reports.show_appeal', compact('appeal', 'meta'));
  91. }
  92. public function spam(Request $request)
  93. {
  94. $this->validate($request, [
  95. 'tab' => 'sometimes|in:home,not-spam,spam,settings,custom,exemptions',
  96. ]);
  97. $tab = $request->input('tab', 'home');
  98. $openCount = Cache::remember('admin-dash:reports:spam-count', 3600, function () {
  99. return AccountInterstitial::whereType('post.autospam')
  100. ->whereNull('appeal_handled_at')
  101. ->count();
  102. });
  103. $monthlyCount = Cache::remember('admin-dash:reports:spam-count:30d', 43200, function () {
  104. return AccountInterstitial::whereType('post.autospam')
  105. ->where('created_at', '>', now()->subMonth())
  106. ->count();
  107. });
  108. $totalCount = Cache::remember('admin-dash:reports:spam-count:total', 43200, function () {
  109. return AccountInterstitial::whereType('post.autospam')->count();
  110. });
  111. $uncategorized = Cache::remember('admin-dash:reports:spam-sync', 3600, function () {
  112. return AccountInterstitial::whereType('post.autospam')
  113. ->whereIsSpam(null)
  114. ->whereNotNull('appeal_handled_at')
  115. ->exists();
  116. });
  117. $avg = Cache::remember('admin-dash:reports:spam-count:avg', 43200, function () {
  118. if (config('database.default') != 'mysql') {
  119. return 0;
  120. }
  121. return AccountInterstitial::selectRaw('*, count(id) as counter')
  122. ->whereType('post.autospam')
  123. ->groupBy('user_id')
  124. ->get()
  125. ->avg('counter');
  126. });
  127. $avgOpen = Cache::remember('admin-dash:reports:spam-count:avgopen', 43200, function () {
  128. if (config('database.default') != 'mysql') {
  129. return '0';
  130. }
  131. $seconds = AccountInterstitial::selectRaw('DATE(created_at) AS start_date, AVG(TIME_TO_SEC(TIMEDIFF(appeal_handled_at, created_at))) AS timediff')->whereType('post.autospam')->whereNotNull('appeal_handled_at')->where('created_at', '>', now()->subMonth())->get();
  132. if (! $seconds) {
  133. return '0';
  134. }
  135. $mins = floor($seconds->avg('timediff') / 60);
  136. if ($mins < 60) {
  137. return $mins.' min(s)';
  138. }
  139. if ($mins < 2880) {
  140. return floor($mins / 60).' hour(s)';
  141. }
  142. return floor($mins / 60 / 24).' day(s)';
  143. });
  144. $avgCount = $totalCount && $avg ? floor($totalCount / $avg) : '0';
  145. if (in_array($tab, ['home', 'spam', 'not-spam'])) {
  146. $appeals = AccountInterstitial::whereType('post.autospam')
  147. ->when($tab, function ($q, $tab) {
  148. switch ($tab) {
  149. case 'home':
  150. return $q->whereNull('appeal_handled_at');
  151. break;
  152. case 'spam':
  153. return $q->whereIsSpam(true);
  154. break;
  155. case 'not-spam':
  156. return $q->whereIsSpam(false);
  157. break;
  158. }
  159. })
  160. ->latest()
  161. ->paginate(6);
  162. if ($tab !== 'home') {
  163. $appeals = $appeals->appends(['tab' => $tab]);
  164. }
  165. } else {
  166. $appeals = new class
  167. {
  168. public function count()
  169. {
  170. return 0;
  171. }
  172. public function render()
  173. {
  174. }
  175. };
  176. }
  177. return view('admin.reports.spam', compact('tab', 'appeals', 'openCount', 'monthlyCount', 'totalCount', 'avgCount', 'avgOpen', 'uncategorized'));
  178. }
  179. public function showSpam(Request $request, $id)
  180. {
  181. $appeal = AccountInterstitial::whereType('post.autospam')
  182. ->findOrFail($id);
  183. if ($request->has('ref') && $request->input('ref') == 'email') {
  184. return redirect('/i/admin/reports?tab=autospam&id='.$appeal->id);
  185. }
  186. $meta = json_decode($appeal->meta);
  187. return view('admin.reports.show_spam', compact('appeal', 'meta'));
  188. }
  189. public function fixUncategorizedSpam(Request $request)
  190. {
  191. if (Cache::get('admin-dash:reports:spam-sync-active')) {
  192. return redirect('/i/admin/reports/autospam');
  193. }
  194. Cache::put('admin-dash:reports:spam-sync-active', 1, 900);
  195. AccountInterstitial::chunk(500, function ($reports) {
  196. foreach ($reports as $report) {
  197. if ($report->item_type != 'App\Status') {
  198. continue;
  199. }
  200. if ($report->type != 'post.autospam') {
  201. continue;
  202. }
  203. if ($report->is_spam != null) {
  204. continue;
  205. }
  206. $status = StatusService::get($report->item_id, false);
  207. if (! $status) {
  208. return;
  209. }
  210. $scope = $status['visibility'];
  211. $report->is_spam = $scope == 'unlisted';
  212. $report->in_violation = $report->is_spam;
  213. $report->severity_index = 1;
  214. $report->save();
  215. }
  216. });
  217. Cache::forget('admin-dash:reports:spam-sync');
  218. return redirect('/i/admin/reports/autospam');
  219. }
  220. public function updateSpam(Request $request, $id)
  221. {
  222. $this->validate($request, [
  223. 'action' => 'required|in:dismiss,approve,dismiss-all,approve-all,delete-account,mark-spammer',
  224. ]);
  225. $action = $request->input('action');
  226. $appeal = AccountInterstitial::whereType('post.autospam')
  227. ->whereNull('appeal_handled_at')
  228. ->findOrFail($id);
  229. $meta = json_decode($appeal->meta);
  230. $res = ['status' => 'success'];
  231. $now = now();
  232. Cache::forget('admin-dash:reports:spam-count:total');
  233. Cache::forget('admin-dash:reports:spam-count:30d');
  234. if ($action == 'delete-account') {
  235. if (config('pixelfed.account_deletion') == false) {
  236. abort(404);
  237. }
  238. $user = User::findOrFail($appeal->user_id);
  239. $profile = $user->profile;
  240. if ($user->is_admin == true) {
  241. $mid = $request->user()->id;
  242. abort_if($user->id < $mid, 403);
  243. }
  244. $ts = now()->addMonth();
  245. $user->status = 'delete';
  246. $profile->status = 'delete';
  247. $user->delete_after = $ts;
  248. $profile->delete_after = $ts;
  249. $user->save();
  250. $profile->save();
  251. ModLogService::boot()
  252. ->objectUid($user->id)
  253. ->objectId($user->id)
  254. ->objectType('App\User::class')
  255. ->user($request->user())
  256. ->action('admin.user.delete')
  257. ->accessLevel('admin')
  258. ->save();
  259. Cache::forget('profiles:private');
  260. DeleteAccountPipeline::dispatch($user);
  261. return;
  262. }
  263. if ($action == 'dismiss') {
  264. $appeal->is_spam = true;
  265. $appeal->appeal_handled_at = $now;
  266. $appeal->save();
  267. Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$appeal->user->profile_id);
  268. Cache::forget('pf:bouncer_v0:recent_by_pid:'.$appeal->user->profile_id);
  269. Cache::forget('admin-dash:reports:spam-count');
  270. return $res;
  271. }
  272. if ($action == 'dismiss-all') {
  273. AccountInterstitial::whereType('post.autospam')
  274. ->whereItemType('App\Status')
  275. ->whereNull('appeal_handled_at')
  276. ->whereUserId($appeal->user_id)
  277. ->update(['appeal_handled_at' => $now, 'is_spam' => true]);
  278. Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$appeal->user->profile_id);
  279. Cache::forget('pf:bouncer_v0:recent_by_pid:'.$appeal->user->profile_id);
  280. Cache::forget('admin-dash:reports:spam-count');
  281. return $res;
  282. }
  283. if ($action == 'approve-all') {
  284. AccountInterstitial::whereType('post.autospam')
  285. ->whereItemType('App\Status')
  286. ->whereNull('appeal_handled_at')
  287. ->whereUserId($appeal->user_id)
  288. ->get()
  289. ->each(function ($report) use ($meta) {
  290. $report->is_spam = false;
  291. $report->appeal_handled_at = now();
  292. $report->save();
  293. $status = Status::find($report->item_id);
  294. if ($status) {
  295. $status->is_nsfw = $meta->is_nsfw;
  296. $status->scope = 'public';
  297. $status->visibility = 'public';
  298. $status->save();
  299. StatusService::del($status->id, true);
  300. }
  301. });
  302. Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$appeal->user->profile_id);
  303. Cache::forget('pf:bouncer_v0:recent_by_pid:'.$appeal->user->profile_id);
  304. Cache::forget('admin-dash:reports:spam-count');
  305. return $res;
  306. }
  307. if ($action == 'mark-spammer') {
  308. AccountInterstitial::whereType('post.autospam')
  309. ->whereItemType('App\Status')
  310. ->whereNull('appeal_handled_at')
  311. ->whereUserId($appeal->user_id)
  312. ->update(['appeal_handled_at' => $now, 'is_spam' => true]);
  313. $pro = Profile::whereUserId($appeal->user_id)->firstOrFail();
  314. $pro->update([
  315. 'unlisted' => true,
  316. 'cw' => true,
  317. 'no_autolink' => true,
  318. ]);
  319. Status::whereProfileId($pro->id)
  320. ->get()
  321. ->each(function ($report) {
  322. $status->is_nsfw = $meta->is_nsfw;
  323. $status->scope = 'public';
  324. $status->visibility = 'public';
  325. $status->save();
  326. StatusService::del($status->id, true);
  327. });
  328. Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$appeal->user->profile_id);
  329. Cache::forget('pf:bouncer_v0:recent_by_pid:'.$appeal->user->profile_id);
  330. Cache::forget('admin-dash:reports:spam-count');
  331. return $res;
  332. }
  333. $status = $appeal->status;
  334. $status->is_nsfw = $meta->is_nsfw;
  335. $status->scope = 'public';
  336. $status->visibility = 'public';
  337. $status->save();
  338. $appeal->is_spam = false;
  339. $appeal->appeal_handled_at = now();
  340. $appeal->save();
  341. StatusService::del($status->id);
  342. Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$appeal->user->profile_id);
  343. Cache::forget('pf:bouncer_v0:recent_by_pid:'.$appeal->user->profile_id);
  344. Cache::forget('admin-dash:reports:spam-count');
  345. return $res;
  346. }
  347. public function updateAppeal(Request $request, $id)
  348. {
  349. $this->validate($request, [
  350. 'action' => 'required|in:dismiss,approve',
  351. ]);
  352. $action = $request->input('action');
  353. $appeal = AccountInterstitial::whereNotNull('appeal_requested_at')
  354. ->whereNull('appeal_handled_at')
  355. ->findOrFail($id);
  356. if ($action == 'dismiss') {
  357. $appeal->appeal_handled_at = now();
  358. $appeal->save();
  359. Cache::forget('admin-dash:reports:ai-count');
  360. return redirect('/i/admin/reports/appeals');
  361. }
  362. switch ($appeal->type) {
  363. case 'post.cw':
  364. $status = $appeal->status;
  365. $status->is_nsfw = false;
  366. $status->save();
  367. break;
  368. case 'post.unlist':
  369. $status = $appeal->status;
  370. $status->scope = 'public';
  371. $status->visibility = 'public';
  372. $status->save();
  373. break;
  374. default:
  375. // code...
  376. break;
  377. }
  378. $appeal->appeal_handled_at = now();
  379. $appeal->save();
  380. StatusService::del($status->id, true);
  381. Cache::forget('admin-dash:reports:ai-count');
  382. return redirect('/i/admin/reports/appeals');
  383. }
  384. public function updateReport(Request $request, $id)
  385. {
  386. $this->validate($request, [
  387. 'action' => 'required|string',
  388. ]);
  389. $action = $request->input('action');
  390. $actions = [
  391. 'ignore',
  392. 'cw',
  393. 'unlist',
  394. 'delete',
  395. 'shadowban',
  396. 'ban',
  397. ];
  398. if (! in_array($action, $actions)) {
  399. return abort(403);
  400. }
  401. $report = Report::findOrFail($id);
  402. $this->handleReportAction($report, $action);
  403. Cache::forget('admin-dash:reports:list-cache');
  404. return response()->json(['msg' => 'Success']);
  405. }
  406. public function handleReportAction(Report $report, $action)
  407. {
  408. $item = $report->reported();
  409. $report->admin_seen = Carbon::now();
  410. switch ($action) {
  411. case 'ignore':
  412. $report->not_interested = true;
  413. break;
  414. case 'cw':
  415. Cache::forget('status:thumb:'.$item->id);
  416. $item->is_nsfw = true;
  417. $item->save();
  418. $report->nsfw = true;
  419. StatusService::del($item->id, true);
  420. break;
  421. case 'unlist':
  422. $item->visibility = 'unlisted';
  423. $item->save();
  424. Cache::forget('profiles:private');
  425. StatusService::del($item->id, true);
  426. break;
  427. case 'delete':
  428. // Todo: fire delete job
  429. $report->admin_seen = null;
  430. StatusService::del($item->id, true);
  431. break;
  432. case 'shadowban':
  433. // Todo: fire delete job
  434. $report->admin_seen = null;
  435. break;
  436. case 'ban':
  437. // Todo: fire delete job
  438. $report->admin_seen = null;
  439. break;
  440. default:
  441. $report->admin_seen = null;
  442. break;
  443. }
  444. $report->save();
  445. return $this;
  446. }
  447. protected function actionMap()
  448. {
  449. return [
  450. '1' => 'ignore',
  451. '2' => 'cw',
  452. '3' => 'unlist',
  453. '4' => 'delete',
  454. '5' => 'shadowban',
  455. '6' => 'ban',
  456. ];
  457. }
  458. public function bulkUpdateReport(Request $request)
  459. {
  460. $this->validate($request, [
  461. 'action' => 'required|integer|min:1|max:10',
  462. 'ids' => 'required|array',
  463. ]);
  464. $action = $this->actionMap()[$request->input('action')];
  465. $ids = $request->input('ids');
  466. $reports = Report::whereIn('id', $ids)->whereNull('admin_seen')->get();
  467. foreach ($reports as $report) {
  468. $this->handleReportAction($report, $action);
  469. }
  470. $res = [
  471. 'message' => 'Success',
  472. 'code' => 200,
  473. ];
  474. return response()->json($res);
  475. }
  476. public function reportMailVerifications(Request $request)
  477. {
  478. $ids = Redis::smembers('email:manual');
  479. $ignored = Redis::smembers('email:manual-ignored');
  480. $reports = [];
  481. if ($ids) {
  482. $reports = collect($ids)
  483. ->filter(function ($id) use ($ignored) {
  484. return ! in_array($id, $ignored);
  485. })
  486. ->map(function ($id) {
  487. $user = User::whereProfileId($id)->first();
  488. if (! $user || $user->email_verified_at) {
  489. return [];
  490. }
  491. $account = AccountService::get($id, true);
  492. if (! $account) {
  493. return [];
  494. }
  495. $account['email'] = $user->email;
  496. return $account;
  497. })
  498. ->filter(function ($res) {
  499. return $res && isset($res['id']);
  500. })
  501. ->values();
  502. }
  503. return view('admin.reports.mail_verification', compact('reports', 'ignored'));
  504. }
  505. public function reportMailVerifyIgnore(Request $request)
  506. {
  507. $id = $request->input('id');
  508. Redis::sadd('email:manual-ignored', $id);
  509. return redirect('/i/admin/reports');
  510. }
  511. public function reportMailVerifyApprove(Request $request)
  512. {
  513. $id = $request->input('id');
  514. $user = User::whereProfileId($id)->firstOrFail();
  515. Redis::srem('email:manual', $id);
  516. Redis::srem('email:manual-ignored', $id);
  517. $user->email_verified_at = now();
  518. $user->save();
  519. return redirect('/i/admin/reports');
  520. }
  521. public function reportMailVerifyClearIgnored(Request $request)
  522. {
  523. Redis::del('email:manual-ignored');
  524. return [200];
  525. }
  526. public function reportsStats(Request $request)
  527. {
  528. $stats = [
  529. 'total' => Report::count(),
  530. 'open' => Report::whereNull('admin_seen')->count(),
  531. 'closed' => Report::whereNotNull('admin_seen')->count(),
  532. 'autospam' => AccountInterstitial::whereType('post.autospam')->count(),
  533. 'autospam_open' => AccountInterstitial::whereType('post.autospam')->whereNull(['appeal_handled_at'])->count(),
  534. 'appeals' => AccountInterstitial::whereNotNull('appeal_requested_at')->whereNull('appeal_handled_at')->count(),
  535. 'email_verification_requests' => Redis::scard('email:manual'),
  536. ];
  537. return $stats;
  538. }
  539. public function reportsApiAll(Request $request)
  540. {
  541. $filter = $request->input('filter') == 'closed' ? 'closed' : 'open';
  542. $reports = AdminReport::collection(
  543. Report::orderBy('id', 'desc')
  544. ->when($filter, function ($q, $filter) {
  545. return $filter == 'open' ?
  546. $q->whereNull('admin_seen') :
  547. $q->whereNotNull('admin_seen');
  548. })
  549. ->groupBy(['id', 'object_id', 'object_type', 'profile_id'])
  550. ->cursorPaginate(6)
  551. ->withQueryString()
  552. );
  553. return $reports;
  554. }
  555. public function reportsApiGet(Request $request, $id)
  556. {
  557. $report = Report::findOrFail($id);
  558. return new AdminReport($report);
  559. }
  560. public function reportsApiHandle(Request $request)
  561. {
  562. $this->validate($request, [
  563. 'object_id' => 'required',
  564. 'object_type' => 'required',
  565. 'id' => 'required',
  566. 'action' => 'required|in:ignore,nsfw,unlist,private,delete,delete-all',
  567. 'action_type' => 'required|in:post,profile,story',
  568. ]);
  569. $report = Report::whereObjectId($request->input('object_id'))->findOrFail($request->input('id'));
  570. if ($request->input('action_type') === 'profile') {
  571. return $this->reportsHandleProfileAction($report, $request->input('action'));
  572. } elseif ($request->input('action_type') === 'post') {
  573. return $this->reportsHandleStatusAction($report, $request->input('action'));
  574. } elseif ($request->input('action_type') === 'story') {
  575. return $this->reportsHandleStoryAction($report, $request->input('action'));
  576. }
  577. return $report;
  578. }
  579. protected function reportsHandleStoryAction($report, $action)
  580. {
  581. switch ($action) {
  582. case 'ignore':
  583. Report::whereObjectId($report->object_id)
  584. ->whereObjectType($report->object_type)
  585. ->update([
  586. 'admin_seen' => now(),
  587. ]);
  588. return [200];
  589. break;
  590. case 'delete':
  591. $profile = Profile::find($report->reported_profile_id);
  592. $story = Story::whereProfileId($profile->id)->find($report->object_id);
  593. abort_if(! $story, 400, 'Invalid or missing story');
  594. $story->active = false;
  595. $story->save();
  596. ModLogService::boot()
  597. ->objectUid($profile->id)
  598. ->objectId($report->object_id)
  599. ->objectType('App\Story::class')
  600. ->user(request()->user())
  601. ->action('admin.user.moderate')
  602. ->metadata([
  603. 'action' => 'delete',
  604. 'message' => 'Success!',
  605. ])
  606. ->accessLevel('admin')
  607. ->save();
  608. Report::whereObjectId($report->object_id)
  609. ->whereObjectType($report->object_type)
  610. ->update([
  611. 'admin_seen' => now(),
  612. ]);
  613. StoryDelete::dispatch($story)->onQueue('story');
  614. return [200];
  615. break;
  616. case 'delete-all':
  617. $profile = Profile::find($report->reported_profile_id);
  618. $stories = Story::whereProfileId($profile->id)->whereActive(true)->get();
  619. abort_if(! $stories || ! $stories->count(), 400, 'Invalid or missing stories');
  620. ModLogService::boot()
  621. ->objectUid($profile->id)
  622. ->objectId($report->object_id)
  623. ->objectType('App\Story::class')
  624. ->user(request()->user())
  625. ->action('admin.user.moderate')
  626. ->metadata([
  627. 'action' => 'delete-all',
  628. 'message' => 'Success!',
  629. ])
  630. ->accessLevel('admin')
  631. ->save();
  632. Report::where('reported_profile_id', $profile->id)
  633. ->whereObjectType('App\Story')
  634. ->whereNull('admin_seen')
  635. ->update([
  636. 'admin_seen' => now(),
  637. ]);
  638. $stories->each(function ($story) {
  639. StoryDelete::dispatch($story)->onQueue('story');
  640. });
  641. return [200];
  642. break;
  643. }
  644. }
  645. protected function reportsHandleProfileAction($report, $action)
  646. {
  647. switch ($action) {
  648. case 'ignore':
  649. Report::whereObjectId($report->object_id)
  650. ->whereObjectType($report->object_type)
  651. ->update([
  652. 'admin_seen' => now(),
  653. ]);
  654. return [200];
  655. break;
  656. case 'nsfw':
  657. if ($report->object_type === 'App\Profile') {
  658. $profile = Profile::find($report->object_id);
  659. } elseif ($report->object_type === 'App\Status') {
  660. $status = Status::find($report->object_id);
  661. if (! $status) {
  662. return [200];
  663. }
  664. $profile = Profile::find($status->profile_id);
  665. }
  666. if (! $profile) {
  667. return;
  668. }
  669. abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot moderate an admin account.');
  670. $profile->cw = true;
  671. $profile->save();
  672. foreach (Status::whereProfileId($profile->id)->cursor() as $status) {
  673. $status->is_nsfw = true;
  674. $status->save();
  675. StatusService::del($status->id);
  676. PublicTimelineService::rem($status->id);
  677. }
  678. ModLogService::boot()
  679. ->objectUid($profile->id)
  680. ->objectId($profile->id)
  681. ->objectType('App\Profile::class')
  682. ->user(request()->user())
  683. ->action('admin.user.moderate')
  684. ->metadata([
  685. 'action' => 'cw',
  686. 'message' => 'Success!',
  687. ])
  688. ->accessLevel('admin')
  689. ->save();
  690. Report::whereObjectId($report->object_id)
  691. ->whereObjectType($report->object_type)
  692. ->update([
  693. 'nsfw' => true,
  694. 'admin_seen' => now(),
  695. ]);
  696. return [200];
  697. break;
  698. case 'unlist':
  699. if ($report->object_type === 'App\Profile') {
  700. $profile = Profile::find($report->object_id);
  701. } elseif ($report->object_type === 'App\Status') {
  702. $status = Status::find($report->object_id);
  703. if (! $status) {
  704. return [200];
  705. }
  706. $profile = Profile::find($status->profile_id);
  707. }
  708. if (! $profile) {
  709. return;
  710. }
  711. abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot moderate an admin account.');
  712. $profile->unlisted = true;
  713. $profile->save();
  714. foreach (Status::whereProfileId($profile->id)->whereScope('public')->cursor() as $status) {
  715. $status->scope = 'unlisted';
  716. $status->visibility = 'unlisted';
  717. $status->save();
  718. StatusService::del($status->id);
  719. PublicTimelineService::rem($status->id);
  720. }
  721. ModLogService::boot()
  722. ->objectUid($profile->id)
  723. ->objectId($profile->id)
  724. ->objectType('App\Profile::class')
  725. ->user(request()->user())
  726. ->action('admin.user.moderate')
  727. ->metadata([
  728. 'action' => 'unlisted',
  729. 'message' => 'Success!',
  730. ])
  731. ->accessLevel('admin')
  732. ->save();
  733. Report::whereObjectId($report->object_id)
  734. ->whereObjectType($report->object_type)
  735. ->update([
  736. 'admin_seen' => now(),
  737. ]);
  738. return [200];
  739. break;
  740. case 'private':
  741. if ($report->object_type === 'App\Profile') {
  742. $profile = Profile::find($report->object_id);
  743. } elseif ($report->object_type === 'App\Status') {
  744. $status = Status::find($report->object_id);
  745. if (! $status) {
  746. return [200];
  747. }
  748. $profile = Profile::find($status->profile_id);
  749. }
  750. if (! $profile) {
  751. return;
  752. }
  753. abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot moderate an admin account.');
  754. $profile->unlisted = true;
  755. $profile->save();
  756. foreach (Status::whereProfileId($profile->id)->cursor() as $status) {
  757. $status->scope = 'private';
  758. $status->visibility = 'private';
  759. $status->save();
  760. StatusService::del($status->id);
  761. PublicTimelineService::rem($status->id);
  762. }
  763. ModLogService::boot()
  764. ->objectUid($profile->id)
  765. ->objectId($profile->id)
  766. ->objectType('App\Profile::class')
  767. ->user(request()->user())
  768. ->action('admin.user.moderate')
  769. ->metadata([
  770. 'action' => 'private',
  771. 'message' => 'Success!',
  772. ])
  773. ->accessLevel('admin')
  774. ->save();
  775. Report::whereObjectId($report->object_id)
  776. ->whereObjectType($report->object_type)
  777. ->update([
  778. 'admin_seen' => now(),
  779. ]);
  780. return [200];
  781. break;
  782. case 'delete':
  783. if (config('pixelfed.account_deletion') == false) {
  784. abort(404);
  785. }
  786. if ($report->object_type === 'App\Profile') {
  787. $profile = Profile::find($report->object_id);
  788. } elseif ($report->object_type === 'App\Status') {
  789. $status = Status::find($report->object_id);
  790. if (! $status) {
  791. return [200];
  792. }
  793. $profile = Profile::find($status->profile_id);
  794. }
  795. if (! $profile) {
  796. return;
  797. }
  798. abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot delete an admin account.');
  799. $ts = now()->addMonth();
  800. if ($profile->user_id) {
  801. $user = $profile->user;
  802. abort_if($user->is_admin, 403, 'You cannot delete admin accounts.');
  803. $user->status = 'delete';
  804. $user->delete_after = $ts;
  805. $user->save();
  806. }
  807. $profile->status = 'delete';
  808. $profile->delete_after = $ts;
  809. $profile->save();
  810. ModLogService::boot()
  811. ->objectUid($profile->id)
  812. ->objectId($profile->id)
  813. ->objectType('App\Profile::class')
  814. ->user(request()->user())
  815. ->action('admin.user.delete')
  816. ->accessLevel('admin')
  817. ->save();
  818. Report::whereObjectId($report->object_id)
  819. ->whereObjectType($report->object_type)
  820. ->update([
  821. 'admin_seen' => now(),
  822. ]);
  823. if ($profile->user_id) {
  824. DB::table('oauth_access_tokens')->whereUserId($user->id)->delete();
  825. DB::table('oauth_auth_codes')->whereUserId($user->id)->delete();
  826. $user->email = $user->id;
  827. $user->password = '';
  828. $user->status = 'delete';
  829. $user->save();
  830. $profile->status = 'delete';
  831. $profile->delete_after = now()->addMonth();
  832. $profile->save();
  833. AccountService::del($profile->id);
  834. DeleteAccountPipeline::dispatch($user)->onQueue('high');
  835. } else {
  836. $profile->status = 'delete';
  837. $profile->delete_after = now()->addMonth();
  838. $profile->save();
  839. AccountService::del($profile->id);
  840. DeleteRemoteProfilePipeline::dispatch($profile)->onQueue('high');
  841. }
  842. return [200];
  843. break;
  844. }
  845. }
  846. protected function reportsHandleStatusAction($report, $action)
  847. {
  848. switch ($action) {
  849. case 'ignore':
  850. Report::whereObjectId($report->object_id)
  851. ->whereObjectType($report->object_type)
  852. ->update([
  853. 'admin_seen' => now(),
  854. ]);
  855. return [200];
  856. break;
  857. case 'nsfw':
  858. $status = Status::find($report->object_id);
  859. if (! $status) {
  860. return [200];
  861. }
  862. abort_if($status->profile->user && $status->profile->user->is_admin, 400, 'Cannot moderate an admin account post.');
  863. $status->is_nsfw = true;
  864. $status->save();
  865. StatusService::del($status->id);
  866. ModLogService::boot()
  867. ->objectUid($status->profile_id)
  868. ->objectId($status->profile_id)
  869. ->objectType('App\Status::class')
  870. ->user(request()->user())
  871. ->action('admin.status.moderate')
  872. ->metadata([
  873. 'action' => 'cw',
  874. 'message' => 'Success!',
  875. ])
  876. ->accessLevel('admin')
  877. ->save();
  878. Report::whereObjectId($report->object_id)
  879. ->whereObjectType($report->object_type)
  880. ->update([
  881. 'nsfw' => true,
  882. 'admin_seen' => now(),
  883. ]);
  884. return [200];
  885. break;
  886. case 'private':
  887. $status = Status::find($report->object_id);
  888. if (! $status) {
  889. return [200];
  890. }
  891. abort_if($status->profile->user && $status->profile->user->is_admin, 400, 'Cannot moderate an admin account post.');
  892. $status->scope = 'private';
  893. $status->visibility = 'private';
  894. $status->save();
  895. StatusService::del($status->id);
  896. PublicTimelineService::rem($status->id);
  897. ModLogService::boot()
  898. ->objectUid($status->profile_id)
  899. ->objectId($status->profile_id)
  900. ->objectType('App\Status::class')
  901. ->user(request()->user())
  902. ->action('admin.status.moderate')
  903. ->metadata([
  904. 'action' => 'private',
  905. 'message' => 'Success!',
  906. ])
  907. ->accessLevel('admin')
  908. ->save();
  909. Report::whereObjectId($report->object_id)
  910. ->whereObjectType($report->object_type)
  911. ->update([
  912. 'admin_seen' => now(),
  913. ]);
  914. return [200];
  915. break;
  916. case 'unlist':
  917. $status = Status::find($report->object_id);
  918. if (! $status) {
  919. return [200];
  920. }
  921. abort_if($status->profile->user && $status->profile->user->is_admin, 400, 'Cannot moderate an admin account post.');
  922. if ($status->scope === 'public') {
  923. $status->scope = 'unlisted';
  924. $status->visibility = 'unlisted';
  925. $status->save();
  926. StatusService::del($status->id);
  927. PublicTimelineService::rem($status->id);
  928. }
  929. ModLogService::boot()
  930. ->objectUid($status->profile_id)
  931. ->objectId($status->profile_id)
  932. ->objectType('App\Status::class')
  933. ->user(request()->user())
  934. ->action('admin.status.moderate')
  935. ->metadata([
  936. 'action' => 'unlist',
  937. 'message' => 'Success!',
  938. ])
  939. ->accessLevel('admin')
  940. ->save();
  941. Report::whereObjectId($report->object_id)
  942. ->whereObjectType($report->object_type)
  943. ->update([
  944. 'admin_seen' => now(),
  945. ]);
  946. return [200];
  947. break;
  948. case 'delete':
  949. $status = Status::find($report->object_id);
  950. if (! $status) {
  951. return [200];
  952. }
  953. $profile = $status->profile;
  954. abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot delete an admin account post.');
  955. StatusService::del($status->id);
  956. if ($profile->user_id != null && $profile->domain == null) {
  957. PublicTimelineService::del($status->id);
  958. StatusDelete::dispatch($status)->onQueue('high');
  959. } else {
  960. NetworkTimelineService::del($status->id);
  961. RemoteStatusDelete::dispatch($status)->onQueue('high');
  962. }
  963. Report::whereObjectId($report->object_id)
  964. ->whereObjectType($report->object_type)
  965. ->update([
  966. 'admin_seen' => now(),
  967. ]);
  968. return [200];
  969. break;
  970. }
  971. }
  972. public function reportsApiSpamAll(Request $request)
  973. {
  974. $tab = $request->input('tab', 'home');
  975. $appeals = AdminSpamReport::collection(
  976. AccountInterstitial::orderBy('id', 'desc')
  977. ->whereType('post.autospam')
  978. ->whereNull('appeal_handled_at')
  979. ->cursorPaginate(6)
  980. ->withQueryString()
  981. );
  982. return $appeals;
  983. }
  984. public function reportsApiSpamHandle(Request $request)
  985. {
  986. $this->validate($request, [
  987. 'id' => 'required',
  988. 'action' => 'required|in:mark-read,mark-not-spam,mark-all-read,mark-all-not-spam,delete-profile',
  989. ]);
  990. $action = $request->input('action');
  991. abort_if(
  992. $action === 'delete-profile' &&
  993. ! config('pixelfed.account_deletion'),
  994. 404,
  995. "Cannot delete profile, account_deletion is disabled.\n\n Set `ACCOUNT_DELETION=true` in .env and re-cache config."
  996. );
  997. $report = AccountInterstitial::with('user')
  998. ->whereType('post.autospam')
  999. ->whereNull('appeal_handled_at')
  1000. ->findOrFail($request->input('id'));
  1001. $this->reportsHandleSpamAction($report, $action);
  1002. Cache::forget('admin-dash:reports:spam-count');
  1003. Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$report->user->profile_id);
  1004. Cache::forget('pf:bouncer_v0:recent_by_pid:'.$report->user->profile_id);
  1005. return [$action, $report];
  1006. }
  1007. public function reportsHandleSpamAction($appeal, $action)
  1008. {
  1009. $meta = json_decode($appeal->meta);
  1010. if ($action == 'mark-read') {
  1011. $appeal->is_spam = true;
  1012. $appeal->appeal_handled_at = now();
  1013. $appeal->save();
  1014. PublicTimelineService::del($appeal->item_id);
  1015. }
  1016. if ($action == 'mark-not-spam') {
  1017. $status = $appeal->status;
  1018. $status->is_nsfw = $meta->is_nsfw;
  1019. $status->scope = 'public';
  1020. $status->visibility = 'public';
  1021. $status->save();
  1022. $appeal->is_spam = false;
  1023. $appeal->appeal_handled_at = now();
  1024. $appeal->save();
  1025. Notification::whereAction('autospam.warning')
  1026. ->whereProfileId($appeal->user->profile_id)
  1027. ->get()
  1028. ->each(function ($n) use ($appeal) {
  1029. NotificationService::del($appeal->user->profile_id, $n->id);
  1030. $n->forceDelete();
  1031. });
  1032. StatusService::del($status->id);
  1033. StatusService::get($status->id);
  1034. if ($status->in_reply_to_id == null && $status->reblog_of_id == null) {
  1035. PublicTimelineService::add($status->id);
  1036. }
  1037. }
  1038. if ($action == 'mark-all-read') {
  1039. AccountInterstitial::whereType('post.autospam')
  1040. ->whereItemType('App\Status')
  1041. ->whereNull('appeal_handled_at')
  1042. ->whereUserId($appeal->user_id)
  1043. ->update([
  1044. 'appeal_handled_at' => now(),
  1045. 'is_spam' => true,
  1046. ]);
  1047. }
  1048. if ($action == 'mark-all-not-spam') {
  1049. AccountInterstitial::whereType('post.autospam')
  1050. ->whereItemType('App\Status')
  1051. ->whereUserId($appeal->user_id)
  1052. ->get()
  1053. ->each(function ($report) use ($meta) {
  1054. $report->is_spam = false;
  1055. $report->appeal_handled_at = now();
  1056. $report->save();
  1057. $status = Status::find($report->item_id);
  1058. if ($status) {
  1059. $status->is_nsfw = $meta->is_nsfw;
  1060. $status->scope = 'public';
  1061. $status->visibility = 'public';
  1062. $status->save();
  1063. StatusService::del($status->id);
  1064. }
  1065. Notification::whereAction('autospam.warning')
  1066. ->whereProfileId($report->user->profile_id)
  1067. ->get()
  1068. ->each(function ($n) use ($report) {
  1069. NotificationService::del($report->user->profile_id, $n->id);
  1070. $n->forceDelete();
  1071. });
  1072. });
  1073. }
  1074. if ($action == 'delete-profile') {
  1075. $user = User::findOrFail($appeal->user_id);
  1076. $profile = $user->profile;
  1077. if ($user->is_admin == true) {
  1078. $mid = request()->user()->id;
  1079. abort_if($user->id < $mid, 403, 'You cannot delete an admin account.');
  1080. }
  1081. $ts = now()->addMonth();
  1082. $user->status = 'delete';
  1083. $profile->status = 'delete';
  1084. $user->delete_after = $ts;
  1085. $profile->delete_after = $ts;
  1086. $user->save();
  1087. $profile->save();
  1088. $appeal->appeal_handled_at = now();
  1089. $appeal->save();
  1090. ModLogService::boot()
  1091. ->objectUid($user->id)
  1092. ->objectId($user->id)
  1093. ->objectType('App\User::class')
  1094. ->user(request()->user())
  1095. ->action('admin.user.delete')
  1096. ->accessLevel('admin')
  1097. ->save();
  1098. Cache::forget('profiles:private');
  1099. DeleteAccountPipeline::dispatch($user);
  1100. }
  1101. }
  1102. public function reportsApiSpamGet(Request $request, $id)
  1103. {
  1104. $report = AccountInterstitial::findOrFail($id);
  1105. return new AdminSpamReport($report);
  1106. }
  1107. }