mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-11-22 06:21:27 +00:00
commit
eebed73a5e
42 changed files with 1669 additions and 354 deletions
|
@ -12,6 +12,7 @@
|
|||
- Added `app:hashtag-cached-count-update` command to update cached_count of hashtags and add to scheduler to run every 25 minutes past the hour ([1e31fee6](https://github.com/pixelfed/pixelfed/commit/1e31fee6))
|
||||
- Added `app:hashtag-related-generate` command to generate related hashtags ([176b4ed7](https://github.com/pixelfed/pixelfed/commit/176b4ed7))
|
||||
- Added Mutual Followers API endpoint ([33dbbe46](https://github.com/pixelfed/pixelfed/commit/33dbbe46))
|
||||
- Added User Domain Blocks ([#4834](https://github.com/pixelfed/pixelfed/pull/4834)) ([fa0380ac](https://github.com/pixelfed/pixelfed/commit/fa0380ac))
|
||||
|
||||
### Federation
|
||||
- Update Privacy Settings, add support for Mastodon `indexable` search flag ([fc24630e](https://github.com/pixelfed/pixelfed/commit/fc24630e))
|
||||
|
@ -80,6 +81,8 @@
|
|||
- Update Inbox, improve tombstone query efficiency ([759a4393](https://github.com/pixelfed/pixelfed/commit/759a4393))
|
||||
- Update AccountService, add setLastActive method ([ebbd98e7](https://github.com/pixelfed/pixelfed/commit/ebbd98e7))
|
||||
- Update ApiV1Controller, set last_active_at ([b6419545](https://github.com/pixelfed/pixelfed/commit/b6419545))
|
||||
- Update AdminShadowFilter, fix deleted profile bug ([a492a95a](https://github.com/pixelfed/pixelfed/commit/a492a95a))
|
||||
- Update FollowerService, add $silent param to remove method to more efficently purge relationships ([1664a5bc](https://github.com/pixelfed/pixelfed/commit/1664a5bc))
|
||||
- ([](https://github.com/pixelfed/pixelfed/commit/))
|
||||
|
||||
## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9)
|
||||
|
|
106
app/Console/Commands/AddUserDomainBlock.php
Normal file
106
app/Console/Commands/AddUserDomainBlock.php
Normal file
|
@ -0,0 +1,106 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\User;
|
||||
use App\Models\DefaultDomainBlock;
|
||||
use App\Models\UserDomainBlock;
|
||||
use function Laravel\Prompts\text;
|
||||
use function Laravel\Prompts\confirm;
|
||||
use function Laravel\Prompts\progress;
|
||||
|
||||
class AddUserDomainBlock extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:add-user-domain-block';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Apply a domain block to all users';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$domain = text('Enter domain you want to block');
|
||||
$domain = strtolower($domain);
|
||||
$domain = $this->validateDomain($domain);
|
||||
if(!$domain || empty($domain)) {
|
||||
$this->error('Invalid domain');
|
||||
return;
|
||||
}
|
||||
$this->processBlocks($domain);
|
||||
return;
|
||||
}
|
||||
|
||||
protected function validateDomain($domain)
|
||||
{
|
||||
if(!strpos($domain, '.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(str_starts_with($domain, 'https://')) {
|
||||
$domain = str_replace('https://', '', $domain);
|
||||
}
|
||||
|
||||
if(str_starts_with($domain, 'http://')) {
|
||||
$domain = str_replace('http://', '', $domain);
|
||||
}
|
||||
|
||||
$domain = strtolower(parse_url('https://' . $domain, PHP_URL_HOST));
|
||||
|
||||
$valid = filter_var($domain, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME|FILTER_NULL_ON_FAILURE);
|
||||
if(!$valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if($domain === config('pixelfed.domain.app')) {
|
||||
$this->error('Invalid domain');
|
||||
return;
|
||||
}
|
||||
|
||||
$confirmed = confirm('Are you sure you want to block ' . $domain . '?');
|
||||
if(!$confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
return $domain;
|
||||
}
|
||||
|
||||
protected function processBlocks($domain)
|
||||
{
|
||||
DefaultDomainBlock::updateOrCreate([
|
||||
'domain' => $domain
|
||||
]);
|
||||
progress(
|
||||
label: 'Updating user domain blocks...',
|
||||
steps: User::lazyById(500),
|
||||
callback: fn ($user) => $this->performTask($user, $domain),
|
||||
);
|
||||
}
|
||||
|
||||
protected function performTask($user, $domain)
|
||||
{
|
||||
if(!$user->profile_id || $user->delete_after) {
|
||||
return;
|
||||
}
|
||||
|
||||
if($user->status != null && $user->status != 'disabled') {
|
||||
return;
|
||||
}
|
||||
|
||||
UserDomainBlock::updateOrCreate([
|
||||
'profile_id' => $user->profile_id,
|
||||
'domain' => $domain
|
||||
]);
|
||||
}
|
||||
}
|
96
app/Console/Commands/DeleteUserDomainBlock.php
Normal file
96
app/Console/Commands/DeleteUserDomainBlock.php
Normal file
|
@ -0,0 +1,96 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\User;
|
||||
use App\Models\DefaultDomainBlock;
|
||||
use App\Models\UserDomainBlock;
|
||||
use function Laravel\Prompts\text;
|
||||
use function Laravel\Prompts\confirm;
|
||||
use function Laravel\Prompts\progress;
|
||||
|
||||
class DeleteUserDomainBlock extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:delete-user-domain-block';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Remove a domain block for all users';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$domain = text('Enter domain you want to unblock');
|
||||
$domain = strtolower($domain);
|
||||
$domain = $this->validateDomain($domain);
|
||||
if(!$domain || empty($domain)) {
|
||||
$this->error('Invalid domain');
|
||||
return;
|
||||
}
|
||||
$this->processUnblocks($domain);
|
||||
return;
|
||||
}
|
||||
|
||||
protected function validateDomain($domain)
|
||||
{
|
||||
if(!strpos($domain, '.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(str_starts_with($domain, 'https://')) {
|
||||
$domain = str_replace('https://', '', $domain);
|
||||
}
|
||||
|
||||
if(str_starts_with($domain, 'http://')) {
|
||||
$domain = str_replace('http://', '', $domain);
|
||||
}
|
||||
|
||||
$domain = strtolower(parse_url('https://' . $domain, PHP_URL_HOST));
|
||||
|
||||
$valid = filter_var($domain, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME|FILTER_NULL_ON_FAILURE);
|
||||
if(!$valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if($domain === config('pixelfed.domain.app')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$confirmed = confirm('Are you sure you want to unblock ' . $domain . '?');
|
||||
if(!$confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
return $domain;
|
||||
}
|
||||
|
||||
protected function processUnblocks($domain)
|
||||
{
|
||||
DefaultDomainBlock::whereDomain($domain)->delete();
|
||||
if(!UserDomainBlock::whereDomain($domain)->count()) {
|
||||
$this->info('No results found!');
|
||||
return;
|
||||
}
|
||||
progress(
|
||||
label: 'Updating user domain blocks...',
|
||||
steps: UserDomainBlock::whereDomain($domain)->lazyById(500),
|
||||
callback: fn ($domainBlock) => $this->performTask($domainBlock),
|
||||
);
|
||||
}
|
||||
|
||||
protected function performTask($domainBlock)
|
||||
{
|
||||
$domainBlock->deleteQuietly();
|
||||
}
|
||||
}
|
|
@ -19,7 +19,8 @@ class AdminShadowFilterController extends Controller
|
|||
{
|
||||
$filter = $request->input('filter');
|
||||
$searchQuery = $request->input('q');
|
||||
$filters = AdminShadowFilter::when($filter, function($q, $filter) {
|
||||
$filters = AdminShadowFilter::whereHas('profile')
|
||||
->when($filter, function($q, $filter) {
|
||||
if($filter == 'all') {
|
||||
return $q;
|
||||
} else if($filter == 'inactive') {
|
||||
|
|
|
@ -31,6 +31,7 @@ use App\{
|
|||
UserSetting,
|
||||
UserFilter,
|
||||
};
|
||||
use App\Models\UserDomainBlock;
|
||||
use League\Fractal;
|
||||
use App\Transformer\Api\Mastodon\v1\{
|
||||
AccountTransformer,
|
||||
|
@ -2422,6 +2423,7 @@ class ApiV1Controller extends Controller
|
|||
$local = $request->has('local');
|
||||
$filtered = $user ? UserFilterService::filters($user->profile_id) : [];
|
||||
AccountService::setLastActive($user->id);
|
||||
$domainBlocks = UserFilterService::domainBlocks($user->profile_id);
|
||||
|
||||
if($remote && config('instance.timeline.network.cached')) {
|
||||
Cache::remember('api:v1:timelines:network:cache_check', 10368000, function() {
|
||||
|
@ -2496,6 +2498,13 @@ class ApiV1Controller extends Controller
|
|||
->filter(function($s) use($filtered) {
|
||||
return $s && isset($s['account']) && in_array($s['account']['id'], $filtered) == false;
|
||||
})
|
||||
->filter(function($s) use($domainBlocks) {
|
||||
if(!$domainBlocks || !count($domainBlocks)) {
|
||||
return $s;
|
||||
}
|
||||
$domain = strtolower(parse_url($s['url'], PHP_URL_HOST));
|
||||
return !in_array($domain, $domainBlocks);
|
||||
})
|
||||
->take($limit)
|
||||
->values();
|
||||
|
||||
|
@ -3276,6 +3285,7 @@ class ApiV1Controller extends Controller
|
|||
$limit = $request->input('limit', 20);
|
||||
$onlyMedia = $request->input('only_media', true);
|
||||
$pe = $request->has(self::PF_API_ENTITY_KEY);
|
||||
$pid = $request->user()->profile_id;
|
||||
|
||||
if($min || $max) {
|
||||
$minMax = SnowflakeService::byDate(now()->subMonths(6));
|
||||
|
@ -3287,7 +3297,8 @@ class ApiV1Controller extends Controller
|
|||
}
|
||||
}
|
||||
|
||||
$filters = UserFilterService::filters($request->user()->profile_id);
|
||||
$filters = UserFilterService::filters($pid);
|
||||
$domainBlocks = UserFilterService::domainBlocks($pid);
|
||||
|
||||
if(!$min && !$max) {
|
||||
$id = 1;
|
||||
|
@ -3313,10 +3324,11 @@ class ApiV1Controller extends Controller
|
|||
if($onlyMedia && !isset($i['media_attachments']) || !count($i['media_attachments'])) {
|
||||
return false;
|
||||
}
|
||||
return $i && isset($i['account']);
|
||||
return $i && isset($i['account'], $i['url']);
|
||||
})
|
||||
->filter(function($i) use($filters) {
|
||||
return !in_array($i['account']['id'], $filters);
|
||||
->filter(function($i) use($filters, $domainBlocks) {
|
||||
$domain = strtolower(parse_url($i['url'], PHP_URL_HOST));
|
||||
return !in_array($i['account']['id'], $filters) && !in_array($domain, $domainBlocks);
|
||||
})
|
||||
->values()
|
||||
->toArray();
|
||||
|
@ -3619,25 +3631,31 @@ class ApiV1Controller extends Controller
|
|||
|
||||
$pid = $request->user()->profile_id;
|
||||
|
||||
$ids = Cache::remember('api:v1.1:discover:accounts:popular', 86400, function() {
|
||||
$ids = Cache::remember('api:v1.1:discover:accounts:popular', 3600, function() {
|
||||
return DB::table('profiles')
|
||||
->where('is_private', false)
|
||||
->whereNull('status')
|
||||
->orderByDesc('profiles.followers_count')
|
||||
->limit(20)
|
||||
->limit(30)
|
||||
->get();
|
||||
});
|
||||
|
||||
$filters = UserFilterService::filters($pid);
|
||||
$ids = $ids->map(function($profile) {
|
||||
return AccountService::get($profile->id, true);
|
||||
})
|
||||
->filter(function($profile) use($pid) {
|
||||
return $profile && isset($profile['id']);
|
||||
return $profile && isset($profile['id'], $profile['locked']) && !$profile['locked'];
|
||||
})
|
||||
->filter(function($profile) use($pid) {
|
||||
return $profile['id'] != $pid;
|
||||
})
|
||||
->take(6)
|
||||
->filter(function($profile) use($pid) {
|
||||
return !FollowerService::follows($pid, $profile['id'], true);
|
||||
})
|
||||
->filter(function($profile) use($filters) {
|
||||
return !in_array($profile['id'], $filters);
|
||||
})
|
||||
->take(16)
|
||||
->values();
|
||||
|
||||
return $this->json($ids);
|
||||
|
|
118
app/Http/Controllers/Api/V1/DomainBlockController.php
Normal file
118
app/Http/Controllers/Api/V1/DomainBlockController.php
Normal file
|
@ -0,0 +1,118 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\UserDomainBlock;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use App\Services\UserFilterService;
|
||||
use Illuminate\Bus\Batch;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use App\Jobs\HomeFeedPipeline\FeedRemoveDomainPipeline;
|
||||
use App\Jobs\ProfilePipeline\ProfilePurgeNotificationsByDomain;
|
||||
use App\Jobs\ProfilePipeline\ProfilePurgeFollowersByDomain;
|
||||
|
||||
class DomainBlockController extends Controller
|
||||
{
|
||||
public function json($res, $code = 200, $headers = [])
|
||||
{
|
||||
return response()->json($res, $code, $headers, JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
abort_unless($request->user(), 403);
|
||||
$this->validate($request, [
|
||||
'limit' => 'sometimes|integer|min:1|max:200'
|
||||
]);
|
||||
$limit = $request->input('limit', 100);
|
||||
$id = $request->user()->profile_id;
|
||||
$filters = UserDomainBlock::whereProfileId($id)->orderByDesc('id')->cursorPaginate($limit);
|
||||
$links = null;
|
||||
$headers = [];
|
||||
|
||||
if($filters->nextCursor()) {
|
||||
$links .= '<'.$filters->nextPageUrl().'&limit='.$limit.'>; rel="next"';
|
||||
}
|
||||
|
||||
if($filters->previousCursor()) {
|
||||
if($links != null) {
|
||||
$links .= ', ';
|
||||
}
|
||||
$links .= '<'.$filters->previousPageUrl().'&limit='.$limit.'>; rel="prev"';
|
||||
}
|
||||
|
||||
if($links) {
|
||||
$headers = ['Link' => $links];
|
||||
}
|
||||
return $this->json($filters->pluck('domain'), 200, $headers);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
abort_unless($request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'domain' => 'required|active_url|min:1|max:120'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
|
||||
$domain = trim($request->input('domain'));
|
||||
|
||||
if(Helpers::validateUrl($domain) == false) {
|
||||
return abort(500, 'Invalid domain or already blocked by server admins');
|
||||
}
|
||||
|
||||
$domain = strtolower(parse_url($domain, PHP_URL_HOST));
|
||||
|
||||
abort_if(config_cache('pixelfed.domain.app') == $domain, 400, 'Cannot ban your own server');
|
||||
|
||||
$existingCount = UserDomainBlock::whereProfileId($pid)->count();
|
||||
$maxLimit = config('instance.user_filters.max_domain_blocks');
|
||||
$errorMsg = __('profile.block.domain.max', ['max' => $maxLimit]);
|
||||
|
||||
abort_if($existingCount >= $maxLimit, 400, $errorMsg);
|
||||
|
||||
$block = UserDomainBlock::updateOrCreate([
|
||||
'profile_id' => $pid,
|
||||
'domain' => $domain
|
||||
]);
|
||||
|
||||
if($block->wasRecentlyCreated) {
|
||||
Bus::batch([
|
||||
[
|
||||
new FeedRemoveDomainPipeline($pid, $domain),
|
||||
new ProfilePurgeNotificationsByDomain($pid, $domain),
|
||||
new ProfilePurgeFollowersByDomain($pid, $domain)
|
||||
]
|
||||
])->allowFailures()->onQueue('feed')->dispatch();
|
||||
|
||||
Cache::forget('profile:following:' . $pid);
|
||||
UserFilterService::domainBlocks($pid, true);
|
||||
}
|
||||
|
||||
return $this->json([]);
|
||||
}
|
||||
|
||||
public function delete(Request $request)
|
||||
{
|
||||
abort_unless($request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'domain' => 'required|min:1|max:120'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
|
||||
$domain = strtolower(trim($request->input('domain')));
|
||||
|
||||
$filters = UserDomainBlock::whereProfileId($pid)->whereDomain($domain)->delete();
|
||||
|
||||
UserFilterService::domainBlocks($pid, true);
|
||||
|
||||
return $this->json([]);
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@ use App\Util\Lexer\PrettyNumber;
|
|||
use App\Util\ActivityPub\Helpers;
|
||||
use Auth, Cache, DB;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\UserDomainBlock;
|
||||
|
||||
trait PrivacySettings
|
||||
{
|
||||
|
@ -149,47 +150,25 @@ trait PrivacySettings
|
|||
|
||||
public function blockedInstances()
|
||||
{
|
||||
$pid = Auth::user()->profile->id;
|
||||
$filters = UserFilter::whereUserId($pid)
|
||||
->whereFilterableType('App\Instance')
|
||||
->whereFilterType('block')
|
||||
->orderByDesc('id')
|
||||
->paginate(10);
|
||||
return view('settings.privacy.blocked-instances', compact('filters'));
|
||||
// deprecated
|
||||
abort(404);
|
||||
}
|
||||
|
||||
public function domainBlocks()
|
||||
{
|
||||
return view('settings.privacy.domain-blocks');
|
||||
}
|
||||
|
||||
public function blockedInstanceStore(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'domain' => 'required|url|min:1|max:120'
|
||||
]);
|
||||
$domain = $request->input('domain');
|
||||
if(Helpers::validateUrl($domain) == false) {
|
||||
return abort(400, 'Invalid domain');
|
||||
}
|
||||
$domain = parse_url($domain, PHP_URL_HOST);
|
||||
$instance = Instance::firstOrCreate(['domain' => $domain]);
|
||||
$filter = new UserFilter;
|
||||
$filter->user_id = Auth::user()->profile->id;
|
||||
$filter->filterable_id = $instance->id;
|
||||
$filter->filterable_type = 'App\Instance';
|
||||
$filter->filter_type = 'block';
|
||||
$filter->save();
|
||||
return response()->json(['msg' => 200]);
|
||||
// deprecated
|
||||
abort(404);
|
||||
}
|
||||
|
||||
public function blockedInstanceUnblock(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'id' => 'required|integer|min:1'
|
||||
]);
|
||||
$pid = Auth::user()->profile->id;
|
||||
|
||||
$filter = UserFilter::whereFilterableType('App\Instance')
|
||||
->whereUserId($pid)
|
||||
->findOrFail($request->input('id'));
|
||||
$filter->delete();
|
||||
return redirect(route('settings.privacy.blocked-instances'));
|
||||
// deprecated
|
||||
abort(404);
|
||||
}
|
||||
|
||||
public function blockedKeywords()
|
||||
|
|
|
@ -76,10 +76,7 @@ class DeleteRemoteStatusPipeline implements ShouldQueue
|
|||
});
|
||||
Mention::whereStatusId($status->id)->forceDelete();
|
||||
Report::whereObjectType('App\Status')->whereObjectId($status->id)->delete();
|
||||
$statusHashtags = StatusHashtag::whereStatusId($status->id)->get();
|
||||
foreach($statusHashtags as $stag) {
|
||||
$stag->delete();
|
||||
}
|
||||
StatusHashtag::whereStatusId($status->id)->deleteQuietly();
|
||||
StatusView::whereStatusId($status->id)->delete();
|
||||
Status::whereReblogOfId($status->id)->forceDelete();
|
||||
$status->forceDelete();
|
||||
|
|
|
@ -11,6 +11,7 @@ use Illuminate\Queue\SerializesModels;
|
|||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
|
||||
use App\UserFilter;
|
||||
use App\Models\UserDomainBlock;
|
||||
use App\Services\FollowerService;
|
||||
use App\Services\HomeTimelineService;
|
||||
use App\Services\StatusService;
|
||||
|
@ -69,7 +70,7 @@ class FeedInsertPipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing
|
|||
$sid = $this->sid;
|
||||
$status = StatusService::get($sid, false);
|
||||
|
||||
if(!$status || !isset($status['account']) || !isset($status['account']['id'])) {
|
||||
if(!$status || !isset($status['account']) || !isset($status['account']['id'], $status['url'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -85,7 +86,24 @@ class FeedInsertPipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing
|
|||
return;
|
||||
}
|
||||
|
||||
$skipIds = UserFilter::whereFilterableType('App\Profile')->whereFilterableId($status['account']['id'])->whereIn('filter_type', ['mute', 'block'])->pluck('user_id')->toArray();
|
||||
$domain = strtolower(parse_url($status['url'], PHP_URL_HOST));
|
||||
$skipIds = [];
|
||||
|
||||
if(strtolower(config('pixelfed.domain.app')) !== $domain) {
|
||||
$skipIds = UserDomainBlock::where('domain', $domain)->pluck('profile_id')->toArray();
|
||||
}
|
||||
|
||||
$filters = UserFilter::whereFilterableType('App\Profile')
|
||||
->whereFilterableId($status['account']['id'])
|
||||
->whereIn('filter_type', ['mute', 'block'])
|
||||
->pluck('user_id')
|
||||
->toArray();
|
||||
|
||||
if($filters && count($filters)) {
|
||||
$skipIds = array_merge($skipIds, $filters);
|
||||
}
|
||||
|
||||
$skipIds = array_unique(array_values($skipIds));
|
||||
|
||||
foreach($ids as $id) {
|
||||
if(!in_array($id, $skipIds)) {
|
||||
|
|
|
@ -11,6 +11,7 @@ use Illuminate\Queue\SerializesModels;
|
|||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
|
||||
use App\UserFilter;
|
||||
use App\Models\UserDomainBlock;
|
||||
use App\Services\FollowerService;
|
||||
use App\Services\HomeTimelineService;
|
||||
use App\Services\StatusService;
|
||||
|
@ -69,7 +70,7 @@ class FeedInsertRemotePipeline implements ShouldQueue, ShouldBeUniqueUntilProces
|
|||
$sid = $this->sid;
|
||||
$status = StatusService::get($sid, false);
|
||||
|
||||
if(!$status || !isset($status['account']) || !isset($status['account']['id'])) {
|
||||
if(!$status || !isset($status['account']) || !isset($status['account']['id'], $status['url'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -83,7 +84,24 @@ class FeedInsertRemotePipeline implements ShouldQueue, ShouldBeUniqueUntilProces
|
|||
return;
|
||||
}
|
||||
|
||||
$skipIds = UserFilter::whereFilterableType('App\Profile')->whereFilterableId($status['account']['id'])->whereIn('filter_type', ['mute', 'block'])->pluck('user_id')->toArray();
|
||||
$domain = strtolower(parse_url($status['url'], PHP_URL_HOST));
|
||||
$skipIds = [];
|
||||
|
||||
if(strtolower(config('pixelfed.domain.app')) !== $domain) {
|
||||
$skipIds = UserDomainBlock::where('domain', $domain)->pluck('profile_id')->toArray();
|
||||
}
|
||||
|
||||
$filters = UserFilter::whereFilterableType('App\Profile')
|
||||
->whereFilterableId($status['account']['id'])
|
||||
->whereIn('filter_type', ['mute', 'block'])
|
||||
->pluck('user_id')
|
||||
->toArray();
|
||||
|
||||
if($filters && count($filters)) {
|
||||
$skipIds = array_merge($skipIds, $filters);
|
||||
}
|
||||
|
||||
$skipIds = array_unique(array_values($skipIds));
|
||||
|
||||
foreach($ids as $id) {
|
||||
if(!in_array($id, $skipIds)) {
|
||||
|
|
98
app/Jobs/HomeFeedPipeline/FeedRemoveDomainPipeline.php
Normal file
98
app/Jobs/HomeFeedPipeline/FeedRemoveDomainPipeline.php
Normal file
|
@ -0,0 +1,98 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\HomeFeedPipeline;
|
||||
|
||||
use Illuminate\Bus\Batchable;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
|
||||
use App\Services\StatusService;
|
||||
use App\Services\HomeTimelineService;
|
||||
|
||||
class FeedRemoveDomainPipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing
|
||||
{
|
||||
use Batchable, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $pid;
|
||||
protected $domain;
|
||||
|
||||
public $timeout = 900;
|
||||
public $tries = 3;
|
||||
public $maxExceptions = 1;
|
||||
public $failOnTimeout = true;
|
||||
|
||||
/**
|
||||
* The number of seconds after which the job's unique lock will be released.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $uniqueFor = 3600;
|
||||
|
||||
/**
|
||||
* Get the unique ID for the job.
|
||||
*/
|
||||
public function uniqueId(): string
|
||||
{
|
||||
return 'hts:feed:remove:domain:' . $this->pid . ':d-' . $this->domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the middleware the job should pass through.
|
||||
*
|
||||
* @return array<int, object>
|
||||
*/
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new WithoutOverlapping("hts:feed:remove:domain:{$this->pid}:d-{$this->domain}"))->shared()->dontRelease()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct($pid, $domain)
|
||||
{
|
||||
$this->pid = $pid;
|
||||
$this->domain = $domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
if(!config('exp.cached_home_timeline')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->batch()->cancelled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!$this->pid || !$this->domain) {
|
||||
return;
|
||||
}
|
||||
$domain = strtolower($this->domain);
|
||||
$pid = $this->pid;
|
||||
$posts = HomeTimelineService::get($pid, '0', '-1');
|
||||
|
||||
foreach($posts as $post) {
|
||||
$status = StatusService::get($post, false);
|
||||
if(!$status || !isset($status['url'])) {
|
||||
HomeTimelineService::rem($pid, $post);
|
||||
continue;
|
||||
}
|
||||
$host = strtolower(parse_url($status['url'], PHP_URL_HOST));
|
||||
if($host === strtolower(config('pixelfed.domain.app')) || !$host) {
|
||||
continue;
|
||||
}
|
||||
if($host === $domain) {
|
||||
HomeTimelineService::rem($pid, $status['id']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@ use Illuminate\Queue\SerializesModels;
|
|||
use App\Hashtag;
|
||||
use App\StatusHashtag;
|
||||
use App\UserFilter;
|
||||
use App\Models\UserDomainBlock;
|
||||
use App\Services\HashtagFollowService;
|
||||
use App\Services\HomeTimelineService;
|
||||
use App\Services\StatusService;
|
||||
|
@ -77,7 +78,7 @@ class HashtagInsertFanoutPipeline implements ShouldQueue, ShouldBeUniqueUntilPro
|
|||
$sid = $hashtag->status_id;
|
||||
$status = StatusService::get($sid, false);
|
||||
|
||||
if(!$status || !isset($status['account']) || !isset($status['account']['id'])) {
|
||||
if(!$status || !isset($status['account']) || !isset($status['account']['id'], $status['url'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -85,7 +86,20 @@ class HashtagInsertFanoutPipeline implements ShouldQueue, ShouldBeUniqueUntilPro
|
|||
return;
|
||||
}
|
||||
|
||||
$skipIds = UserFilter::whereFilterableType('App\Profile')->whereFilterableId($status['account']['id'])->whereIn('filter_type', ['mute', 'block'])->pluck('user_id')->toArray();
|
||||
$domain = strtolower(parse_url($status['url'], PHP_URL_HOST));
|
||||
$skipIds = [];
|
||||
|
||||
if(strtolower(config('pixelfed.domain.app')) !== $domain) {
|
||||
$skipIds = UserDomainBlock::where('domain', $domain)->pluck('profile_id')->toArray();
|
||||
}
|
||||
|
||||
$filters = UserFilter::whereFilterableType('App\Profile')->whereFilterableId($status['account']['id'])->whereIn('filter_type', ['mute', 'block'])->pluck('user_id')->toArray();
|
||||
|
||||
if($filters && count($filters)) {
|
||||
$skipIds = array_merge($skipIds, $filters);
|
||||
}
|
||||
|
||||
$skipIds = array_unique(array_values($skipIds));
|
||||
|
||||
$ids = HashtagFollowService::getPidByHid($hashtag->hashtag_id);
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ use Illuminate\Contracts\Queue\ShouldQueue;
|
|||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Log;
|
||||
|
||||
class ImageResize implements ShouldQueue
|
||||
{
|
||||
|
@ -46,6 +47,7 @@ class ImageResize implements ShouldQueue
|
|||
}
|
||||
$path = storage_path('app/'.$media->media_path);
|
||||
if (!is_file($path) || $media->skip_optimize) {
|
||||
Log::info('Tried to optimize media that does not exist or is not readable. ' . $path);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -57,6 +59,7 @@ class ImageResize implements ShouldQueue
|
|||
$img = new Image();
|
||||
$img->resizeImage($media);
|
||||
} catch (Exception $e) {
|
||||
Log::error($e);
|
||||
}
|
||||
|
||||
ImageThumbnail::dispatch($media)->onQueue('mmo');
|
||||
|
|
119
app/Jobs/ProfilePipeline/ProfilePurgeFollowersByDomain.php
Normal file
119
app/Jobs/ProfilePipeline/ProfilePurgeFollowersByDomain.php
Normal file
|
@ -0,0 +1,119 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\ProfilePipeline;
|
||||
|
||||
use Illuminate\Bus\Batchable;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
|
||||
use App\Follower;
|
||||
use App\Profile;
|
||||
use App\Notification;
|
||||
use DB;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\FollowerService;
|
||||
use App\Services\NotificationService;
|
||||
|
||||
class ProfilePurgeFollowersByDomain implements ShouldQueue, ShouldBeUniqueUntilProcessing
|
||||
{
|
||||
use Batchable, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $pid;
|
||||
protected $domain;
|
||||
|
||||
public $timeout = 900;
|
||||
public $tries = 3;
|
||||
public $maxExceptions = 1;
|
||||
public $failOnTimeout = true;
|
||||
|
||||
/**
|
||||
* The number of seconds after which the job's unique lock will be released.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $uniqueFor = 3600;
|
||||
|
||||
/**
|
||||
* Get the unique ID for the job.
|
||||
*/
|
||||
public function uniqueId(): string
|
||||
{
|
||||
return 'followers:v1:purge-by-domain:' . $this->pid . ':d-' . $this->domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the middleware the job should pass through.
|
||||
*
|
||||
* @return array<int, object>
|
||||
*/
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new WithoutOverlapping("followers:v1:purge-by-domain:{$this->pid}:d-{$this->domain}"))->shared()->dontRelease()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct($pid, $domain)
|
||||
{
|
||||
$this->pid = $pid;
|
||||
$this->domain = $domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
if ($this->batch()->cancelled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$pid = $this->pid;
|
||||
$domain = $this->domain;
|
||||
|
||||
$query = 'SELECT f.*
|
||||
FROM followers f
|
||||
JOIN profiles p ON p.id = f.profile_id OR p.id = f.following_id
|
||||
WHERE (f.profile_id = ? OR f.following_id = ?)
|
||||
AND p.domain = ?;';
|
||||
$params = [$pid, $pid, $domain];
|
||||
|
||||
foreach(DB::cursor($query, $params) as $n) {
|
||||
if(!$n || !$n->id) {
|
||||
continue;
|
||||
}
|
||||
$follower = Follower::find($n->id);
|
||||
if($follower->following_id == $pid && $follower->profile_id) {
|
||||
FollowerService::remove($follower->profile_id, $pid, true);
|
||||
$follower->delete();
|
||||
} else if ($follower->profile_id == $pid && $follower->following_id) {
|
||||
FollowerService::remove($follower->following_id, $pid, true);
|
||||
$follower->delete();
|
||||
}
|
||||
}
|
||||
|
||||
$profile = Profile::find($pid);
|
||||
|
||||
$followerCount = DB::table('profiles')
|
||||
->join('followers', 'profiles.id', '=', 'followers.following_id')
|
||||
->where('followers.following_id', $pid)
|
||||
->count();
|
||||
|
||||
$followingCount = DB::table('profiles')
|
||||
->join('followers', 'profiles.id', '=', 'followers.following_id')
|
||||
->where('followers.profile_id', $pid)
|
||||
->count();
|
||||
|
||||
$profile->followers_count = $followerCount;
|
||||
$profile->following_count = $followingCount;
|
||||
$profile->save();
|
||||
|
||||
AccountService::del($profile->id);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\ProfilePipeline;
|
||||
|
||||
use Illuminate\Bus\Batchable;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
|
||||
use App\Notification;
|
||||
use DB;
|
||||
use App\Services\NotificationService;
|
||||
|
||||
class ProfilePurgeNotificationsByDomain implements ShouldQueue, ShouldBeUniqueUntilProcessing
|
||||
{
|
||||
use Batchable, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $pid;
|
||||
protected $domain;
|
||||
|
||||
public $timeout = 900;
|
||||
public $tries = 3;
|
||||
public $maxExceptions = 1;
|
||||
public $failOnTimeout = true;
|
||||
|
||||
/**
|
||||
* The number of seconds after which the job's unique lock will be released.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $uniqueFor = 3600;
|
||||
|
||||
/**
|
||||
* Get the unique ID for the job.
|
||||
*/
|
||||
public function uniqueId(): string
|
||||
{
|
||||
return 'notify:v1:purge-by-domain:' . $this->pid . ':d-' . $this->domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the middleware the job should pass through.
|
||||
*
|
||||
* @return array<int, object>
|
||||
*/
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new WithoutOverlapping("notify:v1:purge-by-domain:{$this->pid}:d-{$this->domain}"))->shared()->dontRelease()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct($pid, $domain)
|
||||
{
|
||||
$this->pid = $pid;
|
||||
$this->domain = $domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
if ($this->batch()->cancelled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$pid = $this->pid;
|
||||
$domain = $this->domain;
|
||||
|
||||
$query = 'SELECT notifications.*
|
||||
FROM profiles
|
||||
JOIN notifications on profiles.id = notifications.actor_id
|
||||
WHERE notifications.profile_id = ?
|
||||
AND profiles.domain = ?';
|
||||
$params = [$pid, $domain];
|
||||
|
||||
foreach(DB::cursor($query, $params) as $n) {
|
||||
if(!$n || !$n->id) {
|
||||
continue;
|
||||
}
|
||||
Notification::where('id', $n->id)->delete();
|
||||
NotificationService::del($pid, $n->id);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -174,10 +174,7 @@ class RemoteStatusDelete implements ShouldQueue, ShouldBeUniqueUntilProcessing
|
|||
->whereObjectId($status->id)
|
||||
->delete();
|
||||
StatusArchived::whereStatusId($status->id)->delete();
|
||||
$statusHashtags = StatusHashtag::whereStatusId($status->id)->get();
|
||||
foreach($statusHashtags as $stag) {
|
||||
$stag->delete();
|
||||
}
|
||||
StatusHashtag::whereStatusId($status->id)->deleteQuietly();
|
||||
StatusView::whereStatusId($status->id)->delete();
|
||||
Status::whereInReplyToId($status->id)->update(['in_reply_to_id' => null]);
|
||||
|
||||
|
|
|
@ -151,10 +151,7 @@ class StatusDelete implements ShouldQueue
|
|||
->delete();
|
||||
|
||||
StatusArchived::whereStatusId($status->id)->delete();
|
||||
$statusHashtags = StatusHashtag::whereStatusId($status->id)->get();
|
||||
foreach($statusHashtags as $stag) {
|
||||
$stag->delete();
|
||||
}
|
||||
StatusHashtag::whereStatusId($status->id)->deleteQuietly();
|
||||
StatusView::whereStatusId($status->id)->delete();
|
||||
Status::whereInReplyToId($status->id)->update(['in_reply_to_id' => null]);
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ namespace App\Models;
|
|||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Services\AccountService;
|
||||
use App\Profile;
|
||||
|
||||
class AdminShadowFilter extends Model
|
||||
{
|
||||
|
@ -24,4 +25,9 @@ class AdminShadowFilter extends Model
|
|||
|
||||
return;
|
||||
}
|
||||
|
||||
public function profile()
|
||||
{
|
||||
return $this->belongsTo(Profile::class, 'item_id');
|
||||
}
|
||||
}
|
||||
|
|
13
app/Models/DefaultDomainBlock.php
Normal file
13
app/Models/DefaultDomainBlock.php
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class DefaultDomainBlock extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $guarded = [];
|
||||
}
|
21
app/Models/UserDomainBlock.php
Normal file
21
app/Models/UserDomainBlock.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Profile;
|
||||
|
||||
class UserDomainBlock extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
public function profile()
|
||||
{
|
||||
return $this->belongsTo(Profile::class, 'profile_id');
|
||||
}
|
||||
}
|
|
@ -7,6 +7,9 @@ use App\Follower;
|
|||
use App\Profile;
|
||||
use App\User;
|
||||
use App\UserSetting;
|
||||
use App\Services\UserFilterService;
|
||||
use App\Models\DefaultDomainBlock;
|
||||
use App\Models\UserDomainBlock;
|
||||
use App\Jobs\FollowPipeline\FollowPipeline;
|
||||
use DB;
|
||||
use App\Services\FollowerService;
|
||||
|
@ -14,7 +17,18 @@ use App\Services\FollowerService;
|
|||
class UserObserver
|
||||
{
|
||||
/**
|
||||
* Listen to the User created event.
|
||||
* Handle the notification "created" event.
|
||||
*
|
||||
* @param \App\User $user
|
||||
* @return void
|
||||
*/
|
||||
public function created(User $user): void
|
||||
{
|
||||
$this->handleUser($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen to the User saved event.
|
||||
*
|
||||
* @param \App\User $user
|
||||
*
|
||||
|
@ -22,7 +36,38 @@ class UserObserver
|
|||
*/
|
||||
public function saved(User $user)
|
||||
{
|
||||
if($user->status == 'deleted') {
|
||||
$this->handleUser($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen to the User updated event.
|
||||
*
|
||||
* @param \App\User $user
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function updated(User $user): void
|
||||
{
|
||||
$this->handleUser($user);
|
||||
if($user->profile) {
|
||||
$this->applyDefaultDomainBlocks($user);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the user "deleted" event.
|
||||
*
|
||||
* @param \App\User $user
|
||||
* @return void
|
||||
*/
|
||||
public function deleted(User $user)
|
||||
{
|
||||
FollowerService::delCache($user->profile_id);
|
||||
}
|
||||
|
||||
protected function handleUser($user)
|
||||
{
|
||||
if(in_array($user->status, ['deleted', 'delete'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -49,9 +94,11 @@ class UserObserver
|
|||
$profile->private_key = $pki_private;
|
||||
$profile->public_key = $pki_public;
|
||||
$profile->save();
|
||||
$this->applyDefaultDomainBlocks($user);
|
||||
return $profile;
|
||||
});
|
||||
|
||||
|
||||
DB::transaction(function() use($user, $profile) {
|
||||
$user = User::findOrFail($user->id);
|
||||
$user->profile_id = $profile->id;
|
||||
|
@ -92,14 +139,22 @@ class UserObserver
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the user "deleted" event.
|
||||
*
|
||||
* @param \App\User $user
|
||||
* @return void
|
||||
*/
|
||||
public function deleted(User $user)
|
||||
protected function applyDefaultDomainBlocks($user)
|
||||
{
|
||||
FollowerService::delCache($user->profile_id);
|
||||
if($user->profile_id == null) {
|
||||
return;
|
||||
}
|
||||
$defaultDomainBlocks = DefaultDomainBlock::pluck('domain')->toArray();
|
||||
|
||||
if(!$defaultDomainBlocks || !count($defaultDomainBlocks)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach($defaultDomainBlocks as $domain) {
|
||||
UserDomainBlock::updateOrCreate([
|
||||
'profile_id' => $user->profile_id,
|
||||
'domain' => strtolower(trim($domain))
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ use App\Profile;
|
|||
use App\Status;
|
||||
use App\User;
|
||||
use App\UserSetting;
|
||||
use App\Models\UserDomainBlock;
|
||||
use App\Transformer\Api\AccountTransformer;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
|
@ -234,4 +235,13 @@ class AccountService
|
|||
}
|
||||
return;
|
||||
}
|
||||
|
||||
public static function blocksDomain($pid, $domain = false)
|
||||
{
|
||||
if(!$domain) {
|
||||
return;
|
||||
}
|
||||
|
||||
return UserDomainBlock::whereProfileId($pid)->whereDomain($domain)->exists();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,16 +35,18 @@ class FollowerService
|
|||
Cache::forget('profile:following:' . $actor);
|
||||
}
|
||||
|
||||
public static function remove($actor, $target)
|
||||
public static function remove($actor, $target, $silent = false)
|
||||
{
|
||||
Redis::zrem(self::FOLLOWING_KEY . $actor, $target);
|
||||
Redis::zrem(self::FOLLOWERS_KEY . $target, $actor);
|
||||
Cache::forget('pf:services:follower:audience:' . $actor);
|
||||
Cache::forget('pf:services:follower:audience:' . $target);
|
||||
if($silent !== true) {
|
||||
AccountService::del($actor);
|
||||
AccountService::del($target);
|
||||
RelationshipService::refresh($actor, $target);
|
||||
Cache::forget('profile:following:' . $actor);
|
||||
} else {
|
||||
RelationshipService::forget($actor, $target);
|
||||
}
|
||||
}
|
||||
|
||||
public static function followers($id, $start = 0, $stop = 10)
|
||||
|
@ -89,12 +91,16 @@ class FollowerService
|
|||
return Redis::zCard(self::FOLLOWING_KEY . $id);
|
||||
}
|
||||
|
||||
public static function follows(string $actor, string $target)
|
||||
public static function follows(string $actor, string $target, $quickCheck = false)
|
||||
{
|
||||
if($actor == $target) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if($quickCheck) {
|
||||
return (bool) Redis::zScore(self::FOLLOWERS_KEY . $target, $actor);
|
||||
}
|
||||
|
||||
if(self::followerCount($target, false) && self::followingCount($actor, false)) {
|
||||
self::cacheSyncCheck($target, 'followers');
|
||||
return (bool) Redis::zScore(self::FOLLOWERS_KEY . $target, $actor);
|
||||
|
|
|
@ -6,6 +6,7 @@ use Illuminate\Support\Facades\Cache;
|
|||
use Illuminate\Support\Facades\Redis;
|
||||
use App\Follower;
|
||||
use App\Status;
|
||||
use App\Models\UserDomainBlock;
|
||||
|
||||
class HomeTimelineService
|
||||
{
|
||||
|
@ -81,6 +82,8 @@ class HomeTimelineService
|
|||
$following = array_diff($following, $filters);
|
||||
}
|
||||
|
||||
$domainBlocks = UserDomainBlock::whereProfileId($id)->pluck('domain')->toArray();
|
||||
|
||||
$ids = Status::where('id', '>', $minId)
|
||||
->whereIn('profile_id', $following)
|
||||
->whereNull(['in_reply_to_id', 'reblog_of_id'])
|
||||
|
@ -91,6 +94,16 @@ class HomeTimelineService
|
|||
->pluck('id');
|
||||
|
||||
foreach($ids as $pid) {
|
||||
$status = StatusService::get($pid, false);
|
||||
if(!$status || !isset($status['account'], $status['url'])) {
|
||||
continue;
|
||||
}
|
||||
if($domainBlocks && count($domainBlocks)) {
|
||||
$domain = strtolower(parse_url($status['url'], PHP_URL_HOST));
|
||||
if(in_array($domain, $domainBlocks)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
self::add($id, $pid);
|
||||
}
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ class MarkerService
|
|||
return Cache::get(self::CACHE_KEY . $timeline . ':' . $profileId);
|
||||
}
|
||||
|
||||
public static function set($profileId, $timeline = 'home', $entityId)
|
||||
public static function set($profileId, $timeline = 'home', $entityId = false)
|
||||
{
|
||||
$existing = self::get($profileId, $timeline);
|
||||
$key = self::CACHE_KEY . $timeline . ':' . $profileId;
|
||||
|
|
|
@ -95,7 +95,15 @@ class SearchApiV2Service
|
|||
if(substr($webfingerQuery, 0, 1) !== '@') {
|
||||
$webfingerQuery = '@' . $webfingerQuery;
|
||||
}
|
||||
$banned = InstanceService::getBannedDomains();
|
||||
$banned = InstanceService::getBannedDomains() ?? [];
|
||||
$domainBlocks = UserFilterService::domainBlocks($user->profile_id);
|
||||
if($domainBlocks && count($domainBlocks)) {
|
||||
$banned = array_unique(
|
||||
array_values(
|
||||
array_merge($banned, $domainBlocks)
|
||||
)
|
||||
);
|
||||
}
|
||||
$operator = config('database.default') === 'pgsql' ? 'ilike' : 'like';
|
||||
$results = Profile::select('username', 'id', 'followers_count', 'domain')
|
||||
->where('username', $operator, $query)
|
||||
|
@ -172,8 +180,18 @@ class SearchApiV2Service
|
|||
'hashtags' => [],
|
||||
'statuses' => [],
|
||||
];
|
||||
$user = request()->user();
|
||||
$mastodonMode = self::$mastodonMode;
|
||||
$query = urldecode($this->query->input('q'));
|
||||
$banned = InstanceService::getBannedDomains();
|
||||
$domainBlocks = UserFilterService::domainBlocks($user->profile_id);
|
||||
if($domainBlocks && count($domainBlocks)) {
|
||||
$banned = array_unique(
|
||||
array_values(
|
||||
array_merge($banned, $domainBlocks)
|
||||
)
|
||||
);
|
||||
}
|
||||
if(substr($query, 0, 1) === '@' && !Str::contains($query, '.')) {
|
||||
$default['accounts'] = $this->accounts(substr($query, 1));
|
||||
return $default;
|
||||
|
@ -197,7 +215,11 @@ class SearchApiV2Service
|
|||
} catch (\Exception $e) {
|
||||
return $default;
|
||||
}
|
||||
if($res && isset($res['id'])) {
|
||||
if($res && isset($res['id'], $res['url'])) {
|
||||
$domain = strtolower(parse_url($res['url'], PHP_URL_HOST));
|
||||
if(in_array($domain, $banned)) {
|
||||
return $default;
|
||||
}
|
||||
$default['accounts'][] = $res;
|
||||
return $default;
|
||||
} else {
|
||||
|
@ -212,6 +234,10 @@ class SearchApiV2Service
|
|||
return $default;
|
||||
}
|
||||
if($res && isset($res['id'])) {
|
||||
$domain = strtolower(parse_url($res['url'], PHP_URL_HOST));
|
||||
if(in_array($domain, $banned)) {
|
||||
return $default;
|
||||
}
|
||||
$default['accounts'][] = $res;
|
||||
return $default;
|
||||
} else {
|
||||
|
@ -221,6 +247,9 @@ class SearchApiV2Service
|
|||
|
||||
if($sid = Status::whereUri($query)->first()) {
|
||||
$s = StatusService::get($sid->id, false);
|
||||
if(!$s) {
|
||||
return $default;
|
||||
}
|
||||
if(in_array($s['visibility'], ['public', 'unlisted'])) {
|
||||
$default['statuses'][] = $s;
|
||||
return $default;
|
||||
|
@ -229,7 +258,7 @@ class SearchApiV2Service
|
|||
|
||||
try {
|
||||
$res = ActivityPubFetchService::get($query);
|
||||
$banned = InstanceService::getBannedDomains();
|
||||
|
||||
if($res) {
|
||||
$json = json_decode($res, true);
|
||||
|
||||
|
|
|
@ -4,12 +4,14 @@ namespace App\Services;
|
|||
|
||||
use Cache;
|
||||
use App\UserFilter;
|
||||
use App\Models\UserDomainBlock;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
class UserFilterService
|
||||
{
|
||||
const USER_MUTES_KEY = 'pf:services:mutes:ids:';
|
||||
const USER_BLOCKS_KEY = 'pf:services:blocks:ids:';
|
||||
const USER_DOMAIN_KEY = 'pf:services:domain-blocks:ids:';
|
||||
|
||||
public static function mutes(int $profile_id)
|
||||
{
|
||||
|
@ -145,4 +147,17 @@ class UserFilterService
|
|||
{
|
||||
return Redis::zcard(self::USER_MUTES_KEY . $profile_id);
|
||||
}
|
||||
|
||||
public static function domainBlocks($pid, $purge = false)
|
||||
{
|
||||
if($purge) {
|
||||
Cache::forget(self::USER_DOMAIN_KEY . $pid);
|
||||
}
|
||||
return Cache::remember(
|
||||
self::USER_DOMAIN_KEY . $pid,
|
||||
21600,
|
||||
function() use($pid) {
|
||||
return UserDomainBlock::whereProfileId($pid)->pluck('domain')->toArray();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,4 +33,9 @@ class UserFilter extends Model
|
|||
{
|
||||
return $this->belongsTo(Instance::class, 'filterable_id');
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(Profile::class, 'user_id');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,6 +39,7 @@ use App\Util\ActivityPub\Validator\Like as LikeValidator;
|
|||
use App\Util\ActivityPub\Validator\UndoFollow as UndoFollowValidator;
|
||||
use App\Util\ActivityPub\Validator\UpdatePersonValidator;
|
||||
|
||||
use App\Services\AccountService;
|
||||
use App\Services\PollService;
|
||||
use App\Services\FollowerService;
|
||||
use App\Services\ReblogService;
|
||||
|
@ -372,7 +373,11 @@ class Inbox
|
|||
->whereUsername(array_last(explode('/', $activity['to'][0])))
|
||||
->firstOrFail();
|
||||
|
||||
if(in_array($actor->id, $profile->blockedIds()->toArray())) {
|
||||
if(!$actor || in_array($actor->id, $profile->blockedIds()->toArray())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(AccountService::blocksDomain($profile->id, $actor->domain) == true) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -510,6 +515,10 @@ class Inbox
|
|||
return;
|
||||
}
|
||||
|
||||
if(AccountService::blocksDomain($target->id, $actor->domain) == true) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(
|
||||
Follower::whereProfileId($actor->id)
|
||||
->whereFollowingId($target->id)
|
||||
|
@ -581,6 +590,10 @@ class Inbox
|
|||
return;
|
||||
}
|
||||
|
||||
if(AccountService::blocksDomain($parent->profile_id, $actor->domain) == true) {
|
||||
return;
|
||||
}
|
||||
|
||||
$blocks = UserFilterService::blocks($parent->profile_id);
|
||||
if($blocks && in_array($actor->id, $blocks)) {
|
||||
return;
|
||||
|
@ -634,6 +647,10 @@ class Inbox
|
|||
return;
|
||||
}
|
||||
|
||||
if(AccountService::blocksDomain($target->id, $actor->domain) == true) {
|
||||
return;
|
||||
}
|
||||
|
||||
$request = FollowRequest::whereFollowerId($actor->id)
|
||||
->whereFollowingId($target->id)
|
||||
->whereIsRejected(false)
|
||||
|
@ -759,6 +776,10 @@ class Inbox
|
|||
return;
|
||||
}
|
||||
|
||||
if(AccountService::blocksDomain($status->profile_id, $profile->domain) == true) {
|
||||
return;
|
||||
}
|
||||
|
||||
$blocks = UserFilterService::blocks($status->profile_id);
|
||||
if($blocks && in_array($profile->id, $blocks)) {
|
||||
return;
|
||||
|
@ -816,6 +837,9 @@ class Inbox
|
|||
if(!$status) {
|
||||
return;
|
||||
}
|
||||
if(AccountService::blocksDomain($status->profile_id, $profile->domain) == true) {
|
||||
return;
|
||||
}
|
||||
FeedRemoveRemotePipeline::dispatch($status->id, $status->profile_id)->onQueue('feed');
|
||||
Status::whereProfileId($profile->id)
|
||||
->whereReblogOfId($status->id)
|
||||
|
@ -837,6 +861,9 @@ class Inbox
|
|||
if(!$following) {
|
||||
return;
|
||||
}
|
||||
if(AccountService::blocksDomain($following->id, $profile->domain) == true) {
|
||||
return;
|
||||
}
|
||||
Follower::whereProfileId($profile->id)
|
||||
->whereFollowingId($following->id)
|
||||
->delete();
|
||||
|
@ -862,6 +889,9 @@ class Inbox
|
|||
if(!$status) {
|
||||
return;
|
||||
}
|
||||
if(AccountService::blocksDomain($status->profile_id, $profile->domain) == true) {
|
||||
return;
|
||||
}
|
||||
Like::whereProfileId($profile->id)
|
||||
->whereStatusId($status->id)
|
||||
->forceDelete();
|
||||
|
@ -915,6 +945,10 @@ class Inbox
|
|||
return;
|
||||
}
|
||||
|
||||
if(AccountService::blocksDomain($story->profile_id, $profile->domain) == true) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!FollowerService::follows($profile->id, $story->profile_id)) {
|
||||
return;
|
||||
}
|
||||
|
@ -985,6 +1019,10 @@ class Inbox
|
|||
|
||||
$actorProfile = Helpers::profileFetch($actor);
|
||||
|
||||
if(AccountService::blocksDomain($targetProfile->id, $actorProfile->domain) == true) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!FollowerService::follows($actorProfile->id, $targetProfile->id)) {
|
||||
return;
|
||||
}
|
||||
|
@ -1103,6 +1141,11 @@ class Inbox
|
|||
|
||||
$actorProfile = Helpers::profileFetch($actor);
|
||||
|
||||
|
||||
if(AccountService::blocksDomain($targetProfile->id, $actorProfile->domain) == true) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!FollowerService::follows($actorProfile->id, $targetProfile->id)) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -110,7 +110,8 @@ return [
|
|||
|
||||
'user_filters' => [
|
||||
'max_user_blocks' => env('PF_MAX_USER_BLOCKS', 50),
|
||||
'max_user_mutes' => env('PF_MAX_USER_MUTES', 50)
|
||||
'max_user_mutes' => env('PF_MAX_USER_MUTES', 50),
|
||||
'max_domain_blocks' => env('PF_MAX_DOMAIN_BLOCKS', 50),
|
||||
],
|
||||
|
||||
'reports' => [
|
||||
|
|
|
@ -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('user_domain_blocks', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('profile_id')->index();
|
||||
$table->string('domain')->index();
|
||||
$table->unique(['profile_id', 'domain'], 'user_domain_blocks_by_id');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('user_domain_blocks');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,35 @@
|
|||
<?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('job_batches', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->string('name');
|
||||
$table->integer('total_jobs');
|
||||
$table->integer('pending_jobs');
|
||||
$table->integer('failed_jobs');
|
||||
$table->longText('failed_job_ids');
|
||||
$table->mediumText('options')->nullable();
|
||||
$table->integer('cancelled_at')->nullable();
|
||||
$table->integer('created_at');
|
||||
$table->integer('finished_at')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('job_batches');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use App\StatusHashtag;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
StatusHashtag::doesntHave('status')->lazyById(200)->each->deleteQuietly();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
};
|
|
@ -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('default_domain_blocks', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('domain')->unique()->index();
|
||||
$table->text('note')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('default_domain_blocks');
|
||||
}
|
||||
};
|
|
@ -12,4 +12,10 @@ return [
|
|||
|
||||
'status.disabled.header' => 'Profile Unavailable',
|
||||
'status.disabled.body' => 'Sorry, this profile is not available at the moment. Please try again shortly.',
|
||||
|
||||
'block.domain.max' => 'Max limit of domain blocks reached! You can only block :max domains at a time. Ask your admin to adjust this limit.',
|
||||
|
||||
'mutedAccounts' => 'Muted Accounts',
|
||||
'blockedAccounts' => 'Blocked Accounts',
|
||||
'blockedDomains' => 'Blocked Domains',
|
||||
];
|
||||
|
|
|
@ -8,8 +8,9 @@
|
|||
<hr>
|
||||
<div class="form-group pb-1">
|
||||
<p>
|
||||
<a class="btn btn-outline-secondary py-0 font-weight-bold" href="{{route('settings.privacy.muted-users')}}">Muted Users</a>
|
||||
<a class="btn btn-outline-secondary py-0 font-weight-bold" href="{{route('settings.privacy.blocked-users')}}">Blocked Users</a>
|
||||
<a class="btn btn-link py-0 font-weight-bold" href="{{route('settings.privacy.muted-users')}}">{{ __('profile.mutedAccounts') }}</a>
|
||||
<a class="btn btn-link py-0 font-weight-bold" href="{{route('settings.privacy.blocked-users')}}">{{ __('profile.blockedAccounts') }}</a>
|
||||
<a class="btn btn-link py-0 font-weight-bold" href="{{route('settings.privacy.domain-blocks')}}">{{ __('profile.blockedDomains') }}</a>
|
||||
</p>
|
||||
</div>
|
||||
<form method="post">
|
||||
|
|
|
@ -2,40 +2,36 @@
|
|||
|
||||
@section('section')
|
||||
|
||||
<div class="title">
|
||||
<h3 class="font-weight-bold">Blocked Users</h3>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="title d-flex align-items-center" style="gap: 1rem;">
|
||||
<p class="mb-0"><a href="/settings/privacy"><i class="far fa-chevron-left fa-lg"></i></a></p>
|
||||
<h3 class="font-weight-bold mb-0">Blocked Accounts</h3>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="form-group pb-1">
|
||||
<p>
|
||||
<a class="btn btn-outline-secondary py-0 font-weight-bold" href="{{route('settings.privacy.muted-users')}}">Muted Users</a>
|
||||
<a class="btn btn-outline-primary py-0 font-weight-bold" href="{{route('settings.privacy.blocked-users')}}">Blocked Users</a>
|
||||
{{-- <a class="btn btn-outline-secondary py-0 font-weight-bold" href="{{route('settings.privacy.blocked-keywords')}}">Blocked keywords</a>
|
||||
<a class="btn btn-outline-secondary py-0 font-weight-bold" href="{{route('settings.privacy.blocked-instances')}}">Blocked instances</a> --}}
|
||||
</p>
|
||||
</div>
|
||||
@if($users->count() > 0)
|
||||
<ul class="list-group list-group-flush">
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
@if($users->count() > 0)
|
||||
<div class="list-group">
|
||||
@foreach($users as $user)
|
||||
<li class="list-group-item">
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex justify-content-between align-items-center font-weight-bold">
|
||||
<span><a href="{{$user->url()}}" class="text-decoration-none text-dark"><img class="rounded-circle mr-3" src="{{$user->avatarUrl()}}" width="32px">{{$user->username}}</a></span>
|
||||
<span><a href="{{$user->url()}}" class="text-decoration-none text-dark"><img class="rounded-circle mr-3" src="{{$user->avatarUrl()}}" width="32px" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg?v=0';">{{$user->username}}</a></span>
|
||||
<span class="btn-group">
|
||||
<form method="post">
|
||||
@csrf
|
||||
<input type="hidden" name="profile_id" value="{{$user->id}}">
|
||||
<button type="submit" class="btn btn-outline-secondary btn-sm px-3 font-weight-bold">Unblock</button>
|
||||
<button type="submit" class="btn btn-link btn-sm px-3 font-weight-bold">Unblock</button>
|
||||
</form>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
<div class="d-flex justify-content-center mt-3 font-weight-bold">
|
||||
{{$users->links()}}
|
||||
</div>
|
||||
@else
|
||||
<p class="lead">You are not blocking any accounts.</p>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
<div class="d-flex justify-content-center mt-3 font-weight-bold">
|
||||
{{$users->links()}}
|
||||
</div>
|
||||
@else
|
||||
<p class="lead text-center font-weight-bold">You are not blocking any accounts.</p>
|
||||
@endif
|
||||
|
||||
@endsection
|
272
resources/views/settings/privacy/domain-blocks.blade.php
Normal file
272
resources/views/settings/privacy/domain-blocks.blade.php
Normal file
|
@ -0,0 +1,272 @@
|
|||
@extends('settings.template-vue')
|
||||
|
||||
@section('section')
|
||||
<div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="title d-flex align-items-center" style="gap: 1rem;">
|
||||
<p class="mb-0"><a href="/settings/privacy"><i class="far fa-chevron-left fa-lg"></i></a></p>
|
||||
<h3 class="font-weight-bold mb-0">Domain Blocks</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="mt-3 mb-n2 small">You can block entire domains, this prevents users on that instance from interacting with your content and from you seeing content from that domain on public feeds.</p>
|
||||
|
||||
<hr />
|
||||
|
||||
<div v-if="!loaded" class="d-flex justify-content-center align-items-center flex-grow-1">
|
||||
<b-spinner />
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div class="mb-3 d-flex flex-column flex-md-row justify-content-between align-items-center" style="gap: 2rem;">
|
||||
<div style="width: 60%;">
|
||||
<div class="input-group align-items-center">
|
||||
<input class="form-control form-control-sm rounded-lg" v-model="q" placeholder="Search by domain..." style="padding-right: 60px;" :disabled="!blocks || !blocks.length">
|
||||
<div style="margin-left: -60px;width: 60px;z-index:3">
|
||||
<button class="btn btn-link" type="button" style="font-size: 12px;text-decoration: none;" v-html="q && q.length ? 'Clear': ' '" @click="searchAction()"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-outline-primary btn-sm font-weight-bold px-3 flex-grow" @click="openModal">
|
||||
<i class="fas fa-plus mr-1"></i> New Block
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="blocks && blocks.length" class="list-group">
|
||||
<div
|
||||
v-for="(item, idx) in chunks[index]"
|
||||
class="list-group-item">
|
||||
<div class="d-flex justify-content-between align-items-center font-weight-bold">
|
||||
<span>
|
||||
<span v-text="item"></span>
|
||||
</span>
|
||||
<span class="btn-group">
|
||||
<button type="button" class="btn btn-link btn-sm px-3 font-weight-bold" @click="handleUnblock(item)">Unblock</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav v-if="blocks && blocks.length && chunks && chunks.length > 1" class="mt-3" aria-label="Domain block pagination">
|
||||
<ul class="pagination justify-content-center" style="gap: 1rem">
|
||||
<li
|
||||
class="page-item"
|
||||
:class="[ !index ? 'disabled' : 'font-weight-bold' ]"
|
||||
:disabled="!index"
|
||||
@click="paginate('prev')">
|
||||
<span class="page-link px-5 rounded-lg">Previous</span>
|
||||
</li>
|
||||
<li
|
||||
class="page-item"
|
||||
:class="[ index + 1 === chunks.length ? 'disabled' : 'font-weight-bold' ]"
|
||||
@click="paginate('next')">
|
||||
<span class="page-link px-5 rounded-lg" href="#">Next</span>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div v-if="!blocks || !blocks.length">
|
||||
<hr />
|
||||
<p class="lead text-center font-weight-bold">You are not blocking any domains.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script type="text/javascript">
|
||||
let app = new Vue({
|
||||
el: '#content',
|
||||
|
||||
data: {
|
||||
loaded: false,
|
||||
q: undefined,
|
||||
blocks: [],
|
||||
filteredBlocks: [],
|
||||
chunks: [],
|
||||
index: 0,
|
||||
pagination: [],
|
||||
},
|
||||
|
||||
watch: {
|
||||
q: function(newVal, oldVal) {
|
||||
this.filterResults(newVal)
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.fetchBlocks()
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchBlocks() {
|
||||
axios.get('/api/v1/domain_blocks', { params: { 'limit': 200 }})
|
||||
.then(res => {
|
||||
let pages = false
|
||||
if(res.headers?.link) {
|
||||
pages = this.parseLinkHeader(res.headers['link'])
|
||||
}
|
||||
this.blocks = res.data
|
||||
if(!pages || !pages.hasOwnProperty('next')) {
|
||||
this.buildList()
|
||||
} else {
|
||||
this.handlePagination(pages)
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err.response)
|
||||
})
|
||||
},
|
||||
|
||||
handlePagination(pages) {
|
||||
if(!pages || !pages.hasOwnProperty('next')) {
|
||||
this.buildList()
|
||||
return
|
||||
}
|
||||
this.pagination = pages
|
||||
this.fetchPagination()
|
||||
},
|
||||
|
||||
buildList() {
|
||||
this.index = 0
|
||||
this.chunks = this.chunkify(this.blocks)
|
||||
this.loaded = true
|
||||
},
|
||||
|
||||
buildSearchList() {
|
||||
this.index = 0
|
||||
this.chunks = this.chunkify(this.filteredBlocks)
|
||||
this.loaded = true
|
||||
},
|
||||
|
||||
fetchPagination() {
|
||||
axios.get(this.pagination.next)
|
||||
.then(res => {
|
||||
let pages = false
|
||||
if(res.headers?.link) {
|
||||
pages = this.parseLinkHeader(res.headers['link'])
|
||||
}
|
||||
this.blocks.push(...res.data)
|
||||
if(!pages || !pages.hasOwnProperty('next')) {
|
||||
this.buildList()
|
||||
} else {
|
||||
this.handlePagination(pages)
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
this.buildList()
|
||||
})
|
||||
},
|
||||
|
||||
handleUnblock(domain) {
|
||||
this.loaded = false
|
||||
axios.delete('/api/v1/domain_blocks', {
|
||||
params: {
|
||||
domain: domain
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
this.blocks = this.blocks.filter(d => d != domain)
|
||||
this.buildList()
|
||||
})
|
||||
.catch(err => {
|
||||
this.buildList()
|
||||
})
|
||||
},
|
||||
|
||||
filterResults(query) {
|
||||
this.loaded = false
|
||||
let formattedQuery = query.trim().toLowerCase()
|
||||
this.filteredBlocks = this.blocks.filter(domain => domain.toLowerCase().startsWith(formattedQuery))
|
||||
this.buildSearchList()
|
||||
},
|
||||
|
||||
searchAction($event) {
|
||||
event.currentTarget.blur()
|
||||
this.q = ''
|
||||
},
|
||||
|
||||
openModal() {
|
||||
swal({
|
||||
title: 'Domain Block',
|
||||
text: 'Add domain to block, must start with https://',
|
||||
content: "input",
|
||||
button: {
|
||||
text: "Block",
|
||||
closeModal: false,
|
||||
}
|
||||
}).then(val => {
|
||||
if (!val) {
|
||||
swal.stopLoading()
|
||||
swal.close()
|
||||
return
|
||||
}
|
||||
|
||||
axios.post('/api/v1/domain_blocks', { domain: val })
|
||||
.then(res => {
|
||||
let parsedUrl = new URL(val)
|
||||
swal.stopLoading()
|
||||
swal.close()
|
||||
this.index = 0
|
||||
this.blocks.unshift(parsedUrl.hostname)
|
||||
this.buildList()
|
||||
})
|
||||
.catch(err => {
|
||||
swal.stopLoading()
|
||||
swal.close()
|
||||
if(err.response?.data?.message || err.response?.data?.error) {
|
||||
swal('Error', err.response?.data?.message ?? err.response?.data?.error, 'error')
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
chunkify(arr, len = 10) {
|
||||
var chunks = [],
|
||||
i = 0,
|
||||
n = arr.length
|
||||
|
||||
while (i < n) {
|
||||
chunks.push(arr.slice(i, i += len))
|
||||
}
|
||||
|
||||
return chunks
|
||||
},
|
||||
|
||||
paginate(dir) {
|
||||
if(dir === 'prev' && this.index > 0) {
|
||||
this.index--
|
||||
return
|
||||
}
|
||||
|
||||
if(dir === 'next' && this.index + 1 < this.chunks.length) {
|
||||
this.index++
|
||||
return
|
||||
}
|
||||
},
|
||||
|
||||
parseLinkHeader(linkHeader) {
|
||||
const links = {}
|
||||
|
||||
if (!linkHeader) {
|
||||
return links
|
||||
}
|
||||
|
||||
linkHeader.split(',').forEach(part => {
|
||||
const match = part.match(/<([^>]+)>;\s*rel="([^"]+)"/)
|
||||
if (match) {
|
||||
const url = match[1]
|
||||
const rel = match[2]
|
||||
|
||||
if (rel === 'prev' || rel === 'next') {
|
||||
links[rel] = url
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return links
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@endpush
|
|
@ -1,41 +1,35 @@
|
|||
@extends('settings.template')
|
||||
|
||||
@section('section')
|
||||
|
||||
<div class="title">
|
||||
<h3 class="font-weight-bold">Muted Users</h3>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="title d-flex align-items-center" style="gap: 1rem;">
|
||||
<p class="mb-0"><a href="/settings/privacy"><i class="far fa-chevron-left fa-lg"></i></a></p>
|
||||
<h3 class="font-weight-bold mb-0">Muted Accounts</h3>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="form-group pb-1">
|
||||
<p>
|
||||
<a class="btn btn-outline-primary py-0 font-weight-bold" href="{{route('settings.privacy.muted-users')}}">Muted Users</a>
|
||||
<a class="btn btn-outline-secondary py-0 font-weight-bold" href="{{route('settings.privacy.blocked-users')}}">Blocked Users</a>
|
||||
{{-- <a class="btn btn-outline-secondary py-0 font-weight-bold" href="{{route('settings.privacy.blocked-keywords')}}">Blocked keywords</a>
|
||||
<a class="btn btn-outline-secondary py-0 font-weight-bold" href="{{route('settings.privacy.blocked-instances')}}">Blocked instances</a> --}}
|
||||
</p>
|
||||
</div>
|
||||
@if($users->count() > 0)
|
||||
<ul class="list-group list-group-flush">
|
||||
</div>
|
||||
<hr />
|
||||
@if($users->count() > 0)
|
||||
<div class="list-group">
|
||||
@foreach($users as $user)
|
||||
<li class="list-group-item">
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex justify-content-between align-items-center font-weight-bold">
|
||||
<span><a href="{{$user->url()}}" class="text-decoration-none text-dark"><img class="rounded-circle mr-3" src="{{$user->avatarUrl()}}" width="32px">{{$user->username}}</a></span>
|
||||
<span><a href="{{$user->url()}}" class="text-decoration-none text-dark"><img class="rounded-circle mr-3" src="{{$user->avatarUrl()}}" width="32px" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg?v=0';">{{$user->username}}</a></span>
|
||||
<span class="btn-group">
|
||||
<form method="post">
|
||||
@csrf
|
||||
<input type="hidden" name="profile_id" value="{{$user->id}}">
|
||||
<button type="submit" class="btn btn-outline-secondary btn-sm px-3 font-weight-bold">Unmute</button>
|
||||
<button type="submit" class="btn btn-link btn-sm px-3 font-weight-bold">Unmute</button>
|
||||
</form>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
<div class="d-flex justify-content-center mt-3 font-weight-bold">
|
||||
{{$users->links()}}
|
||||
</div>
|
||||
@else
|
||||
<p class="lead">You are not muting any accounts.</p>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
<div class="d-flex justify-content-center mt-3 font-weight-bold">
|
||||
{{$users->links()}}
|
||||
</div>
|
||||
@else
|
||||
<p class="lead text-center font-weight-bold">You are not muting any accounts.</p>
|
||||
@endif
|
||||
|
||||
@endsection
|
37
resources/views/settings/template-vue.blade.php
Normal file
37
resources/views/settings/template-vue.blade.php
Normal file
|
@ -0,0 +1,37 @@
|
|||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
@if (session('status'))
|
||||
<div class="alert alert-primary px-3 h6 text-center">
|
||||
{{ session('status') }}
|
||||
</div>
|
||||
@endif
|
||||
@if ($errors->any())
|
||||
<div class="alert alert-danger px-3 h6 text-center">
|
||||
@foreach($errors->all() as $error)
|
||||
<p class="font-weight-bold mb-1">{{ $error }}</p>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
@if (session('error'))
|
||||
<div class="alert alert-danger px-3 h6 text-center">
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="container">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-none border mt-5">
|
||||
<div class="card-body p-0">
|
||||
<div class="row">
|
||||
@include('settings.partial.sidebar')
|
||||
<div class="col-12 col-md-9 p-5">
|
||||
@yield('section')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
|
@ -51,9 +51,9 @@ Route::group(['prefix' => 'api'], function() use($middleware) {
|
|||
Route::get('blocks', 'Api\ApiV1Controller@accountBlocks')->middleware($middleware);
|
||||
Route::get('conversations', 'Api\ApiV1Controller@conversations')->middleware($middleware);
|
||||
Route::get('custom_emojis', 'Api\ApiV1Controller@customEmojis');
|
||||
Route::get('domain_blocks', 'Api\ApiV1Controller@accountDomainBlocks')->middleware($middleware);
|
||||
Route::post('domain_blocks', 'Api\ApiV1Controller@accountDomainBlocks')->middleware($middleware);
|
||||
Route::delete('domain_blocks', 'Api\ApiV1Controller@accountDomainBlocks')->middleware($middleware);
|
||||
Route::get('domain_blocks', 'Api\V1\DomainBlockController@index')->middleware($middleware);
|
||||
Route::post('domain_blocks', 'Api\V1\DomainBlockController@store')->middleware($middleware);
|
||||
Route::delete('domain_blocks', 'Api\V1\DomainBlockController@delete')->middleware($middleware);
|
||||
Route::get('endorsements', 'Api\ApiV1Controller@accountEndorsements')->middleware($middleware);
|
||||
Route::get('favourites', 'Api\ApiV1Controller@accountFavourites')->middleware($middleware);
|
||||
Route::get('filters', 'Api\ApiV1Controller@accountFilters')->middleware($middleware);
|
||||
|
|
|
@ -489,6 +489,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
|
|||
Route::post('privacy/muted-users', 'SettingsController@mutedUsersUpdate');
|
||||
Route::get('privacy/blocked-users', 'SettingsController@blockedUsers')->name('settings.privacy.blocked-users');
|
||||
Route::post('privacy/blocked-users', 'SettingsController@blockedUsersUpdate');
|
||||
Route::get('privacy/domain-blocks', 'SettingsController@domainBlocks')->name('settings.privacy.domain-blocks');
|
||||
Route::get('privacy/blocked-instances', 'SettingsController@blockedInstances')->name('settings.privacy.blocked-instances');
|
||||
Route::post('privacy/blocked-instances', 'SettingsController@blockedInstanceStore');
|
||||
Route::post('privacy/blocked-instances/unblock', 'SettingsController@blockedInstanceUnblock')->name('settings.privacy.blocked-instances.unblock');
|
||||
|
|
Loading…
Reference in a new issue