Unverified Commit 2d297f45 authored by Yassine Doghri's avatar Yassine Doghri
Browse files

feat: add cache to ActivityPub sql queries + cache activity and note pages

- authenticated pages are not cached
- add AnalyticsTrait to register a podcast webpage hit across
mutliple controllers
- set actor_id as unique in podcasts table
- fix issues with preview card not appearing
- update codeigniter4-uuid
parent 54b84f96
......@@ -81,6 +81,10 @@ Events::on('on_note_add', function ($note) {
->where('id', $note->episode_id)
->increment('notes_total');
}
// Removing all of the podcast pages is a bit overkill, but works perfectly
// same for other events below
cache()->deleteMatching("page_podcast#{$note->actor->podcast->id}*");
});
Events::on('on_note_remove', function ($note) {
......@@ -97,6 +101,9 @@ Events::on('on_note_remove', function ($note) {
->where('id', $note->episode_id)
->decrement('favourites_total', $note->favourites_count);
}
cache()->deleteMatching("page_podcast#{$note->actor->podcast->id}*");
cache()->deleteMatching("page_note#{$note->id}*");
});
Events::on('on_note_reblog', function ($actor, $note) {
......@@ -109,10 +116,18 @@ Events::on('on_note_reblog', function ($actor, $note) {
->where('id', $episodeId)
->increment('notes_total');
}
cache()->deleteMatching("page_podcast#{$note->actor->podcast->id}*");
cache()->deleteMatching("page_note#{$note->id}*");
if ($actor->is_podcast) {
cache()->deleteMatching("page_podcast#{$actor->podcast->id}*");
}
});
Events::on('on_note_undo_reblog', function ($reblogNote) {
if ($episodeId = $reblogNote->reblog_of_note->episode_id) {
$note = $reblogNote->reblog_of_note;
if ($episodeId = $note->episode_id) {
model('EpisodeModel')
->where('id', $episodeId)
->decrement('reblogs_total');
......@@ -121,6 +136,29 @@ Events::on('on_note_undo_reblog', function ($reblogNote) {
->where('id', $episodeId)
->decrement('notes_total');
}
cache()->deleteMatching("page_podcast#{$note->actor->podcast->id}*");
cache()->deleteMatching("page_note#{$note->id}*");
if ($reblogNote->actor->is_podcast) {
cache()->deleteMatching(
"page_podcast#{$reblogNote->actor->podcast->id}*",
);
}
});
Events::on('on_note_reply', function ($reply) {
$note = $reply->reply_to_note;
cache()->deleteMatching("page_podcast#{$note->actor->podcast->id}*");
cache()->deleteMatching("page_note#{$note->id}*");
});
Events::on('on_reply_remove', function ($reply) {
$note = $reply->reply_to_note;
cache()->deleteMatching("page_podcast#{$note->actor->podcast->id}*");
cache()->deleteMatching("page_note#{$note->id}*");
});
Events::on('on_note_favourite', function ($actor, $note) {
......@@ -129,6 +167,13 @@ Events::on('on_note_favourite', function ($actor, $note) {
->where('id', $note->episode_id)
->increment('favourites_total');
}
cache()->deleteMatching("page_podcast#{$actor->podcast->id}*");
cache()->deleteMatching("page_note#{$note->id}*");
if ($note->in_reply_to_id) {
cache()->deleteMatching("page_note#{$note->in_reply_to_id}*");
}
});
Events::on('on_note_undo_favourite', function ($actor, $note) {
......@@ -137,4 +182,31 @@ Events::on('on_note_undo_favourite', function ($actor, $note) {
->where('id', $note->episode_id)
->decrement('favourites_total');
}
cache()->deleteMatching("page_podcast#{$actor->podcast->id}*");
cache()->deleteMatching("page_note#{$note->id}*");
if ($note->in_reply_to_id) {
cache()->deleteMatching("page_note#{$note->in_reply_to_id}*");
}
});
Events::on('on_block_actor', function ($actorId) {
cache()->deleteMatching('page_podcast*');
cache()->deleteMatching('page_note*');
});
Events::on('on_unblock_actor', function ($actorId) {
cache()->deleteMatching('page_podcast*');
cache()->deleteMatching('page_note*');
});
Events::on('on_block_domain', function ($domainName) {
cache()->deleteMatching('page_podcast*');
cache()->deleteMatching('page_note*');
});
Events::on('on_unblock_domain', function ($domainName) {
cache()->deleteMatching('page_podcast*');
cache()->deleteMatching('page_note*');
});
......@@ -8,15 +8,32 @@
namespace App\Controllers;
use Analytics\AnalyticsTrait;
class Actor extends \ActivityPub\Controllers\ActorController
{
use AnalyticsTrait;
public function follow()
{
helper(['form', 'components', 'svg']);
$data = [
'actor' => $this->actor,
];
// Prevent analytics hit when authenticated
if (!can_user_interact()) {
$this->registerPodcastWebpageHit($this->actor->podcast->id);
}
$cacheName = "page_podcast@{$this->actor->username}_follow";
if (!($cachedView = cache($cacheName))) {
helper(['form', 'components', 'svg']);
$data = [
'actor' => $this->actor,
];
return view('podcast/follow', $data, [
'cache' => DECADE,
'cache_name' => $cacheName,
]);
}
return view('podcast/follow', $data);
return $cachedView;
}
}
......@@ -26,7 +26,7 @@ class BaseController extends Controller
*
* @var array
*/
protected $helpers = ['auth', 'analytics', 'svg', 'components', 'misc'];
protected $helpers = ['auth', 'svg', 'components', 'misc'];
/**
* Constructor.
......@@ -47,15 +47,5 @@ class BaseController extends Controller
// Preload any models, libraries, etc, here.
//--------------------------------------------------------------------
// E.g.: $this->session = \Config\Services::session();
set_user_session_deny_list_ip();
set_user_session_browser();
set_user_session_referer();
set_user_session_entry_page();
}
protected static function triggerWebpageHit($podcastId)
{
webpage_hit($podcastId);
}
}
......@@ -8,12 +8,15 @@
namespace App\Controllers;
use Analytics\AnalyticsTrait;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use SimpleXMLElement;
class Episode extends BaseController
{
use AnalyticsTrait;
/**
* @var \App\Entities\Podcast
*/
......@@ -44,10 +47,15 @@ class Episode extends BaseController
public function index()
{
self::triggerWebpageHit($this->podcast->id);
// Prevent analytics hit when authenticated
if (!can_user_interact()) {
$this->registerPodcastWebpageHit($this->episode->podcast_id);
}
$locale = service('request')->getLocale();
$cacheName = "page_podcast#{$this->podcast->id}_episode{$this->episode->id}_{$locale}";
$cacheName =
"page_podcast#{$this->podcast->id}_episode#{$this->episode->id}_{$locale}" .
(can_user_interact() ? '_authenticated' : '');
if (!($cachedView = cache($cacheName))) {
helper('persons');
......@@ -69,13 +77,7 @@ class Episode extends BaseController
if (can_user_interact()) {
helper('form');
// The page cache is set to a decade so it is deleted manually upon podcast update
return view('podcast/episode_authenticated', $data, [
'cache' => $secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
: DECADE,
'cache_name' => $cacheName . '_authenticated',
]);
return view('podcast/episode_authenticated', $data);
} else {
// The page cache is set to a decade so it is deleted manually upon podcast update
return view('podcast/episode', $data, [
......@@ -94,7 +96,10 @@ class Episode extends BaseController
{
header('Content-Security-Policy: frame-ancestors https://* http://*');
self::triggerWebpageHit($this->episode->podcast_id);
// Prevent analytics hit when authenticated
if (!can_user_interact()) {
$this->registerPodcastWebpageHit($this->episode->podcast_id);
}
$session = \Config\Services::session();
$session->start();
......@@ -107,7 +112,7 @@ class Episode extends BaseController
$locale = service('request')->getLocale();
$cacheName = "page_podcast#{$this->podcast->id}_episode{$this->episode->id}_embeddable_player_{$theme}_{$locale}";
$cacheName = "page_podcast#{$this->podcast->id}_episode#{$this->episode->id}_embeddable_player_{$theme}_{$locale}";
if (!($cachedView = cache($cacheName))) {
$theme = EpisodeModel::$themes[$theme];
......
......@@ -8,6 +8,7 @@
namespace App\Controllers;
use Analytics\AnalyticsTrait;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use CodeIgniter\HTTP\URI;
......@@ -15,6 +16,8 @@ use CodeIgniter\I18n\Time;
class Note extends \ActivityPub\Controllers\NoteController
{
use AnalyticsTrait;
/**
* @var \App\Entities\Podcast
*/
......@@ -47,24 +50,46 @@ class Note extends \ActivityPub\Controllers\NoteController
public function index()
{
helper('persons');
$persons = [];
construct_person_array($this->podcast->persons, $persons);
$data = [
'podcast' => $this->podcast,
'actor' => $this->actor,
'note' => $this->note,
'persons' => $persons,
];
// Prevent analytics hit when authenticated
if (!can_user_interact()) {
$this->registerPodcastWebpageHit($this->podcast->id);
}
// if user is logged in then send to the authenticated activity view
if (can_user_interact()) {
helper('form');
return view('podcast/note_authenticated', $data);
} else {
return view('podcast/note', $data);
$cacheName = implode(
'_',
array_filter([
'page',
"note#{$this->note->id}",
service('request')->getLocale(),
can_user_interact() ? '_authenticated' : null,
]),
);
if (!($cachedView = cache($cacheName))) {
helper('persons');
$persons = [];
construct_person_array($this->podcast->persons, $persons);
$data = [
'podcast' => $this->podcast,
'actor' => $this->actor,
'note' => $this->note,
'persons' => $persons,
];
// if user is logged in then send to the authenticated activity view
if (can_user_interact()) {
helper('form');
return view('podcast/note_authenticated', $data);
} else {
return view('podcast/note', $data, [
'cache' => DECADE,
'cache_name' => $cacheName,
]);
}
}
return $cachedView;
}
public function attemptCreate()
......@@ -198,15 +223,37 @@ class Note extends \ActivityPub\Controllers\NoteController
public function remoteAction($action)
{
$data = [
'podcast' => $this->podcast,
'actor' => $this->actor,
'note' => $this->note,
'action' => $action,
];
// Prevent analytics hit when authenticated
if (!can_user_interact()) {
$this->registerPodcastWebpageHit($this->podcast->id);
}
$cacheName = implode(
'_',
array_filter([
'page',
"note#{$this->note->id}",
"remote_{$action}",
service('request')->getLocale(),
]),
);
helper('form');
if (!($cachedView = cache($cacheName))) {
$data = [
'podcast' => $this->podcast,
'actor' => $this->actor,
'note' => $this->note,
'action' => $action,
];
helper('form');
return view('podcast/note_remote_action', $data, [
'cache' => DECADE,
'cache_name' => $cacheName,
]);
}
return view('podcast/note_remote_action', $data);
return $cachedView;
}
}
......@@ -56,7 +56,7 @@ class Page extends BaseController
$locale = service('request')->getLocale();
$allPodcasts = (new PodcastModel())->findAll();
$cacheName = "paĝe_credits_{$locale}";
$cacheName = "page_credits_{$locale}";
if (!($found = cache($cacheName))) {
$page = new \App\Entities\Page([
'title' => lang('Person.credits', [], $locale),
......
......@@ -8,12 +8,15 @@
namespace App\Controllers;
use Analytics\AnalyticsTrait;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use App\Models\NoteModel;
class Podcast extends BaseController
{
use AnalyticsTrait;
/**
* @var \App\Entities\Podcast|null
*/
......@@ -37,32 +40,56 @@ class Podcast extends BaseController
public function activity()
{
self::triggerWebpageHit($this->podcast->id);
helper('persons');
$persons = [];
construct_person_array($this->podcast->persons, $persons);
$data = [
'podcast' => $this->podcast,
'notes' => (new NoteModel())->getActorNotes(
$this->podcast->actor_id,
),
'persons' => $persons,
];
// if user is logged in then send to the authenticated activity view
if (can_user_interact()) {
helper('form');
return view('podcast/activity_authenticated', $data);
} else {
return view('podcast/activity', $data);
// Prevent analytics hit when authenticated
if (!can_user_interact()) {
$this->registerPodcastWebpageHit($this->podcast->id);
}
$cacheName = implode(
'_',
array_filter([
'page',
"podcast#{$this->podcast->id}",
'activity',
service('request')->getLocale(),
can_user_interact() ? '_authenticated' : null,
]),
);
if (!($cachedView = cache($cacheName))) {
helper('persons');
$persons = [];
construct_person_array($this->podcast->persons, $persons);
$data = [
'podcast' => $this->podcast,
'notes' => (new NoteModel())->getActorPublishedNotes(
$this->podcast->actor_id,
),
'persons' => $persons,
];
// if user is logged in then send to the authenticated activity view
if (can_user_interact()) {
helper('form');
return view('podcast/activity_authenticated', $data);
} else {
return view('podcast/activity', $data, [
'cache' => DECADE,
'cache_name' => $cacheName,
]);
}
}
return $cachedView;
}
public function episodes()
{
self::triggerWebpageHit($this->podcast->id);
// Prevent analytics hit when authenticated
if (!can_user_interact()) {
$this->registerPodcastWebpageHit($this->podcast->id);
}
$yearQuery = $this->request->getGet('year');
$seasonQuery = $this->request->getGet('season');
......@@ -85,14 +112,15 @@ class Podcast extends BaseController
array_filter([
'page',
"podcast#{$this->podcast->id}",
'episodes',
$yearQuery ? 'year' . $yearQuery : null,
$seasonQuery ? 'season' . $seasonQuery : null,
service('request')->getLocale(),
can_user_interact() ? '_interact' : '',
can_user_interact() ? '_authenticated' : null,
]),
);
if (!($found = cache($cacheName))) {
if (!($cachedView = cache($cacheName))) {
// Build navigation array
$podcastModel = new PodcastModel();
$years = $podcastModel->getYears($this->podcast->id);
......@@ -171,14 +199,9 @@ class Podcast extends BaseController
// if user is logged in then send to the authenticated episodes view
if (can_user_interact()) {
$found = view('podcast/episodes_authenticated', $data, [
'cache' => $secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
: DECADE,
'cache_name' => $cacheName,
]);
return view('podcast/episodes_authenticated', $data);
} else {
$found = view('podcast/episodes', $data, [
return view('podcast/episodes', $data, [
'cache' => $secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
: DECADE,
......@@ -187,6 +210,6 @@ class Podcast extends BaseController
}
}
return $found;
return $cachedView;
}
}
......@@ -187,7 +187,9 @@ class AddPodcasts extends Migration
]);
$this->forge->addPrimaryKey('id');
// TODO: remove name in favor of username from actor
$this->forge->addUniqueKey('name');
$this->forge->addUniqueKey('actor_id');
$this->forge->addForeignKey(
'actor_id',
'activitypub_actors',
......
<?php
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Entities;
use App\Models\PodcastModel;
class Actor extends \ActivityPub\Entities\Actor
{
/**
* @var App\Entities\Podcast|null
*/
protected $podcast;
/**
* @var boolean
*/
protected $is_podcast;
public function getIsPodcast()
{
return !empty($this->podcast);
}
public function getPodcast()
{
if (empty($this->id)) {
throw new \RuntimeException(
'Actor must be created before getting associated podcast.',
);
}
if (empty($this->podcast)) {
$this->podcast = (new PodcastModel())->getPodcastByActorId(
$this->id,
);
}
return $this->podcast;
}
}
......@@ -31,7 +31,7 @@ class Category extends Entity
$parentId = $this->attributes['parent_id'];
return $parentId != 0
? (new CategoryModel())->findParent($parentId)
? (new CategoryModel())->getCategoryById($parentId)
: null;
}
}
......@@ -143,7 +143,7 @@ class Podcast extends Entity
}
if (empty($this->actor)) {
$this->actor = (new ActorModel())->getActorById($this->actor_id);
$this->actor = model('ActorModel')->getActorById($this->actor_id);
}
return $this->actor;
......@@ -254,7 +254,9 @@ class Podcast extends Entity
}
if (empty($this->category)) {
$this->category = (new CategoryModel())->find($this->category_id);
$this->category = (new CategoryModel())->getCategoryById(
$this->category_id,
);
}
return $this->category;
......
......@@ -6,7 +6,6 @@
* @link https://castopod.org/
*/
use ActivityPub\Models\ActorModel;
use CodeIgniter\Database\Exceptions\DataException;
use Config\Services;
......@@ -68,7 +67,7 @@ if (!function_exists('interact_as_actor')) {
$session = session();
if ($session->has('interact_as_actor_id')) {