Browse Source

Add RemoteFollowPipeline

Still a WIP
Daniel Supernault 7 years ago
parent
commit
e2afe175f4

+ 226 - 0
app/Jobs/RemoteFollowPipeline/RemoteFollowImportRecent.php

@@ -0,0 +1,226 @@
+<?php
+
+namespace App\Jobs\RemoteFollowPipeline;
+
+use Zttp\Zttp;
+use Log, Storage;
+use Carbon\Carbon;
+use Illuminate\Http\File;
+use App\{Media, Profile, Status};
+use Illuminate\Bus\Queueable;
+use Illuminate\Queue\SerializesModels;
+use Illuminate\Queue\InteractsWithQueue;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Foundation\Bus\Dispatchable;
+use App\Jobs\StatusPipeline\NewStatusPipeline;
+
+class RemoteFollowImportRecent implements ShouldQueue
+{
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+    protected $actor;
+    protected $profile;
+    protected $outbox;
+    protected $mediaCount;
+    protected $cursor;
+    protected $nextUrl;
+    protected $supported;
+
+    /**
+     * Create a new job instance.
+     *
+     * @return void
+     */
+    public function __construct($actorObject, $profile)
+    {
+        $this->actor = $actorObject;
+        $this->profile = $profile;
+        $this->cursor = 1;
+        $this->mediaCount = 0;
+        $this->supported = [
+            'image/jpg',
+            'image/jpeg',
+            'image/png',
+            'image/gif'
+        ];
+    }
+
+    /**
+     * Execute the job.
+     *
+     * @return void
+     */
+    public function handle()
+    {
+        $outbox = $this->fetchOutbox();
+    }
+
+    public function fetchOutbox($url = false)
+    {
+        Log::info(json_encode($url));
+        $url = ($url == false) ? $this->actor['outbox'] : $url;
+
+        $response = Zttp::withHeaders([
+            'User-Agent' => 'PixelFedBot v0.1 - https://pixelfed.org'
+        ])->get($url);
+
+        $this->outbox = $response->json();
+        $this->parseOutbox($this->outbox);
+    }
+
+    public function parseOutbox($outbox)
+    {
+        $types = ['OrderedCollection', 'OrderedCollectionPage'];
+
+        if(isset($outbox['totalItems']) && $outbox['totalItems'] < 1) {
+            // Skip remote fetch, not enough posts
+            Log::info('not enough items');
+            return;
+        }
+
+        if(isset($outbox['type']) && in_array($outbox['type'], $types)) {
+            Log::info('handle ordered collection');
+            $this->handleOrderedCollection();
+        }
+    }
+
+    public function handleOrderedCollection()
+    {
+        $outbox = $this->outbox;
+
+        if(!isset($outbox['next']) && !isset($outbox['first']['next']) && $this->cursor !== 1) {
+            $this->cursor = 40;
+            $outbox['next'] = false;
+        }
+
+        if($outbox['type'] == 'OrderedCollectionPage') {
+            $this->nextUrl = $outbox['next'];
+        }
+
+        if(isset($outbox['first']) && !is_array($outbox['first'])) {
+            // Mastodon detected
+            Log::info('Mastodon detected...');
+            $this->nextUrl = $outbox['first'];
+            return $this->fetchOutbox($this->nextUrl);
+        } else {
+            // Pleroma detected.
+            $this->nextUrl = isset($outbox['next']) ? $outbox['next'] : (isset($outbox['first']['next']) ? $outbox['first']['next'] : $outbox['next']);
+            Log::info('Checking ordered items...');
+            $orderedItems = isset($outbox['orderedItems']) ? $outbox['orderedItems'] : $outbox['first']['orderedItems'];
+        }
+
+
+        foreach($orderedItems as $item) {
+            Log::info('Parsing items...');
+            $parsed = $this->parseObject($item);
+            if($parsed !== 0) {
+                Log::info('Found media!');
+                $this->importActivity($item);
+            }
+        }
+
+        if($this->cursor < 40 && $this->mediaCount < 9) {
+            $this->cursor++;
+            $this->mediaCount++;
+            $this->fetchOutbox($this->nextUrl);
+        }
+
+    }
+
+    public function parseObject($parsed)
+    {
+        if($parsed['type'] !== 'Create') {
+            return 0;
+        }
+
+        $activity = $parsed['object'];
+
+        if(isset($activity['attachment']) && !empty($activity['attachment'])) {
+            return $this->detectSupportedMedia($activity['attachment']);
+        }
+    }
+
+    public function detectSupportedMedia($attachments)
+    {
+        $supported = $this->supported;
+        $count = 0;
+
+        foreach($attachments as $media) {
+            $mime = $media['mediaType'];
+            $count = in_array($mime, $supported) ? ($count + 1) : $count;
+        }
+
+        return $count;
+    }
+
+    public function importActivity($activity)
+    {
+        $profile = $this->profile;
+        $supported = $this->supported;
+        $attachments = $activity['object']['attachment'];
+        $caption = str_limit($activity['object']['content'], 125);
+
+        if(Status::whereUrl($activity['id'])->count() !== 0) {
+            return true;
+        }
+
+        $status = new Status;
+        $status->profile_id = $profile->id;
+        $status->url = $activity['id'];
+        $status->local = false;
+        $status->caption = strip_tags($caption);
+        $status->created_at = Carbon::parse($activity['published']);
+
+        $count = 0;
+
+        foreach($attachments as $media) {
+            Log::info($media['mediaType'] . ' - ' . $media['url']);
+            $url = $media['url'];
+            $mime = $media['mediaType'];
+            if(!in_array($mime, $supported)) {
+                Log::info('Invalid media, skipping. ' . $mime);
+                continue;
+            }
+            $count++;
+
+            if($count === 1) {
+                $status->save();
+            }
+            $this->importMedia($url, $mime, $status);
+        }
+        Log::info(count($attachments) . ' media found...');
+
+        if($count !== 0) {
+            NewStatusPipeline::dispatch($status, $status->media->first());
+        }
+    }
+
+    public function importMedia($url, $mime, $status)
+    {
+      $user = $this->profile;
+      $monthHash = hash('sha1', date('Y') . date('m'));
+      $userHash = hash('sha1', $user->id . (string) $user->created_at);
+      $storagePath = "public/m/{$monthHash}/{$userHash}";
+      try {
+          $info = pathinfo($url);
+          $img = file_get_contents($url);
+          $file = '/tmp/' . str_random(12) . $info['basename'];
+          file_put_contents($file, $img);
+          $path = Storage::putFile($storagePath, new File($file), 'public');
+          
+          $media = new Media;
+          $media->status_id = $status->id;
+          $media->profile_id = $status->profile_id;
+          $media->user_id = null;
+          $media->media_path = $path;
+          $media->size = 0;
+          $media->mime = $mime;
+          $media->save();
+
+          return true;
+      } catch (Exception $e) {
+          return false;
+      }
+    }
+
+}

+ 105 - 0
app/Jobs/RemoteFollowPipeline/RemoteFollowPipeline.php

@@ -0,0 +1,105 @@
+<?php
+
+namespace App\Jobs\RemoteFollowPipeline;
+
+use Zttp\Zttp;
+use App\{Profile};
+use GuzzleHttp\Client;
+use HttpSignatures\Context;
+use HttpSignatures\GuzzleHttpSignatures;
+use App\Jobs\RemoteFollowPipeline\RemoteFollowImportRecent;
+use App\Jobs\AvatarPipeline\CreateAvatar;
+use Illuminate\Bus\Queueable;
+use Illuminate\Queue\SerializesModels;
+use Illuminate\Queue\InteractsWithQueue;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Foundation\Bus\Dispatchable;
+
+class RemoteFollowPipeline implements ShouldQueue
+{
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+    protected $url;
+    protected $follower;
+    protected $response;
+
+    /**
+     * Create a new job instance.
+     *
+     * @return void
+     */
+    public function __construct($follower, $url)
+    {
+        $this->follower = $follower;
+        $this->url = $url;
+    }
+
+    /**
+     * Execute the job.
+     *
+     * @return void
+     */
+    public function handle()
+    {
+        $follower = $this->follower;
+        $url = $this->url;
+
+        if(Profile::whereRemoteUrl($url)->count() !== 0) {
+            return true;
+        }
+
+        $this->discover($url);
+        return true;
+    }
+
+    public function discover($url)
+    {
+        $context = new Context([
+            'keys' => ['examplekey' => 'secret-key-here'],
+            'algorithm' => 'hmac-sha256',
+            'headers' => ['(request-target)', 'date'],
+        ]);
+
+        $handlerStack = GuzzleHttpSignatures::defaultHandlerFromContext($context);
+        $client = new Client(['handler' => $handlerStack]);
+        $response = Zttp::withHeaders([
+            'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
+            'User-Agent' => 'PixelFedBot v0.1 - https://pixelfed.org'
+        ])->get($url);
+        $this->response = $response->json();
+
+        $this->storeProfile();
+    }
+
+    public function storeProfile()
+    {
+        $res = $this->response;
+        $domain = parse_url($res['url'], PHP_URL_HOST);
+        $username = $res['preferredUsername'];
+        $remoteUsername = "@{$username}@{$domain}";
+
+        $profile = new Profile;
+        $profile->user_id = null;
+        $profile->domain = $domain;
+        $profile->username = $remoteUsername;
+        $profile->name = $res['name'];
+        $profile->bio = str_limit($res['summary'], 125);
+        $profile->sharedInbox = $res['endpoints']['sharedInbox'];
+        $profile->remote_url = $res['url'];
+        $profile->save();
+
+        RemoteFollowImportRecent::dispatch($this->response, $profile);
+        CreateAvatar::dispatch($profile);
+    }
+
+    public function sendActivity()
+    {
+        $res = $this->response;
+        $url = $res['inbox'];
+
+        $activity = Zttp::withHeaders(['Content-Type' => 'application/activity+json'])->post($url, [
+            'type' => 'Follow',
+            'object' => $this->follower->url()
+        ]);
+    }
+}