diff --git a/app/Http/Controllers/AdminController.php b/app/Http/Controllers/AdminController.php index f044ec9fa..f0583a5ce 100644 --- a/app/Http/Controllers/AdminController.php +++ b/app/Http/Controllers/AdminController.php @@ -104,6 +104,56 @@ class AdminController extends Controller 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(); + + 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(); + + return redirect('/i/admin/reports/autospam'); + } + public function updateAppeal(Request $request, $id) { $this->validate($request, [ diff --git a/app/Jobs/StatusPipeline/StatusEntityLexer.php b/app/Jobs/StatusPipeline/StatusEntityLexer.php index 82ef38890..ce04c6873 100644 --- a/app/Jobs/StatusPipeline/StatusEntityLexer.php +++ b/app/Jobs/StatusPipeline/StatusEntityLexer.php @@ -11,6 +11,7 @@ use App\StatusHashtag; use App\Services\PublicTimelineService; use App\Util\Lexer\Autolink; use App\Util\Lexer\Extractor; +use App\Util\Sentiment\Bouncer; use DB; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; @@ -139,6 +140,10 @@ class StatusEntityLexer implements ShouldQueue { $status = $this->status; + if(config('pixelfed.bouncer.enabled')) { + Bouncer::get($status); + } + if($status->uri == null && $status->scope == 'public') { PublicTimelineService::add($status->id); } diff --git a/app/Util/Sentiment/Bouncer.php b/app/Util/Sentiment/Bouncer.php new file mode 100644 index 000000000..e710266e2 --- /dev/null +++ b/app/Util/Sentiment/Bouncer.php @@ -0,0 +1,79 @@ +uri || $status->scope != 'public') { + return; + } + + $recentKey = 'pf:bouncer:recent_by_pid:' . $status->profile_id; + $recentTtl = now()->addMinutes(5); + $recent = Cache::remember($recentKey, $recentTtl, function() use($status) { + return $status->profile->created_at->gt(now()->subWeek()) || $status->profile->statuses()->count() == 0; + }); + + if(!$recent) { + return; + } + + if($status->profile->followers()->count() > 100) { + return; + } + + if(!Str::contains($status->caption, ['https://', 'http://', 'hxxps://', 'hxxp://', 'www.', '.com', '.net', '.org'])) { + return; + } + + if($status->profile->user->is_admin == true) { + return; + } + + return (new self)->handle($status); + } + + protected function handle($status) + { + $media = $status->media; + + $ai = new AccountInterstitial; + $ai->user_id = $status->profile->user_id; + $ai->type = 'post.autospam'; + $ai->view = 'account.moderation.post.autospam'; + $ai->item_type = 'App\Status'; + $ai->item_id = $status->id; + $ai->has_media = (bool) $media->count(); + $ai->blurhash = $media->count() ? $media->first()->blurhash : null; + $ai->meta = json_encode([ + 'caption' => $status->caption, + 'created_at' => $status->created_at, + 'type' => $status->type, + 'url' => $status->url(), + 'is_nsfw' => $status->is_nsfw, + 'scope' => $status->scope, + 'reblog' => $status->reblog_of_id, + 'likes_count' => $status->likes_count, + 'reblogs_count' => $status->reblogs_count, + ]); + $ai->save(); + + $u = $status->profile->user; + $u->has_interstitial = true; + $u->save(); + + $status->scope = 'unlisted'; + $status->visibility = 'unlisted'; + $status->is_nsfw = true; + $status->save(); + + } + +} \ No newline at end of file diff --git a/config/pixelfed.php b/config/pixelfed.php index 9df6bc99c..b3fd21d7a 100644 --- a/config/pixelfed.php +++ b/config/pixelfed.php @@ -260,4 +260,8 @@ return [ 'admin' => [ 'env_editor' => env('ADMIN_ENV_EDITOR', false) ], + + 'bouncer' => [ + 'enabled' => env('PF_BOUNCER_ENABLED', false), + ] ]; diff --git a/resources/views/account/moderation/post/autospam.blade.php b/resources/views/account/moderation/post/autospam.blade.php new file mode 100644 index 000000000..91296759d --- /dev/null +++ b/resources/views/account/moderation/post/autospam.blade.php @@ -0,0 +1,103 @@ +@extends('layouts.blank') + +@section('content') + +
+
+
+

Suspicious Activity Detected

+

We detected suspicious activity based on your recent post, it has been flagged for review by our moderation team.

+
+
+
+
+
+

Post Details

+ @if($interstitial->has_media) +
+
+ @if($interstitial->blurhash) + + @else + No preview available + @endif +
+
+ @if($meta->caption) +

+ Caption: {{$meta->caption}} +

+ @endif +

+ Like Count: {{$meta->likes_count}} +

+

+ Share Count: {{$meta->reblogs_count}} +

+

+ Timestamp: {{now()->parse($meta->created_at)->format('r')}} +

+

+ URL: {{$meta->url}} +

+
+
+ @else +
+
+ @if($meta->caption) +

+ Comment: {{$meta->caption}} +

+ @endif +

+ Like Count: {{$meta->likes_count}} +

+

+ Share Count: {{$meta->reblogs_count}} +

+

+ Timestamp: {{now()->parse($meta->created_at)->format('r')}} +

+

+ URL: {{$meta->url}} +

+
+
+ @endif +
+
+
+

Review the Community Guidelines

+

We want to keep {{config('app.name')}} a safe place for everyone, and we created these Community Guidelines to support and protect our community.

+
+
+ +
+ +
+ @csrf + + + + + +
+
+
+
+ +@endsection + +@push('scripts') +@if($interstitial->blurhash) + +@endif +@endpush \ No newline at end of file diff --git a/resources/views/admin/reports/home.blade.php b/resources/views/admin/reports/home.blade.php index d78924e5b..e22ee0c47 100644 --- a/resources/views/admin/reports/home.blade.php +++ b/resources/views/admin/reports/home.blade.php @@ -16,12 +16,17 @@ @php($ai = App\AccountInterstitial::whereNotNull('appeal_requested_at')->whereNull('appeal_handled_at')->count()) -@if($ai) +@php($spam = App\AccountInterstitial::whereType('post.autospam')->whereNull('appeal_handled_at')->count()) +@if($ai || $spam)
- +

{{$ai}}

Appeal {{$ai == 1 ? 'Request' : 'Requests'}}
+ +

{{$spam}}

+ Flagged {{$ai == 1 ? 'Post' : 'Posts'}} +
@endif @if($reports->count()) diff --git a/resources/views/admin/reports/show_spam.blade.php b/resources/views/admin/reports/show_spam.blade.php new file mode 100644 index 000000000..9648de03b --- /dev/null +++ b/resources/views/admin/reports/show_spam.blade.php @@ -0,0 +1,91 @@ +@extends('admin.partial.template-full') + +@section('section') +
+
+

Autospam

+

Detected {{$appeal->created_at->diffForHumans()}} from @{{$appeal->user->username}}.

+
+
+
+
+
+
+ @if($appeal->type == 'post.autospam') +
+
Unlisted + Content Warning
+ @if($appeal->has_media) + + @endif +
+
+ @if($meta->caption) +

+ {{$appeal->has_media ? 'Caption' : 'Comment'}}: {{$meta->caption}} +

+ @endif +

+ Like Count: {{$meta->likes_count}} +

+

+ Share Count: {{$meta->reblogs_count}} +

+

+ Timestamp: {{now()->parse($meta->created_at)->format('r')}} +

+

+ URL: {{$meta->url}} +

+
+
+
+ @endif +
+
+
+ @csrf + + +
+ +
+
+ @{{$appeal->user->username}} stats +
+
+

+ Open Appeals: {{App\AccountInterstitial::whereUserId($appeal->user_id)->whereNotNull('appeal_requested_at')->whereNull('appeal_handled_at')->count()}} +

+

+ Total Appeals: {{App\AccountInterstitial::whereUserId($appeal->user_id)->whereNotNull('appeal_requested_at')->count()}} +

+

+ Total Warnings: {{App\AccountInterstitial::whereUserId($appeal->user_id)->count()}} +

+

+ Status Count: {{$appeal->user->statuses()->count()}} +

+

+ Joined: {{$appeal->user->created_at->diffForHumans(null, null, false)}} +

+
+
+
+
+@endsection + +@push('scripts') + +@endpush \ No newline at end of file diff --git a/resources/views/admin/reports/spam.blade.php b/resources/views/admin/reports/spam.blade.php new file mode 100644 index 000000000..2e69c09ad --- /dev/null +++ b/resources/views/admin/reports/spam.blade.php @@ -0,0 +1,64 @@ +@extends('admin.partial.template-full') + +@section('section') +
+

Autospam

+

Posts flagged as spam

+ + +
+
+
+
+
+

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

+

active cases

+
+
+ +
+
+

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

+

total cases

+
+
+
+
+ +

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

+
+
+ +@endsection \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index 432fd44c1..36ee013f4 100644 --- a/routes/web.php +++ b/routes/web.php @@ -8,6 +8,9 @@ Route::domain(config('pixelfed.domain.admin'))->prefix('i/admin')->group(functio Route::get('reports/show/{id}', 'AdminController@showReport'); Route::post('reports/show/{id}', 'AdminController@updateReport'); Route::post('reports/bulk', 'AdminController@bulkUpdateReport'); + Route::get('reports/autospam/{id}', 'AdminController@showSpam'); + Route::post('reports/autospam/{id}', 'AdminController@updateSpam'); + Route::get('reports/autospam', 'AdminController@spam'); Route::get('reports/appeals', 'AdminController@appeals'); Route::get('reports/appeal/{id}', 'AdminController@showAppeal'); Route::post('reports/appeal/{id}', 'AdminController@updateAppeal');