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

View file

@ -43,6 +43,7 @@ use App\Transformer\Api\{
use App\Http\Controllers\FollowerController;
use League\Fractal\Serializer\ArraySerializer;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use App\Http\Controllers\AccountController;
use App\Http\Controllers\StatusController;
use App\Jobs\AvatarPipeline\AvatarOptimize;
@ -939,6 +940,25 @@ class ApiV1Controller extends Controller
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($pid)->whereFollowingId($profile->id)->delete();
Notification::whereProfileId($pid)->whereActorId($profile->id)->delete();
@ -950,8 +970,6 @@ class ApiV1Controller extends Controller
'filter_type' => 'block',
]);
Cache::forget("user:filter:list:$pid");
Cache::forget("api:local:exp:rec:$pid");
RelationshipService::refresh($pid, $id);
$resource = new Fractal\Resource\Item($profile, new RelationshipTransformer());
@ -980,15 +998,17 @@ class ApiV1Controller extends Controller
$profile = Profile::findOrFail($id);
UserFilter::whereUserId($pid)
$filter = UserFilter::whereUserId($pid)
->whereFilterableId($profile->id)
->whereFilterableType('App\Profile')
->whereFilterType('block')
->delete();
->first();
Cache::forget("user:filter:list:$pid");
Cache::forget("api:local:exp:rec:$pid");
RelationshipService::refresh($pid, $id);
if($filter) {
$filter->delete();
UserFilterService::unblock($pid, $profile->id);
RelationshipService::refresh($pid, $id);
}
$resource = new Fractal\Resource\Item($profile, new RelationshipTransformer());
$res = $this->fractal->createData($resource)->toArray();
@ -1823,6 +1843,25 @@ class ApiV1Controller extends Controller
$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([
'user_id' => $pid,
'filterable_id' => $account->id,
@ -1830,9 +1869,6 @@ class ApiV1Controller extends Controller
'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);
$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);
}
$account = Profile::findOrFail($id);
$profile = Profile::findOrFail($id);
$filter = UserFilter::whereUserId($pid)
->whereFilterableId($account->id)
->whereFilterableId($profile->id)
->whereFilterableType('App\Profile')
->whereFilterType('mute')
->first();
if($filter) {
$filter->delete();
Cache::forget("user:filter:list:$pid");
Cache::forget("feature:discover:posts:$pid");
Cache::forget("api:local:exp:rec:$pid");
UserFilterService::unmute($pid, $profile->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();
return $this->json($res);
}

View file

@ -14,7 +14,7 @@ class UserFilterService
public static function mutes(int $profile_id)
{
$key = self::USER_MUTES_KEY . $profile_id;
$warm = Cache::has($key . ':cached');
$warm = Cache::has($key . ':cached-v0');
if($warm) {
return Redis::zrevrange($key, 0, -1) ?? [];
} else {
@ -24,11 +24,22 @@ class UserFilterService
$ids = UserFilter::whereFilterType('mute')
->whereUserId($profile_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();
foreach ($ids as $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;
}
}
@ -36,7 +47,7 @@ class UserFilterService
public static function blocks(int $profile_id)
{
$key = self::USER_BLOCKS_KEY . $profile_id;
$warm = Cache::has($key . ':cached');
$warm = Cache::has($key . ':cached-v0');
if($warm) {
return Redis::zrevrange($key, 0, -1) ?? [];
} else {
@ -46,11 +57,22 @@ class UserFilterService
$ids = UserFilter::whereFilterType('block')
->whereUserId($profile_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();
foreach ($ids as $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;
}
}
@ -62,6 +84,9 @@ class UserFilterService
public static function mute(int $profile_id, int $muted_id)
{
if($profile_id == $muted_id) {
return false;
}
$key = self::USER_MUTES_KEY . $profile_id;
$mutes = self::mutes($profile_id);
$exists = in_array($muted_id, $mutes);
@ -73,6 +98,9 @@ class UserFilterService
public static function unmute(int $profile_id, string $muted_id)
{
if($profile_id == $muted_id) {
return false;
}
$key = self::USER_MUTES_KEY . $profile_id;
$mutes = self::mutes($profile_id);
$exists = in_array($muted_id, $mutes);
@ -84,6 +112,9 @@ class UserFilterService
public static function block(int $profile_id, int $blocked_id)
{
if($profile_id == $blocked_id) {
return false;
}
$key = self::USER_BLOCKS_KEY . $profile_id;
$exists = in_array($blocked_id, self::blocks($profile_id));
if(!$exists) {
@ -94,6 +125,9 @@ class UserFilterService
public static function unblock(int $profile_id, string $blocked_id)
{
if($profile_id == $blocked_id) {
return false;
}
$key = self::USER_BLOCKS_KEY . $profile_id;
$exists = in_array($blocked_id, self::blocks($profile_id));
if($exists) {

View file

@ -107,4 +107,9 @@ return [
'admin_invites' => [
'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)
]
];