浏览代码

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

Christian Winther 1 年之前
父节点
当前提交
ca7c2d34f2

+ 9 - 0
CHANGELOG.md

@@ -10,6 +10,15 @@
 - Update DiscoverController, handle discover hashtag redirects ([18382e8a](https://github.com/pixelfed/pixelfed/commit/18382e8a))
 - Update ApiV1Controller, use admin filter service ([94503a1c](https://github.com/pixelfed/pixelfed/commit/94503a1c))
 - Update SearchApiV2Service, use more efficient query ([cee618e8](https://github.com/pixelfed/pixelfed/commit/cee618e8))
+- Update Curated Onboarding view, fix concierge form ([15ad69f7](https://github.com/pixelfed/pixelfed/commit/15ad69f7))
+- Update AP Profile Transformer, add `suspended` attribute ([25f3fa06](https://github.com/pixelfed/pixelfed/commit/25f3fa06))
+- Update AP Profile Transformer, fix movedTo attribute ([63100fe9](https://github.com/pixelfed/pixelfed/commit/63100fe9))
+- Update AP Profile Transformer, fix suspended attributes ([2e5e68e4](https://github.com/pixelfed/pixelfed/commit/2e5e68e4))
+- Update PrivacySettings controller, add cache invalidation ([e742d595](https://github.com/pixelfed/pixelfed/commit/e742d595))
+- Update ProfileController, preserve deleted actor objects for federated account deletion and use more efficient account cache lookup ([853a729f](https://github.com/pixelfed/pixelfed/commit/853a729f))
+- Update SiteController, add curatedOnboarding method that gracefully falls back to open registration when applicable ([95199843](https://github.com/pixelfed/pixelfed/commit/95199843))
+- Update AP transformers, add DeleteActor activity ([bcce1df6](https://github.com/pixelfed/pixelfed/commit/bcce1df6))
+- Update commands, add user account delete cli command to federate account deletion ([4aa0e25f](https://github.com/pixelfed/pixelfed/commit/4aa0e25f))
 -  ([](https://github.com/pixelfed/pixelfed/commit/))
 
 ## [v0.11.13 (2024-03-05)](https://github.com/pixelfed/pixelfed/compare/v0.11.12...v0.11.13)

+ 123 - 0
app/Console/Commands/UserAccountDelete.php

@@ -0,0 +1,123 @@
+<?php
+
+namespace App\Console\Commands;
+
+use App\Instance;
+use App\Profile;
+use App\Transformer\ActivityPub\Verb\DeleteActor;
+use App\User;
+use App\Util\ActivityPub\HttpSignature;
+use GuzzleHttp\Client;
+use GuzzleHttp\Pool;
+use Illuminate\Console\Command;
+use League\Fractal;
+use League\Fractal\Serializer\ArraySerializer;
+
+use function Laravel\Prompts\confirm;
+use function Laravel\Prompts\search;
+use function Laravel\Prompts\table;
+
+class UserAccountDelete extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'app:user-account-delete';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Federate Account Deletion';
+
+    /**
+     * Execute the console command.
+     */
+    public function handle()
+    {
+        $id = search(
+            label: 'Search for the account to delete by username',
+            placeholder: 'john.appleseed',
+            options: fn (string $value) => strlen($value) > 0
+                ? User::withTrashed()->whereStatus('deleted')->where('username', 'like', "%{$value}%")->pluck('username', 'id')->all()
+                : [],
+        );
+
+        $user = User::withTrashed()->find($id);
+
+        table(
+            ['Username', 'Name', 'Email', 'Created'],
+            [[$user->username, $user->name, $user->email, $user->created_at]]
+        );
+
+        $confirmed = confirm(
+            label: 'Do you want to federate this account deletion?',
+            default: false,
+            yes: 'Proceed',
+            no: 'Cancel',
+            hint: 'This action is irreversible'
+        );
+
+        if (! $confirmed) {
+            $this->error('Aborting...');
+            exit;
+        }
+
+        $profile = Profile::withTrashed()->find($user->profile_id);
+
+        $fractal = new Fractal\Manager();
+        $fractal->setSerializer(new ArraySerializer());
+        $resource = new Fractal\Resource\Item($profile, new DeleteActor());
+        $activity = $fractal->createData($resource)->toArray();
+
+        $audience = Instance::whereNotNull(['shared_inbox', 'nodeinfo_last_fetched'])
+            ->where('nodeinfo_last_fetched', '>', now()->subHours(12))
+            ->distinct()
+            ->pluck('shared_inbox');
+
+        $payload = json_encode($activity);
+
+        $client = new Client([
+            'timeout' => 10,
+        ]);
+
+        $version = config('pixelfed.version');
+        $appUrl = config('app.url');
+        $userAgent = "(Pixelfed/{$version}; +{$appUrl})";
+
+        $requests = function ($audience) use ($client, $activity, $profile, $payload, $userAgent) {
+            foreach ($audience as $url) {
+                $headers = HttpSignature::sign($profile, $url, $activity, [
+                    'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
+                    'User-Agent' => $userAgent,
+                ]);
+                yield function () use ($client, $url, $headers, $payload) {
+                    return $client->postAsync($url, [
+                        'curl' => [
+                            CURLOPT_HTTPHEADER => $headers,
+                            CURLOPT_POSTFIELDS => $payload,
+                            CURLOPT_HEADER => true,
+                            CURLOPT_SSL_VERIFYPEER => false,
+                            CURLOPT_SSL_VERIFYHOST => false,
+                        ],
+                    ]);
+                };
+            }
+        };
+
+        $pool = new Pool($client, $requests($audience), [
+            'concurrency' => 50,
+            'fulfilled' => function ($response, $index) {
+            },
+            'rejected' => function ($reason, $index) {
+            },
+        ]);
+
+        $promise = $pool->promise();
+
+        $promise->wait();
+    }
+}

+ 7 - 3
app/Http/Controllers/Api/ApiV1Controller.php

@@ -1664,7 +1664,7 @@ class ApiV1Controller extends Controller
                     ],
                     'statuses' => [
                         'characters_reserved_per_url' => 23,
-                        'max_characters' => (int) config('pixelfed.max_caption_length'),
+                        'max_characters' => (int) config_cache('pixelfed.max_caption_length'),
                         'max_media_attachments' => (int) config('pixelfed.max_album_length'),
                     ],
                 ],
@@ -3308,7 +3308,7 @@ class ApiV1Controller extends Controller
         abort_unless($request->user()->tokenCan('write'), 403);
 
         $this->validate($request, [
-            'status' => 'nullable|string',
+            'status' => 'nullable|string|max:' . config_cache('pixelfed.max_caption_length'),
             'in_reply_to_id' => 'nullable',
             'media_ids' => 'sometimes|array|max:'.config_cache('pixelfed.max_album_length'),
             'sensitive' => 'nullable',
@@ -4066,7 +4066,7 @@ class ApiV1Controller extends Controller
 
         $pid = $request->user()->profile_id;
 
-        $ids = Cache::remember('api:v1.1:discover:accounts:popular', 3600, function () {
+        $ids = Cache::remember('api:v1.1:discover:accounts:popular', 14400, function () {
             return DB::table('profiles')
                 ->where('is_private', false)
                 ->whereNull('status')
@@ -4075,6 +4075,7 @@ class ApiV1Controller extends Controller
                 ->get();
         });
         $filters = UserFilterService::filters($pid);
+        $asf = AdminShadowFilterService::getHideFromPublicFeedsList();
         $ids = $ids->map(function ($profile) {
             return AccountService::get($profile->id, true);
         })
@@ -4087,6 +4088,9 @@ class ApiV1Controller extends Controller
             ->filter(function ($profile) use ($pid) {
                 return ! FollowerService::follows($pid, $profile['id'], true);
             })
+            ->filter(function ($profile) use ($asf) {
+                return ! in_array($profile['id'], $asf);
+            })
             ->filter(function ($profile) use ($filters) {
                 return ! in_array($profile['id'], $filters);
             })

+ 5 - 5
app/Http/Controllers/Api/ApiV1Dot1Controller.php

@@ -473,15 +473,15 @@ class ApiV1Dot1Controller extends Controller
 	{
 		return [
 			'open' => (bool) config_cache('pixelfed.open_registration'),
-			'iara' => config('pixelfed.allow_app_registration')
+			'iara' => (bool) config_cache('pixelfed.allow_app_registration'),
 		];
 	}
 
 	public function inAppRegistration(Request $request)
 	{
 		abort_if($request->user(), 404);
-		abort_unless(config_cache('pixelfed.open_registration'), 404);
-		abort_unless(config('pixelfed.allow_app_registration'), 404);
+		abort_unless((bool) config_cache('pixelfed.open_registration'), 404);
+		abort_unless((bool) config_cache('pixelfed.allow_app_registration'), 404);
 		abort_unless($request->hasHeader('X-PIXELFED-APP'), 403);
 		if(config('pixelfed.bouncer.cloud_ips.ban_signups')) {
 			abort_if(BouncerService::checkIp($request->ip()), 404);
@@ -609,8 +609,8 @@ class ApiV1Dot1Controller extends Controller
 	public function inAppRegistrationConfirm(Request $request)
 	{
 		abort_if($request->user(), 404);
-		abort_unless(config_cache('pixelfed.open_registration'), 404);
-		abort_unless(config('pixelfed.allow_app_registration'), 404);
+		abort_unless((bool) config_cache('pixelfed.open_registration'), 404);
+		abort_unless((bool) config_cache('pixelfed.allow_app_registration'), 404);
 		abort_unless($request->hasHeader('X-PIXELFED-APP'), 403);
 		if(config('pixelfed.bouncer.cloud_ips.ban_signups')) {
 			abort_if(BouncerService::checkIp($request->ip()), 404);

+ 1 - 1
app/Http/Controllers/Api/ApiV2Controller.php

@@ -104,7 +104,7 @@ class ApiV2Controller extends Controller
                         'max_featured_tags' => 0,
                     ],
                     'statuses' => [
-                        'max_characters' => (int) config('pixelfed.max_caption_length'),
+                        'max_characters' => (int) config_cache('pixelfed.max_caption_length'),
                         'max_media_attachments' => (int) config_cache('pixelfed.max_album_length'),
                         'characters_reserved_per_url' => 23
                     ],

+ 12 - 17
app/Http/Controllers/CommentController.php

@@ -2,23 +2,18 @@
 
 namespace App\Http\Controllers;
 
-use Illuminate\Http\Request;
-use Auth;
-use DB;
-use Cache;
-
-use App\Comment;
 use App\Jobs\CommentPipeline\CommentPipeline;
 use App\Jobs\StatusPipeline\NewStatusPipeline;
-use App\Util\Lexer\Autolink;
-use App\Profile;
+use App\Services\StatusService;
 use App\Status;
+use App\Transformer\Api\StatusTransformer;
 use App\UserFilter;
+use App\Util\Lexer\Autolink;
+use Auth;
+use DB;
+use Illuminate\Http\Request;
 use League\Fractal;
-use App\Transformer\Api\StatusTransformer;
 use League\Fractal\Serializer\ArraySerializer;
-use League\Fractal\Pagination\IlluminatePaginatorAdapter;
-use App\Services\StatusService;
 
 class CommentController extends Controller
 {
@@ -33,9 +28,9 @@ class CommentController extends Controller
             abort(403);
         }
         $this->validate($request, [
-            'item'    => 'required|integer|min:1',
-            'comment' => 'required|string|max:'.(int) config('pixelfed.max_caption_length'),
-            'sensitive' => 'nullable|boolean'
+            'item' => 'required|integer|min:1',
+            'comment' => 'required|string|max:'.config_cache('pixelfed.max_caption_length'),
+            'sensitive' => 'nullable|boolean',
         ]);
         $comment = $request->input('comment');
         $statusId = $request->input('item');
@@ -45,7 +40,7 @@ class CommentController extends Controller
         $profile = $user->profile;
         $status = Status::findOrFail($statusId);
 
-        if($status->comments_disabled == true) {
+        if ($status->comments_disabled == true) {
             return;
         }
 
@@ -55,11 +50,11 @@ class CommentController extends Controller
             ->whereFilterableId($profile->id)
             ->exists();
 
-        if($filtered == true) {
+        if ($filtered == true) {
             return;
         }
 
-        $reply = DB::transaction(function() use($comment, $status, $profile, $nsfw) {
+        $reply = DB::transaction(function () use ($comment, $status, $profile, $nsfw) {
             $scope = $profile->is_private == true ? 'private' : 'public';
             $autolink = Autolink::create()->autolink($comment);
             $reply = new Status();

+ 189 - 207
app/Http/Controllers/ComposeController.php

@@ -2,59 +2,38 @@
 
 namespace App\Http\Controllers;
 
-use Illuminate\Http\Request;
-use Auth, Cache, DB, Storage, URL;
-use Carbon\Carbon;
-use App\{
-    Avatar,
-    Collection,
-    CollectionItem,
-    Hashtag,
-    Like,
-    Media,
-    MediaTag,
-    Notification,
-    Profile,
-    Place,
-    Status,
-    UserFilter,
-    UserSetting
-};
-use App\Models\Poll;
-use App\Transformer\Api\{
-    MediaTransformer,
-    MediaDraftTransformer,
-    StatusTransformer,
-    StatusStatelessTransformer
-};
-use League\Fractal;
-use App\Util\Media\Filter;
-use League\Fractal\Serializer\ArraySerializer;
-use League\Fractal\Pagination\IlluminatePaginatorAdapter;
-use App\Jobs\AvatarPipeline\AvatarOptimize;
+use App\Collection;
+use App\CollectionItem;
+use App\Hashtag;
 use App\Jobs\ImageOptimizePipeline\ImageOptimize;
-use App\Jobs\ImageOptimizePipeline\ImageThumbnail;
 use App\Jobs\StatusPipeline\NewStatusPipeline;
-use App\Jobs\VideoPipeline\{
-    VideoOptimize,
-    VideoPostProcess,
-    VideoThumbnail
-};
+use App\Jobs\VideoPipeline\VideoThumbnail;
+use App\Media;
+use App\MediaTag;
+use App\Models\Poll;
+use App\Notification;
+use App\Profile;
 use App\Services\AccountService;
 use App\Services\CollectionService;
-use App\Services\NotificationService;
-use App\Services\MediaPathService;
 use App\Services\MediaBlocklistService;
+use App\Services\MediaPathService;
 use App\Services\MediaStorageService;
 use App\Services\MediaTagService;
-use App\Services\StatusService;
 use App\Services\SnowflakeService;
-use Illuminate\Support\Str;
+use App\Services\UserRoleService;
+use App\Status;
+use App\Transformer\Api\MediaTransformer;
+use App\UserFilter;
 use App\Util\Lexer\Autolink;
-use App\Util\Lexer\Extractor;
+use App\Util\Media\Filter;
 use App\Util\Media\License;
-use Image;
-use App\Services\UserRoleService;
+use Auth;
+use Cache;
+use DB;
+use Illuminate\Http\Request;
+use Illuminate\Support\Str;
+use League\Fractal;
+use League\Fractal\Serializer\ArraySerializer;
 
 class ComposeController extends Controller
 {
@@ -74,30 +53,30 @@ class ComposeController extends Controller
 
     public function mediaUpload(Request $request)
     {
-        abort_if(!$request->user(), 403);
+        abort_if(! $request->user(), 403);
 
         $this->validate($request, [
             'file.*' => [
                 'required_without:file',
-                'mimetypes:' . config_cache('pixelfed.media_types'),
-                'max:' . config_cache('pixelfed.max_photo_size'),
+                'mimetypes:'.config_cache('pixelfed.media_types'),
+                'max:'.config_cache('pixelfed.max_photo_size'),
             ],
             'file' => [
                 'required_without:file.*',
-                'mimetypes:' . config_cache('pixelfed.media_types'),
-                'max:' . config_cache('pixelfed.max_photo_size'),
+                'mimetypes:'.config_cache('pixelfed.media_types'),
+                'max:'.config_cache('pixelfed.max_photo_size'),
             ],
             'filter_name' => 'nullable|string|max:24',
-            'filter_class' => 'nullable|alpha_dash|max:24'
+            'filter_class' => 'nullable|alpha_dash|max:24',
         ]);
 
         $user = Auth::user();
         $profile = $user->profile;
-        abort_if($user->has_roles && !UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action');
+        abort_if($user->has_roles && ! UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action');
 
-        $limitKey = 'compose:rate-limit:media-upload:' . $user->id;
+        $limitKey = 'compose:rate-limit:media-upload:'.$user->id;
         $limitTtl = now()->addMinutes(15);
-        $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) {
+        $limitReached = Cache::remember($limitKey, $limitTtl, function () use ($user) {
             $dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count();
 
             return $dailyLimit >= 1250;
@@ -105,8 +84,8 @@ class ComposeController extends Controller
 
         abort_if($limitReached == true, 429);
 
-        if(config_cache('pixelfed.enforce_account_limit') == true) {
-            $size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) {
+        if (config_cache('pixelfed.enforce_account_limit') == true) {
+            $size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function () use ($user) {
                 return Media::whereUserId($user->id)->sum('size') / 1000;
             });
             $limit = (int) config_cache('pixelfed.max_account_size');
@@ -144,24 +123,24 @@ class ComposeController extends Controller
         $media->version = 3;
         $media->save();
 
-        $preview_url = $media->url() . '?v=' . time();
-        $url = $media->url() . '?v=' . time();
+        $preview_url = $media->url().'?v='.time();
+        $url = $media->url().'?v='.time();
 
         switch ($media->mime) {
             case 'image/jpeg':
             case 'image/png':
             case 'image/webp':
-            ImageOptimize::dispatch($media)->onQueue('mmo');
-            break;
+                ImageOptimize::dispatch($media)->onQueue('mmo');
+                break;
 
             case 'video/mp4':
-            VideoThumbnail::dispatch($media)->onQueue('mmo');
-            $preview_url = '/storage/no-preview.png';
-            $url = '/storage/no-preview.png';
-            break;
+                VideoThumbnail::dispatch($media)->onQueue('mmo');
+                $preview_url = '/storage/no-preview.png';
+                $url = '/storage/no-preview.png';
+                break;
 
             default:
-            break;
+                break;
         }
 
         Cache::forget($limitKey);
@@ -169,6 +148,7 @@ class ComposeController extends Controller
         $res = $this->fractal->createData($resource)->toArray();
         $res['preview_url'] = $preview_url;
         $res['url'] = $url;
+
         return response()->json($res);
     }
 
@@ -176,21 +156,21 @@ class ComposeController extends Controller
     {
         $this->validate($request, [
             'id' => 'required',
-            'file' => function() {
+            'file' => function () {
                 return [
                     'required',
-                    'mimetypes:' . config_cache('pixelfed.media_types'),
-                    'max:' . config_cache('pixelfed.max_photo_size'),
+                    'mimetypes:'.config_cache('pixelfed.media_types'),
+                    'max:'.config_cache('pixelfed.max_photo_size'),
                 ];
             },
         ]);
 
         $user = Auth::user();
-        abort_if($user->has_roles && !UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action');
+        abort_if($user->has_roles && ! UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action');
 
-        $limitKey = 'compose:rate-limit:media-updates:' . $user->id;
+        $limitKey = 'compose:rate-limit:media-updates:'.$user->id;
         $limitTtl = now()->addMinutes(15);
-        $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) {
+        $limitReached = Cache::remember($limitKey, $limitTtl, function () use ($user) {
             $dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count();
 
             return $dailyLimit >= 1500;
@@ -202,9 +182,9 @@ class ComposeController extends Controller
         $id = $request->input('id');
 
         $media = Media::whereUserId($user->id)
-        ->whereProfileId($user->profile_id)
-        ->whereNull('status_id')
-        ->findOrFail($id);
+            ->whereProfileId($user->profile_id)
+            ->whereNull('status_id')
+            ->findOrFail($id);
 
         $media->save();
 
@@ -214,47 +194,48 @@ class ComposeController extends Controller
         $dir = implode('/', $fragments);
         $path = $photo->storePubliclyAs($dir, $name);
         $res = [
-            'url' => $media->url() . '?v=' . time()
+            'url' => $media->url().'?v='.time(),
         ];
         ImageOptimize::dispatch($media)->onQueue('mmo');
         Cache::forget($limitKey);
+
         return $res;
     }
 
     public function mediaDelete(Request $request)
     {
-        abort_if(!$request->user(), 403);
+        abort_if(! $request->user(), 403);
 
         $this->validate($request, [
-            'id' => 'required|integer|min:1|exists:media,id'
+            'id' => 'required|integer|min:1|exists:media,id',
         ]);
 
-        abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
+        abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
 
         $media = Media::whereNull('status_id')
-        ->whereUserId(Auth::id())
-        ->findOrFail($request->input('id'));
+            ->whereUserId(Auth::id())
+            ->findOrFail($request->input('id'));
 
         MediaStorageService::delete($media, true);
 
         return response()->json([
             'msg' => 'Successfully deleted',
-            'code' => 200
+            'code' => 200,
         ]);
     }
 
     public function searchTag(Request $request)
     {
-        abort_if(!$request->user(), 403);
+        abort_if(! $request->user(), 403);
 
         $this->validate($request, [
-            'q' => 'required|string|min:1|max:50'
+            'q' => 'required|string|min:1|max:50',
         ]);
 
         $q = $request->input('q');
 
-        if(Str::of($q)->startsWith('@')) {
-            if(strlen($q) < 3) {
+        if (Str::of($q)->startsWith('@')) {
+            if (strlen($q) < 3) {
                 return [];
             }
             $q = mb_substr($q, 1);
@@ -262,7 +243,7 @@ class ComposeController extends Controller
 
         $user = $request->user();
 
-        abort_if($user->has_roles && !UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action');
+        abort_if($user->has_roles && ! UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action');
 
         $blocked = UserFilter::whereFilterableType('App\Profile')
             ->whereFilterType('block')
@@ -271,34 +252,34 @@ class ComposeController extends Controller
 
         $blocked->push($request->user()->profile_id);
 
-        $results = Profile::select('id','domain','username')
+        $results = Profile::select('id', 'domain', 'username')
             ->whereNotIn('id', $blocked)
             ->whereNull('domain')
-            ->where('username','like','%'.$q.'%')
+            ->where('username', 'like', '%'.$q.'%')
             ->limit(15)
             ->get()
-            ->map(function($r) {
+            ->map(function ($r) {
                 return [
                     'id' => (string) $r->id,
                     'name' => $r->username,
                     'privacy' => true,
-                    'avatar' => $r->avatarUrl()
+                    'avatar' => $r->avatarUrl(),
                 ];
-        });
+            });
 
         return $results;
     }
 
     public function searchUntag(Request $request)
     {
-        abort_if(!$request->user(), 403);
+        abort_if(! $request->user(), 403);
 
         $this->validate($request, [
             'status_id' => 'required',
-            'profile_id' => 'required'
+            'profile_id' => 'required',
         ]);
 
-        abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
+        abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
 
         $user = $request->user();
         $status_id = $request->input('status_id');
@@ -310,7 +291,7 @@ class ComposeController extends Controller
             ->whereProfileId($profile_id)
             ->first();
 
-        if(!$tag) {
+        if (! $tag) {
             return [];
         }
         Notification::whereItemType('App\MediaTag')
@@ -326,37 +307,38 @@ class ComposeController extends Controller
 
     public function searchLocation(Request $request)
     {
-        abort_if(!$request->user(), 403);
+        abort_if(! $request->user(), 403);
         $this->validate($request, [
-            'q' => 'required|string|max:100'
+            'q' => 'required|string|max:100',
         ]);
-        abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
+        abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
         $pid = $request->user()->profile_id;
-        abort_if(!$pid, 400);
+        abort_if(! $pid, 400);
         $q = e($request->input('q'));
 
-        $popular = Cache::remember('pf:search:location:v1:popular', 1209600, function() {
+        $popular = Cache::remember('pf:search:location:v1:popular', 1209600, function () {
             $minId = SnowflakeService::byDate(now()->subDays(290));
-            if(config('database.default') == 'pgsql') {
+            if (config('database.default') == 'pgsql') {
                 return Status::selectRaw('id, place_id, count(place_id) as pc')
-                ->whereNotNull('place_id')
-                ->where('id', '>', $minId)
-                ->orderByDesc('pc')
-                ->groupBy(['place_id', 'id'])
-                ->limit(400)
-                ->get()
-                ->filter(function($post) {
-                    return $post;
-                })
-                ->map(function($place) {
-                    return [
-                        'id' => $place->place_id,
-                        'count' => $place->pc
-                    ];
-                })
-                ->unique('id')
-                ->values();
+                    ->whereNotNull('place_id')
+                    ->where('id', '>', $minId)
+                    ->orderByDesc('pc')
+                    ->groupBy(['place_id', 'id'])
+                    ->limit(400)
+                    ->get()
+                    ->filter(function ($post) {
+                        return $post;
+                    })
+                    ->map(function ($place) {
+                        return [
+                            'id' => $place->place_id,
+                            'count' => $place->pc,
+                        ];
+                    })
+                    ->unique('id')
+                    ->values();
             }
+
             return Status::selectRaw('id, place_id, count(place_id) as pc')
                 ->whereNotNull('place_id')
                 ->where('id', '>', $minId)
@@ -364,57 +346,58 @@ class ComposeController extends Controller
                 ->orderByDesc('pc')
                 ->limit(400)
                 ->get()
-                ->filter(function($post) {
+                ->filter(function ($post) {
                     return $post;
                 })
-                ->map(function($place) {
+                ->map(function ($place) {
                     return [
                         'id' => $place->place_id,
-                        'count' => $place->pc
+                        'count' => $place->pc,
                     ];
                 });
         });
-        $q = '%' . $q . '%';
+        $q = '%'.$q.'%';
         $wildcard = config('database.default') === 'pgsql' ? 'ilike' : 'like';
 
         $places = DB::table('places')
-        ->where('name', $wildcard, $q)
-        ->limit((strlen($q) > 5 ? 360 : 30))
-        ->get()
-        ->sortByDesc(function($place, $key) use($popular) {
-            return $popular->filter(function($p) use($place) {
-                return $p['id'] == $place->id;
-            })->map(function($p) use($place) {
-                return in_array($place->country, ['Canada', 'USA', 'France', 'Germany', 'United Kingdom']) ? $p['count'] : 1;
-            })->values();
-        })
-        ->map(function($r) {
-            return [
-                'id' => $r->id,
-                'name' => $r->name,
-                'country' => $r->country,
-                'url'   => url('/discover/places/' . $r->id . '/' . $r->slug)
-            ];
-        })
-        ->values()
-        ->all();
+            ->where('name', $wildcard, $q)
+            ->limit((strlen($q) > 5 ? 360 : 30))
+            ->get()
+            ->sortByDesc(function ($place, $key) use ($popular) {
+                return $popular->filter(function ($p) use ($place) {
+                    return $p['id'] == $place->id;
+                })->map(function ($p) use ($place) {
+                    return in_array($place->country, ['Canada', 'USA', 'France', 'Germany', 'United Kingdom']) ? $p['count'] : 1;
+                })->values();
+            })
+            ->map(function ($r) {
+                return [
+                    'id' => $r->id,
+                    'name' => $r->name,
+                    'country' => $r->country,
+                    'url' => url('/discover/places/'.$r->id.'/'.$r->slug),
+                ];
+            })
+            ->values()
+            ->all();
+
         return $places;
     }
 
     public function searchMentionAutocomplete(Request $request)
     {
-        abort_if(!$request->user(), 403);
+        abort_if(! $request->user(), 403);
 
         $this->validate($request, [
-            'q' => 'required|string|min:2|max:50'
+            'q' => 'required|string|min:2|max:50',
         ]);
 
-        abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
+        abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
 
         $q = $request->input('q');
 
-        if(Str::of($q)->startsWith('@')) {
-            if(strlen($q) < 3) {
+        if (Str::of($q)->startsWith('@')) {
+            if (strlen($q) < 3) {
                 return [];
             }
         }
@@ -426,32 +409,33 @@ class ComposeController extends Controller
 
         $blocked->push($request->user()->profile_id);
 
-        $results = Profile::select('id','domain','username')
+        $results = Profile::select('id', 'domain', 'username')
             ->whereNotIn('id', $blocked)
-            ->where('username','like','%'.$q.'%')
+            ->where('username', 'like', '%'.$q.'%')
             ->groupBy('id', 'domain')
             ->limit(15)
             ->get()
-            ->map(function($profile) {
+            ->map(function ($profile) {
                 $username = $profile->domain ? substr($profile->username, 1) : $profile->username;
+
                 return [
-                    'key' => '@' . str_limit($username, 30),
+                    'key' => '@'.str_limit($username, 30),
                     'value' => $username,
                 ];
-        });
+            });
 
         return $results;
     }
 
     public function searchHashtagAutocomplete(Request $request)
     {
-        abort_if(!$request->user(), 403);
+        abort_if(! $request->user(), 403);
 
         $this->validate($request, [
-            'q' => 'required|string|min:2|max:50'
+            'q' => 'required|string|min:2|max:50',
         ]);
 
-        abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
+        abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
 
         $q = $request->input('q');
 
@@ -461,12 +445,12 @@ class ComposeController extends Controller
             ->whereIsBanned(false)
             ->limit(5)
             ->get()
-            ->map(function($tag) {
+            ->map(function ($tag) {
                 return [
-                    'key' => '#' . $tag->slug,
-                    'value' => $tag->slug
+                    'key' => '#'.$tag->slug,
+                    'value' => $tag->slug,
                 ];
-        });
+            });
 
         return $results;
     }
@@ -474,8 +458,8 @@ class ComposeController extends Controller
     public function store(Request $request)
     {
         $this->validate($request, [
-            'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500),
-            'media.*'   => 'required',
+            'caption' => 'nullable|string|max:'.config_cache('pixelfed.max_caption_length', 500),
+            'media.*' => 'required',
             'media.*.id' => 'required|integer|min:1',
             'media.*.filter_class' => 'nullable|alpha_dash|max:30',
             'media.*.license' => 'nullable|string|max:140',
@@ -491,14 +475,14 @@ class ComposeController extends Controller
             // 'optimize_media' => 'nullable'
         ]);
 
-        abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
+        abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
 
-        if(config('costar.enabled') == true) {
+        if (config('costar.enabled') == true) {
             $blockedKeywords = config('costar.keyword.block');
-            if($blockedKeywords !== null && $request->caption) {
+            if ($blockedKeywords !== null && $request->caption) {
                 $keywords = config('costar.keyword.block');
-                foreach($keywords as $kw) {
-                    if(Str::contains($request->caption, $kw) == true) {
+                foreach ($keywords as $kw) {
+                    if (Str::contains($request->caption, $kw) == true) {
                         abort(400, 'Invalid object');
                     }
                 }
@@ -508,9 +492,9 @@ class ComposeController extends Controller
         $user = $request->user();
         $profile = $user->profile;
 
-        $limitKey = 'compose:rate-limit:store:' . $user->id;
+        $limitKey = 'compose:rate-limit:store:'.$user->id;
         $limitTtl = now()->addMinutes(15);
-        $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) {
+        $limitReached = Cache::remember($limitKey, $limitTtl, function () use ($user) {
             $dailyLimit = Status::whereProfileId($user->profile_id)
                 ->whereNull('in_reply_to_id')
                 ->whereNull('reblog_of_id')
@@ -534,12 +518,12 @@ class ComposeController extends Controller
         $tagged = $request->input('tagged');
         $optimize_media = (bool) $request->input('optimize_media');
 
-        foreach($medias as $k => $media) {
-            if($k + 1 > config_cache('pixelfed.max_album_length')) {
+        foreach ($medias as $k => $media) {
+            if ($k + 1 > config_cache('pixelfed.max_album_length')) {
                 continue;
             }
             $m = Media::findOrFail($media['id']);
-            if($m->profile_id !== $profile->id || $m->status_id) {
+            if ($m->profile_id !== $profile->id || $m->status_id) {
                 abort(403, 'Invalid media id');
             }
             $m->filter_class = in_array($media['filter_class'], Filter::classes()) ? $media['filter_class'] : null;
@@ -547,7 +531,7 @@ class ComposeController extends Controller
             $m->caption = isset($media['alt']) ? strip_tags($media['alt']) : null;
             $m->order = isset($media['cursor']) && is_int($media['cursor']) ? (int) $media['cursor'] : $k;
 
-            if($cw == true || $profile->cw == true) {
+            if ($cw == true || $profile->cw == true) {
                 $m->is_nsfw = $cw;
                 $status->is_nsfw = $cw;
             }
@@ -560,19 +544,19 @@ class ComposeController extends Controller
 
         $mediaType = StatusController::mimeTypeCheck($mimes);
 
-        if(in_array($mediaType, ['photo', 'video', 'photo:album']) == false) {
+        if (in_array($mediaType, ['photo', 'video', 'photo:album']) == false) {
             abort(400, __('exception.compose.invalid.album'));
         }
 
-        if($place && is_array($place)) {
+        if ($place && is_array($place)) {
             $status->place_id = $place['id'];
         }
 
-        if($request->filled('comments_disabled')) {
+        if ($request->filled('comments_disabled')) {
             $status->comments_disabled = (bool) $request->input('comments_disabled');
         }
 
-        if($request->filled('spoiler_text') && $cw) {
+        if ($request->filled('spoiler_text') && $cw) {
             $status->cw_summary = $request->input('spoiler_text');
         }
 
@@ -583,7 +567,7 @@ class ComposeController extends Controller
         $status->profile_id = $profile->id;
         $status->save();
 
-        foreach($attachments as $media) {
+        foreach ($attachments as $media) {
             $media->status_id = $status->id;
             $media->save();
         }
@@ -597,7 +581,7 @@ class ComposeController extends Controller
         $status->type = $mediaType;
         $status->save();
 
-        foreach($tagged as $tg) {
+        foreach ($tagged as $tg) {
             $mt = new MediaTag;
             $mt->status_id = $status->id;
             $mt->media_id = $status->media->first()->id;
@@ -612,17 +596,17 @@ class ComposeController extends Controller
             MediaTagService::sendNotification($mt);
         }
 
-        if($request->filled('collections')) {
+        if ($request->filled('collections')) {
             $collections = Collection::whereProfileId($profile->id)
                 ->find($request->input('collections'))
-                ->each(function($collection) use($status) {
+                ->each(function ($collection) use ($status) {
                     $count = $collection->items()->count();
                     CollectionItem::firstOrCreate([
                         'collection_id' => $collection->id,
                         'object_type' => 'App\Status',
-                        'object_id' => $status->id
+                        'object_id' => $status->id,
                     ], [
-                        'order' => $count
+                        'order' => $count,
                     ]);
 
                     CollectionService::addItem(
@@ -643,7 +627,7 @@ class ComposeController extends Controller
         Cache::forget('profile:status_count:'.$profile->id);
         Cache::forget('status:transformer:media:attachments:'.$status->id);
         Cache::forget($user->storageUsedKey());
-        Cache::forget('profile:embed:' . $status->profile_id);
+        Cache::forget('profile:embed:'.$status->profile_id);
         Cache::forget($limitKey);
 
         return $status->url();
@@ -653,7 +637,7 @@ class ComposeController extends Controller
     {
         abort_unless(config('exp.top'), 404);
         $this->validate($request, [
-            'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500),
+            'caption' => 'nullable|string|max:'.config_cache('pixelfed.max_caption_length', 500),
             'cw' => 'nullable|boolean',
             'visibility' => 'required|string|in:public,private,unlisted|min:2|max:10',
             'place' => 'nullable',
@@ -661,14 +645,14 @@ class ComposeController extends Controller
             'tagged' => 'nullable',
         ]);
 
-        abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
+        abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
 
-        if(config('costar.enabled') == true) {
+        if (config('costar.enabled') == true) {
             $blockedKeywords = config('costar.keyword.block');
-            if($blockedKeywords !== null && $request->caption) {
+            if ($blockedKeywords !== null && $request->caption) {
                 $keywords = config('costar.keyword.block');
-                foreach($keywords as $kw) {
-                    if(Str::contains($request->caption, $kw) == true) {
+                foreach ($keywords as $kw) {
+                    if (Str::contains($request->caption, $kw) == true) {
                         abort(400, 'Invalid object');
                     }
                 }
@@ -683,11 +667,11 @@ class ComposeController extends Controller
         $cw = $request->input('cw');
         $tagged = $request->input('tagged');
 
-        if($place && is_array($place)) {
+        if ($place && is_array($place)) {
             $status->place_id = $place['id'];
         }
 
-        if($request->filled('comments_disabled')) {
+        if ($request->filled('comments_disabled')) {
             $status->comments_disabled = (bool) $request->input('comments_disabled');
         }
 
@@ -707,11 +691,11 @@ class ComposeController extends Controller
                 'bg_id' => 1,
                 'font_size' => strlen($status->caption) <= 140 ? 'h1' : 'h3',
                 'length' => strlen($status->caption),
-            ]
+            ],
         ], $entities), JSON_UNESCAPED_SLASHES);
         $status->save();
 
-        foreach($tagged as $tg) {
+        foreach ($tagged as $tg) {
             $mt = new MediaTag;
             $mt->status_id = $status->id;
             $mt->media_id = $status->media->first()->id;
@@ -726,7 +710,6 @@ class ComposeController extends Controller
             MediaTagService::sendNotification($mt);
         }
 
-
         Cache::forget('user:account:id:'.$profile->user_id);
         Cache::forget('_api:statuses:recent_9:'.$profile->id);
         Cache::forget('profile:status_count:'.$profile->id);
@@ -737,18 +720,18 @@ class ComposeController extends Controller
     public function mediaProcessingCheck(Request $request)
     {
         $this->validate($request, [
-            'id' => 'required|integer|min:1'
+            'id' => 'required|integer|min:1',
         ]);
 
-        abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
+        abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
 
         $media = Media::whereUserId($request->user()->id)
             ->whereNull('status_id')
             ->findOrFail($request->input('id'));
 
-        if(config('pixelfed.media_fast_process')) {
+        if (config('pixelfed.media_fast_process')) {
             return [
-                'finished' => true
+                'finished' => true,
             ];
         }
 
@@ -762,27 +745,27 @@ class ComposeController extends Controller
                 break;
 
             default:
-                # code...
+                // code...
                 break;
         }
 
         return [
-            'finished' => $finished
+            'finished' => $finished,
         ];
     }
 
     public function composeSettings(Request $request)
     {
         $uid = $request->user()->id;
-        abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
+        abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
 
         $default = [
             'default_license' => 1,
             'media_descriptions' => false,
-            'max_altext_length' => config_cache('pixelfed.max_altext_length')
+            'max_altext_length' => config_cache('pixelfed.max_altext_length'),
         ];
         $settings = AccountService::settings($uid);
-        if(isset($settings['other']) && isset($settings['other']['scope'])) {
+        if (isset($settings['other']) && isset($settings['other']['scope'])) {
             $s = $settings['compose_settings'];
             $s['default_scope'] = $settings['other']['scope'];
             $settings['compose_settings'] = $s;
@@ -794,23 +777,22 @@ class ComposeController extends Controller
     public function createPoll(Request $request)
     {
         $this->validate($request, [
-            'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500),
+            'caption' => 'nullable|string|max:'.config_cache('pixelfed.max_caption_length', 500),
             'cw' => 'nullable|boolean',
             'visibility' => 'required|string|in:public,private',
             'comments_disabled' => 'nullable',
             'expiry' => 'required|in:60,360,1440,10080',
-            'pollOptions' => 'required|array|min:1|max:4'
+            'pollOptions' => 'required|array|min:1|max:4',
         ]);
         abort(404);
         abort_if(config('instance.polls.enabled') == false, 404, 'Polls not enabled');
-        abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
+        abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
 
         abort_if(Status::whereType('poll')
             ->whereProfileId($request->user()->profile_id)
             ->whereCaption($request->input('caption'))
             ->where('created_at', '>', now()->subDays(2))
-            ->exists()
-        , 422, 'Duplicate detected.');
+            ->exists(), 422, 'Duplicate detected.');
 
         $status = new Status;
         $status->profile_id = $request->user()->profile_id;
@@ -827,7 +809,7 @@ class ComposeController extends Controller
         $poll->profile_id = $status->profile_id;
         $poll->poll_options = $request->input('pollOptions');
         $poll->expires_at = now()->addMinutes($request->input('expiry'));
-        $poll->cached_tallies = collect($poll->poll_options)->map(function($o) {
+        $poll->cached_tallies = collect($poll->poll_options)->map(function ($o) {
             return 0;
         })->toArray();
         $poll->save();

+ 43 - 0
app/Http/Controllers/DiscoverController.php

@@ -5,8 +5,11 @@ namespace App\Http\Controllers;
 use App\Hashtag;
 use App\Instance;
 use App\Like;
+use App\Services\AccountService;
+use App\Services\AdminShadowFilterService;
 use App\Services\BookmarkService;
 use App\Services\ConfigCacheService;
+use App\Services\FollowerService;
 use App\Services\HashtagService;
 use App\Services\LikeService;
 use App\Services\ReblogService;
@@ -377,4 +380,44 @@ class DiscoverController extends Controller
 
         return $res;
     }
+
+    public function discoverAccountsPopular(Request $request)
+    {
+        abort_if(! $request->user(), 403);
+
+        $pid = $request->user()->profile_id;
+
+        $ids = Cache::remember('api:v1.1:discover:accounts:popular', 14400, function () {
+            return DB::table('profiles')
+                ->where('is_private', false)
+                ->whereNull('status')
+                ->orderByDesc('profiles.followers_count')
+                ->limit(30)
+                ->get();
+        });
+        $filters = UserFilterService::filters($pid);
+        $asf = AdminShadowFilterService::getHideFromPublicFeedsList();
+        $ids = $ids->map(function ($profile) {
+            return AccountService::get($profile->id, true);
+        })
+            ->filter(function ($profile) {
+                return $profile && isset($profile['id'], $profile['locked']) && ! $profile['locked'];
+            })
+            ->filter(function ($profile) use ($pid) {
+                return $profile['id'] != $pid;
+            })
+            ->filter(function ($profile) use ($pid) {
+                return ! FollowerService::follows($pid, $profile['id'], true);
+            })
+            ->filter(function ($profile) use ($asf) {
+                return ! in_array($profile['id'], $asf);
+            })
+            ->filter(function ($profile) use ($filters) {
+                return ! in_array($profile['id'], $filters);
+            })
+            ->take(16)
+            ->values();
+
+        return response()->json($ids, 200, [], JSON_UNESCAPED_SLASHES);
+    }
 }

+ 370 - 341
app/Http/Controllers/ProfileController.php

@@ -2,356 +2,385 @@
 
 namespace App\Http\Controllers;
 
-use Illuminate\Http\Request;
-use Auth;
-use Cache;
-use DB;
-use View;
 use App\AccountInterstitial;
 use App\Follower;
 use App\FollowRequest;
 use App\Profile;
-use App\Story;
-use App\Status;
-use App\User;
-use App\UserSetting;
-use App\UserFilter;
-use League\Fractal;
 use App\Services\AccountService;
 use App\Services\FollowerService;
 use App\Services\StatusService;
-use App\Util\Lexer\Nickname;
-use App\Util\Webfinger\Webfinger;
-use App\Transformer\ActivityPub\ProfileOutbox;
+use App\Status;
+use App\Story;
 use App\Transformer\ActivityPub\ProfileTransformer;
+use App\User;
+use App\UserFilter;
+use App\UserSetting;
+use Auth;
+use Cache;
+use Illuminate\Http\Request;
+use League\Fractal;
+use View;
 
 class ProfileController extends Controller
 {
-	public function show(Request $request, $username)
-	{
-		// redirect authed users to Metro 2.0
-		if($request->user()) {
-			// unless they force static view
-			if(!$request->has('fs') || $request->input('fs') != '1') {
-				$pid = AccountService::usernameToId($username);
-				if($pid) {
-					return redirect('/i/web/profile/' . $pid);
-				}
-			}
-		}
-
-		$user = Profile::whereNull('domain')
-			->whereNull('status')
-			->whereUsername($username)
-			->firstOrFail();
-
-
-		if($request->wantsJson() && config_cache('federation.activitypub.enabled')) {
-			return $this->showActivityPub($request, $user);
-		}
-
-		$aiCheck = Cache::remember('profile:ai-check:spam-login:' . $user->id, 86400, function() use($user) {
-			$exists = AccountInterstitial::whereUserId($user->user_id)->where('is_spam', 1)->count();
-			if($exists) {
-				return true;
-			}
-
-			return false;
-		});
-		if($aiCheck) {
-			return redirect('/login');
-		}
-		return $this->buildProfile($request, $user);
-	}
-
-	protected function buildProfile(Request $request, $user)
-	{
-		$username = $user->username;
-		$loggedIn = Auth::check();
-		$isPrivate = false;
-		$isBlocked = false;
-		if(!$loggedIn) {
-			$key = 'profile:settings:' . $user->id;
-			$ttl = now()->addHours(6);
-			$settings = Cache::remember($key, $ttl, function() use($user) {
-				return $user->user->settings;
-			});
-
-			if ($user->is_private == true) {
-				$profile = null;
-				return view('profile.private', compact('user'));
-			}
-
-			$owner = false;
-			$is_following = false;
-
-			$profile = $user;
-			$settings = [
-				'crawlable' => $settings->crawlable,
-				'following' => [
-					'count' => $settings->show_profile_following_count,
-					'list' => $settings->show_profile_following
-				],
-				'followers' => [
-					'count' => $settings->show_profile_follower_count,
-					'list' => $settings->show_profile_followers
-				]
-			];
-			return view('profile.show', compact('profile', 'settings'));
-		} else {
-			$key = 'profile:settings:' . $user->id;
-			$ttl = now()->addHours(6);
-			$settings = Cache::remember($key, $ttl, function() use($user) {
-				return $user->user->settings;
-			});
-
-			if ($user->is_private == true) {
-				$isPrivate = $this->privateProfileCheck($user, $loggedIn);
-			}
-
-			$isBlocked = $this->blockedProfileCheck($user);
-
-			$owner = $loggedIn && Auth::id() === $user->user_id;
-			$is_following = ($owner == false && Auth::check()) ? $user->followedBy(Auth::user()->profile) : false;
-
-			if ($isPrivate == true || $isBlocked == true) {
-				$requested = Auth::check() ? FollowRequest::whereFollowerId(Auth::user()->profile_id)
-					->whereFollowingId($user->id)
-					->exists() : false;
-				return view('profile.private', compact('user', 'is_following', 'requested'));
-			}
-
-			$is_admin = is_null($user->domain) ? $user->user->is_admin : false;
-			$profile = $user;
-			$settings = [
-				'crawlable' => $settings->crawlable,
-				'following' => [
-					'count' => $settings->show_profile_following_count,
-					'list' => $settings->show_profile_following
-				],
-				'followers' => [
-					'count' => $settings->show_profile_follower_count,
-					'list' => $settings->show_profile_followers
-				]
-			];
-			return view('profile.show', compact('profile', 'settings'));
-		}
-	}
-
-	public function permalinkRedirect(Request $request, $username)
-	{
-		$user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
-
-		if ($request->wantsJson() && config_cache('federation.activitypub.enabled')) {
-			return $this->showActivityPub($request, $user);
-		}
-
-		return redirect($user->url());
-	}
-
-	protected function privateProfileCheck(Profile $profile, $loggedIn)
-	{
-		if (!Auth::check()) {
-			return true;
-		}
-
-		$user = Auth::user()->profile;
-		if($user->id == $profile->id || !$profile->is_private) {
-			return false;
-		}
-
-		$follows = Follower::whereProfileId($user->id)->whereFollowingId($profile->id)->exists();
-		if ($follows == false) {
-			return true;
-		}
-
-		return false;
-	}
-
-	public static function accountCheck(Profile $profile)
-	{
-		switch ($profile->status) {
-			case 'disabled':
-			case 'suspended':
-			case 'delete':
-				return view('profile.disabled');
-				break;
-
-			default:
-				break;
-		}
-		return abort(404);
-	}
-
-	protected function blockedProfileCheck(Profile $profile)
-	{
-		$pid = Auth::user()->profile->id;
-		$blocks = UserFilter::whereUserId($profile->id)
-				->whereFilterType('block')
-				->whereFilterableType('App\Profile')
-				->pluck('filterable_id')
-				->toArray();
-		if (in_array($pid, $blocks)) {
-			return true;
-		}
-
-		return false;
-	}
-
-	public function showActivityPub(Request $request, $user)
-	{
-		abort_if(!config_cache('federation.activitypub.enabled'), 404);
-		abort_if($user->domain, 404);
-
-		return Cache::remember('pf:activitypub:user-object:by-id:' . $user->id, 3600, function() use($user) {
-			$fractal = new Fractal\Manager();
-			$resource = new Fractal\Resource\Item($user, new ProfileTransformer);
-			$res = $fractal->createData($resource)->toArray();
-			return response(json_encode($res['data']))->header('Content-Type', 'application/activity+json');
-		});
-	}
-
-	public function showAtomFeed(Request $request, $user)
-	{
-		abort_if(!config('federation.atom.enabled'), 404);
-
-		$pid = AccountService::usernameToId($user);
-
-		abort_if(!$pid, 404);
-
-		$profile = AccountService::get($pid, true);
-
-		abort_if(!$profile || $profile['locked'] || !$profile['local'], 404);
-
-		$aiCheck = Cache::remember('profile:ai-check:spam-login:' . $profile['id'], 86400, function() use($profile) {
-			$uid = User::whereProfileId($profile['id'])->first();
-			if(!$uid) {
-				return true;
-			}
-			$exists = AccountInterstitial::whereUserId($uid->id)->where('is_spam', 1)->count();
-			if($exists) {
-				return true;
-			}
-
-			return false;
-		});
-
-		abort_if($aiCheck, 404);
-
-		$enabled = Cache::remember('profile:atom:enabled:' . $profile['id'], 84600, function() use ($profile) {
-			$uid = User::whereProfileId($profile['id'])->first();
-			if(!$uid) {
-				return false;
-			}
-			$settings = UserSetting::whereUserId($uid->id)->first();
-			if(!$settings) {
-				return false;
-			}
-
-			return $settings->show_atom;
-		});
-
-		abort_if(!$enabled, 404);
-
-		$data = Cache::remember('pf:atom:user-feed:by-id:' . $profile['id'], 900, function() use($pid, $profile) {
-			$items = Status::whereProfileId($pid)
-				->whereScope('public')
-				->whereIn('type', ['photo', 'photo:album'])
-				->orderByDesc('id')
-				->take(10)
-				->get()
-				->map(function($status) {
-					return StatusService::get($status->id, true);
-				})
-				->filter(function($status) {
-					return $status &&
-						isset($status['account']) &&
-						isset($status['media_attachments']) &&
-						count($status['media_attachments']);
-				})
-				->values();
-			$permalink = config('app.url') . "/users/{$profile['username']}.atom";
-			$headers = ['Content-Type' => 'application/atom+xml'];
-
-			if($items && $items->count()) {
-				$headers['Last-Modified'] = now()->parse($items->first()['created_at'])->toRfc7231String();
-			}
-
-			return compact('items', 'permalink', 'headers');
-		});
-		abort_if(!$data || !isset($data['items']) || !isset($data['permalink']), 404);
-		return response()
-			->view('atom.user',
-				[
-					'profile' => $profile,
-					'items' => $data['items'],
-					'permalink' => $data['permalink']
-				]
-			)
-			->withHeaders($data['headers']);
-	}
-
-	public function meRedirect()
-	{
-		abort_if(!Auth::check(), 404);
-		return redirect(Auth::user()->url());
-	}
-
-	public function embed(Request $request, $username)
-	{
-		$res = view('profile.embed-removed');
-
-		if(!config('instance.embed.profile')) {
-			return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
-		}
-
-		if(strlen($username) > 15 || strlen($username) < 2) {
-			return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
-		}
-
-		$profile = Profile::whereUsername($username)
-			->whereIsPrivate(false)
-			->whereNull('status')
-			->whereNull('domain')
-			->first();
-
-		if(!$profile) {
-			return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
-		}
-
-		$aiCheck = Cache::remember('profile:ai-check:spam-login:' . $profile->id, 86400, function() use($profile) {
-			$exists = AccountInterstitial::whereUserId($profile->user_id)->where('is_spam', 1)->count();
-			if($exists) {
-				return true;
-			}
-
-			return false;
-		});
-
-		if($aiCheck) {
-			return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
-		}
-
-		if(AccountService::canEmbed($profile->user_id) == false) {
-			return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
-		}
-
-		$profile = AccountService::get($profile->id);
-		$res = view('profile.embed', compact('profile'));
-		return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
-	}
-
-	public function stories(Request $request, $username)
-	{
-		abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
-		$profile = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
-		$pid = $profile->id;
-		$authed = Auth::user()->profile_id;
-		abort_if($pid != $authed && !FollowerService::follows($authed, $pid), 404);
-		$exists = Story::whereProfileId($pid)
-			->whereActive(true)
-			->exists();
-		abort_unless($exists, 404);
-		return view('profile.story', compact('pid', 'profile'));
-	}
+    public function show(Request $request, $username)
+    {
+        if ($request->wantsJson() && (bool) config_cache('federation.activitypub.enabled')) {
+            $user = $this->getCachedUser($username, true);
+            abort_if(! $user, 404, 'Not found');
+
+            return $this->showActivityPub($request, $user);
+        }
+
+        // redirect authed users to Metro 2.0
+        if ($request->user()) {
+            // unless they force static view
+            if (! $request->has('fs') || $request->input('fs') != '1') {
+                $pid = AccountService::usernameToId($username);
+                if ($pid) {
+                    return redirect('/i/web/profile/'.$pid);
+                }
+            }
+        }
+
+        $user = $this->getCachedUser($username);
+
+        abort_unless($user, 404);
+
+        $aiCheck = Cache::remember('profile:ai-check:spam-login:'.$user->id, 3600, function () use ($user) {
+            $exists = AccountInterstitial::whereUserId($user->user_id)->where('is_spam', 1)->count();
+            if ($exists) {
+                return true;
+            }
+
+            return false;
+        });
+        if ($aiCheck) {
+            return redirect('/login');
+        }
+
+        return $this->buildProfile($request, $user);
+    }
+
+    protected function buildProfile(Request $request, $user)
+    {
+        $username = $user->username;
+        $loggedIn = Auth::check();
+        $isPrivate = false;
+        $isBlocked = false;
+        if (! $loggedIn) {
+            $key = 'profile:settings:'.$user->id;
+            $ttl = now()->addHours(6);
+            $settings = Cache::remember($key, $ttl, function () use ($user) {
+                return $user->user->settings;
+            });
+
+            if ($user->is_private == true) {
+                $profile = null;
+
+                return view('profile.private', compact('user'));
+            }
+
+            $owner = false;
+            $is_following = false;
+
+            $profile = $user;
+            $settings = [
+                'crawlable' => $settings->crawlable,
+                'following' => [
+                    'count' => $settings->show_profile_following_count,
+                    'list' => $settings->show_profile_following,
+                ],
+                'followers' => [
+                    'count' => $settings->show_profile_follower_count,
+                    'list' => $settings->show_profile_followers,
+                ],
+            ];
+
+            return view('profile.show', compact('profile', 'settings'));
+        } else {
+            $key = 'profile:settings:'.$user->id;
+            $ttl = now()->addHours(6);
+            $settings = Cache::remember($key, $ttl, function () use ($user) {
+                return $user->user->settings;
+            });
+
+            if ($user->is_private == true) {
+                $isPrivate = $this->privateProfileCheck($user, $loggedIn);
+            }
+
+            $isBlocked = $this->blockedProfileCheck($user);
+
+            $owner = $loggedIn && Auth::id() === $user->user_id;
+            $is_following = ($owner == false && Auth::check()) ? $user->followedBy(Auth::user()->profile) : false;
+
+            if ($isPrivate == true || $isBlocked == true) {
+                $requested = Auth::check() ? FollowRequest::whereFollowerId(Auth::user()->profile_id)
+                    ->whereFollowingId($user->id)
+                    ->exists() : false;
+
+                return view('profile.private', compact('user', 'is_following', 'requested'));
+            }
+
+            $is_admin = is_null($user->domain) ? $user->user->is_admin : false;
+            $profile = $user;
+            $settings = [
+                'crawlable' => $settings->crawlable,
+                'following' => [
+                    'count' => $settings->show_profile_following_count,
+                    'list' => $settings->show_profile_following,
+                ],
+                'followers' => [
+                    'count' => $settings->show_profile_follower_count,
+                    'list' => $settings->show_profile_followers,
+                ],
+            ];
+
+            return view('profile.show', compact('profile', 'settings'));
+        }
+    }
+
+    protected function getCachedUser($username, $withTrashed = false)
+    {
+        $val = str_replace(['_', '.', '-'], '', $username);
+        if (! ctype_alnum($val)) {
+            return;
+        }
+        $hash = ($withTrashed ? 'wt:' : 'wot:').strtolower($username);
+
+        return Cache::remember('pfc:cached-user:'.$hash, ($withTrashed ? 14400 : 900), function () use ($username, $withTrashed) {
+            if (! $withTrashed) {
+                return Profile::whereNull(['domain', 'status'])
+                    ->whereUsername($username)
+                    ->first();
+            } else {
+                return Profile::withTrashed()
+                    ->whereNull('domain')
+                    ->whereUsername($username)
+                    ->first();
+            }
+        });
+    }
+
+    public function permalinkRedirect(Request $request, $username)
+    {
+        if ($request->wantsJson() && (bool) config_cache('federation.activitypub.enabled')) {
+            $user = $this->getCachedUser($username, true);
+
+            return $this->showActivityPub($request, $user);
+        }
+
+        $user = $this->getCachedUser($username);
+
+        return redirect($user->url());
+    }
+
+    protected function privateProfileCheck(Profile $profile, $loggedIn)
+    {
+        if (! Auth::check()) {
+            return true;
+        }
+
+        $user = Auth::user()->profile;
+        if ($user->id == $profile->id || ! $profile->is_private) {
+            return false;
+        }
+
+        $follows = Follower::whereProfileId($user->id)->whereFollowingId($profile->id)->exists();
+        if ($follows == false) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public static function accountCheck(Profile $profile)
+    {
+        switch ($profile->status) {
+            case 'disabled':
+            case 'suspended':
+            case 'delete':
+                return view('profile.disabled');
+                break;
+
+            default:
+                break;
+        }
+
+        return abort(404);
+    }
+
+    protected function blockedProfileCheck(Profile $profile)
+    {
+        $pid = Auth::user()->profile->id;
+        $blocks = UserFilter::whereUserId($profile->id)
+            ->whereFilterType('block')
+            ->whereFilterableType('App\Profile')
+            ->pluck('filterable_id')
+            ->toArray();
+        if (in_array($pid, $blocks)) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function showActivityPub(Request $request, $user)
+    {
+        abort_if(! config_cache('federation.activitypub.enabled'), 404);
+        abort_if(! $user, 404, 'Not found');
+        abort_if($user->domain, 404);
+
+        return Cache::remember('pf:activitypub:user-object:by-id:'.$user->id, 1800, function () use ($user) {
+            $fractal = new Fractal\Manager();
+            $resource = new Fractal\Resource\Item($user, new ProfileTransformer);
+            $res = $fractal->createData($resource)->toArray();
+
+            return response(json_encode($res['data']))->header('Content-Type', 'application/activity+json');
+        });
+    }
+
+    public function showAtomFeed(Request $request, $user)
+    {
+        abort_if(! config('federation.atom.enabled'), 404);
+
+        $pid = AccountService::usernameToId($user);
+
+        abort_if(! $pid, 404);
+
+        $profile = AccountService::get($pid, true);
+
+        abort_if(! $profile || $profile['locked'] || ! $profile['local'], 404);
+
+        $aiCheck = Cache::remember('profile:ai-check:spam-login:'.$profile['id'], 86400, function () use ($profile) {
+            $uid = User::whereProfileId($profile['id'])->first();
+            if (! $uid) {
+                return true;
+            }
+            $exists = AccountInterstitial::whereUserId($uid->id)->where('is_spam', 1)->count();
+            if ($exists) {
+                return true;
+            }
+
+            return false;
+        });
+
+        abort_if($aiCheck, 404);
+
+        $enabled = Cache::remember('profile:atom:enabled:'.$profile['id'], 84600, function () use ($profile) {
+            $uid = User::whereProfileId($profile['id'])->first();
+            if (! $uid) {
+                return false;
+            }
+            $settings = UserSetting::whereUserId($uid->id)->first();
+            if (! $settings) {
+                return false;
+            }
+
+            return $settings->show_atom;
+        });
+
+        abort_if(! $enabled, 404);
+
+        $data = Cache::remember('pf:atom:user-feed:by-id:'.$profile['id'], 14400, function () use ($pid, $profile) {
+            $items = Status::whereProfileId($pid)
+                ->whereScope('public')
+                ->whereIn('type', ['photo', 'photo:album'])
+                ->orderByDesc('id')
+                ->take(10)
+                ->get()
+                ->map(function ($status) {
+                    return StatusService::get($status->id, true);
+                })
+                ->filter(function ($status) {
+                    return $status &&
+                        isset($status['account']) &&
+                        isset($status['media_attachments']) &&
+                        count($status['media_attachments']);
+                })
+                ->values();
+            $permalink = config('app.url')."/users/{$profile['username']}.atom";
+            $headers = ['Content-Type' => 'application/atom+xml'];
+
+            if ($items && $items->count()) {
+                $headers['Last-Modified'] = now()->parse($items->first()['created_at'])->toRfc7231String();
+            }
+
+            return compact('items', 'permalink', 'headers');
+        });
+        abort_if(! $data || ! isset($data['items']) || ! isset($data['permalink']), 404);
+
+        return response()
+            ->view('atom.user',
+                [
+                    'profile' => $profile,
+                    'items' => $data['items'],
+                    'permalink' => $data['permalink'],
+                ]
+            )
+            ->withHeaders($data['headers']);
+    }
+
+    public function meRedirect()
+    {
+        abort_if(! Auth::check(), 404);
+
+        return redirect(Auth::user()->url());
+    }
+
+    public function embed(Request $request, $username)
+    {
+        $res = view('profile.embed-removed');
+
+        if (! (bool) config_cache('instance.embed.profile')) {
+            return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
+        }
+
+        if (strlen($username) > 15 || strlen($username) < 2) {
+            return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
+        }
+
+        $profile = $this->getCachedUser($username);
+
+        if (! $profile || $profile->is_private) {
+            return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
+        }
+
+        $aiCheck = Cache::remember('profile:ai-check:spam-login:'.$profile->id, 86400, function () use ($profile) {
+            $exists = AccountInterstitial::whereUserId($profile->user_id)->where('is_spam', 1)->count();
+            if ($exists) {
+                return true;
+            }
+
+            return false;
+        });
+
+        if ($aiCheck) {
+            return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
+        }
+
+        if (AccountService::canEmbed($profile->user_id) == false) {
+            return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
+        }
+
+        $profile = AccountService::get($profile->id);
+        $res = view('profile.embed', compact('profile'));
+
+        return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
+    }
+
+    public function stories(Request $request, $username)
+    {
+        abort_if(! config_cache('instance.stories.enabled') || ! $request->user(), 404);
+        $profile = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
+        $pid = $profile->id;
+        $authed = Auth::user()->profile_id;
+        abort_if($pid != $authed && ! FollowerService::follows($authed, $pid), 404);
+        $exists = Story::whereProfileId($pid)
+            ->whereActive(true)
+            ->exists();
+        abort_unless($exists, 404);
+
+        return view('profile.story', compact('pid', 'profile'));
+    }
 }

+ 2 - 0
app/Http/Controllers/Settings/PrivacySettings.php

@@ -95,6 +95,8 @@ trait PrivacySettings
         Cache::forget('pf:acct:settings:hidden-following:' . $pid);
         Cache::forget('pf:acct-trans:hideFollowing:' . $pid);
         Cache::forget('pf:acct-trans:hideFollowers:' . $pid);
+        Cache::forget('pfc:cached-user:wt:' . strtolower($profile->username));
+        Cache::forget('pfc:cached-user:wot:' . strtolower($profile->username));
         return redirect(route('settings.privacy'))->with('status', 'Settings successfully updated!');
     }
 

+ 193 - 157
app/Http/Controllers/SiteController.php

@@ -2,166 +2,202 @@
 
 namespace App\Http\Controllers;
 
-use Illuminate\Http\Request;
-use Illuminate\Support\Str;
-use App, Auth, Cache, View;
-use App\Util\Lexer\PrettyNumber;
-use App\{Follower, Page, Profile, Status, User, UserFilter};
-use App\Util\Localization\Localization;
+use App\Page;
+use App\Profile;
 use App\Services\FollowerService;
+use App\Status;
+use App\User;
 use App\Util\ActivityPub\Helpers;
+use App\Util\Localization\Localization;
+use Auth;
+use Cache;
+use Illuminate\Http\Request;
+use Illuminate\Support\Str;
+use View;
 
 class SiteController extends Controller
 {
-	public function home(Request $request)
-	{
-		if (Auth::check()) {
-			return $this->homeTimeline($request);
-		} else {
-			return $this->homeGuest();
-		}
-	}
-
-	public function homeGuest()
-	{
-		return view('site.index');
-	}
-
-	public function homeTimeline(Request $request)
-	{
-		if($request->has('force_old_ui')) {
-			return view('timeline.home', ['layout' => 'feed']);
-		}
-
-		return redirect('/i/web');
-	}
-
-	public function changeLocale(Request $request, $locale)
-	{
-		// todo: add other locales after pushing new l10n strings
-		$locales = Localization::languages();
-		if(in_array($locale, $locales)) {
-			if($request->user()) {
-				$user = $request->user();
-				$user->language = $locale;
-				$user->save();
-			}
-		  session()->put('locale', $locale);
-		}
-
-		return redirect(route('site.language'));
-	}
-
-	public function about()
-	{
-		return Cache::remember('site.about_v2', now()->addMinutes(15), function() {
-			$user_count = number_format(User::count());
-			$post_count = number_format(Status::count());
-			$rules = config_cache('app.rules') ? json_decode(config_cache('app.rules'), true) : null;
-			return view('site.about', compact('rules', 'user_count', 'post_count'))->render();
-		});
-	}
-
-	public function language()
-	{
-	  return view('site.language');
-	}
-
-	public function communityGuidelines(Request $request)
-	{
-		return Cache::remember('site:help:community-guidelines', now()->addDays(120), function() {
-			$slug = '/site/kb/community-guidelines';
-			$page = Page::whereSlug($slug)->whereActive(true)->first();
-			return View::make('site.help.community-guidelines')->with(compact('page'))->render();
-		});
-	}
-
-	public function privacy(Request $request)
-	{
-		$page = Cache::remember('site:privacy', now()->addDays(120), function() {
-			$slug = '/site/privacy';
-			return Page::whereSlug($slug)->whereActive(true)->first();
-		});
-		return View::make('site.privacy')->with(compact('page'))->render();
-	}
-
-	public function terms(Request $request)
-	{
-		$page = Cache::remember('site:terms', now()->addDays(120), function() {
-			$slug = '/site/terms';
-			return Page::whereSlug($slug)->whereActive(true)->first();
-		});
-		return View::make('site.terms')->with(compact('page'))->render();
-	}
-
-	public function redirectUrl(Request $request)
-	{
-		abort_if(!$request->user(), 404);
-		$this->validate($request, [
-			'url' => 'required|url'
-		]);
-		$url = request()->input('url');
-		abort_if(Helpers::validateUrl($url) == false, 404);
-		return view('site.redirect', compact('url'));
-	}
-
-	public function followIntent(Request $request)
-	{
-		$this->validate($request, [
-			'user' => 'string|min:1|max:15|exists:users,username',
-		]);
-		$profile = Profile::whereUsername($request->input('user'))->firstOrFail();
-		$user = $request->user();
-		abort_if($user && $profile->id == $user->profile_id, 404);
-		$following = $user != null ? FollowerService::follows($user->profile_id, $profile->id) : false;
-		return view('site.intents.follow', compact('profile', 'user', 'following'));
-	}
-
-	public function legacyProfileRedirect(Request $request, $username)
-	{
-		$username = Str::contains($username, '@') ? '@' . $username : $username;
-		if(str_contains($username, '@')) {
-			$profile = Profile::whereUsername($username)
-				->firstOrFail();
-
-			if($profile->domain == null) {
-				$url = "/$profile->username";
-			} else {
-				$url = "/i/web/profile/_/{$profile->id}";
-			}
-
-		} else {
-			$profile = Profile::whereUsername($username)
-				->whereNull('domain')
-				->firstOrFail();
-			$url = "/$profile->username";
-		}
-
-		return redirect($url);
-	}
-
-	public function legacyWebfingerRedirect(Request $request, $username, $domain)
-	{
-		$un = '@'.$username.'@'.$domain;
-		$profile = Profile::whereUsername($un)
-			->firstOrFail();
-
-		if($profile->domain == null) {
-			$url = "/$profile->username";
-		} else {
-			$url = $request->user() ? "/i/web/profile/_/{$profile->id}" : $profile->url();
-		}
-
-		return redirect($url);
-	}
-
-	public function legalNotice(Request $request)
-	{
-		$page = Cache::remember('site:legal-notice', now()->addDays(120), function() {
-			$slug = '/site/legal-notice';
-			return Page::whereSlug($slug)->whereActive(true)->first();
-		});
-		abort_if(!$page, 404);
-		return View::make('site.legal-notice')->with(compact('page'))->render();
-	}
+    public function home(Request $request)
+    {
+        if (Auth::check()) {
+            return $this->homeTimeline($request);
+        } else {
+            return $this->homeGuest();
+        }
+    }
+
+    public function homeGuest()
+    {
+        return view('site.index');
+    }
+
+    public function homeTimeline(Request $request)
+    {
+        if ($request->has('force_old_ui')) {
+            return view('timeline.home', ['layout' => 'feed']);
+        }
+
+        return redirect('/i/web');
+    }
+
+    public function changeLocale(Request $request, $locale)
+    {
+        // todo: add other locales after pushing new l10n strings
+        $locales = Localization::languages();
+        if (in_array($locale, $locales)) {
+            if ($request->user()) {
+                $user = $request->user();
+                $user->language = $locale;
+                $user->save();
+            }
+            session()->put('locale', $locale);
+        }
+
+        return redirect(route('site.language'));
+    }
+
+    public function about()
+    {
+        return Cache::remember('site.about_v2', now()->addMinutes(15), function () {
+            $user_count = number_format(User::count());
+            $post_count = number_format(Status::count());
+            $rules = config_cache('app.rules') ? json_decode(config_cache('app.rules'), true) : null;
+
+            return view('site.about', compact('rules', 'user_count', 'post_count'))->render();
+        });
+    }
+
+    public function language()
+    {
+        return view('site.language');
+    }
+
+    public function communityGuidelines(Request $request)
+    {
+        return Cache::remember('site:help:community-guidelines', now()->addDays(120), function () {
+            $slug = '/site/kb/community-guidelines';
+            $page = Page::whereSlug($slug)->whereActive(true)->first();
+
+            return View::make('site.help.community-guidelines')->with(compact('page'))->render();
+        });
+    }
+
+    public function privacy(Request $request)
+    {
+        $page = Cache::remember('site:privacy', now()->addDays(120), function () {
+            $slug = '/site/privacy';
+
+            return Page::whereSlug($slug)->whereActive(true)->first();
+        });
+
+        return View::make('site.privacy')->with(compact('page'))->render();
+    }
+
+    public function terms(Request $request)
+    {
+        $page = Cache::remember('site:terms', now()->addDays(120), function () {
+            $slug = '/site/terms';
+
+            return Page::whereSlug($slug)->whereActive(true)->first();
+        });
+
+        return View::make('site.terms')->with(compact('page'))->render();
+    }
+
+    public function redirectUrl(Request $request)
+    {
+        abort_if(! $request->user(), 404);
+        $this->validate($request, [
+            'url' => 'required|url',
+        ]);
+        $url = request()->input('url');
+        abort_if(Helpers::validateUrl($url) == false, 404);
+
+        return view('site.redirect', compact('url'));
+    }
+
+    public function followIntent(Request $request)
+    {
+        $this->validate($request, [
+            'user' => 'string|min:1|max:15|exists:users,username',
+        ]);
+        $profile = Profile::whereUsername($request->input('user'))->firstOrFail();
+        $user = $request->user();
+        abort_if($user && $profile->id == $user->profile_id, 404);
+        $following = $user != null ? FollowerService::follows($user->profile_id, $profile->id) : false;
+
+        return view('site.intents.follow', compact('profile', 'user', 'following'));
+    }
+
+    public function legacyProfileRedirect(Request $request, $username)
+    {
+        $username = Str::contains($username, '@') ? '@'.$username : $username;
+        if (str_contains($username, '@')) {
+            $profile = Profile::whereUsername($username)
+                ->firstOrFail();
+
+            if ($profile->domain == null) {
+                $url = "/$profile->username";
+            } else {
+                $url = "/i/web/profile/_/{$profile->id}";
+            }
+
+        } else {
+            $profile = Profile::whereUsername($username)
+                ->whereNull('domain')
+                ->firstOrFail();
+            $url = "/$profile->username";
+        }
+
+        return redirect($url);
+    }
+
+    public function legacyWebfingerRedirect(Request $request, $username, $domain)
+    {
+        $un = '@'.$username.'@'.$domain;
+        $profile = Profile::whereUsername($un)
+            ->firstOrFail();
+
+        if ($profile->domain == null) {
+            $url = "/$profile->username";
+        } else {
+            $url = $request->user() ? "/i/web/profile/_/{$profile->id}" : $profile->url();
+        }
+
+        return redirect($url);
+    }
+
+    public function legalNotice(Request $request)
+    {
+        $page = Cache::remember('site:legal-notice', now()->addDays(120), function () {
+            $slug = '/site/legal-notice';
+
+            return Page::whereSlug($slug)->whereActive(true)->first();
+        });
+        abort_if(! $page, 404);
+
+        return View::make('site.legal-notice')->with(compact('page'))->render();
+    }
+
+    public function curatedOnboarding(Request $request)
+    {
+        if ($request->user()) {
+            return redirect('/i/web');
+        }
+
+        $regOpen = (bool) config_cache('pixelfed.open_registration');
+        $curOnboarding = (bool) config_cache('instance.curated_registration.enabled');
+        $curOnlyClosed = (bool) config('instance.curated_registration.state.only_enabled_on_closed_reg');
+        if ($regOpen) {
+            if ($curOnlyClosed) {
+                return redirect('/register');
+            }
+        } else {
+            if (! $curOnboarding) {
+                return redirect('/');
+            }
+        }
+
+        return view('auth.curated-register.index', ['step' => 1]);
+    }
 }

+ 450 - 442
app/Http/Controllers/StatusController.php

@@ -2,458 +2,466 @@
 
 namespace App\Http\Controllers;
 
-use App\Jobs\ImageOptimizePipeline\ImageOptimize;
-use App\Jobs\StatusPipeline\NewStatusPipeline;
-use App\Jobs\StatusPipeline\StatusDelete;
-use App\Jobs\StatusPipeline\RemoteStatusDelete;
+use App\AccountInterstitial;
 use App\Jobs\SharePipeline\SharePipeline;
 use App\Jobs\SharePipeline\UndoSharePipeline;
-use App\AccountInterstitial;
-use App\Media;
+use App\Jobs\StatusPipeline\RemoteStatusDelete;
+use App\Jobs\StatusPipeline\StatusDelete;
 use App\Profile;
+use App\Services\HashidService;
+use App\Services\ReblogService;
+use App\Services\StatusService;
 use App\Status;
-use App\StatusArchived;
 use App\StatusView;
-use App\Transformer\ActivityPub\StatusTransformer;
 use App\Transformer\ActivityPub\Verb\Note;
 use App\Transformer\ActivityPub\Verb\Question;
-use App\User;
-use Auth, DB, Cache;
+use App\Util\Media\License;
+use Auth;
+use Cache;
+use DB;
 use Illuminate\Http\Request;
 use League\Fractal;
-use App\Util\Media\Filter;
-use Illuminate\Support\Str;
-use App\Services\HashidService;
-use App\Services\StatusService;
-use App\Util\Media\License;
-use App\Services\ReblogService;
 
 class StatusController extends Controller
 {
-	public function show(Request $request, $username, $id)
-	{
-		// redirect authed users to Metro 2.0
-		if($request->user()) {
-			// unless they force static view
-			if(!$request->has('fs') || $request->input('fs') != '1') {
-				return redirect('/i/web/post/' . $id);
-			}
-		}
-
-		$user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
-
-		if($user->status != null) {
-			return ProfileController::accountCheck($user);
-		}
-
-		$status = Status::whereProfileId($user->id)
-				->whereNull('reblog_of_id')
-				->whereIn('scope', ['public','unlisted', 'private'])
-				->findOrFail($id);
-
-		if($status->uri || $status->url) {
-			$url = $status->uri ?? $status->url;
-			if(ends_with($url, '/activity')) {
-				$url = str_replace('/activity', '', $url);
-			}
-			return redirect($url);
-		}
-
-		if($status->visibility == 'private' || $user->is_private) {
-			if(!Auth::check()) {
-				abort(404);
-			}
-			$pid = Auth::user()->profile;
-			if($user->followedBy($pid) == false && $user->id !== $pid->id && Auth::user()->is_admin == false) {
-				abort(404);
-			}
-		}
-
-		if($status->type == 'archived') {
-			if(Auth::user()->profile_id !== $status->profile_id) {
-				abort(404);
-			}
-		}
-
-		if($request->user() && $request->user()->profile_id != $status->profile_id) {
-			StatusView::firstOrCreate([
-				'status_id' => $status->id,
-				'status_profile_id' => $status->profile_id,
-				'profile_id' => $request->user()->profile_id
-			]);
-		}
-
-		if ($request->wantsJson() && config_cache('federation.activitypub.enabled')) {
-			return $this->showActivityPub($request, $status);
-		}
-
-		$template = $status->in_reply_to_id ? 'status.reply' : 'status.show';
-		return view($template, compact('user', 'status'));
-	}
-
-	public function shortcodeRedirect(Request $request, $id)
-	{
-		abort(404);
-	}
-
-	public function showId(int $id)
-	{
-		abort(404);
-		$status = Status::whereNull('reblog_of_id')
-				->whereIn('scope', ['public', 'unlisted'])
-				->findOrFail($id);
-		return redirect($status->url());
-	}
-
-	public function showEmbed(Request $request, $username, int $id)
-	{
-		if(!config('instance.embed.post')) {
-			$res = view('status.embed-removed');
-			return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
-		}
-
-		$profile = Profile::whereNull(['domain','status'])
-			->whereIsPrivate(false)
-			->whereUsername($username)
-			->first();
-
-		if(!$profile) {
-			$content = view('status.embed-removed');
-			return response($content)->header('X-Frame-Options', 'ALLOWALL');
-		}
-
-		$aiCheck = Cache::remember('profile:ai-check:spam-login:' . $profile->id, 86400, function() use($profile) {
-			$exists = AccountInterstitial::whereUserId($profile->user_id)->where('is_spam', 1)->count();
-			if($exists) {
-				return true;
-			}
-
-			return false;
-		});
-
-		if($aiCheck) {
-			$res = view('status.embed-removed');
-			return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
-		}
-		$status = Status::whereProfileId($profile->id)
-			->whereNull('uri')
-			->whereScope('public')
-			->whereIsNsfw(false)
-			->whereIn('type', ['photo', 'video','photo:album'])
-			->find($id);
-		if(!$status) {
-			$content = view('status.embed-removed');
-			return response($content)->header('X-Frame-Options', 'ALLOWALL');
-		}
-		$showLikes = $request->filled('likes') && $request->likes == true;
-		$showCaption = $request->filled('caption') && $request->caption !== false;
-		$layout = $request->filled('layout') && $request->layout == 'compact' ? 'compact' : 'full';
-		$content = view('status.embed', compact('status', 'showLikes', 'showCaption', 'layout'));
-		return response($content)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
-	}
-
-	public function showObject(Request $request, $username, int $id)
-	{
-		$user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
-
-		if($user->status != null) {
-			return ProfileController::accountCheck($user);
-		}
-
-		$status = Status::whereProfileId($user->id)
-				->whereNotIn('visibility',['draft','direct'])
-				->findOrFail($id);
-
-		abort_if($status->uri, 404);
-
-		if($status->visibility == 'private' || $user->is_private) {
-			if(!Auth::check()) {
-				abort(403);
-			}
-			$pid = Auth::user()->profile;
-			if($user->followedBy($pid) == false && $user->id !== $pid->id) {
-				abort(403);
-			}
-		}
-
-		return $this->showActivityPub($request, $status);
-	}
-
-	public function compose()
-	{
-		$this->authCheck();
-
-		return view('status.compose');
-	}
-
-	public function store(Request $request)
-	{
-		return;
-	}
-
-	public function delete(Request $request)
-	{
-		$this->authCheck();
-
-		$this->validate($request, [
-		  'item'  => 'required|integer|min:1',
-		]);
-
-		$status = Status::findOrFail($request->input('item'));
-
-		$user = Auth::user();
-
-		if($status->profile_id != $user->profile->id &&
-			$user->is_admin == true &&
-			$status->uri == null
-		) {
-			$media = $status->media;
-
-			$ai = new AccountInterstitial;
-			$ai->user_id = $status->profile->user_id;
-			$ai->type = 'post.removed';
-			$ai->view = 'account.moderation.post.removed';
-			$ai->item_type = 'App\Status';
-			$ai->item_id = $status->id;
-			$ai->has_media = (bool) $media->count();
-			$ai->blurhash = $media->count() ? $media->first()->blurhash : null;
-			$ai->meta = json_encode([
-				'caption' => $status->caption,
-				'created_at' => $status->created_at,
-				'type' => $status->type,
-				'url' => $status->url(),
-				'is_nsfw' => $status->is_nsfw,
-				'scope' => $status->scope,
-				'reblog' => $status->reblog_of_id,
-				'likes_count' => $status->likes_count,
-				'reblogs_count' => $status->reblogs_count,
-			]);
-			$ai->save();
-
-			$u = $status->profile->user;
-			$u->has_interstitial = true;
-			$u->save();
-		}
-
-		if($status->in_reply_to_id) {
-			$parent = Status::find($status->in_reply_to_id);
-			if($parent && ($parent->profile_id == $user->profile_id) || ($status->profile_id == $user->profile_id) || $user->is_admin) {
-				Cache::forget('_api:statuses:recent_9:' . $status->profile_id);
-				Cache::forget('profile:status_count:' . $status->profile_id);
-				Cache::forget('profile:embed:' . $status->profile_id);
-				StatusService::del($status->id, true);
-				Cache::forget('profile:status_count:'.$status->profile_id);
-				$status->uri ? RemoteStatusDelete::dispatch($status) : StatusDelete::dispatch($status);
-			}
-		} else if ($status->profile_id == $user->profile_id || $user->is_admin == true) {
-			Cache::forget('_api:statuses:recent_9:' . $status->profile_id);
-			Cache::forget('profile:status_count:' . $status->profile_id);
-			Cache::forget('profile:embed:' . $status->profile_id);
-			StatusService::del($status->id, true);
-			Cache::forget('profile:status_count:'.$status->profile_id);
-			$status->uri ? RemoteStatusDelete::dispatch($status) : StatusDelete::dispatch($status);
-		}
-
-		if($request->wantsJson()) {
-			return response()->json(['Status successfully deleted.']);
-		} else {
-			return redirect($user->url());
-		}
-	}
-
-	public function storeShare(Request $request)
-	{
-		$this->authCheck();
-
-		$this->validate($request, [
-		  'item'    => 'required|integer|min:1',
-		]);
-
-		$user = Auth::user();
-		$profile = $user->profile;
-		$status = Status::whereScope('public')
-			->findOrFail($request->input('item'));
-
-		$count = $status->reblogs_count;
-
-		$exists = Status::whereProfileId(Auth::user()->profile->id)
-				  ->whereReblogOfId($status->id)
-				  ->exists();
-		if ($exists == true) {
-			$shares = Status::whereProfileId(Auth::user()->profile->id)
-				  ->whereReblogOfId($status->id)
-				  ->get();
-			foreach ($shares as $share) {
-				UndoSharePipeline::dispatch($share);
-				ReblogService::del($profile->id, $status->id);
-				$count--;
-			}
-		} else {
-			$share = new Status();
-			$share->profile_id = $profile->id;
-			$share->reblog_of_id = $status->id;
-			$share->in_reply_to_profile_id = $status->profile_id;
-			$share->type = 'share';
-			$share->save();
-			$count++;
-			SharePipeline::dispatch($share);
-			ReblogService::add($profile->id, $status->id);
-		}
-
-		Cache::forget('status:'.$status->id.':sharedby:userid:'.$user->id);
-		StatusService::del($status->id);
-
-		if ($request->ajax()) {
-			$response = ['code' => 200, 'msg' => 'Share saved', 'count' => $count];
-		} else {
-			$response = redirect($status->url());
-		}
-
-		return $response;
-	}
-
-	public function showActivityPub(Request $request, $status)
-	{
-		$object = $status->type == 'poll' ? new Question() : new Note();
-		$fractal = new Fractal\Manager();
-		$resource = new Fractal\Resource\Item($status, $object);
-		$res = $fractal->createData($resource)->toArray();
-
-		return response()->json($res['data'], 200, ['Content-Type' => 'application/activity+json'], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
-	}
-
-	public function edit(Request $request, $username, $id)
-	{
-		$this->authCheck();
-		$user = Auth::user()->profile;
-		$status = Status::whereProfileId($user->id)
-				->with(['media'])
-				->findOrFail($id);
-		$licenses = License::get();
-		return view('status.edit', compact('user', 'status', 'licenses'));
-	}
-
-	public function editStore(Request $request, $username, $id)
-	{
-		$this->authCheck();
-		$user = Auth::user()->profile;
-		$status = Status::whereProfileId($user->id)
-				->with(['media'])
-				->findOrFail($id);
-
-		$this->validate($request, [
-		  'license'      => 'nullable|integer|min:1|max:16',
-		]);
-
-		$licenseId = $request->input('license');
-
-		$status->media->each(function($media) use($licenseId) {
-			$media->license = $licenseId;
-			$media->save();
-			Cache::forget('status:transformer:media:attachments:'.$media->status_id);
-		});
-
-		return redirect($status->url());
-	}
-
-	protected function authCheck()
-	{
-		if (Auth::check() == false) {
-			abort(403);
-		}
-	}
-
-	protected function validateVisibility($visibility)
-	{
-		$allowed = ['public', 'unlisted', 'private'];
-		return in_array($visibility, $allowed) ? $visibility : 'public';
-	}
-
-	public static function mimeTypeCheck($mimes)
-	{
-		$allowed = explode(',', config_cache('pixelfed.media_types'));
-		$count = count($mimes);
-		$photos = 0;
-		$videos = 0;
-		foreach($mimes as $mime) {
-			if(in_array($mime, $allowed) == false && $mime !== 'video/mp4') {
-				continue;
-			}
-			if(str_contains($mime, 'image/')) {
-				$photos++;
-			}
-			if(str_contains($mime, 'video/')) {
-				$videos++;
-			}
-		}
-		if($photos == 1 && $videos == 0) {
-			return 'photo';
-		}
-		if($videos == 1 && $photos == 0) {
-			return 'video';
-		}
-		if($photos > 1 && $videos == 0) {
-			return 'photo:album';
-		}
-		if($videos > 1 && $photos == 0) {
-			return 'video:album';
-		}
-		if($photos >= 1 && $videos >= 1) {
-			return 'photo:video:album';
-		}
-
-		return 'text';
-	}
-
-	public function toggleVisibility(Request $request) {
-		$this->authCheck();
-		$this->validate($request, [
-			'item' => 'required|string|min:1|max:20',
-			'disableComments' => 'required|boolean'
-		]);
-
-		$user = Auth::user();
-		$id = $request->input('item');
-		$state = $request->input('disableComments');
-
-		$status = Status::findOrFail($id);
-
-		if($status->profile_id != $user->profile->id && $user->is_admin == false) {
-			abort(403);
-		}
-
-		$status->comments_disabled = $status->comments_disabled == true ? false : true;
-		$status->save();
-
-		return response()->json([200]);
-	}
-
-	public function storeView(Request $request)
-	{
-		abort_if(!$request->user(), 403);
-
-		$views = $request->input('_v');
-		$uid = $request->user()->profile_id;
-
-		if(empty($views) || !is_array($views)) {
-			return response()->json(0);
-		}
-
-		Cache::forget('profile:home-timeline-cursor:' . $request->user()->id);
-
-		foreach($views as $view) {
-			if(!isset($view['sid']) || !isset($view['pid'])) {
-				continue;
-			}
-			DB::transaction(function () use($view, $uid) {
-				StatusView::firstOrCreate([
-						'status_id' => $view['sid'],
-						'status_profile_id' => $view['pid'],
-						'profile_id' => $uid
-				]);
-			});
-		}
-
-		return response()->json(1);
-	}
+    public function show(Request $request, $username, $id)
+    {
+        // redirect authed users to Metro 2.0
+        if ($request->user()) {
+            // unless they force static view
+            if (! $request->has('fs') || $request->input('fs') != '1') {
+                return redirect('/i/web/post/'.$id);
+            }
+        }
+
+        $user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
+
+        if ($user->status != null) {
+            return ProfileController::accountCheck($user);
+        }
+
+        $status = Status::whereProfileId($user->id)
+            ->whereNull('reblog_of_id')
+            ->whereIn('scope', ['public', 'unlisted', 'private'])
+            ->findOrFail($id);
+
+        if ($status->uri || $status->url) {
+            $url = $status->uri ?? $status->url;
+            if (ends_with($url, '/activity')) {
+                $url = str_replace('/activity', '', $url);
+            }
+
+            return redirect($url);
+        }
+
+        if ($status->visibility == 'private' || $user->is_private) {
+            if (! Auth::check()) {
+                abort(404);
+            }
+            $pid = Auth::user()->profile;
+            if ($user->followedBy($pid) == false && $user->id !== $pid->id && Auth::user()->is_admin == false) {
+                abort(404);
+            }
+        }
+
+        if ($status->type == 'archived') {
+            if (Auth::user()->profile_id !== $status->profile_id) {
+                abort(404);
+            }
+        }
+
+        if ($request->user() && $request->user()->profile_id != $status->profile_id) {
+            StatusView::firstOrCreate([
+                'status_id' => $status->id,
+                'status_profile_id' => $status->profile_id,
+                'profile_id' => $request->user()->profile_id,
+            ]);
+        }
+
+        if ($request->wantsJson() && config_cache('federation.activitypub.enabled')) {
+            return $this->showActivityPub($request, $status);
+        }
+
+        $template = $status->in_reply_to_id ? 'status.reply' : 'status.show';
+
+        return view($template, compact('user', 'status'));
+    }
+
+    public function shortcodeRedirect(Request $request, $id)
+    {
+        $hid = HashidService::decode($id);
+        abort_if(! $hid, 404);
+
+        return redirect('/i/web/post/'.$hid);
+    }
+
+    public function showId(int $id)
+    {
+        abort(404);
+        $status = Status::whereNull('reblog_of_id')
+            ->whereIn('scope', ['public', 'unlisted'])
+            ->findOrFail($id);
+
+        return redirect($status->url());
+    }
+
+    public function showEmbed(Request $request, $username, int $id)
+    {
+        if (! (bool) config_cache('instance.embed.post')) {
+            $res = view('status.embed-removed');
+
+            return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
+        }
+
+        $profile = Profile::whereNull(['domain', 'status'])
+            ->whereIsPrivate(false)
+            ->whereUsername($username)
+            ->first();
+
+        if (! $profile) {
+            $content = view('status.embed-removed');
+
+            return response($content)->header('X-Frame-Options', 'ALLOWALL');
+        }
+
+        $aiCheck = Cache::remember('profile:ai-check:spam-login:'.$profile->id, 86400, function () use ($profile) {
+            $exists = AccountInterstitial::whereUserId($profile->user_id)->where('is_spam', 1)->count();
+            if ($exists) {
+                return true;
+            }
+
+            return false;
+        });
+
+        if ($aiCheck) {
+            $res = view('status.embed-removed');
+
+            return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
+        }
+        $status = Status::whereProfileId($profile->id)
+            ->whereNull('uri')
+            ->whereScope('public')
+            ->whereIsNsfw(false)
+            ->whereIn('type', ['photo', 'video', 'photo:album'])
+            ->find($id);
+        if (! $status) {
+            $content = view('status.embed-removed');
+
+            return response($content)->header('X-Frame-Options', 'ALLOWALL');
+        }
+        $showLikes = $request->filled('likes') && $request->likes == true;
+        $showCaption = $request->filled('caption') && $request->caption !== false;
+        $layout = $request->filled('layout') && $request->layout == 'compact' ? 'compact' : 'full';
+        $content = view('status.embed', compact('status', 'showLikes', 'showCaption', 'layout'));
+
+        return response($content)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
+    }
+
+    public function showObject(Request $request, $username, int $id)
+    {
+        $user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
+
+        if ($user->status != null) {
+            return ProfileController::accountCheck($user);
+        }
+
+        $status = Status::whereProfileId($user->id)
+            ->whereNotIn('visibility', ['draft', 'direct'])
+            ->findOrFail($id);
+
+        abort_if($status->uri, 404);
+
+        if ($status->visibility == 'private' || $user->is_private) {
+            if (! Auth::check()) {
+                abort(403);
+            }
+            $pid = Auth::user()->profile;
+            if ($user->followedBy($pid) == false && $user->id !== $pid->id) {
+                abort(403);
+            }
+        }
+
+        return $this->showActivityPub($request, $status);
+    }
+
+    public function compose()
+    {
+        $this->authCheck();
+
+        return view('status.compose');
+    }
+
+    public function store(Request $request)
+    {
+
+    }
+
+    public function delete(Request $request)
+    {
+        $this->authCheck();
+
+        $this->validate($request, [
+            'item' => 'required|integer|min:1',
+        ]);
+
+        $status = Status::findOrFail($request->input('item'));
+
+        $user = Auth::user();
+
+        if ($status->profile_id != $user->profile->id &&
+            $user->is_admin == true &&
+            $status->uri == null
+        ) {
+            $media = $status->media;
+
+            $ai = new AccountInterstitial;
+            $ai->user_id = $status->profile->user_id;
+            $ai->type = 'post.removed';
+            $ai->view = 'account.moderation.post.removed';
+            $ai->item_type = 'App\Status';
+            $ai->item_id = $status->id;
+            $ai->has_media = (bool) $media->count();
+            $ai->blurhash = $media->count() ? $media->first()->blurhash : null;
+            $ai->meta = json_encode([
+                'caption' => $status->caption,
+                'created_at' => $status->created_at,
+                'type' => $status->type,
+                'url' => $status->url(),
+                'is_nsfw' => $status->is_nsfw,
+                'scope' => $status->scope,
+                'reblog' => $status->reblog_of_id,
+                'likes_count' => $status->likes_count,
+                'reblogs_count' => $status->reblogs_count,
+            ]);
+            $ai->save();
+
+            $u = $status->profile->user;
+            $u->has_interstitial = true;
+            $u->save();
+        }
+
+        if ($status->in_reply_to_id) {
+            $parent = Status::find($status->in_reply_to_id);
+            if ($parent && ($parent->profile_id == $user->profile_id) || ($status->profile_id == $user->profile_id) || $user->is_admin) {
+                Cache::forget('_api:statuses:recent_9:'.$status->profile_id);
+                Cache::forget('profile:status_count:'.$status->profile_id);
+                Cache::forget('profile:embed:'.$status->profile_id);
+                StatusService::del($status->id, true);
+                Cache::forget('profile:status_count:'.$status->profile_id);
+                $status->uri ? RemoteStatusDelete::dispatch($status) : StatusDelete::dispatch($status);
+            }
+        } elseif ($status->profile_id == $user->profile_id || $user->is_admin == true) {
+            Cache::forget('_api:statuses:recent_9:'.$status->profile_id);
+            Cache::forget('profile:status_count:'.$status->profile_id);
+            Cache::forget('profile:embed:'.$status->profile_id);
+            StatusService::del($status->id, true);
+            Cache::forget('profile:status_count:'.$status->profile_id);
+            $status->uri ? RemoteStatusDelete::dispatch($status) : StatusDelete::dispatch($status);
+        }
+
+        if ($request->wantsJson()) {
+            return response()->json(['Status successfully deleted.']);
+        } else {
+            return redirect($user->url());
+        }
+    }
+
+    public function storeShare(Request $request)
+    {
+        $this->authCheck();
+
+        $this->validate($request, [
+            'item' => 'required|integer|min:1',
+        ]);
+
+        $user = Auth::user();
+        $profile = $user->profile;
+        $status = Status::whereScope('public')
+            ->findOrFail($request->input('item'));
+
+        $count = $status->reblogs_count;
+
+        $exists = Status::whereProfileId(Auth::user()->profile->id)
+            ->whereReblogOfId($status->id)
+            ->exists();
+        if ($exists == true) {
+            $shares = Status::whereProfileId(Auth::user()->profile->id)
+                ->whereReblogOfId($status->id)
+                ->get();
+            foreach ($shares as $share) {
+                UndoSharePipeline::dispatch($share);
+                ReblogService::del($profile->id, $status->id);
+                $count--;
+            }
+        } else {
+            $share = new Status();
+            $share->profile_id = $profile->id;
+            $share->reblog_of_id = $status->id;
+            $share->in_reply_to_profile_id = $status->profile_id;
+            $share->type = 'share';
+            $share->save();
+            $count++;
+            SharePipeline::dispatch($share);
+            ReblogService::add($profile->id, $status->id);
+        }
+
+        Cache::forget('status:'.$status->id.':sharedby:userid:'.$user->id);
+        StatusService::del($status->id);
+
+        if ($request->ajax()) {
+            $response = ['code' => 200, 'msg' => 'Share saved', 'count' => $count];
+        } else {
+            $response = redirect($status->url());
+        }
+
+        return $response;
+    }
+
+    public function showActivityPub(Request $request, $status)
+    {
+        $object = $status->type == 'poll' ? new Question() : new Note();
+        $fractal = new Fractal\Manager();
+        $resource = new Fractal\Resource\Item($status, $object);
+        $res = $fractal->createData($resource)->toArray();
+
+        return response()->json($res['data'], 200, ['Content-Type' => 'application/activity+json'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
+    }
+
+    public function edit(Request $request, $username, $id)
+    {
+        $this->authCheck();
+        $user = Auth::user()->profile;
+        $status = Status::whereProfileId($user->id)
+            ->with(['media'])
+            ->findOrFail($id);
+        $licenses = License::get();
+
+        return view('status.edit', compact('user', 'status', 'licenses'));
+    }
+
+    public function editStore(Request $request, $username, $id)
+    {
+        $this->authCheck();
+        $user = Auth::user()->profile;
+        $status = Status::whereProfileId($user->id)
+            ->with(['media'])
+            ->findOrFail($id);
+
+        $this->validate($request, [
+            'license' => 'nullable|integer|min:1|max:16',
+        ]);
+
+        $licenseId = $request->input('license');
+
+        $status->media->each(function ($media) use ($licenseId) {
+            $media->license = $licenseId;
+            $media->save();
+            Cache::forget('status:transformer:media:attachments:'.$media->status_id);
+        });
+
+        return redirect($status->url());
+    }
+
+    protected function authCheck()
+    {
+        if (Auth::check() == false) {
+            abort(403);
+        }
+    }
+
+    protected function validateVisibility($visibility)
+    {
+        $allowed = ['public', 'unlisted', 'private'];
+
+        return in_array($visibility, $allowed) ? $visibility : 'public';
+    }
+
+    public static function mimeTypeCheck($mimes)
+    {
+        $allowed = explode(',', config_cache('pixelfed.media_types'));
+        $count = count($mimes);
+        $photos = 0;
+        $videos = 0;
+        foreach ($mimes as $mime) {
+            if (in_array($mime, $allowed) == false && $mime !== 'video/mp4') {
+                continue;
+            }
+            if (str_contains($mime, 'image/')) {
+                $photos++;
+            }
+            if (str_contains($mime, 'video/')) {
+                $videos++;
+            }
+        }
+        if ($photos == 1 && $videos == 0) {
+            return 'photo';
+        }
+        if ($videos == 1 && $photos == 0) {
+            return 'video';
+        }
+        if ($photos > 1 && $videos == 0) {
+            return 'photo:album';
+        }
+        if ($videos > 1 && $photos == 0) {
+            return 'video:album';
+        }
+        if ($photos >= 1 && $videos >= 1) {
+            return 'photo:video:album';
+        }
+
+        return 'text';
+    }
+
+    public function toggleVisibility(Request $request)
+    {
+        $this->authCheck();
+        $this->validate($request, [
+            'item' => 'required|string|min:1|max:20',
+            'disableComments' => 'required|boolean',
+        ]);
+
+        $user = Auth::user();
+        $id = $request->input('item');
+        $state = $request->input('disableComments');
+
+        $status = Status::findOrFail($id);
+
+        if ($status->profile_id != $user->profile->id && $user->is_admin == false) {
+            abort(403);
+        }
+
+        $status->comments_disabled = $status->comments_disabled == true ? false : true;
+        $status->save();
+
+        return response()->json([200]);
+    }
+
+    public function storeView(Request $request)
+    {
+        abort_if(! $request->user(), 403);
+
+        $views = $request->input('_v');
+        $uid = $request->user()->profile_id;
+
+        if (empty($views) || ! is_array($views)) {
+            return response()->json(0);
+        }
+
+        Cache::forget('profile:home-timeline-cursor:'.$request->user()->id);
+
+        foreach ($views as $view) {
+            if (! isset($view['sid']) || ! isset($view['pid'])) {
+                continue;
+            }
+            DB::transaction(function () use ($view, $uid) {
+                StatusView::firstOrCreate([
+                    'status_id' => $view['sid'],
+                    'status_profile_id' => $view['pid'],
+                    'profile_id' => $uid,
+                ]);
+            });
+        }
+
+        return response()->json(1);
+    }
 }

+ 14 - 0
app/Services/ConfigCacheService.php

@@ -75,6 +75,20 @@ class ConfigCacheService
                 'instance.curated_registration.enabled',
 
                 'federation.migration',
+
+                'pixelfed.max_caption_length',
+                'pixelfed.max_bio_length',
+                'pixelfed.max_name_length',
+                'pixelfed.min_password_length',
+                'pixelfed.max_avatar_size',
+                'pixelfed.max_altext_length',
+                'pixelfed.allow_app_registration',
+                'pixelfed.app_registration_rate_limit_attempts',
+                'pixelfed.app_registration_rate_limit_decay',
+                'pixelfed.app_registration_confirm_rate_limit_attempts',
+                'pixelfed.app_registration_confirm_rate_limit_decay',
+                'instance.embed.profile',
+                'instance.embed.post',
                 // 'system.user_mode'
             ];
 

+ 34 - 50
app/Services/HashidService.php

@@ -2,54 +2,38 @@
 
 namespace App\Services;
 
-use Cache;
-
-class HashidService {
-
-	public const MIN_LIMIT = 15;
-	public const CMAP = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
-
-	public static function encode($id, $minLimit = true)
-	{
-		if(!is_numeric($id) || $id > PHP_INT_MAX) {
-			return null;
-		}
-
-		if($minLimit && strlen($id) < self::MIN_LIMIT) {
-			return null;
-		}
-
-		$key = "hashids:{$id}";
-		return Cache::remember($key, now()->hours(48), function() use($id) {
-			$cmap = self::CMAP;
-			$base = strlen($cmap);
-			$shortcode = '';
-			while($id) {
-				$id = ($id - ($r = $id % $base)) / $base;
-				$shortcode = $cmap[$r] . $shortcode;
-			}
-			return $shortcode;
-		});
-	}
-
-	public static function decode($short)
-	{
-		$len = strlen($short);
-		if($len < 3 || $len > 11) {
-			return null;
-		}
-		$id = 0;
-		foreach(str_split($short) as $needle) {
-			$pos = strpos(self::CMAP, $needle);
-			// if(!$pos) {
-			// 	return null;
-			// }
-			$id = ($id*64) + $pos;
-		}
-		if(strlen($id) < self::MIN_LIMIT) {
-			return null;
-		}
-		return $id;
-	}
-
+class HashidService
+{
+    public const CMAP = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
+
+    public static function encode($id, $minLimit = true)
+    {
+        if (! is_numeric($id) || $id > PHP_INT_MAX) {
+            return null;
+        }
+
+        $cmap = self::CMAP;
+        $base = strlen($cmap);
+        $shortcode = '';
+        while ($id) {
+            $id = ($id - ($r = $id % $base)) / $base;
+            $shortcode = $cmap[$r].$shortcode;
+        }
+
+        return $shortcode;
+    }
+
+    public static function decode($short = false)
+    {
+        if (! $short) {
+            return;
+        }
+        $id = 0;
+        foreach (str_split($short) as $needle) {
+            $pos = strpos(self::CMAP, $needle);
+            $id = ($id * 64) + $pos;
+        }
+
+        return $id;
+    }
 }

+ 86 - 87
app/Services/LandingService.php

@@ -2,105 +2,104 @@
 
 namespace App\Services;
 
-use App\Util\ActivityPub\Helpers;
-use Illuminate\Support\Str;
-use Illuminate\Support\Facades\Cache;
-use Illuminate\Support\Facades\Redis;
 use App\Status;
 use App\User;
-use App\Services\AccountService;
 use App\Util\Site\Nodeinfo;
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Str;
 
 class LandingService
 {
-	public static function get($json = true)
-	{
-		$activeMonth = Nodeinfo::activeUsersMonthly();
+    public static function get($json = true)
+    {
+        $activeMonth = Nodeinfo::activeUsersMonthly();
+
+        $totalUsers = Cache::remember('api:nodeinfo:users', 43200, function () {
+            return User::count();
+        });
+
+        $postCount = Cache::remember('api:nodeinfo:statuses', 21600, function () {
+            return Status::whereLocal(true)->count();
+        });
 
-		$totalUsers = Cache::remember('api:nodeinfo:users', 43200, function() {
-			return User::count();
-		});
+        $contactAccount = Cache::remember('api:v1:instance-data:contact', 604800, function () {
+            if (config_cache('instance.admin.pid')) {
+                return AccountService::getMastodon(config_cache('instance.admin.pid'), true);
+            }
+            $admin = User::whereIsAdmin(true)->first();
 
-		$postCount = Cache::remember('api:nodeinfo:statuses', 21600, function() {
-			return Status::whereLocal(true)->count();
-		});
+            return $admin && isset($admin->profile_id) ?
+                AccountService::getMastodon($admin->profile_id, true) :
+                null;
+        });
 
-		$contactAccount = Cache::remember('api:v1:instance-data:contact', 604800, function () {
-			if(config_cache('instance.admin.pid')) {
-				return AccountService::getMastodon(config_cache('instance.admin.pid'), true);
-			}
-			$admin = User::whereIsAdmin(true)->first();
-			return $admin && isset($admin->profile_id) ?
-				AccountService::getMastodon($admin->profile_id, true) :
-				null;
-		});
+        $rules = Cache::remember('api:v1:instance-data:rules', 604800, function () {
+            return config_cache('app.rules') ?
+                collect(json_decode(config_cache('app.rules'), true))
+                    ->map(function ($rule, $key) {
+                        $id = $key + 1;
 
-		$rules = Cache::remember('api:v1:instance-data:rules', 604800, function () {
-			return config_cache('app.rules') ?
-				collect(json_decode(config_cache('app.rules'), true))
-				->map(function($rule, $key) {
-					$id = $key + 1;
-					return [
-						'id' => "{$id}",
-						'text' => $rule
-					];
-				})
-				->toArray() : [];
-		});
+                        return [
+                            'id' => "{$id}",
+                            'text' => $rule,
+                        ];
+                    })
+                    ->toArray() : [];
+        });
 
-		$openReg = (bool) config_cache('pixelfed.open_registration');
+        $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' => (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'),
-				'short_description' => config_cache('app.short_description'),
-				'description' => config_cache('app.description'),
-			],
-			'stats' => [
-				'active_users' => (int) $activeMonth,
-				'posts_count' => (int) $postCount,
-				'total_users' => (int) $totalUsers
-			],
-			'contact' => [
-				'account' => $contactAccount,
-				'email' => config('instance.email')
-			],
-			'rules' => $rules,
-			'uploader' => [
-				'max_photo_size' => (int) (config('pixelfed.max_photo_size') * 1024),
-				'max_caption_length' => (int) config('pixelfed.max_caption_length'),
-				'max_altext_length' => (int) config('pixelfed.max_altext_length', 150),
-				'album_limit' => (int) config_cache('pixelfed.max_album_length'),
-				'image_quality' => (int) config_cache('pixelfed.image_quality'),
-				'max_collection_length' => (int) config('pixelfed.max_collection_length', 18),
-				'optimize_image' => (bool) config('pixelfed.optimize_image'),
-				'optimize_video' => (bool) config('pixelfed.optimize_video'),
-				'media_types' => config_cache('pixelfed.media_types'),
-			],
-			'features' => [
-				'federation' => config_cache('federation.activitypub.enabled'),
-				'timelines' => [
-					'local' => true,
-					'network' => (bool) config('federation.network_timeline'),
-				],
-				'mobile_apis' => (bool) config_cache('pixelfed.oauth_enabled'),
-				'stories' => (bool) config_cache('instance.stories.enabled'),
-				'video'	=> Str::contains(config_cache('pixelfed.media_types'), 'video/mp4'),
-			]
-		];
+        $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' => (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'),
+                'short_description' => config_cache('app.short_description'),
+                'description' => config_cache('app.description'),
+            ],
+            'stats' => [
+                'active_users' => (int) $activeMonth,
+                'posts_count' => (int) $postCount,
+                'total_users' => (int) $totalUsers,
+            ],
+            'contact' => [
+                'account' => $contactAccount,
+                'email' => config('instance.email'),
+            ],
+            'rules' => $rules,
+            'uploader' => [
+                'max_photo_size' => (int) (config_cache('pixelfed.max_photo_size') * 1024),
+                'max_caption_length' => (int) config_cache('pixelfed.max_caption_length'),
+                'max_altext_length' => (int) config_cache('pixelfed.max_altext_length', 150),
+                'album_limit' => (int) config_cache('pixelfed.max_album_length'),
+                'image_quality' => (int) config_cache('pixelfed.image_quality'),
+                'max_collection_length' => (int) config('pixelfed.max_collection_length', 18),
+                'optimize_image' => (bool) config_cache('pixelfed.optimize_image'),
+                'optimize_video' => (bool) config_cache('pixelfed.optimize_video'),
+                'media_types' => config_cache('pixelfed.media_types'),
+            ],
+            'features' => [
+                'federation' => config_cache('federation.activitypub.enabled'),
+                'timelines' => [
+                    'local' => true,
+                    'network' => (bool) config_cache('federation.network_timeline'),
+                ],
+                'mobile_apis' => (bool) config_cache('pixelfed.oauth_enabled'),
+                'stories' => (bool) config_cache('instance.stories.enabled'),
+                'video' => Str::contains(config_cache('pixelfed.media_types'), 'video/mp4'),
+            ],
+        ];
 
-		if($json) {
-			return json_encode($res);
-		}
+        if ($json) {
+            return json_encode($res);
+        }
 
-		return $res;
-	}
+        return $res;
+    }
 }

+ 64 - 51
app/Transformer/ActivityPub/ProfileTransformer.php

@@ -3,67 +3,80 @@
 namespace App\Transformer\ActivityPub;
 
 use App\Profile;
-use League\Fractal;
 use App\Services\AccountService;
+use League\Fractal;
 
 class ProfileTransformer extends Fractal\TransformerAbstract
 {
     public function transform(Profile $profile)
     {
         $res = [
-          '@context' => [
-            'https://w3id.org/security/v1',
-            'https://www.w3.org/ns/activitystreams',
-            [
-              'toot' => 'http://joinmastodon.org/ns#',
-              'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers',
-              'alsoKnownAs' => [
-                    '@id' => 'as:alsoKnownAs',
-                    '@type' => '@id'
-              ],
-              'movedTo' => [
-                    '@id' => 'as:movedTo',
-                    '@type' => '@id'
-              ],
-              'indexable' => 'toot:indexable',
+            '@context' => [
+                'https://w3id.org/security/v1',
+                'https://www.w3.org/ns/activitystreams',
+                [
+                    'toot' => 'http://joinmastodon.org/ns#',
+                    'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers',
+                    'alsoKnownAs' => [
+                        '@id' => 'as:alsoKnownAs',
+                        '@type' => '@id',
+                    ],
+                    'movedTo' => [
+                        '@id' => 'as:movedTo',
+                        '@type' => '@id',
+                    ],
+                    'indexable' => 'toot:indexable',
+                    'suspended' => 'toot:suspended',
+                ],
+            ],
+            'id' => $profile->permalink(),
+            'type' => 'Person',
+            'following' => $profile->permalink('/following'),
+            'followers' => $profile->permalink('/followers'),
+            'inbox' => $profile->permalink('/inbox'),
+            'outbox' => $profile->permalink('/outbox'),
+            'preferredUsername' => $profile->username,
+            'name' => $profile->name,
+            'summary' => $profile->bio,
+            'url' => $profile->url(),
+            'manuallyApprovesFollowers' => (bool) $profile->is_private,
+            'indexable' => (bool) $profile->indexable,
+            'published' => $profile->created_at->format('Y-m-d').'T00:00:00Z',
+            'publicKey' => [
+                'id' => $profile->permalink().'#main-key',
+                'owner' => $profile->permalink(),
+                'publicKeyPem' => $profile->public_key,
+            ],
+            'icon' => [
+                'type' => 'Image',
+                'mediaType' => 'image/jpeg',
+                'url' => $profile->avatarUrl(),
+            ],
+            'endpoints' => [
+                'sharedInbox' => config('app.url').'/f/inbox',
             ],
-          ],
-          'id'                        => $profile->permalink(),
-          'type'                      => 'Person',
-          'following'                 => $profile->permalink('/following'),
-          'followers'                 => $profile->permalink('/followers'),
-          'inbox'                     => $profile->permalink('/inbox'),
-          'outbox'                    => $profile->permalink('/outbox'),
-          'preferredUsername'         => $profile->username,
-          'name'                      => $profile->name,
-          'summary'                   => $profile->bio,
-          'url'                       => $profile->url(),
-          'manuallyApprovesFollowers' => (bool) $profile->is_private,
-          'indexable'                 => (bool) $profile->indexable,
-          'published'                 => $profile->created_at->format('Y-m-d') . 'T00:00:00Z',
-          'publicKey' => [
-            'id'           => $profile->permalink().'#main-key',
-            'owner'        => $profile->permalink(),
-            'publicKeyPem' => $profile->public_key,
-          ],
-          'icon' => [
-            'type'      => 'Image',
-            'mediaType' => 'image/jpeg',
-            'url'       => $profile->avatarUrl(),
-          ],
-          'endpoints' => [
-            'sharedInbox' => config('app.url') . '/f/inbox'
-          ]
-      ];
+        ];
 
-      if($profile->aliases->count()) {
-        $res['alsoKnownAs'] = $profile->aliases->map(fn($alias) => $alias->uri);
-      }
+        if ($profile->status === 'delete' || $profile->deleted_at != null) {
+            $res['suspended'] = true;
+            $res['name'] = '';
+            unset($res['icon']);
+            $res['summary'] = '';
+            $res['indexable'] = false;
+            $res['manuallyApprovesFollowers'] = false;
+        } else {
+            if ($profile->aliases->count()) {
+                $res['alsoKnownAs'] = $profile->aliases->map(fn ($alias) => $alias->uri);
+            }
 
-      if($profile->moved_to_profile_id) {
-        $res['movedTo'] = AccountService::get($profile->moved_to_profile_id)['url'];
-      }
+            if ($profile->moved_to_profile_id) {
+                $movedTo = AccountService::get($profile->moved_to_profile_id);
+                if ($movedTo && isset($movedTo['url'], $movedTo['id'])) {
+                    $res['movedTo'] = $movedTo['url'];
+                }
+            }
+        }
 
-      return $res;
+        return $res;
     }
 }

+ 24 - 0
app/Transformer/ActivityPub/Verb/DeleteActor.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace App\Transformer\ActivityPub\Verb;
+
+use App\Profile;
+use League\Fractal;
+
+class DeleteActor extends Fractal\TransformerAbstract
+{
+    public function transform(Profile $profile)
+    {
+        return [
+            '@context' => 'https://www.w3.org/ns/activitystreams',
+            'id' => $profile->permalink('#delete'),
+            'type' => 'Delete',
+            'actor' => $profile->permalink(),
+            'to' => [
+                'https://www.w3.org/ns/activitystreams#Public'
+            ],
+            'object' => $profile->permalink()
+        ];
+    }
+
+}

+ 29 - 20
app/Util/Site/Config.php

@@ -5,32 +5,34 @@ namespace App\Util\Site;
 use Cache;
 use Illuminate\Support\Str;
 
-class Config {
-
+class Config
+{
     const CACHE_KEY = 'api:site:configuration:_v0.8';
 
-    public static function get() {
-        return Cache::remember(self::CACHE_KEY, 900, function() {
+    public static function get()
+    {
+        return Cache::remember(self::CACHE_KEY, 900, function () {
             $hls = [
                 'enabled' => config('media.hls.enabled'),
             ];
-            if(config('media.hls.enabled')) {
+            if (config('media.hls.enabled')) {
                 $hls = [
                     'enabled' => true,
                     'debug' => (bool) config('media.hls.debug'),
                     'p2p' => (bool) config('media.hls.p2p'),
                     'p2p_debug' => (bool) config('media.hls.p2p_debug'),
                     'tracker' => config('media.hls.tracker'),
-                    'ice' => config('media.hls.ice')
+                    'ice' => config('media.hls.ice'),
                 ];
             }
+
             return [
                 'version' => config('pixelfed.version'),
                 'open_registration' => (bool) config_cache('pixelfed.open_registration'),
                 'uploader' => [
                     'max_photo_size' => (int) config('pixelfed.max_photo_size'),
-                    'max_caption_length' => (int) config('pixelfed.max_caption_length'),
-                    'max_altext_length' => (int) config('pixelfed.max_altext_length', 150),
+                    'max_caption_length' => (int) config_cache('pixelfed.max_caption_length'),
+                    'max_altext_length' => (int) config_cache('pixelfed.max_altext_length', 150),
                     'album_limit' => (int) config_cache('pixelfed.max_album_length'),
                     'image_quality' => (int) config_cache('pixelfed.image_quality'),
 
@@ -41,12 +43,12 @@ class Config {
 
                     'media_types' => config_cache('pixelfed.media_types'),
                     'mime_types' => config_cache('pixelfed.media_types') ? explode(',', config_cache('pixelfed.media_types')) : [],
-                    'enforce_account_limit' => (bool) config_cache('pixelfed.enforce_account_limit')
+                    'enforce_account_limit' => (bool) config_cache('pixelfed.enforce_account_limit'),
                 ],
 
                 'activitypub' => [
                     'enabled' => (bool) config_cache('federation.activitypub.enabled'),
-                    'remote_follow' => config('federation.activitypub.remoteFollow')
+                    'remote_follow' => config('federation.activitypub.remoteFollow'),
                 ],
 
                 'ab' => config('exp'),
@@ -54,8 +56,8 @@ class Config {
                 'site' => [
                     'name' => config_cache('app.name'),
                     'domain' => config('pixelfed.domain.app'),
-                    'url'    => config('app.url'),
-                    'description' => config_cache('app.short_description')
+                    'url' => config('app.url'),
+                    'description' => config_cache('app.short_description'),
                 ],
 
                 'account' => [
@@ -63,15 +65,15 @@ class Config {
                     'max_bio_length' => config('pixelfed.max_bio_length'),
                     'max_name_length' => config('pixelfed.max_name_length'),
                     'min_password_length' => config('pixelfed.min_password_length'),
-                    'max_account_size' => config('pixelfed.max_account_size')
+                    'max_account_size' => config('pixelfed.max_account_size'),
                 ],
 
                 'username' => [
                     'remote' => [
                         'formats' => config('instance.username.remote.formats'),
                         'format' => config('instance.username.remote.format'),
-                        'custom' => config('instance.username.remote.custom')
-                    ]
+                        'custom' => config('instance.username.remote.custom'),
+                    ],
                 ],
 
                 'features' => [
@@ -85,22 +87,29 @@ class Config {
                     'import' => [
                         'instagram' => (bool) config_cache('pixelfed.import.instagram.enabled'),
                         'mastodon' => false,
-                        'pixelfed' => false
+                        'pixelfed' => false,
                     ],
                     'label' => [
                         'covid' => [
                             'enabled' => (bool) config('instance.label.covid.enabled'),
                             'org' => config('instance.label.covid.org'),
                             'url' => config('instance.label.covid.url'),
-                        ]
+                        ],
                     ],
-                    'hls' => $hls
-                ]
+                    'hls' => $hls,
+                ],
             ];
         });
     }
 
-    public static function json() {
+    public static function refresh()
+    {
+        Cache::forget(self::CACHE_KEY);
+        return self::get();
+    }
+
+    public static function json()
+    {
         return json_encode(self::get(), JSON_FORCE_OBJECT);
     }
 }

+ 28 - 0
database/migrations/2024_03_08_122947_add_shared_inbox_attribute_to_instances_table.php

@@ -0,0 +1,28 @@
+<?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::table('instances', function (Blueprint $table) {
+            $table->string('shared_inbox')->nullable()->index();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::table('instances', function (Blueprint $table) {
+            $table->dropColumn('shared_inbox');
+        });
+    }
+};

+ 32 - 0
database/migrations/2024_03_08_123356_add_shared_inboxes_to_instances_table.php

@@ -0,0 +1,32 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+use App\Instance;
+use App\Profile;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        foreach(Instance::lazyById(50, 'id') as $instance) {
+            $si = Profile::whereDomain($instance->domain)->whereNotNull('sharedInbox')->first();
+            if($si && $si->sharedInbox) {
+                $instance->shared_inbox = $si->sharedInbox;
+                $instance->save();
+            }
+        }
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+
+    }
+};

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

@@ -25,7 +25,7 @@
             <hr class="border-dark">
             <p>From our Admins:</p>
             <div class="card card-body mb-1 bg-dark border border-secondary" style="border-style: dashed !important;">
-                <p class="lead mb-0" style="white-space: pre; opacity: 0.8">{{ $activity->message }}</p>
+                <p class="lead mb-0" style="white-space: pre-wrap; opacity: 0.8;">{{ $activity->message }}</p>
             </div>
             <p class="mb-3 small text-muted">If you don't understand this request, or need additional context you should request clarification from the admin team.</p>
             {{-- <hr class="border-dark"> --}}

+ 1 - 1
resources/views/site/help/sharing-media.blade.php

@@ -50,7 +50,7 @@
 		</a>
 		<div class="collapse" id="collapse3">
 			<div>
-				During the compose process, you will see the <span class="font-weight-bold">Caption</span> input. Captions are optional and limited to <span class="font-weight-bold">{{config('pixelfed.max_caption_length')}}</span> characters.
+				During the compose process, you will see the <span class="font-weight-bold">Caption</span> input. Captions are optional and limited to <span class="font-weight-bold">{{config_cache('pixelfed.max_caption_length')}}</span> characters.
 			</div>
 		</div>
 	</p>

+ 1 - 1
routes/web-api.php

@@ -115,7 +115,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
                 Route::post('discover/admin/features', 'DiscoverController@updateFeatures');
             });
 
-            Route::get('discover/accounts/popular', 'Api\ApiV1Controller@discoverAccountsPopular');
+            Route::get('discover/accounts/popular', 'DiscoverController@discoverAccountsPopular');
             Route::post('web/change-language.json', 'SpaController@updateLanguage');
         });
 

+ 1 - 1
routes/web.php

@@ -29,7 +29,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
     Route::get('auth/pci/{id}/{code}', 'ParentalControlsController@inviteRegister');
     Route::post('auth/pci/{id}/{code}', 'ParentalControlsController@inviteRegisterStore');
 
-    Route::get('auth/sign_up', 'CuratedRegisterController@index')->name('auth.curated-onboarding');
+    Route::get('auth/sign_up', 'SiteController@curatedOnboarding')->name('auth.curated-onboarding');
     Route::post('auth/sign_up', 'CuratedRegisterController@proceed');
     Route::get('auth/sign_up/concierge/response-sent', 'CuratedRegisterController@conciergeResponseSent');
     Route::get('auth/sign_up/concierge', 'CuratedRegisterController@concierge');