diff --git a/CHANGELOG.md b/CHANGELOG.md index 8626c7d4a..010f5ce8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ - Implement Admin Domain Blocks API (Mastodon API Compatible) [ThisIsMissEm](https://github.com/ThisIsMissEm) ([#5021](https://github.com/pixelfed/pixelfed/pull/5021)) - Authorize Interaction support (for handling remote interactions) ([4ca7c6c3](https://github.com/pixelfed/pixelfed/commit/4ca7c6c3)) +### Federation +- Add ActiveSharedInboxService, for efficient sharedInbox caching ([1a6a3397](https://github.com/pixelfed/pixelfed/commit/1a6a3397)) +- Add MovePipeline queue jobs ([9904d05f](https://github.com/pixelfed/pixelfed/commit/9904d05f)) +- Add ActivityPub Move validator ([909a6c72](https://github.com/pixelfed/pixelfed/commit/909a6c72)) +- Add delay to move handler to allow for remote cache invalidation ([8a362c12](https://github.com/pixelfed/pixelfed/commit/8a362c12)) + ### Updates - Update ApiV1Controller, add support for notification filter types ([f61159a1](https://github.com/pixelfed/pixelfed/commit/f61159a1)) - Update ApiV1Dot1Controller, fix mutual api ([a8bb97b2](https://github.com/pixelfed/pixelfed/commit/a8bb97b2)) @@ -17,6 +23,11 @@ - Update AdminSettings component, add link to Custom CSS settings ([958daac4](https://github.com/pixelfed/pixelfed/commit/958daac4)) - Update ApiV1Controller, fix v1/instance stats, force cast to int ([dcd95d68](https://github.com/pixelfed/pixelfed/commit/dcd95d68)) - Update BeagleService, disable discovery if AP is disabled ([6cd1cbb4](https://github.com/pixelfed/pixelfed/commit/6cd1cbb4)) +- Update NodeinfoService, fix typo ([edad436d](https://github.com/pixelfed/pixelfed/commit/edad436d)) +- Update ActivityPubFetchService, reduce cache ttl from 1 hour to 7.5 mins and add uncached fetchRequest method ([21da2b64](https://github.com/pixelfed/pixelfed/commit/21da2b64)) +- Update UserAccountDelete command, increase sharedInbox ttl from 12h to 14d ([be02f48a](https://github.com/pixelfed/pixelfed/commit/be02f48a)) +- Update HttpSignature, add signRaw method and improve error checking ([d4cf9181](https://github.com/pixelfed/pixelfed/commit/d4cf9181)) +- Update AP helpers, add forceBanCheck param to validateUrl method ([42424028](https://github.com/pixelfed/pixelfed/commit/42424028)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.12.3 (2024-07-01)](https://github.com/pixelfed/pixelfed/compare/v0.12.2...v0.12.3) diff --git a/app/Console/Commands/UserAccountDelete.php b/app/Console/Commands/UserAccountDelete.php index 68fad1e92..5032676c4 100644 --- a/app/Console/Commands/UserAccountDelete.php +++ b/app/Console/Commands/UserAccountDelete.php @@ -74,14 +74,14 @@ class UserAccountDelete extends Command $activity = $fractal->createData($resource)->toArray(); $audience = Instance::whereNotNull(['shared_inbox', 'nodeinfo_last_fetched']) - ->where('nodeinfo_last_fetched', '>', now()->subHours(12)) + ->where('nodeinfo_last_fetched', '>', now()->subDays(14)) ->distinct() ->pluck('shared_inbox'); $payload = json_encode($activity); $client = new Client([ - 'timeout' => 10, + 'timeout' => 5, ]); $version = config('pixelfed.version'); diff --git a/app/Http/Controllers/Admin/AdminDirectoryController.php b/app/Http/Controllers/Admin/AdminDirectoryController.php index ce53ea560..a5923894b 100644 --- a/app/Http/Controllers/Admin/AdminDirectoryController.php +++ b/app/Http/Controllers/Admin/AdminDirectoryController.php @@ -67,7 +67,9 @@ trait AdminDirectoryController $res['community_guidelines'] = config_cache('app.rules') ? json_decode(config_cache('app.rules'), true) : []; $res['curated_onboarding'] = (bool) config_cache('instance.curated_registration.enabled'); $res['open_registration'] = (bool) config_cache('pixelfed.open_registration'); - $res['oauth_enabled'] = (bool) config_cache('pixelfed.oauth_enabled') && file_exists(storage_path('oauth-public.key')) && file_exists(storage_path('oauth-private.key')); + $res['oauth_enabled'] = (bool) config_cache('pixelfed.oauth_enabled') && + (file_exists(storage_path('oauth-public.key')) || config_cache('passport.public_key')) && + (file_exists(storage_path('oauth-private.key')) || config_cache('passport.private_key')); $res['activitypub_enabled'] = (bool) config_cache('federation.activitypub.enabled'); diff --git a/app/Http/Controllers/Admin/AdminSettingsController.php b/app/Http/Controllers/Admin/AdminSettingsController.php index f1c2ca3ab..17ffd98fc 100644 --- a/app/Http/Controllers/Admin/AdminSettingsController.php +++ b/app/Http/Controllers/Admin/AdminSettingsController.php @@ -195,7 +195,9 @@ trait AdminSettingsController if ($key == 'mobile_apis' && $active && ! file_exists(storage_path('oauth-public.key')) && - ! file_exists(storage_path('oauth-private.key')) + ! config_cache('passport.public_key') && + ! file_exists(storage_path('oauth-private.key')) && + ! config_cache('passport.private_key') ) { Artisan::call('passport:keys'); Artisan::call('route:cache'); diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index 253e21f7d..10f8264ab 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -67,9 +67,7 @@ use App\Transformer\Api\Mastodon\v1\AccountTransformer; use App\Transformer\Api\Mastodon\v1\MediaTransformer; use App\Transformer\Api\Mastodon\v1\NotificationTransformer; use App\Transformer\Api\Mastodon\v1\StatusTransformer; -use App\Transformer\Api\{ - RelationshipTransformer, -}; +use App\Transformer\Api\RelationshipTransformer; use App\User; use App\UserFilter; use App\UserSetting; @@ -98,8 +96,8 @@ class ApiV1Controller extends Controller public function __construct() { - $this->fractal = new Fractal\Manager(); - $this->fractal->setSerializer(new ArraySerializer()); + $this->fractal = new Fractal\Manager; + $this->fractal->setSerializer(new ArraySerializer); } public function json($res, $code = 200, $headers = []) @@ -490,6 +488,7 @@ class ApiV1Controller extends Controller $account = AccountService::get($id); abort_if(! $account, 404); + abort_if(isset($account['moved'], $account['moved']['id']), 404, 'Account moved'); $pid = $request->user()->profile_id; $this->validate($request, [ 'limit' => 'sometimes|integer|min:1', @@ -591,6 +590,7 @@ class ApiV1Controller extends Controller $account = AccountService::get($id); abort_if(! $account, 404); + abort_if(isset($account['moved'], $account['moved']['id']), 404, 'Account moved'); $pid = $request->user()->profile_id; $this->validate($request, [ 'limit' => 'sometimes|integer|min:1', @@ -818,6 +818,8 @@ class ApiV1Controller extends Controller ->whereNull('status') ->findOrFail($id); + abort_if($target && $target->moved_to_profile_id, 400, 'Cannot follow an account that has moved!'); + if ($target && $target->domain) { $domain = $target->domain; abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); @@ -857,7 +859,7 @@ class ApiV1Controller extends Controller 'following_id' => $target->id, ]); if ($remote == true && config('federation.activitypub.remoteFollow') == true) { - (new FollowerController())->sendFollow($user->profile, $target); + (new FollowerController)->sendFollow($user->profile, $target); } } else { $follower = Follower::firstOrCreate([ @@ -866,7 +868,7 @@ class ApiV1Controller extends Controller ]); if ($remote == true && config('federation.activitypub.remoteFollow') == true) { - (new FollowerController())->sendFollow($user->profile, $target); + (new FollowerController)->sendFollow($user->profile, $target); } FollowPipeline::dispatch($follower)->onQueue('high'); } @@ -925,7 +927,7 @@ class ApiV1Controller extends Controller $followRequest->delete(); RelationshipService::refresh($target->id, $user->profile_id); } - $resource = new Fractal\Resource\Item($target, new RelationshipTransformer()); + $resource = new Fractal\Resource\Item($target, new RelationshipTransformer); $res = $this->fractal->createData($resource)->toArray(); return $this->json($res); @@ -938,7 +940,7 @@ class ApiV1Controller extends Controller UnfollowPipeline::dispatch($user->profile_id, $target->id)->onQueue('high'); if ($remote == true && config('federation.activitypub.remoteFollow') == true) { - (new FollowerController())->sendUndoFollow($user->profile, $target); + (new FollowerController)->sendUndoFollow($user->profile, $target); } RelationshipService::refresh($user->profile_id, $target->id); @@ -1132,6 +1134,8 @@ class ApiV1Controller extends Controller $profile = Profile::findOrFail($id); + abort_if($profile->moved_to_profile_id, 422, 'Cannot block an account that has migrated!'); + if ($profile->user && $profile->user->is_admin == true) { abort(400, 'You cannot block an admin'); } @@ -1198,7 +1202,7 @@ class ApiV1Controller extends Controller UserFilterService::block($pid, $id); RelationshipService::refresh($pid, $id); - $resource = new Fractal\Resource\Item($profile, new RelationshipTransformer()); + $resource = new Fractal\Resource\Item($profile, new RelationshipTransformer); $res = $this->fractal->createData($resource)->toArray(); return $this->json($res); @@ -1225,6 +1229,8 @@ class ApiV1Controller extends Controller $profile = Profile::findOrFail($id); + abort_if($profile->moved_to_profile_id, 422, 'Cannot unblock an account that has migrated!'); + $filter = UserFilter::whereUserId($pid) ->whereFilterableId($profile->id) ->whereFilterableType('App\Profile') @@ -1237,7 +1243,7 @@ class ApiV1Controller extends Controller } RelationshipService::refresh($pid, $id); - $resource = new Fractal\Resource\Item($profile, new RelationshipTransformer()); + $resource = new Fractal\Resource\Item($profile, new RelationshipTransformer); $res = $this->fractal->createData($resource)->toArray(); return $this->json($res); @@ -1372,6 +1378,8 @@ class ApiV1Controller extends Controller abort_unless($status, 404); + abort_if(isset($status['moved'], $status['moved']['id']), 422, 'Cannot like a post from an account that has migrated'); + if ($status && isset($status['account'], $status['account']['acct']) && strpos($status['account']['acct'], '@') != -1) { $domain = parse_url($status['account']['url'], PHP_URL_HOST); abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); @@ -1440,6 +1448,7 @@ class ApiV1Controller extends Controller $status = $napi ? StatusService::get($id, false) : StatusService::getMastodon($id, false); abort_unless($status && isset($status['account']), 404); + abort_if(isset($status['moved'], $status['moved']['id']), 422, 'Cannot unlike a post from an account that has migrated'); if ($status && isset($status['account'], $status['account']['acct']) && strpos($status['account']['acct'], '@') != -1) { $domain = parse_url($status['account']['url'], PHP_URL_HOST); @@ -1540,6 +1549,8 @@ class ApiV1Controller extends Controller $pid = $request->user()->profile_id; $target = AccountService::getMastodon($id); + abort_if(isset($target['moved'], $target['moved']['id']), 422, 'Cannot accept a request from an account that has migrated!'); + if (! $target) { return response()->json(['error' => 'Record not found'], 404); } @@ -1556,7 +1567,7 @@ class ApiV1Controller extends Controller } $follower = $followRequest->follower; - $follow = new Follower(); + $follow = new Follower; $follow->profile_id = $follower->id; $follow->following_id = $pid; $follow->save(); @@ -1603,6 +1614,8 @@ class ApiV1Controller extends Controller return response()->json(['error' => 'Record not found'], 404); } + abort_if(isset($target['moved'], $target['moved']['id']), 422, 'Cannot reject a request from an account that has migrated!'); + $followRequest = FollowRequest::whereFollowingId($pid)->whereFollowerId($id)->first(); if (! $followRequest) { @@ -1661,7 +1674,7 @@ class ApiV1Controller extends Controller null; }); - $stats = Cache::remember('api:v1:instance-data:stats', 43200, function () { + $stats = Cache::remember('api:v1:instance-data:stats:v0', 43200, function () { return [ 'user_count' => (int) User::count(), 'status_count' => (int) StatusService::totalLocalStatuses(), @@ -1857,7 +1870,7 @@ class ApiV1Controller extends Controller abort_if(MediaBlocklistService::exists($hash) == true, 451); - $media = new Media(); + $media = new Media; $media->status_id = null; $media->profile_id = $profile->id; $media->user_id = $user->id; @@ -1891,7 +1904,7 @@ class ApiV1Controller extends Controller $user->save(); Cache::forget($limitKey); - $resource = new Fractal\Resource\Item($media, new MediaTransformer()); + $resource = new Fractal\Resource\Item($media, new MediaTransformer); $res = $this->fractal->createData($resource)->toArray(); $res['preview_url'] = $media->url().'?v='.time(); $res['url'] = $media->url().'?v='.time(); @@ -1946,9 +1959,9 @@ class ApiV1Controller extends Controller ], 429); } - $fractal = new Fractal\Manager(); - $fractal->setSerializer(new ArraySerializer()); - $resource = new Fractal\Resource\Item($media, new MediaTransformer()); + $fractal = new Fractal\Manager; + $fractal->setSerializer(new ArraySerializer); + $resource = new Fractal\Resource\Item($media, new MediaTransformer); return $this->json($fractal->createData($resource)->toArray()); } @@ -1972,7 +1985,7 @@ class ApiV1Controller extends Controller ->whereNull('status_id') ->findOrFail($id); - $resource = new Fractal\Resource\Item($media, new MediaTransformer()); + $resource = new Fractal\Resource\Item($media, new MediaTransformer); $res = $this->fractal->createData($resource)->toArray(); return $this->json($res); @@ -2085,7 +2098,7 @@ class ApiV1Controller extends Controller } } - $media = new Media(); + $media = new Media; $media->status_id = null; $media->profile_id = $profile->id; $media->user_id = $user->id; @@ -2119,7 +2132,7 @@ class ApiV1Controller extends Controller $user->save(); Cache::forget($limitKey); - $resource = new Fractal\Resource\Item($media, new MediaTransformer()); + $resource = new Fractal\Resource\Item($media, new MediaTransformer); $res = $this->fractal->createData($resource)->toArray(); $res['preview_url'] = $media->url().'?v='.time(); $res['url'] = null; @@ -2204,6 +2217,8 @@ class ApiV1Controller extends Controller $account = Profile::findOrFail($id); + abort_if($account->moved_to_profile_id, 422, 'Cannot mute an account that has migrated!'); + if ($account && $account->domain) { $domain = $account->domain; abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); @@ -2237,7 +2252,7 @@ class ApiV1Controller extends Controller RelationshipService::refresh($pid, $id); - $resource = new Fractal\Resource\Item($account, new RelationshipTransformer()); + $resource = new Fractal\Resource\Item($account, new RelationshipTransformer); $res = $this->fractal->createData($resource)->toArray(); return $this->json($res); @@ -2263,6 +2278,8 @@ class ApiV1Controller extends Controller $profile = Profile::findOrFail($id); + abort_if($profile->moved_to_profile_id, 422, 'Cannot unmute an account that has migrated!'); + $filter = UserFilter::whereUserId($pid) ->whereFilterableId($profile->id) ->whereFilterableType('App\Profile') @@ -2276,7 +2293,7 @@ class ApiV1Controller extends Controller RelationshipService::refresh($pid, $id); - $resource = new Fractal\Resource\Item($profile, new RelationshipTransformer()); + $resource = new Fractal\Resource\Item($profile, new RelationshipTransformer); $res = $this->fractal->createData($resource)->toArray(); return $this->json($res); @@ -3209,6 +3226,7 @@ class ApiV1Controller extends Controller $status = Status::findOrFail($id); $account = AccountService::get($status->profile_id, true); abort_if(! $account, 404); + abort_if(isset($account['moved'], $account['moved']['id']), 404, 'Account moved'); if ($account && strpos($account['acct'], '@') != -1) { $domain = parse_url($account['url'], PHP_URL_HOST); abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); @@ -3308,11 +3326,12 @@ class ApiV1Controller extends Controller $pid = $user->profile_id; $status = Status::findOrFail($id); $account = AccountService::get($status->profile_id, true); + abort_if(! $account, 404); + abort_if(isset($account['moved'], $account['moved']['id']), 404, 'Account moved'); if ($account && strpos($account['acct'], '@') != -1) { $domain = parse_url($account['url'], PHP_URL_HOST); abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); } - abort_if(! $account, 404); $author = intval($status->profile_id) === intval($pid) || $user->is_admin; $napi = $request->has(self::PF_API_ENTITY_KEY); @@ -3617,7 +3636,7 @@ class ApiV1Controller extends Controller $status = Status::whereProfileId($request->user()->profile->id) ->findOrFail($id); - $resource = new Fractal\Resource\Item($status, new StatusTransformer()); + $resource = new Fractal\Resource\Item($status, new StatusTransformer); Cache::forget('profile:status_count:'.$status->profile_id); StatusDelete::dispatch($status); @@ -3644,6 +3663,8 @@ class ApiV1Controller extends Controller abort_if($user->has_roles && ! UserRoleService::can('can-share', $user->id), 403, 'Invalid permissions for this action'); AccountService::setLastActive($user->id); $status = Status::whereScope('public')->findOrFail($id); + $account = AccountService::get($status->profile_id); + abort_if(isset($account['moved'], $account['moved']['id']), 422, 'Cannot share a post from an account that has migrated'); if ($status && ($status->uri || $status->url || $status->object_url)) { $url = $status->uri ?? $status->url ?? $status->object_url; $domain = parse_url($url, PHP_URL_HOST); @@ -3696,6 +3717,8 @@ class ApiV1Controller extends Controller abort_if($user->has_roles && ! UserRoleService::can('can-share', $user->id), 403, 'Invalid permissions for this action'); AccountService::setLastActive($user->id); $status = Status::whereScope('public')->findOrFail($id); + $account = AccountService::get($status->profile_id); + abort_if(isset($account['moved'], $account['moved']['id']), 422, 'Cannot unshare a post from an account that has migrated'); if (intval($status->profile_id) !== intval($user->profile_id)) { if ($status->scope == 'private') { @@ -3929,7 +3952,8 @@ class ApiV1Controller extends Controller $status = Status::findOrFail($id); $pid = $request->user()->profile_id; - + $account = AccountService::get($status->profile_id); + abort_if(isset($account['moved'], $account['moved']['id']), 422, 'Cannot bookmark a post from an account that has migrated'); abort_if($user->has_roles && ! UserRoleService::can('can-bookmark', $user->id), 403, 'Invalid permissions for this action'); abort_if($status->in_reply_to_id || $status->reblog_of_id, 404); abort_if(! in_array($status->scope, ['public', 'unlisted', 'private']), 404); @@ -4045,8 +4069,8 @@ class ApiV1Controller extends Controller } $pid = $request->user()->profile_id; $status = StatusService::getMastodon($id, false); - abort_if(! $status, 404); + abort_if(isset($status['account'], $account['account']['moved']['id']), 404, 'Account moved'); if ($status['visibility'] == 'private') { if ($pid != $status['account']['id']) { @@ -4135,11 +4159,11 @@ class ApiV1Controller extends Controller { abort_if(! $request->user(), 403); - $status = Status::findOrFail($id); - $pid = $request->user()->profile_id; - abort_if(! in_array($status->scope, ['public', 'unlisted', 'private']), 404); + $status = StatusService::get($id, false, true); + abort_if(! $status, 404); + abort_if(! in_array($status['visibility'], ['public', 'unlisted', 'private']), 404); - return $this->json(StatusService::getState($status->id, $pid)); + return $this->json(StatusService::getState($status['id'], $request->user()->profile_id)); } /** diff --git a/app/Http/Controllers/BookmarkController.php b/app/Http/Controllers/BookmarkController.php index d1d793dd2..14b1d9e3f 100644 --- a/app/Http/Controllers/BookmarkController.php +++ b/app/Http/Controllers/BookmarkController.php @@ -3,12 +3,12 @@ namespace App\Http\Controllers; use App\Bookmark; -use App\Status; -use Auth; -use Illuminate\Http\Request; +use App\Services\AccountService; use App\Services\BookmarkService; use App\Services\FollowerService; use App\Services\UserRoleService; +use App\Status; +use Illuminate\Http\Request; class BookmarkController extends Controller { @@ -25,15 +25,16 @@ class BookmarkController extends Controller $user = $request->user(); $status = Status::findOrFail($request->input('item')); - - abort_if($user->has_roles && !UserRoleService::can('can-bookmark', $user->id), 403, 'Invalid permissions for this action'); + $account = AccountService::get($status->profile_id); + abort_if(isset($account['moved'], $account['moved']['id']), 422, 'Cannot bookmark or unbookmark a post from an account that has migrated'); + abort_if($user->has_roles && ! UserRoleService::can('can-bookmark', $user->id), 403, 'Invalid permissions for this action'); abort_if($status->in_reply_to_id || $status->reblog_of_id, 404); - abort_if(!in_array($status->scope, ['public', 'unlisted', 'private']), 404); - abort_if(!in_array($status->type, ['photo','photo:album', 'video', 'video:album', 'photo:video:album']), 404); + abort_if(! in_array($status->scope, ['public', 'unlisted', 'private']), 404); + abort_if(! in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']), 404); - if($status->scope == 'private') { - if($user->profile_id !== $status->profile_id && !FollowerService::follows($user->profile_id, $status->profile_id)) { - if($exists = Bookmark::whereStatusId($status->id)->whereProfileId($user->profile_id)->first()) { + if ($status->scope == 'private') { + if ($user->profile_id !== $status->profile_id && ! FollowerService::follows($user->profile_id, $status->profile_id)) { + if ($exists = Bookmark::whereStatusId($status->id)->whereProfileId($user->profile_id)->first()) { BookmarkService::del($user->profile_id, $status->id); $exists->delete(); @@ -51,7 +52,7 @@ class BookmarkController extends Controller ['status_id' => $status->id], ['profile_id' => $user->profile_id] ); - if (!$bookmark->wasRecentlyCreated) { + if (! $bookmark->wasRecentlyCreated) { BookmarkService::del($user->profile_id, $status->id); $bookmark->delete(); } else { diff --git a/app/Http/Controllers/PixelfedDirectoryController.php b/app/Http/Controllers/PixelfedDirectoryController.php index 0477c5170..d6a014d07 100644 --- a/app/Http/Controllers/PixelfedDirectoryController.php +++ b/app/Http/Controllers/PixelfedDirectoryController.php @@ -90,7 +90,8 @@ class PixelfedDirectoryController extends Controller $oauthEnabled = ConfigCache::whereK('pixelfed.oauth_enabled')->first(); if ($oauthEnabled) { - $keys = file_exists(storage_path('oauth-public.key')) && file_exists(storage_path('oauth-private.key')); + $keys = (file_exists(storage_path('oauth-public.key')) || config_cache('passport.public_key')) && + (file_exists(storage_path('oauth-private.key')) || config_cache('passport.private_key')); $res['oauth_enabled'] = (bool) $oauthEnabled && $keys; } diff --git a/app/Http/Controllers/StatusController.php b/app/Http/Controllers/StatusController.php index ba02cd015..14a5e5b7d 100644 --- a/app/Http/Controllers/StatusController.php +++ b/app/Http/Controllers/StatusController.php @@ -121,7 +121,8 @@ class StatusController extends Controller ! $status || ! isset($status['account'], $status['account']['id'], $status['local']) || ! $status['local'] || - strtolower($status['account']['username']) !== strtolower($username) + strtolower($status['account']['username']) !== strtolower($username) || + isset($status['account']['moved'], $status['account']['moved']['id']) ) { $content = view('status.embed-removed'); @@ -220,10 +221,7 @@ class StatusController extends Controller return view('status.compose'); } - public function store(Request $request) - { - - } + public function store(Request $request) {} public function delete(Request $request) { @@ -307,6 +305,8 @@ class StatusController extends Controller $profile = $user->profile; $status = Status::whereScope('public') ->findOrFail($request->input('item')); + $statusAccount = AccountService::get($status->profile_id); + abort_if(! $statusAccount || isset($statusAccount['moved'], $statusAccount['moved']['id']), 422, 'Account moved'); $count = $status->reblogs_count; @@ -323,7 +323,7 @@ class StatusController extends Controller $count--; } } else { - $share = new Status(); + $share = new Status; $share->profile_id = $profile->id; $share->reblog_of_id = $status->id; $share->in_reply_to_profile_id = $status->profile_id; @@ -352,8 +352,8 @@ class StatusController extends Controller return Cache::remember($key, 3600, function () use ($status) { $status = Status::findOrFail($status['id']); - $object = $status->type == 'poll' ? new Question() : new Note(); - $fractal = new Fractal\Manager(); + $object = $status->type == 'poll' ? new Question : new Note; + $fractal = new Fractal\Manager; $resource = new Fractal\Resource\Item($status, $object); $res = $fractal->createData($resource)->toArray(); diff --git a/app/Jobs/MovePipeline/CleanupLegacyAccountMovePipeline.php b/app/Jobs/MovePipeline/CleanupLegacyAccountMovePipeline.php new file mode 100644 index 000000000..d26ad5624 --- /dev/null +++ b/app/Jobs/MovePipeline/CleanupLegacyAccountMovePipeline.php @@ -0,0 +1,103 @@ +target = $target; + $this->activity = $activity; + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware(): array + { + return [ + new WithoutOverlapping('process-move-cleanup-legacy-followers:'.$this->target), + (new ThrottlesExceptions(2, 5 * 60))->backoff(5), + ]; + } + + /** + * Determine the time at which the job should timeout. + */ + public function retryUntil(): DateTime + { + return now()->addMinutes(5); + } + + /** + * Execute the job. + */ + public function handle(): void + { + if (config('app.env') !== 'production' || (bool) config_cache('federation.activitypub.enabled') == false) { + throw new Exception('Activitypub not enabled'); + } + + $target = $this->target; + $actor = $this->activity; + + $targetAccount = Helpers::profileFetch($target); + $actorAccount = Helpers::profileFetch($actor); + + if (! $targetAccount || ! $actorAccount) { + throw new Exception('Invalid move accounts'); + } + + UserFilter::where('filterable_type', 'App\Profile') + ->where('filterable_id', $actorAccount['id']) + ->update(['filterable_id' => $targetAccount['id']]); + + Follower::whereFollowingId($actorAccount['id'])->delete(); + + $oldProfile = Profile::find($actorAccount['id']); + + if ($oldProfile) { + $oldProfile->moved_to_profile_id = $targetAccount['id']; + $oldProfile->save(); + AccountService::del($oldProfile->id); + AccountService::del($targetAccount['id']); + } + } +} diff --git a/app/Jobs/MovePipeline/MoveMigrateFollowersPipeline.php b/app/Jobs/MovePipeline/MoveMigrateFollowersPipeline.php new file mode 100644 index 000000000..1cde3818e --- /dev/null +++ b/app/Jobs/MovePipeline/MoveMigrateFollowersPipeline.php @@ -0,0 +1,131 @@ +target = $target; + $this->activity = $activity; + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware(): array + { + return [ + new WithoutOverlapping('process-move-migrate-followers:'.$this->target), + (new ThrottlesExceptionsWithRedis(5, 2 * 60))->backoff(1), + ]; + } + + /** + * Determine the time at which the job should timeout. + */ + public function retryUntil(): DateTime + { + return now()->addMinutes(15); + } + + /** + * Execute the job. + */ + public function handle(): void + { + if (config('app.env') !== 'production' || (bool) config_cache('federation.activitypub.enabled') == false) { + throw new Exception('Activitypub not enabled'); + } + + $target = $this->target; + $actor = $this->activity; + + $targetAccount = Helpers::profileFetch($target); + $actorAccount = Helpers::profileFetch($actor); + + if (! $targetAccount || ! $actorAccount) { + throw new Exception('Invalid move accounts'); + } + + $activity = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Follow', + 'actor' => null, + 'object' => $target, + ]; + + $version = config('pixelfed.version'); + $appUrl = config('app.url'); + $userAgent = "(Pixelfed/{$version}; +{$appUrl})"; + $addlHeaders = [ + 'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + 'User-Agent' => $userAgent, + ]; + $targetInbox = $targetAccount['sharedInbox'] ?? $targetAccount['inbox_url']; + $targetPid = $targetAccount['id']; + + DB::table('followers') + ->join('profiles', 'followers.profile_id', '=', 'profiles.id') + ->where('followers.following_id', $actorAccount['id']) + ->whereNotNull('profiles.user_id') + ->whereNull('profiles.deleted_at') + ->select('profiles.id', 'profiles.user_id', 'profiles.username', 'profiles.private_key', 'profiles.status') + ->chunkById(100, function ($followers) use ($targetInbox, $targetPid, $target) { + foreach ($followers as $follower) { + if (! $follower->private_key || ! $follower->username || ! $follower->user_id || $follower->status === 'delete') { + continue; + } + + Follower::updateOrCreate([ + 'profile_id' => $follower->id, + 'following_id' => $targetPid, + ]); + + MoveSendFollowPipeline::dispatch($follower, $targetInbox, $targetPid, $target)->onQueue('follow'); + } + }, 'id'); + } +} diff --git a/app/Jobs/MovePipeline/MoveSendFollowPipeline.php b/app/Jobs/MovePipeline/MoveSendFollowPipeline.php new file mode 100644 index 000000000..6d1cef5e1 --- /dev/null +++ b/app/Jobs/MovePipeline/MoveSendFollowPipeline.php @@ -0,0 +1,113 @@ + + */ + public function middleware(): array + { + return [ + new WithoutOverlapping('move-send-follow:'.$this->follower->id.':target:'.$this->target), + (new ThrottlesExceptions(2, 5 * 60))->backoff(5), + ]; + } + + /** + * Create a new job instance. + */ + public function __construct($follower, $targetInbox, $targetPid, $target) + { + $this->follower = $follower; + $this->targetInbox = $targetInbox; + $this->targetPid = $targetPid; + $this->target = $target; + } + + /** + * Execute the job. + */ + public function handle(): void + { + $follower = $this->follower; + $targetPid = $this->targetPid; + $targetInbox = $this->targetInbox; + $target = $this->target; + + if (! $follower->username || ! $follower->private_key) { + return; + } + + $permalink = 'https://'.config('pixelfed.domain.app').'/users/'.$follower->username; + $version = config('pixelfed.version'); + $appUrl = config('app.url'); + $userAgent = "(Pixelfed/{$version}; +{$appUrl})"; + $addlHeaders = [ + 'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + 'User-Agent' => $userAgent, + ]; + + $activity = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Follow', + 'actor' => $permalink, + 'object' => $target, + ]; + + $keyId = $permalink.'#main-key'; + $payload = json_encode($activity); + $headers = HttpSignature::signRaw($follower->private_key, $keyId, $targetInbox, $activity, $addlHeaders); + + $client = new Client([ + 'timeout' => config('federation.activitypub.delivery.timeout'), + ]); + + try { + $client->post($targetInbox, [ + 'curl' => [ + CURLOPT_HTTPHEADER => $headers, + CURLOPT_POSTFIELDS => $payload, + CURLOPT_HEADER => true, + ], + ]); + } catch (ClientException $e) { + + } + } +} diff --git a/app/Jobs/MovePipeline/MoveSendUndoFollowPipeline.php b/app/Jobs/MovePipeline/MoveSendUndoFollowPipeline.php new file mode 100644 index 000000000..952e8c1fa --- /dev/null +++ b/app/Jobs/MovePipeline/MoveSendUndoFollowPipeline.php @@ -0,0 +1,119 @@ + + */ + public function middleware(): array + { + return [ + new WithoutOverlapping('move-send-unfollow:'.$this->follower->id.':actor:'.$this->actor), + (new ThrottlesExceptions(2, 5 * 60))->backoff(5), + ]; + } + + /** + * Create a new job instance. + */ + public function __construct($follower, $targetInbox, $targetPid, $actor) + { + $this->follower = $follower; + $this->targetInbox = $targetInbox; + $this->targetPid = $targetPid; + $this->actor = $actor; + } + + /** + * Execute the job. + */ + public function handle(): void + { + $follower = $this->follower; + $targetPid = $this->targetPid; + $targetInbox = $this->targetInbox; + $actor = $this->actor; + + if (! $follower->username || ! $follower->private_key) { + return; + } + + $permalink = 'https://'.config('pixelfed.domain.app').'/users/'.$follower->username; + $version = config('pixelfed.version'); + $appUrl = config('app.url'); + $userAgent = "(Pixelfed/{$version}; +{$appUrl})"; + $addlHeaders = [ + 'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + 'User-Agent' => $userAgent, + ]; + + $activity = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Undo', + 'id' => $permalink.'#follow/'.$targetPid.'/undo', + 'actor' => $permalink, + 'object' => [ + 'type' => 'Follow', + 'id' => $permalink.'#follows/'.$targetPid, + 'object' => $actor, + 'actor' => $permalink, + ], + ]; + + $keyId = $permalink.'#main-key'; + $payload = json_encode($activity); + $headers = HttpSignature::signRaw($follower->private_key, $keyId, $targetInbox, $activity, $addlHeaders); + + $client = new Client([ + 'timeout' => config('federation.activitypub.delivery.timeout'), + ]); + + try { + $client->post($targetInbox, [ + 'curl' => [ + CURLOPT_HTTPHEADER => $headers, + CURLOPT_POSTFIELDS => $payload, + CURLOPT_HEADER => true, + ], + ]); + } catch (ClientException $e) { + + } + } +} diff --git a/app/Jobs/MovePipeline/ProcessMovePipeline.php b/app/Jobs/MovePipeline/ProcessMovePipeline.php new file mode 100644 index 000000000..1ff95f96c --- /dev/null +++ b/app/Jobs/MovePipeline/ProcessMovePipeline.php @@ -0,0 +1,156 @@ +target = $target; + $this->activity = $activity; + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware(): array + { + return [ + new WithoutOverlapping('process-move:'.$this->target), + (new ThrottlesExceptionsWithRedis(5, 2 * 60))->backoff(1), + ]; + } + + /** + * Determine the time at which the job should timeout. + */ + public function retryUntil(): DateTime + { + return now()->addMinutes(10); + } + + /** + * Execute the job. + */ + public function handle(): void + { + if (config('app.env') !== 'production' || (bool) config_cache('federation.activitypub.enabled') == false) { + throw new Exception('Activitypub not enabled'); + } + + $validTarget = $this->checkTarget(); + if (! $validTarget) { + throw new Exception('Invalid target'); + } + + $validActor = $this->checkActor(); + if (! $validActor) { + throw new Exception('Invalid actor'); + } + + } + + protected function checkTarget() + { + $fetchTargetUrl = $this->target.'?cb='.time(); + $res = ActivityPubFetchService::fetchRequest($fetchTargetUrl, true); + + if (! $res || ! isset($res['alsoKnownAs'])) { + return false; + } + + $targetRes = Helpers::profileFetch($this->target); + if (! $targetRes) { + return false; + } + + if (is_string($res['alsoKnownAs'])) { + return $this->lowerTrim($res['alsoKnownAs']) === $this->lowerTrim($this->activity); + } + + if (is_array($res['alsoKnownAs'])) { + $map = Arr::map($res['alsoKnownAs'], function ($value, $key) { + return trim(strtolower($value)); + }); + + $res = in_array($this->activity, $map); + + return $res; + } + + return false; + } + + protected function checkActor() + { + $fetchActivityUrl = $this->activity.'?cb='.time(); + $res = ActivityPubFetchService::fetchRequest($fetchActivityUrl, true); + + if (! $res || ! isset($res['movedTo']) || empty($res['movedTo'])) { + return false; + } + + $actorRes = Helpers::profileFetch($this->activity); + if (! $actorRes) { + return false; + } + + if (is_string($res['movedTo'])) { + $match = $this->lowerTrim($res['movedTo']) === $this->lowerTrim($this->target); + if (! $match) { + return false; + } + + return $match; + } + + return false; + } + + protected function lowerTrim($str) + { + return trim(strtolower($str)); + } +} diff --git a/app/Jobs/MovePipeline/UnfollowLegacyAccountMovePipeline.php b/app/Jobs/MovePipeline/UnfollowLegacyAccountMovePipeline.php new file mode 100644 index 000000000..47ed2aeb6 --- /dev/null +++ b/app/Jobs/MovePipeline/UnfollowLegacyAccountMovePipeline.php @@ -0,0 +1,111 @@ +target = $target; + $this->activity = $activity; + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware(): array + { + return [ + new WithoutOverlapping('process-move-undo-legacy-followers:'.$this->target), + (new ThrottlesExceptions(2, 5 * 60))->backoff(5), + ]; + } + + /** + * Determine the time at which the job should timeout. + */ + public function retryUntil(): DateTime + { + return now()->addMinutes(5); + } + + /** + * Execute the job. + */ + public function handle(): void + { + if (config('app.env') !== 'production' || (bool) config_cache('federation.activitypub.enabled') == false) { + throw new Exception('Activitypub not enabled'); + } + + $target = $this->target; + $actor = $this->activity; + + $targetAccount = Helpers::profileFetch($target); + $actorAccount = Helpers::profileFetch($actor); + + if (! $targetAccount || ! $actorAccount) { + throw new Exception('Invalid move accounts'); + } + + $version = config('pixelfed.version'); + $appUrl = config('app.url'); + $userAgent = "(Pixelfed/{$version}; +{$appUrl})"; + $addlHeaders = [ + 'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + 'User-Agent' => $userAgent, + ]; + $targetInbox = $actorAccount['sharedInbox'] ?? $actorAccount['inbox_url']; + $targetPid = $actorAccount['id']; + + DB::table('followers') + ->join('profiles', 'followers.profile_id', '=', 'profiles.id') + ->where('followers.following_id', $actorAccount['id']) + ->whereNotNull('profiles.user_id') + ->whereNull('profiles.deleted_at') + ->select('profiles.id', 'profiles.user_id', 'profiles.username', 'profiles.private_key', 'profiles.status') + ->chunkById(100, function ($followers) use ($actor, $targetInbox, $targetPid) { + foreach ($followers as $follower) { + if (! $follower->id || ! $follower->private_key || ! $follower->username || ! $follower->user_id || $follower->status === 'delete') { + continue; + } + + MoveSendUndoFollowPipeline::dispatch($follower, $targetInbox, $targetPid, $actor)->onQueue('move'); + } + }, 'id'); + } +} diff --git a/app/Services/AccountService.php b/app/Services/AccountService.php index c22226030..3ce04d610 100644 --- a/app/Services/AccountService.php +++ b/app/Services/AccountService.php @@ -24,13 +24,13 @@ class AccountService public static function get($id, $softFail = false) { $res = Cache::remember(self::CACHE_KEY.$id, 43200, function () use ($id) { - $fractal = new Fractal\Manager(); - $fractal->setSerializer(new ArraySerializer()); + $fractal = new Fractal\Manager; + $fractal->setSerializer(new ArraySerializer); $profile = Profile::find($id); if (! $profile || $profile->status === 'delete') { return null; } - $resource = new Fractal\Resource\Item($profile, new AccountTransformer()); + $resource = new Fractal\Resource\Item($profile, new AccountTransformer); return $fractal->createData($resource)->toArray(); }); @@ -202,7 +202,7 @@ class AccountService } $count = Status::whereProfileId($id) - ->whereNull(['in_reply_to_id','reblog_of_id']) + ->whereNull(['in_reply_to_id', 'reblog_of_id']) ->whereIn('scope', ['public', 'unlisted', 'private']) ->count(); @@ -291,7 +291,6 @@ class AccountService $user->save(); Cache::put($key, 1, 14400); } - } public static function blocksDomain($pid, $domain = false) diff --git a/app/Services/ActivityPubFetchService.php b/app/Services/ActivityPubFetchService.php index 2e9f68402..761be5c77 100644 --- a/app/Services/ActivityPubFetchService.php +++ b/app/Services/ActivityPubFetchService.php @@ -25,59 +25,8 @@ class ActivityPubFetchService $urlKey = hash('sha256', $url); $key = self::CACHE_KEY.$domainKey.':'.$urlKey; - return Cache::remember($key, 3600, function () use ($url) { - $baseHeaders = [ - 'Accept' => 'application/activity+json', - ]; - - $headers = HttpSignature::instanceActorSign($url, false, $baseHeaders, 'get'); - $headers['Accept'] = 'application/activity+json'; - $headers['User-Agent'] = 'PixelFedBot/1.0.0 (Pixelfed/'.config('pixelfed.version').'; +'.config('app.url').')'; - - try { - $res = Http::withOptions([ - 'allow_redirects' => [ - 'max' => 2, - 'protocols' => ['https'], - ]]) - ->withHeaders($headers) - ->timeout(30) - ->connectTimeout(5) - ->retry(3, 500) - ->get($url); - } catch (RequestException $e) { - return; - } catch (ConnectionException $e) { - return; - } catch (Exception $e) { - return; - } - - if (! $res->ok()) { - return; - } - - if (! $res->hasHeader('Content-Type')) { - return; - } - - $acceptedTypes = [ - 'application/activity+json; charset=utf-8', - 'application/activity+json', - 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', - ]; - - $contentType = $res->getHeader('Content-Type')[0]; - - if (! $contentType) { - return; - } - - if (! in_array($contentType, $acceptedTypes)) { - return; - } - - return $res->body(); + return Cache::remember($key, 450, function () use ($url) { + return self::fetchRequest($url); }); } @@ -130,4 +79,60 @@ class ActivityPubFetchService return $url; } + + public static function fetchRequest($url, $returnJsonFormat = false) + { + $baseHeaders = [ + 'Accept' => 'application/activity+json', + ]; + + $headers = HttpSignature::instanceActorSign($url, false, $baseHeaders, 'get'); + $headers['Accept'] = 'application/activity+json'; + $headers['User-Agent'] = 'PixelFedBot/1.0.0 (Pixelfed/'.config('pixelfed.version').'; +'.config('app.url').')'; + + try { + $res = Http::withOptions([ + 'allow_redirects' => [ + 'max' => 2, + 'protocols' => ['https'], + ]]) + ->withHeaders($headers) + ->timeout(30) + ->connectTimeout(5) + ->retry(3, 500) + ->get($url); + } catch (RequestException $e) { + return; + } catch (ConnectionException $e) { + return; + } catch (Exception $e) { + return; + } + + if (! $res->ok()) { + return; + } + + if (! $res->hasHeader('Content-Type')) { + return; + } + + $acceptedTypes = [ + 'application/activity+json; charset=utf-8', + 'application/activity+json', + 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + ]; + + $contentType = $res->getHeader('Content-Type')[0]; + + if (! $contentType) { + return; + } + + if (! in_array($contentType, $acceptedTypes)) { + return; + } + + return $returnJsonFormat ? $res->json() : $res->body(); + } } diff --git a/app/Services/FollowerService.php b/app/Services/FollowerService.php index cec8f7068..ff2a191f2 100644 --- a/app/Services/FollowerService.php +++ b/app/Services/FollowerService.php @@ -19,7 +19,7 @@ class FollowerService const FOLLOWING_SYNC_KEY = 'pf:services:followers:sync-following:'; const FOLLOWING_KEY = 'pf:services:follow:following:id:'; const FOLLOWERS_KEY = 'pf:services:follow:followers:id:'; - const FOLLOWERS_LOCAL_KEY = 'pf:services:follow:local-follower-ids:'; + const FOLLOWERS_LOCAL_KEY = 'pf:services:follow:local-follower-ids:v1:'; const FOLLOWERS_INTER_KEY = 'pf:services:follow:followers:inter:id:'; public static function add($actor, $target, $refresh = true) @@ -33,12 +33,16 @@ class FollowerService Redis::zadd(self::FOLLOWING_KEY . $actor, $ts, $target); Redis::zadd(self::FOLLOWERS_KEY . $target, $ts, $actor); Cache::forget('profile:following:' . $actor); + Cache::forget(self::FOLLOWERS_LOCAL_KEY . $actor); + Cache::forget(self::FOLLOWERS_LOCAL_KEY . $target); } public static function remove($actor, $target, $silent = false) { Redis::zrem(self::FOLLOWING_KEY . $actor, $target); Redis::zrem(self::FOLLOWERS_KEY . $target, $actor); + Cache::forget(self::FOLLOWERS_LOCAL_KEY . $actor); + Cache::forget(self::FOLLOWERS_LOCAL_KEY . $target); if($silent !== true) { AccountService::del($actor); AccountService::del($target); @@ -151,18 +155,26 @@ class FollowerService protected function getAudienceInboxes($pid, $scope = null) { $key = 'pf:services:follower:audience:' . $pid; - $domains = Cache::remember($key, 432000, function() use($pid) { + $bannedDomains = InstanceService::getBannedDomains(); + $domains = Cache::remember($key, 432000, function() use($pid, $bannedDomains) { $profile = Profile::whereNull(['status', 'domain'])->find($pid); if(!$profile) { return []; } - return $profile - ->followers() + return DB::table('followers') + ->join('profiles', 'followers.profile_id', '=', 'profiles.id') + ->where('followers.following_id', $pid) + ->whereNotNull('profiles.inbox_url') + ->whereNull('profiles.deleted_at') + ->select('followers.profile_id', 'followers.following_id', 'profiles.id', 'profiles.user_id', 'profiles.deleted_at', 'profiles.sharedInbox', 'profiles.inbox_url') ->get() - ->map(function($follow) { - return $follow->sharedInbox ?? $follow->inbox_url; + ->map(function($r) { + return $r->sharedInbox ?? $r->inbox_url; + }) + ->filter(function($r) use($bannedDomains) { + $domain = parse_url($r, PHP_URL_HOST); + return $r && !in_array($domain, $bannedDomains); }) - ->filter() ->unique() ->values(); }); @@ -241,7 +253,13 @@ class FollowerService { $key = self::FOLLOWERS_LOCAL_KEY . $pid; $res = Cache::remember($key, 7200, function() use($pid) { - return DB::table('followers')->whereFollowingId($pid)->whereLocalProfile(true)->pluck('profile_id')->sort(); + return DB::table('followers') + ->join('profiles', 'followers.profile_id', '=', 'profiles.id') + ->where('followers.following_id', $pid) + ->whereNotNull('profiles.user_id') + ->whereNull('profiles.deleted_at') + ->select('followers.profile_id', 'followers.following_id', 'profiles.id', 'profiles.user_id', 'profiles.deleted_at') + ->pluck('followers.profile_id'); }); return $limit ? $res->take($limit)->values()->toArray() : diff --git a/app/Services/NodeinfoService.php b/app/Services/NodeinfoService.php index 6284538f0..d0de127e6 100644 --- a/app/Services/NodeinfoService.php +++ b/app/Services/NodeinfoService.php @@ -2,32 +2,31 @@ namespace App\Services; -use Illuminate\Support\Str; -use Illuminate\Support\Facades\Http; -use Illuminate\Http\Client\RequestException; use Illuminate\Http\Client\ConnectionException; +use Illuminate\Http\Client\RequestException; +use Illuminate\Support\Facades\Http; class NodeinfoService { public static function get($domain) { - $version = config('pixelfed.version'); - $appUrl = config('app.url'); - $headers = [ - 'Accept' => 'application/json', - 'User-Agent' => "(Pixelfed/{$version}; +{$appUrl})", - ]; + $version = config('pixelfed.version'); + $appUrl = config('app.url'); + $headers = [ + 'Accept' => 'application/json', + 'User-Agent' => "(Pixelfed/{$version}; +{$appUrl})", + ]; - $url = 'https://' . $domain; - $wk = $url . '/.well-known/nodeinfo'; + $url = 'https://'.$domain; + $wk = $url.'/.well-known/nodeinfo'; try { $res = Http::withOptions([ 'allow_redirects' => false, ]) - ->withHeaders($headers) - ->timeout(5) - ->get($wk); + ->withHeaders($headers) + ->timeout(5) + ->get($wk); } catch (RequestException $e) { return false; } catch (ConnectionException $e) { @@ -36,18 +35,18 @@ class NodeinfoService return false; } - if(!$res) { + if (! $res) { return false; } $json = $res->json(); - if( !isset($json['links'])) { + if (! isset($json['links'])) { return false; } - if(is_array($json['links'])) { - if(isset($json['links']['href'])) { + if (is_array($json['links'])) { + if (isset($json['links']['href'])) { $href = $json['links']['href']; } else { $href = $json['links'][0]['href']; @@ -59,17 +58,17 @@ class NodeinfoService $domain = parse_url($url, PHP_URL_HOST); $hrefDomain = parse_url($href, PHP_URL_HOST); - if($domain !== $hrefDomain) { - return 60; + if ($domain !== $hrefDomain) { + return false; } try { $res = Http::withOptions([ 'allow_redirects' => false, ]) - ->withHeaders($headers) - ->timeout(5) - ->get($href); + ->withHeaders($headers) + ->timeout(5) + ->get($href); } catch (RequestException $e) { return false; } catch (ConnectionException $e) { @@ -77,6 +76,7 @@ class NodeinfoService } catch (\Exception $e) { return false; } + return $res->json(); } } diff --git a/app/Services/SearchApiV2Service.php b/app/Services/SearchApiV2Service.php index 2f7ec51f2..fc87d2c04 100644 --- a/app/Services/SearchApiV2Service.php +++ b/app/Services/SearchApiV2Service.php @@ -11,7 +11,6 @@ use Illuminate\Support\Str; use League\Fractal; use League\Fractal\Serializer\ArraySerializer; - class SearchApiV2Service { private $query; @@ -119,7 +118,7 @@ class SearchApiV2Service AccountService::get($res['id']); }) ->filter(function ($account) { - return $account && isset($account['id']); + return $account && isset($account['id']) && ! isset($account['moved'], $account['moved']['id']); }) ->values(); @@ -248,7 +247,7 @@ class SearchApiV2Service if ($sid = Status::whereUri($query)->first()) { $s = StatusService::get($sid->id, false); - if (! $s) { + if (! $s || isset($s['account']['moved'], $s['account']['moved']['id'])) { return $default; } if (in_array($s['visibility'], ['public', 'unlisted'])) { @@ -359,7 +358,7 @@ class SearchApiV2Service ->whereUsername($query) ->first(); - if (! $profile) { + if (! $profile || $profile->moved_to_profile_id) { return [ 'accounts' => [], 'hashtags' => [], @@ -367,9 +366,9 @@ class SearchApiV2Service ]; } - $fractal = new Fractal\Manager(); - $fractal->setSerializer(new ArraySerializer()); - $resource = new Fractal\Resource\Item($profile, new AccountTransformer()); + $fractal = new Fractal\Manager; + $fractal->setSerializer(new ArraySerializer); + $resource = new Fractal\Resource\Item($profile, new AccountTransformer); return [ 'accounts' => [$fractal->createData($resource)->toArray()], @@ -393,9 +392,9 @@ class SearchApiV2Service ]; } - $fractal = new Fractal\Manager(); - $fractal->setSerializer(new ArraySerializer()); - $resource = new Fractal\Resource\Item($profile, new AccountTransformer()); + $fractal = new Fractal\Manager; + $fractal->setSerializer(new ArraySerializer); + $resource = new Fractal\Resource\Item($profile, new AccountTransformer); return [ 'accounts' => [$fractal->createData($resource)->toArray()], diff --git a/app/Services/StatusService.php b/app/Services/StatusService.php index d73f6c018..621574f09 100644 --- a/app/Services/StatusService.php +++ b/app/Services/StatusService.php @@ -30,9 +30,9 @@ class StatusService if (! $status) { return null; } - $fractal = new Fractal\Manager(); - $fractal->setSerializer(new ArraySerializer()); - $resource = new Fractal\Resource\Item($status, new StatusStatelessTransformer()); + $fractal = new Fractal\Manager; + $fractal->setSerializer(new ArraySerializer); + $resource = new Fractal\Resource\Item($status, new StatusStatelessTransformer); $res = $fractal->createData($resource)->toArray(); $res['_pid'] = isset($res['account']) && isset($res['account']['id']) ? $res['account']['id'] : null; if (isset($res['_pid'])) { @@ -112,7 +112,7 @@ class StatusService { $status = self::get($id, false); - if (! $status) { + if (! $status || ! $pid) { return [ 'liked' => false, 'shared' => false, @@ -146,9 +146,9 @@ class StatusService return null; } - $fractal = new Fractal\Manager(); - $fractal->setSerializer(new ArraySerializer()); - $resource = new Fractal\Resource\Item($status, new StatusStatelessTransformer()); + $fractal = new Fractal\Manager; + $fractal->setSerializer(new ArraySerializer); + $resource = new Fractal\Resource\Item($status, new StatusStatelessTransformer); return $fractal->createData($resource)->toArray(); } diff --git a/app/Util/ActivityPub/Helpers.php b/app/Util/ActivityPub/Helpers.php index fe82eb2e8..2111e3b30 100644 --- a/app/Util/ActivityPub/Helpers.php +++ b/app/Util/ActivityPub/Helpers.php @@ -155,7 +155,7 @@ class Helpers return in_array($url, $audience['to']) || in_array($url, $audience['cc']); } - public static function validateUrl($url = null, $disableDNSCheck = false) + public static function validateUrl($url = null, $disableDNSCheck = false, $forceBanCheck = false) { if (is_array($url) && ! empty($url)) { $url = $url[0]; @@ -212,7 +212,7 @@ class Helpers } } - if ($disableDNSCheck !== true && app()->environment() === 'production') { + if ($forceBanCheck || $disableDNSCheck !== true && app()->environment() === 'production') { $bannedInstances = InstanceService::getBannedDomains(); if (in_array($host, $bannedInstances)) { return false; @@ -739,7 +739,7 @@ class Helpers $width = isset($media['width']) ? $media['width'] : false; $height = isset($media['height']) ? $media['height'] : false; - $media = new Media(); + $media = new Media; $media->blurhash = $blurhash; $media->remote_media = true; $media->status_id = $status->id; @@ -801,12 +801,19 @@ class Helpers return self::profileUpdateOrCreate($url); } - public static function profileUpdateOrCreate($url) + public static function profileUpdateOrCreate($url, $movedToCheck = false) { + $movedToPid = null; $res = self::fetchProfileFromUrl($url); if (! $res || isset($res['id']) == false) { return; } + if (! self::validateUrl($res['inbox'])) { + return; + } + if (! self::validateUrl($res['id'])) { + return; + } $urlDomain = parse_url($url, PHP_URL_HOST); $domain = parse_url($res['id'], PHP_URL_HOST); if (strtolower($urlDomain) !== strtolower($domain)) { @@ -829,13 +836,6 @@ class Helpers $remoteUsername = $username; $webfinger = "@{$username}@{$domain}"; - if (! self::validateUrl($res['inbox'])) { - return; - } - if (! self::validateUrl($res['id'])) { - return; - } - $instance = Instance::updateOrCreate([ 'domain' => $domain, ]); @@ -843,6 +843,13 @@ class Helpers \App\Jobs\InstancePipeline\FetchNodeinfoPipeline::dispatch($instance)->onQueue('low'); } + if (! $movedToCheck && isset($res['movedTo']) && Helpers::validateUrl($res['movedTo'])) { + $movedTo = self::profileUpdateOrCreate($res['movedTo'], true); + if ($movedTo) { + $movedToPid = $movedTo->id; + } + } + $profile = Profile::updateOrCreate( [ 'domain' => strtolower($domain), @@ -859,6 +866,7 @@ class Helpers 'outbox_url' => isset($res['outbox']) ? $res['outbox'] : null, 'public_key' => $res['publicKey']['publicKeyPem'], 'indexable' => isset($res['indexable']) && is_bool($res['indexable']) ? $res['indexable'] : false, + 'moved_to_profile_id' => $movedToPid, ] ); diff --git a/app/Util/ActivityPub/HttpSignature.php b/app/Util/ActivityPub/HttpSignature.php index 35facb82b..e6834aaef 100644 --- a/app/Util/ActivityPub/HttpSignature.php +++ b/app/Util/ActivityPub/HttpSignature.php @@ -25,7 +25,13 @@ class HttpSignature $stringToSign = self::_headersToSigningString($headers); $signedHeaders = implode(' ', array_map('strtolower', array_keys($headers))); $key = openssl_pkey_get_private($user->private_key); + if (empty($key)) { + return []; + } openssl_sign($stringToSign, $signature, $key, OPENSSL_ALGO_SHA256); + if (empty($signature)) { + return []; + } $signature = base64_encode($signature); $signatureHeader = 'keyId="'.$user->keyId().'",headers="'.$signedHeaders.'",algorithm="rsa-sha256",signature="'.$signature.'"'; unset($headers['(request-target)']); @@ -34,6 +40,34 @@ class HttpSignature return self::_headersToCurlArray($headers); } + public static function signRaw($privateKey, $keyId, $url, $body = false, $addlHeaders = []) + { + if (empty($privateKey) || empty($keyId)) { + return []; + } + if ($body) { + $digest = self::_digest($body); + } + $headers = self::_headersToSign($url, $body ? $digest : false); + $headers = array_merge($headers, $addlHeaders); + $stringToSign = self::_headersToSigningString($headers); + $signedHeaders = implode(' ', array_map('strtolower', array_keys($headers))); + $key = openssl_pkey_get_private($privateKey); + if (empty($key)) { + return []; + } + openssl_sign($stringToSign, $signature, $key, OPENSSL_ALGO_SHA256); + if (empty($signature)) { + return []; + } + $signature = base64_encode($signature); + $signatureHeader = 'keyId="'.$keyId.'",headers="'.$signedHeaders.'",algorithm="rsa-sha256",signature="'.$signature.'"'; + unset($headers['(request-target)']); + $headers['Signature'] = $signatureHeader; + + return self::_headersToCurlArray($headers); + } + public static function instanceActorSign($url, $body = false, $addlHeaders = [], $method = 'post') { $keyId = config('app.url').'/i/actor#main-key'; diff --git a/app/Util/ActivityPub/Inbox.php b/app/Util/ActivityPub/Inbox.php index 4ab87f40b..6db3589f5 100644 --- a/app/Util/ActivityPub/Inbox.php +++ b/app/Util/ActivityPub/Inbox.php @@ -2,59 +2,60 @@ namespace App\Util\ActivityPub; -use Cache, DB, Log, Purify, Redis, Storage, Validator; -use App\{ - Activity, - DirectMessage, - Follower, - FollowRequest, - Instance, - Like, - Notification, - Media, - Profile, - Status, - StatusHashtag, - Story, - StoryView, - UserFilter -}; -use Carbon\Carbon; -use App\Util\ActivityPub\Helpers; -use Illuminate\Support\Str; -use App\Jobs\LikePipeline\LikePipeline; -use App\Jobs\FollowPipeline\FollowPipeline; +use App\DirectMessage; +use App\Follower; +use App\FollowRequest; +use App\Instance; use App\Jobs\DeletePipeline\DeleteRemoteProfilePipeline; +use App\Jobs\FollowPipeline\FollowPipeline; +use App\Jobs\HomeFeedPipeline\FeedRemoveRemotePipeline; +use App\Jobs\LikePipeline\LikePipeline; +use App\Jobs\MovePipeline\CleanupLegacyAccountMovePipeline; +use App\Jobs\MovePipeline\MoveMigrateFollowersPipeline; +use App\Jobs\MovePipeline\ProcessMovePipeline; +use App\Jobs\MovePipeline\UnfollowLegacyAccountMovePipeline; +use App\Jobs\ProfilePipeline\HandleUpdateActivity; use App\Jobs\StatusPipeline\RemoteStatusDelete; +use App\Jobs\StatusPipeline\StatusRemoteUpdatePipeline; use App\Jobs\StoryPipeline\StoryExpire; use App\Jobs\StoryPipeline\StoryFetch; -use App\Jobs\StatusPipeline\StatusRemoteUpdatePipeline; -use App\Jobs\ProfilePipeline\HandleUpdateActivity; - +use App\Like; +use App\Media; +use App\Models\Conversation; +use App\Models\RemoteReport; +use App\Notification; +use App\Profile; +use App\Services\AccountService; +use App\Services\FollowerService; +use App\Services\PollService; +use App\Services\ReblogService; +use App\Services\UserFilterService; +use App\Status; +use App\Story; +use App\StoryView; +use App\UserFilter; use App\Util\ActivityPub\Validator\Accept as AcceptValidator; -use App\Util\ActivityPub\Validator\Add as AddValidator; use App\Util\ActivityPub\Validator\Announce as AnnounceValidator; use App\Util\ActivityPub\Validator\Follow as FollowValidator; use App\Util\ActivityPub\Validator\Like as LikeValidator; -use App\Util\ActivityPub\Validator\UndoFollow as UndoFollowValidator; +use App\Util\ActivityPub\Validator\MoveValidator; use App\Util\ActivityPub\Validator\UpdatePersonValidator; - -use App\Services\AccountService; -use App\Services\PollService; -use App\Services\FollowerService; -use App\Services\ReblogService; -use App\Services\StatusService; -use App\Services\UserFilterService; -use App\Services\NetworkTimelineService; -use App\Models\Conversation; -use App\Models\RemoteReport; -use App\Jobs\HomeFeedPipeline\FeedRemoveRemotePipeline; +use Cache; +use Illuminate\Support\Facades\Bus; +use Illuminate\Support\Facades\Log; +use Illuminate\Support\Str; +use Purify; +use Storage; +use Throwable; class Inbox { protected $headers; + protected $profile; + protected $payload; + protected $logger; public function __construct($headers, $profile, $payload) @@ -67,7 +68,7 @@ class Inbox public function handle() { $this->handleVerb(); - return; + } public function handleVerb() @@ -84,17 +85,23 @@ class Inbox break; case 'Follow': - if(FollowValidator::validate($this->payload) == false) { return; } + if (FollowValidator::validate($this->payload) == false) { + return; + } $this->handleFollowActivity(); break; case 'Announce': - if(AnnounceValidator::validate($this->payload) == false) { return; } + if (AnnounceValidator::validate($this->payload) == false) { + return; + } $this->handleAnnounceActivity(); break; case 'Accept': - if(AcceptValidator::validate($this->payload) == false) { return; } + if (AcceptValidator::validate($this->payload) == false) { + return; + } $this->handleAcceptActivity(); break; @@ -103,7 +110,9 @@ class Inbox break; case 'Like': - if(LikeValidator::validate($this->payload) == false) { return; } + if (LikeValidator::validate($this->payload) == false) { + return; + } $this->handleLikeActivity(); break; @@ -135,6 +144,15 @@ class Inbox $this->handleUpdateActivity(); break; + case 'Move': + if (MoveValidator::validate($this->payload) == false) { + Log::info('[AP][INBOX][MOVE] VALIDATE_FAILURE '.json_encode($this->payload)); + + return; + } + $this->handleMoveActivity(); + break; + default: // TODO: decide how to handle invalid verbs. break; @@ -145,8 +163,8 @@ class Inbox { $activity = $this->payload['object']; - if(isset($activity['inReplyTo']) && - !empty($activity['inReplyTo']) && + if (isset($activity['inReplyTo']) && + ! empty($activity['inReplyTo']) && Helpers::validateUrl($activity['inReplyTo']) ) { // reply detected, skip attachment check @@ -167,7 +185,7 @@ class Inbox { // stories ;) - if(!isset( + if (! isset( $this->payload['actor'], $this->payload['object'] )) { @@ -177,85 +195,86 @@ class Inbox $actor = $this->payload['actor']; $obj = $this->payload['object']; - if(!Helpers::validateUrl($actor)) { + if (! Helpers::validateUrl($actor)) { return; } - if(!isset($obj['type'])) { + if (! isset($obj['type'])) { return; } - switch($obj['type']) { + switch ($obj['type']) { case 'Story': StoryFetch::dispatch($this->payload); - break; + break; } - return; } public function handleCreateActivity() { $activity = $this->payload['object']; - if(config('autospam.live_filters.enabled')) { + if (config('autospam.live_filters.enabled')) { $filters = config('autospam.live_filters.filters'); - if(!empty($filters) && isset($activity['content']) && !empty($activity['content']) && strlen($filters) > 3) { + if (! empty($filters) && isset($activity['content']) && ! empty($activity['content']) && strlen($filters) > 3) { $filters = array_map('trim', explode(',', $filters)); $content = $activity['content']; - foreach($filters as $filter) { + foreach ($filters as $filter) { $filter = trim(strtolower($filter)); - if(!$filter || !strlen($filter)) { + if (! $filter || ! strlen($filter)) { continue; } - if(str_contains(strtolower($content), $filter)) { + if (str_contains(strtolower($content), $filter)) { return; } } } } $actor = $this->actorFirstOrCreate($this->payload['actor']); - if(!$actor || $actor->domain == null) { + if (! $actor || $actor->domain == null) { return; } - if(!isset($activity['to'])) { + if (! isset($activity['to'])) { return; } $to = isset($activity['to']) ? $activity['to'] : []; $cc = isset($activity['cc']) ? $activity['cc'] : []; - if($activity['type'] == 'Question') { + if ($activity['type'] == 'Question') { $this->handlePollCreate(); + return; } - if( is_array($to) && + if (is_array($to) && is_array($cc) && count($to) == 1 && count($cc) == 0 && parse_url($to[0], PHP_URL_HOST) == config('pixelfed.domain.app') ) { $this->handleDirectMessage(); + return; } - if($activity['type'] == 'Note' && !empty($activity['inReplyTo'])) { + if ($activity['type'] == 'Note' && ! empty($activity['inReplyTo'])) { $this->handleNoteReply(); - } elseif ($activity['type'] == 'Note' && !empty($activity['attachment'])) { - if(!$this->verifyNoteAttachment()) { + } elseif ($activity['type'] == 'Note' && ! empty($activity['attachment'])) { + if (! $this->verifyNoteAttachment()) { return; } $this->handleNoteCreate(); } - return; + } public function handleNoteReply() { $activity = $this->payload['object']; $actor = $this->actorFirstOrCreate($this->payload['actor']); - if(!$actor || $actor->domain == null) { + if (! $actor || $actor->domain == null) { return; } @@ -263,42 +282,43 @@ class Inbox $url = isset($activity['url']) ? $activity['url'] : $activity['id']; Helpers::statusFirstOrFetch($url, true); - return; + } public function handlePollCreate() { $activity = $this->payload['object']; $actor = $this->actorFirstOrCreate($this->payload['actor']); - if(!$actor || $actor->domain == null) { + if (! $actor || $actor->domain == null) { return; } $url = isset($activity['url']) ? $activity['url'] : $activity['id']; Helpers::statusFirstOrFetch($url); - return; + } public function handleNoteCreate() { $activity = $this->payload['object']; $actor = $this->actorFirstOrCreate($this->payload['actor']); - if(!$actor || $actor->domain == null) { + if (! $actor || $actor->domain == null) { return; } - if( isset($activity['inReplyTo']) && + if (isset($activity['inReplyTo']) && isset($activity['name']) && - !isset($activity['content']) && - !isset($activity['attachment']) && + ! isset($activity['content']) && + ! isset($activity['attachment']) && Helpers::validateLocalUrl($activity['inReplyTo']) ) { $this->handlePollVote(); + return; } - if($actor->followers_count == 0) { - if(config('federation.activitypub.ingest.store_notes_without_followers')) { - } else if(FollowerService::followerCount($actor->id, true) == 0) { + if ($actor->followers_count == 0) { + if (config('federation.activitypub.ingest.store_notes_without_followers')) { + } elseif (FollowerService::followerCount($actor->id, true) == 0) { return; } } @@ -306,12 +326,12 @@ class Inbox $hasUrl = isset($activity['url']); $url = isset($activity['url']) ? $activity['url'] : $activity['id']; - if($hasUrl) { - if(Status::whereUri($url)->exists()) { + if ($hasUrl) { + if (Status::whereUri($url)->exists()) { return; } } else { - if(Status::whereObjectUrl($url)->exists()) { + if (Status::whereObjectUrl($url)->exists()) { return; } } @@ -321,7 +341,7 @@ class Inbox $actor, $activity ); - return; + } public function handlePollVote() @@ -329,34 +349,34 @@ class Inbox $activity = $this->payload['object']; $actor = $this->actorFirstOrCreate($this->payload['actor']); - if(!$actor) { + if (! $actor) { return; } $status = Helpers::statusFetch($activity['inReplyTo']); - if(!$status) { + if (! $status) { return; } $poll = $status->poll; - if(!$poll) { + if (! $poll) { return; } - if(now()->gt($poll->expires_at)) { + if (now()->gt($poll->expires_at)) { return; } $choices = $poll->poll_options; $choice = array_search($activity['name'], $choices); - if($choice === false) { + if ($choice === false) { return; } - if(PollVote::whereStatusId($status->id)->whereProfileId($actor->id)->exists()) { + if (PollVote::whereStatusId($status->id)->whereProfileId($actor->id)->exists()) { return; } @@ -376,7 +396,6 @@ class Inbox PollService::del($status->id); - return; } public function handleDirectMessage() @@ -387,24 +406,24 @@ class Inbox ->whereUsername(array_last(explode('/', $activity['to'][0]))) ->firstOrFail(); - if(!$actor || in_array($actor->id, $profile->blockedIds()->toArray())) { + if (! $actor || in_array($actor->id, $profile->blockedIds()->toArray())) { return; } - if(AccountService::blocksDomain($profile->id, $actor->domain) == true) { + if (AccountService::blocksDomain($profile->id, $actor->domain) == true) { return; } $msg = $activity['content']; $msgText = strip_tags($activity['content']); - if(Str::startsWith($msgText, '@' . $profile->username)) { - $len = strlen('@' . $profile->username); + if (Str::startsWith($msgText, '@'.$profile->username)) { + $len = strlen('@'.$profile->username); $msgText = substr($msgText, $len + 1); } - if($profile->user->settings->public_dm == false || $profile->is_private) { - if($profile->follows($actor) == true) { + if ($profile->user->settings->public_dm == false || $profile->is_private) { + if ($profile->follows($actor) == true) { $hidden = false; } else { $hidden = true; @@ -436,30 +455,30 @@ class Inbox Conversation::updateOrInsert( [ 'to_id' => $profile->id, - 'from_id' => $actor->id + 'from_id' => $actor->id, ], [ 'type' => 'text', 'status_id' => $status->id, 'dm_id' => $dm->id, - 'is_hidden' => $hidden + 'is_hidden' => $hidden, ] ); - if(count($activity['attachment'])) { + if (count($activity['attachment'])) { $photos = 0; $videos = 0; $allowed = explode(',', config_cache('pixelfed.media_types')); $activity['attachment'] = array_slice($activity['attachment'], 0, config_cache('pixelfed.max_album_length')); - foreach($activity['attachment'] as $a) { + foreach ($activity['attachment'] as $a) { $type = $a['mediaType']; $url = $a['url']; $valid = Helpers::validateUrl($url); - if(in_array($type, $allowed) == false || $valid == false) { + if (in_array($type, $allowed) == false || $valid == false) { continue; } - $media = new Media(); + $media = new Media; $media->remote_media = true; $media->status_id = $status->id; $media->profile_id = $status->profile_id; @@ -468,31 +487,31 @@ class Inbox $media->remote_url = $url; $media->mime = $type; $media->save(); - if(explode('/', $type)[0] == 'image') { + if (explode('/', $type)[0] == 'image') { $photos = $photos + 1; } - if(explode('/', $type)[0] == 'video') { + if (explode('/', $type)[0] == 'video') { $videos = $videos + 1; } } - if($photos && $videos == 0) { + if ($photos && $videos == 0) { $dm->type = $photos == 1 ? 'photo' : 'photos'; $dm->save(); } - if($videos && $photos == 0) { + if ($videos && $photos == 0) { $dm->type = $videos == 1 ? 'video' : 'videos'; $dm->save(); } } - if(filter_var($msgText, FILTER_VALIDATE_URL)) { - if(Helpers::validateUrl($msgText)) { + if (filter_var($msgText, FILTER_VALIDATE_URL)) { + if (Helpers::validateUrl($msgText)) { $dm->type = 'link'; $dm->meta = [ 'domain' => parse_url($msgText, PHP_URL_HOST), 'local' => parse_url($msgText, PHP_URL_HOST) == - parse_url(config('app.url'), PHP_URL_HOST) + parse_url(config('app.url'), PHP_URL_HOST), ]; $dm->save(); } @@ -504,8 +523,8 @@ class Inbox ->whereFilterType('dm.mute') ->exists(); - if($profile->domain == null && $hidden == false && !$nf) { - $notification = new Notification(); + if ($profile->domain == null && $hidden == false && ! $nf) { + $notification = new Notification; $notification->profile_id = $profile->id; $notification->actor_id = $actor->id; $notification->action = 'dm'; @@ -514,26 +533,25 @@ class Inbox $notification->save(); } - return; } public function handleFollowActivity() { $actor = $this->actorFirstOrCreate($this->payload['actor']); $target = $this->actorFirstOrCreate($this->payload['object']); - if(!$actor || !$target) { + if (! $actor || ! $target) { return; } - if($actor->domain == null || $target->domain !== null) { + if ($actor->domain == null || $target->domain !== null) { return; } - if(AccountService::blocksDomain($target->id, $actor->domain) == true) { + if (AccountService::blocksDomain($target->id, $actor->domain) == true) { return; } - if( + if ( Follower::whereProfileId($actor->id) ->whereFollowingId($target->id) ->exists() || @@ -545,16 +563,16 @@ class Inbox } $blocks = UserFilterService::blocks($target->id); - if($blocks && in_array($actor->id, $blocks)) { + if ($blocks && in_array($actor->id, $blocks)) { return; } - if($target->is_private == true) { + if ($target->is_private == true) { FollowRequest::updateOrCreate([ 'follower_id' => $actor->id, 'following_id' => $target->id, - ],[ - 'activity' => collect($this->payload)->only(['id','actor','object','type'])->toArray() + ], [ + 'activity' => collect($this->payload)->only(['id', 'actor', 'object', 'type'])->toArray(), ]); } else { $follower = new Follower; @@ -569,15 +587,15 @@ class Inbox // send Accept to remote profile $accept = [ '@context' => 'https://www.w3.org/ns/activitystreams', - 'id' => $target->permalink().'#accepts/follows/' . $follower->id, - 'type' => 'Accept', - 'actor' => $target->permalink(), - 'object' => [ - 'id' => $this->payload['id'], - 'actor' => $actor->permalink(), - 'type' => 'Follow', - 'object' => $target->permalink() - ] + 'id' => $target->permalink().'#accepts/follows/'.$follower->id, + 'type' => 'Accept', + 'actor' => $target->permalink(), + 'object' => [ + 'id' => $this->payload['id'], + 'actor' => $actor->permalink(), + 'type' => 'Follow', + 'object' => $target->permalink(), + ], ]; Helpers::sendSignedObject($target, $actor->inbox_url, $accept); Cache::forget('profile:follower_count:'.$target->id); @@ -586,7 +604,6 @@ class Inbox Cache::forget('profile:following_count:'.$actor->id); } - return; } public function handleAnnounceActivity() @@ -594,29 +611,29 @@ class Inbox $actor = $this->actorFirstOrCreate($this->payload['actor']); $activity = $this->payload['object']; - if(!$actor || $actor->domain == null) { + if (! $actor || $actor->domain == null) { return; } $parent = Helpers::statusFetch($activity); - if(!$parent || empty($parent)) { + if (! $parent || empty($parent)) { return; } - if(AccountService::blocksDomain($parent->profile_id, $actor->domain) == true) { + if (AccountService::blocksDomain($parent->profile_id, $actor->domain) == true) { return; } $blocks = UserFilterService::blocks($parent->profile_id); - if($blocks && in_array($actor->id, $blocks)) { + if ($blocks && in_array($actor->id, $blocks)) { return; } $status = Status::firstOrCreate([ 'profile_id' => $actor->id, 'reblog_of_id' => $parent->id, - 'type' => 'share' + 'type' => 'share', ]); Notification::firstOrCreate( @@ -634,7 +651,6 @@ class Inbox ReblogService::addPostReblog($parent->profile_id, $status->id); - return; } public function handleAcceptActivity() @@ -643,25 +659,25 @@ class Inbox $obj = $this->payload['object']['object']; $type = $this->payload['object']['type']; - if($type !== 'Follow') { + if ($type !== 'Follow') { return; } $actor = Helpers::validateLocalUrl($actor); $target = Helpers::validateUrl($obj); - if(!$actor || !$target) { + if (! $actor || ! $target) { return; } $actor = Helpers::profileFetch($actor); $target = Helpers::profileFetch($target); - if(!$actor || !$target) { + if (! $actor || ! $target) { return; } - if(AccountService::blocksDomain($target->id, $actor->domain) == true) { + if (AccountService::blocksDomain($target->id, $actor->domain) == true) { return; } @@ -670,7 +686,7 @@ class Inbox ->whereIsRejected(false) ->first(); - if(!$request) { + if (! $request) { return; } @@ -682,12 +698,11 @@ class Inbox $request->delete(); - return; } public function handleDeleteActivity() { - if(!isset( + if (! isset( $this->payload['actor'], $this->payload['object'] )) { @@ -695,15 +710,16 @@ class Inbox } $actor = $this->payload['actor']; $obj = $this->payload['object']; - if(is_string($obj) == true && $actor == $obj && Helpers::validateUrl($obj)) { + if (is_string($obj) == true && $actor == $obj && Helpers::validateUrl($obj)) { $profile = Profile::whereRemoteUrl($obj)->first(); - if(!$profile || $profile->private_key != null) { + if (! $profile || $profile->private_key != null) { return; } DeleteRemoteProfilePipeline::dispatch($profile)->onQueue('inbox'); + return; } else { - if(!isset( + if (! isset( $obj['id'], $this->payload['object'], $this->payload['object']['id'], @@ -713,54 +729,57 @@ class Inbox } $type = $this->payload['object']['type']; $typeCheck = in_array($type, ['Person', 'Tombstone', 'Story']); - if(!Helpers::validateUrl($actor) || !Helpers::validateUrl($obj['id']) || !$typeCheck) { + if (! Helpers::validateUrl($actor) || ! Helpers::validateUrl($obj['id']) || ! $typeCheck) { return; } - if(parse_url($obj['id'], PHP_URL_HOST) !== parse_url($actor, PHP_URL_HOST)) { + if (parse_url($obj['id'], PHP_URL_HOST) !== parse_url($actor, PHP_URL_HOST)) { return; } $id = $this->payload['object']['id']; switch ($type) { case 'Person': - $profile = Profile::whereRemoteUrl($actor)->first(); - if(!$profile || $profile->private_key != null) { - return; - } - DeleteRemoteProfilePipeline::dispatch($profile)->onQueue('inbox'); + $profile = Profile::whereRemoteUrl($actor)->first(); + if (! $profile || $profile->private_key != null) { return; + } + DeleteRemoteProfilePipeline::dispatch($profile)->onQueue('inbox'); + + return; break; case 'Tombstone': - $profile = Profile::whereRemoteUrl($actor)->first(); - if(!$profile || $profile->private_key != null) { - return; - } - - $status = Status::where('object_url', $id)->first(); - if(!$status) { - $status = Status::where('url', $id)->first(); - if(!$status) { - return; - } - } - if($status->profile_id != $profile->id) { - return; - } - if($status->scope && in_array($status->scope, ['public', 'unlisted', 'private'])) { - if($status->type && !in_array($status->type, ['story:reaction', 'story:reply', 'reply'])) { - FeedRemoveRemotePipeline::dispatch($status->id, $status->profile_id)->onQueue('feed'); - } - } - RemoteStatusDelete::dispatch($status)->onQueue('high'); + $profile = Profile::whereRemoteUrl($actor)->first(); + if (! $profile || $profile->private_key != null) { return; + } + + $status = Status::where('object_url', $id)->first(); + if (! $status) { + $status = Status::where('url', $id)->first(); + if (! $status) { + return; + } + } + if ($status->profile_id != $profile->id) { + return; + } + if ($status->scope && in_array($status->scope, ['public', 'unlisted', 'private'])) { + if ($status->type && ! in_array($status->type, ['story:reaction', 'story:reply', 'reply'])) { + FeedRemoveRemotePipeline::dispatch($status->id, $status->profile_id)->onQueue('feed'); + } + } + RemoteStatusDelete::dispatch($status)->onQueue('high'); + + return; break; case 'Story': $story = Story::whereObjectId($id) ->first(); - if($story) { + if ($story) { StoryExpire::dispatch($story)->onQueue('story'); } + return; break; @@ -769,53 +788,50 @@ class Inbox break; } } - return; + } public function handleLikeActivity() { $actor = $this->payload['actor']; - if(!Helpers::validateUrl($actor)) { + if (! Helpers::validateUrl($actor)) { return; } $profile = self::actorFirstOrCreate($actor); $obj = $this->payload['object']; - if(!Helpers::validateUrl($obj)) { + if (! Helpers::validateUrl($obj)) { return; } $status = Helpers::statusFirstOrFetch($obj); - if(!$status || !$profile) { + if (! $status || ! $profile) { return; } - if(AccountService::blocksDomain($status->profile_id, $profile->domain) == true) { + if (AccountService::blocksDomain($status->profile_id, $profile->domain) == true) { return; } $blocks = UserFilterService::blocks($status->profile_id); - if($blocks && in_array($profile->id, $blocks)) { + if ($blocks && in_array($profile->id, $blocks)) { return; } $like = Like::firstOrCreate([ 'profile_id' => $profile->id, - 'status_id' => $status->id + 'status_id' => $status->id, ]); - if($like->wasRecentlyCreated == true) { + if ($like->wasRecentlyCreated == true) { $status->likes_count = $status->likes_count + 1; $status->save(); LikePipeline::dispatch($like); } - return; } - public function handleRejectActivity() - { - } + public function handleRejectActivity() {} public function handleUndoActivity() { @@ -823,11 +839,11 @@ class Inbox $profile = self::actorFirstOrCreate($actor); $obj = $this->payload['object']; - if(!$profile) { + if (! $profile) { return; } // TODO: Some implementations do not inline the object, skip for now - if(!$obj || !is_array($obj) || !isset($obj['type'])) { + if (! $obj || ! is_array($obj) || ! isset($obj['type'])) { return; } @@ -836,22 +852,22 @@ class Inbox break; case 'Announce': - if(is_array($obj) && isset($obj['object'])) { + if (is_array($obj) && isset($obj['object'])) { $obj = $obj['object']; } - if(!is_string($obj)) { + if (! is_string($obj)) { return; } - if(Helpers::validateLocalUrl($obj)) { + if (Helpers::validateLocalUrl($obj)) { $parsedId = last(explode('/', $obj)); $status = Status::find($parsedId); } else { $status = Status::whereUri($obj)->first(); } - if(!$status) { + if (! $status) { return; } - if(AccountService::blocksDomain($status->profile_id, $profile->domain) == true) { + if (AccountService::blocksDomain($status->profile_id, $profile->domain) == true) { return; } FeedRemoveRemotePipeline::dispatch($status->id, $status->profile_id)->onQueue('feed'); @@ -872,10 +888,10 @@ class Inbox case 'Follow': $following = self::actorFirstOrCreate($obj['object']); - if(!$following) { + if (! $following) { return; } - if(AccountService::blocksDomain($following->id, $profile->domain) == true) { + if (AccountService::blocksDomain($following->id, $profile->domain) == true) { return; } Follower::whereProfileId($profile->id) @@ -892,18 +908,18 @@ class Inbox case 'Like': $objectUri = $obj['object']; - if(!is_string($objectUri)) { - if(is_array($objectUri) && isset($objectUri['id']) && is_string($objectUri['id'])) { + if (! is_string($objectUri)) { + if (is_array($objectUri) && isset($objectUri['id']) && is_string($objectUri['id'])) { $objectUri = $objectUri['id']; } else { return; } } $status = Helpers::statusFirstOrFetch($objectUri); - if(!$status) { + if (! $status) { return; } - if(AccountService::blocksDomain($status->profile_id, $profile->domain) == true) { + if (AccountService::blocksDomain($status->profile_id, $profile->domain) == true) { return; } Like::whereProfileId($profile->id) @@ -917,12 +933,12 @@ class Inbox ->forceDelete(); break; } - return; + } public function handleViewActivity() { - if(!isset( + if (! isset( $this->payload['actor'], $this->payload['object'] )) { @@ -932,19 +948,19 @@ class Inbox $actor = $this->payload['actor']; $obj = $this->payload['object']; - if(!Helpers::validateUrl($actor)) { + if (! Helpers::validateUrl($actor)) { return; } - if(!$obj || !is_array($obj)) { + if (! $obj || ! is_array($obj)) { return; } - if(!isset($obj['type']) || !isset($obj['object']) || $obj['type'] != 'Story') { + if (! isset($obj['type']) || ! isset($obj['object']) || $obj['type'] != 'Story') { return; } - if(!Helpers::validateLocalUrl($obj['object'])) { + if (! Helpers::validateLocalUrl($obj['object'])) { return; } @@ -955,34 +971,33 @@ class Inbox ->whereLocal(true) ->find($storyId); - if(!$story) { + if (! $story) { return; } - if(AccountService::blocksDomain($story->profile_id, $profile->domain) == true) { + if (AccountService::blocksDomain($story->profile_id, $profile->domain) == true) { return; } - if(!FollowerService::follows($profile->id, $story->profile_id)) { + if (! FollowerService::follows($profile->id, $story->profile_id)) { return; } $view = StoryView::firstOrCreate([ 'story_id' => $story->id, - 'profile_id' => $profile->id + 'profile_id' => $profile->id, ]); - if($view->wasRecentlyCreated == true) { + if ($view->wasRecentlyCreated == true) { $story->view_count++; $story->save(); } - return; } public function handleStoryReactionActivity() { - if(!isset( + if (! isset( $this->payload['actor'], $this->payload['id'], $this->payload['inReplyTo'], @@ -997,23 +1012,23 @@ class Inbox $to = $this->payload['to']; $text = Purify::clean($this->payload['content']); - if(parse_url($id, PHP_URL_HOST) !== parse_url($actor, PHP_URL_HOST)) { + if (parse_url($id, PHP_URL_HOST) !== parse_url($actor, PHP_URL_HOST)) { return; } - if(!Helpers::validateUrl($id) || !Helpers::validateUrl($actor)) { + if (! Helpers::validateUrl($id) || ! Helpers::validateUrl($actor)) { return; } - if(!Helpers::validateLocalUrl($storyUrl)) { + if (! Helpers::validateLocalUrl($storyUrl)) { return; } - if(!Helpers::validateLocalUrl($to)) { + if (! Helpers::validateLocalUrl($to)) { return; } - if(Status::whereObjectUrl($id)->exists()) { + if (Status::whereObjectUrl($id)->exists()) { return; } @@ -1023,27 +1038,27 @@ class Inbox $story = Story::whereProfileId($targetProfile->id) ->find($storyId); - if(!$story) { + if (! $story) { return; } - if($story->can_react == false) { + if ($story->can_react == false) { return; } $actorProfile = Helpers::profileFetch($actor); - if(AccountService::blocksDomain($targetProfile->id, $actorProfile->domain) == true) { + if (AccountService::blocksDomain($targetProfile->id, $actorProfile->domain) == true) { return; } - if(!FollowerService::follows($actorProfile->id, $targetProfile->id)) { + if (! FollowerService::follows($actorProfile->id, $targetProfile->id)) { return; } $url = $id; - if(str_ends_with($url, '/activity')) { + if (str_ends_with($url, '/activity')) { $url = substr($url, 0, -9); } @@ -1060,7 +1075,7 @@ class Inbox $status->in_reply_to_profile_id = $story->profile_id; $status->entities = json_encode([ 'story_id' => $story->id, - 'reaction' => $text + 'reaction' => $text, ]); $status->save(); @@ -1074,20 +1089,20 @@ class Inbox 'story_actor_username' => $actorProfile->username, 'story_id' => $story->id, 'story_media_url' => url(Storage::url($story->path)), - 'reaction' => $text + 'reaction' => $text, ]); $dm->save(); Conversation::updateOrInsert( [ 'to_id' => $story->profile_id, - 'from_id' => $actorProfile->id + 'from_id' => $actorProfile->id, ], [ 'type' => 'story:react', 'status_id' => $status->id, 'dm_id' => $dm->id, - 'is_hidden' => false + 'is_hidden' => false, ] ); @@ -1099,12 +1114,11 @@ class Inbox $n->action = 'story:react'; $n->save(); - return; } public function handleStoryReplyActivity() { - if(!isset( + if (! isset( $this->payload['actor'], $this->payload['id'], $this->payload['inReplyTo'], @@ -1119,23 +1133,23 @@ class Inbox $to = $this->payload['to']; $text = Purify::clean($this->payload['content']); - if(parse_url($id, PHP_URL_HOST) !== parse_url($actor, PHP_URL_HOST)) { + if (parse_url($id, PHP_URL_HOST) !== parse_url($actor, PHP_URL_HOST)) { return; } - if(!Helpers::validateUrl($id) || !Helpers::validateUrl($actor)) { + if (! Helpers::validateUrl($id) || ! Helpers::validateUrl($actor)) { return; } - if(!Helpers::validateLocalUrl($storyUrl)) { + if (! Helpers::validateLocalUrl($storyUrl)) { return; } - if(!Helpers::validateLocalUrl($to)) { + if (! Helpers::validateLocalUrl($to)) { return; } - if(Status::whereObjectUrl($id)->exists()) { + if (Status::whereObjectUrl($id)->exists()) { return; } @@ -1145,28 +1159,27 @@ class Inbox $story = Story::whereProfileId($targetProfile->id) ->find($storyId); - if(!$story) { + if (! $story) { return; } - if($story->can_react == false) { + if ($story->can_react == false) { return; } $actorProfile = Helpers::profileFetch($actor); - - if(AccountService::blocksDomain($targetProfile->id, $actorProfile->domain) == true) { + if (AccountService::blocksDomain($targetProfile->id, $actorProfile->domain) == true) { return; } - if(!FollowerService::follows($actorProfile->id, $targetProfile->id)) { + if (! FollowerService::follows($actorProfile->id, $targetProfile->id)) { return; } $url = $id; - if(str_ends_with($url, '/activity')) { + if (str_ends_with($url, '/activity')) { $url = substr($url, 0, -9); } @@ -1183,7 +1196,7 @@ class Inbox $status->in_reply_to_profile_id = $story->profile_id; $status->entities = json_encode([ 'story_id' => $story->id, - 'caption' => $text + 'caption' => $text, ]); $status->save(); @@ -1197,20 +1210,20 @@ class Inbox 'story_actor_username' => $actorProfile->username, 'story_id' => $story->id, 'story_media_url' => url(Storage::url($story->path)), - 'caption' => $text + 'caption' => $text, ]); $dm->save(); Conversation::updateOrInsert( [ 'to_id' => $story->profile_id, - 'from_id' => $actorProfile->id + 'from_id' => $actorProfile->id, ], [ 'type' => 'story:comment', 'status_id' => $status->id, 'dm_id' => $dm->id, - 'is_hidden' => false + 'is_hidden' => false, ] ); @@ -1222,12 +1235,11 @@ class Inbox $n->action = 'story:comment'; $n->save(); - return; } public function handleFlagActivity() { - if(!isset( + if (! isset( $this->payload['id'], $this->payload['type'], $this->payload['actor'], @@ -1239,43 +1251,43 @@ class Inbox $id = $this->payload['id']; $actor = $this->payload['actor']; - if(Helpers::validateLocalUrl($id) || parse_url($id, PHP_URL_HOST) !== parse_url($actor, PHP_URL_HOST)) { + if (Helpers::validateLocalUrl($id) || parse_url($id, PHP_URL_HOST) !== parse_url($actor, PHP_URL_HOST)) { return; } $content = null; - if(isset($this->payload['content'])) { - if(strlen($this->payload['content']) > 5000) { - $content = Purify::clean(substr($this->payload['content'], 0, 5000) . ' ... (truncated message due to exceeding max length)'); + if (isset($this->payload['content'])) { + if (strlen($this->payload['content']) > 5000) { + $content = Purify::clean(substr($this->payload['content'], 0, 5000).' ... (truncated message due to exceeding max length)'); } else { $content = Purify::clean($this->payload['content']); } } $object = $this->payload['object']; - if(empty($object) || (!is_array($object) && !is_string($object))) { + if (empty($object) || (! is_array($object) && ! is_string($object))) { return; } - if(is_array($object) && count($object) > 100) { + if (is_array($object) && count($object) > 100) { return; } $objects = collect([]); $accountId = null; - foreach($object as $objectUrl) { - if(!Helpers::validateLocalUrl($objectUrl)) { + foreach ($object as $objectUrl) { + if (! Helpers::validateLocalUrl($objectUrl)) { return; } - if(str_contains($objectUrl, '/users/')) { + if (str_contains($objectUrl, '/users/')) { $username = last(explode('/', $objectUrl)); $profileId = Profile::whereUsername($username)->first(); - if($profileId) { + if ($profileId) { $accountId = $profileId->id; } - } else if(str_contains($objectUrl, '/p/')) { + } elseif (str_contains($objectUrl, '/p/')) { $postId = last(explode('/', $objectUrl)); $objects->push($postId); } else { @@ -1283,14 +1295,14 @@ class Inbox } } - if(!$accountId && !$objects->count()) { + if (! $accountId && ! $objects->count()) { return; } - if($objects->count()) { + if ($objects->count()) { $obc = $objects->count(); - if($obc > 25) { - if($obc > 30) { + if ($obc > 25) { + if ($obc > 30) { return; } else { $objLimit = $objects->take(20); @@ -1299,7 +1311,7 @@ class Inbox } } $count = Status::whereProfileId($accountId)->find($objects)->count(); - if($obc !== $count) { + if ($obc !== $count) { return; } } @@ -1307,7 +1319,7 @@ class Inbox $instanceHost = parse_url($id, PHP_URL_HOST); $instance = Instance::updateOrCreate([ - 'domain' => $instanceHost + 'domain' => $instanceHost, ]); $report = new RemoteReport; @@ -1318,33 +1330,59 @@ class Inbox $report->instance_id = $instance->id; $report->report_meta = [ 'actor' => $actor, - 'object' => $object + 'object' => $object, ]; $report->save(); - return; } public function handleUpdateActivity() { $activity = $this->payload['object']; - if(!isset($activity['type'], $activity['id'])) { + if (! isset($activity['type'], $activity['id'])) { return; } - if(!Helpers::validateUrl($activity['id'])) { + if (! Helpers::validateUrl($activity['id'])) { return; } - if($activity['type'] === 'Note') { - if(Status::whereObjectUrl($activity['id'])->exists()) { + if ($activity['type'] === 'Note') { + if (Status::whereObjectUrl($activity['id'])->exists()) { StatusRemoteUpdatePipeline::dispatch($activity); } - } else if ($activity['type'] === 'Person') { - if(UpdatePersonValidator::validate($this->payload)) { + } elseif ($activity['type'] === 'Person') { + if (UpdatePersonValidator::validate($this->payload)) { HandleUpdateActivity::dispatch($this->payload)->onQueue('low'); } } } + + public function handleMoveActivity() + { + $actor = $this->payload['actor']; + $activity = $this->payload['object']; + $target = $this->payload['target']; + if ( + ! Helpers::validateUrl($actor) || + ! Helpers::validateUrl($activity) || + ! Helpers::validateUrl($target) + ) { + return; + } + + Bus::chain([ + new ProcessMovePipeline($target, $activity), + new MoveMigrateFollowersPipeline($target, $activity), + new UnfollowLegacyAccountMovePipeline($target, $activity), + new CleanupLegacyAccountMovePipeline($target, $activity), + ]) + ->catch(function (Throwable $e) { + Log::error($e); + }) + ->onQueue('move') + ->delay(now()->addMinutes(random_int(1, 3))) + ->dispatch(); + } } diff --git a/app/Util/ActivityPub/Validator/MoveValidator.php b/app/Util/ActivityPub/Validator/MoveValidator.php new file mode 100644 index 000000000..a7ca43297 --- /dev/null +++ b/app/Util/ActivityPub/Validator/MoveValidator.php @@ -0,0 +1,23 @@ + 'required', + 'type' => [ + 'required', + Rule::in(['Move']), + ], + 'actor' => 'required|url', + 'object' => 'required|url', + 'target' => 'required|url', + ])->passes(); + } +} diff --git a/config/horizon.php b/config/horizon.php index a155e9536..0a4add6f8 100644 --- a/config/horizon.php +++ b/config/horizon.php @@ -92,6 +92,7 @@ return [ 'redis:intbg' => 30, 'redis:adelete' => 30, 'redis:groups' => 30, + 'redis:move' => 30, ], /* @@ -175,7 +176,7 @@ return [ 'production' => [ 'supervisor-1' => [ 'connection' => 'redis', - 'queue' => ['high', 'default', 'follow', 'shared', 'inbox', 'feed', 'low', 'story', 'delete', 'mmo', 'intbg', 'groups', 'adelete'], + 'queue' => ['high', 'default', 'follow', 'shared', 'inbox', 'feed', 'low', 'story', 'delete', 'mmo', 'intbg', 'groups', 'adelete', 'move'], 'balance' => env('HORIZON_BALANCE_STRATEGY', 'auto'), 'minProcesses' => env('HORIZON_MIN_PROCESSES', 1), 'maxProcesses' => env('HORIZON_MAX_PROCESSES', 20), @@ -189,7 +190,7 @@ return [ 'local' => [ 'supervisor-1' => [ 'connection' => 'redis', - 'queue' => ['high', 'default', 'follow', 'shared', 'inbox', 'feed', 'low', 'story', 'delete', 'mmo', 'intbg', 'groups', 'adelete'], + 'queue' => ['high', 'default', 'follow', 'shared', 'inbox', 'feed', 'low', 'story', 'delete', 'mmo', 'intbg', 'groups', 'adelete', 'move'], 'balance' => 'auto', 'minProcesses' => 1, 'maxProcesses' => 20,