1
0
Fork 0
mirror of https://github.com/YunoHost-Apps/pixelfed_ynh.git synced 2024-09-03 20:06:04 +02:00

Update mute/block logic with admin defined limits and improved filtering to skip deleted accounts

This commit is contained in:
Daniel Supernault 2023-03-01 04:16:42 -07:00
parent 18940cb209
commit 5b879f0156
No known key found for this signature in database
GPG key ID: 0DEF1C662C9033F7
4 changed files with 176 additions and 90 deletions

View file

@ -17,16 +17,20 @@ use App\{
EmailVerification, EmailVerification,
Follower, Follower,
FollowRequest, FollowRequest,
Media,
Notification, Notification,
Profile, Profile,
User, User,
UserFilter UserDevice,
UserFilter,
UserSetting
}; };
use League\Fractal; use League\Fractal;
use League\Fractal\Serializer\ArraySerializer; use League\Fractal\Serializer\ArraySerializer;
use League\Fractal\Pagination\IlluminatePaginatorAdapter; use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use App\Transformer\Api\Mastodon\v1\AccountTransformer; use App\Transformer\Api\Mastodon\v1\AccountTransformer;
use App\Services\AccountService; use App\Services\AccountService;
use App\Services\NotificationService;
use App\Services\UserFilterService; use App\Services\UserFilterService;
use App\Services\RelationshipService; use App\Services\RelationshipService;
use App\Jobs\FollowPipeline\FollowAcceptPipeline; use App\Jobs\FollowPipeline\FollowAcceptPipeline;
@ -39,7 +43,8 @@ class AccountController extends Controller
'user.block', 'user.block',
]; ];
const FILTER_LIMIT = 'You cannot block or mute more than 100 accounts'; const FILTER_LIMIT_MUTE_TEXT = 'You cannot mute more than ';
const FILTER_LIMIT_BLOCK_TEXT = 'You cannot block more than ';
public function __construct() public function __construct()
{ {
@ -145,16 +150,17 @@ class AccountController extends Controller
public function mute(Request $request) public function mute(Request $request)
{ {
$this->validate($request, [ $this->validate($request, [
'type' => 'required|alpha_dash', 'type' => 'required|string|in:user',
'item' => 'required|integer|min:1', 'item' => 'required|integer|min:1',
]); ]);
$user = Auth::user()->profile; $pid = $request->user()->profile_id;
$count = UserFilterService::muteCount($user->id); $count = UserFilterService::muteCount($pid);
abort_if($count >= 100, 422, self::FILTER_LIMIT); $maxLimit = intval(config('instance.user_filters.max_user_mutes'));
abort_if($count >= $maxLimit, 422, self::FILTER_LIMIT_MUTE_TEXT . $maxLimit . ' accounts');
if($count == 0) { if($count == 0) {
$filterCount = UserFilter::whereUserId($user->id)->count(); $filterCount = UserFilter::whereUserId($pid)->count();
abort_if($filterCount >= 100, 422, self::FILTER_LIMIT); abort_if($filterCount >= $maxLimit, 422, self::FILTER_LIMIT_MUTE_TEXT . $maxLimit . ' accounts');
} }
$type = $request->input('type'); $type = $request->input('type');
$item = $request->input('item'); $item = $request->input('item');
@ -167,7 +173,7 @@ class AccountController extends Controller
switch ($type) { switch ($type) {
case 'user': case 'user':
$profile = Profile::findOrFail($item); $profile = Profile::findOrFail($item);
if ($profile->id == $user->id) { if ($profile->id == $pid) {
return abort(403); return abort(403);
} }
$class = get_class($profile); $class = get_class($profile);
@ -177,29 +183,30 @@ class AccountController extends Controller
} }
$filter = UserFilter::firstOrCreate([ $filter = UserFilter::firstOrCreate([
'user_id' => $user->id, 'user_id' => $pid,
'filterable_id' => $filterable['id'], 'filterable_id' => $filterable['id'],
'filterable_type' => $filterable['type'], 'filterable_type' => $filterable['type'],
'filter_type' => 'mute', 'filter_type' => 'mute',
]); ]);
$pid = $user->id; UserFilterService::mute($pid, $filterable['id']);
Cache::forget("user:filter:list:$pid"); $res = RelationshipService::refresh($pid, $profile->id);
Cache::forget("feature:discover:posts:$pid");
Cache::forget("api:local:exp:rec:$pid");
RelationshipService::refresh($pid, $profile->id);
return redirect()->back(); if($request->wantsJson()) {
return response()->json($res);
} else {
return redirect()->back();
}
} }
public function unmute(Request $request) public function unmute(Request $request)
{ {
$this->validate($request, [ $this->validate($request, [
'type' => 'required|alpha_dash', 'type' => 'required|string|in:user',
'item' => 'required|integer|min:1', 'item' => 'required|integer|min:1',
]); ]);
$user = Auth::user()->profile; $pid = $request->user()->profile_id;
$type = $request->input('type'); $type = $request->input('type');
$item = $request->input('item'); $item = $request->input('item');
$action = $type . '.mute'; $action = $type . '.mute';
@ -211,7 +218,7 @@ class AccountController extends Controller
switch ($type) { switch ($type) {
case 'user': case 'user':
$profile = Profile::findOrFail($item); $profile = Profile::findOrFail($item);
if ($profile->id == $user->id) { if ($profile->id == $pid) {
return abort(403); return abort(403);
} }
$class = get_class($profile); $class = get_class($profile);
@ -224,24 +231,21 @@ class AccountController extends Controller
break; break;
} }
$filter = UserFilter::whereUserId($user->id) $filter = UserFilter::whereUserId($pid)
->whereFilterableId($filterable['id']) ->whereFilterableId($filterable['id'])
->whereFilterableType($filterable['type']) ->whereFilterableType($filterable['type'])
->whereFilterType('mute') ->whereFilterType('mute')
->first(); ->first();
if($filter) { if($filter) {
UserFilterService::unmute($pid, $filterable['id']);
$filter->delete(); $filter->delete();
} }
$pid = $user->id; $res = RelationshipService::refresh($pid, $profile->id);
Cache::forget("user:filter:list:$pid");
Cache::forget("feature:discover:posts:$pid");
Cache::forget("api:local:exp:rec:$pid");
RelationshipService::refresh($pid, $profile->id);
if($request->wantsJson()) { if($request->wantsJson()) {
return response()->json([200]); return response()->json($res);
} else { } else {
return redirect()->back(); return redirect()->back();
} }
@ -250,16 +254,16 @@ class AccountController extends Controller
public function block(Request $request) public function block(Request $request)
{ {
$this->validate($request, [ $this->validate($request, [
'type' => 'required|alpha_dash', 'type' => 'required|string|in:user',
'item' => 'required|integer|min:1', 'item' => 'required|integer|min:1',
]); ]);
$pid = $request->user()->profile_id;
$user = Auth::user()->profile; $count = UserFilterService::blockCount($pid);
$count = UserFilterService::blockCount($user->id); $maxLimit = intval(config('instance.user_filters.max_user_blocks'));
abort_if($count >= 100, 422, self::FILTER_LIMIT); abort_if($count >= $maxLimit, 422, self::FILTER_LIMIT_BLOCK_TEXT . $maxLimit . ' accounts');
if($count == 0) { if($count == 0) {
$filterCount = UserFilter::whereUserId($user->id)->count(); $filterCount = UserFilter::whereUserId($pid)->whereFilterType('block')->count();
abort_if($filterCount >= 100, 422, self::FILTER_LIMIT); abort_if($filterCount >= $maxLimit, 422, self::FILTER_LIMIT_BLOCK_TEXT . $maxLimit . ' accounts');
} }
$type = $request->input('type'); $type = $request->input('type');
$item = $request->input('item'); $item = $request->input('item');
@ -271,41 +275,49 @@ class AccountController extends Controller
switch ($type) { switch ($type) {
case 'user': case 'user':
$profile = Profile::findOrFail($item); $profile = Profile::findOrFail($item);
if ($profile->id == $user->id || ($profile->user && $profile->user->is_admin == true)) { if ($profile->id == $pid || ($profile->user && $profile->user->is_admin == true)) {
return abort(403); return abort(403);
} }
$class = get_class($profile); $class = get_class($profile);
$filterable['id'] = $profile->id; $filterable['id'] = $profile->id;
$filterable['type'] = $class; $filterable['type'] = $class;
Follower::whereProfileId($profile->id)->whereFollowingId($user->id)->delete(); Follower::whereProfileId($profile->id)->whereFollowingId($pid)->delete();
Notification::whereProfileId($user->id)->whereActorId($profile->id)->delete(); Notification::whereProfileId($pid)
->whereActorId($profile->id)
->get()
->map(function($n) use($pid) {
NotificationService::del($pid, $n['id']);
$n->forceDelete();
});
break; break;
} }
$filter = UserFilter::firstOrCreate([ $filter = UserFilter::firstOrCreate([
'user_id' => $user->id, 'user_id' => $pid,
'filterable_id' => $filterable['id'], 'filterable_id' => $filterable['id'],
'filterable_type' => $filterable['type'], 'filterable_type' => $filterable['type'],
'filter_type' => 'block', 'filter_type' => 'block',
]); ]);
$pid = $user->id; UserFilterService::block($pid, $filterable['id']);
Cache::forget("user:filter:list:$pid"); $res = RelationshipService::refresh($pid, $profile->id);
Cache::forget("api:local:exp:rec:$pid");
RelationshipService::refresh($pid, $profile->id);
return redirect()->back(); if($request->wantsJson()) {
return response()->json($res);
} else {
return redirect()->back();
}
} }
public function unblock(Request $request) public function unblock(Request $request)
{ {
$this->validate($request, [ $this->validate($request, [
'type' => 'required|alpha_dash', 'type' => 'required|string|in:user',
'item' => 'required|integer|min:1', 'item' => 'required|integer|min:1',
]); ]);
$user = Auth::user()->profile; $pid = $request->user()->profile_id;
$type = $request->input('type'); $type = $request->input('type');
$item = $request->input('item'); $item = $request->input('item');
$action = $type . '.block'; $action = $type . '.block';
@ -316,7 +328,7 @@ class AccountController extends Controller
switch ($type) { switch ($type) {
case 'user': case 'user':
$profile = Profile::findOrFail($item); $profile = Profile::findOrFail($item);
if ($profile->id == $user->id) { if ($profile->id == $pid) {
return abort(403); return abort(403);
} }
$class = get_class($profile); $class = get_class($profile);
@ -330,23 +342,24 @@ class AccountController extends Controller
} }
$filter = UserFilter::whereUserId($user->id) $filter = UserFilter::whereUserId($pid)
->whereFilterableId($filterable['id']) ->whereFilterableId($filterable['id'])
->whereFilterableType($filterable['type']) ->whereFilterableType($filterable['type'])
->whereFilterType('block') ->whereFilterType('block')
->first(); ->first();
if($filter) { if($filter) {
UserFilterService::unblock($pid, $filterable['id']);
$filter->delete(); $filter->delete();
} }
$pid = $user->id; $res = RelationshipService::refresh($pid, $profile->id);
Cache::forget("user:filter:list:$pid");
Cache::forget("feature:discover:posts:$pid");
Cache::forget("api:local:exp:rec:$pid");
RelationshipService::refresh($pid, $profile->id);
return redirect()->back(); if($request->wantsJson()) {
return response()->json($res);
} else {
return redirect()->back();
}
} }
public function followRequests(Request $request) public function followRequests(Request $request)
@ -513,25 +526,25 @@ class AccountController extends Controller
} }
} }
protected function twoFactorBackupCheck($request, $code, User $user) protected function twoFactorBackupCheck($request, $code, User $user)
{ {
$backupCodes = $user->{'2fa_backup_codes'}; $backupCodes = $user->{'2fa_backup_codes'};
if($backupCodes) { if($backupCodes) {
$codes = json_decode($backupCodes, true); $codes = json_decode($backupCodes, true);
foreach ($codes as $c) { foreach ($codes as $c) {
if(hash_equals($c, $code)) { if(hash_equals($c, $code)) {
$codes = array_flatten(array_diff($codes, [$code])); $codes = array_flatten(array_diff($codes, [$code]));
$user->{'2fa_backup_codes'} = json_encode($codes); $user->{'2fa_backup_codes'} = json_encode($codes);
$user->save(); $user->save();
$request->session()->push('2fa.session.active', true); $request->session()->push('2fa.session.active', true);
return true; return true;
} }
} }
return false; return false;
} else { } else {
return false; return false;
} }
} }
public function accountRestored(Request $request) public function accountRestored(Request $request)
{ {

View file

@ -43,6 +43,7 @@ use App\Transformer\Api\{
use App\Http\Controllers\FollowerController; use App\Http\Controllers\FollowerController;
use League\Fractal\Serializer\ArraySerializer; use League\Fractal\Serializer\ArraySerializer;
use League\Fractal\Pagination\IlluminatePaginatorAdapter; use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use App\Http\Controllers\AccountController;
use App\Http\Controllers\StatusController; use App\Http\Controllers\StatusController;
use App\Jobs\AvatarPipeline\AvatarOptimize; use App\Jobs\AvatarPipeline\AvatarOptimize;
@ -939,6 +940,25 @@ class ApiV1Controller extends Controller
abort(400, 'You cannot block an admin'); abort(400, 'You cannot block an admin');
} }
$count = UserFilterService::blockCount($pid);
$maxLimit = intval(config('instance.user_filters.max_user_blocks'));
if($count == 0) {
$filterCount = UserFilter::whereUserId($pid)
->whereFilterType('block')
->get()
->map(function($rec) {
return AccountService::get($rec->filterable_id, true);
})
->filter(function($account) {
return $account && isset($account['id']);
})
->values()
->count();
abort_if($filterCount >= $maxLimit, 422, AccountController::FILTER_LIMIT_BLOCK_TEXT . $maxLimit . ' accounts');
} else {
abort_if($count >= $maxLimit, 422, AccountController::FILTER_LIMIT_BLOCK_TEXT . $maxLimit . ' accounts');
}
Follower::whereProfileId($profile->id)->whereFollowingId($pid)->delete(); Follower::whereProfileId($profile->id)->whereFollowingId($pid)->delete();
Follower::whereProfileId($pid)->whereFollowingId($profile->id)->delete(); Follower::whereProfileId($pid)->whereFollowingId($profile->id)->delete();
Notification::whereProfileId($pid)->whereActorId($profile->id)->delete(); Notification::whereProfileId($pid)->whereActorId($profile->id)->delete();
@ -950,8 +970,6 @@ class ApiV1Controller extends Controller
'filter_type' => 'block', 'filter_type' => 'block',
]); ]);
Cache::forget("user:filter:list:$pid");
Cache::forget("api:local:exp:rec:$pid");
RelationshipService::refresh($pid, $id); RelationshipService::refresh($pid, $id);
$resource = new Fractal\Resource\Item($profile, new RelationshipTransformer()); $resource = new Fractal\Resource\Item($profile, new RelationshipTransformer());
@ -980,15 +998,17 @@ class ApiV1Controller extends Controller
$profile = Profile::findOrFail($id); $profile = Profile::findOrFail($id);
UserFilter::whereUserId($pid) $filter = UserFilter::whereUserId($pid)
->whereFilterableId($profile->id) ->whereFilterableId($profile->id)
->whereFilterableType('App\Profile') ->whereFilterableType('App\Profile')
->whereFilterType('block') ->whereFilterType('block')
->delete(); ->first();
Cache::forget("user:filter:list:$pid"); if($filter) {
Cache::forget("api:local:exp:rec:$pid"); $filter->delete();
RelationshipService::refresh($pid, $id); UserFilterService::unblock($pid, $profile->id);
RelationshipService::refresh($pid, $id);
}
$resource = new Fractal\Resource\Item($profile, new RelationshipTransformer()); $resource = new Fractal\Resource\Item($profile, new RelationshipTransformer());
$res = $this->fractal->createData($resource)->toArray(); $res = $this->fractal->createData($resource)->toArray();
@ -1823,6 +1843,25 @@ class ApiV1Controller extends Controller
$account = Profile::findOrFail($id); $account = Profile::findOrFail($id);
$count = UserFilterService::muteCount($pid);
$maxLimit = intval(config('instance.user_filters.max_user_mutes'));
if($count == 0) {
$filterCount = UserFilter::whereUserId($pid)
->whereFilterType('mute')
->get()
->map(function($rec) {
return AccountService::get($rec->filterable_id, true);
})
->filter(function($account) {
return $account && isset($account['id']);
})
->values()
->count();
abort_if($filterCount >= $maxLimit, 422, AccountController::FILTER_LIMIT_MUTE_TEXT . $maxLimit . ' accounts');
} else {
abort_if($count >= $maxLimit, 422, AccountController::FILTER_LIMIT_MUTE_TEXT . $maxLimit . ' accounts');
}
$filter = UserFilter::firstOrCreate([ $filter = UserFilter::firstOrCreate([
'user_id' => $pid, 'user_id' => $pid,
'filterable_id' => $account->id, 'filterable_id' => $account->id,
@ -1830,9 +1869,6 @@ class ApiV1Controller extends Controller
'filter_type' => 'mute', 'filter_type' => 'mute',
]); ]);
Cache::forget("user:filter:list:$pid");
Cache::forget("feature:discover:posts:$pid");
Cache::forget("api:local:exp:rec:$pid");
RelationshipService::refresh($pid, $id); RelationshipService::refresh($pid, $id);
$resource = new Fractal\Resource\Item($account, new RelationshipTransformer()); $resource = new Fractal\Resource\Item($account, new RelationshipTransformer());
@ -1858,23 +1894,21 @@ class ApiV1Controller extends Controller
return $this->json(['error' => 'You cannot unmute yourself'], 500); return $this->json(['error' => 'You cannot unmute yourself'], 500);
} }
$account = Profile::findOrFail($id); $profile = Profile::findOrFail($id);
$filter = UserFilter::whereUserId($pid) $filter = UserFilter::whereUserId($pid)
->whereFilterableId($account->id) ->whereFilterableId($profile->id)
->whereFilterableType('App\Profile') ->whereFilterableType('App\Profile')
->whereFilterType('mute') ->whereFilterType('mute')
->first(); ->first();
if($filter) { if($filter) {
$filter->delete(); $filter->delete();
Cache::forget("user:filter:list:$pid"); UserFilterService::unmute($pid, $profile->id);
Cache::forget("feature:discover:posts:$pid");
Cache::forget("api:local:exp:rec:$pid");
RelationshipService::refresh($pid, $id); RelationshipService::refresh($pid, $id);
} }
$resource = new Fractal\Resource\Item($account, new RelationshipTransformer()); $resource = new Fractal\Resource\Item($profile, new RelationshipTransformer());
$res = $this->fractal->createData($resource)->toArray(); $res = $this->fractal->createData($resource)->toArray();
return $this->json($res); return $this->json($res);
} }

View file

@ -14,7 +14,7 @@ class UserFilterService
public static function mutes(int $profile_id) public static function mutes(int $profile_id)
{ {
$key = self::USER_MUTES_KEY . $profile_id; $key = self::USER_MUTES_KEY . $profile_id;
$warm = Cache::has($key . ':cached'); $warm = Cache::has($key . ':cached-v0');
if($warm) { if($warm) {
return Redis::zrevrange($key, 0, -1) ?? []; return Redis::zrevrange($key, 0, -1) ?? [];
} else { } else {
@ -24,11 +24,22 @@ class UserFilterService
$ids = UserFilter::whereFilterType('mute') $ids = UserFilter::whereFilterType('mute')
->whereUserId($profile_id) ->whereUserId($profile_id)
->pluck('filterable_id') ->pluck('filterable_id')
->map(function($id) {
$acct = AccountService::get($id, true);
if(!$acct) {
return false;
}
return $acct['id'];
})
->filter(function($res) {
return $res;
})
->values()
->toArray(); ->toArray();
foreach ($ids as $muted_id) { foreach ($ids as $muted_id) {
Redis::zadd($key, (int) $muted_id, (int) $muted_id); Redis::zadd($key, (int) $muted_id, (int) $muted_id);
} }
Cache::set($key . ':cached', 1, 7776000); Cache::set($key . ':cached-v0', 1, 7776000);
return $ids; return $ids;
} }
} }
@ -36,7 +47,7 @@ class UserFilterService
public static function blocks(int $profile_id) public static function blocks(int $profile_id)
{ {
$key = self::USER_BLOCKS_KEY . $profile_id; $key = self::USER_BLOCKS_KEY . $profile_id;
$warm = Cache::has($key . ':cached'); $warm = Cache::has($key . ':cached-v0');
if($warm) { if($warm) {
return Redis::zrevrange($key, 0, -1) ?? []; return Redis::zrevrange($key, 0, -1) ?? [];
} else { } else {
@ -46,11 +57,22 @@ class UserFilterService
$ids = UserFilter::whereFilterType('block') $ids = UserFilter::whereFilterType('block')
->whereUserId($profile_id) ->whereUserId($profile_id)
->pluck('filterable_id') ->pluck('filterable_id')
->map(function($id) {
$acct = AccountService::get($id, true);
if(!$acct) {
return false;
}
return $acct['id'];
})
->filter(function($res) {
return $res;
})
->values()
->toArray(); ->toArray();
foreach ($ids as $blocked_id) { foreach ($ids as $blocked_id) {
Redis::zadd($key, (int) $blocked_id, (int) $blocked_id); Redis::zadd($key, (int) $blocked_id, (int) $blocked_id);
} }
Cache::set($key . ':cached', 1, 7776000); Cache::set($key . ':cached-v0', 1, 7776000);
return $ids; return $ids;
} }
} }
@ -62,6 +84,9 @@ class UserFilterService
public static function mute(int $profile_id, int $muted_id) public static function mute(int $profile_id, int $muted_id)
{ {
if($profile_id == $muted_id) {
return false;
}
$key = self::USER_MUTES_KEY . $profile_id; $key = self::USER_MUTES_KEY . $profile_id;
$mutes = self::mutes($profile_id); $mutes = self::mutes($profile_id);
$exists = in_array($muted_id, $mutes); $exists = in_array($muted_id, $mutes);
@ -73,6 +98,9 @@ class UserFilterService
public static function unmute(int $profile_id, string $muted_id) public static function unmute(int $profile_id, string $muted_id)
{ {
if($profile_id == $muted_id) {
return false;
}
$key = self::USER_MUTES_KEY . $profile_id; $key = self::USER_MUTES_KEY . $profile_id;
$mutes = self::mutes($profile_id); $mutes = self::mutes($profile_id);
$exists = in_array($muted_id, $mutes); $exists = in_array($muted_id, $mutes);
@ -84,6 +112,9 @@ class UserFilterService
public static function block(int $profile_id, int $blocked_id) public static function block(int $profile_id, int $blocked_id)
{ {
if($profile_id == $blocked_id) {
return false;
}
$key = self::USER_BLOCKS_KEY . $profile_id; $key = self::USER_BLOCKS_KEY . $profile_id;
$exists = in_array($blocked_id, self::blocks($profile_id)); $exists = in_array($blocked_id, self::blocks($profile_id));
if(!$exists) { if(!$exists) {
@ -94,6 +125,9 @@ class UserFilterService
public static function unblock(int $profile_id, string $blocked_id) public static function unblock(int $profile_id, string $blocked_id)
{ {
if($profile_id == $blocked_id) {
return false;
}
$key = self::USER_BLOCKS_KEY . $profile_id; $key = self::USER_BLOCKS_KEY . $profile_id;
$exists = in_array($blocked_id, self::blocks($profile_id)); $exists = in_array($blocked_id, self::blocks($profile_id));
if($exists) { if($exists) {

View file

@ -107,4 +107,9 @@ return [
'admin_invites' => [ 'admin_invites' => [
'enabled' => env('PF_ADMIN_INVITES_ENABLED', true) 'enabled' => env('PF_ADMIN_INVITES_ENABLED', true)
], ],
'user_filters' => [
'max_user_blocks' => env('PF_MAX_USER_BLOCKS', 50),
'max_user_mutes' => env('PF_MAX_USER_MUTES', 50)
]
]; ];