Browse Source

Merge pull request #5005 from pixelfed/staging

Update Admin Settings
daniel 1 năm trước cách đây
mục cha
commit
0b162dc15e
100 tập tin đã thay đổi với 5096 bổ sung2800 xóa
  1. 8 4
      .circleci/config.yml
  2. 5 5
      app/Console/Commands/AvatarStorage.php
  3. 1 1
      app/Console/Commands/AvatarStorageDeepClean.php
  4. 5 1
      app/Console/Commands/CloudMediaMigrate.php
  5. 7 7
      app/Console/Commands/FetchMissingMediaMimeType.php
  6. 1 1
      app/Console/Commands/FixMediaDriver.php
  7. 1 1
      app/Console/Commands/MediaCloudUrlRewrite.php
  8. 1 1
      app/Console/Commands/MediaS3GarbageCollector.php
  9. 1 1
      app/Console/Kernel.php
  10. 2 2
      app/Http/Controllers/AccountController.php
  11. 94 96
      app/Http/Controllers/Admin/AdminDirectoryController.php
  12. 572 0
      app/Http/Controllers/Admin/AdminSettingsController.php
  13. 6 6
      app/Http/Controllers/AdminController.php
  14. 11 11
      app/Http/Controllers/Api/ApiV1Controller.php
  15. 1 1
      app/Http/Controllers/Api/V1/DomainBlockController.php
  16. 1 1
      app/Http/Controllers/Auth/ForgotPasswordController.php
  17. 3 3
      app/Http/Controllers/Auth/LoginController.php
  18. 1 1
      app/Http/Controllers/Auth/RegisterController.php
  19. 1 1
      app/Http/Controllers/Auth/ResetPasswordController.php
  20. 1 1
      app/Http/Controllers/ComposeController.php
  21. 87 91
      app/Http/Controllers/FederationController.php
  22. 25 1
      app/Http/Controllers/Import/Instagram.php
  23. 1 1
      app/Http/Controllers/ImportPostController.php
  24. 20 21
      app/Http/Controllers/LandingController.php
  25. 19 18
      app/Http/Controllers/MediaController.php
  26. 52 42
      app/Http/Controllers/PixelfedDirectoryController.php
  27. 3 1
      app/Http/Controllers/ProfileController.php
  28. 90 80
      app/Http/Controllers/RemoteAuthController.php
  29. 353 354
      app/Http/Controllers/SearchController.php
  30. 1 1
      app/Http/Controllers/StatusController.php
  31. 131 121
      app/Http/Controllers/Stories/StoryApiV1Controller.php
  32. 89 93
      app/Http/Controllers/StoryComposeController.php
  33. 9 9
      app/Http/Controllers/StoryController.php
  34. 1 1
      app/Http/Controllers/UserEmailForgotController.php
  35. 32 31
      app/Http/Requests/Status/StoreStatusEditRequest.php
  36. 76 76
      app/Jobs/AvatarPipeline/AvatarOptimize.php
  37. 95 100
      app/Jobs/AvatarPipeline/RemoteAvatarFetch.php
  38. 76 81
      app/Jobs/AvatarPipeline/RemoteAvatarFetchFromUrl.php
  39. 1 1
      app/Jobs/ImageOptimizePipeline/ImageOptimize.php
  40. 1 1
      app/Jobs/ImageOptimizePipeline/ImageResize.php
  41. 1 1
      app/Jobs/ImageOptimizePipeline/ImageUpdate.php
  42. 49 46
      app/Jobs/MediaPipeline/MediaDeletePipeline.php
  43. 61 60
      app/Jobs/MediaPipeline/MediaFixLocalFilesystemCleanupPipeline.php
  44. 2 4
      app/Jobs/RemoteFollowPipeline/RemoteFollowImportRecent.php
  45. 136 135
      app/Jobs/SharePipeline/SharePipeline.php
  46. 126 123
      app/Jobs/SharePipeline/UndoSharePipeline.php
  47. 162 166
      app/Jobs/StatusPipeline/StatusDelete.php
  48. 22 21
      app/Jobs/StatusPipeline/StatusEntityLexer.php
  49. 155 156
      app/Jobs/StatusPipeline/StatusRemoteUpdatePipeline.php
  50. 1 1
      app/Models/CustomEmoji.php
  51. 4 10
      app/Observers/AvatarObserver.php
  52. 1 1
      app/Observers/UserObserver.php
  53. 4 4
      app/Providers/AuthServiceProvider.php
  54. 166 0
      app/Services/AdminSettingsService.php
  55. 73 68
      app/Services/AutospamService.php
  56. 68 4
      app/Services/ConfigCacheService.php
  57. 2 2
      app/Services/CustomEmojiService.php
  58. 82 0
      app/Services/FilesystemService.php
  59. 44 0
      app/Services/Internal/BeagleService.php
  60. 3 3
      app/Services/LandingService.php
  61. 56 61
      app/Services/MediaStorageService.php
  62. 164 167
      app/Util/ActivityPub/Helpers.php
  63. 24 26
      app/Util/ActivityPub/Outbox.php
  64. 4 4
      app/Util/Site/Config.php
  65. 1 1
      database/migrations/2023_02_04_053028_fix_cloud_media_paths.php
  66. BIN
      public/css/admin.css
  67. BIN
      public/css/spa.css
  68. BIN
      public/js/admin.js
  69. BIN
      public/js/manifest.js
  70. BIN
      public/js/profile.chunk.a2234f891ba86efd.js
  71. BIN
      public/mix-manifest.json
  72. 3 10
      resources/assets/components/Hashtag.vue
  73. 9 4
      resources/assets/components/admin/AdminInstances.vue
  74. 1550 0
      resources/assets/components/admin/AdminSettings.vue
  75. 1 1
      resources/assets/components/admin/partial/AdminReadMore.vue
  76. 42 0
      resources/assets/components/admin/partial/AdminSettingsCheckbox.vue
  77. 74 0
      resources/assets/components/admin/partial/AdminSettingsInput.vue
  78. 63 0
      resources/assets/components/admin/partial/AdminSettingsTabHeader.vue
  79. 10 0
      resources/assets/js/admin.js
  80. 1 0
      resources/assets/sass/lib/nucleo.css
  81. 4 0
      resources/assets/sass/spa.scss
  82. 2 2
      resources/views/account/moderation/post/autospam.blade.php
  83. 2 2
      resources/views/account/moderation/post/cw.blade.php
  84. 2 2
      resources/views/account/moderation/post/removed.blade.php
  85. 2 2
      resources/views/account/moderation/post/unlist.blade.php
  86. 10 10
      resources/views/admin/diagnostics/home.blade.php
  87. 2 411
      resources/views/admin/settings/home.blade.php
  88. 1 1
      resources/views/auth/email/forgot.blade.php
  89. 3 3
      resources/views/auth/login.blade.php
  90. 1 1
      resources/views/auth/passwords/email.blade.php
  91. 1 1
      resources/views/auth/passwords/reset.blade.php
  92. 1 1
      resources/views/auth/register.blade.php
  93. 2 2
      resources/views/home.blade.php
  94. 3 3
      resources/views/layouts/app-guest.blade.php
  95. 2 2
      resources/views/layouts/app.blade.php
  96. 2 2
      resources/views/layouts/blank.blade.php
  97. 3 3
      resources/views/layouts/bundle.blade.php
  98. 1 1
      resources/views/layouts/partial/nav.blade.php
  99. 1 1
      resources/views/layouts/partial/noauthnav.blade.php
  100. 2 2
      resources/views/portfolio/layout.blade.php

+ 8 - 4
.circleci/config.yml

@@ -21,7 +21,12 @@ jobs:
     steps:
       - checkout
 
-      - run: sudo apt update && sudo apt install zlib1g-dev libsqlite3-dev
+      - run:
+          name: "Create Environment file and generate app key"
+          command: |
+            mv .env.testing .env
+
+      - run: sudo apt install zlib1g-dev libsqlite3-dev
 
       # Download and cache dependencies
 
@@ -36,18 +41,17 @@ jobs:
       - run: composer install -n --prefer-dist
 
       - save_cache:
-          key: composer-v2-{{ checksum "composer.lock" }}
+          key: v2-dependencies-{{ checksum "composer.json" }}
           paths:
             - vendor
 
-      - run: cp .env.testing .env
       - run: php artisan config:cache
       - run: php artisan route:clear
       - run: php artisan storage:link
       - run: php artisan key:generate
 
       # run tests with phpunit or codecept
-      - run: ./vendor/bin/phpunit
+      - run: php artisan test
       - store_test_results:
           path: tests/_output
       - store_artifacts:

+ 5 - 5
app/Console/Commands/AvatarStorage.php

@@ -82,7 +82,7 @@ class AvatarStorage extends Command
 
         $this->line(' ');
 
-        if(config_cache('pixelfed.cloud_storage')) {
+        if((bool) config_cache('pixelfed.cloud_storage')) {
             $this->info('✅ - Cloud storage configured');
             $this->line(' ');
         }
@@ -92,7 +92,7 @@ class AvatarStorage extends Command
             $this->line(' ');
         }
 
-        if(config_cache('pixelfed.cloud_storage') && config('instance.avatar.local_to_cloud')) {
+        if((bool) config_cache('pixelfed.cloud_storage') && config('instance.avatar.local_to_cloud')) {
             $disk = Storage::disk(config_cache('filesystems.cloud'));
             $exists = $disk->exists('cache/avatars/default.jpg');
             $state = $exists ? '✅' : '❌';
@@ -100,7 +100,7 @@ class AvatarStorage extends Command
             $this->info($msg);
         }
 
-        $options = config_cache('pixelfed.cloud_storage') && config('instance.avatar.local_to_cloud') ?
+        $options = (bool) config_cache('pixelfed.cloud_storage') && config('instance.avatar.local_to_cloud') ?
             [
                 'Cancel',
                 'Upload default avatar to cloud',
@@ -164,7 +164,7 @@ class AvatarStorage extends Command
 
     protected function uploadAvatarsToCloud()
     {
-        if(!config_cache('pixelfed.cloud_storage') || !config('instance.avatar.local_to_cloud')) {
+        if(!(bool) config_cache('pixelfed.cloud_storage') || !config('instance.avatar.local_to_cloud')) {
             $this->error('Enable cloud storage and avatar cloud storage to perform this action');
             return;
         }
@@ -213,7 +213,7 @@ class AvatarStorage extends Command
             return;
         }
 
-        if(config_cache('pixelfed.cloud_storage') == false && config_cache('federation.avatars.store_local') == false) {
+        if((bool) config_cache('pixelfed.cloud_storage') == false && config_cache('federation.avatars.store_local') == false) {
             $this->error('You have cloud storage disabled and local avatar storage disabled, we cannot refetch avatars.');
             return;
         }

+ 1 - 1
app/Console/Commands/AvatarStorageDeepClean.php

@@ -44,7 +44,7 @@ class AvatarStorageDeepClean extends Command
         $this->line(' ');
 
         $storage = [
-            'cloud' => boolval(config_cache('pixelfed.cloud_storage')),
+            'cloud' => (bool) config_cache('pixelfed.cloud_storage'),
             'local' => boolval(config_cache('federation.avatars.store_local'))
         ];
 

+ 5 - 1
app/Console/Commands/CloudMediaMigrate.php

@@ -35,12 +35,16 @@ class CloudMediaMigrate extends Command
      */
     public function handle()
     {
-        $enabled = config('pixelfed.cloud_storage');
+        $enabled = (bool) config_cache('pixelfed.cloud_storage');
         if(!$enabled) {
             $this->error('Cloud storage not enabled. Exiting...');
             return;
         }
 
+        if(!$this->confirm('Are you sure you want to proceed?')) {
+            return;
+        }
+
         $limit = $this->option('limit');
         $hugeMode = $this->option('huge');
 

+ 7 - 7
app/Console/Commands/FetchMissingMediaMimeType.php

@@ -2,11 +2,11 @@
 
 namespace App\Console\Commands;
 
-use Illuminate\Console\Command;
 use App\Media;
-use Illuminate\Support\Facades\Http;
 use App\Services\MediaService;
 use App\Services\StatusService;
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Http;
 
 class FetchMissingMediaMimeType extends Command
 {
@@ -29,20 +29,20 @@ class FetchMissingMediaMimeType extends Command
      */
     public function handle()
     {
-        foreach(Media::whereNotNull(['remote_url', 'status_id'])->whereNull('mime')->lazyByIdDesc(50, 'id') as $media) {
+        foreach (Media::whereNotNull(['remote_url', 'status_id'])->whereNull('mime')->lazyByIdDesc(50, 'id') as $media) {
             $res = Http::retry(2, 100, throw: false)->head($media->remote_url);
 
-            if(!$res->successful()) {
+            if (! $res->successful()) {
                 continue;
             }
 
-            if(!in_array($res->header('content-type'), explode(',',config('pixelfed.media_types')))) {
+            if (! in_array($res->header('content-type'), explode(',', config_cache('pixelfed.media_types')))) {
                 continue;
             }
 
             $media->mime = $res->header('content-type');
 
-            if($res->hasHeader('content-length')) {
+            if ($res->hasHeader('content-length')) {
                 $media->size = $res->header('content-length');
             }
 
@@ -50,7 +50,7 @@ class FetchMissingMediaMimeType extends Command
 
             MediaService::del($media->status_id);
             StatusService::del($media->status_id);
-            $this->info('mid:'.$media->id . ' (' . $res->header('content-type') . ':' . $res->header('content-length') . ' bytes)');
+            $this->info('mid:'.$media->id.' ('.$res->header('content-type').':'.$res->header('content-length').' bytes)');
         }
     }
 }

+ 1 - 1
app/Console/Commands/FixMediaDriver.php

@@ -37,7 +37,7 @@ class FixMediaDriver extends Command
 			return Command::SUCCESS;
 		}
 
-		if(config_cache('pixelfed.cloud_storage') == false) {
+		if((bool) config_cache('pixelfed.cloud_storage') == false) {
 			$this->error('Cloud storage not enabled, exiting...');
 			return Command::SUCCESS;
 		}

+ 1 - 1
app/Console/Commands/MediaCloudUrlRewrite.php

@@ -47,7 +47,7 @@ class MediaCloudUrlRewrite extends Command implements PromptsForMissingInput
 
     protected function preflightCheck()
     {
-        if(config_cache('pixelfed.cloud_storage') != true) {
+        if(!(bool) config_cache('pixelfed.cloud_storage')) {
             $this->info('Error: Cloud storage is not enabled!');
             $this->error('Aborting...');
             exit;

+ 1 - 1
app/Console/Commands/MediaS3GarbageCollector.php

@@ -45,7 +45,7 @@ class MediaS3GarbageCollector extends Command
     */
     public function handle()
     {
-        $enabled = in_array(config_cache('pixelfed.cloud_storage'), ['1', true, 'true']);
+        $enabled = (bool) config_cache('pixelfed.cloud_storage');
         if(!$enabled) {
             $this->error('Cloud storage not enabled. Exiting...');
             return;

+ 1 - 1
app/Console/Kernel.php

@@ -33,7 +33,7 @@ class Kernel extends ConsoleKernel
         $schedule->command('gc:passwordreset')->dailyAt('09:41')->onOneServer();
         $schedule->command('gc:sessions')->twiceDaily(13, 23)->onOneServer();
 
-        if (in_array(config_cache('pixelfed.cloud_storage'), ['1', true, 'true']) && config('media.delete_local_after_cloud')) {
+        if ((bool) config_cache('pixelfed.cloud_storage') && (bool) config_cache('media.delete_local_after_cloud')) {
             $schedule->command('media:s3gc')->hourlyAt(15);
         }
 

+ 2 - 2
app/Http/Controllers/AccountController.php

@@ -157,7 +157,7 @@ class AccountController extends Controller
 
 		$pid = $request->user()->profile_id;
 		$count = UserFilterService::muteCount($pid);
-		$maxLimit = intval(config('instance.user_filters.max_user_mutes'));
+		$maxLimit = (int) config_cache('instance.user_filters.max_user_mutes');
 		abort_if($count >= $maxLimit, 422, self::FILTER_LIMIT_MUTE_TEXT . $maxLimit . ' accounts');
 		if($count == 0) {
 			$filterCount = UserFilter::whereUserId($pid)->count();
@@ -260,7 +260,7 @@ class AccountController extends Controller
 		]);
 		$pid = $request->user()->profile_id;
 		$count = UserFilterService::blockCount($pid);
-		$maxLimit = intval(config('instance.user_filters.max_user_blocks'));
+		$maxLimit = (int) config_cache('instance.user_filters.max_user_blocks');
 		abort_if($count >= $maxLimit, 422, self::FILTER_LIMIT_BLOCK_TEXT . $maxLimit . ' accounts');
 		if($count == 0) {
 			$filterCount = UserFilter::whereUserId($pid)->whereFilterType('block')->count();

+ 94 - 96
app/Http/Controllers/Admin/AdminDirectoryController.php

@@ -2,30 +2,20 @@
 
 namespace App\Http\Controllers\Admin;
 
-use DB, Cache;
-use App\{
-    DiscoverCategory,
-    DiscoverCategoryHashtag,
-    Hashtag,
-    Media,
-    Profile,
-    Status,
-    StatusHashtag,
-    User
-};
+use App\Http\Controllers\PixelfedDirectoryController;
 use App\Models\ConfigCache;
 use App\Services\AccountService;
 use App\Services\ConfigCacheService;
 use App\Services\StatusService;
-use Carbon\Carbon;
+use App\Status;
+use App\User;
+use Cache;
 use Illuminate\Http\Request;
-use Illuminate\Validation\Rule;
-use League\ISO3166\ISO3166;
-use Illuminate\Support\Str;
+use Illuminate\Support\Facades\Http;
 use Illuminate\Support\Facades\Storage;
 use Illuminate\Support\Facades\Validator;
-use Illuminate\Support\Facades\Http;
-use App\Http\Controllers\PixelfedDirectoryController;
+use Illuminate\Support\Str;
+use League\ISO3166\ISO3166;
 
 trait AdminDirectoryController
 {
@@ -41,37 +31,37 @@ trait AdminDirectoryController
         $res['countries'] = collect((new ISO3166)->all())->pluck('name');
         $res['admins'] = User::whereIsAdmin(true)
             ->where('2fa_enabled', true)
-            ->get()->map(function($user) {
-            return [
-                'uid' => (string) $user->id,
-                'pid' => (string) $user->profile_id,
-                'username' => $user->username,
-                'created_at' => $user->created_at
-            ];
-        });
+            ->get()->map(function ($user) {
+                return [
+                    'uid' => (string) $user->id,
+                    'pid' => (string) $user->profile_id,
+                    'username' => $user->username,
+                    'created_at' => $user->created_at,
+                ];
+            });
         $config = ConfigCache::whereK('pixelfed.directory')->first();
-        if($config) {
+        if ($config) {
             $data = $config->v ? json_decode($config->v, true) : [];
             $res = array_merge($res, $data);
         }
 
-        if(empty($res['summary'])) {
+        if (empty($res['summary'])) {
             $summary = ConfigCache::whereK('app.short_description')->pluck('v');
             $res['summary'] = $summary ? $summary[0] : null;
         }
 
-        if(isset($res['banner_image']) && !empty($res['banner_image'])) {
+        if (isset($res['banner_image']) && ! empty($res['banner_image'])) {
             $res['banner_image'] = url(Storage::url($res['banner_image']));
         }
 
-        if(isset($res['favourite_posts'])) {
-            $res['favourite_posts'] = collect($res['favourite_posts'])->map(function($id) {
+        if (isset($res['favourite_posts'])) {
+            $res['favourite_posts'] = collect($res['favourite_posts'])->map(function ($id) {
                 return StatusService::get($id);
             })
-            ->filter(function($post) {
-                return $post && isset($post['account']);
-            })
-            ->values();
+                ->filter(function ($post) {
+                    return $post && isset($post['account']);
+                })
+                ->values();
         }
 
         $res['community_guidelines'] = config_cache('app.rules') ? json_decode(config_cache('app.rules'), true) : [];
@@ -84,22 +74,22 @@ trait AdminDirectoryController
         $res['feature_config'] = [
             'media_types' => Str::of(config_cache('pixelfed.media_types'))->explode(','),
             'image_quality' => config_cache('pixelfed.image_quality'),
-            'optimize_image' => config_cache('pixelfed.optimize_image'),
+            'optimize_image' => (bool) config_cache('pixelfed.optimize_image'),
             'max_photo_size' => config_cache('pixelfed.max_photo_size'),
             'max_caption_length' => config_cache('pixelfed.max_caption_length'),
             'max_altext_length' => config_cache('pixelfed.max_altext_length'),
-            'enforce_account_limit' => config_cache('pixelfed.enforce_account_limit'),
+            'enforce_account_limit' => (bool) config_cache('pixelfed.enforce_account_limit'),
             'max_account_size' => config_cache('pixelfed.max_account_size'),
             'max_album_length' => config_cache('pixelfed.max_album_length'),
-            'account_deletion' => config_cache('pixelfed.account_deletion'),
+            'account_deletion' => (bool) config_cache('pixelfed.account_deletion'),
         ];
 
-        if(config_cache('pixelfed.directory.testimonials')) {
-            $testimonials = collect(json_decode(config_cache('pixelfed.directory.testimonials'),true))
-                ->map(function($t) {
+        if (config_cache('pixelfed.directory.testimonials')) {
+            $testimonials = collect(json_decode(config_cache('pixelfed.directory.testimonials'), true))
+                ->map(function ($t) {
                     return [
                         'profile' => AccountService::get($t['profile_id']),
-                        'body' => $t['body']
+                        'body' => $t['body'],
                     ];
                 });
             $res['testimonials'] = $testimonials;
@@ -108,8 +98,8 @@ trait AdminDirectoryController
         $validator = Validator::make($res['feature_config'], [
             'media_types' => [
                 'required',
-                 function ($attribute, $value, $fail) {
-                    if (!in_array('image/jpeg', $value->toArray()) || !in_array('image/png', $value->toArray())) {
+                function ($attribute, $value, $fail) {
+                    if (! in_array('image/jpeg', $value->toArray()) || ! in_array('image/png', $value->toArray())) {
                         $fail('You must enable image/jpeg and image/png support.');
                     }
                 },
@@ -120,7 +110,7 @@ trait AdminDirectoryController
             'max_account_size' => 'required_if:enforce_account_limit,true|integer|min:1000000',
             'max_album_length' => 'required|integer|min:4|max:20',
             'account_deletion' => 'required|accepted',
-            'max_caption_length' => 'required|integer|min:500|max:10000'
+            'max_caption_length' => 'required|integer|min:500|max:10000',
         ]);
 
         $res['requirements_validator'] = $validator->errors();
@@ -146,11 +136,11 @@ trait AdminDirectoryController
         foreach (new \DirectoryIterator($path) as $io) {
             $name = $io->getFilename();
             $skip = ['vendor'];
-            if($io->isDot() || in_array($name, $skip)) {
+            if ($io->isDot() || in_array($name, $skip)) {
                 continue;
             }
 
-            if($io->isDir()) {
+            if ($io->isDir()) {
                 $langs->push(['code' => $name, 'name' => locale_get_display_name($name)]);
             }
         }
@@ -159,25 +149,26 @@ trait AdminDirectoryController
         $res['primary_locale'] = config('app.locale');
 
         $submissionState = Http::withoutVerifying()
-        ->post('https://pixelfed.org/api/v1/directory/check-submission', [
-            'domain' => config('pixelfed.domain.app')
-        ]);
+            ->post('https://pixelfed.org/api/v1/directory/check-submission', [
+                'domain' => config('pixelfed.domain.app'),
+            ]);
 
         $res['submission_state'] = $submissionState->json();
+
         return $res;
     }
 
     protected function validVal($res, $val, $count = false, $minLen = false)
     {
-        if(!isset($res[$val])) {
+        if (! isset($res[$val])) {
             return false;
         }
 
-        if($count) {
+        if ($count) {
             return count($res[$val]) >= $count;
         }
 
-        if($minLen) {
+        if ($minLen) {
             return strlen($res[$val]) >= $minLen;
         }
 
@@ -194,11 +185,11 @@ trait AdminDirectoryController
             'favourite_posts' => 'array|max:12',
             'favourite_posts.*' => 'distinct',
             'privacy_pledge' => 'sometimes',
-            'banner_image' => 'sometimes|mimes:jpg,png|dimensions:width=1920,height:1080|max:5000'
+            'banner_image' => 'sometimes|mimes:jpg,png|dimensions:width=1920,height:1080|max:5000',
         ]);
 
         $config = ConfigCache::firstOrNew([
-            'k' => 'pixelfed.directory'
+            'k' => 'pixelfed.directory',
         ]);
 
         $res = $config->v ? json_decode($config->v, true) : [];
@@ -208,26 +199,27 @@ trait AdminDirectoryController
         $res['contact_email'] = $request->input('contact_email');
         $res['privacy_pledge'] = (bool) $request->input('privacy_pledge');
 
-        if($request->filled('location')) {
+        if ($request->filled('location')) {
             $exists = (new ISO3166)->name($request->location);
-            if($exists) {
+            if ($exists) {
                 $res['location'] = $request->input('location');
             }
         }
 
-        if($request->hasFile('banner_image')) {
+        if ($request->hasFile('banner_image')) {
             collect(Storage::files('public/headers'))
-            ->filter(function($name) {
-                $protected = [
-                    'public/headers/.gitignore',
-                    'public/headers/default.jpg',
-                    'public/headers/missing.png'
-                ];
-                return !in_array($name, $protected);
-            })
-            ->each(function($name) {
-                Storage::delete($name);
-            });
+                ->filter(function ($name) {
+                    $protected = [
+                        'public/headers/.gitignore',
+                        'public/headers/default.jpg',
+                        'public/headers/missing.png',
+                    ];
+
+                    return ! in_array($name, $protected);
+                })
+                ->each(function ($name) {
+                    Storage::delete($name);
+                });
             $path = $request->file('banner_image')->storePublicly('public/headers');
             $res['banner_image'] = $path;
             ConfigCacheService::put('app.banner_image', url(Storage::url($path)));
@@ -240,9 +232,10 @@ trait AdminDirectoryController
 
         ConfigCacheService::put('pixelfed.directory', $config->v);
         $updated = json_decode($config->v, true);
-        if(isset($updated['banner_image'])) {
+        if (isset($updated['banner_image'])) {
             $updated['banner_image'] = url(Storage::url($updated['banner_image']));
         }
+
         return $updated;
     }
 
@@ -253,7 +246,7 @@ trait AdminDirectoryController
             'open_registration' => (bool) config_cache('pixelfed.open_registration'),
             'curated_onboarding' => (bool) config_cache('instance.curated_registration.enabled'),
             'activitypub_enabled' => config_cache('federation.activitypub.enabled'),
-            'oauth_enabled' => config_cache('pixelfed.oauth_enabled'),
+            'oauth_enabled' => (bool) config_cache('pixelfed.oauth_enabled'),
             'media_types' => Str::of(config_cache('pixelfed.media_types'))->explode(','),
             'image_quality' => config_cache('pixelfed.image_quality'),
             'optimize_image' => config_cache('pixelfed.optimize_image'),
@@ -273,8 +266,8 @@ trait AdminDirectoryController
             'oauth_enabled' => 'required|accepted',
             'media_types' => [
                 'required',
-                 function ($attribute, $value, $fail) {
-                    if (!in_array('image/jpeg', $value->toArray()) || !in_array('image/png', $value->toArray())) {
+                function ($attribute, $value, $fail) {
+                    if (! in_array('image/jpeg', $value->toArray()) || ! in_array('image/png', $value->toArray())) {
                         $fail('You must enable image/jpeg and image/png support.');
                     }
                 },
@@ -285,10 +278,10 @@ trait AdminDirectoryController
             'max_account_size' => 'required_if:enforce_account_limit,true|integer|min:1000000',
             'max_album_length' => 'required|integer|min:4|max:20',
             'account_deletion' => 'required|accepted',
-            'max_caption_length' => 'required|integer|min:500|max:10000'
+            'max_caption_length' => 'required|integer|min:500|max:10000',
         ]);
 
-        if(!$validator->validate()) {
+        if (! $validator->validate()) {
             return response()->json($validator->errors(), 422);
         }
 
@@ -297,6 +290,7 @@ trait AdminDirectoryController
 
         $data = (new PixelfedDirectoryController())->buildListing();
         $res = Http::withoutVerifying()->post('https://pixelfed.org/api/v1/directory/submission', $data);
+
         return 200;
     }
 
@@ -304,7 +298,7 @@ trait AdminDirectoryController
     {
         $bannerImage = ConfigCache::whereK('app.banner_image')->first();
         $directory = ConfigCache::whereK('pixelfed.directory')->first();
-        if(!$bannerImage && !$directory || empty($directory->v)) {
+        if (! $bannerImage && ! $directory || empty($directory->v)) {
             return;
         }
         $directoryArr = json_decode($directory->v, true);
@@ -312,12 +306,12 @@ trait AdminDirectoryController
         $protected = [
             'public/headers/.gitignore',
             'public/headers/default.jpg',
-            'public/headers/missing.png'
+            'public/headers/missing.png',
         ];
-        if(!$path || in_array($path, $protected)) {
+        if (! $path || in_array($path, $protected)) {
             return;
         }
-        if(Storage::exists($directoryArr['banner_image'])) {
+        if (Storage::exists($directoryArr['banner_image'])) {
             Storage::delete($directoryArr['banner_image']);
         }
 
@@ -328,12 +322,13 @@ trait AdminDirectoryController
         $bannerImage->save();
         Cache::forget('api:v1:instance-data-response-v1');
         ConfigCacheService::put('pixelfed.directory', $directory);
+
         return $bannerImage->v;
     }
 
     public function directoryGetPopularPosts(Request $request)
     {
-        $ids = Cache::remember('admin:api:popular_posts', 86400, function() {
+        $ids = Cache::remember('admin:api:popular_posts', 86400, function () {
             return Status::whereLocal(true)
                 ->whereScope('public')
                 ->whereType('photo')
@@ -343,21 +338,21 @@ trait AdminDirectoryController
                 ->pluck('id');
         });
 
-        $res = $ids->map(function($id) {
+        $res = $ids->map(function ($id) {
             return StatusService::get($id);
         })
-        ->filter(function($post) {
-            return $post && isset($post['account']);
-        })
-        ->values();
+            ->filter(function ($post) {
+                return $post && isset($post['account']);
+            })
+            ->values();
 
-        return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
+        return response()->json($res, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
     }
 
     public function directoryGetAddPostByIdSearch(Request $request)
     {
         $this->validate($request, [
-            'q' => 'required|integer'
+            'q' => 'required|integer',
         ]);
 
         $id = $request->input('q');
@@ -380,11 +375,12 @@ trait AdminDirectoryController
         $profile_id = $request->input('profile_id');
         $testimonials = ConfigCache::whereK('pixelfed.directory.testimonials')->firstOrFail();
         $existing = collect(json_decode($testimonials->v, true))
-            ->filter(function($t) use($profile_id) {
+            ->filter(function ($t) use ($profile_id) {
                 return $t['profile_id'] !== $profile_id;
             })
             ->values();
         ConfigCacheService::put('pixelfed.directory.testimonials', $existing);
+
         return $existing;
     }
 
@@ -392,13 +388,13 @@ trait AdminDirectoryController
     {
         $this->validate($request, [
             'username' => 'required',
-            'body' => 'required|string|min:5|max:500'
+            'body' => 'required|string|min:5|max:500',
         ]);
 
         $user = User::whereUsername($request->input('username'))->whereNull('status')->firstOrFail();
 
         $configCache = ConfigCache::firstOrCreate([
-            'k' => 'pixelfed.directory.testimonials'
+            'k' => 'pixelfed.directory.testimonials',
         ]);
 
         $testimonials = $configCache->v ? collect(json_decode($configCache->v, true)) : collect([]);
@@ -409,7 +405,7 @@ trait AdminDirectoryController
         $testimonials->push([
             'profile_id' => (string) $user->profile_id,
             'username' => $request->input('username'),
-            'body' => $request->input('body')
+            'body' => $request->input('body'),
         ]);
 
         $configCache->v = json_encode($testimonials->toArray());
@@ -417,8 +413,9 @@ trait AdminDirectoryController
         ConfigCacheService::put('pixelfed.directory.testimonials', $configCache->v);
         $res = [
             'profile' => AccountService::get($user->profile_id),
-            'body' => $request->input('body')
+            'body' => $request->input('body'),
         ];
+
         return $res;
     }
 
@@ -426,7 +423,7 @@ trait AdminDirectoryController
     {
         $this->validate($request, [
             'profile_id' => 'required',
-            'body' => 'required|string|min:5|max:500'
+            'body' => 'required|string|min:5|max:500',
         ]);
 
         $profile_id = $request->input('profile_id');
@@ -434,18 +431,19 @@ trait AdminDirectoryController
         $user = User::whereProfileId($profile_id)->firstOrFail();
 
         $configCache = ConfigCache::firstOrCreate([
-            'k' => 'pixelfed.directory.testimonials'
+            'k' => 'pixelfed.directory.testimonials',
         ]);
 
         $testimonials = $configCache->v ? collect(json_decode($configCache->v, true)) : collect([]);
 
-        $updated = $testimonials->map(function($t) use($profile_id, $body) {
-            if($t['profile_id'] == $profile_id) {
+        $updated = $testimonials->map(function ($t) use ($profile_id, $body) {
+            if ($t['profile_id'] == $profile_id) {
                 $t['body'] = $body;
             }
+
             return $t;
         })
-        ->values();
+            ->values();
 
         $configCache->v = json_encode($updated);
         $configCache->save();

+ 572 - 0
app/Http/Controllers/Admin/AdminSettingsController.php

@@ -7,7 +7,9 @@ use App\Models\InstanceActor;
 use App\Page;
 use App\Profile;
 use App\Services\AccountService;
+use App\Services\AdminSettingsService;
 use App\Services\ConfigCacheService;
+use App\Services\FilesystemService;
 use App\User;
 use App\Util\Site\Config;
 use Artisan;
@@ -71,6 +73,7 @@ trait AdminSettingsController
             'admin_account_id' => 'nullable',
             'regs' => 'required|in:open,filtered,closed',
             'account_migration' => 'nullable',
+            'rule_delete' => 'sometimes',
         ]);
 
         $orb = false;
@@ -310,4 +313,573 @@ trait AdminSettingsController
 
         return view('admin.settings.system', compact('sys'));
     }
+
+    public function settingsApiFetch(Request $request)
+    {
+        $cloud_storage = ConfigCacheService::get('pixelfed.cloud_storage');
+        $cloud_disk = config('filesystems.cloud');
+        $cloud_ready = ! empty(config('filesystems.disks.'.$cloud_disk.'.key')) && ! empty(config('filesystems.disks.'.$cloud_disk.'.secret'));
+        $types = explode(',', ConfigCacheService::get('pixelfed.media_types'));
+        $rules = ConfigCacheService::get('app.rules') ? json_decode(ConfigCacheService::get('app.rules'), true) : [];
+        $jpeg = in_array('image/jpg', $types) || in_array('image/jpeg', $types);
+        $png = in_array('image/png', $types);
+        $gif = in_array('image/gif', $types);
+        $mp4 = in_array('video/mp4', $types);
+        $webp = in_array('image/webp', $types);
+
+        $availableAdmins = User::whereIsAdmin(true)->get();
+        $currentAdmin = config_cache('instance.admin.pid') ? AccountService::get(config_cache('instance.admin.pid'), true) : null;
+        $openReg = (bool) config_cache('pixelfed.open_registration');
+        $curOnboarding = (bool) config_cache('instance.curated_registration.enabled');
+        $regState = $openReg ? 'open' : ($curOnboarding ? 'filtered' : 'closed');
+        $accountMigration = (bool) config_cache('federation.migration');
+        $autoFollow = config_cache('account.autofollow_usernames');
+        if (strlen($autoFollow) > 3) {
+            $autoFollow = explode(',', $autoFollow);
+        }
+
+        $res = AdminSettingsService::getAll();
+
+        return response()->json($res);
+    }
+
+    public function settingsApiRulesAdd(Request $request)
+    {
+        $this->validate($request, [
+            'rule' => 'required|string|min:5|max:1000',
+        ]);
+
+        $rules = ConfigCacheService::get('app.rules');
+        $val = $request->input('rule');
+        if (! $rules) {
+            ConfigCacheService::put('app.rules', json_encode([$val]));
+        } else {
+            $json = json_decode($rules, true);
+            $count = count($json);
+            if ($count >= 30) {
+                return response()->json(['message' => 'Max rules limit reached, you can set up to 30 rules at a time.'], 400);
+            }
+            $json[] = $val;
+            ConfigCacheService::put('app.rules', json_encode(array_values($json)));
+        }
+        Cache::forget('api:v1:instance-data:rules');
+        Cache::forget('api:v1:instance-data-response-v1');
+        Cache::forget('api:v2:instance-data-response-v2');
+        Config::refresh();
+
+        return [$val];
+    }
+
+    public function settingsApiRulesDelete(Request $request)
+    {
+        $this->validate($request, [
+            'rule' => 'required|string',
+        ]);
+
+        $rules = ConfigCacheService::get('app.rules');
+        $val = $request->input('rule');
+
+        if (! $rules) {
+            return [];
+        } else {
+            $json = json_decode($rules, true);
+            $idx = array_search($val, $json);
+            if ($idx !== false) {
+                unset($json[$idx]);
+                $json = array_values($json);
+            }
+            ConfigCacheService::put('app.rules', json_encode(array_values($json)));
+        }
+
+        Cache::forget('api:v1:instance-data:rules');
+        Cache::forget('api:v1:instance-data-response-v1');
+        Cache::forget('api:v2:instance-data-response-v2');
+        Config::refresh();
+
+        return response()->json($json);
+    }
+
+    public function settingsApiRulesDeleteAll(Request $request)
+    {
+        $rules = ConfigCacheService::get('app.rules');
+
+        if (! $rules) {
+            return [];
+        } else {
+            ConfigCacheService::put('app.rules', json_encode([]));
+        }
+
+        Cache::forget('api:v1:instance-data:rules');
+        Cache::forget('api:v1:instance-data-response-v1');
+        Cache::forget('api:v2:instance-data-response-v2');
+        Config::refresh();
+
+        return response()->json([]);
+    }
+
+    public function settingsApiAutofollowDelete(Request $request)
+    {
+        $this->validate($request, [
+            'username' => 'required|string',
+        ]);
+
+        $username = $request->input('username');
+        $names = [];
+        $existing = config_cache('account.autofollow_usernames');
+        if ($existing) {
+            $names = explode(',', $existing);
+        }
+
+        if (in_array($username, $names)) {
+            $key = array_search($username, $names);
+            if ($key !== false) {
+                unset($names[$key]);
+            }
+        }
+        ConfigCacheService::put('account.autofollow_usernames', implode(',', $names));
+
+        return response()->json(['accounts' => array_values($names)]);
+    }
+
+    public function settingsApiAutofollowAdd(Request $request)
+    {
+        $this->validate($request, [
+            'username' => 'required|string',
+        ]);
+
+        $username = $request->input('username');
+        $names = [];
+        $existing = config_cache('account.autofollow_usernames');
+        if ($existing) {
+            $names = explode(',', $existing);
+        }
+
+        if ($existing && count($names)) {
+            if (count($names) >= 5) {
+                return response()->json(['message' => 'You can only add up to 5 accounts to be autofollowed.'], 400);
+            }
+            if (in_array(strtolower($username), array_map('strtolower', $names))) {
+                return response()->json(['message' => 'User already exists, please try again.'], 400);
+            }
+        }
+
+        $p = User::whereUsername($username)->whereNull('status')->first();
+        if (! $p || in_array($p->username, $names)) {
+            abort(404);
+        }
+        array_push($names, $p->username);
+        ConfigCacheService::put('account.autofollow_usernames', implode(',', $names));
+
+        return response()->json(['accounts' => array_values($names)]);
+    }
+
+    public function settingsApiUpdateType(Request $request, $type)
+    {
+        abort_unless(in_array($type, [
+            'posts',
+            'platform',
+            'home',
+            'landing',
+            'branding',
+            'media',
+            'users',
+            'storage',
+        ]), 400);
+
+        switch ($type) {
+            case 'home':
+                return $this->settingsApiUpdateHomeType($request);
+                break;
+
+            case 'landing':
+                return $this->settingsApiUpdateLandingType($request);
+                break;
+
+            case 'posts':
+                return $this->settingsApiUpdatePostsType($request);
+                break;
+
+            case 'platform':
+                return $this->settingsApiUpdatePlatformType($request);
+                break;
+
+            case 'branding':
+                return $this->settingsApiUpdateBrandingType($request);
+                break;
+
+            case 'media':
+                return $this->settingsApiUpdateMediaType($request);
+                break;
+
+            case 'users':
+                return $this->settingsApiUpdateUsersType($request);
+                break;
+
+            case 'storage':
+                return $this->settingsApiUpdateStorageType($request);
+                break;
+
+            default:
+                abort(404);
+                break;
+        }
+    }
+
+    public function settingsApiUpdateHomeType($request)
+    {
+        $this->validate($request, [
+            'registration_status' => 'required|in:open,filtered,closed',
+            'cloud_storage' => 'required',
+            'activitypub_enabled' => 'required',
+            'account_migration' => 'required',
+            'mobile_apis' => 'required',
+            'stories' => 'required',
+            'instagram_import' => 'required',
+            'autospam_enabled' => 'required',
+        ]);
+
+        $regStatus = $request->input('registration_status');
+        ConfigCacheService::put('pixelfed.open_registration', $regStatus === 'open');
+        ConfigCacheService::put('instance.curated_registration.enabled', $regStatus === 'filtered');
+        $cloudStorage = $request->boolean('cloud_storage');
+        if ($cloudStorage !== (bool) config_cache('pixelfed.cloud_storage')) {
+            if (! $cloudStorage) {
+                ConfigCacheService::put('pixelfed.cloud_storage', false);
+            } else {
+                $cloud_disk = config('filesystems.cloud');
+                $cloud_ready = ! empty(config('filesystems.disks.'.$cloud_disk.'.key')) && ! empty(config('filesystems.disks.'.$cloud_disk.'.secret'));
+                if (! $cloud_ready) {
+                    return redirect()->back()->withErrors(['cloud_storage' => 'Must configure cloud storage before enabling!']);
+                } else {
+                    ConfigCacheService::put('pixelfed.cloud_storage', true);
+                }
+            }
+        }
+        ConfigCacheService::put('federation.activitypub.enabled', $request->boolean('activitypub_enabled'));
+        ConfigCacheService::put('federation.migration', $request->boolean('account_migration'));
+        ConfigCacheService::put('pixelfed.oauth_enabled', $request->boolean('mobile_apis'));
+        ConfigCacheService::put('instance.stories.enabled', $request->boolean('stories'));
+        ConfigCacheService::put('pixelfed.import.instagram.enabled', $request->boolean('instagram_import'));
+        ConfigCacheService::put('pixelfed.bouncer.enabled', $request->boolean('autospam_enabled'));
+
+        Cache::forget('api:v1:instance-data-response-v1');
+        Cache::forget('api:v2:instance-data-response-v2');
+        Cache::forget('api:v1:instance-data:contact');
+        Config::refresh();
+
+        return $request->all();
+    }
+
+    public function settingsApiUpdateLandingType($request)
+    {
+        $this->validate($request, [
+            'current_admin' => 'required',
+            'show_directory' => 'required',
+            'show_explore' => 'required',
+        ]);
+
+        ConfigCacheService::put('instance.admin.pid', $request->input('current_admin'));
+        ConfigCacheService::put('instance.landing.show_directory', $request->boolean('show_directory'));
+        ConfigCacheService::put('instance.landing.show_explore', $request->boolean('show_explore'));
+
+        Cache::forget('api:v1:instance-data:rules');
+        Cache::forget('api:v1:instance-data-response-v1');
+        Cache::forget('api:v2:instance-data-response-v2');
+        Cache::forget('api:v1:instance-data:contact');
+        Config::refresh();
+
+        return $request->all();
+    }
+
+    public function settingsApiUpdateMediaType($request)
+    {
+        $this->validate($request, [
+            'image_quality' => 'required|integer|min:1|max:100',
+            'max_album_length' => 'required|integer|min:1|max:20',
+            'max_photo_size' => 'required|integer|min:100|max:50000',
+            'media_types' => 'required',
+            'optimize_image' => 'required',
+            'optimize_video' => 'required',
+        ]);
+
+        $mediaTypes = $request->input('media_types');
+        $mediaArray = explode(',', $mediaTypes);
+        foreach ($mediaArray as $mediaType) {
+            if (! in_array($mediaType, ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'video/mp4'])) {
+                return redirect()->back()->withErrors(['media_types' => 'Invalid media type']);
+            }
+        }
+
+        ConfigCacheService::put('pixelfed.media_types', $request->input('media_types'));
+        ConfigCacheService::put('pixelfed.image_quality', $request->input('image_quality'));
+        ConfigCacheService::put('pixelfed.max_album_length', $request->input('max_album_length'));
+        ConfigCacheService::put('pixelfed.max_photo_size', $request->input('max_photo_size'));
+        ConfigCacheService::put('pixelfed.optimize_image', $request->boolean('optimize_image'));
+        ConfigCacheService::put('pixelfed.optimize_video', $request->boolean('optimize_video'));
+
+        Cache::forget('api:v1:instance-data:rules');
+        Cache::forget('api:v1:instance-data-response-v1');
+        Cache::forget('api:v2:instance-data-response-v2');
+        Cache::forget('api:v1:instance-data:contact');
+        Config::refresh();
+
+        return $request->all();
+    }
+
+    public function settingsApiUpdateBrandingType($request)
+    {
+        $this->validate($request, [
+            'name' => 'required',
+            'short_description' => 'required',
+            'long_description' => 'required',
+        ]);
+
+        ConfigCacheService::put('app.name', $request->input('name'));
+        ConfigCacheService::put('app.short_description', $request->input('short_description'));
+        ConfigCacheService::put('app.description', $request->input('long_description'));
+
+        Cache::forget('api:v1:instance-data:rules');
+        Cache::forget('api:v1:instance-data-response-v1');
+        Cache::forget('api:v2:instance-data-response-v2');
+        Cache::forget('api:v1:instance-data:contact');
+        Config::refresh();
+
+        return $request->all();
+    }
+
+    public function settingsApiUpdatePostsType($request)
+    {
+        $this->validate($request, [
+            'max_caption_length' => 'required|integer|min:5|max:10000',
+            'max_altext_length' => 'required|integer|min:5|max:40000',
+        ]);
+
+        ConfigCacheService::put('pixelfed.max_caption_length', $request->input('max_caption_length'));
+        ConfigCacheService::put('pixelfed.max_altext_length', $request->input('max_altext_length'));
+        $res = [
+            'max_caption_length' => $request->input('max_caption_length'),
+            'max_altext_length' => $request->input('max_altext_length'),
+        ];
+        Cache::forget('api:v1:instance-data:rules');
+        Cache::forget('api:v1:instance-data-response-v1');
+        Cache::forget('api:v2:instance-data-response-v2');
+        Config::refresh();
+
+        return $res;
+    }
+
+    public function settingsApiUpdatePlatformType($request)
+    {
+        $this->validate($request, [
+            'allow_app_registration' => 'required',
+            'app_registration_rate_limit_attempts' => 'required|integer|min:1',
+            'app_registration_rate_limit_decay' => 'required|integer|min:1',
+            'app_registration_confirm_rate_limit_attempts' => 'required|integer|min:1',
+            'app_registration_confirm_rate_limit_decay' => 'required|integer|min:1',
+            'allow_post_embeds' => 'required',
+            'allow_profile_embeds' => 'required',
+            'captcha_enabled' => 'required',
+            'captcha_on_login' => 'required_if_accepted:captcha_enabled',
+            'captcha_on_register' => 'required_if_accepted:captcha_enabled',
+            'captcha_secret' => 'required_if_accepted:captcha_enabled',
+            'captcha_sitekey' => 'required_if_accepted:captcha_enabled',
+            'custom_emoji_enabled' => 'required',
+        ]);
+
+        ConfigCacheService::put('pixelfed.allow_app_registration', $request->boolean('allow_app_registration'));
+        ConfigCacheService::put('pixelfed.app_registration_rate_limit_attempts', $request->input('app_registration_rate_limit_attempts'));
+        ConfigCacheService::put('pixelfed.app_registration_rate_limit_decay', $request->input('app_registration_rate_limit_decay'));
+        ConfigCacheService::put('pixelfed.app_registration_confirm_rate_limit_attempts', $request->input('app_registration_confirm_rate_limit_attempts'));
+        ConfigCacheService::put('pixelfed.app_registration_confirm_rate_limit_decay', $request->input('app_registration_confirm_rate_limit_decay'));
+        ConfigCacheService::put('instance.embed.post', $request->boolean('allow_post_embeds'));
+        ConfigCacheService::put('instance.embed.profile', $request->boolean('allow_profile_embeds'));
+        ConfigCacheService::put('federation.custom_emoji.enabled', $request->boolean('custom_emoji_enabled'));
+        $captcha = $request->boolean('captcha_enabled');
+        if ($captcha) {
+            $secret = $request->input('captcha_secret');
+            $sitekey = $request->input('captcha_sitekey');
+            if (config_cache('captcha.secret') != $secret && strpos($secret, '*') === false) {
+                ConfigCacheService::put('captcha.secret', $secret);
+            }
+            if (config_cache('captcha.sitekey') != $sitekey && strpos($sitekey, '*') === false) {
+                ConfigCacheService::put('captcha.sitekey', $sitekey);
+            }
+            ConfigCacheService::put('captcha.active.login', $request->boolean('captcha_on_login'));
+            ConfigCacheService::put('captcha.active.register', $request->boolean('captcha_on_register'));
+            ConfigCacheService::put('captcha.triggers.login.enabled', $request->boolean('captcha_on_login'));
+            ConfigCacheService::put('captcha.enabled', true);
+        } else {
+            ConfigCacheService::put('captcha.enabled', false);
+        }
+        $res = [
+            'allow_app_registration' => $request->boolean('allow_app_registration'),
+            'app_registration_rate_limit_attempts' => $request->input('app_registration_rate_limit_attempts'),
+            'app_registration_rate_limit_decay' => $request->input('app_registration_rate_limit_decay'),
+            'app_registration_confirm_rate_limit_attempts' => $request->input('app_registration_confirm_rate_limit_attempts'),
+            'app_registration_confirm_rate_limit_decay' => $request->input('app_registration_confirm_rate_limit_decay'),
+            'allow_post_embeds' => $request->boolean('allow_post_embeds'),
+            'allow_profile_embeds' => $request->boolean('allow_profile_embeds'),
+            'captcha_enabled' => $request->boolean('captcha_enabled'),
+            'captcha_on_login' => $request->boolean('captcha_on_login'),
+            'captcha_on_register' => $request->boolean('captcha_on_register'),
+            'captcha_secret' => $request->input('captcha_secret'),
+            'captcha_sitekey' => $request->input('captcha_sitekey'),
+            'custom_emoji_enabled' => $request->boolean('custom_emoji_enabled'),
+        ];
+        Cache::forget('api:v1:instance-data:rules');
+        Cache::forget('api:v1:instance-data-response-v1');
+        Cache::forget('api:v2:instance-data-response-v2');
+        Config::refresh();
+
+        return $res;
+    }
+
+    public function settingsApiUpdateUsersType($request)
+    {
+        $this->validate($request, [
+            'require_email_verification' => 'required',
+            'enforce_account_limit' => 'required',
+            'max_account_size' => 'required|integer|min:50000',
+            'admin_autofollow' => 'required',
+            'admin_autofollow_accounts' => 'sometimes',
+            'max_user_blocks' => 'required|integer|min:0|max:5000',
+            'max_user_mutes' => 'required|integer|min:0|max:5000',
+            'max_domain_blocks' => 'required|integer|min:0|max:5000',
+        ]);
+
+        $adminAutofollow = $request->boolean('admin_autofollow');
+        $adminAutofollowAccounts = $request->input('admin_autofollow_accounts');
+        if ($adminAutofollow) {
+            if ($request->filled('admin_autofollow_accounts')) {
+                $names = [];
+                $existing = config_cache('account.autofollow_usernames');
+                if ($existing) {
+                    $names = explode(',', $existing);
+                    foreach (array_map('strtolower', $adminAutofollowAccounts) as $afc) {
+                        if (in_array(strtolower($afc), array_map('strtolower', $names))) {
+                            continue;
+                        }
+                        $names[] = $afc;
+                    }
+                } else {
+                    $names = $adminAutofollowAccounts;
+                }
+                if (! $names || count($names) == 0) {
+                    return response()->json(['message' => 'You need to assign autofollow accounts before you can enable it.'], 400);
+                }
+                if (count($names) > 5) {
+                    return response()->json(['message' => 'You can only add up to 5 accounts to be autofollowed.'.json_encode($names)], 400);
+                }
+                $autofollows = User::whereIn('username', $names)->whereNull('status')->pluck('username');
+                $adminAutofollowAccounts = $autofollows->implode(',');
+                ConfigCacheService::put('account.autofollow_usernames', $adminAutofollowAccounts);
+            } else {
+                return response()->json(['message' => 'You need to assign autofollow accounts before you can enable it.'], 400);
+            }
+        }
+
+        ConfigCacheService::put('pixelfed.enforce_email_verification', $request->boolean('require_email_verification'));
+        ConfigCacheService::put('pixelfed.enforce_account_limit', $request->boolean('enforce_account_limit'));
+        ConfigCacheService::put('pixelfed.max_account_size', $request->input('max_account_size'));
+        ConfigCacheService::put('account.autofollow', $request->boolean('admin_autofollow'));
+        ConfigCacheService::put('instance.user_filters.max_user_blocks', (int) $request->input('max_user_blocks'));
+        ConfigCacheService::put('instance.user_filters.max_user_mutes', (int) $request->input('max_user_mutes'));
+        ConfigCacheService::put('instance.user_filters.max_domain_blocks', (int) $request->input('max_domain_blocks'));
+        $res = [
+            'require_email_verification' => $request->boolean('require_email_verification'),
+            'enforce_account_limit' => $request->boolean('enforce_account_limit'),
+            'admin_autofollow' => $request->boolean('admin_autofollow'),
+            'admin_autofollow_accounts' => $adminAutofollowAccounts,
+            'max_user_blocks' => $request->input('max_user_blocks'),
+            'max_user_mutes' => $request->input('max_user_mutes'),
+            'max_domain_blocks' => $request->input('max_domain_blocks'),
+        ];
+        Cache::forget('api:v1:instance-data:rules');
+        Cache::forget('api:v1:instance-data-response-v1');
+        Cache::forget('api:v2:instance-data-response-v2');
+        Config::refresh();
+
+        return $res;
+    }
+
+    public function settingsApiUpdateStorageType($request)
+    {
+        $this->validate($request, [
+            'primary_disk' => 'required|in:local,cloud',
+            'update_disk' => 'sometimes',
+            'disk_config' => 'required_if_accepted:update_disk',
+            'disk_config.driver' => 'required|in:s3,spaces',
+            'disk_config.key' => 'required',
+            'disk_config.secret' => 'required',
+            'disk_config.region' => 'required',
+            'disk_config.bucket' => 'required',
+            'disk_config.visibility' => 'required',
+            'disk_config.endpoint' => 'required',
+            'disk_config.url' => 'nullable',
+        ]);
+
+        ConfigCacheService::put('pixelfed.cloud_storage', $request->input('primary_disk') === 'cloud');
+        $res = [
+            'primary_disk' => $request->input('primary_disk'),
+        ];
+        if ($request->has('update_disk')) {
+            $res['disk_config'] = $request->input('disk_config');
+            $changes = [];
+            $dkey = $request->input('disk_config.driver') === 's3' ? 'filesystems.disks.s3.' : 'filesystems.disks.spaces.';
+            $key = $request->input('disk_config.key');
+            $ckey = null;
+            $secret = $request->input('disk_config.secret');
+            $csecret = null;
+            $region = $request->input('disk_config.region');
+            $bucket = $request->input('disk_config.bucket');
+            $visibility = $request->input('disk_config.visibility');
+            $url = $request->input('disk_config.url');
+            $endpoint = $request->input('disk_config.endpoint');
+            if (strpos($key, '*') === false && $key != config_cache($dkey.'key')) {
+                array_push($changes, 'key');
+            } else {
+                $ckey = config_cache($dkey.'key');
+            }
+            if (strpos($secret, '*') === false && $secret != config_cache($dkey.'secret')) {
+                array_push($changes, 'secret');
+            } else {
+                $csecret = config_cache($dkey.'secret');
+            }
+            if ($region != config_cache($dkey.'region')) {
+                array_push($changes, 'region');
+            }
+            if ($bucket != config_cache($dkey.'bucket')) {
+                array_push($changes, 'bucket');
+            }
+            if ($visibility != config_cache($dkey.'visibility')) {
+                array_push($changes, 'visibility');
+            }
+            if ($url != config_cache($dkey.'url')) {
+                array_push($changes, 'url');
+            }
+            if ($endpoint != config_cache($dkey.'endpoint')) {
+                array_push($changes, 'endpoint');
+            }
+
+            if ($changes && count($changes)) {
+                $isValid = FilesystemService::getVerifyCredentials(
+                    $ckey ?? $key,
+                    $csecret ?? $secret,
+                    $region,
+                    $bucket,
+                    $endpoint,
+                );
+                if (! $isValid) {
+                    return response()->json(['error' => true, 's3_vce' => true, 'message' => "<div class='border border-danger text-danger p-3 font-weight-bold rounded-lg'>The S3/Spaces credentials you provided are invalid, or the bucket does not have the proper permissions.</div><br/>Please check all fields and try again.<br/><br/><strong>Any cloud storage configuration changes you made have NOT been saved due to invalid credentials.</strong>"], 400);
+                }
+            }
+            $res['changes'] = json_encode($changes);
+        }
+        Cache::forget('api:v1:instance-data:rules');
+        Cache::forget('api:v1:instance-data-response-v1');
+        Cache::forget('api:v2:instance-data-response-v2');
+        Config::refresh();
+
+        return $res;
+    }
 }

+ 6 - 6
app/Http/Controllers/AdminController.php

@@ -424,7 +424,7 @@ class AdminController extends Controller
 
 	public function customEmojiHome(Request $request)
 	{
-		if(!config('federation.custom_emoji.enabled')) {
+		if(!(bool) config_cache('federation.custom_emoji.enabled')) {
 			return view('admin.custom-emoji.not-enabled');
 		}
 		$this->validate($request, [
@@ -497,7 +497,7 @@ class AdminController extends Controller
 
 	public function customEmojiToggleActive(Request $request, $id)
 	{
-		abort_unless(config('federation.custom_emoji.enabled'), 404);
+		abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404);
 		$emoji = CustomEmoji::findOrFail($id);
 		$emoji->disabled = !$emoji->disabled;
 		$emoji->save();
@@ -508,13 +508,13 @@ class AdminController extends Controller
 
 	public function customEmojiAdd(Request $request)
 	{
-		abort_unless(config('federation.custom_emoji.enabled'), 404);
+		abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404);
 		return view('admin.custom-emoji.add');
 	}
 
 	public function customEmojiStore(Request $request)
 	{
-		abort_unless(config('federation.custom_emoji.enabled'), 404);
+		abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404);
 		$this->validate($request, [
 			'shortcode' => [
 				'required',
@@ -545,7 +545,7 @@ class AdminController extends Controller
 
 	public function customEmojiDelete(Request $request, $id)
 	{
-		abort_unless(config('federation.custom_emoji.enabled'), 404);
+		abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404);
 		$emoji = CustomEmoji::findOrFail($id);
 		Storage::delete("public/{$emoji->media_path}");
 		Cache::forget('pf:custom_emoji');
@@ -555,7 +555,7 @@ class AdminController extends Controller
 
 	public function customEmojiShowDuplicates(Request $request, $id)
 	{
-		abort_unless(config('federation.custom_emoji.enabled'), 404);
+		abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404);
 		$emoji = CustomEmoji::orderBy('id')->whereDisabled(false)->whereShortcode($id)->firstOrFail();
 		$emojis = CustomEmoji::whereShortcode($id)->where('id', '!=', $emoji->id)->cursorPaginate(10);
 		return view('admin.custom-emoji.duplicates', compact('emoji', 'emojis'));

+ 11 - 11
app/Http/Controllers/Api/ApiV1Controller.php

@@ -131,7 +131,7 @@ class ApiV1Controller extends Controller
      */
     public function apps(Request $request)
     {
-        abort_if(! config_cache('pixelfed.oauth_enabled'), 404);
+        abort_if(! (bool) config_cache('pixelfed.oauth_enabled'), 404);
 
         $this->validate($request, [
             'client_name' => 'required',
@@ -1103,7 +1103,7 @@ class ApiV1Controller extends Controller
         }
 
         $count = UserFilterService::blockCount($pid);
-        $maxLimit = intval(config('instance.user_filters.max_user_blocks'));
+        $maxLimit = (int) config_cache('instance.user_filters.max_user_blocks');
         if ($count == 0) {
             $filterCount = UserFilter::whereUserId($pid)
                 ->whereFilterType('block')
@@ -1632,7 +1632,7 @@ class ApiV1Controller extends Controller
 
             return [
                 'uri' => config('pixelfed.domain.app'),
-                'title' => config('app.name'),
+                'title' => config_cache('app.name'),
                 'short_description' => config_cache('app.short_description'),
                 'description' => config_cache('app.description'),
                 'email' => config('instance.email'),
@@ -1650,11 +1650,11 @@ class ApiV1Controller extends Controller
                 'configuration' => [
                     'media_attachments' => [
                         'image_matrix_limit' => 16777216,
-                        'image_size_limit' => config('pixelfed.max_photo_size') * 1024,
-                        'supported_mime_types' => explode(',', config('pixelfed.media_types')),
+                        'image_size_limit' => config_cache('pixelfed.max_photo_size') * 1024,
+                        'supported_mime_types' => explode(',', config_cache('pixelfed.media_types')),
                         'video_frame_rate_limit' => 120,
                         'video_matrix_limit' => 2304000,
-                        'video_size_limit' => config('pixelfed.max_photo_size') * 1024,
+                        'video_size_limit' => config_cache('pixelfed.max_photo_size') * 1024,
                     ],
                     'polls' => [
                         'max_characters_per_option' => 50,
@@ -1665,7 +1665,7 @@ class ApiV1Controller extends Controller
                     'statuses' => [
                         'characters_reserved_per_url' => 23,
                         'max_characters' => (int) config_cache('pixelfed.max_caption_length'),
-                        'max_media_attachments' => (int) config('pixelfed.max_album_length'),
+                        'max_media_attachments' => (int) config_cache('pixelfed.max_album_length'),
                     ],
                 ],
             ];
@@ -2145,7 +2145,7 @@ class ApiV1Controller extends Controller
         }
 
         $count = UserFilterService::muteCount($pid);
-        $maxLimit = intval(config('instance.user_filters.max_user_mutes'));
+        $maxLimit = (int) config_cache('instance.user_filters.max_user_mutes');
         if ($count == 0) {
             $filterCount = UserFilter::whereUserId($pid)
                 ->whereFilterType('mute')
@@ -3308,9 +3308,9 @@ class ApiV1Controller extends Controller
         abort_unless($request->user()->tokenCan('write'), 403);
 
         $this->validate($request, [
-            'status' => 'nullable|string|max:' . config_cache('pixelfed.max_caption_length'),
+            'status' => 'nullable|string|max:'.(int) config_cache('pixelfed.max_caption_length'),
             'in_reply_to_id' => 'nullable',
-            'media_ids' => 'sometimes|array|max:'.config_cache('pixelfed.max_album_length'),
+            'media_ids' => 'sometimes|array|max:'.(int) config_cache('pixelfed.max_album_length'),
             'sensitive' => 'nullable',
             'visibility' => 'string|in:private,unlisted,public',
             'spoiler_text' => 'sometimes|max:140',
@@ -3436,7 +3436,7 @@ class ApiV1Controller extends Controller
             $mimes = [];
 
             foreach ($ids as $k => $v) {
-                if ($k + 1 > config_cache('pixelfed.max_album_length')) {
+                if ($k + 1 > (int) config_cache('pixelfed.max_album_length')) {
                     continue;
                 }
                 $m = Media::whereUserId($user->id)->whereNull('status_id')->findOrFail($v);

+ 1 - 1
app/Http/Controllers/Api/V1/DomainBlockController.php

@@ -72,7 +72,7 @@ class DomainBlockController extends Controller
         abort_if(config_cache('pixelfed.domain.app') == $domain, 400, 'Cannot ban your own server');
 
         $existingCount = UserDomainBlock::whereProfileId($pid)->count();
-        $maxLimit = config('instance.user_filters.max_domain_blocks');
+        $maxLimit = (int) config_cache('instance.user_filters.max_domain_blocks');
         $errorMsg =  __('profile.block.domain.max', ['max' => $maxLimit]);
 
         abort_if($existingCount >= $maxLimit, 400, $errorMsg);

+ 1 - 1
app/Http/Controllers/Auth/ForgotPasswordController.php

@@ -62,7 +62,7 @@ class ForgotPasswordController extends Controller
 
 		usleep(random_int(100000, 3000000));
 
-    	if(config('captcha.enabled')) {
+    	if((bool) config_cache('captcha.enabled')) {
             $rules = [
 	    		'email' => 'required|email',
             	'h-captcha-response' => 'required|captcha'

+ 3 - 3
app/Http/Controllers/Auth/LoginController.php

@@ -74,10 +74,10 @@ class LoginController extends Controller
         $messages = [];
 
         if(
-        	config('captcha.enabled') ||
-        	config('captcha.active.login') ||
+        	(bool) config_cache('captcha.enabled') &&
+        	(bool) config_cache('captcha.active.login') ||
         	(
-				config('captcha.triggers.login.enabled') &&
+				(bool) config_cache('captcha.triggers.login.enabled') &&
 				request()->session()->has('login_attempts') &&
 				request()->session()->get('login_attempts') >= config('captcha.triggers.login.attempts')
 			)

+ 1 - 1
app/Http/Controllers/Auth/RegisterController.php

@@ -137,7 +137,7 @@ class RegisterController extends Controller
 			'password' => 'required|string|min:'.config('pixelfed.min_password_length').'|confirmed',
 		];
 
-		if(config('captcha.enabled') || config('captcha.active.register')) {
+		if((bool) config_cache('captcha.enabled') && (bool) config_cache('captcha.active.register')) {
 			$rules['h-captcha-response'] = 'required|captcha';
 		}
 

+ 1 - 1
app/Http/Controllers/Auth/ResetPasswordController.php

@@ -50,7 +50,7 @@ class ResetPasswordController extends Controller
     {
     	usleep(random_int(100000, 3000000));
 
-        if(config('captcha.enabled')) {
+        if((bool) config_cache('captcha.enabled')) {
             return [
 	            'token' => 'required',
 	            'email' => 'required|email',

+ 1 - 1
app/Http/Controllers/ComposeController.php

@@ -741,7 +741,7 @@ class ComposeController extends Controller
             case 'image/jpeg':
             case 'image/png':
             case 'video/mp4':
-                $finished = config_cache('pixelfed.cloud_storage') ? (bool) $media->cdn_url : (bool) $media->processed_at;
+                $finished = (bool) config_cache('pixelfed.cloud_storage') ? (bool) $media->cdn_url : (bool) $media->processed_at;
                 break;
 
             default:

+ 87 - 91
app/Http/Controllers/FederationController.php

@@ -2,57 +2,42 @@
 
 namespace App\Http\Controllers;
 
-use App\Jobs\InboxPipeline\{
-    DeleteWorker,
-    InboxWorker,
-    InboxValidator
-};
-use App\Jobs\RemoteFollowPipeline\RemoteFollowPipeline;
-use App\{
-    AccountLog,
-    Like,
-    Profile,
-    Status,
-    User
-};
+use App\Jobs\InboxPipeline\DeleteWorker;
+use App\Jobs\InboxPipeline\InboxValidator;
+use App\Jobs\InboxPipeline\InboxWorker;
+use App\Profile;
+use App\Services\AccountService;
+use App\Services\InstanceService;
+use App\Status;
 use App\Util\Lexer\Nickname;
+use App\Util\Site\Nodeinfo;
 use App\Util\Webfinger\Webfinger;
-use Auth;
 use Cache;
-use Carbon\Carbon;
 use Illuminate\Http\Request;
-use League\Fractal;
-use App\Util\Site\Nodeinfo;
-use App\Util\ActivityPub\{
-    Helpers,
-    HttpSignature,
-    Outbox
-};
-use Zttp\Zttp;
-use App\Services\InstanceService;
-use App\Services\AccountService;
 
 class FederationController extends Controller
 {
     public function nodeinfoWellKnown()
     {
-        abort_if(!config('federation.nodeinfo.enabled'), 404);
+        abort_if(! config('federation.nodeinfo.enabled'), 404);
+
         return response()->json(Nodeinfo::wellKnown(), 200, [], JSON_UNESCAPED_SLASHES)
-            ->header('Access-Control-Allow-Origin','*');
+            ->header('Access-Control-Allow-Origin', '*');
     }
 
     public function nodeinfo()
     {
-        abort_if(!config('federation.nodeinfo.enabled'), 404);
+        abort_if(! config('federation.nodeinfo.enabled'), 404);
+
         return response()->json(Nodeinfo::get(), 200, [], JSON_UNESCAPED_SLASHES)
-            ->header('Access-Control-Allow-Origin','*');
+            ->header('Access-Control-Allow-Origin', '*');
     }
 
     public function webfinger(Request $request)
     {
-        if (!config('federation.webfinger.enabled') ||
-            !$request->has('resource') ||
-            !$request->filled('resource')
+        if (! config('federation.webfinger.enabled') ||
+            ! $request->has('resource') ||
+            ! $request->filled('resource')
         ) {
             return response('', 400);
         }
@@ -60,55 +45,56 @@ class FederationController extends Controller
         $resource = $request->input('resource');
         $domain = config('pixelfed.domain.app');
 
-        if(config('federation.activitypub.sharedInbox') &&
-            $resource == 'acct:' . $domain . '@' . $domain) {
+        if (config('federation.activitypub.sharedInbox') &&
+            $resource == 'acct:'.$domain.'@'.$domain) {
             $res = [
-                'subject' => 'acct:' . $domain . '@' . $domain,
+                'subject' => 'acct:'.$domain.'@'.$domain,
                 'aliases' => [
-                    'https://' . $domain . '/i/actor'
+                    'https://'.$domain.'/i/actor',
                 ],
                 'links' => [
                     [
                         'rel' => 'http://webfinger.net/rel/profile-page',
                         'type' => 'text/html',
-                        'href' => 'https://' . $domain . '/site/kb/instance-actor'
+                        'href' => 'https://'.$domain.'/site/kb/instance-actor',
                     ],
                     [
                         'rel' => 'self',
                         'type' => 'application/activity+json',
-                        'href' => 'https://' . $domain . '/i/actor'
-                    ]
-                ]
+                        'href' => 'https://'.$domain.'/i/actor',
+                    ],
+                ],
             ];
+
             return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES);
         }
         $hash = hash('sha256', $resource);
-        $key = 'federation:webfinger:sha256:' . $hash;
-        if($cached = Cache::get($key)) {
+        $key = 'federation:webfinger:sha256:'.$hash;
+        if ($cached = Cache::get($key)) {
             return response()->json($cached, 200, [], JSON_UNESCAPED_SLASHES);
         }
-        if(strpos($resource, $domain) == false) {
+        if (strpos($resource, $domain) == false) {
             return response('', 400);
         }
         $parsed = Nickname::normalizeProfileUrl($resource);
-        if(empty($parsed) || $parsed['domain'] !== $domain) {
+        if (empty($parsed) || $parsed['domain'] !== $domain) {
             return response('', 400);
         }
         $username = $parsed['username'];
         $profile = Profile::whereNull('domain')->whereUsername($username)->first();
-        if(!$profile || $profile->status !== null) {
+        if (! $profile || $profile->status !== null) {
             return response('', 400);
         }
         $webfinger = (new Webfinger($profile))->generate();
         Cache::put($key, $webfinger, 1209600);
 
         return response()->json($webfinger, 200, [], JSON_UNESCAPED_SLASHES)
-            ->header('Access-Control-Allow-Origin','*');
+            ->header('Access-Control-Allow-Origin', '*');
     }
 
     public function hostMeta(Request $request)
     {
-        abort_if(!config('federation.webfinger.enabled'), 404);
+        abort_if(! config('federation.webfinger.enabled'), 404);
 
         $path = route('well-known.webfinger');
         $xml = '<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="lrdd" type="application/xrd+xml" template="'.$path.'?resource={uri}"/></XRD>';
@@ -118,19 +104,19 @@ class FederationController extends Controller
 
     public function userOutbox(Request $request, $username)
     {
-        abort_if(!config_cache('federation.activitypub.enabled'), 404);
+        abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404);
 
-        if(!$request->wantsJson()) {
-            return redirect('/' . $username);
+        if (! $request->wantsJson()) {
+            return redirect('/'.$username);
         }
 
         $id = AccountService::usernameToId($username);
-        abort_if(!$id, 404);
+        abort_if(! $id, 404);
         $account = AccountService::get($id);
-        abort_if(!$account || !isset($account['statuses_count']), 404);
+        abort_if(! $account || ! isset($account['statuses_count']), 404);
         $res = [
             '@context' => 'https://www.w3.org/ns/activitystreams',
-            'id' => 'https://' . config('pixelfed.domain.app') . '/users/' . $username . '/outbox',
+            'id' => 'https://'.config('pixelfed.domain.app').'/users/'.$username.'/outbox',
             'type' => 'OrderedCollection',
             'totalItems' => $account['statuses_count'] ?? 0,
         ];
@@ -140,135 +126,145 @@ class FederationController extends Controller
 
     public function userInbox(Request $request, $username)
     {
-        abort_if(!config_cache('federation.activitypub.enabled'), 404);
-        abort_if(!config('federation.activitypub.inbox'), 404);
+        abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404);
+        abort_if(! config('federation.activitypub.inbox'), 404);
 
         $headers = $request->headers->all();
         $payload = $request->getContent();
-        if(!$payload || empty($payload)) {
+        if (! $payload || empty($payload)) {
             return;
         }
         $obj = json_decode($payload, true, 8);
-        if(!isset($obj['id'])) {
+        if (! isset($obj['id'])) {
             return;
         }
         $domain = parse_url($obj['id'], PHP_URL_HOST);
-        if(in_array($domain, InstanceService::getBannedDomains())) {
+        if (in_array($domain, InstanceService::getBannedDomains())) {
             return;
         }
 
-        if(isset($obj['type']) && $obj['type'] === 'Delete') {
-            if(isset($obj['object']) && isset($obj['object']['type']) && isset($obj['object']['id'])) {
-                if($obj['object']['type'] === 'Person') {
-                    if(Profile::whereRemoteUrl($obj['object']['id'])->exists()) {
+        if (isset($obj['type']) && $obj['type'] === 'Delete') {
+            if (isset($obj['object']) && isset($obj['object']['type']) && isset($obj['object']['id'])) {
+                if ($obj['object']['type'] === 'Person') {
+                    if (Profile::whereRemoteUrl($obj['object']['id'])->exists()) {
                         dispatch(new DeleteWorker($headers, $payload))->onQueue('inbox');
+
                         return;
                     }
                 }
 
-                if($obj['object']['type'] === 'Tombstone') {
-                    if(Status::whereObjectUrl($obj['object']['id'])->exists()) {
+                if ($obj['object']['type'] === 'Tombstone') {
+                    if (Status::whereObjectUrl($obj['object']['id'])->exists()) {
                         dispatch(new DeleteWorker($headers, $payload))->onQueue('delete');
+
                         return;
                     }
                 }
 
-                if($obj['object']['type'] === 'Story') {
+                if ($obj['object']['type'] === 'Story') {
                     dispatch(new DeleteWorker($headers, $payload))->onQueue('story');
+
                     return;
                 }
             }
+
             return;
-        } else if( isset($obj['type']) && in_array($obj['type'], ['Follow', 'Accept'])) {
+        } elseif (isset($obj['type']) && in_array($obj['type'], ['Follow', 'Accept'])) {
             dispatch(new InboxValidator($username, $headers, $payload))->onQueue('follow');
         } else {
             dispatch(new InboxValidator($username, $headers, $payload))->onQueue('high');
         }
-        return;
+
     }
 
     public function sharedInbox(Request $request)
     {
-        abort_if(!config_cache('federation.activitypub.enabled'), 404);
-        abort_if(!config('federation.activitypub.sharedInbox'), 404);
+        abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404);
+        abort_if(! config('federation.activitypub.sharedInbox'), 404);
 
         $headers = $request->headers->all();
         $payload = $request->getContent();
 
-        if(!$payload || empty($payload)) {
+        if (! $payload || empty($payload)) {
             return;
         }
 
         $obj = json_decode($payload, true, 8);
-        if(!isset($obj['id'])) {
+        if (! isset($obj['id'])) {
             return;
         }
 
         $domain = parse_url($obj['id'], PHP_URL_HOST);
-        if(in_array($domain, InstanceService::getBannedDomains())) {
+        if (in_array($domain, InstanceService::getBannedDomains())) {
             return;
         }
 
-        if(isset($obj['type']) && $obj['type'] === 'Delete') {
-            if(isset($obj['object']) && isset($obj['object']['type']) && isset($obj['object']['id'])) {
-                if($obj['object']['type'] === 'Person') {
-                    if(Profile::whereRemoteUrl($obj['object']['id'])->exists()) {
+        if (isset($obj['type']) && $obj['type'] === 'Delete') {
+            if (isset($obj['object']) && isset($obj['object']['type']) && isset($obj['object']['id'])) {
+                if ($obj['object']['type'] === 'Person') {
+                    if (Profile::whereRemoteUrl($obj['object']['id'])->exists()) {
                         dispatch(new DeleteWorker($headers, $payload))->onQueue('inbox');
+
                         return;
                     }
                 }
 
-                if($obj['object']['type'] === 'Tombstone') {
-                    if(Status::whereObjectUrl($obj['object']['id'])->exists()) {
+                if ($obj['object']['type'] === 'Tombstone') {
+                    if (Status::whereObjectUrl($obj['object']['id'])->exists()) {
                         dispatch(new DeleteWorker($headers, $payload))->onQueue('delete');
+
                         return;
                     }
                 }
 
-                if($obj['object']['type'] === 'Story') {
+                if ($obj['object']['type'] === 'Story') {
                     dispatch(new DeleteWorker($headers, $payload))->onQueue('story');
+
                     return;
                 }
             }
+
             return;
-        } else if( isset($obj['type']) && in_array($obj['type'], ['Follow', 'Accept'])) {
+        } elseif (isset($obj['type']) && in_array($obj['type'], ['Follow', 'Accept'])) {
             dispatch(new InboxWorker($headers, $payload))->onQueue('follow');
         } else {
             dispatch(new InboxWorker($headers, $payload))->onQueue('shared');
         }
-        return;
+
     }
 
     public function userFollowing(Request $request, $username)
     {
-        abort_if(!config_cache('federation.activitypub.enabled'), 404);
+        abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404);
 
         $id = AccountService::usernameToId($username);
-        abort_if(!$id, 404);
+        abort_if(! $id, 404);
         $account = AccountService::get($id);
-        abort_if(!$account || !isset($account['following_count']), 404);
+        abort_if(! $account || ! isset($account['following_count']), 404);
         $obj = [
             '@context' => 'https://www.w3.org/ns/activitystreams',
-            'id'       => $request->getUri(),
-            'type'     => 'OrderedCollection',
+            'id' => $request->getUri(),
+            'type' => 'OrderedCollection',
             'totalItems' => $account['following_count'] ?? 0,
         ];
+
         return response()->json($obj)->header('Content-Type', 'application/activity+json');
     }
 
     public function userFollowers(Request $request, $username)
     {
-        abort_if(!config_cache('federation.activitypub.enabled'), 404);
+        abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404);
         $id = AccountService::usernameToId($username);
-        abort_if(!$id, 404);
+        abort_if(! $id, 404);
         $account = AccountService::get($id);
-        abort_if(!$account || !isset($account['followers_count']), 404);
+        abort_if(! $account || ! isset($account['followers_count']), 404);
         $obj = [
             '@context' => 'https://www.w3.org/ns/activitystreams',
-            'id'       => $request->getUri(),
-            'type'     => 'OrderedCollection',
+            'id' => $request->getUri(),
+            'type' => 'OrderedCollection',
             'totalItems' => $account['followers_count'] ?? 0,
         ];
+
         return response()->json($obj)->header('Content-Type', 'application/activity+json');
     }
 }

+ 25 - 1
app/Http/Controllers/Import/Instagram.php

@@ -17,7 +17,7 @@ trait Instagram
 {
 	public function instagram()
 	{
-		if(config_cache('pixelfed.import.instagram.enabled') != true) {
+		if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
 			abort(404, 'Feature not enabled');
 		}
 		return view('settings.import.instagram.home');
@@ -25,6 +25,9 @@ trait Instagram
 
     public function instagramStart(Request $request)
     {	
+        if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
+            abort(404, 'Feature not enabled');
+        }
         $completed = ImportJob::whereProfileId(Auth::user()->profile->id)
             ->whereService('instagram')
             ->whereNotNull('completed_at')
@@ -38,6 +41,9 @@ trait Instagram
 
     protected function instagramRedirectOrNew()
     {
+        if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
+            abort(404, 'Feature not enabled');
+        }
     	$profile = Auth::user()->profile;
     	$exists = ImportJob::whereProfileId($profile->id)
     		->whereService('instagram')
@@ -61,6 +67,9 @@ trait Instagram
 
     public function instagramStepOne(Request $request, $uuid)
     {
+        if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
+            abort(404, 'Feature not enabled');
+        }
     	$profile = Auth::user()->profile;
     	$job = ImportJob::whereProfileId($profile->id)
     		->whereNull('completed_at')
@@ -72,6 +81,9 @@ trait Instagram
 
     public function instagramStepOneStore(Request $request, $uuid)
     {
+        if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
+            abort(404, 'Feature not enabled');
+        }
         $max = 'max:' . config('pixelfed.import.instagram.limits.size');
     	$this->validate($request, [
     		'media.*' => 'required|mimes:bin,jpeg,png,gif|'.$max,
@@ -114,6 +126,9 @@ trait Instagram
 
     public function instagramStepTwo(Request $request, $uuid)
     {
+        if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
+            abort(404, 'Feature not enabled');
+        }
     	$profile = Auth::user()->profile;
     	$job = ImportJob::whereProfileId($profile->id)
     		->whereNull('completed_at')
@@ -125,6 +140,9 @@ trait Instagram
 
     public function instagramStepTwoStore(Request $request, $uuid)
     {
+        if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
+            abort(404, 'Feature not enabled');
+        }
     	$this->validate($request, [
     		'media' => 'required|file|max:1000'
     	]);
@@ -150,6 +168,9 @@ trait Instagram
 
     public function instagramStepThree(Request $request, $uuid)
     {
+        if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
+            abort(404, 'Feature not enabled');
+        }
     	$profile = Auth::user()->profile;
     	$job = ImportJob::whereProfileId($profile->id)
             ->whereService('instagram')
@@ -162,6 +183,9 @@ trait Instagram
 
     public function instagramStepThreeStore(Request $request, $uuid)
     {
+        if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
+            abort(404, 'Feature not enabled');
+        }
         $profile = Auth::user()->profile;
 
         try {

+ 1 - 1
app/Http/Controllers/ImportPostController.php

@@ -179,7 +179,7 @@ class ImportPostController extends Controller
                 'required',
                 'file',
                 $mimes,
-                'max:' . config('pixelfed.max_photo_size')
+                'max:' . config_cache('pixelfed.max_photo_size')
             ]
         ]);
 

+ 20 - 21
app/Http/Controllers/LandingController.php

@@ -2,44 +2,43 @@
 
 namespace App\Http\Controllers;
 
-use Illuminate\Http\Request;
-use App\Profile;
-use App\Services\AccountService;
 use App\Http\Resources\DirectoryProfile;
+use App\Profile;
+use Illuminate\Http\Request;
 
 class LandingController extends Controller
 {
     public function directoryRedirect(Request $request)
     {
-    	if($request->user()) {
-    		return redirect('/');
-    	}
+        if ($request->user()) {
+            return redirect('/');
+        }
 
-    	abort_if(config_cache('instance.landing.show_directory') == false, 404);
+        abort_if((bool) config_cache('instance.landing.show_directory') == false, 404);
 
-    	return view('site.index');
+        return view('site.index');
     }
 
     public function exploreRedirect(Request $request)
     {
-    	if($request->user()) {
-    		return redirect('/');
-    	}
+        if ($request->user()) {
+            return redirect('/');
+        }
 
-    	abort_if(config_cache('instance.landing.show_explore') == false, 404);
+        abort_if((bool) config_cache('instance.landing.show_explore') == false, 404);
 
-    	return view('site.index');
+        return view('site.index');
     }
 
     public function getDirectoryApi(Request $request)
     {
-    	abort_if(config_cache('instance.landing.show_directory') == false, 404);
-
-    	return DirectoryProfile::collection(
-    		Profile::whereNull('domain')
-    		->whereIsSuggestable(true)
-    		->orderByDesc('updated_at')
-    		->cursorPaginate(20)
-    	);
+        abort_if((bool) config_cache('instance.landing.show_directory') == false, 404);
+
+        return DirectoryProfile::collection(
+            Profile::whereNull('domain')
+                ->whereIsSuggestable(true)
+                ->orderByDesc('updated_at')
+                ->cursorPaginate(20)
+        );
     }
 }

+ 19 - 18
app/Http/Controllers/MediaController.php

@@ -2,30 +2,31 @@
 
 namespace App\Http\Controllers;
 
-use Illuminate\Http\Request;
 use App\Media;
+use Illuminate\Http\Request;
 
 class MediaController extends Controller
 {
-	public function index(Request $request)
-	{
-		//return view('settings.drive.index');
-	}
+    public function index(Request $request)
+    {
+        //return view('settings.drive.index');
+        abort(404);
+    }
 
-	public function composeUpdate(Request $request, $id)
-	{
+    public function composeUpdate(Request $request, $id)
+    {
         abort(400, 'Endpoint deprecated');
-	}	
+    }
 
-	public function fallbackRedirect(Request $request, $pid, $mhash, $uhash, $f)
-	{
-		abort_if(!config_cache('pixelfed.cloud_storage'), 404);
-		$path = 'public/m/_v2/' . $pid . '/' . $mhash . '/' . $uhash . '/' . $f;
-		$media = Media::whereProfileId($pid)
-			->whereMediaPath($path)
-			->whereNotNull('cdn_url')
-			->firstOrFail();
+    public function fallbackRedirect(Request $request, $pid, $mhash, $uhash, $f)
+    {
+        abort_if(! (bool) config_cache('pixelfed.cloud_storage'), 404);
+        $path = 'public/m/_v2/'.$pid.'/'.$mhash.'/'.$uhash.'/'.$f;
+        $media = Media::whereProfileId($pid)
+            ->whereMediaPath($path)
+            ->whereNotNull('cdn_url')
+            ->firstOrFail();
 
-		return redirect()->away($media->cdn_url);
-	}
+        return redirect()->away($media->cdn_url);
+    }
 }

+ 52 - 42
app/Http/Controllers/PixelfedDirectoryController.php

@@ -2,37 +2,41 @@
 
 namespace App\Http\Controllers;
 
-use Illuminate\Http\Request;
 use App\Models\ConfigCache;
-use Storage;
 use App\Services\AccountService;
 use App\Services\StatusService;
+use Illuminate\Http\Request;
 use Illuminate\Support\Str;
+use Cache;
+use Storage;
+use App\Status;
+use App\User;
 
 class PixelfedDirectoryController extends Controller
 {
     public function get(Request $request)
     {
-        if(!$request->filled('sk')) {
+        if (! $request->filled('sk')) {
             abort(404);
         }
 
-        if(!config_cache('pixelfed.directory.submission-key')) {
+        if (! config_cache('pixelfed.directory.submission-key')) {
             abort(404);
         }
 
-        if(!hash_equals(config_cache('pixelfed.directory.submission-key'), $request->input('sk'))) {
+        if (! hash_equals(config_cache('pixelfed.directory.submission-key'), $request->input('sk'))) {
             abort(403);
         }
 
         $res = $this->buildListing();
-        return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
+
+        return response()->json($res, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
     }
 
     public function buildListing()
     {
         $res = config_cache('pixelfed.directory');
-        if($res) {
+        if ($res) {
             $res = is_string($res) ? json_decode($res, true) : $res;
         }
 
@@ -41,40 +45,40 @@ class PixelfedDirectoryController extends Controller
         $res['_ts'] = config_cache('pixelfed.directory.submission-ts');
         $res['version'] = config_cache('pixelfed.version');
 
-        if(empty($res['summary'])) {
+        if (empty($res['summary'])) {
             $summary = ConfigCache::whereK('app.short_description')->pluck('v');
             $res['summary'] = $summary ? $summary[0] : null;
         }
 
-        if(isset($res['admin'])) {
+        if (isset($res['admin'])) {
             $res['admin'] = AccountService::get($res['admin'], true);
         }
 
-        if(isset($res['banner_image']) && !empty($res['banner_image'])) {
+        if (isset($res['banner_image']) && ! empty($res['banner_image'])) {
             $res['banner_image'] = url(Storage::url($res['banner_image']));
         }
 
-        if(isset($res['favourite_posts'])) {
-            $res['favourite_posts'] = collect($res['favourite_posts'])->map(function($id) {
+        if (isset($res['favourite_posts'])) {
+            $res['favourite_posts'] = collect($res['favourite_posts'])->map(function ($id) {
                 return StatusService::get($id);
             })
-            ->filter(function($post) {
-                return $post && isset($post['account']);
-            })
-            ->map(function($post) {
-                return [
-                    'avatar' => $post['account']['avatar'],
-                    'display_name' => $post['account']['display_name'],
-                    'username' => $post['account']['username'],
-                    'media' => $post['media_attachments'][0]['url'],
-                    'url' => $post['url']
-                ];
-            })
-            ->values();
+                ->filter(function ($post) {
+                    return $post && isset($post['account']);
+                })
+                ->map(function ($post) {
+                    return [
+                        'avatar' => $post['account']['avatar'],
+                        'display_name' => $post['account']['display_name'],
+                        'username' => $post['account']['username'],
+                        'media' => $post['media_attachments'][0]['url'],
+                        'url' => $post['url'],
+                    ];
+                })
+                ->values();
         }
 
         $guidelines = ConfigCache::whereK('app.rules')->first();
-        if($guidelines) {
+        if ($guidelines) {
             $res['community_guidelines'] = json_decode($guidelines->v, true);
         }
 
@@ -85,27 +89,27 @@ class PixelfedDirectoryController extends Controller
         $res['curated_onboarding'] = $curatedOnboarding;
 
         $oauthEnabled = ConfigCache::whereK('pixelfed.oauth_enabled')->first();
-        if($oauthEnabled) {
+        if ($oauthEnabled) {
             $keys = file_exists(storage_path('oauth-public.key')) && file_exists(storage_path('oauth-private.key'));
             $res['oauth_enabled'] = (bool) $oauthEnabled && $keys;
         }
 
         $activityPubEnabled = ConfigCache::whereK('federation.activitypub.enabled')->first();
-        if($activityPubEnabled) {
+        if ($activityPubEnabled) {
             $res['activitypub_enabled'] = (bool) $activityPubEnabled;
         }
 
         $res['feature_config'] = [
             'media_types' => Str::of(config_cache('pixelfed.media_types'))->explode(','),
             'image_quality' => config_cache('pixelfed.image_quality'),
-            'optimize_image' => config_cache('pixelfed.optimize_image'),
+            'optimize_image' => (bool) config_cache('pixelfed.optimize_image'),
             'max_photo_size' => config_cache('pixelfed.max_photo_size'),
             'max_caption_length' => config_cache('pixelfed.max_caption_length'),
             'max_altext_length' => config_cache('pixelfed.max_altext_length'),
-            'enforce_account_limit' => config_cache('pixelfed.enforce_account_limit'),
+            'enforce_account_limit' => (bool) config_cache('pixelfed.enforce_account_limit'),
             'max_account_size' => config_cache('pixelfed.max_account_size'),
             'max_album_length' => config_cache('pixelfed.max_album_length'),
-            'account_deletion' => config_cache('pixelfed.account_deletion'),
+            'account_deletion' => (bool) config_cache('pixelfed.account_deletion'),
         ];
 
         $res['is_eligible'] = $this->validVal($res, 'admin') &&
@@ -115,29 +119,36 @@ class PixelfedDirectoryController extends Controller
             $this->validVal($res, 'privacy_pledge') &&
             $this->validVal($res, 'location');
 
-        if(config_cache('pixelfed.directory.testimonials')) {
+        if (config_cache('pixelfed.directory.testimonials')) {
             $res['testimonials'] = collect(json_decode(config_cache('pixelfed.directory.testimonials'), true))
-                ->map(function($testimonial) {
+                ->map(function ($testimonial) {
                     $profile = AccountService::get($testimonial['profile_id']);
+
                     return [
                         'profile' => [
                             'username' => $profile['username'],
                             'display_name' => $profile['display_name'],
                             'avatar' => $profile['avatar'],
-                            'created_at' => $profile['created_at']
+                            'created_at' => $profile['created_at'],
                         ],
-                        'body' => $testimonial['body']
+                        'body' => $testimonial['body'],
                     ];
                 });
         }
 
         $res['features_enabled'] = [
-            'stories' => (bool) config_cache('instance.stories.enabled')
+            'stories' => (bool) config_cache('instance.stories.enabled'),
         ];
 
+        $statusesCount = Cache::remember('api:nodeinfo:statuses', 21600, function() {
+            return Status::whereLocal(true)->count();
+        });
+        $usersCount = Cache::remember('api:nodeinfo:users', 43200, function() {
+            return User::count();
+        });
         $res['stats'] = [
-            'user_count' => \App\User::count(),
-            'post_count' => \App\Status::whereNull('uri')->count(),
+            'user_count' => (int) $usersCount,
+            'post_count' => (int) $statusesCount,
         ];
 
         $res['primary_locale'] = config('app.locale');
@@ -150,19 +161,18 @@ class PixelfedDirectoryController extends Controller
 
     protected function validVal($res, $val, $count = false, $minLen = false)
     {
-        if(!isset($res[$val])) {
+        if (! isset($res[$val])) {
             return false;
         }
 
-        if($count) {
+        if ($count) {
             return count($res[$val]) >= $count;
         }
 
-        if($minLen) {
+        if ($minLen) {
             return strlen($res[$val]) >= $minLen;
         }
 
         return $res[$val];
     }
-
 }

+ 3 - 1
app/Http/Controllers/ProfileController.php

@@ -172,6 +172,8 @@ class ProfileController extends Controller
 
         $user = $this->getCachedUser($username);
 
+        abort_if(!$user, 404);
+
         return redirect($user->url());
     }
 
@@ -371,7 +373,7 @@ class ProfileController extends Controller
 
     public function stories(Request $request, $username)
     {
-        abort_if(! config_cache('instance.stories.enabled') || ! $request->user(), 404);
+        abort_if(!(bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
         $profile = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
         $pid = $profile->id;
         $authed = Auth::user()->profile_id;

+ 90 - 80
app/Http/Controllers/RemoteAuthController.php

@@ -2,22 +2,20 @@
 
 namespace App\Http\Controllers;
 
-use Illuminate\Support\Str;
-use Illuminate\Http\Request;
-use App\Services\Account\RemoteAuthService;
 use App\Models\RemoteAuth;
-use App\Profile;
-use App\Instance;
-use App\User;
-use Purify;
-use Illuminate\Support\Facades\Auth;
-use Illuminate\Support\Facades\Hash;
-use Illuminate\Auth\Events\Registered;
-use App\Util\Lexer\RestrictedNames;
+use App\Services\Account\RemoteAuthService;
 use App\Services\EmailService;
 use App\Services\MediaStorageService;
+use App\User;
 use App\Util\ActivityPub\Helpers;
+use App\Util\Lexer\RestrictedNames;
+use Illuminate\Auth\Events\Registered;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Hash;
+use Illuminate\Support\Str;
 use InvalidArgumentException;
+use Purify;
 
 class RemoteAuthController extends Controller
 {
@@ -30,9 +28,10 @@ class RemoteAuthController extends Controller
             config('remote-auth.mastodon.ignore_closed_state') &&
             config('remote-auth.mastodon.enabled')
         ), 404);
-        if($request->user()) {
+        if ($request->user()) {
             return redirect('/');
         }
+
         return view('auth.remote.start');
     }
 
@@ -51,25 +50,27 @@ class RemoteAuthController extends Controller
             config('remote-auth.mastodon.enabled')
         ), 404);
 
-        if(config('remote-auth.mastodon.domains.only_custom')) {
+        if (config('remote-auth.mastodon.domains.only_custom')) {
             $res = config('remote-auth.mastodon.domains.custom');
-            if(!$res || !strlen($res)) {
+            if (! $res || ! strlen($res)) {
                 return [];
             }
             $res = explode(',', $res);
+
             return response()->json($res);
         }
 
-        if( config('remote-auth.mastodon.domains.custom') &&
-            !config('remote-auth.mastodon.domains.only_default') &&
+        if (config('remote-auth.mastodon.domains.custom') &&
+            ! config('remote-auth.mastodon.domains.only_default') &&
             strlen(config('remote-auth.mastodon.domains.custom')) > 3 &&
             strpos(config('remote-auth.mastodon.domains.custom'), '.') > -1
         ) {
             $res = config('remote-auth.mastodon.domains.custom');
-            if(!$res || !strlen($res)) {
+            if (! $res || ! strlen($res)) {
                 return [];
             }
             $res = explode(',', $res);
+
             return response()->json($res);
         }
 
@@ -93,57 +94,62 @@ class RemoteAuthController extends Controller
 
         $domain = $request->input('domain');
 
-        if(str_starts_with(strtolower($domain), 'http')) {
+        if (str_starts_with(strtolower($domain), 'http')) {
             $res = [
                 'domain' => $domain,
                 'ready' => false,
-                'action' => 'incompatible_domain'
+                'action' => 'incompatible_domain',
             ];
+
             return response()->json($res);
         }
 
-        $validateInstance = Helpers::validateUrl('https://' . $domain . '/?block-check=' . time());
+        $validateInstance = Helpers::validateUrl('https://'.$domain.'/?block-check='.time());
 
-        if(!$validateInstance) {
-             $res = [
+        if (! $validateInstance) {
+            $res = [
                 'domain' => $domain,
                 'ready' => false,
-                'action' => 'blocked_domain'
+                'action' => 'blocked_domain',
             ];
+
             return response()->json($res);
         }
 
         $compatible = RemoteAuthService::isDomainCompatible($domain);
 
-        if(!$compatible) {
+        if (! $compatible) {
             $res = [
                 'domain' => $domain,
                 'ready' => false,
-                'action' => 'incompatible_domain'
+                'action' => 'incompatible_domain',
             ];
+
             return response()->json($res);
         }
 
-        if(config('remote-auth.mastodon.domains.only_default')) {
+        if (config('remote-auth.mastodon.domains.only_default')) {
             $defaultDomains = explode(',', config('remote-auth.mastodon.domains.default'));
-            if(!in_array($domain, $defaultDomains)) {
+            if (! in_array($domain, $defaultDomains)) {
                 $res = [
                     'domain' => $domain,
                     'ready' => false,
-                    'action' => 'incompatible_domain'
+                    'action' => 'incompatible_domain',
                 ];
+
                 return response()->json($res);
             }
         }
 
-        if(config('remote-auth.mastodon.domains.only_custom') && config('remote-auth.mastodon.domains.custom')) {
+        if (config('remote-auth.mastodon.domains.only_custom') && config('remote-auth.mastodon.domains.custom')) {
             $customDomains = explode(',', config('remote-auth.mastodon.domains.custom'));
-            if(!in_array($domain, $customDomains)) {
+            if (! in_array($domain, $customDomains)) {
                 $res = [
                     'domain' => $domain,
                     'ready' => false,
-                    'action' => 'incompatible_domain'
+                    'action' => 'incompatible_domain',
                 ];
+
                 return response()->json($res);
             }
         }
@@ -163,13 +169,13 @@ class RemoteAuthController extends Controller
             'state' => $state,
         ]);
 
-        $request->session()->put('oauth_redirect_to', 'https://' . $domain . '/oauth/authorize?' . $query);
+        $request->session()->put('oauth_redirect_to', 'https://'.$domain.'/oauth/authorize?'.$query);
 
         $dsh = Str::random(17);
         $res = [
             'domain' => $domain,
             'ready' => true,
-            'dsh' => $dsh
+            'dsh' => $dsh,
         ];
 
         return response()->json($res);
@@ -185,7 +191,7 @@ class RemoteAuthController extends Controller
             config('remote-auth.mastodon.enabled')
         ), 404);
 
-        if(!$request->filled('d') || !$request->filled('dsh') || !$request->session()->exists('oauth_redirect_to')) {
+        if (! $request->filled('d') || ! $request->filled('dsh') || ! $request->session()->exists('oauth_redirect_to')) {
             return redirect('/login');
         }
 
@@ -204,7 +210,7 @@ class RemoteAuthController extends Controller
 
         $domain = $request->session()->get('oauth_domain');
 
-        if($request->filled('code')) {
+        if ($request->filled('code')) {
             $code = $request->input('code');
             $state = $request->session()->pull('state');
 
@@ -216,12 +222,14 @@ class RemoteAuthController extends Controller
 
             $res = RemoteAuthService::getToken($domain, $code);
 
-            if(!$res || !isset($res['access_token'])) {
+            if (! $res || ! isset($res['access_token'])) {
                 $request->session()->regenerate();
+
                 return redirect('/login');
             }
 
             $request->session()->put('oauth_remote_session_token', $res['access_token']);
+
             return redirect('/auth/mastodon/getting-started');
         }
 
@@ -237,9 +245,10 @@ class RemoteAuthController extends Controller
             config('remote-auth.mastodon.ignore_closed_state') &&
             config('remote-auth.mastodon.enabled')
         ), 404);
-        if($request->user()) {
+        if ($request->user()) {
             return redirect('/');
         }
+
         return view('auth.remote.onboarding');
     }
 
@@ -261,36 +270,36 @@ class RemoteAuthController extends Controller
 
         $res = RemoteAuthService::getVerifyCredentials($domain, $token);
 
-        abort_if(!$res || !isset($res['acct']), 403, 'Invalid credentials');
+        abort_if(! $res || ! isset($res['acct']), 403, 'Invalid credentials');
 
-        $webfinger = strtolower('@' . $res['acct'] . '@' . $domain);
+        $webfinger = strtolower('@'.$res['acct'].'@'.$domain);
         $request->session()->put('oauth_masto_webfinger', $webfinger);
 
-        if(config('remote-auth.mastodon.max_uses.enabled')) {
+        if (config('remote-auth.mastodon.max_uses.enabled')) {
             $limit = config('remote-auth.mastodon.max_uses.limit');
             $uses = RemoteAuthService::lookupWebfingerUses($webfinger);
-            if($uses >= $limit) {
+            if ($uses >= $limit) {
                 return response()->json([
                     'code' => 200,
                     'msg' => 'Success!',
-                    'action' => 'max_uses_reached'
+                    'action' => 'max_uses_reached',
                 ]);
             }
         }
 
         $exists = RemoteAuth::whereDomain($domain)->where('webfinger', $webfinger)->whereNotNull('user_id')->first();
-        if($exists && $exists->user_id) {
+        if ($exists && $exists->user_id) {
             return response()->json([
                 'code' => 200,
                 'msg' => 'Success!',
-                'action' => 'redirect_existing_user'
+                'action' => 'redirect_existing_user',
             ]);
         }
 
         return response()->json([
             'code' => 200,
             'msg' => 'Success!',
-            'action' => 'onboard'
+            'action' => 'onboard',
         ]);
     }
 
@@ -311,7 +320,7 @@ class RemoteAuthController extends Controller
         $token = $request->session()->get('oauth_remote_session_token');
 
         $res = RemoteAuthService::getVerifyCredentials($domain, $token);
-        $res['_webfinger'] = strtolower('@' . $res['acct'] . '@' . $domain);
+        $res['_webfinger'] = strtolower('@'.$res['acct'].'@'.$domain);
         $res['_domain'] = strtolower($domain);
         $request->session()->put('oauth_remasto_id', $res['id']);
 
@@ -324,7 +333,7 @@ class RemoteAuthController extends Controller
             'bearer_token' => $token,
             'verify_credentials' => $res,
             'last_verify_credentials_at' => now(),
-            'last_successful_login_at' => now()
+            'last_successful_login_at' => now(),
         ]);
 
         $request->session()->put('oauth_masto_raid', $ra->id);
@@ -355,24 +364,24 @@ class RemoteAuthController extends Controller
                     $underscore = substr_count($value, '_');
                     $period = substr_count($value, '.');
 
-                    if(ends_with($value, ['.php', '.js', '.css'])) {
+                    if (ends_with($value, ['.php', '.js', '.css'])) {
                         return $fail('Username is invalid.');
                     }
 
-                    if(($dash + $underscore + $period) > 1) {
+                    if (($dash + $underscore + $period) > 1) {
                         return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).');
                     }
 
-                    if (!ctype_alnum($value[0])) {
+                    if (! ctype_alnum($value[0])) {
                         return $fail('Username is invalid. Must start with a letter or number.');
                     }
 
-                    if (!ctype_alnum($value[strlen($value) - 1])) {
+                    if (! ctype_alnum($value[strlen($value) - 1])) {
                         return $fail('Username is invalid. Must end with a letter or number.');
                     }
 
                     $val = str_replace(['_', '.', '-'], '', $value);
-                    if(!ctype_alnum($val)) {
+                    if (! ctype_alnum($val)) {
                         return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).');
                     }
 
@@ -380,8 +389,8 @@ class RemoteAuthController extends Controller
                     if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
                         return $fail('Username cannot be used.');
                     }
-                }
-            ]
+                },
+            ],
         ]);
         $username = strtolower($request->input('username'));
 
@@ -390,7 +399,7 @@ class RemoteAuthController extends Controller
         return response()->json([
             'code' => 200,
             'username' => $username,
-            'exists' => $exists
+            'exists' => $exists,
         ]);
     }
 
@@ -411,7 +420,7 @@ class RemoteAuthController extends Controller
             'email' => [
                 'required',
                 'email:strict,filter_unicode,dns,spoof',
-            ]
+            ],
         ]);
 
         $email = $request->input('email');
@@ -422,7 +431,7 @@ class RemoteAuthController extends Controller
             'code' => 200,
             'email' => $email,
             'exists' => $exists,
-            'banned' => $banned
+            'banned' => $banned,
         ]);
     }
 
@@ -445,18 +454,18 @@ class RemoteAuthController extends Controller
 
         $res = RemoteAuthService::getFollowing($domain, $token, $id);
 
-        if(!$res) {
+        if (! $res) {
             return response()->json([
                 'code' => 200,
-                'following' => []
+                'following' => [],
             ]);
         }
 
-        $res = collect($res)->filter(fn($acct) => Helpers::validateUrl($acct['url']))->values()->toArray();
+        $res = collect($res)->filter(fn ($acct) => Helpers::validateUrl($acct['url']))->values()->toArray();
 
         return response()->json([
             'code' => 200,
-            'following' => $res
+            'following' => $res,
         ]);
     }
 
@@ -487,24 +496,24 @@ class RemoteAuthController extends Controller
                     $underscore = substr_count($value, '_');
                     $period = substr_count($value, '.');
 
-                    if(ends_with($value, ['.php', '.js', '.css'])) {
+                    if (ends_with($value, ['.php', '.js', '.css'])) {
                         return $fail('Username is invalid.');
                     }
 
-                    if(($dash + $underscore + $period) > 1) {
+                    if (($dash + $underscore + $period) > 1) {
                         return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).');
                     }
 
-                    if (!ctype_alnum($value[0])) {
+                    if (! ctype_alnum($value[0])) {
                         return $fail('Username is invalid. Must start with a letter or number.');
                     }
 
-                    if (!ctype_alnum($value[strlen($value) - 1])) {
+                    if (! ctype_alnum($value[strlen($value) - 1])) {
                         return $fail('Username is invalid. Must end with a letter or number.');
                     }
 
                     $val = str_replace(['_', '.', '-'], '', $value);
-                    if(!ctype_alnum($val)) {
+                    if (! ctype_alnum($val)) {
                         return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).');
                     }
 
@@ -512,10 +521,10 @@ class RemoteAuthController extends Controller
                     if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
                         return $fail('Username cannot be used.');
                     }
-                }
+                },
             ],
             'password' => 'required|string|min:8|confirmed',
-            'name' => 'nullable|max:30'
+            'name' => 'nullable|max:30',
         ]);
 
         $email = $request->input('email');
@@ -527,7 +536,7 @@ class RemoteAuthController extends Controller
             'name' => $name,
             'username' => $username,
             'password' => $password,
-            'email' => $email
+            'email' => $email,
         ]);
 
         $raid = $request->session()->pull('oauth_masto_raid');
@@ -541,7 +550,7 @@ class RemoteAuthController extends Controller
         return [
             'code' => 200,
             'msg' => 'Success',
-            'token' => $token
+            'token' => $token,
         ];
     }
 
@@ -585,7 +594,7 @@ class RemoteAuthController extends Controller
         abort_unless($request->session()->exists('oauth_remasto_id'), 403);
 
         $this->validate($request, [
-            'account' => 'required|url'
+            'account' => 'required|url',
         ]);
 
         $account = $request->input('account');
@@ -594,10 +603,10 @@ class RemoteAuthController extends Controller
         $host = strtolower(config('pixelfed.domain.app'));
         $domain = strtolower(parse_url($account, PHP_URL_HOST));
 
-        if($domain == $host) {
+        if ($domain == $host) {
             $username = Str::of($account)->explode('/')->last();
             $user = User::where('username', $username)->first();
-            if($user) {
+            if ($user) {
                 return ['id' => (string) $user->profile_id];
             } else {
                 return [];
@@ -605,7 +614,7 @@ class RemoteAuthController extends Controller
         } else {
             try {
                 $profile = Helpers::profileFetch($account);
-                if($profile) {
+                if ($profile) {
                     return ['id' => (string) $profile->id];
                 } else {
                     return [];
@@ -635,13 +644,13 @@ class RemoteAuthController extends Controller
         $user = $request->user();
         $profile = $user->profile;
 
-        abort_if(!$profile->avatar, 404, 'Missing avatar');
+        abort_if(! $profile->avatar, 404, 'Missing avatar');
 
         $avatar = $profile->avatar;
         $avatar->remote_url = $request->input('avatar_url');
         $avatar->save();
 
-        MediaStorageService::avatar($avatar, config_cache('pixelfed.cloud_storage') == false);
+        MediaStorageService::avatar($avatar, (bool) config_cache('pixelfed.cloud_storage') == false);
 
         return [200];
     }
@@ -657,7 +666,7 @@ class RemoteAuthController extends Controller
         ), 404);
         abort_unless($request->user(), 404);
 
-        $currentWebfinger = '@' . $request->user()->username . '@' . config('pixelfed.domain.app');
+        $currentWebfinger = '@'.$request->user()->username.'@'.config('pixelfed.domain.app');
         $ra = RemoteAuth::where('user_id', $request->user()->id)->firstOrFail();
         RemoteAuthService::submitToBeagle(
             $ra->webfinger,
@@ -691,19 +700,20 @@ class RemoteAuthController extends Controller
         $user = User::findOrFail($ra->user_id);
         abort_if($user->is_admin || $user->status != null, 422, 'Invalid auth action');
         Auth::loginUsingId($ra->user_id);
+
         return [200];
     }
 
     protected function createUser($data)
     {
         event(new Registered($user = User::create([
-            'name'     => Purify::clean($data['name']),
+            'name' => Purify::clean($data['name']),
             'username' => $data['username'],
-            'email'    => $data['email'],
+            'email' => $data['email'],
             'password' => Hash::make($data['password']),
             'email_verified_at' => config('remote-auth.mastodon.contraints.skip_email_verification') ? now() : null,
             'app_register_ip' => request()->ip(),
-            'register_source' => 'mastodon'
+            'register_source' => 'mastodon',
         ])));
 
         $this->guarder()->login($user);

+ 353 - 354
app/Http/Controllers/SearchController.php

@@ -2,368 +2,367 @@
 
 namespace App\Http\Controllers;
 
-use Auth;
 use App\Hashtag;
 use App\Place;
 use App\Profile;
+use App\Services\WebfingerService;
 use App\Status;
-use Illuminate\Http\Request;
 use App\Util\ActivityPub\Helpers;
+use Auth;
+use Illuminate\Http\Request;
 use Illuminate\Support\Facades\Cache;
 use Illuminate\Support\Str;
-use App\Transformer\Api\{
-	AccountTransformer,
-	HashtagTransformer,
-	StatusTransformer,
-};
-use App\Services\WebfingerService;
 
 class SearchController extends Controller
 {
-	public $tokens = [];
-	public $term = '';
-	public $hash = '';
-	public $cacheKey = 'api:search:tag:';
-
-	public function __construct()
-	{
-		$this->middleware('auth');
-	}
-
-	public function searchAPI(Request $request)
-	{
-		$this->validate($request, [
-			'q' => 'required|string|min:3|max:120',
-			'src' => 'required|string|in:metro',
-			'v' => 'required|integer|in:2',
-			'scope' => 'required|in:all,hashtag,profile,remote,webfinger'
-		]);
-
-		$scope = $request->input('scope') ?? 'all';
-		$this->term = e(urldecode($request->input('q')));
-		$this->hash = hash('sha256', $this->term);
-
-		switch ($scope) {
-			case 'all':
-				$this->getHashtags();
-				$this->getPosts();
-				$this->getProfiles();
-				// $this->getPlaces();
-				break;
-
-			case 'hashtag':
-				$this->getHashtags();
-				break;
-
-			case 'profile':
-				$this->getProfiles();
-				break;
-
-			case 'webfinger':
-				$this->webfingerSearch();
-				break;
-
-			case 'remote':
-				$this->remoteLookupSearch();
-				break;
-
-			case 'place':
-				$this->getPlaces();
-				break;
-
-			default:
-				break;
-		}
-
-		return response()->json($this->tokens, 200, [], JSON_PRETTY_PRINT);
-	}
-
-	protected function getPosts()
-	{
-		$tag = $this->term;
-		$hash = hash('sha256', $tag);
-		if( Helpers::validateUrl($tag) != false &&
-			Helpers::validateLocalUrl($tag) != true &&
-			config_cache('federation.activitypub.enabled') == true &&
-			config('federation.activitypub.remoteFollow') == true
-		) {
-			$remote = Helpers::fetchFromUrl($tag);
-			if( isset($remote['type']) &&
-				$remote['type'] == 'Note') {
-				$item = Helpers::statusFetch($tag);
-				$this->tokens['posts'] = [[
-					'count'  => 0,
-					'url'    => $item->url(),
-					'type'   => 'status',
-					'value'  => "by {$item->profile->username} <span class='float-right'>{$item->created_at->diffForHumans(null, true, true)}</span>",
-					'tokens' => [$item->caption],
-					'name'   => $item->caption,
-					'thumb'  => $item->thumb(),
-				]];
-			}
-		} else {
-			$posts = Status::select('id', 'profile_id', 'caption', 'created_at')
-						->whereHas('media')
-						->whereNull('in_reply_to_id')
-						->whereNull('reblog_of_id')
-						->whereProfileId(Auth::user()->profile_id)
-						->where('caption', 'like', '%'.$tag.'%')
-						->latest()
-						->limit(10)
-						->get();
-
-			if($posts->count() > 0) {
-				$posts = $posts->map(function($item, $key) {
-					return [
-						'count'  => 0,
-						'url'    => $item->url(),
-						'type'   => 'status',
-						'value'  => "by {$item->profile->username} <span class='float-right'>{$item->created_at->diffForHumans(null, true, true)}</span>",
-						'tokens' => [$item->caption],
-						'name'   => $item->caption,
-						'thumb'  => $item->thumb(),
-						'filter' => $item->firstMedia()->filter_class
-					];
-				});
-				$this->tokens['posts'] = $posts;
-			}
-		}
-	}
-
-	protected function getHashtags()
-	{
-		$tag = $this->term;
-		$key = $this->cacheKey . 'hashtags:' . $this->hash;
-		$ttl = now()->addMinutes(1);
-		$tokens = Cache::remember($key, $ttl, function() use($tag) {
-			$htag = Str::startsWith($tag, '#') == true ? mb_substr($tag, 1) : $tag;
-			$hashtags = Hashtag::select('id', 'name', 'slug')
-				->where('slug', 'like', '%'.$htag.'%')
-				->whereHas('posts')
-				->limit(20)
-				->get();
-			if($hashtags->count() > 0) {
-				$tags = $hashtags->map(function ($item, $key) {
-					return [
-						'count'  => $item->posts()->count(),
-						'url'    => $item->url(),
-						'type'   => 'hashtag',
-						'value'  => $item->name,
-						'tokens' => '',
-						'name'   => null,
-					];
-				});
-				return $tags;
-			}
-		});
-		$this->tokens['hashtags'] = $tokens;
-	}
-
-	protected function getPlaces()
-	{
-		$tag = $this->term;
-		// $key = $this->cacheKey . 'places:' . $this->hash;
-		// $ttl = now()->addHours(12);
-		// $tokens = Cache::remember($key, $ttl, function() use($tag) {
-			$htag = Str::contains($tag, ',') == true ? explode(',', $tag) : [$tag];
-			$hashtags = Place::select('id', 'name', 'slug', 'country')
-				->where('name', 'like', '%'.$htag[0].'%')
-				->paginate(20);
-			$tags = [];
-			if($hashtags->count() > 0) {
-				$tags = $hashtags->map(function ($item, $key) {
-					return [
-						'count'     => null,
-						'url'       => $item->url(),
-						'type'      => 'place',
-						'value'     => $item->name . ', ' . $item->country,
-						'tokens'    => '',
-						'name'      => null,
-						'city'      => $item->name,
-						'country'   => $item->country
-					];
-				});
-				// return $tags;
-			}
-		// });
-		$this->tokens['places'] = $tags;
-		$this->tokens['placesPagination'] = [
-			'total' => $hashtags->total(),
-			'current_page' => $hashtags->currentPage(),
-			'last_page' => $hashtags->lastPage()
-		];
-	}
-
-	protected function getProfiles()
-	{
-		$tag = $this->term;
-		$remoteKey = $this->cacheKey . 'profiles:remote:' . $this->hash;
-		$key = $this->cacheKey . 'profiles:' . $this->hash;
-		$remoteTtl = now()->addMinutes(15);
-		$ttl = now()->addHours(2);
-		if( Helpers::validateUrl($tag) != false &&
-			Helpers::validateLocalUrl($tag) != true &&
-			config_cache('federation.activitypub.enabled') == true &&
-			config('federation.activitypub.remoteFollow') == true
-		) {
-			$remote = Helpers::fetchFromUrl($tag);
-			if( isset($remote['type']) &&
-				$remote['type'] == 'Person'
-			) {
-				$this->tokens['profiles'] = Cache::remember($remoteKey, $remoteTtl, function() use($tag) {
-					$item = Helpers::profileFirstOrNew($tag);
-					$tokens = [[
-						'count'  => 1,
-						'url'    => $item->url(),
-						'type'   => 'profile',
-						'value'  => $item->username,
-						'tokens' => [$item->username],
-						'name'   => $item->name,
-						'entity' => [
-							'id' => (string) $item->id,
-							'following' => $item->followedBy(Auth::user()->profile),
-							'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id),
-							'thumb' => $item->avatarUrl(),
-							'local' => (bool) !$item->domain,
-							'post_count' => $item->statuses()->count()
-						]
-					]];
-					return $tokens;
-				});
-			}
-		}
-
-		else {
-			$this->tokens['profiles'] = Cache::remember($key, $ttl, function() use($tag) {
-				if(Str::startsWith($tag, '@')) {
-					$tag = substr($tag, 1);
-				}
-				$users = Profile::select('status', 'domain', 'username', 'name', 'id')
-					->whereNull('status')
-					->where('username', 'like', '%'.$tag.'%')
-					->limit(20)
-					->orderBy('domain')
-					->get();
-
-				if($users->count() > 0) {
-					return $users->map(function ($item, $key) {
-						return [
-							'count'  => 0,
-							'url'    => $item->url(),
-							'type'   => 'profile',
-							'value'  => $item->username,
-							'tokens' => [$item->username],
-							'name'   => $item->name,
-							'avatar' => $item->avatarUrl(),
-							'id'     =>  (string) $item->id,
-							'entity' => [
-								'id' => (string) $item->id,
-								'following' => $item->followedBy(Auth::user()->profile),
-								'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id),
-								'thumb' => $item->avatarUrl(),
-								'local' => (bool) !$item->domain,
-								'post_count' => $item->statuses()->count()
-							]
-						];
-					});
-				}
-			});
-		}
-	}
-
-	public function results(Request $request)
-	{
-		$this->validate($request, [
-			'q' => 'required|string|min:1',
-		]);
-
-		return view('search.results');
-	}
-
-	protected function webfingerSearch()
-	{
-		$wfs = WebfingerService::lookup($this->term);
-
-		if(empty($wfs)) {
-			return;
-		}
-
-		$this->tokens['profiles'] = [
-			[
-				'count'  => 1,
-				'url'    => $wfs['url'],
-				'type'   => 'profile',
-				'value'  => $wfs['username'],
-				'tokens' => [$wfs['username']],
-				'name'   => $wfs['display_name'],
-				'entity' => [
-					'id' => (string) $wfs['id'],
-					'following' => null,
-					'follow_request' => null,
-					'thumb' => $wfs['avatar'],
-					'local' => (bool) $wfs['local']
-				]
-			]
-		];
-		return;
-	}
-
-	protected function remotePostLookup()
-	{
-		$tag = $this->term;
-		$hash = hash('sha256', $tag);
-		$local = Helpers::validateLocalUrl($tag);
-		$valid = Helpers::validateUrl($tag);
-
-		if($valid == false || $local == true) {
-			return;
-		}
-
-		if(Status::whereUri($tag)->whereLocal(false)->exists()) {
-			$item = Status::whereUri($tag)->first();
-			$media = $item->firstMedia();
-			$url = null;
-			if($media) {
-				$url = $media->remote_url;
-			}
-			$this->tokens['posts'] = [[
-				'count'  => 0,
-				'url'    => "/i/web/post/_/$item->profile_id/$item->id",
-				'type'   => 'status',
-				'username' => $item->profile->username,
-				'caption'   => $item->rendered ?? $item->caption,
-				'thumb'  => $url,
-				'timestamp' => $item->created_at->diffForHumans()
-			]];
-		}
-
-		$remote = Helpers::fetchFromUrl($tag);
-
-		if(isset($remote['type']) && $remote['type'] == 'Note') {
-			$item = Helpers::statusFetch($tag);
-			$media = $item->firstMedia();
-			$url = null;
-			if($media) {
-				$url = $media->remote_url;
-			}
-			$this->tokens['posts'] = [[
-				'count'  => 0,
-				'url'    => "/i/web/post/_/$item->profile_id/$item->id",
-				'type'   => 'status',
-				'username' => $item->profile->username,
-				'caption'   => $item->rendered ?? $item->caption,
-				'thumb'  => $url,
-				'timestamp' => $item->created_at->diffForHumans()
-			]];
-		}
-	}
-
-	protected function remoteLookupSearch()
-	{
-		if(!Helpers::validateUrl($this->term)) {
-			return;
-		}
-		$this->getProfiles();
-		$this->remotePostLookup();
-	}
+    public $tokens = [];
+
+    public $term = '';
+
+    public $hash = '';
+
+    public $cacheKey = 'api:search:tag:';
+
+    public function __construct()
+    {
+        $this->middleware('auth');
+    }
+
+    public function searchAPI(Request $request)
+    {
+        $this->validate($request, [
+            'q' => 'required|string|min:3|max:120',
+            'src' => 'required|string|in:metro',
+            'v' => 'required|integer|in:2',
+            'scope' => 'required|in:all,hashtag,profile,remote,webfinger',
+        ]);
+
+        $scope = $request->input('scope') ?? 'all';
+        $this->term = e(urldecode($request->input('q')));
+        $this->hash = hash('sha256', $this->term);
+
+        switch ($scope) {
+            case 'all':
+                $this->getHashtags();
+                $this->getPosts();
+                $this->getProfiles();
+                // $this->getPlaces();
+                break;
+
+            case 'hashtag':
+                $this->getHashtags();
+                break;
+
+            case 'profile':
+                $this->getProfiles();
+                break;
+
+            case 'webfinger':
+                $this->webfingerSearch();
+                break;
+
+            case 'remote':
+                $this->remoteLookupSearch();
+                break;
+
+            case 'place':
+                $this->getPlaces();
+                break;
+
+            default:
+                break;
+        }
+
+        return response()->json($this->tokens, 200, [], JSON_PRETTY_PRINT);
+    }
+
+    protected function getPosts()
+    {
+        $tag = $this->term;
+        $hash = hash('sha256', $tag);
+        if (Helpers::validateUrl($tag) != false &&
+            Helpers::validateLocalUrl($tag) != true &&
+            (bool) config_cache('federation.activitypub.enabled') == true &&
+            config('federation.activitypub.remoteFollow') == true
+        ) {
+            $remote = Helpers::fetchFromUrl($tag);
+            if (isset($remote['type']) &&
+                in_array($remote['type'], ['Note', 'Question'])
+            ) {
+                $item = Helpers::statusFetch($tag);
+                $this->tokens['posts'] = [[
+                    'count' => 0,
+                    'url' => $item->url(),
+                    'type' => 'status',
+                    'value' => "by {$item->profile->username} <span class='float-right'>{$item->created_at->diffForHumans(null, true, true)}</span>",
+                    'tokens' => [$item->caption],
+                    'name' => $item->caption,
+                    'thumb' => $item->thumb(),
+                ]];
+            }
+        } else {
+            $posts = Status::select('id', 'profile_id', 'caption', 'created_at')
+                ->whereHas('media')
+                ->whereNull('in_reply_to_id')
+                ->whereNull('reblog_of_id')
+                ->whereProfileId(Auth::user()->profile_id)
+                ->where('caption', 'like', '%'.$tag.'%')
+                ->latest()
+                ->limit(10)
+                ->get();
+
+            if ($posts->count() > 0) {
+                $posts = $posts->map(function ($item, $key) {
+                    return [
+                        'count' => 0,
+                        'url' => $item->url(),
+                        'type' => 'status',
+                        'value' => "by {$item->profile->username} <span class='float-right'>{$item->created_at->diffForHumans(null, true, true)}</span>",
+                        'tokens' => [$item->caption],
+                        'name' => $item->caption,
+                        'thumb' => $item->thumb(),
+                        'filter' => $item->firstMedia()->filter_class,
+                    ];
+                });
+                $this->tokens['posts'] = $posts;
+            }
+        }
+    }
+
+    protected function getHashtags()
+    {
+        $tag = $this->term;
+        $key = $this->cacheKey.'hashtags:'.$this->hash;
+        $ttl = now()->addMinutes(1);
+        $tokens = Cache::remember($key, $ttl, function () use ($tag) {
+            $htag = Str::startsWith($tag, '#') == true ? mb_substr($tag, 1) : $tag;
+            $hashtags = Hashtag::select('id', 'name', 'slug')
+                ->where('slug', 'like', '%'.$htag.'%')
+                ->whereHas('posts')
+                ->limit(20)
+                ->get();
+            if ($hashtags->count() > 0) {
+                $tags = $hashtags->map(function ($item, $key) {
+                    return [
+                        'count' => $item->posts()->count(),
+                        'url' => $item->url(),
+                        'type' => 'hashtag',
+                        'value' => $item->name,
+                        'tokens' => '',
+                        'name' => null,
+                    ];
+                });
+
+                return $tags;
+            }
+        });
+        $this->tokens['hashtags'] = $tokens;
+    }
+
+    protected function getPlaces()
+    {
+        $tag = $this->term;
+        // $key = $this->cacheKey . 'places:' . $this->hash;
+        // $ttl = now()->addHours(12);
+        // $tokens = Cache::remember($key, $ttl, function() use($tag) {
+        $htag = Str::contains($tag, ',') == true ? explode(',', $tag) : [$tag];
+        $hashtags = Place::select('id', 'name', 'slug', 'country')
+            ->where('name', 'like', '%'.$htag[0].'%')
+            ->paginate(20);
+        $tags = [];
+        if ($hashtags->count() > 0) {
+            $tags = $hashtags->map(function ($item, $key) {
+                return [
+                    'count' => null,
+                    'url' => $item->url(),
+                    'type' => 'place',
+                    'value' => $item->name.', '.$item->country,
+                    'tokens' => '',
+                    'name' => null,
+                    'city' => $item->name,
+                    'country' => $item->country,
+                ];
+            });
+            // return $tags;
+        }
+        // });
+        $this->tokens['places'] = $tags;
+        $this->tokens['placesPagination'] = [
+            'total' => $hashtags->total(),
+            'current_page' => $hashtags->currentPage(),
+            'last_page' => $hashtags->lastPage(),
+        ];
+    }
+
+    protected function getProfiles()
+    {
+        $tag = $this->term;
+        $remoteKey = $this->cacheKey.'profiles:remote:'.$this->hash;
+        $key = $this->cacheKey.'profiles:'.$this->hash;
+        $remoteTtl = now()->addMinutes(15);
+        $ttl = now()->addHours(2);
+        if (Helpers::validateUrl($tag) != false &&
+            Helpers::validateLocalUrl($tag) != true &&
+            (bool) config_cache('federation.activitypub.enabled') == true &&
+            config('federation.activitypub.remoteFollow') == true
+        ) {
+            $remote = Helpers::fetchFromUrl($tag);
+            if (isset($remote['type']) &&
+                $remote['type'] == 'Person'
+            ) {
+                $this->tokens['profiles'] = Cache::remember($remoteKey, $remoteTtl, function () use ($tag) {
+                    $item = Helpers::profileFirstOrNew($tag);
+                    $tokens = [[
+                        'count' => 1,
+                        'url' => $item->url(),
+                        'type' => 'profile',
+                        'value' => $item->username,
+                        'tokens' => [$item->username],
+                        'name' => $item->name,
+                        'entity' => [
+                            'id' => (string) $item->id,
+                            'following' => $item->followedBy(Auth::user()->profile),
+                            'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id),
+                            'thumb' => $item->avatarUrl(),
+                            'local' => (bool) ! $item->domain,
+                            'post_count' => $item->statuses()->count(),
+                        ],
+                    ]];
+
+                    return $tokens;
+                });
+            }
+        } else {
+            $this->tokens['profiles'] = Cache::remember($key, $ttl, function () use ($tag) {
+                if (Str::startsWith($tag, '@')) {
+                    $tag = substr($tag, 1);
+                }
+                $users = Profile::select('status', 'domain', 'username', 'name', 'id')
+                    ->whereNull('status')
+                    ->where('username', 'like', '%'.$tag.'%')
+                    ->limit(20)
+                    ->orderBy('domain')
+                    ->get();
+
+                if ($users->count() > 0) {
+                    return $users->map(function ($item, $key) {
+                        return [
+                            'count' => 0,
+                            'url' => $item->url(),
+                            'type' => 'profile',
+                            'value' => $item->username,
+                            'tokens' => [$item->username],
+                            'name' => $item->name,
+                            'avatar' => $item->avatarUrl(),
+                            'id' => (string) $item->id,
+                            'entity' => [
+                                'id' => (string) $item->id,
+                                'following' => $item->followedBy(Auth::user()->profile),
+                                'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id),
+                                'thumb' => $item->avatarUrl(),
+                                'local' => (bool) ! $item->domain,
+                                'post_count' => $item->statuses()->count(),
+                            ],
+                        ];
+                    });
+                }
+            });
+        }
+    }
+
+    public function results(Request $request)
+    {
+        $this->validate($request, [
+            'q' => 'required|string|min:1',
+        ]);
+
+        return view('search.results');
+    }
+
+    protected function webfingerSearch()
+    {
+        $wfs = WebfingerService::lookup($this->term);
+
+        if (empty($wfs)) {
+            return;
+        }
+
+        $this->tokens['profiles'] = [
+            [
+                'count' => 1,
+                'url' => $wfs['url'],
+                'type' => 'profile',
+                'value' => $wfs['username'],
+                'tokens' => [$wfs['username']],
+                'name' => $wfs['display_name'],
+                'entity' => [
+                    'id' => (string) $wfs['id'],
+                    'following' => null,
+                    'follow_request' => null,
+                    'thumb' => $wfs['avatar'],
+                    'local' => (bool) $wfs['local'],
+                ],
+            ],
+        ];
+
+    }
+
+    protected function remotePostLookup()
+    {
+        $tag = $this->term;
+        $hash = hash('sha256', $tag);
+        $local = Helpers::validateLocalUrl($tag);
+        $valid = Helpers::validateUrl($tag);
+
+        if ($valid == false || $local == true) {
+            return;
+        }
+
+        if (Status::whereUri($tag)->whereLocal(false)->exists()) {
+            $item = Status::whereUri($tag)->first();
+            $media = $item->firstMedia();
+            $url = null;
+            if ($media) {
+                $url = $media->remote_url;
+            }
+            $this->tokens['posts'] = [[
+                'count' => 0,
+                'url' => "/i/web/post/_/$item->profile_id/$item->id",
+                'type' => 'status',
+                'username' => $item->profile->username,
+                'caption' => $item->rendered ?? $item->caption,
+                'thumb' => $url,
+                'timestamp' => $item->created_at->diffForHumans(),
+            ]];
+        }
+
+        $remote = Helpers::fetchFromUrl($tag);
+
+        if (isset($remote['type']) && $remote['type'] == 'Note') {
+            $item = Helpers::statusFetch($tag);
+            $media = $item->firstMedia();
+            $url = null;
+            if ($media) {
+                $url = $media->remote_url;
+            }
+            $this->tokens['posts'] = [[
+                'count' => 0,
+                'url' => "/i/web/post/_/$item->profile_id/$item->id",
+                'type' => 'status',
+                'username' => $item->profile->username,
+                'caption' => $item->rendered ?? $item->caption,
+                'thumb' => $url,
+                'timestamp' => $item->created_at->diffForHumans(),
+            ]];
+        }
+    }
+
+    protected function remoteLookupSearch()
+    {
+        if (! Helpers::validateUrl($this->term)) {
+            return;
+        }
+        $this->getProfiles();
+        $this->remotePostLookup();
+    }
 }

+ 1 - 1
app/Http/Controllers/StatusController.php

@@ -78,7 +78,7 @@ class StatusController extends Controller
             ]);
         }
 
-        if ($request->wantsJson() && config_cache('federation.activitypub.enabled')) {
+        if ($request->wantsJson() && (bool) config_cache('federation.activitypub.enabled')) {
             return $this->showActivityPub($request, $status);
         }
 

+ 131 - 121
app/Http/Controllers/Stories/StoryApiV1Controller.php

@@ -2,54 +2,56 @@
 
 namespace App\Http\Controllers\Stories;
 
-use App\Http\Controllers\Controller;
-use Illuminate\Http\Request;
-use Illuminate\Support\Str;
-use Illuminate\Support\Facades\Cache;
-use Illuminate\Support\Facades\Storage;
-use App\Models\Conversation;
 use App\DirectMessage;
-use App\Notification;
-use App\Story;
-use App\Status;
-use App\StoryView;
+use App\Http\Controllers\Controller;
+use App\Http\Resources\StoryView as StoryViewResource;
 use App\Jobs\StoryPipeline\StoryDelete;
 use App\Jobs\StoryPipeline\StoryFanout;
 use App\Jobs\StoryPipeline\StoryReplyDeliver;
 use App\Jobs\StoryPipeline\StoryViewDeliver;
+use App\Models\Conversation;
+use App\Notification;
 use App\Services\AccountService;
 use App\Services\MediaPathService;
 use App\Services\StoryService;
-use App\Http\Resources\StoryView as StoryViewResource;
+use App\Status;
+use App\Story;
+use App\StoryView;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\Storage;
+use Illuminate\Support\Str;
 
 class StoryApiV1Controller extends Controller
 {
     const RECENT_KEY = 'pf:stories:recent-by-id:';
+
     const RECENT_TTL = 300;
 
     public function carousel(Request $request)
     {
-        abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
+        abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
         $pid = $request->user()->profile_id;
 
-        if(config('database.default') == 'pgsql') {
-            $s = Cache::remember(self::RECENT_KEY . $pid, self::RECENT_TTL, function() use($pid) {
+        if (config('database.default') == 'pgsql') {
+            $s = Cache::remember(self::RECENT_KEY.$pid, self::RECENT_TTL, function () use ($pid) {
                 return Story::select('stories.*', 'followers.following_id')
                     ->leftJoin('followers', 'followers.following_id', 'stories.profile_id')
                     ->where('followers.profile_id', $pid)
                     ->where('stories.active', true)
-                    ->map(function($s) {
-                        $r  = new \StdClass;
+                    ->map(function ($s) {
+                        $r = new \StdClass;
                         $r->id = $s->id;
                         $r->profile_id = $s->profile_id;
                         $r->type = $s->type;
                         $r->path = $s->path;
+
                         return $r;
                     })
                     ->unique('profile_id');
             });
         } else {
-            $s = Cache::remember(self::RECENT_KEY . $pid, self::RECENT_TTL, function() use($pid) {
+            $s = Cache::remember(self::RECENT_KEY.$pid, self::RECENT_TTL, function () use ($pid) {
                 return Story::select('stories.*', 'followers.following_id')
                     ->leftJoin('followers', 'followers.following_id', 'stories.profile_id')
                     ->where('followers.profile_id', $pid)
@@ -59,9 +61,9 @@ class StoryApiV1Controller extends Controller
             });
         }
 
-        $nodes = $s->map(function($s) use($pid) {
+        $nodes = $s->map(function ($s) use ($pid) {
             $profile = AccountService::get($s->profile_id, true);
-            if(!$profile || !isset($profile['id'])) {
+            if (! $profile || ! isset($profile['id'])) {
                 return false;
             }
 
@@ -72,50 +74,51 @@ class StoryApiV1Controller extends Controller
                 'src' => url(Storage::url($s->path)),
                 'duration' => $s->duration ?? 3,
                 'seen' => StoryService::hasSeen($pid, $s->id),
-                'created_at' => $s->created_at->format('c')
+                'created_at' => $s->created_at->format('c'),
             ];
         })
-        ->filter()
-        ->groupBy('pid')
-        ->map(function($item) use($pid) {
-            $profile = AccountService::get($item[0]['pid'], true);
-            $url = $profile['local'] ? url("/stories/{$profile['username']}") :
-                url("/i/rs/{$profile['id']}");
-            return [
-                'id' => 'pfs:' . $profile['id'],
-                'user' => [
-                    'id' => (string) $profile['id'],
-                    'username' => $profile['username'],
-                    'username_acct' => $profile['acct'],
-                    'avatar' => $profile['avatar'],
-                    'local' => $profile['local'],
-                    'is_author' => $profile['id'] == $pid
-                ],
-                'nodes' => $item,
-                'url' => $url,
-                'seen' => StoryService::hasSeen($pid, StoryService::latest($profile['id'])),
-            ];
-        })
-        ->sortBy('seen')
-        ->values();
+            ->filter()
+            ->groupBy('pid')
+            ->map(function ($item) use ($pid) {
+                $profile = AccountService::get($item[0]['pid'], true);
+                $url = $profile['local'] ? url("/stories/{$profile['username']}") :
+                    url("/i/rs/{$profile['id']}");
+
+                return [
+                    'id' => 'pfs:'.$profile['id'],
+                    'user' => [
+                        'id' => (string) $profile['id'],
+                        'username' => $profile['username'],
+                        'username_acct' => $profile['acct'],
+                        'avatar' => $profile['avatar'],
+                        'local' => $profile['local'],
+                        'is_author' => $profile['id'] == $pid,
+                    ],
+                    'nodes' => $item,
+                    'url' => $url,
+                    'seen' => StoryService::hasSeen($pid, StoryService::latest($profile['id'])),
+                ];
+            })
+            ->sortBy('seen')
+            ->values();
 
         $res = [
             'self' => [],
             'nodes' => $nodes,
         ];
 
-        if(Story::whereProfileId($pid)->whereActive(true)->exists()) {
+        if (Story::whereProfileId($pid)->whereActive(true)->exists()) {
             $selfStories = Story::whereProfileId($pid)
                 ->whereActive(true)
                 ->get()
-                ->map(function($s) use($pid) {
+                ->map(function ($s) {
                     return [
                         'id' => (string) $s->id,
                         'type' => $s->type,
                         'src' => url(Storage::url($s->path)),
                         'duration' => $s->duration,
                         'seen' => true,
-                        'created_at' => $s->created_at->format('c')
+                        'created_at' => $s->created_at->format('c'),
                     ];
                 })
                 ->sortBy('id')
@@ -127,38 +130,40 @@ class StoryApiV1Controller extends Controller
                     'username' => $selfProfile['acct'],
                     'avatar' => $selfProfile['avatar'],
                     'local' => $selfProfile['local'],
-                    'is_author' => true
+                    'is_author' => true,
                 ],
 
                 'nodes' => $selfStories,
             ];
         }
-        return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
+
+        return response()->json($res, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
     }
 
     public function selfCarousel(Request $request)
     {
-        abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
+        abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
         $pid = $request->user()->profile_id;
 
-        if(config('database.default') == 'pgsql') {
-            $s = Cache::remember(self::RECENT_KEY . $pid, self::RECENT_TTL, function() use($pid) {
+        if (config('database.default') == 'pgsql') {
+            $s = Cache::remember(self::RECENT_KEY.$pid, self::RECENT_TTL, function () use ($pid) {
                 return Story::select('stories.*', 'followers.following_id')
                     ->leftJoin('followers', 'followers.following_id', 'stories.profile_id')
                     ->where('followers.profile_id', $pid)
                     ->where('stories.active', true)
-                    ->map(function($s) {
-                        $r  = new \StdClass;
+                    ->map(function ($s) {
+                        $r = new \StdClass;
                         $r->id = $s->id;
                         $r->profile_id = $s->profile_id;
                         $r->type = $s->type;
                         $r->path = $s->path;
+
                         return $r;
                     })
                     ->unique('profile_id');
             });
         } else {
-            $s = Cache::remember(self::RECENT_KEY . $pid, self::RECENT_TTL, function() use($pid) {
+            $s = Cache::remember(self::RECENT_KEY.$pid, self::RECENT_TTL, function () use ($pid) {
                 return Story::select('stories.*', 'followers.following_id')
                     ->leftJoin('followers', 'followers.following_id', 'stories.profile_id')
                     ->where('followers.profile_id', $pid)
@@ -168,9 +173,9 @@ class StoryApiV1Controller extends Controller
             });
         }
 
-        $nodes = $s->map(function($s) use($pid) {
+        $nodes = $s->map(function ($s) use ($pid) {
             $profile = AccountService::get($s->profile_id, true);
-            if(!$profile || !isset($profile['id'])) {
+            if (! $profile || ! isset($profile['id'])) {
                 return false;
             }
 
@@ -181,32 +186,33 @@ class StoryApiV1Controller extends Controller
                 'src' => url(Storage::url($s->path)),
                 'duration' => $s->duration ?? 3,
                 'seen' => StoryService::hasSeen($pid, $s->id),
-                'created_at' => $s->created_at->format('c')
+                'created_at' => $s->created_at->format('c'),
             ];
         })
-        ->filter()
-        ->groupBy('pid')
-        ->map(function($item) use($pid) {
-            $profile = AccountService::get($item[0]['pid'], true);
-            $url = $profile['local'] ? url("/stories/{$profile['username']}") :
-                url("/i/rs/{$profile['id']}");
-            return [
-                'id' => 'pfs:' . $profile['id'],
-                'user' => [
-                    'id' => (string) $profile['id'],
-                    'username' => $profile['username'],
-                    'username_acct' => $profile['acct'],
-                    'avatar' => $profile['avatar'],
-                    'local' => $profile['local'],
-                    'is_author' => $profile['id'] == $pid
-                ],
-                'nodes' => $item,
-                'url' => $url,
-                'seen' => StoryService::hasSeen($pid, StoryService::latest($profile['id'])),
-            ];
-        })
-        ->sortBy('seen')
-        ->values();
+            ->filter()
+            ->groupBy('pid')
+            ->map(function ($item) use ($pid) {
+                $profile = AccountService::get($item[0]['pid'], true);
+                $url = $profile['local'] ? url("/stories/{$profile['username']}") :
+                    url("/i/rs/{$profile['id']}");
+
+                return [
+                    'id' => 'pfs:'.$profile['id'],
+                    'user' => [
+                        'id' => (string) $profile['id'],
+                        'username' => $profile['username'],
+                        'username_acct' => $profile['acct'],
+                        'avatar' => $profile['avatar'],
+                        'local' => $profile['local'],
+                        'is_author' => $profile['id'] == $pid,
+                    ],
+                    'nodes' => $item,
+                    'url' => $url,
+                    'seen' => StoryService::hasSeen($pid, StoryService::latest($profile['id'])),
+                ];
+            })
+            ->sortBy('seen')
+            ->values();
 
         $selfProfile = AccountService::get($pid, true);
         $res = [
@@ -216,7 +222,7 @@ class StoryApiV1Controller extends Controller
                     'username' => $selfProfile['acct'],
                     'avatar' => $selfProfile['avatar'],
                     'local' => $selfProfile['local'],
-                    'is_author' => true
+                    'is_author' => true,
                 ],
 
                 'nodes' => [],
@@ -224,40 +230,41 @@ class StoryApiV1Controller extends Controller
             'nodes' => $nodes,
         ];
 
-        if(Story::whereProfileId($pid)->whereActive(true)->exists()) {
+        if (Story::whereProfileId($pid)->whereActive(true)->exists()) {
             $selfStories = Story::whereProfileId($pid)
                 ->whereActive(true)
                 ->get()
-                ->map(function($s) use($pid) {
+                ->map(function ($s) {
                     return [
                         'id' => (string) $s->id,
                         'type' => $s->type,
                         'src' => url(Storage::url($s->path)),
                         'duration' => $s->duration,
                         'seen' => true,
-                        'created_at' => $s->created_at->format('c')
+                        'created_at' => $s->created_at->format('c'),
                     ];
                 })
                 ->sortBy('id')
                 ->values();
             $res['self']['nodes'] = $selfStories;
         }
-        return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
+
+        return response()->json($res, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
     }
 
     public function add(Request $request)
     {
-        abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
+        abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
 
         $this->validate($request, [
-            'file' => function() {
+            'file' => function () {
                 return [
                     'required',
                     'mimetypes:image/jpeg,image/png,video/mp4',
-                    'max:' . config_cache('pixelfed.max_photo_size'),
+                    'max:'.config_cache('pixelfed.max_photo_size'),
                 ];
             },
-            'duration' => 'sometimes|integer|min:0|max:30'
+            'duration' => 'sometimes|integer|min:0|max:30',
         ]);
 
         $user = $request->user();
@@ -267,7 +274,7 @@ class StoryApiV1Controller extends Controller
             ->where('expires_at', '>', now())
             ->count();
 
-        if($count >= Story::MAX_PER_DAY) {
+        if ($count >= Story::MAX_PER_DAY) {
             abort(418, 'You have reached your limit for new Stories today.');
         }
 
@@ -277,7 +284,7 @@ class StoryApiV1Controller extends Controller
         $story = new Story();
         $story->duration = $request->input('duration', 3);
         $story->profile_id = $user->profile_id;
-        $story->type = Str::endsWith($photo->getMimeType(), 'mp4') ? 'video' :'photo';
+        $story->type = Str::endsWith($photo->getMimeType(), 'mp4') ? 'video' : 'photo';
         $story->mime = $photo->getMimeType();
         $story->path = $path;
         $story->local = true;
@@ -290,10 +297,10 @@ class StoryApiV1Controller extends Controller
 
         $res = [
             'code' => 200,
-            'msg'  => 'Successfully added',
+            'msg' => 'Successfully added',
             'media_id' => (string) $story->id,
-            'media_url' => url(Storage::url($url)) . '?v=' . time(),
-            'media_type' => $story->type
+            'media_url' => url(Storage::url($url)).'?v='.time(),
+            'media_type' => $story->type,
         ];
 
         return $res;
@@ -301,13 +308,13 @@ class StoryApiV1Controller extends Controller
 
     public function publish(Request $request)
     {
-        abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
+        abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
 
         $this->validate($request, [
             'media_id' => 'required',
             'duration' => 'required|integer|min:0|max:30',
             'can_reply' => 'required|boolean',
-            'can_react' => 'required|boolean'
+            'can_react' => 'required|boolean',
         ]);
 
         $id = $request->input('media_id');
@@ -327,13 +334,13 @@ class StoryApiV1Controller extends Controller
 
         return [
             'code' => 200,
-            'msg'  => 'Successfully published',
+            'msg' => 'Successfully published',
         ];
     }
 
     public function delete(Request $request, $id)
     {
-        abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
+        abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
 
         $user = $request->user();
 
@@ -346,16 +353,16 @@ class StoryApiV1Controller extends Controller
 
         return [
             'code' => 200,
-            'msg'  => 'Successfully deleted'
+            'msg' => 'Successfully deleted',
         ];
     }
 
     public function viewed(Request $request)
     {
-        abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
+        abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
 
         $this->validate($request, [
-            'id'    => 'required|min:1',
+            'id' => 'required|min:1',
         ]);
         $id = $request->input('id');
 
@@ -367,44 +374,45 @@ class StoryApiV1Controller extends Controller
 
         $profile = $story->profile;
 
-        if($story->profile_id == $authed->id) {
+        if ($story->profile_id == $authed->id) {
             return [];
         }
 
         $publicOnly = (bool) $profile->followedBy($authed);
-        abort_if(!$publicOnly, 403);
+        abort_if(! $publicOnly, 403);
 
         $v = StoryView::firstOrCreate([
             'story_id' => $id,
-            'profile_id' => $authed->id
+            'profile_id' => $authed->id,
         ]);
 
-        if($v->wasRecentlyCreated) {
+        if ($v->wasRecentlyCreated) {
             Story::findOrFail($story->id)->increment('view_count');
 
-            if($story->local == false) {
+            if ($story->local == false) {
                 StoryViewDeliver::dispatch($story, $authed)->onQueue('story');
             }
         }
 
-        Cache::forget('stories:recent:by_id:' . $authed->id);
+        Cache::forget('stories:recent:by_id:'.$authed->id);
         StoryService::addSeen($authed->id, $story->id);
+
         return ['code' => 200];
     }
 
     public function comment(Request $request)
     {
-        abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
+        abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
         $this->validate($request, [
             'sid' => 'required',
-            'caption' => 'required|string'
+            'caption' => 'required|string',
         ]);
         $pid = $request->user()->profile_id;
         $text = $request->input('caption');
 
         $story = Story::findOrFail($request->input('sid'));
 
-        abort_if(!$story->can_reply, 422);
+        abort_if(! $story->can_reply, 422);
 
         $status = new Status;
         $status->type = 'story:reply';
@@ -415,7 +423,7 @@ class StoryApiV1Controller extends Controller
         $status->visibility = 'direct';
         $status->in_reply_to_profile_id = $story->profile_id;
         $status->entities = json_encode([
-            'story_id' => $story->id
+            'story_id' => $story->id,
         ]);
         $status->save();
 
@@ -429,24 +437,24 @@ class StoryApiV1Controller extends Controller
             'story_actor_username' => $request->user()->username,
             'story_id' => $story->id,
             'story_media_url' => url(Storage::url($story->path)),
-            'caption' => $text
+            'caption' => $text,
         ]);
         $dm->save();
 
         Conversation::updateOrInsert(
             [
                 'to_id' => $story->profile_id,
-                'from_id' => $pid
+                'from_id' => $pid,
             ],
             [
                 'type' => 'story:comment',
                 'status_id' => $status->id,
                 'dm_id' => $dm->id,
-                'is_hidden' => false
+                'is_hidden' => false,
             ]
         );
 
-        if($story->local) {
+        if ($story->local) {
             $n = new Notification;
             $n->profile_id = $dm->to_id;
             $n->actor_id = $dm->from_id;
@@ -460,33 +468,35 @@ class StoryApiV1Controller extends Controller
 
         return [
             'code' => 200,
-            'msg'  => 'Sent!'
+            'msg' => 'Sent!',
         ];
     }
 
     protected function storeMedia($photo, $user)
     {
         $mimes = explode(',', config_cache('pixelfed.media_types'));
-        if(in_array($photo->getMimeType(), [
+        if (in_array($photo->getMimeType(), [
             'image/jpeg',
             'image/png',
-            'video/mp4'
+            'video/mp4',
         ]) == false) {
             abort(400, 'Invalid media type');
+
             return;
         }
 
         $storagePath = MediaPathService::story($user->profile);
-        $path = $photo->storePubliclyAs($storagePath, Str::random(random_int(2, 12)) . '_' . Str::random(random_int(32, 35)) . '_' . Str::random(random_int(1, 14)) . '.' . $photo->extension());
+        $path = $photo->storePubliclyAs($storagePath, Str::random(random_int(2, 12)).'_'.Str::random(random_int(32, 35)).'_'.Str::random(random_int(1, 14)).'.'.$photo->extension());
+
         return $path;
     }
 
     public function viewers(Request $request)
     {
-        abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
+        abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
 
         $this->validate($request, [
-            'sid' => 'required|string|min:1|max:50'
+            'sid' => 'required|string|min:1|max:50',
         ]);
 
         $pid = $request->user()->profile_id;

+ 89 - 93
app/Http/Controllers/StoryComposeController.php

@@ -2,59 +2,52 @@
 
 namespace App\Http\Controllers;
 
-use Illuminate\Http\Request;
-use Illuminate\Support\Str;
-use App\Media;
-use App\Profile;
-use App\Report;
 use App\DirectMessage;
-use App\Notification;
-use App\Status;
-use App\Story;
-use App\StoryView;
+use App\Jobs\StoryPipeline\StoryDelete;
+use App\Jobs\StoryPipeline\StoryFanout;
+use App\Jobs\StoryPipeline\StoryReactionDeliver;
+use App\Jobs\StoryPipeline\StoryReplyDeliver;
+use App\Models\Conversation;
 use App\Models\Poll;
 use App\Models\PollVote;
-use App\Services\ProfileService;
-use App\Services\StoryService;
-use Cache, Storage;
-use Image as Intervention;
+use App\Notification;
+use App\Report;
 use App\Services\FollowerService;
 use App\Services\MediaPathService;
-use FFMpeg;
-use FFMpeg\Coordinate\Dimension;
-use FFMpeg\Format\Video\X264;
-use App\Jobs\StoryPipeline\StoryReactionDeliver;
-use App\Jobs\StoryPipeline\StoryReplyDeliver;
-use App\Jobs\StoryPipeline\StoryFanout;
-use App\Jobs\StoryPipeline\StoryDelete;
-use ImageOptimizer;
-use App\Models\Conversation;
+use App\Services\StoryService;
 use App\Services\UserRoleService;
+use App\Status;
+use App\Story;
+use FFMpeg;
+use Illuminate\Http\Request;
+use Illuminate\Support\Str;
+use Image as Intervention;
+use Storage;
 
 class StoryComposeController extends Controller
 {
     public function apiV1Add(Request $request)
     {
-        abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
+        abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
 
         $this->validate($request, [
-            'file' => function() {
+            'file' => function () {
                 return [
                     'required',
                     'mimetypes:image/jpeg,image/png,video/mp4',
-                    'max:' . config_cache('pixelfed.max_photo_size'),
+                    'max:'.config_cache('pixelfed.max_photo_size'),
                 ];
             },
         ]);
 
         $user = $request->user();
-        abort_if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action');
+        abort_if($user->has_roles && ! UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action');
         $count = Story::whereProfileId($user->profile_id)
             ->whereActive(true)
             ->where('expires_at', '>', now())
             ->count();
 
-        if($count >= Story::MAX_PER_DAY) {
+        if ($count >= Story::MAX_PER_DAY) {
             abort(418, 'You have reached your limit for new Stories today.');
         }
 
@@ -64,7 +57,7 @@ class StoryComposeController extends Controller
         $story = new Story();
         $story->duration = 3;
         $story->profile_id = $user->profile_id;
-        $story->type = Str::endsWith($photo->getMimeType(), 'mp4') ? 'video' :'photo';
+        $story->type = Str::endsWith($photo->getMimeType(), 'mp4') ? 'video' : 'photo';
         $story->mime = $photo->getMimeType();
         $story->path = $path;
         $story->local = true;
@@ -77,21 +70,22 @@ class StoryComposeController extends Controller
 
         $res = [
             'code' => 200,
-            'msg'  => 'Successfully added',
+            'msg' => 'Successfully added',
             'media_id' => (string) $story->id,
-            'media_url' => url(Storage::url($url)) . '?v=' . time(),
-            'media_type' => $story->type
+            'media_url' => url(Storage::url($url)).'?v='.time(),
+            'media_type' => $story->type,
         ];
 
-        if($story->type === 'video') {
+        if ($story->type === 'video') {
             $video = FFMpeg::open($path);
             $duration = $video->getDurationInSeconds();
             $res['media_duration'] = $duration;
-            if($duration > 500) {
+            if ($duration > 500) {
                 Storage::delete($story->path);
                 $story->delete();
+
                 return response()->json([
-                    'message' => 'Video duration cannot exceed 60 seconds'
+                    'message' => 'Video duration cannot exceed 60 seconds',
                 ], 422);
             }
         }
@@ -102,37 +96,39 @@ class StoryComposeController extends Controller
     protected function storePhoto($photo, $user)
     {
         $mimes = explode(',', config_cache('pixelfed.media_types'));
-        if(in_array($photo->getMimeType(), [
+        if (in_array($photo->getMimeType(), [
             'image/jpeg',
             'image/png',
-            'video/mp4'
+            'video/mp4',
         ]) == false) {
             abort(400, 'Invalid media type');
+
             return;
         }
 
         $storagePath = MediaPathService::story($user->profile);
-        $path = $photo->storePubliclyAs($storagePath, Str::random(random_int(2, 12)) . '_' . Str::random(random_int(32, 35)) . '_' . Str::random(random_int(1, 14)) . '.' . $photo->extension());
-        if(in_array($photo->getMimeType(), ['image/jpeg','image/png'])) {
-            $fpath = storage_path('app/' . $path);
+        $path = $photo->storePubliclyAs($storagePath, Str::random(random_int(2, 12)).'_'.Str::random(random_int(32, 35)).'_'.Str::random(random_int(1, 14)).'.'.$photo->extension());
+        if (in_array($photo->getMimeType(), ['image/jpeg', 'image/png'])) {
+            $fpath = storage_path('app/'.$path);
             $img = Intervention::make($fpath);
             $img->orientate();
             $img->save($fpath, config_cache('pixelfed.image_quality'));
             $img->destroy();
         }
+
         return $path;
     }
 
     public function cropPhoto(Request $request)
     {
-        abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
+        abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
 
         $this->validate($request, [
             'media_id' => 'required|integer|min:1',
             'width' => 'required',
             'height' => 'required',
             'x' => 'required',
-            'y' => 'required'
+            'y' => 'required',
         ]);
 
         $user = $request->user();
@@ -144,13 +140,13 @@ class StoryComposeController extends Controller
 
         $story = Story::whereProfileId($user->profile_id)->findOrFail($id);
 
-        $path = storage_path('app/' . $story->path);
+        $path = storage_path('app/'.$story->path);
 
-        if(!is_file($path)) {
+        if (! is_file($path)) {
             abort(400, 'Invalid or missing media.');
         }
 
-        if($story->type === 'photo') {
+        if ($story->type === 'photo') {
             $img = Intervention::make($path);
             $img->crop($width, $height, $x, $y);
             $img->resize(1080, 1920, function ($constraint) {
@@ -161,24 +157,24 @@ class StoryComposeController extends Controller
 
         return [
             'code' => 200,
-            'msg'  => 'Successfully cropped',
+            'msg' => 'Successfully cropped',
         ];
     }
 
     public function publishStory(Request $request)
     {
-        abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
+        abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
 
         $this->validate($request, [
             'media_id' => 'required',
             'duration' => 'required|integer|min:3|max:120',
             'can_reply' => 'required|boolean',
-            'can_react' => 'required|boolean'
+            'can_react' => 'required|boolean',
         ]);
 
         $id = $request->input('media_id');
         $user = $request->user();
-        abort_if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action');
+        abort_if($user->has_roles && ! UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action');
         $story = Story::whereProfileId($user->profile_id)
             ->findOrFail($id);
 
@@ -194,13 +190,13 @@ class StoryComposeController extends Controller
 
         return [
             'code' => 200,
-            'msg'  => 'Successfully published',
+            'msg' => 'Successfully published',
         ];
     }
 
     public function apiV1Delete(Request $request, $id)
     {
-        abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
+        abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
 
         $user = $request->user();
 
@@ -213,40 +209,40 @@ class StoryComposeController extends Controller
 
         return [
             'code' => 200,
-            'msg'  => 'Successfully deleted'
+            'msg' => 'Successfully deleted',
         ];
     }
 
     public function compose(Request $request)
     {
-        abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
+        abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
         $user = $request->user();
-        abort_if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action');
+        abort_if($user->has_roles && ! UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action');
 
         return view('stories.compose');
     }
 
     public function createPoll(Request $request)
     {
-        abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
-        abort_if(!config_cache('instance.polls.enabled'), 404);
+        abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
+        abort_if(! config_cache('instance.polls.enabled'), 404);
 
         return $request->all();
     }
 
     public function publishStoryPoll(Request $request)
     {
-        abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
+        abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
 
         $this->validate($request, [
             'question' => 'required|string|min:6|max:140',
             'options' => 'required|array|min:2|max:4',
             'can_reply' => 'required|boolean',
-            'can_react' => 'required|boolean'
+            'can_react' => 'required|boolean',
         ]);
 
         $user = $request->user();
-        abort_if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action');
+        abort_if($user->has_roles && ! UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action');
         $pid = $request->user()->profile_id;
 
         $count = Story::whereProfileId($pid)
@@ -254,7 +250,7 @@ class StoryComposeController extends Controller
             ->where('expires_at', '>', now())
             ->count();
 
-        if($count >= Story::MAX_PER_DAY) {
+        if ($count >= Story::MAX_PER_DAY) {
             abort(418, 'You have reached your limit for new Stories today.');
         }
 
@@ -262,7 +258,7 @@ class StoryComposeController extends Controller
         $story->type = 'poll';
         $story->story = json_encode([
             'question' => $request->input('question'),
-            'options' => $request->input('options')
+            'options' => $request->input('options'),
         ]);
         $story->public = false;
         $story->local = true;
@@ -278,7 +274,7 @@ class StoryComposeController extends Controller
         $poll->profile_id = $pid;
         $poll->poll_options = $request->input('options');
         $poll->expires_at = $story->expires_at;
-        $poll->cached_tallies = collect($poll->poll_options)->map(function($o) {
+        $poll->cached_tallies = collect($poll->poll_options)->map(function ($o) {
             return 0;
         })->toArray();
         $poll->save();
@@ -290,23 +286,23 @@ class StoryComposeController extends Controller
 
         return [
             'code' => 200,
-            'msg'  => 'Successfully published',
+            'msg' => 'Successfully published',
         ];
     }
 
     public function storyPollVote(Request $request)
     {
-        abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
+        abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
 
         $this->validate($request, [
             'sid' => 'required',
-            'ci' => 'required|integer|min:0|max:3'
+            'ci' => 'required|integer|min:0|max:3',
         ]);
 
         $pid = $request->user()->profile_id;
         $ci = $request->input('ci');
         $story = Story::findOrFail($request->input('sid'));
-        abort_if(!FollowerService::follows($pid, $story->profile_id), 403);
+        abort_if(! FollowerService::follows($pid, $story->profile_id), 403);
         $poll = Poll::whereStoryId($story->id)->firstOrFail();
 
         $vote = new PollVote;
@@ -318,7 +314,7 @@ class StoryComposeController extends Controller
         $vote->save();
 
         $poll->votes_count = $poll->votes_count + 1;
-        $poll->cached_tallies = collect($poll->getTallies())->map(function($tally, $key) use($ci) {
+        $poll->cached_tallies = collect($poll->getTallies())->map(function ($tally, $key) use ($ci) {
             return $ci == $key ? $tally + 1 : $tally;
         })->toArray();
         $poll->save();
@@ -328,15 +324,15 @@ class StoryComposeController extends Controller
 
     public function storeReport(Request $request)
     {
-        abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
+        abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
 
         $this->validate($request, [
-            'type'  => 'required|alpha_dash',
-            'id'    => 'required|integer|min:1',
+            'type' => 'required|alpha_dash',
+            'id' => 'required|integer|min:1',
         ]);
 
         $user = $request->user();
-        abort_if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action');
+        abort_if($user->has_roles && ! UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action');
 
         $pid = $request->user()->profile_id;
         $sid = $request->input('id');
@@ -353,24 +349,24 @@ class StoryComposeController extends Controller
             'copyright',
             'impersonation',
             'scam',
-            'terrorism'
+            'terrorism',
         ];
 
-        abort_if(!in_array($type, $types), 422, 'Invalid story report type');
+        abort_if(! in_array($type, $types), 422, 'Invalid story report type');
 
         $story = Story::findOrFail($sid);
 
         abort_if($story->profile_id == $pid, 422, 'Cannot report your own story');
-        abort_if(!FollowerService::follows($pid, $story->profile_id), 422, 'Cannot report a story from an account you do not follow');
+        abort_if(! FollowerService::follows($pid, $story->profile_id), 422, 'Cannot report a story from an account you do not follow');
 
-        if( Report::whereProfileId($pid)
+        if (Report::whereProfileId($pid)
             ->whereObjectType('App\Story')
             ->whereObjectId($story->id)
             ->exists()
         ) {
             return response()->json(['error' => [
                 'code' => 409,
-                'message' => 'Cannot report the same story again'
+                'message' => 'Cannot report the same story again',
             ]], 409);
         }
 
@@ -389,18 +385,18 @@ class StoryComposeController extends Controller
 
     public function react(Request $request)
     {
-        abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
+        abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
         $this->validate($request, [
             'sid' => 'required',
-            'reaction' => 'required|string'
+            'reaction' => 'required|string',
         ]);
         $pid = $request->user()->profile_id;
         $text = $request->input('reaction');
         $user = $request->user();
-        abort_if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action');
+        abort_if($user->has_roles && ! UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action');
         $story = Story::findOrFail($request->input('sid'));
 
-        abort_if(!$story->can_react, 422);
+        abort_if(! $story->can_react, 422);
         abort_if(StoryService::reactCounter($story->id, $pid) >= 5, 422, 'You have already reacted to this story');
 
         $status = new Status;
@@ -413,7 +409,7 @@ class StoryComposeController extends Controller
         $status->in_reply_to_profile_id = $story->profile_id;
         $status->entities = json_encode([
             'story_id' => $story->id,
-            'reaction' => $text
+            'reaction' => $text,
         ]);
         $status->save();
 
@@ -427,24 +423,24 @@ class StoryComposeController extends Controller
             'story_actor_username' => $request->user()->username,
             'story_id' => $story->id,
             'story_media_url' => url(Storage::url($story->path)),
-            'reaction' => $text
+            'reaction' => $text,
         ]);
         $dm->save();
 
         Conversation::updateOrInsert(
             [
                 'to_id' => $story->profile_id,
-                'from_id' => $pid
+                'from_id' => $pid,
             ],
             [
                 'type' => 'story:react',
                 'status_id' => $status->id,
                 'dm_id' => $dm->id,
-                'is_hidden' => false
+                'is_hidden' => false,
             ]
         );
 
-        if($story->local) {
+        if ($story->local) {
             // generate notification
             $n = new Notification;
             $n->profile_id = $dm->to_id;
@@ -464,18 +460,18 @@ class StoryComposeController extends Controller
 
     public function comment(Request $request)
     {
-        abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
+        abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
         $this->validate($request, [
             'sid' => 'required',
-            'caption' => 'required|string'
+            'caption' => 'required|string',
         ]);
         $pid = $request->user()->profile_id;
         $text = $request->input('caption');
         $user = $request->user();
-        abort_if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action');
+        abort_if($user->has_roles && ! UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action');
         $story = Story::findOrFail($request->input('sid'));
 
-        abort_if(!$story->can_reply, 422);
+        abort_if(! $story->can_reply, 422);
 
         $status = new Status;
         $status->type = 'story:reply';
@@ -486,7 +482,7 @@ class StoryComposeController extends Controller
         $status->visibility = 'direct';
         $status->in_reply_to_profile_id = $story->profile_id;
         $status->entities = json_encode([
-            'story_id' => $story->id
+            'story_id' => $story->id,
         ]);
         $status->save();
 
@@ -500,24 +496,24 @@ class StoryComposeController extends Controller
             'story_actor_username' => $request->user()->username,
             'story_id' => $story->id,
             'story_media_url' => url(Storage::url($story->path)),
-            'caption' => $text
+            'caption' => $text,
         ]);
         $dm->save();
 
         Conversation::updateOrInsert(
             [
                 'to_id' => $story->profile_id,
-                'from_id' => $pid
+                'from_id' => $pid,
             ],
             [
                 'type' => 'story:comment',
                 'status_id' => $status->id,
                 'dm_id' => $dm->id,
-                'is_hidden' => false
+                'is_hidden' => false,
             ]
         );
 
-        if($story->local) {
+        if ($story->local) {
             // generate notification
             $n = new Notification;
             $n->profile_id = $dm->to_id;

+ 9 - 9
app/Http/Controllers/StoryController.php

@@ -34,7 +34,7 @@ class StoryController extends StoryComposeController
 {
     public function recent(Request $request)
     {
-        abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
+        abort_if(!(bool) config_cache('instance.stories.enabled') || !$request->user(), 404);
         $user = $request->user();
         if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id)) {
             return [];
@@ -117,7 +117,7 @@ class StoryController extends StoryComposeController
 
     public function profile(Request $request, $id)
     {
-        abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
+        abort_if(!(bool) config_cache('instance.stories.enabled') || !$request->user(), 404);
 
         $user = $request->user();
         if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id)) {
@@ -176,7 +176,7 @@ class StoryController extends StoryComposeController
 
     public function viewed(Request $request)
     {
-        abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
+        abort_if(!(bool) config_cache('instance.stories.enabled') || !$request->user(), 404);
 
         $this->validate($request, [
             'id'    => 'required|min:1',
@@ -221,7 +221,7 @@ class StoryController extends StoryComposeController
 
     public function exists(Request $request, $id)
     {
-        abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
+        abort_if(!(bool) config_cache('instance.stories.enabled') || !$request->user(), 404);
         $user = $request->user();
         if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id)) {
             return response()->json(false);
@@ -233,7 +233,7 @@ class StoryController extends StoryComposeController
 
     public function iRedirect(Request $request)
     {
-        abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
+        abort_if(!(bool) config_cache('instance.stories.enabled') || !$request->user(), 404);
 
         $user = $request->user();
         abort_if(!$user, 404);
@@ -243,7 +243,7 @@ class StoryController extends StoryComposeController
 
     public function viewers(Request $request)
     {
-        abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
+        abort_if(!(bool) config_cache('instance.stories.enabled') || !$request->user(), 404);
 
         $this->validate($request, [
             'sid' => 'required|string'
@@ -274,7 +274,7 @@ class StoryController extends StoryComposeController
 
     public function remoteStory(Request $request, $id)
     {
-        abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
+        abort_if(!(bool) config_cache('instance.stories.enabled') || !$request->user(), 404);
 
         $profile = Profile::findOrFail($id);
         if($profile->user_id != null || $profile->domain == null) {
@@ -286,7 +286,7 @@ class StoryController extends StoryComposeController
 
     public function pollResults(Request $request)
     {
-        abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
+        abort_if(!(bool) config_cache('instance.stories.enabled') || !$request->user(), 404);
 
         $this->validate($request, [
             'sid' => 'required|string'
@@ -304,7 +304,7 @@ class StoryController extends StoryComposeController
 
     public function getActivityObject(Request $request, $username, $id)
     {
-        abort_if(!config_cache('instance.stories.enabled'), 404);
+        abort_if(!(bool) config_cache('instance.stories.enabled'), 404);
 
         if(!$request->wantsJson()) {
             return redirect('/stories/' . $username);

+ 1 - 1
app/Http/Controllers/UserEmailForgotController.php

@@ -34,7 +34,7 @@ class UserEmailForgotController extends Controller
             'username.exists' => 'This username is no longer active or does not exist!'
         ];
 
-        if(config('captcha.enabled') || config('captcha.active.login') || config('captcha.active.register')) {
+        if((bool) config_cache('captcha.enabled')) {
             $rules['h-captcha-response'] = 'required|captcha';
             $messages['h-captcha-response.required'] = 'You need to complete the captcha!';
         }

+ 32 - 31
app/Http/Requests/Status/StoreStatusEditRequest.php

@@ -2,10 +2,10 @@
 
 namespace App\Http\Requests\Status;
 
-use Illuminate\Foundation\Http\FormRequest;
 use App\Media;
 use App\Status;
 use Closure;
+use Illuminate\Foundation\Http\FormRequest;
 
 class StoreStatusEditRequest extends FormRequest
 {
@@ -14,24 +14,25 @@ class StoreStatusEditRequest extends FormRequest
      */
     public function authorize(): bool
     {
-    	$profile = $this->user()->profile;
-    	if($profile->status != null) {
-    		return false;
-    	}
-    	if($profile->unlisted == true && $profile->cw == true) {
-    		return false;
-    	}
-    	$types = [
-			"photo",
-			"photo:album",
-			"photo:video:album",
-			"reply",
-			"text",
-			"video",
-			"video:album"
-    	];
-    	$scopes = ['public', 'unlisted', 'private'];
-    	$status = Status::whereNull('reblog_of_id')->whereIn('type', $types)->whereIn('scope', $scopes)->find($this->route('id'));
+        $profile = $this->user()->profile;
+        if ($profile->status != null) {
+            return false;
+        }
+        if ($profile->unlisted == true && $profile->cw == true) {
+            return false;
+        }
+        $types = [
+            'photo',
+            'photo:album',
+            'photo:video:album',
+            'reply',
+            'text',
+            'video',
+            'video:album',
+        ];
+        $scopes = ['public', 'unlisted', 'private'];
+        $status = Status::whereNull('reblog_of_id')->whereIn('type', $types)->whereIn('scope', $scopes)->find($this->route('id'));
+
         return $status && $this->user()->profile_id === $status->profile_id;
     }
 
@@ -47,18 +48,18 @@ class StoreStatusEditRequest extends FormRequest
             'spoiler_text' => 'nullable|string|max:140',
             'sensitive' => 'sometimes|boolean',
             'media_ids' => [
-            	'nullable',
-            	'required_without:status',
-            	'array',
-            	'max:' . config('pixelfed.max_album_length'),
-				function (string $attribute, mixed $value, Closure $fail) {
-					Media::whereProfileId($this->user()->profile_id)
-						->where(function($query) {
-							return $query->whereNull('status_id')
-							->orWhere('status_id', '=', $this->route('id'));
-						})
-						->findOrFail($value);
-				},
+                'nullable',
+                'required_without:status',
+                'array',
+                'max:'.(int) config_cache('pixelfed.max_album_length'),
+                function (string $attribute, mixed $value, Closure $fail) {
+                    Media::whereProfileId($this->user()->profile_id)
+                        ->where(function ($query) {
+                            return $query->whereNull('status_id')
+                                ->orWhere('status_id', '=', $this->route('id'));
+                        })
+                        ->findOrFail($value);
+                },
             ],
             'location' => 'sometimes|nullable',
             'location.id' => 'sometimes|integer|min:1|max:128769',

+ 76 - 76
app/Jobs/AvatarPipeline/AvatarOptimize.php

@@ -2,9 +2,9 @@
 
 namespace App\Jobs\AvatarPipeline;
 
-use Cache;
 use App\Avatar;
 use App\Profile;
+use Cache;
 use Carbon\Carbon;
 use Illuminate\Bus\Queueable;
 use Illuminate\Contracts\Queue\ShouldQueue;
@@ -17,88 +17,88 @@ use Storage;
 
 class AvatarOptimize implements ShouldQueue
 {
-	use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+    protected $profile;
 
-	protected $profile;
-	protected $current;
+    protected $current;
 
-	/**
-	 * Delete the job if its models no longer exist.
-	 *
-	 * @var bool
-	 */
-	public $deleteWhenMissingModels = true;
+    /**
+     * Delete the job if its models no longer exist.
+     *
+     * @var bool
+     */
+    public $deleteWhenMissingModels = true;
 
-	/**
-	 * Create a new job instance.
-	 *
-	 * @return void
-	 */
-	public function __construct(Profile $profile, $current)
-	{
-		$this->profile = $profile;
-		$this->current = $current;
-	}
+    /**
+     * Create a new job instance.
+     *
+     * @return void
+     */
+    public function __construct(Profile $profile, $current)
+    {
+        $this->profile = $profile;
+        $this->current = $current;
+    }
 
-	/**
-	 * Execute the job.
-	 *
-	 * @return void
-	 */
-	public function handle()
-	{
-		$avatar = $this->profile->avatar;
-		$file = storage_path("app/$avatar->media_path");
+    /**
+     * Execute the job.
+     *
+     * @return void
+     */
+    public function handle()
+    {
+        $avatar = $this->profile->avatar;
+        $file = storage_path("app/$avatar->media_path");
 
-		try {
-			$img = Intervention::make($file)->orientate();
-			$img->fit(200, 200, function ($constraint) {
-				$constraint->upsize();
-			});
-			$quality = config_cache('pixelfed.image_quality');
-			$img->save($file, $quality);
+        try {
+            $img = Intervention::make($file)->orientate();
+            $img->fit(200, 200, function ($constraint) {
+                $constraint->upsize();
+            });
+            $quality = config_cache('pixelfed.image_quality');
+            $img->save($file, $quality);
 
-			$avatar = Avatar::whereProfileId($this->profile->id)->firstOrFail();
-			$avatar->change_count = ++$avatar->change_count;
-			$avatar->last_processed_at = Carbon::now();
-			$avatar->save();
-			Cache::forget('avatar:' . $avatar->profile_id);
-			$this->deleteOldAvatar($avatar->media_path, $this->current);
+            $avatar = Avatar::whereProfileId($this->profile->id)->firstOrFail();
+            $avatar->change_count = ++$avatar->change_count;
+            $avatar->last_processed_at = Carbon::now();
+            $avatar->save();
+            Cache::forget('avatar:'.$avatar->profile_id);
+            $this->deleteOldAvatar($avatar->media_path, $this->current);
 
-			if(config_cache('pixelfed.cloud_storage') && config('instance.avatar.local_to_cloud')) {
-				$this->uploadToCloud($avatar);
-			} else {
-				$avatar->cdn_url = null;
-				$avatar->save();
-			}
-		} catch (Exception $e) {
-		}
-	}
+            if ((bool) config_cache('pixelfed.cloud_storage') && (bool) config_cache('instance.avatar.local_to_cloud')) {
+                $this->uploadToCloud($avatar);
+            } else {
+                $avatar->cdn_url = null;
+                $avatar->save();
+            }
+        } catch (Exception $e) {
+        }
+    }
 
-	protected function deleteOldAvatar($new, $current)
-	{
-		if ( storage_path('app/'.$new) == $current ||
-			 Str::endsWith($current, 'avatars/default.png') ||
-			 Str::endsWith($current, 'avatars/default.jpg'))
-		{
-			return;
-		}
-		if (is_file($current)) {
-			@unlink($current);
-		}
-	}
+    protected function deleteOldAvatar($new, $current)
+    {
+        if (storage_path('app/'.$new) == $current ||
+             Str::endsWith($current, 'avatars/default.png') ||
+             Str::endsWith($current, 'avatars/default.jpg')) {
+            return;
+        }
+        if (is_file($current)) {
+            @unlink($current);
+        }
+    }
 
-	protected function uploadToCloud($avatar)
-	{
-		$base = 'cache/avatars/' . $avatar->profile_id;
-		$disk = Storage::disk(config('filesystems.cloud'));
-		$disk->deleteDirectory($base);
-		$path = $base . '/' . 'avatar_' . strtolower(Str::random(random_int(3,6))) . $avatar->change_count . '.' . pathinfo($avatar->media_path, PATHINFO_EXTENSION);
-		$url = $disk->put($path, Storage::get($avatar->media_path));
-		$avatar->media_path = $path;
-		$avatar->cdn_url = $disk->url($path);
-		$avatar->save();
-		Storage::delete($avatar->media_path);
-		Cache::forget('avatar:' . $avatar->profile_id);
-	}
+    protected function uploadToCloud($avatar)
+    {
+        $base = 'cache/avatars/'.$avatar->profile_id;
+        $disk = Storage::disk(config('filesystems.cloud'));
+        $disk->deleteDirectory($base);
+        $path = $base.'/'.'avatar_'.strtolower(Str::random(random_int(3, 6))).$avatar->change_count.'.'.pathinfo($avatar->media_path, PATHINFO_EXTENSION);
+        $url = $disk->put($path, Storage::get($avatar->media_path));
+        $avatar->media_path = $path;
+        $avatar->cdn_url = $disk->url($path);
+        $avatar->save();
+        Storage::delete($avatar->media_path);
+        Cache::forget('avatar:'.$avatar->profile_id);
+    }
 }

+ 95 - 100
app/Jobs/AvatarPipeline/RemoteAvatarFetch.php

@@ -4,112 +4,107 @@ namespace App\Jobs\AvatarPipeline;
 
 use App\Avatar;
 use App\Profile;
+use App\Services\MediaStorageService;
+use App\Util\ActivityPub\Helpers;
 use Illuminate\Bus\Queueable;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Foundation\Bus\Dispatchable;
 use Illuminate\Queue\InteractsWithQueue;
 use Illuminate\Queue\SerializesModels;
-use App\Util\ActivityPub\Helpers;
-use Illuminate\Support\Str;
-use Zttp\Zttp;
-use App\Http\Controllers\AvatarController;
-use Storage;
-use Log;
-use Illuminate\Http\File;
-use App\Services\MediaStorageService;
-use App\Services\ActivityPubFetchService;
 
 class RemoteAvatarFetch implements ShouldQueue
 {
-	use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
-
-	protected $profile;
-
-	/**
-	* Delete the job if its models no longer exist.
-	*
-	* @var bool
-	*/
-	public $deleteWhenMissingModels = true;
-
-	/**
-	 * The number of times the job may be attempted.
-	 *
-	 * @var int
-	 */
-	public $tries = 1;
-	public $timeout = 300;
-	public $maxExceptions = 1;
-
-	/**
-	* Create a new job instance.
-	*
-	* @return void
-	*/
-	public function __construct(Profile $profile)
-	{
-		$this->profile = $profile;
-	}
-
-	/**
-	* Execute the job.
-	*
-	* @return void
-	*/
-	public function handle()
-	{
-		$profile = $this->profile;
-
-		if(boolval(config_cache('pixelfed.cloud_storage')) == false && boolval(config_cache('federation.avatars.store_local')) == false) {
-			return 1;
-		}
-
-		if($profile->domain == null || $profile->private_key) {
-			return 1;
-		}
-
-		$avatar = Avatar::whereProfileId($profile->id)->first();
-
-		if(!$avatar) {
-			$avatar = new Avatar;
-			$avatar->profile_id = $profile->id;
-			$avatar->save();
-		}
-
-		if($avatar->media_path == null && $avatar->remote_url == null) {
-			$avatar->media_path = 'public/avatars/default.jpg';
-			$avatar->is_remote = true;
-			$avatar->save();
-		}
-
-		$person = Helpers::fetchFromUrl($profile->remote_url);
-
-		if(!$person || !isset($person['@context'])) {
-			return 1;
-		}
-
-		if( !isset($person['icon']) ||
-			!isset($person['icon']['type']) ||
-			!isset($person['icon']['url'])
-		) {
-			return 1;
-		}
-
-		if($person['icon']['type'] !== 'Image') {
-			return 1;
-		}
-
-		if(!Helpers::validateUrl($person['icon']['url'])) {
-			return 1;
-		}
-
-		$icon = $person['icon'];
-
-		$avatar->remote_url = $icon['url'];
-		$avatar->save();
-
-		MediaStorageService::avatar($avatar, boolval(config_cache('pixelfed.cloud_storage')) == false, true);
-
-		return 1;
-	}
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+    protected $profile;
+
+    /**
+     * Delete the job if its models no longer exist.
+     *
+     * @var bool
+     */
+    public $deleteWhenMissingModels = true;
+
+    /**
+     * The number of times the job may be attempted.
+     *
+     * @var int
+     */
+    public $tries = 1;
+
+    public $timeout = 300;
+
+    public $maxExceptions = 1;
+
+    /**
+     * Create a new job instance.
+     *
+     * @return void
+     */
+    public function __construct(Profile $profile)
+    {
+        $this->profile = $profile;
+    }
+
+    /**
+     * Execute the job.
+     *
+     * @return void
+     */
+    public function handle()
+    {
+        $profile = $this->profile;
+
+        if ((bool) config_cache('pixelfed.cloud_storage') == false && (bool) config_cache('federation.avatars.store_local') == false) {
+            return 1;
+        }
+
+        if ($profile->domain == null || $profile->private_key) {
+            return 1;
+        }
+
+        $avatar = Avatar::whereProfileId($profile->id)->first();
+
+        if (! $avatar) {
+            $avatar = new Avatar;
+            $avatar->profile_id = $profile->id;
+            $avatar->save();
+        }
+
+        if ($avatar->media_path == null && $avatar->remote_url == null) {
+            $avatar->media_path = 'public/avatars/default.jpg';
+            $avatar->is_remote = true;
+            $avatar->save();
+        }
+
+        $person = Helpers::fetchFromUrl($profile->remote_url);
+
+        if (! $person || ! isset($person['@context'])) {
+            return 1;
+        }
+
+        if (! isset($person['icon']) ||
+            ! isset($person['icon']['type']) ||
+            ! isset($person['icon']['url'])
+        ) {
+            return 1;
+        }
+
+        if ($person['icon']['type'] !== 'Image') {
+            return 1;
+        }
+
+        if (! Helpers::validateUrl($person['icon']['url'])) {
+            return 1;
+        }
+
+        $icon = $person['icon'];
+
+        $avatar->remote_url = $icon['url'];
+        $avatar->save();
+
+        MediaStorageService::avatar($avatar, (bool) config_cache('pixelfed.cloud_storage') == false, true);
+
+        return 1;
+    }
 }

+ 76 - 81
app/Jobs/AvatarPipeline/RemoteAvatarFetchFromUrl.php

@@ -4,93 +4,88 @@ namespace App\Jobs\AvatarPipeline;
 
 use App\Avatar;
 use App\Profile;
+use App\Services\AccountService;
+use App\Services\MediaStorageService;
+use Cache;
 use Illuminate\Bus\Queueable;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Foundation\Bus\Dispatchable;
 use Illuminate\Queue\InteractsWithQueue;
 use Illuminate\Queue\SerializesModels;
-use App\Util\ActivityPub\Helpers;
-use Illuminate\Support\Str;
-use Zttp\Zttp;
-use App\Http\Controllers\AvatarController;
-use Cache;
-use Storage;
-use Log;
-use Illuminate\Http\File;
-use App\Services\AccountService;
-use App\Services\MediaStorageService;
-use App\Services\ActivityPubFetchService;
 
 class RemoteAvatarFetchFromUrl implements ShouldQueue
 {
-	use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
-
-	protected $profile;
-	protected $url;
-
-	/**
-	* Delete the job if its models no longer exist.
-	*
-	* @var bool
-	*/
-	public $deleteWhenMissingModels = true;
-
-	/**
-	 * The number of times the job may be attempted.
-	 *
-	 * @var int
-	 */
-	public $tries = 1;
-	public $timeout = 300;
-	public $maxExceptions = 1;
-
-	/**
-	* Create a new job instance.
-	*
-	* @return void
-	*/
-	public function __construct(Profile $profile, $url)
-	{
-		$this->profile = $profile;
-		$this->url = $url;
-	}
-
-	/**
-	* Execute the job.
-	*
-	* @return void
-	*/
-	public function handle()
-	{
-		$profile = $this->profile;
-
-		Cache::forget('avatar:' . $profile->id);
-		AccountService::del($profile->id);
-
-		if(boolval(config_cache('pixelfed.cloud_storage')) == false && boolval(config_cache('federation.avatars.store_local')) == false) {
-			return 1;
-		}
-
-		if($profile->domain == null || $profile->private_key) {
-			return 1;
-		}
-
-		$avatar = Avatar::whereProfileId($profile->id)->first();
-
-		if(!$avatar) {
-			$avatar = new Avatar;
-			$avatar->profile_id = $profile->id;
-			$avatar->is_remote = true;
-			$avatar->remote_url = $this->url;
-			$avatar->save();
-		} else {
-			$avatar->remote_url = $this->url;
-			$avatar->is_remote = true;
-			$avatar->save();
-		}
-
-		MediaStorageService::avatar($avatar, boolval(config_cache('pixelfed.cloud_storage')) == false, true);
-
-		return 1;
-	}
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+    protected $profile;
+
+    protected $url;
+
+    /**
+     * Delete the job if its models no longer exist.
+     *
+     * @var bool
+     */
+    public $deleteWhenMissingModels = true;
+
+    /**
+     * The number of times the job may be attempted.
+     *
+     * @var int
+     */
+    public $tries = 1;
+
+    public $timeout = 300;
+
+    public $maxExceptions = 1;
+
+    /**
+     * Create a new job instance.
+     *
+     * @return void
+     */
+    public function __construct(Profile $profile, $url)
+    {
+        $this->profile = $profile;
+        $this->url = $url;
+    }
+
+    /**
+     * Execute the job.
+     *
+     * @return void
+     */
+    public function handle()
+    {
+        $profile = $this->profile;
+
+        Cache::forget('avatar:'.$profile->id);
+        AccountService::del($profile->id);
+
+        if ((bool) config_cache('pixelfed.cloud_storage') == false && (bool) config_cache('federation.avatars.store_local') == false) {
+            return 1;
+        }
+
+        if ($profile->domain == null || $profile->private_key) {
+            return 1;
+        }
+
+        $avatar = Avatar::whereProfileId($profile->id)->first();
+
+        if (! $avatar) {
+            $avatar = new Avatar;
+            $avatar->profile_id = $profile->id;
+            $avatar->is_remote = true;
+            $avatar->remote_url = $this->url;
+            $avatar->save();
+        } else {
+            $avatar->remote_url = $this->url;
+            $avatar->is_remote = true;
+            $avatar->save();
+        }
+
+        MediaStorageService::avatar($avatar, (bool) config_cache('pixelfed.cloud_storage') == false, true);
+
+        return 1;
+    }
 }

+ 1 - 1
app/Jobs/ImageOptimizePipeline/ImageOptimize.php

@@ -45,7 +45,7 @@ class ImageOptimize implements ShouldQueue
             return;
         }
 
-        if(config('pixelfed.optimize_image') == false) {
+        if((bool) config_cache('pixelfed.optimize_image') == false) {
         	ImageThumbnail::dispatch($media)->onQueue('mmo');
     		return;
     	} else {

+ 1 - 1
app/Jobs/ImageOptimizePipeline/ImageResize.php

@@ -51,7 +51,7 @@ class ImageResize implements ShouldQueue
             return;
         }
 
-        if(!config('pixelfed.optimize_image')) {
+        if((bool) config_cache('pixelfed.optimize_image') === false) {
         	ImageThumbnail::dispatch($media)->onQueue('mmo');
         	return;
         }

+ 1 - 1
app/Jobs/ImageOptimizePipeline/ImageUpdate.php

@@ -61,7 +61,7 @@ class ImageUpdate implements ShouldQueue
 			return;
 		}
 
-		if(config('pixelfed.optimize_image')) {
+		if((bool) config_cache('pixelfed.optimize_image')) {
 			if (in_array($media->mime, $this->protectedMimes) == true) {
 				ImageOptimizer::optimize($thumb);
 				if(!$media->skip_optimize) {

+ 49 - 46
app/Jobs/MediaPipeline/MediaDeletePipeline.php

@@ -3,27 +3,30 @@
 namespace App\Jobs\MediaPipeline;
 
 use App\Media;
+use App\Services\Media\MediaHlsService;
 use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Foundation\Bus\Dispatchable;
 use Illuminate\Queue\InteractsWithQueue;
+use Illuminate\Queue\Middleware\WithoutOverlapping;
 use Illuminate\Queue\SerializesModels;
-use Illuminate\Support\Facades\Redis;
 use Illuminate\Support\Facades\Storage;
-use App\Services\Media\MediaHlsService;
-use Illuminate\Queue\Middleware\WithoutOverlapping;
-use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
 
-class MediaDeletePipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing
+class MediaDeletePipeline implements ShouldBeUniqueUntilProcessing, ShouldQueue
 {
-	use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
 
-	protected $media;
+    protected $media;
 
     public $timeout = 300;
+
     public $tries = 3;
+
     public $maxExceptions = 1;
+
     public $failOnTimeout = true;
+
     public $deleteWhenMissingModels = true;
 
     /**
@@ -38,7 +41,7 @@ class MediaDeletePipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing
      */
     public function uniqueId(): string
     {
-        return 'media:purge-job:id-' . $this->media->id;
+        return 'media:purge-job:id-'.$this->media->id;
     }
 
     /**
@@ -51,58 +54,58 @@ class MediaDeletePipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing
         return [(new WithoutOverlapping("media:purge-job:id-{$this->media->id}"))->shared()->dontRelease()];
     }
 
-	public function __construct(Media $media)
-	{
-		$this->media = $media;
-	}
+    public function __construct(Media $media)
+    {
+        $this->media = $media;
+    }
 
-	public function handle()
-	{
-		$media = $this->media;
-		$path = $media->media_path;
-		$thumb = $media->thumbnail_path;
+    public function handle()
+    {
+        $media = $this->media;
+        $path = $media->media_path;
+        $thumb = $media->thumbnail_path;
 
-		if(!$path) {
-			return 1;
-		}
+        if (! $path) {
+            return 1;
+        }
 
-		$e = explode('/', $path);
-		array_pop($e);
-		$i = implode('/', $e);
+        $e = explode('/', $path);
+        array_pop($e);
+        $i = implode('/', $e);
 
-		if(config_cache('pixelfed.cloud_storage') == true) {
-			$disk = Storage::disk(config('filesystems.cloud'));
+        if ((bool) config_cache('pixelfed.cloud_storage') == true) {
+            $disk = Storage::disk(config('filesystems.cloud'));
 
-			if($path && $disk->exists($path)) {
-				$disk->delete($path);
-			}
+            if ($path && $disk->exists($path)) {
+                $disk->delete($path);
+            }
 
-			if($thumb && $disk->exists($thumb)) {
-				$disk->delete($thumb);
-			}
-		}
+            if ($thumb && $disk->exists($thumb)) {
+                $disk->delete($thumb);
+            }
+        }
 
-		$disk = Storage::disk(config('filesystems.local'));
+        $disk = Storage::disk(config('filesystems.local'));
 
-		if($path && $disk->exists($path)) {
-			$disk->delete($path);
-		}
+        if ($path && $disk->exists($path)) {
+            $disk->delete($path);
+        }
 
-		if($thumb && $disk->exists($thumb)) {
-			$disk->delete($thumb);
-		}
+        if ($thumb && $disk->exists($thumb)) {
+            $disk->delete($thumb);
+        }
 
-		if($media->hls_path != null) {
+        if ($media->hls_path != null) {
             $files = MediaHlsService::allFiles($media);
-            if($files && count($files)) {
-                foreach($files as $file) {
+            if ($files && count($files)) {
+                foreach ($files as $file) {
                     $disk->delete($file);
                 }
             }
-		}
+        }
 
-		$media->delete();
+        $media->delete();
 
-		return 1;
-	}
+        return 1;
+    }
 }

+ 61 - 60
app/Jobs/MediaPipeline/MediaFixLocalFilesystemCleanupPipeline.php

@@ -8,68 +8,69 @@ use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Foundation\Bus\Dispatchable;
 use Illuminate\Queue\InteractsWithQueue;
 use Illuminate\Queue\SerializesModels;
-use Illuminate\Support\Facades\Redis;
 use Illuminate\Support\Facades\Storage;
 
 class MediaFixLocalFilesystemCleanupPipeline implements ShouldQueue
 {
-	use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
-
-	public $timeout = 1800;
-	public $tries = 5;
-	public $maxExceptions = 1;
-
-	public function handle()
-	{
-		if(config_cache('pixelfed.cloud_storage') == false) {
-			// Only run if cloud storage is enabled
-			return;
-		}
-
-		$disk = Storage::disk('local');
-		$cloud = Storage::disk(config('filesystems.cloud'));
-
-		Media::whereNotNull(['status_id', 'cdn_url', 'replicated_at'])
-		->chunk(20, function ($medias) use($disk, $cloud) {
-			foreach($medias as $media) {
-				if(!str_starts_with($media->media_path, 'public')) {
-					continue;
-				}
-
-				if($disk->exists($media->media_path) && $cloud->exists($media->media_path)) {
-					$disk->delete($media->media_path);
-				}
-
-				if($media->thumbnail_path) {
-					if($disk->exists($media->thumbnail_path)) {
-						$disk->delete($media->thumbnail_path);
-					}
-				}
-
-				$paths = explode('/', $media->media_path);
-				if(count($paths) === 7) {
-					array_pop($paths);
-					$baseDir = implode('/', $paths);
-
-					if(count($disk->allFiles($baseDir)) === 0) {
-						$disk->deleteDirectory($baseDir);
-
-						array_pop($paths);
-						$baseDir = implode('/', $paths);
-
-						if(count($disk->allFiles($baseDir)) === 0) {
-							$disk->deleteDirectory($baseDir);
-
-							array_pop($paths);
-							$baseDir = implode('/', $paths);
-
-							if(count($disk->allFiles($baseDir)) === 0) {
-								$disk->deleteDirectory($baseDir);
-							}
-						}
-					}
-				}
-			}
-		});
-	}
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+    public $timeout = 1800;
+
+    public $tries = 5;
+
+    public $maxExceptions = 1;
+
+    public function handle()
+    {
+        if ((bool) config_cache('pixelfed.cloud_storage') == false) {
+            // Only run if cloud storage is enabled
+            return;
+        }
+
+        $disk = Storage::disk('local');
+        $cloud = Storage::disk(config('filesystems.cloud'));
+
+        Media::whereNotNull(['status_id', 'cdn_url', 'replicated_at'])
+            ->chunk(20, function ($medias) use ($disk, $cloud) {
+                foreach ($medias as $media) {
+                    if (! str_starts_with($media->media_path, 'public')) {
+                        continue;
+                    }
+
+                    if ($disk->exists($media->media_path) && $cloud->exists($media->media_path)) {
+                        $disk->delete($media->media_path);
+                    }
+
+                    if ($media->thumbnail_path) {
+                        if ($disk->exists($media->thumbnail_path)) {
+                            $disk->delete($media->thumbnail_path);
+                        }
+                    }
+
+                    $paths = explode('/', $media->media_path);
+                    if (count($paths) === 7) {
+                        array_pop($paths);
+                        $baseDir = implode('/', $paths);
+
+                        if (count($disk->allFiles($baseDir)) === 0) {
+                            $disk->deleteDirectory($baseDir);
+
+                            array_pop($paths);
+                            $baseDir = implode('/', $paths);
+
+                            if (count($disk->allFiles($baseDir)) === 0) {
+                                $disk->deleteDirectory($baseDir);
+
+                                array_pop($paths);
+                                $baseDir = implode('/', $paths);
+
+                                if (count($disk->allFiles($baseDir)) === 0) {
+                                    $disk->deleteDirectory($baseDir);
+                                }
+                            }
+                        }
+                    }
+                }
+            });
+    }
 }

+ 2 - 4
app/Jobs/RemoteFollowPipeline/RemoteFollowImportRecent.php

@@ -17,6 +17,7 @@ use Log;
 use Storage;
 use Zttp\Zttp;
 use App\Util\ActivityPub\Helpers;
+use App\Services\MediaPathService;
 
 class RemoteFollowImportRecent implements ShouldQueue
 {
@@ -45,7 +46,6 @@ class RemoteFollowImportRecent implements ShouldQueue
             'image/jpg',
             'image/jpeg',
             'image/png',
-            'image/gif',
         ];
     }
 
@@ -208,9 +208,7 @@ class RemoteFollowImportRecent implements ShouldQueue
     public function importMedia($url, $mime, $status)
     {
         $user = $this->profile;
-        $monthHash = hash('sha1', date('Y').date('m'));
-        $userHash = hash('sha1', $user->id.(string) $user->created_at);
-        $storagePath = "public/m/{$monthHash}/{$userHash}";
+        $storagePath = MediaPathService::get($user, 2);
 
         try {
             $info = pathinfo($url);

+ 136 - 135
app/Jobs/SharePipeline/SharePipeline.php

@@ -2,9 +2,15 @@
 
 namespace App\Jobs\SharePipeline;
 
-use Cache, Log;
-use Illuminate\Support\Facades\Redis;
-use App\{Status, Notification};
+use App\Jobs\HomeFeedPipeline\FeedInsertPipeline;
+use App\Notification;
+use App\Services\ReblogService;
+use App\Services\StatusService;
+use App\Status;
+use App\Transformer\ActivityPub\Verb\Announce;
+use App\Util\ActivityPub\HttpSignature;
+use GuzzleHttp\Client;
+use GuzzleHttp\Pool;
 use Illuminate\Bus\Queueable;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Foundation\Bus\Dispatchable;
@@ -12,141 +18,136 @@ use Illuminate\Queue\InteractsWithQueue;
 use Illuminate\Queue\SerializesModels;
 use League\Fractal;
 use League\Fractal\Serializer\ArraySerializer;
-use App\Transformer\ActivityPub\Verb\Announce;
-use GuzzleHttp\{Pool, Client, Promise};
-use App\Util\ActivityPub\HttpSignature;
-use App\Services\ReblogService;
-use App\Services\StatusService;
-use App\Jobs\HomeFeedPipeline\FeedInsertPipeline;
 
 class SharePipeline implements ShouldQueue
 {
-	use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
-
-	protected $status;
-
-	/**
-	 * Delete the job if its models no longer exist.
-	 *
-	 * @var bool
-	 */
-	public $deleteWhenMissingModels = true;
-
-	/**
-	 * Create a new job instance.
-	 *
-	 * @return void
-	 */
-	public function __construct(Status $status)
-	{
-		$this->status = $status;
-	}
-
-	/**
-	 * Execute the job.
-	 *
-	 * @return void
-	 */
-	public function handle()
-	{
-		$status = $this->status;
-		$parent = Status::find($this->status->reblog_of_id);
-        if(!$parent) {
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+    protected $status;
+
+    /**
+     * Delete the job if its models no longer exist.
+     *
+     * @var bool
+     */
+    public $deleteWhenMissingModels = true;
+
+    /**
+     * Create a new job instance.
+     *
+     * @return void
+     */
+    public function __construct(Status $status)
+    {
+        $this->status = $status;
+    }
+
+    /**
+     * Execute the job.
+     *
+     * @return void
+     */
+    public function handle()
+    {
+        $status = $this->status;
+        $parent = Status::find($this->status->reblog_of_id);
+        if (! $parent) {
             return;
         }
-		$actor = $status->profile;
-		$target = $parent->profile;
-
-		if ($status->uri !== null) {
-			// Ignore notifications to remote statuses
-			return;
-		}
-
-		if($target->id === $status->profile_id) {
-			$this->remoteAnnounceDeliver();
-			return true;
-		}
-
-		ReblogService::addPostReblog($parent->profile_id, $status->id);
-
-		$parent->reblogs_count = $parent->reblogs_count + 1;
-		$parent->save();
-		StatusService::del($parent->id);
-
-		Notification::firstOrCreate(
-			[
-				'profile_id' => $target->id,
-				'actor_id' => $actor->id,
-				'action' => 'share',
-				'item_type' => 'App\Status',
-				'item_id' => $status->reblog_of_id ?? $status->id,
-			]
-		);
-
-		FeedInsertPipeline::dispatch($status->id, $status->profile_id)->onQueue('feed');
-
-		return $this->remoteAnnounceDeliver();
-	}
-
-	public function remoteAnnounceDeliver()
-	{
-		if(config('app.env') !== 'production' || config_cache('federation.activitypub.enabled') == false) {
-			return true;
-		}
-		$status = $this->status;
-		$profile = $status->profile;
-
-		$fractal = new Fractal\Manager();
-		$fractal->setSerializer(new ArraySerializer());
-		$resource = new Fractal\Resource\Item($status, new Announce());
-		$activity = $fractal->createData($resource)->toArray();
-
-		$audience = $status->profile->getAudienceInbox();
-
-		if(empty($audience) || $status->scope != 'public') {
-			// Return on profiles with no remote followers
-			return;
-		}
-
-		$payload = json_encode($activity);
-
-		$client = new Client([
-			'timeout'  => config('federation.activitypub.delivery.timeout')
-		]);
-
-		$version = config('pixelfed.version');
-		$appUrl = config('app.url');
-		$userAgent = "(Pixelfed/{$version}; +{$appUrl})";
-
-		$requests = function($audience) use ($client, $activity, $profile, $payload, $userAgent) {
-			foreach($audience as $url) {
-				$headers = HttpSignature::sign($profile, $url, $activity, [
-					'Content-Type'	=> 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
-					'User-Agent'	=> $userAgent,
-				]);
-				yield function() use ($client, $url, $headers, $payload) {
-					return $client->postAsync($url, [
-						'curl' => [
-							CURLOPT_HTTPHEADER => $headers,
-							CURLOPT_POSTFIELDS => $payload,
-							CURLOPT_HEADER => true
-						]
-					]);
-				};
-			}
-		};
-
-		$pool = new Pool($client, $requests($audience), [
-			'concurrency' => config('federation.activitypub.delivery.concurrency'),
-			'fulfilled' => function ($response, $index) {
-			},
-			'rejected' => function ($reason, $index) {
-			}
-		]);
-
-		$promise = $pool->promise();
-
-		$promise->wait();
-
-	}
+        $actor = $status->profile;
+        $target = $parent->profile;
+
+        if ($status->uri !== null) {
+            // Ignore notifications to remote statuses
+            return;
+        }
+
+        if ($target->id === $status->profile_id) {
+            $this->remoteAnnounceDeliver();
+
+            return true;
+        }
+
+        ReblogService::addPostReblog($parent->profile_id, $status->id);
+
+        $parent->reblogs_count = $parent->reblogs_count + 1;
+        $parent->save();
+        StatusService::del($parent->id);
+
+        Notification::firstOrCreate(
+            [
+                'profile_id' => $target->id,
+                'actor_id' => $actor->id,
+                'action' => 'share',
+                'item_type' => 'App\Status',
+                'item_id' => $status->reblog_of_id ?? $status->id,
+            ]
+        );
+
+        FeedInsertPipeline::dispatch($status->id, $status->profile_id)->onQueue('feed');
+
+        return $this->remoteAnnounceDeliver();
+    }
+
+    public function remoteAnnounceDeliver()
+    {
+        if (config('app.env') !== 'production' || (bool) config_cache('federation.activitypub.enabled') == false) {
+            return true;
+        }
+        $status = $this->status;
+        $profile = $status->profile;
+
+        $fractal = new Fractal\Manager();
+        $fractal->setSerializer(new ArraySerializer());
+        $resource = new Fractal\Resource\Item($status, new Announce());
+        $activity = $fractal->createData($resource)->toArray();
+
+        $audience = $status->profile->getAudienceInbox();
+
+        if (empty($audience) || $status->scope != 'public') {
+            // Return on profiles with no remote followers
+            return;
+        }
+
+        $payload = json_encode($activity);
+
+        $client = new Client([
+            'timeout' => config('federation.activitypub.delivery.timeout'),
+        ]);
+
+        $version = config('pixelfed.version');
+        $appUrl = config('app.url');
+        $userAgent = "(Pixelfed/{$version}; +{$appUrl})";
+
+        $requests = function ($audience) use ($client, $activity, $profile, $payload, $userAgent) {
+            foreach ($audience as $url) {
+                $headers = HttpSignature::sign($profile, $url, $activity, [
+                    'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
+                    'User-Agent' => $userAgent,
+                ]);
+                yield function () use ($client, $url, $headers, $payload) {
+                    return $client->postAsync($url, [
+                        'curl' => [
+                            CURLOPT_HTTPHEADER => $headers,
+                            CURLOPT_POSTFIELDS => $payload,
+                            CURLOPT_HEADER => true,
+                        ],
+                    ]);
+                };
+            }
+        };
+
+        $pool = new Pool($client, $requests($audience), [
+            'concurrency' => config('federation.activitypub.delivery.concurrency'),
+            'fulfilled' => function ($response, $index) {
+            },
+            'rejected' => function ($reason, $index) {
+            },
+        ]);
+
+        $promise = $pool->promise();
+
+        $promise->wait();
+
+    }
 }

+ 126 - 123
app/Jobs/SharePipeline/UndoSharePipeline.php

@@ -2,9 +2,15 @@
 
 namespace App\Jobs\SharePipeline;
 
-use Cache, Log;
-use Illuminate\Support\Facades\Redis;
-use App\{Status, Notification};
+use App\Jobs\HomeFeedPipeline\FeedRemovePipeline;
+use App\Notification;
+use App\Services\ReblogService;
+use App\Services\StatusService;
+use App\Status;
+use App\Transformer\ActivityPub\Verb\UndoAnnounce;
+use App\Util\ActivityPub\HttpSignature;
+use GuzzleHttp\Client;
+use GuzzleHttp\Pool;
 use Illuminate\Bus\Queueable;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Foundation\Bus\Dispatchable;
@@ -12,128 +18,125 @@ use Illuminate\Queue\InteractsWithQueue;
 use Illuminate\Queue\SerializesModels;
 use League\Fractal;
 use League\Fractal\Serializer\ArraySerializer;
-use App\Transformer\ActivityPub\Verb\UndoAnnounce;
-use GuzzleHttp\{Pool, Client, Promise};
-use App\Util\ActivityPub\HttpSignature;
-use App\Services\ReblogService;
-use App\Services\StatusService;
-use App\Jobs\HomeFeedPipeline\FeedRemovePipeline;
 
 class UndoSharePipeline implements ShouldQueue
 {
-	use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
-	protected $status;
-	public $deleteWhenMissingModels = true;
-
-	public function __construct(Status $status)
-	{
-		$this->status = $status;
-	}
-
-	public function handle()
-	{
-		$status = $this->status;
-		$actor = $status->profile;
-		$parent = Status::find($status->reblog_of_id);
-
-		FeedRemovePipeline::dispatch($status->id, $status->profile_id)->onQueue('feed');
-
-		if($parent) {
-			$target = $parent->profile_id;
-			ReblogService::removePostReblog($parent->profile_id, $status->id);
-
-			if($parent->reblogs_count > 0) {
-				$parent->reblogs_count = $parent->reblogs_count - 1;
-				$parent->save();
-				StatusService::del($parent->id);
-			}
-
-			$notification = Notification::whereProfileId($target)
-				->whereActorId($status->profile_id)
-				->whereAction('share')
-				->whereItemId($status->reblog_of_id)
-				->whereItemType('App\Status')
-				->first();
-
-			if($notification) {
-				$notification->forceDelete();
-			}
-		}
-
-		if ($status->uri != null) {
-			return;
-		}
-
-		if(config('app.env') !== 'production' || config_cache('federation.activitypub.enabled') == false) {
-			return $status->delete();
-		} else {
-			return $this->remoteAnnounceDeliver();
-		}
-	}
-
-	public function remoteAnnounceDeliver()
-	{
-		if(config('app.env') !== 'production' || config_cache('federation.activitypub.enabled') == false) {
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+    protected $status;
+
+    public $deleteWhenMissingModels = true;
+
+    public function __construct(Status $status)
+    {
+        $this->status = $status;
+    }
+
+    public function handle()
+    {
+        $status = $this->status;
+        $actor = $status->profile;
+        $parent = Status::find($status->reblog_of_id);
+
+        FeedRemovePipeline::dispatch($status->id, $status->profile_id)->onQueue('feed');
+
+        if ($parent) {
+            $target = $parent->profile_id;
+            ReblogService::removePostReblog($parent->profile_id, $status->id);
+
+            if ($parent->reblogs_count > 0) {
+                $parent->reblogs_count = $parent->reblogs_count - 1;
+                $parent->save();
+                StatusService::del($parent->id);
+            }
+
+            $notification = Notification::whereProfileId($target)
+                ->whereActorId($status->profile_id)
+                ->whereAction('share')
+                ->whereItemId($status->reblog_of_id)
+                ->whereItemType('App\Status')
+                ->first();
+
+            if ($notification) {
+                $notification->forceDelete();
+            }
+        }
+
+        if ($status->uri != null) {
+            return;
+        }
+
+        if (config('app.env') !== 'production' || (bool) config_cache('federation.activitypub.enabled') == false) {
+            return $status->delete();
+        } else {
+            return $this->remoteAnnounceDeliver();
+        }
+    }
+
+    public function remoteAnnounceDeliver()
+    {
+        if (config('app.env') !== 'production' || (bool) config_cache('federation.activitypub.enabled') == false) {
             $status->delete();
-			return 1;
-		}
-
-		$status = $this->status;
-		$profile = $status->profile;
-
-		$fractal = new Fractal\Manager();
-		$fractal->setSerializer(new ArraySerializer());
-		$resource = new Fractal\Resource\Item($status, new UndoAnnounce());
-		$activity = $fractal->createData($resource)->toArray();
-
-		$audience = $status->profile->getAudienceInbox();
-
-		if(empty($audience) || $status->scope != 'public') {
-			return 1;
-		}
-
-		$payload = json_encode($activity);
-
-		$client = new Client([
-			'timeout'  => config('federation.activitypub.delivery.timeout')
-		]);
-
-		$version = config('pixelfed.version');
-		$appUrl = config('app.url');
-		$userAgent = "(Pixelfed/{$version}; +{$appUrl})";
-
-		$requests = function($audience) use ($client, $activity, $profile, $payload, $userAgent) {
-			foreach($audience as $url) {
-				$headers = HttpSignature::sign($profile, $url, $activity, [
-					'Content-Type'	=> 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
-					'User-Agent'	=> $userAgent,
-				]);
-				yield function() use ($client, $url, $headers, $payload) {
-					return $client->postAsync($url, [
-						'curl' => [
-							CURLOPT_HTTPHEADER => $headers,
-							CURLOPT_POSTFIELDS => $payload,
-							CURLOPT_HEADER => true
-						]
-					]);
-				};
-			}
-		};
-
-		$pool = new Pool($client, $requests($audience), [
-			'concurrency' => config('federation.activitypub.delivery.concurrency'),
-			'fulfilled' => function ($response, $index) {
-			},
-			'rejected' => function ($reason, $index) {
-			}
-		]);
-
-		$promise = $pool->promise();
-
-		$promise->wait();
-
-		$status->delete();
-
-		return 1;
-	}
+
+            return 1;
+        }
+
+        $status = $this->status;
+        $profile = $status->profile;
+
+        $fractal = new Fractal\Manager();
+        $fractal->setSerializer(new ArraySerializer());
+        $resource = new Fractal\Resource\Item($status, new UndoAnnounce());
+        $activity = $fractal->createData($resource)->toArray();
+
+        $audience = $status->profile->getAudienceInbox();
+
+        if (empty($audience) || $status->scope != 'public') {
+            return 1;
+        }
+
+        $payload = json_encode($activity);
+
+        $client = new Client([
+            'timeout' => config('federation.activitypub.delivery.timeout'),
+        ]);
+
+        $version = config('pixelfed.version');
+        $appUrl = config('app.url');
+        $userAgent = "(Pixelfed/{$version}; +{$appUrl})";
+
+        $requests = function ($audience) use ($client, $activity, $profile, $payload, $userAgent) {
+            foreach ($audience as $url) {
+                $headers = HttpSignature::sign($profile, $url, $activity, [
+                    'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
+                    'User-Agent' => $userAgent,
+                ]);
+                yield function () use ($client, $url, $headers, $payload) {
+                    return $client->postAsync($url, [
+                        'curl' => [
+                            CURLOPT_HTTPHEADER => $headers,
+                            CURLOPT_POSTFIELDS => $payload,
+                            CURLOPT_HEADER => true,
+                        ],
+                    ]);
+                };
+            }
+        };
+
+        $pool = new Pool($client, $requests($audience), [
+            'concurrency' => config('federation.activitypub.delivery.concurrency'),
+            'fulfilled' => function ($response, $index) {
+            },
+            'rejected' => function ($reason, $index) {
+            },
+        ]);
+
+        $promise = $pool->promise();
+
+        $promise->wait();
+
+        $status->delete();
+
+        return 1;
+    }
 }

+ 162 - 166
app/Jobs/StatusPipeline/StatusDelete.php

@@ -2,126 +2,122 @@
 
 namespace App\Jobs\StatusPipeline;
 
-use DB, Cache, Storage;
-use App\{
-	AccountInterstitial,
-    Bookmark,
-	CollectionItem,
-    DirectMessage,
-    Like,
-    Media,
-	MediaTag,
-    Mention,
-	Notification,
-	Report,
-	Status,
-    StatusArchived,
-	StatusHashtag,
-    StatusView
-};
+use App\AccountInterstitial;
+use App\Bookmark;
+use App\CollectionItem;
+use App\DirectMessage;
+use App\Jobs\MediaPipeline\MediaDeletePipeline;
+use App\Like;
+use App\Media;
+use App\MediaTag;
+use App\Mention;
+use App\Notification;
+use App\Report;
+use App\Services\CollectionService;
+use App\Services\NotificationService;
+use App\Services\StatusService;
+use App\Status;
+use App\StatusArchived;
+use App\StatusHashtag;
+use App\StatusView;
+use App\Transformer\ActivityPub\Verb\DeleteNote;
+use App\Util\ActivityPub\HttpSignature;
+use Cache;
+use GuzzleHttp\Client;
+use GuzzleHttp\Pool;
 use Illuminate\Bus\Queueable;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Foundation\Bus\Dispatchable;
 use Illuminate\Queue\InteractsWithQueue;
 use Illuminate\Queue\SerializesModels;
 use League\Fractal;
-use Illuminate\Support\Str;
 use League\Fractal\Serializer\ArraySerializer;
-use App\Transformer\ActivityPub\Verb\DeleteNote;
-use App\Util\ActivityPub\Helpers;
-use GuzzleHttp\Pool;
-use GuzzleHttp\Client;
-use GuzzleHttp\Promise;
-use App\Util\ActivityPub\HttpSignature;
-use App\Services\CollectionService;
-use App\Services\StatusService;
-use App\Services\NotificationService;
-use App\Jobs\MediaPipeline\MediaDeletePipeline;
 
 class StatusDelete implements ShouldQueue
 {
-	use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
 
-	protected $status;
+    protected $status;
 
-	/**
-	 * Delete the job if its models no longer exist.
-	 *
-	 * @var bool
-	 */
-	public $deleteWhenMissingModels = true;
+    /**
+     * Delete the job if its models no longer exist.
+     *
+     * @var bool
+     */
+    public $deleteWhenMissingModels = true;
 
     public $timeout = 900;
+
     public $tries = 2;
 
-	/**
-	 * Create a new job instance.
-	 *
-	 * @return void
-	 */
-	public function __construct(Status $status)
-	{
-		$this->status = $status;
-	}
-
-	/**
-	 * Execute the job.
-	 *
-	 * @return void
-	 */
-	public function handle()
-	{
-		$status = $this->status;
-		$profile = $this->status->profile;
-
-		StatusService::del($status->id, true);
-		if($profile) {
-			if(in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])) {
-				$profile->status_count = $profile->status_count - 1;
-				$profile->save();
-			}
-		}
-
-		Cache::forget('pf:atom:user-feed:by-id:' . $status->profile_id);
-
-		if(config_cache('federation.activitypub.enabled') == true) {
-			return $this->fanoutDelete($status);
-		} else {
-			return $this->unlinkRemoveMedia($status);
-		}
-	}
-
-	public function unlinkRemoveMedia($status)
-	{
+    /**
+     * Create a new job instance.
+     *
+     * @return void
+     */
+    public function __construct(Status $status)
+    {
+        $this->status = $status;
+    }
+
+    /**
+     * Execute the job.
+     *
+     * @return void
+     */
+    public function handle()
+    {
+        $status = $this->status;
+        $profile = $this->status->profile;
+
+        StatusService::del($status->id, true);
+        if ($profile) {
+            if (in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])) {
+                $profile->status_count = $profile->status_count - 1;
+                $profile->save();
+            }
+        }
+
+        Cache::forget('pf:atom:user-feed:by-id:'.$status->profile_id);
+
+        if ((bool) config_cache('federation.activitypub.enabled') == true) {
+            return $this->fanoutDelete($status);
+        } else {
+            return $this->unlinkRemoveMedia($status);
+        }
+    }
+
+    public function unlinkRemoveMedia($status)
+    {
         Media::whereStatusId($status->id)
-        ->get()
-        ->each(function($media) {
-            MediaDeletePipeline::dispatch($media);
-        });
-
-		if($status->in_reply_to_id) {
-			$parent = Status::findOrFail($status->in_reply_to_id);
-			--$parent->reply_count;
-			$parent->save();
-			StatusService::del($parent->id);
-		}
+            ->get()
+            ->each(function ($media) {
+                MediaDeletePipeline::dispatch($media);
+            });
+
+        if ($status->in_reply_to_id) {
+            $parent = Status::findOrFail($status->in_reply_to_id);
+            $parent->reply_count--;
+            $parent->save();
+            StatusService::del($parent->id);
+        }
 
         Bookmark::whereStatusId($status->id)->delete();
 
         CollectionItem::whereObjectType('App\Status')
             ->whereObjectId($status->id)
             ->get()
-            ->each(function($col) {
+            ->each(function ($col) {
                 CollectionService::removeItem($col->collection_id, $col->object_id);
                 $col->delete();
-        });
+            });
 
         $dms = DirectMessage::whereStatusId($status->id)->get();
-        foreach($dms as $dm) {
+        foreach ($dms as $dm) {
             $not = Notification::whereItemType('App\DirectMessage')
                 ->whereItemId($dm->id)
                 ->first();
-            if($not) {
+            if ($not) {
                 NotificationService::del($not->profile_id, $not->id);
                 $not->forceDeleteQuietly();
             }
@@ -130,11 +126,11 @@ class StatusDelete implements ShouldQueue
         Like::whereStatusId($status->id)->delete();
 
         $mediaTags = MediaTag::where('status_id', $status->id)->get();
-        foreach($mediaTags as $mtag) {
+        foreach ($mediaTags as $mtag) {
             $not = Notification::whereItemType('App\MediaTag')
                 ->whereItemId($mtag->id)
                 ->first();
-            if($not) {
+            if ($not) {
                 NotificationService::del($not->profile_id, $not->id);
                 $not->forceDeleteQuietly();
             }
@@ -142,85 +138,85 @@ class StatusDelete implements ShouldQueue
         }
         Mention::whereStatusId($status->id)->forceDelete();
 
-		Notification::whereItemType('App\Status')
-			->whereItemId($status->id)
-			->forceDelete();
+        Notification::whereItemType('App\Status')
+            ->whereItemId($status->id)
+            ->forceDelete();
 
-		Report::whereObjectType('App\Status')
-			->whereObjectId($status->id)
-			->delete();
+        Report::whereObjectType('App\Status')
+            ->whereObjectId($status->id)
+            ->delete();
 
         StatusArchived::whereStatusId($status->id)->delete();
         StatusHashtag::whereStatusId($status->id)->delete();
         StatusView::whereStatusId($status->id)->delete();
-		Status::whereInReplyToId($status->id)->update(['in_reply_to_id' => null]);
-
-		AccountInterstitial::where('item_type', 'App\Status')
-			->where('item_id', $status->id)
-			->delete();
-
-		$status->delete();
-
-		return 1;
-	}
-
-	public function fanoutDelete($status)
-	{
-		$profile = $status->profile;
-
-		if(!$profile) {
-			return;
-		}
-
-		$audience = $status->profile->getAudienceInbox();
-
-		$fractal = new Fractal\Manager();
-		$fractal->setSerializer(new ArraySerializer());
-		$resource = new Fractal\Resource\Item($status, new DeleteNote());
-		$activity = $fractal->createData($resource)->toArray();
-
-		$this->unlinkRemoveMedia($status);
-
-		$payload = json_encode($activity);
-
-		$client = new Client([
-			'timeout'  => config('federation.activitypub.delivery.timeout')
-		]);
-
-		$version = config('pixelfed.version');
-		$appUrl = config('app.url');
-		$userAgent = "(Pixelfed/{$version}; +{$appUrl})";
-
-		$requests = function($audience) use ($client, $activity, $profile, $payload, $userAgent) {
-			foreach($audience as $url) {
-				$headers = HttpSignature::sign($profile, $url, $activity, [
-					'Content-Type'	=> 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
-					'User-Agent'	=> $userAgent,
-				]);
-				yield function() use ($client, $url, $headers, $payload) {
-					return $client->postAsync($url, [
-						'curl' => [
-							CURLOPT_HTTPHEADER => $headers,
-							CURLOPT_POSTFIELDS => $payload,
-							CURLOPT_HEADER => true
-						]
-					]);
-				};
-			}
-		};
-
-		$pool = new Pool($client, $requests($audience), [
-			'concurrency' => config('federation.activitypub.delivery.concurrency'),
-			'fulfilled' => function ($response, $index) {
-			},
-			'rejected' => function ($reason, $index) {
-			}
-		]);
-
-		$promise = $pool->promise();
-
-		$promise->wait();
+        Status::whereInReplyToId($status->id)->update(['in_reply_to_id' => null]);
+
+        AccountInterstitial::where('item_type', 'App\Status')
+            ->where('item_id', $status->id)
+            ->delete();
+
+        $status->delete();
+
+        return 1;
+    }
+
+    public function fanoutDelete($status)
+    {
+        $profile = $status->profile;
+
+        if (! $profile) {
+            return;
+        }
+
+        $audience = $status->profile->getAudienceInbox();
+
+        $fractal = new Fractal\Manager();
+        $fractal->setSerializer(new ArraySerializer());
+        $resource = new Fractal\Resource\Item($status, new DeleteNote());
+        $activity = $fractal->createData($resource)->toArray();
+
+        $this->unlinkRemoveMedia($status);
+
+        $payload = json_encode($activity);
+
+        $client = new Client([
+            'timeout' => config('federation.activitypub.delivery.timeout'),
+        ]);
+
+        $version = config('pixelfed.version');
+        $appUrl = config('app.url');
+        $userAgent = "(Pixelfed/{$version}; +{$appUrl})";
+
+        $requests = function ($audience) use ($client, $activity, $profile, $payload, $userAgent) {
+            foreach ($audience as $url) {
+                $headers = HttpSignature::sign($profile, $url, $activity, [
+                    'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
+                    'User-Agent' => $userAgent,
+                ]);
+                yield function () use ($client, $url, $headers, $payload) {
+                    return $client->postAsync($url, [
+                        'curl' => [
+                            CURLOPT_HTTPHEADER => $headers,
+                            CURLOPT_POSTFIELDS => $payload,
+                            CURLOPT_HEADER => true,
+                        ],
+                    ]);
+                };
+            }
+        };
+
+        $pool = new Pool($client, $requests($audience), [
+            'concurrency' => config('federation.activitypub.delivery.concurrency'),
+            'fulfilled' => function ($response, $index) {
+            },
+            'rejected' => function ($reason, $index) {
+            },
+        ]);
+
+        $promise = $pool->promise();
+
+        $promise->wait();
 
         return 1;
-	}
+    }
 }

+ 22 - 21
app/Jobs/StatusPipeline/StatusEntityLexer.php

@@ -3,12 +3,16 @@
 namespace App\Jobs\StatusPipeline;
 
 use App\Hashtag;
+use App\Jobs\HomeFeedPipeline\FeedInsertPipeline;
 use App\Jobs\MentionPipeline\MentionPipeline;
 use App\Mention;
 use App\Profile;
+use App\Services\AdminShadowFilterService;
+use App\Services\PublicTimelineService;
+use App\Services\StatusService;
+use App\Services\UserFilterService;
 use App\Status;
 use App\StatusHashtag;
-use App\Services\PublicTimelineService;
 use App\Util\Lexer\Autolink;
 use App\Util\Lexer\Extractor;
 use App\Util\Sentiment\Bouncer;
@@ -19,18 +23,15 @@ use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Foundation\Bus\Dispatchable;
 use Illuminate\Queue\InteractsWithQueue;
 use Illuminate\Queue\SerializesModels;
-use App\Services\StatusService;
-use App\Services\UserFilterService;
-use App\Services\AdminShadowFilterService;
-use App\Jobs\HomeFeedPipeline\FeedInsertPipeline;
-use App\Jobs\HomeFeedPipeline\HashtagInsertFanoutPipeline;
 
 class StatusEntityLexer implements ShouldQueue
 {
     use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
 
     protected $status;
+
     protected $entities;
+
     protected $autolink;
 
     /**
@@ -60,12 +61,12 @@ class StatusEntityLexer implements ShouldQueue
         $profile = $this->status->profile;
         $status = $this->status;
 
-        if(in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])) {
+        if (in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])) {
             $profile->status_count = $profile->status_count + 1;
             $profile->save();
         }
 
-        if($profile->no_autolink == false) {
+        if ($profile->no_autolink == false) {
             $this->parseEntities();
         }
     }
@@ -103,16 +104,16 @@ class StatusEntityLexer implements ShouldQueue
         $status = $this->status;
 
         foreach ($tags as $tag) {
-            if(mb_strlen($tag) > 124) {
+            if (mb_strlen($tag) > 124) {
                 continue;
             }
             DB::transaction(function () use ($status, $tag) {
                 $slug = str_slug($tag, '-', false);
 
                 $hashtag = Hashtag::firstOrCreate([
-                    'slug' => $slug
+                    'slug' => $slug,
                 ], [
-                    'name' => $tag
+                    'name' => $tag,
                 ]);
 
                 StatusHashtag::firstOrCreate(
@@ -136,11 +137,11 @@ class StatusEntityLexer implements ShouldQueue
         foreach ($mentions as $mention) {
             $mentioned = Profile::whereUsername($mention)->first();
 
-            if (empty($mentioned) || !isset($mentioned->id)) {
+            if (empty($mentioned) || ! isset($mentioned->id)) {
                 continue;
             }
             $blocks = UserFilterService::blocks($mentioned->id);
-            if($blocks && in_array($status->profile_id, $blocks)) {
+            if ($blocks && in_array($status->profile_id, $blocks)) {
                 continue;
             }
 
@@ -161,8 +162,8 @@ class StatusEntityLexer implements ShouldQueue
         $status = $this->status;
         StatusService::refresh($status->id);
 
-        if(config('exp.cached_home_timeline')) {
-            if( $status->in_reply_to_id === null &&
+        if (config('exp.cached_home_timeline')) {
+            if ($status->in_reply_to_id === null &&
                 in_array($status->scope, ['public', 'unlisted', 'private'])
             ) {
                 FeedInsertPipeline::dispatch($status->id, $status->profile_id)->onQueue('feed');
@@ -179,28 +180,28 @@ class StatusEntityLexer implements ShouldQueue
             'photo:album',
             'video',
             'video:album',
-            'photo:video:album'
+            'photo:video:album',
         ];
 
-        if(config_cache('pixelfed.bouncer.enabled')) {
+        if ((bool) config_cache('pixelfed.bouncer.enabled')) {
             Bouncer::get($status);
         }
 
-        Cache::forget('pf:atom:user-feed:by-id:' . $status->profile_id);
+        Cache::forget('pf:atom:user-feed:by-id:'.$status->profile_id);
         $hideNsfw = config('instance.hide_nsfw_on_public_feeds');
-        if( $status->uri == null &&
+        if ($status->uri == null &&
             $status->scope == 'public' &&
             in_array($status->type, $types) &&
             $status->in_reply_to_id === null &&
             $status->reblog_of_id === null &&
             ($hideNsfw ? $status->is_nsfw == false : true)
         ) {
-            if(AdminShadowFilterService::canAddToPublicFeedByProfileId($status->profile_id)) {
+            if (AdminShadowFilterService::canAddToPublicFeedByProfileId($status->profile_id)) {
                 PublicTimelineService::add($status->id);
             }
         }
 
-        if(config_cache('federation.activitypub.enabled') == true && config('app.env') == 'production') {
+        if ((bool) config_cache('federation.activitypub.enabled') == true && config('app.env') == 'production') {
             StatusActivityPubDeliver::dispatch($status);
         }
     }

+ 155 - 156
app/Jobs/StatusPipeline/StatusRemoteUpdatePipeline.php

@@ -2,172 +2,171 @@
 
 namespace App\Jobs\StatusPipeline;
 
+use App\Media;
+use App\Models\StatusEdit;
+use App\ModLog;
+use App\Profile;
+use App\Services\StatusService;
+use App\Status;
 use Illuminate\Bus\Queueable;
-use Illuminate\Contracts\Queue\ShouldBeUnique;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Foundation\Bus\Dispatchable;
 use Illuminate\Queue\InteractsWithQueue;
 use Illuminate\Queue\SerializesModels;
-use App\Media;
-use App\ModLog;
-use App\Profile;
-use App\Status;
-use App\Models\StatusEdit;
-use App\Services\StatusService;
-use Purify;
 use Illuminate\Support\Facades\Http;
+use Purify;
 
 class StatusRemoteUpdatePipeline implements ShouldQueue
 {
-	use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
-
-	public $activity;
-
-	/**
-	 * Create a new job instance.
-	 */
-	public function __construct($activity)
-	{
-		$this->activity = $activity;
-	}
-
-	/**
-	 * Execute the job.
-	 */
-	public function handle(): void
-	{
-		$activity = $this->activity;
-		$status = Status::with('media')->whereObjectUrl($activity['id'])->first();
-		if(!$status) {
-			return;
-		}
-		$this->createPreviousEdit($status);
-		$this->updateMedia($status, $activity);
-		$this->updateImmediateAttributes($status, $activity);
-		$this->createEdit($status, $activity);
-	}
-
-	protected function createPreviousEdit($status)
-	{
-		if(!$status->edits()->count()) {
-			StatusEdit::create([
-				'status_id' => $status->id,
-				'profile_id' => $status->profile_id,
-				'caption' => $status->caption,
-				'spoiler_text' => $status->cw_summary,
-				'is_nsfw' => $status->is_nsfw,
-				'ordered_media_attachment_ids' => $status->media()->orderBy('order')->pluck('id')->toArray(),
-				'created_at' => $status->created_at
-			]);
-		}
-	}
-
-	protected function updateMedia($status, $activity)
-	{
-		if(!isset($activity['attachment'])) {
-			return;
-		}
-		$ogm = $status->media->count() ? $status->media()->orderBy('order')->get() : collect([]);
-		$nm = collect($activity['attachment'])->filter(function($nm) {
-			return isset(
-				$nm['type'],
-				$nm['mediaType'],
-				$nm['url']
-			) &&
-			in_array($nm['type'], ['Document', 'Image', 'Video']) &&
-			in_array($nm['mediaType'], explode(',', config('pixelfed.media_types')));
-		});
-
-		// Skip when no media
-		if(!$ogm->count() && !$nm->count()) {
-			return;
-		}
-
-		Media::whereProfileId($status->profile_id)
-			->whereStatusId($status->id)
-			->update([
-				'status_id' => null
-			]);
-
-		$nm->each(function($n, $key) use($status) {
-			$res = Http::withOptions(['allow_redirects' => false])->retry(3, 100, throw: false)->head($n['url']);
-
-			if(!$res->successful()) {
-				return;
-			}
-
-			if(!in_array($res->header('content-type'), explode(',',config('pixelfed.media_types')))) {
-				return;
-			}
-
-			$m = new Media;
-			$m->status_id = $status->id;
-			$m->profile_id = $status->profile_id;
-			$m->remote_media = true;
-			$m->media_path = $n['url'];
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+    public $activity;
+
+    /**
+     * Create a new job instance.
+     */
+    public function __construct($activity)
+    {
+        $this->activity = $activity;
+    }
+
+    /**
+     * Execute the job.
+     */
+    public function handle(): void
+    {
+        $activity = $this->activity;
+        $status = Status::with('media')->whereObjectUrl($activity['id'])->first();
+        if (! $status) {
+            return;
+        }
+        $this->createPreviousEdit($status);
+        $this->updateMedia($status, $activity);
+        $this->updateImmediateAttributes($status, $activity);
+        $this->createEdit($status, $activity);
+    }
+
+    protected function createPreviousEdit($status)
+    {
+        if (! $status->edits()->count()) {
+            StatusEdit::create([
+                'status_id' => $status->id,
+                'profile_id' => $status->profile_id,
+                'caption' => $status->caption,
+                'spoiler_text' => $status->cw_summary,
+                'is_nsfw' => $status->is_nsfw,
+                'ordered_media_attachment_ids' => $status->media()->orderBy('order')->pluck('id')->toArray(),
+                'created_at' => $status->created_at,
+            ]);
+        }
+    }
+
+    protected function updateMedia($status, $activity)
+    {
+        if (! isset($activity['attachment'])) {
+            return;
+        }
+        $ogm = $status->media->count() ? $status->media()->orderBy('order')->get() : collect([]);
+        $nm = collect($activity['attachment'])->filter(function ($nm) {
+            return isset(
+                $nm['type'],
+                $nm['mediaType'],
+                $nm['url']
+            ) &&
+            in_array($nm['type'], ['Document', 'Image', 'Video']) &&
+            in_array($nm['mediaType'], explode(',', config_cache('pixelfed.media_types')));
+        });
+
+        // Skip when no media
+        if (! $ogm->count() && ! $nm->count()) {
+            return;
+        }
+
+        Media::whereProfileId($status->profile_id)
+            ->whereStatusId($status->id)
+            ->update([
+                'status_id' => null,
+            ]);
+
+        $nm->each(function ($n, $key) use ($status) {
+            $res = Http::withOptions(['allow_redirects' => false])->retry(3, 100, throw: false)->head($n['url']);
+
+            if (! $res->successful()) {
+                return;
+            }
+
+            if (! in_array($res->header('content-type'), explode(',', config_cache('pixelfed.media_types')))) {
+                return;
+            }
+
+            $m = new Media;
+            $m->status_id = $status->id;
+            $m->profile_id = $status->profile_id;
+            $m->remote_media = true;
+            $m->media_path = $n['url'];
             $m->mime = $res->header('content-type');
             $m->size = $res->hasHeader('content-length') ? $res->header('content-length') : null;
-			$m->caption = isset($n['name']) && !empty($n['name']) ? Purify::clean($n['name']) : null;
-			$m->remote_url = $n['url'];
+            $m->caption = isset($n['name']) && ! empty($n['name']) ? Purify::clean($n['name']) : null;
+            $m->remote_url = $n['url'];
             $m->blurhash = isset($n['blurhash']) && (strlen($n['blurhash']) < 50) ? $n['blurhash'] : null;
-			$m->width = isset($n['width']) && !empty($n['width']) ? $n['width'] : null;
-			$m->height = isset($n['height']) && !empty($n['height']) ? $n['height'] : null;
-			$m->skip_optimize = true;
-			$m->order = $key + 1;
-			$m->save();
-		});
-	}
-
-	protected function updateImmediateAttributes($status, $activity)
-	{
-		if(isset($activity['content'])) {
-			$status->caption = strip_tags($activity['content']);
-			$status->rendered = Purify::clean($activity['content']);
-		}
-
-		if(isset($activity['sensitive'])) {
-			if((bool) $activity['sensitive'] == false) {
-				$status->is_nsfw = false;
-				$exists = ModLog::whereObjectType('App\Status::class')
-					->whereObjectId($status->id)
-					->whereAction('admin.status.moderate')
-					->exists();
-				if($exists == true) {
-					$status->is_nsfw = true;
-				}
-				$profile = Profile::find($status->profile_id);
-				if(!$profile || $profile->cw == true) {
-					$status->is_nsfw = true;
-				}
-			} else {
-				$status->is_nsfw = true;
-			}
-		}
-
-		if(isset($activity['summary'])) {
-			$status->cw_summary = Purify::clean($activity['summary']);
-		} else {
-			$status->cw_summary = null;
-		}
-
-		$status->edited_at = now();
-		$status->save();
-		StatusService::del($status->id);
-	}
-
-	protected function createEdit($status, $activity)
-	{
-		$cleaned = isset($activity['content']) ? Purify::clean($activity['content']) : null;
-		$spoiler_text = isset($activity['summary']) ? Purify::clean($activity['summary']) : null;
-		$sensitive = isset($activity['sensitive']) ? $activity['sensitive'] : null;
-		$mids = $status->media()->count() ? $status->media()->orderBy('order')->pluck('id')->toArray() : null;
-		StatusEdit::create([
-			'status_id' => $status->id,
-			'profile_id' => $status->profile_id,
-			'caption' => $cleaned,
-			'spoiler_text' => $spoiler_text,
-			'is_nsfw' => $sensitive,
-			'ordered_media_attachment_ids' => $mids
-		]);
-	}
+            $m->width = isset($n['width']) && ! empty($n['width']) ? $n['width'] : null;
+            $m->height = isset($n['height']) && ! empty($n['height']) ? $n['height'] : null;
+            $m->skip_optimize = true;
+            $m->order = $key + 1;
+            $m->save();
+        });
+    }
+
+    protected function updateImmediateAttributes($status, $activity)
+    {
+        if (isset($activity['content'])) {
+            $status->caption = strip_tags($activity['content']);
+            $status->rendered = Purify::clean($activity['content']);
+        }
+
+        if (isset($activity['sensitive'])) {
+            if ((bool) $activity['sensitive'] == false) {
+                $status->is_nsfw = false;
+                $exists = ModLog::whereObjectType('App\Status::class')
+                    ->whereObjectId($status->id)
+                    ->whereAction('admin.status.moderate')
+                    ->exists();
+                if ($exists == true) {
+                    $status->is_nsfw = true;
+                }
+                $profile = Profile::find($status->profile_id);
+                if (! $profile || $profile->cw == true) {
+                    $status->is_nsfw = true;
+                }
+            } else {
+                $status->is_nsfw = true;
+            }
+        }
+
+        if (isset($activity['summary'])) {
+            $status->cw_summary = Purify::clean($activity['summary']);
+        } else {
+            $status->cw_summary = null;
+        }
+
+        $status->edited_at = now();
+        $status->save();
+        StatusService::del($status->id);
+    }
+
+    protected function createEdit($status, $activity)
+    {
+        $cleaned = isset($activity['content']) ? Purify::clean($activity['content']) : null;
+        $spoiler_text = isset($activity['summary']) ? Purify::clean($activity['summary']) : null;
+        $sensitive = isset($activity['sensitive']) ? $activity['sensitive'] : null;
+        $mids = $status->media()->count() ? $status->media()->orderBy('order')->pluck('id')->toArray() : null;
+        StatusEdit::create([
+            'status_id' => $status->id,
+            'profile_id' => $status->profile_id,
+            'caption' => $cleaned,
+            'spoiler_text' => $spoiler_text,
+            'is_nsfw' => $sensitive,
+            'ordered_media_attachment_ids' => $mids,
+        ]);
+    }
 }

+ 1 - 1
app/Models/CustomEmoji.php

@@ -18,7 +18,7 @@ class CustomEmoji extends Model
 
 	public static function scan($text, $activitypub = false)
 	{
-		if(config('federation.custom_emoji.enabled') == false) {
+		if((bool) config_cache('federation.custom_emoji.enabled') == false) {
 			return [];
 		}
 

+ 4 - 10
app/Observers/AvatarObserver.php

@@ -3,9 +3,9 @@
 namespace App\Observers;
 
 use App\Avatar;
+use App\Services\AccountService;
 use Illuminate\Support\Facades\Storage;
 use Illuminate\Support\Str;
-use App\Services\AccountService;
 
 class AvatarObserver
 {
@@ -19,7 +19,6 @@ class AvatarObserver
     /**
      * Handle the avatar "created" event.
      *
-     * @param  \App\Avatar  $avatar
      * @return void
      */
     public function created(Avatar $avatar)
@@ -30,7 +29,6 @@ class AvatarObserver
     /**
      * Handle the avatar "updated" event.
      *
-     * @param  \App\Avatar  $avatar
      * @return void
      */
     public function updated(Avatar $avatar)
@@ -41,7 +39,6 @@ class AvatarObserver
     /**
      * Handle the avatar "deleted" event.
      *
-     * @param  \App\Avatar  $avatar
      * @return void
      */
     public function deleted(Avatar $avatar)
@@ -52,23 +49,22 @@ class AvatarObserver
     /**
      * Handle the avatar "deleting" event.
      *
-     * @param  \App\Avatar  $avatar
      * @return void
      */
     public function deleting(Avatar $avatar)
     {
         $path = storage_path('app/'.$avatar->media_path);
-        if( is_file($path) && 
+        if (is_file($path) &&
             $avatar->media_path != 'public/avatars/default.png' &&
             $avatar->media_path != 'public/avatars/default.jpg'
         ) {
             @unlink($path);
         }
 
-        if(config_cache('pixelfed.cloud_storage')) {
+        if ((bool) config_cache('pixelfed.cloud_storage')) {
             $disk = Storage::disk(config('filesystems.cloud'));
             $base = Str::startsWith($avatar->media_path, 'cache/avatars/');
-            if($base && $disk->exists($avatar->media_path)) {
+            if ($base && $disk->exists($avatar->media_path)) {
                 $disk->delete($avatar->media_path);
             }
         }
@@ -78,7 +74,6 @@ class AvatarObserver
     /**
      * Handle the avatar "restored" event.
      *
-     * @param  \App\Avatar  $avatar
      * @return void
      */
     public function restored(Avatar $avatar)
@@ -89,7 +84,6 @@ class AvatarObserver
     /**
      * Handle the avatar "force deleted" event.
      *
-     * @param  \App\Avatar  $avatar
      * @return void
      */
     public function forceDeleted(Avatar $avatar)

+ 1 - 1
app/Observers/UserObserver.php

@@ -107,7 +107,7 @@ class UserObserver
                 CreateAvatar::dispatch($profile);
             });
 
-            if(config_cache('account.autofollow') == true) {
+            if((bool) config_cache('account.autofollow') == true) {
                 $names = config_cache('account.autofollow_usernames');
                 $names = explode(',', $names);
 

+ 4 - 4
app/Providers/AuthServiceProvider.php

@@ -2,9 +2,9 @@
 
 namespace App\Providers;
 
+use Gate;
 use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
 use Laravel\Passport\Passport;
-use Gate;
 
 class AuthServiceProvider extends ServiceProvider
 {
@@ -24,11 +24,11 @@ class AuthServiceProvider extends ServiceProvider
      */
     public function boot()
     {
-        if(config('app.env') === 'production' && config('pixelfed.oauth_enabled') == true) {
+        if (config('app.env') === 'production' && (bool) config_cache('pixelfed.oauth_enabled') == true) {
             Passport::tokensExpireIn(now()->addDays(config('instance.oauth.token_expiration', 356)));
             Passport::refreshTokensExpireIn(now()->addDays(config('instance.oauth.refresh_expiration', 400)));
             Passport::enableImplicitGrant();
-            if(config('instance.oauth.pat.enabled')) {
+            if (config('instance.oauth.pat.enabled')) {
                 Passport::personalAccessClientId(config('instance.oauth.pat.id'));
             }
 
@@ -38,7 +38,7 @@ class AuthServiceProvider extends ServiceProvider
                 'follow' => 'Ability to follow other profiles',
                 'admin:read' => 'Read all data on the server',
                 'admin:write' => 'Modify all data on the server',
-                'push'  => 'Receive your push notifications'
+                'push' => 'Receive your push notifications',
             ]);
 
             Passport::setDefaultScope([

+ 166 - 0
app/Services/AdminSettingsService.php

@@ -0,0 +1,166 @@
+<?php
+
+namespace App\Services;
+
+use App\Services\Internal\BeagleService;
+use App\User;
+use Illuminate\Support\Str;
+
+class AdminSettingsService
+{
+    public static function getAll()
+    {
+        return [
+            'features' => self::getFeatures(),
+            'landing' => self::getLanding(),
+            'branding' => self::getBranding(),
+            'media' => self::getMedia(),
+            'rules' => self::getRules(),
+            'suggested_rules' => self::getSuggestedRules(),
+            'users' => self::getUsers(),
+            'posts' => self::getPosts(),
+            'platform' => self::getPlatform(),
+            'storage' => self::getStorage(),
+        ];
+    }
+
+    public static function getFeatures()
+    {
+        $cloud_storage = (bool) config_cache('pixelfed.cloud_storage');
+        $cloud_disk = config('filesystems.cloud');
+        $cloud_ready = ! empty(config('filesystems.disks.'.$cloud_disk.'.key')) && ! empty(config('filesystems.disks.'.$cloud_disk.'.secret'));
+        $openReg = (bool) config_cache('pixelfed.open_registration');
+        $curOnboarding = (bool) config_cache('instance.curated_registration.enabled');
+        $regState = $openReg ? 'open' : ($curOnboarding ? 'filtered' : 'closed');
+
+        return [
+            'registration_status' => $regState,
+            'cloud_storage' => $cloud_ready && $cloud_storage,
+            'activitypub_enabled' => (bool) config_cache('federation.activitypub.enabled'),
+            'account_migration' => (bool) config_cache('federation.migration'),
+            'mobile_apis' => (bool) config_cache('pixelfed.oauth_enabled'),
+            'stories' => (bool) config_cache('instance.stories.enabled'),
+            'instagram_import' => (bool) config_cache('pixelfed.import.instagram.enabled'),
+            'autospam_enabled' => (bool) config_cache('pixelfed.bouncer.enabled'),
+        ];
+    }
+
+    public static function getLanding()
+    {
+        $availableAdmins = User::whereIsAdmin(true)->get();
+        $currentAdmin = config_cache('instance.admin.pid');
+
+        return [
+            'admins' => $availableAdmins,
+            'current_admin' => $currentAdmin,
+            'show_directory' => (bool) config_cache('instance.landing.show_directory'),
+            'show_explore' => (bool) config_cache('instance.landing.show_explore'),
+        ];
+    }
+
+    public static function getBranding()
+    {
+        return [
+            'name' => config_cache('app.name'),
+            'short_description' => config_cache('app.short_description'),
+            'long_description' => config_cache('app.description'),
+        ];
+    }
+
+    public static function getMedia()
+    {
+        return [
+            'max_photo_size' => config_cache('pixelfed.max_photo_size'),
+            'max_album_length' => config_cache('pixelfed.max_album_length'),
+            'image_quality' => config_cache('pixelfed.image_quality'),
+            'media_types' => config_cache('pixelfed.media_types'),
+            'optimize_image' => (bool) config_cache('pixelfed.optimize_image'),
+            'optimize_video' => (bool) config_cache('pixelfed.optimize_video'),
+        ];
+    }
+
+    public static function getRules()
+    {
+        return config_cache('app.rules') ? json_decode(config_cache('app.rules'), true) : [];
+    }
+
+    public static function getSuggestedRules()
+    {
+        return BeagleService::getDefaultRules();
+    }
+
+    public static function getUsers()
+    {
+        $autoFollow = config_cache('account.autofollow_usernames');
+        if (strlen($autoFollow) >= 2) {
+            $autoFollow = explode(',', $autoFollow);
+        } else {
+            $autoFollow = [];
+        }
+
+        return [
+            'require_email_verification' => (bool) config_cache('pixelfed.enforce_email_verification'),
+            'enforce_account_limit' => (bool) config_cache('pixelfed.enforce_account_limit'),
+            'max_account_size' => config_cache('pixelfed.max_account_size'),
+            'admin_autofollow' => (bool) config_cache('account.autofollow'),
+            'admin_autofollow_accounts' => $autoFollow,
+            'max_user_blocks' => (int) config_cache('instance.user_filters.max_user_blocks'),
+            'max_user_mutes' => (int) config_cache('instance.user_filters.max_user_mutes'),
+            'max_domain_blocks' => (int) config_cache('instance.user_filters.max_domain_blocks'),
+        ];
+    }
+
+    public static function getPosts()
+    {
+        return [
+            'max_caption_length' => config_cache('pixelfed.max_caption_length'),
+            'max_altext_length' => config_cache('pixelfed.max_altext_length'),
+        ];
+    }
+
+    public static function getPlatform()
+    {
+        return [
+            'allow_app_registration' => (bool) config_cache('pixelfed.allow_app_registration'),
+            'app_registration_rate_limit_attempts' => config_cache('pixelfed.app_registration_rate_limit_attempts'),
+            'app_registration_rate_limit_decay' => config_cache('pixelfed.app_registration_rate_limit_decay'),
+            'app_registration_confirm_rate_limit_attempts' => config_cache('pixelfed.app_registration_confirm_rate_limit_attempts'),
+            'app_registration_confirm_rate_limit_decay' => config_cache('pixelfed.app_registration_confirm_rate_limit_decay'),
+            'allow_post_embeds' => (bool) config_cache('instance.embed.post'),
+            'allow_profile_embeds' => (bool) config_cache('instance.embed.profile'),
+            'captcha_enabled' => (bool) config_cache('captcha.enabled'),
+            'captcha_on_login' => (bool) config_cache('captcha.active.login'),
+            'captcha_on_register' => (bool) config_cache('captcha.active.register'),
+            'captcha_secret' => Str::of(config_cache('captcha.secret'))->mask('*', 4, -4),
+            'captcha_sitekey' => Str::of(config_cache('captcha.sitekey'))->mask('*', 4, -4),
+            'custom_emoji_enabled' => (bool) config_cache('federation.custom_emoji.enabled'),
+        ];
+    }
+
+    public static function getStorage()
+    {
+        $cloud_storage = (bool) config_cache('pixelfed.cloud_storage');
+        $cloud_disk = config('filesystems.cloud');
+        $cloud_ready = ! empty(config('filesystems.disks.'.$cloud_disk.'.key')) && ! empty(config('filesystems.disks.'.$cloud_disk.'.secret'));
+        $primaryDisk = (bool) $cloud_ready && $cloud_storage;
+        $pkey = 'filesystems.disks.'.$cloud_disk.'.';
+        $disk = [
+            'driver' => $cloud_disk,
+            'key' => Str::of(config_cache($pkey.'key'))->mask('*', 0, -2),
+            'secret' => Str::of(config_cache($pkey.'secret'))->mask('*', 0, -2),
+            'region' => config_cache($pkey.'region'),
+            'bucket' => config_cache($pkey.'bucket'),
+            'visibility' => config_cache($pkey.'visibility'),
+            'endpoint' => config_cache($pkey.'endpoint'),
+            'url' => config_cache($pkey.'url'),
+            'use_path_style_endpoint' => config_cache($pkey.'use_path_style_endpoint'),
+        ];
+
+        return [
+            'primary_disk' => $primaryDisk ? 'cloud' : 'local',
+            'cloud_ready' => (bool) $cloud_ready,
+            'cloud_disk' => $cloud_disk,
+            'disk_config' => $disk,
+        ];
+    }
+}

+ 73 - 68
app/Services/AutospamService.php

@@ -2,77 +2,82 @@
 
 namespace App\Services;
 
+use App\Util\Lexer\Classifier;
 use Illuminate\Support\Facades\Cache;
 use Illuminate\Support\Facades\Storage;
-use App\Util\Lexer\Classifier;
 
 class AutospamService
 {
-	const CHCKD_CACHE_KEY = 'pf:services:autospam:nlp:checked';
-	const MODEL_CACHE_KEY = 'pf:services:autospam:nlp:model-cache';
-	const MODEL_FILE_PATH = 'nlp/active-training-data.json';
-	const MODEL_SPAM_PATH = 'nlp/spam.json';
-	const MODEL_HAM_PATH  = 'nlp/ham.json';
-
-	public static function check($text)
-	{
-		if(!$text || strlen($text) == 0) {
-			false;
-		}
-		if(!self::active()) {
-			return null;
-		}
-		$model = self::getCachedModel();
-		$classifier = new Classifier;
-		$classifier->import($model['documents'], $model['words']);
-		return $classifier->most($text) === 'spam';
-	}
-
-	public static function eligible()
-	{
-		return Cache::remember(self::CHCKD_CACHE_KEY, 86400, function() {
-			if(!config_cache('pixelfed.bouncer.enabled') || !config('autospam.enabled')) {
-				return false;
-			}
-
-			if(!Storage::exists(self::MODEL_SPAM_PATH)) {
-				return false;
-			}
-
-			if(!Storage::exists(self::MODEL_HAM_PATH)) {
-				return false;
-			}
-
-			if(!Storage::exists(self::MODEL_FILE_PATH)) {
-				return false;
-			} else {
-				if(Storage::size(self::MODEL_FILE_PATH) < 1000) {
-					return false;
-				}
-			}
-
-			return true;
-		});
-	}
-
-	public static function active()
-	{
-		return config_cache('autospam.nlp.enabled') && self::eligible();
-	}
-
-	public static function getCachedModel()
-	{
-		if(!self::active()) {
-			return null;
-		}
-
-		return Cache::remember(self::MODEL_CACHE_KEY, 86400, function() {
-			$res = Storage::get(self::MODEL_FILE_PATH);
-			if(!$res || empty($res)) {
-				return null;
-			}
-
-			return json_decode($res, true);
-		});
-	}
+    const CHCKD_CACHE_KEY = 'pf:services:autospam:nlp:checked';
+
+    const MODEL_CACHE_KEY = 'pf:services:autospam:nlp:model-cache';
+
+    const MODEL_FILE_PATH = 'nlp/active-training-data.json';
+
+    const MODEL_SPAM_PATH = 'nlp/spam.json';
+
+    const MODEL_HAM_PATH = 'nlp/ham.json';
+
+    public static function check($text)
+    {
+        if (! $text || strlen($text) == 0) {
+
+        }
+        if (! self::active()) {
+            return null;
+        }
+        $model = self::getCachedModel();
+        $classifier = new Classifier;
+        $classifier->import($model['documents'], $model['words']);
+
+        return $classifier->most($text) === 'spam';
+    }
+
+    public static function eligible()
+    {
+        return Cache::remember(self::CHCKD_CACHE_KEY, 86400, function () {
+            if (! (bool) config_cache('pixelfed.bouncer.enabled') || ! (bool) config_cache('autospam.enabled')) {
+                return false;
+            }
+
+            if (! Storage::exists(self::MODEL_SPAM_PATH)) {
+                return false;
+            }
+
+            if (! Storage::exists(self::MODEL_HAM_PATH)) {
+                return false;
+            }
+
+            if (! Storage::exists(self::MODEL_FILE_PATH)) {
+                return false;
+            } else {
+                if (Storage::size(self::MODEL_FILE_PATH) < 1000) {
+                    return false;
+                }
+            }
+
+            return true;
+        });
+    }
+
+    public static function active()
+    {
+        return config_cache('autospam.nlp.enabled') && self::eligible();
+    }
+
+    public static function getCachedModel()
+    {
+        if (! self::active()) {
+            return null;
+        }
+
+        return Cache::remember(self::MODEL_CACHE_KEY, 86400, function () {
+            $res = Storage::get(self::MODEL_FILE_PATH);
+            if (! $res || empty($res)) {
+                return null;
+            }
+
+            return json_decode($res, true);
+        });
+    }
 }

+ 68 - 4
app/Services/ConfigCacheService.php

@@ -8,6 +8,14 @@ use Cache;
 class ConfigCacheService
 {
     const CACHE_KEY = 'config_cache:_v0-key:';
+    const PROTECTED_KEYS = [
+        'filesystems.disks.s3.key',
+        'filesystems.disks.s3.secret',
+        'filesystems.disks.spaces.key',
+        'filesystems.disks.spaces.secret',
+        'captcha.secret',
+        'captcha.sitekey',
+    ];
 
     public static function get($key)
     {
@@ -89,6 +97,41 @@ class ConfigCacheService
                 'pixelfed.app_registration_confirm_rate_limit_decay',
                 'instance.embed.profile',
                 'instance.embed.post',
+
+                'captcha.enabled',
+                'captcha.secret',
+                'captcha.sitekey',
+                'captcha.active.login',
+                'captcha.active.register',
+                'captcha.triggers.login.enabled',
+                'captcha.triggers.login.attempts',
+                'federation.custom_emoji.enabled',
+
+                'pixelfed.optimize_image',
+                'pixelfed.optimize_video',
+                'pixelfed.max_collection_length',
+                'media.delete_local_after_cloud',
+                'instance.user_filters.max_user_blocks',
+                'instance.user_filters.max_user_mutes',
+                'instance.user_filters.max_domain_blocks',
+
+                'filesystems.disks.s3.key',
+                'filesystems.disks.s3.secret',
+                'filesystems.disks.s3.region',
+                'filesystems.disks.s3.bucket',
+                'filesystems.disks.s3.visibility',
+                'filesystems.disks.s3.url',
+                'filesystems.disks.s3.endpoint',
+                'filesystems.disks.s3.use_path_style_endpoint',
+
+                'filesystems.disks.spaces.key',
+                'filesystems.disks.spaces.secret',
+                'filesystems.disks.spaces.region',
+                'filesystems.disks.spaces.bucket',
+                'filesystems.disks.spaces.visibility',
+                'filesystems.disks.spaces.url',
+                'filesystems.disks.spaces.endpoint',
+                'filesystems.disks.spaces.use_path_style_endpoint',
                 // 'system.user_mode'
             ];
 
@@ -100,20 +143,34 @@ class ConfigCacheService
                 return config($key);
             }
 
+            $protect = false;
+            $protected = null;
+            if(in_array($key, self::PROTECTED_KEYS)) {
+                $protect = true;
+            }
+
             $v = config($key);
             $c = ConfigCacheModel::where('k', $key)->first();
 
             if ($c) {
-                return $c->v ?? config($key);
+                if($protect) {
+                    return decrypt($c->v) ?? config($key);
+                } else {
+                    return $c->v ?? config($key);
+                }
             }
 
             if (! $v) {
                 return;
             }
 
+            if($protect && $v) {
+                $protected = encrypt($v);
+            }
+
             $cc = new ConfigCacheModel;
             $cc->k = $key;
-            $cc->v = $v;
+            $cc->v = $protect ? $protected : $v;
             $cc->save();
 
             return $v;
@@ -124,8 +181,15 @@ class ConfigCacheService
     {
         $exists = ConfigCacheModel::whereK($key)->first();
 
+        $protect = false;
+        $protected = null;
+        if(in_array($key, self::PROTECTED_KEYS)) {
+            $protect = true;
+            $protected = encrypt($val);
+        }
+
         if ($exists) {
-            $exists->v = $val;
+            $exists->v = $protect ? $protected : $val;
             $exists->save();
             Cache::put(self::CACHE_KEY.$key, $val, now()->addHours(12));
 
@@ -134,7 +198,7 @@ class ConfigCacheService
 
         $cc = new ConfigCacheModel;
         $cc->k = $key;
-        $cc->v = $val;
+        $cc->v = $protect ? $protected : $val;
         $cc->save();
 
         Cache::put(self::CACHE_KEY.$key, $val, now()->addHours(12));

+ 2 - 2
app/Services/CustomEmojiService.php

@@ -13,7 +13,7 @@ class CustomEmojiService
 {
 	public static function get($shortcode)
 	{
-		if(config('federation.custom_emoji.enabled') == false) {
+		if((bool) config_cache('federation.custom_emoji.enabled') == false) {
 			return;
 		}
 
@@ -22,7 +22,7 @@ class CustomEmojiService
 
 	public static function import($url, $id = false)
 	{
-		if(config('federation.custom_emoji.enabled') == false) {
+		if((bool) config_cache('federation.custom_emoji.enabled') == false) {
 			return;
 		}
 

+ 82 - 0
app/Services/FilesystemService.php

@@ -0,0 +1,82 @@
+<?php
+
+namespace App\Services;
+
+use Aws\S3\S3Client;
+use Aws\S3\Exception\S3Exception;
+use GuzzleHttp\Exception\ConnectException;
+use League\Flysystem\AwsS3V3\AwsS3V3Adapter;
+use League\Flysystem\Filesystem;
+use League\Flysystem\UnableToRetrieveMetadata;
+use League\Flysystem\FilesystemException;
+use League\Flysystem\UnableToListContents;
+use League\Flysystem\FileAttributes;
+use League\Flysystem\UnableToWriteFile;
+
+class FilesystemService
+{
+    const VERIFY_FILE_NAME = 'cfstest.txt';
+
+    public static function getVerifyCredentials($key, $secret, $region, $bucket, $endpoint)
+    {
+        $client = new S3Client([
+            'version' => 'latest',
+            'region' => $region,
+            'endpoint' => $endpoint,
+            'credentials' => [
+                'key' => $key,
+                'secret' => $secret,
+            ]
+        ]);
+
+        $adapter = new AwsS3V3Adapter(
+            $client,
+            $bucket,
+        );
+
+        $throw = false;
+        $filesystem = new Filesystem($adapter);
+
+        $writable = false;
+        try {
+            $filesystem->write(self::VERIFY_FILE_NAME, 'ok', []);
+            $writable = true;
+        } catch (FilesystemException | UnableToWriteFile $exception) {
+            $writable = false;
+        }
+
+        if(!$writable) {
+            return false;
+        }
+
+        try {
+            $response = $filesystem->read(self::VERIFY_FILE_NAME);
+            if($response === 'ok') {
+                $writable = true;
+                $res[] = self::VERIFY_FILE_NAME;
+            } else {
+                $writable = false;
+            }
+        } catch (FilesystemException | UnableToReadFile $exception) {
+            $writable = false;
+        }
+
+        if(in_array(self::VERIFY_FILE_NAME, $res)) {
+            try {
+                $filesystem->delete(self::VERIFY_FILE_NAME);
+            } catch (FilesystemException | UnableToDeleteFile $exception) {
+                $writable = false;
+            }
+        }
+
+        if(!$writable) {
+            return false;
+        }
+
+        if(in_array(self::VERIFY_FILE_NAME, $res)) {
+            return true;
+        }
+
+        return false;
+    }
+}

+ 44 - 0
app/Services/Internal/BeagleService.php

@@ -0,0 +1,44 @@
+<?php
+
+namespace App\Services\Internal;
+
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\Http;
+use Illuminate\Http\Client\ConnectionException;
+use Illuminate\Http\Client\RequestException;
+
+class BeagleService
+{
+    const DEFAULT_RULES_CACHE_KEY = 'pf:services:beagle:default_rules:v1';
+
+    public static function getDefaultRules()
+    {
+        return Cache::remember(self::DEFAULT_RULES_CACHE_KEY, now()->addDays(7), function() {
+            try {
+                $res = Http::withOptions(['allow_redirects' => false])
+                    ->timeout(5)
+                    ->connectTimeout(5)
+                    ->retry(2, 500)
+                    ->get('https://beagle.pixelfed.net/api/v1/common/suggestions/rules');
+            } catch (RequestException $e) {
+                return;
+            } catch (ConnectionException $e) {
+                return;
+            } catch (Exception $e) {
+                return;
+            }
+
+            if(!$res->ok()) {
+                return;
+            }
+
+            $json = $res->json();
+
+            if(!isset($json['rule_suggestions']) || !count($json['rule_suggestions'])) {
+                return [];
+            }
+            return $json['rule_suggestions'];
+        });
+    }
+
+}

+ 3 - 3
app/Services/LandingService.php

@@ -53,8 +53,8 @@ class LandingService
             'name' => config_cache('app.name'),
             'url' => config_cache('app.url'),
             'domain' => config('pixelfed.domain.app'),
-            'show_directory' => config_cache('instance.landing.show_directory'),
-            'show_explore_feed' => config_cache('instance.landing.show_explore'),
+            'show_directory' => (bool) config_cache('instance.landing.show_directory'),
+            'show_explore_feed' => (bool) config_cache('instance.landing.show_explore'),
             'open_registration' => (bool) $openReg,
             'curated_onboarding' => (bool) config_cache('instance.curated_registration.enabled'),
             'version' => config('pixelfed.version'),
@@ -85,7 +85,7 @@ class LandingService
                 'media_types' => config_cache('pixelfed.media_types'),
             ],
             'features' => [
-                'federation' => config_cache('federation.activitypub.enabled'),
+                'federation' => (bool) config_cache('federation.activitypub.enabled'),
                 'timelines' => [
                     'local' => true,
                     'network' => (bool) config_cache('federation.network_timeline'),

+ 56 - 61
app/Services/MediaStorageService.php

@@ -2,44 +2,38 @@
 
 namespace App\Services;
 
+use App\Jobs\AvatarPipeline\AvatarStorageCleanup;
+use App\Jobs\MediaPipeline\MediaDeletePipeline;
+use App\Media;
 use App\Util\ActivityPub\Helpers;
+use GuzzleHttp\Client;
+use GuzzleHttp\Exception\RequestException;
 use Illuminate\Http\File;
+use Illuminate\Support\Arr;
 use Illuminate\Support\Facades\Cache;
-use Illuminate\Support\Facades\Redis;
 use Illuminate\Support\Facades\Storage;
 use Illuminate\Support\Str;
-use App\Media;
-use App\Profile;
-use App\User;
-use GuzzleHttp\Client;
-use App\Services\AccountService;
-use App\Http\Controllers\AvatarController;
-use GuzzleHttp\Exception\RequestException;
-use App\Jobs\MediaPipeline\MediaDeletePipeline;
-use Illuminate\Support\Arr;
-use App\Jobs\AvatarPipeline\AvatarStorageCleanup;
-
-class MediaStorageService {
 
+class MediaStorageService
+{
     public static function store(Media $media)
     {
-        if(config_cache('pixelfed.cloud_storage') == true) {
+        if ((bool) config_cache('pixelfed.cloud_storage') == true) {
             (new self())->cloudStore($media);
         }
 
-        return;
     }
 
     public static function move(Media $media)
     {
-        if($media->remote_media) {
+        if ($media->remote_media) {
             return;
         }
 
-        if(config_cache('pixelfed.cloud_storage') == true) {
+        if ((bool) config_cache('pixelfed.cloud_storage') == true) {
             return (new self())->cloudMove($media);
         }
-        return;
+
     }
 
     public static function avatar($avatar, $local = false, $skipRecentCheck = false)
@@ -56,31 +50,31 @@ class MediaStorageService {
             return false;
         }
 
-        $h = Arr::mapWithKeys($r->getHeaders(), function($item, $key) {
+        $h = Arr::mapWithKeys($r->getHeaders(), function ($item, $key) {
             return [strtolower($key) => last($item)];
         });
 
-        if(!isset($h['content-length'], $h['content-type'])) {
+        if (! isset($h['content-length'], $h['content-type'])) {
             return false;
         }
 
         $len = (int) $h['content-length'];
         $mime = $h['content-type'];
 
-        if($len < 10 || $len > ((config_cache('pixelfed.max_photo_size') * 1000))) {
+        if ($len < 10 || $len > ((config_cache('pixelfed.max_photo_size') * 1000))) {
             return false;
         }
 
         return [
             'length' => $len,
-            'mime' => $mime
+            'mime' => $mime,
         ];
     }
 
     protected function cloudStore($media)
     {
-        if($media->remote_media == true) {
-            if(config('media.storage.remote.cloud')) {
+        if ($media->remote_media == true) {
+            if (config('media.storage.remote.cloud')) {
                 (new self())->remoteToCloud($media);
             }
         } else {
@@ -100,7 +94,7 @@ class MediaStorageService {
         $storagePath = implode('/', $p);
 
         $url = ResilientMediaStorageService::store($storagePath, $path, $name);
-        if($thumb) {
+        if ($thumb) {
             $thumbUrl = ResilientMediaStorageService::store($storagePath, $thumb, $thumbname);
             $media->thumbnail_url = $thumbUrl;
         }
@@ -108,8 +102,8 @@ class MediaStorageService {
         $media->optimized_url = $url;
         $media->replicated_at = now();
         $media->save();
-        if($media->status_id) {
-            Cache::forget('status:transformer:media:attachments:' . $media->status_id);
+        if ($media->status_id) {
+            Cache::forget('status:transformer:media:attachments:'.$media->status_id);
             MediaService::del($media->status_id);
             StatusService::del($media->status_id, false);
         }
@@ -119,20 +113,20 @@ class MediaStorageService {
     {
         $url = $media->remote_url;
 
-        if(!Helpers::validateUrl($url)) {
+        if (! Helpers::validateUrl($url)) {
             return;
         }
 
         $head = $this->head($media->remote_url);
 
-        if(!$head) {
+        if (! $head) {
             return;
         }
 
         $mimes = [
             'image/jpeg',
             'image/png',
-            'video/mp4'
+            'video/mp4',
         ];
 
         $mime = $head['mime'];
@@ -141,11 +135,11 @@ class MediaStorageService {
         $media->remote_media = true;
         $media->save();
 
-        if(!in_array($mime, $mimes)) {
+        if (! in_array($mime, $mimes)) {
             return;
         }
 
-        if($head['length'] >= $max_size) {
+        if ($head['length'] >= $max_size) {
             return;
         }
 
@@ -168,10 +162,10 @@ class MediaStorageService {
         }
 
         $base = MediaPathService::get($media->profile);
-        $path = Str::random(40) . $ext;
+        $path = Str::random(40).$ext;
         $tmpBase = storage_path('app/remcache/');
-        $tmpPath = $media->profile_id . '-' . $path;
-        $tmpName = $tmpBase . $tmpPath;
+        $tmpPath = $media->profile_id.'-'.$path;
+        $tmpName = $tmpBase.$tmpPath;
         $data = file_get_contents($url, false, null, 0, $head['length']);
         file_put_contents($tmpName, $data);
         $hash = hash_file('sha256', $tmpName);
@@ -186,8 +180,8 @@ class MediaStorageService {
         $media->replicated_at = now();
         $media->save();
 
-        if($media->status_id) {
-            Cache::forget('status:transformer:media:attachments:' . $media->status_id);
+        if ($media->status_id) {
+            Cache::forget('status:transformer:media:attachments:'.$media->status_id);
         }
 
         unlink($tmpName);
@@ -199,13 +193,13 @@ class MediaStorageService {
         $url = $avatar->remote_url;
         $driver = $local ? 'local' : config('filesystems.cloud');
 
-        if(empty($url) || Helpers::validateUrl($url) == false) {
+        if (empty($url) || Helpers::validateUrl($url) == false) {
             return;
         }
 
         $head = $this->head($url);
 
-        if($head == false) {
+        if ($head == false) {
             return;
         }
 
@@ -218,46 +212,47 @@ class MediaStorageService {
         $mime = $head['mime'];
         $max_size = (int) config('pixelfed.max_avatar_size') * 1000;
 
-        if(!$skipRecentCheck) {
-            if($avatar->last_fetched_at && $avatar->last_fetched_at->gt(now()->subMonths(3))) {
+        if (! $skipRecentCheck) {
+            if ($avatar->last_fetched_at && $avatar->last_fetched_at->gt(now()->subMonths(3))) {
                 return;
             }
         }
 
-        Cache::forget('avatar:' . $avatar->profile_id);
+        Cache::forget('avatar:'.$avatar->profile_id);
         AccountService::del($avatar->profile_id);
 
         // handle pleroma edge case
-        if(Str::endsWith($mime, '; charset=utf-8')) {
+        if (Str::endsWith($mime, '; charset=utf-8')) {
             $mime = str_replace('; charset=utf-8', '', $mime);
         }
 
-        if(!in_array($mime, $mimes)) {
+        if (! in_array($mime, $mimes)) {
             return;
         }
 
-        if($head['length'] >= $max_size) {
+        if ($head['length'] >= $max_size) {
             return;
         }
 
-        $base = ($local ? 'public/cache/' : 'cache/') . 'avatars/' . $avatar->profile_id;
+        $base = ($local ? 'public/cache/' : 'cache/').'avatars/'.$avatar->profile_id;
         $ext = $head['mime'] == 'image/jpeg' ? 'jpg' : 'png';
-        $path = 'avatar_' . strtolower(Str::random(random_int(3,6))) . '.' . $ext;
+        $path = 'avatar_'.strtolower(Str::random(random_int(3, 6))).'.'.$ext;
         $tmpBase = storage_path('app/remcache/');
-        $tmpPath = 'avatar_' . $avatar->profile_id . '-' . $path;
-        $tmpName = $tmpBase . $tmpPath;
+        $tmpPath = 'avatar_'.$avatar->profile_id.'-'.$path;
+        $tmpName = $tmpBase.$tmpPath;
         $data = @file_get_contents($url, false, null, 0, $head['length']);
-        if(!$data) {
+        if (! $data) {
             return;
         }
         file_put_contents($tmpName, $data);
 
-        $mimeCheck = Storage::mimeType('remcache/' . $tmpPath);
+        $mimeCheck = Storage::mimeType('remcache/'.$tmpPath);
 
-        if(!$mimeCheck || !in_array($mimeCheck, ['image/png', 'image/jpeg'])) {
+        if (! $mimeCheck || ! in_array($mimeCheck, ['image/png', 'image/jpeg'])) {
             $avatar->last_fetched_at = now();
             $avatar->save();
             unlink($tmpName);
+
             return;
         }
 
@@ -265,15 +260,15 @@ class MediaStorageService {
         $file = $disk->putFileAs($base, new File($tmpName), $path, 'public');
         $permalink = $disk->url($file);
 
-        $avatar->media_path = $base . '/' . $path;
+        $avatar->media_path = $base.'/'.$path;
         $avatar->is_remote = true;
-        $avatar->cdn_url = $local ? config('app.url') . $permalink : $permalink;
+        $avatar->cdn_url = $local ? config('app.url').$permalink : $permalink;
         $avatar->size = $head['length'];
         $avatar->change_count = $avatar->change_count + 1;
         $avatar->last_fetched_at = now();
         $avatar->save();
 
-        Cache::forget('avatar:' . $avatar->profile_id);
+        Cache::forget('avatar:'.$avatar->profile_id);
         AccountService::del($avatar->profile_id);
         AvatarStorageCleanup::dispatch($avatar)->onQueue($queue)->delay(now()->addMinutes(random_int(3, 15)));
 
@@ -282,7 +277,7 @@ class MediaStorageService {
 
     public static function delete(Media $media, $confirm = false)
     {
-        if(!$confirm) {
+        if (! $confirm) {
             return;
         }
         MediaDeletePipeline::dispatch($media)->onQueue('mmo');
@@ -290,13 +285,13 @@ class MediaStorageService {
 
     protected function cloudMove($media)
     {
-        if(!Storage::exists($media->media_path)) {
+        if (! Storage::exists($media->media_path)) {
             return 'invalid file';
         }
 
         $path = storage_path('app/'.$media->media_path);
         $thumb = false;
-        if($media->thumbnail_path) {
+        if ($media->thumbnail_path) {
             $thumb = storage_path('app/'.$media->thumbnail_path);
             $pt = explode('/', $media->thumbnail_path);
             $thumbname = array_pop($pt);
@@ -307,7 +302,7 @@ class MediaStorageService {
         $storagePath = implode('/', $p);
 
         $url = ResilientMediaStorageService::store($storagePath, $path, $name);
-        if($thumb) {
+        if ($thumb) {
             $thumbUrl = ResilientMediaStorageService::store($storagePath, $thumb, $thumbname);
             $media->thumbnail_url = $thumbUrl;
         }
@@ -316,8 +311,8 @@ class MediaStorageService {
         $media->replicated_at = now();
         $media->save();
 
-        if($media->status_id) {
-            Cache::forget('status:transformer:media:attachments:' . $media->status_id);
+        if ($media->status_id) {
+            Cache::forget('status:transformer:media:attachments:'.$media->status_id);
             MediaService::del($media->status_id);
             StatusService::del($media->status_id, false);
         }

+ 164 - 167
app/Util/ActivityPub/Helpers.php

@@ -2,49 +2,34 @@
 
 namespace App\Util\ActivityPub;
 
-use DB, Cache, Purify, Storage, Request, Validator;
-use App\{
-    Activity,
-    Follower,
-    Instance,
-    Like,
-    Media,
-    Notification,
-    Profile,
-    Status
-};
-use Zttp\Zttp;
-use Carbon\Carbon;
-use GuzzleHttp\Client;
-use Illuminate\Http\File;
-use Illuminate\Validation\Rule;
-use App\Jobs\AvatarPipeline\CreateAvatar;
-use App\Jobs\RemoteFollowPipeline\RemoteFollowImportRecent;
-use App\Jobs\ImageOptimizePipeline\{ImageOptimize,ImageThumbnail};
-use App\Jobs\StatusPipeline\NewStatusPipeline;
+use App\Instance;
+use App\Jobs\AvatarPipeline\RemoteAvatarFetch;
+use App\Jobs\HomeFeedPipeline\FeedInsertRemotePipeline;
+use App\Jobs\MediaPipeline\MediaStoragePipeline;
 use App\Jobs\StatusPipeline\StatusReplyPipeline;
 use App\Jobs\StatusPipeline\StatusTagsPipeline;
-use App\Util\ActivityPub\HttpSignature;
-use Illuminate\Support\Str;
-use App\Services\ActivityPubFetchService;
+use App\Media;
+use App\Models\Poll;
+use App\Profile;
+use App\Services\Account\AccountStatService;
 use App\Services\ActivityPubDeliveryService;
-use App\Services\CustomEmojiService;
+use App\Services\ActivityPubFetchService;
+use App\Services\DomainService;
 use App\Services\InstanceService;
 use App\Services\MediaPathService;
-use App\Services\MediaStorageService;
 use App\Services\NetworkTimelineService;
-use App\Jobs\MediaPipeline\MediaStoragePipeline;
-use App\Jobs\AvatarPipeline\RemoteAvatarFetch;
-use App\Jobs\HomeFeedPipeline\FeedInsertRemotePipeline;
-use App\Util\Media\License;
-use App\Models\Poll;
-use Illuminate\Contracts\Cache\LockTimeoutException;
-use App\Services\DomainService;
 use App\Services\UserFilterService;
-use App\Services\Account\AccountStatService;
-
-class Helpers {
+use App\Status;
+use App\Util\Media\License;
+use Cache;
+use Carbon\Carbon;
+use Illuminate\Support\Str;
+use Illuminate\Validation\Rule;
+use Purify;
+use Validator;
 
+class Helpers
+{
     public static function validateObject($data)
     {
         $verbs = ['Create', 'Announce', 'Like', 'Follow', 'Delete', 'Accept', 'Reject', 'Undo', 'Tombstone'];
@@ -53,14 +38,14 @@ class Helpers {
             'type' => [
                 'required',
                 'string',
-                Rule::in($verbs)
+                Rule::in($verbs),
             ],
             'id' => 'required|string',
             'actor' => 'required|string|url',
             'object' => 'required',
             'object.type' => 'required_if:type,Create',
             'object.attributedTo' => 'required_if:type,Create|url',
-            'published' => 'required_if:type,Create|date'
+            'published' => 'required_if:type,Create|date',
         ])->passes();
 
         return $valid;
@@ -68,8 +53,8 @@ class Helpers {
 
     public static function verifyAttachments($data)
     {
-        if(!isset($data['object']) || empty($data['object'])) {
-            $data = ['object'=>$data];
+        if (! isset($data['object']) || empty($data['object'])) {
+            $data = ['object' => $data];
         }
 
         $activity = $data['object'];
@@ -80,7 +65,7 @@ class Helpers {
         // Peertube
         // $mediaTypes = in_array('video/mp4', $mimeTypes) ? ['Document', 'Image', 'Video', 'Link'] : ['Document', 'Image'];
 
-        if(!isset($activity['attachment']) || empty($activity['attachment'])) {
+        if (! isset($activity['attachment']) || empty($activity['attachment'])) {
             return false;
         }
 
@@ -100,13 +85,13 @@ class Helpers {
             '*.type' => [
                 'required',
                 'string',
-                Rule::in($mediaTypes)
+                Rule::in($mediaTypes),
             ],
             '*.url' => 'required|url',
-            '*.mediaType'  => [
+            '*.mediaType' => [
                 'required',
                 'string',
-                Rule::in($mimeTypes)
+                Rule::in($mimeTypes),
             ],
             '*.name' => 'sometimes|nullable|string',
             '*.blurhash' => 'sometimes|nullable|string|min:6|max:164',
@@ -119,7 +104,7 @@ class Helpers {
 
     public static function normalizeAudience($data, $localOnly = true)
     {
-        if(!isset($data['to'])) {
+        if (! isset($data['to'])) {
             return;
         }
 
@@ -128,32 +113,35 @@ class Helpers {
         $audience['cc'] = [];
         $scope = 'private';
 
-        if(is_array($data['to']) && !empty($data['to'])) {
+        if (is_array($data['to']) && ! empty($data['to'])) {
             foreach ($data['to'] as $to) {
-                if($to == 'https://www.w3.org/ns/activitystreams#Public') {
+                if ($to == 'https://www.w3.org/ns/activitystreams#Public') {
                     $scope = 'public';
+
                     continue;
                 }
                 $url = $localOnly ? self::validateLocalUrl($to) : self::validateUrl($to);
-                if($url != false) {
+                if ($url != false) {
                     array_push($audience['to'], $url);
                 }
             }
         }
 
-        if(is_array($data['cc']) && !empty($data['cc'])) {
+        if (is_array($data['cc']) && ! empty($data['cc'])) {
             foreach ($data['cc'] as $cc) {
-                if($cc == 'https://www.w3.org/ns/activitystreams#Public') {
+                if ($cc == 'https://www.w3.org/ns/activitystreams#Public') {
                     $scope = 'unlisted';
+
                     continue;
                 }
                 $url = $localOnly ? self::validateLocalUrl($cc) : self::validateUrl($cc);
-                if($url != false) {
+                if ($url != false) {
                     array_push($audience['cc'], $url);
                 }
             }
         }
         $audience['scope'] = $scope;
+
         return $audience;
     }
 
@@ -161,56 +149,57 @@ class Helpers {
     {
         $audience = self::normalizeAudience($data);
         $url = $profile->permalink();
+
         return in_array($url, $audience['to']) || in_array($url, $audience['cc']);
     }
 
     public static function validateUrl($url)
     {
-        if(is_array($url)) {
+        if (is_array($url)) {
             $url = $url[0];
         }
 
         $hash = hash('sha256', $url);
         $key = "helpers:url:valid:sha256-{$hash}";
 
-        $valid = Cache::remember($key, 900, function() use($url) {
+        $valid = Cache::remember($key, 900, function () use ($url) {
             $localhosts = [
-                '127.0.0.1', 'localhost', '::1'
+                '127.0.0.1', 'localhost', '::1',
             ];
 
-            if(strtolower(mb_substr($url, 0, 8)) !== 'https://') {
+            if (strtolower(mb_substr($url, 0, 8)) !== 'https://') {
                 return false;
             }
 
-            if(substr_count($url, '://') !== 1) {
+            if (substr_count($url, '://') !== 1) {
                 return false;
             }
 
-            if(mb_substr($url, 0, 8) !== 'https://') {
-                $url = 'https://' . substr($url, 8);
+            if (mb_substr($url, 0, 8) !== 'https://') {
+                $url = 'https://'.substr($url, 8);
             }
 
             $valid = filter_var($url, FILTER_VALIDATE_URL);
 
-            if(!$valid) {
+            if (! $valid) {
                 return false;
             }
 
             $host = parse_url($valid, PHP_URL_HOST);
 
-            if(in_array($host, $localhosts)) {
+            if (in_array($host, $localhosts)) {
                 return false;
             }
 
-            if(config('security.url.verify_dns')) {
-                if(DomainService::hasValidDns($host) === false) {
+            if (config('security.url.verify_dns')) {
+                if (DomainService::hasValidDns($host) === false) {
                     return false;
                 }
             }
 
-            if(app()->environment() === 'production') {
+            if (app()->environment() === 'production') {
                 $bannedInstances = InstanceService::getBannedDomains();
-                if(in_array($host, $bannedInstances)) {
+                if (in_array($host, $bannedInstances)) {
                     return false;
                 }
             }
@@ -224,12 +213,14 @@ class Helpers {
     public static function validateLocalUrl($url)
     {
         $url = self::validateUrl($url);
-        if($url == true) {
+        if ($url == true) {
             $domain = config('pixelfed.domain.app');
             $host = parse_url($url, PHP_URL_HOST);
             $url = strtolower($domain) === strtolower($host) ? $url : false;
+
             return $url;
         }
+
         return false;
     }
 
@@ -237,15 +228,16 @@ class Helpers {
     {
         $version = config('pixelfed.version');
         $url = config('app.url');
+
         return [
-            'Accept'     => 'application/activity+json',
+            'Accept' => 'application/activity+json',
             'User-Agent' => "(Pixelfed/{$version}; +{$url})",
         ];
     }
 
     public static function fetchFromUrl($url = false)
     {
-        if(self::validateUrl($url) == false) {
+        if (self::validateUrl($url) == false) {
             return;
         }
 
@@ -253,13 +245,13 @@ class Helpers {
         $key = "helpers:url:fetcher:sha256-{$hash}";
         $ttl = now()->addMinutes(15);
 
-        return Cache::remember($key, $ttl, function() use($url) {
+        return Cache::remember($key, $ttl, function () use ($url) {
             $res = ActivityPubFetchService::get($url);
-            if(!$res || empty($res)) {
+            if (! $res || empty($res)) {
                 return false;
             }
             $res = json_decode($res, true, 8);
-            if(json_last_error() == JSON_ERROR_NONE) {
+            if (json_last_error() == JSON_ERROR_NONE) {
                 return $res;
             } else {
                 return false;
@@ -274,12 +266,12 @@ class Helpers {
 
     public static function pluckval($val)
     {
-        if(is_string($val)) {
+        if (is_string($val)) {
             return $val;
         }
 
-        if(is_array($val)) {
-            return !empty($val) ? head($val) : null;
+        if (is_array($val)) {
+            return ! empty($val) ? head($val) : null;
         }
 
         return null;
@@ -288,51 +280,52 @@ class Helpers {
     public static function statusFirstOrFetch($url, $replyTo = false)
     {
         $url = self::validateUrl($url);
-        if($url == false) {
+        if ($url == false) {
             return;
         }
 
         $host = parse_url($url, PHP_URL_HOST);
         $local = config('pixelfed.domain.app') == $host ? true : false;
 
-        if($local) {
+        if ($local) {
             $id = (int) last(explode('/', $url));
-            return Status::whereNotIn('scope', ['draft','archived'])->findOrFail($id);
+
+            return Status::whereNotIn('scope', ['draft', 'archived'])->findOrFail($id);
         }
 
-        $cached = Status::whereNotIn('scope', ['draft','archived'])
+        $cached = Status::whereNotIn('scope', ['draft', 'archived'])
             ->whereUri($url)
             ->orWhere('object_url', $url)
             ->first();
 
-        if($cached) {
+        if ($cached) {
             return $cached;
         }
 
         $res = self::fetchFromUrl($url);
 
-        if(!$res || empty($res) || isset($res['error']) || !isset($res['@context']) || !isset($res['published']) ) {
+        if (! $res || empty($res) || isset($res['error']) || ! isset($res['@context']) || ! isset($res['published'])) {
             return;
         }
 
-        if(config('autospam.live_filters.enabled')) {
+        if (config('autospam.live_filters.enabled')) {
             $filters = config('autospam.live_filters.filters');
-            if(!empty($filters) && isset($res['content']) && !empty($res['content']) && strlen($filters) > 3) {
+            if (! empty($filters) && isset($res['content']) && ! empty($res['content']) && strlen($filters) > 3) {
                 $filters = array_map('trim', explode(',', $filters));
                 $content = $res['content'];
-                foreach($filters as $filter) {
+                foreach ($filters as $filter) {
                     $filter = trim(strtolower($filter));
-                    if(!$filter || !strlen($filter)) {
+                    if (! $filter || ! strlen($filter)) {
                         continue;
                     }
-                    if(str_contains(strtolower($content), $filter)) {
+                    if (str_contains(strtolower($content), $filter)) {
                         return;
                     }
                 }
             }
         }
 
-        if(isset($res['object'])) {
+        if (isset($res['object'])) {
             $activity = $res;
         } else {
             $activity = ['object' => $res];
@@ -342,37 +335,37 @@ class Helpers {
 
         $cw = isset($res['sensitive']) ? (bool) $res['sensitive'] : false;
 
-        if(isset($res['to']) == true) {
-            if(is_array($res['to']) && in_array('https://www.w3.org/ns/activitystreams#Public', $res['to'])) {
+        if (isset($res['to']) == true) {
+            if (is_array($res['to']) && in_array('https://www.w3.org/ns/activitystreams#Public', $res['to'])) {
                 $scope = 'public';
             }
-            if(is_string($res['to']) && 'https://www.w3.org/ns/activitystreams#Public' == $res['to']) {
+            if (is_string($res['to']) && $res['to'] == 'https://www.w3.org/ns/activitystreams#Public') {
                 $scope = 'public';
             }
         }
 
-        if(isset($res['cc']) == true) {
-            if(is_array($res['cc']) && in_array('https://www.w3.org/ns/activitystreams#Public', $res['cc'])) {
+        if (isset($res['cc']) == true) {
+            if (is_array($res['cc']) && in_array('https://www.w3.org/ns/activitystreams#Public', $res['cc'])) {
                 $scope = 'unlisted';
             }
-            if(is_string($res['cc']) && 'https://www.w3.org/ns/activitystreams#Public' == $res['cc']) {
+            if (is_string($res['cc']) && $res['cc'] == 'https://www.w3.org/ns/activitystreams#Public') {
                 $scope = 'unlisted';
             }
         }
 
-        if(config('costar.enabled') == true) {
+        if (config('costar.enabled') == true) {
             $blockedKeywords = config('costar.keyword.block');
-            if($blockedKeywords !== null) {
+            if ($blockedKeywords !== null) {
                 $keywords = config('costar.keyword.block');
-                foreach($keywords as $kw) {
-                    if(Str::contains($res['content'], $kw) == true) {
+                foreach ($keywords as $kw) {
+                    if (Str::contains($res['content'], $kw) == true) {
                         return;
                     }
                 }
             }
 
             $unlisted = config('costar.domain.unlisted');
-            if(in_array(parse_url($url, PHP_URL_HOST), $unlisted) == true) {
+            if (in_array(parse_url($url, PHP_URL_HOST), $unlisted) == true) {
                 $unlisted = true;
                 $scope = 'unlisted';
             } else {
@@ -380,7 +373,7 @@ class Helpers {
             }
 
             $cwDomains = config('costar.domain.cw');
-            if(in_array(parse_url($url, PHP_URL_HOST), $cwDomains) == true) {
+            if (in_array(parse_url($url, PHP_URL_HOST), $cwDomains) == true) {
                 $cw = true;
             }
         }
@@ -389,15 +382,15 @@ class Helpers {
         $idDomain = parse_url($id, PHP_URL_HOST);
         $urlDomain = parse_url($url, PHP_URL_HOST);
 
-        if($idDomain && $urlDomain && strtolower($idDomain) !== strtolower($urlDomain)) {
+        if ($idDomain && $urlDomain && strtolower($idDomain) !== strtolower($urlDomain)) {
             return;
         }
 
-        if(!self::validateUrl($id)) {
+        if (! self::validateUrl($id)) {
             return;
         }
 
-        if(!isset($activity['object']['attributedTo'])) {
+        if (! isset($activity['object']['attributedTo'])) {
             return;
         }
 
@@ -405,39 +398,38 @@ class Helpers {
             $activity['object']['attributedTo'] :
             (is_array($activity['object']['attributedTo']) ?
                 collect($activity['object']['attributedTo'])
-                    ->filter(function($o) {
+                    ->filter(function ($o) {
                         return $o && isset($o['type']) && $o['type'] == 'Person';
                     })
                     ->pluck('id')
                     ->first() : null
             );
 
-        if($attributedTo) {
+        if ($attributedTo) {
             $actorDomain = parse_url($attributedTo, PHP_URL_HOST);
-            if(!self::validateUrl($attributedTo) ||
+            if (! self::validateUrl($attributedTo) ||
                 $idDomain !== $actorDomain ||
                 $actorDomain !== $urlDomain
-            )
-            {
+            ) {
                 return;
             }
         }
 
-        if($idDomain !== $urlDomain) {
+        if ($idDomain !== $urlDomain) {
             return;
         }
 
         $profile = self::profileFirstOrNew($attributedTo);
 
-        if(!$profile) {
+        if (! $profile) {
             return;
         }
 
-        if(isset($activity['object']['inReplyTo']) && !empty($activity['object']['inReplyTo']) || $replyTo == true) {
+        if (isset($activity['object']['inReplyTo']) && ! empty($activity['object']['inReplyTo']) || $replyTo == true) {
             $reply_to = self::statusFirstOrFetch(self::pluckval($activity['object']['inReplyTo']), false);
-            if($reply_to) {
+            if ($reply_to) {
                 $blocks = UserFilterService::blocks($reply_to->profile_id);
-                if(in_array($profile->id, $blocks)) {
+                if (in_array($profile->id, $blocks)) {
                     return;
                 }
             }
@@ -447,15 +439,15 @@ class Helpers {
         }
         $ts = self::pluckval($res['published']);
 
-        if($scope == 'public' && in_array($urlDomain, InstanceService::getUnlistedDomains())) {
+        if ($scope == 'public' && in_array($urlDomain, InstanceService::getUnlistedDomains())) {
             $scope = 'unlisted';
         }
 
-        if(in_array($urlDomain, InstanceService::getNsfwDomains())) {
+        if (in_array($urlDomain, InstanceService::getNsfwDomains())) {
             $cw = true;
         }
 
-        if($res['type'] === 'Question') {
+        if ($res['type'] === 'Question') {
             $status = self::storePoll(
                 $profile,
                 $res,
@@ -466,6 +458,7 @@ class Helpers {
                 $scope,
                 $id
             );
+
             return $status;
         } else {
             $status = self::storeStatus($url, $profile, $res);
@@ -482,12 +475,12 @@ class Helpers {
         $idDomain = parse_url($id, PHP_URL_HOST);
         $urlDomain = parse_url($url, PHP_URL_HOST);
         $originalUrlDomain = parse_url($originalUrl, PHP_URL_HOST);
-        if(!self::validateUrl($id) || !self::validateUrl($url)) {
+        if (! self::validateUrl($id) || ! self::validateUrl($url)) {
             return;
         }
 
-        if( strtolower($originalUrlDomain) !== strtolower($idDomain) ||
-            strtolower($originalUrlDomain) !== strtolower($urlDomain) ) {
+        if (strtolower($originalUrlDomain) !== strtolower($idDomain) ||
+            strtolower($originalUrlDomain) !== strtolower($urlDomain)) {
             return;
         }
 
@@ -498,21 +491,21 @@ class Helpers {
         $cw = self::getSensitive($activity, $url);
         $pid = is_object($profile) ? $profile->id : (is_array($profile) ? $profile['id'] : null);
         $isUnlisted = is_object($profile) ? $profile->unlisted : (is_array($profile) ? $profile['unlisted'] : false);
-        $commentsDisabled = isset($activity['commentsEnabled']) ? !boolval($activity['commentsEnabled']) : false;
+        $commentsDisabled = isset($activity['commentsEnabled']) ? ! boolval($activity['commentsEnabled']) : false;
 
-        if(!$pid) {
+        if (! $pid) {
             return;
         }
 
-        if($scope == 'public') {
-            if($isUnlisted == true) {
+        if ($scope == 'public') {
+            if ($isUnlisted == true) {
                 $scope = 'unlisted';
             }
         }
 
         $status = Status::updateOrCreate(
             [
-                'uri' => $url
+                'uri' => $url,
             ], [
                 'profile_id' => $pid,
                 'url' => $url,
@@ -527,24 +520,24 @@ class Helpers {
                 'visibility' => $scope,
                 'cw_summary' => ($cw == true && isset($activity['summary']) ?
                     Purify::clean(strip_tags($activity['summary'])) : null),
-                'comments_disabled' => $commentsDisabled
+                'comments_disabled' => $commentsDisabled,
             ]
         );
 
-        if($reply_to == null) {
+        if ($reply_to == null) {
             self::importNoteAttachment($activity, $status);
         } else {
-            if(isset($activity['attachment']) && !empty($activity['attachment'])) {
+            if (isset($activity['attachment']) && ! empty($activity['attachment'])) {
                 self::importNoteAttachment($activity, $status);
             }
             StatusReplyPipeline::dispatch($status);
         }
 
-        if(isset($activity['tag']) && is_array($activity['tag']) && !empty($activity['tag'])) {
+        if (isset($activity['tag']) && is_array($activity['tag']) && ! empty($activity['tag'])) {
             StatusTagsPipeline::dispatch($activity, $status);
         }
 
-        if( config('instance.timeline.network.cached') &&
+        if (config('instance.timeline.network.cached') &&
             $status->in_reply_to_id === null &&
             $status->reblog_of_id === null &&
             in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) &&
@@ -556,8 +549,8 @@ class Helpers {
                 ->unique()
                 ->values()
                 ->toArray();
-            if(!in_array($urlDomain, $filteredDomains)) {
-                if(!$isUnlisted) {
+            if (! in_array($urlDomain, $filteredDomains)) {
+                if (! $isUnlisted) {
                     NetworkTimelineService::add($status->id);
                 }
             }
@@ -565,7 +558,7 @@ class Helpers {
 
         AccountStatService::incrementPostCount($pid);
 
-        if( $status->in_reply_to_id === null &&
+        if ($status->in_reply_to_id === null &&
             in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
         ) {
             FeedInsertRemotePipeline::dispatch($status->id, $pid)->onQueue('feed');
@@ -576,14 +569,14 @@ class Helpers {
 
     public static function getSensitive($activity, $url)
     {
-        if(!$url || !strlen($url)) {
+        if (! $url || ! strlen($url)) {
             return true;
         }
 
         $urlDomain = parse_url($url, PHP_URL_HOST);
         $cw = isset($activity['sensitive']) ? (bool) $activity['sensitive'] : false;
 
-        if(in_array($urlDomain, InstanceService::getNsfwDomains())) {
+        if (in_array($urlDomain, InstanceService::getNsfwDomains())) {
             $cw = true;
         }
 
@@ -593,13 +586,13 @@ class Helpers {
     public static function getReplyTo($activity)
     {
         $reply_to = null;
-        $inReplyTo = isset($activity['inReplyTo']) && !empty($activity['inReplyTo']) ?
+        $inReplyTo = isset($activity['inReplyTo']) && ! empty($activity['inReplyTo']) ?
             self::pluckval($activity['inReplyTo']) :
             false;
 
-        if($inReplyTo) {
+        if ($inReplyTo) {
             $reply_to = self::statusFirstOrFetch($inReplyTo);
-            if($reply_to) {
+            if ($reply_to) {
                 $reply_to = optional($reply_to)->id;
             }
         } else {
@@ -616,25 +609,25 @@ class Helpers {
         $urlDomain = parse_url(self::pluckval($url), PHP_URL_HOST);
         $scope = 'private';
 
-        if(isset($activity['to']) == true) {
-            if(is_array($activity['to']) && in_array('https://www.w3.org/ns/activitystreams#Public', $activity['to'])) {
+        if (isset($activity['to']) == true) {
+            if (is_array($activity['to']) && in_array('https://www.w3.org/ns/activitystreams#Public', $activity['to'])) {
                 $scope = 'public';
             }
-            if(is_string($activity['to']) && 'https://www.w3.org/ns/activitystreams#Public' == $activity['to']) {
+            if (is_string($activity['to']) && $activity['to'] == 'https://www.w3.org/ns/activitystreams#Public') {
                 $scope = 'public';
             }
         }
 
-        if(isset($activity['cc']) == true) {
-            if(is_array($activity['cc']) && in_array('https://www.w3.org/ns/activitystreams#Public', $activity['cc'])) {
+        if (isset($activity['cc']) == true) {
+            if (is_array($activity['cc']) && in_array('https://www.w3.org/ns/activitystreams#Public', $activity['cc'])) {
                 $scope = 'unlisted';
             }
-            if(is_string($activity['cc']) && 'https://www.w3.org/ns/activitystreams#Public' == $activity['cc']) {
+            if (is_string($activity['cc']) && $activity['cc'] == 'https://www.w3.org/ns/activitystreams#Public') {
                 $scope = 'unlisted';
             }
         }
 
-        if($scope == 'public' && in_array($urlDomain, InstanceService::getUnlistedDomains())) {
+        if ($scope == 'public' && in_array($urlDomain, InstanceService::getUnlistedDomains())) {
             $scope = 'unlisted';
         }
 
@@ -643,15 +636,15 @@ class Helpers {
 
     private static function storePoll($profile, $res, $url, $ts, $reply_to, $cw, $scope, $id)
     {
-        if(!isset($res['endTime']) || !isset($res['oneOf']) || !is_array($res['oneOf']) || count($res['oneOf']) > 4) {
+        if (! isset($res['endTime']) || ! isset($res['oneOf']) || ! is_array($res['oneOf']) || count($res['oneOf']) > 4) {
             return;
         }
 
-        $options = collect($res['oneOf'])->map(function($option) {
+        $options = collect($res['oneOf'])->map(function ($option) {
             return $option['name'];
         })->toArray();
 
-        $cachedTallies = collect($res['oneOf'])->map(function($option) {
+        $cachedTallies = collect($res['oneOf'])->map(function ($option) {
             return $option['replies']['totalItems'] ?? 0;
         })->toArray();
 
@@ -697,9 +690,10 @@ class Helpers {
 
     public static function importNoteAttachment($data, Status $status)
     {
-        if(self::verifyAttachments($data) == false) {
+        if (self::verifyAttachments($data) == false) {
             // \Log::info('importNoteAttachment::failedVerification.', [$data['id']]);
             $status->viewType();
+
             return;
         }
         $attachments = isset($data['object']) ? $data['object']['attachment'] : $data['attachment'];
@@ -712,11 +706,11 @@ class Helpers {
         $storagePath = MediaPathService::get($user, 2);
         $allowed = explode(',', config_cache('pixelfed.media_types'));
 
-        foreach($attachments as $key => $media) {
+        foreach ($attachments as $key => $media) {
             $type = $media['mediaType'];
             $url = $media['url'];
             $valid = self::validateUrl($url);
-            if(in_array($type, $allowed) == false || $valid == false) {
+            if (in_array($type, $allowed) == false || $valid == false) {
                 continue;
             }
             $blurhash = isset($media['blurhash']) ? $media['blurhash'] : null;
@@ -735,50 +729,52 @@ class Helpers {
             $media->remote_url = $url;
             $media->caption = $caption;
             $media->order = $key + 1;
-            if($width) {
+            if ($width) {
                 $media->width = $width;
             }
-            if($height) {
+            if ($height) {
                 $media->height = $height;
             }
-            if($license) {
+            if ($license) {
                 $media->license = $license;
             }
             $media->mime = $type;
             $media->version = 3;
             $media->save();
 
-            if(config_cache('pixelfed.cloud_storage') == true) {
+            if ((bool) config_cache('pixelfed.cloud_storage') == true) {
                 MediaStoragePipeline::dispatch($media);
             }
         }
 
         $status->viewType();
-        return;
+
     }
 
     public static function profileFirstOrNew($url)
     {
         $url = self::validateUrl($url);
-        if($url == false) {
+        if ($url == false) {
             return;
         }
 
         $host = parse_url($url, PHP_URL_HOST);
         $local = config('pixelfed.domain.app') == $host ? true : false;
 
-        if($local == true) {
+        if ($local == true) {
             $id = last(explode('/', $url));
+
             return Profile::whereNull('status')
                 ->whereNull('domain')
                 ->whereUsername($id)
                 ->firstOrFail();
         }
 
-        if($profile = Profile::whereRemoteUrl($url)->first()) {
-            if($profile->last_fetched_at && $profile->last_fetched_at->lt(now()->subHours(24))) {
+        if ($profile = Profile::whereRemoteUrl($url)->first()) {
+            if ($profile->last_fetched_at && $profile->last_fetched_at->lt(now()->subHours(24))) {
                 return self::profileUpdateOrCreate($url);
             }
+
             return $profile;
         }
 
@@ -788,42 +784,42 @@ class Helpers {
     public static function profileUpdateOrCreate($url)
     {
         $res = self::fetchProfileFromUrl($url);
-        if(!$res || isset($res['id']) == false) {
+        if (! $res || isset($res['id']) == false) {
             return;
         }
         $urlDomain = parse_url($url, PHP_URL_HOST);
         $domain = parse_url($res['id'], PHP_URL_HOST);
-        if(strtolower($urlDomain) !== strtolower($domain)) {
+        if (strtolower($urlDomain) !== strtolower($domain)) {
             return;
         }
-        if(!isset($res['preferredUsername']) && !isset($res['nickname'])) {
+        if (! isset($res['preferredUsername']) && ! isset($res['nickname'])) {
             return;
         }
         // skip invalid usernames
-        if(!ctype_alnum($res['preferredUsername'])) {
+        if (! ctype_alnum($res['preferredUsername'])) {
             $tmpUsername = str_replace(['_', '.', '-'], '', $res['preferredUsername']);
-            if(!ctype_alnum($tmpUsername)) {
+            if (! ctype_alnum($tmpUsername)) {
                 return;
             }
         }
         $username = (string) Purify::clean($res['preferredUsername'] ?? $res['nickname']);
-        if(empty($username)) {
+        if (empty($username)) {
             return;
         }
         $remoteUsername = $username;
         $webfinger = "@{$username}@{$domain}";
 
-        if(!self::validateUrl($res['inbox'])) {
+        if (! self::validateUrl($res['inbox'])) {
             return;
         }
-        if(!self::validateUrl($res['id'])) {
+        if (! self::validateUrl($res['id'])) {
             return;
         }
 
         $instance = Instance::updateOrCreate([
-            'domain' => $domain
+            'domain' => $domain,
         ]);
-        if($instance->wasRecentlyCreated == true) {
+        if ($instance->wasRecentlyCreated == true) {
             \App\Jobs\InstancePipeline\FetchNodeinfoPipeline::dispatch($instance)->onQueue('low');
         }
 
@@ -846,13 +842,14 @@ class Helpers {
             ]
         );
 
-        if( $profile->last_fetched_at == null ||
+        if ($profile->last_fetched_at == null ||
             $profile->last_fetched_at->lt(now()->subMonths(3))
         ) {
             RemoteAvatarFetch::dispatch($profile);
         }
         $profile->last_fetched_at = now();
         $profile->save();
+
         return $profile;
     }
 
@@ -863,7 +860,7 @@ class Helpers {
 
     public static function sendSignedObject($profile, $url, $body)
     {
-        if(app()->environment() !== 'production') {
+        if (app()->environment() !== 'production') {
             return;
         }
         ActivityPubDeliveryService::queue()

+ 24 - 26
app/Util/ActivityPub/Outbox.php

@@ -2,34 +2,32 @@
 
 namespace App\Util\ActivityPub;
 
-use App\Profile;
-use App\Status;
-use League\Fractal;
 use App\Http\Controllers\ProfileController;
-use App\Transformer\ActivityPub\ProfileOutbox;
+use App\Status;
 use App\Transformer\ActivityPub\Verb\CreateNote;
+use League\Fractal;
 
-class Outbox {
-
-	public static function get($profile)
-	{
-        abort_if(!config_cache('federation.activitypub.enabled'), 404);
-        abort_if(!config('federation.activitypub.outbox'), 404);
+class Outbox
+{
+    public static function get($profile)
+    {
+        abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404);
+        abort_if(! config('federation.activitypub.outbox'), 404);
 
-        if($profile->status != null) {
+        if ($profile->status != null) {
             return ProfileController::accountCheck($profile);
         }
 
-        if($profile->is_private) {
-            return ['error'=>'403', 'msg' => 'private profile'];
+        if ($profile->is_private) {
+            return ['error' => '403', 'msg' => 'private profile'];
         }
 
         $timeline = $profile
-                    ->statuses()
-                    ->whereScope('public')
-                    ->orderBy('created_at', 'desc')
-                    ->take(10)
-                    ->get();
+            ->statuses()
+            ->whereScope('public')
+            ->orderBy('created_at', 'desc')
+            ->take(10)
+            ->get();
 
         $count = Status::whereProfileId($profile->id)->count();
 
@@ -38,14 +36,14 @@ class Outbox {
         $res = $fractal->createData($resource)->toArray();
 
         $outbox = [
-            '@context'     => 'https://www.w3.org/ns/activitystreams',
-            '_debug'       => 'Outbox only supports latest 10 objects, pagination is not supported',
-            'id'           => $profile->permalink('/outbox'),
-            'type'         => 'OrderedCollection',
-            'totalItems'   => $count,
-            'orderedItems' => $res['data']
+            '@context' => 'https://www.w3.org/ns/activitystreams',
+            '_debug' => 'Outbox only supports latest 10 objects, pagination is not supported',
+            'id' => $profile->permalink('/outbox'),
+            'type' => 'OrderedCollection',
+            'totalItems' => $count,
+            'orderedItems' => $res['data'],
         ];
-        return $outbox;
-	}
 
+        return $outbox;
+    }
 }

+ 4 - 4
app/Util/Site/Config.php

@@ -30,16 +30,16 @@ class Config
                 'version' => config('pixelfed.version'),
                 'open_registration' => (bool) config_cache('pixelfed.open_registration'),
                 'uploader' => [
-                    'max_photo_size' => (int) config('pixelfed.max_photo_size'),
+                    'max_photo_size' => (int) config_cache('pixelfed.max_photo_size'),
                     'max_caption_length' => (int) config_cache('pixelfed.max_caption_length'),
                     'max_altext_length' => (int) config_cache('pixelfed.max_altext_length', 150),
                     'album_limit' => (int) config_cache('pixelfed.max_album_length'),
                     'image_quality' => (int) config_cache('pixelfed.image_quality'),
 
-                    'max_collection_length' => (int) config('pixelfed.max_collection_length', 18),
+                    'max_collection_length' => (int) config_cache('pixelfed.max_collection_length', 18),
 
-                    'optimize_image' => (bool) config('pixelfed.optimize_image'),
-                    'optimize_video' => (bool) config('pixelfed.optimize_video'),
+                    'optimize_image' => (bool) config_cache('pixelfed.optimize_image'),
+                    'optimize_video' => (bool) config_cache('pixelfed.optimize_video'),
 
                     'media_types' => config_cache('pixelfed.media_types'),
                     'mime_types' => config_cache('pixelfed.media_types') ? explode(',', config_cache('pixelfed.media_types')) : [],

+ 1 - 1
database/migrations/2023_02_04_053028_fix_cloud_media_paths.php

@@ -19,7 +19,7 @@ return new class extends Migration
     public function up()
     {
         ini_set('memory_limit', '-1');
-        if(config_cache('pixelfed.cloud_storage') == false) {
+        if((bool) config_cache('pixelfed.cloud_storage') == false) {
             return;
         }
 

BIN
public/css/admin.css


BIN
public/css/spa.css


BIN
public/js/admin.js


BIN
public/js/manifest.js


BIN
public/js/profile.chunk.f74967e7910990ca.js → public/js/profile.chunk.a2234f891ba86efd.js


BIN
public/mix-manifest.json


+ 3 - 10
resources/assets/components/Hashtag.vue

@@ -202,16 +202,12 @@
 					if(res.data && res.data.length) {
 						this.feed = res.data;
 						this.maxId = res.data[res.data.length - 1].id;
-						return true;
+					    this.canLoadMore = true;
 					} else {
 						this.feedLoaded = true;
 						this.isLoaded = true;
-						return false;
 					}
 				})
-				.then(res => {
-					this.canLoadMore = res;
-				})
 				.finally(() => {
 					this.feedLoaded = true;
 					this.isLoaded = true;
@@ -242,14 +238,11 @@
 					if(res.data && res.data.length) {
 						this.feed.push(...res.data);
 						this.maxId = res.data[res.data.length - 1].id;
-						return true;
+                        this.canLoadMore = true;
 					} else {
-						return false;
+                        this.canLoadMore = false;
 					}
 				})
-				.then(res => {
-					this.canLoadMore = res;
-				})
 				.finally(() => {
 					this.isIntersecting = false;
 				})

+ 9 - 4
resources/assets/components/admin/AdminInstances.vue

@@ -251,10 +251,11 @@
 			</div>
 		  <div>
 			  <b-button
-				variant="secondary"
-				@click="showInstanceModal = false"
+				variant="link-dark"
+                size="sm"
+				@click="onViewMoreInstance"
 			  >
-				Close
+				View More
 			  </b-button>
 			  <b-button
 				variant="primary"
@@ -885,8 +886,12 @@
 						};
 					}
 				}
-			}
+			},
 
+            onViewMoreInstance() {
+                this.showInstanceModal = false;
+                window.location.href = '/i/admin/instances/show/' + this.instanceModal.id
+            }
 		}
 	}
 </script>

+ 1550 - 0
resources/assets/components/admin/AdminSettings.vue

@@ -0,0 +1,1550 @@
+<template>
+<div v-if="loaded">
+    <div class="header bg-primary pb-2 mt-n4">
+        <div class="container-fluid">
+            <div class="header-body">
+                <div class="row align-items-center py-4">
+                    <div class="col-lg-6 col-7">
+                        <p class="display-1 text-white d-inline-block mb-0">Settings</p>
+                        <p class="h3 text-white font-weight-light">Manage your server settings</p>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <div class="container">
+        <div class="row">
+            <div class="col-12 col-md-3">
+                <div class="nav-wrapper">
+                    <div class="nav flex-column nav-pills" id="tabs-icons-text" role="tablist" aria-orientation="vertical">
+                        <div v-for="tab in tabs" class="nav-item">
+                            <a class="nav-link mb-sm-3" :class="{ active: tabIndex === tab.id }" href="#" @click.prevent="toggleTab(tab.id)">
+                                <i :class="tab.icon"></i>
+                                <span class="ml-2">{{ tab.title }}</span>
+                            </a>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <div class="col-12 col-md-9">
+                <div class="card shadow mt-3">
+                    <div class="card-body">
+                        <div class="tab-content">
+
+                            <div v-if="tabIndex === 1" class="tab-pane fade show active">
+                                <tab-header title="Settings" :saving="isSubmitting" :saved="isSubmittingTimeout" @save="handleSave('overview')" />
+
+                                <div class="row">
+                                    <div class="col-12 col-md-6">
+                                        <div class="card shadow-none border card-body" style="padding: 1.1rem 1.6rem">
+                                            <div class="form-group mb-0">
+                                                <label for="form-summary" class="font-weight-bold">Registration Status</label>
+                                                <select v-model="features.registration_status" class="form-control form-control-muted">
+                                                    <option value="open" >Open - Anyone can register</option>
+                                                    <option value="filtered">Filtered - Anyone can apply (Curated Onboarding)</option>
+                                                    <option value="closed">Closed - Nobody can register</option>
+                                                </select>
+                                            </div>
+                                        </div>
+
+                                        <checkbox
+                                            name="Cloud Storage"
+                                            :value="features.cloud_storage"
+                                            description="Store photos and videos on S3 compatible object storage providers."
+                                            @change="handleChange($event, 'features', 'cloud_storage')"
+                                        />
+
+                                        <checkbox
+                                            name="ActivityPub"
+                                            :value="features.activitypub_enabled"
+                                            description="ActivityPub federation, compatible with Pixelfed, Mastodon and other projects."
+                                            @change="handleChange($event, 'features', 'activitypub_enabled')"
+                                        />
+
+                                        <checkbox
+                                            name="Account Migration"
+                                            :value="features.account_migration"
+                                            description="Allow local accounts to migrate to other local or remote accounts."
+                                            @change="handleChange($event, 'features', 'account_migration')"
+                                        />
+                                    </div>
+
+                                    <div class="col-12 col-md-6">
+                                        <checkbox
+                                            name="Mobile APIs"
+                                            :value="features.mobile_apis"
+                                            description="Enable apis required for official mobile app support and 3rd party apps."
+                                            @change="handleChange($event, 'features', 'mobile_apis')"
+                                        />
+
+                                        <checkbox
+                                            name="Stories"
+                                            :value="features.stories"
+                                            description="Allow users to share federated ephemeral Stories that disappear after 24 hours."
+                                            @change="handleChange($event, 'features', 'stories')"
+                                        />
+
+                                        <checkbox
+                                            name="Instagram Import"
+                                            :value="features.instagram_import"
+                                            description="Enable users to use the <span class='font-weight-bold'>experimental</span> Instagram Import support."
+                                            @change="handleChange($event, 'features', 'instagram_import')"
+                                        />
+
+                                        <checkbox
+                                            name="Spam detection"
+                                            :value="features.autospam_enabled"
+                                            description="Detect and remove spam from timelines using the automated Autospam detection."
+                                            @change="handleChange($event, 'features', 'autospam_enabled')"
+                                        />
+                                    </div>
+                                </div>
+                            </div>
+
+                            <div v-else-if="tabIndex === 'landing'" class="tab-pane fade show active" role="tabpanel">
+                                <tab-header title="Landing" :saving="isSubmitting" :saved="isSubmittingTimeout" @save="handleSave('landing')" />
+
+                                <div class="row">
+                                   <div class="col-12 col-md-6">
+                                        <div class="card shadow-none border card-body" style="padding: 1.1rem 1.6rem">
+                                            <div class="form-group mb-0">
+                                                <label for="form-summary" class="font-weight-bold">Admin Account</label>
+                                                <select v-model="landing.current_admin" class="form-control form-control-muted">
+                                                    <option disabled="" value="0">Select a designated admin</option>
+                                                    <option v-for="(acct, index) in landing.admins" :key="'pfc-' + acct + index" :value="acct.profile_id">{{ acct.username }}</option>
+                                                </select>
+                                            </div>
+                                        </div>
+
+                                        <checkbox
+                                            name="Show Directory"
+                                            :value="landing.show_directory"
+                                            description="Show the account directory on the landing page for guest users."
+                                            @change="handleChange($event, 'landing', 'show_directory')"
+                                        />
+                                    </div>
+
+                                    <div class="col-12 col-md-6">
+                                        <checkbox
+                                            name="Show Explore Feed"
+                                            :value="landing.show_explore"
+                                            description="Show the explore feed of popular posts on the landing page for guest users."
+                                            @change="handleChange($event, 'landing', 'show_explore')"
+                                        />
+                                    </div>
+                                </div>
+                            </div>
+
+                            <div v-else-if="tabIndex === 'branding'" class="tab-pane fade show active" role="tabpanel">
+                                <tab-header title="Branding" :saving="isSubmitting" :saved="isSubmittingTimeout" @save="handleSave('branding')" />
+
+                                <div class="row">
+                                    <div class="col-12 col-md-8">
+                                        <div class="card shadow-none border card-body" style="padding: 1.1rem 1.6rem">
+                                            <div class="form-group mb-1">
+                                                <label for="form-summary" class="font-weight-bold">Server Name</label>
+                                                <input
+                                                    class="form-control form-control-muted"
+                                                    placeholder="Pixelfed"
+                                                    v-model="branding.name" />
+                                            </div>
+                                            <p class="help-text small text-muted mb-0">
+                                                The instance name used in titles, metadata and apis.
+                                            </p>
+                                        </div>
+                                    </div>
+
+                                    <div class="col-12 col-md-8">
+                                        <div class="card shadow-none border card-body">
+                                            <div class="form-group mb-1">
+                                                <label for="form-summary" class="font-weight-bold">Short Description</label>
+                                                <textarea
+                                                    class="form-control form-control-muted"
+                                                    placeholder="Pixelfed"
+                                                    rows="4"
+                                                    v-model="branding.short_description"></textarea>
+                                            </div>
+                                            <p class="help-text small text-muted mb-0">
+                                                Short description of instance used on various pages and apis.
+                                            </p>
+                                        </div>
+
+                                        <div class="card shadow-none border card-body">
+                                            <div class="form-group mb-1">
+                                                <label for="form-summary" class="font-weight-bold">Long Description</label>
+                                                <textarea
+                                                    class="form-control form-control-muted"
+                                                    placeholder="Pixelfed"
+                                                    rows="8"
+                                                    v-model="branding.long_description"></textarea>
+                                            </div>
+                                            <p class="help-text small text-muted mb-0">
+                                                Longer description of instance used on about page.
+                                            </p>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+
+                            <div v-else-if="tabIndex === 'media'" class="tab-pane fade show active" role="tabpanel">
+                                <tab-header title="Media" :saving="isSubmitting" :saved="isSubmittingTimeout" @save="handleSave('media')" />
+
+                                <div class="row">
+                                    <div class="col-12 col-md-6">
+                                        <div class="card shadow-none border card-body">
+                                            <div class="form-group mb-1">
+                                                <label class="font-weight-bold text-muted">Max Media Size</label>
+                                                <div class="input-group mb-0">
+                                                    <input
+                                                        type="text"
+                                                        class="form-control"
+                                                        placeholder="15000"
+                                                        aria-label="Max media size"
+                                                        aria-describedby="maxMediaSize"
+                                                        v-model="media.max_photo_size">
+                                                    <div class="input-group-append">
+                                                        <span class="input-group-text" id="maxMediaSize">= {{ maxMediaSizeToMb }}</span>
+                                                    </div>
+                                                </div>
+                                            </div>
+                                            <p class="help-text small text-muted mb-0">
+                                                Maximum file upload size in KB
+                                            </p>
+                                        </div>
+
+                                        <checkbox
+                                            name="Optimize Images"
+                                            :value="media.optimize_image"
+                                            description="Enable to optimize images and generate thumbnails for local image media uploads."
+                                            @change="handleChange($event, 'media', 'optimize_image')"
+                                        />
+
+                                        <checkbox
+                                            name="Optimize Video"
+                                            :value="media.optimize_video"
+                                            description="Enable to generate video thumbnails for local video media uploads."
+                                            @change="handleChange($event, 'media', 'optimize_video')"
+                                        />
+
+                                        <div class="card shadow-none border card-body">
+                                            <div class="form-group mb-1">
+                                                <label class="font-weight-bold text-muted">Media Types</label>
+
+                                                <div class="list-group">
+                                                    <div v-for="(mediaType, key) in mediaTypes" class="list-group-item py-2">
+                                                        <div class="custom-control custom-checkbox">
+                                                            <input
+                                                                type="checkbox"
+                                                                class="custom-control-input"
+                                                                :name="key"
+                                                                :id="key"
+                                                                v-model="mediaTypes[key]">
+                                                            <label class="custom-control-label font-weight-bold" :for="key">{{ key }}</label>
+                                                        </div>
+                                                    </div>
+                                                </div>
+                                            </div>
+                                            <p class="help-text small text-muted mb-0">
+                                                Supported mime types for media uploads
+                                            </p>
+                                        </div>
+                                    </div>
+                                    <div class="col-12 col-md-6">
+                                        <div class="card shadow-none border card-body">
+                                            <div class="form-group mb-1">
+                                                <label class="font-weight-bold text-muted">Photo Album Limit</label>
+                                                <input
+                                                    type="number"
+                                                    min="1"
+                                                    max="20"
+                                                    class="form-control"
+                                                    name="max_album_length"
+                                                    v-model="media.max_album_length">
+                                            </div>
+                                            <p class="help-text small text-muted mb-0">
+                                                The maximum number of photos or videos per album
+                                            </p>
+                                        </div>
+
+                                        <transition name="fade">
+                                            <div v-if="media.optimize_image" class="card shadow-none border card-body">
+                                                <div class="form-group mb-1">
+                                                    <label class="font-weight-bold text-muted">Image Quality</label>
+                                                    <input
+                                                        type="number"
+                                                        min="20"
+                                                        max="100"
+                                                        class="form-control"
+                                                        name="image_quality"
+                                                        v-model="media.image_quality">
+                                                </div>
+                                                <p class="help-text small text-muted mb-0">
+                                                    Image optimization quality from 0-100%.
+                                                </p>
+                                            </div>
+                                        </transition>
+                                    </div>
+                                </div>
+                            </div>
+
+                            <div v-else-if="tabIndex === 'platform'" class="tab-pane fade show active" role="tabpanel">
+                                <tab-header title="Platform" :saving="isSubmitting" :saved="isSubmittingTimeout" @save="handleSave('platform')" />
+
+                                <div class="row">
+                                    <div class="col-12 col-md-6">
+                                        <checkbox
+                                            name="Allow Profile Embeds"
+                                            :value="platform.allow_profile_embeds"
+                                            description="Allow anyone to embed public profiles on other websites."
+                                            @change="handleChange($event, 'platform', 'allow_profile_embeds')"
+                                        />
+
+                                        <div class="card shadow-none border card-body">
+                                            <div class="form-group mb-0">
+                                                <div class="custom-control custom-checkbox">
+                                                    <input
+                                                        type="checkbox"
+                                                        name="allow_app_registrations"
+                                                        class="custom-control-input"
+                                                        id="platform1"
+                                                        :disabled="features.registration_status !== 'open'"
+                                                        v-model="platform.allow_app_registration">
+                                                    <label class="custom-control-label font-weight-bold" for="platform1">Allow App Registrations</label>
+                                                </div>
+                                                <p v-if="features.registration_status !== 'open'" class="mb-0 small text-muted">Requires open registration to be enabled.</p>
+                                                <p v-else class="mb-0 small">Allow users to register via the official Pixelfed mobile application.</p>
+                                            </div>
+                                        </div>
+
+                                        <checkbox
+                                            name="Custom Emoji"
+                                            :value="platform.custom_emoji_enabled"
+                                            description="Enable federated custom emoji that is compatible with Mastodon, Pleroma and others."
+                                            @change="handleChange($event, 'platform', 'custom_emoji_enabled')"
+                                        />
+
+                                        <template v-if="features.registration_status === 'open' && features.allow_app_registration">
+                                            <div class="card shadow-none border card-body">
+                                                <div class="form-group mb-1">
+                                                    <label class="font-weight-bold text-muted">app_registration_rate_limit_attempts</label>
+                                                    <input
+                                                        type="number"
+                                                        class="form-control"
+                                                        name="app_registration_rate_limit_attempts"
+                                                        v-model="platform.app_registration_rate_limit_attempts">
+                                                </div>
+                                                <p class="help-text small text-muted mb-0">
+                                                    app_registration_rate_limit_attempts.
+                                                </p>
+                                            </div>
+                                            <div class="card shadow-none border card-body">
+                                                <div class="form-group mb-1">
+                                                    <label class="font-weight-bold text-muted">app_registration_rate_limit_decay</label>
+                                                    <input
+                                                        type="number"
+                                                        class="form-control"
+                                                        name="app_registration_rate_limit_decay"
+                                                        v-model="platform.app_registration_rate_limit_decay">
+                                                </div>
+                                                <p class="help-text small text-muted mb-0">
+                                                    app_registration_rate_limit_decay
+                                                </p>
+                                            </div>
+                                        </template>
+                                    </div>
+
+                                    <div class="col-12 col-md-6">
+                                        <checkbox
+                                            name="Allow Post Embeds"
+                                            :value="platform.allow_post_embeds"
+                                            description="Allow anyone to embed public posts on other websites."
+                                            @change="handleChange($event, 'platform', 'allow_post_embeds')"
+                                        />
+
+                                        <div class="card shadow-none border card-body">
+                                            <div class="form-group mb-1">
+                                                <div class="custom-control custom-checkbox">
+                                                    <input
+                                                        type="checkbox"
+                                                        name="hcaps"
+                                                        class="custom-control-input"
+                                                        id="hcp"
+                                                        v-model="platform.captcha_enabled">
+                                                    <label class="custom-control-label font-weight-bold" for="hcp">Enable hCaptcha</label>
+                                                </div>
+                                            </div>
+                                            <template v-if="platform.captcha_enabled">
+                                                <hr class="my-2">
+                                                <div class="row">
+                                                    <div class="col-12 col-md-6">
+                                                        <div class="form-group my-1">
+                                                            <label class="text-muted small">hCaptcha Secret</label>
+                                                            <input
+                                                                type="text"
+                                                                class="form-control"
+                                                                name="captcha_secret"
+                                                                v-model="platform.captcha_secret">
+                                                        </div>
+                                                    </div>
+                                                    <div class="col-12 col-md-6">
+                                                        <div class="form-group my-1">
+                                                            <label class="text-muted small">hCaptcha Sitekey</label>
+                                                            <input
+                                                                type="text"
+                                                                class="form-control"
+                                                                name="captcha_sitekey"
+                                                                v-model="platform.captcha_sitekey">
+                                                        </div>
+                                                    </div>
+                                                </div>
+                                                <hr class="mt-2 mb-4">
+                                                <div class="row">
+                                                    <div class="col-12 col-lg-6">
+                                                        <div class="custom-control custom-checkbox">
+                                                            <input
+                                                                type="checkbox"
+                                                                name="captcha_on_login"
+                                                                class="custom-control-input"
+                                                                id="captcha_on_login"
+                                                                v-model="platform.captcha_on_login">
+                                                            <label class="custom-control-label font-weight-bold" for="captcha_on_login">Login Captcha</label>
+                                                        </div>
+                                                    </div>
+                                                    <div class="col-12 col-lg-6">
+                                                        <div class="custom-control custom-checkbox">
+                                                            <input
+                                                                type="checkbox"
+                                                                name="captcha_on_register"
+                                                                class="custom-control-input"
+                                                                id="captcha_on_register"
+                                                                v-model="platform.captcha_on_register">
+                                                            <label class="custom-control-label font-weight-bold" for="captcha_on_register">Register Captcha</label>
+                                                        </div>
+                                                    </div>
+                                                </div>
+                                                <hr class="mt-4 mb-2">
+                                            </template>
+                                            <p class="help-text small text-muted mb-0">
+                                                Enable hCaptcha on login and register pages
+                                            </p>
+                                        </div>
+
+                                        <template v-if="features.registration_status === 'open' && features.allow_app_registration">
+                                            <div class="card shadow-none border card-body">
+                                                <div class="form-group mb-1">
+                                                    <label class="font-weight-bold text-muted">app_registration_confirm_rate_limit_attempts</label>
+                                                    <input
+                                                        type="number"
+                                                        class="form-control"
+                                                        name="app_registration_confirm_rate_limit_attempts"
+                                                        v-model="platform.app_registration_confirm_rate_limit_attempts">
+                                                </div>
+                                                <p class="help-text small text-muted mb-0">
+                                                    app_registration_confirm_rate_limit_attempts.
+                                                </p>
+                                            </div>
+                                            <div class="card shadow-none border card-body">
+                                                <div class="form-group mb-1">
+                                                    <label class="font-weight-bold text-muted">app_registration_confirm_rate_limit_decay</label>
+                                                    <input
+                                                        type="number"
+                                                        class="form-control"
+                                                        name="app_registration_confirm_rate_limit_decay"
+                                                        v-model="platform.app_registration_confirm_rate_limit_decay">
+                                                </div>
+                                                <p class="help-text small text-muted mb-0">
+                                                    app_registration_confirm_rate_limit_decay.
+                                                </p>
+                                            </div>
+                                        </template>
+                                    </div>
+                                </div>
+                            </div>
+
+                            <div v-else-if="tabIndex === 'posts'" class="tab-pane fade show active" role="tabpanel">
+                                <tab-header title="Posts" :saving="isSubmitting" :saved="isSubmittingTimeout" @save="handleSave('posts')" />
+
+                                <div class="row">
+                                    <div class="col-12 col-md-6">
+                                        <div class="card shadow-none border card-body">
+                                            <div class="form-group mb-1">
+                                                <label class="font-weight-bold text-muted">Max Caption Length</label>
+                                                <input
+                                                    type="number"
+                                                    min="1"
+                                                    max="10000"
+                                                    class="form-control"
+                                                    name="max_caption_limit"
+                                                    v-model="posts.max_caption_length">
+                                            </div>
+                                            <p class="help-text small text-muted mb-0">
+                                                The maximum character count of post captions. We recommend a limit between 500-2000.
+                                            </p>
+                                        </div>
+                                    </div>
+
+                                    <div class="col-12 col-md-6">
+                                        <div class="card shadow-none border card-body">
+                                            <div class="form-group mb-1">
+                                                <label class="font-weight-bold text-muted">Max Alttext Length</label>
+                                                <input
+                                                    type="number"
+                                                    min="1"
+                                                    max="10000"
+                                                    class="form-control"
+                                                    name="max_altext_length"
+                                                    v-model="posts.max_altext_length">
+                                            </div>
+                                            <p class="help-text small text-muted mb-0">
+                                                The maximum character count of post media alttext captions. We recommend a limit between 2000-10000.
+                                            </p>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+
+                            <div v-else-if="tabIndex === 'rules'" class="tab-pane fade show active" role="tabpanel">
+                                <tab-header title="Rules" :saving="isSubmitting" :saved="isSubmittingTimeout" @save="handleSave('rules')" />
+
+                                <div class="row">
+                                    <div class="col-12 mb-3">
+                                        <div v-if="hasDuplicateRulesComputed" class="alert alert-danger">
+                                            <p class="font-weight-bold mb-0">Duplicate rules detected, you should fix this!</p>
+                                        </div>
+                                        <div class="position-relative">
+                                            <div class="card shadow-none border">
+                                                <div class="card-header py-2 bg-primary text-white font-weight-bold text-center">Active Rules</div>
+                                                <div class="list-group list-group-flush">
+                                                    <div
+                                                        v-for="(rule, idx) in rulesComputed"
+                                                        class="list-group-item">
+                                                        <div class="d-flex justify-content-between align-items-start">
+                                                            <div class="d-flex gap-1 align-items-start">
+                                                                <div class="rule-badge">
+                                                                    <div class="rule-badge-inner">{{ idx + 1 }}</div>
+                                                                </div>
+                                                                <admin-read-more
+                                                                    :key="rule"
+                                                                    class="text-dark rule-text"
+                                                                    :content="rule"
+                                                                    :maxLength="140"
+                                                                    :initialLimit="30"
+                                                                    fontSize="13" />
+                                                            </div>
+
+                                                            <button
+                                                                class="btn btn-link btn-sm"
+                                                                :disabled="isDeletingRule"
+                                                                @click.prevent="handleDeleteRule(rule, idx, $event)">
+                                                                <i class="fas fa-trash-alt text-danger"></i>
+                                                            </button>
+                                                        </div>
+                                                    </div>
+
+
+                                                    <div v-if="!rules || !rules.length" class="list-group-item">
+                                                        <p class="text-center mb-0">No rules set!</p>
+                                                    </div>
+                                                </div>
+                                            </div>
+                                            <div v-if="!showAllRules && rules.length > 2" class="d-flex justify-content-center" style="position:absolute;width: 100%;padding-top: 10rem;bottom:0;background: linear-gradient(to bottom, rgba(255,255,255,0), rgba(255,255,255, 1));">
+                                                <button class="btn btn-dark font-weight-bold rounded-pill btn-block" @click.prevent="showAllRules = true">Show all rules</button>
+                                        </div>
+                                        </div>
+                                    </div>
+
+                                    <div class="col-12 col-md-6">
+                                        <div class="card shadow-none border card-body">
+                                            <div class="form-group mb-1">
+                                                <label class="font-weight-bold text-muted">Add New Rule</label>
+                                                <textarea
+                                                    type="text"
+                                                    class="form-control"
+                                                    name="new_rule"
+                                                    rows="5"
+                                                    minlength="5"
+                                                    maxlength="1000"
+                                                    placeholder="Add your new rule here..."
+                                                    :disabled="isSubmittingNewRule || isDeletingRule"
+                                                    v-model="newRule"></textarea>
+                                            </div>
+                                            <div class="d-flex justify-content-between align-items-center">
+                                                <p class="help-text small text-muted mb-0">
+                                                    Add a new rule
+                                                </p>
+                                                <p class="help-text small text-muted mb-0">
+                                                    {{ newRule && newRule.length ? newRule.length : 0 }}/1000
+                                                </p>
+                                            </div>
+                                            <hr class="my-2">
+                                            <p class="mb-0">
+                                                <button
+                                                    class="btn btn-primary btn-sm btn-block font-weight-bold rounded-pill"
+                                                    :disabled="!newRule || !newRule.length || isSubmittingNewRule || isDeletingRule"
+                                                    @click.prevent="handleAddRule">Add Rule</button>
+                                            </p>
+                                        </div>
+
+                                        <button v-if="rules && rules.length" class="btn btn-outline-danger rounded-pill btn-block btn-sm" @click.prevent="handleDeleteAllRules">Delete all rules</button>
+                                    </div>
+
+                                    <div v-if="suggestedRulesComputed && suggestedRulesComputed.length" class="col-12 col-md-6">
+                                        <div class="border-bottom pb-2 mb-3 d-flex justify-content-between align-items-center">
+                                            <p class="font-weight-bold mb-0">Suggested Rules</p>
+                                            <a v-if="!rules.length" class="font-weight-bold small" href="#" @click.prevent="importAllDefaultRules">Import All</a>
+                                        </div>
+
+                                        <div class="list-group">
+                                            <a
+                                                v-for="rule in suggestedRulesComputed"
+                                                class="list-group-item small"
+                                                href="#"
+                                                @click.prevent="addSuggestedRule(rule, $event)">{{ rule }}</a>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+
+                            <div v-else-if="tabIndex === 'storage'" class="tab-pane fade show active" role="tabpanel">
+                                <tab-header title="Storage" :saving="isSubmitting" :saved="isSubmittingTimeout" @save="handleSave('storage')" />
+
+                                <div class="row">
+                                    <div class="col-12 col-md-6">
+                                        <div class="card shadow-none border card-body" style="padding: 1.1rem 1.6rem">
+                                            <div class="form-group mb-0">
+                                                <label for="form-summary" class="font-weight-bold">Primary Storage Disk</label>
+                                                <select v-model="storage.primary_disk" class="form-control form-control-muted">
+                                                    <option value="local" >Local</option>
+                                                    <option value="cloud">Cloud/S3</option>
+                                                </select>
+                                            </div>
+                                            <p class="help-text small text-muted mt-2 mb-0">
+                                                The storage disk where avatars and media uploads are stored.
+                                            </p>
+                                        </div>
+                                    </div>
+
+                                    <div class="col-12 col-md-6">
+                                        <div class="card border">
+                                            <div class="card-header bg-gradient-primary">
+                                                <p class="text-center mb-0 text-white font-weight-bold">Cloud Disk Config</p>
+                                            </div>
+
+                                            <div v-if="!showDiskConfig" class="card-body">
+                                                <p class="text-center mb-0">
+                                                    <a
+                                                        class="btn btn-primary bg-gradient-primary shadow-lg rounded-pill"
+                                                        href="#"
+                                                        @click.prevent="showDiskConfig = true">
+                                                        View/Edit
+                                                    </a>
+                                                </p>
+                                            </div>
+                                            <div v-else class="card-body">
+                                                <div class="form-group mb-4 d-flex align-items-center gap-1">
+                                                    <label for="form-summary" class="font-weight-bold mb-0">Disk</label>
+                                                    <select v-model="storage.disk_config.driver" class="form-control form-control-muted mb-0">
+                                                        <option value="s3" >S3</option>
+                                                        <option value="spaces">DigitalOcean Spaces</option>
+                                                    </select>
+                                                </div>
+
+                                                <form-input
+                                                    name="Key"
+                                                    :value="storage.disk_config.key"
+                                                    description=""
+                                                    :isCard="false"
+                                                    :isInline="true"
+                                                    @change="handleSubChange($event, 'storage', 'disk_config', 'key')"
+                                                />
+                                                <form-input
+                                                    name="Secret"
+                                                    :value="storage.disk_config.secret"
+                                                    description=""
+                                                    :isCard="false"
+                                                    :isInline="true"
+                                                    @change="handleSubChange($event, 'storage', 'disk_config', 'secret')"
+                                                />
+                                                <form-input
+                                                    name="Region"
+                                                    :value="storage.disk_config.region"
+                                                    description=""
+                                                    :isCard="false"
+                                                    :isInline="true"
+                                                    @change="handleSubChange($event, 'storage', 'disk_config', 'region')"
+                                                />
+                                                <form-input
+                                                    name="Bucket"
+                                                    :value="storage.disk_config.bucket"
+                                                    description=""
+                                                    :isCard="false"
+                                                    :isInline="true"
+                                                    @change="handleSubChange($event, 'storage', 'disk_config', 'bucket')"
+                                                />
+                                                <form-input
+                                                    name="Endpoint"
+                                                    :value="storage.disk_config.endpoint"
+                                                    description=""
+                                                    :isCard="false"
+                                                    :isInline="true"
+                                                    @change="handleSubChange($event, 'storage', 'disk_config', 'endpoint')"
+                                                />
+                                                <form-input
+                                                    name="Visibility"
+                                                    :value="storage.disk_config.visibility"
+                                                    description=""
+                                                    :isCard="false"
+                                                    :isInline="true"
+                                                    :isDisabled="true"
+                                                    @change="handleSubChange($event, 'storage', 'disk_config', 'visibility')"
+                                                />
+                                                <form-input
+                                                    name="Url"
+                                                    :value="storage.disk_config.url"
+                                                    description=""
+                                                    :isCard="false"
+                                                    :isInline="true"
+                                                    @change="handleSubChange($event, 'storage', 'disk_config', 'url')"
+                                                />
+                                            </div>
+                                        </div>
+
+                                    </div>
+                                </div>
+                            </div>
+
+                            <div v-else-if="tabIndex === 'users'" class="tab-pane fade show active" role="tabpanel">
+                                <tab-header title="Users" :saving="isSubmitting" :saved="isSubmittingTimeout" @save="handleSave('users')" />
+
+                                <div class="row">
+                                    <div class="col-12 col-md-6">
+                                        <checkbox
+                                            name="Require Email Verifications"
+                                            :value="users.require_email_verification"
+                                            description="Require users to verify their email address is valid before they can use the account."
+                                            @change="handleChange($event, 'users', 'require_email_verification')"
+                                        />
+
+                                        <form-input
+                                            name="Max User Blocks"
+                                            :value="users.max_user_blocks.toString()"
+                                            description="The max number of account blocks per user."
+                                            @change="handleChange($event, 'users', 'max_user_blocks')"
+                                        />
+
+                                        <form-input
+                                            name="Max User Mutes"
+                                            :value="users.max_user_mutes.toString()"
+                                            description="The max number of account mutes per user."
+                                            @change="handleChange($event, 'users', 'max_user_mutes')"
+                                        />
+
+                                        <form-input
+                                            name="Max User Domain Blocks"
+                                            :value="users.max_domain_blocks.toString()"
+                                            description="The max number of domain blocks per user."
+                                            @change="handleChange($event, 'users', 'max_domain_blocks')"
+                                        />
+                                    </div>
+
+                                    <div class="col-12 col-md-6">
+                                        <div class="card shadow-none border card-body">
+                                            <div class="form-group mb-0">
+                                                <div class="custom-control custom-checkbox">
+                                                    <input type="checkbox" name="enforce_account_limit" class="custom-control-input" id="users2" v-model="users.enforce_account_limit">
+                                                    <label class="custom-control-label font-weight-bold" for="users2">Enforce Account Limit</label>
+                                                </div>
+                                                <p class="mb-0 small">Set a storage limit per user account for all uploaded media (photo + video).</p>
+                                            </div>
+                                            <transition name="fade">
+                                            <div v-if="users.enforce_account_limit">
+                                                <hr class="my-2">
+                                                <div class="form-group mb-1">
+                                                    <div class="input-group mb-0">
+                                                        <input
+                                                            type="text"
+                                                            class="form-control"
+                                                            placeholder="15000"
+                                                            aria-label="Max account size"
+                                                            aria-describedby="maxMediaSize"
+                                                            v-model="users.max_account_size">
+                                                        <div class="input-group-append">
+                                                            <span class="input-group-text">= {{maxAccountSizeToMb }}</span>
+                                                        </div>
+                                                    </div>
+                                                </div>
+                                                <p class="help-text small text-muted mb-0">
+                                                    Maximum file storage limit per user account.
+                                                </p>
+                                            </div>
+                                            </transition>
+                                        </div>
+
+                                        <div class="card shadow-none border">
+                                            <div class="card-body">
+                                                <div class="form-group mb-0">
+                                                    <div class="custom-control custom-checkbox">
+                                                        <input type="checkbox" name="admin_autofollow" class="custom-control-input" id="users4" v-model="users.admin_autofollow">
+                                                        <label class="custom-control-label font-weight-bold" for="users4">Autofollow Accounts</label>
+                                                    </div>
+                                                    <p class="mb-0 small">Force new accounts to follow accounts you specify below</p>
+                                                </div>
+                                            </div>
+                                            <transition name="fade">
+                                                <div v-if="users.admin_autofollow" class="list-group list-group-flush">
+                                                    <div v-if="users.admin_autofollow_accounts?.length">
+                                                        <div v-for="user in users.admin_autofollow_accounts" class="list-group-item">
+                                                            <div class="d-flex justify-content-between align-items-center">
+                                                                <p class="font-weight-bold mb-0">&commat;{{ user }}</p>
+                                                                <button class="btn btn-link p-0" @click.prevent="removeAutofollow(user, $event)"><i class="fas fa-trash-alt text-danger"></i></button>
+                                                            </div>
+                                                        </div>
+                                                    </div>
+                                                    <div v-else class="list-group-item">
+                                                        <p class="text-center mb-0">No autofollow accounts active.</p>
+                                                    </div>
+                                                </div>
+                                            </transition>
+                                            <transition name="fade">
+                                                <div v-if="users.admin_autofollow && (users.admin_autofollow_accounts && users.admin_autofollow_accounts.length < 5)" class="card-footer">
+                                                    <button
+                                                        class="btn btn-primary btn-block rounded-pill"
+                                                        @click.prevent="addAutofollow">Add Autofollow Account</button>
+                                                </div>
+                                            </transition>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+<div v-else>
+    <div class="container my-5 py-5 text-center">
+        <div class="spinner-border text-primary" role="status">
+            <span class="sr-only">Loading...</span>
+        </div>
+    </div>
+</div>
+</template>
+
+<script type="text/javascript">
+    import AdminReadMore from "./partial/AdminReadMore.vue";
+    import AdminSettingsTabHeader from "./partial/AdminSettingsTabHeader.vue";
+    import Checkbox from "./partial/AdminSettingsCheckbox.vue";
+    import FormInput from "./partial/AdminSettingsInput.vue";
+
+    export default {
+        components: {
+            "admin-read-more": AdminReadMore,
+            "tab-header": AdminSettingsTabHeader,
+            "checkbox": Checkbox,
+            "form-input": FormInput
+        },
+
+        data() {
+            return {
+                loaded: false,
+                initialData: {},
+                tabIndex: 1,
+                tabbies: [
+                    'landing',
+                    'branding',
+                    'media',
+                    'posts',
+                    'platform',
+                    'rules',
+                    'users',
+                    'storage'
+                ],
+                tabs: [
+                    { id: 1, title: "Overview", icon: "far fa-home" },
+                    // { id: 2, title: "Status", icon: "far fa-asterisk" },
+                    { id: 'landing', title: "Landing", icon: "far fa-info-circle" },
+                    { id: 'branding', title: "Branding", icon: "far fa-user-crown" },
+                    { id: 'media', title: "Media", icon: "far fa-image" },
+                    { id: 'platform', title: "Platform", icon: "far fa-database" },
+                    { id: 'posts', title: "Posts", icon: "far fa-heart" },
+                    { id: 'rules', title: "Rules", icon: "far fa-eye-slash" },
+                    { id: 'storage', title: "Storage", icon: "far fa-hdd" },
+                    { id: 'users', title: "Users", icon: "far fa-users" },
+                ],
+
+                isSubmitting: false,
+                isSubmittingTimeout: false,
+                isSubmittingTimeoutHandler: undefined,
+
+                features: [],
+                landing: {
+                    current_admin: 0,
+                },
+                branding: [],
+                media: [],
+                mediaTypes: {
+                    jpeg: false,
+                    png: false,
+                    gif: false,
+                    webp: false,
+                    avif: false,
+                    heic: false,
+                    mp4: false,
+                    mov: false,
+                },
+                rules: [],
+                users: [],
+                posts: [],
+                platform: [],
+                storage: [],
+                newRule: undefined,
+                isSubmittingNewRule: false,
+                isDeletingRule: false,
+                suggestedRules: [],
+                hasDuplicateRules: false,
+                showAllRules: false,
+                showDiskConfig: false,
+            }
+        },
+
+        computed: {
+            maxMediaSizeToMb: {
+                get() {
+                    if(!this.media || !this.media.max_photo_size) {
+                        return '0.00 MB';
+                    }
+
+                    return (this.media.max_photo_size / 1000).toFixed(2) + ' MB';
+                }
+            },
+
+            maxAccountSizeToMb: {
+                get() {
+                    if(!this.users || !this.users.max_account_size) {
+                        return '0.00 MB';
+                    }
+
+                    const mb = (this.users.max_account_size / 1024);
+
+                    if(mb > 1000000) {
+                        return (mb / 1000000).toFixed(1) + 'TB';
+                    }
+
+                    if(mb > 1000) {
+                        return (mb / 1024).toFixed(2) + 'GB';
+                    }
+                    return (this.users.max_account_size / 1024).toFixed(2) + ' MB';
+                }
+            },
+
+            rulesComputed: {
+                get() {
+                    if(!this.rules || !this.rules.length) {
+                        return [];
+                    }
+
+                    if(this.rules.length > 2) {
+                        if(!this.showAllRules) {
+                            return this.rules.slice(0, 2);
+                        }
+                    }
+                    return this.rules;
+                }
+            },
+
+            suggestedRulesComputed: {
+                get() {
+                    if(!this.rules || !this.rules.length) {
+                        return this.suggestedRules;
+                    }
+
+                    return this.suggestedRules.filter(rule => {
+                        if(this.rules.includes(rule)) {
+                            return false;
+                        }
+
+                        return true;
+                    });
+                }
+            },
+
+            hasDuplicateRulesComputed: {
+                get() {
+                    if(!this.rules || !this.rules.length) {
+                        return false;
+                    }
+                    const array = this.rules;
+                    const duplicates = array.filter((item, index) => array.indexOf(item) !== index);
+
+                    return duplicates.length;
+                }
+            },
+
+            activeMediaTypes: {
+                get() {
+                    let res = '';
+
+                    if(this.mediaTypes.jpeg) {
+                        res += 'image/jpeg,'
+                    }
+
+                    if(this.mediaTypes.png) {
+                        res += 'image/png,'
+                    }
+
+                    if(this.mediaTypes.gif) {
+                        res += 'image/gif,'
+                    }
+
+                    if(this.mediaTypes.webp) {
+                        res += 'image/webp,'
+                    }
+
+                    if(this.mediaTypes.mp4) {
+                        res += 'video/mp4'
+                    }
+
+                    if(res.endsWith(',')) {
+                        res = res.slice(0, -1);
+                    }
+                    return res;
+                }
+            }
+        },
+
+        mounted() {
+            this.fetchInitialData();
+
+            const params = new URL(window.location.href);
+
+            if(params.searchParams.has('t')) {
+                const tab = params.searchParams.get('t');
+                if(this.tabbies.includes(tab)) {
+                    this.tabIndex = tab;
+                } else {
+                    window.history.pushState(null, null, '/i/admin/settings')
+                }
+            }
+        },
+
+        methods: {
+            toggleTab(idx) {
+                clearTimeout(this.isSubmittingTimeoutHandler)
+                this.isSubmittingTimeout = false;
+                this.tabIndex = idx;
+                this.showAllRules = false;
+                if(this.tabbies.includes(idx)) {
+                    window.history.pushState(null, null, '/i/admin/settings?t=' + idx);
+                } else {
+                    window.history.pushState(null, null, '/i/admin/settings');
+                }
+            },
+
+            fetchInitialData() {
+                axios.get('/i/admin/api/settings/fetch')
+                .then(res => {
+                    this.initialData = res.data;
+
+                    this.features = res.data.features;
+                    this.landing = res.data.landing;
+                    this.branding = res.data.branding;
+                    this.media = res.data.media;
+                    this.setMediaTypes();
+                    this.rules = res.data.rules;
+                    this.users = res.data.users;
+                    this.suggestedRules = res.data['suggested_rules'];
+                    this.posts = res.data.posts;
+                    this.platform = res.data.platform;
+                    this.storage = res.data.storage;
+                })
+                .then(() => {
+                    this.loaded = true;
+                })
+            },
+
+            setMediaTypes() {
+                const types = this.media.media_types.split(',');
+                if(types && types.length) {
+                    types.forEach((type) => {
+                        let mime = type.split('/')[1];
+                        if(['jpeg', 'png', 'gif', 'webp', 'mp4'].includes(mime)) {
+                            this.mediaTypes[mime] = true;
+                        }
+                    })
+                }
+            },
+
+            formatCount(c) {
+                return window.App.util.format.count(c);
+            },
+
+            formatDateTime(ts) {
+                let date = new Date(ts);
+                return new Intl.DateTimeFormat('en-US', {dateStyle: 'medium', timeStyle: 'short'}).format(date);
+            },
+
+            formatDate(ts) {
+                let date = new Date(ts);
+                return new Intl.DateTimeFormat('en-US', {month: 'short', year: 'numeric'}).format(date);
+            },
+
+            formatTimestamp(ts) {
+                return window.App.util.format.timeAgo(ts);
+            },
+
+            handleSave(type) {
+                this.isSubmitting = true;
+                switch(type) {
+                    case 'overview':
+                        return this.saveHome();
+                    break;
+                    case 'landing':
+                        return this.saveLanding();
+                    break;
+                    case 'branding':
+                        return this.saveBranding();
+                    break;
+                    case 'posts':
+                        return this.savePosts();
+                    break;
+                    case 'media':
+                        return this.saveMedia();
+                    break;
+                    case 'platform':
+                        return this.savePlatform();
+                    break;
+                    case 'users':
+                        return this.saveUsers();
+                    break;
+                    case 'storage':
+                        return this.saveStorage();
+                    break;
+                }
+            },
+
+            handleAddRule($event) {
+                $event.currentTarget?.blur();
+                this.isSubmittingNewRule = true;
+
+                axios.post('/i/admin/api/settings/rules/add', {
+                    rule: this.newRule
+                }).then(res => {
+                    this.rules.push(this.newRule);
+                    this.newRule = undefined;
+                    this.isSubmittingNewRule = false;
+                    this.showAllRules = true;
+                })
+                .catch(err => {
+                    if(err.response.data && err.response.data?.message) {
+                        swal('Error', err.response.data.message, 'error');
+                    }
+                    this.isSubmittingNewRule = false;
+                })
+            },
+
+            addSuggestedRule(rule, $event) {
+                $event.currentTarget?.blur();
+
+                this.newRule = rule;
+            },
+
+            importAllDefaultRules($event) {
+                $event.currentTarget?.blur();
+                this.isSubmittingNewRule = true;
+                this.showAllRules = true;
+                for (var i = this.suggestedRules.length - 1; i >= 0; i--) {
+                    const rule = this.suggestedRules[i]
+                    setTimeout(() => {
+                        axios.post('/i/admin/api/settings/rules/add', {
+                            rule: rule
+                        }).then(res => {
+                            this.rules.push(rule);
+                        })
+                    }, (i * 300))
+                }
+                this.isSubmittingNewRule = false;
+            },
+
+            handleDeleteRule(rule, idx, $event) {
+                $event.currentTarget?.blur();
+                this.isDeletingRule = true;
+
+                axios.post('/i/admin/api/settings/rules/delete', {
+                    rule: rule,
+                }).then(res => {
+                    this.isDeletingRule = false;
+                    this.rules = res.data;
+                })
+                .catch(err => {
+
+                })
+            },
+
+            handleDeleteAllRules($event) {
+                $event.currentTarget?.blur();
+                this.isDeletingRule = true;
+
+                swal({
+                    title: 'Confirm',
+                    text: 'Are you sure you want to delete all rules?',
+                    buttons: true,
+                    dangerMode: true,
+                }).then(res => {
+                    if(res === true) {
+                        axios.post('/i/admin/api/settings/rules/delete/all')
+                        .then(res => {
+                            this.isDeletingRule = false;
+                            this.rules = []
+                        })
+                        .catch(err => {
+
+                        })
+                    } else {
+                        this.isDeletingRule = false;
+                    }
+                })
+            },
+
+            removeAutofollow(username, $event) {
+                $event.currentTarget?.blur();
+
+                axios.post('/i/admin/api/settings/autofollow/delete', {
+                    username: username
+                }).then(res => {
+                    this.users.admin_autofollow_accounts = res.data.accounts;
+                }).catch(err => {
+                    swal("Oops!", "An error occurred, please try again later!", "error");
+                });
+            },
+
+            addAutofollow($event) {
+                $event.currentTarget?.blur();
+
+                swal({
+                    text: 'Enter account username',
+                    content: "input",
+                    button: {
+                        text: "Add Autofollow",
+                        closeModal: false,
+                    },
+                }).then(username => {
+                    if (!username) throw null;
+
+                    axios.post('/i/admin/api/settings/autofollow/add', {
+                        username: username
+                    })
+                    .then(res => {
+                        if(!res.data.accounts.map(acc => acc.toLowerCase()).includes(username.toLowerCase())) {
+                            swal("Oops!", "The account you attempted to add does not exist or cannot be added!", "error");
+                        }
+                        this.users.admin_autofollow_accounts = res.data.accounts;
+                        swal.stopLoading();
+                        swal.close();
+                    })
+                    .catch(err => {
+                        if(err.response.data && err.response.data.message) {
+                            swal('Error', err.response.data.message, 'error');
+                        } else {
+                            swal("Oops!", "The account you attempted to add does not exist or cannot be added!", "error");
+                        }
+                        swal.stopLoading();
+                        swal.close();
+                    });
+                })
+
+            },
+
+            saveHome() {
+                axios.post('/i/admin/api/settings/update/home', {
+                    registration_status: this.features.registration_status,
+                    cloud_storage: this.features.cloud_storage,
+                    activitypub_enabled: this.features.activitypub_enabled,
+                    account_migration: this.features.account_migration,
+                    mobile_apis: this.features.mobile_apis,
+                    stories: this.features.stories,
+                    instagram_import: this.features.instagram_import,
+                    autospam_enabled: this.features.autospam_enabled,
+                }).then(res => {
+                    this.isSubmitting = false;
+                    this.isSubmittingTimeout = true;
+                    this.isSubmittingTimeoutHandler = setTimeout(() => {
+                        this.isSubmittingTimeout = false;
+                    }, 4000);
+                })
+            },
+
+            saveLanding() {
+                axios.post('/i/admin/api/settings/update/landing', {
+                    current_admin: this.landing.current_admin,
+                    show_directory: this.landing.show_directory,
+                    show_explore: this.landing.show_explore
+                }).then(res => {
+                    this.isSubmitting = false;
+                    this.isSubmittingTimeout = true;
+                    this.isSubmittingTimeoutHandler = setTimeout(() => {
+                        this.isSubmittingTimeout = false;
+                    }, 4000);
+                })
+            },
+
+            saveBranding() {
+                axios.post('/i/admin/api/settings/update/branding', {
+                    name: this.branding.name,
+                    short_description: this.branding.short_description,
+                    long_description: this.branding.long_description
+                }).then(res => {
+                    this.isSubmitting = false;
+                    this.isSubmittingTimeout = true;
+                    this.isSubmittingTimeoutHandler = setTimeout(() => {
+                        this.isSubmittingTimeout = false;
+                    }, 4000);
+                })
+            },
+
+            savePosts() {
+                axios.post('/i/admin/api/settings/update/posts', {
+                    max_caption_length: this.posts.max_caption_length,
+                    max_altext_length: this.posts.max_altext_length,
+                }).then(res => {
+                    this.posts = res.data;
+                    this.isSubmitting = false;
+                    this.isSubmittingTimeout = true;
+                    this.isSubmittingTimeoutHandler = setTimeout(() => {
+                        this.isSubmittingTimeout = false;
+                    }, 4000);
+                })
+                .catch(err => {
+                    this.isSubmitting = false;
+                    if(err.response.data && err.response.data.message) {
+                        swal('Error', err.response.data.message, 'error');
+                    } else {
+                        swal('Oops!', 'An error occured', 'error');
+                    }
+                })
+            },
+
+            saveMedia() {
+                axios.post('/i/admin/api/settings/update/media', {
+                    image_quality: this.media.image_quality,
+                    max_album_length: this.media.max_album_length,
+                    max_photo_size: this.media.max_photo_size,
+                    media_types: this.activeMediaTypes,
+                    optimize_image: this.media.optimize_image,
+                    optimize_video: this.media.optimize_video,
+                }).then(res => {
+                    this.isSubmitting = false;
+                    this.isSubmittingTimeout = true;
+                    this.isSubmittingTimeoutHandler = setTimeout(() => {
+                        this.isSubmittingTimeout = false;
+                    }, 4000);
+                }).catch(err => {
+                    this.isSubmitting = false;
+                    if(err.response.data && err.response.data.message) {
+                        swal('Error', err.response.data.message, 'error');
+                    } else {
+                        swal('Oops!', 'An error occured', 'error');
+                    }
+                })
+            },
+
+            savePlatform() {
+                axios.post('/i/admin/api/settings/update/platform', {
+                    allow_app_registration: this.platform.allow_app_registration,
+                    app_registration_rate_limit_attempts: this.platform.app_registration_rate_limit_attempts,
+                    app_registration_rate_limit_decay: this.platform.app_registration_rate_limit_decay,
+                    app_registration_confirm_rate_limit_attempts: this.platform.app_registration_confirm_rate_limit_attempts,
+                    app_registration_confirm_rate_limit_decay: this.platform.app_registration_confirm_rate_limit_decay,
+                    allow_post_embeds: this.platform.allow_post_embeds,
+                    allow_profile_embeds: this.platform.allow_profile_embeds,
+                    captcha_enabled: this.platform.captcha_enabled,
+                    captcha_secret: this.platform.captcha_secret,
+                    captcha_sitekey: this.platform.captcha_sitekey,
+                    captcha_on_login: this.platform.captcha_on_login,
+                    captcha_on_register: this.platform.captcha_on_register,
+                    custom_emoji_enabled: this.platform.custom_emoji_enabled,
+                }).then(res => {
+                    this.platform = res.data;
+                    this.isSubmitting = false;
+                    this.isSubmittingTimeout = true;
+                    this.isSubmittingTimeoutHandler = setTimeout(() => {
+                        this.isSubmittingTimeout = false;
+                    }, 4000);
+                })
+                .catch(err => {
+                    this.isSubmitting = false;
+                    if(err.response.data && err.response.data.message) {
+                        swal('Error', err.response.data.message, 'error');
+                    } else {
+                        swal('Oops!', 'An error occured', 'error');
+                    }
+                })
+            },
+
+            saveUsers() {
+                axios.post('/i/admin/api/settings/update/users', {
+                    require_email_verification: this.users.require_email_verification,
+                    enforce_account_limit: this.users.enforce_account_limit,
+                    max_account_size: this.users.max_account_size,
+                    admin_autofollow: this.users.admin_autofollow,
+                    admin_autofollow_accounts: this.users.admin_autofollow_accounts,
+                    max_user_blocks: this.users.max_user_blocks,
+                    max_user_mutes: this.users.max_user_mutes,
+                    max_domain_blocks: this.users.max_domain_blocks,
+                }).then(res => {
+                    this.isSubmitting = false;
+                    this.isSubmittingTimeout = true;
+                    this.isSubmittingTimeoutHandler = setTimeout(() => {
+                        this.isSubmittingTimeout = false;
+                    }, 4000);
+                }).catch(err => {
+                    if(err.response.data.message) {
+                        swal('Error', err.response.data.message, 'error');
+                    } else {
+                        swal('Error', 'An unexpected error occurred, please try again!', 'error');
+                    }
+                    this.isSubmitting = false;
+                })
+            },
+
+            saveStorage() {
+                let data = this.showDiskConfig ?
+                    {
+                        primary_disk: this.storage.primary_disk,
+                        update_disk: true,
+                        disk_config: this.storage.disk_config,
+                    } : {
+                        primary_disk: this.storage.primary_disk,
+                    }
+                axios.post('/i/admin/api/settings/update/storage', data)
+                .then(res => {
+                    this.features.cloud_storage = res.data.primary_disk === 'cloud';
+                    this.isSubmitting = false;
+                    this.isSubmittingTimeout = true;
+                    this.isSubmittingTimeoutHandler = setTimeout(() => {
+                        this.isSubmittingTimeout = false;
+                    }, 4000);
+                }).catch(err => {
+                    if(err.response.data.error) {
+                        if(err.response.data.s3_vce) {
+                            let el = document.createElement('div');
+                            el.classList.add('text-left');
+                            el.innerHTML = err.response.data.message;
+                            let wrapper = document.createElement('div');
+                            wrapper.appendChild(el);
+                            swal({
+                                title: 'Invalid S3 Credentials',
+                                content: wrapper,
+                                icon: 'error'
+                            });
+                        } else {
+                            swal('Error', err.response.data.message, 'error');
+                        }
+                    }
+                    this.isSubmitting = false;
+                })
+            },
+
+            handleChange($event, cat, type) {
+                switch(cat) {
+                    case 'features':
+                        this.features[type] = $event;
+                    break;
+
+                    case 'landing':
+                        this.landing[type] = $event;
+                    break;
+
+                    case 'platform':
+                        this.platform[type] = $event;
+                    break;
+
+                    case 'media':
+                        this.media[type] = $event;
+                    break;
+
+                    case 'users':
+                        this.users[type] = $event;
+                    break;
+
+                    case 'storage':
+                        this.storage[type] = $event;
+                    break;
+                }
+                console.log($event)
+                console.log(type)
+            },
+
+            handleSubChange($event, cat, type, sub) {
+                switch(cat) {
+                    case 'features':
+                        this.features[type][sub] = $event;
+                    break;
+
+                    case 'landing':
+                        this.landing[type][sub] = $event;
+                    break;
+
+                    case 'platform':
+                        this.platform[type][sub] = $event;
+                    break;
+
+                    case 'media':
+                        this.media[type][sub] = $event;
+                    break;
+
+                    case 'users':
+                        this.users[type][sub] = $event;
+                    break;
+
+                    case 'storage':
+                        this.storage[type][sub] = $event;
+                    break;
+                }
+                console.log($event)
+                console.log(type)
+            },
+        },
+
+        watch: {
+
+        }
+    }
+</script>
+
+<style lang="scss" scoped>
+    .rule-badge {
+        display: flex;
+        width: 34px;
+        height: 34px;
+        justify-content: center;
+        align-items: center;
+        background-color: #fff;
+        border-radius: 34px;
+        border: 2px solid var(--primary);
+
+        &-inner {
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            width: 26px;
+            height: 26px;
+            border-radius: 26px;
+            background-color: var(--primary);
+            color: #fff;
+            font-weight: bold;
+            font-size: 13px;
+        }
+    }
+    .rule-text {
+        max-width: 90%;
+        margin-bottom: 0px;
+        font-size: 14px;
+    }
+    .gap-1 {
+        gap: 1rem;
+    }
+</style>

+ 1 - 1
resources/assets/components/admin/partial/AdminReadMore.vue

@@ -1,7 +1,7 @@
 <template>
     <div>
         <div class="mb-0" :style="{ 'font-size':`${fontSize}px` }">{{ contentText }}</div>
-        <p class="mb-0"><a v-if="canStepExpand || (canExpand && !expanded)" class="font-weight-bold small" href="#" @click="expand()">Read more</a></p>
+        <p class="mb-0"><a v-if="canStepExpand || (canExpand && !expanded)" class="font-weight-bold small" href="#" @click.prevent="expand()">Read more</a></p>
     </div>
 </template>
 

+ 42 - 0
resources/assets/components/admin/partial/AdminSettingsCheckbox.vue

@@ -0,0 +1,42 @@
+<template>
+    <div class="card shadow-none border card-body">
+        <div class="form-group mb-0">
+            <div class="custom-control custom-checkbox">
+                <input type="checkbox" :name="elementId" class="custom-control-input" :id="elementId" :checked="value" @change="$emit('change', !value)">
+                <label class="custom-control-label font-weight-bold" :for="elementId">{{ name }}</label>
+            </div>
+            <p class="mt-1 mb-0 small text-muted" v-html="description"></p>
+        </div>
+    </div>
+</template>
+
+<script>
+    export default {
+        props: {
+            name: {
+                type: String
+            },
+
+            value: {
+                type: Boolean
+            },
+
+            description: {
+                type: String
+            }
+        },
+
+        computed: {
+            elementId: {
+                get() {
+                    let name = this.name;
+                    name = name.toLowerCase();
+                    name = name.replace(/[^a-z0-9 -]/g, ' ');
+                    name = name.replace(/\s+/g, '-');
+                    name = name.replace(/^-+|-+$/g, '');
+                    return 'fec_' + name;
+                }
+            }
+        }
+    }
+</script>

+ 74 - 0
resources/assets/components/admin/partial/AdminSettingsInput.vue

@@ -0,0 +1,74 @@
+<template>
+    <div :class="[ isCard ? 'card shadow-none border card-body' : '' ]">
+        <div
+            class="form-group"
+            :class="[ isInline ? 'd-flex align-items-center gap-1' : 'mb-1' ]">
+            <label :for="elementId" class="font-weight-bold mb-0">{{ name }}</label>
+            <input
+                :id="elementId"
+                class="form-control form-control-muted mb-0"
+                :placeholder="placeholder"
+                :value="value"
+                @input="$emit('change', $event.target.value)"
+                :disabled="isDisabled" />
+        </div>
+        <p v-if="description && description.length" class="help-text small text-muted mb-0" v-html="description">
+        </p>
+    </div>
+</template>
+
+<script>
+    export default {
+        props: {
+            name: {
+                type: String
+            },
+
+            value: {
+                type: String
+            },
+
+            placeholder: {
+                type: String
+            },
+
+            description: {
+                type: String
+            },
+
+            isCard: {
+                type: Boolean,
+                default: true
+            },
+
+            isInline: {
+                type: Boolean,
+                default: false
+            },
+
+            isDisabled: {
+                type: Boolean,
+                default: false
+            }
+        },
+
+        computed: {
+            elementId: {
+                get() {
+                    let name = this.name;
+                    name = name.toLowerCase();
+                    name = name.replace(/[^a-z0-9 -]/g, ' ');
+                    name = name.replace(/\s+/g, '-');
+                    name = name.replace(/^-+|-+$/g, '');
+                    return 'fec_' + name;
+                }
+            }
+        }
+    }
+</script>
+
+<style lang="scss" scoped="true">
+    .gap-1 {
+        gap: 1rem;
+    }
+</style>

+ 63 - 0
resources/assets/components/admin/partial/AdminSettingsTabHeader.vue

@@ -0,0 +1,63 @@
+<template>
+    <div>
+        <div class="d-flex justify-content-between align-items-center">
+            <div style="width:100px;"></div>
+            <div>
+                <h2 class="display-4 mb-0" style="font-weight: 800;">{{ title }}</h2>
+            </div>
+            <div>
+                <button
+                    class="btn btn-primary rounded-pill font-weight-bold px-5"
+                    :disabled="isSaving || saved"
+                    @click.prevent="save">
+                    <template v-if="isSaving === true"><b-spinner small class="mx-2" /></template>
+                    <template v-else>{{ buttonLabel }}</template>
+                </button>
+            </div>
+        </div>
+        <hr class="mt-3">
+    </div>
+</template>
+
+<script>
+    export default {
+        props: {
+            title: {
+                type: String
+            },
+            saving: {
+                type: Boolean
+            },
+            saved: {
+                type: Boolean
+            }
+        },
+
+        computed: {
+            buttonLabel: {
+                get() {
+                    if(this.saved) {
+                        return 'Saved';
+                    }
+                    if(this.saving) {
+                        return 'Saving';
+                    }
+
+                    return 'Save';
+                }
+            },
+            isSaving: {
+                get() {
+                    return this.saving;
+                }
+            }
+        },
+
+        methods: {
+            save($event) {
+                $event.currentTarget?.blur();
+                this.$emit('save');
+            }
+        }
+    }
+</script>

+ 10 - 0
resources/assets/js/admin.js

@@ -36,11 +36,21 @@ Vue.component(
     require('./../components/admin/AdminReports.vue').default
 );
 
+Vue.component(
+    'admin-settings',
+    require('./../components/admin/AdminSettings.vue').default
+);
+
 Vue.component(
     'instances-component',
     require('./../components/admin/AdminInstances.vue').default
 );
 
+// Vue.component(
+//     'instance-details-component',
+//     require('./../components/admin/AdminInstanceDetails.vue').default
+// );
+
 Vue.component(
     'hashtag-component',
     require('./../components/admin/AdminHashtags.vue').default

+ 1 - 0
resources/assets/sass/lib/nucleo.css

@@ -10,6 +10,7 @@ License - nucleoapp.com/license/
   src: url('/fonts/nucleo-icons.eot') format('embedded-opentype'), url('/fonts/nucleo-icons.woff2') format('woff2'), url('/fonts/nucleo-icons.woff') format('woff'), url('/fonts/nucleo-icons.ttf') format('truetype'), url('/fonts/nucleo-icons.svg') format('svg');
   font-weight: normal;
   font-style: normal;
+  font-display: swap;
 }
 /*------------------------
     base class definition

+ 4 - 0
resources/assets/sass/spa.scss

@@ -392,6 +392,10 @@ span.twitter-typeahead .tt-suggestion:focus {
     font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
 }
 
+.timestamp-overlay-badge {
+    color: var(--dark);
+}
+
 .timeline-status-component {
     .username {
         font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

+ 2 - 2
resources/views/account/moderation/post/autospam.blade.php

@@ -69,7 +69,7 @@
 		<div class="col-12 col-md-6 offset-md-3 my-3">
 			<div class="border rounded p-3 border-primary">
 				<p class="h4 font-weight-bold pt-2 text-primary">Review the Community Guidelines</p>
-				<p class="lead pt-4 text-primary">We want to keep {{config('app.name')}} a safe place for everyone, and we created these <a class="font-weight-bold text-primary" href="{{route('help.community-guidelines')}}">Community Guidelines</a> to support and protect our community.</p>
+				<p class="lead pt-4 text-primary">We want to keep {{config_cache('app.name')}} a safe place for everyone, and we created these <a class="font-weight-bold text-primary" href="{{route('help.community-guidelines')}}">Community Guidelines</a> to support and protect our community.</p>
 			</div>
 		</div>
 
@@ -100,4 +100,4 @@
 	ctx.putImageData(imageData, 0, 0);
 </script>
 @endif
-@endpush
+@endpush

+ 2 - 2
resources/views/account/moderation/post/cw.blade.php

@@ -70,7 +70,7 @@
 		<div class="col-12 col-md-6 offset-md-3 my-3">
 			<div class="border rounded p-3 border-primary">
 				<p class="h4 font-weight-bold pt-2 text-primary">Review the Community Guidelines</p>
-				<p class="lead pt-4 text-primary">We want to keep {{config('app.name')}} a safe place for everyone, and we created these <a class="font-weight-bold text-primary" href="{{route('help.community-guidelines')}}">Community Guidelines</a> to support and protect our community.</p>
+				<p class="lead pt-4 text-primary">We want to keep {{config_cache('app.name')}} a safe place for everyone, and we created these <a class="font-weight-bold text-primary" href="{{route('help.community-guidelines')}}">Community Guidelines</a> to support and protect our community.</p>
 			</div>
 		</div>
 
@@ -127,4 +127,4 @@
 	ctx.putImageData(imageData, 0, 0);
 </script>
 @endif
-@endpush
+@endpush

+ 2 - 2
resources/views/account/moderation/post/removed.blade.php

@@ -62,7 +62,7 @@
 		<div class="col-12 col-md-6 offset-md-3 my-3">
 			<div class="border rounded p-3 border-primary">
 				<p class="h4 font-weight-bold pt-2 text-primary">Review the Community Guidelines</p>
-				<p class="lead pt-4 text-primary">We want to keep {{config('app.name')}} a safe place for everyone, and we created these <a class="font-weight-bold text-primary" href="{{route('help.community-guidelines')}}">Community Guidelines</a> to support and protect our community.</p>
+				<p class="lead pt-4 text-primary">We want to keep {{config_cache('app.name')}} a safe place for everyone, and we created these <a class="font-weight-bold text-primary" href="{{route('help.community-guidelines')}}">Community Guidelines</a> to support and protect our community.</p>
 			</div>
 		</div>
 		<div class="col-12 col-md-6 offset-md-3 mt-4 mb-5">
@@ -96,4 +96,4 @@
 	ctx.putImageData(imageData, 0, 0);
 </script>
 @endif
-@endpush
+@endpush

+ 2 - 2
resources/views/account/moderation/post/unlist.blade.php

@@ -69,7 +69,7 @@
 		<div class="col-12 col-md-6 offset-md-3 my-3">
 			<div class="border rounded p-3 border-primary">
 				<p class="h4 font-weight-bold pt-2 text-primary">Review the Community Guidelines</p>
-				<p class="lead pt-4 text-primary">We want to keep {{config('app.name')}} a safe place for everyone, and we created these <a class="font-weight-bold text-primary" href="{{route('help.community-guidelines')}}">Community Guidelines</a> to support and protect our community.</p>
+				<p class="lead pt-4 text-primary">We want to keep {{config_cache('app.name')}} a safe place for everyone, and we created these <a class="font-weight-bold text-primary" href="{{route('help.community-guidelines')}}">Community Guidelines</a> to support and protect our community.</p>
 			</div>
 		</div>
 
@@ -125,4 +125,4 @@
 	ctx.putImageData(imageData, 0, 0);
 </script>
 @endif
-@endpush
+@endpush

+ 10 - 10
resources/views/admin/diagnostics/home.blade.php

@@ -66,7 +66,7 @@
 		</li>
 		<li>
 			<strong><span class="badge badge-primary">OAUTH</span> enabled: </strong>
-			<span>{{ config_cache('pixelfed.oauth_enabled') ? '✅ true' : '❌ false' }}</span>
+			<span>{{ (bool) config_cache('pixelfed.oauth_enabled') ? '✅ true' : '❌ false' }}</span>
 		</li>
 		<li>
 			<strong><span class="badge badge-primary">OAUTH</span> token_expiration</strong>
@@ -298,7 +298,7 @@
 	<tr>
 		<td><span class="badge badge-primary">FEDERATION</span></td>
 		<td><strong>ACTIVITY_PUB</strong></td>
-		<td><span>{{config_cache('federation.activitypub.enabled') ? '✅ true' : '❌ false' }}</span></td>
+		<td><span>{{(bool) config_cache('federation.activitypub.enabled') ? '✅ true' : '❌ false' }}</span></td>
 	</tr>
 	<tr>
 		<td><span class="badge badge-primary">FEDERATION</span></td>
@@ -358,7 +358,7 @@
 	<tr>
 		<td><span class="badge badge-primary">FEDERATION</span></td>
 		<td><strong>PF_NETWORK_TIMELINE</strong></td>
-		<td><span>{{config_cache('federation.network_timeline') ? '✅ true' : '❌ false' }}</span></td>
+		<td><span>{{(bool) config_cache('federation.network_timeline') ? '✅ true' : '❌ false' }}</span></td>
 	</tr>
 	<tr>
 		<td><span class="badge badge-primary">FEDERATION</span></td>
@@ -368,7 +368,7 @@
 	<tr>
 		<td><span class="badge badge-primary">FEDERATION</span></td>
 		<td><strong>CUSTOM_EMOJI</strong></td>
-		<td><span>{{config_cache('federation.custom_emoji.enabled') ? '✅ true' : '❌ false' }}</span></td>
+		<td><span>{{(bool) config_cache('federation.custom_emoji.enabled') ? '✅ true' : '❌ false' }}</span></td>
 	</tr>
 	<tr>
 		<td><span class="badge badge-primary">FEDERATION</span></td>
@@ -545,7 +545,7 @@
 	<tr>
 		<td><span class="badge badge-primary">INSTANCE</span></td>
 		<td><strong>STORIES_ENABLED</strong></td>
-		<td><span>{{config_cache('instance.stories.enabled') ? '✅ true' : '❌ false' }}</span></td>
+		<td><span>{{(bool) config_cache('instance.stories.enabled') ? '✅ true' : '❌ false' }}</span></td>
 	</tr>
 	<tr>
 		<td><span class="badge badge-primary">INSTANCE</span></td>
@@ -740,7 +740,7 @@
 	<tr>
 		<td><span class="badge badge-primary">PIXELFED</span></td>
 		<td><strong>PF_ENABLE_CLOUD</strong></td>
-		<td><span>{{config_cache('pixelfed.cloud_storage') ? '✅ true' : '❌ false' }}</span></td>
+		<td><span>{{(bool) config_cache('pixelfed.cloud_storage') ? '✅ true' : '❌ false' }}</span></td>
 	</tr>
 	<tr>
 		<td><span class="badge badge-primary">PIXELFED</span></td>
@@ -750,12 +750,12 @@
 	<tr>
 		<td><span class="badge badge-primary">PIXELFED</span></td>
 		<td><strong>PF_OPTIMIZE_IMAGES</strong></td>
-		<td><span>{{config_cache('pixelfed.optimize_image') ? '✅ true' : '❌ false' }}</span></td>
+		<td><span>{{(bool) config_cache('pixelfed.optimize_image') ? '✅ true' : '❌ false' }}</span></td>
 	</tr>
 	<tr>
 		<td><span class="badge badge-primary">PIXELFED</span></td>
 		<td><strong>PF_OPTIMIZE_VIDEOS</strong></td>
-		<td><span>{{config_cache('pixelfed.optimize_video') ? '✅ true' : '❌ false' }}</span></td>
+		<td><span>{{(bool) config_cache('pixelfed.optimize_video') ? '✅ true' : '❌ false' }}</span></td>
 	</tr>
 	<tr>
 		<td><span class="badge badge-primary">PIXELFED</span></td>
@@ -810,12 +810,12 @@
 	<tr>
 		<td><span class="badge badge-primary">PIXELFED</span></td>
 		<td><strong>OAUTH_ENABLED</strong></td>
-		<td><span>{{config_cache('pixelfed.oauth_enabled') ? '✅ true' : '❌ false' }}</span></td>
+		<td><span>{{ (bool) config_cache('pixelfed.oauth_enabled') ? '✅ true' : '❌ false' }}</span></td>
 	</tr>
 	<tr>
 		<td><span class="badge badge-primary">PIXELFED</span></td>
 		<td><strong>PF_BOUNCER_ENABLED</strong></td>
-		<td><span>{{config_cache('pixelfed.bouncer.enabled') ? '✅ true' : '❌ false' }}</span></td>
+		<td><span>{{(bool) config_cache('pixelfed.bouncer.enabled') ? '✅ true' : '❌ false' }}</span></td>
 	</tr>
 	<tr>
 		<td><span class="badge badge-primary">PIXELFED</span></td>

+ 2 - 411
resources/views/admin/settings/home.blade.php

@@ -1,421 +1,12 @@
 @extends('admin.partial.template-full')
 
 @section('section')
-<div class="title mb-4">
-	<h3 class="font-weight-bold">Settings</h3>
-@if(config('instance.enable_cc'))
-	<p class="lead mb-0">Manage instance settings</p>
 </div>
-<form method="post">
-	@csrf
-	<ul class="nav nav-tabs nav-fill border-bottom-0" id="myTab" role="tablist">
-		<li class="nav-item">
-			<a class="nav-link font-weight-bold active" id="home-tab" data-toggle="tab" href="#home" role="tab" aria-controls="home" aria-selected="true"><i class="fas fa-home"></i></a>
-		</li>
-		<li class="nav-item border-none">
-			<a class="nav-link font-weight-bold px-4" id="landing-tab" data-toggle="tab" href="#landing" role="tab" aria-controls="landing">Landing</a>
-		</li>
-		<li class="nav-item border-none">
-			<a class="nav-link font-weight-bold px-4" id="brand-tab" data-toggle="tab" href="#brand" role="tab" aria-controls="brand">Brand</a>
-		</li>
-		{{-- <li class="nav-item border-none">
-			<a class="nav-link font-weight-bold px-4" id="media-tab" data-toggle="tab" href="#media" role="tab" aria-controls="media">Mail</a>
-		</li> --}}
-		<li class="nav-item border-none">
-			<a class="nav-link font-weight-bold px-4" id="media-tab" data-toggle="tab" href="#media" role="tab" aria-controls="media">Media</a>
-		</li>
-		<li class="nav-item border-none">
-			<a class="nav-link font-weight-bold px-4" id="rules-tab" data-toggle="tab" href="#rules" role="tab" aria-controls="rules">Rules</a>
-		</li>
-		<li class="nav-item border-none">
-			<a class="nav-link font-weight-bold px-4" id="users-tab" data-toggle="tab" href="#users" role="tab" aria-controls="users">Users</a>
-		</li>
-		<li class="nav-item">
-			<a class="nav-link font-weight-bold px-4" id="advanced-tab" data-toggle="tab" href="#advanced" role="tab" aria-controls="advanced">Advanced</a>
-		</li>
-	</ul>
-	<div class="tab-content" id="myTabContent">
-
-	<div class="tab-pane fade show active" id="home" role="tabpanel" aria-labelledby="home-tab">
-		{{-- <div class="ml-n4 mr-n2 p-3 border-top border-bottom">
-			<label class="font-weight-bold text-muted">System Configuration</label>
-			<ul class="list-unstyled">
-				<li>
-					<span class="text-muted">Max Upload Size: </span>
-					<span class="font-weight-bold">{{$system['max_upload_size']}}</span>
-				</li>
-				<li>
-					<span class="text-muted">Image Driver: </span>
-					<span class="font-weight-bold">{{$system['image_driver']}}</span>
-				</li>
-				<li>
-					<span class="text-muted">Image Driver Loaded: </span>
-					<span class="font-weight-bold">
-						@if($system['image_driver_loaded'])
-						<i class="fas fa-check text-success"></i>
-						@else
-						<i class="fas fa-times text-danger"></i>
-						@endif
-					</span>
-				</li>
-				<li>
-					<span class="text-muted">File Permissions: </span>
-					<span class="font-weight-bold">
-						@if($system['permissions'])
-						<i class="fas fa-check text-success"></i>
-						@else
-						<i class="fas fa-times text-danger"></i>
-						@endif
-					</span>
-				</li>
-				<li>
-					<span class="text-muted"></span>
-					<span class="font-weight-bold"></span>
-				</li>
-			</ul>
-		</div> --}}
-		<div class="form-group mb-0">
-			<div class="ml-n4 mr-n2 p-3 border-top border-bottom">
-				<label class="font-weight-bold text-muted">Features</label>
-
-				<div class="form-group row mb-5">
-					<label for="staticEmail" class="col-sm-12 col-form-label font-weight-bold">Registration Status</label>
-					<div class="col-sm-4">
-						<select class="custom-select" name="regs">
-							<option value="open" {{ $regState === 'open' ? 'selected' : '' }}>Open - Anyone can register</option>
-							<option value="filtered" {{ $regState === 'filtered' ? 'selected' : '' }}>Filtered - Anyone can apply (Curated Onboarding)</option>
-							<option value="closed" {{ $regState === 'closed' ? 'selected' : '' }}>Closed - Nobody can register</option>
-						</select>
-					</div>
-				</div>
-
-				@if($cloud_ready)
-				<div class="custom-control custom-checkbox mt-2">
-					<input type="checkbox" name="cloud_storage" class="custom-control-input" id="cls1" {{config_cache('pixelfed.cloud_storage') ? 'checked' : ''}}>
-					<label class="custom-control-label font-weight-bold" for="cls1">Cloud Storage</label>
-				</div>
-				<p class="mb-4 small">Store photos &amp; videos on S3 compatible object storage providers.</p>
-				@endif
-
-				<div class="custom-control custom-checkbox mt-2">
-					<input type="checkbox" name="activitypub" class="custom-control-input" id="ap" {{config_cache('federation.activitypub.enabled') ? 'checked' : ''}}>
-					<label class="custom-control-label font-weight-bold" for="ap">ActivityPub</label>
-				</div>
-				<p class="mb-4 small">ActivityPub federation, compatible with Pixelfed, Mastodon and other projects.</p>
-
-                <div class="custom-control custom-checkbox mt-2">
-                    <input type="checkbox" name="account_migration" class="custom-control-input" id="ap_mig" {{(bool)config_cache('federation.migration') ? 'checked' : ''}} {{(bool) config_cache('federation.activitypub.enabled') ? '' : 'disabled="disabled"'}}>
-                    <label class="custom-control-label font-weight-bold" for="ap_mig">Account Migration</label>
-                </div>
-                @if((bool) config_cache('federation.activitypub.enabled'))
-                <p class="mb-4 small">Allow local accounts to migrate to other local or remote accounts.</p>
-                @else
-                <p class="mb-4 small text-muted"><strong>ActivityPub Required</strong> Allow local accounts to migrate to other local or remote accounts.</p>
-                @endif
-
-				{{-- <div class="custom-control custom-checkbox mt-2">
-					<input type="checkbox" name="open_registration" class="custom-control-input" id="openReg" {{config_cache('pixelfed.open_registration') ? 'checked' : ''}}>
-					<label class="custom-control-label font-weight-bold" for="openReg">Open Registrations</label>
-				</div>
-				<p class="mb-4 small">Allow new user registrations.</p> --}}
-
-
-                {{-- <div class="custom-control custom-checkbox mt-2">
-                    <input type="checkbox" name="registration_approvals" class="custom-control-input" id="openRegApproval" {{config_cache('pixelfed.registration_approvals') ? 'checked' : ''}}>
-                    <label class="custom-control-label font-weight-bold" for="openRegApproval">Registration Approval Mode</label>
-                </div>
-                <p class="mb-4 small">Manually review new account registration applications.</p> --}}
-
-				<div class="custom-control custom-checkbox mt-2">
-					<input type="checkbox" name="mobile_apis" class="custom-control-input" id="cf2" {{config_cache('pixelfed.oauth_enabled') ? 'checked' : ''}}>
-					<label class="custom-control-label font-weight-bold" for="cf2">Mobile APIs</label>
-				</div>
-				<p class="mb-4 small">Enable apis required for mobile app support.</p>
-
-				<div class="custom-control custom-checkbox mt-2">
-					<input type="checkbox" name="stories" class="custom-control-input" id="cf3" {{config_cache('instance.stories.enabled') ? 'checked' : ''}}>
-					<label class="custom-control-label font-weight-bold" for="cf3">Stories</label>
-				</div>
-				<p class="mb-4 small">Allow users to share ephemeral Stories.</p>
-
-				<div class="custom-control custom-checkbox mt-2">
-					<input type="checkbox" name="ig_import" class="custom-control-input" id="cf4" {{config_cache('pixelfed.import.instagram.enabled') ? 'checked' : ''}}>
-					<label class="custom-control-label font-weight-bold" for="cf4">Instagram Import</label>
-				</div>
-				<p class="mb-4 small">Allow <span class="font-weight-bold">experimental</span> Instagram Import support.</p>
-
-				<div class="custom-control custom-checkbox mt-2">
-					<input type="checkbox" name="spam_detection" class="custom-control-input" id="cf5" {{config_cache('pixelfed.bouncer.enabled') ? 'checked' : ''}}>
-					<label class="custom-control-label font-weight-bold" for="cf5">Spam detection</label>
-				</div>
-				<p class="mb-4 small">Detect and remove spam from timelines.</p>
-			</div>
-		</div>
-		{{-- <div class="form-group mb-0">
-			<div class="ml-n4 mr-n2 p-3 border-top border-bottom">
-				<label class="font-weight-bold text-muted">Name</label>
-				<input class="form-control col-8" name="name" placeholder="Pixelfed" value="{{config_cache('app.name')}}">
-				<p class="help-text small text-muted mt-3 mb-0">The instance name used in titles, metadata and apis.</p>
-			</div>
-		</div>
-		<div class="form-group mb-0">
-			<div class="ml-n4 mr-n2 p-3 border-bottom">
-				<label class="font-weight-bold text-muted">Short Description</label>
-				<textarea class="form-control" rows="3" name="short_description">{{config_cache('app.short_description')}}</textarea>
-				<p class="help-text small text-muted mt-3 mb-0">Short description of instance used on various pages and apis.</p>
-			</div>
-		</div>
-		<div class="form-group mb-0">
-			<div class="ml-n4 mr-n2 p-3 border-bottom">
-				<label class="font-weight-bold text-muted">Long Description</label>
-				<textarea class="form-control" rows="3" name="long_description">{{config_cache('app.description')}}</textarea>
-				<p class="help-text small text-muted mt-3 mb-0">Longer description of instance used on about page.</p>
-			</div>
-		</div> --}}
-	</div>
-
-	<div class="tab-pane" id="landing" role="tabpanel" aria-labelledby="landing-tab">
-		<div class="form-group mb-0">
-			<div class="ml-n4 mr-n2 p-3 border-top border-bottom">
-				<p class="mb-0 small">Configure your landing page</p>
-			</div>
-		</div>
-		<div class="form-group mb-0">
-			<div class="ml-n4 mr-n2 p-3 border-bottom">
-				<p class="font-weight-bold text-muted">Discovery</p>
-
-				<div class="my-3">
-					<div class="custom-control custom-checkbox">
-						<input type="checkbox" class="custom-control-input" id="show_directory" name="show_directory" {{ config_cache('instance.landing.show_directory') ? 'checked' : ''}}>
-						<label class="custom-control-label font-weight-bold" for="show_directory">Show Directory</label>
-					</div>
-				</div>
-
-				<div class="my-3">
-					<div class="custom-control custom-checkbox">
-						<input type="checkbox" class="custom-control-input" id="show_explore_feed" name="show_explore_feed" {{ config_cache('instance.landing.show_explore') ? 'checked' : ''}}>
-						<label class="custom-control-label font-weight-bold" for="show_explore_feed">Show Explore Feed</label>
-					</div>
-				</div>
-			</div>
-		</div>
-		<div class="form-group mb-0">
-			<div class="ml-n4 mr-n2 p-3 border-bottom">
-				<p class="font-weight-bold text-muted">Admin Account</p>
-
-				<div class="my-3">
-					<select class="custom-select" name="admin_account_id" style="max-width: 300px;">
-						<option selected disabled>Select an admin account</option>
-						@foreach($availableAdmins as $acct)
-							<option
-								value="{{ $acct->profile_id }}" {!! $currentAdmin && $currentAdmin['id'] == $acct->profile_id ? 'selected' : null !!}
-								>
-								<span class="font-weight-bold">&commat;{{ $acct->username }}</span>
-							</option>
-						@endforeach
-					</select>
-				</div>
-			</div>
-		</div>
-	</div>
-
-	<div class="tab-pane" id="brand" role="tabpanel" aria-labelledby="brand-tab">
-		<div class="form-group mb-0">
-			<div class="ml-n4 mr-n2 p-3 border-top border-bottom">
-				<label class="font-weight-bold text-muted">Name</label>
-				<input class="form-control col-8" name="name" placeholder="Pixelfed" value="{{config_cache('app.name')}}">
-				<p class="help-text small text-muted mt-3 mb-0">The instance name used in titles, metadata and apis.</p>
-			</div>
-		</div>
-		<div class="form-group mb-0">
-			<div class="ml-n4 mr-n2 p-3 border-bottom">
-				<label class="font-weight-bold text-muted">Short Description</label>
-				<textarea class="form-control" rows="3" name="short_description">{{config_cache('app.short_description')}}</textarea>
-				<p class="help-text small text-muted mt-3 mb-0">Short description of instance used on various pages and apis.</p>
-			</div>
-		</div>
-		<div class="form-group mb-0">
-			<div class="ml-n4 mr-n2 p-3 border-bottom">
-				<label class="font-weight-bold text-muted">Long Description</label>
-				<textarea class="form-control" rows="3" name="long_description">{{config_cache('app.description')}}</textarea>
-				<p class="help-text small text-muted mt-3 mb-0">Longer description of instance used on about page.</p>
-			</div>
-		</div>
-		<div class="form-group mb-0">
-			<div class="ml-n4 mr-n2 p-3 border-top border-bottom">
-				<label class="font-weight-bold text-muted">About Title</label>
-				<input class="form-control col-8" name="about_title" placeholder="Photo Sharing. For Everyone" value="{{config_cache('about.title')}}">
-				<p class="help-text small text-muted mt-3 mb-0">The header title used on the <a href="/site/about">about page</a>.</p>
-			</div>
-		</div>
-	</div>
-
-	<div class="tab-pane" id="users" role="tabpanel" aria-labelledby="users-tab">
-		<div class="form-group mb-0">
-			<div class="ml-n4 mr-n2 p-3 border-top">
-				<div class="custom-control custom-checkbox mt-2">
-					<input type="checkbox" name="require_email_verification" class="custom-control-input" id="mailVerification" {{config_cache('pixelfed.enforce_email_verification') ? 'checked' : ''}}>
-					<label class="custom-control-label font-weight-bold" for="mailVerification">Require Email Verification</label>
-				</div>
-			</div>
-		</div>
-
-		<div class="form-group">
-			<div class="ml-n4 mr-n2 p-3 border-top">
-				<div class="custom-control custom-checkbox my-2">
-					<input type="checkbox" name="enforce_account_limit" class="custom-control-input" id="userEnforceLimit" {{config_cache('pixelfed.enforce_account_limit') ? 'checked' : ''}}>
-					<label class="custom-control-label font-weight-bold" for="userEnforceLimit">Enable account storage limit</label>
-					<p class="help-text small text-muted">Set a storage limit per user account.</p>
-				</div>
-				<label class="font-weight-bold text-muted">Account Limit</label>
-				<input class="form-control" name="account_limit" placeholder="Pixelfed" value="{{config_cache('pixelfed.max_account_size')}}">
-				<p class="help-text small text-muted mt-3 mb-0">Account limit size in KB.</p>
-				<p class="help-text small text-muted mb-0">{{config_cache('pixelfed.max_account_size')}} KB = {{floor(config_cache('pixelfed.max_account_size') / 1024)}} MB</p>
-			</div>
-		</div>
-
-		<div class="form-group">
-			<div class="ml-n4 mr-n2 p-3 border-top">
-				<div class="custom-control custom-checkbox my-2">
-					<input type="checkbox" name="account_autofollow" class="custom-control-input" id="userAccountAutofollow" {{config_cache('account.autofollow') ? 'checked' : ''}}>
-					<label class="custom-control-label font-weight-bold" for="userAccountAutofollow">Auto Follow Accounts</label>
-					<p class="help-text small text-muted">Enable auto follow accounts, new accounts will follow accounts you set.</p>
-				</div>
-				<label class="font-weight-bold text-muted">Accounts</label>
-				<textarea class="form-control" name="account_autofollow_usernames" placeholder="Add account usernames to follow separated by commas">{{config_cache('account.autofollow_usernames')}}</textarea>
-				<p class="help-text small text-muted mt-3 mb-0">Add account usernames to follow separated by commas.</p>
-			</div>
-		</div>
-	</div>
-
-	<div class="tab-pane" id="media" role="tabpanel" aria-labelledby="media-tab">
-		<div class="form-group mb-0">
-			<div class="ml-n4 mr-n2 p-3 border-top">
-				<label class="font-weight-bold text-muted">Max Size</label>
-				<input class="form-control" name="max_photo_size" value="{{config_cache('pixelfed.max_photo_size')}}">
-				<p class="help-text small text-muted mt-3 mb-0">Maximum file upload size in KB</p>
-				<p class="help-text small text-muted mb-0">{{config_cache('pixelfed.max_photo_size')}} KB = {{number_format(config_cache('pixelfed.max_photo_size') / 1024)}} MB</p>
-			</div>
-		</div>
-		<div class="form-group mb-0">
-			<div class="ml-n4 mr-n2 p-3 border-top">
-				<label class="font-weight-bold text-muted">Photo Album Limit</label>
-				<input class="form-control" name="max_album_length" value="{{config_cache('pixelfed.max_album_length')}}">
-				<p class="help-text small text-muted mt-3 mb-0">The maximum number of photos or videos per album</p>
-			</div>
-		</div>
-		<div class="form-group mb-0">
-			<div class="ml-n4 mr-n2 p-3 border-top">
-				<label class="font-weight-bold text-muted">Image Quality</label>
-				<input class="form-control" name="image_quality" value="{{config_cache('pixelfed.image_quality')}}">
-				<p class="help-text small text-muted mt-3 mb-0">Image optimization quality from 0-100%. Set to 0 to disable image optimization.</p>
-			</div>
-		</div>
-		<div class="form-group">
-			<div class="ml-n4 mr-n2 p-3 border-top border-bottom">
-				<label class="font-weight-bold text-muted">Media Types</label>
-				<div class="custom-control custom-checkbox mt-2">
-					<input type="checkbox" name="type_jpeg" class="custom-control-input" id="mediaType1" {{$jpeg ? 'checked' : ''}}>
-					<label class="custom-control-label" for="mediaType1"><span class="border border-dark px-1 rounded font-weight-bold">JPEG</span></label>
-				</div>
-				<div class="custom-control custom-checkbox mt-2">
-					<input type="checkbox" name="type_png" class="custom-control-input" id="mediaType2" {{$png ? 'checked' : ''}}>
-					<label class="custom-control-label" for="mediaType2"><span class="border border-dark px-1 rounded font-weight-bold">PNG</span></label>
-				</div>
-				<div class="custom-control custom-checkbox mt-2">
-					<input type="checkbox" name="type_gif" class="custom-control-input" id="mediaType3" {{$gif ? 'checked' : ''}}>
-					<label class="custom-control-label" for="mediaType3"><span class="border border-dark px-1 rounded font-weight-bold">GIF</span></label>
-				</div>
-				<div class="custom-control custom-checkbox mt-2">
-					<input type="checkbox" name="type_webp" class="custom-control-input" id="mediaType4" {{$webp ? 'checked' : ''}}>
-					<label class="custom-control-label" for="mediaType4"><span class="border border-dark px-1 rounded font-weight-bold">WebP</span></label>
-				</div>
-				<div class="custom-control custom-checkbox mt-2">
-					<input type="checkbox" name="type_mp4" class="custom-control-input" id="mediaType5" {{$mp4 ? 'checked' : ''}}>
-					<label class="custom-control-label" for="mediaType5"><span class="border border-dark px-1 rounded font-weight-bold">MP4</span></label>
-				</div>
-				<p class="help-text small text-muted mt-3 mb-0">Allowed media types.</p>
-			</div>
-		</div>
-	</div>
-
-	<div class="tab-pane" id="rules" role="tabpanel" aria-labelledby="rules-tab">
-		<div class="border-top">
-			<p class="lead mt-3 py-3 text-center">Add rules that explain what is acceptable use.</p>
-		</div>
-		<div class="ml-n4 mr-n2 p-3 border-top border-bottom">
-			<p class="font-weight-bold text-muted">Active Rules</p>
-			<ol class="font-weight-bold">
-				@if($rules)
-				@foreach($rules as $rule)
-				<li class="mb-4">
-					<p class="mb-0">
-						{{$rule}}
-					</p>
-					<p>
-						<button type="button" class="btn btn-outline-danger btn-sm py-0 rule-delete" data-index="{{$loop->index}}">Delete</button>
-					</p>
-				</li>
-				@endforeach
-				@endif
-			</ol>
-		</div>
-		<div class="form-group mb-0">
-			<div class="ml-n4 mr-n2 p-3 border-top border-bottom">
-				<label class="font-weight-bold text-muted">Add Rule</label>
-				<input class="form-control" name="new_rule" placeholder="Add a new rule, we recommend being descriptive but keeping it short"/>
-			</div>
-		</div>
-	</div>
-
-	<div class="tab-pane" id="advanced" role="tabpanel" aria-labelledby="advanced-tab">
-		<div class="form-group mb-0">
-			<div class="ml-n4 mr-n2 p-3 border-top border-bottom">
-				<label class="font-weight-bold text-muted">Custom CSS</label>
-				<div class="custom-control custom-checkbox my-2">
-					<input type="checkbox" name="show_custom_css" class="custom-control-input" id="showCustomCss" {{config_cache('uikit.show_custom.css') ? 'checked' : ''}}>
-					<label class="custom-control-label font-weight-bold" for="showCustomCss">Enable custom CSS</label>
-				</div>
-				<textarea class="form-control" name="custom_css" rows="3">{{config_cache('uikit.custom.css')}}</textarea>
-				<p class="help-text small text-muted mt-3 mb-0">Add custom CSS, will be used on all pages</p>
-			</div>
-		</div>
-	</div>
-
-	</div>
-
-	<div class="form-group row mb-0 mt-4">
-		<div class="col-12 text-right">
-			<button type="submit" class="btn btn-primary font-weight-bold px-5">Save</button>
-		</div>
-	</div>
-</form>
-@else
-</div>
-<div class="py-5">
-	<p class="lead text-center font-weight-bold">Not enabled</p>
-	<p class="text-center">Add <code>ENABLE_CONFIG_CACHE=true</code> in your <span class="font-weight-bold">.env</span> file <br /> and run <span class="font-weight-bold">php artisan config:cache</span></p>
-</div>
-@endif
+<admin-settings />
 @endsection
 
 @push('scripts')
 <script type="text/javascript">
-	$('.rule-delete').on('click', function(e) {
-		if(window.confirm('Are you sure you want to delete this rule?')) {
-			let idx = e.target.dataset.index;
-			axios.post(window.location.href, {
-				'rule_delete': idx
-			}).then(res => {
-				$('.rule-delete[data-index="'+idx+'"]').parents().eq(1).remove();
-			});
-		}
-	});
-
-	$(document).ready(function() {
-		setTimeout(() => {
-			$('.alert-success').fadeOut();
-		}, 1000);
-	});
+    new Vue({ el: '#panel'});
 </script>
 @endpush

+ 1 - 1
resources/views/auth/email/forgot.blade.php

@@ -65,7 +65,7 @@
                                 </div>
                             </div>
 
-                            @if(config('captcha.enabled') || config('captcha.active.login') || config('captcha.active.register'))
+                            @if((bool) config_cache('captcha.enabled'))
                             <label class="font-weight-bold small text-muted">Captcha</label>
                             <div class="d-flex flex-grow-1">
                                 {!! Captcha::display(['data-theme' => 'dark']) !!}

+ 3 - 3
resources/views/auth/login.blade.php

@@ -76,10 +76,10 @@
                         </div>
 
                         @if(
-                        	config('captcha.enabled') ||
-                        	config('captcha.active.login') ||
+                        	(bool) config_cache('captcha.enabled') &&
+                        	(bool) config_cache('captcha.active.login') ||
                         	(
-                        		config('captcha.triggers.login.enabled') &&
+                        		(bool) config_cache('captcha.triggers.login.enabled') &&
                         		request()->session()->has('login_attempts') &&
                         		request()->session()->get('login_attempts') >= config('captcha.triggers.login.attempts')
                         	)

+ 1 - 1
resources/views/auth/passwords/email.blade.php

@@ -54,7 +54,7 @@
 	                            </div>
 	                        </div>
 
-							@if(config('captcha.enabled'))
+							@if((bool) config_cache('captcha.enabled'))
 							<label class="font-weight-bold small text-muted">Captcha</label>
 	                        <div class="d-flex flex-grow-1">
 	                            {!! Captcha::display(['data-theme' => 'dark']) !!}

+ 1 - 1
resources/views/auth/passwords/reset.blade.php

@@ -109,7 +109,7 @@
 	                            </div>
 	                        </div>
 
-							@if(config('captcha.enabled'))
+							@if((bool) config_cache('captcha.enabled'))
 							<label class="font-weight-bold small pt-3 text-muted">Captcha</label>
 	                        <div class="d-flex flex-grow-1">
 	                            {!! Captcha::display(['data-theme' => 'dark']) !!}

+ 1 - 1
resources/views/auth/register.blade.php

@@ -81,7 +81,7 @@
                             </div>
                         </div>
 
-                        @if(config('captcha.enabled') || config('captcha.active.register'))
+                        @if((bool) config_cache('captcha.enabled') && (bool) config_cache('captcha.active.register'))
                         <div class="d-flex justify-content-center my-3">
                             {!! Captcha::display() !!}
                         </div>

+ 2 - 2
resources/views/home.blade.php

@@ -1,4 +1,4 @@
-@extends('layouts.app',['title' => 'Welcome to ' . config('app.name')])
+@extends('layouts.app',['title' => 'Welcome to ' . config_cache('app.name')])
 
 @section('content')
 <div class="container mt-4">
@@ -14,7 +14,7 @@
                         </div>
                     @endif
 
-                    <p class="lead mb-0">Welcome to {{config('app.name')}}!</p>
+                    <p class="lead mb-0">Welcome to {{config_cache('app.name')}}!</p>
                 </div>
             </div>
 

+ 3 - 3
resources/views/layouts/app-guest.blade.php

@@ -5,11 +5,11 @@
 	<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
 	<meta name="mobile-web-app-capable" content="yes">
 
-	<title>{{ $title ?? config('app.name', 'Pixelfed') }}</title>
+	<title>{{ $title ?? config_cache('app.name', 'Pixelfed') }}</title>
 	<link rel="manifest" href="{{url('/manifest.json')}}">
 
-	<meta property="og:site_name" content="{{ config('app.name', 'pixelfed') }}">
-	<meta property="og:title" content="{{ $title ?? config('app.name', 'pixelfed') }}">
+	<meta property="og:site_name" content="{{ config_cache('app.name', 'pixelfed') }}">
+	<meta property="og:title" content="{{ $title ?? config_cache('app.name', 'pixelfed') }}">
 	<meta property="og:type" content="article">
 	<meta property="og:url" content="{{url(request()->url())}}">
 	@stack('meta')

+ 2 - 2
resources/views/layouts/app.blade.php

@@ -70,11 +70,11 @@
 	<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
 	<meta name="mobile-web-app-capable" content="yes">
 
-	<title>{{ $title ?? config('app.name', 'Pixelfed') }}</title>
+	<title>{{ $title ?? config_cache('app.name', 'Pixelfed') }}</title>
 	<link rel="manifest" href="/manifest.json">
 
 	<meta property="og:site_name" content="Pixelfed">
-	<meta property="og:title" content="{{ $ogTitle ?? $title ?? config('app.name', 'pixelfed') }}">
+	<meta property="og:title" content="{{ $ogTitle ?? $title ?? config_cache('app.name', 'pixelfed') }}">
 	<meta property="og:type" content="{{ $ogType ?? 'article' }}">
 	<meta property="og:url" content="{{url(request()->url())}}">
 	@stack('meta')

+ 2 - 2
resources/views/layouts/blank.blade.php

@@ -11,8 +11,8 @@
 
 	<title>{{ $title ?? config_cache('app.name') }}</title>
 
-	<meta property="og:site_name" content="{{ config('app.name', 'pixelfed') }}">
-	<meta property="og:title" content="{{ $title ?? config('app.name', 'pixelfed') }}">
+	<meta property="og:site_name" content="{{ config_cache('app.name', 'pixelfed') }}">
+	<meta property="og:title" content="{{ $title ?? config_cache('app.name', 'pixelfed') }}">
 	<meta property="og:type" content="article">
 	<meta property="og:url" content="{{request()->url()}}">
 	@stack('meta')

+ 3 - 3
resources/views/layouts/bundle.blade.php

@@ -9,11 +9,11 @@
 
     <meta name="mobile-web-app-capable" content="yes">
 
-    <title>{{ $title ?? config('app.name', 'Laravel') }}</title>
+    <title>{{ $title ?? config_cache('app.name', 'Pixelfed') }}</title>
     <link rel="manifest" href="/manifest.json">
 
-    <meta property="og:site_name" content="{{ config('app.name', 'pixelfed') }}">
-    <meta property="og:title" content="{{ $title ?? config('app.name', 'pixelfed') }}">
+    <meta property="og:site_name" content="{{ config_cache('app.name', 'pixelfed') }}">
+    <meta property="og:title" content="{{ $title ?? config_cache('app.name', 'pixelfed') }}">
     <meta property="og:type" content="article">
     <meta property="og:url" content="{{request()->url()}}">
     @stack('meta')

+ 1 - 1
resources/views/layouts/partial/nav.blade.php

@@ -105,7 +105,7 @@
 									{{__('navmenu.discover')}}
 								</a>
 
-								@if(config_cache('instance.stories.enabled'))
+								@if((bool) config_cache('instance.stories.enabled'))
 								<a class="dropdown-item lead" href="/i/stories/new">
 									<span style="width: 50px;margin-right:14px;">
 										<span class="fal fa-history text-lighter fa-lg"></span>

+ 1 - 1
resources/views/layouts/partial/noauthnav.blade.php

@@ -2,7 +2,7 @@
     <div class="container">
         <a class="navbar-brand d-flex align-items-center" href="{{ url('/') }}">
             <img src="/img/pixelfed-icon-color.svg" height="30px" class="px-2" alt="Pixelfed logo">
-            <span class="font-weight-bold mb-0" style="font-size:20px;">{{ config('app.name', 'Laravel') }}</span>
+            <span class="font-weight-bold mb-0" style="font-size:20px;">{{ config_cache('app.name', 'Pixelfed') }}</span>
         </a>
     </div>
 </nav>

+ 2 - 2
resources/views/portfolio/layout.blade.php

@@ -11,8 +11,8 @@
 
 	<title>{!! $title ?? config_cache('app.name') !!}</title>
 
-	<meta property="og:site_name" content="{{ config('app.name', 'pixelfed') }}">
-	<meta property="og:title" content="{{ $title ?? config('app.name', 'pixelfed') }}">
+	<meta property="og:site_name" content="{{ config_cache('app.name', 'pixelfed') }}">
+	<meta property="og:title" content="{{ $title ?? config_cache('app.name', 'pixelfed') }}">
 	<meta property="og:type" content="article">
 	<meta property="og:url" content="{{request()->url()}}">
 	@stack('meta')

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác