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

This commit is contained in:
Daniel Supernault 2021-11-10 21:46:31 -07:00
parent dff3dad1c8
commit ae8c751796
No known key found for this signature in database
GPG key ID: 0DEF1C662C9033F7
6 changed files with 428 additions and 84 deletions

View file

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

View file

@ -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');
});
}
}

View file

@ -16,7 +16,6 @@
</div> </div>
</div> </div>
@if($ai || $spam || $mailVerifications)
<div class="col-12 col-md-8 offset-md-2"> <div class="col-12 col-md-8 offset-md-2">
<div class="mb-4"> <div class="mb-4">
<a class="btn btn-outline-primary px-5 py-3 mr-3" href="/i/admin/reports/email-verifications"> <a class="btn btn-outline-primary px-5 py-3 mr-3" href="/i/admin/reports/email-verifications">
@ -33,7 +32,6 @@
</a> </a>
</div> </div>
</div> </div>
@endif
@if($reports->count()) @if($reports->count())
<div class="col-12 col-md-8 offset-md-2"> <div class="col-12 col-md-8 offset-md-2">
<div class="card shadow-none border"> <div class="card shadow-none border">
@ -43,7 +41,7 @@
<div class="p-0"> <div class="p-0">
<div class="media d-flex align-items-center"> <div class="media d-flex align-items-center">
<a class="text-decoration-none" href="{{$report->url()}}"> <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> </a>
<div class="media-body"> <div class="media-body">
<p class="mb-1 small"><span class="font-weight-bold text-uppercase text-danger">{{$report->type}}</span></p> <p class="mb-1 small"><span class="font-weight-bold text-uppercase text-danger">{{$report->type}}</span></p>

View file

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

View file

@ -1,63 +1,163 @@
@extends('admin.partial.template-full') @extends('admin.partial.template-full')
@section('section') @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>
<div class="row"> <div class="header bg-primary pb-3 mt-n4">
<div class="col-12 col-md-3 mb-3"> <div class="container-fluid">
<div class="card border bg-primary text-white rounded-pill shadow"> <div class="header-body">
<div class="card-body pl-4 ml-3"> <div class="row align-items-center py-4">
<p class="h1 font-weight-bold mb-1" style="font-weight: 700">{{App\AccountInterstitial::whereNull('appeal_handled_at')->whereType('post.autospam')->count()}}</p> <div class="col-lg-6 col-7">
<p class="lead mb-0 font-weight-lighter">active cases</p> <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> <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="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>
@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>
@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>
<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>
</div> </div>
<div class="col-12 col-md-8 offset-md-1"> </div>
<ul class="list-group">
@if($appeals->count() == 0) <div class="container-fluid mt-4">
<li class="list-group-item text-center py-5"> <div class="row justify-content-center">
<p class="mb-0 py-5 font-weight-bold">No autospam cases found!</p> <div class="col-12 col-md-8">
</li> <ul class="nav nav-pills nav-fill mb-4">
@endif <li class="nav-item">
@foreach($appeals as $appeal) <a class="nav-link {{$tab=='home'?'active':''}}" href="/i/admin/reports/autospam">Active</a>
<a class="list-group-item text-decoration-none text-dark" href="/i/admin/reports/autospam/{{$appeal->id}}"> </li>
<div class="d-flex justify-content-between align-items-center"> <li class="nav-item">
<div class="d-flex align-items-center"> <a class="nav-link {{$tab=='spam'?'active':''}}" href="/i/admin/reports/autospam?tab=spam">Spam</a>
<img src="{{$appeal->has_media ? $appeal->status->thumb(true) : '/storage/no-preview.png'}}" width="64" height="64" class="rounded border"> </li>
<div class="ml-2"> <li class="nav-item">
<span class="d-inline-block text-truncate"> <a class="nav-link {{$tab=='not-spam'?'active':''}}" href="/i/admin/reports/autospam?tab=not-spam">Not Spam</a>
<p class="mb-0 small font-weight-bold text-primary">{{$appeal->type}}</p> </li>
@if($appeal->item_type) {{-- <li class="nav-item">
<p class="mb-0 font-weight-bold">{{starts_with($appeal->item_type, 'App\\') ? explode('\\',$appeal->item_type)[1] : $appeal->item_type}}</p> <a class="nav-link" href="#">Closed</a>
@endif </li> --}}
</span> {{-- <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>
</div> </div>
<div class="d-block"> </a>
<p class="mb-0 font-weight-bold">&commat;{{$appeal->user->username}}</p> @endforeach
<p class="mb-0 small text-muted font-weight-bold">{{$appeal->created_at->diffForHumans(null, null, true)}}</p> </ul>
</div> <p>{!!$appeals->render()!!}</p>
<div class="d-inline-block"> </div>
<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> </div>

View file

@ -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/show/{id}', 'AdminController@updateReport');
Route::post('reports/bulk', 'AdminController@bulkUpdateReport'); Route::post('reports/bulk', 'AdminController@bulkUpdateReport');
Route::get('reports/autospam/{id}', 'AdminController@showSpam'); Route::get('reports/autospam/{id}', 'AdminController@showSpam');
Route::post('reports/autospam/sync', 'AdminController@fixUncategorizedSpam');
Route::post('reports/autospam/{id}', 'AdminController@updateSpam'); Route::post('reports/autospam/{id}', 'AdminController@updateSpam');
Route::get('reports/autospam', 'AdminController@spam'); Route::get('reports/autospam', 'AdminController@spam');
Route::get('reports/appeals', 'AdminController@appeals'); Route::get('reports/appeals', 'AdminController@appeals');