From 5b879f01561fb9d44bdc1386d7d43f6c95cdb88d Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 1 Mar 2023 04:16:42 -0700 Subject: [PATCH] Update mute/block logic with admin defined limits and improved filtering to skip deleted accounts --- app/Http/Controllers/AccountController.php | 153 ++++++++++--------- app/Http/Controllers/Api/ApiV1Controller.php | 66 ++++++-- app/Services/UserFilterService.php | 42 ++++- config/instance.php | 5 + 4 files changed, 176 insertions(+), 90 deletions(-) diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 2dfe0696..9e1c5e73 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -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) { diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index 5030fd84..a6e3359b 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -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); } diff --git a/app/Services/UserFilterService.php b/app/Services/UserFilterService.php index 01e2e405..1dcdc819 100644 --- a/app/Services/UserFilterService.php +++ b/app/Services/UserFilterService.php @@ -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) { diff --git a/config/instance.php b/config/instance.php index a443eb6b..3e550a80 100644 --- a/config/instance.php +++ b/config/instance.php @@ -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) + ] ];