Переглянути джерело

Merge branch 'staging' of github.com:pixelfed/pixelfed into jippi-fork

Christian Winther 1 рік тому
батько
коміт
5a9cfe1f2a
100 змінених файлів з 2257 додано та 820 видалено
  1. 7 0
      .gitattributes
  2. 14 1
      CHANGELOG.md
  3. 284 265
      app/Http/Controllers/Admin/AdminSettingsController.php
  4. 226 0
      app/Http/Controllers/AdminCuratedRegisterController.php
  5. 130 45
      app/Http/Controllers/Api/ApiV1Controller.php
  6. 65 60
      app/Http/Controllers/Api/ApiV2Controller.php
  7. 6 2
      app/Http/Controllers/Auth/RegisterController.php
  8. 398 0
      app/Http/Controllers/CuratedRegisterController.php
  9. 65 0
      app/Jobs/CuratedOnboarding/CuratedOnboardingNotifyAdminNewApplicationPipeline.php
  10. 55 0
      app/Mail/CuratedRegisterAcceptUser.php
  11. 55 0
      app/Mail/CuratedRegisterConfirmEmail.php
  12. 55 0
      app/Mail/CuratedRegisterNotifyAdmin.php
  13. 55 0
      app/Mail/CuratedRegisterNotifyAdminUserResponse.php
  14. 55 0
      app/Mail/CuratedRegisterRejectUser.php
  15. 58 0
      app/Mail/CuratedRegisterRequestDetailsFromUser.php
  16. 55 0
      app/Mail/CuratedRegisterSendMessage.php
  17. 49 0
      app/Models/CuratedRegister.php
  18. 38 0
      app/Models/CuratedRegisterActivity.php
  19. 2 0
      app/Services/ConfigCacheService.php
  20. 4 1
      app/Services/LandingService.php
  21. 1 1
      composer.json
  22. 199 196
      composer.lock
  23. 1 1
      config/cache.php
  24. 1 1
      config/federation.php
  25. 31 0
      config/instance.php
  26. 44 0
      database/migrations/2024_01_16_073327_create_curated_registers_table.php
  27. 42 0
      database/migrations/2024_01_20_091352_create_curated_register_activities_table.php
  28. 259 244
      package-lock.json
  29. 1 1
      package.json
  30. BIN
      public/js/account-import.js
  31. BIN
      public/js/activity.js
  32. BIN
      public/js/admin.js
  33. BIN
      public/js/admin_invite.js
  34. BIN
      public/js/app.js
  35. BIN
      public/js/changelog.bundle.742a06ba0a547120.js
  36. BIN
      public/js/changelog.bundle.bf44edbbfa14bd53.js
  37. BIN
      public/js/collectioncompose.js
  38. BIN
      public/js/collections.js
  39. BIN
      public/js/components.js
  40. BIN
      public/js/compose-classic.js
  41. BIN
      public/js/compose.chunk.1ac292c93b524406.js
  42. BIN
      public/js/compose.chunk.ffae318db42f1072.js
  43. BIN
      public/js/compose.js
  44. BIN
      public/js/daci.chunk.34dc7bad3a0792cc.js
  45. BIN
      public/js/daci.chunk.8d4acc1db3f27a51.js
  46. BIN
      public/js/developers.js
  47. BIN
      public/js/direct.js
  48. BIN
      public/js/discover.chunk.b1846efb6bd1e43c.js
  49. BIN
      public/js/discover.chunk.c2229e1d15bd3ada.js
  50. BIN
      public/js/discover.js
  51. BIN
      public/js/discover~findfriends.chunk.941b524eee8b8d63.js
  52. BIN
      public/js/discover~findfriends.chunk.b1858bea66d9723b.js
  53. BIN
      public/js/discover~hashtag.bundle.6c2ff384b17ea58d.js
  54. BIN
      public/js/discover~hashtag.bundle.a0f00fc7df1f313c.js
  55. BIN
      public/js/discover~memories.chunk.37e0c325f900e163.js
  56. BIN
      public/js/discover~memories.chunk.7d917826c3e9f17b.js
  57. BIN
      public/js/discover~myhashtags.chunk.8886fc0d4736d819.js
  58. BIN
      public/js/discover~myhashtags.chunk.a72fc4882db8afd3.js
  59. BIN
      public/js/discover~serverfeed.chunk.262bf7e3bce843c3.js
  60. BIN
      public/js/discover~serverfeed.chunk.8365948d1867de3a.js
  61. BIN
      public/js/discover~settings.chunk.65d6f3cbe5323ed4.js
  62. BIN
      public/js/discover~settings.chunk.be88dc5ba1a24a7d.js
  63. BIN
      public/js/dms.chunk.2b55effc0e8ba89f.js
  64. BIN
      public/js/dms.chunk.53a951c5de2d95ac.js
  65. BIN
      public/js/dms~message.chunk.76edeafda3d92320.js
  66. BIN
      public/js/dms~message.chunk.976f7edaa6f71137.js
  67. BIN
      public/js/error404.bundle.3bbc118159460db6.js
  68. BIN
      public/js/error404.bundle.b397483e3991ab20.js
  69. BIN
      public/js/hashtag.js
  70. BIN
      public/js/home.chunk.264eeb47bfac56c1.js
  71. 0 0
      public/js/home.chunk.264eeb47bfac56c1.js.LICENSE.txt
  72. BIN
      public/js/home.chunk.88eeebf6c53d4dca.js
  73. BIN
      public/js/i18n.bundle.47cbf9f04d955267.js
  74. BIN
      public/js/i18n.bundle.93a02e275ac1a708.js
  75. BIN
      public/js/landing.js
  76. BIN
      public/js/manifest.js
  77. BIN
      public/js/notifications.chunk.0c5151643e4534aa.js
  78. BIN
      public/js/notifications.chunk.3b92cf46da469de1.js
  79. BIN
      public/js/portfolio.js
  80. BIN
      public/js/post.chunk.5ff16664f9adb901.js
  81. 0 0
      public/js/post.chunk.5ff16664f9adb901.js.LICENSE.txt
  82. BIN
      public/js/post.chunk.eb9804ff282909ae.js
  83. BIN
      public/js/profile-directory.js
  84. BIN
      public/js/profile.chunk.7a6c846c4cb3cfd4.js
  85. BIN
      public/js/profile.chunk.d52916cb68c9a146.js
  86. BIN
      public/js/profile.js
  87. BIN
      public/js/profile~followers.bundle.5d796e79f32d066c.js
  88. BIN
      public/js/profile~followers.bundle.5deed93248f20662.js
  89. BIN
      public/js/profile~following.bundle.7ca7cfa5aaae75e2.js
  90. BIN
      public/js/profile~following.bundle.d2b3b1fc2e05dbd3.js
  91. BIN
      public/js/remote_auth.js
  92. BIN
      public/js/search.js
  93. BIN
      public/js/spa.js
  94. BIN
      public/js/status.js
  95. BIN
      public/js/stories.js
  96. BIN
      public/js/story-compose.js
  97. BIN
      public/js/timeline.js
  98. BIN
      public/js/vendor.js
  99. 2 2
      public/js/vendor.js.LICENSE.txt
  100. BIN
      public/mix-manifest.json

+ 7 - 0
.gitattributes

@@ -3,3 +3,10 @@
 *.scss linguist-vendored
 *.js linguist-vendored
 CHANGELOG.md export-ignore
+
+# Collapse diffs for generated files:
+public/**/*.js text -diff
+public/**/*.json text -diff
+public/**/*.css text -diff
+public/img/* binary -diff
+public/fonts/* binary -diff

+ 14 - 1
CHANGELOG.md

@@ -1,6 +1,19 @@
 # Release Notes
 
 ## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.12...dev)
+
+### Features
+
+- Curated Onboarding ([8dac2caf](https://github.com/pixelfed/pixelfed/commit/8dac2caf))
+
+### Updates
+
+- Update Inbox, cast live filters to lowercase ([d835e0ad](https://github.com/pixelfed/pixelfed/commit/d835e0ad))
+- Update federation config, increase default timeline days falloff to 90 days from 2 days. Fixes #4905 ([011834f4](https://github.com/pixelfed/pixelfed/commit/011834f4))
+- Update cache config, use predis as default redis driver client ([ea6b1623](https://github.com/pixelfed/pixelfed/commit/ea6b1623))
+- Update .gitattributes to collapse diffs on generated files ([ThisIsMissEm](https://github.com/pixelfed/pixelfed/commit/9978b2b9))
+- Update api v1/v2 instance endpoints, bump mastoapi version from 2.7.2 to 3.5.3 ([545f7d5e](https://github.com/pixelfed/pixelfed/commit/545f7d5e))
+- Update ApiV1Controller, implement better limit logic to gracefully handle requests with limits that exceed the max ([1f74a95d](https://github.com/pixelfed/pixelfed/commit/1f74a95d))
 -  ([](https://github.com/pixelfed/pixelfed/commit/))
 
 ## [v0.11.12 (2024-02-16)](https://github.com/pixelfed/pixelfed/compare/v0.11.11...v0.11.12)
@@ -9,7 +22,7 @@
 - Autospam Live Filters - block remote activities based on comma separated keywords ([40b45b2a](https://github.com/pixelfed/pixelfed/commit/40b45b2a))
 - Added Software Update banner to admin home feeds ([b0fb1988](https://github.com/pixelfed/pixelfed/commit/b0fb1988))
 
-### Updated
+### Updates
 
 - Update ApiV1Controller, fix network timeline ([0faf59e3](https://github.com/pixelfed/pixelfed/commit/0faf59e3))
 - Update public/network timelines, fix non-redis response and fix reblogs in home feed ([8b4ac5cc](https://github.com/pixelfed/pixelfed/commit/8b4ac5cc))

+ 284 - 265
app/Http/Controllers/Admin/AdminSettingsController.php

@@ -17,269 +17,288 @@ use Illuminate\Support\Str;
 
 trait AdminSettingsController
 {
-	public function settings(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) : null;
-		$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;
-
-		// $system = [
-		// 	'permissions' => is_writable(base_path('storage')) && is_writable(base_path('bootstrap')),
-		// 	'max_upload_size' => ini_get('post_max_size'),
-		// 	'image_driver' => config('image.driver'),
-		// 	'image_driver_loaded' => extension_loaded(config('image.driver'))
-		// ];
-
-		return view('admin.settings.home', compact(
-			'jpeg',
-			'png',
-			'gif',
-			'mp4',
-			'webp',
-			'rules',
-			'cloud_storage',
-			'cloud_disk',
-			'cloud_ready',
-			'availableAdmins',
-			'currentAdmin'
-			// 'system'
-		));
-	}
-
-	public function settingsHomeStore(Request $request)
-	{
-		$this->validate($request, [
-			'name' => 'nullable|string',
-			'short_description' => 'nullable',
-			'long_description' => 'nullable',
-			'max_photo_size' => 'nullable|integer|min:1',
-			'max_album_length' => 'nullable|integer|min:1|max:100',
-			'image_quality' => 'nullable|integer|min:1|max:100',
-			'type_jpeg' => 'nullable',
-			'type_png' => 'nullable',
-			'type_gif' => 'nullable',
-			'type_mp4' => 'nullable',
-			'type_webp' => 'nullable',
-			'admin_account_id' => 'nullable',
-		]);
-
-		if($request->filled('admin_account_id')) {
-			ConfigCacheService::put('instance.admin.pid', $request->admin_account_id);
-			Cache::forget('api:v1:instance-data:contact');
-			Cache::forget('api:v1:instance-data-response-v1');
-		}
-		if($request->filled('rule_delete')) {
-			$index = (int) $request->input('rule_delete');
-			$rules = ConfigCacheService::get('app.rules');
-			$json = json_decode($rules, true);
-			if(!$rules || empty($json)) {
-				return;
-			}
-			unset($json[$index]);
-			$json = json_encode(array_values($json));
-			ConfigCacheService::put('app.rules', $json);
-			Cache::forget('api:v1:instance-data:rules');
-			Cache::forget('api:v1:instance-data-response-v1');
-			return 200;
-		}
-
-		$media_types = explode(',', config_cache('pixelfed.media_types'));
-		$media_types_original = $media_types;
-
-		$mimes = [
-			'type_jpeg' => 'image/jpeg',
-			'type_png' => 'image/png',
-			'type_gif' => 'image/gif',
-			'type_mp4' => 'video/mp4',
-			'type_webp' => 'image/webp',
-		];
-
-		foreach ($mimes as $key => $value) {
-			if($request->input($key) == 'on') {
-				if(!in_array($value, $media_types)) {
-					array_push($media_types, $value);
-				}
-			} else {
-				$media_types = array_diff($media_types, [$value]);
-			}
-		}
-
-		if($media_types !== $media_types_original) {
-			ConfigCacheService::put('pixelfed.media_types', implode(',', array_unique($media_types)));
-		}
-
-		$keys = [
-			'name' => 'app.name',
-			'short_description' => 'app.short_description',
-			'long_description' => 'app.description',
-			'max_photo_size' => 'pixelfed.max_photo_size',
-			'max_album_length' => 'pixelfed.max_album_length',
-			'image_quality' => 'pixelfed.image_quality',
-			'account_limit' => 'pixelfed.max_account_size',
-			'custom_css' => 'uikit.custom.css',
-			'custom_js' => 'uikit.custom.js',
-			'about_title' => 'about.title'
-		];
-
-		foreach ($keys as $key => $value) {
-			$cc = ConfigCache::whereK($value)->first();
-			$val = $request->input($key);
-			if($cc && $cc->v != $val) {
-				ConfigCacheService::put($value, $val);
-			} else if(!empty($val)) {
-				ConfigCacheService::put($value, $val);
-			}
-		}
-
-		$bools = [
-			'activitypub' => 'federation.activitypub.enabled',
-			'open_registration' => 'pixelfed.open_registration',
-			'mobile_apis' => 'pixelfed.oauth_enabled',
-			'stories' => 'instance.stories.enabled',
-			'ig_import' => 'pixelfed.import.instagram.enabled',
-			'spam_detection' => 'pixelfed.bouncer.enabled',
-			'require_email_verification' => 'pixelfed.enforce_email_verification',
-			'enforce_account_limit' => 'pixelfed.enforce_account_limit',
-			'show_custom_css' => 'uikit.show_custom.css',
-			'show_custom_js' => 'uikit.show_custom.js',
-			'cloud_storage' => 'pixelfed.cloud_storage',
-			'account_autofollow' => 'account.autofollow',
-			'show_directory' => 'instance.landing.show_directory',
-			'show_explore_feed' => 'instance.landing.show_explore',
-		];
-
-		foreach ($bools as $key => $value) {
-			$active = $request->input($key) == 'on';
-
-			if($key == 'activitypub' && $active && !InstanceActor::exists()) {
-				Artisan::call('instance:actor');
-			}
-
-			if( $key == 'mobile_apis' &&
-				$active &&
-				!file_exists(storage_path('oauth-public.key')) &&
-				!file_exists(storage_path('oauth-private.key'))
-			) {
-				Artisan::call('passport:keys');
-				Artisan::call('route:cache');
-			}
-
-			if(config_cache($value) !== $active) {
-				ConfigCacheService::put($value, (bool) $active);
-			}
-		}
-
-		if($request->filled('new_rule')) {
-			$rules = ConfigCacheService::get('app.rules');
-			$val = $request->input('new_rule');
-			if(!$rules) {
-				ConfigCacheService::put('app.rules', json_encode([$val]));
-			} else {
-				$json = json_decode($rules, true);
-				$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');
-		}
-
-		if($request->filled('account_autofollow_usernames')) {
-			$usernames = explode(',', $request->input('account_autofollow_usernames'));
-			$names = [];
-
-			foreach($usernames as $n) {
-				$p = Profile::whereUsername($n)->first();
-				if(!$p) {
-					continue;
-				}
-				array_push($names, $p->username);
-			}
-
-			ConfigCacheService::put('account.autofollow_usernames', implode(',', $names));
-		}
-
-		Cache::forget(Config::CACHE_KEY);
-
-		return redirect('/i/admin/settings')->with('status', 'Successfully updated settings!');
-	}
-
-	public function settingsBackups(Request $request)
-	{
-		$path = storage_path('app/'.config('app.name'));
-		$files = is_dir($path) ? new \DirectoryIterator($path) : [];
-		return view('admin.settings.backups', compact('files'));
-	}
-
-	public function settingsMaintenance(Request $request)
-	{
-		return view('admin.settings.maintenance');
-	}
-
-	public function settingsStorage(Request $request)
-	{
-		$storage = [];
-		return view('admin.settings.storage', compact('storage'));
-	}
-
-	public function settingsFeatures(Request $request)
-	{
-		return view('admin.settings.features');
-	}
-
-	public function settingsPages(Request $request)
-	{
-		$pages = Page::orderByDesc('updated_at')->paginate(10);
-		return view('admin.pages.home', compact('pages'));
-	}
-
-	public function settingsPageEdit(Request $request)
-	{
-		return view('admin.pages.edit');
-	}
-
-	public function settingsSystem(Request $request)
-	{
-		$sys = [
-			'pixelfed' => config('pixelfed.version'),
-			'php' => phpversion(),
-			'laravel' => app()->version(),
-		];
-		switch (config('database.default')) {
-			case 'pgsql':
-			$exp = DB::raw('select version();');
-			$expQuery = $exp->getValue(DB::connection()->getQueryGrammar());
-			$sys['database'] = [
-				'name' => 'Postgres',
-				'version' => explode(' ', DB::select($expQuery)[0]->version)[1]
-			];
-			break;
-
-			case 'mysql':
-			$exp = DB::raw('select version()');
-			$expQuery = $exp->getValue(DB::connection()->getQueryGrammar());
-			$sys['database'] = [
-				'name' => 'MySQL',
-				'version' => DB::select($expQuery)[0]->{'version()'}
-			];
-			break;
-
-			default:
-			$sys['database'] = [
-				'name' => 'Unknown',
-				'version' => '?'
-			];
-			break;
-		}
-		return view('admin.settings.system', compact('sys'));
-	}
+    public function settings(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) : null;
+        $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');
+
+        return view('admin.settings.home', compact(
+            'jpeg',
+            'png',
+            'gif',
+            'mp4',
+            'webp',
+            'rules',
+            'cloud_storage',
+            'cloud_disk',
+            'cloud_ready',
+            'availableAdmins',
+            'currentAdmin',
+            'regState'
+        ));
+    }
+
+    public function settingsHomeStore(Request $request)
+    {
+        $this->validate($request, [
+            'name' => 'nullable|string',
+            'short_description' => 'nullable',
+            'long_description' => 'nullable',
+            'max_photo_size' => 'nullable|integer|min:1',
+            'max_album_length' => 'nullable|integer|min:1|max:100',
+            'image_quality' => 'nullable|integer|min:1|max:100',
+            'type_jpeg' => 'nullable',
+            'type_png' => 'nullable',
+            'type_gif' => 'nullable',
+            'type_mp4' => 'nullable',
+            'type_webp' => 'nullable',
+            'admin_account_id' => 'nullable',
+            'regs' => 'required|in:open,filtered,closed'
+        ]);
+
+        $orb = false;
+        $cob = false;
+        switch($request->input('regs')) {
+            case 'open':
+                $orb = true;
+                $cob = false;
+            break;
+
+            case 'filtered':
+                $orb = false;
+                $cob = true;
+            break;
+
+            case 'closed':
+                $orb = false;
+                $cob = false;
+            break;
+        }
+
+        ConfigCacheService::put('pixelfed.open_registration', (bool) $orb);
+        ConfigCacheService::put('instance.curated_registration.enabled', (bool) $cob);
+
+        if($request->filled('admin_account_id')) {
+            ConfigCacheService::put('instance.admin.pid', $request->admin_account_id);
+            Cache::forget('api:v1:instance-data:contact');
+            Cache::forget('api:v1:instance-data-response-v1');
+        }
+        if($request->filled('rule_delete')) {
+            $index = (int) $request->input('rule_delete');
+            $rules = ConfigCacheService::get('app.rules');
+            $json = json_decode($rules, true);
+            if(!$rules || empty($json)) {
+                return;
+            }
+            unset($json[$index]);
+            $json = json_encode(array_values($json));
+            ConfigCacheService::put('app.rules', $json);
+            Cache::forget('api:v1:instance-data:rules');
+            Cache::forget('api:v1:instance-data-response-v1');
+            return 200;
+        }
+
+        $media_types = explode(',', config_cache('pixelfed.media_types'));
+        $media_types_original = $media_types;
+
+        $mimes = [
+            'type_jpeg' => 'image/jpeg',
+            'type_png' => 'image/png',
+            'type_gif' => 'image/gif',
+            'type_mp4' => 'video/mp4',
+            'type_webp' => 'image/webp',
+        ];
+
+        foreach ($mimes as $key => $value) {
+            if($request->input($key) == 'on') {
+                if(!in_array($value, $media_types)) {
+                    array_push($media_types, $value);
+                }
+            } else {
+                $media_types = array_diff($media_types, [$value]);
+            }
+        }
+
+        if($media_types !== $media_types_original) {
+            ConfigCacheService::put('pixelfed.media_types', implode(',', array_unique($media_types)));
+        }
+
+        $keys = [
+            'name' => 'app.name',
+            'short_description' => 'app.short_description',
+            'long_description' => 'app.description',
+            'max_photo_size' => 'pixelfed.max_photo_size',
+            'max_album_length' => 'pixelfed.max_album_length',
+            'image_quality' => 'pixelfed.image_quality',
+            'account_limit' => 'pixelfed.max_account_size',
+            'custom_css' => 'uikit.custom.css',
+            'custom_js' => 'uikit.custom.js',
+            'about_title' => 'about.title'
+        ];
+
+        foreach ($keys as $key => $value) {
+            $cc = ConfigCache::whereK($value)->first();
+            $val = $request->input($key);
+            if($cc && $cc->v != $val) {
+                ConfigCacheService::put($value, $val);
+            } else if(!empty($val)) {
+                ConfigCacheService::put($value, $val);
+            }
+        }
+
+        $bools = [
+            'activitypub' => 'federation.activitypub.enabled',
+            // 'open_registration' => 'pixelfed.open_registration',
+            'mobile_apis' => 'pixelfed.oauth_enabled',
+            'stories' => 'instance.stories.enabled',
+            'ig_import' => 'pixelfed.import.instagram.enabled',
+            'spam_detection' => 'pixelfed.bouncer.enabled',
+            'require_email_verification' => 'pixelfed.enforce_email_verification',
+            'enforce_account_limit' => 'pixelfed.enforce_account_limit',
+            'show_custom_css' => 'uikit.show_custom.css',
+            'show_custom_js' => 'uikit.show_custom.js',
+            'cloud_storage' => 'pixelfed.cloud_storage',
+            'account_autofollow' => 'account.autofollow',
+            'show_directory' => 'instance.landing.show_directory',
+            'show_explore_feed' => 'instance.landing.show_explore',
+        ];
+
+        foreach ($bools as $key => $value) {
+            $active = $request->input($key) == 'on';
+
+            if($key == 'activitypub' && $active && !InstanceActor::exists()) {
+                Artisan::call('instance:actor');
+            }
+
+            if( $key == 'mobile_apis' &&
+                $active &&
+                !file_exists(storage_path('oauth-public.key')) &&
+                !file_exists(storage_path('oauth-private.key'))
+            ) {
+                Artisan::call('passport:keys');
+                Artisan::call('route:cache');
+            }
+
+            if(config_cache($value) !== $active) {
+                ConfigCacheService::put($value, (bool) $active);
+            }
+        }
+
+        if($request->filled('new_rule')) {
+            $rules = ConfigCacheService::get('app.rules');
+            $val = $request->input('new_rule');
+            if(!$rules) {
+                ConfigCacheService::put('app.rules', json_encode([$val]));
+            } else {
+                $json = json_decode($rules, true);
+                $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');
+        }
+
+        if($request->filled('account_autofollow_usernames')) {
+            $usernames = explode(',', $request->input('account_autofollow_usernames'));
+            $names = [];
+
+            foreach($usernames as $n) {
+                $p = Profile::whereUsername($n)->first();
+                if(!$p) {
+                    continue;
+                }
+                array_push($names, $p->username);
+            }
+
+            ConfigCacheService::put('account.autofollow_usernames', implode(',', $names));
+        }
+
+        Cache::forget(Config::CACHE_KEY);
+
+        return redirect('/i/admin/settings')->with('status', 'Successfully updated settings!');
+    }
+
+    public function settingsBackups(Request $request)
+    {
+        $path = storage_path('app/'.config('app.name'));
+        $files = is_dir($path) ? new \DirectoryIterator($path) : [];
+        return view('admin.settings.backups', compact('files'));
+    }
+
+    public function settingsMaintenance(Request $request)
+    {
+        return view('admin.settings.maintenance');
+    }
+
+    public function settingsStorage(Request $request)
+    {
+        $storage = [];
+        return view('admin.settings.storage', compact('storage'));
+    }
+
+    public function settingsFeatures(Request $request)
+    {
+        return view('admin.settings.features');
+    }
+
+    public function settingsPages(Request $request)
+    {
+        $pages = Page::orderByDesc('updated_at')->paginate(10);
+        return view('admin.pages.home', compact('pages'));
+    }
+
+    public function settingsPageEdit(Request $request)
+    {
+        return view('admin.pages.edit');
+    }
+
+    public function settingsSystem(Request $request)
+    {
+        $sys = [
+            'pixelfed' => config('pixelfed.version'),
+            'php' => phpversion(),
+            'laravel' => app()->version(),
+        ];
+        switch (config('database.default')) {
+            case 'pgsql':
+            $exp = DB::raw('select version();');
+            $expQuery = $exp->getValue(DB::connection()->getQueryGrammar());
+            $sys['database'] = [
+                'name' => 'Postgres',
+                'version' => explode(' ', DB::select($expQuery)[0]->version)[1]
+            ];
+            break;
+
+            case 'mysql':
+            $exp = DB::raw('select version()');
+            $expQuery = $exp->getValue(DB::connection()->getQueryGrammar());
+            $sys['database'] = [
+                'name' => 'MySQL',
+                'version' => DB::select($expQuery)[0]->{'version()'}
+            ];
+            break;
+
+            default:
+            $sys['database'] = [
+                'name' => 'Unknown',
+                'version' => '?'
+            ];
+            break;
+        }
+        return view('admin.settings.system', compact('sys'));
+    }
 }

+ 226 - 0
app/Http/Controllers/AdminCuratedRegisterController.php

@@ -0,0 +1,226 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Request;
+use App\Models\CuratedRegister;
+use App\Models\CuratedRegisterActivity;
+use Illuminate\Support\Str;
+use Illuminate\Support\Facades\Mail;
+use App\Mail\CuratedRegisterRequestDetailsFromUser;
+use App\Mail\CuratedRegisterAcceptUser;
+use App\Mail\CuratedRegisterRejectUser;
+use App\User;
+
+class AdminCuratedRegisterController extends Controller
+{
+    public function __construct()
+    {
+        $this->middleware(['auth','admin']);
+    }
+
+    public function index(Request $request)
+    {
+        $this->validate($request, [
+            'filter' => 'sometimes|in:open,all,awaiting,approved,rejected'
+        ]);
+        $filter = $request->input('filter', 'open');
+        $records = CuratedRegister::when($filter, function($q, $filter) {
+                if($filter === 'open') {
+                    return $q->where('is_rejected', false)
+                    ->whereNotNull('email_verified_at')
+                    ->whereIsClosed(false);
+                } else if($filter === 'all') {
+                    return $q;
+                } elseif ($filter === 'awaiting') {
+                    return $q->whereIsClosed(false)
+                        ->whereNull('is_rejected')
+                        ->whereNull('is_approved');
+                } elseif ($filter === 'approved') {
+                    return $q->whereIsClosed(true)->whereIsApproved(true);
+                } elseif ($filter === 'rejected') {
+                    return $q->whereIsClosed(true)->whereIsRejected(true);
+                }
+            })
+            ->latest()
+            ->paginate(10);
+        return view('admin.curated-register.index', compact('records', 'filter'));
+    }
+
+    public function show(Request $request, $id)
+    {
+        $record = CuratedRegister::findOrFail($id);
+        return view('admin.curated-register.show', compact('record'));
+    }
+
+    public function apiActivityLog(Request $request, $id)
+    {
+        $record = CuratedRegister::findOrFail($id);
+
+        $res = collect([
+            [
+                'id' => 1,
+                'action' => 'created',
+                'title' => 'Onboarding application created',
+                'message' => null,
+                'link' => null,
+                'timestamp' => $record->created_at,
+            ]
+        ]);
+
+        if($record->email_verified_at) {
+            $res->push([
+                'id' => 3,
+                'action' => 'email_verified_at',
+                'title' => 'Applicant successfully verified email address',
+                'message' => null,
+                'link' => null,
+                'timestamp' => $record->email_verified_at,
+            ]);
+        }
+
+        $activities = CuratedRegisterActivity::whereRegisterId($record->id)->get();
+
+        $idx = 4;
+        $userResponses = collect([]);
+
+        foreach($activities as $activity) {
+            $idx++;
+            if($activity->from_user) {
+                $userResponses->push($activity);
+                continue;
+            }
+            $res->push([
+                'id' => $idx,
+                'aid' => $activity->id,
+                'action' => $activity->type,
+                'title' => $activity->from_admin ? 'Admin requested info' : 'User responded',
+                'message' => $activity->message,
+                'link' => $activity->adminReviewUrl(),
+                'timestamp' => $activity->created_at,
+            ]);
+        }
+
+        foreach($userResponses as $ur) {
+            $res = $res->map(function($r) use($ur) {
+                if(!isset($r['aid'])) {
+                    return $r;
+                }
+                if($ur->reply_to_id === $r['aid']) {
+                    $r['user_response'] = $ur;
+                    return $r;
+                }
+                return $r;
+            });
+        }
+
+        if($record->is_approved) {
+            $idx++;
+            $res->push([
+                'id' => $idx,
+                'action' => 'approved',
+                'title' => 'Application Approved',
+                'message' => null,
+                'link' => null,
+                'timestamp' => $record->action_taken_at,
+            ]);
+        } else if ($record->is_rejected) {
+            $idx++;
+            $res->push([
+                'id' => $idx,
+                'action' => 'rejected',
+                'title' => 'Application Rejected',
+                'message' => null,
+                'link' => null,
+                'timestamp' => $record->action_taken_at,
+            ]);
+        }
+
+        return $res->reverse()->values();
+    }
+
+    public function apiMessagePreviewStore(Request $request, $id)
+    {
+        $record = CuratedRegister::findOrFail($id);
+        return $request->all();
+    }
+
+    public function apiMessageSendStore(Request $request, $id)
+    {
+        $this->validate($request, [
+            'message' => 'required|string|min:5|max:1000'
+        ]);
+        $record = CuratedRegister::findOrFail($id);
+        abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email');
+        $activity = new CuratedRegisterActivity;
+        $activity->register_id = $record->id;
+        $activity->admin_id = $request->user()->id;
+        $activity->secret_code = Str::random(32);
+        $activity->type = 'request_details';
+        $activity->from_admin = true;
+        $activity->message = $request->input('message');
+        $activity->save();
+        $record->is_awaiting_more_info = true;
+        $record->save();
+        Mail::to($record->email)->send(new CuratedRegisterRequestDetailsFromUser($record, $activity));
+        return $request->all();
+    }
+
+    public function previewDetailsMessageShow(Request $request, $id)
+    {
+        $record = CuratedRegister::findOrFail($id);
+        abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email');
+        $activity = new CuratedRegisterActivity;
+        $activity->message = $request->input('message');
+        return new \App\Mail\CuratedRegisterRequestDetailsFromUser($record, $activity);
+    }
+
+
+    public function previewMessageShow(Request $request, $id)
+    {
+        $record = CuratedRegister::findOrFail($id);
+        abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email');
+        $record->message = $request->input('message');
+        return new \App\Mail\CuratedRegisterSendMessage($record);
+    }
+
+    public function apiHandleReject(Request $request, $id)
+    {
+        $this->validate($request, [
+            'action' => 'required|in:reject-email,reject-silent'
+        ]);
+        $action = $request->input('action');
+        $record = CuratedRegister::findOrFail($id);
+        abort_if($record->email_verified_at === null, 400, 'Cannot reject an unverified email');
+        $record->is_rejected = true;
+        $record->is_closed = true;
+        $record->action_taken_at = now();
+        $record->save();
+        if($action === 'reject-email') {
+            Mail::to($record->email)->send(new CuratedRegisterRejectUser($record));
+        }
+        return [200];
+    }
+
+    public function apiHandleApprove(Request $request, $id)
+    {
+        $record = CuratedRegister::findOrFail($id);
+        abort_if($record->email_verified_at === null, 400, 'Cannot reject an unverified email');
+        $record->is_approved = true;
+        $record->is_closed = true;
+        $record->action_taken_at = now();
+        $record->save();
+        $user = User::create([
+            'name' => $record->username,
+            'username' => $record->username,
+            'email' => $record->email,
+            'password' => $record->password,
+            'app_register_ip' => $record->ip_address,
+            'email_verified_at' => now(),
+            'register_source' => 'cur_onboarding'
+        ]);
+
+        Mail::to($record->email)->send(new CuratedRegisterAcceptUser($record));
+        return [200];
+    }
+}

+ 130 - 45
app/Http/Controllers/Api/ApiV1Controller.php

@@ -496,9 +496,12 @@ class ApiV1Controller extends Controller
         abort_if(!$account, 404);
         $pid = $request->user()->profile_id;
         $this->validate($request, [
-            'limit' => 'sometimes|integer|min:1|max:80'
+            'limit' => 'sometimes|integer|min:1'
         ]);
         $limit = $request->input('limit', 10);
+        if($limit > 80) {
+            $limit = 80;
+        }
         $napi = $request->has(self::PF_API_ENTITY_KEY);
 
         if($account && strpos($account['acct'], '@') != -1) {
@@ -594,9 +597,12 @@ class ApiV1Controller extends Controller
         abort_if(!$account, 404);
         $pid = $request->user()->profile_id;
         $this->validate($request, [
-            'limit' => 'sometimes|integer|min:1|max:80'
+            'limit' => 'sometimes|integer|min:1'
         ]);
         $limit = $request->input('limit', 10);
+        if($limit > 80) {
+            $limit = 80;
+        }
         $napi = $request->has(self::PF_API_ENTITY_KEY);
 
         if($account && strpos($account['acct'], '@') != -1) {
@@ -698,7 +704,7 @@ class ApiV1Controller extends Controller
             'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
             'since_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
             'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
-            'limit' => 'nullable|integer|min:1|max:100'
+            'limit' => 'nullable|integer|min:1'
         ]);
 
         $napi = $request->has(self::PF_API_ENTITY_KEY);
@@ -713,7 +719,10 @@ class ApiV1Controller extends Controller
             abort_if(in_array($domain, InstanceService::getBannedDomains()), 404);
         }
 
-        $limit = $request->limit ?? 20;
+        $limit = $request->input('limit') ?? 20;
+        if($limit > 40) {
+            $limit = 40;
+        }
         $max_id = $request->max_id;
         $min_id = $request->min_id;
 
@@ -959,12 +968,16 @@ class ApiV1Controller extends Controller
         abort_if(!$request->user(), 403);
 
         $this->validate($request, [
-            'id'    => 'required|array|min:1|max:20',
+            'id'    => 'required|array|min:1',
             'id.*'  => 'required|integer|min:1|max:' . PHP_INT_MAX
         ]);
+        $ids = $request->input('id');
+        if(count($ids) > 20) {
+            $ids = collect($ids)->take(20)->toArray();
+        }
         $napi = $request->has(self::PF_API_ENTITY_KEY);
         $pid = $request->user()->profile_id ?? $request->user()->profile->id;
-        $res = collect($request->input('id'))
+        $res = collect($ids)
             ->filter(function($id) use($pid) {
                 return intval($id) !== intval($pid);
             })
@@ -989,8 +1002,8 @@ class ApiV1Controller extends Controller
         abort_unless($request->user()->tokenCan('read'), 403);
 
         $this->validate($request, [
-            'q'         => 'required|string|min:1|max:255',
-            'limit'     => 'nullable|integer|min:1|max:40',
+            'q'         => 'required|string|min:1|max:30',
+            'limit'     => 'nullable|integer|min:1',
             'resolve'   => 'nullable'
         ]);
 
@@ -1000,22 +1013,23 @@ class ApiV1Controller extends Controller
         AccountService::setLastActive($user->id);
         $query = $request->input('q');
         $limit = $request->input('limit') ?? 20;
-        $resolve = (bool) $request->input('resolve', false);
-        $q = '%' . $query . '%';
-
-        $profiles = Cache::remember('api:v1:accounts:search:' . sha1($query) . ':limit:' . $limit, 86400, function() use($q, $limit) {
-            return Profile::whereNull('status')
-                ->where('username', 'like', $q)
-                ->orWhere('name', 'like', $q)
-                ->limit($limit)
-                ->pluck('id')
-                ->map(function($id) {
-                    return AccountService::getMastodon($id);
-                })
-                ->filter(function($account) {
-                    return $account && isset($account['id']);
-                });
-        });
+        if($limit > 20) {
+            $limit = 20;
+        }
+        $resolve = $request->boolean('resolve', false);
+        $q = $query . '%';
+
+        $profiles = Profile::where('username', 'like', $q)
+            ->orderByDesc('followers_count')
+            ->limit($limit)
+            ->pluck('id')
+            ->map(function($id) {
+                return AccountService::getMastodon($id);
+            })
+            ->filter(function($account) {
+                return $account && isset($account['id']);
+            })
+            ->values();
 
         return $this->json($profiles);
     }
@@ -1033,20 +1047,25 @@ class ApiV1Controller extends Controller
         abort_unless($request->user()->tokenCan('read'), 403);
 
         $this->validate($request, [
-            'limit'     => 'nullable|integer|min:1|max:40',
-            'page'      => 'nullable|integer|min:1|max:10'
+            'limit'     => 'sometimes|integer|min:1',
+            'page'      => 'sometimes|integer|min:1'
         ]);
 
         $user = $request->user();
         $limit = $request->input('limit') ?? 40;
+        if($limit > 80) {
+            $limit = 80;
+        }
 
-        $blocked = UserFilter::select('filterable_id','filterable_type','filter_type','user_id')
+        $blocks = UserFilter::select('filterable_id','filterable_type','filter_type','user_id')
             ->whereUserId($user->profile_id)
             ->whereFilterableType('App\Profile')
             ->whereFilterType('block')
             ->orderByDesc('id')
             ->simplePaginate($limit)
-            ->pluck('filterable_id')
+            ->withQueryString();
+
+        $res = $blocks->pluck('filterable_id')
             ->map(function($id) {
                 return AccountService::get($id, true);
             })
@@ -1055,7 +1074,23 @@ class ApiV1Controller extends Controller
             })
             ->values();
 
-        return $this->json($blocked);
+        $baseUrl = config('app.url') . '/api/v1/blocks?limit=' . $limit . '&';
+        $next = $blocks->nextPageUrl();
+        $prev = $blocks->previousPageUrl();
+
+        if($next && !$prev) {
+            $link = '<'.$next.'>; rel="next"';
+        }
+
+        if(!$next && $prev) {
+            $link = '<'.$prev.'>; rel="prev"';
+        }
+
+        if($next && $prev) {
+            $link = '<'.$next.'>; rel="next",<'.$prev.'>; rel="prev"';
+        }
+        $headers = isset($link) ? ['Link' => $link] : [];
+        return $this->json($res, 200, $headers);
     }
 
     /**
@@ -1247,13 +1282,16 @@ class ApiV1Controller extends Controller
         abort_unless($request->user()->tokenCan('read'), 403);
 
         $this->validate($request, [
-            'limit' => 'sometimes|integer|min:1|max:40'
+            'limit' => 'sometimes|integer|min:1'
         ]);
 
         $user = $request->user();
         $maxId = $request->input('max_id');
         $minId = $request->input('min_id');
         $limit = $request->input('limit') ?? 10;
+        if($limit > 40) {
+            $limit = 40;
+        }
 
         $res = Like::whereProfileId($user->profile_id)
             ->when($maxId, function($q, $maxId) {
@@ -1612,15 +1650,15 @@ class ApiV1Controller extends Controller
                 'short_description' => config_cache('app.short_description'),
                 'description' => config_cache('app.description'),
                 'email' => config('instance.email'),
-                'version' => '2.7.2 (compatible; Pixelfed ' . config('pixelfed.version') .')',
+                'version' => '3.5.3 (compatible; Pixelfed ' . config('pixelfed.version') .')',
                 'urls' => [
-                    'streaming_api' => 'wss://' . config('pixelfed.domain.app')
+                    'streaming_api' => null,
                 ],
                 'stats' => $stats,
                 'thumbnail' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')),
                 'languages' => [config('app.locale')],
                 'registrations' => (bool) config_cache('pixelfed.open_registration'),
-                'approval_required' => false,
+                'approval_required' => (bool) config_cache('instance.curated_registration.enabled'),
                 'contact_account' => $contact,
                 'rules' => $rules,
                 'configuration' => [
@@ -2049,18 +2087,23 @@ class ApiV1Controller extends Controller
         abort_unless($request->user()->tokenCan('read'), 403);
 
         $this->validate($request, [
-            'limit' => 'nullable|integer|min:1|max:40'
+            'limit' => 'sometimes|integer|min:1'
         ]);
 
         $user = $request->user();
         $limit = $request->input('limit', 40);
+        if($limit > 80) {
+            $limit = 80;
+        }
 
         $mutes = UserFilter::whereUserId($user->profile_id)
             ->whereFilterableType('App\Profile')
             ->whereFilterType('mute')
             ->orderByDesc('id')
             ->simplePaginate($limit)
-            ->pluck('filterable_id')
+            ->withQueryString();
+
+        $res = $mutes->pluck('filterable_id')
             ->map(function($id) {
                 return AccountService::get($id, true);
             })
@@ -2069,7 +2112,23 @@ class ApiV1Controller extends Controller
             })
             ->values();
 
-        return $this->json($mutes);
+        $baseUrl = config('app.url') . '/api/v1/mutes?limit=' . $limit . '&';
+        $next = $mutes->nextPageUrl();
+        $prev = $mutes->previousPageUrl();
+
+        if($next && !$prev) {
+            $link = '<'.$next.'>; rel="next"';
+        }
+
+        if(!$next && $prev) {
+            $link = '<'.$prev.'>; rel="prev"';
+        }
+
+        if($next && $prev) {
+            $link = '<'.$next.'>; rel="next",<'.$prev.'>; rel="prev"';
+        }
+        $headers = isset($link) ? ['Link' => $link] : [];
+        return $this->json($res, 200, $headers);
     }
 
     /**
@@ -2181,7 +2240,7 @@ class ApiV1Controller extends Controller
         abort_unless($request->user()->tokenCan('read'), 403);
 
         $this->validate($request, [
-            'limit' => 'nullable|integer|min:1|max:100',
+            'limit' => 'sometimes|integer|min:1',
             'min_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX,
             'max_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX,
             'since_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX,
@@ -2191,6 +2250,9 @@ class ApiV1Controller extends Controller
 
         $pid = $request->user()->profile_id;
         $limit = $request->input('limit', 20);
+        if($limit > 40) {
+            $limit = 40;
+        }
 
         $since = $request->input('since_id');
         $min = $request->input('min_id');
@@ -2200,6 +2262,10 @@ class ApiV1Controller extends Controller
             $min = 1;
         }
 
+        if($since) {
+            $min = $since + 1;
+        }
+
         $types = $request->input('types');
 
         $maxId = null;
@@ -2261,7 +2327,7 @@ class ApiV1Controller extends Controller
             'page'        => 'sometimes|integer|max:40',
             'min_id'      => 'sometimes|integer|min:0|max:' . PHP_INT_MAX,
             'max_id'      => 'sometimes|integer|min:0|max:' . PHP_INT_MAX,
-            'limit'       => 'sometimes|integer|min:1|max:40',
+            'limit'       => 'sometimes|integer|min:1',
             'include_reblogs' => 'sometimes',
         ]);
 
@@ -2270,6 +2336,9 @@ class ApiV1Controller extends Controller
         $min = $request->input('min_id');
         $max = $request->input('max_id');
         $limit = $request->input('limit') ?? 20;
+        if($limit > 40) {
+            $limit = 40;
+        }
         $pid = $request->user()->profile_id;
         $includeReblogs = $request->filled('include_reblogs') ? $request->boolean('include_reblogs') : false;
         $nullFields = $includeReblogs ?
@@ -2515,7 +2584,7 @@ class ApiV1Controller extends Controller
         $this->validate($request,[
           'min_id'      => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
           'max_id'      => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
-          'limit'       => 'nullable|integer|max:100',
+          'limit'       => 'sometimes|integer|min:1',
           'remote'      => 'sometimes',
           'local'       => 'sometimes'
         ]);
@@ -2525,6 +2594,9 @@ class ApiV1Controller extends Controller
         $max = $request->input('max_id');
         $minOrMax = $request->anyFilled(['max_id', 'min_id']);
         $limit = $request->input('limit') ?? 20;
+        if($limit > 40) {
+            $limit = 40;
+        }
         $user = $request->user();
 
         $remote = $request->has('remote');
@@ -3043,10 +3115,13 @@ class ApiV1Controller extends Controller
         abort_unless($request->user()->tokenCan('read'), 403);
 
         $this->validate($request, [
-            'limit' => 'nullable|integer|min:1|max:80'
+            'limit' => 'sometimes|integer|min:1'
         ]);
 
-        $limit = $request->input('limit', 10);
+        $limit = $request->input('limit', 40);
+        if($limit > 80) {
+            $limit = 80;
+        }
         $user = $request->user();
         $pid = $user->profile_id;
         $status = Status::findOrFail($id);
@@ -3485,7 +3560,7 @@ class ApiV1Controller extends Controller
           'page'        => 'nullable|integer|max:40',
           'min_id'      => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
           'max_id'      => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
-          'limit'       => 'nullable|integer|max:100',
+          'limit'       => 'sometimes|integer|min:1',
           'only_media'  => 'sometimes|boolean',
           '_pe'         => 'sometimes'
         ]);
@@ -3518,6 +3593,9 @@ class ApiV1Controller extends Controller
         $min = $request->input('min_id');
         $max = $request->input('max_id');
         $limit = $request->input('limit', 20);
+        if($limit > 40) {
+            $limit = 40;
+        }
         $onlyMedia = $request->input('only_media', true);
         $pe = $request->has(self::PF_API_ENTITY_KEY);
         $pid = $request->user()->profile_id;
@@ -3547,7 +3625,7 @@ class ApiV1Controller extends Controller
             ->whereStatusVisibility('public')
             ->where('status_id', $dir, $id)
             ->orderBy('status_id', 'desc')
-            ->limit($limit)
+            ->limit(100)
             ->pluck('status_id')
             ->map(function ($i) use($pe) {
                 return $pe ? StatusService::get($i) : StatusService::getMastodon($i);
@@ -3565,6 +3643,7 @@ class ApiV1Controller extends Controller
                 $domain = strtolower(parse_url($i['url'], PHP_URL_HOST));
                 return !in_array($i['account']['id'], $filters) && !in_array($domain, $domainBlocks);
             })
+            ->take($limit)
             ->values()
             ->toArray();
 
@@ -3584,7 +3663,7 @@ class ApiV1Controller extends Controller
         abort_unless($request->user()->tokenCan('read'), 403);
 
         $this->validate($request, [
-            'limit' => 'nullable|integer|min:1|max:40',
+            'limit' => 'sometimes|integer|min:1',
             'max_id' => 'nullable|integer|min:0',
             'since_id' => 'nullable|integer|min:0',
             'min_id' => 'nullable|integer|min:0'
@@ -3593,6 +3672,9 @@ class ApiV1Controller extends Controller
         $pe = $request->has('_pe');
         $pid = $request->user()->profile_id;
         $limit = $request->input('limit') ?? 20;
+        if($limit > 40) {
+            $limit = 40;
+        }
         $max_id = $request->input('max_id');
         $since_id = $request->input('since_id');
         $min_id = $request->input('min_id');
@@ -3758,11 +3840,14 @@ class ApiV1Controller extends Controller
         abort_if(!$request->user(), 403);
 
         $this->validate($request, [
-            'limit' => 'int|min:1|max:10',
+            'limit' => 'sometimes|integer|min:1',
             'sort' => 'in:all,newest,popular'
         ]);
 
         $limit = $request->input('limit', 3);
+        if($limit > 10) {
+            $limit = 10;
+        }
         $pid = $request->user()->profile_id;
         $status = StatusService::getMastodon($id, false);
 

+ 65 - 60
app/Http/Controllers/Api/ApiV2Controller.php

@@ -71,72 +71,77 @@ class ApiV2Controller extends Controller
                 ->toArray() : [];
         });
 
-        $res = [
-            'domain' => config('pixelfed.domain.app'),
-            'title' => config_cache('app.name'),
-            'version' => config('pixelfed.version'),
-            'source_url' => 'https://github.com/pixelfed/pixelfed',
-            'description' => config_cache('app.short_description'),
-            'usage' => [
-                'users' => [
-                    'active_month' => (int) Nodeinfo::activeUsersMonthly()
-                ]
-            ],
-            'thumbnail' => [
-                'url' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')),
-                'blurhash' => InstanceService::headerBlurhash(),
-                'versions' => [
-                    '@1x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')),
-                    '@2x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg'))
-                ]
-            ],
-            'languages' => [config('app.locale')],
-            'configuration' => [
-                'urls' => [
-                    'streaming' => 'wss://' . config('pixelfed.domain.app'),
-                    'status' => null
-                ],
-                'vapid' => [
-                    'public_key' => config('webpush.vapid.public_key'),
-                ],
-                'accounts' => [
-                    'max_featured_tags' => 0,
+        $res = Cache::remember('api:v2:instance-data-response-v2', 1800, function () use($contact, $rules) {
+            return [
+                'domain' => config('pixelfed.domain.app'),
+                'title' => config_cache('app.name'),
+                'version' => '3.5.3 (compatible; Pixelfed ' . config('pixelfed.version') .')',
+                'source_url' => 'https://github.com/pixelfed/pixelfed',
+                'description' => config_cache('app.short_description'),
+                'usage' => [
+                    'users' => [
+                        'active_month' => (int) Nodeinfo::activeUsersMonthly()
+                    ]
                 ],
-                'statuses' => [
-                    'max_characters' => (int) config('pixelfed.max_caption_length'),
-                    'max_media_attachments' => (int) config_cache('pixelfed.max_album_length'),
-                    'characters_reserved_per_url' => 23
+                'thumbnail' => [
+                    'url' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')),
+                    'blurhash' => InstanceService::headerBlurhash(),
+                    'versions' => [
+                        '@1x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')),
+                        '@2x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg'))
+                    ]
                 ],
-                'media_attachments' => [
-                    'supported_mime_types' => explode(',', config_cache('pixelfed.media_types')),
-                    'image_size_limit' => config_cache('pixelfed.max_photo_size') * 1024,
-                    'image_matrix_limit' => 3686400,
-                    'video_size_limit' => config_cache('pixelfed.max_photo_size') * 1024,
-                    'video_frame_rate_limit' => 240,
-                    'video_matrix_limit' => 3686400
+                'languages' => [config('app.locale')],
+                'configuration' => [
+                    'urls' => [
+                        'streaming' => null,
+                        'status' => null
+                    ],
+                    'vapid' => [
+                        'public_key' => config('webpush.vapid.public_key'),
+                    ],
+                    'accounts' => [
+                        'max_featured_tags' => 0,
+                    ],
+                    'statuses' => [
+                        'max_characters' => (int) config('pixelfed.max_caption_length'),
+                        'max_media_attachments' => (int) config_cache('pixelfed.max_album_length'),
+                        'characters_reserved_per_url' => 23
+                    ],
+                    'media_attachments' => [
+                        'supported_mime_types' => explode(',', config_cache('pixelfed.media_types')),
+                        'image_size_limit' => config_cache('pixelfed.max_photo_size') * 1024,
+                        'image_matrix_limit' => 3686400,
+                        'video_size_limit' => config_cache('pixelfed.max_photo_size') * 1024,
+                        'video_frame_rate_limit' => 240,
+                        'video_matrix_limit' => 3686400
+                    ],
+                    'polls' => [
+                        'max_options' => 0,
+                        'max_characters_per_option' => 0,
+                        'min_expiration' => 0,
+                        'max_expiration' => 0,
+                    ],
+                    'translation' => [
+                        'enabled' => false,
+                    ],
                 ],
-                'polls' => [
-                    'max_options' => 4,
-                    'max_characters_per_option' => 50,
-                    'min_expiration' => 300,
-                    'max_expiration' => 2629746,
+                'registrations' => [
+                    'enabled' => null,
+                    'approval_required' => false,
+                    'message' => null,
+                    'url' => null,
                 ],
-                'translation' => [
-                    'enabled' => false,
+                'contact' => [
+                    'email' => config('instance.email'),
+                    'account' => $contact
                 ],
-            ],
-            'registrations' => [
-                'enabled' => (bool) config_cache('pixelfed.open_registration'),
-                'approval_required' => false,
-                'message' => null
-            ],
-            'contact' => [
-                'email' => config('instance.email'),
-                'account' => $contact
-            ],
-            'rules' => $rules
-        ];
+                'rules' => $rules
+            ];
+        });
 
+        $res['registrations']['enabled'] = (bool) config_cache('pixelfed.open_registration');
+        $res['registrations']['approval_required'] = (bool) config_cache('instance.curated_registration.enabled');
         return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES);
     }
 

+ 6 - 2
app/Http/Controllers/Auth/RegisterController.php

@@ -174,7 +174,7 @@ class RegisterController extends Controller
 	 */
 	public function showRegistrationForm()
 	{
-		if(config_cache('pixelfed.open_registration')) {
+		if((bool) config_cache('pixelfed.open_registration')) {
 			if(config('pixelfed.bouncer.cloud_ips.ban_signups')) {
 				abort_if(BouncerService::checkIp(request()->ip()), 404);
 			}
@@ -191,7 +191,11 @@ class RegisterController extends Controller
 				return view('auth.register');
 			}
 		} else {
-			abort(404);
+			if((bool) config_cache('instance.curated_registration.enabled') && config('instance.curated_registration.state.fallback_on_closed_reg')) {
+				return redirect('/auth/sign_up');
+			} else {
+				abort(404);
+			}
 		}
 	}
 

+ 398 - 0
app/Http/Controllers/CuratedRegisterController.php

@@ -0,0 +1,398 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Request;
+use Illuminate\Support\Str;
+use App\User;
+use App\Models\CuratedRegister;
+use App\Models\CuratedRegisterActivity;
+use App\Services\EmailService;
+use App\Services\BouncerService;
+use App\Util\Lexer\RestrictedNames;
+use App\Mail\CuratedRegisterConfirmEmail;
+use App\Mail\CuratedRegisterNotifyAdmin;
+use Illuminate\Support\Facades\Mail;
+use App\Jobs\CuratedOnboarding\CuratedOnboardingNotifyAdminNewApplicationPipeline;
+
+class CuratedRegisterController extends Controller
+{
+    public function __construct()
+    {
+        abort_unless((bool) config_cache('instance.curated_registration.enabled'), 404);
+
+        if((bool) config_cache('pixelfed.open_registration')) {
+            abort_if(config('instance.curated_registration.state.only_enabled_on_closed_reg'), 404);
+        } else {
+            abort_unless(config('instance.curated_registration.state.fallback_on_closed_reg'), 404);
+        }
+    }
+
+    public function index(Request $request)
+    {
+        abort_if($request->user(), 404);
+        return view('auth.curated-register.index', ['step' => 1]);
+    }
+
+    public function concierge(Request $request)
+    {
+        abort_if($request->user(), 404);
+        $emailConfirmed = $request->session()->has('cur-reg-con.email-confirmed') &&
+            $request->has('next') &&
+            $request->session()->has('cur-reg-con.cr-id');
+        return view('auth.curated-register.concierge', compact('emailConfirmed'));
+    }
+
+    public function conciergeResponseSent(Request $request)
+    {
+        return view('auth.curated-register.user_response_sent');
+    }
+
+    public function conciergeFormShow(Request $request)
+    {
+        abort_if($request->user(), 404);
+        abort_unless(
+            $request->session()->has('cur-reg-con.email-confirmed') &&
+            $request->session()->has('cur-reg-con.cr-id') &&
+            $request->session()->has('cur-reg-con.ac-id'), 404);
+        $crid = $request->session()->get('cur-reg-con.cr-id');
+        $arid = $request->session()->get('cur-reg-con.ac-id');
+        $showCaptcha = config('instance.curated_registration.captcha_enabled');
+        if($attempts = $request->session()->get('cur-reg-con-attempt')) {
+            $showCaptcha = $attempts && $attempts >= 2;
+        } else {
+            $showCaptcha = false;
+        }
+        $activity = CuratedRegisterActivity::whereRegisterId($crid)->whereFromAdmin(true)->findOrFail($arid);
+        return view('auth.curated-register.concierge_form', compact('activity', 'showCaptcha'));
+    }
+
+    public function conciergeFormStore(Request $request)
+    {
+        abort_if($request->user(), 404);
+        $request->session()->increment('cur-reg-con-attempt');
+        abort_unless(
+            $request->session()->has('cur-reg-con.email-confirmed') &&
+            $request->session()->has('cur-reg-con.cr-id') &&
+            $request->session()->has('cur-reg-con.ac-id'), 404);
+        $attempts = $request->session()->get('cur-reg-con-attempt');
+        $messages = [];
+        $rules = [
+            'response' => 'required|string|min:5|max:1000',
+            'crid' => 'required|integer|min:1',
+            'acid' => 'required|integer|min:1'
+        ];
+        if(config('instance.curated_registration.captcha_enabled') && $attempts >= 3) {
+            $rules['h-captcha-response'] = 'required|captcha';
+            $messages['h-captcha-response.required'] = 'The captcha must be filled';
+        }
+        $this->validate($request, $rules, $messages);
+        $crid = $request->session()->get('cur-reg-con.cr-id');
+        $acid = $request->session()->get('cur-reg-con.ac-id');
+        abort_if((string) $crid !== $request->input('crid'), 404);
+        abort_if((string) $acid !== $request->input('acid'), 404);
+
+        if(CuratedRegisterActivity::whereRegisterId($crid)->whereReplyToId($acid)->exists()) {
+            return redirect()->back()->withErrors(['code' => 'You already replied to this request.']);
+        }
+
+        $act = CuratedRegisterActivity::create([
+            'register_id' => $crid,
+            'reply_to_id' => $acid,
+            'type' => 'user_response',
+            'message' => $request->input('response'),
+            'from_user' => true,
+            'action_required' => true,
+        ]);
+
+        $request->session()->pull('cur-reg-con');
+        $request->session()->pull('cur-reg-con-attempt');
+
+        return view('auth.curated-register.user_response_sent');
+    }
+
+    public function conciergeStore(Request $request)
+    {
+        abort_if($request->user(), 404);
+        $rules = [
+            'sid' => 'required_if:action,email|integer|min:1|max:20000000',
+            'id' => 'required_if:action,email|integer|min:1|max:20000000',
+            'code' => 'required_if:action,email',
+            'action' => 'required|string|in:email,message',
+            'email' => 'required_if:action,email|email',
+            'response' => 'required_if:action,message|string|min:20|max:1000',
+        ];
+        $messages = [];
+        if(config('instance.curated_registration.captcha_enabled')) {
+            $rules['h-captcha-response'] = 'required|captcha';
+            $messages['h-captcha-response.required'] = 'The captcha must be filled';
+        }
+        $this->validate($request, $rules, $messages);
+
+        $action = $request->input('action');
+        $sid = $request->input('sid');
+        $id = $request->input('id');
+        $code = $request->input('code');
+        $email = $request->input('email');
+
+        $cr = CuratedRegister::whereIsClosed(false)->findOrFail($sid);
+        $ac = CuratedRegisterActivity::whereRegisterId($cr->id)->whereFromAdmin(true)->findOrFail($id);
+
+        if(!hash_equals($ac->secret_code, $code)) {
+            return redirect()->back()->withErrors(['code' => 'Invalid code']);
+        }
+
+        if(!hash_equals($cr->email, $email)) {
+            return redirect()->back()->withErrors(['email' => 'Invalid email']);
+        }
+
+        $request->session()->put('cur-reg-con.email-confirmed', true);
+        $request->session()->put('cur-reg-con.cr-id', $cr->id);
+        $request->session()->put('cur-reg-con.ac-id', $ac->id);
+        $emailConfirmed = true;
+        return redirect('/auth/sign_up/concierge/form');
+    }
+
+    public function confirmEmail(Request $request)
+    {
+        if($request->user()) {
+            return redirect(route('help.email-confirmation-issues'));
+        }
+        return view('auth.curated-register.confirm_email');
+    }
+
+    public function emailConfirmed(Request $request)
+    {
+        if($request->user()) {
+            return redirect(route('help.email-confirmation-issues'));
+        }
+        return view('auth.curated-register.email_confirmed');
+    }
+
+    public function resendConfirmation(Request $request)
+    {
+        return view('auth.curated-register.resend-confirmation');
+    }
+
+    public function resendConfirmationProcess(Request $request)
+    {
+        $rules = [
+            'email' => [
+                'required',
+                'string',
+                app()->environment() === 'production' ? 'email:rfc,dns,spoof' : 'email',
+                'exists:curated_registers',
+            ]
+        ];
+
+        $messages = [];
+
+        if(config('instance.curated_registration.captcha_enabled')) {
+            $rules['h-captcha-response'] = 'required|captcha';
+            $messages['h-captcha-response.required'] = 'The captcha must be filled';
+        }
+
+        $this->validate($request, $rules, $messages);
+
+        $cur = CuratedRegister::whereEmail($request->input('email'))->whereIsClosed(false)->first();
+        if(!$cur) {
+            return redirect()->back()->withErrors(['email' => 'The selected email is invalid.']);
+        }
+
+        $totalCount = CuratedRegisterActivity::whereRegisterId($cur->id)
+            ->whereType('user_resend_email_confirmation')
+            ->count();
+
+        if($totalCount && $totalCount >= config('instance.curated_registration.resend_confirmation_limit')) {
+            return redirect()->back()->withErrors(['email' => 'You have re-attempted too many times. To proceed with your application, please <a href="/site/contact" class="text-white" style="text-decoration: underline;">contact the admin team</a>.']);
+        }
+
+        $count = CuratedRegisterActivity::whereRegisterId($cur->id)
+            ->whereType('user_resend_email_confirmation')
+            ->where('created_at', '>', now()->subHours(12))
+            ->count();
+
+        if($count) {
+            return redirect()->back()->withErrors(['email' => 'You can only re-send the confirmation email once per 12 hours. Try again later.']);
+        }
+
+        CuratedRegisterActivity::create([
+            'register_id' => $cur->id,
+            'type' => 'user_resend_email_confirmation',
+            'admin_only_view' => true,
+            'from_admin' => false,
+            'from_user' => false,
+            'action_required' => false,
+        ]);
+
+        Mail::to($cur->email)->send(new CuratedRegisterConfirmEmail($cur));
+        return view('auth.curated-register.resent-confirmation');
+        return $request->all();
+    }
+
+    public function confirmEmailHandle(Request $request)
+    {
+        $rules = [
+            'sid' => 'required',
+            'code' => 'required'
+        ];
+        $messages = [];
+        if(config('instance.curated_registration.captcha_enabled')) {
+            $rules['h-captcha-response'] = 'required|captcha';
+            $messages['h-captcha-response.required'] = 'The captcha must be filled';
+        }
+        $this->validate($request, $rules, $messages);
+
+        $cr = CuratedRegister::whereNull('email_verified_at')
+            ->where('created_at', '>', now()->subHours(24))
+            ->find($request->input('sid'));
+        if(!$cr) {
+            return redirect(route('help.email-confirmation-issues'));
+        }
+        if(!hash_equals($cr->verify_code, $request->input('code'))) {
+            return redirect(route('help.email-confirmation-issues'));
+        }
+        $cr->email_verified_at = now();
+        $cr->save();
+
+        if(config('instance.curated_registration.notify.admin.on_verify_email.enabled')) {
+            CuratedOnboardingNotifyAdminNewApplicationPipeline::dispatch($cr);
+        }
+        return view('auth.curated-register.email_confirmed');
+    }
+
+    public function proceed(Request $request)
+    {
+        $this->validate($request, [
+            'step' => 'required|integer|in:1,2,3,4'
+        ]);
+        $step = $request->input('step');
+
+        switch($step) {
+            case 1:
+                $step = 2;
+                $request->session()->put('cur-step', 1);
+                return view('auth.curated-register.index', compact('step'));
+            break;
+
+            case 2:
+                $this->stepTwo($request);
+                $step = 3;
+                $request->session()->put('cur-step', 2);
+                return view('auth.curated-register.index', compact('step'));
+            break;
+
+            case 3:
+                $this->stepThree($request);
+                $step = 3;
+                $request->session()->put('cur-step', 3);
+                $verifiedEmail = true;
+                $request->session()->pull('cur-reg');
+                return view('auth.curated-register.index', compact('step', 'verifiedEmail'));
+            break;
+        }
+    }
+
+    protected function stepTwo($request)
+    {
+        if($request->filled('reason')) {
+            $request->session()->put('cur-reg.form-reason', $request->input('reason'));
+        }
+        if($request->filled('username')) {
+            $request->session()->put('cur-reg.form-username', $request->input('username'));
+        }
+        if($request->filled('email')) {
+            $request->session()->put('cur-reg.form-email', $request->input('email'));
+        }
+        $this->validate($request, [
+            'username' => [
+                'required',
+                'min:2',
+                'max:15',
+                'unique:curated_registers',
+                'unique:users',
+                function ($attribute, $value, $fail) {
+                    $dash = substr_count($value, '-');
+                    $underscore = substr_count($value, '_');
+                    $period = substr_count($value, '.');
+
+                    if(ends_with($value, ['.php', '.js', '.css'])) {
+                        return $fail('Username is invalid.');
+                    }
+
+                    if(($dash + $underscore + $period) > 1) {
+                        return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).');
+                    }
+
+                    if (!ctype_alnum($value[0])) {
+                        return $fail('Username is invalid. Must start with a letter or number.');
+                    }
+
+                    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)) {
+                        return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).');
+                    }
+
+                    $restricted = RestrictedNames::get();
+                    if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
+                        return $fail('Username cannot be used.');
+                    }
+                },
+            ],
+            'email' => [
+                'required',
+                'string',
+                app()->environment() === 'production' ? 'email:rfc,dns,spoof' : 'email',
+                'max:255',
+                'unique:users',
+                'unique:curated_registers',
+                function ($attribute, $value, $fail) {
+                    $banned = EmailService::isBanned($value);
+                    if($banned) {
+                        return $fail('Email is invalid.');
+                    }
+                },
+            ],
+            'password' => 'required|min:8',
+            'password_confirmation' => 'required|same:password',
+            'reason' => 'required|min:20|max:1000',
+            'agree' => 'required|accepted'
+        ]);
+        $request->session()->put('cur-reg.form-email', $request->input('email'));
+        $request->session()->put('cur-reg.form-password', $request->input('password'));
+    }
+
+    protected function stepThree($request)
+    {
+        $this->validate($request, [
+            'email' => [
+                'required',
+                'string',
+                app()->environment() === 'production' ? 'email:rfc,dns,spoof' : 'email',
+                'max:255',
+                'unique:users',
+                'unique:curated_registers',
+                function ($attribute, $value, $fail) {
+                    $banned = EmailService::isBanned($value);
+                    if($banned) {
+                        return $fail('Email is invalid.');
+                    }
+                },
+            ]
+        ]);
+        $cr = new CuratedRegister;
+        $cr->email = $request->email;
+        $cr->username = $request->session()->get('cur-reg.form-username');
+        $cr->password = bcrypt($request->session()->get('cur-reg.form-password'));
+        $cr->ip_address = $request->ip();
+        $cr->reason_to_join = $request->session()->get('cur-reg.form-reason');
+        $cr->verify_code = Str::random(40);
+        $cr->save();
+
+        Mail::to($cr->email)->send(new CuratedRegisterConfirmEmail($cr));
+    }
+}

+ 65 - 0
app/Jobs/CuratedOnboarding/CuratedOnboardingNotifyAdminNewApplicationPipeline.php

@@ -0,0 +1,65 @@
+<?php
+
+namespace App\Jobs\CuratedOnboarding;
+
+use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Foundation\Bus\Dispatchable;
+use Illuminate\Queue\InteractsWithQueue;
+use Illuminate\Queue\SerializesModels;
+use App\Models\CuratedRegister;
+use App\User;
+use Illuminate\Support\Facades\Mail;
+use Illuminate\Support\Facades\Storage;
+use App\Mail\CuratedRegisterNotifyAdmin;
+
+class CuratedOnboardingNotifyAdminNewApplicationPipeline implements ShouldQueue
+{
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+    public $cr;
+
+    /**
+     * Create a new job instance.
+     */
+    public function __construct(CuratedRegister $cr)
+    {
+        $this->cr = $cr;
+    }
+
+    /**
+     * Execute the job.
+     */
+    public function handle(): void
+    {
+        if(!config('instance.curated_registration.notify.admin.on_verify_email.enabled')) {
+            return;
+        }
+
+        config('instance.curated_registration.notify.admin.on_verify_email.bundle') ?
+            $this->handleBundled() :
+            $this->handleUnbundled();
+    }
+
+    protected function handleBundled()
+    {
+        $cr = $this->cr;
+        Storage::append('conanap.json', json_encode([
+            'id' => $cr->id,
+            'email' => $cr->email,
+            'created_at' => $cr->created_at,
+            'updated_at' => $cr->updated_at,
+        ]));
+    }
+
+    protected function handleUnbundled()
+    {
+        $cr = $this->cr;
+        if($aid = config_cache('instance.admin.pid')) {
+            $admin = User::whereProfileId($aid)->first();
+            if($admin && $admin->email) {
+                Mail::to($admin->email)->send(new CuratedRegisterNotifyAdmin($cr));
+            }
+        }
+    }
+}

+ 55 - 0
app/Mail/CuratedRegisterAcceptUser.php

@@ -0,0 +1,55 @@
+<?php
+
+namespace App\Mail;
+
+use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Mail\Mailable;
+use Illuminate\Mail\Mailables\Content;
+use Illuminate\Mail\Mailables\Envelope;
+use Illuminate\Queue\SerializesModels;
+
+class CuratedRegisterAcceptUser extends Mailable
+{
+    use Queueable, SerializesModels;
+
+    public $verify;
+
+    /**
+     * Create a new message instance.
+     */
+    public function __construct($verify)
+    {
+        $this->verify = $verify;
+    }
+
+    /**
+     * Get the message envelope.
+     */
+    public function envelope(): Envelope
+    {
+        return new Envelope(
+            subject: 'Your ' . config('pixelfed.domain.app') . ' Registration Update',
+        );
+    }
+
+    /**
+     * Get the message content definition.
+     */
+    public function content(): Content
+    {
+        return new Content(
+            markdown: 'emails.curated-register.request-accepted',
+        );
+    }
+
+    /**
+     * Get the attachments for the message.
+     *
+     * @return array<int, \Illuminate\Mail\Mailables\Attachment>
+     */
+    public function attachments(): array
+    {
+        return [];
+    }
+}

+ 55 - 0
app/Mail/CuratedRegisterConfirmEmail.php

@@ -0,0 +1,55 @@
+<?php
+
+namespace App\Mail;
+
+use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Mail\Mailable;
+use Illuminate\Mail\Mailables\Content;
+use Illuminate\Mail\Mailables\Envelope;
+use Illuminate\Queue\SerializesModels;
+use App\Models\CuratedRegister;
+
+class CuratedRegisterConfirmEmail extends Mailable
+{
+    use Queueable, SerializesModels;
+    public $verify;
+
+    /**
+     * Create a new message instance.
+     */
+    public function __construct(CuratedRegister $verify)
+    {
+        $this->verify = $verify;
+    }
+
+    /**
+     * Get the message envelope.
+     */
+    public function envelope(): Envelope
+    {
+        return new Envelope(
+            subject: 'Welcome to Pixelfed! Please Confirm Your Email',
+        );
+    }
+
+    /**
+     * Get the message content definition.
+     */
+    public function content(): Content
+    {
+        return new Content(
+            markdown: 'emails.curated-register.confirm_email',
+        );
+    }
+
+    /**
+     * Get the attachments for the message.
+     *
+     * @return array<int, \Illuminate\Mail\Mailables\Attachment>
+     */
+    public function attachments(): array
+    {
+        return [];
+    }
+}

+ 55 - 0
app/Mail/CuratedRegisterNotifyAdmin.php

@@ -0,0 +1,55 @@
+<?php
+
+namespace App\Mail;
+
+use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Mail\Mailable;
+use Illuminate\Mail\Mailables\Content;
+use Illuminate\Mail\Mailables\Envelope;
+use Illuminate\Queue\SerializesModels;
+use App\Models\CuratedRegister;
+
+class CuratedRegisterNotifyAdmin extends Mailable
+{
+    use Queueable, SerializesModels;
+    public $verify;
+
+    /**
+     * Create a new message instance.
+     */
+    public function __construct(CuratedRegister $verify)
+    {
+        $this->verify = $verify;
+    }
+
+    /**
+     * Get the message envelope.
+     */
+    public function envelope(): Envelope
+    {
+        return new Envelope(
+            subject: '[Requires Action]: New Curated Onboarding Application',
+        );
+    }
+
+    /**
+     * Get the message content definition.
+     */
+    public function content(): Content
+    {
+        return new Content(
+            markdown: 'emails.curated-register.admin_notify',
+        );
+    }
+
+    /**
+     * Get the attachments for the message.
+     *
+     * @return array<int, \Illuminate\Mail\Mailables\Attachment>
+     */
+    public function attachments(): array
+    {
+        return [];
+    }
+}

+ 55 - 0
app/Mail/CuratedRegisterNotifyAdminUserResponse.php

@@ -0,0 +1,55 @@
+<?php
+
+namespace App\Mail;
+
+use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Mail\Mailable;
+use Illuminate\Mail\Mailables\Content;
+use Illuminate\Mail\Mailables\Envelope;
+use Illuminate\Queue\SerializesModels;
+
+class CuratedRegisterNotifyAdminUserResponse extends Mailable
+{
+    use Queueable, SerializesModels;
+
+    public $activity;
+
+    /**
+     * Create a new message instance.
+     */
+    public function __construct($activity)
+    {
+        $this->activity = $activity;
+    }
+
+    /**
+     * Get the message envelope.
+     */
+    public function envelope(): Envelope
+    {
+        return new Envelope(
+            subject: 'Curated Register Notify Admin User Response',
+        );
+    }
+
+    /**
+     * Get the message content definition.
+     */
+    public function content(): Content
+    {
+        return new Content(
+            markdown: 'emails.curated-register.admin_notify_user_response',
+        );
+    }
+
+    /**
+     * Get the attachments for the message.
+     *
+     * @return array<int, \Illuminate\Mail\Mailables\Attachment>
+     */
+    public function attachments(): array
+    {
+        return [];
+    }
+}

+ 55 - 0
app/Mail/CuratedRegisterRejectUser.php

@@ -0,0 +1,55 @@
+<?php
+
+namespace App\Mail;
+
+use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Mail\Mailable;
+use Illuminate\Mail\Mailables\Content;
+use Illuminate\Mail\Mailables\Envelope;
+use Illuminate\Queue\SerializesModels;
+
+class CuratedRegisterRejectUser extends Mailable
+{
+    use Queueable, SerializesModels;
+
+    public $verify;
+
+    /**
+     * Create a new message instance.
+     */
+    public function __construct($verify)
+    {
+        $this->verify = $verify;
+    }
+
+    /**
+     * Get the message envelope.
+     */
+    public function envelope(): Envelope
+    {
+        return new Envelope(
+            subject: 'Your ' . config('pixelfed.domain.app') . ' Registration Update',
+        );
+    }
+
+    /**
+     * Get the message content definition.
+     */
+    public function content(): Content
+    {
+        return new Content(
+            markdown: 'emails.curated-register.request-rejected',
+        );
+    }
+
+    /**
+     * Get the attachments for the message.
+     *
+     * @return array<int, \Illuminate\Mail\Mailables\Attachment>
+     */
+    public function attachments(): array
+    {
+        return [];
+    }
+}

+ 58 - 0
app/Mail/CuratedRegisterRequestDetailsFromUser.php

@@ -0,0 +1,58 @@
+<?php
+
+namespace App\Mail;
+
+use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Mail\Mailable;
+use Illuminate\Mail\Mailables\Content;
+use Illuminate\Mail\Mailables\Envelope;
+use Illuminate\Queue\SerializesModels;
+use App\Models\CuratedRegister;
+use App\Models\CuratedRegisterActivity;
+
+class CuratedRegisterRequestDetailsFromUser extends Mailable
+{
+    use Queueable, SerializesModels;
+    public $verify;
+    public $activity;
+
+    /**
+     * Create a new message instance.
+     */
+    public function __construct(CuratedRegister $verify, CuratedRegisterActivity $activity)
+    {
+        $this->verify = $verify;
+        $this->activity = $activity;
+    }
+
+    /**
+     * Get the message envelope.
+     */
+    public function envelope(): Envelope
+    {
+        return new Envelope(
+            subject: '[Action Needed]: Additional information requested',
+        );
+    }
+
+    /**
+     * Get the message content definition.
+     */
+    public function content(): Content
+    {
+        return new Content(
+            markdown: 'emails.curated-register.request-details-from-user',
+        );
+    }
+
+    /**
+     * Get the attachments for the message.
+     *
+     * @return array<int, \Illuminate\Mail\Mailables\Attachment>
+     */
+    public function attachments(): array
+    {
+        return [];
+    }
+}

+ 55 - 0
app/Mail/CuratedRegisterSendMessage.php

@@ -0,0 +1,55 @@
+<?php
+
+namespace App\Mail;
+
+use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Mail\Mailable;
+use Illuminate\Mail\Mailables\Content;
+use Illuminate\Mail\Mailables\Envelope;
+use Illuminate\Queue\SerializesModels;
+
+class CuratedRegisterSendMessage extends Mailable
+{
+    use Queueable, SerializesModels;
+
+    public $verify;
+
+    /**
+     * Create a new message instance.
+     */
+    public function __construct($verify)
+    {
+        $this->verify = $verify;
+    }
+
+    /**
+     * Get the message envelope.
+     */
+    public function envelope(): Envelope
+    {
+        return new Envelope(
+            subject: 'Your ' . config('pixelfed.domain.app') . ' Registration Update',
+        );
+    }
+
+    /**
+     * Get the message content definition.
+     */
+    public function content(): Content
+    {
+        return new Content(
+            markdown: 'emails.curated-register.message-from-admin',
+        );
+    }
+
+    /**
+     * Get the attachments for the message.
+     *
+     * @return array<int, \Illuminate\Mail\Mailables\Attachment>
+     */
+    public function attachments(): array
+    {
+        return [];
+    }
+}

+ 49 - 0
app/Models/CuratedRegister.php

@@ -0,0 +1,49 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class CuratedRegister extends Model
+{
+    use HasFactory;
+
+    protected $casts = [
+        'autofollow_account_ids' => 'array',
+        'admin_notes' => 'array',
+        'email_verified_at' => 'datetime',
+        'admin_notified_at' => 'datetime',
+        'action_taken_at' => 'datetime',
+    ];
+
+    public function adminStatusLabel()
+    {
+        if(!$this->email_verified_at) {
+            return '<span class="border border-danger px-3 py-1 rounded text-white font-weight-bold">Unverified email</span>';
+        }
+        if($this->is_accepted) { return 'Approved'; }
+        if($this->is_rejected) { return 'Rejected'; }
+        if($this->is_awaiting_more_info ) {
+            return '<span class="border border-info px-3 py-1 rounded text-white font-weight-bold">Awaiting Details</span>';
+        }
+        if($this->is_closed ) { return 'Closed'; }
+
+        return '<span class="border border-success px-3 py-1 rounded text-white font-weight-bold">Open</span>';
+    }
+
+    public function emailConfirmUrl()
+    {
+        return url('/auth/sign_up/confirm?sid=' . $this->id . '&code=' . $this->verify_code);
+    }
+
+    public function emailReplyUrl()
+    {
+        return url('/auth/sign_up/concierge?sid=' . $this->id . '&code=' . $this->verify_code . '&sc=' . str_random(8));
+    }
+
+    public function adminReviewUrl()
+    {
+        return url('/i/admin/curated-onboarding/show/' . $this->id);
+    }
+}

+ 38 - 0
app/Models/CuratedRegisterActivity.php

@@ -0,0 +1,38 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class CuratedRegisterActivity extends Model
+{
+    use HasFactory;
+
+    protected $guarded = [];
+
+    protected $casts = [
+        'metadata' => 'array',
+        'admin_notified_at' => 'datetime',
+        'action_taken_at' => 'datetime',
+    ];
+
+    public function application()
+    {
+        return $this->belongsTo(CuratedRegister::class, 'register_id');
+    }
+
+    public function emailReplyUrl()
+    {
+        return url('/auth/sign_up/concierge?sid='.$this->register_id . '&id=' . $this->id . '&code=' . $this->secret_code);
+    }
+
+    public function adminReviewUrl()
+    {
+        $url = '/i/admin/curated-onboarding/show/' . $this->register_id . '/?ah=' . $this->id;
+        if($this->reply_to_id) {
+            $url .= '&rtid=' . $this->reply_to_id;
+        }
+        return url($url);
+    }
+}

+ 2 - 0
app/Services/ConfigCacheService.php

@@ -72,6 +72,8 @@ class ConfigCacheService
 				'instance.banner.blurhash',
 
 				'autospam.nlp.enabled',
+
+				'instance.curated_registration.enabled',
 				// 'system.user_mode'
 			];
 

+ 4 - 1
app/Services/LandingService.php

@@ -48,13 +48,16 @@ class LandingService
 				->toArray() : [];
 		});
 
+		$openReg = (bool) config_cache('pixelfed.open_registration');
+
 		$res = [
 			'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'),
-			'open_registration' => config_cache('pixelfed.open_registration') == 1,
+			'open_registration' => (bool) $openReg,
+			'curated_onboarding' => (bool) config_cache('instance.curated_registration.enabled'),
 			'version' => config('pixelfed.version'),
 			'about' => [
 				'banner_image' => config_cache('app.banner_image') ?? url('/storage/headers/default.jpg'),

+ 1 - 1
composer.json

@@ -5,7 +5,7 @@
 	"license": "AGPL-3.0-only",
 	"type": "project",
 	"require": {
-		"php": "^8.1|^8.2",
+		"php": "^8.1|^8.2|^8.3",
 		"ext-bcmath": "*",
 		"ext-ctype": "*",
 		"ext-curl": "*",

Різницю між файлами не показано, бо вона завелика
+ 199 - 196
composer.lock


+ 1 - 1
config/cache.php

@@ -74,7 +74,7 @@ return [
         'redis' => [
             'driver' => 'redis',
             'lock_connection' => 'default',
-            'client' => env('REDIS_CLIENT', 'phpredis'),
+            'client' => env('REDIS_CLIENT', 'predis'),
 
             'default' => [
                 'scheme'   => env('REDIS_SCHEME', 'tcp'),

+ 1 - 1
config/federation.php

@@ -49,7 +49,7 @@ return [
     ],
 
     'network_timeline' => env('PF_NETWORK_TIMELINE', true),
-    'network_timeline_days_falloff' => env('PF_NETWORK_TIMELINE_DAYS_FALLOFF', 2),
+    'network_timeline_days_falloff' => env('PF_NETWORK_TIMELINE_DAYS_FALLOFF', 90),
 
     'custom_emoji' => [
         'enabled' => env('CUSTOM_EMOJI', false),

+ 31 - 0
config/instance.php

@@ -145,4 +145,35 @@ return [
     'software-update' => [
         'disable_failed_warning' => env('INSTANCE_SOFTWARE_UPDATE_DISABLE_FAILED_WARNING', false)
     ],
+
+    'notifications' => [
+        'gc' => [
+            'enabled' => env('INSTANCE_NOTIFY_AUTO_GC', false),
+            'delete_after_days' => env('INSTANCE_NOTIFY_AUTO_GC_DEL_AFTER_DAYS', 365)
+        ]
+    ],
+
+    'curated_registration' => [
+        'enabled' => env('INSTANCE_CUR_REG', false),
+
+        'resend_confirmation_limit' => env('INSTANCE_CUR_REG_RESEND_LIMIT', 5),
+
+        'captcha_enabled' => env('INSTANCE_CUR_REG_CAPTCHA', env('CAPTCHA_ENABLED', false)),
+
+        'state' => [
+            'fallback_on_closed_reg' => true,
+            'only_enabled_on_closed_reg' => env('INSTANCE_CUR_REG_STATE_ONLY_ON_CLOSED', true),
+        ],
+
+        'notify' => [
+            'admin' => [
+                'on_verify_email' => [
+                    'enabled' => env('INSTANCE_CUR_REG_NOTIFY_ADMIN_ON_VERIFY', false),
+                    'bundle' => env('INSTANCE_CUR_REG_NOTIFY_ADMIN_ON_VERIFY_BUNDLE', false),
+                    'max_per_day' => env('INSTANCE_CUR_REG_NOTIFY_ADMIN_ON_VERIFY_MPD', 10),
+                ],
+                'on_user_response' => env('INSTANCE_CUR_REG_NOTIFY_ADMIN_ON_USER_RESPONSE', false),
+            ]
+        ],
+    ],
 ];

+ 44 - 0
database/migrations/2024_01_16_073327_create_curated_registers_table.php

@@ -0,0 +1,44 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::create('curated_registers', function (Blueprint $table) {
+            $table->id();
+            $table->string('email')->unique()->nullable()->index();
+            $table->string('username')->unique()->nullable()->index();
+            $table->string('password')->nullable();
+            $table->string('ip_address')->nullable();
+            $table->string('verify_code')->nullable();
+            $table->text('reason_to_join')->nullable();
+            $table->unsignedBigInteger('invited_by')->nullable()->index();
+            $table->boolean('is_approved')->default(0)->index();
+            $table->boolean('is_rejected')->default(0)->index();
+            $table->boolean('is_awaiting_more_info')->default(0)->index();
+            $table->boolean('is_closed')->default(0)->index();
+            $table->json('autofollow_account_ids')->nullable();
+            $table->json('admin_notes')->nullable();
+            $table->unsignedInteger('approved_by_admin_id')->nullable();
+            $table->timestamp('email_verified_at')->nullable();
+            $table->timestamp('admin_notified_at')->nullable();
+            $table->timestamp('action_taken_at')->nullable();
+            $table->timestamps();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::dropIfExists('curated_registers');
+    }
+};

+ 42 - 0
database/migrations/2024_01_20_091352_create_curated_register_activities_table.php

@@ -0,0 +1,42 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::create('curated_register_activities', function (Blueprint $table) {
+            $table->id();
+            $table->unsignedInteger('register_id')->nullable()->index();
+            $table->unsignedInteger('admin_id')->nullable();
+            $table->unsignedInteger('reply_to_id')->nullable()->index();
+            $table->string('secret_code')->nullable();
+            $table->string('type')->nullable()->index();
+            $table->string('title')->nullable();
+            $table->string('link')->nullable();
+            $table->text('message')->nullable();
+            $table->json('metadata')->nullable();
+            $table->boolean('from_admin')->default(false)->index();
+            $table->boolean('from_user')->default(false)->index();
+            $table->boolean('admin_only_view')->default(true);
+            $table->boolean('action_required')->default(false);
+            $table->timestamp('admin_notified_at')->nullable();
+            $table->timestamp('action_taken_at')->nullable();
+            $table->timestamps();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::dropIfExists('curated_register_activities');
+    }
+};

Різницю між файлами не показано, бо вона завелика
+ 259 - 244
package-lock.json


+ 1 - 1
package.json

@@ -10,7 +10,7 @@
 	},
 	"devDependencies": {
 		"acorn": "^8.7.1",
-		"axios": "^0.21.1",
+		"axios": ">=1.6.0",
 		"bootstrap": "^4.5.2",
 		"cross-env": "^5.2.1",
 		"jquery": "^3.6.0",

BIN
public/js/account-import.js


BIN
public/js/activity.js


BIN
public/js/admin.js


BIN
public/js/admin_invite.js


BIN
public/js/app.js


BIN
public/js/changelog.bundle.742a06ba0a547120.js


BIN
public/js/changelog.bundle.bf44edbbfa14bd53.js


BIN
public/js/collectioncompose.js


BIN
public/js/collections.js


BIN
public/js/components.js


BIN
public/js/compose-classic.js


BIN
public/js/compose.chunk.1ac292c93b524406.js


BIN
public/js/compose.chunk.ffae318db42f1072.js


BIN
public/js/compose.js


BIN
public/js/daci.chunk.34dc7bad3a0792cc.js


BIN
public/js/daci.chunk.8d4acc1db3f27a51.js


BIN
public/js/developers.js


BIN
public/js/direct.js


BIN
public/js/discover.chunk.b1846efb6bd1e43c.js


BIN
public/js/discover.chunk.c2229e1d15bd3ada.js


BIN
public/js/discover.js


BIN
public/js/discover~findfriends.chunk.941b524eee8b8d63.js


BIN
public/js/discover~findfriends.chunk.b1858bea66d9723b.js


BIN
public/js/discover~hashtag.bundle.6c2ff384b17ea58d.js


BIN
public/js/discover~hashtag.bundle.a0f00fc7df1f313c.js


BIN
public/js/discover~memories.chunk.37e0c325f900e163.js


BIN
public/js/discover~memories.chunk.7d917826c3e9f17b.js


BIN
public/js/discover~myhashtags.chunk.8886fc0d4736d819.js


BIN
public/js/discover~myhashtags.chunk.a72fc4882db8afd3.js


BIN
public/js/discover~serverfeed.chunk.262bf7e3bce843c3.js


BIN
public/js/discover~serverfeed.chunk.8365948d1867de3a.js


BIN
public/js/discover~settings.chunk.65d6f3cbe5323ed4.js


BIN
public/js/discover~settings.chunk.be88dc5ba1a24a7d.js


BIN
public/js/dms.chunk.2b55effc0e8ba89f.js


BIN
public/js/dms.chunk.53a951c5de2d95ac.js


BIN
public/js/dms~message.chunk.76edeafda3d92320.js


BIN
public/js/dms~message.chunk.976f7edaa6f71137.js


BIN
public/js/error404.bundle.3bbc118159460db6.js


BIN
public/js/error404.bundle.b397483e3991ab20.js


BIN
public/js/hashtag.js


BIN
public/js/home.chunk.264eeb47bfac56c1.js


+ 0 - 0
public/js/home.chunk.88eeebf6c53d4dca.js.LICENSE.txt → public/js/home.chunk.264eeb47bfac56c1.js.LICENSE.txt


BIN
public/js/home.chunk.88eeebf6c53d4dca.js


BIN
public/js/i18n.bundle.47cbf9f04d955267.js


BIN
public/js/i18n.bundle.93a02e275ac1a708.js


BIN
public/js/landing.js


BIN
public/js/manifest.js


BIN
public/js/notifications.chunk.0c5151643e4534aa.js


BIN
public/js/notifications.chunk.3b92cf46da469de1.js


BIN
public/js/portfolio.js


BIN
public/js/post.chunk.5ff16664f9adb901.js


+ 0 - 0
public/js/post.chunk.eb9804ff282909ae.js.LICENSE.txt → public/js/post.chunk.5ff16664f9adb901.js.LICENSE.txt


BIN
public/js/post.chunk.eb9804ff282909ae.js


BIN
public/js/profile-directory.js


BIN
public/js/profile.chunk.7a6c846c4cb3cfd4.js


BIN
public/js/profile.chunk.d52916cb68c9a146.js


BIN
public/js/profile.js


BIN
public/js/profile~followers.bundle.5d796e79f32d066c.js


BIN
public/js/profile~followers.bundle.5deed93248f20662.js


BIN
public/js/profile~following.bundle.7ca7cfa5aaae75e2.js


BIN
public/js/profile~following.bundle.d2b3b1fc2e05dbd3.js


BIN
public/js/remote_auth.js


BIN
public/js/search.js


BIN
public/js/spa.js


BIN
public/js/status.js


BIN
public/js/stories.js


BIN
public/js/story-compose.js


BIN
public/js/timeline.js


BIN
public/js/vendor.js


+ 2 - 2
public/js/vendor.js.LICENSE.txt

@@ -71,8 +71,8 @@
  */
 
 /*!
- * Vue.js v2.7.14
- * (c) 2014-2022 Evan You
+ * Vue.js v2.7.16
+ * (c) 2014-2023 Evan You
  * Released under the MIT License.
  */
 

BIN
public/mix-manifest.json


Деякі файли не було показано, через те що забагато файлів було змінено