diff --git a/plugins/ActorCircles/ActorCircles.php b/plugins/ActorCircles/ActorCircles.php new file mode 100644 index 0000000000..8ad42a21a1 --- /dev/null +++ b/plugins/ActorCircles/ActorCircles.php @@ -0,0 +1,172 @@ +. +// }}} +/** + * Actor Circles for GNU social + * + * @package GNUsocial + * @category Plugin + * + * @author Phablulo + * @copyright 2018-2019, 2021 Free Software Foundation, Inc http://www.fsf.org + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + */ + +namespace Plugin\ActorCircles; + +use App\Core\DB\DB; +use App\Core\Event; +use function App\Core\I18n\_m; +use App\Core\Modules\Collection; +use App\Core\Router\RouteLoader; +use App\Core\Router\Router; +use App\Entity\Actor; +use App\Entity\Feed; +use App\Entity\LocalUser; +use App\Util\Nickname; +use Plugin\ActorCircles\Controller as C; +use Plugin\ActorCircles\Entity as E; +use Symfony\Component\HttpFoundation\Request; + +class ActorCircles extends Collection +{ + protected string $slug = 'circle'; + protected string $plural_slug = 'circles'; + + private function getActorIdFromVars(array $vars): int + { + $id = $vars['request']->get('id', null); + if ($id) { + return (int) $id; + } + $nick = $vars['request']->get('nickname'); + $actor = DB::findOneBy(Actor::class, ['nickname' => $nick]); + return $actor->getId(); + } + protected function createCollection(Actor $owner, array $vars, string $name) + { + $actor_id = $this->getActorIdFromVars($vars); + $col = E\ActorCircles::create([ + 'name' => $name, + 'actor_id' => $owner->getId(), + ]); + DB::persist($col); + DB::persist(E\ActorCirclesEntry::create([ + 'actor_id' => $actor_id, + 'circle_id' => $col->getId(), + ])); + } + protected function removeItems(Actor $owner, array $vars, $items, array $collections) + { + $actor_id = $this->getActorIdFromVars($vars); + // can only delete what you own + $items = array_filter($items, fn ($x) => \in_array($x, $collections)); + DB::dql(<<<'EOF' + DELETE FROM \Plugin\ActorCircles\Entity\ActorCirclesEntry AS entry + WHERE entry.actor_id = :actor_id AND entry.circle_id IN (:ids) + EOF, [ + 'actor_id' => $actor_id, + 'ids' => $items, + ]); + } + protected function addItems(Actor $owner, array $vars, $items, array $collections) + { + $actor_id = $this->getActorIdFromVars($vars); + foreach ($items as $id) { + // prevent user from putting something in a collection (s)he doesn't own: + if (\in_array($id, $collections)) { + DB::persist(E\ActorCirclesEntry::create([ + 'actor_id' => $actor_id, + 'circle_id' => $id, + ])); + } + } + } + protected function shouldAddToRightPanel(Actor $user, $vars, Request $request): bool + { + return + $vars['path'] === 'actor_view_nickname' + || $vars['path'] === 'actor_view_id' + || $vars['path'] === 'group_actor_view_nickname' + || $vars['path'] === 'group_actor_view_id'; + } + protected function getCollectionsBy(Actor $owner, ?array $vars = null, bool $ids_only = false): array + { + if (\is_null($vars)) { + $res = DB::findBy(E\ActorCircles::class, ['actor_id' => $owner->getId()]); + } else { + $actor_id = $this->getActorIdFromVars($vars); + $res = DB::dql( + <<<'EOF' + SELECT entry.circle_id FROM \Plugin\ActorCircles\Entity\ActorCirclesEntry AS entry + INNER JOIN \Plugin\ActorCircles\Entity\ActorCircles AS circle + WITH circle.id = entry.circle_id + WHERE circle.actor_id = :owner_id AND entry.actor_id = :actor_id + EOF, + [ + 'owner_id' => $owner->getId(), + 'actor_id' => $actor_id, + ], + ); + } + if (!$ids_only) { + return $res; + } + return array_map(fn ($x) => $x['circle_id'], $res); + } + + public function onAddRoute(RouteLoader $r): bool + { + // View all circles by actor id and nickname + $r->connect( + id: 'actor_circles_view_by_actor_id', + uri_path: '/actor/{id<\d+>}/circles', + target: [C\Controller::class, 'collectionsViewByActorId'], + ); + $r->connect( + id: 'actor_circles_view_by_nickname', + uri_path: '/@{nickname<' . Nickname::DISPLAY_FMT . '>}/circles', + target: [C\Controller::class, 'collectionsByActorNickname'], + ); + // View notes from a circle by actor id and nickname + $r->connect( + id: 'actor_circles_notes_view_by_actor_id', + uri_path: '/actor/{id<\d+>}/circles/{cid<\d+>}', + target: [C\Controller::class, 'collectionNotesViewByActorId'], + ); + $r->connect( + id: 'actor_circles_notes_view_by_nickname', + uri_path: '/@{nickname<' . Nickname::DISPLAY_FMT . '>}/circles/{cid<\d+>}', + target: [C\Controller::class, 'collectionNotesByNickname'], + ); + return Event::next; + } + public function onCreateDefaultFeeds(int $actor_id, LocalUser $user, int &$ordering) + { + DB::persist(Feed::create([ + 'actor_id' => $actor_id, + 'url' => Router::url($route = 'actor_circles_view_by_nickname', ['nickname' => $user->getNickname()]), + 'route' => $route, + 'title' => _m('Circles'), + 'ordering' => $ordering++, + ])); + return Event::next; + } +} diff --git a/plugins/ActorCircles/Controller/Controller.php b/plugins/ActorCircles/Controller/Controller.php new file mode 100644 index 0000000000..42a2bdea4a --- /dev/null +++ b/plugins/ActorCircles/Controller/Controller.php @@ -0,0 +1,84 @@ +. + +// }}} + +namespace Plugin\ActorCircles\Controller; + +use App\Core\Controller\CollectionController; +use App\Core\DB\DB; +use App\Core\Router\Router; +use Plugin\ActorCircles\Entity\ActorCircles; + +class Controller extends CollectionController +{ + protected string $slug = 'circle'; + protected string $plural_slug = 'circles'; + protected string $page_title = 'Actor circles'; + + public function createCollection(int $owner_id, string $name) + { + DB::persist(ActorCircles::create([ + 'name' => $name, + 'actor_id' => $owner_id, + ])); + } + public function getCollectionUrl(int $owner_id, ?string $owner_nickname, int $collection_id): string + { + if (\is_null($owner_nickname)) { + return Router::url( + 'actor_circles_notes_view_by_actor_id', + ['id' => $owner_id, 'cid' => $collection_id], + ); + } + return Router::url( + 'actor_circles_notes_view_by_nickname', + ['nickname' => $owner_nickname, 'cid' => $collection_id], + ); + } + public function getCollectionItems(int $owner_id, $collection_id): array + { + $notes = DB::dql( + <<<'EOF' + SELECT n FROM \App\Entity\Note as n WHERE n.actor_id in ( + SELECT entry.actor_id FROM \Plugin\ActorCircles\Entity\ActorCirclesEntry as entry + inner join \Plugin\ActorCircles\Entity\ActorCircles as ac + with ac.id = entry.circle_id + WHERE ac.id = :circle_id + ) + ORDER BY n.created DESC, n.id DESC + EOF, + ['circle_id' => $collection_id], + ); + return [ + '_template' => 'feed/feed.html.twig', + 'notes' => array_values($notes), + ]; + } + public function getCollectionsBy(int $owner_id): array + { + return DB::findBy(ActorCircles::class, ['actor_id' => $owner_id], order_by: ['id' => 'desc']); + } + public function getCollectionBy(int $owner_id, int $collection_id): ActorCircles + { + return DB::findOneBy(ActorCircles::class, ['id' => $collection_id]); + } +} diff --git a/plugins/ActorCircles/Entity/ActorCircles.php b/plugins/ActorCircles/Entity/ActorCircles.php new file mode 100644 index 0000000000..592d9926b7 --- /dev/null +++ b/plugins/ActorCircles/Entity/ActorCircles.php @@ -0,0 +1,65 @@ +id = $id; + return $this; + } + + public function getId(): int + { + return $this->id; + } + + public function setName(string $name): self + { + $this->name = mb_substr($name, 0, 255); + return $this; + } + + public function getName(): string + { + return $this->name; + } + + public function setActorId(int $actor_id): self + { + $this->actor_id = $actor_id; + return $this; + } + + public function getActorId(): int + { + return $this->actor_id; + } + + // @codeCoverageIgnoreEnd + // }}} Autocode + public static function schemaDef() + { + return [ + 'name' => 'actor_circles_a', + 'fields' => [ + 'id' => ['type' => 'serial', 'not null' => true, 'description' => 'unique identifier'], + 'name' => ['type' => 'varchar', 'length' => 255, 'not null' => true, 'description' => 'collection\'s name'], + 'actor_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to many', 'not null' => true, 'description' => 'foreign key to actor table'], + ], + 'primary key' => ['id'], + ]; + } +} diff --git a/plugins/ActorCircles/Entity/ActorCirclesEntry.php b/plugins/ActorCircles/Entity/ActorCirclesEntry.php new file mode 100644 index 0000000000..859d10f4f1 --- /dev/null +++ b/plugins/ActorCircles/Entity/ActorCirclesEntry.php @@ -0,0 +1,66 @@ +id = $id; + return $this; + } + + public function getId(): int + { + return $this->id; + } + + public function setActorId(int $actor_id): self + { + $this->actor_id = $actor_id; + return $this; + } + + public function getActorId(): int + { + return $this->actor_id; + } + + public function setCircleId(int $circle_id): self + { + $this->circle_id = $circle_id; + return $this; + } + + public function getCircleId(): int + { + return $this->circle_id; + } + + // @codeCoverageIgnoreEnd + // }}} Autocode + + public static function schemaDef() + { + return [ + 'name' => 'actor_circles_entry', + 'fields' => [ + 'id' => ['type' => 'serial', 'not null' => true, 'description' => 'unique identifier'], + 'actor_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'foreign key to attachment table'], + 'circle_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'actor_circles_a.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'foreign key to collection table'], + ], + 'primary key' => ['id'], + ]; + } +}