Bladeren bron

Merge branch 'staging' of github.com:pixelfed/pixelfed into jippi-fork

Christian Winther 1 jaar geleden
bovenliggende
commit
5d56460082

+ 6 - 0
CHANGELOG.md

@@ -5,6 +5,8 @@
 ### Features
 
 - Curated Onboarding ([#4946](https://github.com/pixelfed/pixelfed/pull/4946)) ([8dac2caf](https://github.com/pixelfed/pixelfed/commit/8dac2caf))
+- Add Curated Onboarding Templates ([071163b4](https://github.com/pixelfed/pixelfed/commit/071163b4))
+- Add Remote Reports to Admin Dashboard Reports page ([ef0ff78e](https://github.com/pixelfed/pixelfed/commit/ef0ff78e))
 
 ### Updates
 
@@ -18,6 +20,10 @@
 - Update Directory logic, add curated onboarding support ([59c70239](https://github.com/pixelfed/pixelfed/commit/59c70239))
 - Update Inbox and StatusObserver, fix silently rejected direct messages due to saveQuietly which failed to generate a snowflake id ([089ba3c4](https://github.com/pixelfed/pixelfed/commit/089ba3c4))
 - Update Curated Onboarding dashboard, improve application filtering and make it easier to distinguish response state ([2b5d7235](https://github.com/pixelfed/pixelfed/commit/2b5d7235))
+- Update AdminReports, add story reports and fix cs ([767522a8](https://github.com/pixelfed/pixelfed/commit/767522a8))
+- Update AdminReportController, add story report support ([a16309ac](https://github.com/pixelfed/pixelfed/commit/a16309ac))
+- Update kb, add email confirmation issues page ([2f48df8c](https://github.com/pixelfed/pixelfed/commit/2f48df8c))
+- Update AdminCuratedRegisterController, filter confirmation activities from activitylog ([ab9ecb6e](https://github.com/pixelfed/pixelfed/commit/ab9ecb6e))
 -  ([](https://github.com/pixelfed/pixelfed/commit/))
 
 ## [v0.11.12 (2024-02-16)](https://github.com/pixelfed/pixelfed/compare/v0.11.11...v0.11.12)

+ 1346 - 1048
app/Http/Controllers/Admin/AdminReportController.php

@@ -2,461 +2,471 @@
 
 namespace App\Http\Controllers\Admin;
 
-use Cache;
-use Carbon\Carbon;
-use Illuminate\Http\Request;
-use Illuminate\Support\Facades\DB;
-use Illuminate\Support\Facades\Redis;
-use App\Services\AccountService;
-use App\Services\StatusService;
-use App\{
-	AccountInterstitial,
-	Contact,
-	Hashtag,
-	Newsroom,
-	Notification,
-	OauthClient,
-	Profile,
-	Report,
-	Status,
-	Story,
-	User
-};
-use Illuminate\Validation\Rule;
-use App\Services\StoryService;
-use App\Services\ModLogService;
+use App\AccountInterstitial;
+use App\Http\Resources\AdminReport;
+use App\Http\Resources\AdminRemoteReport;
+use App\Http\Resources\AdminSpamReport;
 use App\Jobs\DeletePipeline\DeleteAccountPipeline;
 use App\Jobs\DeletePipeline\DeleteRemoteProfilePipeline;
-use App\Jobs\StatusPipeline\StatusDelete;
 use App\Jobs\StatusPipeline\RemoteStatusDelete;
-use App\Http\Resources\AdminReport;
-use App\Http\Resources\AdminSpamReport;
+use App\Jobs\StatusPipeline\StatusDelete;
+use App\Jobs\StoryPipeline\StoryDelete;
+use App\Notification;
+use App\Profile;
+use App\Report;
+use App\Models\RemoteReport;
+use App\Services\AccountService;
+use App\Services\ModLogService;
+use App\Services\NetworkTimelineService;
 use App\Services\NotificationService;
 use App\Services\PublicTimelineService;
-use App\Services\NetworkTimelineService;
+use App\Services\StatusService;
+use App\Status;
+use App\Story;
+use App\User;
+use Cache;
+use Storage;
+use Carbon\Carbon;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Redis;
 
 trait AdminReportController
 {
-	public function reports(Request $request)
-	{
-		$filter = $request->input('filter') == 'closed' ? 'closed' : 'open';
-		$page = $request->input('page') ?? 1;
-
-		$ai = Cache::remember('admin-dash:reports:ai-count', 3600, function() {
-			return AccountInterstitial::whereNotNull('appeal_requested_at')->whereNull('appeal_handled_at')->count();
-		});
-
-		$spam = Cache::remember('admin-dash:reports:spam-count', 3600, function() {
-			return AccountInterstitial::whereType('post.autospam')->whereNull('appeal_handled_at')->count();
-		});
-
-		$mailVerifications = Redis::scard('email:manual');
-
-		if($filter == 'open' && $page == 1) {
-			$reports = Cache::remember('admin-dash:reports:list-cache', 300, function() use($page, $filter) {
-				return Report::whereHas('status')
-					->whereHas('reportedUser')
-					->whereHas('reporter')
-					->orderBy('created_at','desc')
-					->when($filter, function($q, $filter) {
-						return $filter == 'open' ?
-						$q->whereNull('admin_seen') :
-						$q->whereNotNull('admin_seen');
-					})
-					->paginate(6);
-			});
-		} else {
-			$reports = Report::whereHas('status')
-			->whereHas('reportedUser')
-			->whereHas('reporter')
-			->orderBy('created_at','desc')
-			->when($filter, function($q, $filter) {
-				return $filter == 'open' ?
-				$q->whereNull('admin_seen') :
-				$q->whereNotNull('admin_seen');
-			})
-			->paginate(6);
-		}
-
-		return view('admin.reports.home', compact('reports', 'ai', 'spam', 'mailVerifications'));
-	}
-
-	public function showReport(Request $request, $id)
-	{
-		$report = Report::with('status')->findOrFail($id);
-		if($request->has('ref') && $request->input('ref') == 'email') {
-			return redirect('/i/admin/reports?tab=report&id=' . $report->id);
-		}
-		return view('admin.reports.show', compact('report'));
-	}
-
-	public function appeals(Request $request)
-	{
-		$appeals = AccountInterstitial::whereNotNull('appeal_requested_at')
-			->whereNull('appeal_handled_at')
-			->latest()
-			->paginate(6);
-		return view('admin.reports.appeals', compact('appeals'));
-	}
-
-	public function showAppeal(Request $request, $id)
-	{
-		$appeal = AccountInterstitial::whereNotNull('appeal_requested_at')
-			->whereNull('appeal_handled_at')
-			->findOrFail($id);
-		$meta = json_decode($appeal->meta);
-		return view('admin.reports.show_appeal', compact('appeal', 'meta'));
-	}
-
-	public function spam(Request $request)
-	{
-		$this->validate($request, [
-			'tab' => 'sometimes|in:home,not-spam,spam,settings,custom,exemptions'
-		]);
-
-		$tab = $request->input('tab', 'home');
-
-		$openCount = Cache::remember('admin-dash:reports:spam-count', 3600, function() {
-			return AccountInterstitial::whereType('post.autospam')
-				->whereNull('appeal_handled_at')
-				->count();
-		});
-
-		$monthlyCount = Cache::remember('admin-dash:reports:spam-count:30d', 43200, function() {
-			return AccountInterstitial::whereType('post.autospam')
-				->where('created_at', '>', now()->subMonth())
-				->count();
-		});
-
-		$totalCount = Cache::remember('admin-dash:reports:spam-count:total', 43200, function() {
-			return AccountInterstitial::whereType('post.autospam')->count();
-		});
-
-		$uncategorized = Cache::remember('admin-dash:reports:spam-sync', 3600, function() {
-			return AccountInterstitial::whereType('post.autospam')
-				->whereIsSpam(null)
-				->whereNotNull('appeal_handled_at')
-				->exists();
-		});
-
-		$avg = Cache::remember('admin-dash:reports:spam-count:avg', 43200, function() {
-			if(config('database.default') != 'mysql') {
-				return 0;
-			}
-			return AccountInterstitial::selectRaw('*, count(id) as counter')
-				->whereType('post.autospam')
-				->groupBy('user_id')
-				->get()
-				->avg('counter');
-		});
-
-		$avgOpen = Cache::remember('admin-dash:reports:spam-count:avgopen', 43200, function() {
-			if(config('database.default') != 'mysql') {
-				return "0";
-			}
-			$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();
-			if(!$seconds) {
-				return "0";
-			}
-			$mins = floor($seconds->avg('timediff') / 60);
-
-			if($mins < 60) {
-				return $mins . ' min(s)';
-			}
-
-			if($mins < 2880) {
-				return floor($mins / 60) . ' hour(s)';
-			}
-
-			return floor($mins / 60 / 24) . ' day(s)';
-		});
-		$avgCount = $totalCount && $avg ? floor($totalCount / $avg) : "0";
-
-		if(in_array($tab, ['home', 'spam', 'not-spam'])) {
-			$appeals = AccountInterstitial::whereType('post.autospam')
-				->when($tab, function($q, $tab) {
-					switch($tab) {
-						case 'home':
-							return $q->whereNull('appeal_handled_at');
-						break;
-						case 'spam':
-							return $q->whereIsSpam(true);
-						break;
-						case 'not-spam':
-							return $q->whereIsSpam(false);
-						break;
-					}
-				})
-				->latest()
-				->paginate(6);
-
-			if($tab !== 'home') {
-				$appeals = $appeals->appends(['tab' => $tab]);
-			}
-		} else {
-			$appeals = new class {
-				public function count() {
-					return 0;
-				}
-
-				public function render() {
-					return;
-				}
-			};
-		}
-
-
-		return view('admin.reports.spam', compact('tab', 'appeals', 'openCount', 'monthlyCount', 'totalCount', 'avgCount', 'avgOpen', 'uncategorized'));
-	}
-
-	public function showSpam(Request $request, $id)
-	{
-		$appeal = AccountInterstitial::whereType('post.autospam')
-			->findOrFail($id);
-		if($request->has('ref') && $request->input('ref') == 'email') {
-			return redirect('/i/admin/reports?tab=autospam&id=' . $appeal->id);
-		}
-		$meta = json_decode($appeal->meta);
-		return view('admin.reports.show_spam', compact('appeal', 'meta'));
-	}
-
-	public function fixUncategorizedSpam(Request $request)
-	{
-		if(Cache::get('admin-dash:reports:spam-sync-active')) {
-			return redirect('/i/admin/reports/autospam');
-		}
-
-		Cache::put('admin-dash:reports:spam-sync-active', 1, 900);
-
-		AccountInterstitial::chunk(500, function($reports) {
-			foreach($reports as $report) {
-				if($report->item_type != 'App\Status') {
-					continue;
-				}
-
-				if($report->type != 'post.autospam') {
-					continue;
-				}
-
-				if($report->is_spam != null) {
-					continue;
-				}
-
-				$status = StatusService::get($report->item_id, false);
-				if(!$status) {
-					return;
-				}
-				$scope = $status['visibility'];
-				$report->is_spam = $scope == 'unlisted';
-				$report->in_violation = $report->is_spam;
-				$report->severity_index = 1;
-				$report->save();
-			}
-		});
-
-		Cache::forget('admin-dash:reports:spam-sync');
-		return redirect('/i/admin/reports/autospam');
-	}
-
-	public function updateSpam(Request $request, $id)
-	{
-		$this->validate($request, [
-			'action' => 'required|in:dismiss,approve,dismiss-all,approve-all,delete-account,mark-spammer'
-		]);
-
-		$action = $request->input('action');
-		$appeal = AccountInterstitial::whereType('post.autospam')
-			->whereNull('appeal_handled_at')
-			->findOrFail($id);
-
-		$meta = json_decode($appeal->meta);
-		$res = ['status' => 'success'];
-		$now = now();
-		Cache::forget('admin-dash:reports:spam-count:total');
-		Cache::forget('admin-dash:reports:spam-count:30d');
-
-		if($action == 'delete-account') {
-			if(config('pixelfed.account_deletion') == false) {
-				abort(404);
-			}
-
-			$user = User::findOrFail($appeal->user_id);
-			$profile = $user->profile;
-
-			if($user->is_admin == true) {
-				$mid = $request->user()->id;
-				abort_if($user->id < $mid, 403);
-			}
-
-			$ts = now()->addMonth();
-			$user->status = 'delete';
-			$profile->status = 'delete';
-			$user->delete_after = $ts;
-			$profile->delete_after = $ts;
-			$user->save();
-			$profile->save();
-
-			ModLogService::boot()
-				->objectUid($user->id)
-				->objectId($user->id)
-				->objectType('App\User::class')
-				->user($request->user())
-				->action('admin.user.delete')
-				->accessLevel('admin')
-				->save();
-
-			Cache::forget('profiles:private');
-			DeleteAccountPipeline::dispatch($user);
-			return;
-		}
-
-		if($action == 'dismiss') {
-			$appeal->is_spam = true;
-			$appeal->appeal_handled_at = $now;
-			$appeal->save();
-
-			Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id);
-			Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id);
-			Cache::forget('admin-dash:reports:spam-count');
-			return $res;
-		}
-
-		if($action == 'dismiss-all') {
-			AccountInterstitial::whereType('post.autospam')
-				->whereItemType('App\Status')
-				->whereNull('appeal_handled_at')
-				->whereUserId($appeal->user_id)
-				->update(['appeal_handled_at' => $now, 'is_spam' => true]);
-			Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id);
-			Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id);
-			Cache::forget('admin-dash:reports:spam-count');
-			return $res;
-		}
-
-		if($action == 'approve-all') {
-			AccountInterstitial::whereType('post.autospam')
-				->whereItemType('App\Status')
-				->whereNull('appeal_handled_at')
-				->whereUserId($appeal->user_id)
-				->get()
-				->each(function($report) use($meta) {
-					$report->is_spam = false;
-					$report->appeal_handled_at = now();
-					$report->save();
-					$status = Status::find($report->item_id);
-					if($status) {
-						$status->is_nsfw = $meta->is_nsfw;
-						$status->scope = 'public';
-						$status->visibility = 'public';
-						$status->save();
-						StatusService::del($status->id, true);
-					}
-				});
-			Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id);
-			Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id);
-			Cache::forget('admin-dash:reports:spam-count');
-			return $res;
-		}
-
-		if($action == 'mark-spammer') {
-			AccountInterstitial::whereType('post.autospam')
-				->whereItemType('App\Status')
-				->whereNull('appeal_handled_at')
-				->whereUserId($appeal->user_id)
-				->update(['appeal_handled_at' => $now, 'is_spam' => true]);
-
-			$pro = Profile::whereUserId($appeal->user_id)->firstOrFail();
-
-			$pro->update([
-				'unlisted' => true,
-				'cw' => true,
-				'no_autolink' => true
-			]);
-
-			Status::whereProfileId($pro->id)
-				->get()
-				->each(function($report) {
-					$status->is_nsfw = $meta->is_nsfw;
-					$status->scope = 'public';
-					$status->visibility = 'public';
-					$status->save();
-					StatusService::del($status->id, true);
-				});
-
-			Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id);
-			Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id);
-			Cache::forget('admin-dash:reports:spam-count');
-			return $res;
-		}
-
-		$status = $appeal->status;
-		$status->is_nsfw = $meta->is_nsfw;
-		$status->scope = 'public';
-		$status->visibility = 'public';
-		$status->save();
-
-		$appeal->is_spam = false;
-		$appeal->appeal_handled_at = now();
-		$appeal->save();
-
-		StatusService::del($status->id);
-
-		Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id);
-		Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id);
-		Cache::forget('admin-dash:reports:spam-count');
-
-		return $res;
-	}
-
-	public function updateAppeal(Request $request, $id)
-	{
-		$this->validate($request, [
-			'action' => 'required|in:dismiss,approve'
-		]);
-
-		$action = $request->input('action');
-		$appeal = AccountInterstitial::whereNotNull('appeal_requested_at')
-			->whereNull('appeal_handled_at')
-			->findOrFail($id);
-
-		if($action == 'dismiss') {
-			$appeal->appeal_handled_at = now();
-			$appeal->save();
-			Cache::forget('admin-dash:reports:ai-count');
-			return redirect('/i/admin/reports/appeals');
-		}
-
-		switch ($appeal->type) {
-			case 'post.cw':
-				$status = $appeal->status;
-				$status->is_nsfw = false;
-				$status->save();
-				break;
-
-			case 'post.unlist':
-				$status = $appeal->status;
-				$status->scope = 'public';
-				$status->visibility = 'public';
-				$status->save();
-				break;
-
-			default:
-				# code...
-				break;
-		}
-
-		$appeal->appeal_handled_at = now();
-		$appeal->save();
-		StatusService::del($status->id, true);
-		Cache::forget('admin-dash:reports:ai-count');
-
-		return redirect('/i/admin/reports/appeals');
-	}
+    public function reports(Request $request)
+    {
+        $filter = $request->input('filter') == 'closed' ? 'closed' : 'open';
+        $page = $request->input('page') ?? 1;
+
+        $ai = Cache::remember('admin-dash:reports:ai-count', 3600, function () {
+            return AccountInterstitial::whereNotNull('appeal_requested_at')->whereNull('appeal_handled_at')->count();
+        });
+
+        $spam = Cache::remember('admin-dash:reports:spam-count', 3600, function () {
+            return AccountInterstitial::whereType('post.autospam')->whereNull('appeal_handled_at')->count();
+        });
+
+        $mailVerifications = Redis::scard('email:manual');
+
+        if ($filter == 'open' && $page == 1) {
+            $reports = Cache::remember('admin-dash:reports:list-cache', 300, function () use ($filter) {
+                return Report::whereHas('status')
+                    ->whereHas('reportedUser')
+                    ->whereHas('reporter')
+                    ->orderBy('created_at', 'desc')
+                    ->when($filter, function ($q, $filter) {
+                        return $filter == 'open' ?
+                        $q->whereNull('admin_seen') :
+                        $q->whereNotNull('admin_seen');
+                    })
+                    ->paginate(6);
+            });
+        } else {
+            $reports = Report::whereHas('status')
+                ->whereHas('reportedUser')
+                ->whereHas('reporter')
+                ->orderBy('created_at', 'desc')
+                ->when($filter, function ($q, $filter) {
+                    return $filter == 'open' ?
+                    $q->whereNull('admin_seen') :
+                    $q->whereNotNull('admin_seen');
+                })
+                ->paginate(6);
+        }
+
+        return view('admin.reports.home', compact('reports', 'ai', 'spam', 'mailVerifications'));
+    }
+
+    public function showReport(Request $request, $id)
+    {
+        $report = Report::with('status')->findOrFail($id);
+        if ($request->has('ref') && $request->input('ref') == 'email') {
+            return redirect('/i/admin/reports?tab=report&id='.$report->id);
+        }
+
+        return view('admin.reports.show', compact('report'));
+    }
+
+    public function appeals(Request $request)
+    {
+        $appeals = AccountInterstitial::whereNotNull('appeal_requested_at')
+            ->whereNull('appeal_handled_at')
+            ->latest()
+            ->paginate(6);
+
+        return view('admin.reports.appeals', compact('appeals'));
+    }
+
+    public function showAppeal(Request $request, $id)
+    {
+        $appeal = AccountInterstitial::whereNotNull('appeal_requested_at')
+            ->whereNull('appeal_handled_at')
+            ->findOrFail($id);
+        $meta = json_decode($appeal->meta);
+
+        return view('admin.reports.show_appeal', compact('appeal', 'meta'));
+    }
+
+    public function spam(Request $request)
+    {
+        $this->validate($request, [
+            'tab' => 'sometimes|in:home,not-spam,spam,settings,custom,exemptions',
+        ]);
+
+        $tab = $request->input('tab', 'home');
+
+        $openCount = Cache::remember('admin-dash:reports:spam-count', 3600, function () {
+            return AccountInterstitial::whereType('post.autospam')
+                ->whereNull('appeal_handled_at')
+                ->count();
+        });
+
+        $monthlyCount = Cache::remember('admin-dash:reports:spam-count:30d', 43200, function () {
+            return AccountInterstitial::whereType('post.autospam')
+                ->where('created_at', '>', now()->subMonth())
+                ->count();
+        });
+
+        $totalCount = Cache::remember('admin-dash:reports:spam-count:total', 43200, function () {
+            return AccountInterstitial::whereType('post.autospam')->count();
+        });
+
+        $uncategorized = Cache::remember('admin-dash:reports:spam-sync', 3600, function () {
+            return AccountInterstitial::whereType('post.autospam')
+                ->whereIsSpam(null)
+                ->whereNotNull('appeal_handled_at')
+                ->exists();
+        });
+
+        $avg = Cache::remember('admin-dash:reports:spam-count:avg', 43200, function () {
+            if (config('database.default') != 'mysql') {
+                return 0;
+            }
+
+            return AccountInterstitial::selectRaw('*, count(id) as counter')
+                ->whereType('post.autospam')
+                ->groupBy('user_id')
+                ->get()
+                ->avg('counter');
+        });
+
+        $avgOpen = Cache::remember('admin-dash:reports:spam-count:avgopen', 43200, function () {
+            if (config('database.default') != 'mysql') {
+                return '0';
+            }
+            $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();
+            if (! $seconds) {
+                return '0';
+            }
+            $mins = floor($seconds->avg('timediff') / 60);
+
+            if ($mins < 60) {
+                return $mins.' min(s)';
+            }
+
+            if ($mins < 2880) {
+                return floor($mins / 60).' hour(s)';
+            }
+
+            return floor($mins / 60 / 24).' day(s)';
+        });
+        $avgCount = $totalCount && $avg ? floor($totalCount / $avg) : '0';
+
+        if (in_array($tab, ['home', 'spam', 'not-spam'])) {
+            $appeals = AccountInterstitial::whereType('post.autospam')
+                ->when($tab, function ($q, $tab) {
+                    switch ($tab) {
+                        case 'home':
+                            return $q->whereNull('appeal_handled_at');
+                            break;
+                        case 'spam':
+                            return $q->whereIsSpam(true);
+                            break;
+                        case 'not-spam':
+                            return $q->whereIsSpam(false);
+                            break;
+                    }
+                })
+                ->latest()
+                ->paginate(6);
+
+            if ($tab !== 'home') {
+                $appeals = $appeals->appends(['tab' => $tab]);
+            }
+        } else {
+            $appeals = new class
+            {
+                public function count()
+                {
+                    return 0;
+                }
+
+                public function render()
+                {
+
+                }
+            };
+        }
+
+        return view('admin.reports.spam', compact('tab', 'appeals', 'openCount', 'monthlyCount', 'totalCount', 'avgCount', 'avgOpen', 'uncategorized'));
+    }
+
+    public function showSpam(Request $request, $id)
+    {
+        $appeal = AccountInterstitial::whereType('post.autospam')
+            ->findOrFail($id);
+        if ($request->has('ref') && $request->input('ref') == 'email') {
+            return redirect('/i/admin/reports?tab=autospam&id='.$appeal->id);
+        }
+        $meta = json_decode($appeal->meta);
+
+        return view('admin.reports.show_spam', compact('appeal', 'meta'));
+    }
+
+    public function fixUncategorizedSpam(Request $request)
+    {
+        if (Cache::get('admin-dash:reports:spam-sync-active')) {
+            return redirect('/i/admin/reports/autospam');
+        }
+
+        Cache::put('admin-dash:reports:spam-sync-active', 1, 900);
+
+        AccountInterstitial::chunk(500, function ($reports) {
+            foreach ($reports as $report) {
+                if ($report->item_type != 'App\Status') {
+                    continue;
+                }
+
+                if ($report->type != 'post.autospam') {
+                    continue;
+                }
+
+                if ($report->is_spam != null) {
+                    continue;
+                }
+
+                $status = StatusService::get($report->item_id, false);
+                if (! $status) {
+                    return;
+                }
+                $scope = $status['visibility'];
+                $report->is_spam = $scope == 'unlisted';
+                $report->in_violation = $report->is_spam;
+                $report->severity_index = 1;
+                $report->save();
+            }
+        });
+
+        Cache::forget('admin-dash:reports:spam-sync');
+
+        return redirect('/i/admin/reports/autospam');
+    }
+
+    public function updateSpam(Request $request, $id)
+    {
+        $this->validate($request, [
+            'action' => 'required|in:dismiss,approve,dismiss-all,approve-all,delete-account,mark-spammer',
+        ]);
+
+        $action = $request->input('action');
+        $appeal = AccountInterstitial::whereType('post.autospam')
+            ->whereNull('appeal_handled_at')
+            ->findOrFail($id);
+
+        $meta = json_decode($appeal->meta);
+        $res = ['status' => 'success'];
+        $now = now();
+        Cache::forget('admin-dash:reports:spam-count:total');
+        Cache::forget('admin-dash:reports:spam-count:30d');
+
+        if ($action == 'delete-account') {
+            if (config('pixelfed.account_deletion') == false) {
+                abort(404);
+            }
+
+            $user = User::findOrFail($appeal->user_id);
+            $profile = $user->profile;
+
+            if ($user->is_admin == true) {
+                $mid = $request->user()->id;
+                abort_if($user->id < $mid, 403);
+            }
+
+            $ts = now()->addMonth();
+            $user->status = 'delete';
+            $profile->status = 'delete';
+            $user->delete_after = $ts;
+            $profile->delete_after = $ts;
+            $user->save();
+            $profile->save();
+
+            ModLogService::boot()
+                ->objectUid($user->id)
+                ->objectId($user->id)
+                ->objectType('App\User::class')
+                ->user($request->user())
+                ->action('admin.user.delete')
+                ->accessLevel('admin')
+                ->save();
+
+            Cache::forget('profiles:private');
+            DeleteAccountPipeline::dispatch($user);
+
+            return;
+        }
+
+        if ($action == 'dismiss') {
+            $appeal->is_spam = true;
+            $appeal->appeal_handled_at = $now;
+            $appeal->save();
+
+            Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$appeal->user->profile_id);
+            Cache::forget('pf:bouncer_v0:recent_by_pid:'.$appeal->user->profile_id);
+            Cache::forget('admin-dash:reports:spam-count');
+
+            return $res;
+        }
+
+        if ($action == 'dismiss-all') {
+            AccountInterstitial::whereType('post.autospam')
+                ->whereItemType('App\Status')
+                ->whereNull('appeal_handled_at')
+                ->whereUserId($appeal->user_id)
+                ->update(['appeal_handled_at' => $now, 'is_spam' => true]);
+            Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$appeal->user->profile_id);
+            Cache::forget('pf:bouncer_v0:recent_by_pid:'.$appeal->user->profile_id);
+            Cache::forget('admin-dash:reports:spam-count');
+
+            return $res;
+        }
+
+        if ($action == 'approve-all') {
+            AccountInterstitial::whereType('post.autospam')
+                ->whereItemType('App\Status')
+                ->whereNull('appeal_handled_at')
+                ->whereUserId($appeal->user_id)
+                ->get()
+                ->each(function ($report) use ($meta) {
+                    $report->is_spam = false;
+                    $report->appeal_handled_at = now();
+                    $report->save();
+                    $status = Status::find($report->item_id);
+                    if ($status) {
+                        $status->is_nsfw = $meta->is_nsfw;
+                        $status->scope = 'public';
+                        $status->visibility = 'public';
+                        $status->save();
+                        StatusService::del($status->id, true);
+                    }
+                });
+            Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$appeal->user->profile_id);
+            Cache::forget('pf:bouncer_v0:recent_by_pid:'.$appeal->user->profile_id);
+            Cache::forget('admin-dash:reports:spam-count');
+
+            return $res;
+        }
+
+        if ($action == 'mark-spammer') {
+            AccountInterstitial::whereType('post.autospam')
+                ->whereItemType('App\Status')
+                ->whereNull('appeal_handled_at')
+                ->whereUserId($appeal->user_id)
+                ->update(['appeal_handled_at' => $now, 'is_spam' => true]);
+
+            $pro = Profile::whereUserId($appeal->user_id)->firstOrFail();
+
+            $pro->update([
+                'unlisted' => true,
+                'cw' => true,
+                'no_autolink' => true,
+            ]);
+
+            Status::whereProfileId($pro->id)
+                ->get()
+                ->each(function ($report) {
+                    $status->is_nsfw = $meta->is_nsfw;
+                    $status->scope = 'public';
+                    $status->visibility = 'public';
+                    $status->save();
+                    StatusService::del($status->id, true);
+                });
+
+            Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$appeal->user->profile_id);
+            Cache::forget('pf:bouncer_v0:recent_by_pid:'.$appeal->user->profile_id);
+            Cache::forget('admin-dash:reports:spam-count');
+
+            return $res;
+        }
+
+        $status = $appeal->status;
+        $status->is_nsfw = $meta->is_nsfw;
+        $status->scope = 'public';
+        $status->visibility = 'public';
+        $status->save();
+
+        $appeal->is_spam = false;
+        $appeal->appeal_handled_at = now();
+        $appeal->save();
+
+        StatusService::del($status->id);
+
+        Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$appeal->user->profile_id);
+        Cache::forget('pf:bouncer_v0:recent_by_pid:'.$appeal->user->profile_id);
+        Cache::forget('admin-dash:reports:spam-count');
+
+        return $res;
+    }
+
+    public function updateAppeal(Request $request, $id)
+    {
+        $this->validate($request, [
+            'action' => 'required|in:dismiss,approve',
+        ]);
+
+        $action = $request->input('action');
+        $appeal = AccountInterstitial::whereNotNull('appeal_requested_at')
+            ->whereNull('appeal_handled_at')
+            ->findOrFail($id);
+
+        if ($action == 'dismiss') {
+            $appeal->appeal_handled_at = now();
+            $appeal->save();
+            Cache::forget('admin-dash:reports:ai-count');
+
+            return redirect('/i/admin/reports/appeals');
+        }
+
+        switch ($appeal->type) {
+            case 'post.cw':
+                $status = $appeal->status;
+                $status->is_nsfw = false;
+                $status->save();
+                break;
+
+            case 'post.unlist':
+                $status = $appeal->status;
+                $status->scope = 'public';
+                $status->visibility = 'public';
+                $status->save();
+                break;
+
+            default:
+                // code...
+                break;
+        }
+
+        $appeal->appeal_handled_at = now();
+        $appeal->save();
+        StatusService::del($status->id, true);
+        Cache::forget('admin-dash:reports:ai-count');
+
+        return redirect('/i/admin/reports/appeals');
+    }
 
     public function updateReport(Request $request, $id)
     {
         $this->validate($request, [
-            'action'	=> 'required|string',
+            'action' => 'required|string',
         ]);
 
         $action = $request->input('action');
@@ -470,7 +480,7 @@ trait AdminReportController
             'ban',
         ];
 
-        if (!in_array($action, $actions)) {
+        if (! in_array($action, $actions)) {
             return abort(403);
         }
 
@@ -479,7 +489,7 @@ trait AdminReportController
         $this->handleReportAction($report, $action);
         Cache::forget('admin-dash:reports:list-cache');
 
-        return response()->json(['msg'=> 'Success']);
+        return response()->json(['msg' => 'Success']);
     }
 
     public function handleReportAction(Report $report, $action)
@@ -541,7 +551,7 @@ trait AdminReportController
             '3' => 'unlist',
             '4' => 'delete',
             '5' => 'shadowban',
-            '6' => 'ban'
+            '6' => 'ban',
         ];
     }
 
@@ -549,675 +559,963 @@ trait AdminReportController
     {
         $this->validate($request, [
             'action' => 'required|integer|min:1|max:10',
-            'ids'    => 'required|array'
+            'ids' => 'required|array',
         ]);
         $action = $this->actionMap()[$request->input('action')];
         $ids = $request->input('ids');
         $reports = Report::whereIn('id', $ids)->whereNull('admin_seen')->get();
-        foreach($reports as $report) {
+        foreach ($reports as $report) {
             $this->handleReportAction($report, $action);
         }
         $res = [
             'message' => 'Success',
-            'code'    => 200
+            'code' => 200,
         ];
+
         return response()->json($res);
     }
 
     public function reportMailVerifications(Request $request)
     {
-    	$ids = Redis::smembers('email:manual');
-    	$ignored = Redis::smembers('email:manual-ignored');
-    	$reports = [];
-    	if($ids) {
-			$reports = collect($ids)
-				->filter(function($id) use($ignored) {
-					return !in_array($id, $ignored);
-				})
-				->map(function($id) {
-					$user = User::whereProfileId($id)->first();
-					if(!$user || $user->email_verified_at) {
-						return [];
-					}
-					$account = AccountService::get($id, true);
-					if(!$account) {
-						return [];
-					}
-					$account['email'] = $user->email;
-					return $account;
-				})
-				->filter(function($res) {
-					return $res && isset($res['id']);
-				})
-				->values();
-    	}
-    	return view('admin.reports.mail_verification', compact('reports', 'ignored'));
+        $ids = Redis::smembers('email:manual');
+        $ignored = Redis::smembers('email:manual-ignored');
+        $reports = [];
+        if ($ids) {
+            $reports = collect($ids)
+                ->filter(function ($id) use ($ignored) {
+                    return ! in_array($id, $ignored);
+                })
+                ->map(function ($id) {
+                    $user = User::whereProfileId($id)->first();
+                    if (! $user || $user->email_verified_at) {
+                        return [];
+                    }
+                    $account = AccountService::get($id, true);
+                    if (! $account) {
+                        return [];
+                    }
+                    $account['email'] = $user->email;
+
+                    return $account;
+                })
+                ->filter(function ($res) {
+                    return $res && isset($res['id']);
+                })
+                ->values();
+        }
+
+        return view('admin.reports.mail_verification', compact('reports', 'ignored'));
     }
 
     public function reportMailVerifyIgnore(Request $request)
     {
-    	$id = $request->input('id');
-    	Redis::sadd('email:manual-ignored', $id);
-    	return redirect('/i/admin/reports');
+        $id = $request->input('id');
+        Redis::sadd('email:manual-ignored', $id);
+
+        return redirect('/i/admin/reports');
     }
 
     public function reportMailVerifyApprove(Request $request)
     {
-    	$id = $request->input('id');
-    	$user = User::whereProfileId($id)->firstOrFail();
-    	Redis::srem('email:manual', $id);
-    	Redis::srem('email:manual-ignored', $id);
-    	$user->email_verified_at = now();
-    	$user->save();
-    	return redirect('/i/admin/reports');
+        $id = $request->input('id');
+        $user = User::whereProfileId($id)->firstOrFail();
+        Redis::srem('email:manual', $id);
+        Redis::srem('email:manual-ignored', $id);
+        $user->email_verified_at = now();
+        $user->save();
+
+        return redirect('/i/admin/reports');
     }
 
     public function reportMailVerifyClearIgnored(Request $request)
     {
-    	Redis::del('email:manual-ignored');
-    	return [200];
+        Redis::del('email:manual-ignored');
+
+        return [200];
     }
 
     public function reportsStats(Request $request)
     {
-    	$stats = [
-    		'total' => Report::count(),
-    		'open' => Report::whereNull('admin_seen')->count(),
-    		'closed' => Report::whereNotNull('admin_seen')->count(),
-    		'autospam' => AccountInterstitial::whereType('post.autospam')->count(),
-    		'autospam_open' => AccountInterstitial::whereType('post.autospam')->whereNull(['appeal_handled_at'])->count(),
-    		'appeals' => AccountInterstitial::whereNotNull('appeal_requested_at')->whereNull('appeal_handled_at')->count(),
-    		'email_verification_requests' => Redis::scard('email:manual')
-    	];
-    	return $stats;
+        $stats = [
+            'total' => Report::count(),
+            'open' => Report::whereNull('admin_seen')->count(),
+            'closed' => Report::whereNotNull('admin_seen')->count(),
+            'autospam' => AccountInterstitial::whereType('post.autospam')->count(),
+            'autospam_open' => AccountInterstitial::whereType('post.autospam')->whereNull(['appeal_handled_at'])->count(),
+            'appeals' => AccountInterstitial::whereNotNull('appeal_requested_at')->whereNull('appeal_handled_at')->count(),
+            'remote_open' => RemoteReport::whereNull('action_taken_at')->count(),
+            'email_verification_requests' => Redis::scard('email:manual'),
+        ];
+
+        return $stats;
     }
 
     public function reportsApiAll(Request $request)
     {
-    	$filter = $request->input('filter') == 'closed' ? 'closed' : 'open';
-
-    	$reports = AdminReport::collection(
-    		Report::orderBy('id','desc')
-			->when($filter, function($q, $filter) {
-				return $filter == 'open' ?
-				$q->whereNull('admin_seen') :
-				$q->whereNotNull('admin_seen');
-			})
-			->groupBy(['id', 'object_id', 'object_type', 'profile_id'])
-			->cursorPaginate(6)
-			->withQueryString()
-		);
-
-		return $reports;
+        $filter = $request->input('filter') == 'closed' ? 'closed' : 'open';
+
+        $reports = AdminReport::collection(
+            Report::orderBy('id', 'desc')
+                ->when($filter, function ($q, $filter) {
+                    return $filter == 'open' ?
+                    $q->whereNull('admin_seen') :
+                    $q->whereNotNull('admin_seen');
+                })
+                ->groupBy(['id', 'object_id', 'object_type', 'profile_id'])
+                ->cursorPaginate(6)
+                ->withQueryString()
+        );
+
+        return $reports;
+    }
+
+    public function reportsApiRemote(Request $request)
+    {
+        $filter = $request->input('filter') == 'closed' ? 'closed' : 'open';
+
+        $reports = AdminRemoteReport::collection(
+            RemoteReport::orderBy('id', 'desc')
+                ->when($filter, function ($q, $filter) {
+                    return $filter == 'open' ?
+                    $q->whereNull('action_taken_at') :
+                    $q->whereNotNull('action_taken_at');
+                })
+                ->cursorPaginate(6)
+                ->withQueryString()
+        );
+
+        return $reports;
     }
 
     public function reportsApiGet(Request $request, $id)
     {
-    	$report = Report::findOrFail($id);
-    	return new AdminReport($report);
+        $report = Report::findOrFail($id);
+
+        return new AdminReport($report);
     }
 
     public function reportsApiHandle(Request $request)
     {
-    	$this->validate($request, [
-    		'object_id' => 'required',
-    		'object_type' => 'required',
-    		'id' => 'required',
-    		'action' => 'required|in:ignore,nsfw,unlist,private,delete',
-    		'action_type' => 'required|in:post,profile'
-    	]);
-
-    	$report = Report::whereObjectId($request->input('object_id'))->findOrFail($request->input('id'));
-
-    	if($request->input('action_type') === 'profile') {
-    		return $this->reportsHandleProfileAction($report, $request->input('action'));
-    	} else if($request->input('action_type') === 'post') {
-    		return $this->reportsHandleStatusAction($report, $request->input('action'));
-    	}
-
-    	return $report;
+        $this->validate($request, [
+            'object_id' => 'required',
+            'object_type' => 'required',
+            'id' => 'required',
+            'action' => 'required|in:ignore,nsfw,unlist,private,delete,delete-all',
+            'action_type' => 'required|in:post,profile,story',
+        ]);
+
+        $report = Report::whereObjectId($request->input('object_id'))->findOrFail($request->input('id'));
+
+        if ($request->input('action_type') === 'profile') {
+            return $this->reportsHandleProfileAction($report, $request->input('action'));
+        } elseif ($request->input('action_type') === 'post') {
+            return $this->reportsHandleStatusAction($report, $request->input('action'));
+        } elseif ($request->input('action_type') === 'story') {
+            return $this->reportsHandleStoryAction($report, $request->input('action'));
+        }
+
+        return $report;
+    }
+
+    protected function reportsHandleStoryAction($report, $action)
+    {
+        switch ($action) {
+            case 'ignore':
+                Report::whereObjectId($report->object_id)
+                    ->whereObjectType($report->object_type)
+                    ->update([
+                        'admin_seen' => now(),
+                    ]);
+
+                return [200];
+                break;
+
+            case 'delete':
+                $profile = Profile::find($report->reported_profile_id);
+                $story = Story::whereProfileId($profile->id)->find($report->object_id);
+
+                abort_if(! $story, 400, 'Invalid or missing story');
+
+                $story->active = false;
+                $story->save();
+
+                ModLogService::boot()
+                    ->objectUid($profile->id)
+                    ->objectId($report->object_id)
+                    ->objectType('App\Story::class')
+                    ->user(request()->user())
+                    ->action('admin.user.moderate')
+                    ->metadata([
+                        'action' => 'delete',
+                        'message' => 'Success!',
+                    ])
+                    ->accessLevel('admin')
+                    ->save();
+
+                Report::whereObjectId($report->object_id)
+                    ->whereObjectType($report->object_type)
+                    ->update([
+                        'admin_seen' => now(),
+                    ]);
+                StoryDelete::dispatch($story)->onQueue('story');
+
+                return [200];
+                break;
+
+            case 'delete-all':
+                $profile = Profile::find($report->reported_profile_id);
+                $stories = Story::whereProfileId($profile->id)->whereActive(true)->get();
+
+                abort_if(! $stories || ! $stories->count(), 400, 'Invalid or missing stories');
+
+                ModLogService::boot()
+                    ->objectUid($profile->id)
+                    ->objectId($report->object_id)
+                    ->objectType('App\Story::class')
+                    ->user(request()->user())
+                    ->action('admin.user.moderate')
+                    ->metadata([
+                        'action' => 'delete-all',
+                        'message' => 'Success!',
+                    ])
+                    ->accessLevel('admin')
+                    ->save();
+
+                Report::where('reported_profile_id', $profile->id)
+                    ->whereObjectType('App\Story')
+                    ->whereNull('admin_seen')
+                    ->update([
+                        'admin_seen' => now(),
+                    ]);
+                $stories->each(function ($story) {
+                    StoryDelete::dispatch($story)->onQueue('story');
+                });
+
+                return [200];
+                break;
+        }
     }
 
     protected function reportsHandleProfileAction($report, $action)
     {
-    	switch($action) {
-    		case 'ignore':
-    			Report::whereObjectId($report->object_id)
-    				->whereObjectType($report->object_type)
-    				->update([
-    					'admin_seen' => now()
-    				]);
-    			return [200];
-    		break;
-
-    		case 'nsfw':
-    			if($report->object_type === 'App\Profile') {
-    				$profile = Profile::find($report->object_id);
-    			} else if($report->object_type === 'App\Status') {
-    				$status = Status::find($report->object_id);
-    				if(!$status) {
-    					return [200];
-    				}
-    				$profile = Profile::find($status->profile_id);
-    			}
-
-    			if(!$profile) {
-    				return;
-    			}
-
-    			abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot moderate an admin account.');
-
-    			$profile->cw = true;
-    			$profile->save();
-
-    			foreach(Status::whereProfileId($profile->id)->cursor() as $status) {
-    				$status->is_nsfw = true;
-    				$status->save();
-    				StatusService::del($status->id);
-    				PublicTimelineService::rem($status->id);
-    			}
-
-				ModLogService::boot()
-					->objectUid($profile->id)
-					->objectId($profile->id)
-					->objectType('App\Profile::class')
-					->user(request()->user())
-					->action('admin.user.moderate')
-					->metadata([
-	                    'action' => 'cw',
-	                    'message' => 'Success!'
-                	])
-					->accessLevel('admin')
-					->save();
-
-    			Report::whereObjectId($report->object_id)
-    				->whereObjectType($report->object_type)
-    				->update([
-    					'nsfw' => true,
-    					'admin_seen' => now()
-    				]);
-    			return [200];
-    		break;
-
-    		case 'unlist':
-    			if($report->object_type === 'App\Profile') {
-    				$profile = Profile::find($report->object_id);
-    			} else if($report->object_type === 'App\Status') {
-    				$status = Status::find($report->object_id);
-    				if(!$status) {
-    					return [200];
-    				}
-    				$profile = Profile::find($status->profile_id);
-    			}
-
-    			if(!$profile) {
-    				return;
-    			}
-
-    			abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot moderate an admin account.');
-
-    			$profile->unlisted = true;
-    			$profile->save();
-
-    			foreach(Status::whereProfileId($profile->id)->whereScope('public')->cursor() as $status) {
-					$status->scope = 'unlisted';
-					$status->visibility = 'unlisted';
-					$status->save();
-					StatusService::del($status->id);
-					PublicTimelineService::rem($status->id);
-    			}
-
-				ModLogService::boot()
-					->objectUid($profile->id)
-					->objectId($profile->id)
-					->objectType('App\Profile::class')
-					->user(request()->user())
-					->action('admin.user.moderate')
-					->metadata([
-	                    'action' => 'unlisted',
-	                    'message' => 'Success!'
-                	])
-					->accessLevel('admin')
-					->save();
-
-    			Report::whereObjectId($report->object_id)
-    				->whereObjectType($report->object_type)
-    				->update([
-    					'admin_seen' => now()
-    				]);
-    			return [200];
-    		break;
-
-    		case 'private':
-    			if($report->object_type === 'App\Profile') {
-    				$profile = Profile::find($report->object_id);
-    			} else if($report->object_type === 'App\Status') {
-    				$status = Status::find($report->object_id);
-    				if(!$status) {
-    					return [200];
-    				}
-    				$profile = Profile::find($status->profile_id);
-    			}
-
-    			if(!$profile) {
-    				return;
-    			}
-
-    			abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot moderate an admin account.');
-
-    			$profile->unlisted = true;
-    			$profile->save();
-
-    			foreach(Status::whereProfileId($profile->id)->cursor() as $status) {
-					$status->scope = 'private';
-					$status->visibility = 'private';
-					$status->save();
-					StatusService::del($status->id);
-					PublicTimelineService::rem($status->id);
-    			}
-
-				ModLogService::boot()
-					->objectUid($profile->id)
-					->objectId($profile->id)
-					->objectType('App\Profile::class')
-					->user(request()->user())
-					->action('admin.user.moderate')
-					->metadata([
-	                    'action' => 'private',
-	                    'message' => 'Success!'
-                	])
-					->accessLevel('admin')
-					->save();
-
-    			Report::whereObjectId($report->object_id)
-    				->whereObjectType($report->object_type)
-    				->update([
-    					'admin_seen' => now()
-    				]);
-    			return [200];
-    		break;
-
-    		case 'delete':
-				if(config('pixelfed.account_deletion') == false) {
-					abort(404);
-				}
-
-    			if($report->object_type === 'App\Profile') {
-    				$profile = Profile::find($report->object_id);
-    			} else if($report->object_type === 'App\Status') {
-    				$status = Status::find($report->object_id);
-    				if(!$status) {
-    					return [200];
-    				}
-    				$profile = Profile::find($status->profile_id);
-    			}
-
-    			if(!$profile) {
-    				return;
-    			}
-
-    			abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot delete an admin account.');
-
-				$ts = now()->addMonth();
-
-    			if($profile->user_id) {
-	    			$user = $profile->user;
-					abort_if($user->is_admin, 403, 'You cannot delete admin accounts.');
-					$user->status = 'delete';
-					$user->delete_after = $ts;
-					$user->save();
-    			}
-
-				$profile->status = 'delete';
-				$profile->delete_after = $ts;
-				$profile->save();
-
-				ModLogService::boot()
-					->objectUid($profile->id)
-					->objectId($profile->id)
-					->objectType('App\Profile::class')
-					->user(request()->user())
-					->action('admin.user.delete')
-					->accessLevel('admin')
-					->save();
-
-    			Report::whereObjectId($report->object_id)
-    				->whereObjectType($report->object_type)
-    				->update([
-    					'admin_seen' => now()
-    				]);
-
-    			if($profile->user_id) {
-    				DB::table('oauth_access_tokens')->whereUserId($user->id)->delete();
-    				DB::table('oauth_auth_codes')->whereUserId($user->id)->delete();
-					$user->email = $user->id;
-					$user->password = '';
-					$user->status = 'delete';
-					$user->save();
-					$profile->status = 'delete';
-					$profile->delete_after = now()->addMonth();
-					$profile->save();
-    				AccountService::del($profile->id);
-    				DeleteAccountPipeline::dispatch($user)->onQueue('high');
-    			} else {
-    				$profile->status = 'delete';
-					$profile->delete_after = now()->addMonth();
-					$profile->save();
-    				AccountService::del($profile->id);
-    				DeleteRemoteProfilePipeline::dispatch($profile)->onQueue('high');
-    			}
-    			return [200];
-    		break;
-    	}
+        switch ($action) {
+            case 'ignore':
+                Report::whereObjectId($report->object_id)
+                    ->whereObjectType($report->object_type)
+                    ->update([
+                        'admin_seen' => now(),
+                    ]);
+
+                return [200];
+                break;
+
+            case 'nsfw':
+                if ($report->object_type === 'App\Profile') {
+                    $profile = Profile::find($report->object_id);
+                } elseif ($report->object_type === 'App\Status') {
+                    $status = Status::find($report->object_id);
+                    if (! $status) {
+                        return [200];
+                    }
+                    $profile = Profile::find($status->profile_id);
+                }
+
+                if (! $profile) {
+                    return;
+                }
+
+                abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot moderate an admin account.');
+
+                $profile->cw = true;
+                $profile->save();
+
+                foreach (Status::whereProfileId($profile->id)->cursor() as $status) {
+                    $status->is_nsfw = true;
+                    $status->save();
+                    StatusService::del($status->id);
+                    PublicTimelineService::rem($status->id);
+                }
+
+                ModLogService::boot()
+                    ->objectUid($profile->id)
+                    ->objectId($profile->id)
+                    ->objectType('App\Profile::class')
+                    ->user(request()->user())
+                    ->action('admin.user.moderate')
+                    ->metadata([
+                        'action' => 'cw',
+                        'message' => 'Success!',
+                    ])
+                    ->accessLevel('admin')
+                    ->save();
+
+                Report::whereObjectId($report->object_id)
+                    ->whereObjectType($report->object_type)
+                    ->update([
+                        'nsfw' => true,
+                        'admin_seen' => now(),
+                    ]);
+
+                return [200];
+                break;
+
+            case 'unlist':
+                if ($report->object_type === 'App\Profile') {
+                    $profile = Profile::find($report->object_id);
+                } elseif ($report->object_type === 'App\Status') {
+                    $status = Status::find($report->object_id);
+                    if (! $status) {
+                        return [200];
+                    }
+                    $profile = Profile::find($status->profile_id);
+                }
+
+                if (! $profile) {
+                    return;
+                }
+
+                abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot moderate an admin account.');
+
+                $profile->unlisted = true;
+                $profile->save();
+
+                foreach (Status::whereProfileId($profile->id)->whereScope('public')->cursor() as $status) {
+                    $status->scope = 'unlisted';
+                    $status->visibility = 'unlisted';
+                    $status->save();
+                    StatusService::del($status->id);
+                    PublicTimelineService::rem($status->id);
+                }
+
+                ModLogService::boot()
+                    ->objectUid($profile->id)
+                    ->objectId($profile->id)
+                    ->objectType('App\Profile::class')
+                    ->user(request()->user())
+                    ->action('admin.user.moderate')
+                    ->metadata([
+                        'action' => 'unlisted',
+                        'message' => 'Success!',
+                    ])
+                    ->accessLevel('admin')
+                    ->save();
+
+                Report::whereObjectId($report->object_id)
+                    ->whereObjectType($report->object_type)
+                    ->update([
+                        'admin_seen' => now(),
+                    ]);
+
+                return [200];
+                break;
+
+            case 'private':
+                if ($report->object_type === 'App\Profile') {
+                    $profile = Profile::find($report->object_id);
+                } elseif ($report->object_type === 'App\Status') {
+                    $status = Status::find($report->object_id);
+                    if (! $status) {
+                        return [200];
+                    }
+                    $profile = Profile::find($status->profile_id);
+                }
+
+                if (! $profile) {
+                    return;
+                }
+
+                abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot moderate an admin account.');
+
+                $profile->unlisted = true;
+                $profile->save();
+
+                foreach (Status::whereProfileId($profile->id)->cursor() as $status) {
+                    $status->scope = 'private';
+                    $status->visibility = 'private';
+                    $status->save();
+                    StatusService::del($status->id);
+                    PublicTimelineService::rem($status->id);
+                }
+
+                ModLogService::boot()
+                    ->objectUid($profile->id)
+                    ->objectId($profile->id)
+                    ->objectType('App\Profile::class')
+                    ->user(request()->user())
+                    ->action('admin.user.moderate')
+                    ->metadata([
+                        'action' => 'private',
+                        'message' => 'Success!',
+                    ])
+                    ->accessLevel('admin')
+                    ->save();
+
+                Report::whereObjectId($report->object_id)
+                    ->whereObjectType($report->object_type)
+                    ->update([
+                        'admin_seen' => now(),
+                    ]);
+
+                return [200];
+                break;
+
+            case 'delete':
+                if (config('pixelfed.account_deletion') == false) {
+                    abort(404);
+                }
+
+                if ($report->object_type === 'App\Profile') {
+                    $profile = Profile::find($report->object_id);
+                } elseif ($report->object_type === 'App\Status') {
+                    $status = Status::find($report->object_id);
+                    if (! $status) {
+                        return [200];
+                    }
+                    $profile = Profile::find($status->profile_id);
+                }
+
+                if (! $profile) {
+                    return;
+                }
+
+                abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot delete an admin account.');
+
+                $ts = now()->addMonth();
+
+                if ($profile->user_id) {
+                    $user = $profile->user;
+                    abort_if($user->is_admin, 403, 'You cannot delete admin accounts.');
+                    $user->status = 'delete';
+                    $user->delete_after = $ts;
+                    $user->save();
+                }
+
+                $profile->status = 'delete';
+                $profile->delete_after = $ts;
+                $profile->save();
+
+                ModLogService::boot()
+                    ->objectUid($profile->id)
+                    ->objectId($profile->id)
+                    ->objectType('App\Profile::class')
+                    ->user(request()->user())
+                    ->action('admin.user.delete')
+                    ->accessLevel('admin')
+                    ->save();
+
+                Report::whereObjectId($report->object_id)
+                    ->whereObjectType($report->object_type)
+                    ->update([
+                        'admin_seen' => now(),
+                    ]);
+
+                if ($profile->user_id) {
+                    DB::table('oauth_access_tokens')->whereUserId($user->id)->delete();
+                    DB::table('oauth_auth_codes')->whereUserId($user->id)->delete();
+                    $user->email = $user->id;
+                    $user->password = '';
+                    $user->status = 'delete';
+                    $user->save();
+                    $profile->status = 'delete';
+                    $profile->delete_after = now()->addMonth();
+                    $profile->save();
+                    AccountService::del($profile->id);
+                    DeleteAccountPipeline::dispatch($user)->onQueue('high');
+                } else {
+                    $profile->status = 'delete';
+                    $profile->delete_after = now()->addMonth();
+                    $profile->save();
+                    AccountService::del($profile->id);
+                    DeleteRemoteProfilePipeline::dispatch($profile)->onQueue('high');
+                }
+
+                return [200];
+                break;
+        }
     }
 
     protected function reportsHandleStatusAction($report, $action)
     {
-    	switch($action) {
-    		case 'ignore':
-    			Report::whereObjectId($report->object_id)
-    				->whereObjectType($report->object_type)
-    				->update([
-    					'admin_seen' => now()
-    				]);
-    			return [200];
-    		break;
-
-    		case 'nsfw':
-    			$status = Status::find($report->object_id);
-
-    			if(!$status) {
-    				return [200];
-    			}
-
-				abort_if($status->profile->user && $status->profile->user->is_admin, 400, 'Cannot moderate an admin account post.');
-    			$status->is_nsfw = true;
-    			$status->save();
-    			StatusService::del($status->id);
-
-				ModLogService::boot()
-					->objectUid($status->profile_id)
-					->objectId($status->profile_id)
-					->objectType('App\Status::class')
-					->user(request()->user())
-					->action('admin.status.moderate')
-					->metadata([
-	                    'action' => 'cw',
-	                    'message' => 'Success!'
-                	])
-					->accessLevel('admin')
-					->save();
-
-    			Report::whereObjectId($report->object_id)
-    				->whereObjectType($report->object_type)
-    				->update([
-    					'nsfw' => true,
-    					'admin_seen' => now()
-    				]);
-    			return [200];
-    		break;
-
-    		case 'private':
-    			$status = Status::find($report->object_id);
-
-    			if(!$status) {
-    				return [200];
-    			}
-
-				abort_if($status->profile->user && $status->profile->user->is_admin, 400, 'Cannot moderate an admin account post.');
-
-    			$status->scope = 'private';
-    			$status->visibility = 'private';
-    			$status->save();
-    			StatusService::del($status->id);
-				PublicTimelineService::rem($status->id);
-
-				ModLogService::boot()
-					->objectUid($status->profile_id)
-					->objectId($status->profile_id)
-					->objectType('App\Status::class')
-					->user(request()->user())
-					->action('admin.status.moderate')
-					->metadata([
-	                    'action' => 'private',
-	                    'message' => 'Success!'
-                	])
-					->accessLevel('admin')
-					->save();
-
-    			Report::whereObjectId($report->object_id)
-    				->whereObjectType($report->object_type)
-    				->update([
-    					'admin_seen' => now()
-    				]);
-    			return [200];
-    		break;
-
-    		case 'unlist':
-    			$status = Status::find($report->object_id);
-
-    			if(!$status) {
-    				return [200];
-    			}
-
-				abort_if($status->profile->user && $status->profile->user->is_admin, 400, 'Cannot moderate an admin account post.');
-
-    			if($status->scope === 'public') {
-	    			$status->scope = 'unlisted';
-	    			$status->visibility = 'unlisted';
-	    			$status->save();
-	    			StatusService::del($status->id);
-    				PublicTimelineService::rem($status->id);
-    			}
-
-				ModLogService::boot()
-					->objectUid($status->profile_id)
-					->objectId($status->profile_id)
-					->objectType('App\Status::class')
-					->user(request()->user())
-					->action('admin.status.moderate')
-					->metadata([
-	                    'action' => 'unlist',
-	                    'message' => 'Success!'
-                	])
-					->accessLevel('admin')
-					->save();
-
-    			Report::whereObjectId($report->object_id)
-    				->whereObjectType($report->object_type)
-    				->update([
-    					'admin_seen' => now()
-    				]);
-    			return [200];
-    		break;
-
-    		case 'delete':
-    			$status = Status::find($report->object_id);
-
-    			if(!$status) {
-    				return [200];
-    			}
-
-    			$profile = $status->profile;
-
-    			abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot delete an admin account post.');
-
-    			StatusService::del($status->id);
-
-    			if($profile->user_id != null && $profile->domain == null) {
-    				PublicTimelineService::del($status->id);
-    				StatusDelete::dispatch($status)->onQueue('high');
-    			} else {
-    				NetworkTimelineService::del($status->id);
-    				RemoteStatusDelete::dispatch($status)->onQueue('high');
-    			}
-
-    			Report::whereObjectId($report->object_id)
-    				->whereObjectType($report->object_type)
-    				->update([
-    					'admin_seen' => now()
-    				]);
-
-    			return [200];
-    		break;
-    	}
+        switch ($action) {
+            case 'ignore':
+                Report::whereObjectId($report->object_id)
+                    ->whereObjectType($report->object_type)
+                    ->update([
+                        'admin_seen' => now(),
+                    ]);
+
+                return [200];
+                break;
+
+            case 'nsfw':
+                $status = Status::find($report->object_id);
+
+                if (! $status) {
+                    return [200];
+                }
+
+                abort_if($status->profile->user && $status->profile->user->is_admin, 400, 'Cannot moderate an admin account post.');
+                $status->is_nsfw = true;
+                $status->save();
+                StatusService::del($status->id);
+
+                ModLogService::boot()
+                    ->objectUid($status->profile_id)
+                    ->objectId($status->profile_id)
+                    ->objectType('App\Status::class')
+                    ->user(request()->user())
+                    ->action('admin.status.moderate')
+                    ->metadata([
+                        'action' => 'cw',
+                        'message' => 'Success!',
+                    ])
+                    ->accessLevel('admin')
+                    ->save();
+
+                Report::whereObjectId($report->object_id)
+                    ->whereObjectType($report->object_type)
+                    ->update([
+                        'nsfw' => true,
+                        'admin_seen' => now(),
+                    ]);
+
+                return [200];
+                break;
+
+            case 'private':
+                $status = Status::find($report->object_id);
+
+                if (! $status) {
+                    return [200];
+                }
+
+                abort_if($status->profile->user && $status->profile->user->is_admin, 400, 'Cannot moderate an admin account post.');
+
+                $status->scope = 'private';
+                $status->visibility = 'private';
+                $status->save();
+                StatusService::del($status->id);
+                PublicTimelineService::rem($status->id);
+
+                ModLogService::boot()
+                    ->objectUid($status->profile_id)
+                    ->objectId($status->profile_id)
+                    ->objectType('App\Status::class')
+                    ->user(request()->user())
+                    ->action('admin.status.moderate')
+                    ->metadata([
+                        'action' => 'private',
+                        'message' => 'Success!',
+                    ])
+                    ->accessLevel('admin')
+                    ->save();
+
+                Report::whereObjectId($report->object_id)
+                    ->whereObjectType($report->object_type)
+                    ->update([
+                        'admin_seen' => now(),
+                    ]);
+
+                return [200];
+                break;
+
+            case 'unlist':
+                $status = Status::find($report->object_id);
+
+                if (! $status) {
+                    return [200];
+                }
+
+                abort_if($status->profile->user && $status->profile->user->is_admin, 400, 'Cannot moderate an admin account post.');
+
+                if ($status->scope === 'public') {
+                    $status->scope = 'unlisted';
+                    $status->visibility = 'unlisted';
+                    $status->save();
+                    StatusService::del($status->id);
+                    PublicTimelineService::rem($status->id);
+                }
+
+                ModLogService::boot()
+                    ->objectUid($status->profile_id)
+                    ->objectId($status->profile_id)
+                    ->objectType('App\Status::class')
+                    ->user(request()->user())
+                    ->action('admin.status.moderate')
+                    ->metadata([
+                        'action' => 'unlist',
+                        'message' => 'Success!',
+                    ])
+                    ->accessLevel('admin')
+                    ->save();
+
+                Report::whereObjectId($report->object_id)
+                    ->whereObjectType($report->object_type)
+                    ->update([
+                        'admin_seen' => now(),
+                    ]);
+
+                return [200];
+                break;
+
+            case 'delete':
+                $status = Status::find($report->object_id);
+
+                if (! $status) {
+                    return [200];
+                }
+
+                $profile = $status->profile;
+
+                abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot delete an admin account post.');
+
+                StatusService::del($status->id);
+
+                if ($profile->user_id != null && $profile->domain == null) {
+                    PublicTimelineService::del($status->id);
+                    StatusDelete::dispatch($status)->onQueue('high');
+                } else {
+                    NetworkTimelineService::del($status->id);
+                    RemoteStatusDelete::dispatch($status)->onQueue('high');
+                }
+
+                Report::whereObjectId($report->object_id)
+                    ->whereObjectType($report->object_type)
+                    ->update([
+                        'admin_seen' => now(),
+                    ]);
+
+                return [200];
+                break;
+        }
     }
 
     public function reportsApiSpamAll(Request $request)
     {
-    	$tab = $request->input('tab', 'home');
+        $tab = $request->input('tab', 'home');
 
-		$appeals = AdminSpamReport::collection(
-			AccountInterstitial::orderBy('id', 'desc')
-			->whereType('post.autospam')
-			->whereNull('appeal_handled_at')
-			->cursorPaginate(6)
-			->withQueryString()
-		);
+        $appeals = AdminSpamReport::collection(
+            AccountInterstitial::orderBy('id', 'desc')
+                ->whereType('post.autospam')
+                ->whereNull('appeal_handled_at')
+                ->cursorPaginate(6)
+                ->withQueryString()
+        );
 
-		return $appeals;
+        return $appeals;
     }
 
     public function reportsApiSpamHandle(Request $request)
     {
-    	$this->validate($request, [
-    		'id' => 'required',
-    		'action' => 'required|in:mark-read,mark-not-spam,mark-all-read,mark-all-not-spam,delete-profile',
-    	]);
-
-    	$action = $request->input('action');
-
-		abort_if(
-			$action === 'delete-profile' &&
-			!config('pixelfed.account_deletion'),
-			404,
-			"Cannot delete profile, account_deletion is disabled.\n\n Set `ACCOUNT_DELETION=true` in .env and re-cache config."
-		);
-
-    	$report = AccountInterstitial::with('user')
-    		->whereType('post.autospam')
-    		->whereNull('appeal_handled_at')
-    		->findOrFail($request->input('id'));
-
-    	$this->reportsHandleSpamAction($report, $action);
-    	Cache::forget('admin-dash:reports:spam-count');
-    	Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $report->user->profile_id);
-		Cache::forget('pf:bouncer_v0:recent_by_pid:' . $report->user->profile_id);
-    	return [$action, $report];
+        $this->validate($request, [
+            'id' => 'required',
+            'action' => 'required|in:mark-read,mark-not-spam,mark-all-read,mark-all-not-spam,delete-profile',
+        ]);
+
+        $action = $request->input('action');
+
+        abort_if(
+            $action === 'delete-profile' &&
+            ! config('pixelfed.account_deletion'),
+            404,
+            "Cannot delete profile, account_deletion is disabled.\n\n Set `ACCOUNT_DELETION=true` in .env and re-cache config."
+        );
+
+        $report = AccountInterstitial::with('user')
+            ->whereType('post.autospam')
+            ->whereNull('appeal_handled_at')
+            ->findOrFail($request->input('id'));
+
+        $this->reportsHandleSpamAction($report, $action);
+        Cache::forget('admin-dash:reports:spam-count');
+        Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$report->user->profile_id);
+        Cache::forget('pf:bouncer_v0:recent_by_pid:'.$report->user->profile_id);
+
+        return [$action, $report];
     }
 
     public function reportsHandleSpamAction($appeal, $action)
     {
-    	$meta = json_decode($appeal->meta);
-
-		if($action == 'mark-read') {
-			$appeal->is_spam = true;
-			$appeal->appeal_handled_at = now();
-			$appeal->save();
-			PublicTimelineService::del($appeal->item_id);
-		}
-
-		if($action == 'mark-not-spam') {
-			$status = $appeal->status;
-			$status->is_nsfw = $meta->is_nsfw;
-			$status->scope = 'public';
-			$status->visibility = 'public';
-			$status->save();
-
-			$appeal->is_spam = false;
-			$appeal->appeal_handled_at = now();
-			$appeal->save();
-
-			Notification::whereAction('autospam.warning')
-				->whereProfileId($appeal->user->profile_id)
-				->get()
-				->each(function($n) use($appeal) {
-					NotificationService::del($appeal->user->profile_id, $n->id);
-					$n->forceDelete();
-				});
-
-			StatusService::del($status->id);
-			StatusService::get($status->id);
-			if($status->in_reply_to_id == null && $status->reblog_of_id == null) {
-				PublicTimelineService::add($status->id);
-			}
-		}
-
-		if($action == 'mark-all-read') {
-			AccountInterstitial::whereType('post.autospam')
-				->whereItemType('App\Status')
-				->whereNull('appeal_handled_at')
-				->whereUserId($appeal->user_id)
-				->update([
-					'appeal_handled_at' => now(),
-					'is_spam' => true
-				]);
-		}
-
-		if($action == 'mark-all-not-spam') {
-			AccountInterstitial::whereType('post.autospam')
-				->whereItemType('App\Status')
-				->whereUserId($appeal->user_id)
-				->get()
-				->each(function($report) use($meta) {
-					$report->is_spam = false;
-					$report->appeal_handled_at = now();
-					$report->save();
-					$status = Status::find($report->item_id);
-					if($status) {
-						$status->is_nsfw = $meta->is_nsfw;
-						$status->scope = 'public';
-						$status->visibility = 'public';
-						$status->save();
-						StatusService::del($status->id);
-					}
-					Notification::whereAction('autospam.warning')
-						->whereProfileId($report->user->profile_id)
-						->get()
-						->each(function($n) use($report) {
-							NotificationService::del($report->user->profile_id, $n->id);
-							$n->forceDelete();
-						});
-				});
-		}
-
-		if($action == 'delete-profile') {
-			$user = User::findOrFail($appeal->user_id);
-			$profile = $user->profile;
-
-			if($user->is_admin == true) {
-				$mid = request()->user()->id;
-				abort_if($user->id < $mid, 403, 'You cannot delete an admin account.');
-			}
-
-			$ts = now()->addMonth();
-			$user->status = 'delete';
-			$profile->status = 'delete';
-			$user->delete_after = $ts;
-			$profile->delete_after = $ts;
-			$user->save();
-			$profile->save();
-
-			$appeal->appeal_handled_at = now();
-			$appeal->save();
-
-			ModLogService::boot()
-				->objectUid($user->id)
-				->objectId($user->id)
-				->objectType('App\User::class')
-				->user(request()->user())
-				->action('admin.user.delete')
-				->accessLevel('admin')
-				->save();
-
-			Cache::forget('profiles:private');
-			DeleteAccountPipeline::dispatch($user);
-		}
+        $meta = json_decode($appeal->meta);
+
+        if ($action == 'mark-read') {
+            $appeal->is_spam = true;
+            $appeal->appeal_handled_at = now();
+            $appeal->save();
+            PublicTimelineService::del($appeal->item_id);
+        }
+
+        if ($action == 'mark-not-spam') {
+            $status = $appeal->status;
+            $status->is_nsfw = $meta->is_nsfw;
+            $status->scope = 'public';
+            $status->visibility = 'public';
+            $status->save();
+
+            $appeal->is_spam = false;
+            $appeal->appeal_handled_at = now();
+            $appeal->save();
+
+            Notification::whereAction('autospam.warning')
+                ->whereProfileId($appeal->user->profile_id)
+                ->get()
+                ->each(function ($n) use ($appeal) {
+                    NotificationService::del($appeal->user->profile_id, $n->id);
+                    $n->forceDelete();
+                });
+
+            StatusService::del($status->id);
+            StatusService::get($status->id);
+            if ($status->in_reply_to_id == null && $status->reblog_of_id == null) {
+                PublicTimelineService::add($status->id);
+            }
+        }
+
+        if ($action == 'mark-all-read') {
+            AccountInterstitial::whereType('post.autospam')
+                ->whereItemType('App\Status')
+                ->whereNull('appeal_handled_at')
+                ->whereUserId($appeal->user_id)
+                ->update([
+                    'appeal_handled_at' => now(),
+                    'is_spam' => true,
+                ]);
+        }
+
+        if ($action == 'mark-all-not-spam') {
+            AccountInterstitial::whereType('post.autospam')
+                ->whereItemType('App\Status')
+                ->whereUserId($appeal->user_id)
+                ->get()
+                ->each(function ($report) use ($meta) {
+                    $report->is_spam = false;
+                    $report->appeal_handled_at = now();
+                    $report->save();
+                    $status = Status::find($report->item_id);
+                    if ($status) {
+                        $status->is_nsfw = $meta->is_nsfw;
+                        $status->scope = 'public';
+                        $status->visibility = 'public';
+                        $status->save();
+                        StatusService::del($status->id);
+                    }
+                    Notification::whereAction('autospam.warning')
+                        ->whereProfileId($report->user->profile_id)
+                        ->get()
+                        ->each(function ($n) use ($report) {
+                            NotificationService::del($report->user->profile_id, $n->id);
+                            $n->forceDelete();
+                        });
+                });
+        }
+
+        if ($action == 'delete-profile') {
+            $user = User::findOrFail($appeal->user_id);
+            $profile = $user->profile;
+
+            if ($user->is_admin == true) {
+                $mid = request()->user()->id;
+                abort_if($user->id < $mid, 403, 'You cannot delete an admin account.');
+            }
+
+            $ts = now()->addMonth();
+            $user->status = 'delete';
+            $profile->status = 'delete';
+            $user->delete_after = $ts;
+            $profile->delete_after = $ts;
+            $user->save();
+            $profile->save();
+
+            $appeal->appeal_handled_at = now();
+            $appeal->save();
+
+            ModLogService::boot()
+                ->objectUid($user->id)
+                ->objectId($user->id)
+                ->objectType('App\User::class')
+                ->user(request()->user())
+                ->action('admin.user.delete')
+                ->accessLevel('admin')
+                ->save();
+
+            Cache::forget('profiles:private');
+            DeleteAccountPipeline::dispatch($user);
+        }
     }
 
     public function reportsApiSpamGet(Request $request, $id)
     {
-    	$report = AccountInterstitial::findOrFail($id);
-    	return new AdminSpamReport($report);
+        $report = AccountInterstitial::findOrFail($id);
+
+        return new AdminSpamReport($report);
+    }
+
+    public function reportsApiRemoteHandle(Request $request)
+    {
+        $this->validate($request, [
+            'id' => 'required|exists:remote_reports,id',
+            'action' => 'required|in:mark-read,cw-posts,unlist-posts,delete-posts,private-posts,mark-all-read-by-domain,mark-all-read-by-username,cw-all-posts,private-all-posts,unlist-all-posts'
+        ]);
+
+        $report = RemoteReport::findOrFail($request->input('id'));
+        $user = User::whereProfileId($report->account_id)->first();
+        $ogPublicStatuses = [];
+        $ogUnlistedStatuses = [];
+        $ogNonCwStatuses = [];
+
+        switch ($request->input('action')) {
+            case 'mark-read':
+                $report->action_taken_at = now();
+                $report->save();
+                break;
+            case 'mark-all-read-by-domain':
+                RemoteReport::whereInstanceId($report->instance_id)->update(['action_taken_at' => now()]);
+                break;
+            case 'cw-posts':
+                $statuses = Status::find($report->status_ids);
+                foreach($statuses as $status) {
+                    if($report->account_id != $status->profile_id) {
+                        continue;
+                    }
+                    if(!$status->is_nsfw) {
+                        $ogNonCwStatuses[] = $status->id;
+                    }
+                    $status->is_nsfw = true;
+                    $status->saveQuietly();
+                    StatusService::del($status->id);
+                }
+                $report->action_taken_at = now();
+                $report->save();
+                break;
+            case 'cw-all-posts':
+                foreach(Status::whereProfileId($report->account_id)->lazyById(50, 'id') as $status) {
+                    if($status->is_nsfw || $status->reblog_of_id) {
+                        continue;
+                    }
+                    if(!$status->is_nsfw) {
+                        $ogNonCwStatuses[] = $status->id;
+                    }
+                    $status->is_nsfw = true;
+                    $status->saveQuietly();
+                    StatusService::del($status->id);
+                }
+                break;
+            case 'unlist-posts':
+                $statuses = Status::find($report->status_ids);
+                foreach($statuses as $status) {
+                    if($report->account_id != $status->profile_id) {
+                        continue;
+                    }
+                    if($status->scope === 'public') {
+                        $ogPublicStatuses[] = $status->id;
+                        $status->scope = 'unlisted';
+                        $status->visibility = 'unlisted';
+                        $status->saveQuietly();
+                        StatusService::del($status->id);
+                    }
+                }
+                $report->action_taken_at = now();
+                $report->save();
+                break;
+            case 'unlist-all-posts':
+                foreach(Status::whereProfileId($report->account_id)->lazyById(50, 'id') as $status) {
+                    if($status->visibility !== 'public' || $status->reblog_of_id) {
+                        continue;
+                    }
+                    $ogPublicStatuses[] = $status->id;
+                    $status->visibility = 'unlisted';
+                    $status->scope = 'unlisted';
+                    $status->saveQuietly();
+                    StatusService::del($status->id);
+                }
+                break;
+            case 'private-posts':
+                $statuses = Status::find($report->status_ids);
+                foreach($statuses as $status) {
+                    if($report->account_id != $status->profile_id) {
+                        continue;
+                    }
+                    if(in_array($status->scope, ['public', 'unlisted', 'private'])) {
+                        if($status->scope === 'public') {
+                            $ogPublicStatuses[] = $status->id;
+                        }
+                        $status->scope = 'private';
+                        $status->visibility = 'private';
+                        $status->saveQuietly();
+                        StatusService::del($status->id);
+                    }
+                }
+                $report->action_taken_at = now();
+                $report->save();
+                break;
+            case 'private-all-posts':
+                foreach(Status::whereProfileId($report->account_id)->lazyById(50, 'id') as $status) {
+                    if(!in_array($status->visibility, ['public', 'unlisted']) || $status->reblog_of_id) {
+                        continue;
+                    }
+                    if($status->visibility === 'public') {
+                        $ogPublicStatuses[] = $status->id;
+                    } else if($status->visibility === 'unlisted') {
+                        $ogUnlistedStatuses[] = $status->id;
+                    }
+                    $status->visibility = 'private';
+                    $status->scope = 'private';
+                    $status->saveQuietly();
+                    StatusService::del($status->id);
+                }
+                break;
+            case 'delete-posts':
+                $statuses = Status::find($report->status_ids);
+                foreach($statuses as $status) {
+                    if($report->account_id != $status->profile_id) {
+                        continue;
+                    }
+                    StatusDelete::dispatch($status);
+                }
+                $report->action_taken_at = now();
+                $report->save();
+                break;
+            case 'mark-all-read-by-username':
+                RemoteReport::whereNull('action_taken_at')->whereAccountId($report->account_id)->update(['action_taken_at' => now()]);
+                break;
+
+            default:
+                abort(404);
+                break;
+        }
+
+        if($ogPublicStatuses && count($ogPublicStatuses)) {
+            Storage::disk('local')->put('mod-log-cache/' . $report->account_id . '/' . now()->format('Y-m-d') . '-og-public-statuses.json', json_encode($ogPublicStatuses, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES));
+        }
+
+        if($ogNonCwStatuses && count($ogNonCwStatuses)) {
+            Storage::disk('local')->put('mod-log-cache/' . $report->account_id . '/' . now()->format('Y-m-d') . '-og-noncw-statuses.json', json_encode($ogNonCwStatuses, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES));
+        }
+
+        if($ogUnlistedStatuses && count($ogUnlistedStatuses)) {
+            Storage::disk('local')->put('mod-log-cache/' . $report->account_id . '/' . now()->format('Y-m-d') . '-og-unlisted-statuses.json', json_encode($ogUnlistedStatuses, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES));
+        }
+
+        ModLogService::boot()
+            ->user(request()->user())
+            ->objectUid($user ? $user->id : null)
+            ->objectId($report->id)
+            ->objectType('App\Report::class')
+            ->action('admin.report.moderate')
+            ->metadata([
+                'action' => $request->input('action'),
+                'duration_active' => now()->parse($report->created_at)->diffForHumans()
+            ])
+            ->accessLevel('admin')
+            ->save();
+
+        if($report->status_ids) {
+            foreach($report->status_ids as $sid) {
+                RemoteReport::whereNull('action_taken_at')
+                    ->whereJsonContains('status_ids', [$sid])
+                    ->update(['action_taken_at' => now()]);
+            }
+        }
+        return [200];
     }
 }

+ 4 - 0
app/Http/Controllers/AdminCuratedRegisterController.php

@@ -104,6 +104,10 @@ class AdminCuratedRegisterController extends Controller
 
         foreach ($activities as $activity) {
             $idx++;
+
+            if ($activity->type === 'user_resend_email_confirmation') {
+                continue;
+            }
             if ($activity->from_user) {
                 $userResponses->push($activity);
 

+ 49 - 0
app/Http/Resources/AdminRemoteReport.php

@@ -0,0 +1,49 @@
+<?php
+
+namespace App\Http\Resources;
+
+use Illuminate\Http\Request;
+use Illuminate\Http\Resources\Json\JsonResource;
+use App\Instance;
+use App\Services\AccountService;
+use App\Services\StatusService;
+
+class AdminRemoteReport extends JsonResource
+{
+    /**
+     * Transform the resource into an array.
+     *
+     * @return array<string, mixed>
+     */
+    public function toArray(Request $request): array
+    {
+        $instance = parse_url($this->uri, PHP_URL_HOST);
+        $statuses = [];
+        if($this->status_ids && count($this->status_ids)) {
+            foreach($this->status_ids as $sid) {
+                $s = StatusService::get($sid, false);
+                if($s && $s['in_reply_to_id'] != null) {
+                    $parent = StatusService::get($s['in_reply_to_id'], false);
+                    if($parent) {
+                        $s['parent'] = $parent;
+                    }
+                }
+                if($s) {
+                    $statuses[] = $s;
+                }
+            }
+        }
+        $res = [
+            'id' => $this->id,
+            'instance' => $instance,
+            'reported' => AccountService::get($this->account_id, true),
+            'status_ids' => $this->status_ids,
+            'statuses' => $statuses,
+            'message' => $this->comment,
+            'report_meta' => $this->report_meta,
+            'created_at' => optional($this->created_at)->format('c'),
+            'action_taken_at' => optional($this->action_taken_at)->format('c'),
+        ];
+        return $res;
+    }
+}

+ 26 - 2
app/Util/ActivityPub/Inbox.php

@@ -1243,7 +1243,14 @@ class Inbox
             return;
         }
 
-        $content = isset($this->payload['content']) ? Purify::clean($this->payload['content']) : null;
+        $content = null;
+        if(isset($this->payload['content'])) {
+            if(strlen($this->payload['content']) > 5000) {
+                $content = Purify::clean(substr($this->payload['content'], 0, 5000) . ' ... (truncated message due to exceeding max length)');
+            } else {
+                $content = Purify::clean($this->payload['content']);
+            }
+        }
         $object = $this->payload['object'];
 
         if(empty($object) || (!is_array($object) && !is_string($object))) {
@@ -1259,7 +1266,7 @@ class Inbox
 
         foreach($object as $objectUrl) {
             if(!Helpers::validateLocalUrl($objectUrl)) {
-                continue;
+                return;
             }
 
             if(str_contains($objectUrl, '/users/')) {
@@ -1280,6 +1287,23 @@ class Inbox
             return;
         }
 
+        if($objects->count()) {
+            $obc = $objects->count();
+            if($obc > 25) {
+                if($obc > 30) {
+                    return;
+                } else {
+                    $objLimit = $objects->take(20);
+                    $objects = collect($objLimit->all());
+                    $obc = $objects->count();
+                }
+            }
+            $count = Status::whereProfileId($accountId)->find($objects)->count();
+            if($obc !== $count) {
+                return;
+            }
+        }
+
         $instanceHost = parse_url($id, PHP_URL_HOST);
 
         $instance = Instance::updateOrCreate([

BIN
public/js/admin.js


BIN
public/mix-manifest.json


File diff suppressed because it is too large
+ 692 - 529
resources/assets/components/admin/AdminReports.vue


+ 139 - 0
resources/assets/components/admin/partial/AdminModalPost.vue

@@ -0,0 +1,139 @@
+<template>
+<div class="mb-3">
+    <div v-if="status.media_attachments && status.media_attachments.length" class="list-group-item" style="gap:1rem;overflow:hidden;">
+        <div class="text-center text-muted small font-weight-bold mb-3">Reported Post Media</div>
+        <div v-if="status.media_attachments && status.media_attachments.length" class="d-flex flex-grow-1" style="gap: 1rem;overflow-x:auto;">
+            <template
+                v-for="media in status.media_attachments">
+                <img
+                    v-if="media.type === 'image'"
+                    :src="media.url"
+                    width="70"
+                    height="70"
+                    class="rounded"
+                    style="object-fit: cover;"
+                    @click="toggleLightbox"
+                    onerror="this.src='/storage/no-preview.png';this.error=null;" />
+
+                <video
+                    v-else-if="media.type === 'video'"
+                    width="140"
+                    height="90"
+                    playsinline
+                    @click.prevent="toggleVideoLightbox($event, media.url)"
+                    class="rounded"
+                    >
+                    <source :src="media.url" :type="media.mime">
+                </video>
+            </template>
+        </div>
+    </div>
+    <div class="list-group-item d-flex flex-row flex-grow-1" style="gap:1rem;">
+        <div class="flex-grow-1">
+            <div v-if="status && status.in_reply_to_id && status.parent && status.parent.account" class="mb-3">
+                <template v-if="showInReplyTo">
+                    <div class="mt-n1 text-center text-muted small font-weight-bold mb-1">Reply to</div>
+                    <div class="media" style="gap: 1rem;">
+                        <img
+                            :src="status.parent.account.avatar"
+                            width="40"
+                            height="40"
+                            class="rounded-lg"
+                            onerror="this.onerror=null;this.src='/storage/avatars/default.jpg?v=0';">
+                        <div class="d-flex flex-column">
+                            <p class="font-weight-bold mb-0" style="font-size: 11px;">
+                                <a :href="`/i/web/profile/${status.parent.account.id}`" target="_blank">{{ status.parent.account.acct }}</a>
+                            </p>
+                            <admin-read-more :content="status.parent.content_text" />
+                            <p class="mb-1">
+                                <a :href="`/i/web/post/${status.parent.id}`" target="_blank" class="text-muted" style="font-size: 11px;">
+                                    <i class="far fa-link mr-1"></i> {{ formatDate(status.parent.created_at)}}
+                                </a>
+                            </p>
+                        </div>
+                    </div>
+                    <hr class="my-1">
+                </template>
+                <a v-else class="btn btn-dark font-weight-bold btn-block btn-sm" href="#" @click.prevent="showInReplyTo = true">Show parent post</a>
+            </div>
+
+            <div>
+                <div class="mt-n1 text-center text-muted small font-weight-bold mb-1">Reported Post</div>
+                <div class="media" style="gap: 1rem;">
+                    <img
+                        :src="status.account.avatar"
+                        width="40"
+                        height="40"
+                        class="rounded-lg"
+                        onerror="this.onerror=null;this.src='/storage/avatars/default.jpg?v=0';">
+                    <div class="d-flex flex-column">
+                        <p class="font-weight-bold mb-0" style="font-size: 11px;">
+                            <a :href="`/i/web/profile/${status.account.id}`" target="_blank">{{ status.account.acct }}</a>
+                        </p>
+                        <template v-if="status && status.content_text && status.content_text.length">
+                            <admin-read-more :content="status.content_text" />
+                        </template>
+                        <template v-else>
+                            <admin-read-more content="EMPTY CAPTION" class="font-weight-bold text-muted" />
+                        </template>
+                        <p class="mb-0">
+                            <a :href="`/i/web/post/${status.id}`" target="_blank" class="text-muted" style="font-size: 11px;">
+                                <i class="far fa-link mr-1"></i> {{ formatDate(status.created_at)}}
+                            </a>
+                        </p>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+</template>
+
+<script>
+    import BigPicture from 'bigpicture';
+    import AdminReadMore from './AdminReadMore.vue';
+
+    export default {
+        props: {
+            status: {
+                type: Object
+            }
+        },
+
+        data() {
+            return {
+                showInReplyTo: false,
+            }
+        },
+
+        components: {
+            "admin-read-more": AdminReadMore
+        },
+
+        methods: {
+            toggleLightbox(e) {
+                BigPicture({
+                    el: e.target
+                })
+            },
+
+            toggleVideoLightbox($event, src) {
+                BigPicture({
+                    el: event.target,
+                    vidSrc: src
+                })
+            },
+
+            formatDate(str) {
+                let date = new Date(str);
+                return new Intl.DateTimeFormat('default', {
+                    month: 'long',
+                    day: 'numeric',
+                    year: 'numeric',
+                    hour: 'numeric',
+                    minute: 'numeric'
+                }).format(date);
+            },
+        }
+    }
+</script>

+ 106 - 0
resources/assets/components/admin/partial/AdminReadMore.vue

@@ -0,0 +1,106 @@
+<template>
+    <div>
+        <div class="mb-0" :style="{ 'font-size':`${fontSize}px` }">{{ contentText }}</div>
+        <p class="mb-0"><a v-if="canStepExpand || (canExpand && !expanded)" class="font-weight-bold small" href="#" @click="expand()">Read more</a></p>
+    </div>
+</template>
+
+<script>
+    export default {
+        props: {
+            content: {
+                type: String
+            },
+            maxLength: {
+                type: Number,
+                default: 140
+            },
+            fontSize: {
+                type: String,
+                default: "13"
+            },
+            step: {
+                type: Boolean,
+                default: false
+            },
+            stepLimit: {
+                type: Number,
+                default: 140
+            },
+            initialLimit: {
+                type: Number,
+                default: 10
+            }
+        },
+
+        computed: {
+            contentText: {
+                get() {
+                    if(this.step) {
+                        const len = this.content.length;
+                        const steps = len / this.stepLimit;
+                        if(this.stepIndex == 1 || steps < this.stepIndex) {
+                            this.canStepExpand = true;
+                        }
+
+                        return this.steppedTruncate();
+                    }
+                    if(this.content && this.content.length > this.maxLength) {
+                        this.canExpand = true;
+                    }
+                    return this.expanded ? this.content : this.truncate();
+                }
+            }
+        },
+
+        data() {
+            return {
+                expanded: false,
+                canExpand: false,
+                canStepExpand: false,
+                stepIndex: 1,
+            }
+        },
+
+        methods: {
+            expand() {
+                if(this.step) {
+                    this.stepIndex++;
+                    this.canStepExpand = true;
+                } else {
+                    this.expanded = true;
+                }
+            },
+
+            truncate() {
+                if(!this.content || !this.content.length) {
+                    return;
+                }
+
+                if(this.content && this.content.length < this.maxLength) {
+                    return this.content;
+                }
+
+                return this.content.slice(0, this.maxLength) + '...';
+            },
+
+            steppedTruncate() {
+                if(!this.content || !this.content.length) {
+                    return;
+                }
+                const len = this.content.length;
+                const steps = len / this.stepLimit;
+                const maxLen = this.stepLimit * this.stepIndex;
+                if(this.initialLimit != 10 && this.stepIndex === 1 && this.canStepExpand) {
+                    this.canStepExpand = len > this.stepLimit;
+                    return this.content.slice(0, this.initialLimit);
+                } else if(this.canStepExpand && this.stepIndex < steps) {
+                    return this.content.slice(0, maxLen);
+                } else {
+                    this.canStepExpand = false;
+                    return this.content;
+                }
+            }
+        }
+    }
+</script>

+ 304 - 0
resources/assets/components/admin/partial/AdminRemoteReportModal.vue

@@ -0,0 +1,304 @@
+<template>
+    <b-modal
+        v-model="isOpen"
+        title="Remote Report"
+        :ok-only="true"
+        ok-title="Close"
+        :lazy="true"
+        :scrollable="true"
+        ok-variant="outline-primary"
+        v-on:hide="$emit('close')">
+        <div v-if="isLoading" class="d-flex align-items-center justify-content-center">
+            <b-spinner />
+        </div>
+
+        <template v-else>
+            <div class="list-group">
+                <div class="list-group-item d-flex justify-content-between align-items-center">
+                    <div class="text-muted small font-weight-bold">Instance</div>
+                    <div class="font-weight-bold">{{ model.instance }}</div>
+                </div>
+                <div v-if="model.message && model.message.length" class="list-group-item d-flex justify-content-between align-items-center flex-column gap-1">
+                    <div class="text-muted small font-weight-bold mb-2">Message</div>
+                    <div class="text-wrap w-100" style="word-break:break-all;font-size:12.5px;">
+                        <admin-read-more
+                            :content="model.message"
+                            font-size="11"
+                            :step="true"
+                            :initial-limit="100"
+                            :stepLimit="1000" />
+                    </div>
+                </div>
+            </div>
+            <div class="list-group list-group-horizontal mt-3">
+                <div
+                    v-if="model && model.reported"
+                    class="list-group-item d-flex align-items-center justify-content-between flex-row flex-grow-1"
+                    style="gap:0.4rem;">
+                    <div class="text-muted small font-weight-bold">Reported Account</div>
+
+                    <div class="d-flex justify-content-end flex-grow-1">
+                        <a v-if="model.reported && model.reported.id" :href="`/i/web/profile/${model.reported.id}`" target="_blank" class="text-primary">
+                            <div class="d-flex align-items-center" style="gap:0.61rem;">
+                                <img
+                                    :src="model.reported.avatar"
+                                    width="30"
+                                    height="30"
+                                    style="object-fit: cover;border-radius:30px;"
+                                    onerror="this.src='/storage/avatars/default.png';this.error=null;">
+
+                                <div class="d-flex flex-column">
+                                    <p class="font-weight-bold mb-0 text-break" style="font-size: 12px;max-width: 140px;line-height: 16px;" :class="[ model.reported.is_admin ? 'text-danger': '']">@{{model.reported.acct}}</p>
+                                    <div class="d-flex text-muted mb-0" style="font-size: 10px;gap: 0.5rem;">
+                                        <span>{{prettyCount(model.reported.followers_count)}} Followers</span>
+                                        <span>·</span>
+                                        <span>Joined {{ timeAgo(model.reported.created_at) }}</span>
+                                    </div>
+                                </div>
+                            </div>
+                        </a>
+                    </div>
+                </div>
+                <div
+                    v-else
+                    class="list-group-item d-flex align-items-center justify-content-center flex-column flex-grow-1">
+                    <p class="font-weight-bold mb-0">Reported Account Unavailable</p>
+                    <p class="small mb-0">The reported account may have been deleted, or is otherwise not currently active. You can safely <strong>Close Report</strong> to mark this report as read.</p>
+                </div>
+            </div>
+
+            <div v-if="model && model.statuses && model.statuses.length" class="list-group mt-3">
+                <admin-modal-post
+                    v-for="(status, idx) in model.statuses"
+                    :key="`admin-modal-post-remote-post:${status.id}:${idx}`"
+                    :status="status"
+                />
+            </div>
+
+            <div class="mt-4">
+                <div>
+                    <button
+                        type="button"
+                        class="btn btn-dark btn-block rounded-pill"
+                        @click="handleAction('mark-read')">
+                        Close Report
+                    </button>
+
+                    <button
+                        type="button"
+                        class="btn btn-outline-dark btn-block text-center rounded-pill"
+                        style="word-break: break-all;"
+                        @click="handleAction('mark-all-read-by-domain')">
+                        <span class="font-weight-light">Close all reports from</span> <strong>{{ model.instance}}</strong>
+                    </button>
+                    <button
+                        v-if="model.reported"
+                        type="button"
+                        class="btn btn-outline-dark btn-block rounded-pill flex-grow-1"
+                        @click="handleAction('mark-all-read-by-username')">
+                        <span class="font-weight-light">Close all reports against</span> <strong>&commat;{{ model.reported.username }}</strong>
+                    </button>
+
+                    <template
+                        v-if="model && model.statuses && model.statuses.length && model.reported">
+                        <hr class="mt-3 mb-1">
+
+                        <div
+                            class="d-flex flex-row mt-2"
+                            style="gap:0.3rem;">
+                            <button
+                                type="button"
+                                class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0"
+                                @click="handleAction('cw-posts')">
+                                Apply CW to Post(s)
+                            </button>
+
+                            <button
+                                type="button"
+                                class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0"
+                                @click="handleAction('unlist-posts')">
+                                Unlist Post(s)
+                            </button>
+                        </div>
+                        <div class="d-flex flex-row mt-2">
+                            <button
+                                type="button"
+                                class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0"
+                                @click="handleAction('private-posts')">
+                                Make Post(s) Private
+                            </button>
+                            <button
+                                type="button"
+                                class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0"
+                                @click="handleAction('delete-posts')">
+                                Delete Post(s)
+                            </button>
+                        </div>
+                    </template>
+
+                    <template v-else-if="model && model.statuses && !model.statuses.length && model.reported">
+                        <hr class="mt-3 mb-1">
+
+                        <div
+                            class="d-flex flex-row mt-2"
+                            style="gap:0.3rem;">
+                            <button
+                                type="button"
+                                class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0"
+                                @click="handleAction('cw-all-posts')">
+                                Apply CW to all posts
+                            </button>
+
+                            <button
+                                type="button"
+                                class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0"
+                                @click="handleAction('unlist-all-posts')">
+                                Unlist all account posts
+                            </button>
+                        </div>
+                        <div
+                            class="d-flex flex-row mt-2"
+                            style="gap:0.3rem;">
+
+                            <button
+                                type="button"
+                                class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0"
+                                @click="handleAction('private-all-posts')">
+                                Make all posts private
+                            </button>
+                        </div>
+                    </template>
+                </div>
+            </div>
+        </template>
+    </b-modal>
+</template>
+
+<script>
+    import AdminModalPost from "./AdminModalPost.vue";
+    import AdminReadMore from "./AdminReadMore.vue";
+
+    export default {
+        props: {
+            open: {
+                type: Boolean,
+                default: false
+            },
+            model: {
+                type: Object
+            }
+        },
+
+        components: {
+            "admin-modal-post": AdminModalPost,
+            "admin-read-more": AdminReadMore
+        },
+
+        watch: {
+            open: {
+                handler() {
+                    this.isOpen = this.open;
+                },
+                immediate: true,
+                deep: true,
+            }
+        },
+
+        data() {
+            return {
+                isLoading: true,
+                isOpen: false,
+                actions: [
+                    'mark-read',
+                    'cw-posts',
+                    'unlist-posts',
+                    'private-posts',
+                    'delete-posts',
+                    'mark-all-read-by-domain',
+                    'mark-all-read-by-username',
+                    'cw-all-posts',
+                    'unlist-all-posts',
+                    'private-all-posts',
+                ],
+                actionMap: {
+                    'cw-posts': 'apply content warnings to all post(s) in this report?',
+                    'unlist-posts': 'unlist all post(s) in this report?',
+                    'delete-posts': 'delete all post(s) in this report?',
+                    'private-posts': 'make all post(s) in this report private/followers-only?',
+                    'mark-all-read-by-domain': 'mark all reports by this instance as closed?',
+                    'mark-all-read-by-username': 'mark all reports against this user as closed?',
+                    'cw-all-posts': 'apply content warnings to all post(s) belonging to this account?',
+                    'unlist-all-posts': 'make all post(s) belonging to this account as unlisted?',
+                    'private-all-posts': 'make all post(s) belonging to this account as private?',
+                }
+            }
+        },
+
+        mounted() {
+            setTimeout(() => {
+                this.isLoading = false;
+            }, 300);
+        },
+
+        methods: {
+            prettyCount(str) {
+                if(str) {
+                   return str.toLocaleString('en-CA', { compactDisplay: "short", notation: "compact"});
+                }
+                return str;
+            },
+
+            timeAgo(str) {
+                if(!str) {
+                    return str;
+                }
+                return App.util.format.timeAgo(str);
+            },
+
+            formatDate(str) {
+                let date = new Date(str);
+                return new Intl.DateTimeFormat('default', {
+                    month: 'long',
+                    day: 'numeric',
+                    year: 'numeric',
+                    hour: 'numeric',
+                    minute: 'numeric'
+                }).format(date);
+            },
+
+            handleAction(action) {
+                if(action === 'mark-read') {
+                    axios.post('/i/admin/api/reports/remote/handle', {
+                        id: this.model.id,
+                        action: action,
+                    }).then(res => {
+                        console.log(res.data)
+                    })
+                    .finally(() => {
+                        this.$emit('refresh');
+                        this.$emit('close');
+                    })
+                    return;
+                }
+
+                swal({
+                    title: 'Confirm',
+                    text: 'Are you sure you want to ' + this.actionMap[action],
+                    icon: 'warning',
+                    buttons: true,
+                    dangerMode: true,
+                }).then(res => {
+                    if(res === true) {
+                        axios.post('/i/admin/api/reports/remote/handle', {
+                            id: this.model.id,
+                            action: action,
+                        }).finally(() => {
+                            this.$emit('refresh');
+                            this.$emit('close');
+                        })
+                    }
+                });
+            }
+        }
+    }
+</script>

+ 16 - 0
resources/views/site/help/email-confirmation-issues.blade.php

@@ -0,0 +1,16 @@
+@extends('site.help.partial.template', ['breadcrumb'=>'Email Confirmation Issues'])
+
+@section('section')
+<div class="title">
+    <h3 class="font-weight-bold">Email Confirmation Issues</h3>
+</div>
+<hr>
+<p>If you have been redirected to this page, it may be due to one of the following reasons:</p>
+
+<ul>
+    <li>The email confirmation link has already been used.</li>
+    <li>The email confirmation link may have expired, they are only valid for 24 hours.</li>
+    <li>You cannot confirm an email for another account while logged in to a different account. Try logging out, or use a different browser to open the email confirmation link.</li>
+    <li>The account the associated email belongs to may have been deleted, or the account may have changed the email address.</li>
+</ul>
+@endsection

+ 2 - 0
routes/web-admin.php

@@ -146,6 +146,8 @@ Route::domain(config('pixelfed.domain.admin'))->prefix('i/admin')->group(functio
         Route::post('instances/import-data', 'AdminController@importBackup');
         Route::get('reports/stats', 'AdminController@reportsStats');
         Route::get('reports/all', 'AdminController@reportsApiAll');
+        Route::get('reports/remote', 'AdminController@reportsApiRemote');
+        Route::post('reports/remote/handle', 'AdminController@reportsApiRemoteHandle');
         Route::get('reports/get/{id}', 'AdminController@reportsApiGet');
         Route::post('reports/handle', 'AdminController@reportsApiHandle');
         Route::get('reports/spam/all', 'AdminController@reportsApiSpamAll');

+ 1 - 1
routes/web.php

@@ -307,7 +307,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
             Route::view('instance-max-users-limit', 'site.help.instance-max-users')->name('help.instance-max-users-limit');
             Route::view('import', 'site.help.import')->name('help.import');
             Route::view('parental-controls', 'site.help.parental-controls');
-            // Route::view('email-confirmation-issues', 'site.help.email-confirmation-issues')->name('help.email-confirmation-issues');
+            Route::view('email-confirmation-issues', 'site.help.email-confirmation-issues')->name('help.email-confirmation-issues');
             Route::view('curated-onboarding', 'site.help.curated-onboarding')->name('help.curated-onboarding');
         });
         Route::get('newsroom/{year}/{month}/{slug}', 'NewsroomController@show');

Some files were not shown because too many files changed in this diff