Commit bb4752c3 authored by Yassine Doghri's avatar Yassine Doghri
Browse files

feat(comments): add comments to episodes + update naming of status to post

- remove confusing counts for episode (total favourites, total reblogs)
- add comments section to
episode page to display episode comments + post replies linked to the episode
parent 3ff13649
Loading
Loading
Loading
Loading
+86 −117
Original line number Diff line number Diff line
@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace Config;

use App\Entities\Actor;
use App\Entities\Status;
use App\Entities\Post;
use App\Entities\User;
use CodeIgniter\Events\Events;
use CodeIgniter\Exceptions\FrameworkException;
@@ -120,82 +120,72 @@ Events::on('on_undo_follow', function ($actor, $targetActor): void {
});

/**
 * @param Status $status
 * @param Post $post
 */
Events::on('on_status_add', function ($status): void {
    if ($status->in_reply_to_id !== null) {
        $status = $status->reply_to_status;
Events::on('on_post_add', function ($post): void {
    $isReply = $post->in_reply_to_id !== null;

    if ($isReply) {
        $post = $post->reply_to_post;
    }

    if ($status->episode_id) {
        model('EpisodeModel')
            ->where('id', $status->episode_id)
            ->increment('statuses_total');
    if ($post->episode_id !== null) {
        if ($isReply) {
            model('EpisodeModel', false)
                ->where('id', $post->episode_id)
                ->increment('comments_count');
        } else {
            model('EpisodeModel', false)
                ->where('id', $post->episode_id)
                ->increment('posts_count');
        }
    }

    if ($status->actor->is_podcast) {
    if ($post->actor->is_podcast) {
        // Removing all of the podcast pages is a bit overkill, but works to avoid caching bugs
        // same for other events below
        cache()
            ->deleteMatching("podcast#{$status->actor->podcast->id}*");
            ->deleteMatching("podcast#{$post->actor->podcast->id}*");
        cache()
            ->deleteMatching("page_podcast#{$status->actor->podcast->id}*");
            ->deleteMatching("page_podcast#{$post->actor->podcast->id}*");
    }
});

/**
 * @param Status $status
 * @param Post $post
 */
Events::on('on_status_remove', function ($status): void {
    if ($status->in_reply_to_id !== null) {
        Events::trigger('on_status_remove', $status->reply_to_status);
Events::on('on_post_remove', function ($post): void {
    if ($post->in_reply_to_id !== null) {
        Events::trigger('on_post_remove', $post->reply_to_post);
    }

    if ($episodeId = $status->episode_id) {
        model('EpisodeModel')
            ->where('id', $episodeId)
            ->decrement('statuses_total', 1 + $status->reblogs_count);

        model('EpisodeModel')
            ->where('id', $episodeId)
            ->decrement('reblogs_total', $status->reblogs_count);

        model('EpisodeModel')
    if ($episodeId = $post->episode_id) {
        model('EpisodeModel', false)
            ->where('id', $episodeId)
            ->decrement('favourites_total', $status->favourites_count);
            ->decrement('posts_count');
    }

    if ($status->actor->is_podcast) {
    if ($post->actor->is_podcast) {
        cache()
            ->deleteMatching("podcast#{$status->actor->podcast->id}*");
            ->deleteMatching("podcast#{$post->actor->podcast->id}*");
        cache()
            ->deleteMatching("page_podcast#{$status->actor->podcast->id}*");
            ->deleteMatching("page_podcast#{$post->actor->podcast->id}*");
    }

    cache()
        ->deleteMatching("page_status#{$status->id}*");
        ->deleteMatching("page_post#{$post->id}*");
});

/**
 * @param Actor $actor
 * @param Status $status
 * @param Post $post
 */
Events::on('on_status_reblog', function ($actor, $status): void {
    if ($episodeId = $status->episode_id) {
        model('EpisodeModel')
            ->where('id', $episodeId)
            ->increment('reblogs_total');

        model('EpisodeModel')
            ->where('id', $episodeId)
            ->increment('statuses_total');
    }

    if ($status->actor->is_podcast) {
Events::on('on_post_reblog', function ($actor, $post): void {
    if ($post->actor->is_podcast) {
        cache()
            ->deleteMatching("podcast#{$status->actor->podcast->id}*");
            ->deleteMatching("podcast#{$post->actor->podcast->id}*");
        cache()
            ->deleteMatching("page_podcast#{$status->actor->podcast->id}*");
            ->deleteMatching("page_podcast#{$post->actor->podcast->id}*");
    }

    if ($actor->is_podcast) {
@@ -205,111 +195,96 @@ Events::on('on_status_reblog', function ($actor, $status): void {
    }

    cache()
        ->deleteMatching("page_status#{$status->id}*");
        ->deleteMatching("page_post#{$post->id}*");

    if ($status->in_reply_to_id !== null) {
        cache()->deleteMatching("page_status#{$status->in_reply_to_id}");
    if ($post->in_reply_to_id !== null) {
        cache()->deleteMatching("page_post#{$post->in_reply_to_id}");
    }
});

/**
 * @param Status $reblogStatus
 * @param Post $reblogPost
 */
Events::on('on_status_undo_reblog', function ($reblogStatus): void {
    $status = $reblogStatus->reblog_of_status;
    if ($episodeId = $status->episode_id) {
        model('EpisodeModel')
            ->where('id', $episodeId)
            ->decrement('reblogs_total');

        model('EpisodeModel')
            ->where('id', $episodeId)
            ->decrement('statuses_total');
    }
Events::on('on_post_undo_reblog', function ($reblogPost): void {
    $post = $reblogPost->reblog_of_post;

    if ($status->actor->is_podcast) {
    if ($post->actor->is_podcast) {
        cache()
            ->deleteMatching("podcast#{$status->actor->podcast->id}*");
            ->deleteMatching("podcast#{$post->actor->podcast->id}*");
        cache()
            ->deleteMatching("page_podcast#{$status->actor->podcast->id}*");
            ->deleteMatching("page_podcast#{$post->actor->podcast->id}*");
    }

    cache()
        ->deleteMatching("page_status#{$status->id}*");
        ->deleteMatching("page_post#{$post->id}*");
    cache()
        ->deleteMatching("page_status#{$reblogStatus->id}*");
        ->deleteMatching("page_post#{$reblogPost->id}*");

    if ($status->in_reply_to_id !== null) {
        cache()->deleteMatching("page_status#{$status->in_reply_to_id}");
    if ($post->in_reply_to_id !== null) {
        cache()->deleteMatching("page_post#{$post->in_reply_to_id}");
    }

    if ($reblogStatus->actor->is_podcast) {
    if ($reblogPost->actor->is_podcast) {
        cache()
            ->deleteMatching("podcast#{$reblogStatus->actor->podcast->id}*");
            ->deleteMatching("podcast#{$reblogPost->actor->podcast->id}*");
        cache()
            ->deleteMatching("page_podcast#{$reblogStatus->actor->podcast->id}*");
            ->deleteMatching("page_podcast#{$reblogPost->actor->podcast->id}*");
    }
});

/**
 * @param Status $reply
 * @param Post $reply
 */
Events::on('on_status_reply', function ($reply): void {
    $status = $reply->reply_to_status;
Events::on('on_post_reply', function ($reply): void {
    $post = $reply->reply_to_post;

    if ($status->actor->is_podcast) {
    if ($post->actor->is_podcast) {
        cache()
            ->deleteMatching("podcast#{$status->actor->podcast->id}*");
            ->deleteMatching("podcast#{$post->actor->podcast->id}*");
        cache()
            ->deleteMatching("page_podcast#{$status->actor->podcast->id}*");
            ->deleteMatching("page_podcast#{$post->actor->podcast->id}*");
    }

    cache()
        ->deleteMatching("page_status#{$status->id}*");
        ->deleteMatching("page_post#{$post->id}*");
});

/**
 * @param Status $reply
 * @param Post $reply
 */
Events::on('on_reply_remove', function ($reply): void {
    $status = $reply->reply_to_status;
    $post = $reply->reply_to_post;

    if ($status->actor->is_podcast) {
    if ($post->actor->is_podcast) {
        cache()
            ->deleteMatching("page_podcast#{$status->actor->podcast->id}*");
            ->deleteMatching("page_podcast#{$post->actor->podcast->id}*");
        cache()
            ->deleteMatching("podcast#{$status->actor->podcast->id}*");
            ->deleteMatching("podcast#{$post->actor->podcast->id}*");
    }

    cache()
        ->deleteMatching("page_status#{$status->id}*");
        ->deleteMatching("page_post#{$post->id}*");
    cache()
        ->deleteMatching("page_status#{$reply->id}*");
        ->deleteMatching("page_post#{$reply->id}*");
});

/**
 * @param Actor $actor
 * @param Status $status
 * @param Post $post
 */
Events::on('on_status_favourite', function ($actor, $status): void {
    if ($status->episode_id) {
        model('EpisodeModel')
            ->where('id', $status->episode_id)
            ->increment('favourites_total');
    }

    if ($status->actor->is_podcast) {
Events::on('on_post_favourite', function ($actor, $post): void {
    if ($post->actor->is_podcast) {
        cache()
            ->deleteMatching("podcast#{$status->actor->podcast->id}*");
            ->deleteMatching("podcast#{$post->actor->podcast->id}*");
        cache()
            ->deleteMatching("page_podcast#{$status->actor->podcast->id}*");
            ->deleteMatching("page_podcast#{$post->actor->podcast->id}*");
    }

    cache()
        ->deleteMatching("page_status#{$status->id}*");
        ->deleteMatching("page_post#{$post->id}*");

    if ($status->in_reply_to_id !== null) {
        cache()->deleteMatching("page_status#{$status->in_reply_to_id}*");
    if ($post->in_reply_to_id !== null) {
        cache()->deleteMatching("page_post#{$post->in_reply_to_id}*");
    }

    if ($actor->is_podcast) {
@@ -321,27 +296,21 @@ Events::on('on_status_favourite', function ($actor, $status): void {

/**
 * @param Actor $actor
 * @param Status $status
 * @param Post $post
 */
Events::on('on_status_undo_favourite', function ($actor, $status): void {
    if ($status->episode_id) {
        model('EpisodeModel')
            ->where('id', $status->episode_id)
            ->decrement('favourites_total');
    }

    if ($status->actor->is_podcast) {
Events::on('on_post_undo_favourite', function ($actor, $post): void {
    if ($post->actor->is_podcast) {
        cache()
            ->deleteMatching("podcast#{$status->actor->podcast->id}*");
            ->deleteMatching("podcast#{$post->actor->podcast->id}*");
        cache()
            ->deleteMatching("page_podcast#{$status->actor->podcast->id}*");
            ->deleteMatching("page_podcast#{$post->actor->podcast->id}*");
    }

    cache()
        ->deleteMatching("page_status#{$status->id}*");
        ->deleteMatching("page_post#{$post->id}*");

    if ($status->in_reply_to_id !== null) {
        cache()->deleteMatching("page_status#{$status->in_reply_to_id}*");
    if ($post->in_reply_to_id !== null) {
        cache()->deleteMatching("page_post#{$post->in_reply_to_id}*");
    }

    if ($actor->is_podcast) {
@@ -356,7 +325,7 @@ Events::on('on_block_actor', function (int $actorId): void {
    cache()
        ->deleteMatching('podcast*');
    cache()
        ->deleteMatching('page_status*');
        ->deleteMatching('page_post*');
});

Events::on('on_unblock_actor', function (int $actorId): void {
@@ -364,7 +333,7 @@ Events::on('on_unblock_actor', function (int $actorId): void {
    cache()
        ->deleteMatching('podcast*');
    cache()
        ->deleteMatching('page_status*');
        ->deleteMatching('page_post*');
});

Events::on('on_block_domain', function (string $domainName): void {
@@ -372,7 +341,7 @@ Events::on('on_block_domain', function (string $domainName): void {
    cache()
        ->deleteMatching('podcast*');
    cache()
        ->deleteMatching('page_status*');
        ->deleteMatching('page_post*');
});

Events::on('on_unblock_domain', function (string $domainName): void {
@@ -380,5 +349,5 @@ Events::on('on_unblock_domain', function (string $domainName): void {
    cache()
        ->deleteMatching('podcast*');
    cache()
        ->deleteMatching('page_status*');
        ->deleteMatching('page_post*');
});
+51 −25
Original line number Diff line number Diff line
@@ -32,10 +32,10 @@ $routes->setAutoRoute(false);
 */

$routes->addPlaceholder('podcastHandle', '[a-zA-Z0-9\_]{1,32}');
$routes->addPlaceholder('slug', '[a-zA-Z0-9\-]{1,191}');
$routes->addPlaceholder('slug', '[a-zA-Z0-9\-]{1,128}');
$routes->addPlaceholder('base64', '[A-Za-z0-9\.\_]+\-{0,2}');
$routes->addPlaceholder('platformType', '\bpodcasting|\bsocial|\bfunding');
$routes->addPlaceholder('statusAction', '\bfavourite|\breblog|\breply');
$routes->addPlaceholder('postAction', '\bfavourite|\breblog|\breply');
$routes->addPlaceholder('embeddablePlayerTheme', '\blight|\bdark|\blight-transparent|\bdark-transparent');
$routes->addPlaceholder(
    'uuid',
@@ -416,6 +416,25 @@ $routes->group(
                                ],
                            );
                        });

                        $routes->group('comments', function ($routes): void {
                            $routes->post(
                                'new',
                                'EpisodeController::attemptCommentCreate/$1/$2',
                                [
                                    'as' => 'comment-attempt-create',
                                    'filter' => 'permission:podcast-manage_publications',
                                ]
                            );
                            $routes->post(
                                'delete',
                                'EpisodeController::attemptCommentDelete/$1/$2',
                                [
                                    'as' => 'comment-attempt-delete',
                                    'filter' => 'permission:podcast-manage_publications',
                                ]
                            );
                        });
                    });
                });

@@ -752,6 +771,12 @@ $routes->group('@(:podcastHandle)', function ($routes): void {
                'controller-method' => 'EpisodeController::comments/$1/$2',
            ],
        ]);
        $routes->get('comments/(:uuid)', 'EpisodeController::comment/$1/$2/$3', [
            'as' => 'comment',
        ]);
        $routes->get('comments/(:uuid)/replies', 'EpisodeController::commentReplies/$1/$2/$3', [
            'as' => 'comment-replies',
        ]);
        $routes->get('oembed.json', 'EpisodeController::oembedJSON/$1/$2', [
            'as' => 'episode-oembed-json',
        ]);
@@ -803,73 +828,74 @@ $routes->post('interact-as-actor', 'AuthController::attemptInteractAsActor', [
 * Overwriting ActivityPub routes file
 */
$routes->group('@(:podcastHandle)', function ($routes): void {
    $routes->post('statuses/new', 'StatusController::attemptCreate/$1', [
        'as' => 'status-attempt-create',
    $routes->post('posts/new', 'PostController::attemptCreate/$1', [
        'as' => 'post-attempt-create',
        'filter' => 'permission:podcast-manage_publications',
    ]);
    // Status
    $routes->group('statuses/(:uuid)', function ($routes): void {

    // Post
    $routes->group('posts/(:uuid)', function ($routes): void {
        $routes->options('/', 'ActivityPubController::preflight');
        $routes->get('/', 'StatusController::view/$1/$2', [
            'as' => 'status',
        $routes->get('/', 'PostController::view/$1/$2', [
            'as' => 'post',
            'alternate-content' => [
                'application/activity+json' => [
                    'namespace' => 'ActivityPub\Controllers',
                    'controller-method' => 'StatusController/$2',
                    'controller-method' => 'PostController/$2',
                ],
                'application/ld+json; profile="https://www.w3.org/ns/activitystreams' => [
                    'namespace' => 'ActivityPub\Controllers',
                    'controller-method' => 'StatusController/$2',
                    'controller-method' => 'PostController/$2',
                ],
            ],
        ]);
        $routes->options('replies', 'ActivityPubController::preflight');
        $routes->get('replies', 'StatusController/$1/$2', [
            'as' => 'status-replies',
        $routes->get('replies', 'PostController/$1/$2', [
            'as' => 'post-replies',
            'alternate-content' => [
                'application/activity+json' => [
                    'namespace' => 'ActivityPub\Controllers',
                    'controller-method' => 'StatusController::replies/$2',
                    'controller-method' => 'PostController::replies/$2',
                ],
                'application/ld+json; profile="https://www.w3.org/ns/activitystreams' => [
                    'namespace' => 'ActivityPub\Controllers',
                    'controller-method' => 'StatusController::replies/$2',
                    'controller-method' => 'PostController::replies/$2',
                ],
            ],
        ]);

        // Actions
        $routes->post('action', 'StatusController::attemptAction/$1/$2', [
            'as' => 'status-attempt-action',
        $routes->post('action', 'PostController::attemptAction/$1/$2', [
            'as' => 'post-attempt-action',
            'filter' => 'permission:podcast-interact_as',
        ]);

        $routes->post(
            'block-actor',
            'StatusController::attemptBlockActor/$1/$2',
            'PostController::attemptBlockActor/$1/$2',
            [
                'as' => 'status-attempt-block-actor',
                'as' => 'post-attempt-block-actor',
                'filter' => 'permission:fediverse-block_actors',
            ],
        );
        $routes->post(
            'block-domain',
            'StatusController::attemptBlockDomain/$1/$2',
            'PostController::attemptBlockDomain/$1/$2',
            [
                'as' => 'status-attempt-block-domain',
                'as' => 'post-attempt-block-domain',
                'filter' => 'permission:fediverse-block_domains',
            ],
        );
        $routes->post('delete', 'StatusController::attemptDelete/$1/$2', [
            'as' => 'status-attempt-delete',
        $routes->post('delete', 'PostController::attemptDelete/$1/$2', [
            'as' => 'post-attempt-delete',
            'filter' => 'permission:podcast-manage_publications',
        ]);

        $routes->get(
            'remote/(:statusAction)',
            'StatusController::remoteAction/$1/$2/$3',
            'remote/(:postAction)',
            'PostController::remoteAction/$1/$2/$3',
            [
                'as' => 'status-remote-action',
                'as' => 'post-remote-action',
            ],
        );
    });
+61 −22
Original line number Diff line number Diff line
@@ -10,15 +10,17 @@ declare(strict_types=1);

namespace App\Controllers\Admin;

use App\Entities\Comment;
use App\Entities\Episode;
use App\Entities\Image;
use App\Entities\Location;
use App\Entities\Podcast;
use App\Entities\Status;
use App\Entities\Post;
use App\Models\CommentModel;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use App\Models\PostModel;
use App\Models\SoundbiteModel;
use App\Models\StatusModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\I18n\Time;
@@ -429,7 +431,7 @@ class EpisodeController extends BaseController
        $db = db_connect();
        $db->transStart();

        $newStatus = new Status([
        $newPost = new Post([
            'actor_id' => $this->podcast->actor_id,
            'episode_id' => $this->episode->id,
            'message' => $this->request->getPost('message'),
@@ -456,15 +458,15 @@ class EpisodeController extends BaseController
            $this->episode->published_at = Time::now();
        }

        $newStatus->published_at = $this->episode->published_at;
        $newPost->published_at = $this->episode->published_at;

        $statusModel = new StatusModel();
        if (! $statusModel->addStatus($newStatus)) {
        $postModel = new PostModel();
        if (! $postModel->addPost($newPost)) {
            $db->transRollback();
            return redirect()
                ->back()
                ->withInput()
                ->with('errors', $statusModel->errors());
                ->with('errors', $postModel->errors());
        }

        $episodeModel = new EpisodeModel();
@@ -489,7 +491,7 @@ class EpisodeController extends BaseController
            $data = [
                'podcast' => $this->podcast,
                'episode' => $this->episode,
                'status' => (new StatusModel())
                'post' => (new PostModel())
                    ->where([
                        'actor_id' => $this->podcast->actor_id,
                        'episode_id' => $this->episode->id,
@@ -513,7 +515,7 @@ class EpisodeController extends BaseController
    public function attemptPublishEdit(): RedirectResponse
    {
        $rules = [
            'status_id' => 'required',
            'post_id' => 'required',
            'publication_method' => 'required',
            'scheduled_publication_date' =>
                'valid_date[Y-m-d H:i]|permit_empty',
@@ -549,19 +551,19 @@ class EpisodeController extends BaseController
            $this->episode->published_at = Time::now();
        }

        $status = (new StatusModel())->getStatusById($this->request->getPost('status_id'));
        $post = (new PostModel())->getPostById($this->request->getPost('post_id'));

        if ($status !== null) {
            $status->message = $this->request->getPost('message');
            $status->published_at = $this->episode->published_at;
        if ($post !== null) {
            $post->message = $this->request->getPost('message');
            $post->published_at = $this->episode->published_at;

            $statusModel = new StatusModel();
            if (! $statusModel->editStatus($status)) {
            $postModel = new PostModel();
            if (! $postModel->editPost($post)) {
                $db->transRollback();
                return redirect()
                    ->back()
                    ->withInput()
                    ->with('errors', $statusModel->errors());
                    ->with('errors', $postModel->errors());
            }
        }

@@ -585,14 +587,14 @@ class EpisodeController extends BaseController
            $db = db_connect();
            $db->transStart();

            $statusModel = new StatusModel();
            $status = $statusModel
            $postModel = new PostModel();
            $post = $postModel
                ->where([
                    'actor_id' => $this->podcast->actor_id,
                    'episode_id' => $this->episode->id,
                ])
                ->first();
            $statusModel->removeStatus($status);
            $postModel->removePost($post);

            $this->episode->published_at = null;

@@ -656,13 +658,13 @@ class EpisodeController extends BaseController

        $db->transStart();

        $allStatusesLinkedToEpisode = (new StatusModel())
        $allPostsLinkedToEpisode = (new PostModel())
            ->where([
                'episode_id' => $this->episode->id,
            ])
            ->findAll();
        foreach ($allStatusesLinkedToEpisode as $status) {
            (new StatusModel())->removeStatus($status);
        foreach ($allPostsLinkedToEpisode as $post) {
            (new PostModel())->removePost($post);
        }

        // set episode published_at to null to unpublish
@@ -782,4 +784,41 @@ class EpisodeController extends BaseController
        ]);
        return view('admin/episode/embeddable_player', $data);
    }

    public function attemptCommentCreate(): RedirectResponse
    {
        $rules = [
            'message' => 'required|max_length[500]',
        ];

        if (! $this->validate($rules)) {
            return redirect()
                ->back()
                ->withInput()
                ->with('errors', $this->validator->getErrors());
        }

        $message = $this->request->getPost('message');

        $newComment = new Comment([
            'actor_id' => interact_as_actor_id(),
            'episode_id' => $this->episode->id,
            'message' => $message,
            'created_at' => new Time('now'),
            'created_by' => user_id(),
        ]);

        $commentModel = new CommentModel();
        if (
            ! $commentModel->addComment($newComment, true)
        ) {
            return redirect()
                ->back()
                ->withInput()
                ->with('errors', $commentModel->errors());
        }

        // Comment has been successfully created
        return redirect()->back();
    }
}
+57 −2

File changed.

Preview size limit exceeded, changes collapsed.

+2 −2
Original line number Diff line number Diff line
@@ -18,7 +18,7 @@ use App\Libraries\PodcastActor;
use App\Libraries\PodcastEpisode;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use App\Models\StatusModel;
use App\Models\PostModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\Response;

@@ -81,7 +81,7 @@ class PodcastController extends BaseController
        if (! ($cachedView = cache($cacheName))) {
            $data = [
                'podcast' => $this->podcast,
                'statuses' => (new StatusModel())->getActorPublishedStatuses($this->podcast->actor_id),
                'posts' => (new PostModel())->getActorPublishedPosts($this->podcast->actor_id),
            ];

            // if user is logged in then send to the authenticated activity view
Loading