mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-11-22 06:21:27 +00:00
Add AdminShadowFilter feature
This commit is contained in:
parent
a510c3e89c
commit
33ed7a8c91
8 changed files with 399 additions and 5 deletions
122
app/Http/Controllers/AdminShadowFilterController.php
Normal file
122
app/Http/Controllers/AdminShadowFilterController.php
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,6 +20,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
use App\Services\UserFilterService;
|
use App\Services\UserFilterService;
|
||||||
|
use App\Services\AdminShadowFilterService;
|
||||||
|
|
||||||
class StatusEntityLexer implements ShouldQueue
|
class StatusEntityLexer implements ShouldQueue
|
||||||
{
|
{
|
||||||
|
@ -176,7 +177,9 @@ class StatusEntityLexer implements ShouldQueue
|
||||||
$status->reblog_of_id === null &&
|
$status->reblog_of_id === null &&
|
||||||
($hideNsfw ? $status->is_nsfw == false : true)
|
($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') {
|
if(config_cache('federation.activitypub.enabled') == true && config('app.env') == 'production') {
|
||||||
|
|
51
app/Services/AdminShadowFilterService.php
Normal file
51
app/Services/AdminShadowFilterService.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -95,7 +95,7 @@ class PublicTimelineService {
|
||||||
if(self::count() == 0 || $force == true) {
|
if(self::count() == 0 || $force == true) {
|
||||||
$hideNsfw = config('instance.hide_nsfw_on_public_feeds');
|
$hideNsfw = config('instance.hide_nsfw_on_public_feeds');
|
||||||
Redis::del(self::CACHE_KEY);
|
Redis::del(self::CACHE_KEY);
|
||||||
$minId = SnowflakeService::byDate(now()->subDays(14));
|
$minId = SnowflakeService::byDate(now()->subDays(90));
|
||||||
$ids = Status::where('id', '>', $minId)
|
$ids = Status::where('id', '>', $minId)
|
||||||
->whereNull(['uri', 'in_reply_to_id', 'reblog_of_id'])
|
->whereNull(['uri', 'in_reply_to_id', 'reblog_of_id'])
|
||||||
->when($hideNsfw, function($q, $hideNsfw) {
|
->when($hideNsfw, function($q, $hideNsfw) {
|
||||||
|
@ -105,9 +105,11 @@ class PublicTimelineService {
|
||||||
->whereScope('public')
|
->whereScope('public')
|
||||||
->orderByDesc('id')
|
->orderByDesc('id')
|
||||||
->limit($limit)
|
->limit($limit)
|
||||||
->pluck('id');
|
->pluck('id', 'profile_id');
|
||||||
foreach($ids as $id) {
|
foreach($ids as $k => $id) {
|
||||||
self::add($id);
|
if(AdminShadowFilterService::canAddToPublicFeedByProfileId($k)) {
|
||||||
|
self::add($id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
64
resources/views/admin/asf/create.blade.php
Normal file
64
resources/views/admin/asf/create.blade.php
Normal 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
|
64
resources/views/admin/asf/edit.blade.php
Normal file
64
resources/views/admin/asf/edit.blade.php
Normal 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
|
81
resources/views/admin/asf/home.blade.php
Normal file
81
resources/views/admin/asf/home.blade.php
Normal 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">
|
||||||
|
@{{ $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
|
|
@ -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::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::prefix('api')->group(function() {
|
||||||
Route::get('stats', 'AdminController@getStats');
|
Route::get('stats', 'AdminController@getStats');
|
||||||
Route::get('accounts', 'AdminController@getAccounts');
|
Route::get('accounts', 'AdminController@getAccounts');
|
||||||
|
|
Loading…
Reference in a new issue