Commit 9e1e5d2e authored by Yassine Doghri's avatar Yassine Doghri
Browse files

feat(activitypub): add Podcast actor and PodcastEpisode object with comments

parent b814cfaf
Loading
Loading
Loading
Loading
Loading
+0 −3
Original line number Diff line number Diff line
@@ -6,7 +6,6 @@ namespace Config;

use ActivityPub\Config\ActivityPub as ActivityPubBase;
use App\Libraries\NoteObject;
use App\Libraries\PodcastActor;

class ActivityPub extends ActivityPubBase
{
@@ -15,8 +14,6 @@ class ActivityPub extends ActivityPubBase
     * ActivityPub Objects
     * --------------------------------------------------------------------
     */
    public string $actorObject = PodcastActor::class;

    public string $noteObject = NoteObject::class;

    /**
+38 −0
Original line number Diff line number Diff line
@@ -697,6 +697,10 @@ $routes->group('@(:podcastName)', function ($routes): void {
                'namespace' => 'ActivityPub\Controllers',
                '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' => [
                'namespace' => 'ActivityPub\Controllers',
                'controller-method' => 'ActorController/$1',
@@ -705,10 +709,44 @@ $routes->group('@(:podcastName)', function ($routes): void {
    ]);
    $routes->get('episodes', 'PodcastController::episodes/$1', [
        '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->get('/', 'EpisodeController/$1/$2', [
            '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', [
            'as' => 'episode-oembed-json',
+61 −0
Original line number Diff line number Diff line
@@ -10,12 +10,18 @@ declare(strict_types=1);

namespace App\Controllers;

use ActivityPub\Objects\OrderedCollectionObject;
use ActivityPub\Objects\OrderedCollectionPage;
use Analytics\AnalyticsTrait;
use App\Entities\Episode;
use App\Entities\Podcast;
use App\Libraries\NoteObject;
use App\Libraries\PodcastEpisode;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use CodeIgniter\Database\BaseBuilder;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\Response;
use CodeIgniter\HTTP\ResponseInterface;
use Config\Services;
use SimpleXMLElement;
@@ -191,4 +197,59 @@ class EpisodeController extends BaseController

        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());
    }
}
+57 −0
Original line number Diff line number Diff line
@@ -10,12 +10,18 @@ declare(strict_types=1);

namespace App\Controllers;

use ActivityPub\Objects\OrderedCollectionObject;
use ActivityPub\Objects\OrderedCollectionPage;
use Analytics\AnalyticsTrait;
use App\Entities\Podcast;
use App\Libraries\PodcastActor;
use App\Libraries\PodcastEpisode;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use App\Models\StatusModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\HTTP\Response;

class PodcastController extends BaseController
{
@@ -42,6 +48,15 @@ class PodcastController extends BaseController
        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
    {
        // Prevent analytics hit when authenticated
@@ -209,4 +224,46 @@ class PodcastController extends BaseController

        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());
    }
}
+22 −1
Original line number Diff line number Diff line
@@ -121,6 +121,11 @@ class Episode extends Entity
     */
    protected ?array $statuses = null;

    /**
     * @var Status[]|null
     */
    protected ?array $comments = null;

    protected ?Location $location = null;

    protected string $custom_rss_string;
@@ -387,7 +392,7 @@ class Episode extends Entity
    public function getStatuses(): array
    {
        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) {
@@ -397,6 +402,22 @@ class Episode extends Entity
        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
    {
        return base_url(route_to('episode', $this->getPodcast() ->name, $this->attributes['slug']));
Loading