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
......@@ -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*');
});
......@@ -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',
],
);
});
......
......@@ -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->