mirror of https://github.com/pixelfed/pixelfed.git
Merge pull request #4649 from pixelfed/staging
Add AdminShadowFilter model/migration
This commit is contained in:
commit
155e1704ff
|
@ -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\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') {
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Services\AccountService;
|
||||
|
||||
class AdminShadowFilter extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected $casts = [
|
||||
'created_at' => 'datetime'
|
||||
];
|
||||
|
||||
public function account()
|
||||
{
|
||||
if($this->item_type === 'App\Profile') {
|
||||
return AccountService::get($this->item_id, true);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
$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;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('admin_shadow_filters', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
|
@ -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
|
|
@ -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
|
|
@ -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::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');
|
||||
|
|
Loading…
Reference in New Issue