feat: add WebSub module for pushing feed updates to open hubs

This commit is contained in:
Yassine Doghri 2022-03-15 16:47:35 +00:00
parent 1253096197
commit 10d3f73786
17 changed files with 244 additions and 3 deletions

View File

@ -49,6 +49,7 @@ class Autoload extends AutoloadConfig
'Modules\Analytics' => ROOTPATH . 'modules/Analytics/',
'Modules\Install' => ROOTPATH . 'modules/Install/',
'Modules\Fediverse' => ROOTPATH . 'modules/Fediverse/',
'Modules\WebSub' => ROOTPATH . 'modules/WebSub/',
'Config' => APPPATH . 'Config/',
'ViewComponents' => APPPATH . 'Libraries/ViewComponents/',
'ViewThemes' => APPPATH . 'Libraries/ViewThemes/',

View File

@ -69,6 +69,7 @@ use RuntimeException;
* @property string|null $location_osm
* @property array|null $custom_rss
* @property string $custom_rss_string
* @property bool $is_published_on_hubs
* @property int $posts_count
* @property int $comments_count
* @property int $created_by
@ -164,6 +165,7 @@ class Episode extends Entity
'location_geo' => '?string',
'location_osm' => '?string',
'custom_rss' => '?json-array',
'is_published_on_hubs' => 'boolean',
'posts_count' => 'integer',
'comments_count' => 'integer',
'created_by' => 'integer',

View File

@ -73,6 +73,7 @@ use RuntimeException;
* @property string|null $payment_pointer
* @property array|null $custom_rss
* @property string $custom_rss_string
* @property bool $is_published_on_hubs
* @property string|null $partner_id
* @property string|null $partner_link_url
* @property string|null $partner_image_url
@ -180,6 +181,7 @@ class Podcast extends Entity
'location_osm' => '?string',
'payment_pointer' => '?string',
'custom_rss' => '?json-array',
'is_published_on_hubs' => 'boolean',
'partner_id' => '?string',
'partner_link_url' => '?string',
'partner_image_url' => '?string',

View File

@ -41,6 +41,16 @@ if (! function_exists('get_rss_feed')) {
$atomLink->addAttribute('rel', 'self');
$atomLink->addAttribute('type', 'application/rss+xml');
// websub: add links to hubs defined in config
$websubHubs = config('WebSub')
->hubs;
foreach ($websubHubs as $websubHub) {
$atomLinkHub = $channel->addChild('atom:link', null, 'http://www.w3.org/2005/Atom');
$atomLinkHub->addAttribute('href', $websubHub);
$atomLinkHub->addAttribute('rel', 'hub');
$atomLinkHub->addAttribute('type', 'application/rss+xml');
}
if ($podcast->new_feed_url !== null) {
$channel->addChild('new-feed-url', $podcast->new_feed_url, $itunesNamespace);
}

View File

@ -81,6 +81,7 @@ class EpisodeModel extends Model
'location_geo',
'location_osm',
'custom_rss',
'is_published_on_hubs',
'posts_count',
'comments_count',
'published_at',
@ -378,7 +379,7 @@ class EpisodeModel extends Model
/**
* @param mixed[] $data
*
* @return array<string, array<string|int, mixed>>
* @return mixed[]
*/
public function clearCache(array $data): array
{
@ -404,7 +405,7 @@ class EpisodeModel extends Model
/**
* @param mixed[] $data
*
* @return array<string, array<string|int, mixed>>
* @return mixed[]
*/
protected function writeEnclosureMetadata(array $data): array
{

View File

@ -60,6 +60,7 @@ class PodcastModel extends Model
'location_osm',
'payment_pointer',
'custom_rss',
'is_published_on_hubs',
'partner_id',
'partner_link_url',
'partner_image_url',

View File

@ -1,2 +1,3 @@
* * * * * /usr/local/bin/php /castopod/public/index.php scheduled-activities
* * * * * /usr/local/bin/php /castopod/public/index.php scheduled-video-clips
* * * * * /usr/local/bin/php /castopod/public/index.php scheduled-websub-publish

View File

@ -88,6 +88,13 @@ want to generate Video Clips. The following extensions must be installed:
* * * * * /path/to/php /path/to/castopod/public/index.php scheduled-activities
```
- For having your episodes be broadcasted on open hubs upon publication using
[WebSub](https://en.wikipedia.org/wiki/WebSub):
```bash
* * * * * /usr/local/bin/php /castopod/public/index.php scheduled-websub-publish
```
- For Video Clips to be created (see
[FFmpeg requirements](#ffmpeg-v418-or-higher-for-video-clips)):

View File

@ -280,6 +280,9 @@ class EpisodeController extends BaseController
$this->episode->setAudio($this->request->getFile('audio_file'));
$this->episode->setCover($this->request->getFile('cover'));
// republish on websub hubs upon edit
$this->episode->is_published_on_hubs = false;
$transcriptChoice = $this->request->getPost('transcript-choice');
if ($transcriptChoice === 'upload-file') {
$transcriptFile = $this->request->getFile('transcript_file');
@ -725,6 +728,11 @@ class EpisodeController extends BaseController
(new PostModel())->removePost($post);
}
// set podcast is_published_on_hubs to false to trigger websub push
(new PodcastModel())->update($this->episode->podcast->id, [
'is_published_on_hubs' => false,
]);
$episodeModel = new EpisodeModel();
if ($this->episode->published_at !== null) {
// if episode is published, set episode published_at to null to unpublish before deletion

View File

@ -340,6 +340,9 @@ class PodcastController extends BaseController
$this->podcast->is_locked = $this->request->getPost('lock') === 'yes';
$this->podcast->updated_by = (int) user_id();
// republish on websub hubs upon edit
$this->podcast->is_published_on_hubs = false;
$db = db_connect();
$db->transStart();

View File

@ -10,7 +10,7 @@ declare(strict_types=1);
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
namespace Modules\Auth\Database\Migrations;
use CodeIgniter\Database\Migration;

View File

@ -251,6 +251,8 @@ class InstallController extends Controller
->latest();
$migrations->setNamespace(APP_NAMESPACE)
->latest();
$migrations->setNamespace('Modules\WebSub')
->latest();
$migrations->setNamespace('Modules\Auth')
->latest();
$migrations->setNamespace('Modules\Analytics')

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
/**
* @copyright 2022 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
$routes = service('routes');
/**
* WebSub routes file
*/
$routes->group('', [
'namespace' => 'Modules\WebSub\Controllers',
], function ($routes): void {
$routes->cli('scheduled-websub-publish', 'WebSubController::publish');
});

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Modules\WebSub\Config;
use CodeIgniter\Config\BaseConfig;
class WebSub extends BaseConfig
{
/**
* --------------------------------------------------------------------------
* Hubs to ping
* --------------------------------------------------------------------------
* @var string[]
*/
public array $hubs = [
'https://pubsubhubbub.appspot.com/',
'https://pubsubhubbub.superfeedr.com/',
'https://websubhub.com/hub',
'https://switchboard.p3k.io/',
];
}

View File

@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
/**
* @copyright 2022 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace Modules\WebSub\Controllers;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use CodeIgniter\Controller;
use Config\Services;
use Exception;
class WebSubController extends Controller
{
public function publish(): void
{
if (ENVIRONMENT !== 'production') {
return;
}
// get all podcasts that haven't been published yet
// or having a published episode that hasn't been pushed yet
$podcastModel = new PodcastModel();
$podcasts = $podcastModel
->distinct()
->select('podcasts.*')
->join('episodes', 'podcasts.id = episodes.podcast_id', 'left outer')
->where('podcasts.is_published_on_hubs', false)
->orGroupStart()
->where('episodes.is_published_on_hubs', false)
->where('`' . $podcastModel->db->getPrefix() . 'episodes`.`published_at` <= NOW()', null, false)
->groupEnd()
->findAll();
if ($podcasts === []) {
return;
}
$request = Services::curlrequest();
$requestOptions = [
'headers' => [
'User-Agent' => 'Castopod/' . CP_VERSION . '; +' . base_url('', 'https'),
'Content-Type' => 'application/x-www-form-urlencoded',
],
];
$hubUrls = config('WebSub')
->hubs;
foreach ($podcasts as $podcast) {
$requestOptions['form_params'] = [
'hub.mode' => 'publish',
'hub.url' => $podcast->feed_url,
];
foreach ($hubUrls as $hub) {
try {
$request->post($hub, $requestOptions);
} catch (Exception $exception) {
log_message(
'critical',
"COULD NOT PUBLISH @{$podcast->handle} ON {$hub}" . PHP_EOL . $exception->getMessage()
);
}
}
// set podcast feed as having been pushed onto hubs
(new PodcastModel())->update($podcast->id, [
'is_published_on_hubs' => true,
]);
// set newly published episodes as pushed onto hubs
(new EpisodeModel())->set('is_published_on_hubs', true)
->where([
'podcast_id' => $podcast->id,
'is_published_on_hubs' => false,
])
->where('`published_at` <= NOW()', null, false)
->update();
}
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
/**
* @copyright 2022 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace Modules\WebSub\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddIsPublishedOnHubsToPodcasts extends Migration
{
public function up(): void
{
$prefix = $this->db->getPrefix();
$createQuery = <<<CODE_SAMPLE
ALTER TABLE {$prefix}podcasts
ADD COLUMN `is_published_on_hubs` BOOLEAN NOT NULL DEFAULT 0 AFTER `custom_rss`;
CODE_SAMPLE;
$this->db->query($createQuery);
}
public function down(): void
{
$prefix = $this->db->getPrefix();
$this->forge->dropColumn($prefix . 'podcasts', 'is_published_on_hubs');
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
/**
* @copyright 2022 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace Modules\WebSub\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddIsPublishedOnHubsToEpisodes extends Migration
{
public function up(): void
{
$prefix = $this->db->getPrefix();
$createQuery = <<<CODE_SAMPLE
ALTER TABLE {$prefix}episodes
ADD COLUMN `is_published_on_hubs` BOOLEAN NOT NULL DEFAULT 0 AFTER `custom_rss`;
CODE_SAMPLE;
$this->db->query($createQuery);
}
public function down(): void
{
$prefix = $this->db->getPrefix();
$this->forge->dropColumn($prefix . 'episodes', 'is_published_on_hubs');
}
}