mirror of
https://github.com/ad-aures/castopod.git
synced 2026-04-16 13:07:46 +02:00
feat(fediverse): implement activitypub protocols + update user interface
- add "ActivityPub" library to handle server to server federation and basic
client to server protocols using activitypub:
- add webfinger endpoint to look for actor
- add actor definition with inbox / outbox / followers
- remote follow an actor
- create notes with possible preview cards
- interract with favourites, reblogs and replies
- block incoming actors and/or domains
- broadcast/schedule activities to fediverse followers using a cron task
- For castopod, the podcast is the actor:
- overwrite the activitypub library for castopod's specific needs
- perform basic interactions administrating a podcast to interact with fediverse users:
- create notes with episode attachment
- favourite and share a note + reply
- add specific castopod_namespaces for podcasts and episodes definitions
- overwrite CodeIgniter's Route service to include alternate-content option for
activitystream requests
- update episode publication logic:
- remove publication inputs in create / edit episode form
- publish / schedule or unpublish an episode after creation
- the podcaster publishes a note when publishing an episode
- Javascript / Typescript modules:
- fix Dropdown.ts to keep dropdown menu in foreground
- add Modal.ts for funding links modal
- add Toggler.ts to toggle various css states in ui
- User Interface:
- update tailwindcss to v2
- use castopod's pine and rose colors
- update public layout to a 3 column layout
- add pages in public for podcast activity, episode list and notes
- update episode page to include linked notes
- remove previous and next episodes from episode pages
- show different public views depending on whether user is authenticated or not
- use Kumbh Sans and Montserrat fonts
- update CodeIgniter's config files
- with CodeIgniter's new requirements, update docker environments are now based on
php v7.3 image
- move Image entity to Libraries
- update composer and npm packages to latest versions
closes #69 #65 #85, fixes #51 #91 #92 #88
This commit is contained in:
parent
dd3ac9b4ab
commit
2f525c0f6e
476 changed files with 26120 additions and 11160 deletions
278
app/Libraries/ActivityPub/Controllers/NoteController.php
Normal file
278
app/Libraries/ActivityPub/Controllers/NoteController.php
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Controllers;
|
||||
|
||||
use ActivityPub\Config\ActivityPub;
|
||||
use ActivityPub\Objects\OrderedCollectionObject;
|
||||
use ActivityPub\Objects\OrderedCollectionPage;
|
||||
use CodeIgniter\Controller;
|
||||
use CodeIgniter\I18n\Time;
|
||||
|
||||
class NoteController extends Controller
|
||||
{
|
||||
protected $helpers = ['activitypub'];
|
||||
|
||||
/**
|
||||
* @var \ActivityPub\Entities\Note|null
|
||||
*/
|
||||
protected $note;
|
||||
|
||||
/**
|
||||
* @var \ActivityPub\Config\ActivityPub
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->config = config('ActivityPub');
|
||||
}
|
||||
|
||||
public function _remap($method, ...$params)
|
||||
{
|
||||
if (!($this->note = model('NoteModel')->getNoteById($params[0]))) {
|
||||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
unset($params[0]);
|
||||
|
||||
return $this->$method(...$params);
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$noteObjectClass = $this->config->noteObject;
|
||||
$noteObject = new $noteObjectClass($this->note);
|
||||
|
||||
return $this->response
|
||||
->setContentType('application/activity+json')
|
||||
->setBody($noteObject->toJSON());
|
||||
}
|
||||
|
||||
public function replies()
|
||||
{
|
||||
// get note replies
|
||||
$noteReplies = model('NoteModel')
|
||||
->where(
|
||||
'in_reply_to_id',
|
||||
service('uuid')
|
||||
->fromString($this->note->id)
|
||||
->getBytes(),
|
||||
)
|
||||
->where('`published_at` <= NOW()', null, false)
|
||||
->orderBy('published_at', 'ASC');
|
||||
|
||||
$pageNumber = $this->request->getGet('page');
|
||||
|
||||
if (!isset($pageNumber)) {
|
||||
$noteReplies->paginate(12);
|
||||
$pager = $noteReplies->pager;
|
||||
$collection = new OrderedCollectionObject(null, $pager);
|
||||
} else {
|
||||
$paginatedReplies = $noteReplies->paginate(
|
||||
12,
|
||||
'default',
|
||||
$pageNumber,
|
||||
);
|
||||
$pager = $noteReplies->pager;
|
||||
|
||||
$orderedItems = [];
|
||||
$noteObjectClass = $this->config->noteObject;
|
||||
foreach ($paginatedReplies as $reply) {
|
||||
$replyObject = new $noteObjectClass($reply);
|
||||
array_push($orderedItems, $replyObject->toJSON());
|
||||
}
|
||||
$collection = new OrderedCollectionPage($pager, $orderedItems);
|
||||
}
|
||||
|
||||
return $this->response
|
||||
->setContentType('application/activity+json')
|
||||
->setBody($collection->toJSON());
|
||||
}
|
||||
|
||||
public function attemptCreate()
|
||||
{
|
||||
$rules = [
|
||||
'actor_id' => 'required|is_natural_no_zero',
|
||||
'message' => 'required|max_length[500]',
|
||||
];
|
||||
|
||||
if (!$this->validate($rules)) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('errors', $this->validator->getErrors());
|
||||
}
|
||||
|
||||
$newNote = new \ActivityPub\Entities\Note([
|
||||
'actor_id' => $this->request->getPost('actor_id'),
|
||||
'message' => $this->request->getPost('message'),
|
||||
'published_at' => Time::now(),
|
||||
]);
|
||||
|
||||
if (!model('NoteModel')->addNote($newNote)) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
// TODO: translate
|
||||
->with('error', 'Couldn\'t create Note');
|
||||
}
|
||||
|
||||
// Note without preview card has been successfully created
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function attemptFavourite()
|
||||
{
|
||||
$rules = [
|
||||
'actor_id' => 'required|is_natural_no_zero',
|
||||
];
|
||||
|
||||
if (!$this->validate($rules)) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('errors', $this->validator->getErrors());
|
||||
}
|
||||
|
||||
$actor = model('ActorModel')->getActorById(
|
||||
$this->request->getPost('actor_id'),
|
||||
);
|
||||
|
||||
model('FavouriteModel')->toggleFavourite($actor, $this->note->id);
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function attemptReblog()
|
||||
{
|
||||
$rules = [
|
||||
'actor_id' => 'required|is_natural_no_zero',
|
||||
];
|
||||
|
||||
if (!$this->validate($rules)) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('errors', $this->validator->getErrors());
|
||||
}
|
||||
|
||||
$actor = model('ActorModel')->getActorById(
|
||||
$this->request->getPost('actor_id'),
|
||||
);
|
||||
|
||||
model('NoteModel')->toggleReblog($actor, $this->note);
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function attemptReply()
|
||||
{
|
||||
$rules = [
|
||||
'actor_id' => 'required|is_natural_no_zero',
|
||||
'message' => 'required|max_length[500]',
|
||||
];
|
||||
|
||||
if (!$this->validate($rules)) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('errors', $this->validator->getErrors());
|
||||
}
|
||||
|
||||
$newReplyNote = new \ActivityPub\Entities\Note([
|
||||
'actor_id' => $this->request->getPost('actor_id'),
|
||||
'in_reply_to_id' => $this->note->id,
|
||||
'message' => $this->request->getPost('message'),
|
||||
'published_at' => Time::now(),
|
||||
]);
|
||||
|
||||
if (!model('NoteModel')->addReply($newReplyNote)) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
// TODO: translate
|
||||
->with('error', 'Couldn\'t create Reply');
|
||||
}
|
||||
|
||||
// Reply note without preview card has been successfully created
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function attemptRemoteAction($action)
|
||||
{
|
||||
$rules = [
|
||||
'handle' =>
|
||||
'regex_match[/^@?(?P<username>[\w\.\-]+)@(?P<host>[\w\.\-]+)(?P<port>:[\d]+)?$/]',
|
||||
];
|
||||
|
||||
if (!$this->validate($rules)) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('errors', $this->validator->getErrors());
|
||||
}
|
||||
|
||||
helper('text');
|
||||
|
||||
// get webfinger data from actor
|
||||
// parse activityPub id to get actor and domain
|
||||
// check if actor and domain exist
|
||||
try {
|
||||
if ($parts = split_handle($this->request->getPost('handle'))) {
|
||||
extract($parts);
|
||||
|
||||
$data = get_webfinger_data($username, $domain);
|
||||
}
|
||||
} catch (\CodeIgniter\HTTP\Exceptions\HTTPException $e) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('error', lang('ActivityPub.follow.accountNotFound'));
|
||||
}
|
||||
|
||||
$ostatusKey = array_search(
|
||||
'http://ostatus.org/schema/1.0/subscribe',
|
||||
array_column($data->links, 'rel'),
|
||||
);
|
||||
|
||||
if (!$ostatusKey) {
|
||||
// TODO: error, couldn't remote favourite/share/reply to note
|
||||
// The instance doesn't allow its users remote actions on notes
|
||||
return $this->response->setJSON([]);
|
||||
}
|
||||
|
||||
return redirect()->to(
|
||||
str_replace(
|
||||
'{uri}',
|
||||
urlencode($this->note->uri),
|
||||
$data->links[$ostatusKey]->template,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public function attemptBlockActor()
|
||||
{
|
||||
model('ActorModel')->blockActor($this->note->actor->id);
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function attemptBlockDomain()
|
||||
{
|
||||
model('BlockedDomainModel')->blockDomain($this->note->actor->domain);
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function attemptDelete()
|
||||
{
|
||||
model('NoteModel', false)->removeNote($this->note);
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue