mirror of https://github.com/pixelfed/pixelfed
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
266 lines
7.0 KiB
PHTML
266 lines
7.0 KiB
PHTML
|
1 year ago
|
<?php
|
||
|
|
|
||
|
|
namespace App\Models;
|
||
|
|
|
||
|
|
use App\Profile;
|
||
|
|
use Illuminate\Database\Eloquent\Model;
|
||
|
|
use Illuminate\Support\Facades\Cache;
|
||
|
|
|
||
|
|
class CustomFilter extends Model
|
||
|
|
{
|
||
|
|
public $shouldInvalidateCache = false;
|
||
|
|
|
||
|
|
protected $fillable = [
|
||
|
|
'phrase', 'context', 'expires_at', 'action', 'profile_id',
|
||
|
|
];
|
||
|
|
|
||
|
|
protected $casts = [
|
||
|
|
'context' => 'array',
|
||
|
|
'expires_at' => 'datetime',
|
||
|
|
'action' => 'integer',
|
||
|
|
];
|
||
|
|
|
||
|
|
protected $guarded = ['shouldInvalidateCache'];
|
||
|
|
|
||
|
|
const VALID_CONTEXTS = [
|
||
|
|
'home',
|
||
|
|
'notifications',
|
||
|
|
'public',
|
||
|
|
'thread',
|
||
|
|
'account',
|
||
|
|
];
|
||
|
|
|
||
|
|
const EXPIRATION_DURATIONS = [
|
||
|
|
1800, // 30 minutes
|
||
|
|
3600, // 1 hour
|
||
|
|
21600, // 6 hours
|
||
|
|
43200, // 12 hours
|
||
|
|
86400, // 1 day
|
||
|
|
604800, // 1 week
|
||
|
|
];
|
||
|
|
|
||
|
|
const ACTION_WARN = 0;
|
||
|
|
|
||
|
|
const ACTION_HIDE = 1;
|
||
|
|
|
||
|
|
const ACTION_BLUR = 2;
|
||
|
|
|
||
|
|
public function account()
|
||
|
|
{
|
||
|
|
return $this->belongsTo(Profile::class, 'profile_id');
|
||
|
|
}
|
||
|
|
|
||
|
|
public function keywords()
|
||
|
|
{
|
||
|
|
return $this->hasMany(CustomFilterKeyword::class);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function statuses()
|
||
|
|
{
|
||
|
|
return $this->hasMany(CustomFilterStatus::class);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function getTitleAttribute()
|
||
|
|
{
|
||
|
|
return $this->phrase;
|
||
|
|
}
|
||
|
|
|
||
|
|
public function setTitleAttribute($value)
|
||
|
|
{
|
||
|
|
$this->attributes['phrase'] = $value;
|
||
|
|
}
|
||
|
|
|
||
|
|
public function getFilterActionAttribute()
|
||
|
|
{
|
||
|
|
return $this->action;
|
||
|
|
}
|
||
|
|
|
||
|
|
public function setFilterActionAttribute($value)
|
||
|
|
{
|
||
|
|
$this->attributes['action'] = $value;
|
||
|
|
}
|
||
|
|
|
||
|
|
public function setIrreversibleAttribute($value)
|
||
|
|
{
|
||
|
|
$this->attributes['action'] = $value ? self::ACTION_HIDE : self::ACTION_WARN;
|
||
|
|
}
|
||
|
|
|
||
|
|
public function getIrreversibleAttribute()
|
||
|
|
{
|
||
|
|
return $this->action === self::ACTION_HIDE;
|
||
|
|
}
|
||
|
|
|
||
|
|
public function getExpiresInAttribute()
|
||
|
|
{
|
||
|
|
if ($this->expires_at === null) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
$now = now();
|
||
|
|
foreach (self::EXPIRATION_DURATIONS as $duration) {
|
||
|
|
if ($now->addSeconds($duration)->gte($this->expires_at)) {
|
||
|
|
return $duration;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
public function scopeUnexpired($query)
|
||
|
|
{
|
||
|
|
return $query->where(function ($q) {
|
||
|
|
$q->whereNull('expires_at')
|
||
|
|
->orWhere('expires_at', '>', now());
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
public function isExpired()
|
||
|
|
{
|
||
|
|
return $this->expires_at !== null && $this->expires_at->isPast();
|
||
|
|
}
|
||
|
|
|
||
|
|
protected static function boot()
|
||
|
|
{
|
||
|
|
parent::boot();
|
||
|
|
|
||
|
|
static::saving(function ($model) {
|
||
|
|
$model->prepareContextForStorage();
|
||
|
|
$model->shouldInvalidateCache = true;
|
||
|
|
});
|
||
|
|
|
||
|
|
static::deleting(function ($model) {
|
||
|
|
$model->shouldInvalidateCache = true;
|
||
|
|
});
|
||
|
|
|
||
|
|
static::saved(function ($model) {
|
||
|
|
$model->invalidateCache();
|
||
|
|
});
|
||
|
|
|
||
|
|
static::deleted(function ($model) {
|
||
|
|
$model->invalidateCache();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
protected function prepareContextForStorage()
|
||
|
|
{
|
||
|
|
if (is_array($this->context)) {
|
||
|
|
$this->context = array_values(array_filter(array_map('trim', $this->context)));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
protected function invalidateCache()
|
||
|
|
{
|
||
|
|
if (! isset($this->shouldInvalidateCache) || ! $this->shouldInvalidateCache) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->shouldInvalidateCache = false;
|
||
|
|
|
||
|
|
Cache::forget("filters:v3:{$this->profile_id}");
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function getCachedFiltersForAccount($profileId)
|
||
|
|
{
|
||
|
|
$activeFilters = Cache::remember("filters:v3:{$profileId}", 3600, function () use ($profileId) {
|
||
|
|
$filtersHash = [];
|
||
|
|
|
||
|
|
// Get keyword filters
|
||
|
|
$keywordFilters = CustomFilterKeyword::with(['customFilter' => function ($query) use ($profileId) {
|
||
|
|
$query->unexpired()->where('profile_id', $profileId);
|
||
|
|
}])->get();
|
||
|
|
|
||
|
|
$keywordFilters->groupBy('custom_filter_id')->each(function ($keywords, $filterId) use (&$filtersHash) {
|
||
|
|
$filter = $keywords->first()->customFilter;
|
||
|
|
|
||
|
|
if (! $filter) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
$regexPatterns = $keywords->map(function ($keyword) {
|
||
|
|
$pattern = preg_quote($keyword->keyword, '/');
|
||
|
|
if ($keyword->whole_word) {
|
||
|
|
$pattern = '\b'.$pattern.'\b';
|
||
|
|
}
|
||
|
|
|
||
|
|
return $pattern;
|
||
|
|
})->toArray();
|
||
|
|
|
||
|
|
$filtersHash[$filterId] = [
|
||
|
|
'keywords' => '/'.implode('|', $regexPatterns).'/i',
|
||
|
|
'filter' => $filter,
|
||
|
|
];
|
||
|
|
});
|
||
|
|
|
||
|
|
$statusFilters = CustomFilterStatus::with(['customFilter' => function ($query) use ($profileId) {
|
||
|
|
$query->unexpired()->where('profile_id', $profileId);
|
||
|
|
}])->get();
|
||
|
|
|
||
|
|
$statusFilters->groupBy('custom_filter_id')->each(function ($statuses, $filterId) use (&$filtersHash) {
|
||
|
|
$filter = $statuses->first()->customFilter;
|
||
|
|
|
||
|
|
if (! $filter) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (! isset($filtersHash[$filterId])) {
|
||
|
|
$filtersHash[$filterId] = ['filter' => $filter];
|
||
|
|
}
|
||
|
|
|
||
|
|
$filtersHash[$filterId]['status_ids'] = $statuses->pluck('status_id')->toArray();
|
||
|
|
});
|
||
|
|
|
||
|
|
return array_map(function ($item) {
|
||
|
|
$filter = $item['filter'];
|
||
|
|
unset($item['filter']);
|
||
|
|
|
||
|
|
return [$filter, $item];
|
||
|
|
}, $filtersHash);
|
||
|
|
});
|
||
|
|
|
||
|
|
return collect($activeFilters)->reject(function ($item) {
|
||
|
|
[$filter, $rules] = $item;
|
||
|
|
|
||
|
|
return $filter->isExpired();
|
||
|
|
})->toArray();
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function applyCachedFilters($cachedFilters, $status)
|
||
|
|
{
|
||
|
|
$results = [];
|
||
|
|
|
||
|
|
foreach ($cachedFilters as [$filter, $rules]) {
|
||
|
|
$keywordMatches = [];
|
||
|
|
$statusMatches = [];
|
||
|
|
|
||
|
|
if (isset($rules['keywords'])) {
|
||
|
|
$text = $status['content'];
|
||
|
|
preg_match_all($rules['keywords'], $text, $matches);
|
||
|
|
if (! empty($matches[0])) {
|
||
|
|
$keywordMatches = $matches[0];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check for status matches
|
||
|
|
if (isset($rules['status_ids'])) {
|
||
|
|
$statusId = $status->id;
|
||
|
|
$reblogId = $status->reblog_of_id ?? null;
|
||
|
|
|
||
|
|
$matchingIds = array_intersect($rules['status_ids'], array_filter([$statusId, $reblogId]));
|
||
|
|
if (! empty($matchingIds)) {
|
||
|
|
$statusMatches = $matchingIds;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (! empty($keywordMatches) || ! empty($statusMatches)) {
|
||
|
|
$results[] = [
|
||
|
|
'filter' => $filter,
|
||
|
|
'keyword_matches' => $keywordMatches,
|
||
|
|
'status_matches' => $statusMatches,
|
||
|
|
];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return $results;
|
||
|
|
}
|
||
|
|
}
|