Skip to content
Snippets Groups Projects
PostModel.php 18.2 KiB
Newer Older
  • Learn to ignore specific revisions
  • /**
     * @copyright  2021 Podlibre
     * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
     * @link       https://castopod.org/
     */
    
    namespace ActivityPub\Models;
    
    use ActivityPub\Activities\AnnounceActivity;
    use ActivityPub\Activities\CreateActivity;
    use ActivityPub\Activities\DeleteActivity;
    use ActivityPub\Activities\UndoActivity;
    
    use ActivityPub\Objects\TombstoneObject;
    
    use CodeIgniter\Database\BaseResult;
    
    use CodeIgniter\Events\Events;
    use CodeIgniter\HTTP\URI;
    use CodeIgniter\I18n\Time;
    
        protected $table = 'activitypub_posts';
    
        protected $uuidFields = ['id', 'in_reply_to_id', 'reblog_of_id'];
    
    
        protected $allowedFields = [
            'id',
            'uri',
            'actor_id',
            'in_reply_to_id',
            'reblog_of_id',
            'message',
            'message_html',
            'favourites_count',
            'reblogs_count',
            'replies_count',
            'published_at',
        ];
    
    
        protected $validationRules = [
            'actor_id' => 'required',
    
        protected $beforeInsert = ['setPostId'];
    
        public function getPostById(string $postId): ?Post
    
            if (! ($found = cache($cacheName))) {
    
                cache()
                    ->save($cacheName, $found, DECADE);
    
        public function getPostByUri(string $postUri): ?Post
    
                    ->cachePrefix . "post-{$hashedPostUri}";
    
                $found = $this->where('uri', $postUri)
    
                cache()
                    ->save($cacheName, $found, DECADE);
    
         * Retrieves all published posts for a given actor ordered by publication date
    
        public function getActorPublishedPosts(int $actorId): array
    
                $found = $this->where([
                    'actor_id' => $actorId,
                    'in_reply_to_id' => null,
                ])
                    ->where('`published_at` <= NOW()', null, false)
                    ->orderBy('published_at', 'DESC')
                    ->findAll();
    
    
                $secondsToNextUnpublishedPost = $this->getSecondsToNextUnpublishedPosts($actorId);
    
                    ->save($cacheName, $found, $secondsToNextUnpublishedPost ? $secondsToNextUnpublishedPost : DECADE);
    
         * Returns the timestamp difference in seconds between the next post to publish and the current timestamp. Returns
         * false if there's no post to publish
    
        public function getSecondsToNextUnpublishedPosts(int $actorId): int | false
    
            $result = $this->select('TIMESTAMPDIFF(SECOND, NOW(), `published_at`) as timestamp_diff')
    
                ->where([
                    'actor_id' => $actorId,
                ])
                ->where('`published_at` > NOW()', null, false)
                ->orderBy('published_at', 'asc')
                ->get()
                ->getResultArray();
    
    
            return $result !== []
    
         * Retrieves all published replies for a given post. By default, it does not get replies from blocked actors.
    
        public function getPostReplies(string $postId, bool $withBlocked = false): array
    
            if (! ($found = cache($cacheName))) {
                if (! $withBlocked) {
    
                    $this->select('activitypub_posts.*')
                        ->join('activitypub_actors', 'activitypub_actors.id = activitypub_posts.actor_id', 'inner')
    
                        ->where('activitypub_actors.is_blocked', 0);
                }
    
                $this->where('in_reply_to_id', $this->uuid->fromString($postId) ->getBytes())
    
                    ->where('`published_at` <= NOW()', null, false)
                    ->orderBy('published_at', 'ASC');
                $found = $this->findAll();
    
                cache()
                    ->save($cacheName, $found, DECADE);
    
         * Retrieves all published reblogs for a given post
    
        public function getPostReblogs(string $postId): array
    
                    ->cachePrefix . "post#{$postId}_reblogs";
    
                $found = $this->where('reblog_of_id', $this->uuid->fromString($postId) ->getBytes())
    
                    ->where('`published_at` <= NOW()', null, false)
                    ->orderBy('published_at', 'ASC')
                    ->findAll();
    
    
                cache()
                    ->save($cacheName, $found, DECADE);
    
        public function addPreviewCard(string $postId, int $previewCardId): Query | bool
    
            return $this->db->table('activitypub_posts_preview_cards')
    
                    'post_id' => $this->uuid->fromString($postId)
    
                        ->getBytes(),
                    'preview_card_id' => $previewCardId,
                ]);
    
         * Adds post in database along preview card if relevant
    
         * @return string|false returns the new post id if success or false otherwise
    
            bool $createPreviewCard = true,
            bool $registerActivity = true
    
            if (! ($newPostId = $this->insert($post, true))) {
    
                return false;
            }
    
            if ($createPreviewCard) {
                // parse message
    
                $messageUrls = extract_urls_from_message($post->message);
    
                    ($previewCard = get_or_create_preview_card_from_url(new URI($messageUrls[0]))) &&
    
                    ! $this->addPreviewCard($newPostId, $previewCard->id)
    
                    // problem when linking post to preview card
    
            model('ActorModel', false)
                ->where('id', $post->actor_id)
                ->increment('posts_count');
    
                // set post id and uri to construct NoteObject
                $post->id = $newPostId;
                $post->uri = url_to('post', $post->actor->username, $newPostId);
    
                $noteObjectClass = config('ActivityPub')
                    ->noteObject;
    
                    ->set('actor', $post->actor->uri)
                    ->set('object', new $noteObjectClass($post));
    
                $activityId = model('ActivityModel', false)
    
                $createActivity->set('id', url_to('activity', $post->actor->username, $activityId));
    
                    ->update($activityId, [
                        'payload' => $createActivity->toJSON(),
                    ]);
    
            Events::trigger('on_post_add', $post);
    
        public function editPost(Post $updatedPost): bool
    
            // update post create activity schedule in database
            $scheduledActivity = model('ActivityModel', false)
    
                    'post_id' => $this->uuid
                        ->fromString($updatedPost->id)
    
                        ->getBytes(),
                ])
                ->first();
    
            // update published date in payload
            $newPayload = $scheduledActivity->payload;
    
            $newPayload->object->published = $updatedPost->published_at->format(DATE_W3C);
            model('ActivityModel', false)
    
                ->update($scheduledActivity->id, [
                    'payload' => json_encode($newPayload, JSON_THROW_ON_ERROR),
    
                    'scheduled_at' => $updatedPost->published_at,
    
            // update post
            $updateResult = $this->update($updatedPost->id, $updatedPost);
    
            Events::trigger('on_post_edit', $updatedPost);
    
         * Removes a post from the database and decrements meta data
    
        public function removePost(Post $post, bool $registerActivity = true): BaseResult | bool
    
            model('ActorModel', false)
                ->where('id', $post->actor_id)
                ->decrement('posts_count');
    
            if ($post->in_reply_to_id !== null) {
                // Post to remove is a reply
                model('PostModel', false)
                    ->where('id', $this->uuid->fromString($post->in_reply_to_id) ->getBytes())
    
                Events::trigger('on_reply_remove', $post);
    
            // remove all post reblogs
            foreach ($post->reblogs as $reblog) {
    
                // FIXME: issue when actor is not local, can't get actor information
    
            // remove all post replies
            foreach ($post->replies as $reply) {
                $this->removePost($reply);
    
            // check that preview card is no longer used elsewhere before deleting it
    
                    ->table('activitypub_posts_preview_cards')
                    ->where('preview_card_id', $post->preview_card->id)
    
                model('PreviewCardModel', false)->deletePreviewCard($post->preview_card->id, $post->preview_card->url);
    
            if ($registerActivity) {
                $deleteActivity = new DeleteActivity();
                $tombstoneObject = new TombstoneObject();
    
                $tombstoneObject->set('id', $post->uri);
    
                $activityId = model('ActivityModel', false)
    
                        null,
                        null,
                        $deleteActivity->toJSON(),
                        Time::now(),
                        'queued',
                    );
    
    
                $deleteActivity->set('id', url_to('activity', $post->actor->username, $activityId));
    
                    ->update($activityId, [
                        'payload' => $deleteActivity->toJSON(),
                    ]);
    
            $result = model('PostModel', false)
                ->delete($post->id);
    
            Events::trigger('on_post_remove', $post);
    
            $this->db->transComplete();
    
            return $result;
        }
    
        public function addReply(
    
            bool $createPreviewCard = true,
            bool $registerActivity = true
    
        ): string | false {
            if (! $reply->in_reply_to_id) {
    
                throw new Exception('Passed post is not a reply!');
    
            $postId = $this->addPost($reply, $createPreviewCard, $registerActivity);
    
                ->where('id', $this->uuid->fromString($reply->in_reply_to_id) ->getBytes())
    
            Events::trigger('on_post_reply', $reply);
    
        public function reblog(Actor $actor, Post $post, bool $registerActivity = true): string | false
    
            $reblogId = $this->insert($reblog);
    
            model('PostModel', false)
                ->where('id', $this->uuid->fromString($post->id)->getBytes())
    
                ->increment('reblogs_count');
    
            if ($registerActivity) {
                $announceActivity = new AnnounceActivity($reblog);
    
    
                $activityId = model('ActivityModel', false)
    
                        $announceActivity->toJSON(),
                        $reblog->published_at,
                        'queued',
                    );
    
    
                $announceActivity->set('id', url_to('activity', $post->actor->username, $activityId));
    
                    ->update($activityId, [
                        'payload' => $announceActivity->toJSON(),
                    ]);
    
            Events::trigger('on_post_reblog', $actor, $post);
    
        public function undoReblog(Post $reblogPost, bool $registerActivity = true): BaseResult | bool
    
            model('ActorModel', false)
                ->where('id', $reblogPost->actor_id)
                ->decrement('posts_count');
    
            model('PostModel', false)
                ->where('id', $this->uuid->fromString($reblogPost->reblog_of_id) ->getBytes())
    
                ->decrement('reblogs_count');
    
            if ($registerActivity) {
                $undoActivity = new UndoActivity();
                // get like activity
    
                $activity = model('ActivityModel', false)
    
                        'actor_id' => $reblogPost->actor_id,
                        'post_id' => $this->uuid
                            ->fromString($reblogPost->reblog_of_id)
    
                $announceActivity = new AnnounceActivity($reblogPost);
                $announceActivity->set('id', url_to('activity', $reblogPost->actor->username, $activity->id),);
    
                    ->set('actor', $reblogPost->actor->uri)
    
                $activityId = model('ActivityModel', false)
    
                $undoActivity->set('id', url_to('activity', $reblogPost->actor->username, $activityId));
    
                    ->update($activityId, [
                        'payload' => $undoActivity->toJSON(),
                    ]);
    
            $result = model('PostModel', false)
                ->delete($reblogPost->id);
    
            Events::trigger('on_post_undo_reblog', $reblogPost);
    
        public function toggleReblog(Actor $actor, Post $post): void
    
        public function clearCache(Post $post): void
    
            model('ActorModel', false)
                ->clearCache($post->actor);
    
                ->deleteMatching($cachePrefix . "post#{$post->id}*");
    
                ->deleteMatching($cachePrefix . "post-{$hashedPostUri}*");
    
            if ($post->in_reply_to_id !== null) {
                $this->clearCache($post->reply_to_post);
    
            if ($post->reblog_of_id !== null) {
                $this->clearCache($post->reblog_of_post);
    
         * @param array<string, array<string|int, mixed>> $data
         * @return array<string, array<string|int, mixed>>
         */
    
        protected function setPostId(array $data): array
    
            $uuid4 = $this->uuid->{$this->uuidVersion}();
            $data['data']['id'] = $uuid4->toString();
    
                    ->getActorById((int) $data['data']['actor_id']);
    
                $data['data']['uri'] = url_to('post', $actor->username, $uuid4->toString());