Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • alpha
  • beta
  • develop
  • docs/fix-readme
  • docs/update-vitepress
  • draft/rss-feed
  • feat/dashboard
  • feat/episodes-page-ux
  • feat/generator-user-agent
  • feat/headliner
  • feat/new-languages
  • feat/plugins
  • fix/federation
  • fix/forms-ux
  • i18n
  • main
  • next
  • refactor/transcripts
  • v1.0.0
  • v1.0.0-alpha.1
  • v1.0.0-alpha.10
  • v1.0.0-alpha.11
  • v1.0.0-alpha.12
  • v1.0.0-alpha.13
  • v1.0.0-alpha.14
  • v1.0.0-alpha.15
  • v1.0.0-alpha.16
  • v1.0.0-alpha.17
  • v1.0.0-alpha.18
  • v1.0.0-alpha.19
  • v1.0.0-alpha.2
  • v1.0.0-alpha.20
  • v1.0.0-alpha.21
  • v1.0.0-alpha.22
  • v1.0.0-alpha.23
  • v1.0.0-alpha.24
  • v1.0.0-alpha.25
  • v1.0.0-alpha.26
  • v1.0.0-alpha.27
  • v1.0.0-alpha.28
  • v1.0.0-alpha.29
  • v1.0.0-alpha.3
  • v1.0.0-alpha.30
  • v1.0.0-alpha.31
  • v1.0.0-alpha.32
  • v1.0.0-alpha.33
  • v1.0.0-alpha.34
  • v1.0.0-alpha.35
  • v1.0.0-alpha.36
  • v1.0.0-alpha.37
  • v1.0.0-alpha.38
  • v1.0.0-alpha.39
  • v1.0.0-alpha.4
  • v1.0.0-alpha.40
  • v1.0.0-alpha.41
  • v1.0.0-alpha.42
  • v1.0.0-alpha.43
  • v1.0.0-alpha.44
  • v1.0.0-alpha.45
  • v1.0.0-alpha.46
  • v1.0.0-alpha.47
  • v1.0.0-alpha.48
  • v1.0.0-alpha.49
  • v1.0.0-alpha.5
  • v1.0.0-alpha.50
  • v1.0.0-alpha.51
  • v1.0.0-alpha.52
  • v1.0.0-alpha.53
  • v1.0.0-alpha.54
  • v1.0.0-alpha.55
  • v1.0.0-alpha.56
  • v1.0.0-alpha.57
  • v1.0.0-alpha.58
  • v1.0.0-alpha.59
  • v1.0.0-alpha.6
  • v1.0.0-alpha.60
  • v1.0.0-alpha.61
  • v1.0.0-alpha.62
  • v1.0.0-alpha.63
  • v1.0.0-alpha.64
  • v1.0.0-alpha.65
  • v1.0.0-alpha.66
  • v1.0.0-alpha.67
  • v1.0.0-alpha.68
  • v1.0.0-alpha.69
  • v1.0.0-alpha.7
  • v1.0.0-alpha.70
  • v1.0.0-alpha.71
  • v1.0.0-alpha.72
  • v1.0.0-alpha.73
  • v1.0.0-alpha.74
  • v1.0.0-alpha.75
  • v1.0.0-alpha.76
  • v1.0.0-alpha.77
  • v1.0.0-alpha.78
  • v1.0.0-alpha.79
  • v1.0.0-alpha.8
  • v1.0.0-alpha.80
  • v1.0.0-alpha.9
  • v1.0.0-beta.1
  • v1.0.0-beta.10
  • v1.0.0-beta.11
  • v1.0.0-beta.12
  • v1.0.0-beta.13
  • v1.0.0-beta.14
  • v1.0.0-beta.15
  • v1.0.0-beta.16
  • v1.0.0-beta.17
  • v1.0.0-beta.18
  • v1.0.0-beta.19
  • v1.0.0-beta.2
  • v1.0.0-beta.20
  • v1.0.0-beta.21
  • v1.0.0-beta.22
  • v1.0.0-beta.23
  • v1.0.0-beta.24
  • v1.0.0-beta.3
  • v1.0.0-beta.4
118 results

Target

Select target project
  • adaures/castopod
  • mkljczk/castopod-host
  • spaetz/castopod-host
  • PatrykMis/castopod
  • jonas/castopod
  • ajeremias/castopod
  • misuzu/castopod
  • KrzysztofDomanczyk/castopod
  • Behel/castopod
  • nebulon/castopod
  • ewen/castopod
  • NeoluxConsulting/castopod
  • nateritter/castopod-og
  • prcutler/castopod
14 results
Select Git revision
  • alpha
  • beta
  • develop
  • docs/fix-readme
  • docs/update-vitepress
  • draft/rss-feed
  • feat/dashboard
  • feat/episodes-page-ux
  • feat/generator-user-agent
  • feat/headliner
  • feat/new-languages
  • feat/plugins
  • fix/federation
  • fix/forms-ux
  • i18n
  • main
  • next
  • refactor/transcripts
  • v1.0.0
  • v1.0.0-alpha.1
  • v1.0.0-alpha.10
  • v1.0.0-alpha.11
  • v1.0.0-alpha.12
  • v1.0.0-alpha.13
  • v1.0.0-alpha.14
  • v1.0.0-alpha.15
  • v1.0.0-alpha.16
  • v1.0.0-alpha.17
  • v1.0.0-alpha.18
  • v1.0.0-alpha.19
  • v1.0.0-alpha.2
  • v1.0.0-alpha.20
  • v1.0.0-alpha.21
  • v1.0.0-alpha.22
  • v1.0.0-alpha.23
  • v1.0.0-alpha.24
  • v1.0.0-alpha.25
  • v1.0.0-alpha.26
  • v1.0.0-alpha.27
  • v1.0.0-alpha.28
  • v1.0.0-alpha.29
  • v1.0.0-alpha.3
  • v1.0.0-alpha.30
  • v1.0.0-alpha.31
  • v1.0.0-alpha.32
  • v1.0.0-alpha.33
  • v1.0.0-alpha.34
  • v1.0.0-alpha.35
  • v1.0.0-alpha.36
  • v1.0.0-alpha.37
  • v1.0.0-alpha.38
  • v1.0.0-alpha.39
  • v1.0.0-alpha.4
  • v1.0.0-alpha.40
  • v1.0.0-alpha.41
  • v1.0.0-alpha.42
  • v1.0.0-alpha.43
  • v1.0.0-alpha.44
  • v1.0.0-alpha.45
  • v1.0.0-alpha.46
  • v1.0.0-alpha.47
  • v1.0.0-alpha.48
  • v1.0.0-alpha.49
  • v1.0.0-alpha.5
  • v1.0.0-alpha.50
  • v1.0.0-alpha.51
  • v1.0.0-alpha.52
  • v1.0.0-alpha.53
  • v1.0.0-alpha.54
  • v1.0.0-alpha.55
  • v1.0.0-alpha.56
  • v1.0.0-alpha.57
  • v1.0.0-alpha.58
  • v1.0.0-alpha.59
  • v1.0.0-alpha.6
  • v1.0.0-alpha.60
  • v1.0.0-alpha.61
  • v1.0.0-alpha.62
  • v1.0.0-alpha.63
  • v1.0.0-alpha.64
  • v1.0.0-alpha.65
  • v1.0.0-alpha.66
  • v1.0.0-alpha.67
  • v1.0.0-alpha.68
  • v1.0.0-alpha.69
  • v1.0.0-alpha.7
  • v1.0.0-alpha.70
  • v1.0.0-alpha.71
  • v1.0.0-alpha.72
  • v1.0.0-alpha.73
  • v1.0.0-alpha.74
  • v1.0.0-alpha.75
  • v1.0.0-alpha.76
  • v1.0.0-alpha.77
  • v1.0.0-alpha.78
  • v1.0.0-alpha.79
  • v1.0.0-alpha.8
  • v1.0.0-alpha.80
  • v1.0.0-alpha.9
  • v1.0.0-beta.1
  • v1.0.0-beta.10
  • v1.0.0-beta.11
  • v1.0.0-beta.12
  • v1.0.0-beta.13
  • v1.0.0-beta.14
  • v1.0.0-beta.15
  • v1.0.0-beta.16
  • v1.0.0-beta.17
  • v1.0.0-beta.18
  • v1.0.0-beta.19
  • v1.0.0-beta.2
  • v1.0.0-beta.20
  • v1.0.0-beta.21
  • v1.0.0-beta.22
  • v1.0.0-beta.23
  • v1.0.0-beta.24
  • v1.0.0-beta.3
  • v1.0.0-beta.4
118 results
Show changes
Commits on Source (2)
Showing
with 328 additions and 22 deletions
# [1.0.0-alpha.64](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.63...v1.0.0-alpha.64) (2021-07-12)
### Features
- **activitypub:** add Podcast actor and PodcastEpisode object with comments
([9e1e5d2](https://code.podlibre.org/podlibre/castopod-host/commit/9e1e5d2e862d6a3345d11ca7f96b955c76bfa013))
# [1.0.0-alpha.63](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.62...v1.0.0-alpha.63) (2021-07-12)
### Features
......
......@@ -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;
/**
......
......@@ -11,7 +11,7 @@ declare(strict_types=1);
|
| NOTE: this constant is updated upon release with Continuous Integration.
*/
defined('CP_VERSION') || define('CP_VERSION', '1.0.0-alpha.63');
defined('CP_VERSION') || define('CP_VERSION', '1.0.0-alpha.64');
/*
| --------------------------------------------------------------------
......
......@@ -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',
......
......@@ -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());
}
}
......@@ -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());
}
}
......@@ -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']));
......
......@@ -92,7 +92,7 @@ class StatusController extends Controller
if ($paginatedReplies !== null) {
foreach ($paginatedReplies as $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->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];
}
......
......@@ -28,7 +28,7 @@ class OrderedCollectionObject extends ObjectType
protected ?string $last = null;
/**
* @param ObjectType[] $orderedItems
* @param ObjectType[]|null $orderedItems
*/
public function __construct(
protected ?array $orderedItems = null,
......
......@@ -40,7 +40,7 @@ trait AnalyticsTrait
$procedureName = $db->prefixTable('analytics_website');
$db->query("call {$procedureName}(?,?,?,?,?,?)", [
$podcastId,
$session->get('browser'),
$session->get('browser') ?? '',
$session->get('entryPage'),
$referer,
$domain,
......
......@@ -10,21 +10,39 @@ declare(strict_types=1);
namespace App\Libraries;
use ActivityPub\Entities\Actor;
use ActivityPub\Objects\ActorObject;
use App\Models\PodcastModel;
use App\Entities\Podcast;
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);
$this->context[] = 'https://github.com/Podcastindex-org/activitypub-spec-work/blob/main/docs/1.0.md';
$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;
$podcast = (new PodcastModel())->where('actor_id', $actor->id)
->first();
$this->category = $category;
$this->rss = $podcast->feed_url;
$this->episodes = url_to('podcast-episodes', $podcast->name);
}
}
<?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 App\Entities\Status;
use CodeIgniter\Database\BaseBuilder;
class StatusModel extends ActivityPubStatusModel
{
......@@ -53,4 +54,21 @@ class StatusModel extends ActivityPubStatusModel
->orderBy('published_at', 'DESC')
->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>
</header>
<?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): ?>
<article class="flex flex-col flex-shrink-0 w-56 overflow-hidden bg-white border shadow rounded-xl">
<img
......
{
"name": "podlibre/castopod-host",
"version": "1.0.0-alpha63",
"version": "1.0.0-alpha64",
"type": "project",
"description": "Castopod Host is an open-source hosting platform made for podcasters who want engage and interact with their audience.",
"homepage": "https://castopod.org",
......
{
"name": "castopod-host",
"version": "1.0.0-alpha.63",
"version": "1.0.0-alpha.64",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"version": "1.0.0-alpha.63",
"version": "1.0.0-alpha.64",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@amcharts/amcharts4": "^4.10.17",
{
"name": "castopod-host",
"version": "1.0.0-alpha.63",
"version": "1.0.0-alpha.64",
"description": "Castopod Host is an open-source hosting platform made for podcasters who want engage and interact with their audience.",
"private": true,
"license": "AGPL-3.0-or-later",
......