From 4f850e54ad537747568763e0d60d9659989aa34c Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 22 Jun 2023 01:08:15 -0600 Subject: [PATCH 1/3] Update AdminApiController, include more data for getUser method --- .../Controllers/Api/AdminApiController.php | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/app/Http/Controllers/Api/AdminApiController.php b/app/Http/Controllers/Api/AdminApiController.php index ad53ce0d3..12b45f7ba 100644 --- a/app/Http/Controllers/Api/AdminApiController.php +++ b/app/Http/Controllers/Api/AdminApiController.php @@ -18,6 +18,8 @@ use App\{ Status, User }; +use App\Models\Conversation; +use App\Models\RemoteReport; use App\Services\AccountService; use App\Services\AdminStatsService; use App\Services\ConfigCacheService; @@ -405,6 +407,9 @@ class AdminApiController extends Controller { abort_if(!$request->user(), 404); abort_unless($request->user()->is_admin == 1, 404); + $this->validate($request, [ + 'sort' => 'sometimes|in:asc,desc', + ]); $q = $request->input('q'); $sort = $request->input('sort', 'desc') === 'asc' ? 'asc' : 'desc'; $res = User::whereNull('status') @@ -422,17 +427,29 @@ class AdminApiController extends Controller abort_unless($request->user()->is_admin == 1, 404); $id = $request->input('user_id'); - $user = User::findOrFail($id); - $profile = $user->profile; - $account = AccountService::get($user->profile_id, true); - return (new AdminUser($user))->additional(['meta' => [ - 'account' => $account, - 'moderation' => [ - 'unlisted' => (bool) $profile->unlisted, - 'cw' => (bool) $profile->cw, - 'no_autolink' => (bool) $profile->no_autolink - ] - ]]); + $key = 'pf-admin-api:getUser:byId:' . $id; + if($request->has('refresh')) { + Cache::forget($key); + } + return Cache::remember($key, 86400, function() use($id) { + $user = User::findOrFail($id); + $profile = $user->profile; + $account = AccountService::get($user->profile_id, true); + $res = (new AdminUser($user))->additional(['meta' => [ + 'cached_at' => str_replace('+00:00', 'Z', now()->format(DATE_RFC3339_EXTENDED)), + 'account' => $account, + 'dms_sent' => Conversation::whereFromId($profile->id)->count(), + 'report_count' => Report::where('object_id', $profile->id)->orWhere('reported_profile_id', $profile->id)->count(), + 'remote_report_count' => RemoteReport::whereAccountId($profile->id)->count(), + 'moderation' => [ + 'unlisted' => (bool) $profile->unlisted, + 'cw' => (bool) $profile->cw, + 'no_autolink' => (bool) $profile->no_autolink + ] + ]]); + + return $res; + }); } public function userAdminAction(Request $request) From 71ad7d5d43bfad7525d5676ece5af4ca26755cc1 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 22 Jun 2023 01:12:53 -0600 Subject: [PATCH 2/3] Update AdminUser resource --- app/Http/Resources/AdminUser.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/Http/Resources/AdminUser.php b/app/Http/Resources/AdminUser.php index bfbc9e425..75bac9f62 100644 --- a/app/Http/Resources/AdminUser.php +++ b/app/Http/Resources/AdminUser.php @@ -23,9 +23,11 @@ class AdminUser extends JsonResource 'name' => $this->name, 'username' => $this->username, 'is_admin' => (bool) $this->is_admin, + 'email' => $this->email, 'email_verified_at' => $this->email_verified_at, 'two_factor_enabled' => (bool) $this->{'2fa_enabled'}, 'register_source' => $this->register_source, + 'app_register_ip' => $this->app_register_ip, 'last_active_at' => $this->last_active_at, 'created_at' => $this->created_at, ]; From 763ce19a0a19972d71da8a8914f89167ec426826 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 22 Jun 2023 05:43:42 -0600 Subject: [PATCH 3/3] Update AdminApiController, improve admin moderation tools --- .../Controllers/Api/AdminApiController.php | 150 +++++++++++++++++- app/Services/NetworkTimelineService.php | 20 +++ app/Services/PublicTimelineService.php | 20 +++ 3 files changed, 185 insertions(+), 5 deletions(-) diff --git a/app/Http/Controllers/Api/AdminApiController.php b/app/Http/Controllers/Api/AdminApiController.php index 12b45f7ba..76f73e720 100644 --- a/app/Http/Controllers/Api/AdminApiController.php +++ b/app/Http/Controllers/Api/AdminApiController.php @@ -27,10 +27,14 @@ use App\Services\InstanceService; use App\Services\ModLogService; use App\Services\SnowflakeService; use App\Services\StatusService; +use App\Services\PublicTimelineService; use App\Services\NetworkTimelineService; use App\Services\NotificationService; use App\Http\Resources\AdminInstance; use App\Http\Resources\AdminUser; +use App\Jobs\DeletePipeline\DeleteAccountPipeline; +use App\Jobs\DeletePipeline\DeleteRemoteProfilePipeline; +use App\Jobs\DeletePipeline\DeleteRemoteStatusPipeline; class AdminApiController extends Controller { @@ -95,7 +99,7 @@ class AdminApiController extends Controller abort_unless($request->user()->is_admin == 1, 404); $this->validate($request, [ - 'action' => 'required|in:dismiss,approve,dismiss-all,approve-all', + 'action' => 'required|in:dismiss,approve,dismiss-all,approve-all,delete-post,delete-account', 'id' => 'required' ]); @@ -107,14 +111,53 @@ class AdminApiController extends Controller $now = now(); $res = ['status' => 'success']; $meta = json_decode($appeal->meta); + $user = $appeal->user; + $profile = $user->profile; if($action == 'dismiss') { $appeal->is_spam = true; $appeal->appeal_handled_at = $now; $appeal->save(); - Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id); - Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id); + Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $profile->id); + Cache::forget('pf:bouncer_v0:recent_by_pid:' . $profile->id); + Cache::forget('admin-dash:reports:spam-count'); + return $res; + } + + if($action == 'delete-post') { + $appeal->appeal_handled_at = now(); + $appeal->is_spam = true; + $appeal->save(); + ModLogService::boot() + ->objectUid($profile->id) + ->objectId($appeal->status->id) + ->objectType('App\Status::class') + ->user($request->user()) + ->action('admin.status.delete') + ->accessLevel('admin') + ->save(); + PublicTimelineService::deleteByProfileId($profile->id); + StatusDelete::dispatch($appeal->status)->onQueue('high'); + Cache::forget('admin-dash:reports:spam-count'); + return $res; + } + + if($action == 'delete-account') { + abort_if($user->is_admin, 400, 'Cannot delete an admin account.'); + $appeal->appeal_handled_at = now(); + $appeal->is_spam = true; + $appeal->save(); + ModLogService::boot() + ->objectUid($profile->id) + ->objectId($profile->id) + ->objectType('App\User::class') + ->user($request->user()) + ->action('admin.user.delete') + ->accessLevel('admin') + ->save(); + PublicTimelineService::deleteByProfileId($profile->id); + DeleteAccountPipeline::dispatch($appeal->user)->onQueue('high'); Cache::forget('admin-dash:reports:spam-count'); return $res; } @@ -459,7 +502,7 @@ class AdminApiController extends Controller $this->validate($request, [ 'id' => 'required', - 'action' => 'required|in:unlisted,cw,no_autolink,refresh_stats,verify_email', + 'action' => 'required|in:unlisted,cw,no_autolink,refresh_stats,verify_email,delete', 'value' => 'sometimes' ]); @@ -470,7 +513,59 @@ class AdminApiController extends Controller abort_if($user->is_admin == true && $action !== 'refresh_stats', 400, 'Cannot moderate admin accounts'); - if($action === 'refresh_stats') { + if($action === 'delete') { + if(config('pixelfed.account_deletion') == false) { + abort(404); + } + + abort_if($user->is_admin, 400, 'Cannot delete an admin account.'); + + $ts = now()->addMonth(); + + $user->status = 'delete'; + $user->delete_after = $ts; + $user->save(); + + $profile->status = 'delete'; + $profile->delete_after = $ts; + $profile->save(); + + ModLogService::boot() + ->objectUid($profile->id) + ->objectId($profile->id) + ->objectType('App\Profile::class') + ->user($request->user()) + ->action('admin.user.delete') + ->accessLevel('admin') + ->save(); + + PublicTimelineService::deleteByProfileId($profile->id); + NetworkTimelineService::deleteByProfileId($profile->id); + + if($profile->user_id) { + DB::table('oauth_access_tokens')->whereUserId($user->id)->delete(); + DB::table('oauth_auth_codes')->whereUserId($user->id)->delete(); + $user->email = $user->id; + $user->password = ''; + $user->status = 'delete'; + $user->save(); + $profile->status = 'delete'; + $profile->delete_after = now()->addMonth(); + $profile->save(); + AccountService::del($profile->id); + DeleteAccountPipeline::dispatch($user)->onQueue('high'); + } else { + $profile->status = 'delete'; + $profile->delete_after = now()->addMonth(); + $profile->save(); + AccountService::del($profile->id); + DeleteRemoteProfilePipeline::dispatch($profile)->onQueue('high'); + } + return [ + 'status' => 200, + 'msg' => 'deleted', + ]; + } else if($action === 'refresh_stats') { $profile->following_count = DB::table('followers')->whereProfileId($user->profile_id)->count(); $profile->followers_count = DB::table('followers')->whereFollowingId($user->profile_id)->count(); $statusCount = Status::whereProfileId($user->profile_id) @@ -496,6 +591,51 @@ class AdminApiController extends Controller ]) ->accessLevel('admin') ->save(); + } else if($action === 'unlisted') { + ModLogService::boot() + ->objectUid($profile->id) + ->objectId($profile->id) + ->objectType('App\Profile::class') + ->user($request->user()) + ->action('admin.user.moderate') + ->metadata([ + 'action' => $action, + 'message' => 'Success!' + ]) + ->accessLevel('admin') + ->save(); + $profile->unlisted = !$profile->unlisted; + $profile->save(); + } else if($action === 'cw') { + ModLogService::boot() + ->objectUid($profile->id) + ->objectId($profile->id) + ->objectType('App\Profile::class') + ->user($request->user()) + ->action('admin.user.moderate') + ->metadata([ + 'action' => $action, + 'message' => 'Success!' + ]) + ->accessLevel('admin') + ->save(); + $profile->cw = !$profile->cw; + $profile->save(); + } else if($action === 'no_autolink') { + ModLogService::boot() + ->objectUid($profile->id) + ->objectId($profile->id) + ->objectType('App\Profile::class') + ->user($request->user()) + ->action('admin.user.moderate') + ->metadata([ + 'action' => $action, + 'message' => 'Success!' + ]) + ->accessLevel('admin') + ->save(); + $profile->no_autolink = !$profile->no_autolink; + $profile->save(); } else { $profile->{$action} = filter_var($request->input('value'), FILTER_VALIDATE_BOOLEAN); $profile->save(); diff --git a/app/Services/NetworkTimelineService.php b/app/Services/NetworkTimelineService.php index 52fc9b0bd..570899017 100644 --- a/app/Services/NetworkTimelineService.php +++ b/app/Services/NetworkTimelineService.php @@ -72,6 +72,26 @@ class NetworkTimelineService return Redis::zcard(self::CACHE_KEY); } + public static function deleteByProfileId($profileId) + { + $res = Redis::zrange(self::CACHE_KEY, 0, '-1'); + if(!$res) { + return; + } + foreach($res as $postId) { + $s = StatusService::get($postId); + if(!$s) { + self::rem($postId); + continue; + } + if($s['account']['id'] == $profileId) { + self::rem($postId); + } + } + + return; + } + public static function warmCache($force = false, $limit = 100) { if(self::count() == 0 || $force == true) { diff --git a/app/Services/PublicTimelineService.php b/app/Services/PublicTimelineService.php index 2c9fbc7e3..e1275065c 100644 --- a/app/Services/PublicTimelineService.php +++ b/app/Services/PublicTimelineService.php @@ -72,6 +72,26 @@ class PublicTimelineService { return Redis::zcard(self::CACHE_KEY); } + public static function deleteByProfileId($profileId) + { + $res = Redis::zrange(self::CACHE_KEY, 0, '-1'); + if(!$res) { + return; + } + foreach($res as $postId) { + $s = StatusService::get($postId); + if(!$s) { + self::rem($postId); + continue; + } + if($s['account']['id'] == $profileId) { + self::rem($postId); + } + } + + return; + } + public static function warmCache($force = false, $limit = 100) { if(self::count() == 0 || $force == true) {