From c6cc6327d37e2a142abe04326b5ec7da22d2ab02 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 24 Apr 2023 01:52:59 -0600 Subject: [PATCH 1/3] Redesigned Admin Dashboard Reports/Moderation --- .../Admin/AdminReportController.php | 598 +++++++++++ app/Http/Resources/AdminReport.php | 38 + app/Http/Resources/AdminSpamReport.php | 33 + .../assets/components/admin/AdminReports.vue | 937 ++++++++++++++++++ resources/views/admin/reports/home.blade.php | 97 +- resources/views/admin/reports/show.blade.php | 16 +- .../views/admin/reports/show_spam.blade.php | 19 +- routes/web.php | 7 + 8 files changed, 1651 insertions(+), 94 deletions(-) create mode 100644 app/Http/Resources/AdminReport.php create mode 100644 app/Http/Resources/AdminSpamReport.php create mode 100644 resources/assets/components/admin/AdminReports.vue diff --git a/app/Http/Controllers/Admin/AdminReportController.php b/app/Http/Controllers/Admin/AdminReportController.php index 59ae9dfe9..2ee8e02c5 100644 --- a/app/Http/Controllers/Admin/AdminReportController.php +++ b/app/Http/Controllers/Admin/AdminReportController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers\Admin; use Cache; use Carbon\Carbon; use Illuminate\Http\Request; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Redis; use App\Services\AccountService; use App\Services\StatusService; @@ -24,6 +25,13 @@ use Illuminate\Validation\Rule; use App\Services\StoryService; use App\Services\ModLogService; use App\Jobs\DeletePipeline\DeleteAccountPipeline; +use App\Jobs\DeletePipeline\DeleteRemoteProfilePipeline; +use App\Jobs\DeletePipeline\DeleteRemoteStatusPipeline; +use App\Jobs\StatusPipeline\StatusDelete; +use App\Http\Resources\AdminReport; +use App\Http\Resources\AdminSpamReport; +use App\Services\PublicTimelineService; +use App\Services\NetworkTimelineService; trait AdminReportController { @@ -74,6 +82,9 @@ trait AdminReportController public function showReport(Request $request, $id) { $report = Report::with('status')->findOrFail($id); + if($request->has('ref') && $request->input('ref') == 'email') { + return redirect('/i/admin/reports?tab=report&id=' . $report->id); + } return view('admin.reports.show', compact('report')); } @@ -200,6 +211,9 @@ trait AdminReportController { $appeal = AccountInterstitial::whereType('post.autospam') ->findOrFail($id); + if($request->has('ref') && $request->input('ref') == 'email') { + return redirect('/i/admin/reports?tab=autospam&id=' . $appeal->id); + } $meta = json_decode($appeal->meta); return view('admin.reports.show_spam', compact('appeal', 'meta')); } @@ -601,4 +615,588 @@ trait AdminReportController Redis::del('email:manual-ignored'); return [200]; } + + public function reportsStats(Request $request) + { + $stats = [ + 'total' => Report::count(), + 'open' => Report::whereNull('admin_seen')->count(), + 'closed' => Report::whereNotNull('admin_seen')->count(), + 'autospam' => AccountInterstitial::whereType('post.autospam')->count(), + 'autospam_open' => AccountInterstitial::whereType('post.autospam')->whereNull(['appeal_handled_at'])->count(), + 'appeals' => AccountInterstitial::whereNotNull('appeal_requested_at')->whereNull('appeal_handled_at')->count(), + 'email_verification_requests' => Redis::scard('email:manual') + ]; + return $stats; + } + + public function reportsApiAll(Request $request) + { + $filter = $request->input('filter') == 'closed' ? 'closed' : 'open'; + + $reports = AdminReport::collection( + Report::orderBy('id','desc') + ->when($filter, function($q, $filter) { + return $filter == 'open' ? + $q->whereNull('admin_seen') : + $q->whereNotNull('admin_seen'); + }) + ->groupBy(['object_id', 'object_type']) + ->cursorPaginate(6) + ->withQueryString() + ); + + return $reports; + } + + public function reportsApiGet(Request $request, $id) + { + $report = Report::findOrFail($id); + return new AdminReport($report); + } + + public function reportsApiHandle(Request $request) + { + $this->validate($request, [ + 'object_id' => 'required', + 'object_type' => 'required', + 'id' => 'required', + 'action' => 'required|in:ignore,nsfw,unlist,private,delete', + 'action_type' => 'required|in:post,profile' + ]); + + $report = Report::whereObjectId($request->input('object_id'))->findOrFail($request->input('id')); + + if($request->input('action_type') === 'profile') { + return $this->reportsHandleProfileAction($report, $request->input('action')); + } else if($request->input('action_type') === 'post') { + return $this->reportsHandleStatusAction($report, $request->input('action')); + } + + return $report; + } + + protected function reportsHandleProfileAction($report, $action) + { + switch($action) { + case 'ignore': + Report::whereObjectId($report->object_id) + ->whereObjectType($report->object_type) + ->update([ + 'admin_seen' => now() + ]); + return [200]; + break; + + case 'nsfw': + if($report->object_type === 'App\Profile') { + $profile = Profile::find($report->object_id); + } else if($report->object_type === 'App\Status') { + $status = Status::find($report->object_id); + if(!$status) { + return [200]; + } + $profile = Profile::find($status->profile_id); + } + + if(!$profile) { + return; + } + + abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot moderate an admin account.'); + + $profile->cw = true; + $profile->save(); + + foreach(Status::whereProfileId($profile->id)->cursor() as $status) { + $status->is_nsfw = true; + $status->save(); + StatusService::del($status->id); + PublicTimelineService::rem($status->id); + } + + ModLogService::boot() + ->objectUid($profile->id) + ->objectId($profile->id) + ->objectType('App\Profile::class') + ->user(request()->user()) + ->action('admin.user.moderate') + ->metadata([ + 'action' => 'cw', + 'message' => 'Success!' + ]) + ->accessLevel('admin') + ->save(); + + Report::whereObjectId($report->object_id) + ->whereObjectType($report->object_type) + ->update([ + 'nsfw' => true, + 'admin_seen' => now() + ]); + return [200]; + break; + + case 'unlist': + if($report->object_type === 'App\Profile') { + $profile = Profile::find($report->object_id); + } else if($report->object_type === 'App\Status') { + $status = Status::find($report->object_id); + if(!$status) { + return [200]; + } + $profile = Profile::find($status->profile_id); + } + + if(!$profile) { + return; + } + + abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot moderate an admin account.'); + + $profile->unlisted = true; + $profile->save(); + + foreach(Status::whereProfileId($profile->id)->whereScope('public')->cursor() as $status) { + $status->scope = 'unlisted'; + $status->visibility = 'unlisted'; + $status->save(); + StatusService::del($status->id); + PublicTimelineService::rem($status->id); + } + + ModLogService::boot() + ->objectUid($profile->id) + ->objectId($profile->id) + ->objectType('App\Profile::class') + ->user(request()->user()) + ->action('admin.user.moderate') + ->metadata([ + 'action' => 'unlisted', + 'message' => 'Success!' + ]) + ->accessLevel('admin') + ->save(); + + Report::whereObjectId($report->object_id) + ->whereObjectType($report->object_type) + ->update([ + 'admin_seen' => now() + ]); + return [200]; + break; + + case 'private': + if($report->object_type === 'App\Profile') { + $profile = Profile::find($report->object_id); + } else if($report->object_type === 'App\Status') { + $status = Status::find($report->object_id); + if(!$status) { + return [200]; + } + $profile = Profile::find($status->profile_id); + } + + if(!$profile) { + return; + } + + abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot moderate an admin account.'); + + $profile->unlisted = true; + $profile->save(); + + foreach(Status::whereProfileId($profile->id)->cursor() as $status) { + $status->scope = 'private'; + $status->visibility = 'private'; + $status->save(); + StatusService::del($status->id); + PublicTimelineService::rem($status->id); + } + + ModLogService::boot() + ->objectUid($profile->id) + ->objectId($profile->id) + ->objectType('App\Profile::class') + ->user(request()->user()) + ->action('admin.user.moderate') + ->metadata([ + 'action' => 'private', + 'message' => 'Success!' + ]) + ->accessLevel('admin') + ->save(); + + Report::whereObjectId($report->object_id) + ->whereObjectType($report->object_type) + ->update([ + 'admin_seen' => now() + ]); + return [200]; + break; + + case 'delete': + if(config('pixelfed.account_deletion') == false) { + abort(404); + } + + if($report->object_type === 'App\Profile') { + $profile = Profile::find($report->object_id); + } else if($report->object_type === 'App\Status') { + $status = Status::find($report->object_id); + if(!$status) { + return [200]; + } + $profile = Profile::find($status->profile_id); + } + + if(!$profile) { + return; + } + + abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot delete an admin account.'); + + $ts = now()->addMonth(); + + if($profile->user_id) { + $user = $profile->user; + abort_if($user->is_admin, 403, 'You cannot delete admin accounts.'); + $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(); + + Report::whereObjectId($report->object_id) + ->whereObjectType($report->object_type) + ->update([ + 'admin_seen' => now() + ]); + + 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 [200]; + break; + } + } + + protected function reportsHandleStatusAction($report, $action) + { + switch($action) { + case 'ignore': + Report::whereObjectId($report->object_id) + ->whereObjectType($report->object_type) + ->update([ + 'admin_seen' => now() + ]); + return [200]; + break; + + case 'nsfw': + $status = Status::find($report->object_id); + + if(!$status) { + return [200]; + } + + abort_if($status->profile->user && $status->profile->user->is_admin, 400, 'Cannot moderate an admin account post.'); + $status->is_nsfw = true; + $status->save(); + StatusService::del($status->id); + + ModLogService::boot() + ->objectUid($status->profile_id) + ->objectId($status->profile_id) + ->objectType('App\Status::class') + ->user(request()->user()) + ->action('admin.status.moderate') + ->metadata([ + 'action' => 'cw', + 'message' => 'Success!' + ]) + ->accessLevel('admin') + ->save(); + + Report::whereObjectId($report->object_id) + ->whereObjectType($report->object_type) + ->update([ + 'nsfw' => true, + 'admin_seen' => now() + ]); + return [200]; + break; + + case 'private': + $status = Status::find($report->object_id); + + if(!$status) { + return [200]; + } + + abort_if($status->profile->user && $status->profile->user->is_admin, 400, 'Cannot moderate an admin account post.'); + + $status->scope = 'private'; + $status->visibility = 'private'; + $status->save(); + StatusService::del($status->id); + PublicTimelineService::rem($status->id); + + ModLogService::boot() + ->objectUid($status->profile_id) + ->objectId($status->profile_id) + ->objectType('App\Status::class') + ->user(request()->user()) + ->action('admin.status.moderate') + ->metadata([ + 'action' => 'private', + 'message' => 'Success!' + ]) + ->accessLevel('admin') + ->save(); + + Report::whereObjectId($report->object_id) + ->whereObjectType($report->object_type) + ->update([ + 'admin_seen' => now() + ]); + return [200]; + break; + + case 'unlist': + $status = Status::find($report->object_id); + + if(!$status) { + return [200]; + } + + abort_if($status->profile->user && $status->profile->user->is_admin, 400, 'Cannot moderate an admin account post.'); + + if($status->scope === 'public') { + $status->scope = 'unlisted'; + $status->visibility = 'unlisted'; + $status->save(); + StatusService::del($status->id); + PublicTimelineService::rem($status->id); + } + + ModLogService::boot() + ->objectUid($status->profile_id) + ->objectId($status->profile_id) + ->objectType('App\Status::class') + ->user(request()->user()) + ->action('admin.status.moderate') + ->metadata([ + 'action' => 'unlist', + 'message' => 'Success!' + ]) + ->accessLevel('admin') + ->save(); + + Report::whereObjectId($report->object_id) + ->whereObjectType($report->object_type) + ->update([ + 'admin_seen' => now() + ]); + return [200]; + break; + + case 'delete': + $status = Status::find($report->object_id); + + if(!$status) { + return [200]; + } + + $profile = $status->profile; + + abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot delete an admin account post.'); + + StatusService::del($status->id); + + if($profile->user_id != null && $profile->domain == null) { + PublicTimelineService::del($status->id); + StatusDelete::dispatch($status)->onQueue('high'); + } else { + NetworkTimelineService::del($status->id); + DeleteRemoteStatusPipeline::dispatch($status)->onQueue('high'); + } + + Report::whereObjectId($report->object_id) + ->whereObjectType($report->object_type) + ->update([ + 'admin_seen' => now() + ]); + + return [200]; + break; + } + } + + public function reportsApiSpamAll(Request $request) + { + $tab = $request->input('tab', 'home'); + + $appeals = AdminSpamReport::collection( + AccountInterstitial::orderBy('id', 'desc') + ->whereType('post.autospam') + ->whereNull('appeal_handled_at') + ->cursorPaginate(6) + ->withQueryString() + ); + + return $appeals; + } + + public function reportsApiSpamHandle(Request $request) + { + $this->validate($request, [ + 'id' => 'required', + 'action' => 'required|in:mark-read,mark-not-spam,mark-all-read,mark-all-not-spam,delete-profile', + ]); + + $action = $request->input('action'); + + abort_if( + $action === 'delete-profile' && + !config('pixelfed.account_deletion'), + 404, + "Cannot delete profile, account_deletion is disabled.\n\n Set `ACCOUNT_DELETION=true` in .env and re-cache config." + ); + + $report = AccountInterstitial::with('user') + ->whereType('post.autospam') + ->whereNull('appeal_handled_at') + ->findOrFail($request->input('id')); + + $this->reportsHandleSpamAction($report, $action); + Cache::forget('admin-dash:reports:spam-count'); + Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $report->user->profile_id); + Cache::forget('pf:bouncer_v0:recent_by_pid:' . $report->user->profile_id); + PublicTimelineService::warmCache(true, 400); + return [$action, $report]; + } + + public function reportsHandleSpamAction($appeal, $action) + { + $meta = json_decode($appeal->meta); + + if($action == 'mark-read') { + $appeal->is_spam = true; + $appeal->appeal_handled_at = now(); + $appeal->save(); + } + + if($action == 'mark-not-spam') { + $status = $appeal->status; + $status->is_nsfw = $meta->is_nsfw; + $status->scope = 'public'; + $status->visibility = 'public'; + $status->save(); + + $appeal->is_spam = false; + $appeal->appeal_handled_at = now(); + $appeal->save(); + + StatusService::del($status->id); + } + + if($action == 'mark-all-read') { + AccountInterstitial::whereType('post.autospam') + ->whereItemType('App\Status') + ->whereNull('appeal_handled_at') + ->whereUserId($appeal->user_id) + ->update([ + 'appeal_handled_at' => now(), + 'is_spam' => true + ]); + } + + if($action == 'mark-all-not-spam') { + AccountInterstitial::whereType('post.autospam') + ->whereItemType('App\Status') + ->whereUserId($appeal->user_id) + ->get() + ->each(function($report) use($meta) { + $report->is_spam = false; + $report->appeal_handled_at = now(); + $report->save(); + $status = Status::find($report->item_id); + if($status) { + $status->is_nsfw = $meta->is_nsfw; + $status->scope = 'public'; + $status->visibility = 'public'; + $status->save(); + StatusService::del($status->id); + } + }); + } + + if($action == 'delete-profile') { + $user = User::findOrFail($appeal->user_id); + $profile = $user->profile; + + if($user->is_admin == true) { + $mid = request()->user()->id; + abort_if($user->id < $mid, 403, 'You cannot delete an admin account.'); + } + + $ts = now()->addMonth(); + $user->status = 'delete'; + $profile->status = 'delete'; + $user->delete_after = $ts; + $profile->delete_after = $ts; + $user->save(); + $profile->save(); + + $appeal->appeal_handled_at = now(); + $appeal->save(); + + ModLogService::boot() + ->objectUid($user->id) + ->objectId($user->id) + ->objectType('App\User::class') + ->user(request()->user()) + ->action('admin.user.delete') + ->accessLevel('admin') + ->save(); + + Cache::forget('profiles:private'); + DeleteAccountPipeline::dispatch($user); + } + } + + public function reportsApiSpamGet(Request $request, $id) + { + $report = AccountInterstitial::findOrFail($id); + return new AdminSpamReport($report); + } } diff --git a/app/Http/Resources/AdminReport.php b/app/Http/Resources/AdminReport.php new file mode 100644 index 000000000..c541e58cd --- /dev/null +++ b/app/Http/Resources/AdminReport.php @@ -0,0 +1,38 @@ + + */ + public function toArray(Request $request): array + { + $res = [ + 'id' => $this->id, + 'reporter' => AccountService::get($this->profile_id, true), + 'type' => $this->type, + 'object_id' => (string) $this->object_id, + 'object_type' => $this->object_type, + 'reported' => AccountService::get($this->reported_profile_id, true), + 'status' => null, + 'reporter_message' => $this->message, + 'admin_seen_at' => $this->admin_seen, + 'created_at' => $this->created_at, + ]; + + if($this->object_id && $this->object_type === 'App\Status') { + $res['status'] = StatusService::get($this->object_id, false); + } + + return $res; + } +} diff --git a/app/Http/Resources/AdminSpamReport.php b/app/Http/Resources/AdminSpamReport.php new file mode 100644 index 000000000..6e2badd0f --- /dev/null +++ b/app/Http/Resources/AdminSpamReport.php @@ -0,0 +1,33 @@ + + */ + public function toArray(Request $request): array + { + $res = [ + 'id' => $this->id, + 'type' => $this->type, + 'status' => null, + 'read_at' => $this->read_at, + 'created_at' => $this->created_at, + ]; + + if($this->item_id && $this->item_type === 'App\Status') { + $res['status'] = StatusService::get($this->item_id, false); + } + + return $res; + } +} diff --git a/resources/assets/components/admin/AdminReports.vue b/resources/assets/components/admin/AdminReports.vue new file mode 100644 index 000000000..fdd11b012 --- /dev/null +++ b/resources/assets/components/admin/AdminReports.vue @@ -0,0 +1,937 @@ + + + diff --git a/resources/views/admin/reports/home.blade.php b/resources/views/admin/reports/home.blade.php index 996ad1b1e..de0d9ea92 100644 --- a/resources/views/admin/reports/home.blade.php +++ b/resources/views/admin/reports/home.blade.php @@ -1,93 +1,12 @@ @extends('admin.partial.template-full') @section('section') -
-

Reports

-
- @if(request()->has('filter') && request()->filter == 'closed') - - View Open Reports - - @else - - View Closed Reports - - @endif -
-
- -
- -
- @if($reports->count()) -
-
-
- @foreach($reports as $report) -
-
-
- - - -
-

{{$report->type}}

- @if($report->reporter && $report->status) -

{{$report->reporter->username}} reported this post

- @else -

- @if(!$report->reporter) - Deleted user - @else - {{$report->reporter->username}} - @endif - reported this - @if(!$report->status) - deleted post - @else - post - @endif - -

- - @endif -
-
- @if($report->status) - - View - - @endif -
-
-
-
- @endforeach -
-
-
- @else -
-
-

No reports found

-
-
- @endif - -
- {{$reports->appends(['layout'=>request()->layout, 'filter' => request()->filter])->links()}} -
+ + @endsection + +@push('scripts') + +@endpush diff --git a/resources/views/admin/reports/show.blade.php b/resources/views/admin/reports/show.blade.php index 3ab9d5f7b..da0579613 100644 --- a/resources/views/admin/reports/show.blade.php +++ b/resources/views/admin/reports/show.blade.php @@ -1,6 +1,14 @@ @extends('admin.partial.template-full') @section('section') +
+
+

Try the new Report UI

+

We are deprecating this Report UI in the next major version release. The updated Report UI is easier, faster and provides more options to handle reports and spam.

+
+ View in new Report UI +
+

@@ -16,16 +24,17 @@

- @if($report->status->media()->count()) + @if($report->status && $report->status->media()->count()) @endif
- @if($report->status->caption) + @if($report->status && $report->status->caption)

{{$report->status->media()->count() ? 'Caption' : 'Comment'}}: {{$report->status->caption}}

@endif + @if($report->status)

Like Count: {{$report->status->likes_count}}

@@ -41,7 +50,8 @@

Local URL: {{url('/i/web/post/' . $report->status->id)}}

- @if($report->status->in_reply_to_id) + @endif + @if($report->status && $report->status->in_reply_to_id)

Parent Post: {{url('/i/web/post/' . $report->status->in_reply_to_id)}}

diff --git a/resources/views/admin/reports/show_spam.blade.php b/resources/views/admin/reports/show_spam.blade.php index c001b39a1..2559bfaa8 100644 --- a/resources/views/admin/reports/show_spam.blade.php +++ b/resources/views/admin/reports/show_spam.blade.php @@ -1,6 +1,13 @@ @extends('admin.partial.template-full') @section('section') +
+
+

Try the new Report UI

+

We are deprecating this Report UI in the next major version release. The updated Report UI is easier, faster and provides more options to handle reports and spam.

+
+ View in new Report UI +

Autospam

@@ -15,7 +22,7 @@
Unlisted + Content Warning
@if($appeal->has_media) - + @endif
@@ -34,7 +41,7 @@ Timestamp: {{now()->parse($meta->created_at)->format('r')}}

- URL: {{$meta->url}} + URL: {{$meta->url}}

@@ -49,6 +56,8 @@
+
+ @endif
@@ -107,6 +116,12 @@ } break; + case 'mark-spammer': + if(!window.confirm('Are you sure you want to mark this account as a spammer?')) { + return; + } + break; + case 'delete-account': if(!window.confirm('Are you sure you want to delete this account?')) { return; diff --git a/routes/web.php b/routes/web.php index 828b13fcf..c0d12153b 100644 --- a/routes/web.php +++ b/routes/web.php @@ -122,6 +122,13 @@ Route::domain(config('pixelfed.domain.admin'))->prefix('i/admin')->group(functio Route::post('instances/refresh-stats', 'AdminController@postInstanceRefreshStatsApi'); Route::get('instances/download-backup', 'AdminController@downloadBackup'); Route::post('instances/import-data', 'AdminController@importBackup'); + Route::get('reports/stats', 'AdminController@reportsStats'); + Route::get('reports/all', 'AdminController@reportsApiAll'); + Route::get('reports/get/{id}', 'AdminController@reportsApiGet'); + Route::post('reports/handle', 'AdminController@reportsApiHandle'); + Route::get('reports/spam/all', 'AdminController@reportsApiSpamAll'); + Route::get('reports/spam/get/{id}', 'AdminController@reportsApiSpamGet'); + Route::post('reports/spam/handle', 'AdminController@reportsApiSpamHandle'); }); }); From c3ec81e52581b966d3c188d95d7d18fbed6b0d64 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 24 Apr 2023 01:54:26 -0600 Subject: [PATCH 2/3] Update compiled assets --- public/js/admin.js | Bin 130433 -> 164840 bytes public/mix-manifest.json | Bin 5977 -> 5977 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/public/js/admin.js b/public/js/admin.js index c51e4892d0a919367c9d03294559ccf88996e837..b2efdf1dd798f26f4ad267323289e9939de19ac6 100644 GIT binary patch delta 20201 zcmd5^dvH|Od7ontPw^D!#p~)yNPErQ)vknith`79j14v*BL~MpvV5hzD_yg@cYXIt zLJ`W^#%?o-(|}Jir=4^rq-oMlQ`=BCRi=3W?$n-QhuC@Gahk?;JDs+FB*B?9jT0yC zch0%beF#Zl#_L{;7>-PD1-ko z65o)DO-!~YM$&_(rl*C`fHX1bd`sZHEebkv# z${4z-o8uW(lH-9r_)mB3Kzs1_c6GqNt2=Zw_QDe#ZD{=w`4P8gYyzgun#RbWsT`@dJsQ(S8T!lfFj8${PJCAVGqN1j{tXn za`#@~Mg9KH@VwUT+i_>86?bkbKl9I#T-H>~Y;*#sQj*cItY_3T{25H@Sv3wHlo3LT)} zrjBXpA^vMFI;v}NA?*M4UR{Yp7uwq)0H(G-cl(yGf2D(fW*9>k?E#{TFYLC!+LhkL z-4^_u(1=&vC+E!2u#ydkuvJlNY_z^C*UO{-r=z#P`Hp@eD<4);*@ z2GS$Rq%1aT%}P9_rJI$E)@*UCnKHLoNhU=}_KdQL-)jb}ndzF66rGM9K4l>__)TH` zL!1|RvjyNF;`&$!$OYjV4`oKOjAE5joJY1rvY%46KMDU{5DuuK9I$niYwX;npOo+? z%fwuEOalf6Ole|J$*Mv)>fsMtwKzi=FWUzURXN_y21aH+up^-*P1T4|@z1Hiaqm1W z1@q+br}(i>&W#E33btKa$6_Y_r;lp*mHJ&cxorRbxSCK#lBOt6r1Z2o92Jv#dPtPx z%6L>vkEB#X8x-YnRWUqYhxHNfR}jPzQ^nt1R-NMqd#^H}CZp~FN)z@8g!F+&)IqbK z87_7jVpk^9+uLpEz%W&e8mc*Bq(w0VrfN{3GE2M4qG0!s*irCNR3%2ToY*3ECDz*{_CK%=H`F&Tg&Ay0v9fdnzJ0TqbBUW|#*lw^rlxwp zB*JGtEkhXtqgU7%Gx?8sHaJ!nmJ4Na`}GyS1B5Zgch;!IW8xMwq{Yd_&!d{AjCMjz zCe(Nc)GnUV4V7|9j;O7`sbM7@PpVzi`QyhME6XUM?fCWe6*%0#FkXaiXH#KW)P~Y9 z8?|Zb$&(h<%*`4pZe_vjvZ>Qde3{viECZZm4gq+ByVliOLY{5rpo!XC``B#5&}FE?xIQ9eN30KH3ho?U5aOpRP(lQ2I)MvVhsZ5PI-3}?O$C;R zYnxMwaoj&Z$qI*h_B~*a7)d9=n|a4{u`ys4gbWy#tS~TMFaaN=#_dTmhBm60j%A}O zcU*rKYm_}nTurLJIU1wZCOgDQH3>eAv5^|P>+_J3u*JtO7N1Oeo-0qMxOs_q+e>O}OR~NVGGc@V;&hm_U|8zzjtOF&%V|smCv?B-0 z98j?ef&vDqd+@5}AxEjQFq*}rT&+Ogfa}Tb?Lb}X$XgQxlqrJEzca9gF8l&H#o$CI{ll(`< zAO1rLXJ@ag+YV3iySsUozq`YyL8Y9YOCNQ7+&OvtQf_8`NwvL*JDq2{MOvwUQkj2p zM_6(U*_&$KuccId#0-#cJ%^TSvrVKR@@NZ(sTcM8ICm|FI`DIzKn|~Js9Wo)i;h_tSXCa-LbO;cZC|sg#)OabT309d2kcDf;&zx zT$W7*w+VxKGMF3+wi2Zd^)I6~V^kG5@5mPgbhKAiOw#}qLi7aRV$_MgW&V36sSK(q zH4V;P9MDZuPl>XrnSiyJAqHcFC^E4{6zCt@mWZEw6b0t@n&KAnzJS)1(Kd-ZA47Hc z$`gyo*Bj905`?a`2&G&U_JSv8p&KJOeX|I*O)tY#Y|68A!MMF(R-7b{HK6)szP{u> z5rv8qt}gJfw*O{P$^asuko|R-%Sa2L88K8{UWAj{&7#ldjzeNT7yWYJICqQm<)_Q> z$%z#t)P(*PpZ)tra`7W*KB)+xhBXT03WG@{3+R@ZRz`zKEqxq6`jd6!z5uErEj_4# zJRe6JaQ|d^Jy;zGX^yD@5tdJj@-Z{iKN=7ZQ-S2ClEu%TTDvgIM70Co_i7b+?T=7- zE}Ietl=zSuOeA#x&(cHI*SL~~r+Uzi>v86bJ{F1?Vv@+`ic1$}y zyv$l|%O0bX3zU~@tJ~tiw4PRlcrXbn%CtVIF_U>tdS@|Pz+0W zRgfjz zd2~dDw2$iD#%zeq)bE|C->`bP683@dr+%8{XU((&*I}BH$*4-QIHLf+gEk*?mVmw8>j+DdiQXj>%`_%wmak_H;v9KH=*Mg`sEHGOJ7L3^^ zOAIJxXMGGM)%1`#+yRyrY)w)nD??~4xzLAJkj{2gUuAXk0Md$_l_p5}R&;2Ypd=xa z4yLp?08`{_8>+@zCbundBRc%>FSp`{KeLd$yA>TS_x8q{Ua2MzA4FwK1_ADcI}@;_ z1c|5;f}Tt>T5pAo9E?&V3BUfE3X<7|R)@T^&|N9kR@8W7qx%`caJnajbTy#bHnHG@ z$JBvlD;a5a;8x#o37@~T2LE!R@|jaLQl6U_QZi979NHE!sO91Mqon{ip1_Y^uFJ^~ zRxv+g!1W7-XvNkr^$|7|Sz|EjSxW(kL@^-BjZ*}=H7v@*sx~xi^3P!KD5wQd9EF)+ zXsoH3-ZCgdYBL|1ZMI2HFvO`6pZZ823+d!qpz&Pou8_J3CL^9Q6+x%mF1n*{@Xm;_xkC) zDMAT7nbgNrBiql?P-zQ(`eHo}em4-tIpU6eVEVKxbZVKZsd#&3}=ihPp_s5F6LR4+zyC}2eu(0 z|JbW>^ZRRV%ce-=)2NXJMk`HkGO0+D_|m1?vM@D`g(YC#Q4`7E_G;;@-7GhTW7rM6nd3vu>%v6YhU(xF%k@6o6VO@?iOSXw;(i&I>PQasd!Q=Qq8Og1Qc;?lxL%=OHFkt|IRxJ%a9YQua0{?HZ0yn5zU^%V;bpeq3#hAOd^FOb0 za@Fco+(ye15U>PwPKMr1CU&A`&)2W+L|^cHy(j8xort0y^3xbfXMS=>0M~?Qhpu`+$6c(sCuz;D@VSy3)MDy+O1@q8XBiQ#zHDBGf6h> zLMv89!TR|+(N_F#|8bCHI?%oI?}rOX@-_lo>)ekkm%3pp4^R*A$k@6q_0I!WAd?^* zo&(KWD8%Yl`m?H$~W%HHuzJV*}1Z^eI~?6K~~EIP#(u1OA@PP`0xV z0jUh^^Jc5eG->Q?RfZ+CIUBeDSHQj4v_1i9VF6F5zzCJ&|5xTZkNVk(2faD|A^e4_@R{g;icp4efgjugK$ zQDbG{t0{fyh`{6~1?7}+0e|9Gl@wB3SnGt`fRsBvwj0V`M10>R5kK~4HS3r|%vT4# z6a4I@z!r}i79qeKVuf%e`k%WDrF9VcbzuFS%7qquigTC4kPG<_!&>0n<=V6j*nuk$ z<>?T+v<@+@O6Re_x;#$?9z%+Xqgq_mc>!{XB`Xgw8sV&YU${2nSHC_=;1a?#^kf#C zmrOy&(mS|&)zAD*e&!19Odbx%|*tUlT6l`>F631iWnpoTQL$5zA1JQ5*!7+FCR&=0>L5Q+Ajq z*d8lmTN4u6iR#zGjn;8tz)V}0ZY^)RW*yRiiIZ+)x|HJBBtB@LiFduY{3GEHX>9F+mS9T3Ee)UQ@m_CVF zO~^Q?`OCkGwj#e)o^-su2(HK9>OqTcw{NnYQ=sPo_mJMhp!0?Ki9A&5L|8?r_dF>0 z)@uszKYrKLYaXP>P3t|WwdK{J`JKo+J5hK}g!Ppo%ql#cp0tm!>QtAw0=h0mQFVTP z??^+TJ+iYg2m)tzmgMzr6qxa0wD7}FO6}Pyah-0@ds!8%{Um^YzwY%PGrxQDWN}t; z#SLma^~*P^?xl~X@N6oaL-nip@XS?wHp>;RMt#^V(&c>X%6VO&$V_di%CdUyJQzaE zj0_~p-lA1YAMPXnB3dEoS2ck)Zyw#0rXDsuU0UFMec}{c@CAUN zLLqjV92vfaQq_{j0xK?%#Ug?QETH~?Jhu;Sf}eYGNKwmppIg{P=Mb>QNrZJJTC89k!{Jk3mpUId44Jf-n7?;yvXA z&|&W0zGN6udO;72N-s;D{JG!*+@X-*F4WZ9tu2GBRxal@7P*x`QGDi`RWxa*ICo+?oZFqpBZlse)*a|{ zZT9`Borm2ZBA#Pe{Ol3&0-9~QW(UYdY>bg*ysfO_f4OG4|>>q6@#C#4|! zp<;&417A7*$DL(5C83Hr9X9~{JI zpr&ulOEu)sr_o|^p&6AQ;Q=RIkJiiSfDf_<_}H_0-b~AnZP+kW7LYp%T2Nl+B3We(H*IojyoY_hL1=8CjDw zV>s1+1cAbh@=OM=_M_e4!d`Bo|krYVytf=*h+W8VryfQr7|nLJu84>nohAF*k2=TbDbnyT^UQ%JzWz znY_*@+=9y3T;)eS6<&1h7SgmDty&Ih&xAe9LVW!AGnKgW&H3cQXVFU3M!vBEDwyU* z-b|@@LPe{6b-*ONA9zyiIf5!iBMo)wgJV=x7ezCJ> z{Mu9vRfL^QTCQ6C`Apl>}ZR$ zO`$(VTKU3^oRUZ7#MGl_(f3xO4B2}gb;6&g&ZGNLX6i@h(aQ*>$e+J}nwF>Bx2VX0 zQS!zM=u2gz64c?6+~1)WQDSQM*U-8pF!-Sh=o$<SlC6O+=U8rq%I2cYmcMc%oHYRNVDXQ9O2{FQhnyk@HN5_+JlA`=tE z_JnQ(vaxV`_KuNuDMSAHCA14D{hyc6kLOj4O3{&yz)%3@WfR6y5($12RZac;GK!({ zOl(_gW3t^ic`}e8J6=UiF!145QNwL0L7uvT;ALM^OTLSKGp{xSuQ`MFJJhk)ln7cH zaLDwaswadDx$py2H`Vcd6sLUoiyxq`!4^OEL-h3aOz6&o-S5_Z7O5Xkr+Eum{UWc}w{On!SGd1>GbQH~}lfe7YHj!)Zp~d{! QX7c;@P$zlyJ@mEz2MlXG-v9sr delta 1020 zcmY*XO>7%Q6xO`pLVto#OjEaNRL8iy!pQO z=6m1F-Bae3H`!9LJ+UGRw|JkCE(kJ4$+Uoh~!S)+dIE!EQ`s4;r+hYf8P=GRsSisx%nv zh}2+ic?0ol@^5_!VKrcwx}{qejjBkOg;n;(WQ44;Zzf~7_QiCP*t`-OBS*6|(`CwS zY}>TVR>jgyDZ{>vze83wFHXJx7!ES@{6A0F9jer|dXs%Moo5@<@}3o;EK-|oO}~XR zT$pL0yFbn((Tith?IryQcPZ6b7OsJu_ z2km6riKpQOazK^CqeG3Txwa;#{Ot@3W675p@B?13eFpHkoMsmO>_O4mIq+Z*@-XPU zcMg_!X7A0zM|;)4e0nN5yD-hS^Wf$8^04ex1F6%QTwV^+T~rR?KAB!f$s<(ddI9>6 zsNI(h=vsM6H!WFfH5!P&Spd)BNAZSUUVaGQDWDZ9zgK{x1CO%sSJljFv-yzeFT!zD zda4MgvEW(}e3;!TLJ+fGiXcA7d^~b~XL$bddAN-p|1<}q7(DYJ@z+a8R`~7G|L%WZ bf&nz?j}r8IKfVNC6Q^+*&JzCII(+yadkj}6 diff --git a/public/mix-manifest.json b/public/mix-manifest.json index fdef32cdc61aa73c532e9b909346ae830023fe93..c1d72cb209b5f723edef7f77a4d11f047b7d297c 100644 GIT binary patch delta 44 zcmcbqcT;ae3#+_|L8@_*xmi+5qN#bJk)^qXNm7!LrG-VRg{fI`ipA!|tlI Date: Mon, 24 Apr 2023 01:55:10 -0600 Subject: [PATCH 3/3] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8729a93b..f7505491b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - New media:fix-nonlocal-driver command. Fixes s3 media created with invalid FILESYSTEM_DRIVER=s3 configuration ([672cccd4](https://github.com/pixelfed/pixelfed/commit/672cccd4)) - New landing page design ([09c0032b](https://github.com/pixelfed/pixelfed/commit/09c0032b)) - Add cloud ip bans to BouncerService ([50ab2e20](https://github.com/pixelfed/pixelfed/commit/50ab2e20)) +- Redesigned Admin Dashboard Reports/Moderation ([c6cc6327](https://github.com/pixelfed/pixelfed/commit/c6cc6327)) ### Updates - Update ApiV1Controller, fix blocking remote accounts. Closes #4256 ([8e71e0c0](https://github.com/pixelfed/pixelfed/commit/8e71e0c0))