You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
pixelfed/app/Jobs/MovePipeline/MoveMigrateFollowersPipelin...

159 lines
4.8 KiB
PHP

<?php
namespace App\Jobs\MovePipeline;
use App\Follower;
use App\Http\Controllers\FollowerController;
use App\Profile;
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;
use Illuminate\Support\Facades\Log;
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;
// Verify target and actor are provided
if (! $target) {
Log::info('MoveMigrateFollowersPipeline: No target provided, skipping job');
return;
}
if (! $actor) {
Log::info('MoveMigrateFollowersPipeline: No actor provided, skipping job');
return;
}
try {
$targetAccount = Helpers::profileFetch($target);
$actorAccount = Helpers::profileFetch($actor);
} catch (\Exception $e) {
Log::warning('MoveMigrateFollowersPipeline: Failed to fetch profiles: '.$e->getMessage());
throw $e;
}
if (! $targetAccount || ! $actorAccount) {
Log::warning('MoveMigrateFollowersPipeline: Could not fetch target or actor accounts');
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', 'followers.local_profile')
->chunkById(100, function ($followers) use ($targetInbox, $targetPid, $targetAccount) {
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,
]);
// If the remote user has migrated to a different instance,
// send a follow request for each local follower to the new
// instance
if ($targetInbox && $follower->local_profile) {
$followerProfile = Profile::find($follower->id);
(new FollowerController)->sendFollow($followerProfile, $targetAccount);
}
}
}, 'profiles.id', 'id');
}
}