mirror of
https://code.castopod.org/adaures/castopod.git
synced 2024-09-27 20:21:59 +02:00
feat(activitypub): add Podcast actor and PodcastEpisode object with comments
This commit is contained in:
parent
b814cfaf7c
commit
9e1e5d2e86
@ -6,7 +6,6 @@ namespace Config;
|
|||||||
|
|
||||||
use ActivityPub\Config\ActivityPub as ActivityPubBase;
|
use ActivityPub\Config\ActivityPub as ActivityPubBase;
|
||||||
use App\Libraries\NoteObject;
|
use App\Libraries\NoteObject;
|
||||||
use App\Libraries\PodcastActor;
|
|
||||||
|
|
||||||
class ActivityPub extends ActivityPubBase
|
class ActivityPub extends ActivityPubBase
|
||||||
{
|
{
|
||||||
@ -15,8 +14,6 @@ class ActivityPub extends ActivityPubBase
|
|||||||
* ActivityPub Objects
|
* ActivityPub Objects
|
||||||
* --------------------------------------------------------------------
|
* --------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
public string $actorObject = PodcastActor::class;
|
|
||||||
|
|
||||||
public string $noteObject = NoteObject::class;
|
public string $noteObject = NoteObject::class;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -697,6 +697,10 @@ $routes->group('@(:podcastName)', function ($routes): void {
|
|||||||
'namespace' => 'ActivityPub\Controllers',
|
'namespace' => 'ActivityPub\Controllers',
|
||||||
'controller-method' => 'ActorController/$1',
|
'controller-method' => 'ActorController/$1',
|
||||||
],
|
],
|
||||||
|
'application/podcast-activity+json' => [
|
||||||
|
'namespace' => 'App\Controllers',
|
||||||
|
'controller-method' => 'PodcastController::podcastActor/$1',
|
||||||
|
],
|
||||||
'application/ld+json; profile="https://www.w3.org/ns/activitystreams' => [
|
'application/ld+json; profile="https://www.w3.org/ns/activitystreams' => [
|
||||||
'namespace' => 'ActivityPub\Controllers',
|
'namespace' => 'ActivityPub\Controllers',
|
||||||
'controller-method' => 'ActorController/$1',
|
'controller-method' => 'ActorController/$1',
|
||||||
@ -705,10 +709,44 @@ $routes->group('@(:podcastName)', function ($routes): void {
|
|||||||
]);
|
]);
|
||||||
$routes->get('episodes', 'PodcastController::episodes/$1', [
|
$routes->get('episodes', 'PodcastController::episodes/$1', [
|
||||||
'as' => 'podcast-episodes',
|
'as' => 'podcast-episodes',
|
||||||
|
'alternate-content' => [
|
||||||
|
'application/activity+json' => [
|
||||||
|
'controller-method' => 'PodcastController::episodeCollection/$1',
|
||||||
|
],
|
||||||
|
'application/podcast-activity+json' => [
|
||||||
|
'controller-method' => 'PodcastController::episodeCollection/$1',
|
||||||
|
],
|
||||||
|
'application/ld+json; profile="https://www.w3.org/ns/activitystreams' => [
|
||||||
|
'controller-method' => 'PodcastController::episodeCollection/$1',
|
||||||
|
],
|
||||||
|
],
|
||||||
]);
|
]);
|
||||||
$routes->group('episodes/(:slug)', function ($routes): void {
|
$routes->group('episodes/(:slug)', function ($routes): void {
|
||||||
$routes->get('/', 'EpisodeController/$1/$2', [
|
$routes->get('/', 'EpisodeController/$1/$2', [
|
||||||
'as' => 'episode',
|
'as' => 'episode',
|
||||||
|
'alternate-content' => [
|
||||||
|
'application/activity+json' => [
|
||||||
|
'controller-method' => 'EpisodeController::episodeObject/$1/$2',
|
||||||
|
],
|
||||||
|
'application/podcast-activity+json' => [
|
||||||
|
'controller-method' => 'EpisodeController::episodeObject/$1/$2',
|
||||||
|
],
|
||||||
|
'application/ld+json; profile="https://www.w3.org/ns/activitystreams' => [
|
||||||
|
'controller-method' => 'EpisodeController::episodeObject/$1/$2',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
$routes->get('comments', 'EpisodeController::comments/$1/$2', [
|
||||||
|
'as' => 'episode-comments',
|
||||||
|
'application/activity+json' => [
|
||||||
|
'controller-method' => 'EpisodeController::comments/$1/$2',
|
||||||
|
],
|
||||||
|
'application/podcast-activity+json' => [
|
||||||
|
'controller-method' => 'EpisodeController::comments/$1/$2',
|
||||||
|
],
|
||||||
|
'application/ld+json; profile="https://www.w3.org/ns/activitystreams' => [
|
||||||
|
'controller-method' => 'EpisodeController::comments/$1/$2',
|
||||||
|
],
|
||||||
]);
|
]);
|
||||||
$routes->get('oembed.json', 'EpisodeController::oembedJSON/$1/$2', [
|
$routes->get('oembed.json', 'EpisodeController::oembedJSON/$1/$2', [
|
||||||
'as' => 'episode-oembed-json',
|
'as' => 'episode-oembed-json',
|
||||||
|
@ -10,12 +10,18 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Controllers;
|
namespace App\Controllers;
|
||||||
|
|
||||||
|
use ActivityPub\Objects\OrderedCollectionObject;
|
||||||
|
use ActivityPub\Objects\OrderedCollectionPage;
|
||||||
use Analytics\AnalyticsTrait;
|
use Analytics\AnalyticsTrait;
|
||||||
use App\Entities\Episode;
|
use App\Entities\Episode;
|
||||||
use App\Entities\Podcast;
|
use App\Entities\Podcast;
|
||||||
|
use App\Libraries\NoteObject;
|
||||||
|
use App\Libraries\PodcastEpisode;
|
||||||
use App\Models\EpisodeModel;
|
use App\Models\EpisodeModel;
|
||||||
use App\Models\PodcastModel;
|
use App\Models\PodcastModel;
|
||||||
|
use CodeIgniter\Database\BaseBuilder;
|
||||||
use CodeIgniter\Exceptions\PageNotFoundException;
|
use CodeIgniter\Exceptions\PageNotFoundException;
|
||||||
|
use CodeIgniter\HTTP\Response;
|
||||||
use CodeIgniter\HTTP\ResponseInterface;
|
use CodeIgniter\HTTP\ResponseInterface;
|
||||||
use Config\Services;
|
use Config\Services;
|
||||||
use SimpleXMLElement;
|
use SimpleXMLElement;
|
||||||
@ -191,4 +197,59 @@ class EpisodeController extends BaseController
|
|||||||
|
|
||||||
return $this->response->setXML((string) $oembed);
|
return $this->response->setXML((string) $oembed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @noRector ReturnTypeDeclarationRector
|
||||||
|
*/
|
||||||
|
public function episodeObject(): Response
|
||||||
|
{
|
||||||
|
$podcastObject = new PodcastEpisode($this->episode);
|
||||||
|
|
||||||
|
return $this->response
|
||||||
|
->setContentType('application/json')
|
||||||
|
->setBody($podcastObject->toJSON());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @noRector ReturnTypeDeclarationRector
|
||||||
|
*/
|
||||||
|
public function comments(): Response
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* get comments: aggregated replies from posts referring to the episode
|
||||||
|
*/
|
||||||
|
$episodeComments = model('StatusModel')
|
||||||
|
->whereIn('in_reply_to_id', function (BaseBuilder $builder): BaseBuilder {
|
||||||
|
return $builder->select('id')
|
||||||
|
->from('activitypub_statuses')
|
||||||
|
->where('episode_id', $this->episode->id);
|
||||||
|
})
|
||||||
|
->where('`published_at` <= NOW()', null, false)
|
||||||
|
->orderBy('published_at', 'ASC');
|
||||||
|
|
||||||
|
$pageNumber = (int) $this->request->getGet('page');
|
||||||
|
|
||||||
|
if ($pageNumber < 1) {
|
||||||
|
$episodeComments->paginate(12);
|
||||||
|
$pager = $episodeComments->pager;
|
||||||
|
$collection = new OrderedCollectionObject(null, $pager);
|
||||||
|
} else {
|
||||||
|
$paginatedComments = $episodeComments->paginate(12, 'default', $pageNumber);
|
||||||
|
$pager = $episodeComments->pager;
|
||||||
|
|
||||||
|
$orderedItems = [];
|
||||||
|
if ($paginatedComments !== null) {
|
||||||
|
foreach ($paginatedComments as $comment) {
|
||||||
|
$orderedItems[] = (new NoteObject($comment))->toArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @phpstan-ignore-next-line
|
||||||
|
$collection = new OrderedCollectionPage($pager, $orderedItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->response
|
||||||
|
->setContentType('application/activity+json')
|
||||||
|
->setBody($collection->toJSON());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,12 +10,18 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Controllers;
|
namespace App\Controllers;
|
||||||
|
|
||||||
|
use ActivityPub\Objects\OrderedCollectionObject;
|
||||||
|
use ActivityPub\Objects\OrderedCollectionPage;
|
||||||
use Analytics\AnalyticsTrait;
|
use Analytics\AnalyticsTrait;
|
||||||
use App\Entities\Podcast;
|
use App\Entities\Podcast;
|
||||||
|
use App\Libraries\PodcastActor;
|
||||||
|
use App\Libraries\PodcastEpisode;
|
||||||
use App\Models\EpisodeModel;
|
use App\Models\EpisodeModel;
|
||||||
use App\Models\PodcastModel;
|
use App\Models\PodcastModel;
|
||||||
use App\Models\StatusModel;
|
use App\Models\StatusModel;
|
||||||
use CodeIgniter\Exceptions\PageNotFoundException;
|
use CodeIgniter\Exceptions\PageNotFoundException;
|
||||||
|
use CodeIgniter\HTTP\RedirectResponse;
|
||||||
|
use CodeIgniter\HTTP\Response;
|
||||||
|
|
||||||
class PodcastController extends BaseController
|
class PodcastController extends BaseController
|
||||||
{
|
{
|
||||||
@ -42,6 +48,15 @@ class PodcastController extends BaseController
|
|||||||
return $this->{$method}(...$params);
|
return $this->{$method}(...$params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function podcastActor(): RedirectResponse
|
||||||
|
{
|
||||||
|
$podcastActor = new PodcastActor($this->podcast);
|
||||||
|
|
||||||
|
return $this->response
|
||||||
|
->setContentType('application/activity+json')
|
||||||
|
->setBody($podcastActor->toJSON());
|
||||||
|
}
|
||||||
|
|
||||||
public function activity(): string
|
public function activity(): string
|
||||||
{
|
{
|
||||||
// Prevent analytics hit when authenticated
|
// Prevent analytics hit when authenticated
|
||||||
@ -209,4 +224,46 @@ class PodcastController extends BaseController
|
|||||||
|
|
||||||
return $cachedView;
|
return $cachedView;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @noRector ReturnTypeDeclarationRector
|
||||||
|
*/
|
||||||
|
public function episodeCollection(): Response
|
||||||
|
{
|
||||||
|
if ($this->podcast->type === 'serial') {
|
||||||
|
// podcast is serial
|
||||||
|
$episodes = model('EpisodeModel')
|
||||||
|
->where('`published_at` <= NOW()', null, false)
|
||||||
|
->orderBy('season_number DESC, number ASC');
|
||||||
|
} else {
|
||||||
|
$episodes = model('EpisodeModel')
|
||||||
|
->where('`published_at` <= NOW()', null, false)
|
||||||
|
->orderBy('published_at', 'DESC');
|
||||||
|
}
|
||||||
|
|
||||||
|
$pageNumber = (int) $this->request->getGet('page');
|
||||||
|
|
||||||
|
if ($pageNumber < 1) {
|
||||||
|
$episodes->paginate(12);
|
||||||
|
$pager = $episodes->pager;
|
||||||
|
$collection = new OrderedCollectionObject(null, $pager);
|
||||||
|
} else {
|
||||||
|
$paginatedEpisodes = $episodes->paginate(12, 'default', $pageNumber);
|
||||||
|
$pager = $episodes->pager;
|
||||||
|
|
||||||
|
$orderedItems = [];
|
||||||
|
if ($paginatedEpisodes !== null) {
|
||||||
|
foreach ($paginatedEpisodes as $episode) {
|
||||||
|
$orderedItems[] = (new PodcastEpisode($episode))->toArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @phpstan-ignore-next-line
|
||||||
|
$collection = new OrderedCollectionPage($pager, $orderedItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->response
|
||||||
|
->setContentType('application/activity+json')
|
||||||
|
->setBody($collection->toJSON());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -121,6 +121,11 @@ class Episode extends Entity
|
|||||||
*/
|
*/
|
||||||
protected ?array $statuses = null;
|
protected ?array $statuses = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Status[]|null
|
||||||
|
*/
|
||||||
|
protected ?array $comments = null;
|
||||||
|
|
||||||
protected ?Location $location = null;
|
protected ?Location $location = null;
|
||||||
|
|
||||||
protected string $custom_rss_string;
|
protected string $custom_rss_string;
|
||||||
@ -387,7 +392,7 @@ class Episode extends Entity
|
|||||||
public function getStatuses(): array
|
public function getStatuses(): array
|
||||||
{
|
{
|
||||||
if ($this->id === null) {
|
if ($this->id === null) {
|
||||||
throw new RuntimeException('Episode must be created before getting soundbites.');
|
throw new RuntimeException('Episode must be created before getting statuses.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->statuses === null) {
|
if ($this->statuses === null) {
|
||||||
@ -397,6 +402,22 @@ class Episode extends Entity
|
|||||||
return $this->statuses;
|
return $this->statuses;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Status[]
|
||||||
|
*/
|
||||||
|
public function getComments(): array
|
||||||
|
{
|
||||||
|
if ($this->id === null) {
|
||||||
|
throw new RuntimeException('Episode must be created before getting comments.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->comments === null) {
|
||||||
|
$this->comments = (new StatusModel())->getEpisodeComments($this->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->comments;
|
||||||
|
}
|
||||||
|
|
||||||
public function getLink(): string
|
public function getLink(): string
|
||||||
{
|
{
|
||||||
return base_url(route_to('episode', $this->getPodcast() ->name, $this->attributes['slug']));
|
return base_url(route_to('episode', $this->getPodcast() ->name, $this->attributes['slug']));
|
||||||
|
@ -92,7 +92,7 @@ class StatusController extends Controller
|
|||||||
if ($paginatedReplies !== null) {
|
if ($paginatedReplies !== null) {
|
||||||
foreach ($paginatedReplies as $reply) {
|
foreach ($paginatedReplies as $reply) {
|
||||||
$replyObject = new $noteObjectClass($reply);
|
$replyObject = new $noteObjectClass($reply);
|
||||||
$orderedItems[] = $replyObject->toJSON();
|
$orderedItems[] = $replyObject->toArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ class NoteObject extends ObjectType
|
|||||||
$this->inReplyTo = $status->reply_to_status->uri;
|
$this->inReplyTo = $status->reply_to_status->uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->replies = base_url(route_to('status-replies', $status->actor->username, $status->id));
|
$this->replies = url_to('status-replies', $status->actor->username, $status->id);
|
||||||
|
|
||||||
$this->cc = [$status->actor->followers_url];
|
$this->cc = [$status->actor->followers_url];
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ class OrderedCollectionObject extends ObjectType
|
|||||||
protected ?string $last = null;
|
protected ?string $last = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param ObjectType[] $orderedItems
|
* @param ObjectType[]|null $orderedItems
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
protected ?array $orderedItems = null,
|
protected ?array $orderedItems = null,
|
||||||
|
@ -40,7 +40,7 @@ trait AnalyticsTrait
|
|||||||
$procedureName = $db->prefixTable('analytics_website');
|
$procedureName = $db->prefixTable('analytics_website');
|
||||||
$db->query("call {$procedureName}(?,?,?,?,?,?)", [
|
$db->query("call {$procedureName}(?,?,?,?,?,?)", [
|
||||||
$podcastId,
|
$podcastId,
|
||||||
$session->get('browser'),
|
$session->get('browser') ?? '',
|
||||||
$session->get('entryPage'),
|
$session->get('entryPage'),
|
||||||
$referer,
|
$referer,
|
||||||
$domain,
|
$domain,
|
||||||
|
@ -10,21 +10,39 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Libraries;
|
namespace App\Libraries;
|
||||||
|
|
||||||
use ActivityPub\Entities\Actor;
|
|
||||||
use ActivityPub\Objects\ActorObject;
|
use ActivityPub\Objects\ActorObject;
|
||||||
use App\Models\PodcastModel;
|
use App\Entities\Podcast;
|
||||||
|
|
||||||
class PodcastActor extends ActorObject
|
class PodcastActor extends ActorObject
|
||||||
{
|
{
|
||||||
protected string $rss;
|
protected string $rssFeed;
|
||||||
|
|
||||||
public function __construct(Actor $actor)
|
protected string $language;
|
||||||
|
|
||||||
|
protected string $category;
|
||||||
|
|
||||||
|
protected string $episodes;
|
||||||
|
|
||||||
|
public function __construct(Podcast $podcast)
|
||||||
{
|
{
|
||||||
parent::__construct($actor);
|
parent::__construct($podcast->actor);
|
||||||
|
|
||||||
$podcast = (new PodcastModel())->where('actor_id', $actor->id)
|
$this->context[] = 'https://github.com/Podcastindex-org/activitypub-spec-work/blob/main/docs/1.0.md';
|
||||||
->first();
|
|
||||||
|
|
||||||
$this->rss = $podcast->feed_url;
|
$this->type = 'Podcast';
|
||||||
|
|
||||||
|
$this->rssFeed = $podcast->feed_url;
|
||||||
|
|
||||||
|
$this->language = $podcast->language_code;
|
||||||
|
|
||||||
|
$category = '';
|
||||||
|
if ($podcast->category->parent_id !== null) {
|
||||||
|
$category .= $podcast->category->parent->apple_category . ' > ';
|
||||||
|
}
|
||||||
|
$category .= $podcast->category->apple_category;
|
||||||
|
|
||||||
|
$this->category = $category;
|
||||||
|
|
||||||
|
$this->episodes = url_to('podcast-episodes', $podcast->name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
89
app/Libraries/PodcastEpisode.php
Normal file
89
app/Libraries/PodcastEpisode.php
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright 2021 Podlibre
|
||||||
|
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||||
|
* @link https://castopod.org/
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Libraries;
|
||||||
|
|
||||||
|
use ActivityPub\Core\ObjectType;
|
||||||
|
use App\Entities\Episode;
|
||||||
|
|
||||||
|
class PodcastEpisode extends ObjectType
|
||||||
|
{
|
||||||
|
protected string $type = 'PodcastEpisode';
|
||||||
|
|
||||||
|
protected string $attributedTo;
|
||||||
|
|
||||||
|
protected string $comments;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<mixed>
|
||||||
|
*/
|
||||||
|
protected array $description = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, string>
|
||||||
|
*/
|
||||||
|
protected array $image = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<mixed>
|
||||||
|
*/
|
||||||
|
protected array $audio = [];
|
||||||
|
|
||||||
|
public function __construct(Episode $episode)
|
||||||
|
{
|
||||||
|
// TODO: clean things up with specified spec
|
||||||
|
$this->id = $episode->link;
|
||||||
|
|
||||||
|
$this->description = [
|
||||||
|
'type' => 'Note',
|
||||||
|
'mediaType' => 'text/markdown',
|
||||||
|
'content' => $episode->description_markdown,
|
||||||
|
'contentMap' => [
|
||||||
|
$episode->podcast->language_code => $episode->description_html,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->image = [
|
||||||
|
'type' => 'Image',
|
||||||
|
'mediaType' => $episode->image_mimetype,
|
||||||
|
'url' => $episode->image->url,
|
||||||
|
];
|
||||||
|
|
||||||
|
// add audio file
|
||||||
|
$this->audio = [
|
||||||
|
'id' => $episode->audio_file_url,
|
||||||
|
'type' => 'Audio',
|
||||||
|
'name' => $episode->title,
|
||||||
|
'size' => $episode->audio_file_size,
|
||||||
|
'duration' => $episode->audio_file_duration,
|
||||||
|
'url' => [
|
||||||
|
'href' => $episode->audio_file_url,
|
||||||
|
'type' => 'Link',
|
||||||
|
'mediaType' => $episode->audio_file_mimetype,
|
||||||
|
],
|
||||||
|
'transcript' => $episode->transcript_file_url,
|
||||||
|
'chapters' => $episode->chapters_file_url,
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->comments = url_to('episode-comments', $episode->podcast->name, $episode->slug);
|
||||||
|
|
||||||
|
if ($episode->published_at !== null) {
|
||||||
|
$this->published = $episode->published_at->format(DATE_W3C);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($episode->podcast->actor !== null) {
|
||||||
|
$this->attributedTo = $episode->podcast->actor->uri;
|
||||||
|
|
||||||
|
if ($episode->podcast->actor->followers_url) {
|
||||||
|
$this->cc = [$episode->podcast->actor->followers_url];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -12,6 +12,7 @@ namespace App\Models;
|
|||||||
|
|
||||||
use ActivityPub\Models\StatusModel as ActivityPubStatusModel;
|
use ActivityPub\Models\StatusModel as ActivityPubStatusModel;
|
||||||
use App\Entities\Status;
|
use App\Entities\Status;
|
||||||
|
use CodeIgniter\Database\BaseBuilder;
|
||||||
|
|
||||||
class StatusModel extends ActivityPubStatusModel
|
class StatusModel extends ActivityPubStatusModel
|
||||||
{
|
{
|
||||||
@ -53,4 +54,21 @@ class StatusModel extends ActivityPubStatusModel
|
|||||||
->orderBy('published_at', 'DESC')
|
->orderBy('published_at', 'DESC')
|
||||||
->findAll();
|
->findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all published statuses for a given episode ordered by publication date
|
||||||
|
*
|
||||||
|
* @return Status[]
|
||||||
|
*/
|
||||||
|
public function getEpisodeComments(int $episodeId): array
|
||||||
|
{
|
||||||
|
return $this->whereIn('in_reply_to_id', function (BaseBuilder $builder) use (&$episodeId): BaseBuilder {
|
||||||
|
return $builder->select('id')
|
||||||
|
->from('activitypub_statuses')
|
||||||
|
->where('episode_id', $episodeId);
|
||||||
|
})
|
||||||
|
->where('`published_at` <= NOW()', null, false)
|
||||||
|
->orderBy('published_at', 'ASC')
|
||||||
|
->findAll();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</header>
|
</header>
|
||||||
<?php if ($episodes): ?>
|
<?php if ($episodes): ?>
|
||||||
<div class="flex justify-between p-2 space-x-4 overflow-x-auto">
|
<div class="flex p-2 overflow-x-auto gap-x-6">
|
||||||
<?php foreach ($episodes as $episode): ?>
|
<?php foreach ($episodes as $episode): ?>
|
||||||
<article class="flex flex-col flex-shrink-0 w-56 overflow-hidden bg-white border shadow rounded-xl">
|
<article class="flex flex-col flex-shrink-0 w-56 overflow-hidden bg-white border shadow rounded-xl">
|
||||||
<img
|
<img
|
||||||
|
Loading…
Reference in New Issue
Block a user