Daniel Supernault 2 rokov pred
rodič
commit
a224c71151
3 zmenil súbory, kde vykonal 470 pridanie a 87 odobranie
  1. 455 84
      app/Http/Controllers/Api/AdminApiController.php
  2. 15 0
      routes/api.php
  3. 0 3
      routes/web.php

+ 455 - 84
app/Http/Controllers/Api/AdminApiController.php

@@ -5,114 +5,485 @@ namespace App\Http\Controllers\Api;
 use Illuminate\Http\Request;
 use Illuminate\Http\Request;
 use App\Http\Controllers\Controller;
 use App\Http\Controllers\Controller;
 use App\Jobs\StatusPipeline\StatusDelete;
 use App\Jobs\StatusPipeline\StatusDelete;
-use Auth, Cache;
+use Auth, Cache, DB;
 use Carbon\Carbon;
 use Carbon\Carbon;
 use App\{
 use App\{
+    AccountInterstitial,
     Like,
     Like,
     Media,
     Media,
     Profile,
     Profile,
-    Status
+    Report,
+    Status,
+    User
 };
 };
-
+use App\Services\AccountService;
+use App\Services\AdminStatsService;
+use App\Services\ConfigCacheService;
+use App\Services\ModLogService;
+use App\Services\StatusService;
 use App\Services\NotificationService;
 use App\Services\NotificationService;
+use App\Http\Resources\AdminUser;
 
 
 class AdminApiController extends Controller
 class AdminApiController extends Controller
 {
 {
-    public function __construct()
+    public function supported(Request $request)
+    {
+        abort_if(!$request->user(), 404);
+        abort_unless($request->user()->is_admin === 1, 404);
+
+        return response()->json(['supported' => true]);
+    }
+
+    public function getStats(Request $request)
     {
     {
-        $this->middleware(['auth', 'admin']);
+        abort_if(!$request->user(), 404);
+        abort_unless($request->user()->is_admin === 1, 404);
+        $res = AdminStatsService::summary();
+        $res['autospam_count'] = AccountInterstitial::whereType('post.autospam')
+            ->whereNull('appeal_handled_at')
+            ->count();
+        return $res;
     }
     }
 
 
-    public function activity(Request $request)
+    public function autospam(Request $request)
     {
     {
-        $activity = [];
-        
-        $limit = request()->input('limit', 20);
-
-        $activity['captions'] = Status::select(
-            'id', 
-            'caption', 
-            'rendered', 
-            'uri', 
-            'profile_id',
-            'type',
-            'in_reply_to_id',
-            'reblog_of_id',
-            'is_nsfw',
-            'scope',
-            'created_at'
-        )->whereNull('in_reply_to_id')
-        ->whereNull('reblog_of_id')
-        ->orderByDesc('created_at')
-        ->paginate($limit);
-
-        $activity['comments'] = Status::select(
-            'id', 
-            'caption', 
-            'rendered', 
-            'uri', 
-            'profile_id',
-            'type',
-            'in_reply_to_id',
-            'reblog_of_id',
-            'is_nsfw',
-            'scope',
-            'created_at'
-        )->whereNotNull('in_reply_to_id')
-        ->whereNull('reblog_of_id')
-        ->orderByDesc('created_at')
-        ->paginate($limit);
-
-        return response()->json($activity, 200, [], JSON_PRETTY_PRINT);
+        abort_if(!$request->user(), 404);
+        abort_unless($request->user()->is_admin === 1, 404);
+
+        $appeals = AccountInterstitial::whereType('post.autospam')
+            ->whereNull('appeal_handled_at')
+            ->latest()
+            ->simplePaginate(6)
+            ->map(function($report) {
+                $r = [
+                    'id' => $report->id,
+                    'type' => $report->type,
+                    'item_id' => $report->item_id,
+                    'item_type' => $report->item_type,
+                    'created_at' => $report->created_at
+                ];
+                if($report->item_type === 'App\\Status') {
+                    $status = StatusService::get($report->item_id, false);
+                    if(!$status) {
+                        return;
+                    }
+
+                    $r['status'] = $status;
+
+                    if($status['in_reply_to_id']) {
+                        $r['parent'] = StatusService::get($status['in_reply_to_id'], false);
+                    }
+                }
+                return $r;
+            });
+
+        return $appeals;
     }
     }
 
 
-    public function moderateStatus(Request $request)
+    public function autospamHandle(Request $request)
     {
     {
-        abort(400, 'Unpublished API');
-        return;
+        abort_if(!$request->user(), 404);
+        abort_unless($request->user()->is_admin === 1, 404);
+
         $this->validate($request, [
         $this->validate($request, [
-            'type' => 'required|string|in:status,profile',
-            'id'   => 'required|integer|min:1',
-            'action' => 'required|string|in:cw,unlink,unlist,suspend,delete'
+            'action' => 'required|in:dismiss,approve,dismiss-all,approve-all',
+            'id' => 'required'
         ]);
         ]);
 
 
-        $type = $request->input('type');
+        $action = $request->input('action');
         $id = $request->input('id');
         $id = $request->input('id');
+        $appeal = AccountInterstitial::whereType('post.autospam')
+            ->whereNull('appeal_handled_at')
+            ->findOrFail($id);
+        $now = now();
+        $res = ['status' => 'success'];
+        $meta = json_decode($appeal->meta);
+
+        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('admin-dash:reports:spam-count');
+            return $res;
+        }
+
+        if($action == 'dismiss-all') {
+            AccountInterstitial::whereType('post.autospam')
+                ->whereItemType('App\Status')
+                ->whereNull('appeal_handled_at')
+                ->whereUserId($appeal->user_id)
+                ->update(['appeal_handled_at' => $now, 'is_spam' => true]);
+            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('admin-dash:reports:spam-count');
+            return $res;
+        }
+
+        if($action == 'approve') {
+            $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);
+
+            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('admin-dash:reports:spam-count');
+            return $res;
+        }
+
+        if($action == 'approve-all') {
+            AccountInterstitial::whereType('post.autospam')
+                ->whereItemType('App\Status')
+                ->whereNull('appeal_handled_at')
+                ->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, true);
+                    }
+                });
+            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('admin-dash:reports:spam-count');
+            return $res;
+        }
+
+        return $res;
+    }
+
+    public function modReports(Request $request)
+    {
+        abort_if(!$request->user(), 404);
+        abort_unless($request->user()->is_admin === 1, 404);
+
+        $reports = Report::whereNull('admin_seen')
+            ->orderBy('created_at','desc')
+            ->paginate(6)
+            ->map(function($report) {
+                $r = [
+                    'id' => $report->id,
+                    'type' => $report->type,
+                    'message' => $report->message,
+                    'object_id' => $report->object_id,
+                    'object_type' => $report->object_type,
+                    'created_at' => $report->created_at
+                ];
+
+                if($report->profile_id) {
+                    $r['reported_by_account'] = AccountService::get($report->profile_id, true);
+                }
+
+                if($report->object_type === 'App\\Status') {
+                    $status = StatusService::get($report->object_id, false);
+                    if(!$status) {
+                        return;
+                    }
+
+                    $r['status'] = $status;
+
+                    if($status['in_reply_to_id']) {
+                        $r['parent'] = StatusService::get($status['in_reply_to_id'], false);
+                    }
+                }
+
+                if($report->object_type === 'App\\Profile') {
+                    $r['account'] = AccountService::get($report->object_id, false);
+                }
+                return $r;
+            })
+            ->filter()
+            ->values();
+
+        return $reports;
+    }
+
+    public function modReportHandle(Request $request)
+    {
+        abort_if(!$request->user(), 404);
+        abort_unless($request->user()->is_admin === 1, 404);
+
+        $this->validate($request, [
+            'action'    => 'required|string',
+            'id' => 'required'
+        ]);
+
         $action = $request->input('action');
         $action = $request->input('action');
+        $id = $request->input('id');
+
+        $actions = [
+            'ignore',
+            'cw',
+            'unlist'
+        ];
+
+        if (!in_array($action, $actions)) {
+            return abort(403);
+        }
+
+        $report = Report::findOrFail($id);
+        $item = $report->reported();
+        $report->admin_seen = now();
+
+        switch ($action) {
+            case 'ignore':
+                $report->not_interested = true;
+                break;
 
 
-        if ($type == 'status') {
-            $status = Status::findOrFail($id);
-            switch ($action) {
-                case 'cw':
-                    $status->is_nsfw = true;
-                    $status->save();
-                    break;
-                case 'unlink':
-                    $status->rendered = $status->caption;
-                    $status->save();
-                    break;
-                case 'unlist':
-                    $status->scope = 'unlisted';
-                    $status->visibility = 'unlisted';
-                    $status->save();
-                    break;
-                
-                default:
-                    break;
-            }
-        } else if ($type == 'profile') {
-            $profile = Profile::findOrFail($id);
-            switch ($action) {
-
-                case 'delete':
-                    StatusDelete::dispatch($status);
-                    break;
-                
-                default:
-                    break;
-            }
+            case 'cw':
+                Cache::forget('status:thumb:'.$item->id);
+                $item->is_nsfw = true;
+                $item->save();
+                $report->nsfw = true;
+                StatusService::del($item->id, true);
+                break;
+
+            case 'unlist':
+                $item->visibility = 'unlisted';
+                $item->save();
+                StatusService::del($item->id, true);
+                break;
+
+            default:
+                $report->admin_seen = null;
+                break;
         }
         }
 
 
+        $report->save();
+        Cache::forget('admin-dash:reports:list-cache');
+        Cache::forget('admin:dashboard:home:data:v0:15min');
+
+        return ['success' => true];
+    }
+
+    public function getConfiguration(Request $request)
+    {
+        abort_if(!$request->user(), 404);
+        abort_unless($request->user()->is_admin === 1, 404);
+        abort_unless(config('instance.enable_cc'), 400);
+
+        return collect([
+            [
+                'name' => 'ActivityPub Federation',
+                'description' => 'Enable activitypub federation support, compatible with Pixelfed, Mastodon and other platforms.',
+                'key' => 'federation.activitypub.enabled'
+            ],
+
+            [
+                'name' => 'Open Registration',
+                'description' => 'Allow new account registrations.',
+                'key' => 'pixelfed.open_registration'
+            ],
+
+            [
+                'name' => 'Stories',
+                'description' => 'Enable the ephemeral Stories feature.',
+                'key' => 'instance.stories.enabled'
+            ],
+
+            [
+                'name' => 'Require Email Verification',
+                'description' => 'Require new accounts to verify their email address.',
+                'key' => 'pixelfed.enforce_email_verification'
+            ],
+
+            [
+                'name' => 'AutoSpam Detection',
+                'description' => 'Detect and remove spam from public timelines.',
+                'key' => 'pixelfed.bouncer.enabled'
+            ],
+        ])
+        ->map(function($s) {
+            $s['state'] = (bool) config_cache($s['key']);
+            return $s;
+        });
     }
     }
 
 
-}
+    public function updateConfiguration(Request $request)
+    {
+        abort_if(!$request->user(), 404);
+        abort_unless($request->user()->is_admin === 1, 404);
+        abort_unless(config('instance.enable_cc'), 400);
+
+        $this->validate($request, [
+            'key' => 'required',
+            'value' => 'required'
+        ]);
+
+        $allowedKeys = [
+            'federation.activitypub.enabled',
+            'pixelfed.open_registration',
+            'instance.stories.enabled',
+            'pixelfed.enforce_email_verification',
+            'pixelfed.bouncer.enabled',
+        ];
+
+        $key = $request->input('key');
+        $value = (bool) filter_var($request->input('value'), FILTER_VALIDATE_BOOLEAN);
+        abort_if(!in_array($key, $allowedKeys), 400, 'Invalid cache key.');
+
+        ConfigCacheService::put($key, $value);
+
+                return collect([
+            [
+                'name' => 'ActivityPub Federation',
+                'description' => 'Enable activitypub federation support, compatible with Pixelfed, Mastodon and other platforms.',
+                'key' => 'federation.activitypub.enabled'
+            ],
+
+            [
+                'name' => 'Open Registration',
+                'description' => 'Allow new account registrations.',
+                'key' => 'pixelfed.open_registration'
+            ],
+
+            [
+                'name' => 'Stories',
+                'description' => 'Enable the ephemeral Stories feature.',
+                'key' => 'instance.stories.enabled'
+            ],
+
+            [
+                'name' => 'Require Email Verification',
+                'description' => 'Require new accounts to verify their email address.',
+                'key' => 'pixelfed.enforce_email_verification'
+            ],
+
+            [
+                'name' => 'AutoSpam Detection',
+                'description' => 'Detect and remove spam from public timelines.',
+                'key' => 'pixelfed.bouncer.enabled'
+            ],
+        ])
+        ->map(function($s) {
+            $s['state'] = (bool) config_cache($s['key']);
+            return $s;
+        });
+    }
+
+    public function getUsers(Request $request)
+    {
+        abort_if(!$request->user(), 404);
+        abort_unless($request->user()->is_admin === 1, 404);
+        $q = $request->input('q');
+        $sort = $request->input('sort', 'desc') === 'asc' ? 'asc' : 'desc';
+        $res = User::whereNull('status')
+            ->when($q, function($query, $q) {
+                return $query->where('username', 'like', '%' . $q . '%');
+            })
+            ->orderBy('id', $sort)
+            ->cursorPaginate(10);
+        return AdminUser::collection($res);
+    }
+
+    public function getUser(Request $request)
+    {
+        abort_if(!$request->user(), 404);
+        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
+            ]
+        ]]);
+    }
+
+    public function userAdminAction(Request $request)
+    {
+        abort_if(!$request->user(), 404);
+        abort_unless($request->user()->is_admin === 1, 404);
+
+        $this->validate($request, [
+            'id' => 'required',
+            'action' => 'required|in:unlisted,cw,no_autolink,refresh_stats,verify_email',
+            'value' => 'sometimes'
+        ]);
+
+        $id = $request->input('id');
+        $user = User::findOrFail($id);
+        $profile = Profile::findOrFail($user->profile_id);
+        $action = $request->input('action');
+
+        abort_if($user->is_admin == true && $action !== 'refresh_stats', 400, 'Cannot moderate admin accounts');
+
+        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)
+                ->whereNull('in_reply_to_id')
+                ->whereNull('reblog_of_id')
+                ->whereIn('scope', ['public', 'unlisted', 'private'])
+                ->count();
+            $profile->status_count = $statusCount;
+            $profile->save();
+        } else if($action === 'verify_email') {
+            $user->email_verified_at = now();
+            $user->save();
+
+            ModLogService::boot()
+                ->objectUid($user->id)
+                ->objectId($user->id)
+                ->objectType('App\User::class')
+                ->user($request->user())
+                ->action('admin.user.moderate')
+                ->metadata([
+                    'action' => 'Manually verified email address',
+                    'message' => 'Success!'
+                ])
+                ->accessLevel('admin')
+                ->save();
+        } else {
+            $profile->{$action} = filter_var($request->input('value'), FILTER_VALIDATE_BOOLEAN);
+            $profile->save();
+
+            ModLogService::boot()
+                ->objectUid($user->id)
+                ->objectId($user->id)
+                ->objectType('App\User::class')
+                ->user($request->user())
+                ->action('admin.user.moderate')
+                ->metadata([
+                    'action' => $action,
+                    'message' => 'Success!'
+                ])
+                ->accessLevel('admin')
+                ->save();
+        }
+
+        AccountService::del($user->profile_id);
+        $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
+            ]
+        ]]);
+    }
+}

+ 15 - 0
routes/api.php

@@ -179,4 +179,19 @@ Route::group(['prefix' => 'api'], function() use($middleware) {
 		Route::post('broadcast/publish', 'LiveStreamController@clientBroadcastPublish');
 		Route::post('broadcast/publish', 'LiveStreamController@clientBroadcastPublish');
 		Route::post('broadcast/finish', 'LiveStreamController@clientBroadcastFinish');
 		Route::post('broadcast/finish', 'LiveStreamController@clientBroadcastFinish');
 	});
 	});
+
+	Route::group(['prefix' => 'admin'], function() use($middleware) {
+		Route::get('supported', 'Api\AdminApiController@supported')->middleware($middleware);
+		Route::get('stats', 'Api\AdminApiController@getStats')->middleware($middleware);
+
+		Route::get('autospam/list', 'Api\AdminApiController@autospam')->middleware($middleware);
+		Route::post('autospam/handle', 'Api\AdminApiController@autospamHandle')->middleware($middleware);
+		Route::get('mod-reports/list', 'Api\AdminApiController@modReports')->middleware($middleware);
+		Route::post('mod-reports/handle', 'Api\AdminApiController@modReportHandle')->middleware($middleware);
+		Route::get('config', 'Api\AdminApiController@getConfiguration')->middleware($middleware);
+		Route::post('config/update', 'Api\AdminApiController@updateConfiguration')->middleware($middleware);
+		Route::get('users/list', 'Api\AdminApiController@getUsers')->middleware($middleware);
+		Route::get('users/get', 'Api\AdminApiController@getUser')->middleware($middleware);
+		Route::post('users/action', 'Api\AdminApiController@userAdminAction')->middleware($middleware);
+	});
 });
 });

+ 0 - 3
routes/web.php

@@ -286,9 +286,6 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
 			Route::get('compose/location/search', 'ApiController@composeLocationSearch');
 			Route::get('compose/location/search', 'ApiController@composeLocationSearch');
 			Route::post('compose/tag/untagme', 'MediaTagController@untagProfile');
 			Route::post('compose/tag/untagme', 'MediaTagController@untagProfile');
 		});
 		});
-		Route::group(['prefix' => 'admin'], function () {
-			Route::post('moderate', 'Api\AdminApiController@moderate');
-		});
 
 
 		Route::group(['prefix' => 'web/stories'], function () {
 		Route::group(['prefix' => 'web/stories'], function () {
 			Route::get('v1/recent', 'StoryController@recent');
 			Route::get('v1/recent', 'StoryController@recent');