castopod/app/Models/PersonModel.php

407 lines
11 KiB
PHP
Raw Normal View History

<?php
/**
* @copyright 2021 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Models;
use App\Entities\Image;
use App\Entities\Person;
use CodeIgniter\Database\BaseResult;
use CodeIgniter\Model;
class PersonModel extends Model
{
/**
* @var string
*/
protected $table = 'persons';
/**
* @var string
*/
protected $primaryKey = 'id';
/**
* @var string[]
*/
protected $allowedFields = [
'id',
'full_name',
'unique_name',
'information_url',
'image_path',
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
2021-04-02 19:20:02 +02:00
'image_mimetype',
'created_by',
'updated_by',
];
/**
* @var string
*/
protected $returnType = Person::class;
/**
* @var bool
*/
protected $useSoftDeletes = false;
/**
* @var bool
*/
protected $useTimestamps = true;
/**
* @var array<string, string>
*/
protected $validationRules = [
'full_name' => 'required',
'unique_name' =>
'required|regex_match[/^[a-z0-9\-]{1,191}$/]|is_unique[persons.unique_name,id,{id}]',
'image_path' => 'required',
'created_by' => 'required',
'updated_by' => 'required',
];
/**
* @var string[]
*/
protected $afterInsert = ['clearCache'];
/**
* clear cache before update if by any chance, the person name changes, so will the person link
*
* @var string[]
*/
protected $beforeUpdate = ['clearCache'];
/**
* @var string[]
*/
protected $beforeDelete = ['clearCache'];
public function getPersonById(int $personId): ?Person
{
$cacheName = "person#{$personId}";
if (!($found = cache($cacheName))) {
$found = $this->find($personId);
cache()->save($cacheName, $found, DECADE);
}
return $found;
}
public function getPerson(string $fullName): ?Person
{
return $this->where('full_name', $fullName)->first();
}
/**
* @return object[]
*/
public function getPersonRoles(int $personId, int $podcastId, ?int $episodeId): array {
if ($episodeId) {
$cacheName = "podcast#{$podcastId}_episode#{$episodeId}_person#{$personId}_roles";
if (!($found = cache($cacheName))) {
$found = $this
->select('episodes_persons.person_group as group, episodes_persons.person_role as role')
->join('episodes_persons', 'persons.id = episodes_persons.person_id')
->where('persons.id', $personId)
->where('episodes_persons.episode_id', $episodeId)
->get()
->getResultObject();
}
} else {
$cacheName = "podcast#{$podcastId}_person#{$personId}_roles";
if (!($found = cache($cacheName))) {
$found = $this
->select('podcasts_persons.person_group as group, podcasts_persons.person_role as role')
->join('podcasts_persons', 'persons.id = podcasts_persons.person_id')
->where('persons.id', $personId)
->where('podcasts_persons.podcast_id', $podcastId)
->get()
->getResultObject();
}
}
return $found;
}
/**
* @return array<string, string>
*/
public function getPersonOptions(): array
{
$options = [];
if (!($options = cache('person_options'))) {
$options = array_reduce(
$this->select('`id`, `full_name`')
->orderBy('`full_name`', 'ASC')
->findAll(),
function ($result, $person) {
$result[$person->id] = $person->full_name;
return $result;
},
[],
);
cache()->save('person_options', $options, DECADE);
}
return $options;
}
/**
* @return array<string, string>
*/
public function getTaxonomyOptions(): array
{
$options = [];
$locale = service('request')->getLocale();
$cacheName = "taxonomy_options_{$locale}";
/** @var array<string, array> */
$personsTaxonomy = lang('PersonsTaxonomy.persons');
if (!($options = cache($cacheName))) {
foreach ($personsTaxonomy as $group_key => $group) {
foreach ($group['roles'] as $role_key => $role) {
$options[
"{$group_key},{$role_key}"
] = "{$group['label']} {$role['label']}";
}
}
cache()->save($cacheName, $options, DECADE);
}
return $options;
}
public function addPerson(
string $fullName,
?string $informationUrl,
string $image
): int|bool {
$person = new Person([
'full_name' => $fullName,
'unique_name' => slugify($fullName),
'information_url' => $informationUrl,
'image' => new Image(download_file($image)),
'created_by' => user_id(),
'updated_by' => user_id(),
]);
return $this->insert($person);
}
/**
* @return Person[]
*/
public function getEpisodePersons(int $podcastId, int $episodeId): array
{
$cacheName = "podcast#{$podcastId}_episode#{$episodeId}_persons";
if (!($found = cache($cacheName))) {
$found = $this
->select('persons.*, episodes_persons.podcast_id, episodes_persons.episode_id')
->distinct()
->join('episodes_persons', 'persons.id = episodes_persons.person_id')
->where('episodes_persons.episode_id', $episodeId)
->orderby('persons.full_name')
->findAll();
cache()->save($cacheName, $found, DECADE);
}
return $found;
}
/**
* @return Person[]
*/
public function getPodcastPersons(int $podcastId): array
{
$cacheName = "podcast#{$podcastId}_persons";
if (!($found = cache($cacheName))) {
$found = $this
->select('persons.*, podcasts_persons.podcast_id as podcast_id')
->distinct()
->join('podcasts_persons', 'persons.id=podcasts_persons.person_id')
->where('podcasts_persons.podcast_id', $podcastId)
->orderby('persons.full_name')
->findAll();
cache()->save($cacheName, $found, DECADE);
}
return $found;
}
public function addEpisodePerson(
int $podcastId,
int $episodeId,
int $personId,
string $groupSlug,
string $roleSlug
): int|bool {
return $this->db->table('episodes_persons')->insert([
'podcast_id' => $podcastId,
'episode_id' => $episodeId,
'person_id' => $personId,
'person_group' => $groupSlug,
'person_role' => $roleSlug,
]);
}
public function addPodcastPerson(
int $podcastId,
int $personId,
string $groupSlug,
string $roleSlug
): int|bool {
return $this->db->table('podcasts_persons')->insert([
'podcast_id' => $podcastId,
'person_id' => $personId,
'person_group' => $groupSlug,
'person_role' => $roleSlug,
]);
}
/**
* Add persons to podcast
*
* @param array<string> $persons
* @param array<string, string> $roles
*
* @return bool|int Number of rows inserted or FALSE on failure
*/
public function addPodcastPersons(
int $podcastId,
array $persons = [],
array $roles = []
): int|bool {
if ($persons === []) {
return 0;
}
cache()->delete("podcast#{$podcastId}_persons");
(new PodcastModel())->clearCache(['id' => $podcastId]);
$data = [];
foreach ($persons as $person) {
if ($roles === []) {
$data[] = [
'podcast_id' => $podcastId,
'person_id' => $person,
];
}
foreach ($roles as $role) {
$groupRole = explode(',', $role);
$data[] = [
'podcast_id' => $podcastId,
'person_id' => $person,
'person_group' => $groupRole[0],
'person_role' => $groupRole[1],
];
}
}
return $this->db->table('podcasts_persons')->insertBatch($data);
}
/**
* Add persons to episode
*
* @return BaseResult|bool Number of rows inserted or FALSE on failure
*/
public function removePersonFromPodcast(int $podcastId, int $personId): BaseResult|bool
{
return $this->db->table('podcasts_persons')->delete([
'podcast_id' => $podcastId,
'person_id' => $personId,
]);
}
/**
* Add persons to episode
*
* @param int[] $personIds
* @param string[] $groupsRoles
*
* @return bool|int Number of rows inserted or FALSE on failure
*/
public function addEpisodePersons(
int $podcastId,
int $episodeId,
array $personIds,
array $groupsRoles
): bool|int {
if ($personIds !== []) {
(new EpisodeModel())->clearCache(['id' => $episodeId]);
$data = [];
foreach ($personIds as $personId) {
if ($groupsRoles !== []) {
foreach ($groupsRoles as $groupRole) {
$groupRole = explode(',', $groupRole);
$data[] = [
'podcast_id' => $podcastId,
'episode_id' => $episodeId,
'person_id' => $personId,
'person_group' => $groupRole[0],
'person_role' => $groupRole[1],
];
}
} else {
$data[] = [
'podcast_id' => $podcastId,
'episode_id' => $episodeId,
'person_id' => $personId,
];
}
}
return $this->db->table('episodes_persons')->insertBatch($data);
}
return 0;
}
/**
* @return BaseResult|bool
*/
public function removePersonFromEpisode(
int $podcastId,
int $episodeId,
int $personId
): BaseResult|bool {
return $this->db->table('episodes_persons')->delete([
'podcast_id' => $podcastId,
'episode_id' => $episodeId,
'person_id' => $personId,
]);
}
/**
* @param mixed[] $data
*
* @return array<string, array<string|int, mixed>>
*/
protected function clearCache(array $data): array
{
$personId = is_array($data['id']) ? $data['id']['id'] : $data['id'];
cache()->delete('person_options');
cache()->delete("person#{$personId}");
// clear cache for every credits page
cache()->deleteMatching('page_credits_*');
return $data;
}
}