diff --git a/app/Util/ActivityPub/Inbox.php b/app/Util/ActivityPub/Inbox.php index 7a0484d7a..83d5578f9 100644 --- a/app/Util/ActivityPub/Inbox.php +++ b/app/Util/ActivityPub/Inbox.php @@ -2,18 +2,30 @@ namespace App\Util\ActivityPub; -use App\Like; -use App\Profile; +use Cache, DB, Log, Redis, Validator; +use App\{ + Activity, + Follower, + FollowRequest, + Like, + Notification, + Profile, + Status +}; +use Carbon\Carbon; +use App\Util\ActivityPub\Helpers; +use App\Jobs\LikePipeline\LikePipeline; class Inbox { - protected $request; + protected $headers; protected $profile; protected $payload; + protected $logger; - public function __construct($request, Profile $profile, $payload) + public function __construct($headers, $profile, $payload) { - $this->request = $request; + $this->headers = $headers; $this->profile = $profile; $this->payload = $payload; } @@ -25,15 +37,31 @@ class Inbox public function authenticatePayload() { - // todo + try { + $signature = Helpers::validateSignature($this->headers, $this->payload); + $payload = Helpers::validateObject($this->payload); + if($signature == false) { + return; + } + } catch (Exception $e) { + return; + } + $this->payloadLogger(); + } + public function payloadLogger() + { + $logger = new Activity; + $logger->data = json_encode($this->payload); + $logger->save(); + $this->logger = $logger; + Log::info('AP:inbox:activity:new:'.$this->logger->id); $this->handleVerb(); } public function handleVerb() { $verb = $this->payload['type']; - switch ($verb) { case 'Create': $this->handleCreateActivity(); @@ -43,43 +71,254 @@ class Inbox $this->handleFollowActivity(); break; + case 'Announce': + $this->handleAnnounceActivity(); + break; + + case 'Accept': + $this->handleAcceptActivity(); + break; + + case 'Delete': + $this->handleDeleteActivity(); + break; + + case 'Like': + $this->handleLikeActivity(); + break; + + case 'Reject': + $this->handleRejectActivity(); + break; + + case 'Undo': + $this->handleUndoActivity(); + break; + default: // TODO: decide how to handle invalid verbs. break; } } - public function handleCreateActivity() + public function verifyNoteAttachment() { - // todo - } + $activity = $this->payload['object']; - public function handleFollowActivity() - { - $actor = $this->payload['object']; - $target = $this->profile; + if(isset($activity['inReplyTo']) && + !empty($activity['inReplyTo']) && + Helpers::validateUrl($activity['inReplyTo']) + ) { + // reply detected, skip attachment check + return true; + } + + $valid = Helpers::verifyAttachments($activity); + + return $valid; } public function actorFirstOrCreate($actorUrl) { - if (Profile::whereRemoteUrl($actorUrl)->count() !== 0) { - return Profile::whereRemoteUrl($actorUrl)->firstOrFail(); + return Helpers::profileFirstOrNew($actorUrl); + } + + public function handleCreateActivity() + { + $activity = $this->payload['object']; + if(!$this->verifyNoteAttachment()) { + return; + } + if($activity['type'] == 'Note' && !empty($activity['inReplyTo'])) { + $this->handleNoteReply(); + + } elseif($activity['type'] == 'Note' && !empty($activity['attachment'])) { + $this->handleNoteCreate(); + } + } + + public function handleNoteReply() + { + $activity = $this->payload['object']; + $actor = $this->actorFirstOrCreate($this->payload['actor']); + $inReplyTo = $activity['inReplyTo']; + + if(!Helpers::statusFirstOrFetch($activity['url'], true)) { + $this->logger->delete(); + return; } - $res = (new DiscoverActor($url))->discover(); + $this->logger->to_id = $this->profile->id; + $this->logger->from_id = $actor->id; + $this->logger->processed_at = Carbon::now(); + $this->logger->save(); + } - $domain = parse_url($res['url'], PHP_URL_HOST); - $username = $res['preferredUsername']; - $remoteUsername = "@{$username}@{$domain}"; + public function handleNoteCreate() + { + $activity = $this->payload['object']; + $actor = $this->actorFirstOrCreate($this->payload['actor']); + if(!$actor || $actor->domain == null) { + return; + } - $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(); + if(Helpers::userInAudience($this->profile, $this->payload) == false) { + //Log::error('AP:inbox:userInAudience:false - Activity#'.$this->logger->id); + $logger = Activity::find($this->logger->id); + $logger->delete(); + return; + } + + if(Status::whereUrl($activity['url'])->exists()) { + return; + } + + $status = DB::transaction(function() use($activity, $actor) { + $status = new Status; + $status->profile_id = $actor->id; + $status->caption = strip_tags($activity['content']); + $status->visibility = $status->scope = 'public'; + $status->url = $activity['url']; + $status->save(); + return $status; + }); + + Helpers::importNoteAttachment($activity, $status); + + $logger = Activity::find($this->logger->id); + $logger->to_id = $this->profile->id; + $logger->from_id = $actor->id; + $logger->processed_at = Carbon::now(); + $logger->save(); + } + + public function handleFollowActivity() + { + $actor = $this->actorFirstOrCreate($this->payload['actor']); + if(!$actor || $actor->domain == null) { + return; + } + $target = $this->profile; + if($target->is_private == true) { + // make follow request + FollowRequest::firstOrCreate([ + 'follower_id' => $actor->id, + 'following_id' => $target->id + ]); + // todo: send notification + } else { + // store new follower + $follower = Follower::firstOrCreate([ + 'profile_id' => $actor->id, + 'following_id' => $target->id, + 'local_profile' => empty($actor->domain) + ]); + if($follower->wasRecentlyCreated == false) { + $this->logger->delete(); + return; + } + // send notification + $notification = new Notification(); + $notification->profile_id = $target->id; + $notification->actor_id = $actor->id; + $notification->action = 'follow'; + $notification->message = $follower->toText(); + $notification->rendered = $follower->toHtml(); + $notification->item_id = $target->id; + $notification->item_type = "App\Profile"; + $notification->save(); + + \Cache::forever('notification.'.$notification->id, $notification); + + $redis = Redis::connection(); + + $nkey = config('cache.prefix').':user.'.$target->id.'.notifications'; + $redis->lpush($nkey, $notification->id); + + // send Accept to remote profile + $accept = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => $follower->permalink('/accept'), + 'type' => 'Accept', + 'actor' => $target->permalink(), + 'object' => [ + 'id' => $this->payload['id'], + 'type' => 'Follow', + 'actor' => $target->permalink(), + 'object' => $actor->permalink() + ] + ]; + Helpers::sendSignedObject($target, $actor->inbox_url, $accept); + } + $this->logger->to_id = $target->id; + $this->logger->from_id = $actor->id; + $this->logger->processed_at = Carbon::now(); + $this->logger->save(); + } + + public function handleAnnounceActivity() + { + + } + + public function handleAcceptActivity() + { + + } + + public function handleDeleteActivity() + { + + } + + public function handleLikeActivity() + { + $actor = $this->payload['actor']; + $profile = self::actorFirstOrCreate($actor); + $obj = $this->payload['object']; + if(Helpers::validateLocalUrl($obj) == false) { + return; + } + $status = Helpers::statusFirstOrFetch($obj); + $like = Like::firstOrCreate([ + 'profile_id' => $profile->id, + 'status_id' => $status->id + ]); + + if($like->wasRecentlyCreated == false) { + return; + } + LikePipeline::dispatch($like); + $this->logger->to_id = $status->profile_id; + $this->logger->from_id = $profile->id; + $this->logger->processed_at = Carbon::now(); + $this->logger->save(); + } + + + public function handleRejectActivity() + { + + } + + public function handleUndoActivity() + { + $actor = $this->payload['actor']; + $profile = self::actorFirstOrCreate($actor); + $obj = $this->payload['object']; + $status = Helpers::statusFirstOrFetch($obj['object']); + + switch ($obj['type']) { + case 'Like': + Like::whereProfileId($profile->id) + ->whereStatusId($status->id) + ->delete(); + break; + } + + $this->logger->to_id = $status->profile_id; + $this->logger->from_id = $profile->id; + $this->logger->processed_at = Carbon::now(); + $this->logger->save(); } }