Commit 9303e51b authored by Yassine Doghri's avatar Yassine Doghri
Browse files

feat: add task to housekeeping setting for resetting all instance counts

set two toggle switches to run housekeeping tasks seperately if needed
parent e65e236b
Pipeline #1201 passed with stages
in 8 minutes and 17 seconds
......@@ -22,7 +22,6 @@ use CodeIgniter\HTTP\URI;
use CodeIgniter\I18n\Time;
use Modules\Analytics\AnalyticsTrait;
use Modules\Fediverse\Controllers\PostController as FediversePostController;
use Modules\Fediverse\Entities\Post as FediversePost;
use Modules\Fediverse\Models\FavouriteModel;
class PostController extends FediversePostController
......@@ -33,6 +32,11 @@ class PostController extends FediversePostController
protected Actor $actor;
/**
* @var CastopodPost
*/
protected $post;
/**
* @var string[]
*/
......@@ -53,6 +57,7 @@ class PostController extends FediversePostController
count($params) > 1 &&
($post = (new PostModel())->getPostById($params[1])) !== null
) {
/** @var CastopodPost $post */
$this->post = $post;
unset($params[0]);
......@@ -163,7 +168,7 @@ class PostController extends FediversePostController
->with('errors', $this->validator->getErrors());
}
$newPost = new FediversePost([
$newPost = new CastopodPost([
'actor_id' => interact_as_actor_id(),
'in_reply_to_id' => $this->post->id,
'message' => $this->request->getPost('message'),
......@@ -171,6 +176,10 @@ class PostController extends FediversePostController
'created_by' => user_id(),
]);
if ($this->post->in_reply_to_id === null && $this->post->episode_id !== null) {
$newPost->episode_id = $this->post->episode_id;
}
$postModel = new PostModel();
if (! $postModel->addReply($newPost)) {
return redirect()
......
......@@ -149,7 +149,10 @@ class EpisodeCommentModel extends UuidModel
->whereIn('in_reply_to_id', function (BaseBuilder $builder) use (&$episodeId): BaseBuilder {
return $builder->select('id')
->from(config('Fediverse')->tablesPrefix . 'posts')
->where('episode_id', $episodeId);
->where([
'episode_id' => $episodeId,
'in_reply_to_id' => null,
]);
})
->where('`created_at` <= NOW()', null, false)
->getCompiledSelect();
......@@ -179,6 +182,37 @@ class EpisodeCommentModel extends UuidModel
->findAll();
}
public function resetLikesCount(): int | false
{
$commentsLikesCount = $this->db->table('likes')
->select('comment_id as id, COUNT(*) as `likes_count`')
->groupBy('id')
->get()
->getResultArray();
if ($commentsLikesCount !== []) {
$this->uuidUseBytes = false;
return $this->updateBatch($commentsLikesCount, 'id');
}
return 0;
}
public function resetRepliesCount(): int | false
{
$commentsRepliesCount = $this->select('episode_comments.id, COUNT(*) as `replies_count`')
->join('episode_comments as c2', 'episode_comments.id = c2.in_reply_to_id')
->groupBy('episode_comments.id')
->get()
->getResultArray();
if ($commentsRepliesCount !== []) {
$this->uuidUseBytes = false;
return $this->updateBatch($commentsRepliesCount, 'id');
}
return 0;
}
/**
* @param array<string, array<string|int, mixed>> $data
* @return array<string, array<string|int, mixed>>
......
......@@ -322,6 +322,58 @@ class EpisodeModel extends Model
return $stats;
}
public function resetCommentsCount(): int | false
{
$episodeCommentsCount = $this->select('episodes.id, COUNT(*) as `comments_count`')
->join('episode_comments', 'episodes.id = episode_comments.episode_id')
->where('in_reply_to_id', null)
->groupBy('episodes.id')
->getCompiledSelect();
$episodePostsRepliesCount = $this
->select('episodes.id, COUNT(*) as `comments_count`')
->join(
config('Fediverse')
->tablesPrefix . 'posts',
'episodes.id = ' . config('Fediverse')->tablesPrefix . 'posts.episode_id'
)
->where('in_reply_to_id IS NOT', null)
->groupBy('episodes.id')
->getCompiledSelect();
$query = $this->db->query(
'SELECT `id`, SUM(`comments_count`) as `comments_count` FROM (' . $episodeCommentsCount . ' UNION ALL ' . $episodePostsRepliesCount . ') x GROUP BY `id`'
);
$countsPerEpisodeId = $query->getResultArray();
if ($countsPerEpisodeId !== []) {
return $this->updateBatch($countsPerEpisodeId, 'id');
}
return 0;
}
public function resetPostsCount(): int | false
{
$episodePostsCount = $this->select('episodes.id, COUNT(*) as `posts_count`')
->join(
config('Fediverse')
->tablesPrefix . 'posts',
'episodes.id = ' . config('Fediverse')->tablesPrefix . 'posts.episode_id'
)
->where('in_reply_to_id', null)
->groupBy('episodes.id')
->get()
->getResultArray();
if ($episodePostsCount !== []) {
return $this->updateBatch($episodePostsCount, 'id');
}
return 0;
}
/**
* @param mixed[] $data
*
......
......@@ -49,8 +49,33 @@ class PostModel extends FediversePostModel
return $this->where([
'episode_id' => $episodeId,
])
->where('in_reply_to_id', null)
->where('`published_at` <= NOW()', null, false)
->orderBy('published_at', 'DESC')
->findAll();
}
public function setEpisodeIdForRepliesOfEpisodePosts(): int | false
{
// make sure that posts in reply to episode activities have an episode id
$postsToUpdate = $this->db->table(config('Fediverse')->tablesPrefix . 'posts as p1')
->join(config('Fediverse')->tablesPrefix . 'posts as p2', 'p1.id = p2.in_reply_to_id')
->select('p2.id, p1.episode_id')
->where([
'p1.in_reply_to_id' => null,
'p2.in_reply_to_id IS NOT' => null,
'p2.episode_id' => null,
'p1.episode_id IS NOT' => null,
])
->get()
->getResultArray();
if ($postsToUpdate !== []) {
$postModel = new self();
$postModel->uuidUseBytes = false;
return $postModel->updateBatch($postsToUpdate, 'id');
}
return 0;
}
}
......@@ -10,11 +10,13 @@ declare(strict_types=1);
namespace Modules\Admin\Controllers;
use App\Entities\Media\Image;
use App\Models\ActorModel;
use App\Models\EpisodeCommentModel;
use App\Models\EpisodeModel;
use App\Models\MediaModel;
use App\Models\PersonModel;
use App\Models\PodcastModel;
use App\Models\PostModel;
use CodeIgniter\Files\File;
use CodeIgniter\HTTP\RedirectResponse;
use PHP_ICO;
......@@ -158,93 +160,112 @@ class SettingsController extends BaseController
public function runHousekeeping(): RedirectResponse
{
if ($this->request->getPost('reset_counts') === 'yes') {
// recalculate fediverse counts
(new ActorModel())->resetFollowersCount();
(new ActorModel())->resetPostsCount();
(new PostModel())->setEpisodeIdForRepliesOfEpisodePosts();
(new PostModel())->resetFavouritesCount();
(new PostModel())->resetReblogsCount();
(new PostModel())->resetRepliesCount();
(new EpisodeModel())->resetCommentsCount();
(new EpisodeModel())->resetPostsCount();
(new EpisodeCommentModel())->resetLikesCount();
(new EpisodeCommentModel())->resetRepliesCount();
}
helper('media');
// Delete all podcast image sizes to recreate them
$allPodcasts = (new PodcastModel())->findAll();
foreach ($allPodcasts as $podcast) {
$podcastImages = glob(media_path("/podcasts/{$podcast->handle}/*_*{jpg,png,webp}"), GLOB_BRACE);
if ($this->request->getPost('rewrite_media') === 'yes') {
if ($podcastImages) {
foreach ($podcastImages as $podcastImage) {
if (is_file($podcastImage)) {
unlink($podcastImage);
// Delete all podcast image sizes to recreate them
$allPodcasts = (new PodcastModel())->findAll();
foreach ($allPodcasts as $podcast) {
$podcastImages = glob(media_path("/podcasts/{$podcast->handle}/*_*{jpg,png,webp}"), GLOB_BRACE);
if ($podcastImages) {
foreach ($podcastImages as $podcastImage) {
if (is_file($podcastImage)) {
unlink($podcastImage);
}
}
}
}
}
// Delete all person image sizes to recreate them
$personsImages = glob(media_path('/persons/*_*{jpg,png,webp}'), GLOB_BRACE);
if ($personsImages) {
foreach ($personsImages as $personsImage) {
if (is_file($personsImage)) {
unlink($personsImage);
// Delete all person image sizes to recreate them
$personsImages = glob(media_path('/persons/*_*{jpg,png,webp}'), GLOB_BRACE);
if ($personsImages) {
foreach ($personsImages as $personsImage) {
if (is_file($personsImage)) {
unlink($personsImage);
}
}
}
}
$allImages = (new MediaModel('image'))->getAllOfType();
foreach ($allImages as $image) {
if (str_starts_with($image->file_path, 'podcasts')) {
if (str_ends_with($image->file_path, 'banner.jpg') || str_ends_with($image->file_path, 'banner.png')) {
$image->sizes = config('Images')
->podcastBannerSizes;
} else {
$allImages = (new MediaModel('image'))->getAllOfType();
foreach ($allImages as $image) {
if (str_starts_with($image->file_path, 'podcasts')) {
if (str_ends_with($image->file_path, 'banner.jpg') || str_ends_with(
$image->file_path,
'banner.png'
)) {
$image->sizes = config('Images')
->podcastBannerSizes;
} else {
$image->sizes = config('Images')
->podcastCoverSizes;
}
} elseif (str_starts_with($image->file_path, 'persons')) {
$image->sizes = config('Images')
->podcastCoverSizes;
->personAvatarSizes;
}
} elseif (str_starts_with($image->file_path, 'persons')) {
$image->sizes = config('Images')
->personAvatarSizes;
$image->setFile(new File(media_path($image->file_path)));
(new MediaModel('image'))->updateMedia($image);
}
$image->setFile(new File(media_path($image->file_path)));
(new MediaModel('image'))->updateMedia($image);
}
$allAudio = (new MediaModel('audio'))->getAllOfType();
foreach ($allAudio as $audio) {
$audio->setFile(new File(media_path($audio->file_path)));
(new MediaModel('audio'))->updateMedia($audio);
}
$allAudio = (new MediaModel('audio'))->getAllOfType();
foreach ($allAudio as $audio) {
$audio->setFile(new File(media_path($audio->file_path)));
$allTranscripts = (new MediaModel('transcript'))->getAllOfType();
foreach ($allTranscripts as $transcript) {
$transcript->setFile(new File(media_path($transcript->file_path)));
(new MediaModel('audio'))->updateMedia($audio);
}
(new MediaModel('transcript'))->updateMedia($transcript);
}
$allTranscripts = (new MediaModel('transcript'))->getAllOfType();
foreach ($allTranscripts as $transcript) {
$transcript->setFile(new File(media_path($transcript->file_path)));
$allChapters = (new MediaModel('chapters'))->getAllOfType();
foreach ($allChapters as $chapters) {
$chapters->setFile(new File(media_path($chapters->file_path)));
(new MediaModel('transcript'))->updateMedia($transcript);
}
(new MediaModel('chapters'))->updateMedia($chapters);
}
$allChapters = (new MediaModel('chapters'))->getAllOfType();
foreach ($allChapters as $chapters) {
$chapters->setFile(new File(media_path($chapters->file_path)));
$allVideos = (new MediaModel('video'))->getAllOfType();
foreach ($allVideos as $video) {
$video->setFile(new File(media_path($video->file_path)));
(new MediaModel('chapters'))->updateMedia($chapters);
}
(new MediaModel('video'))->updateMedia($video);
}
$allVideos = (new MediaModel('video'))->getAllOfType();
foreach ($allVideos as $video) {
$video->setFile(new File(media_path($video->file_path)));
// reset avatar and banner image urls for each podcast actor
foreach ($allPodcasts as $podcast) {
$actorModel = new ActorModel();
$actor = $actorModel->getActorById($podcast->actor_id);
(new MediaModel('video'))->updateMedia($video);
}
if ($actor !== null) {
// update values
$actor->avatar_image_url = $podcast->cover->federation_url;
$actor->avatar_image_mimetype = $podcast->cover->file_mimetype;
$actor->cover_image_url = $podcast->banner->federation_url;
$actor->cover_image_mimetype = $podcast->banner->file_mimetype;
// reset avatar and banner image urls for each podcast actor
foreach ($allPodcasts as $podcast) {
$actorModel = new ActorModel();
$actor = $actorModel->getActorById($podcast->actor_id);
if ($actor !== null) {
// update values
$actor->avatar_image_url = $podcast->cover->federation_url;
$actor->avatar_image_mimetype = $podcast->cover->file_mimetype;
$actor->cover_image_url = $podcast->banner->federation_url;
$actor->cover_image_mimetype = $podcast->banner->file_mimetype;
if ($actor->hasChanged()) {
$actorModel->update($actor->id, $actor);
if ($actor->hasChanged()) {
$actorModel->update($actor->id, $actor);
}
}
}
}
......
......@@ -30,7 +30,11 @@ return [
],
'housekeeping' => [
'title' => 'Housekeeping',
'subtitle' => 'Runs various housekeeping tasks, such as rewriting media files metadata (images, audio files, transcripts, chapters, …).',
'subtitle' => 'Runs various housekeeping tasks, such as resetting counts and rewriting media files metadata. This may take a while.',
'reset_counts' => 'Reset counts',
'reset_counts_helper' => 'This option will recalculate and reset all data counts (number of followers, posts, comments, …).',
'rewrite_media' => 'Rewrite media metadata',
'rewrite_media_helper' => 'This option will delete all superfluous media files and recreate them (images, audio files, transcripts, chapters, …)',
'run' => 'Run housekeeping',
'runSuccess' => 'Housekeeping has been run successfully!',
],
......
......@@ -31,6 +31,10 @@ return [
'housekeeping' => [
'title' => 'Ménage',
'subtitle' => 'Exécute un nombre de tâches de nettoyage, comme la réécriture de métadonnées des fichiers media (images, fichiers audio, transcript, chapitres, …).',
'reset_counts' => 'Réinitialiser les compteurs',
'reset_counts_helper' => 'Cette option recalcul et réinitialise les compteurs de données (nombre d’abonné·e·s, de publications, de commentaires, …).',
'rewrite_media' => 'Réécrire les métadonnées des fichiers média',
'rewrite_media_helper' => 'Cette option supprimera tous les fichiers média superflus et les recréera (images, fichiers audio, transcripts, chapitrages, …)',
'run' => 'Faire le ménage',
'runSuccess' => 'Le ménage a été effectué avec succès !',
],
......
......@@ -28,7 +28,10 @@ class PostController extends Controller
*/
protected $helpers = ['fediverse'];
protected Post $post;
/**
* @var Post
*/
protected $post;
protected Fediverse $config;
......
......@@ -272,6 +272,47 @@ class ActorModel extends BaseModel
return $found;
}
public function resetFollowersCount(): int | false
{
$tablePrefix = config('Fediverse')
->tablesPrefix;
$actorsFollowersCount = $this->db->table($tablePrefix . 'follows')->select(
'target_actor_id as id, COUNT(*) as `followers_count`'
)
->groupBy('id')
->get()
->getResultArray();
if ($actorsFollowersCount !== []) {
return $this->updateBatch($actorsFollowersCount, 'id');
}
return 0;
}
public function resetPostsCount(): int | false
{
$tablePrefix = config('Fediverse')
->tablesPrefix;
$actorsFollowersCount = $this->db->table($tablePrefix . 'posts')->select(
'actor_id as id, COUNT(*) as `posts_count`'
)
->where([
'in_reply_to_id' => null,
])
->groupBy('actor_id')
->get()
->getResultArray();
if ($actorsFollowersCount !== []) {
return $this->updateBatch($actorsFollowersCount, 'id');
}
return 0;
}
public function clearCache(Actor $actor): void
{
$cachePrefix = config('Fediverse')
......
......@@ -276,9 +276,12 @@ class PostModel extends BaseUuidModel
}
}
model('ActorModel', false)
->where('id', $post->actor_id)
->increment('posts_count');
if ($post->in_reply_to_id === null) {
// increment posts_count only if not reply
model('ActorModel', false)
->where('id', $post->actor_id)
->increment('posts_count');
}
if ($registerActivity) {
// set post id and uri to construct NoteObject
......@@ -619,6 +622,64 @@ class PostModel extends BaseUuidModel
return $found;
}
public function resetFavouritesCount(): int | false
{
$tablePrefix = config('Fediverse')
->tablesPrefix;
$postsFavouritesCount = $this->db->table($tablePrefix . 'favourites')->select(
'post_id as id, COUNT(*) as `favourites_count`'
)
->groupBy('id')
->get()
->getResultArray();
if ($postsFavouritesCount !== []) {
$this->uuidUseBytes = false;
return $this->updateBatch($postsFavouritesCount, 'id');
}
return 0;
}
public function resetReblogsCount(): int | false
{
$tablePrefix = config('Fediverse')
->tablesPrefix;
$postsReblogsCount = $this->select($tablePrefix . 'posts.id, COUNT(*) as `replies_count`')
->join($tablePrefix . 'posts as p2', $tablePrefix . 'posts.id = p2.reblog_of_id')
->groupBy($tablePrefix . 'posts.id')
->get()
->getResultArray();
if ($postsReblogsCount !== []) {
$this->uuidUseBytes = false;
return $this->updateBatch($postsReblogsCount, 'id');
}
return 0;
}
public function resetRepliesCount(): int | false
{
$tablePrefix = config('Fediverse')
->tablesPrefix;
$postsRepliesCount = $this->select($tablePrefix . 'posts.id, COUNT(*) as `replies_count`')
->join($tablePrefix . 'posts as p2', $tablePrefix . 'posts.id = p2.in_reply_to_id')
->groupBy($tablePrefix . 'posts.id')
->get()
->getResultArray();
if ($postsRepliesCount !== []) {
$this->uuidUseBytes = false;
return $this->updateBatch($postsRepliesCount, 'id');
}
return 0;
}
public function clearCache(Post $post): void
{
$cachePrefix = config('Fediverse')
......
......@@ -77,6 +77,9 @@
title="<?= lang('Settings.housekeeping.title') ?>"
subtitle="<?= lang('Settings.housekeeping.subtitle') ?>" >
<Forms.Toggler name="reset_counts" value="yes" size="small" checked="true" hint="<?= lang('Settings.housekeeping.reset_counts_helper') ?>"><?= lang('Settings.housekeeping.reset_counts') ?></Forms.Toggler>
<Forms.Toggler name="rewrite_media" value="yes" size="small" checked="true" hint="<?= lang('Settings.housekeeping.rewrite_media_helper') ?>"><?= lang('Settings.housekeeping.rewrite_media') ?></Forms.Toggler>
<Button variant="primary" type="submit" iconLeft="home-gear"><?= lang('Settings.housekeeping.run') ?></Button>
</Forms.Section>
......
<footer>
<?php if (can_user_interact()): ?>
<form action="<?= route_to('comment-attempt-like', interact_as_actor()->username, $comment->episode->slug, $comment->id) ?>" method="POST" class="flex items-center gap-x-4">
<form action="<?= route_to('episode-comment-attempt-like', interact_as_actor()->username, $comment->episode->slug, $comment->id) ?>" method="POST" class="flex items-center gap-x-4">
<button type="submit" name="action" class="inline-flex items-center hover:underline group" title="<?= lang(
'Comment.likes',
[
'numberOfLikes' => $comment->likes_count,
],
) ?>"><?= icon('heart', 'text-xl mr-1 opacity-40 group-hover:text-red-600 group-hover:opacity-100') . $comment->likes_count ?></button>
) ?>"><?= icon('heart', 'text-xl mr-1 text-gray-400 group-hover:text-red-600') . $comment->likes_count ?></button>
<Button uri="<?= route_to('episode-comment', $comment->episode->podcast->handle, $comment->episode->slug, $comment->id) ?>" size="small"><?= lang('Comment.reply') ?></Button>
</form>
<?php if ($comment->replies_count): ?>
......
......@@ -6,7 +6,7 @@
[
'numberOfLikes' => $comment->likes_count,
],
) ?>"><?= icon('heart', 'text-xl mr-1 opacity-40 group-hover:text-red-600 group-hover:opacity-100') . $comment->likes_count ?></button>
) ?>"><?= icon('heart', 'text-xl mr-1 text-gray-400 group-hover:text-red-600') . $comment->likes_count ?></button>
<Button uri="<?= route_to('post', $podcast->handle, $comment->id) ?>" size="small"><?= lang('Comment.reply') ?></Button>
</form>
<?php if