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

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

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

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

File diff suppressed because it is too large Load diff

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,