mirror of
https://code.castopod.org/adaures/castopod
synced 2025-05-11 16:55:46 +00:00

- 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
658 lines
20 KiB
PHP
658 lines
20 KiB
PHP
<?php
|
|
|
|
/**
|
|
* @copyright 2021 Podlibre
|
|
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
|
* @link https://castopod.org/
|
|
*/
|
|
|
|
namespace ActivityPub\Models;
|
|
|
|
use ActivityPub\Entities\Note;
|
|
use ActivityPub\Activities\AnnounceActivity;
|
|
use ActivityPub\Activities\CreateActivity;
|
|
use ActivityPub\Activities\DeleteActivity;
|
|
use ActivityPub\Activities\UndoActivity;
|
|
use ActivityPub\Objects\TombstoneObject;
|
|
use CodeIgniter\Events\Events;
|
|
use CodeIgniter\HTTP\URI;
|
|
use CodeIgniter\I18n\Time;
|
|
use Michalsn\Uuid\UuidModel;
|
|
|
|
class NoteModel extends UuidModel
|
|
{
|
|
protected $table = 'activitypub_notes';
|
|
protected $primaryKey = 'id';
|
|
|
|
protected $uuidFields = ['id', 'in_reply_to_id', 'reblog_of_id'];
|
|
|
|
protected $allowedFields = [
|
|
'id',
|
|
'uri',
|
|
'actor_id',
|
|
'in_reply_to_id',
|
|
'reblog_of_id',
|
|
'message',
|
|
'message_html',
|
|
'favourites_count',
|
|
'reblogs_count',
|
|
'replies_count',
|
|
'published_at',
|
|
];
|
|
|
|
protected $returnType = \ActivityPub\Entities\Note::class;
|
|
protected $useSoftDeletes = false;
|
|
|
|
protected $useTimestamps = true;
|
|
protected $updatedField = null;
|
|
|
|
protected $validationRules = [
|
|
'actor_id' => 'required',
|
|
'message_html' => 'required_without[reblog_of_id]|max_length[500]',
|
|
];
|
|
|
|
protected $beforeInsert = ['setNoteId'];
|
|
|
|
public function getNoteById($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)
|
|
{
|
|
$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;
|
|
}
|
|
|
|
/**
|
|
* Retrieves all published notes for a given actor ordered by publication date
|
|
*
|
|
* @return \ActivityPub\Entities\Note[]
|
|
*/
|
|
public function getActorPublishedNotes($actorId)
|
|
{
|
|
$cacheName =
|
|
config('ActivityPub')->cachePrefix .
|
|
"actor#{$actorId}_published_notes";
|
|
if (!($found = cache($cacheName))) {
|
|
$found = $this->where([
|
|
'actor_id' => $actorId,
|
|
'in_reply_to_id' => null,
|
|
])
|
|
->where('`published_at` <= NOW()', null, false)
|
|
->orderBy('published_at', 'DESC')
|
|
->findAll();
|
|
|
|
cache()->save($cacheName, $found, DECADE);
|
|
}
|
|
|
|
return $found;
|
|
}
|
|
|
|
/**
|
|
* Retrieves all published replies for a given note.
|
|
* By default, it does not get replies from blocked actors.
|
|
*
|
|
* @param mixed $noteId
|
|
* @param boolean $withBlocked false by default
|
|
* @return array
|
|
*/
|
|
public function getNoteReplies($noteId, $withBlocked = false)
|
|
{
|
|
$cacheName =
|
|
config('ActivityPub')->cachePrefix .
|
|
"note#{$noteId}_replies" .
|
|
($withBlocked ? '_withBlocked' : '');
|
|
|
|
if (!($found = cache($cacheName))) {
|
|
if (!$withBlocked) {
|
|
$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);
|
|
}
|
|
|
|
return $found;
|
|
}
|
|
|
|
/**
|
|
* Retrieves all published reblogs for a given note
|
|
*/
|
|
public function getNoteReblogs($noteId)
|
|
{
|
|
$cacheName =
|
|
config('ActivityPub')->cachePrefix . "note#{$noteId}_reblogs";
|
|
|
|
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)
|
|
{
|
|
return $this->db->table('activitypub_notes_preview_cards')->insert([
|
|
'note_id' => $this->uuid->fromString($noteId)->getBytes(),
|
|
'preview_card_id' => $previewCardId,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Adds note in database along preview card if relevant
|
|
*
|
|
* @param \ActivityPub\Entities\Note $note
|
|
* @param boolean $registerActivity
|
|
* @param boolean $createPreviewCard
|
|
* @return string|false returns the new note id if success or false otherwise
|
|
*/
|
|
public function addNote(
|
|
$note,
|
|
$createPreviewCard = true,
|
|
$registerActivity = true
|
|
) {
|
|
helper('activitypub');
|
|
|
|
$this->db->transStart();
|
|
|
|
if (!($newNoteId = $this->insert($note, true))) {
|
|
$this->db->transRollback();
|
|
|
|
// Couldn't insert note
|
|
return false;
|
|
}
|
|
|
|
if ($createPreviewCard) {
|
|
// parse message
|
|
$messageUrls = extract_urls_from_message($note->message);
|
|
|
|
if (
|
|
!empty($messageUrls) &&
|
|
($previewCard = get_or_create_preview_card_from_url(
|
|
new URI($messageUrls[0]),
|
|
))
|
|
) {
|
|
if (!$this->addPreviewCard($newNoteId, $previewCard->id)) {
|
|
$this->db->transRollback();
|
|
|
|
// problem when linking note to preview card
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
model('ActorModel')
|
|
->where('id', $note->actor_id)
|
|
->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);
|
|
|
|
if ($registerActivity) {
|
|
// set note id and uri to construct NoteObject
|
|
$note->id = $newNoteId;
|
|
$note->uri = base_url(
|
|
route_to('note', $note->actor->username, $newNoteId),
|
|
);
|
|
|
|
$createActivity = new CreateActivity();
|
|
$noteObjectClass = config('ActivityPub')->noteObject;
|
|
$createActivity
|
|
->set('actor', $note->actor->uri)
|
|
->set('object', new $noteObjectClass($note));
|
|
|
|
$activityId = model('ActivityModel')->newActivity(
|
|
'Create',
|
|
$note->actor_id,
|
|
null,
|
|
$newNoteId,
|
|
$createActivity->toJSON(),
|
|
$note->published_at,
|
|
'queued',
|
|
);
|
|
|
|
$createActivity->set(
|
|
'id',
|
|
base_url(
|
|
route_to('activity', $note->actor->username, $activityId),
|
|
),
|
|
);
|
|
|
|
model('ActivityModel')->update($activityId, [
|
|
'payload' => $createActivity->toJSON(),
|
|
]);
|
|
}
|
|
|
|
$this->db->transComplete();
|
|
|
|
return $newNoteId;
|
|
}
|
|
|
|
public function editNote($updatedNote)
|
|
{
|
|
$this->db->transStart();
|
|
|
|
// update note create activity schedule in database
|
|
$scheduledActivity = model('ActivityModel')
|
|
->where([
|
|
'type' => 'Create',
|
|
'note_id' => $this->uuid
|
|
->fromString($updatedNote->id)
|
|
->getBytes(),
|
|
])
|
|
->first();
|
|
|
|
// update published date in payload
|
|
$newPayload = $scheduledActivity->payload;
|
|
$newPayload->object->published = $updatedNote->published_at->format(
|
|
DATE_W3C,
|
|
);
|
|
model('ActivityModel')->update($scheduledActivity->id, [
|
|
'payload' => json_encode($newPayload),
|
|
'scheduled_at' => $updatedNote->published_at,
|
|
]);
|
|
|
|
// update note
|
|
$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();
|
|
|
|
return $updateResult;
|
|
}
|
|
|
|
/**
|
|
* Removes a note from the database and decrements meta data
|
|
*
|
|
* @param \ActivityPub\Entities\Note $note
|
|
* @return mixed
|
|
*/
|
|
public function removeNote($note, $registerActivity = true)
|
|
{
|
|
$this->db->transStart();
|
|
|
|
$cachePrefix = config('ActivityPub')->cachePrefix;
|
|
|
|
model('ActorModel')
|
|
->where('id', $note->actor_id)
|
|
->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) {
|
|
// Note to remove is a reply
|
|
model('NoteModel')
|
|
->where(
|
|
'id',
|
|
$this->uuid->fromString($note->in_reply_to_id)->getBytes(),
|
|
)
|
|
->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 note reblogs
|
|
foreach ($note->reblogs as $reblog) {
|
|
$this->removeNote($reblog);
|
|
}
|
|
|
|
// remove all note replies
|
|
foreach ($note->replies as $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);
|
|
|
|
if ($registerActivity) {
|
|
$deleteActivity = new DeleteActivity();
|
|
$tombstoneObject = new TombstoneObject();
|
|
$tombstoneObject->set('id', $note->uri);
|
|
$deleteActivity
|
|
->set('actor', $note->actor->uri)
|
|
->set('object', $tombstoneObject);
|
|
|
|
$activityId = model('ActivityModel')->newActivity(
|
|
'Delete',
|
|
$note->actor_id,
|
|
null,
|
|
null,
|
|
$deleteActivity->toJSON(),
|
|
Time::now(),
|
|
'queued',
|
|
);
|
|
|
|
$deleteActivity->set(
|
|
'id',
|
|
base_url(
|
|
route_to('activity', $note->actor->username, $activityId),
|
|
),
|
|
);
|
|
|
|
model('ActivityModel')->update($activityId, [
|
|
'payload' => $deleteActivity->toJSON(),
|
|
]);
|
|
}
|
|
|
|
// 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);
|
|
|
|
$this->db->transComplete();
|
|
|
|
return $result;
|
|
}
|
|
|
|
public function addReply(
|
|
$reply,
|
|
$createPreviewCard = true,
|
|
$registerActivity = true
|
|
) {
|
|
if (!$reply->in_reply_to_id) {
|
|
throw new \Exception('Passed note is not a reply!');
|
|
}
|
|
|
|
$this->db->transStart();
|
|
|
|
$noteId = $this->addNote($reply, $createPreviewCard, $registerActivity);
|
|
|
|
model('NoteModel')
|
|
->where(
|
|
'id',
|
|
$this->uuid->fromString($reply->in_reply_to_id)->getBytes(),
|
|
)
|
|
->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);
|
|
|
|
$this->db->transComplete();
|
|
|
|
return $noteId;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param \ActivityPub\Entities\Actor $actor
|
|
* @param \ActivityPub\Entities\Note $note
|
|
* @return ActivityPub\Models\BaseResult|int|string|false
|
|
*/
|
|
public function reblog($actor, $note, $registerActivity = true)
|
|
{
|
|
$this->db->transStart();
|
|
|
|
$reblog = new Note([
|
|
'actor_id' => $actor->id,
|
|
'reblog_of_id' => $note->id,
|
|
'published_at' => Time::now(),
|
|
]);
|
|
|
|
// add reblog
|
|
$reblogId = $this->insert($reblog, true);
|
|
|
|
model('ActorModel')
|
|
->where('id', $actor->id)
|
|
->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')
|
|
->where('id', $this->uuid->fromString($note->id)->getBytes())
|
|
->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);
|
|
|
|
if ($registerActivity) {
|
|
$announceActivity = new AnnounceActivity($reblog);
|
|
|
|
$activityId = model('ActivityModel')->newActivity(
|
|
'Announce',
|
|
$actor->id,
|
|
null,
|
|
$note->id,
|
|
$announceActivity->toJSON(),
|
|
$reblog->published_at,
|
|
'queued',
|
|
);
|
|
|
|
$announceActivity->set(
|
|
'id',
|
|
base_url(
|
|
route_to('activity', $note->actor->username, $activityId),
|
|
),
|
|
);
|
|
|
|
model('ActivityModel')->update($activityId, [
|
|
'payload' => $announceActivity->toJSON(),
|
|
]);
|
|
}
|
|
|
|
$this->db->transComplete();
|
|
|
|
return $reblogId;
|
|
}
|
|
|
|
/**
|
|
* @param \ActivityPub\Entities\Note $reblogNote
|
|
* @return mixed
|
|
*/
|
|
public function undoReblog($reblogNote, $registerActivity = true)
|
|
{
|
|
$this->db->transStart();
|
|
|
|
model('ActorModel')
|
|
->where('id', $reblogNote->actor_id)
|
|
->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')
|
|
->where(
|
|
'id',
|
|
$this->uuid->fromString($reblogNote->reblog_of_id)->getBytes(),
|
|
)
|
|
->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);
|
|
|
|
if ($registerActivity) {
|
|
$undoActivity = new UndoActivity();
|
|
// get like activity
|
|
$activity = model('ActivityModel')
|
|
->where([
|
|
'type' => 'Announce',
|
|
'actor_id' => $reblogNote->actor_id,
|
|
'note_id' => $this->uuid
|
|
->fromString($reblogNote->reblog_of_id)
|
|
->getBytes(),
|
|
])
|
|
->first();
|
|
|
|
$announceActivity = new AnnounceActivity($reblogNote);
|
|
$announceActivity->set(
|
|
'id',
|
|
base_url(
|
|
route_to(
|
|
'activity',
|
|
$reblogNote->actor->username,
|
|
$activity->id,
|
|
),
|
|
),
|
|
);
|
|
|
|
$undoActivity
|
|
->set('actor', $reblogNote->actor->uri)
|
|
->set('object', $announceActivity);
|
|
|
|
$activityId = model('ActivityModel')->newActivity(
|
|
'Undo',
|
|
$reblogNote->actor_id,
|
|
null,
|
|
$reblogNote->reblog_of_id,
|
|
$undoActivity->toJSON(),
|
|
Time::now(),
|
|
'queued',
|
|
);
|
|
|
|
$undoActivity->set(
|
|
'id',
|
|
base_url(
|
|
route_to(
|
|
'activity',
|
|
$reblogNote->actor->username,
|
|
$activityId,
|
|
),
|
|
),
|
|
);
|
|
|
|
model('ActivityModel')->update($activityId, [
|
|
'payload' => $undoActivity->toJSON(),
|
|
]);
|
|
}
|
|
|
|
$result = model('NoteModel', false)->delete($reblogNote->id);
|
|
|
|
$this->db->transComplete();
|
|
|
|
return $result;
|
|
}
|
|
|
|
public function toggleReblog($actor, $note)
|
|
{
|
|
if (
|
|
!($reblogNote = $this->where([
|
|
'actor_id' => $actor->id,
|
|
'reblog_of_id' => $this->uuid
|
|
->fromString($note->id)
|
|
->getBytes(),
|
|
])->first())
|
|
) {
|
|
$this->reblog($actor, $note);
|
|
} else {
|
|
$this->undoReblog($reblogNote);
|
|
}
|
|
}
|
|
|
|
protected function setNoteId($data)
|
|
{
|
|
$uuid4 = $this->uuid->{$this->uuidVersion}();
|
|
$data['data']['id'] = $uuid4->toString();
|
|
|
|
if (!isset($data['data']['uri'])) {
|
|
$actor = model('ActorModel')->getActorById(
|
|
$data['data']['actor_id'],
|
|
);
|
|
|
|
$data['data']['uri'] = base_url(
|
|
route_to('note', $actor->username, $uuid4->toString()),
|
|
);
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
}
|