Преглед на файлове

Update Autospam service, add mark all as read and mark all as not spam options and filter active, spam and not spamreports

Daniel Supernault преди 3 години
родител
ревизия
ae8c751796

+ 182 - 10
app/Http/Controllers/Admin/AdminReportController.php

@@ -95,26 +95,155 @@ trait AdminReportController
 
 	public function spam(Request $request)
 	{
-		$appeals = AccountInterstitial::whereType('post.autospam')
-			->whereNull('appeal_handled_at')
-			->latest()
-			->paginate(6);
-		return view('admin.reports.spam', compact('appeals'));
+		$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')
-			->whereNull('appeal_handled_at')
 			->findOrFail($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'
+			'action' => 'required|in:dismiss,approve,dismiss-all,approve-all'
 		]);
 
 		$action = $request->input('action');
@@ -123,15 +252,57 @@ trait AdminReportController
 			->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 == 'dismiss') {
-			$appeal->appeal_handled_at = now();
+			$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 redirect('/i/admin/reports/autospam');
+			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);
+					}
+				});
+			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;
@@ -140,6 +311,7 @@ trait AdminReportController
 		$status->visibility = 'public';
 		$status->save();
 
+		$appeal->is_spam = false;
 		$appeal->appeal_handled_at = now();
 		$appeal->save();
 
@@ -149,7 +321,7 @@ trait AdminReportController
 		Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id);
 		Cache::forget('admin-dash:reports:spam-count');
 
-		return redirect('/i/admin/reports/autospam');
+		return $res;
 	}
 
 	public function updateAppeal(Request $request, $id)

+ 44 - 0
database/migrations/2021_11_09_105629_add_action_to_account_interstitials_table.php

@@ -0,0 +1,44 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class AddActionToAccountInterstitialsTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::table('account_interstitials', function (Blueprint $table) {
+        	$table->tinyInteger('severity_index')->unsigned()->nullable()->index();
+            $table->boolean('is_spam')->nullable()->index()->after('item_type');
+            $table->boolean('in_violation')->nullable()->index()->after('is_spam');
+            $table->unsignedInteger('violation_id')->nullable()->index()->after('in_violation');
+            $table->boolean('email_notify')->nullable()->index()->after('violation_id');
+            $table->bigInteger('thread_id')->unsigned()->unique()->nullable();
+            $table->timestamp('emailed_at')->nullable();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::table('account_interstitials', function (Blueprint $table) {
+            $table->dropColumn('severity_index');
+            $table->dropColumn('is_spam');
+            $table->dropColumn('in_violation');
+            $table->dropColumn('violation_id');
+            $table->dropColumn('email_notify');
+            $table->dropColumn('thread_id');
+            $table->dropColumn('emailed_at');
+        });
+    }
+}

+ 1 - 3
resources/views/admin/reports/home.blade.php

@@ -16,7 +16,6 @@
 		</div>
 	</div>
 
-	@if($ai || $spam || $mailVerifications)
 	<div class="col-12 col-md-8 offset-md-2">
 		<div class="mb-4">
 			<a class="btn btn-outline-primary px-5 py-3 mr-3" href="/i/admin/reports/email-verifications">
@@ -33,7 +32,6 @@
 			</a>
 		</div>
 	</div>
-	@endif
 	@if($reports->count())
 	<div class="col-12 col-md-8 offset-md-2">
 		<div class="card shadow-none border">
@@ -43,7 +41,7 @@
 					<div class="p-0">
 						<div class="media d-flex align-items-center">
 							<a class="text-decoration-none" href="{{$report->url()}}">
-								<img src="{{$report->status->media->count() ? $report->status->thumb(true) : '/storage/no-preview.png'}}" width="64" height="64" class="rounded border shadow mr-3" style="object-fit: cover">
+								<img src="{{$report->status->media && $report->status->media->count() ? $report->status->thumb(true) : '/storage/no-preview.png'}}" width="64" height="64" class="rounded border shadow mr-3" style="object-fit: cover">
 							</a>
 							<div class="media-body">
 								<p class="mb-1 small"><span class="font-weight-bold text-uppercase text-danger">{{$report->type}}</span></p>

+ 48 - 19
resources/views/admin/reports/show_spam.blade.php

@@ -15,7 +15,7 @@
 		<div class="card shadow-none border">
 			<div class="card-header bg-light h5 font-weight-bold py-4">Unlisted + Content Warning</div>
 			@if($appeal->has_media)
-			<img class="card-img-top border-bottom" src="{{$appeal->status->thumb(true)}}">
+			<img class="card-img-top border-bottom" src="{{$appeal->status->thumb(true)}}" style="max-height: 40vh;object-fit: contain;">
 			@endif
 			<div class="card-body">
 				<div class="mt-2 p-3">
@@ -42,13 +42,15 @@
 		@endif
 	</div>
 	<div class="col-12 col-md-4 mt-3">
-		<form method="post">
-			@csrf
-			<input type="hidden" name="action" value="dismiss">
-			<button type="submit" class="btn btn-primary btn-block font-weight-bold mb-3">Mark as read</button>
-		</form>
-		<button type="button" class="btn btn-light border btn-block font-weight-bold mb-3" onclick="approveWarning()">Mark as not spam</button>
-		<div class="card shadow-none border mt-5">
+		@if($appeal->appeal_handled_at)
+		@else
+		<button type="button" class="btn btn-primary border btn-block font-weight-bold mb-3 action-btn" data-action="dismiss">Mark as read</button>
+		<button type="button" class="btn btn-light border btn-block font-weight-bold mb-3 action-btn" data-action="approve">Mark as not spam</button>
+		<hr>
+		<button type="button" class="btn btn-default border btn-block font-weight-bold mb-3 action-btn" data-action="dismiss-all">Mark all as read</button>
+		<button type="button" class="btn btn-light border btn-block font-weight-bold mb-3 action-btn mb-5" data-action="approve-all">Mark all as not spam</button>
+		@endif
+		<div class="card shadow-none border">
 			<div class="card-header text-center font-weight-bold bg-light">
 				&commat;{{$appeal->user->username}} stats
 			</div>
@@ -76,16 +78,43 @@
 
 @push('scripts')
 <script type="text/javascript">
-	function approveWarning() {
-		if(window.confirm('Are you sure you want to mark this as not spam?') == true) {
-			axios.post(window.location.href,  {
-				action: 'approve'
-			}).then(res => {
-				window.location.href = '/i/admin/reports/autospam';
-			}).catch(err => {
-				swal('Oops!', 'An error occured, please try again later.', 'error');
-			});
+	$('.action-btn').click((e) => {
+		e.preventDefault();
+		e.currentTarget.blur();
+
+		let type = e.currentTarget.getAttribute('data-action');
+
+		switch(type) {
+			case 'dismiss':
+			break;
+
+			case 'approve':
+				if(!window.confirm('Are you sure you want to approve this post?')) {
+					return;
+				}
+			break;
+
+			case 'dismiss-all':
+				if(!window.confirm('Are you sure you want to dismiss all autospam reports?')) {
+					return;
+				}
+			break;
+
+			case 'approve-all':
+				if(!window.confirm('Are you sure you want to approve this post and all other posts by this account?')) {
+					return;
+				}
+			break;
 		}
-	}
+
+		axios.post(window.location.href, {
+			action: type
+		}).then(res => {
+			location.href = '/i/admin/reports/autospam';
+		}).catch(err => {
+			swal('Oops!', 'An error occured', 'error');
+			console.log(err);
+		})
+	});
 </script>
-@endpush
+@endpush

+ 150 - 50
resources/views/admin/reports/spam.blade.php

@@ -1,64 +1,164 @@
 @extends('admin.partial.template-full')
 
 @section('section')
-<div class="title mb-4">
-	<h3 class="font-weight-bold d-inline-block">Autospam</h3>
-	<p class="lead">Posts flagged as spam</p>
-	<span class="float-right">
-	</span>
 </div>
-<div class="row">
-	<div class="col-12 col-md-3 mb-3">
-		<div class="card border bg-primary text-white rounded-pill shadow">
-			<div class="card-body pl-4 ml-3">
-				<p class="h1 font-weight-bold mb-1" style="font-weight: 700">{{App\AccountInterstitial::whereNull('appeal_handled_at')->whereType('post.autospam')->count()}}</p>
-				<p class="lead mb-0 font-weight-lighter">active cases</p>				
+<div class="header bg-primary pb-3 mt-n4">
+	<div class="container-fluid">
+		<div class="header-body">
+			<div class="row align-items-center py-4">
+				<div class="col-lg-6 col-7">
+					<p class="display-1 text-white d-inline-block mb-0">Autospam</p>
+					<p class="lead text-white mb-0 mt-n3">Automated Spam Detection</p>
+				</div>
 			</div>
-		</div>
+			<div class="row">
+				<div class="col-xl-3 col-md-6">
+					<div class="card card-stats">
+						<div class="card-body">
+							<div class="row">
+								<div class="col">
+									<h5 class="card-title text-uppercase text-muted mb-0">Active Reports</h5>
+									<span class="h2 font-weight-bold mb-0">{{$openCount}}</span>
+								</div>
+								<div class="col-auto">
+									<div class="icon icon-shape bg-gradient-primary text-white rounded-circle shadow">
+										<i class="far fa-exclamation-circle"></i>
+									</div>
+								</div>
+							</div>
+							<p class="mt-3 mb-0 text-sm">
+								<span class="text-success mr-2"><i class="fa fa-arrow-up"></i> {{$monthlyCount}}</span>
+								<span class="text-nowrap">in last 30 days</span>
+							</p>
+						</div>
+					</div>
+				</div>
 
-		<div class="mt-3 card border bg-warning text-dark rounded-pill shadow">
-			<div class="card-body pl-4 ml-3">
-				<p class="h1 font-weight-bold mb-1" style="font-weight: 700">{{App\AccountInterstitial::whereType('post.autospam')->count()}}</p>
-				<p class="lead mb-0 font-weight-lighter">total cases</p>				
-			</div>
-		</div>
-	</div>
-	<div class="col-12 col-md-8 offset-md-1">
-		<ul class="list-group">
-			@if($appeals->count() == 0)
-			<li class="list-group-item text-center py-5">
-				<p class="mb-0 py-5 font-weight-bold">No autospam cases found!</p>
-			</li>
-			@endif
-			@foreach($appeals as $appeal)
-			<a class="list-group-item text-decoration-none text-dark" href="/i/admin/reports/autospam/{{$appeal->id}}">
-				<div class="d-flex justify-content-between align-items-center">
-					<div class="d-flex align-items-center">
-						<img src="{{$appeal->has_media ? $appeal->status->thumb(true) : '/storage/no-preview.png'}}" width="64" height="64" class="rounded border">
-						<div class="ml-2">
-							<span class="d-inline-block text-truncate">
-								<p class="mb-0 small font-weight-bold text-primary">{{$appeal->type}}</p>
-								@if($appeal->item_type)
-								<p class="mb-0 font-weight-bold">{{starts_with($appeal->item_type, 'App\\') ? explode('\\',$appeal->item_type)[1] : $appeal->item_type}}</p>
-								@endif
-							</span>
+				<div class="col-xl-3 col-md-6">
+					<div class="card card-stats">
+						<div class="card-body">
+							<div class="row">
+								<div class="col">
+									<h5 class="card-title text-uppercase text-muted mb-0">Avg Response Time</h5>
+									<span class="h2 font-weight-bold mb-0">{{$avgOpen}}</span>
+								</div>
+								<div class="col-auto">
+									<div class="icon icon-shape bg-gradient-primary text-white rounded-circle shadow">
+										<i class="far fa-clock"></i>
+									</div>
+								</div>
+							</div>
+							<p class="mt-3 mb-0 text-sm">
+								<span class="text-nowrap">in last 30 days</span>
+							</p>
 						</div>
 					</div>
-					<div class="d-block">
-						<p class="mb-0 font-weight-bold">&commat;{{$appeal->user->username}}</p>
-						<p class="mb-0 small text-muted font-weight-bold">{{$appeal->created_at->diffForHumans(null, null, true)}}</p>
+				</div>
+
+				@if($uncategorized)
+				<div class="col-xl-3 col-md-6">
+					<div class="card card-stats">
+						<div class="card-body">
+							<div class="row">
+								<div class="col">
+									<h5 class="card-title text-uppercase text-muted mb-0">Uncategorized</h5>
+									<span class="h2 font-weight-bold mb-0">Reports Found</span>
+								</div>
+								<div class="col-auto">
+									<div class="icon icon-shape bg-danger text-white rounded-circle shadow">
+										<i class="far fa-exclamation-triangle"></i>
+									</div>
+								</div>
+							</div>
+							<form action="/i/admin/reports/autospam/sync" method="post" class="mt-2 p-0">
+								@csrf
+								<button type="submit" class="btn btn-danger py-1 px-2"><i class="far fa-ambulance mr-2"></i> Manual Fix</button>
+							</form>
+						</div>
 					</div>
-					<div class="d-inline-block">
-						<p class="mb-0 small">
-							<i class="fas fa-chevron-right fa-2x text-lighter"></i>
-						</p>
+				</div>
+				@endif
+
+				<div class="col-xl-2 col-md-6">
+					<div class="mb-3">
+						<h5 class="text-light text-uppercase mb-0">Total Reports</h5>
+						<span class="text-white h2 font-weight-bold mb-0">{{$totalCount}}</span>
+					</div>
+					<div class="">
+						<h5 class="text-light text-uppercase mb-0">Reports per user</h5>
+						<span class="text-white h2 font-weight-bold mb-0">{{$avgCount}}</span>
 					</div>
 				</div>
-			</a>
-			@endforeach
-		</ul>
-		<p>{!!$appeals->render()!!}</p>
+
+			</div>
+		</div>
+	</div>
+</div>
+
+<div class="container-fluid mt-4">
+	<div class="row justify-content-center">
+		<div class="col-12 col-md-8">
+			<ul class="nav nav-pills nav-fill mb-4">
+				<li class="nav-item">
+					<a class="nav-link {{$tab=='home'?'active':''}}" href="/i/admin/reports/autospam">Active</a>
+				</li>
+				<li class="nav-item">
+					<a class="nav-link {{$tab=='spam'?'active':''}}" href="/i/admin/reports/autospam?tab=spam">Spam</a>
+				</li>
+				<li class="nav-item">
+					<a class="nav-link {{$tab=='not-spam'?'active':''}}" href="/i/admin/reports/autospam?tab=not-spam">Not Spam</a>
+				</li>
+				{{-- <li class="nav-item">
+					<a class="nav-link" href="#">Closed</a>
+				</li> --}}
+				{{-- <li class="nav-item">
+					<a class="nav-link" href="#">Review</a>
+				</li> --}}
+				{{-- <li class="nav-item">
+					<a class="nav-link" href="#">Train</a>
+				</li> --}}
+				{{-- <li class="nav-item">
+					<a class="nav-link {{$tab=='exemptions'?'active':''}}" href="/i/admin/reports/autospam?tab=exemptions">Exemptions</a>
+				</li>
+				<li class="nav-item">
+					<a class="nav-link {{$tab=='custom'?'active':''}}" href="/i/admin/reports/autospam?tab=custom">Custom</a>
+				</li>
+				<li class="nav-item" style="max-width: 50px;">
+					<a class="nav-link {{$tab=='settings'?'active':''}}" href="/i/admin/reports/autospam?tab=settings"><i class="far fa-cog"></i></a>
+				</li> --}}
+			</ul>
+			<ul class="list-group">
+				@if($appeals->count() == 0)
+				<li class="list-group-item text-center py-5">
+					<p class="mb-0 py-5 font-weight-bold">No autospam cases found!</p>
+				</li>
+				@endif
+				@foreach($appeals as $appeal)
+				<a class="list-group-item text-decoration-none text-dark" href="/i/admin/reports/autospam/{{$appeal->id}}">
+					<div class="d-flex justify-content-between align-items-center">
+						<div class="d-flex align-items-center">
+							<img src="{{$appeal->has_media ? $appeal->status->thumb(true) : '/storage/no-preview.png'}}" width="64" height="64" class="rounded border" onerror="this.onerror=null;this.src='/storage/no-preview.png';">
+							<div class="ml-3">
+								<span class="d-inline-block text-truncate">
+									<p class="mb-0 font-weight-bold">&commat;{{$appeal->user->username}}</p>
+									<p class="mb-0 small text-muted font-weight-bold">{{$appeal->created_at->diffForHumans(null, null, true)}}</p>
+								</span>
+							</div>
+						</div>
+						<div class="d-block">
+						</div>
+						<div class="d-inline-block">
+							<p class="mb-0 small">
+								<i class="fas fa-chevron-right fa-2x text-lighter"></i>
+							</p>
+						</div>
+					</div>
+				</a>
+				@endforeach
+			</ul>
+			<p>{!!$appeals->render()!!}</p>
+		</div>
 	</div>
 </div>
 
-@endsection
+@endsection

+ 1 - 0
routes/web.php

@@ -9,6 +9,7 @@ Route::domain(config('pixelfed.domain.admin'))->prefix('i/admin')->group(functio
 	Route::post('reports/show/{id}', 'AdminController@updateReport');
 	Route::post('reports/bulk', 'AdminController@bulkUpdateReport');
 	Route::get('reports/autospam/{id}', 'AdminController@showSpam');
+	Route::post('reports/autospam/sync', 'AdminController@fixUncategorizedSpam');
 	Route::post('reports/autospam/{id}', 'AdminController@updateSpam');
 	Route::get('reports/autospam', 'AdminController@spam');
 	Route::get('reports/appeals', 'AdminController@appeals');