Merge pull request #5282 from pixelfed/staging

Fix Move inbox handler
This commit is contained in:
daniel 2024-09-11 05:11:59 -06:00 committed by GitHub
commit 32d4b63a23
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 1340 additions and 441 deletions

View file

@ -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)

View file

@ -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');

View file

@ -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');

View file

@ -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');

View file

@ -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));
}
/**

View file

@ -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,7 +25,8 @@ class BookmarkController extends Controller
$user = $request->user();
$status = Status::findOrFail($request->input('item'));
$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);

View file

@ -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;
}

View file

@ -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();

View file

@ -0,0 +1,103 @@
<?php
namespace App\Jobs\MovePipeline;
use App\Follower;
use App\Profile;
use App\Services\AccountService;
use App\UserFilter;
use App\Util\ActivityPub\Helpers;
use DateTime;
use Exception;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\Middleware\ThrottlesExceptions;
use Illuminate\Queue\Middleware\WithoutOverlapping;
class CleanupLegacyAccountMovePipeline implements ShouldQueue
{
use Queueable;
public $target;
public $activity;
/**
* The number of times the job may be attempted.
*
* @var int
*/
public $tries = 6;
/**
* The maximum number of unhandled exceptions to allow before failing.
*
* @var int
*/
public $maxExceptions = 3;
/**
* Create a new job instance.
*/
public function __construct($target, $activity)
{
$this->target = $target;
$this->activity = $activity;
}
/**
* Get the middleware the job should pass through.
*
* @return array<int, object>
*/
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']);
}
}
}

View file

@ -0,0 +1,131 @@
<?php
namespace App\Jobs\MovePipeline;
use App\Follower;
use App\Util\ActivityPub\Helpers;
use DateTime;
use DB;
use Exception;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\Middleware\ThrottlesExceptionsWithRedis;
use Illuminate\Queue\Middleware\WithoutOverlapping;
class MoveMigrateFollowersPipeline implements ShouldQueue
{
use Queueable;
public $target;
public $activity;
/**
* The number of times the job may be attempted.
*
* @var int
*/
public $tries = 15;
/**
* The maximum number of unhandled exceptions to allow before failing.
*
* @var int
*/
public $maxExceptions = 5;
/**
* The number of seconds the job can run before timing out.
*
* @var int
*/
public $timeout = 900;
/**
* Create a new job instance.
*/
public function __construct($target, $activity)
{
$this->target = $target;
$this->activity = $activity;
}
/**
* Get the middleware the job should pass through.
*
* @return array<int, object>
*/
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');
}
}

View file

@ -0,0 +1,113 @@
<?php
namespace App\Jobs\MovePipeline;
use App\Util\ActivityPub\HttpSignature;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\Middleware\ThrottlesExceptions;
use Illuminate\Queue\Middleware\WithoutOverlapping;
class MoveSendFollowPipeline implements ShouldQueue
{
use Queueable;
public $follower;
public $targetInbox;
public $targetPid;
public $target;
/**
* The number of times the job may be attempted.
*
* @var int
*/
public $tries = 5;
/**
* The maximum number of unhandled exceptions to allow before failing.
*
* @var int
*/
public $maxExceptions = 3;
/**
* Get the middleware the job should pass through.
*
* @return array<int, object>
*/
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) {
}
}
}

View file

@ -0,0 +1,119 @@
<?php
namespace App\Jobs\MovePipeline;
use App\Util\ActivityPub\HttpSignature;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\Middleware\ThrottlesExceptions;
use Illuminate\Queue\Middleware\WithoutOverlapping;
class MoveSendUndoFollowPipeline implements ShouldQueue
{
use Queueable;
public $follower;
public $targetInbox;
public $targetPid;
public $actor;
/**
* The number of times the job may be attempted.
*
* @var int
*/
public $tries = 5;
/**
* The maximum number of unhandled exceptions to allow before failing.
*
* @var int
*/
public $maxExceptions = 3;
/**
* Get the middleware the job should pass through.
*
* @return array<int, object>
*/
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) {
}
}
}

View file

@ -0,0 +1,156 @@
<?php
namespace App\Jobs\MovePipeline;
use App\Services\ActivityPubFetchService;
use App\Util\ActivityPub\Helpers;
use DateTime;
use Exception;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\Middleware\ThrottlesExceptionsWithRedis;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Support\Arr;
class ProcessMovePipeline implements ShouldQueue
{
use Queueable;
public $target;
public $activity;
/**
* The number of times the job may be attempted.
*
* @var int
*/
public $tries = 15;
/**
* The maximum number of unhandled exceptions to allow before failing.
*
* @var int
*/
public $maxExceptions = 5;
/**
* The number of seconds the job can run before timing out.
*
* @var int
*/
public $timeout = 120;
/**
* Create a new job instance.
*/
public function __construct($target, $activity)
{
$this->target = $target;
$this->activity = $activity;
}
/**
* Get the middleware the job should pass through.
*
* @return array<int, object>
*/
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));
}
}

View file

@ -0,0 +1,111 @@
<?php
namespace App\Jobs\MovePipeline;
use App\Util\ActivityPub\Helpers;
use DateTime;
use DB;
use Exception;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\Middleware\ThrottlesExceptions;
use Illuminate\Queue\Middleware\WithoutOverlapping;
class UnfollowLegacyAccountMovePipeline implements ShouldQueue
{
use Queueable;
public $target;
public $activity;
/**
* The number of times the job may be attempted.
*
* @var int
*/
public $tries = 6;
/**
* The maximum number of unhandled exceptions to allow before failing.
*
* @var int
*/
public $maxExceptions = 3;
/**
* Create a new job instance.
*/
public function __construct($target, $activity)
{
$this->target = $target;
$this->activity = $activity;
}
/**
* Get the middleware the job should pass through.
*
* @return array<int, object>
*/
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');
}
}

View file

@ -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();
});
@ -291,7 +291,6 @@ class AccountService
$user->save();
Cache::put($key, 1, 14400);
}
}
public static function blocksDomain($pid, $domain = false)

View file

@ -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();
}
}

View file

@ -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() :

View file

@ -2,10 +2,9 @@
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
{
@ -60,7 +59,7 @@ class NodeinfoService
$hrefDomain = parse_url($href, PHP_URL_HOST);
if ($domain !== $hrefDomain) {
return 60;
return false;
}
try {
@ -77,6 +76,7 @@ class NodeinfoService
} catch (\Exception $e) {
return false;
}
return $res->json();
}
}

View file

@ -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()],

View file

@ -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();
}

View file

@ -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,
]
);

View file

@ -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';

View file

@ -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;
@ -191,7 +209,6 @@ class Inbox
break;
}
return;
}
public function handleCreateActivity()
@ -226,6 +243,7 @@ class Inbox
if ($activity['type'] == 'Question') {
$this->handlePollCreate();
return;
}
@ -236,6 +254,7 @@ class Inbox
parse_url($to[0], PHP_URL_HOST) == config('pixelfed.domain.app')
) {
$this->handleDirectMessage();
return;
}
@ -248,7 +267,7 @@ class Inbox
}
$this->handleNoteCreate();
}
return;
}
public function handleNoteReply()
@ -263,7 +282,7 @@ class Inbox
$url = isset($activity['url']) ? $activity['url'] : $activity['id'];
Helpers::statusFirstOrFetch($url, true);
return;
}
public function handlePollCreate()
@ -275,7 +294,7 @@ class Inbox
}
$url = isset($activity['url']) ? $activity['url'] : $activity['id'];
Helpers::statusFirstOrFetch($url);
return;
}
public function handleNoteCreate()
@ -293,6 +312,7 @@ class Inbox
Helpers::validateLocalUrl($activity['inReplyTo'])
) {
$this->handlePollVote();
return;
}
@ -321,7 +341,7 @@ class Inbox
$actor,
$activity
);
return;
}
public function handlePollVote()
@ -376,7 +396,6 @@ class Inbox
PollService::del($status->id);
return;
}
public function handleDirectMessage()
@ -436,13 +455,13 @@ 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,
]
);
@ -459,7 +478,7 @@ class Inbox
continue;
}
$media = new Media();
$media = new Media;
$media->remote_media = true;
$media->status_id = $status->id;
$media->profile_id = $status->profile_id;
@ -492,7 +511,7 @@ class Inbox
$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();
}
@ -505,7 +524,7 @@ class Inbox
->exists();
if ($profile->domain == null && $hidden == false && ! $nf) {
$notification = new Notification();
$notification = new Notification;
$notification->profile_id = $profile->id;
$notification->actor_id = $actor->id;
$notification->action = 'dm';
@ -514,7 +533,6 @@ class Inbox
$notification->save();
}
return;
}
public function handleFollowActivity()
@ -554,7 +572,7 @@ class Inbox
'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;
@ -576,8 +594,8 @@ class Inbox
'id' => $this->payload['id'],
'actor' => $actor->permalink(),
'type' => 'Follow',
'object' => $target->permalink()
]
'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()
@ -616,7 +633,7 @@ class Inbox
$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()
@ -682,7 +698,6 @@ class Inbox
$request->delete();
return;
}
public function handleDeleteActivity()
@ -701,6 +716,7 @@ class Inbox
return;
}
DeleteRemoteProfilePipeline::dispatch($profile)->onQueue('inbox');
return;
} else {
if (! isset(
@ -727,6 +743,7 @@ class Inbox
return;
}
DeleteRemoteProfilePipeline::dispatch($profile)->onQueue('inbox');
return;
break;
@ -752,6 +769,7 @@ class Inbox
}
}
RemoteStatusDelete::dispatch($status)->onQueue('high');
return;
break;
@ -761,6 +779,7 @@ class Inbox
if ($story) {
StoryExpire::dispatch($story)->onQueue('story');
}
return;
break;
@ -769,7 +788,7 @@ class Inbox
break;
}
}
return;
}
public function handleLikeActivity()
@ -801,7 +820,7 @@ class Inbox
$like = Like::firstOrCreate([
'profile_id' => $profile->id,
'status_id' => $status->id
'status_id' => $status->id,
]);
if ($like->wasRecentlyCreated == true) {
@ -810,12 +829,9 @@ class Inbox
LikePipeline::dispatch($like);
}
return;
}
public function handleRejectActivity()
{
}
public function handleRejectActivity() {}
public function handleUndoActivity()
{
@ -917,7 +933,7 @@ class Inbox
->forceDelete();
break;
}
return;
}
public function handleViewActivity()
@ -969,7 +985,7 @@ class Inbox
$view = StoryView::firstOrCreate([
'story_id' => $story->id,
'profile_id' => $profile->id
'profile_id' => $profile->id,
]);
if ($view->wasRecentlyCreated == true) {
@ -977,7 +993,6 @@ class Inbox
$story->save();
}
return;
}
public function handleStoryReactionActivity()
@ -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,7 +1114,6 @@ class Inbox
$n->action = 'story:react';
$n->save();
return;
}
public function handleStoryReplyActivity()
@ -1155,7 +1169,6 @@ class Inbox
$actorProfile = Helpers::profileFetch($actor);
if (AccountService::blocksDomain($targetProfile->id, $actorProfile->domain) == true) {
return;
}
@ -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,7 +1235,6 @@ class Inbox
$n->action = 'story:comment';
$n->save();
return;
}
public function handleFlagActivity()
@ -1307,7 +1319,7 @@ class Inbox
$instanceHost = parse_url($id, PHP_URL_HOST);
$instance = Instance::updateOrCreate([
'domain' => $instanceHost
'domain' => $instanceHost,
]);
$report = new RemoteReport;
@ -1318,11 +1330,10 @@ class Inbox
$report->instance_id = $instance->id;
$report->report_meta = [
'actor' => $actor,
'object' => $object
'object' => $object,
];
$report->save();
return;
}
public function handleUpdateActivity()
@ -1347,4 +1358,31 @@ class Inbox
}
}
}
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();
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace App\Util\ActivityPub\Validator;
use Illuminate\Validation\Rule;
use Validator;
class MoveValidator
{
public static function validate($payload)
{
return Validator::make($payload, [
'@context' => 'required',
'type' => [
'required',
Rule::in(['Move']),
],
'actor' => 'required|url',
'object' => 'required|url',
'target' => 'required|url',
])->passes();
}
}

View file

@ -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,