pixelfed/pixelfed

View on GitHub
app/Console/Commands/SendUpdateActor.php

Summary

Maintainability
C
7 hrs
Test Coverage
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Storage;
use App\Profile;
use App\User;
use App\Instance;
use App\Util\ActivityPub\Helpers;
use Symfony\Component\HttpKernel\Exception\HttpException;

class SendUpdateActor extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'ap:update-actors {--force}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Send Update Actor activities to known remote servers to force updates';

    /**
     * Execute the console command.
     *
     * @return int
     */
    public function handle()
    {
        $totalUserCount = Profile::whereNotNull('user_id')->count();
        $totalInstanceCount = Instance::count();
        $this->info('Found ' . $totalUserCount . ' local accounts and ' . $totalInstanceCount . ' remote instances');

        $task = $this->choice(
            'What do you want to do?',
            [
                'View top instances',
                'Send updates to an instance'
            ],
            0
        );

        if($task === 'View top instances') {
            $this->table(
                ['domain', 'user_count', 'last_synced'],
                Instance::orderByDesc('user_count')->take(20)->get(['domain', 'user_count', 'actors_last_synced_at'])->toArray()
            );
            return Command::SUCCESS;
        } else {
            $domain = $this->anticipate('Enter the instance domain', function ($input) {
                return Instance::where('domain', 'like', '%' . $input . '%')->pluck('domain')->toArray();
            });
            if(!$this->confirm('Are you sure you want to send actor updates to ' . $domain . '?')) {
                return;
            }
            if($cur = Instance::whereDomain($domain)->whereNotNull('actors_last_synced_at')->first()) {
                if(!$this->option('force')) {
                    $this->error('ERROR: Cannot re-sync this instance, it was already synced on ' . $cur->actors_last_synced_at);
                    return;
                }
            }
            $this->touchStorageCache($domain);
            $this->line(' ');
            $this->error('Keep this window open during this process or it will not complete!');
            $sharedInbox = Profile::whereDomain($domain)->whereNotNull('sharedInbox')->first();
            if(!$sharedInbox) {
                $this->error('ERROR: Cannot find the sharedInbox of ' . $domain);
                return;
            }
            $url = $sharedInbox->sharedInbox;
            $this->line(' ');
            $this->info('Found sharedInbox: ' . $url);
            $bar = $this->output->createProgressBar($totalUserCount);
            $bar->start();

            $startCache = $this->getStorageCache($domain);
            User::whereNull('status')->when($startCache, function($query, $startCache) use($bar) {
                $bar->advance($startCache);
                return $query->where('id', '>', $startCache);
            })->chunk(50, function($users) use($bar, $url, $domain) {
                foreach($users as $user) {
                    $this->updateStorageCache($domain, $user->id);
                    $profile = Profile::find($user->profile_id);
                    if(!$profile) {
                        continue;
                    }
                    $body = $this->updateObject($profile);
                    try {
                        Helpers::sendSignedObject($profile, $url, $body);
                    } catch (HttpException $e) {
                        continue;
                    }
                    $bar->advance();
                }
            });
            $bar->finish();
            $this->line(' ');
            $instance = Instance::whereDomain($domain)->firstOrFail();
            $instance->actors_last_synced_at = now();
            $instance->save();
            $this->info('Finished!');
            return Command::SUCCESS;
        }

        return Command::SUCCESS;
    }

    protected function updateObject($profile)
    {
        return [
            '@context' => [
                'https://w3id.org/security/v1',
                'https://www.w3.org/ns/activitystreams',
                [
                    'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers',
                ],
            ],
            'id' => $profile->permalink('#updates/' . time()),
            'actor' => $profile->permalink(),
            'type' => 'Update',
            'object' => $this->actorObject($profile)
        ];
    }

    protected function touchStorageCache($domain)
    {
        $path = 'actor-update-cache/' . $domain;
        if(!Storage::exists($path)) {
            Storage::put($path, "");
        }
    }

    protected function getStorageCache($domain)
    {
        $path = 'actor-update-cache/' . $domain;
        return Storage::get($path);
    }

    protected function updateStorageCache($domain, $value)
    {
        $path = 'actor-update-cache/' . $domain;
        Storage::put($path, $value);
    }

    protected function actorObject($profile)
    {
        $permalink = $profile->permalink();
        return [
            'id'                        => $permalink,
            'type'                      => 'Person',
            'following'                 => $permalink . '/following',
            'followers'                 => $permalink . '/followers',
            'inbox'                     => $permalink . '/inbox',
            'outbox'                    => $permalink . '/outbox',
            'preferredUsername'         => $profile->username,
            'name'                      => $profile->name,
            'summary'                   => $profile->bio,
            'url'                       => $profile->url(),
            'manuallyApprovesFollowers' => (bool) $profile->is_private,
            'publicKey' => [
                'id'           => $permalink . '#main-key',
                'owner'        => $permalink,
                'publicKeyPem' => $profile->public_key,
            ],
            'icon' => [
                'type'      => 'Image',
                'mediaType' => 'image/jpeg',
                'url'       => $profile->avatarUrl(),
            ],
            'endpoints' => [
                'sharedInbox' => config('app.url') . '/f/inbox'
            ]
        ];
    }
}