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
This commit is contained in:
Yassine Doghri 2021-04-22 17:20:28 +00:00
parent 54b84f9684
commit 2d297f45b3
No known key found for this signature in database
GPG Key ID: 3E7F89498B960C9F
42 changed files with 880 additions and 537 deletions

View File

@ -81,6 +81,10 @@ Events::on('on_note_add', function ($note) {
->where('id', $note->episode_id) ->where('id', $note->episode_id)
->increment('notes_total'); ->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) { Events::on('on_note_remove', function ($note) {
@ -97,6 +101,9 @@ Events::on('on_note_remove', function ($note) {
->where('id', $note->episode_id) ->where('id', $note->episode_id)
->decrement('favourites_total', $note->favourites_count); ->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) { Events::on('on_note_reblog', function ($actor, $note) {
@ -109,10 +116,18 @@ Events::on('on_note_reblog', function ($actor, $note) {
->where('id', $episodeId) ->where('id', $episodeId)
->increment('notes_total'); ->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) { 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') model('EpisodeModel')
->where('id', $episodeId) ->where('id', $episodeId)
->decrement('reblogs_total'); ->decrement('reblogs_total');
@ -121,6 +136,29 @@ Events::on('on_note_undo_reblog', function ($reblogNote) {
->where('id', $episodeId) ->where('id', $episodeId)
->decrement('notes_total'); ->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) { 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) ->where('id', $note->episode_id)
->increment('favourites_total'); ->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) { 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) ->where('id', $note->episode_id)
->decrement('favourites_total'); ->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*');
}); });

View File

@ -8,15 +8,32 @@
namespace App\Controllers; namespace App\Controllers;
use Analytics\AnalyticsTrait;
class Actor extends \ActivityPub\Controllers\ActorController class Actor extends \ActivityPub\Controllers\ActorController
{ {
use AnalyticsTrait;
public function follow() public function follow()
{ {
helper(['form', 'components', 'svg']); // Prevent analytics hit when authenticated
$data = [ if (!can_user_interact()) {
'actor' => $this->actor, $this->registerPodcastWebpageHit($this->actor->podcast->id);
]; }
return view('podcast/follow', $data); $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 $cachedView;
} }
} }

View File

@ -26,7 +26,7 @@ class BaseController extends Controller
* *
* @var array * @var array
*/ */
protected $helpers = ['auth', 'analytics', 'svg', 'components', 'misc']; protected $helpers = ['auth', 'svg', 'components', 'misc'];
/** /**
* Constructor. * Constructor.
@ -47,15 +47,5 @@ class BaseController extends Controller
// Preload any models, libraries, etc, here. // Preload any models, libraries, etc, here.
//-------------------------------------------------------------------- //--------------------------------------------------------------------
// E.g.: $this->session = \Config\Services::session(); // 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);
} }
} }

View File

@ -8,12 +8,15 @@
namespace App\Controllers; namespace App\Controllers;
use Analytics\AnalyticsTrait;
use App\Models\EpisodeModel; use App\Models\EpisodeModel;
use App\Models\PodcastModel; use App\Models\PodcastModel;
use SimpleXMLElement; use SimpleXMLElement;
class Episode extends BaseController class Episode extends BaseController
{ {
use AnalyticsTrait;
/** /**
* @var \App\Entities\Podcast * @var \App\Entities\Podcast
*/ */
@ -44,10 +47,15 @@ class Episode extends BaseController
public function index() 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(); $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))) { if (!($cachedView = cache($cacheName))) {
helper('persons'); helper('persons');
@ -69,13 +77,7 @@ class Episode extends BaseController
if (can_user_interact()) { if (can_user_interact()) {
helper('form'); helper('form');
// The page cache is set to a decade so it is deleted manually upon podcast update return view('podcast/episode_authenticated', $data);
return view('podcast/episode_authenticated', $data, [
'cache' => $secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
: DECADE,
'cache_name' => $cacheName . '_authenticated',
]);
} else { } else {
// The page cache is set to a decade so it is deleted manually upon podcast update // The page cache is set to a decade so it is deleted manually upon podcast update
return view('podcast/episode', $data, [ return view('podcast/episode', $data, [
@ -94,7 +96,10 @@ class Episode extends BaseController
{ {
header('Content-Security-Policy: frame-ancestors https://* http://*'); 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 = \Config\Services::session();
$session->start(); $session->start();
@ -107,7 +112,7 @@ class Episode extends BaseController
$locale = service('request')->getLocale(); $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))) { if (!($cachedView = cache($cacheName))) {
$theme = EpisodeModel::$themes[$theme]; $theme = EpisodeModel::$themes[$theme];

View File

@ -8,6 +8,7 @@
namespace App\Controllers; namespace App\Controllers;
use Analytics\AnalyticsTrait;
use App\Models\EpisodeModel; use App\Models\EpisodeModel;
use App\Models\PodcastModel; use App\Models\PodcastModel;
use CodeIgniter\HTTP\URI; use CodeIgniter\HTTP\URI;
@ -15,6 +16,8 @@ use CodeIgniter\I18n\Time;
class Note extends \ActivityPub\Controllers\NoteController class Note extends \ActivityPub\Controllers\NoteController
{ {
use AnalyticsTrait;
/** /**
* @var \App\Entities\Podcast * @var \App\Entities\Podcast
*/ */
@ -47,24 +50,46 @@ class Note extends \ActivityPub\Controllers\NoteController
public function index() public function index()
{ {
helper('persons'); // Prevent analytics hit when authenticated
$persons = []; if (!can_user_interact()) {
construct_person_array($this->podcast->persons, $persons); $this->registerPodcastWebpageHit($this->podcast->id);
$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);
} }
$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() public function attemptCreate()
@ -198,15 +223,37 @@ class Note extends \ActivityPub\Controllers\NoteController
public function remoteAction($action) public function remoteAction($action)
{ {
$data = [ // Prevent analytics hit when authenticated
'podcast' => $this->podcast, if (!can_user_interact()) {
'actor' => $this->actor, $this->registerPodcastWebpageHit($this->podcast->id);
'note' => $this->note, }
'action' => $action,
];
helper('form'); $cacheName = implode(
'_',
array_filter([
'page',
"note#{$this->note->id}",
"remote_{$action}",
service('request')->getLocale(),
]),
);
return view('podcast/note_remote_action', $data); 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 $cachedView;
} }
} }

View File

@ -56,7 +56,7 @@ class Page extends BaseController
$locale = service('request')->getLocale(); $locale = service('request')->getLocale();
$allPodcasts = (new PodcastModel())->findAll(); $allPodcasts = (new PodcastModel())->findAll();
$cacheName = "paĝe_credits_{$locale}"; $cacheName = "page_credits_{$locale}";
if (!($found = cache($cacheName))) { if (!($found = cache($cacheName))) {
$page = new \App\Entities\Page([ $page = new \App\Entities\Page([
'title' => lang('Person.credits', [], $locale), 'title' => lang('Person.credits', [], $locale),

View File

@ -8,12 +8,15 @@
namespace App\Controllers; namespace App\Controllers;
use Analytics\AnalyticsTrait;
use App\Models\EpisodeModel; use App\Models\EpisodeModel;
use App\Models\PodcastModel; use App\Models\PodcastModel;
use App\Models\NoteModel; use App\Models\NoteModel;
class Podcast extends BaseController class Podcast extends BaseController
{ {
use AnalyticsTrait;
/** /**
* @var \App\Entities\Podcast|null * @var \App\Entities\Podcast|null
*/ */
@ -37,32 +40,56 @@ class Podcast extends BaseController
public function activity() public function activity()
{ {
self::triggerWebpageHit($this->podcast->id); // Prevent analytics hit when authenticated
if (!can_user_interact()) {
helper('persons'); $this->registerPodcastWebpageHit($this->podcast->id);
$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);
} }
$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() 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'); $yearQuery = $this->request->getGet('year');
$seasonQuery = $this->request->getGet('season'); $seasonQuery = $this->request->getGet('season');
@ -85,14 +112,15 @@ class Podcast extends BaseController
array_filter([ array_filter([
'page', 'page',
"podcast#{$this->podcast->id}", "podcast#{$this->podcast->id}",
'episodes',
$yearQuery ? 'year' . $yearQuery : null, $yearQuery ? 'year' . $yearQuery : null,
$seasonQuery ? 'season' . $seasonQuery : null, $seasonQuery ? 'season' . $seasonQuery : null,
service('request')->getLocale(), service('request')->getLocale(),
can_user_interact() ? '_interact' : '', can_user_interact() ? '_authenticated' : null,
]), ]),
); );
if (!($found = cache($cacheName))) { if (!($cachedView = cache($cacheName))) {
// Build navigation array // Build navigation array
$podcastModel = new PodcastModel(); $podcastModel = new PodcastModel();
$years = $podcastModel->getYears($this->podcast->id); $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 user is logged in then send to the authenticated episodes view
if (can_user_interact()) { if (can_user_interact()) {
$found = view('podcast/episodes_authenticated', $data, [ return view('podcast/episodes_authenticated', $data);
'cache' => $secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
: DECADE,
'cache_name' => $cacheName,
]);
} else { } else {
$found = view('podcast/episodes', $data, [ return view('podcast/episodes', $data, [
'cache' => $secondsToNextUnpublishedEpisode 'cache' => $secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode ? $secondsToNextUnpublishedEpisode
: DECADE, : DECADE,
@ -187,6 +210,6 @@ class Podcast extends BaseController
} }
} }
return $found; return $cachedView;
} }
} }

View File

@ -187,7 +187,9 @@ class AddPodcasts extends Migration
]); ]);
$this->forge->addPrimaryKey('id'); $this->forge->addPrimaryKey('id');
// TODO: remove name in favor of username from actor
$this->forge->addUniqueKey('name'); $this->forge->addUniqueKey('name');
$this->forge->addUniqueKey('actor_id');
$this->forge->addForeignKey( $this->forge->addForeignKey(
'actor_id', 'actor_id',
'activitypub_actors', 'activitypub_actors',

46
app/Entities/Actor.php Normal file
View File

@ -0,0 +1,46 @@
<?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;
}
}

View File

@ -31,7 +31,7 @@ class Category extends Entity
$parentId = $this->attributes['parent_id']; $parentId = $this->attributes['parent_id'];
return $parentId != 0 return $parentId != 0
? (new CategoryModel())->findParent($parentId) ? (new CategoryModel())->getCategoryById($parentId)
: null; : null;
} }
} }

View File

@ -143,7 +143,7 @@ class Podcast extends Entity
} }
if (empty($this->actor)) { if (empty($this->actor)) {
$this->actor = (new ActorModel())->getActorById($this->actor_id); $this->actor = model('ActorModel')->getActorById($this->actor_id);
} }
return $this->actor; return $this->actor;
@ -254,7 +254,9 @@ class Podcast extends Entity
} }
if (empty($this->category)) { if (empty($this->category)) {
$this->category = (new CategoryModel())->find($this->category_id); $this->category = (new CategoryModel())->getCategoryById(
$this->category_id,
);
} }
return $this->category; return $this->category;

View File

@ -6,7 +6,6 @@
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
use ActivityPub\Models\ActorModel;
use CodeIgniter\Database\Exceptions\DataException; use CodeIgniter\Database\Exceptions\DataException;
use Config\Services; use Config\Services;
@ -68,7 +67,7 @@ if (!function_exists('interact_as_actor')) {
$session = session(); $session = session();
if ($session->has('interact_as_actor_id')) { if ($session->has('interact_as_actor_id')) {
return (new ActorModel())->getActorById( return model('ActorModel')->getActorById(
$session->get('interact_as_actor_id'), $session->get('interact_as_actor_id'),
); );
} }

View File

@ -7,7 +7,6 @@
*/ */
use CodeIgniter\Files\File; use CodeIgniter\Files\File;
use CodeIgniter\HTTP\Exceptions\HTTPException;
use CodeIgniter\HTTP\ResponseInterface; use CodeIgniter\HTTP\ResponseInterface;
/** /**
@ -42,8 +41,6 @@ function save_media($file, $folder, $mediaName)
*/ */
function download_file($fileUrl) function download_file($fileUrl)
{ {
var_dump($fileUrl);
$client = \Config\Services::curlrequest(); $client = \Config\Services::curlrequest();
$response = $client->get($fileUrl, [ $response = $client->get($fileUrl, [

View File

@ -30,4 +30,11 @@ class ActivityPub extends BaseConfig
public $defaultCoverImagePath = 'assets/images/cover-default.jpg'; public $defaultCoverImagePath = 'assets/images/cover-default.jpg';
public $defaultCoverImageMimetype = 'image/jpeg'; public $defaultCoverImageMimetype = 'image/jpeg';
/**
* --------------------------------------------------------------------
* Cache options
* --------------------------------------------------------------------
*/
public $cachePrefix = 'ap_';
} }

View File

@ -44,11 +44,21 @@ class Note extends UuidEntity
*/ */
protected $preview_card; protected $preview_card;
/**
* @var boolean
*/
protected $has_preview_card;
/** /**
* @var \ActivityPub\Entities\Note[] * @var \ActivityPub\Entities\Note[]
*/ */
protected $replies; protected $replies;
/**
* @var boolean
*/
protected $has_replies;
/** /**
* @var \ActivityPub\Entities\Note[] * @var \ActivityPub\Entities\Note[]
*/ */
@ -106,6 +116,18 @@ class Note extends UuidEntity
return $this->preview_card; return $this->preview_card;
} }
public function getHasPreviewCard()
{
return !empty($this->getPreviewCard()) ? true : false;
}
public function getIsReply()
{
$this->is_reply = $this->in_reply_to_id !== null;
return $this->is_reply;
}
public function getReplies() public function getReplies()
{ {
if (empty($this->id)) { if (empty($this->id)) {
@ -121,11 +143,9 @@ class Note extends UuidEntity
return $this->replies; return $this->replies;
} }
public function getIsReply() public function getHasReplies()
{ {
$this->is_reply = $this->in_reply_to_id !== null; return !empty($this->getReplies()) ? true : false;
return $this->is_reply;
} }
public function getReplyToNote() public function getReplyToNote()
@ -152,11 +172,7 @@ class Note extends UuidEntity
} }
if (empty($this->reblogs)) { if (empty($this->reblogs)) {
$this->reblogs = model('NoteModel')->getNoteReblogs( $this->reblogs = model('NoteModel')->getNoteReblogs($this->id);
service('uuid')
->fromString($this->id)
->getBytes(),
);
} }
return $this->reblogs; return $this->reblogs;

View File

@ -8,6 +8,8 @@
namespace ActivityPub\Models; namespace ActivityPub\Models;
use Michalsn\Uuid\UuidModel;
class ActivityModel extends UuidModel class ActivityModel extends UuidModel
{ {
protected $table = 'activitypub_activities'; protected $table = 'activitypub_activities';
@ -34,7 +36,15 @@ class ActivityModel extends UuidModel
public function getActivityById($activityId) public function getActivityById($activityId)
{ {
return $this->find($activityId); $cacheName =
config('ActivityPub')->cachePrefix . "activity#{$activityId}";
if (!($found = cache($cacheName))) {
$found = $this->find($activityId);
cache()->save($cacheName, $found, DECADE);
}
return $found;
} }
/** /**

View File

@ -8,6 +8,7 @@
namespace ActivityPub\Models; namespace ActivityPub\Models;
use CodeIgniter\Events\Events;
use CodeIgniter\Model; use CodeIgniter\Model;
class ActorModel extends Model class ActorModel extends Model
@ -42,7 +43,14 @@ class ActorModel extends Model
public function getActorById($id) public function getActorById($id)
{ {
return $this->find($id); $cacheName = config('ActivityPub')->cachePrefix . "actor#{$id}";
if (!($found = cache($cacheName))) {
$found = $this->find($id);
cache()->save($cacheName, $found, DECADE);
}
return $found;
} }
/** /**
@ -76,31 +84,51 @@ class ActorModel extends Model
public function getActorByUri($actorUri) public function getActorByUri($actorUri)
{ {
return $this->where('uri', $actorUri)->first(); $hashedActorUri = md5($actorUri);
$cacheName =
config('ActivityPub')->cachePrefix . "actor@{$hashedActorUri}";
if (!($found = cache($cacheName))) {
$found = $this->where('uri', $actorUri)->first();
cache()->save($cacheName, $found, DECADE);
}
return $found;
} }
public function getFollowers($actorId) public function getFollowers($actorId)
{ {
return $this->join( $cacheName =
'activitypub_follows', config('ActivityPub')->cachePrefix . "actor#{$actorId}_followers";
'activitypub_follows.actor_id = id', if (!($found = cache($cacheName))) {
'inner', $found = $this->join(
) 'activitypub_follows',
->where('activitypub_follows.target_actor_id', $actorId) 'activitypub_follows.actor_id = id',
->findAll(); 'inner',
)
->where('activitypub_follows.target_actor_id', $actorId)
->findAll();
cache()->save($cacheName, $found, DECADE);
}
return $found;
} }
/** /**
* Check if an actor is blocked using its uri * Check if an existing actor is blocked using its uri.
* Returns FALSE if the actor doesn't exist
* *
* @param mixed $actorUri * @param mixed $actorUri
* @return boolean * @return boolean
*/ */
public function isActorBlocked($actorUri) public function isActorBlocked($actorUri)
{ {
return $this->where(['uri' => $actorUri, 'is_blocked' => true])->first() if ($actor = $this->getActorByUri($actorUri)) {
? true return $actor->is_blocked;
: false; }
return false;
} }
/** /**
@ -110,16 +138,35 @@ class ActorModel extends Model
*/ */
public function getBlockedActors() public function getBlockedActors()
{ {
return $this->where('is_blocked', 1)->findAll(); $cacheName = config('ActivityPub')->cachePrefix . 'blocked_actors';
if (!($found = cache($cacheName))) {
$found = $this->where('is_blocked', 1)->findAll();
cache()->save($cacheName, $found, DECADE);
}
return $found;
} }
public function blockActor($actorId) public function blockActor($actorId)
{ {
$prefix = config('ActivityPub')->cachePrefix;
cache()->delete($prefix . 'blocked_actors');
cache()->deleteMatching($prefix . '*replies');
Events::trigger('on_block_actor', $actorId);
$this->update($actorId, ['is_blocked' => 1]); $this->update($actorId, ['is_blocked' => 1]);
} }
public function unblockActor($actorId) public function unblockActor($actorId)
{ {
$prefix = config('ActivityPub')->cachePrefix;
cache()->delete($prefix . 'blocked_actors');
cache()->deleteMatching($prefix . '*replies');
Events::trigger('on_unblock_actor', $actorId);
$this->update($actorId, ['is_blocked' => 0]); $this->update($actorId, ['is_blocked' => 0]);
} }
} }

View File

@ -8,6 +8,7 @@
namespace ActivityPub\Models; namespace ActivityPub\Models;
use CodeIgniter\Events\Events;
use CodeIgniter\Model; use CodeIgniter\Model;
class BlockedDomainModel extends Model class BlockedDomainModel extends Model
@ -30,20 +31,41 @@ class BlockedDomainModel extends Model
*/ */
public function getBlockedDomains() public function getBlockedDomains()
{ {
return $this->findAll(); $cacheName = config('ActivityPub')->cachePrefix . 'blocked_domains';
if (!($found = cache($cacheName))) {
$found = $this->findAll();
cache()->save($cacheName, $found, DECADE);
}
return $found;
} }
public function isDomainBlocked($domain) public function isDomainBlocked($domain)
{ {
if ($this->find($domain)) { $hashedDomain = md5($domain);
return true; $cacheName =
config('ActivityPub')->cachePrefix .
"domain#{$hashedDomain}_isBlocked";
if (!($found = cache($cacheName))) {
$found = $this->find($domain) ? true : false;
cache()->save($cacheName, $found, DECADE);
} }
return false; return $found;
} }
public function blockDomain($name) public function blockDomain($name)
{ {
$hashedDomain = md5($name);
$prefix = config('ActivityPub')->cachePrefix;
cache()->delete($prefix . "domain#{$hashedDomain}_isBlocked");
cache()->delete($prefix . 'blocked_domains');
cache()->deleteMatching($prefix . '*replies');
Events::trigger('on_block_domain', $name);
$this->db->transStart(); $this->db->transStart();
// set all actors from the domain as blocked // set all actors from the domain as blocked
@ -63,6 +85,15 @@ class BlockedDomainModel extends Model
public function unblockDomain($name) public function unblockDomain($name)
{ {
$hashedDomain = md5($name);
$prefix = config('ActivityPub')->cachePrefix;
cache()->delete($prefix . "domain#{$hashedDomain}_isBlocked");
cache()->delete($prefix . 'blocked_domains');
cache()->deleteMatching($prefix . '*replies');
Events::trigger('on_unblock_domain', $name);
$this->db->transStart(); $this->db->transStart();
// unblock all actors from the domain // unblock all actors from the domain
model('ActorModel') model('ActorModel')

View File

@ -11,6 +11,7 @@ namespace ActivityPub\Models;
use ActivityPub\Activities\LikeActivity; use ActivityPub\Activities\LikeActivity;
use ActivityPub\Activities\UndoActivity; use ActivityPub\Activities\UndoActivity;
use CodeIgniter\Events\Events; use CodeIgniter\Events\Events;
use Michalsn\Uuid\UuidModel;
class FavouriteModel extends UuidModel class FavouriteModel extends UuidModel
{ {
@ -49,6 +50,19 @@ class FavouriteModel extends UuidModel
) )
->increment('favourites_count'); ->increment('favourites_count');
$prefix = config('ActivityPub')->cachePrefix;
$hashedNoteUri = md5($note->uri);
cache()->delete($prefix . "note#{$note->id}");
cache()->delete($prefix . "note@{$hashedNoteUri}");
cache()->delete($prefix . "actor#{$actor->id}_published_notes");
if ($note->in_reply_to_id) {
cache()->delete($prefix . "note#{$note->in_reply_to_id}_replies");
cache()->delete(
$prefix . "note#{$note->in_reply_to_id}_replies_withBlocked",
);
}
Events::trigger('on_note_favourite', $actor, $note); Events::trigger('on_note_favourite', $actor, $note);
if ($registerActivity) { if ($registerActivity) {
@ -91,6 +105,19 @@ class FavouriteModel extends UuidModel
) )
->decrement('favourites_count'); ->decrement('favourites_count');
$prefix = config('ActivityPub')->cachePrefix;
$hashedNoteUri = md5($note->uri);
cache()->delete($prefix . "note#{$note->id}");
cache()->delete($prefix . "note@{$hashedNoteUri}");
cache()->delete($prefix . "actor#{$actor->id}_published_notes");
if ($note->in_reply_to_id) {
cache()->delete($prefix . "note#{$note->in_reply_to_id}_replies");
cache()->delete(
$prefix . "note#{$note->in_reply_to_id}_replies_withBlocked",
);
}
$this->table('activitypub_favourites') $this->table('activitypub_favourites')
->where([ ->where([
'actor_id' => $actor->id, 'actor_id' => $actor->id,

View File

@ -27,9 +27,8 @@ class FollowModel extends Model
protected $updatedField = null; protected $updatedField = null;
/** /**
* * @param \ActivityPub\Entities\Actor $actor Actor that is following
* @param \ActivityPub\Entities\Actor $actor * @param \ActivityPub\Entities\Actor $targetActor Actor that is being followed
* @param \ActivityPub\Entities\Actor $targetActor
* @param bool $registerActivity * @param bool $registerActivity
* @return void * @return void
* @throws DatabaseException * @throws DatabaseException
@ -49,6 +48,14 @@ class FollowModel extends Model
->where('id', $targetActor->id) ->where('id', $targetActor->id)
->increment('followers_count'); ->increment('followers_count');
cache()->delete(
config('ActivityPub')->cachePrefix . "actor#{$targetActor->id}",
);
cache()->delete(
config('ActivityPub')->cachePrefix .
"actor#{$targetActor->id}_followers",
);
if ($registerActivity) { if ($registerActivity) {
$followActivity = new FollowActivity(); $followActivity = new FollowActivity();
@ -85,8 +92,8 @@ class FollowModel extends Model
} }
/** /**
* @param \ActivityPub\Entities\Actor $actor * @param \ActivityPub\Entities\Actor $actor Actor that is unfollowing
* @param \ActivityPub\Entities\Actor $targetActor * @param \ActivityPub\Entities\Actor $targetActor Actor that is being unfollowed
* @return void * @return void
* @throws InvalidArgumentException * @throws InvalidArgumentException
* @throws DatabaseException * @throws DatabaseException
@ -108,6 +115,14 @@ class FollowModel extends Model
->where('id', $targetActor->id) ->where('id', $targetActor->id)
->decrement('followers_count'); ->decrement('followers_count');
cache()->delete(
config('ActivityPub')->cachePrefix . "actor#{$targetActor->id}",
);
cache()->delete(
config('ActivityPub')->cachePrefix .
"actor#{$targetActor->id}_followers",
);
if ($registerActivity) { if ($registerActivity) {
$undoActivity = new UndoActivity(); $undoActivity = new UndoActivity();
// get follow activity from database // get follow activity from database

View File

@ -17,6 +17,7 @@ use ActivityPub\Objects\TombstoneObject;
use CodeIgniter\Events\Events; use CodeIgniter\Events\Events;
use CodeIgniter\HTTP\URI; use CodeIgniter\HTTP\URI;
use CodeIgniter\I18n\Time; use CodeIgniter\I18n\Time;
use Michalsn\Uuid\UuidModel;
class NoteModel extends UuidModel class NoteModel extends UuidModel
{ {
@ -54,12 +55,28 @@ class NoteModel extends UuidModel
public function getNoteById($noteId) public function getNoteById($noteId)
{ {
return $this->find($noteId); $cacheName = config('ActivityPub')->cachePrefix . "note#{$noteId}";
if (!($found = cache($cacheName))) {
$found = $this->find($noteId);
cache()->save($cacheName, $found, DECADE);
}
return $found;
} }
public function getNoteByUri($noteUri) public function getNoteByUri($noteUri)
{ {
return $this->where('uri', $noteUri)->first(); $hashedNoteUri = md5($noteUri);
$cacheName =
config('ActivityPub')->cachePrefix . "note@{$hashedNoteUri}";
if (!($found = cache($cacheName))) {
$found = $this->where('uri', $noteUri)->first();
cache()->save($cacheName, $found, DECADE);
}
return $found;
} }
/** /**
@ -67,15 +84,24 @@ class NoteModel extends UuidModel
* *
* @return \ActivityPub\Entities\Note[] * @return \ActivityPub\Entities\Note[]
*/ */
public function getActorNotes($actorId) public function getActorPublishedNotes($actorId)
{ {
return $this->where([ $cacheName =
'actor_id' => $actorId, config('ActivityPub')->cachePrefix .
'in_reply_to_id' => null, "actor#{$actorId}_published_notes";
]) if (!($found = cache($cacheName))) {
->where('`published_at` <= NOW()', null, false) $found = $this->where([
->orderBy('published_at', 'DESC') 'actor_id' => $actorId,
->findAll(); 'in_reply_to_id' => null,
])
->where('`published_at` <= NOW()', null, false)
->orderBy('published_at', 'DESC')
->findAll();
cache()->save($cacheName, $found, DECADE);
}
return $found;
} }
/** /**
@ -88,26 +114,34 @@ class NoteModel extends UuidModel
*/ */
public function getNoteReplies($noteId, $withBlocked = false) public function getNoteReplies($noteId, $withBlocked = false)
{ {
if (!$withBlocked) { $cacheName =
$this->select('activitypub_notes.*') config('ActivityPub')->cachePrefix .
->join( "note#{$noteId}_replies" .
'activitypub_actors', ($withBlocked ? '_withBlocked' : '');
'activitypub_actors.id = activitypub_notes.actor_id',
'inner', if (!($found = cache($cacheName))) {
) if (!$withBlocked) {
->where('activitypub_actors.is_blocked', 0); $this->select('activitypub_notes.*')
->join(
'activitypub_actors',
'activitypub_actors.id = activitypub_notes.actor_id',
'inner',
)
->where('activitypub_actors.is_blocked', 0);
}
$this->where(
'in_reply_to_id',
$this->uuid->fromString($noteId)->getBytes(),
)
->where('`published_at` <= NOW()', null, false)
->orderBy('published_at', 'ASC');
$found = $this->findAll();
cache()->save($cacheName, $found, DECADE);
} }
$this->where( return $found;
'in_reply_to_id',
service('uuid')
->fromString($noteId)
->getBytes(),
)
->where('`published_at` <= NOW()', null, false)
->orderBy('published_at', 'ASC');
return $this->findAll();
} }
/** /**
@ -115,16 +149,28 @@ class NoteModel extends UuidModel
*/ */
public function getNoteReblogs($noteId) public function getNoteReblogs($noteId)
{ {
return $this->where('reblog_of_id', $noteId) $cacheName =
->where('`published_at` <= NOW()', null, false) config('ActivityPub')->cachePrefix . "note#{$noteId}_reblogs";
->orderBy('published_at', 'ASC')
->findAll(); if (!($found = cache($cacheName))) {
$found = $this->where(
'reblog_of_id',
$this->uuid->fromString($noteId)->getBytes(),
)
->where('`published_at` <= NOW()', null, false)
->orderBy('published_at', 'ASC')
->findAll();
cache()->save($cacheName, $found, DECADE);
}
return $found;
} }
public function addPreviewCard($noteId, $previewCardId) public function addPreviewCard($noteId, $previewCardId)
{ {
return $this->db->table('activitypub_notes_preview_cards')->insert([ return $this->db->table('activitypub_notes_preview_cards')->insert([
'note_id' => $noteId, 'note_id' => $this->uuid->fromString($noteId)->getBytes(),
'preview_card_id' => $previewCardId, 'preview_card_id' => $previewCardId,
]); ]);
} }
@ -169,10 +215,6 @@ class NoteModel extends UuidModel
// problem when linking note to preview card // problem when linking note to preview card
return false; return false;
} }
$this->db->transComplete();
return $newNoteId;
} }
} }
@ -180,17 +222,19 @@ class NoteModel extends UuidModel
->where('id', $note->actor_id) ->where('id', $note->actor_id)
->increment('notes_count'); ->increment('notes_count');
$cachePrefix = config('ActivityPub')->cachePrefix;
cache()->delete($cachePrefix . "actor#{$note->actor_id}");
cache()->delete(
$cachePrefix . "actor#{$note->actor_id}_published_notes",
);
Events::trigger('on_note_add', $note); Events::trigger('on_note_add', $note);
if ($registerActivity) { if ($registerActivity) {
$noteUuid = service('uuid')
->fromBytes($newNoteId)
->toString();
// set note id and uri to construct NoteObject // set note id and uri to construct NoteObject
$note->id = $noteUuid; $note->id = $newNoteId;
$note->uri = base_url( $note->uri = base_url(
route_to('note', $note->actor->username, $noteUuid), route_to('note', $note->actor->username, $newNoteId),
); );
$createActivity = new CreateActivity(); $createActivity = new CreateActivity();
@ -203,7 +247,7 @@ class NoteModel extends UuidModel
'Create', 'Create',
$note->actor_id, $note->actor_id,
null, null,
$noteUuid, $newNoteId,
$createActivity->toJSON(), $createActivity->toJSON(),
$note->published_at, $note->published_at,
'queued', 'queued',
@ -234,7 +278,7 @@ class NoteModel extends UuidModel
$scheduledActivity = model('ActivityModel') $scheduledActivity = model('ActivityModel')
->where([ ->where([
'type' => 'Create', 'type' => 'Create',
'note_id' => service('uuid') 'note_id' => $this->uuid
->fromString($updatedNote->id) ->fromString($updatedNote->id)
->getBytes(), ->getBytes(),
]) ])
@ -253,6 +297,12 @@ class NoteModel extends UuidModel
// update note // update note
$updateResult = $this->update($updatedNote->id, $updatedNote); $updateResult = $this->update($updatedNote->id, $updatedNote);
// Clear note cache
$prefix = config('ActivityPub')->cachePrefix;
$hashedNoteUri = md5($updatedNote->uri);
cache()->delete($prefix . "note#{$updatedNote->id}");
cache()->delete($prefix . "note@{$hashedNoteUri}");
$this->db->transComplete(); $this->db->transComplete();
return $updateResult; return $updateResult;
@ -268,32 +318,61 @@ class NoteModel extends UuidModel
{ {
$this->db->transStart(); $this->db->transStart();
$cachePrefix = config('ActivityPub')->cachePrefix;
model('ActorModel') model('ActorModel')
->where('id', $note->actor_id) ->where('id', $note->actor_id)
->decrement('notes_count'); ->decrement('notes_count');
cache()->delete($cachePrefix . "actor#{$note->actor_id}");
cache()->delete(
$cachePrefix . "actor#{$note->actor_id}_published_notes",
);
if ($note->in_reply_to_id) { if ($note->in_reply_to_id) {
// Note to remove is a reply // Note to remove is a reply
model('NoteModel') model('NoteModel')
->where( ->where(
'id', 'id',
service('uuid') $this->uuid->fromString($note->in_reply_to_id)->getBytes(),
->fromString($note->in_reply_to_id)
->getBytes(),
) )
->decrement('replies_count'); ->decrement('replies_count');
$replyToNote = $note->reply_to_note;
cache()->delete($cachePrefix . "note#{$replyToNote->id}");
cache()->delete($cachePrefix . "note@{$replyToNote->uri}");
cache()->delete($cachePrefix . "note#{$replyToNote->id}_replies");
cache()->delete(
$cachePrefix . "note#{$replyToNote->id}_replies_withBlocked",
);
Events::trigger('on_reply_remove', $note);
} }
// remove all reblogs // remove all note reblogs
foreach ($note->reblogs as $reblog) { foreach ($note->reblogs as $reblog) {
$this->removeNote($reblog); $this->removeNote($reblog);
} }
// remove all replies // remove all note replies
foreach ($note->replies as $reply) { foreach ($note->replies as $reply) {
$this->removeNote($reply); $this->removeNote($reply);
} }
if ($note->preview_card) {
// check that preview card in no longer used elsewhere before deleting it
if (
$this->db
->table('activitypub_notes_preview_cards')
->where('preview_card_id', $note->preview_card->id)
->countAll() <= 1
) {
model('PreviewCardModel')->deletePreviewCard(
$note->preview_card->id,
$note->preview_card->url,
);
}
}
Events::trigger('on_note_remove', $note); Events::trigger('on_note_remove', $note);
if ($registerActivity) { if ($registerActivity) {
@ -326,6 +405,15 @@ class NoteModel extends UuidModel
]); ]);
} }
// clear note + replies / reblogs + actor and its published notes
$hashedNoteUri = md5($note->uri);
cache()->delete($cachePrefix . "note#{$note->id}");
cache()->delete($cachePrefix . "note@{$hashedNoteUri}");
cache()->delete($cachePrefix . "note#{$note->id}_replies");
cache()->delete($cachePrefix . "note#{$note->id}_replies_withBlocked");
cache()->delete($cachePrefix . "note#{$note->id}_reblogs");
cache()->delete($cachePrefix . "note#{$note->id}_preview_card");
$result = model('NoteModel', false)->delete($note->id); $result = model('NoteModel', false)->delete($note->id);
$this->db->transComplete(); $this->db->transComplete();
@ -349,12 +437,19 @@ class NoteModel extends UuidModel
model('NoteModel') model('NoteModel')
->where( ->where(
'id', 'id',
service('uuid') $this->uuid->fromString($reply->in_reply_to_id)->getBytes(),
->fromString($reply->in_reply_to_id)
->getBytes(),
) )
->increment('replies_count'); ->increment('replies_count');
$prefix = config('ActivityPub')->cachePrefix;
$hashedNoteUri = md5($reply->reply_to_note->uri);
cache()->delete($prefix . "note#{$reply->in_reply_to_id}");
cache()->delete($prefix . "note@{$hashedNoteUri}");
cache()->delete($prefix . "note#{$reply->in_reply_to_id}_replies");
cache()->delete(
$prefix . "note#{$reply->in_reply_to_id}_replies_withBlocked",
);
Events::trigger('on_note_reply', $reply); Events::trigger('on_note_reply', $reply);
$this->db->transComplete(); $this->db->transComplete();
@ -385,15 +480,19 @@ class NoteModel extends UuidModel
->where('id', $actor->id) ->where('id', $actor->id)
->increment('notes_count'); ->increment('notes_count');
$prefix = config('ActivityPub')->cachePrefix;
cache()->delete($prefix . "actor#{$note->actor_id}");
cache()->delete($prefix . "actor#{$note->actor_id}_published_notes");
model('NoteModel') model('NoteModel')
->where( ->where('id', $this->uuid->fromString($note->id)->getBytes())
'id',
service('uuid')
->fromString($note->id)
->getBytes(),
)
->increment('reblogs_count'); ->increment('reblogs_count');
$hashedNoteUri = md5($note->uri);
cache()->delete($prefix . "note#{$note->id}");
cache()->delete($prefix . "note@{$hashedNoteUri}");
cache()->delete($prefix . "note#{$note->id}_reblogs");
Events::trigger('on_note_reblog', $actor, $note); Events::trigger('on_note_reblog', $actor, $note);
if ($registerActivity) { if ($registerActivity) {
@ -438,15 +537,26 @@ class NoteModel extends UuidModel
->where('id', $reblogNote->actor_id) ->where('id', $reblogNote->actor_id)
->decrement('notes_count'); ->decrement('notes_count');
$cachePrefix = config('ActivityPub')->cachePrefix;
cache()->delete($cachePrefix . "actor#{$reblogNote->actor_id}");
cache()->delete(
$cachePrefix . "actor#{$reblogNote->actor_id}_published_notes",
);
model('NoteModel') model('NoteModel')
->where( ->where(
'id', 'id',
service('uuid') $this->uuid->fromString($reblogNote->reblog_of_id)->getBytes(),
->fromString($reblogNote->reblog_of_id)
->getBytes(),
) )
->decrement('reblogs_count'); ->decrement('reblogs_count');
$hashedReblogNoteUri = md5($reblogNote->uri);
$hashedNoteUri = md5($reblogNote->reblog_of_note->uri);
cache()->delete($cachePrefix . "note#{$reblogNote->id}");
cache()->delete($cachePrefix . "note@{$hashedReblogNoteUri}");
cache()->delete($cachePrefix . "note#{$reblogNote->reblog_of_id}");
cache()->delete($cachePrefix . "note@{$hashedNoteUri}");
Events::trigger('on_note_undo_reblog', $reblogNote); Events::trigger('on_note_undo_reblog', $reblogNote);
if ($registerActivity) { if ($registerActivity) {
@ -456,7 +566,7 @@ class NoteModel extends UuidModel
->where([ ->where([
'type' => 'Announce', 'type' => 'Announce',
'actor_id' => $reblogNote->actor_id, 'actor_id' => $reblogNote->actor_id,
'note_id' => service('uuid') 'note_id' => $this->uuid
->fromString($reblogNote->reblog_of_id) ->fromString($reblogNote->reblog_of_id)
->getBytes(), ->getBytes(),
]) ])
@ -516,7 +626,7 @@ class NoteModel extends UuidModel
if ( if (
!($reblogNote = $this->where([ !($reblogNote = $this->where([
'actor_id' => $actor->id, 'actor_id' => $actor->id,
'reblog_of_id' => service('uuid') 'reblog_of_id' => $this->uuid
->fromString($note->id) ->fromString($note->id)
->getBytes(), ->getBytes(),
])->first()) ])->first())
@ -529,9 +639,8 @@ class NoteModel extends UuidModel
protected function setNoteId($data) protected function setNoteId($data)
{ {
$uuid4 = service('uuid')->uuid4(); $uuid4 = $this->uuid->{$this->uuidVersion}();
$data['id'] = $uuid4->toString(); $data['data']['id'] = $uuid4->toString();
$data['data']['id'] = $uuid4->getBytes();
if (!isset($data['data']['uri'])) { if (!isset($data['data']['uri'])) {
$actor = model('ActorModel')->getActorById( $actor = model('ActorModel')->getActorById(

View File

@ -35,22 +35,50 @@ class PreviewCardModel extends Model
public function getPreviewCardFromUrl($url) public function getPreviewCardFromUrl($url)
{ {
return $this->where('url', $url)->first(); $hashedPreviewCardUrl = md5($url);
$cacheName =
config('ActivityPub')->cachePrefix .
"preview_card@{$hashedPreviewCardUrl}";
if (!($found = cache($cacheName))) {
$found = $this->where('url', $url)->first();
cache()->save($cacheName, $found, DECADE);
}
return $found;
} }
public function getNotePreviewCard($noteId) public function getNotePreviewCard($noteId)
{ {
return $this->join( $cacheName =
'activitypub_notes_preview_cards', config('ActivityPub')->cachePrefix . "note#{$noteId}_preview_card";
'activitypub_notes_preview_cards.preview_card_id = id', if (!($found = cache($cacheName))) {
'inner', $found = $this->join(
) 'activitypub_notes_preview_cards',
->where( 'activitypub_notes_preview_cards.preview_card_id = id',
'note_id', 'inner',
service('uuid')
->fromString($noteId)
->getBytes(),
) )
->first(); ->where(
'note_id',
service('uuid')
->fromString($noteId)
->getBytes(),
)
->first();
cache()->save($cacheName, $found, DECADE);
}
return $found;
}
public function deletePreviewCard($id, $url)
{
$hashedPreviewCardUrl = md5($url);
cache()->delete(
config('ActivityPub')->cachePrefix .
"preview_card@{$hashedPreviewCardUrl}",
);
return $this->delete($id);
} }
} }

View File

@ -1,206 +0,0 @@
<?php
/**
* @copyright 2021 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace ActivityPub\Models;
use CodeIgniter\Database\Exceptions\DataException;
use stdClass;
class UuidModel extends \Michalsn\Uuid\UuidModel
{
/**
* This insert overwrite is added as a means to FIX some bugs
* from the extended Uuid package. See: https://github.com/michalsn/codeigniter4-uuid/issues/2
*
* Inserts data into the current table. If an object is provided,
* it will attempt to convert it to an array.
*
* @param array|object $data
* @param boolean $returnID Whether insert ID should be returned or not.
*
* @return BaseResult|integer|string|false
* @throws \ReflectionException
*/
public function insert($data = null, bool $returnID = true)
{
$escape = null;
$this->insertID = 0;
if (empty($data)) {
$data = $this->tempData['data'] ?? null;
$escape = $this->tempData['escape'] ?? null;
$this->tempData = [];
}
if (empty($data)) {
throw DataException::forEmptyDataset('insert');
}
// If $data is using a custom class with public or protected
// properties representing the table elements, we need to grab
// them as an array.
if (is_object($data) && !$data instanceof stdClass) {
$data = static::classToArray(
$data,
$this->primaryKey,
$this->dateFormat,
false,
);
}
// If it's still a stdClass, go ahead and convert to
// an array so doProtectFields and other model methods
// don't have to do special checks.
if (is_object($data)) {
$data = (array) $data;
}
if (empty($data)) {
throw DataException::forEmptyDataset('insert');
}
// Validate data before saving.
if ($this->skipValidation === false) {
if ($this->cleanRules()->validate($data) === false) {
return false;
}
}
// Must be called first so we don't
// strip out created_at values.
$data = $this->doProtectFields($data);
// Set created_at and updated_at with same time
$date = $this->setDate();
if (
$this->useTimestamps &&
!empty($this->createdField) &&
!array_key_exists($this->createdField, $data)
) {
$data[$this->createdField] = $date;
}
if (
$this->useTimestamps &&
!empty($this->updatedField) &&
!array_key_exists($this->updatedField, $data)
) {
$data[$this->updatedField] = $date;
}
$eventData = ['data' => $data];
if ($this->tempAllowCallbacks) {
$eventData = $this->trigger('beforeInsert', $eventData);
}
// Require non empty primaryKey when
// not using auto-increment feature
if (
!$this->useAutoIncrement &&
empty($eventData['data'][$this->primaryKey])
) {
throw DataException::forEmptyPrimaryKey('insert');
}
if (!empty($this->uuidFields)) {
foreach ($this->uuidFields as $field) {
if ($field === $this->primaryKey) {
$this->uuidTempData[
$field
] = $this->uuid->{$this->uuidVersion}();
if ($this->uuidUseBytes === true) {
$this->builder()->set(
$field,
$this->uuidTempData[$field]->getBytes(),
);
} else {
$this->builder()->set(
$field,
$this->uuidTempData[$field]->toString(),
);
}
} else {
if (
$this->uuidUseBytes === true &&
!empty($eventData['data'][$field])
) {
$this->uuidTempData[$field] = $this->uuid->fromString(
$eventData['data'][$field],
);
$this->builder()->set(
$field,
$this->uuidTempData[$field]->getBytes(),
);
unset($eventData['data'][$field]);
}
}
}
}
// Must use the set() method to ensure objects get converted to arrays
$result = $this->builder()
->set($eventData['data'], '', $escape)
->insert();
// If insertion succeeded then save the insert ID
if ($result) {
if (
!$this->useAutoIncrement ||
isset($eventData['data'][$this->primaryKey])
) {
$this->insertID = $eventData['data'][$this->primaryKey];
} else {
if (in_array($this->primaryKey, $this->uuidFields)) {
$this->insertID = $this->uuidTempData[
$this->primaryKey
]->toString();
} else {
$this->insertID = $this->db->insertID();
}
}
}
// Cleanup data before event trigger
if (!empty($this->uuidFields) && $this->uuidUseBytes === true) {
foreach ($this->uuidFields as $field) {
if (
$field === $this->primaryKey ||
empty($this->uuidTempData[$field])
) {
continue;
}
$eventData['data'][$field] = $this->uuidTempData[
$field
]->toString();
}
}
$eventData = [
'id' => $this->insertID,
'data' => $eventData['data'],
'result' => $result,
];
if ($this->tempAllowCallbacks) {
// Trigger afterInsert events with the inserted data and new ID
$this->trigger('afterInsert', $eventData);
}
$this->tempAllowCallbacks = $this->allowCallbacks;
// If insertion failed, get out of here
if (!$result) {
return $result;
}
// otherwise return the insertID, if requested.
return $returnID ? $this->insertID : $result;
}
}

View File

@ -0,0 +1,51 @@
<?php
/**
* @copyright 2021 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace Analytics;
trait AnalyticsTrait
{
/**
*
* @param integer $podcastId
* @return void
*/
protected function registerPodcastWebpageHit($podcastId)
{
helper('analytics');
set_user_session_deny_list_ip();
set_user_session_browser();
set_user_session_referer();
set_user_session_entry_page();
$session = \Config\Services::session();
$session->start();
if (!$session->get('denyListIp')) {
$db = \Config\Database::connect();
$referer = $session->get('referer');
$domain = empty(parse_url($referer, PHP_URL_HOST))
? '- Direct -'
: parse_url($referer, PHP_URL_HOST);
parse_str(parse_url($referer, PHP_URL_QUERY), $queries);
$keywords = empty($queries['q']) ? null : $queries['q'];
$procedureName = $db->prefixTable('analytics_website');
$db->query("call $procedureName(?,?,?,?,?,?)", [
$podcastId,
$session->get('browser'),
$session->get('entryPage'),
$referer,
$domain,
$keywords,
]);
}
}
}

View File

@ -258,40 +258,6 @@ if (!function_exists('set_user_session_entry_page')) {
} }
} }
if (!function_exists('webpage_hit')) {
/**
*
* @param integer $podcastId
* @return void
*/
function webpage_hit($podcastId)
{
$session = \Config\Services::session();
$session->start();
if (!$session->get('denyListIp')) {
$db = \Config\Database::connect();
$referer = $session->get('referer');
$domain = empty(parse_url($referer, PHP_URL_HOST))
? '- Direct -'
: parse_url($referer, PHP_URL_HOST);
parse_str(parse_url($referer, PHP_URL_QUERY), $queries);
$keywords = empty($queries['q']) ? null : $queries['q'];
$procedureName = $db->prefixTable('analytics_website');
$db->query("call $procedureName(?,?,?,?,?,?)", [
$podcastId,
$session->get('browser'),
$session->get('entryPage'),
$referer,
$domain,
$keywords,
]);
}
}
}
if (!function_exists('podcast_hit')) { if (!function_exists('podcast_hit')) {
/** /**
* Counting podcast episode downloads for analytic purposes * Counting podcast episode downloads for analytic purposes

14
app/Models/ActorModel.php Normal file
View File

@ -0,0 +1,14 @@
<?php
/**
* @copyright 2021 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Models;
class ActorModel extends \ActivityPub\Models\ActorModel
{
protected $returnType = \App\Entities\Actor::class;
}

View File

@ -27,9 +27,9 @@ class CategoryModel extends Model
protected $useTimestamps = false; protected $useTimestamps = false;
public function findParent($parentId) public function getCategoryById($id)
{ {
return $this->find($parentId); return $this->find($id);
} }
public function getCategoryOptions() public function getCategoryOptions()

View File

@ -139,6 +139,7 @@ class EpisodeModel extends Model
public function getEpisodeById($episodeId) public function getEpisodeById($episodeId)
{ {
// TODO: episode id should be a composite key. The cache should include podcast_id.
$cacheName = "podcast_episode#{$episodeId}"; $cacheName = "podcast_episode#{$episodeId}";
if (!($found = cache($cacheName))) { if (!($found = cache($cacheName))) {
$builder = $this->where([ $builder = $this->where([
@ -153,19 +154,16 @@ class EpisodeModel extends Model
return $found; return $found;
} }
public function getPublishedEpisodeById($episodeId, $podcastId = null) public function getPublishedEpisodeById($podcastId, $episodeId)
{ {
$cacheName = "podcast_episode#{$episodeId}_published"; $cacheName = "podcast#{$podcastId}_episode#{$episodeId}_published";
if (!($found = cache($cacheName))) { if (!($found = cache($cacheName))) {
$builder = $this->where([ $found = $this->where([
'id' => $episodeId, 'id' => $episodeId,
])->where('`published_at` <= NOW()', null, false); ])
->where('podcast_id', $podcastId)
if ($podcastId) { ->where('`published_at` <= NOW()', null, false)
$builder->where('podcast_id', $podcastId); ->first();
}
$found = $builder->first();
cache()->save($cacheName, $found, DECADE); cache()->save($cacheName, $found, DECADE);
} }
@ -287,11 +285,17 @@ class EpisodeModel extends Model
// delete model requests cache // delete model requests cache
cache()->delete("podcast#{$episode->podcast_id}_episodes"); cache()->delete("podcast#{$episode->podcast_id}_episodes");
cache()->deleteMatching("podcast_episode#{$episode->id}*"); cache()->delete("podcast_episode#{$episode->id}");
cache()->deleteMatching(
"podcast#{$episode->podcast_id}_episode#{$episode->id}*",
);
cache()->delete( cache()->delete(
"podcast#{$episode->podcast_id}_episode@{$episode->slug}", "podcast#{$episode->podcast_id}_episode@{$episode->slug}",
); );
cache()->deleteMatching(
"page_podcast#{$episode->podcast_id}_activity*",
);
cache()->deleteMatching( cache()->deleteMatching(
"page_podcast#{$episode->podcast_id}_episode#{$episode->id}_*", "page_podcast#{$episode->podcast_id}_episode#{$episode->id}_*",
); );
@ -300,12 +304,12 @@ class EpisodeModel extends Model
if ($episode->season_number) { if ($episode->season_number) {
cache()->deleteMatching("podcast#{$episode->podcast_id}_season*"); cache()->deleteMatching("podcast#{$episode->podcast_id}_season*");
cache()->deleteMatching( cache()->deleteMatching(
"page_podcast#{$episode->podcast_id}_season*", "page_podcast#{$episode->podcast_id}_episodes_season*",
); );
} else { } else {
cache()->deleteMatching("podcast#{$episode->podcast_id}_year*"); cache()->deleteMatching("podcast#{$episode->podcast_id}_year*");
cache()->deleteMatching( cache()->deleteMatching(
"page_podcast#{$episode->podcast_id}_year*", "page_podcast#{$episode->podcast_id}_episodes_year*",
); );
} }

View File

@ -38,9 +38,9 @@ class EpisodePersonModel extends Model
protected $afterInsert = ['clearCache']; protected $afterInsert = ['clearCache'];
protected $beforeDelete = ['clearCache']; protected $beforeDelete = ['clearCache'];
public function getEpisodePersons($episodeId) public function getEpisodePersons($podcastId, $episodeId)
{ {
$cacheName = "podcast_episode#{$episodeId}_persons"; $cacheName = "podcast#{$podcastId}_episode#{$episodeId}_persons";
if (!($found = cache($cacheName))) { if (!($found = cache($cacheName))) {
$found = $this->select('episodes_persons.*') $found = $this->select('episodes_persons.*')
->where('episode_id', $episodeId) ->where('episode_id', $episodeId)
@ -124,7 +124,6 @@ class EpisodePersonModel extends Model
$episodeId = $person->episode_id; $episodeId = $person->episode_id;
} }
cache()->delete("podcast_episode#{$episodeId}_persons");
(new EpisodeModel())->clearCache(['id' => $episodeId]); (new EpisodeModel())->clearCache(['id' => $episodeId]);
return $data; return $data;

View File

@ -1,7 +1,7 @@
<?php <?php
/** /**
* @copyright 2020 Podlibre * @copyright 2021 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */

View File

@ -8,7 +8,6 @@
namespace App\Models; namespace App\Models;
use ActivityPub\Models\ActorModel;
use CodeIgniter\HTTP\URI; use CodeIgniter\HTTP\URI;
use CodeIgniter\Model; use CodeIgniter\Model;
use phpseclib\Crypt\RSA; use phpseclib\Crypt\RSA;
@ -74,7 +73,7 @@ class PodcastModel extends Model
protected $validationMessages = []; protected $validationMessages = [];
protected $beforeInsert = ['createPodcastActor']; protected $beforeInsert = ['createPodcastActor'];
protected $afterInsert = ['setAvatarImageUrl']; protected $afterInsert = ['setActorAvatar'];
protected $afterUpdate = ['updatePodcastActor']; protected $afterUpdate = ['updatePodcastActor'];
// clear cache before update if by any chance, the podcast name changes, so will the podcast link // clear cache before update if by any chance, the podcast name changes, so will the podcast link
@ -104,6 +103,18 @@ class PodcastModel extends Model
return $found; return $found;
} }
public function getPodcastByActorId($actorId)
{
$cacheName = "podcast_actor#{$actorId}";
if (!($found = cache($cacheName))) {
$found = $this->where('actor_id', $actorId)->first();
cache()->save($cacheName, $found, DECADE);
}
return $found;
}
/** /**
* Gets all the podcasts a given user is contributing to * Gets all the podcasts a given user is contributing to
* *
@ -300,25 +311,6 @@ class PodcastModel extends Model
return $defaultQuery; return $defaultQuery;
} }
public function clearCache(array $data)
{
$podcast = (new PodcastModel())->getPodcastById(
is_array($data['id']) ? $data['id'][0] : $data['id'],
);
// delete cache all podcast pages
cache()->deleteMatching("page_podcast#{$podcast->id}_*");
// delete model requests cache, includes feed / query / episode lists, etc.
cache()->deleteMatching("podcast#{$podcast->id}*");
cache()->delete("podcast@{$podcast->name}");
// clear cache for every credit page
cache()->deleteMatching('page_credits_*');
return $data;
}
/** /**
* Creates an actor linked to the podcast * Creates an actor linked to the podcast
* (Triggered before insert) * (Triggered before insert)
@ -359,16 +351,18 @@ class PodcastModel extends Model
return $data; return $data;
} }
protected function setAvatarImageUrl($data) protected function setActorAvatar($data)
{ {
$podcast = (new PodcastModel())->getPodcastById( $podcast = (new PodcastModel())->getPodcastById(
is_array($data['id']) ? $data['id'][0] : $data['id'], is_array($data['id']) ? $data['id'][0] : $data['id'],
); );
$podcast->actor->avatar_image_url = $podcast->image->thumbnail_url; $podcastActor = (new ActorModel())->find($podcast->actor_id);
$podcast->actor->avatar_image_mimetype = $podcast->image_mimetype;
(new ActorModel())->update($podcast->actor->id, $podcast->actor); $podcastActor->avatar_image_url = $podcast->image->thumbnail_url;
$podcastActor->avatar_image_mimetype = $podcast->image_mimetype;
(new ActorModel())->update($podcast->actor_id, $podcastActor);
return $data; return $data;
} }
@ -380,7 +374,7 @@ class PodcastModel extends Model
); );
$actorModel = new ActorModel(); $actorModel = new ActorModel();
$actor = $actorModel->find($podcast->actor_id); $actor = $actorModel->getActorById($podcast->actor_id);
// update values // update values
$actor->display_name = $podcast->title; $actor->display_name = $podcast->title;
@ -394,4 +388,28 @@ class PodcastModel extends Model
return $data; return $data;
} }
public function clearCache(array $data)
{
$podcast = (new PodcastModel())->getPodcastById(
is_array($data['id']) ? $data['id'][0] : $data['id'],
);
// delete cache all podcast pages
cache()->deleteMatching("page_podcast#{$podcast->id}*");
// delete all cache for podcast actor
cache()->deleteMatching(
config('ActivityPub')->cachePrefix . "actor#{$podcast->actor_id}*",
);
// delete model requests cache, includes feed / query / episode lists, etc.
cache()->deleteMatching("podcast#{$podcast->id}*");
cache()->delete("podcast@{$podcast->name}");
// clear cache for every credit page
cache()->deleteMatching('page_credits_*');
return $data;
}
} }

View File

@ -56,7 +56,7 @@ class SoundbiteModel extends Model
*/ */
public function getEpisodeSoundbites(int $podcastId, int $episodeId): array public function getEpisodeSoundbites(int $podcastId, int $episodeId): array
{ {
$cacheName = "podcast_episode#{$episodeId}_soundbites"; $cacheName = "podcast#{$podcastId}_episode#{$episodeId}_soundbites";
if (!($found = cache($cacheName))) { if (!($found = cache($cacheName))) {
$found = $this->where([ $found = $this->where([
'episode_id' => $episodeId, 'episode_id' => $episodeId,
@ -77,7 +77,9 @@ class SoundbiteModel extends Model
: $data['id']['episode_id'], : $data['id']['episode_id'],
); );
cache()->delete("podcast_episode#{$episode->id}_soundbites"); cache()->delete(
"podcast#{$episode->podcast_id}_episode#{$episode->id}_soundbites",
);
// delete cache for rss feed // delete cache for rss feed
cache()->deleteMatching("podcast#{$episode->podcast_id}_feed*"); cache()->deleteMatching("podcast#{$episode->podcast_id}_feed*");

View File

@ -31,7 +31,7 @@
<?= view('podcast/_partials/episode_card', [ <?= view('podcast/_partials/episode_card', [
'episode' => $note->episode, 'episode' => $note->episode,
]) ?> ]) ?>
<?php elseif ($note->preview_card_id): ?> <?php elseif ($note->has_preview_card): ?>
<?= view('podcast/_partials/preview_card', [ <?= view('podcast/_partials/preview_card', [
'preview_card' => $note->preview_card, 'preview_card' => $note->preview_card,
]) ?> ]) ?>

View File

@ -31,7 +31,7 @@
<?= view('podcast/_partials/episode_card', [ <?= view('podcast/_partials/episode_card', [
'episode' => $note->episode, 'episode' => $note->episode,
]) ?> ]) ?>
<?php elseif ($note->preview_card_id): ?> <?php elseif ($note->has_preview_card): ?>
<?= view('podcast/_partials/preview_card', [ <?= view('podcast/_partials/preview_card', [
'preview_card' => $note->preview_card, 'preview_card' => $note->preview_card,
]) ?> ]) ?>

View File

@ -15,7 +15,9 @@
</div> </div>
<?php foreach ($note->replies as $reply): ?> <?php if ($note->has_replies): ?>
<?= view('podcast/_partials/reply', ['reply' => $reply]) ?> <?php foreach ($note->replies as $reply): ?>
<?php endforeach; ?> <?= view('podcast/_partials/reply', ['reply' => $reply]) ?>
<?php endforeach; ?>
<?php endif; ?>
</div> </div>

View File

@ -39,7 +39,11 @@
</div> </div>
<?= form_close() ?> <?= form_close() ?>
<?php foreach ($note->replies as $reply): ?> <?php if ($note->has_replies): ?>
<?= view('podcast/_partials/reply_authenticated', ['reply' => $reply]) ?> <?php foreach ($note->replies as $reply): ?>
<?php endforeach; ?> <?= view('podcast/_partials/reply_authenticated', [
'reply' => $reply,
]) ?>
<?php endforeach; ?>
<?php endif; ?>
</div> </div>

View File

@ -38,7 +38,7 @@
<?= view('podcast/_partials/episode_card', [ <?= view('podcast/_partials/episode_card', [
'episode' => $note->episode, 'episode' => $note->episode,
]) ?> ]) ?>
<?php elseif ($note->preview_card_id): ?> <?php elseif ($note->has_preview_card): ?>
<?= view('podcast/_partials/preview_card', [ <?= view('podcast/_partials/preview_card', [
'preview_card' => $note->preview_card, 'preview_card' => $note->preview_card,
]) ?> ]) ?>

View File

@ -38,7 +38,7 @@
<?= view('podcast/_partials/episode_card', [ <?= view('podcast/_partials/episode_card', [
'episode' => $note->episode, 'episode' => $note->episode,
]) ?> ]) ?>
<?php elseif ($note->preview_card_id): ?> <?php elseif ($note->has_preview_card): ?>
<?= view('podcast/_partials/preview_card', [ <?= view('podcast/_partials/preview_card', [
'preview_card' => $note->preview_card, 'preview_card' => $note->preview_card,
]) ?> ]) ?>

View File

@ -19,7 +19,7 @@
><?= lang('Common.mediumDate', [$reply->published_at]) ?></time> ><?= lang('Common.mediumDate', [$reply->published_at]) ?></time>
</header> </header>
<p class="mb-2 note-content"><?= $reply->message_html ?></p> <p class="mb-2 note-content"><?= $reply->message_html ?></p>
<?php if ($reply->preview_card_id): ?> <?php if ($reply->has_preview_card): ?>
<?= view('podcast/_partials/preview_card', [ <?= view('podcast/_partials/preview_card', [
'preview_card' => $reply->preview_card, 'preview_card' => $reply->preview_card,
]) ?> ]) ?>

View File

@ -19,7 +19,7 @@
><?= lang('Common.mediumDate', [$reply->created_at]) ?></time> ><?= lang('Common.mediumDate', [$reply->created_at]) ?></time>
</header> </header>
<p class="mb-2 note-content"><?= $reply->message_html ?></p> <p class="mb-2 note-content"><?= $reply->message_html ?></p>
<?php if ($reply->preview_card_id): ?> <?php if ($reply->has_preview_card): ?>
<?= view('podcast/_partials/preview_card', [ <?= view('podcast/_partials/preview_card', [
'preview_card' => $reply->preview_card, 'preview_card' => $reply->preview_card,
]) ?> ]) ?>

View File

@ -12,14 +12,14 @@
"geoip2/geoip2": "^v2.11.0", "geoip2/geoip2": "^v2.11.0",
"myth/auth": "dev-develop", "myth/auth": "dev-develop",
"codeigniter4/codeigniter4": "dev-develop", "codeigniter4/codeigniter4": "dev-develop",
"league/commonmark": "^1.5.7", "league/commonmark": "^1.6.0",
"vlucas/phpdotenv": "^v5.3.0", "vlucas/phpdotenv": "^v5.3.0",
"league/html-to-markdown": "^4.10", "league/html-to-markdown": "^4.10",
"opawg/user-agents-php": "^v1.0", "opawg/user-agents-php": "^v1.0",
"podlibre/ipcat": "^v1.0", "podlibre/ipcat": "^v1.0",
"podlibre/podcast-namespace": "^v1.0.6", "podlibre/podcast-namespace": "^v1.0.6",
"phpseclib/phpseclib": "~2.0.30", "phpseclib/phpseclib": "~2.0.30",
"michalsn/codeigniter4-uuid": "^1.0@beta", "michalsn/codeigniter4-uuid": "^v1.0.0",
"essence/essence": "^3.5.4" "essence/essence": "^3.5.4"
}, },
"require-dev": { "require-dev": {

61
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "b5d726bdc7252c80c0fd5a6f53de1948", "content-hash": "f370b196462e2ca2ff3e2df9627f2ba4",
"packages": [ "packages": [
{ {
"name": "brick/math", "name": "brick/math",
@ -68,12 +68,12 @@
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/codeigniter4/CodeIgniter4.git", "url": "https://github.com/codeigniter4/CodeIgniter4.git",
"reference": "dfbc85af9ef408a6654cce6a462c8fdde3ee2446" "reference": "8b2e7c29043977fac378c37690cc951a715c2bd5"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/codeigniter4/CodeIgniter4/zipball/dfbc85af9ef408a6654cce6a462c8fdde3ee2446", "url": "https://api.github.com/repos/codeigniter4/CodeIgniter4/zipball/8b2e7c29043977fac378c37690cc951a715c2bd5",
"reference": "dfbc85af9ef408a6654cce6a462c8fdde3ee2446", "reference": "8b2e7c29043977fac378c37690cc951a715c2bd5",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -91,10 +91,10 @@
"fakerphp/faker": "^1.9", "fakerphp/faker": "^1.9",
"mikey179/vfsstream": "^1.6", "mikey179/vfsstream": "^1.6",
"nexusphp/tachycardia": "^1.0", "nexusphp/tachycardia": "^1.0",
"phpstan/phpstan": "0.12.84", "phpstan/phpstan": "0.12.85",
"phpunit/phpunit": "^9.1", "phpunit/phpunit": "^9.1",
"predis/predis": "^1.1", "predis/predis": "^1.1",
"rector/rector": "0.10.6", "rector/rector": "0.10.17",
"squizlabs/php_codesniffer": "^3.3" "squizlabs/php_codesniffer": "^3.3"
}, },
"suggest": { "suggest": {
@ -139,7 +139,7 @@
"slack": "https://codeigniterchat.slack.com", "slack": "https://codeigniterchat.slack.com",
"issues": "https://github.com/codeigniter4/CodeIgniter4/issues" "issues": "https://github.com/codeigniter4/CodeIgniter4/issues"
}, },
"time": "2021-04-20T08:40:30+00:00" "time": "2021-05-03T08:32:21+00:00"
}, },
{ {
"name": "composer/ca-bundle", "name": "composer/ca-bundle",
@ -823,16 +823,16 @@
}, },
{ {
"name": "league/commonmark", "name": "league/commonmark",
"version": "1.5.8", "version": "1.6.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/thephpleague/commonmark.git", "url": "https://github.com/thephpleague/commonmark.git",
"reference": "08fa59b8e4e34ea8a773d55139ae9ac0e0aecbaf" "reference": "19a9673b833cc37770439097b381d86cd125bfe8"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/thephpleague/commonmark/zipball/08fa59b8e4e34ea8a773d55139ae9ac0e0aecbaf", "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/19a9673b833cc37770439097b381d86cd125bfe8",
"reference": "08fa59b8e4e34ea8a773d55139ae9ac0e0aecbaf", "reference": "19a9673b833cc37770439097b381d86cd125bfe8",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -920,7 +920,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2021-03-28T18:51:39+00:00" "time": "2021-05-01T19:00:49+00:00"
}, },
{ {
"name": "league/html-to-markdown", "name": "league/html-to-markdown",
@ -1125,16 +1125,16 @@
}, },
{ {
"name": "michalsn/codeigniter4-uuid", "name": "michalsn/codeigniter4-uuid",
"version": "v1.0.0-beta3", "version": "v1.0.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/michalsn/codeigniter4-uuid.git", "url": "https://github.com/michalsn/codeigniter4-uuid.git",
"reference": "568aba8f315199b6cc87e76b8441cd03a2bba5b4" "reference": "c8bbd961401015307bc72f6f6aa93509ffac1d5f"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/michalsn/codeigniter4-uuid/zipball/568aba8f315199b6cc87e76b8441cd03a2bba5b4", "url": "https://api.github.com/repos/michalsn/codeigniter4-uuid/zipball/c8bbd961401015307bc72f6f6aa93509ffac1d5f",
"reference": "568aba8f315199b6cc87e76b8441cd03a2bba5b4", "reference": "c8bbd961401015307bc72f6f6aa93509ffac1d5f",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1172,9 +1172,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/michalsn/codeigniter4-uuid/issues", "issues": "https://github.com/michalsn/codeigniter4-uuid/issues",
"source": "https://github.com/michalsn/codeigniter4-uuid/tree/v1.0.0-beta3" "source": "https://github.com/michalsn/codeigniter4-uuid/tree/v1.0.1"
}, },
"time": "2021-04-02T11:08:18+00:00" "time": "2021-05-03T12:47:44+00:00"
}, },
{ {
"name": "myth/auth", "name": "myth/auth",
@ -1182,12 +1182,12 @@
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/lonnieezell/myth-auth.git", "url": "https://github.com/lonnieezell/myth-auth.git",
"reference": "eff9805d7f1d27326f14875b53ff4b3d2a6b72ee" "reference": "2b42da1884745eec22ac10f7941a4f9350576a86"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/lonnieezell/myth-auth/zipball/eff9805d7f1d27326f14875b53ff4b3d2a6b72ee", "url": "https://api.github.com/repos/lonnieezell/myth-auth/zipball/2b42da1884745eec22ac10f7941a4f9350576a86",
"reference": "eff9805d7f1d27326f14875b53ff4b3d2a6b72ee", "reference": "2b42da1884745eec22ac10f7941a4f9350576a86",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1248,7 +1248,7 @@
"type": "patreon" "type": "patreon"
} }
], ],
"time": "2021-04-12T22:34:12+00:00" "time": "2021-05-02T05:32:03+00:00"
}, },
{ {
"name": "opawg/user-agents-php", "name": "opawg/user-agents-php",
@ -1584,16 +1584,16 @@
}, },
{ {
"name": "psr/log", "name": "psr/log",
"version": "1.1.3", "version": "1.1.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/php-fig/log.git", "url": "https://github.com/php-fig/log.git",
"reference": "0f73288fd15629204f9d42b7055f72dacbe811fc" "reference": "d49695b909c3b7628b6289db5479a1c204601f11"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc", "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11",
"reference": "0f73288fd15629204f9d42b7055f72dacbe811fc", "reference": "d49695b909c3b7628b6289db5479a1c204601f11",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1617,7 +1617,7 @@
"authors": [ "authors": [
{ {
"name": "PHP-FIG", "name": "PHP-FIG",
"homepage": "http://www.php-fig.org/" "homepage": "https://www.php-fig.org/"
} }
], ],
"description": "Common interface for logging libraries", "description": "Common interface for logging libraries",
@ -1628,9 +1628,9 @@
"psr-3" "psr-3"
], ],
"support": { "support": {
"source": "https://github.com/php-fig/log/tree/1.1.3" "source": "https://github.com/php-fig/log/tree/1.1.4"
}, },
"time": "2020-03-23T09:12:05+00:00" "time": "2021-05-03T11:20:27+00:00"
}, },
{ {
"name": "ramsey/collection", "name": "ramsey/collection",
@ -4314,8 +4314,7 @@
"stability-flags": { "stability-flags": {
"james-heinrich/getid3": 20, "james-heinrich/getid3": 20,
"myth/auth": 20, "myth/auth": 20,
"codeigniter4/codeigniter4": 20, "codeigniter4/codeigniter4": 20
"michalsn/codeigniter4-uuid": 10
}, },
"prefer-stable": true, "prefer-stable": true,
"prefer-lowest": false, "prefer-lowest": false,