diff --git a/app/Http/Controllers/AdminShadowFilterController.php b/app/Http/Controllers/AdminShadowFilterController.php new file mode 100644 index 000000000..461e1d0c2 --- /dev/null +++ b/app/Http/Controllers/AdminShadowFilterController.php @@ -0,0 +1,122 @@ +middleware(['auth','admin']); + } + + public function home(Request $request) + { + $filter = $request->input('filter'); + $searchQuery = $request->input('q'); + $filters = AdminShadowFilter::when($filter, function($q, $filter) { + if($filter == 'all') { + return $q; + } else if($filter == 'inactive') { + return $q->whereActive(false); + } else { + return $q; + } + }, function($q, $filter) { + return $q->whereActive(true); + }) + ->when($searchQuery, function($q, $searchQuery) { + $ids = Profile::where('username', 'like', '%' . $searchQuery . '%') + ->limit(100) + ->pluck('id') + ->toArray(); + return $q->where('item_type', 'App\Profile')->whereIn('item_id', $ids); + }) + ->latest() + ->paginate(10) + ->withQueryString(); + + return view('admin.asf.home', compact('filters')); + } + + public function create(Request $request) + { + return view('admin.asf.create'); + } + + public function edit(Request $request, $id) + { + $filter = AdminShadowFilter::findOrFail($id); + $profile = AccountService::get($filter->item_id); + return view('admin.asf.edit', compact('filter', 'profile')); + } + + public function store(Request $request) + { + $this->validate($request, [ + 'username' => 'required', + 'active' => 'sometimes', + 'note' => 'sometimes', + 'hide_from_public_feeds' => 'sometimes' + ]); + + $profile = Profile::whereUsername($request->input('username'))->first(); + + if(!$profile) { + return back()->withErrors(['Invalid account']); + } + + if($profile->user && $profile->user->is_admin) { + return back()->withErrors(['Cannot filter an admin account']); + } + + $active = $request->has('active') && $request->has('hide_from_public_feeds'); + + AdminShadowFilter::updateOrCreate([ + 'item_id' => $profile->id, + 'item_type' => get_class($profile) + ], [ + 'is_local' => $profile->domain === null, + 'note' => $request->input('note'), + 'hide_from_public_feeds' => $request->has('hide_from_public_feeds'), + 'admin_id' => $request->user()->profile_id, + 'active' => $active + ]); + + AdminShadowFilterService::refresh(); + + return redirect('/i/admin/asf/home'); + } + + public function storeEdit(Request $request, $id) + { + $this->validate($request, [ + 'active' => 'sometimes', + 'note' => 'sometimes', + 'hide_from_public_feeds' => 'sometimes' + ]); + + $filter = AdminShadowFilter::findOrFail($id); + + $profile = Profile::findOrFail($filter->item_id); + + if($profile->user && $profile->user->is_admin) { + return back()->withErrors(['Cannot filter an admin account']); + } + + $active = $request->has('active'); + $filter->active = $active; + $filter->hide_from_public_feeds = $request->has('hide_from_public_feeds'); + $filter->note = $request->input('note'); + $filter->save(); + + AdminShadowFilterService::refresh(); + + return redirect('/i/admin/asf/home'); + } +} diff --git a/app/Jobs/StatusPipeline/StatusEntityLexer.php b/app/Jobs/StatusPipeline/StatusEntityLexer.php index d205f1e21..2bbc92102 100644 --- a/app/Jobs/StatusPipeline/StatusEntityLexer.php +++ b/app/Jobs/StatusPipeline/StatusEntityLexer.php @@ -20,6 +20,7 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use App\Services\UserFilterService; +use App\Services\AdminShadowFilterService; class StatusEntityLexer implements ShouldQueue { @@ -176,7 +177,9 @@ class StatusEntityLexer implements ShouldQueue $status->reblog_of_id === null && ($hideNsfw ? $status->is_nsfw == false : true) ) { - PublicTimelineService::add($status->id); + if(AdminShadowFilterService::canAddToPublicFeedByProfileId($status->profile_id)) { + PublicTimelineService::add($status->id); + } } if(config_cache('federation.activitypub.enabled') == true && config('app.env') == 'production') { diff --git a/app/Models/AdminShadowFilter.php b/app/Models/AdminShadowFilter.php new file mode 100644 index 000000000..f98086f7f --- /dev/null +++ b/app/Models/AdminShadowFilter.php @@ -0,0 +1,27 @@ + 'datetime' + ]; + + public function account() + { + if($this->item_type === 'App\Profile') { + return AccountService::get($this->item_id, true); + } + + return; + } +} diff --git a/app/Services/AdminShadowFilterService.php b/app/Services/AdminShadowFilterService.php new file mode 100644 index 000000000..a5933508a --- /dev/null +++ b/app/Services/AdminShadowFilterService.php @@ -0,0 +1,51 @@ +whereActive(1) + ->where('hide_from_public_feeds', true) + ->pluck('item_id') + ->toArray(); + } + + public static function getHideFromPublicFeedsList($refresh = false) + { + $key = self::CACHE_KEY . 'list:hide_from_public_feeds'; + if($refresh) { + Cache::forget($key); + } + return Cache::remember($key, 86400, function() { + return AdminShadowFilter::whereItemType('App\Profile') + ->whereActive(1) + ->where('hide_from_public_feeds', true) + ->pluck('item_id') + ->toArray(); + }); + } + + public static function canAddToPublicFeedByProfileId($profileId) + { + return !in_array($profileId, self::getHideFromPublicFeedsList()); + } + + public static function refresh() + { + $keys = [ + self::CACHE_KEY . 'list:hide_from_public_feeds' + ]; + + foreach($keys as $key) { + Cache::forget($key); + } + } +} diff --git a/app/Services/PublicTimelineService.php b/app/Services/PublicTimelineService.php index f2658e4b1..7cd6816b3 100644 --- a/app/Services/PublicTimelineService.php +++ b/app/Services/PublicTimelineService.php @@ -95,7 +95,7 @@ class PublicTimelineService { if(self::count() == 0 || $force == true) { $hideNsfw = config('instance.hide_nsfw_on_public_feeds'); Redis::del(self::CACHE_KEY); - $minId = SnowflakeService::byDate(now()->subDays(14)); + $minId = SnowflakeService::byDate(now()->subDays(90)); $ids = Status::where('id', '>', $minId) ->whereNull(['uri', 'in_reply_to_id', 'reblog_of_id']) ->when($hideNsfw, function($q, $hideNsfw) { @@ -105,9 +105,11 @@ class PublicTimelineService { ->whereScope('public') ->orderByDesc('id') ->limit($limit) - ->pluck('id'); - foreach($ids as $id) { - self::add($id); + ->pluck('id', 'profile_id'); + foreach($ids as $k => $id) { + if(AdminShadowFilterService::canAddToPublicFeedByProfileId($k)) { + self::add($id); + } } return 1; } diff --git a/database/migrations/2023_09_12_044900_create_admin_shadow_filters_table.php b/database/migrations/2023_09_12_044900_create_admin_shadow_filters_table.php new file mode 100644 index 000000000..6b62f32c2 --- /dev/null +++ b/database/migrations/2023_09_12_044900_create_admin_shadow_filters_table.php @@ -0,0 +1,47 @@ +id(); + $table->unsignedBigInteger('admin_id')->nullable(); + $table->morphs('item'); + $table->boolean('is_local')->default(true)->index(); + $table->text('note')->nullable(); + $table->boolean('active')->default(false)->index(); + $table->json('history')->nullable(); + $table->json('ruleset')->nullable(); + $table->boolean('prevent_ap_fanout')->default(false)->index(); + $table->boolean('prevent_new_dms')->default(false)->index(); + $table->boolean('ignore_reports')->default(false)->index(); + $table->boolean('ignore_mentions')->default(false)->index(); + $table->boolean('ignore_links')->default(false)->index(); + $table->boolean('ignore_hashtags')->default(false)->index(); + $table->boolean('hide_from_public_feeds')->default(false)->index(); + $table->boolean('hide_from_tag_feeds')->default(false)->index(); + $table->boolean('hide_embeds')->default(false)->index(); + $table->boolean('hide_from_story_carousel')->default(false)->index(); + $table->boolean('hide_from_search_autocomplete')->default(false)->index(); + $table->boolean('hide_from_search')->default(false)->index(); + $table->boolean('requires_login')->default(false)->index(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('admin_shadow_filters'); + } +}; diff --git a/resources/views/admin/asf/create.blade.php b/resources/views/admin/asf/create.blade.php new file mode 100644 index 000000000..8fc88e4bd --- /dev/null +++ b/resources/views/admin/asf/create.blade.php @@ -0,0 +1,64 @@ +@extends('admin.partial.template-full') + +@section('section') +
+
+
+
+
+

New Shadow Filters

+

Creating a new admin shadow filter

+
+
+
+
+
+
+
+
+
+ @if ($errors->any()) +
+
    + @foreach ($errors->all() as $error) +
  • {{ $error }}
  • + @endforeach +
+
+ @endif +
+
+ @csrf +
+ + +
+ +

Filters

+
+
+
+ + +
+
+ {{--
--}} +
+ +
+ + +
+
+ + +
+
+ +
+
+
+
+
+
+@endsection diff --git a/resources/views/admin/asf/edit.blade.php b/resources/views/admin/asf/edit.blade.php new file mode 100644 index 000000000..6d7a633f0 --- /dev/null +++ b/resources/views/admin/asf/edit.blade.php @@ -0,0 +1,64 @@ +@extends('admin.partial.template-full') + +@section('section') +
+
+
+
+
+

Edit Shadow Filters

+

Editing shadow filters

+
+
+
+
+
+
+
+
+
+ @if ($errors->any()) +
+
    + @foreach ($errors->all() as $error) +
  • {{ $error }}
  • + @endforeach +
+
+ @endif +
+
+ @csrf +
+ + +
+ +

Filters

+
+
+
+ hide_from_public_feeds ? 'checked=""' : '' !!}> + +
+
+ {{--
--}} +
+ +
+ + +
+
+ active ? 'checked=""' : ''}}> + +
+
+ +
+
+
+
+
+
+@endsection diff --git a/resources/views/admin/asf/home.blade.php b/resources/views/admin/asf/home.blade.php new file mode 100644 index 000000000..4fbb7730f --- /dev/null +++ b/resources/views/admin/asf/home.blade.php @@ -0,0 +1,81 @@ +@extends('admin.partial.template-full') + +@section('section') +
+
+
+
+
+

Admin Shadow Filters

+

Manage shadow filters across Accounts, Hashtags, Feeds and Stories

+
+
+
+
+
+
+
+
+
+ +
+ +
+
+ +
+
+
+ +
+ + + + + + + + + + + + @foreach($filters as $filter) + + + + + + + + @endforeach + +
IDUsernameHide FeedsActiveCreated
{{ $filter->id }} +
+ + +

+ @{{ $filter->account()['acct'] }} +

+
+
{{ $filter->hide_from_public_feeds ? '✅' : ''}}{{ $filter->active ? '✅' : ''}}{{ $filter->created_at->diffForHumans() }}
+ +
+ {{ $filters->links() }} +
+
+
+
+@endsection diff --git a/routes/web.php b/routes/web.php index bb091fce5..b823b8729 100644 --- a/routes/web.php +++ b/routes/web.php @@ -96,6 +96,13 @@ Route::domain(config('pixelfed.domain.admin'))->prefix('i/admin')->group(functio Route::get('autospam/home', 'AdminController@autospamHome')->name('admin.autospam'); + Route::redirect('asf/', 'asf/home'); + Route::get('asf/home', 'AdminShadowFilterController@home'); + Route::get('asf/create', 'AdminShadowFilterController@create'); + Route::get('asf/edit/{id}', 'AdminShadowFilterController@edit'); + Route::post('asf/edit/{id}', 'AdminShadowFilterController@storeEdit'); + Route::post('asf/create', 'AdminShadowFilterController@store'); + Route::prefix('api')->group(function() { Route::get('stats', 'AdminController@getStats'); Route::get('accounts', 'AdminController@getAccounts');