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) ++ 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}} +
++ 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}} +
+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.
+{{$ai}}
Appeal {{$ai == 1 ? 'Request' : 'Requests'}} + +{{$spam}}
+ Flagged {{$ai == 1 ? 'Post' : 'Posts'}} +Autospam
+Detected {{$appeal->created_at->diffForHumans()}} from @{{$appeal->user->username}}.
++ {{$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}} +
++ 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)}} +
+Posts flagged as spam
+ + +{{App\AccountInterstitial::whereNull('appeal_handled_at')->whereType('post.autospam')->count()}}
+active cases
+{{App\AccountInterstitial::whereType('post.autospam')->count()}}
+total cases
+No autospam cases found!
+{{$appeal->type}}
+ @if($appeal->item_type) +{{starts_with($appeal->item_type, 'App\\') ? explode('\\',$appeal->item_type)[1] : $appeal->item_type}}
+ @endif + +@{{$appeal->user->username}}
+{{$appeal->created_at->diffForHumans(null, null, true)}}
++ +
+{!!$appeals->render()!!}
+