diff --git a/CHANGELOG.md b/CHANGELOG.md index bb3c238a0..710ffbb79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ - Updated Localization util, filter out .DS_Store. ([0107e8fd](https://github.com/pixelfed/pixelfed/commit/0107e8fd)) - Updated PublicApiController, fix private account statuses api. Closes #2995. ([aa2dd26c](https://github.com/pixelfed/pixelfed/commit/aa2dd26c)) - Updated Status model, use AccountService to generate urls instead of loading profile relation. ([2ae527c0](https://github.com/pixelfed/pixelfed/commit/2ae527c0)) +- Updated Autospam service, add mark all as read and mark all as not spam options and filter active, spam and not spam reports. ([ae8c7517](https://github.com/pixelfed/pixelfed/commit/ae8c7517)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.1 (2021-09-07)](https://github.com/pixelfed/pixelfed/compare/v0.11.0...v0.11.1) diff --git a/app/Http/Controllers/Admin/AdminReportController.php b/app/Http/Controllers/Admin/AdminReportController.php index d2625acff..484d80b46 100644 --- a/app/Http/Controllers/Admin/AdminReportController.php +++ b/app/Http/Controllers/Admin/AdminReportController.php @@ -3,16 +3,372 @@ namespace App\Http\Controllers\Admin; use Cache; -use App\Report; -use App\User; use Carbon\Carbon; use Illuminate\Http\Request; use Illuminate\Support\Facades\Redis; use App\Services\AccountService; use App\Services\StatusService; +use App\{ + AccountInterstitial, + Contact, + Hashtag, + Newsroom, + OauthClient, + Profile, + Report, + Status, + Story, + User +}; +use Illuminate\Validation\Rule; +use App\Services\StoryService; 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::findOrFail($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); + $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' + ]); + + $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 == '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); + } + }); + 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); + Cache::forget('admin-dash:reports:ai-count'); + + return redirect('/i/admin/reports/appeals'); + } + public function updateReport(Request $request, $id) { $this->validate($request, [ diff --git a/app/Http/Controllers/AdminController.php b/app/Http/Controllers/AdminController.php index f31bc823a..cd38e2fc7 100644 --- a/app/Http/Controllers/AdminController.php +++ b/app/Http/Controllers/AdminController.php @@ -74,178 +74,6 @@ class AdminController extends Controller return view('admin.statuses.show', compact('status')); } - 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::findOrFail($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) - { - $appeals = AccountInterstitial::whereType('post.autospam') - ->whereNull('appeal_handled_at') - ->latest() - ->paginate(6); - return view('admin.reports.spam', compact('appeals')); - } - - 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 updateSpam(Request $request, $id) - { - $this->validate($request, [ - 'action' => 'required|in:dismiss,approve' - ]); - - $action = $request->input('action'); - $appeal = AccountInterstitial::whereType('post.autospam') - ->whereNull('appeal_handled_at') - ->findOrFail($id); - - $meta = json_decode($appeal->meta); - - if($action == 'dismiss') { - $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'); - } - - $status = $appeal->status; - $status->is_nsfw = $meta->is_nsfw; - $status->scope = 'public'; - $status->visibility = 'public'; - $status->save(); - - $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 redirect('/i/admin/reports/autospam'); - } - - 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); - Cache::forget('admin-dash:reports:ai-count'); - - return redirect('/i/admin/reports/appeals'); - } - public function profiles(Request $request) { $this->validate($request, [ diff --git a/database/migrations/2021_11_09_105629_add_action_to_account_interstitials_table.php b/database/migrations/2021_11_09_105629_add_action_to_account_interstitials_table.php new file mode 100644 index 000000000..e08738522 --- /dev/null +++ b/database/migrations/2021_11_09_105629_add_action_to_account_interstitials_table.php @@ -0,0 +1,44 @@ +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'); + }); + } +} diff --git a/resources/views/admin/reports/home.blade.php b/resources/views/admin/reports/home.blade.php index 8f881567d..996ad1b1e 100644 --- a/resources/views/admin/reports/home.blade.php +++ b/resources/views/admin/reports/home.blade.php @@ -16,7 +16,6 @@ - @if($ai || $spam || $mailVerifications)
@@ -33,7 +32,6 @@
- @endif @if($reports->count())
@@ -43,7 +41,7 @@
- +

{{$report->type}}

diff --git a/resources/views/admin/reports/show_spam.blade.php b/resources/views/admin/reports/show_spam.blade.php index 9648de03b..970f881ea 100644 --- a/resources/views/admin/reports/show_spam.blade.php +++ b/resources/views/admin/reports/show_spam.blade.php @@ -15,7 +15,7 @@
Unlisted + Content Warning
@if($appeal->has_media) - + @endif
@@ -42,13 +42,15 @@ @endif
-
- @csrf - - -
- -
+ @if($appeal->appeal_handled_at) + @else + + +
+ + + @endif +
@{{$appeal->user->username}} stats
@@ -76,16 +78,43 @@ @push('scripts') -@endpush \ No newline at end of file +@endpush diff --git a/resources/views/admin/reports/spam.blade.php b/resources/views/admin/reports/spam.blade.php index 2e69c09ad..456e38bd1 100644 --- a/resources/views/admin/reports/spam.blade.php +++ b/resources/views/admin/reports/spam.blade.php @@ -1,64 +1,164 @@ @extends('admin.partial.template-full') @section('section') -
-

Autospam

-

Posts flagged as spam

- -
-
-
-
-
-

{{App\AccountInterstitial::whereNull('appeal_handled_at')->whereType('post.autospam')->count()}}

-

active cases

+
+
+
+
+
+

Autospam

+

Automated Spam Detection

+
-
- -
-
-

{{App\AccountInterstitial::whereType('post.autospam')->count()}}

-

total cases

-
-
-
-
- -

{!!$appeals->render()!!}

+ + @if($uncategorized) +
+
+
+
+
+
Uncategorized
+ Reports Found +
+
+
+ +
+
+
+
+ @csrf + +
+
+
+
+ @endif + +
+
+
Total Reports
+ {{$totalCount}} +
+
+
Reports per user
+ {{$avgCount}} +
+
+ +
+
-@endsection \ No newline at end of file +
+
+
+ + +

{!!$appeals->render()!!}

+
+
+
+ +@endsection diff --git a/routes/web.php b/routes/web.php index d0ac72ecb..b66f2dda6 100644 --- a/routes/web.php +++ b/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');