Răsfoiți Sursa

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

Daniel Supernault 2 ani în urmă
părinte
comite
5b879f0156

+ 83 - 70
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;
-                }
-            }
-            return false;
-        } else {
+	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;
+		}
+	}
 
 	public function accountRestored(Request $request)
 	{

+ 50 - 16
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);
 	}

+ 38 - 4
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) {

+ 5 - 0
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)
+	]
 ];