Merge pull request #5928 from pixelfed/staging

Add custom filters
dev
daniel 2025-04-14 05:44:47 -06:00 zatwierdzone przez GitHub
commit 3d6348225b
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
67 zmienionych plików z 4112 dodań i 684 usunięć

Wyświetl plik

@ -4,6 +4,7 @@
### Added
- Pinned Posts ([2f655d000](https://github.com/pixelfed/pixelfed/commit/2f655d000))
- Custom Filters ([#5928](https://github.com/pixelfed/pixelfed/pull/5928)) ([437d742ac](https://github.com/pixelfed/pixelfed/commit/437d742ac))
### Updates
- Update PublicApiController, use pixelfed entities for /api/pixelfed/v1/accounts/id/statuses with bookmarked state ([5ddb6d842](https://github.com/pixelfed/pixelfed/commit/5ddb6d842))
@ -18,6 +19,7 @@
- Update DiscoverController, improve public hashtag feed. Fixes #5866 ([32fc3180c](https://github.com/pixelfed/pixelfed/commit/32fc3180c))
- Update report views, fix missing forms ([475d1d627](https://github.com/pixelfed/pixelfed/commit/475d1d627))
- Update private settings, change "Private Account" to "Manually Review Follow Requests" ([31dd1ab35](https://github.com/pixelfed/pixelfed/commit/31dd1ab35))
- Update ReportController, fix type validation ([ccc7f2fc6](https://github.com/pixelfed/pixelfed/commit/ccc7f2fc6))
- ([](https://github.com/pixelfed/pixelfed/commit/))
## [v0.12.5 (2025-03-23)](https://github.com/pixelfed/pixelfed/compare/v0.12.5...dev)

Wyświetl plik

@ -35,6 +35,7 @@ use App\Jobs\VideoPipeline\VideoThumbnail;
use App\Like;
use App\Media;
use App\Models\Conversation;
use App\Models\CustomFilter;
use App\Notification;
use App\Profile;
use App\Services\AccountService;
@ -2514,6 +2515,14 @@ class ApiV1Controller extends Controller
['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'];
AccountService::setLastActive($request->user()->id);
$cachedFilters = CustomFilter::getCachedFiltersForAccount($pid);
$homeFilters = array_filter($cachedFilters, function ($item) {
[$filter, $rules] = $item;
return in_array('home', $filter->context);
});
if (config('exp.cached_home_timeline')) {
$paddedLimit = $includeReblogs ? $limit + 10 : $limit + 50;
if ($min || $max) {
@ -2550,6 +2559,23 @@ class ApiV1Controller extends Controller
->filter(function ($s) use ($includeReblogs) {
return $includeReblogs ? true : $s['reblog'] == null;
})
->map(function ($status) use ($homeFilters) {
$filterResults = CustomFilter::applyCachedFilters($homeFilters, $status);
if (! empty($filterResults)) {
$status['filtered'] = $filterResults;
$shouldHide = collect($filterResults)->contains(function ($result) {
return $result['filter']['filter_action'] === 'hide';
});
if ($shouldHide) {
return null;
}
}
return $status;
})
->filter()
->take($limit)
->map(function ($status) use ($pid) {
if ($pid) {
@ -2658,6 +2684,23 @@ class ApiV1Controller extends Controller
return $status;
})
->map(function ($status) use ($homeFilters) {
$filterResults = CustomFilter::applyCachedFilters($homeFilters, $status);
if (! empty($filterResults)) {
$status['filtered'] = $filterResults;
$shouldHide = collect($filterResults)->contains(function ($result) {
return $result['filter']['filter_action'] === 'hide';
});
if ($shouldHide) {
return null;
}
}
return $status;
})
->filter()
->take($limit)
->values();
} else {
@ -2712,6 +2755,23 @@ class ApiV1Controller extends Controller
return $status;
})
->map(function ($status) use ($homeFilters) {
$filterResults = CustomFilter::applyCachedFilters($homeFilters, $status);
if (! empty($filterResults)) {
$status['filtered'] = $filterResults;
$shouldHide = collect($filterResults)->contains(function ($result) {
return $result['filter']['filter_action'] === 'hide';
});
if ($shouldHide) {
return null;
}
}
return $status;
})
->filter()
->take($limit)
->values();
}
@ -2773,7 +2833,7 @@ class ApiV1Controller extends Controller
$limit = 40;
}
$user = $request->user();
$pid = $user->profile_id;
$remote = $request->has('remote') && $request->boolean('remote');
$local = $request->boolean('local');
$userRoleKey = $remote ? 'can-view-network-feed' : 'can-view-public-feed';
@ -2786,6 +2846,14 @@ class ApiV1Controller extends Controller
$hideNsfw = config('instance.hide_nsfw_on_public_feeds');
$amin = SnowflakeService::byDate(now()->subDays(config('federation.network_timeline_days_falloff')));
$asf = AdminShadowFilterService::getHideFromPublicFeedsList();
$cachedFilters = CustomFilter::getCachedFiltersForAccount($pid);
$homeFilters = array_filter($cachedFilters, function ($item) {
[$filter, $rules] = $item;
return in_array('public', $filter->context);
});
if ($local && $remote) {
$feed = Status::select(
'id',
@ -2976,6 +3044,23 @@ class ApiV1Controller extends Controller
return true;
})
->map(function ($status) use ($homeFilters) {
$filterResults = CustomFilter::applyCachedFilters($homeFilters, $status);
if (! empty($filterResults)) {
$status['filtered'] = $filterResults;
$shouldHide = collect($filterResults)->contains(function ($result) {
return $result['filter']['filter_action'] === 'hide';
});
if ($shouldHide) {
return null;
}
}
return $status;
})
->filter()
->take($limit)
->values();
@ -3919,8 +4004,16 @@ class ApiV1Controller extends Controller
$pe = $request->has(self::PF_API_ENTITY_KEY);
$pid = $request->user()->profile_id;
$cachedFilters = CustomFilter::getCachedFiltersForAccount($pid);
$tagFilters = array_filter($cachedFilters, function ($item) {
[$filter, $rules] = $item;
return in_array('tags', $filter->context);
});
if ($min || $max) {
$minMax = SnowflakeService::byDate(now()->subMonths(6));
$minMax = SnowflakeService::byDate(now()->subMonths(9));
if ($min && intval($min) < $minMax) {
return [];
}
@ -3975,6 +4068,23 @@ class ApiV1Controller extends Controller
return ! in_array($i['account']['id'], $filters) && ! in_array($domain, $domainBlocks);
})
->map(function ($status) use ($tagFilters) {
$filterResults = CustomFilter::applyCachedFilters($tagFilters, $status);
if (! empty($filterResults)) {
$status['filtered'] = $filterResults;
$shouldHide = collect($filterResults)->contains(function ($result) {
return $result['filter']['filter_action'] === 'hide';
});
if ($shouldHide) {
return null;
}
}
return $status;
})
->filter()
->take($limit)
->values()
->toArray();

Wyświetl plik

@ -30,7 +30,6 @@ use App\Util\Media\License;
use Auth;
use Cache;
use DB;
use Purify;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use League\Fractal;
@ -240,7 +239,13 @@ class ComposeController extends Controller
abort_if(! $request->user(), 403);
$this->validate($request, [
'q' => 'required|string|min:1|max:50',
'q' => [
'required',
'string',
'min:1',
'max:300',
new \App\Rules\WebFinger,
],
]);
$q = $request->input('q');
@ -571,7 +576,7 @@ class ComposeController extends Controller
$status->cw_summary = $request->input('spoiler_text');
}
$defaultCaption = "";
$defaultCaption = '';
$status->caption = strip_tags($request->input('caption')) ?? $defaultCaption;
$status->rendered = $defaultCaption;
$status->scope = 'draft';
@ -677,7 +682,7 @@ class ComposeController extends Controller
$place = $request->input('place');
$cw = $request->input('cw');
$tagged = $request->input('tagged');
$defaultCaption = config_cache('database.default') === 'mysql' ? null : "";
$defaultCaption = config_cache('database.default') === 'mysql' ? null : '';
if ($place && is_array($place)) {
$status->place_id = $place['id'];

Wyświetl plik

@ -0,0 +1,503 @@
<?php
namespace App\Http\Controllers;
use App\Models\CustomFilter;
use App\Models\CustomFilterKeyword;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
use Illuminate\Validation\Rule;
class CustomFilterController extends Controller
{
// const ACTIVE_TYPES = ['home', 'public', 'tags', 'notifications', 'thread', 'profile', 'groups'];
const ACTIVE_TYPES = ['home', 'public', 'tags'];
public function index(Request $request)
{
abort_if(! $request->user() || ! $request->user()->token(), 403);
abort_unless($request->user()->tokenCan('read'), 403);
$filters = CustomFilter::where('profile_id', $request->user()->profile_id)
->unexpired()
->with(['keywords'])
->orderByDesc('updated_at')
->get()
->map(function ($filter) {
return [
'id' => $filter->id,
'title' => $filter->title,
'context' => $filter->context,
'expires_at' => $filter->expires_at,
'filter_action' => $filter->filterAction,
'keywords' => $filter->keywords->map(function ($keyword) {
return [
'id' => $keyword->id,
'keyword' => $keyword->keyword,
'whole_word' => (bool) $keyword->whole_word,
];
}),
'statuses' => [],
];
});
return response()->json($filters);
}
public function show(Request $request, $id)
{
abort_if(! $request->user() || ! $request->user()->token(), 403);
abort_unless($request->user()->tokenCan('read'), 403);
$filter = CustomFilter::findOrFail($id);
Gate::authorize('view', $filter);
$filter->load(['keywords']);
$res = [
'id' => $filter->id,
'title' => $filter->title,
'context' => $filter->context,
'expires_at' => $filter->expires_at,
'filter_action' => $filter->filterAction,
'keywords' => $filter->keywords->map(function ($keyword) {
return [
'id' => $keyword->id,
'keyword' => $keyword->keyword,
'whole_word' => (bool) $keyword->whole_word,
];
}),
'statuses' => [],
];
return response()->json($res);
}
public function store(Request $request)
{
abort_if(! $request->user() || ! $request->user()->token(), 403);
abort_unless($request->user()->tokenCan('write'), 403);
Gate::authorize('create', CustomFilter::class);
$validatedData = $request->validate([
'title' => 'required|string|max:100',
'context' => 'required|array',
'context.*' => [
'string',
Rule::in(self::ACTIVE_TYPES),
],
'filter_action' => 'string|in:warn,hide,blur',
'expires_in' => 'nullable|integer|min:0|max:63072000',
'keywords_attributes' => 'required|array|min:1|max:'.CustomFilter::getMaxKeywordsPerFilter(),
'keywords_attributes.*.keyword' => [
'required',
'string',
'min:1',
'max:'.CustomFilter::getMaxKeywordLength(),
'regex:/^[\p{L}\p{N}\p{Zs}\p{P}\p{M}]+$/u',
function ($attribute, $value, $fail) {
if (preg_match('/(.)\1{20,}/', $value)) {
$fail('The keyword contains excessive character repetition.');
}
},
],
'keywords_attributes.*.whole_word' => 'boolean',
]);
$profile_id = $request->user()->profile_id;
$userFilterCount = CustomFilter::where('profile_id', $profile_id)->count();
$maxFiltersPerUser = CustomFilter::getMaxFiltersPerUser();
if (! $request->user()->is_admin && $userFilterCount >= $maxFiltersPerUser) {
return response()->json([
'error' => 'Filter limit exceeded',
'message' => 'You can only have '.$maxFiltersPerUser.' filters at a time.',
], 422);
}
$rateKey = 'filters_created:'.$request->user()->id;
$maxFiltersPerHour = CustomFilter::getMaxCreatePerHour();
$currentCount = Cache::get($rateKey, 0);
if (! $request->user()->is_admin && $currentCount >= $maxFiltersPerHour) {
return response()->json([
'error' => 'Rate limit exceeded',
'message' => 'You can only create '.$maxFiltersPerHour.' filters per hour.',
], 429);
}
DB::beginTransaction();
try {
$requestedKeywords = array_map(function ($item) {
return mb_strtolower(trim($item['keyword']));
}, $validatedData['keywords_attributes']);
$existingKeywords = DB::table('custom_filter_keywords')
->join('custom_filters', 'custom_filter_keywords.custom_filter_id', '=', 'custom_filters.id')
->where('custom_filters.profile_id', $profile_id)
->whereIn('custom_filter_keywords.keyword', $requestedKeywords)
->pluck('custom_filter_keywords.keyword')
->toArray();
if (! empty($existingKeywords)) {
return response()->json([
'error' => 'Duplicate keywords found',
'message' => 'The following keywords already exist: '.implode(', ', $existingKeywords),
], 422);
}
$expiresAt = null;
if (isset($validatedData['expires_in']) && $validatedData['expires_in'] > 0) {
$expiresAt = now()->addSeconds($validatedData['expires_in']);
}
$action = CustomFilter::ACTION_WARN;
if (isset($validatedData['filter_action'])) {
$action = $this->filterActionToAction($validatedData['filter_action']);
}
$filter = CustomFilter::create([
'title' => $validatedData['title'],
'context' => $validatedData['context'],
'action' => $action,
'expires_at' => $expiresAt,
'profile_id' => $request->user()->profile_id,
]);
if (isset($validatedData['keywords_attributes'])) {
foreach ($validatedData['keywords_attributes'] as $keywordData) {
$keyword = trim($keywordData['keyword']);
$filter->keywords()->create([
'keyword' => $keyword,
'whole_word' => (bool) $keywordData['whole_word'] ?? true,
]);
}
}
Cache::increment($rateKey);
if (! Cache::has($rateKey)) {
Cache::put($rateKey, 1, 3600);
}
Cache::forget("filters:v3:{$profile_id}");
DB::commit();
$filter->load(['keywords', 'statuses']);
$res = [
'id' => $filter->id,
'title' => $filter->title,
'context' => $filter->context,
'expires_at' => $filter->expires_at,
'filter_action' => $filter->filterAction,
'keywords' => $filter->keywords->map(function ($keyword) {
return [
'id' => $keyword->id,
'keyword' => $keyword->keyword,
'whole_word' => (bool) $keyword->whole_word,
];
}),
'statuses' => $filter->statuses->map(function ($status) {
return [
'id' => $status->id,
'status_id' => $status->status_id,
];
}),
];
return response()->json($res, 200);
} catch (\Exception $e) {
DB::rollBack();
return response()->json([
'error' => 'Failed to create filter',
'message' => $e->getMessage(),
], 500);
}
}
/**
* Convert Mastodon filter_action string to internal action value
*
* @param string $filterAction
* @return int
*/
private function filterActionToAction($filterAction)
{
switch ($filterAction) {
case 'warn':
return CustomFilter::ACTION_WARN;
case 'hide':
return CustomFilter::ACTION_HIDE;
case 'blur':
return CustomFilter::ACTION_BLUR;
default:
return CustomFilter::ACTION_WARN;
}
}
public function update(Request $request, $id)
{
abort_if(! $request->user() || ! $request->user()->token(), 403);
abort_unless($request->user()->tokenCan('write'), 403);
$filter = CustomFilter::findOrFail($id);
$pid = $request->user()->profile_id;
if ($filter->profile_id !== $pid) {
return response()->json(['error' => 'This action is unauthorized'], 401);
}
Gate::authorize('update', $filter);
$validatedData = $request->validate([
'title' => 'string|max:100',
'context' => 'array|max:10',
'context.*' => 'string|in:home,notifications,public,thread,account,tags,groups',
'context.*' => [
'string',
Rule::in(self::ACTIVE_TYPES),
],
'filter_action' => 'string|in:warn,hide,blur',
'expires_in' => 'nullable|integer|min:0|max:63072000',
'keywords_attributes' => [
'required',
'array',
'min:1',
function ($attribute, $value, $fail) {
$activeKeywords = collect($value)->filter(function ($keyword) {
return ! isset($keyword['_destroy']) || $keyword['_destroy'] !== true;
})->count();
if ($activeKeywords > CustomFilter::getMaxKeywordsPerFilter()) {
$fail('You may not have more than '.CustomFilter::getMaxKeywordsPerFilter().' active keywords.');
}
},
],
'keywords_attributes.*.id' => 'nullable|integer|exists:custom_filter_keywords,id',
'keywords_attributes.*.keyword' => [
'required_without:keywords_attributes.*.id',
'string',
'min:1',
'max:'.CustomFilter::getMaxKeywordLength(),
'regex:/^[\p{L}\p{N}\p{Zs}\p{P}\p{M}]+$/u',
function ($attribute, $value, $fail) {
if (preg_match('/(.)\1{20,}/', $value)) {
$fail('The keyword contains excessive character repetition.');
}
},
],
'keywords_attributes.*.whole_word' => 'boolean',
'keywords_attributes.*._destroy' => 'boolean',
]);
$rateKey = 'filters_updated:'.$request->user()->id;
$maxUpdatesPerHour = CustomFilter::getMaxUpdatesPerHour();
$currentCount = Cache::get($rateKey, 0);
if (! $request->user()->is_admin && $currentCount >= $maxUpdatesPerHour) {
return response()->json([
'error' => 'Rate limit exceeded',
'message' => 'You can only update filters '.$maxUpdatesPerHour.' times per hour.',
], 429);
}
DB::beginTransaction();
try {
$keywordIds = collect($validatedData['keywords_attributes'])->pluck('id')->filter()->toArray();
if (count($keywordIds) && ! CustomFilterKeyword::whereCustomFilterId($filter->id)->whereIn('id', $keywordIds)->count()) {
return response()->json([
'error' => 'Record not found',
], 404);
}
$requestedKeywords = [];
foreach ($validatedData['keywords_attributes'] as $item) {
if (isset($item['keyword']) && (! isset($item['_destroy']) || ! $item['_destroy'])) {
$requestedKeywords[] = mb_strtolower(trim($item['keyword']));
}
}
if (! empty($requestedKeywords)) {
$existingKeywords = DB::table('custom_filter_keywords')
->join('custom_filters', 'custom_filter_keywords.custom_filter_id', '=', 'custom_filters.id')
->where('custom_filters.profile_id', $pid)
->whereIn('custom_filter_keywords.keyword', $requestedKeywords)
->where('custom_filter_keywords.custom_filter_id', '!=', $id)
->pluck('custom_filter_keywords.keyword')
->toArray();
if (! empty($existingKeywords)) {
return response()->json([
'error' => 'Duplicate keywords found',
'message' => 'The following keywords already exist: '.implode(', ', $existingKeywords),
], 422);
}
}
if (isset($validatedData['expires_in'])) {
if ($validatedData['expires_in'] > 0) {
$filter->expires_at = now()->addSeconds($validatedData['expires_in']);
} else {
$filter->expires_at = null;
}
}
if (isset($validatedData['title'])) {
$filter->title = $validatedData['title'];
}
if (isset($validatedData['context'])) {
$filter->context = $validatedData['context'];
}
if (isset($validatedData['filter_action'])) {
$filter->action = $this->filterActionToAction($validatedData['filter_action']);
}
$filter->save();
if (isset($validatedData['keywords_attributes'])) {
$existingKeywords = $filter->keywords()->pluck('id')->toArray();
$processedIds = [];
foreach ($validatedData['keywords_attributes'] as $keywordData) {
// Case 1: Explicit deletion with _destroy flag
if (isset($keywordData['id']) && isset($keywordData['_destroy']) && (bool) $keywordData['_destroy']) {
// Verify this ID belongs to this filter before deletion
$kwf = CustomFilterKeyword::where('custom_filter_id', $filter->id)
->where('id', $keywordData['id'])
->first();
if ($kwf) {
$kwf->delete();
$processedIds[] = $keywordData['id'];
}
}
// Case 2: Update existing keyword
elseif (isset($keywordData['id'])) {
// Skip if we've already processed this ID
if (in_array($keywordData['id'], $processedIds)) {
continue;
}
// Verify this ID belongs to this filter before updating
$keyword = CustomFilterKeyword::where('custom_filter_id', $filter->id)
->where('id', $keywordData['id'])
->first();
if (! isset($keywordData['_destroy']) && $filter->keywords()->pluck('id')->search($keywordData['id']) === false) {
return response()->json([
'error' => 'Duplicate keywords found',
'message' => 'The following keywords already exist: '.$keywordData['keyword'],
], 422);
}
if ($keyword) {
$updateData = [];
if (isset($keywordData['keyword'])) {
$updateData['keyword'] = trim($keywordData['keyword']);
}
if (isset($keywordData['whole_word'])) {
$updateData['whole_word'] = (bool) $keywordData['whole_word'];
}
if (! empty($updateData)) {
$keyword->update($updateData);
}
$processedIds[] = $keywordData['id'];
}
}
// Case 3: Create new keyword
elseif (isset($keywordData['keyword'])) {
// Check if we're about to exceed the keyword limit
$existingKeywordCount = $filter->keywords()->count();
$maxKeywordsPerFilter = CustomFilter::getMaxKeywordsPerFilter();
if ($existingKeywordCount >= $maxKeywordsPerFilter) {
return response()->json([
'error' => 'Keyword limit exceeded',
'message' => 'A filter can have a maximum of '.$maxKeywordsPerFilter.' keywords.',
], 422);
}
// Skip existing case-insensitive keywords
if ($filter->keywords()->pluck('keyword')->search(mb_strtolower(trim($keywordData['keyword']))) !== false) {
continue;
}
$filter->keywords()->create([
'keyword' => trim($keywordData['keyword']),
'whole_word' => (bool) ($keywordData['whole_word'] ?? true),
]);
}
}
}
Cache::increment($rateKey);
if (! Cache::has($rateKey)) {
Cache::put($rateKey, 1, 3600);
}
Cache::forget("filters:v3:{$pid}");
DB::commit();
$filter->load(['keywords', 'statuses']);
$res = [
'id' => $filter->id,
'title' => $filter->title,
'context' => $filter->context,
'expires_at' => $filter->expires_at,
'filter_action' => $filter->filterAction,
'keywords' => $filter->keywords->map(function ($keyword) {
return [
'id' => $keyword->id,
'keyword' => $keyword->keyword,
'whole_word' => (bool) $keyword->whole_word,
];
}),
'statuses' => $filter->statuses->map(function ($status) {
return [
'id' => $status->id,
'status_id' => $status->status_id,
];
}),
];
return response()->json($res);
} catch (\Exception $e) {
DB::rollBack();
return response()->json([
'error' => 'Failed to update filter',
'message' => $e->getMessage(),
], 500);
}
}
public function delete(Request $request, $id)
{
abort_if(! $request->user() || ! $request->user()->token(), 403);
abort_unless($request->user()->tokenCan('write'), 403);
$filter = CustomFilter::findOrFail($id);
Gate::authorize('delete', $filter);
$filter->delete();
return response()->json((object) [], 200);
}
}

Wyświetl plik

@ -0,0 +1,10 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class CustomFilterKeywordController extends Controller
{
//
}

Wyświetl plik

@ -0,0 +1,10 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class CustomFilterStatusController extends Controller
{
//
}

Wyświetl plik

@ -350,4 +350,9 @@ class SettingsController extends Controller
return redirect(route('settings'))->with('status', 'Media settings successfully updated!');
}
public function filtersHome(Request $request)
{
return view('settings.filters.home');
}
}

Wyświetl plik

@ -0,0 +1,412 @@
<?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 = [
'title', 'phrase', 'context', 'expires_at', 'action', 'profile_id',
];
protected $casts = [
'id' => 'string',
'context' => 'array',
'expires_at' => 'datetime',
'action' => 'integer',
];
protected $guarded = ['shouldInvalidateCache'];
const VALID_CONTEXTS = [
'home',
'notifications',
'public',
'thread',
'account',
];
const MAX_STATUSES_PER_FILTER = 10;
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;
protected static ?int $maxContentScanLimit = null;
protected static ?int $maxFiltersPerUser = null;
protected static ?int $maxKeywordsPerFilter = null;
protected static ?int $maxKeywordsLength = null;
protected static ?int $maxPatternLength = null;
protected static ?int $maxCreatePerHour = null;
protected static ?int $maxUpdatesPerHour = null;
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 toFilterArray()
{
return [
'id' => $this->id,
'title' => $this->title,
'context' => $this->context,
'expires_at' => $this->expires_at,
'filter_action' => $this->filterAction,
];
}
public function getFilterActionAttribute()
{
switch ($this->action) {
case 0:
return 'warn';
break;
case 1:
return 'hide';
break;
case 2:
return 'blur';
break;
}
}
public function getTitleAttribute()
{
return $this->phrase;
}
public function setTitleAttribute($value)
{
$this->attributes['phrase'] = $value;
}
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::updating(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 getMaxContentScanLimit(): int
{
if (self::$maxContentScanLimit === null) {
self::$maxContentScanLimit = config('instance.custom_filters.max_content_scan_limit', 2500);
}
return self::$maxContentScanLimit;
}
public static function getMaxFiltersPerUser(): int
{
if (self::$maxFiltersPerUser === null) {
self::$maxFiltersPerUser = config('instance.custom_filters.max_filters_per_user', 20);
}
return self::$maxFiltersPerUser;
}
public static function getMaxKeywordsPerFilter(): int
{
if (self::$maxKeywordsPerFilter === null) {
self::$maxKeywordsPerFilter = config('instance.custom_filters.max_keywords_per_filter', 10);
}
return self::$maxKeywordsPerFilter;
}
public static function getMaxKeywordLength(): int
{
if (self::$maxKeywordsLength === null) {
self::$maxKeywordsLength = config('instance.custom_filters.max_keyword_length', 40);
}
return self::$maxKeywordsLength;
}
public static function getMaxPatternLength(): int
{
if (self::$maxPatternLength === null) {
self::$maxPatternLength = config('instance.custom_filters.max_pattern_length', 10000);
}
return self::$maxPatternLength;
}
public static function getMaxCreatePerHour(): int
{
if (self::$maxCreatePerHour === null) {
self::$maxCreatePerHour = config('instance.custom_filters.max_create_per_hour', 20);
}
return self::$maxCreatePerHour;
}
public static function getMaxUpdatesPerHour(): int
{
if (self::$maxUpdatesPerHour === null) {
self::$maxUpdatesPerHour = config('instance.custom_filters.max_updates_per_hour', 40);
}
return self::$maxUpdatesPerHour;
}
/**
* Get cached filters for an account with simplified, secure approach
*
* @param int $profileId The profile ID
* @return Collection The collection of filters
*/
public static function getCachedFiltersForAccount($profileId)
{
$activeFilters = Cache::remember("filters:v3:{$profileId}", 3600, function () use ($profileId) {
$filtersHash = [];
$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;
}
$maxPatternsPerFilter = self::getMaxFiltersPerUser();
$keywordsToProcess = $keywords->take($maxPatternsPerFilter);
$regexPatterns = $keywordsToProcess->map(function ($keyword) {
$pattern = preg_quote($keyword->keyword, '/');
if ($keyword->whole_word) {
$pattern = '\b'.$pattern.'\b';
}
return $pattern;
})->toArray();
if (empty($regexPatterns)) {
return;
}
$combinedPattern = implode('|', $regexPatterns);
$maxPatternLength = self::getMaxPatternLength();
if (strlen($combinedPattern) > $maxPatternLength) {
$combinedPattern = substr($combinedPattern, 0, $maxPatternLength);
}
$filtersHash[$filterId] = [
'keywords' => '/'.$combinedPattern.'/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];
// }
// $maxStatusIds = self::MAX_STATUSES_PER_FILTER;
// $filtersHash[$filterId]['status_ids'] = $statuses->take($maxStatusIds)->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();
}
/**
* Apply cached filters to a status with reasonable safety measures
*
* @param array $cachedFilters The cached filters
* @param mixed $status The status to check
* @return array The filter matches
*/
public static function applyCachedFilters($cachedFilters, $status)
{
$results = [];
foreach ($cachedFilters as [$filter, $rules]) {
$keywordMatches = [];
$statusMatches = null;
if (isset($rules['keywords'])) {
$text = strip_tags($status['content']);
$maxContentLength = self::getMaxContentScanLimit();
if (mb_strlen($text) > $maxContentLength) {
$text = mb_substr($text, 0, $maxContentLength);
}
try {
preg_match_all($rules['keywords'], $text, $matches, PREG_PATTERN_ORDER, 0);
if (! empty($matches[0])) {
$maxReportedMatches = (int) config('instance.custom_filters.max_reported_matches', 10);
$keywordMatches = array_slice($matches[0], 0, $maxReportedMatches);
}
} catch (\Throwable $e) {
\Log::error('Filter regex error: '.$e->getMessage(), [
'filter_id' => $filter->id,
]);
}
}
// 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->toFilterArray(),
'keyword_matches' => $keywordMatches ?: null,
'status_matches' => ! empty($statusMatches) ? $statusMatches : null,
];
}
}
return $results;
}
}

Wyświetl plik

@ -0,0 +1,37 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class CustomFilterKeyword extends Model
{
protected $fillable = [
'keyword', 'whole_word', 'custom_filter_id',
];
protected $casts = [
'whole_word' => 'boolean',
];
public function customFilter()
{
return $this->belongsTo(CustomFilter::class);
}
public function setKeywordAttribute($value)
{
$this->attributes['keyword'] = mb_strtolower(trim($value));
}
public function toRegex()
{
$pattern = preg_quote($this->keyword, '/');
if ($this->whole_word) {
$pattern = '\b'.$pattern.'\b';
}
return '/'.$pattern.'/i';
}
}

Wyświetl plik

@ -0,0 +1,23 @@
<?php
namespace App\Models;
use App\Status;
use Illuminate\Database\Eloquent\Model;
class CustomFilterStatus extends Model
{
protected $fillable = [
'custom_filter_id', 'status_id',
];
public function customFilter()
{
return $this->belongsTo(CustomFilter::class);
}
public function status()
{
return $this->belongsTo(Status::class);
}
}

Wyświetl plik

@ -0,0 +1,61 @@
<?php
namespace App\Policies;
use App\Models\CustomFilter;
use App\User;
class CustomFilterPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return false;
}
/**
* Determine whether the user can view the custom filter.
*
* @param \App\User $user
* @param \App\Models\CustomFilter $filter
* @return bool
*/
public function view(User $user, CustomFilter $filter)
{
return $user->profile_id === $filter->profile_id;
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return CustomFilter::whereProfileId($user->profile_id)->count() <= 100;
}
/**
* Determine whether the user can update the custom filter.
*
* @param \App\User $user
* @param \App\Models\CustomFilter $filter
* @return bool
*/
public function update(User $user, CustomFilter $filter)
{
return $user->profile_id === $filter->profile_id;
}
/**
* Determine whether the user can delete the custom filter.
*
* @param \App\User $user
* @param \App\Models\CustomFilter $filter
* @return bool
*/
public function delete(User $user, CustomFilter $filter)
{
return $user->profile_id === $filter->profile_id;
}
}

Wyświetl plik

@ -5,6 +5,8 @@ namespace App\Providers;
use Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Laravel\Passport\Passport;
use App\Models\CustomFilter;
use App\Policies\CustomFilterPolicy;
class AuthServiceProvider extends ServiceProvider
{
@ -14,7 +16,7 @@ class AuthServiceProvider extends ServiceProvider
* @var array
*/
protected $policies = [
// 'App\Model' => 'App\Policies\ModelPolicy',
CustomFilter::class => CustomFilterPolicy::class,
];
/**

Wyświetl plik

@ -0,0 +1,62 @@
<?php
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
class WebFinger implements Rule
{
/**
* Determine if the validation rule passes.
*
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value)
{
if (! is_string($value)) {
return false;
}
$mention = $value;
if (str_starts_with($mention, '@')) {
$mention = substr($mention, 1);
}
$parts = explode('@', $mention);
if (count($parts) !== 2) {
return false;
}
[$username, $domain] = $parts;
if (empty($username) ||
! preg_match('/^[a-zA-Z0-9_.-]+$/', $username) ||
strlen($username) >= 80) {
return false;
}
if (empty($domain) ||
! str_contains($domain, '.') ||
! preg_match('/^[a-zA-Z0-9.-]+$/', $domain) ||
strlen($domain) >= 255) {
return false;
}
// Optional: Check if domain resolves (can be enabled for stricter validation)
// return checkdnsrr($domain, 'A') || checkdnsrr($domain, 'AAAA') || checkdnsrr($domain, 'MX');
return true;
}
/**
* Get the validation error message.
*
* @return string
*/
public function message()
{
return 'The :attribute must be a valid WebFinger address (username@domain.tld or @username@domain.tld)';
}
}

Wyświetl plik

@ -11,10 +11,26 @@ class WebfingerService
{
public static function rawGet($url)
{
if (empty($url)) {
return false;
}
$n = WebfingerUrl::get($url);
if (! $n) {
return false;
}
if (empty($n) || ! str_starts_with($n, 'https://')) {
return false;
}
$host = parse_url($n, PHP_URL_HOST);
if (! $host) {
return false;
}
if (in_array($host, InstanceService::getBannedDomains())) {
return false;
}
$webfinger = FetchCacheService::getJson($n);
if (! $webfinger) {
return false;

Wyświetl plik

@ -190,4 +190,78 @@ return [
'allow_new_account_dms' => env('INSTANCE_ALLOW_NEW_DMS', true),
'total_count_estimate' => env('INSTANCE_TOTAL_POSTS_COUNT_ESTIMATE', false),
'custom_filters' => [
/*
* The maximum number of characters from a status that will be scanned
* for filter matching. Scanning too many characters can hurt performance,
* so this limit ensures that only the most relevant portion of a status is processed.
*
* For remote statuses, you might want to increase this value if you expect
* important content to appear later in long posts.
*/
'max_content_scan_limit' => env('PF_CF_CONTENT_SCAN_LIMIT', 2500),
/*
* The maximum number of filters a single user can create.
* Limiting the number of filters per user helps prevent abuse and
* ensures that the filtering system remains performant.
*/
'max_filters_per_user' => env('PF_CF_MAX_FILTERS_PER_USER', 20),
/*
* The maximum number of keywords that can be associated with a single filter.
* This limit helps control the complexity of the generated regular expressions
* and protects against potential performance issues during content scanning.
*/
'max_keywords_per_filter' => env('PF_CF_MAX_KEYWORDS_PER_FILTER', 10),
/*
* The maximum length allowed for each keyword in a filter.
* Limiting keyword length not only curtails the size of the regex patterns created,
* but also guards against potential abuse where excessively long keywords might
* negatively impact matching performance or lead to unintended behavior.
*/
'max_keyword_length' => env('PF_CF_MAX_KEYWORD_LENGTH', 40),
/*
* The maximum allowed length for the combined regex pattern.
* When constructing a regex that matches multiple filter keywords, each keyword
* (after escaping and adding boundaries) contributes to the total pattern length.
*
* This value is set to 10000 by default. If you increase either the number of keywords
* per filter or the maximum length allowed for each keyword, consider increasing this
* limit accordingly so that the final regex pattern can accommodate the additional length
* without being truncated or causing performance issues.
*/
'max_pattern_length' => env('PF_CF_MAX_PATTERN_LENGTH', 10000),
/*
* The maximum number of keyword matches to report for a given status.
* When a filter is applied to a status, the matching process may find multiple occurrences
* of a keyword. This value limits the number of matches that are reported back,
* which helps manage output volume and processing overhead.
*
* The default is set to 10, but you can adjust this value through your environment configuration.
*/
'max_reported_matches' => env('PF_CF_MAX_REPORTED_MATCHES', 10),
/*
* The maximum number of filter creation operations allowed per hour for a non-admin user.
* This rate limit prevents abuse by restricting how many filters a normal user can create
* within one hour. Admin users are exempt from this limit.
*
* Default is 20 creations per hour.
*/
'max_create_per_hour' => env('PF_CF_MAX_CREATE_PER_HOUR', 20),
/*
* The maximum number of filter update operations allowed per hour for a non-admin user.
* This rate limit is designed to prevent abuse by limiting how many times a normal user
* can update their filters within one hour. Admin users are not subject to these limits.
*
* Default is 40 updates per hour.
*/
'max_updates_per_hour' => env('PF_CF_MAX_UPDATES_PER_HOUR', 40),
],
];

Wyświetl plik

@ -0,0 +1,32 @@
<?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('custom_filters', function (Blueprint $table) {
$table->id();
$table->foreignId('profile_id')->constrained()->onDelete('cascade');
$table->text('phrase')->default('')->nullable(false);
$table->integer('action')->default(0)->nullable(false); // 0=warn, 1=hide, 2=blur
$table->json('context')->nullable(true);
$table->timestamp('expires_at')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('custom_filters');
}
};

Wyświetl plik

@ -0,0 +1,30 @@
<?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('custom_filter_keywords', function (Blueprint $table) {
$table->id();
$table->foreignId('custom_filter_id')->constrained()->onDelete('cascade');
$table->string('keyword', 255)->nullable(false);
$table->boolean('whole_word')->default(true);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('custom_filter_keywords');
}
};

Wyświetl plik

@ -0,0 +1,29 @@
<?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('custom_filter_statuses', function (Blueprint $table) {
$table->id();
$table->foreignId('custom_filter_id')->constrained()->onDelete('cascade');
$table->foreignId('status_id')->constrained()->onDelete('cascade');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('custom_filter_statuses');
}
};

455
package-lock.json wygenerowano
Wyświetl plik

@ -144,12 +144,12 @@
}
},
"node_modules/@babel/generator": {
"version": "7.26.10",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.10.tgz",
"integrity": "sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==",
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz",
"integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==",
"dependencies": {
"@babel/parser": "^7.26.10",
"@babel/types": "^7.26.10",
"@babel/parser": "^7.27.0",
"@babel/types": "^7.27.0",
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25",
"jsesc": "^3.0.2"
@ -170,11 +170,11 @@
}
},
"node_modules/@babel/helper-compilation-targets": {
"version": "7.26.5",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz",
"integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==",
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.0.tgz",
"integrity": "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA==",
"dependencies": {
"@babel/compat-data": "^7.26.5",
"@babel/compat-data": "^7.26.8",
"@babel/helper-validator-option": "^7.25.9",
"browserslist": "^4.24.0",
"lru-cache": "^5.1.1",
@ -193,16 +193,16 @@
}
},
"node_modules/@babel/helper-create-class-features-plugin": {
"version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.26.9.tgz",
"integrity": "sha512-ubbUqCofvxPRurw5L8WTsCLSkQiVpov4Qx0WMA+jUN+nXBK8ADPlJO1grkFw5CWKC5+sZSOfuGMdX1aI1iT9Sg==",
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.0.tgz",
"integrity": "sha512-vSGCvMecvFCd/BdpGlhpXYNhhC4ccxyvQWpbGL4CWbvfEoLFWUZuSuf7s9Aw70flgQF+6vptvgK2IfOnKlRmBg==",
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.25.9",
"@babel/helper-member-expression-to-functions": "^7.25.9",
"@babel/helper-optimise-call-expression": "^7.25.9",
"@babel/helper-replace-supers": "^7.26.5",
"@babel/helper-skip-transparent-expression-wrappers": "^7.25.9",
"@babel/traverse": "^7.26.9",
"@babel/traverse": "^7.27.0",
"semver": "^6.3.1"
},
"engines": {
@ -221,9 +221,9 @@
}
},
"node_modules/@babel/helper-create-regexp-features-plugin": {
"version": "7.26.3",
"resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.26.3.tgz",
"integrity": "sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong==",
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.0.tgz",
"integrity": "sha512-fO8l08T76v48BhpNRW/nQ0MxfnSdoSKUJBMjubOAYffsVuGG5qOfMq7N6Es7UJvi7Y8goXXo07EfcHZXDPuELQ==",
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.25.9",
"regexpu-core": "^6.2.0",
@ -245,9 +245,9 @@
}
},
"node_modules/@babel/helper-define-polyfill-provider": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.3.tgz",
"integrity": "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==",
"version": "0.6.4",
"resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.4.tgz",
"integrity": "sha512-jljfR1rGnXXNWnmQg2K3+bvhkxB51Rl32QRaOTuwwjviGrHzIbSc8+x9CpraDtbT7mfyjXObULP4w/adunNwAw==",
"dependencies": {
"@babel/helper-compilation-targets": "^7.22.6",
"@babel/helper-plugin-utils": "^7.22.5",
@ -400,23 +400,23 @@
}
},
"node_modules/@babel/helpers": {
"version": "7.26.10",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz",
"integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==",
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz",
"integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==",
"dependencies": {
"@babel/template": "^7.26.9",
"@babel/types": "^7.26.10"
"@babel/template": "^7.27.0",
"@babel/types": "^7.27.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.26.10",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz",
"integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==",
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz",
"integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==",
"dependencies": {
"@babel/types": "^7.26.10"
"@babel/types": "^7.27.0"
},
"bin": {
"parser": "bin/babel-parser.js"
@ -655,11 +655,11 @@
}
},
"node_modules/@babel/plugin-transform-block-scoping": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz",
"integrity": "sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==",
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.0.tgz",
"integrity": "sha512-u1jGphZ8uDI2Pj/HJj6YQ6XQLZCNjOlprjxB5SVz6rq2T6SwAR+CdrWK0CP7F+9rDVMXdB0+r6Am5G5aobOjAQ==",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
"@babel/helper-plugin-utils": "^7.26.5"
},
"engines": {
"node": ">=6.9.0"
@ -1158,11 +1158,11 @@
}
},
"node_modules/@babel/plugin-transform-regenerator": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz",
"integrity": "sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==",
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.0.tgz",
"integrity": "sha512-LX/vCajUJQDqE7Aum/ELUMZAY19+cDpghxrnyt5I1tV6X5PyC86AOoWXWFYFeIvauyeSA6/ktn4tQVn/3ZifsA==",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9",
"@babel/helper-plugin-utils": "^7.26.5",
"regenerator-transform": "^0.15.2"
},
"engines": {
@ -1286,9 +1286,9 @@
}
},
"node_modules/@babel/plugin-transform-typeof-symbol": {
"version": "7.26.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.26.7.tgz",
"integrity": "sha512-jfoTXXZTgGg36BmhqT3cAYK5qkmqvJpvNrPhaK/52Vgjhw4Rq29s9UqpWWV0D6yuRmgiFH/BUVlkl96zJWqnaw==",
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.0.tgz",
"integrity": "sha512-+LLkxA9rKJpNoGsbLnAgOCdESl73vwYn+V6b+5wHbrE7OGKVDPHIQvbFSzqE6rwqaCw2RE+zdJrlLkcf8YOA0w==",
"dependencies": {
"@babel/helper-plugin-utils": "^7.26.5"
},
@ -1462,9 +1462,9 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.26.10",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz",
"integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==",
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
"integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
@ -1473,28 +1473,28 @@
}
},
"node_modules/@babel/template": {
"version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz",
"integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==",
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz",
"integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==",
"dependencies": {
"@babel/code-frame": "^7.26.2",
"@babel/parser": "^7.26.9",
"@babel/types": "^7.26.9"
"@babel/parser": "^7.27.0",
"@babel/types": "^7.27.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
"version": "7.26.10",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.10.tgz",
"integrity": "sha512-k8NuDrxr0WrPH5Aupqb2LCVURP/S0vBEn5mK6iH+GIYob66U5EtoZvcdudR2jQ4cmTwhEwW1DLB+Yyas9zjF6A==",
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz",
"integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==",
"dependencies": {
"@babel/code-frame": "^7.26.2",
"@babel/generator": "^7.26.10",
"@babel/parser": "^7.26.10",
"@babel/template": "^7.26.9",
"@babel/types": "^7.26.10",
"@babel/generator": "^7.27.0",
"@babel/parser": "^7.27.0",
"@babel/template": "^7.27.0",
"@babel/types": "^7.27.0",
"debug": "^4.3.1",
"globals": "^11.1.0"
},
@ -1503,9 +1503,9 @@
}
},
"node_modules/@babel/types": {
"version": "7.26.10",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz",
"integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==",
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz",
"integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==",
"dependencies": {
"@babel/helper-string-parser": "^7.25.9",
"@babel/helper-validator-identifier": "^7.25.9"
@ -1532,9 +1532,9 @@
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz",
"integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz",
"integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==",
"cpu": [
"ppc64"
],
@ -1548,9 +1548,9 @@
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.1.tgz",
"integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz",
"integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==",
"cpu": [
"arm"
],
@ -1564,9 +1564,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.1.tgz",
"integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz",
"integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==",
"cpu": [
"arm64"
],
@ -1580,9 +1580,9 @@
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.1.tgz",
"integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz",
"integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==",
"cpu": [
"x64"
],
@ -1596,9 +1596,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.1.tgz",
"integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz",
"integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==",
"cpu": [
"arm64"
],
@ -1612,9 +1612,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.1.tgz",
"integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz",
"integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==",
"cpu": [
"x64"
],
@ -1628,9 +1628,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.1.tgz",
"integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz",
"integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==",
"cpu": [
"arm64"
],
@ -1644,9 +1644,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.1.tgz",
"integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz",
"integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==",
"cpu": [
"x64"
],
@ -1660,9 +1660,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.1.tgz",
"integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz",
"integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==",
"cpu": [
"arm"
],
@ -1676,9 +1676,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.1.tgz",
"integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz",
"integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==",
"cpu": [
"arm64"
],
@ -1692,9 +1692,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.1.tgz",
"integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz",
"integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==",
"cpu": [
"ia32"
],
@ -1708,9 +1708,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.1.tgz",
"integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz",
"integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==",
"cpu": [
"loong64"
],
@ -1724,9 +1724,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.1.tgz",
"integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz",
"integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==",
"cpu": [
"mips64el"
],
@ -1740,9 +1740,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.1.tgz",
"integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz",
"integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==",
"cpu": [
"ppc64"
],
@ -1756,9 +1756,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.1.tgz",
"integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz",
"integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==",
"cpu": [
"riscv64"
],
@ -1772,9 +1772,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.1.tgz",
"integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz",
"integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==",
"cpu": [
"s390x"
],
@ -1788,9 +1788,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz",
"integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz",
"integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==",
"cpu": [
"x64"
],
@ -1804,9 +1804,9 @@
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.1.tgz",
"integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz",
"integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==",
"cpu": [
"arm64"
],
@ -1820,9 +1820,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.1.tgz",
"integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz",
"integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==",
"cpu": [
"x64"
],
@ -1836,9 +1836,9 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.1.tgz",
"integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz",
"integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==",
"cpu": [
"arm64"
],
@ -1852,9 +1852,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.1.tgz",
"integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz",
"integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==",
"cpu": [
"x64"
],
@ -1868,9 +1868,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.1.tgz",
"integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz",
"integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==",
"cpu": [
"x64"
],
@ -1884,9 +1884,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.1.tgz",
"integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz",
"integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==",
"cpu": [
"arm64"
],
@ -1900,9 +1900,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.1.tgz",
"integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz",
"integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==",
"cpu": [
"ia32"
],
@ -1916,9 +1916,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz",
"integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz",
"integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==",
"cpu": [
"x64"
],
@ -3198,9 +3198,9 @@
}
},
"node_modules/@types/babel__generator": {
"version": "7.6.8",
"resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz",
"integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==",
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
"integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
"dependencies": {
"@babel/types": "^7.0.0"
}
@ -3215,9 +3215,9 @@
}
},
"node_modules/@types/babel__traverse": {
"version": "7.20.6",
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz",
"integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==",
"version": "7.20.7",
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz",
"integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==",
"dependencies": {
"@babel/types": "^7.20.7"
}
@ -3284,9 +3284,9 @@
}
},
"node_modules/@types/estree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="
},
"node_modules/@types/express": {
"version": "4.17.21",
@ -3399,11 +3399,11 @@
"integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA=="
},
"node_modules/@types/node": {
"version": "22.13.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz",
"integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==",
"version": "22.14.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz",
"integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==",
"dependencies": {
"undici-types": "~6.20.0"
"undici-types": "~6.21.0"
}
},
"node_modules/@types/node-forge": {
@ -3475,9 +3475,9 @@
"integrity": "sha512-AZU7vQcy/4WFEuwnwsNsJnFwupIpbllH1++LXScN6uxT1Z4zPzdrWG97w4/I7eFKFTvfy/bHFStWjdBAg2Vjug=="
},
"node_modules/@types/ws": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.0.tgz",
"integrity": "sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw==",
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"dependencies": {
"@types/node": "*"
}
@ -3747,9 +3747,9 @@
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="
},
"node_modules/@zip.js/zip.js": {
"version": "2.7.57",
"resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.57.tgz",
"integrity": "sha512-BtonQ1/jDnGiMed6OkV6rZYW78gLmLswkHOzyMrMb+CAR7CZO8phOHO6c2qw6qb1g1betN7kwEHhhZk30dv+NA==",
"version": "2.7.60",
"resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.60.tgz",
"integrity": "sha512-vA3rLyqdxBrVo1FWSsbyoecaqWTV+vgPRf0QKeM7kVDG0r+lHUqd7zQDv1TO9k4BcAoNzNDSNrrel24Mk6addA==",
"engines": {
"bun": ">=0.7.0",
"deno": ">=1.0.0",
@ -4034,9 +4034,9 @@
}
},
"node_modules/axios": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz",
"integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==",
"version": "1.8.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz",
"integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==",
"dev": true,
"dependencies": {
"follow-redirects": "^1.15.6",
@ -4073,12 +4073,12 @@
}
},
"node_modules/babel-plugin-polyfill-corejs2": {
"version": "0.4.12",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.12.tgz",
"integrity": "sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og==",
"version": "0.4.13",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.13.tgz",
"integrity": "sha512-3sX/eOms8kd3q2KZ6DAhKPc0dgm525Gqq5NtWKZ7QYYZEv57OQ54KtblzJzH1lQF/eQxO8KjWGIK9IPUJNus5g==",
"dependencies": {
"@babel/compat-data": "^7.22.6",
"@babel/helper-define-polyfill-provider": "^0.6.3",
"@babel/helper-define-polyfill-provider": "^0.6.4",
"semver": "^6.3.1"
},
"peerDependencies": {
@ -4106,11 +4106,11 @@
}
},
"node_modules/babel-plugin-polyfill-regenerator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.3.tgz",
"integrity": "sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==",
"version": "0.6.4",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.4.tgz",
"integrity": "sha512-7gD3pRadPrbjhjLyxebmx/WrFYcuSjZ0XbdUujQMZ/fcE9oeewk2U/7PCvez84UeuK3oSjmPZ0Ch0dlupQvGzw==",
"dependencies": {
"@babel/helper-define-polyfill-provider": "^0.6.3"
"@babel/helper-define-polyfill-provider": "^0.6.4"
},
"peerDependencies": {
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
@ -4673,9 +4673,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001703",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001703.tgz",
"integrity": "sha512-kRlAGTRWgPsOj7oARC9m1okJEXdL/8fekFVcxA8Hl7GH4r/sN4OJn/i6Flde373T50KS7Y37oFbMwlE8+F42kQ==",
"version": "1.0.30001713",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001713.tgz",
"integrity": "sha512-wCIWIg+A4Xr7NfhTuHdX+/FKh3+Op3LBbSp2N5Pfx6T/LhdQy3GTyoTg48BReaW/MyMNZAkTadsBtai3ldWK0Q==",
"funding": [
{
"type": "opencollective",
@ -5836,9 +5836,9 @@
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
},
"node_modules/electron-to-chromium": {
"version": "1.5.114",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.114.tgz",
"integrity": "sha512-DFptFef3iktoKlFQK/afbo274/XNWD00Am0xa7M8FZUepHlHT8PEuiNBoRfFHbH1okqN58AlhbJ4QTkcnXorjA=="
"version": "1.5.136",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.136.tgz",
"integrity": "sha512-kL4+wUTD7RSA5FHx5YwWtjDnEEkIIikFgWHR4P6fqjw1PPLlqYkxeOb++wAauAssat0YClCy8Y3C5SxgSkjibQ=="
},
"node_modules/elliptic": {
"version": "6.6.1",
@ -5985,9 +5985,9 @@
"integrity": "sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw=="
},
"node_modules/esbuild": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz",
"integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz",
"integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==",
"dev": true,
"hasInstallScript": true,
"bin": {
@ -5997,31 +5997,31 @@
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.1",
"@esbuild/android-arm": "0.25.1",
"@esbuild/android-arm64": "0.25.1",
"@esbuild/android-x64": "0.25.1",
"@esbuild/darwin-arm64": "0.25.1",
"@esbuild/darwin-x64": "0.25.1",
"@esbuild/freebsd-arm64": "0.25.1",
"@esbuild/freebsd-x64": "0.25.1",
"@esbuild/linux-arm": "0.25.1",
"@esbuild/linux-arm64": "0.25.1",
"@esbuild/linux-ia32": "0.25.1",
"@esbuild/linux-loong64": "0.25.1",
"@esbuild/linux-mips64el": "0.25.1",
"@esbuild/linux-ppc64": "0.25.1",
"@esbuild/linux-riscv64": "0.25.1",
"@esbuild/linux-s390x": "0.25.1",
"@esbuild/linux-x64": "0.25.1",
"@esbuild/netbsd-arm64": "0.25.1",
"@esbuild/netbsd-x64": "0.25.1",
"@esbuild/openbsd-arm64": "0.25.1",
"@esbuild/openbsd-x64": "0.25.1",
"@esbuild/sunos-x64": "0.25.1",
"@esbuild/win32-arm64": "0.25.1",
"@esbuild/win32-ia32": "0.25.1",
"@esbuild/win32-x64": "0.25.1"
"@esbuild/aix-ppc64": "0.25.2",
"@esbuild/android-arm": "0.25.2",
"@esbuild/android-arm64": "0.25.2",
"@esbuild/android-x64": "0.25.2",
"@esbuild/darwin-arm64": "0.25.2",
"@esbuild/darwin-x64": "0.25.2",
"@esbuild/freebsd-arm64": "0.25.2",
"@esbuild/freebsd-x64": "0.25.2",
"@esbuild/linux-arm": "0.25.2",
"@esbuild/linux-arm64": "0.25.2",
"@esbuild/linux-ia32": "0.25.2",
"@esbuild/linux-loong64": "0.25.2",
"@esbuild/linux-mips64el": "0.25.2",
"@esbuild/linux-ppc64": "0.25.2",
"@esbuild/linux-riscv64": "0.25.2",
"@esbuild/linux-s390x": "0.25.2",
"@esbuild/linux-x64": "0.25.2",
"@esbuild/netbsd-arm64": "0.25.2",
"@esbuild/netbsd-x64": "0.25.2",
"@esbuild/openbsd-arm64": "0.25.2",
"@esbuild/openbsd-x64": "0.25.2",
"@esbuild/sunos-x64": "0.25.2",
"@esbuild/win32-arm64": "0.25.2",
"@esbuild/win32-ia32": "0.25.2",
"@esbuild/win32-x64": "0.25.2"
}
},
"node_modules/escalade": {
@ -6909,9 +6909,9 @@
}
},
"node_modules/hls.js": {
"version": "1.5.20",
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.5.20.tgz",
"integrity": "sha512-uu0VXUK52JhihhnN/MVVo1lvqNNuhoxkonqgO3IpjvQiGpJBdIXMGkofjQb/j9zvV7a1SW8U9g1FslWx/1HOiQ=="
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.2.tgz",
"integrity": "sha512-rx+pETSCJEDThm/JCm8CuadcAC410cVjb1XVXFNDKFuylaayHk1+tFxhkjvnMDAfqsJHxZXDAJ3Uc2d5xQyWlQ=="
},
"node_modules/hmac-drbg": {
"version": "1.0.1",
@ -6940,9 +6940,9 @@
}
},
"node_modules/html-entities": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz",
"integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==",
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz",
"integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==",
"funding": [
{
"type": "github",
@ -7084,9 +7084,9 @@
}
},
"node_modules/http-parser-js": {
"version": "0.5.9",
"resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.9.tgz",
"integrity": "sha512-n1XsPy3rXVxlqxVioEWdC+0+M+SQw0DpJynwtOPo1X+ZlvdzTLtDBIJJlDQTnwZIFJrZSzSGmIOUdP8tu+SgLw=="
"version": "0.5.10",
"resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz",
"integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA=="
},
"node_modules/http-proxy": {
"version": "1.18.1",
@ -7102,9 +7102,9 @@
}
},
"node_modules/http-proxy-middleware": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz",
"integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==",
"version": "2.0.9",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz",
"integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==",
"dependencies": {
"@types/http-proxy": "^1.17.8",
"http-proxy": "^1.18.1",
@ -7242,9 +7242,9 @@
}
},
"node_modules/immutable": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz",
"integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==",
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.1.tgz",
"integrity": "sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==",
"dev": true
},
"node_modules/import-fresh": {
@ -8198,9 +8198,9 @@
}
},
"node_modules/nanoid": {
"version": "3.3.9",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.9.tgz",
"integrity": "sha512-SppoicMGpZvbF1l3z4x7No3OlIjP7QJvC9XR7AhZr1kL133KHnKPztkKDc+Ir4aJ/1VhTySrtKhrsycmrMQfvg==",
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"funding": [
{
"type": "github",
@ -10028,9 +10028,9 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/sass": {
"version": "1.85.1",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.85.1.tgz",
"integrity": "sha512-Uk8WpxM5v+0cMR0XjX9KfRIacmSG86RH4DCCZjLU2rFh5tyutt9siAXJ7G+YfxQ99Q6wrRMbMlVl6KqUms71ag==",
"version": "1.86.3",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.86.3.tgz",
"integrity": "sha512-iGtg8kus4GrsGLRDLRBRHY9dNVA78ZaS7xr01cWnS7PEMQyFtTqBiyCrfpTYTZXRWM94akzckYjh8oADfFNTzw==",
"dev": true,
"dependencies": {
"chokidar": "^4.0.0",
@ -10689,9 +10689,9 @@
}
},
"node_modules/std-env": {
"version": "3.8.1",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.1.tgz",
"integrity": "sha512-vj5lIj3Mwf9D79hBkltk5qmkFI+biIKWS2IBxEyEU3AX1tUf7AoL8nSazCOiiqQsGKIq01SClsKEzweu34uwvA=="
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz",
"integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="
},
"node_modules/stream-browserify": {
"version": "2.0.2",
@ -11171,9 +11171,9 @@
}
},
"node_modules/undici-types": {
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
},
"node_modules/unicode-canonical-property-names-ecmascript": {
"version": "2.0.1",
@ -11422,7 +11422,8 @@
"node_modules/vue-i18n": {
"version": "8.28.2",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-8.28.2.tgz",
"integrity": "sha512-C5GZjs1tYlAqjwymaaCPDjCyGo10ajUphiwA922jKt9n7KPpqR7oM1PCwYzhB/E7+nT3wfdG3oRre5raIT1rKA=="
"integrity": "sha512-C5GZjs1tYlAqjwymaaCPDjCyGo10ajUphiwA922jKt9n7KPpqR7oM1PCwYzhB/E7+nT3wfdG3oRre5raIT1rKA==",
"deprecated": "Vue I18n v8.x has reached EOL and is no longer actively maintained. About maintenance status, see https://vue-i18n.intlify.dev/guide/maintenance.html"
},
"node_modules/vue-infinite-loading": {
"version": "2.4.5",
@ -11657,9 +11658,9 @@
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"node_modules/webpack": {
"version": "5.98.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.98.0.tgz",
"integrity": "sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==",
"version": "5.99.5",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.5.tgz",
"integrity": "sha512-q+vHBa6H9qwBLUlHL4Y7L0L1/LlyBKZtS9FHNCQmtayxjI5RKC9yD8gpvLeqGv5lCQp1Re04yi0MF40pf30Pvg==",
"dependencies": {
"@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.6",

1
public/js/custom_filters.js vendored 100644

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
public/js/groups.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Wyświetl plik

@ -1 +1 @@
(()=>{"use strict";var e,r,o,a={},t={};function n(e){var r=t[e];if(void 0!==r)return r.exports;var o=t[e]={id:e,loaded:!1,exports:{}};return a[e].call(o.exports,o,o.exports,n),o.loaded=!0,o.exports}n.m=a,e=[],n.O=(r,o,a,t)=>{if(!o){var c=1/0;for(f=0;f<e.length;f++){for(var[o,a,t]=e[f],d=!0,s=0;s<o.length;s++)(!1&t||c>=t)&&Object.keys(n.O).every((e=>n.O[e](o[s])))?o.splice(s--,1):(d=!1,t<c&&(c=t));if(d){e.splice(f--,1);var i=a();void 0!==i&&(r=i)}}return r}t=t||0;for(var f=e.length;f>0&&e[f-1][2]>t;f--)e[f]=e[f-1];e[f]=[o,a,t]},n.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return n.d(r,{a:r}),r},n.d=(e,r)=>{for(var o in r)n.o(r,o)&&!n.o(e,o)&&Object.defineProperty(e,o,{enumerable:!0,get:r[o]})},n.f={},n.e=e=>Promise.all(Object.keys(n.f).reduce(((r,o)=>(n.f[o](e,r),r)),[])),n.u=e=>"js/"+{529:"groups-page",1179:"daci.chunk",1240:"discover~myhashtags.chunk",1645:"profile~following.bundle",2156:"dms.chunk",2822:"group.create",2966:"discover~hashtag.bundle",3688:"discover~serverfeed.chunk",4951:"home.chunk",6250:"discover~settings.chunk",6438:"groups-page-media",6535:"discover.chunk",6740:"discover~memories.chunk",6791:"groups-page-members",7206:"groups-page-topics",7342:"groups-post",7399:"dms~message.chunk",7413:"error404.bundle",7521:"discover~findfriends.chunk",7744:"notifications.chunk",8087:"profile.chunk",8119:"i18n.bundle",8257:"groups-page-about",8408:"post.chunk",8977:"profile~followers.bundle",9124:"compose.chunk",9231:"groups-profile",9919:"changelog.bundle"}[e]+"."+{529:"4a77f2a4e0024224",1179:"8cf1cb07ac8a9100",1240:"03a9fc477579fd24",1645:"8ebe39a19638db1b",2156:"13449036a5b769e6",2822:"38102523ebf4cde9",2966:"9e342ac5d1df33af",3688:"4e135dd1c07c17dd",4951:"abfb6c7049f7833d",6250:"295935b63f9c0971",6438:"526b66b27a0bd091",6535:"0ca404627af971f2",6740:"9621c5ecf4482f0a",6791:"c59de89c3b8e3a02",7206:"d279a2438ee20311",7342:"e160e406bdb4a1b0",7399:"f0d6ccb6f2f1cbf7",7413:"f5958c1713b4ab7c",7521:"bf787612b58e5473",7744:"a8193668255b2c9a",8087:"25876d18c9eeb7c6",8119:"85976a3b9d6b922a",8257:"16d96a32748daa93",8408:"192819f7b133173e",8977:"9d2008cfa13a6f17",9124:"80e32f21442c8a91",9231:"58b5bf1af4d0722e",9919:"efd3d17aee17020e"}[e]+".js",n.miniCssF=e=>({2305:"css/portfolio",2540:"css/landing",3364:"css/admin",4370:"css/profile",6952:"css/appdark",8252:"css/app",8759:"css/spa"}[e]+".css"),n.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),n.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),r={},o="pixelfed:",n.l=(e,a,t,c)=>{if(r[e])r[e].push(a);else{var d,s;if(void 0!==t)for(var i=document.getElementsByTagName("script"),f=0;f<i.length;f++){var u=i[f];if(u.getAttribute("src")==e||u.getAttribute("data-webpack")==o+t){d=u;break}}d||(s=!0,(d=document.createElement("script")).charset="utf-8",d.timeout=120,n.nc&&d.setAttribute("nonce",n.nc),d.setAttribute("data-webpack",o+t),d.src=e),r[e]=[a];var l=(o,a)=>{d.onerror=d.onload=null,clearTimeout(p);var t=r[e];if(delete r[e],d.parentNode&&d.parentNode.removeChild(d),t&&t.forEach((e=>e(a))),o)return o(a)},p=setTimeout(l.bind(null,void 0,{type:"timeout",target:d}),12e4);d.onerror=l.bind(null,d.onerror),d.onload=l.bind(null,d.onload),s&&document.head.appendChild(d)}},n.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),n.p="/",(()=>{var e={461:0,6952:0,8252:0,2305:0,3364:0,2540:0,4370:0,8759:0};n.f.j=(r,o)=>{var a=n.o(e,r)?e[r]:void 0;if(0!==a)if(a)o.push(a[2]);else if(/^((69|82)52|2305|2540|3364|4370|461|8759)$/.test(r))e[r]=0;else{var t=new Promise(((o,t)=>a=e[r]=[o,t]));o.push(a[2]=t);var c=n.p+n.u(r),d=new Error;n.l(c,(o=>{if(n.o(e,r)&&(0!==(a=e[r])&&(e[r]=void 0),a)){var t=o&&("load"===o.type?"missing":o.type),c=o&&o.target&&o.target.src;d.message="Loading chunk "+r+" failed.\n("+t+": "+c+")",d.name="ChunkLoadError",d.type=t,d.request=c,a[1](d)}}),"chunk-"+r,r)}},n.O.j=r=>0===e[r];var r=(r,o)=>{var a,t,[c,d,s]=o,i=0;if(c.some((r=>0!==e[r]))){for(a in d)n.o(d,a)&&(n.m[a]=d[a]);if(s)var f=s(n)}for(r&&r(o);i<c.length;i++)t=c[i],n.o(e,t)&&e[t]&&e[t][0](),e[t]=0;return n.O(f)},o=self.webpackChunkpixelfed=self.webpackChunkpixelfed||[];o.forEach(r.bind(null,0)),o.push=r.bind(null,o.push.bind(o))})(),n.nc=void 0})();
(()=>{"use strict";var e,r,a,o={},t={};function n(e){var r=t[e];if(void 0!==r)return r.exports;var a=t[e]={id:e,loaded:!1,exports:{}};return o[e].call(a.exports,a,a.exports,n),a.loaded=!0,a.exports}n.m=o,e=[],n.O=(r,a,o,t)=>{if(!a){var c=1/0;for(f=0;f<e.length;f++){for(var[a,o,t]=e[f],d=!0,s=0;s<a.length;s++)(!1&t||c>=t)&&Object.keys(n.O).every((e=>n.O[e](a[s])))?a.splice(s--,1):(d=!1,t<c&&(c=t));if(d){e.splice(f--,1);var i=o();void 0!==i&&(r=i)}}return r}t=t||0;for(var f=e.length;f>0&&e[f-1][2]>t;f--)e[f]=e[f-1];e[f]=[a,o,t]},n.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return n.d(r,{a:r}),r},n.d=(e,r)=>{for(var a in r)n.o(r,a)&&!n.o(e,a)&&Object.defineProperty(e,a,{enumerable:!0,get:r[a]})},n.f={},n.e=e=>Promise.all(Object.keys(n.f).reduce(((r,a)=>(n.f[a](e,r),r)),[])),n.u=e=>"js/"+{529:"groups-page",1179:"daci.chunk",1240:"discover~myhashtags.chunk",1645:"profile~following.bundle",2156:"dms.chunk",2822:"group.create",2966:"discover~hashtag.bundle",3688:"discover~serverfeed.chunk",4951:"home.chunk",6250:"discover~settings.chunk",6438:"groups-page-media",6535:"discover.chunk",6740:"discover~memories.chunk",6791:"groups-page-members",7206:"groups-page-topics",7342:"groups-post",7399:"dms~message.chunk",7413:"error404.bundle",7521:"discover~findfriends.chunk",7744:"notifications.chunk",8087:"profile.chunk",8119:"i18n.bundle",8257:"groups-page-about",8408:"post.chunk",8977:"profile~followers.bundle",9124:"compose.chunk",9231:"groups-profile",9919:"changelog.bundle"}[e]+"."+{529:"4a77f2a4e0024224",1179:"4eaae509ed4a084c",1240:"57eeb9257cb300fd",1645:"8ebe39a19638db1b",2156:"13449036a5b769e6",2822:"38102523ebf4cde9",2966:"fffb7ab6f02db6fe",3688:"b7e1082a3be6ef4c",4951:"7b3c50ff0f7828a4",6250:"edeee5803151d4eb",6438:"526b66b27a0bd091",6535:"0ca404627af971f2",6740:"8ea5b8e37111f15f",6791:"c59de89c3b8e3a02",7206:"d279a2438ee20311",7342:"e160e406bdb4a1b0",7399:"f0d6ccb6f2f1cbf7",7413:"f5958c1713b4ab7c",7521:"2ccaf3c586ba03fc",7744:"a8193668255b2c9a",8087:"5d560ecb7d4a57ce",8119:"85976a3b9d6b922a",8257:"16d96a32748daa93",8408:"d0c8b400a930b92a",8977:"9d2008cfa13a6f17",9124:"80e32f21442c8a91",9231:"58b5bf1af4d0722e",9919:"efd3d17aee17020e"}[e]+".js",n.miniCssF=e=>({2305:"css/portfolio",2540:"css/landing",3364:"css/admin",4370:"css/profile",6952:"css/appdark",8252:"css/app",8759:"css/spa"}[e]+".css"),n.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),n.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),r={},a="pixelfed:",n.l=(e,o,t,c)=>{if(r[e])r[e].push(o);else{var d,s;if(void 0!==t)for(var i=document.getElementsByTagName("script"),f=0;f<i.length;f++){var u=i[f];if(u.getAttribute("src")==e||u.getAttribute("data-webpack")==a+t){d=u;break}}d||(s=!0,(d=document.createElement("script")).charset="utf-8",d.timeout=120,n.nc&&d.setAttribute("nonce",n.nc),d.setAttribute("data-webpack",a+t),d.src=e),r[e]=[o];var l=(a,o)=>{d.onerror=d.onload=null,clearTimeout(b);var t=r[e];if(delete r[e],d.parentNode&&d.parentNode.removeChild(d),t&&t.forEach((e=>e(o))),a)return a(o)},b=setTimeout(l.bind(null,void 0,{type:"timeout",target:d}),12e4);d.onerror=l.bind(null,d.onerror),d.onload=l.bind(null,d.onload),s&&document.head.appendChild(d)}},n.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),n.p="/",(()=>{var e={461:0,6952:0,8252:0,2305:0,3364:0,2540:0,4370:0,8759:0};n.f.j=(r,a)=>{var o=n.o(e,r)?e[r]:void 0;if(0!==o)if(o)a.push(o[2]);else if(/^((69|82)52|2305|2540|3364|4370|461|8759)$/.test(r))e[r]=0;else{var t=new Promise(((a,t)=>o=e[r]=[a,t]));a.push(o[2]=t);var c=n.p+n.u(r),d=new Error;n.l(c,(a=>{if(n.o(e,r)&&(0!==(o=e[r])&&(e[r]=void 0),o)){var t=a&&("load"===a.type?"missing":a.type),c=a&&a.target&&a.target.src;d.message="Loading chunk "+r+" failed.\n("+t+": "+c+")",d.name="ChunkLoadError",d.type=t,d.request=c,o[1](d)}}),"chunk-"+r,r)}},n.O.j=r=>0===e[r];var r=(r,a)=>{var o,t,[c,d,s]=a,i=0;if(c.some((r=>0!==e[r]))){for(o in d)n.o(d,o)&&(n.m[o]=d[o]);if(s)var f=s(n)}for(r&&r(a);i<c.length;i++)t=c[i],n.o(e,t)&&e[t]&&e[t][0](),e[t]=0;return n.O(f)},a=self.webpackChunkpixelfed=self.webpackChunkpixelfed||[];a.forEach(r.bind(null,0)),a.push=r.bind(null,a.push.bind(a))})(),n.nc=void 0})();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
public/js/spa.js vendored

File diff suppressed because one or more lines are too long

2
public/js/status.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
public/js/vendor.js vendored

File diff suppressed because one or more lines are too long

Wyświetl plik

@ -158,7 +158,7 @@ See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
/*! Axios v1.8.2 Copyright (c) 2025 Matt Zabriskie and contributors */
/*! Axios v1.8.4 Copyright (c) 2025 Matt Zabriskie and contributors */
/*! https://mths.be/punycode v1.4.1 by @mathias */

Wyświetl plik

@ -3,9 +3,9 @@
"/js/activity.js": "/js/activity.js?id=a7e66fc4edffd7ac88264ec77ecc897f",
"/js/components.js": "/js/components.js?id=9b6f094bb7d0e43a737ed1d1756f8653",
"/js/discover.js": "/js/discover.js?id=0a7264152a6fcef7a5b2a2fc5775c86c",
"/js/profile.js": "/js/profile.js?id=5fc76b62a873d88eea70eff4e7b0c068",
"/js/status.js": "/js/status.js?id=07bbfc11596b0043f7032b616793dd64",
"/js/timeline.js": "/js/timeline.js?id=e2aa9a18c2b16cfae47a8b0cfd0765f2",
"/js/profile.js": "/js/profile.js?id=b96eaa039d2457b0be6dc5b94d3376cc",
"/js/status.js": "/js/status.js?id=509d9f7a92fb4cbbe3e404128c31ba42",
"/js/timeline.js": "/js/timeline.js?id=b7987bb7009319115cc59f71747c725a",
"/js/compose.js": "/js/compose.js?id=85045be7b0a762894ea4d1fff00fd809",
"/js/compose-classic.js": "/js/compose-classic.js?id=2c94b7641c2c5be0bcce17b8a5a5d359",
"/js/search.js": "/js/search.js?id=df0c7f946dc70cac3292736a872864af",
@ -17,34 +17,35 @@
"/js/story-compose.js": "/js/story-compose.js?id=a25351b1487264fd49458d47cd8c121f",
"/js/direct.js": "/js/direct.js?id=3e9c970e8ee5cc4e744a262b6b58339a",
"/js/admin.js": "/js/admin.js?id=4f07b4fac37aa56cf7db83f76a20d0d6",
"/js/spa.js": "/js/spa.js?id=f294392686ec7102a84207cd90421448",
"/js/spa.js": "/js/spa.js?id=26f7ff3340fb7b2e6b90e26a7c5d8c85",
"/js/stories.js": "/js/stories.js?id=f5637cea14c47edfa96df7346b724236",
"/js/portfolio.js": "/js/portfolio.js?id=5f64242a8cccdeb9d0642c9216396192",
"/js/account-import.js": "/js/account-import.js?id=910fa8ccd6563f4711fa4214b00e898e",
"/js/admin_invite.js": "/js/admin_invite.js?id=883bf2f76521fc6b31eb1a3d4fb915ff",
"/js/landing.js": "/js/landing.js?id=d3b87b502df845bcb2d70fd57c763959",
"/js/landing.js": "/js/landing.js?id=ff9ad6c41257eed13f3a275aab4e7faf",
"/js/remote_auth.js": "/js/remote_auth.js?id=1a952303a4e5c6651960a3b92b9f5134",
"/js/groups.js": "/js/groups.js?id=31ff019e974862dc0e565fbc3209166f",
"/js/group-status.js": "/js/group-status.js?id=c5a4b95b4b180f70fa10e01760f8c999",
"/js/group-topic-feed.js": "/js/group-topic-feed.js?id=587c552bb4d1a9f329ac5ed4a5827e61",
"/js/manifest.js": "/js/manifest.js?id=562a659e178c10f73b4ca0053f69f7e3",
"/js/home.chunk.abfb6c7049f7833d.js": "/js/home.chunk.abfb6c7049f7833d.js?id=6e040173a4d04d35d239a59569de809f",
"/js/groups.js": "/js/groups.js?id=328b41b473d4890abed8b24d3c482cef",
"/js/group-status.js": "/js/group-status.js?id=6dfd59071c48823ed2ca6665e2784f01",
"/js/group-topic-feed.js": "/js/group-topic-feed.js?id=ad7231d9f18463043725102768672aee",
"/js/custom_filters.js": "/js/custom_filters.js?id=8502df28c2e32bcc1e0b4bd32eb29193",
"/js/manifest.js": "/js/manifest.js?id=f83beefb303abd604a2f44e0ab4f6f3b",
"/js/home.chunk.7b3c50ff0f7828a4.js": "/js/home.chunk.7b3c50ff0f7828a4.js?id=01c02c2bd0ac9550ccb1f806cdccb398",
"/js/compose.chunk.80e32f21442c8a91.js": "/js/compose.chunk.80e32f21442c8a91.js?id=c27c7ab6f212ffbdbf58f532133ef610",
"/js/post.chunk.192819f7b133173e.js": "/js/post.chunk.192819f7b133173e.js?id=1661efd83647801e6aa5d3cebb2c57bd",
"/js/profile.chunk.25876d18c9eeb7c6.js": "/js/profile.chunk.25876d18c9eeb7c6.js?id=c4c0ef1586def4185037ff1d05c4a18e",
"/js/discover~memories.chunk.9621c5ecf4482f0a.js": "/js/discover~memories.chunk.9621c5ecf4482f0a.js?id=55e3a3786066d7e8cecc3a66a7332960",
"/js/discover~myhashtags.chunk.03a9fc477579fd24.js": "/js/discover~myhashtags.chunk.03a9fc477579fd24.js?id=76b01eea69a257b36cb046cedd1cdfba",
"/js/daci.chunk.8cf1cb07ac8a9100.js": "/js/daci.chunk.8cf1cb07ac8a9100.js?id=e6da2e8e435ba11f28cc14050ba2ec4e",
"/js/discover~findfriends.chunk.bf787612b58e5473.js": "/js/discover~findfriends.chunk.bf787612b58e5473.js?id=76eb7c87adb60409a4831ff975e6c3f4",
"/js/discover~serverfeed.chunk.4e135dd1c07c17dd.js": "/js/discover~serverfeed.chunk.4e135dd1c07c17dd.js?id=52ce6ef8ca5b08628df6530abbab8d2a",
"/js/discover~settings.chunk.295935b63f9c0971.js": "/js/discover~settings.chunk.295935b63f9c0971.js?id=b74753937401ff97936daed7af0aa47f",
"/js/post.chunk.d0c8b400a930b92a.js": "/js/post.chunk.d0c8b400a930b92a.js?id=951d91fc992d6830148c399f6c538fe3",
"/js/profile.chunk.5d560ecb7d4a57ce.js": "/js/profile.chunk.5d560ecb7d4a57ce.js?id=bab41ffcc0a7ac25f550abadddaa9d7d",
"/js/discover~memories.chunk.8ea5b8e37111f15f.js": "/js/discover~memories.chunk.8ea5b8e37111f15f.js?id=12d2cb68321989ad4ae58cab7eaa8a1a",
"/js/discover~myhashtags.chunk.57eeb9257cb300fd.js": "/js/discover~myhashtags.chunk.57eeb9257cb300fd.js?id=80310e89a9766bc8c00e0291c425e462",
"/js/daci.chunk.4eaae509ed4a084c.js": "/js/daci.chunk.4eaae509ed4a084c.js?id=3dd9fbed9b40b2565e26aa2865e22796",
"/js/discover~findfriends.chunk.2ccaf3c586ba03fc.js": "/js/discover~findfriends.chunk.2ccaf3c586ba03fc.js?id=f1133bf060c6741275a5c7765882e183",
"/js/discover~serverfeed.chunk.b7e1082a3be6ef4c.js": "/js/discover~serverfeed.chunk.b7e1082a3be6ef4c.js?id=f69e3eadcb0337334e4e71cee612370b",
"/js/discover~settings.chunk.edeee5803151d4eb.js": "/js/discover~settings.chunk.edeee5803151d4eb.js?id=46a0f1965f37a7d539a65ebe76d1310c",
"/js/discover.chunk.0ca404627af971f2.js": "/js/discover.chunk.0ca404627af971f2.js?id=fb662f204f0a3d50ce8e7ee65f5499d1",
"/js/notifications.chunk.a8193668255b2c9a.js": "/js/notifications.chunk.a8193668255b2c9a.js?id=00edadf32d620edca819d5308873a4e7",
"/js/dms.chunk.13449036a5b769e6.js": "/js/dms.chunk.13449036a5b769e6.js?id=e78688a49ad274ca3bc4cc7bc54a20c4",
"/js/dms~message.chunk.f0d6ccb6f2f1cbf7.js": "/js/dms~message.chunk.f0d6ccb6f2f1cbf7.js?id=e130002bd287f084ffca6de9dd758e9d",
"/js/profile~followers.bundle.9d2008cfa13a6f17.js": "/js/profile~followers.bundle.9d2008cfa13a6f17.js?id=6e9c0c2c42d55c4c3db48aacda336e69",
"/js/profile~following.bundle.8ebe39a19638db1b.js": "/js/profile~following.bundle.8ebe39a19638db1b.js?id=239a879240723ec8cef74958f10167e9",
"/js/discover~hashtag.bundle.9e342ac5d1df33af.js": "/js/discover~hashtag.bundle.9e342ac5d1df33af.js?id=79c91ec5bbe1be32948a832771072017",
"/js/discover~hashtag.bundle.fffb7ab6f02db6fe.js": "/js/discover~hashtag.bundle.fffb7ab6f02db6fe.js?id=f646e0ab831c0f468fcdc22767b269c8",
"/js/error404.bundle.f5958c1713b4ab7c.js": "/js/error404.bundle.f5958c1713b4ab7c.js?id=0dc878fd60f73c85280b293b6d6c091a",
"/js/i18n.bundle.85976a3b9d6b922a.js": "/js/i18n.bundle.85976a3b9d6b922a.js?id=62e1a930a6b89be0b6a72613ec578fb4",
"/js/changelog.bundle.efd3d17aee17020e.js": "/js/changelog.bundle.efd3d17aee17020e.js?id=777875be1b3bf4d1520aafc55e71c4c4",
@ -63,5 +64,5 @@
"/css/landing.css": "/css/landing.css?id=b6e73c3924453e3053aff7d192895d8c",
"/css/profile.css": "/css/profile.css?id=ae4f5db9bb1a89db2ae293d6bde812e3",
"/css/spa.css": "/css/spa.css?id=041aed9a146db0e74b2cfe3ad8eaf96c",
"/js/vendor.js": "/js/vendor.js?id=5da80fcc943883e32e4d77cc685cdd19"
"/js/vendor.js": "/js/vendor.js?id=bbff0fd0a355687a37f533561f2dce81"
}

Wyświetl plik

@ -56,7 +56,11 @@
<div v-if="status.sensitive" class="square-content">
<div class="info-overlay-text-label">
<h5 class="text-white m-auto font-weight-bold">
<span>
<span v-if="status.hasOwnProperty('filtered')">
<span class="far fa-eye-slash fa-lg p-2 d-flex-inline"></span>
<span>Filtered</span>
</span>
<span v-else>
<span class="far fa-eye-slash fa-lg p-2 d-flex-inline"></span>
</span>
</h5>
@ -79,7 +83,15 @@
<span v-if="status.pf_type == 'video'" class="float-right mr-3 post-icon"><i class="fas fa-video fa-2x"></i></span>
<span v-if="status.pf_type == 'video:album'" class="float-right mr-3 post-icon"><i class="fas fa-film fa-2x"></i></span>
<div class="info-overlay-text">
<h5 class="text-white m-auto font-weight-bold">
<h5 class="text-white m-auto font-weight-bold d-flex flex-column" style="gap:.5rem;">
<span>
<span class="far fa-heart fa-lg p-2 d-flex-inline"></span>
<span class="d-flex-inline">{{formatCount(status.favourites_count)}}</span>
</span>
<span>
<span class="far fa-retweet fa-lg p-2 d-flex-inline"></span>
<span class="d-flex-inline">{{formatCount(status.reblogs_count)}}</span>
</span>
<span>
<span class="far fa-comment fa-lg p-2 d-flex-inline"></span>
<span class="d-flex-inline">{{formatCount(status.reply_count)}}</span>
@ -197,7 +209,12 @@
})
.then(res => {
if(res.data && res.data.length) {
this.feed = res.data;
this.feed = res.data.map(s => {
if(s.filtered) {
s.sensitive = true;
}
return s;
});
this.maxId = res.data[res.data.length - 1].id;
this.canLoadMore = true;
} else {
@ -233,7 +250,13 @@
})
.then(res => {
if(res.data && res.data.length) {
this.feed.push(...res.data);
const data = res.data.map(s => {
if(s.filtered) {
s.sensitive = true;
}
return s;
});
this.feed.push(...data);
this.maxId = res.data[res.data.length - 1].id;
this.canLoadMore = true;
} else {

Wyświetl plik

@ -10,46 +10,77 @@
@follow="follow"
@unfollow="unfollow" />
<post-content
:profile="profile"
:status="shadowStatus" />
<template v-if="!isFiltered || (isFiltered && filterType === 'blur')">
<post-content
:profile="profile"
:status="shadowStatus"
:is-filtered="isFiltered"
:filters="filters"
/>
<post-reactions
v-if="reactionBar"
:status="shadowStatus"
:profile="profile"
:admin="admin"
v-on:like="like"
v-on:unlike="unlike"
v-on:share="shareStatus"
v-on:unshare="unshareStatus"
v-on:likes-modal="showLikes"
v-on:shares-modal="showShares"
v-on:toggle-comments="showComments"
v-on:bookmark="handleBookmark"
v-on:mod-tools="openModTools" />
<div v-if="showCommentDrawer" class="card-footer rounded-bottom border-0" style="background: rgba(0,0,0,0.02);z-index: 3;">
<comment-drawer
<post-reactions
v-if="reactionBar"
:status="shadowStatus"
:profile="profile"
v-on:handle-report="handleReport"
v-on:counter-change="counterChange"
v-on:show-likes="showCommentLikes"
v-on:follow="follow"
v-on:unfollow="unfollow" />
</div>
:admin="admin"
@like="like"
@unlike="unlike"
@share="shareStatus"
@unshare="unshareStatus"
@likes-modal="showLikes"
@shares-modal="showShares"
@toggle-comments="showComments"
@bookmark="handleBookmark"
@mod-tools="openModTools" />
<div v-if="showCommentDrawer" class="card-footer rounded-bottom border-0" style="background: rgba(0,0,0,0.02);z-index: 3;">
<comment-drawer
:status="shadowStatus"
:profile="profile"
@handle-report="handleReport"
@counter-change="counterChange"
@show-likes="showCommentLikes"
@follow="follow"
@unfollow="unfollow" />
</div>
</template>
<template v-else>
<div class="card shadow-none mt-n2 mx-3 border-0">
<div class="card-body bg-warning-light p-3 ft-std">
<div class="badge badge-warning p-2" style="border-radius: 10px;">
<i class="fas fa-exclamation-triangle mr-1" aria-hidden="true"></i>
<span>Warning</span>
</div>
<p class="card-text mt-3" style="word-break:break-all;">
This post contains the following filtered keyword{{ filteredTerms?.length > 1 ? 's' : ''}}:
<span v-for="(term, idx) in filteredTerms" class="font-weight-bold">{{ term }}{{filteredTerms?.length === (idx + 1) ? '' : ', '}}</span>
</p>
<button class="btn btn-outline-primary font-weight-bold" @click="showHiddenStatus()" style="border-radius: 10px;">
Show Content
</button>
</div>
</div>
</template>
</div>
</div>
</template>
<script type="text/javascript">
import CommentDrawer from './post/CommentDrawer.vue';
import PostHeader from './post/PostHeader.vue';
import PostContent from './post/PostContent.vue';
import PostReactions from './post/PostReactions.vue';
import CommentDrawer from "./post/CommentDrawer.vue";
import PostHeader from "./post/PostHeader.vue";
import PostContent from "./post/PostContent.vue";
import PostReactions from "./post/PostReactions.vue";
export default {
components: {
"comment-drawer": CommentDrawer,
"post-content": PostContent,
"post-header": PostHeader,
"post-reactions": PostReactions
},
props: {
status: {
type: Object
@ -60,8 +91,8 @@
},
reactionBar: {
type: Boolean,
default: true
type: Boolean,
default: true
},
useDropdownMenu: {
@ -70,13 +101,6 @@
}
},
components: {
"comment-drawer": CommentDrawer,
"post-content": PostContent,
"post-header": PostHeader,
"post-reactions": PostReactions
},
data() {
return {
key: 1,
@ -87,23 +111,12 @@
isBookmarking: false,
owner: false,
admin: false,
license: false
}
},
mounted() {
this.license = this.shadowStatus.media_attachments && this.shadowStatus.media_attachments.length ?
this.shadowStatus
.media_attachments
.filter(m => m.hasOwnProperty('license') && m.license && m.license.hasOwnProperty('id'))
.map(m => m.license)[0] : false;
this.admin = window._sharedData.user.is_admin;
this.owner = this.shadowStatus.account.id == window._sharedData.user.id;
if(this.shadowStatus.reply_count && this.autoloadComments && this.shadowStatus.comments_disabled === false) {
setTimeout(() => {
this.showCommentDrawer = true;
}, 1000);
}
license: false,
isFiltered: false,
filterType: undefined,
filters: [],
filteredTerms: []
};
},
computed: {
@ -128,7 +141,7 @@
newReactions: {
get() {
return this.$store.state.newReactions;
},
}
},
isReblog: {
@ -150,35 +163,25 @@
}
},
watch: {
status: {
deep: true,
immediate: true,
handler: function(o, n) {
this.isBookmarking = false;
}
},
},
methods: {
openMenu() {
this.$emit('menu');
this.$emit("menu");
},
like() {
this.$emit('like');
this.$emit("like");
},
unlike() {
this.$emit('unlike');
this.$emit("unlike");
},
showLikes() {
this.$emit('likes-modal');
this.$emit("likes-modal");
},
showShares() {
this.$emit('shares-modal');
this.$emit("shares-modal");
},
showComments() {
@ -195,47 +198,47 @@
navigator.share({
url: this.status.url
})
.then(() => console.log('Share was successful.'))
.catch((error) => console.log('Sharing failed', error));
.then(() => console.log("Share was successful."))
.catch((error) => console.log("Sharing failed", error));
} else {
swal('Not supported', 'Your current device does not support native sharing.', 'error');
swal("Not supported", "Your current device does not support native sharing.", "error");
}
},
counterChange(type) {
this.$emit('counter-change', type);
this.$emit("counter-change", type);
},
showCommentLikes(post) {
this.$emit('comment-likes-modal', post);
this.$emit("comment-likes-modal", post);
},
shareStatus() {
this.$emit('share');
this.$emit("share");
},
unshareStatus() {
this.$emit('unshare');
this.$emit("unshare");
},
handleReport(post) {
this.$emit('handle-report', post);
this.$emit("handle-report", post);
},
follow() {
this.$emit('follow');
this.$emit("follow");
},
unfollow() {
this.$emit('unfollow');
this.$emit("unfollow");
},
handleReblog() {
this.isReblogging = true;
if(this.status.reblogged) {
this.$emit('unshare');
if (this.status.reblogged) {
this.$emit("unshare");
} else {
this.$emit('share');
this.$emit("share");
}
setTimeout(() => {
@ -246,7 +249,7 @@
handleBookmark() {
event.currentTarget.blur();
this.isBookmarking = true;
this.$emit('bookmark');
this.$emit("bookmark");
setTimeout(() => {
this.isBookmarking = false;
@ -254,7 +257,7 @@
},
getStatusAvatar() {
if(window._sharedData.user.id == this.status.account.id) {
if (window._sharedData.user.id == this.status.account.id) {
return window._sharedData.user.avatar;
}
@ -262,10 +265,73 @@
},
openModTools() {
this.$emit('mod-tools');
this.$emit("mod-tools");
},
applyStatusFilters() {
const filterTypes = this.status.filtered.map(f => f.filter.filter_action);
if (filterTypes.includes("warn")) {
this.applyWarnStatusFilter();
return;
}
if (filterTypes.includes("blur")) {
this.applyBlurStatusFilter();
return;
}
},
applyWarnStatusFilter() {
this.isFiltered = true;
this.filterType = "warn";
this.filters = this.status.filtered;
this.filteredTerms = this.status.filtered.map(f => f.keyword_matches).flat(1);
},
applyBlurStatusFilter() {
this.isFiltered = true;
this.filterType = "blur";
this.filters = this.status.filtered;
this.filteredTerms = this.status.filtered.map(f => f.keyword_matches).flat(1);
},
showHiddenStatus() {
this.isFiltered = false;
this.filterType = null;
this.filters = [];
this.filteredTerms = [];
}
},
mounted() {
this.license = this.shadowStatus.media_attachments && this.shadowStatus.media_attachments.length ?
this.shadowStatus
.media_attachments
.filter(m => m.hasOwnProperty("license") && m.license && m.license.hasOwnProperty("id"))
.map(m => m.license)[0] : false;
this.admin = window._sharedData.user.is_admin;
this.owner = this.shadowStatus.account.id == window._sharedData.user.id;
if (this.shadowStatus.reply_count && this.autoloadComments && this.shadowStatus.comments_disabled === false) {
setTimeout(() => {
this.showCommentDrawer = true;
}, 1000);
}
if (this.status.filtered && this.status.filtered.length) {
this.applyStatusFilters();
}
},
watch: {
status: {
deep: true,
immediate: true,
handler: function(o, n) {
this.isBookmarking = false;
}
}
}
}
};
</script>
<style lang="scss">

Wyświetl plik

@ -1,128 +1,129 @@
<template>
<div class="timeline-status-component-content">
<div v-if="status.pf_type === 'poll'" class="postPresenterContainer" style="background: #000;">
</div>
<div class="timeline-status-component-content">
<div v-if="status.pf_type === 'poll'" class="postPresenterContainer" style="background: #000;">
</div>
<div v-else-if="!fixedHeight" class="postPresenterContainer" style="background: #000;">
<div v-if="status.pf_type === 'photo'" class="w-100">
<photo-presenter
:status="status"
v-on:lightbox="toggleLightbox"
v-on:togglecw="status.sensitive = false"/>
</div>
<div v-else-if="!fixedHeight" class="postPresenterContainer" style="background: #000;">
<div v-if="status.pf_type === 'photo'" class="w-100">
<photo-presenter
:status="status"
:is-filtered="isFiltered"
@lightbox="toggleLightbox"
@togglecw="toggleContentWarning" />
</div>
<div v-else-if="status.pf_type === 'video'" class="w-100">
<video-player :status="status" :fixedHeight="fixedHeight" v-on:togglecw="status.sensitive = false" />
</div>
<div v-else-if="status.pf_type === 'video'" class="w-100">
<video-player
:status="statusRender"
:fixed-height="fixedHeight"
@togglecw="toggleContentWarning" />
</div>
<div v-else-if="status.pf_type === 'photo:album'" class="w-100">
<photo-album-presenter :status="status" v-on:lightbox="toggleLightbox" v-on:togglecw="status.sensitive = false"></photo-album-presenter>
</div>
<div v-else-if="status.pf_type === 'photo:album'" class="w-100">
<photo-album-presenter
:status="status"
@lightbox="toggleLightbox"
@togglecw="toggleContentWarning" />
</div>
<div v-else-if="status.pf_type === 'video:album'" class="w-100">
<video-album-presenter :status="status" v-on:togglecw="status.sensitive = false"></video-album-presenter>
</div>
<div v-else-if="status.pf_type === 'video:album'" class="w-100">
<video-album-presenter
:status="status"
@togglecw="toggleContentWarning" />
</div>
<div v-else-if="status.pf_type === 'photo:video:album'" class="w-100">
<mixed-album-presenter :status="status" v-on:lightbox="toggleLightbox" v-on:togglecw="status.sensitive = false"></mixed-album-presenter>
</div>
</div>
<div v-else-if="status.pf_type === 'photo:video:album'" class="w-100">
<mixed-album-presenter
:status="status"
@lightbox="toggleLightbox"
@togglecw="toggleContentWarning" />
</div>
</div>
<div v-else class="card-body p-0">
<div v-if="status.pf_type === 'photo'" :class="{ fixedHeight: fixedHeight }">
<div v-if="status.sensitive == true" class="content-label-wrapper">
<div class="text-light content-label">
<p class="text-center">
<i class="far fa-eye-slash fa-2x"></i>
</p>
<p class="h4 font-weight-bold text-center">
{{ $t('common.sensitiveContent') }}
</p>
<p class="text-center py-2 content-label-text">
{{ status.spoiler_text ? status.spoiler_text : $t('common.sensitiveContentWarning') }}
</p>
<p class="mb-0">
<button class="btn btn-outline-light btn-block btn-sm font-weight-bold" @click="toggleContentWarning()">See Post</button>
</p>
</div>
<div v-else class="card-body p-0">
<div v-if="status.pf_type === 'photo'" :class="{ fixedHeight: fixedHeight }">
<div v-if="statusRender.sensitive == true" class="content-label-wrapper">
<div class="text-light content-label">
<p class="text-center">
<i class="far fa-eye-slash fa-2x"></i>
</p>
<p class="h4 font-weight-bold text-center">
{{ isFiltered ? 'Filtered Content' : $t('common.sensitiveContent') }}
</p>
<p class="text-center py-2 content-label-text">
{{ status.spoiler_text ? status.spoiler_text : $t('common.sensitiveContentWarning') }}
</p>
<p class="mb-0">
<button class="btn btn-outline-light btn-block btn-sm font-weight-bold" @click="toggleContentWarning()">See Post</button>
</p>
</div>
<blur-hash-image
width="32"
height="32"
:punch="1"
class="blurhash-wrapper"
:hash="status.media_attachments[0].blurhash"
/>
</div>
<div
v-else
@click.prevent="toggleLightbox"
class="content-label-wrapper"
style="position: relative;width:100%;height: 400px;overflow: hidden;z-index:1"
>
<blur-hash-image
width="32"
height="32"
:punch="1"
class="blurhash-wrapper"
:hash="status.media_attachments[0].blurhash"
/>
</div>
<div
v-else
class="content-label-wrapper"
@click.prevent="toggleLightbox"
>
<img
<img
:src="status.media_attachments[0].url"
style="position: absolute;width: 105%;height: 410px;object-fit: cover;z-index: 1;top:0;left:0;filter: brightness(0.35) blur(6px);margin:-5px;">
class="content-label-wrapper-img" />
<!-- <blur-hash-canvas
v-if="status.media_attachments[0].blurhash && status.media_attachments[0].blurhash != 'U4Rfzst8?bt7ogayj[j[~pfQ9Goe%Mj[WBay'"
:key="key"
width="32"
height="32"
:punch="1"
:hash="status.media_attachments[0].blurhash"
style="position: absolute;width: 105%;height: 410px;object-fit: cover;z-index: 1;top:0;left:0;filter: brightness(0.35);"
/> -->
<blur-hash-image
:key="key"
width="32"
height="32"
:punch="1"
:hash="status.media_attachments[0].blurhash"
:src="status.media_attachments[0].url"
class="blurhash-wrapper"
<blur-hash-image
:key="key"
width="32"
height="32"
:punch="1"
:hash="status.media_attachments[0].blurhash"
:src="status.media_attachments[0].url"
class="blurhash-wrapper"
:alt="status.media_attachments[0].description"
:title="status.media_attachments[0].description"
style="width: 100%;position: absolute;z-index:9;top:0:left:0"
/>
style="width: 100%;position: absolute;z-index:9;top:0:left:0"
/>
<p v-if="!status.sensitive && sensitive"
@click="status.sensitive = true"
style="
margin-top: 0;
padding: 10px;
color: #000;
font-size: 10px;
text-align: right;
position: absolute;
top: 0;
right: 0;
border-radius: 11px;
cursor: pointer;
background: rgba(255, 255, 255,.5);
">
<i class="fas fa-eye-slash fa-lg"></i>
</p>
</div>
</div>
<p
v-if="!status.sensitive && sensitive"
class="sensitive-curtain"
@click="status.sensitive = true">
<i class="fas fa-eye-slash fa-lg"></i>
</p>
</div>
</div>
<video-player
v-else-if="status.pf_type === 'video'"
:status="status"
:fixedHeight="fixedHeight"
:fixed-height="fixedHeight"
/>
<div v-else-if="status.pf_type === 'photo:album'" class="card-img-top shadow" style="border-radius: 15px;">
<photo-album-presenter :status="status" v-on:lightbox="toggleLightbox" v-on:togglecw="toggleContentWarning()" style="border-radius:15px !important;object-fit: contain;background-color: #000;overflow: hidden;" :class="{ fixedHeight: fixedHeight }"/>
</div>
<div v-else-if="status.pf_type === 'photo:album'" class="card-img-top shadow" style="border-radius: 15px;">
<photo-album-presenter
:status="status"
class="photo-presenter"
:class="{ fixedHeight: fixedHeight }"
@lightbox="toggleLightbox"
@togglecw="toggleContentWarning()" />
</div>
<div v-else-if="status.pf_type === 'photo:video:album'" class="card-img-top shadow" style="border-radius: 15px;">
<mixed-album-presenter :status="status" v-on:lightbox="toggleLightbox" v-on:togglecw="status.sensitive = false" style="border-radius:15px !important;object-fit: contain;background-color: #000;overflow: hidden;align-items:center" :class="{ fixedHeight: fixedHeight }"></mixed-album-presenter>
</div>
<div v-else-if="status.pf_type === 'photo:video:album'" class="card-img-top shadow" style="border-radius: 15px;">
<mixed-album-presenter
:status="status"
class="mixed-presenter"
:class="{ fixedHeight: fixedHeight }"
@lightbox="toggleLightbox"
@togglecw="status.sensitive = false" />
<div v-else-if="status.pf_type === 'text'">
</div>
<div v-else-if="status.pf_type === 'text'">
<div v-if="status.sensitive" class="border m-3 p-5 rounded-lg">
<p class="text-center">
<i class="far fa-eye-slash fa-2x"></i>
@ -135,85 +136,153 @@
</div>
</div>
<div v-else class="bg-light rounded-lg d-flex align-items-center justify-content-center" style="height: 400px;">
<div>
<p class="text-center">
<i class="fas fa-exclamation-triangle fa-4x"></i>
</p>
<div v-else class="bg-light rounded-lg d-flex align-items-center justify-content-center" style="height: 400px;">
<div>
<p class="text-center">
<i class="fas fa-exclamation-triangle fa-4x"></i>
</p>
<p class="lead text-center mb-0">
Cannot display post
</p>
<p class="lead text-center mb-0">
Cannot display post
</p>
<p class="small text-center mb-0">
<!-- <a class="font-weight-bold primary" href="#">Report issue</a> -->
{{status.pf_type}}:{{status.id}}
</p>
</div>
</div>
</div>
<p class="small text-center mb-0">
{{ status.pf_type }}:{{ status.id }}
</p>
</div>
</div>
</div>
<div
v-if="status.content && !status.sensitive"
class="card-body status-text"
:class="[ status.pf_type === 'text' ? 'py-0' : 'pb-0']">
<p>
<read-more :status="status" :cursor-limit="300"/>
</p>
<!-- <p v-html="status.content_text || status.content">
</p> -->
</div>
</div>
<div
v-if="status.content && !status.sensitive"
class="card-body status-text"
:class="[ status.pf_type === 'text' ? 'py-0' : 'pb-0']">
<p>
<read-more :status="status" :cursor-limit="300" />
</p>
</div>
</div>
</template>
<script type="text/javascript">
import BigPicture from 'bigpicture';
import ReadMore from './ReadMore.vue';
import VideoPlayer from '@/presenter/VideoPlayer.vue';
import BigPicture from "bigpicture";
import ReadMore from "./ReadMore.vue";
import VideoPlayer from "@/presenter/VideoPlayer.vue";
export default {
props: ['status'],
export default {
components: {
"read-more": ReadMore,
components: {
"read-more": ReadMore,
"video-player": VideoPlayer
},
},
props: {
data() {
return {
key: 1,
sensitive: false,
};
},
status: {
type: Object
},
isFiltered: {
type: Boolean
},
filters: {
type: Array
}
},
computed: {
fixedHeight: {
get() {
return this.$store.state.fixedHeight == true;
}
}
},
data() {
return {
key: 1,
sensitive: false
};
},
methods: {
toggleLightbox(e) {
BigPicture({
el: e.target
})
},
computed: {
statusRender: {
get() {
if (this.isFiltered) {
this.status.spoiler_text = "Filtered because it contains the following keywords: " + this.status.filtered.map(f => f.keyword_matches).flat(1).join(", ");
this.status.sensitive = true;
}
return this.status;
}
},
fixedHeight: {
get() {
return this.$store.state.fixedHeight == true;
}
}
},
toggleContentWarning() {
this.key++;
this.sensitive = true;
this.status.sensitive = !this.status.sensitive;
},
methods: {
toggleLightbox(e) {
BigPicture({
el: e.target
});
},
toggleContentWarning() {
this.key++;
this.sensitive = true;
this.status.sensitive = !this.status.sensitive;
},
getPoster(status) {
let url = status.media_attachments[0].preview_url;
if(url.endsWith('no-preview.jpg') || url.endsWith('no-preview.png')) {
if (url.endsWith("no-preview.jpg") || url.endsWith("no-preview.png")) {
return;
}
return url;
}
}
}
}
};
</script>
<style scoped>
.sensitive-curtain {
margin-top: 0;
padding: 10px;
color: #000;
font-size: 10px;
text-align: right;
position: absolute;
top: 0;
right: 0;
border-radius: 11px;
cursor: pointer;
background: rgba(255, 255, 255,.5);
}
.content-label-wrapper {
position: relative;
width:100%;
height: 400px;
overflow: hidden;
z-index:1
}
.content-label-wrapper-img {
position: absolute;
width: 105%;height: 410px;
object-fit: cover;
z-index: 1;
top:0;
left:0;
filter: brightness(0.35) blur(6px);
margin:-5px;
}
.photo-presenter {
border-radius:15px !important;
object-fit: contain;
background-color: #000;
overflow: hidden;
}
.mixed-presenter {
border-radius:15px !important;
object-fit: contain;
background-color: #000;
overflow: hidden;
align-items:center;
}
</style>

Wyświetl plik

@ -1,160 +1,167 @@
<template>
<div v-if="status.sensitive == true" class="content-label-wrapper">
<div class="text-light content-label">
<p class="text-center">
<i class="far fa-eye-slash fa-2x"></i>
</p>
<p class="h4 font-weight-bold text-center">
Sensitive Content
</p>
<p class="text-center py-2 content-label-text">
{{ status.spoiler_text ? status.spoiler_text : 'This post may contain sensitive content.'}}
</p>
<p class="mb-0">
<button @click="toggleContentWarning()" class="btn btn-outline-light btn-block btn-sm font-weight-bold">See Post</button>
</p>
</div>
<blur-hash-image
width="32"
height="32"
:punch="1"
:hash="status.media_attachments[0].blurhash"
:alt="altText(status)"/>
</div>
<div v-else>
<div :title="status.media_attachments[0].description" style="position: relative;">
<img class="card-img-top"
:src="status.media_attachments[0].url"
loading="lazy"
:alt="altText(status)"
:width="width()"
:height="height()"
onerror="this.onerror=null;this.src='/storage/no-preview.png'"
@click.prevent="toggleLightbox">
<div v-if="status.sensitive == true" class="content-label-wrapper">
<div class="text-light content-label">
<p class="text-center">
<i class="far fa-eye-slash fa-2x"></i>
</p>
<p class="h4 font-weight-bold text-center">
{{ isFiltered ? 'Filtered Content' : 'Sensitive Content' }}
</p>
<p class="text-center py-2 content-label-text">
{{ status.spoiler_text ? status.spoiler_text : 'This post may contain sensitive content.' }}
</p>
<p class="mb-0">
<button class="btn btn-outline-light btn-block btn-sm font-weight-bold" @click="toggleContentWarning()">See Post</button>
</p>
</div>
<blur-hash-image
width="32"
height="32"
:punch="1"
:hash="status.media_attachments[0].blurhash"
:alt="altText(status)"
/>
</div>
<div v-else>
<div
:title="status.media_attachments[0].description"
style="position: relative;">
<!-- <blur-hash-image
class="card-img-top"
width="32"
height="32"
:punch="1"
:hash="status.media_attachments[0].blurhash"
:src="status.media_attachments[0].url"
:alt="altText(status)"
@click.prevent="toggleLightbox"/> -->
<img
class="card-img-top"
:src="status.media_attachments[0].url"
loading="lazy"
:alt="altText(status)"
:width="width()"
:height="height()"
onerror="this.onerror=null;this.src='/storage/no-preview.png'"
@click.prevent="toggleLightbox"
/>
<p v-if="!status.sensitive && sensitive"
@click="status.sensitive = true"
style="
margin-top: 0;
padding: 10px;
color: #fff;
font-size: 10px;
text-align: right;
position: absolute;
top: 0;
right: 0;
border-top-left-radius: 5px;
cursor: pointer;
background: linear-gradient(0deg, rgba(0,0,0,0.5), rgba(0,0,0,0.5));
">
<i class="fas fa-eye-slash fa-lg"></i>
</p>
<p
v-if="!status.sensitive && sensitive"
class="sensitive-curtain"
@click="status.sensitive = true">
<i class="fas fa-eye-slash fa-lg"></i>
</p>
<p
v-if="status.media_attachments[0].license"
style="
margin-bottom: 0;
padding: 0 5px;
color: #fff;
font-size: 10px;
text-align: right;
position: absolute;
bottom: 0;
right: 0;
border-top-left-radius: 5px;
background: linear-gradient(0deg, rgba(0,0,0,0.5), rgba(0,0,0,0.5));
"><a :href="status.url" class="font-weight-bold text-light">Photo</a> by <a :href="status.account.url" class="font-weight-bold text-light">&commat;{{status.account.username}}</a> licensed under <a :href="status.media_attachments[0].license.url" class="font-weight-bold text-light">{{status.media_attachments[0].license.title}}</a></p>
</div>
</div>
<p
v-if="status.media_attachments[0].license"
class="photo-license">
<a :href="status.url" class="font-weight-bold text-light">Photo</a> by <a :href="status.account.url" class="font-weight-bold text-light">&commat;{{ status.account.username }}</a> licensed under <a :href="status.media_attachments[0].license.url" class="font-weight-bold text-light">{{ status.media_attachments[0].license.title }}</a>
</p>
</div>
</div>
</template>
<script type="text/javascript">
import BigPicture from "bigpicture";
export default {
props: {
status: {
type: Object
},
isFiltered: {
type: Boolean,
default: false
}
},
data() {
return {
sensitive: this.status.sensitive
};
},
methods: {
altText(status) {
let desc = status.media_attachments[0].description;
if (desc) {
return desc;
}
return "Photo was not tagged with any alt text.";
},
toggleContentWarning(status) {
this.$emit("togglecw");
},
toggleLightbox(e) {
BigPicture({
el: e.target
});
},
width() {
if (!this.status.media_attachments[0].meta ||
!this.status.media_attachments[0].meta.original ||
!this.status.media_attachments[0].meta.original.width) {
return;
}
return this.status.media_attachments[0].meta.original.width;
},
height() {
if (!this.status.media_attachments[0].meta ||
!this.status.media_attachments[0].meta.original ||
!this.status.media_attachments[0].meta.original.height) {
return;
}
return this.status.media_attachments[0].meta.original.height;
}
}
};
</script>
<style type="text/css" scoped>
.card-img-top {
.card-img-top {
border-top-left-radius: 0 !important;
border-top-right-radius: 0 !important;
}
.content-label-wrapper {
position: relative;
}
.content-label {
margin: 0;
position: absolute;
top:50%;
left:50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
z-index: 2;
background: rgba(0, 0, 0, 0.2)
}
}
.content-label-wrapper {
position: relative;
}
.content-label {
margin: 0;
position: absolute;
top:50%;
left:50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
z-index: 2;
background: rgba(0, 0, 0, 0.2)
}
.sensitive-curtain {
margin-top: 0;
padding: 10px;
color: #fff;
font-size: 10px;
text-align: right;
position: absolute;
top: 0;
right: 0;
border-top-left-radius: 5px;
cursor: pointer;
background: linear-gradient(0deg, rgba(0,0,0,0.5), rgba(0,0,0,0.5));
}
.photo-license {
margin-bottom: 0;
padding: 0 5px;
color: #fff;
font-size: 10px;
text-align: right;
position: absolute;
bottom: 0;
right: 0;
border-top-left-radius: 5px;
background: linear-gradient(0deg, rgba(0,0,0,0.5), rgba(0,0,0,0.5));
}
</style>
<script type="text/javascript">
import BigPicture from 'bigpicture';
export default {
props: ['status'],
data() {
return {
sensitive: this.status.sensitive
}
},
mounted() {
},
methods: {
altText(status) {
let desc = status.media_attachments[0].description;
if(desc) {
return desc;
}
return 'Photo was not tagged with any alt text.';
},
toggleContentWarning(status) {
this.$emit('togglecw');
},
toggleLightbox(e) {
BigPicture({
el: e.target
})
},
width() {
if( !this.status.media_attachments[0].meta ||
!this.status.media_attachments[0].meta.original ||
!this.status.media_attachments[0].meta.original.width ) {
return;
}
return this.status.media_attachments[0].meta.original.width;
},
height() {
if( !this.status.media_attachments[0].meta ||
!this.status.media_attachments[0].meta.original ||
!this.status.media_attachments[0].meta.original.height ) {
return;
}
return this.status.media_attachments[0].meta.original.height;
}
}
}
</script>

Wyświetl plik

@ -0,0 +1,143 @@
<template>
<div class="list-group-item">
<div class="d-flex justify-content-between align-items-center">
<div class="filter-card-info cursor-pointer" @click="$emit('edit', filter)">
<div class="d-flex align-items-center" style="gap:0.5rem;">
<div class="d-flex align-items-center" style="gap:5px;">
<div class="font-weight-bold">{{ filter.title }}</div>
<div class="small text-muted">({{ filter.keywords?.length ?? 0 }})</div>
</div>
<div class="text-muted">·</div>
<div v-if="filter.expires_at" class="small text-muted">
Expires: {{ formatExpiry(filter.expires_at) }}
</div>
<div v-else class="small text-muted">
Never expires
</div>
</div>
<div>
<div class="text-muted small">{{ formatAction(filter.filter_action) }} on {{ formatContexts() }}</div>
</div>
</div>
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="customSwitch1" v-model="checked" @click="$emit('delete', filter.id)">
<label class="custom-control-label" for="customSwitch1"></label>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'FilterCard',
props: {
filter: {
type: Object,
required: true
}
},
data() {
return {
checked: true,
}
},
computed: {
actionBadgeClass() {
const classes = {
'warn': 'badge-warning',
'hide': 'badge-danger',
'blur': 'badge-light'
};
return classes[this.filter.filter_action] || 'badge-secondary';
}
},
watch: {
checked: {
deep: true,
handler: function(val, old) {
console.log(val, old)
setTimeout(() => {
this.checked = true;
}, 1000);
},
},
},
methods: {
formatContext(context) {
const contexts = {
'home': 'Home feed',
'notifications': 'Notifications',
'public': 'Public feeds',
'thread': 'Conversations',
'tags': 'Hashtags',
'groups': 'Groups'
};
return contexts[context] || context;
},
formatExpiry(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric'
});
},
formatContexts() {
if (!this.filter.context?.length) return '';
const hasHome = this.filter.context.includes('home');
const hasPublic = this.filter.context.includes('public');
if (hasHome && hasPublic) {
const otherContexts = this.filter.context
.filter(c => c !== 'home' && c !== 'public')
.map(c => this.formatContext(c));
return ['Feeds', ...otherContexts].join(', ');
} else {
return this.filter.context.map(c => this.formatContext(c)).join(', ');
}
},
formatAction(action) {
const actions = {
'warn': 'Warning',
'hide': 'Hidden',
'block': 'Blocked'
};
return actions[action] || action.charAt(0).toUpperCase() + action.slice(1);
},
renderActionDescription() {
console.log(this.filter)
if(this.filter.filter_action === 'warn') {
return `<div><i class="fas fa-exclamation-triangle text-warning mr-1"></i> <span class="font-weight-light text-muted">Warn</span></div>`
}
else if(this.filter.filter_action === 'blur') {
return `<div><i class="fas fa-tint mr-1 text-info"></i> <span class="font-weight-light text-muted">Blur</span></div>`
}
else if(this.filter.filter_action === 'hide') {
return `<div><i class="fas fa-eye-slash mr-1 text-danger"></i> <span class="font-weight-light text-muted">Hide</span></div>`
}
}
}
}
</script>
<style scoped>
.filter-card {
overflow: hidden;
border-radius: 20px;
}
.filter-card:hover {
box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.1) !important;
}
.badge-pill {
padding: 0.35em 0.7em;
}
.card-header {
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
</style>

Wyświetl plik

@ -0,0 +1,264 @@
<template>
<div class="pb-4">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-center mb-4">
<div class="title">
<h3 class="font-weight-bold mb-0">
Filters
</h3>
<p class="lead mb-3 mb-md-0">Manage your custom filters.</p>
</div>
<button
@click="showAddFilterModal = true"
class="btn btn-primary font-weight-bold rounded-pill px-3"
:disabled="filters?.length >= 20">
<i class="fas fa-plus mr-1"></i> Add New Filter
</button>
</div>
<!-- <p>Customize your experience with powerful content filters that screen for specific words or phrases throughout your entire accountincluding home and public timelines, notifications, messages, groups, hashtag feeds, and explore sections.</p> -->
<p>Customize your experience with powerful content filters that screen for specific words or phrases throughout your entire account - including home and public timelines, and hashtag feeds.</p>
<p class="text-muted mb-0">You can add up to <strong>20 filters</strong> that can have up to <strong>10 keywords</strong>.</p>
<p class="text-muted mb-4 small">Learn more in our <a href="/site/help">Help Center</a>.</p>
<div v-if="loading" class="d-flex justify-content-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
<div v-else-if="filters.length === 0" class="bg-light p-4 rounded text-center border">
<div class="py-3">
<i class="fas fa-filter text-secondary fa-3x mb-3"></i>
<p class="font-weight-bold text-secondary">You don't have any content filters yet.</p>
<p class="text-muted small mt-2">
Filters help you hide content containing specific words or phrases from your timelines.
</p>
<button @click="showAddFilterModal = true" class="btn btn-outline-primary rounded-pill font-weight-light mt-2">
<i class="fas fa-plus mr-1"></i> Create Your First Filter
</button>
</div>
</div>
<div v-else>
<div class="d-flex justify-content-between align-items-center mb-3">
<p v-if="!searchQuery || !searchQuery.trim().length" class="text-muted mb-0">
<span class="font-weight-bold">{{ filters.length }}</span>
{{ filters.length === 1 ? 'filter' : 'filters' }} found
</p>
<p v-else class="text-muted mb-0">
<span class="font-weight-bold">{{ filteredFilters.length }}</span>
{{ filteredFilters.length === 1 ? 'filter' : 'filters' }} found
</p>
<div class="input-group input-group-sm" style="max-width: 250px;">
<div class="input-group-prepend">
<span class="input-group-text bg-light border-right-0">
<i class="fas fa-search text-muted"></i>
</span>
</div>
<input
type="text"
v-model="searchQuery"
class="form-control border-left-0 bg-light"
placeholder="Search filters..."
/>
</div>
</div>
<div v-if="searchQuery && filteredFilters.length === 0" class="bg-light p-4 rounded text-center border">
<div class="py-3">
<i class="fas fa-filter text-secondary fa-3x mb-3"></i>
<p class="lead text-secondary">You don't have any content filters that match <strong>{{searchQuery}}</strong>.</p>
<p class="text-muted small mt-2">
Filters help you hide content containing specific words or phrases from your timelines.
</p>
<button @click="showAddFilterModal = true" class="btn btn-outline-primary rounded-pill font-weight-light mt-2">
<i class="fas fa-plus mr-1"></i> Create new Filter
</button>
</div>
</div>
<div class="card-deck-wrapper">
<div class="list-group">
<filter-card
v-for="filter in filteredFilters"
:key="filter.id"
:filter="filter"
@edit="editFilter"
@delete="deleteFilter"
/>
</div>
</div>
</div>
<filter-modal
v-if="showAddFilterModal || showEditFilterModal"
:filter="editingFilter"
:is-editing="showEditFilterModal"
:wizard-mode="wizardMode"
@delete="handleFilterDelete"
@toggle="updateWizardMode"
@close="closeModals"
@save="saveFilter"
/>
</div>
</template>
<script>
import FilterCard from './FilterCard.vue';
import FilterModal from './FilterModal.vue';
export default {
name: 'FiltersList',
components: {
FilterCard,
FilterModal
},
data() {
return {
filters: [],
loading: true,
filtersLoaded: false,
showAddFilterModal: false,
showEditFilterModal: false,
editingFilter: null,
searchQuery: '',
wizardMode: true,
}
},
computed: {
filteredFilters() {
if (!this.searchQuery) return this.filters;
const query = this.searchQuery.toLowerCase().trim();
return this.filters.filter(filter => {
if (filter.title && filter.title.toLowerCase().includes(query)) return true;
if (filter.keywords && filter.keywords.some(k =>
k.keyword && k.keyword.toLowerCase().includes(query)
)) return true;
if (filter.context && filter.context.some(c => c.toLowerCase().includes(query))) return true;
return false;
});
}
},
mounted() {
this.fetchFilters();
},
methods: {
fetchFilters() {
this.loading = true;
axios.get('/api/v2/filters')
.then(response => {
this.filters = response.data;
})
.catch(error => {
console.error('Failed to fetch filters:', error);
swal('Error', 'Failed to load filters. Please try again.', 'error');
})
.finally(() => {
this.loading = false;
this.filtersLoaded = true;
});
},
closeModals() {
this.wizardMode = true;
this.showAddFilterModal = false;
this.showEditFilterModal = false;
this.editingFilter = null;
},
handleFilterDelete() {
this.deleteFilter(this.editingFilter.id);
this.closeModals();
},
updateWizardMode() {
this.wizardMode = !this.wizardMode;
},
editFilter(filter) {
this.wizardMode = false;
this.editingFilter = JSON.parse(JSON.stringify(filter));
this.showEditFilterModal = true;
},
deleteFilter(filterId) {
if (!confirm('Are you sure you want to delete this filter?')) return;
this.loading = true;
axios.delete(`/api/v2/filters/${filterId}`)
.then(() => {
this.filters = this.filters.filter(f => f.id !== filterId);
swal('Success', 'Filter deleted successfully', 'success');
})
.catch(error => {
swal('Error', 'Failed to delete filter. Please try again.', 'error')
})
.finally(() => {
this.loading = false;
});
},
saveFilter(filterData) {
this.loading = true;
if (this.showEditFilterModal) {
axios.put(`/api/v2/filters/${filterData.id}`, filterData)
.then(response => {
const updatedIndex = this.filters.findIndex(f => f.id === filterData.id);
if (updatedIndex !== -1) {
this.$set(this.filters, updatedIndex, response.data);
}
this.$bvToast.toast(`${response.data?.title ?? 'Untitled'} filter updated successfully`, {
title: 'Updated Filter',
autoHideDelay: 5000,
appendToast: true,
variant: 'success'
})
this.closeModals();
})
.catch(error => {
if(error.response?.data?.error) {
swal(error.response?.data?.error, error.response?.data?.message, 'error')
} else if(error.response?.data?.message) {
swal('Error', error.response?.data?.message, 'error')
} else {
swal('Error', 'Failed to update filter. Please try again.', 'error')
}
})
.finally(() => {
this.loading = false;
});
} else {
axios.post('/api/v2/filters', filterData)
.then(response => {
this.filters.unshift(response.data);
this.$bvToast.toast(`${response.data?.title ?? 'Untitled'} filter created`, {
title: 'New Filter',
autoHideDelay: 5000,
appendToast: true,
variant: 'success'
})
this.closeModals();
})
.catch(error => {
if(error.response?.data?.error) {
swal(error.response?.data?.error, error.response?.data?.message, 'error')
} else if(error.response?.data?.message) {
swal('Error', error.response?.data?.message, 'error')
} else {
swal('Error', 'Failed to create filter. Please try again.', 'error')
}
})
.finally(() => {
this.loading = false;
});
}
}
}
}
</script>
<style scoped>
.card-deck-wrapper {
overflow-y: auto;
max-height: 40dvh;
}
</style>

Wyświetl plik

@ -0,0 +1,14 @@
Vue.component(
'filter-card',
require('./components/filters/FilterCard.vue').default
);
Vue.component(
'filter-modal',
require('./components/filters/FilterModal.vue').default
);
Vue.component(
'filters-list',
require('./components/filters/FiltersList.vue').default
);

Wyświetl plik

@ -0,0 +1,11 @@
@extends('settings.template')
@section('section')
<filters-list />
@endsection
@push('scripts')
<script type="text/javascript" src="{{mix('js/custom_filters.js')}}"></script>
@endpush

Wyświetl plik

@ -17,6 +17,9 @@
<li class="nav-item pl-3 {{request()->is('settings/media*')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.media')}}">{{__('settings.media')}}</a>
</li>
<li class="nav-item pl-3 {{request()->is('settings/filters*')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.filters')}}">Filters</a>
</li>
{{-- <li class="nav-item pl-3 {{request()->is('settings/notifications')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.notifications')}}">{{__('settings.notifications')}}</a>
</li> --}}

Wyświetl plik

@ -187,6 +187,12 @@ Route::group(['prefix' => 'api'], function () use ($middleware) {
Route::post('media', 'Api\ApiV2Controller@mediaUploadV2')->middleware($middleware);
Route::get('streaming/config', 'Api\ApiV2Controller@getWebsocketConfig');
Route::get('instance', 'Api\ApiV2Controller@instance');
Route::get('filters', 'CustomFilterController@index')->middleware($middleware);
Route::get('filters/{id}', 'CustomFilterController@show')->middleware($middleware);
Route::post('filters', 'CustomFilterController@store')->middleware($middleware);
Route::put('filters/{id}', 'CustomFilterController@update')->middleware($middleware);
Route::delete('filters/{id}', 'CustomFilterController@delete')->middleware($middleware);
});
Route::group(['prefix' => 'v1.1'], function () use ($middleware) {

Wyświetl plik

@ -365,6 +365,10 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::get('manage', 'ProfileMigrationController@index');
Route::post('manage', 'ProfileMigrationController@store');
});
Route::group(['prefix' => 'filters'], function() {
Route::get('/', 'SettingsController@filtersHome')->name('settings.filters');
});
});
Route::group(['prefix' => 'site'], function () {

1
webpack.mix.js vendored
Wyświetl plik

@ -44,6 +44,7 @@ mix.js('resources/assets/js/app.js', 'public/js')
.js('resources/assets/js/groups.js', 'public/js')
.js('resources/assets/js/group-status.js', 'public/js')
.js('resources/assets/js/group-topic-feed.js', 'public/js')
.js('resources/assets/js/custom_filters.js', 'public/js')
.vue({ version: 2 });
mix.extract();