Skip to content
Snippets Groups Projects
ActorController.php 12.6 KiB
Newer Older
  • Learn to ignore specific revisions
  • <?php
    
    /**
     * @copyright  2021 Podlibre
     * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
     * @link       https://castopod.org/
     */
    
    namespace ActivityPub\Controllers;
    
    use ActivityPub\Objects\OrderedCollectionObject;
    use ActivityPub\Objects\OrderedCollectionPage;
    use CodeIgniter\Controller;
    use CodeIgniter\I18n\Time;
    
    class ActorController extends Controller
    {
        protected $helpers = ['activitypub'];
    
        /**
         * @var \ActivityPub\Entities\Actor
         */
        protected $actor;
    
        /**
         * @var \ActivityPub\Config\ActivityPub
         */
        protected $config;
    
        public function __construct()
        {
            $this->config = config('ActivityPub');
        }
    
        public function _remap($method, ...$params)
        {
            if (count($params) > 0) {
                if (
                    !($this->actor = model('ActorModel')->getActorByUsername(
                        $params[0],
                    ))
                ) {
                    throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
                }
            }
            unset($params[0]);
    
            return $this->$method(...$params);
        }
    
        public function index()
        {
            $actorObjectClass = $this->config->actorObject;
            $actorObject = new $actorObjectClass($this->actor);
    
            return $this->response
                ->setContentType('application/activity+json')
                ->setBody($actorObject->toJSON());
        }
    
        /**
         * Handles incoming requests from fediverse servers
         */
        public function inbox()
        {
            // get json body and parse it
            $payload = $this->request->getJSON();
    
            // retrieve payload actor from database or create it if it doesn't exist
            $payloadActor = get_or_create_actor_from_uri($payload->actor);
    
            // store activity to database
            $activityId = model('ActivityModel')->newActivity(
                $payload->type,
                $payloadActor->id,
                $this->actor->id,
                null,
                json_encode($payload),
            );
    
            // switch/case on activity type
            switch ($payload->type) {
                case 'Create':
                    switch ($payload->object->type) {
                        case 'Note':
                            if (!$payload->object->inReplyTo) {
                                return $this->response
                                    ->setStatusCode(501)
                                    ->setJSON([]);
                            }
    
                            $replyToNote = model('NoteModel')->getNoteByUri(
                                $payload->object->inReplyTo,
                            );
    
                            // TODO: strip content from html to retrieve message
                            // remove all html tags and reconstruct message with mentions?
                            extract_text_from_html($payload->object->content);
    
                            $reply = new \ActivityPub\Entities\Note([
                                'uri' => $payload->object->id,
                                'actor_id' => $payloadActor->id,
                                'in_reply_to_id' => $replyToNote->id,
                                'message' => $payload->object->content,
                                'published_at' => Time::parse(
                                    $payload->object->published,
                                ),
                            ]);
    
                            $noteId = model('NoteModel')->addReply(
                                $reply,
                                true,
                                false,
                            );
    
                            model('ActivityModel')->update($activityId, [
                                'note_id' => service('uuid')
                                    ->fromBytes($noteId)
                                    ->getString(),
                            ]);
    
                            return $this->response->setStatusCode(200)->setJSON([]);
                        default:
                            // return not handled undo error (501 = not implemented)
                            return $this->response->setStatusCode(501)->setJSON([]);
                    }
                    break;
                case 'Delete':
                    $noteToDelete = model('NoteModel')->getNoteByUri(
                        $payload->object->id,
                    );
    
                    model('NoteModel')->removeNote($noteToDelete, false);
    
                    return $this->response->setStatusCode(200)->setJSON([]);
                case 'Follow':
                    // add to followers table
                    model('FollowModel')->addFollower(
                        $payloadActor,
                        $this->actor,
                        false,
                    );
    
                    // Automatically accept follow by returning accept activity
                    accept_follow($this->actor, $payloadActor, $payload->id);
    
                    // TODO: return 202 (Accepted) followed!
                    return $this->response->setStatusCode(202)->setJSON([]);
    
                case 'Like':
                    // get favourited note
                    $note = model('NoteModel')->getNoteByUri($payload->object);
    
                    // Like side-effect
                    model('FavouriteModel')->addFavourite(
                        $payloadActor,
                        $note,
                        false,
                    );
    
                    model('ActivityModel')->update($activityId, [
                        'note_id' => $note->id,
                    ]);
    
                    return $this->response->setStatusCode(200)->setJSON([]);
                case 'Announce':
                    $note = model('NoteModel')->getNoteByUri($payload->object);
    
                    model('ActivityModel')->update($activityId, [
                        'note_id' => $note->id,
                    ]);
    
                    model('NoteModel')->reblog($payloadActor, $note, false);
    
                    return $this->response->setStatusCode(200)->setJSON([]);
                case 'Undo':
                    // switch/case on the type of activity to undo
                    switch ($payload->object->type) {
                        case 'Follow':
                            // revert side-effect by removing follow from database
                            model('FollowModel')->removeFollower(
                                $payloadActor,
                                $this->actor,
                                false,
                            );
    
                            // TODO: undo has been accepted! (202 - Accepted)
                            return $this->response->setStatusCode(202)->setJSON([]);
                        case 'Like':
                            $note = model('NoteModel')->getNoteByUri(
                                $payload->object->object,
                            );
    
                            // revert side-effect by removing favourite from database
                            model('FavouriteModel')->removeFavourite(
                                $payloadActor,
                                $note,
                                false,
                            );
    
                            model('ActivityModel')->update($activityId, [
                                'note_id' => $note->id,
                            ]);
    
                            return $this->response->setStatusCode(200)->setJSON([]);
                        case 'Announce':
                            $note = model('NoteModel')->getNoteByUri(
                                $payload->object->object,
                            );
    
                            $reblogNote = model('NoteModel')
                                ->where([
                                    'actor_id' => $payloadActor->id,
                                    'reblog_of_id' => service('uuid')
                                        ->fromString($note->id)
                                        ->getBytes(),
                                ])
                                ->first();
    
                            model('NoteModel')->undoReblog($reblogNote, false);
    
                            model('ActivityModel')->update($activityId, [
                                'note_id' => $note->id,
                            ]);
    
                            return $this->response->setStatusCode(200)->setJSON([]);
                        default:
                            // return not handled undo error (501 = not implemented)
                            return $this->response->setStatusCode(501)->setJSON([]);
                    }
                default:
                    // return not handled activity error (501 = not implemented)
                    return $this->response->setStatusCode(501)->setJSON([]);
            }
        }
    
        public function outbox()
        {
            // get published activities by publication date
            $actorActivity = model('ActivityModel')
                ->where('actor_id', $this->actor->id)
                ->where('`created_at` <= NOW()', null, false)
                ->orderBy('created_at', 'DESC');
    
            $pageNumber = $this->request->getGet('page');
    
            if (!isset($pageNumber)) {
                $actorActivity->paginate(12);
                $pager = $actorActivity->pager;
                $collection = new OrderedCollectionObject(null, $pager);
            } else {
                $paginatedActivity = $actorActivity->paginate(
                    12,
                    'default',
                    $pageNumber,
                );
                $pager = $actorActivity->pager;
                $orderedItems = [];
                foreach ($paginatedActivity as $activity) {
                    array_push($orderedItems, $activity->payload);
                }
                $collection = new OrderedCollectionPage($pager, $orderedItems);
            }
    
            return $this->response
                ->setContentType('application/activity+json')
                ->setBody($collection->toJSON());
        }
    
        public function followers()
        {
            // get followers for a specific actor
            $followers = model('ActorModel')
                ->join(
                    'activitypub_follows',
                    'activitypub_follows.actor_id = id',
                    'inner',
                )
                ->where('activitypub_follows.target_actor_id', $this->actor->id)
                ->orderBy('activitypub_follows.created_at', 'DESC');
    
            $pageNumber = $this->request->getGet('page');
    
            if (!isset($pageNumber)) {
                $followers->paginate(12);
                $pager = $followers->pager;
                $followersCollection = new OrderedCollectionObject(null, $pager);
            } else {
                $paginatedFollowers = $followers->paginate(
                    12,
                    'default',
                    $pageNumber,
                );
                $pager = $followers->pager;
    
                $orderedItems = [];
                foreach ($paginatedFollowers as $follower) {
                    array_push($orderedItems, $follower->uri);
                }
                $followersCollection = new OrderedCollectionPage(
                    $pager,
                    $orderedItems,
                );
            }
    
            return $this->response
                ->setContentType('application/activity+json')
                ->setBody($followersCollection->toJSON());
        }
    
        public function attemptFollow()
        {
            $rules = [
                'handle' =>
                    'regex_match[/^@?(?P<username>[\w\.\-]+)@(?P<host>[\w\.\-]+)(?P<port>:[\d]+)?$/]',
            ];
    
            if (!$this->validate($rules)) {
                return redirect()
                    ->back()
                    ->withInput()
                    ->with('errors', $this->validator->getErrors());
            }
    
            helper('text');
    
            // get webfinger data from actor
            // parse activityPub id to get actor and domain
            // check if actor and domain exist
    
            try {
                if ($parts = split_handle($this->request->getPost('handle'))) {
                    extract($parts);
    
                    $data = get_webfinger_data($username, $domain);
                }
            } catch (\CodeIgniter\HTTP\Exceptions\HTTPException $e) {
                return redirect()
                    ->back()
                    ->withInput()
                    ->with('error', lang('ActivityPub.follow.accountNotFound'));
            }
    
            $ostatusKey = array_search(
                'http://ostatus.org/schema/1.0/subscribe',
                array_column($data->links, 'rel'),
            );
    
            if (!$ostatusKey) {
                // TODO: error, couldn't subscribe to activitypub account
                // The instance doesn't allow its users to follow others
                return $this->response->setJSON([]);
            }
    
            return redirect()->to(
                str_replace(
                    '{uri}',
                    urlencode($this->actor->uri),
                    $data->links[$ostatusKey]->template,
                ),
            );
        }
    
        public function activity($activityId)
        {
            if (
                !($activity = model('ActivityModel')->getActivityById($activityId))
            ) {
                throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
            }
    
            return $this->response
                ->setContentType('application/activity+json')
                ->setBody(json_encode($activity->payload));
        }
    }