headers = $headers; $this->profile = $profile; $this->payload = $payload; } public function handle() { $this->handleVerb(); } public function authenticatePayload() { 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(); break; case 'Follow': $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 verifyNoteAttachment() { $activity = $this->payload['object']; 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) { 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']; $url = $activity['id']; if(!Helpers::statusFirstOrFetch($url, true)) { return; } } public function handleNoteCreate() { $activity = $this->payload['object']; $actor = $this->actorFirstOrCreate($this->payload['actor']); if(!$actor || $actor->domain == null) { return; } if(Helpers::userInAudience($this->profile, $this->payload) == false) { //Log::error('AP:inbox:userInAudience:false - Activity#'.$this->logger->id); return; } $url = $activity['id']; if(Status::whereUrl($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 = $url; $status->save(); return $status; }); Helpers::importNoteAttachment($activity, $status); } 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) { return; } // send notification Notification::firstOrCreate([ 'profile_id' => $target->id, 'actor_id' => $actor->id, 'action' => 'follow', 'message' => $follower->toText(), 'rendered' => $follower->toHtml(), 'item_id' => $target->id, 'item_type' => 'App\Profile' ]); // send Accept to remote profile $accept = [ '@context' => 'https://www.w3.org/ns/activitystreams', 'id' => $target->permalink().'#accepts/follows/', 'type' => 'Accept', 'actor' => $target->permalink(), 'object' => [ 'id' => $actor->permalink('#follows/'.$target->id), 'type' => 'Follow', 'actor' => $actor->permalink(), 'object' => $target->permalink() ] ]; Helpers::sendSignedObject($target, $actor->inbox_url, $accept); } } public function handleAnnounceActivity() { $actor = $this->actorFirstOrCreate($this->payload['actor']); $activity = $this->payload['object']; if(!$actor || $actor->domain == null) { return; } if(Helpers::validateLocalUrl($activity) == false) { return; } $parent = Helpers::statusFirstOrFetch($activity, true); if(!$parent) { return; } $status = Status::firstOrCreate([ 'profile_id' => $actor->id, 'in_reply_to_id' => $parent->id, 'type' => 'reply' ]); Notification::firstOrCreate([ 'profile_id' => $parent->profile->id, 'actor_id' => $actor->id, 'action' => 'share', 'message' => $status->replyToText(), 'rendered' => $status->replyToHtml(), 'item_id' => $parent->id, 'item_type' => 'App\Status' ]); } public function handleAcceptActivity() { } public function handleDeleteActivity() { $actor = $this->payload['actor']; $obj = $this->payload['object']; if(is_string($obj) && Helpers::validateUrl($obj)) { // actor object detected } else if (is_array($obj) && isset($obj['type']) && $obj['type'] == 'Tombstone') { // tombstone detected $status = Status::whereUri($obj['id'])->first(); if($status == null) { return; } $status->forceDelete(); } } 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); } public function handleRejectActivity() { } public function handleUndoActivity() { $actor = $this->payload['actor']; $profile = self::actorFirstOrCreate($actor); $obj = $this->payload['object']; switch ($obj['type']) { case 'Like': $status = Helpers::statusFirstOrFetch($obj['object']); Like::whereProfileId($profile->id) ->whereStatusId($status->id) ->forceDelete(); break; case 'Announce': $parent = Helpers::statusFirstOrFetch($obj['object']); $status = Status::whereProfileId($profile->id) ->whereReblogOfId($parent->id) ->firstOrFail(); Notification::whereProfileId($parent->profile->id) ->whereActorId($profile->id) ->whereAction('share') ->whereItemId($status->id) ->whereItemType('App\Status') ->forceDelete(); $status->forceDelete(); break; } } }