Add AdminShadowFilter feature

This commit is contained in:
Daniel Supernault 2023-09-14 22:32:37 -06:00
parent a510c3e89c
commit 33ed7a8c91
No known key found for this signature in database
GPG key ID: 0DEF1C662C9033F7
8 changed files with 399 additions and 5 deletions

View file

@ -0,0 +1,122 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\AdminShadowFilter;
use App\Profile;
use App\Services\AccountService;
use App\Services\AdminShadowFilterService;
class AdminShadowFilterController extends Controller
{
public function __construct()
{
$this->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');
}
}

View file

@ -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') {

View file

@ -0,0 +1,51 @@
<?php
namespace App\Services;
use App\Models\AdminShadowFilter;
use Cache;
class AdminShadowFilterService
{
const CACHE_KEY = 'pf:services:asfs:';
public static function queryFilter($name = 'hide_from_public_feeds')
{
return AdminShadowFilter::whereItemType('App\Profile')
->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);
}
}
}

View file

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

View file

@ -0,0 +1,64 @@
@extends('admin.partial.template-full')
@section('section')
</div><div class="header bg-primary pb-3 mt-n4">
<div class="container-fluid">
<div class="header-body">
<div class="row align-items-center py-4">
<div class="col-lg-6 col-7">
<p class="display-1 text-white d-inline-block mb-0">New Shadow Filters</p>
<p class="text-white mb-0">Creating a new admin shadow filter</p>
</div>
</div>
</div>
</div>
</div>
<div class="m-n2 m-lg-4">
<div class="container-fluid mt-4">
<div class="row justify-content-center">
<div class="col-12 col-md-6">
@if ($errors->any())
<div class="alert alert-danger">
<ul class="mb-0">
@foreach ($errors->all() as $error)
<li class="mb-0 font-weight-bold">{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<div class="card card-body">
<form method="post">
@csrf
<div class="form-group">
<label class="font-weight-bold">Username</label>
<input class="form-control" name="username" placeholder="Enter username here" />
</div>
<p class="mb-0 font-weight-bold small">Filters</p>
<div class="list-group mb-3">
<div class="list-group-item">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="hide_from_public_feeds" name="hide_from_public_feeds">
<label class="custom-control-label" for="hide_from_public_feeds">Hide public posts from public feed</label>
</div>
</div>
{{-- <div class="list-group-item"></div> --}}
</div>
<div class="form-group">
<label class="font-weight-bold">Note</label>
<textarea class="form-control" name="note" placeholder="Add an optional note, only visible to admins"></textarea>
</div>
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="active" name="active" checked>
<label class="custom-control-label font-weight-bold" for="active">Mark as Active</label>
</div>
<hr>
<button type="submit" class="btn btn-success">Save</button>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection

View file

@ -0,0 +1,64 @@
@extends('admin.partial.template-full')
@section('section')
</div><div class="header bg-primary pb-3 mt-n4">
<div class="container-fluid">
<div class="header-body">
<div class="row align-items-center py-4">
<div class="col-lg-6 col-7">
<p class="display-1 text-white d-inline-block mb-0">Edit Shadow Filters</p>
<p class="text-white mb-0">Editing shadow filters</p>
</div>
</div>
</div>
</div>
</div>
<div class="m-n2 m-lg-4">
<div class="container-fluid mt-4">
<div class="row justify-content-center">
<div class="col-12 col-md-6">
@if ($errors->any())
<div class="alert alert-danger">
<ul class="mb-0">
@foreach ($errors->all() as $error)
<li class="mb-0 font-weight-bold">{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<div class="card card-body">
<form method="post">
@csrf
<div class="form-group">
<label class="font-weight-bold">Username</label>
<input class="form-control" name="username" placeholder="Enter username here" value="{{ $profile['username'] }}" disabled="disabled" />
</div>
<p class="mb-0 font-weight-bold small">Filters</p>
<div class="list-group mb-3">
<div class="list-group-item">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="hide_from_public_feeds" name="hide_from_public_feeds" {!! $filter->hide_from_public_feeds ? 'checked=""' : '' !!}>
<label class="custom-control-label" for="hide_from_public_feeds">Hide public posts from public feed</label>
</div>
</div>
{{-- <div class="list-group-item"></div> --}}
</div>
<div class="form-group">
<label class="font-weight-bold">Note</label>
<textarea class="form-control" name="note" placeholder="Add an optional note, only visible to admins">{{ $filter->note }}</textarea>
</div>
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="active" name="active" {{ $filter->active ? 'checked=""' : ''}}>
<label class="custom-control-label font-weight-bold" for="active">Mark as Active</label>
</div>
<hr>
<button type="submit" class="btn btn-success">Save</button>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection

View file

@ -0,0 +1,81 @@
@extends('admin.partial.template-full')
@section('section')
</div><div class="header bg-primary pb-3 mt-n4">
<div class="container-fluid">
<div class="header-body">
<div class="row align-items-center py-4">
<div class="col-lg-6 col-7">
<p class="display-1 text-white d-inline-block mb-0">Admin Shadow Filters</p>
<p class="text-white mb-0">Manage shadow filters across Accounts, Hashtags, Feeds and Stories</p>
</div>
</div>
</div>
</div>
</div>
<div class="m-n2 m-lg-4">
<div class="container-fluid mt-4">
<div class="row mb-3 justify-content-between">
<div class="col-12 col-md-8">
<ul class="nav nav-pills">
<li class="nav-item">
<a class="nav-link {{request()->has('filter') ? '':'active'}}" href="/i/admin/asf/home">Active Filters</a>
</li>
<li class="nav-item">
<a class="nav-link {{request()->has('filter') && request()->filter == 'all' ? 'active':''}}" href="/i/admin/asf/home?filter=all">All</a>
</li>
<li class="nav-item">
<a class="nav-link {{request()->has('filter') && request()->filter == 'inactive' ? 'active':''}}" href="/i/admin/asf/home?filter=inactive">Inactive</a>
</li>
<li class="nav-item">
<a class="nav-link {{request()->has('new') ? 'active':''}}" href="/i/admin/asf/create">New</a>
</li>
</ul>
</div>
<div class="col-12 col-md-4">
<form method="get">
<input class="form-control" placeholder="Search by username" name="q" value="{{request()->has('q') ? request()->query('q') : ''}}" />
</form>
</div>
</div>
<div class="table-responsive rounded">
<table class="table table-dark">
<thead class="thead-dark">
<tr>
<th scope="col" class="cursor-pointer">ID</th>
<th scope="col" class="cursor-pointer">Username</th>
<th scope="col" class="cursor-pointer">Hide Feeds</th>
<th scope="col" class="cursor-pointer">Active</th>
<th scope="col" class="cursor-pointer">Created</th>
</tr>
</thead>
<tbody>
@foreach($filters as $filter)
<tr>
<td><a href="/i/admin/asf/edit/{{$filter->id}}">{{ $filter->id }}</a></td>
<td>
<div class="d-flex align-items-center" style="gap: 1rem;">
<img src="{{ $filter->account()['avatar'] }}" class="rounded-circle" width="30" height="30" onerror="this.src='/storage/avatars/default.jpg';this.onerror=null;" />
<p class="font-weight-bold mb-0">
&commat;{{ $filter->account()['acct'] }}
</p>
</div>
</td>
<td>{{ $filter->hide_from_public_feeds ? '✅' : ''}}</td>
<td>{{ $filter->active ? '✅' : ''}}</td>
<td>{{ $filter->created_at->diffForHumans() }}</td>
</tr>
@endforeach
</tbody>
</table>
<div class="d-flex mt-3">
{{ $filters->links() }}
</div>
</div>
</div>
</div>
@endsection

View file

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