. // }}} namespace Component\Circle; use App\Core\Cache; use App\Core\DB; use App\Core\Event; use function App\Core\I18n\_m; use App\Core\Modules\Component; use App\Core\Router; use App\Entity\Actor; use App\Entity\Feed; use App\Entity\LocalUser; use App\Util\Nickname; use Component\Circle\Controller as CircleController; use Component\Circle\Entity\ActorCircle; use Component\Circle\Entity\ActorCircleSubscription; use Component\Circle\Entity\ActorTag; use Component\Collection\Util\MetaCollectionTrait; use Component\Tag\Tag; use EventResult; use Functional as F; use Symfony\Component\HttpFoundation\Request; /** * Component responsible for handling and representing ActorCircles and ActorTags * * @author Hugo Sales * @author Phablulo * @author Diogo Peralta Cordeiro <@diogo.site> * @copyright 2021 Free Software Foundation, Inc http://www.fsf.org * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later */ class Circle extends Component { /** @phpstan-use MetaCollectionTrait */ use MetaCollectionTrait; public const TAG_CIRCLE_REGEX = '/' . Nickname::BEFORE_MENTIONS . '@#([\pL\pN_\-\.]{1,64})/'; protected const SLUG = 'circle'; protected const PLURAL_SLUG = 'circles'; public function onAddRoute(Router $r): EventResult { $r->connect('actor_circle_view_by_circle_id', '/circle/{circle_id<\d+>}', [CircleController\Circle::class, 'circleById']); // View circle members by (tagger id or nickname) and tag $r->connect('actor_circle_view_by_circle_tagger_tag', '/circle/actor/{tagger_id<\d+>/{tag<' . Tag::TAG_SLUG_REGEX . '>}}', [CircleController\Circle::class, 'circleByTaggerIdAndTag']); $r->connect('actor_circle_view_by_circle_tagger_tag', '/circle/@{nickname<' . Nickname::DISPLAY_FMT . '>}/{tag<' . Tag::TAG_SLUG_REGEX . '>}', [CircleController\Circle::class, 'circleByTaggerNicknameAndTag']); // View all circles by actor id or nickname $r->connect( id: 'actor_circles_view_by_actor_id', uri_path: '/actor/{tag<' . Tag::TAG_SLUG_REGEX . '>}/circles', target: [CircleController\Circles::class, 'collectionsViewByActorId'], ); $r->connect( id: 'actor_circles_view_by_nickname', uri_path: '/@{nickname<' . Nickname::DISPLAY_FMT . '>}/circles', target: [CircleController\Circles::class, 'collectionsViewByActorNickname'], ); $r->connect('actor_circle_view_feed_by_circle_id', '/circle/{circle_id<\d+>}/feed', [CircleController\Circles::class, 'feedByCircleId']); // View circle feed by (tagger id or nickname) and tag $r->connect('actor_circle_view_feed_by_circle_tagger_tag', '/circle/actor/{tagger_id<\d+>/{tag<' . Tag::TAG_SLUG_REGEX . '>}}/feed', [CircleController\Circles::class, 'feedByTaggerIdAndTag']); $r->connect('actor_circle_view_feed_by_circle_tagger_tag', '/circle/@{nickname<' . Nickname::DISPLAY_FMT . '>}/{tag<' . Tag::TAG_SLUG_REGEX . '>}/feed', [CircleController\Circles::class, 'feedByTaggerNicknameAndTag']); return Event::next; } public static function cacheKeys(string $tag_single_or_multi): array { return [ 'actor_single' => "actor-tag-feed-{$tag_single_or_multi}", 'actor_multi' => "actor-tags-feed-{$tag_single_or_multi}", ]; } public function onPopulateSettingsTabs(Request $request, string $section, array &$tabs): EventResult { if ($section === 'profile' && \in_array($request->get('_route'), ['person_actor_settings', 'group_actor_settings'])) { $tabs[] = [ 'title' => _m('Self tags'), 'desc' => _m('Add or remove tags to this actor'), 'id' => 'settings-self-tags', 'controller' => CircleController\SelfTagsSettings::settingsSelfTags($request, Actor::getById((int) $request->get('id')), 'settings-self-tags-details'), ]; } return Event::next; } /** * @param Actor[] $targets */ public function onPostingFillTargetChoices(Request $request, Actor $actor, array &$targets): EventResult { $circles = $actor->getCircles(); foreach ($circles as $circle) { $tag = $circle->getTag(); $targets["#{$tag}"] = $tag; } return Event::next; } // Meta Collection ------------------------------------------------------------------- /** * @param array $vars */ private function getActorIdFromVars(array $vars): int { $id = $vars['request']->get('id', null); if ($id) { return (int) $id; } $nick = $vars['request']->get('nickname'); $user = LocalUser::getByNickname($nick); return $user->getId(); } public static function createCircle(Actor|int $tagger_id, string $tag): int|null { $tagger_id = \is_int($tagger_id) ? $tagger_id : $tagger_id->getId(); $circle = ActorCircle::create([ 'tagger' => $tagger_id, 'tag' => $tag, 'description' => null, // TODO 'private' => false, // TODO ]); DB::persist($circle); Cache::delete(Actor::cacheKeys($tagger_id)['circles']); return $circle->getId(); } /** * @param array $vars */ protected function createCollection(Actor $owner, array $vars, string $name): void { $this->createCircle($owner, $name); DB::persist(ActorTag::create([ 'tagger' => $owner->getId(), 'tagged' => self::getActorIdFromVars($vars), 'tag' => $name, ])); } /** * @param array $vars * @param array $items * @param array $collections */ protected function removeItem(Actor $owner, array $vars, array $items, array $collections): bool { $tagger_id = $owner->getId(); $tagged_id = $this->getActorIdFromVars($vars); $circles_to_remove_tagged_from = DB::findBy(ActorCircle::class, ['id' => $items]); foreach ($circles_to_remove_tagged_from as $circle) { DB::removeBy(ActorCircleSubscription::class, ['actor_id' => $tagged_id, 'circle_id' => $circle->getId()]); } $tags = F\map($circles_to_remove_tagged_from, fn ($x) => $x->getTag()); foreach ($tags as $tag) { DB::removeBy(ActorTag::class, ['tagger' => $tagger_id, 'tagged' => $tagged_id, 'tag' => $tag]); } Cache::delete(Actor::cacheKeys($tagger_id)['circles']); return true; } /** * @param array $vars * @param array $items * @param array $collections */ protected function addItem(Actor $owner, array $vars, array $items, array $collections): void { $tagger_id = $owner->getId(); $tagged_id = $this->getActorIdFromVars($vars); $circles_to_add_tagged_to = DB::findBy(ActorCircle::class, ['id' => $items]); foreach ($circles_to_add_tagged_to as $circle) { DB::persist(ActorCircleSubscription::create(['actor_id' => $tagged_id, 'circle_id' => $circle->getId()])); } $tags = F\map($circles_to_add_tagged_to, fn ($x) => $x->getTag()); foreach ($tags as $tag) { DB::persist(ActorTag::create(['tagger' => $tagger_id, 'tagged' => $tagged_id, 'tag' => $tag])); } Cache::delete(Actor::cacheKeys($tagger_id)['circles']); } /** * @see MetaCollectionPlugin->shouldAddToRightPanel * * @param array $vars */ protected function shouldAddToRightPanel(Actor $user, array $vars, Request $request): bool { return \in_array($vars['path'], ['actor_view_nickname', 'actor_view_id']); } /** * Retrieves an array of Collections owned by an Actor. * In this case, Collections of those within Actor's own circle of Actors, aka ActorCircle. * * Differs from the overwritten method in MetaCollectionsTrait, since retrieved Collections come from the $owner * itself, and from every Actor that is a part of its ActorCircle. * * @param Actor $owner the Actor, and by extension its own circle of Actors * @param null|array $vars Page vars sent by AppendRightPanelBlock event * @param bool $ids_only true if only the Collections ids are to be returned * * @return Circle[]|int[] */ protected function getCollectionsBy(Actor $owner, ?array $vars = null, bool $ids_only = false): array { $tagged_id = !\is_null($vars) ? $this->getActorIdFromVars($vars) : null; $circles = \is_null($tagged_id) ? $owner->getCircles() : F\select($owner->getCircles(), function ($x) use ($tagged_id) { foreach ($x->getActorTags() as $at) { if ($at->getTagged() === $tagged_id) { return true; } } return false; }); return $ids_only ? array_map(fn ($x) => $x->getId(), $circles) : $circles; } public function onCreateDefaultFeeds(int $actor_id, LocalUser $user, int &$ordering): EventResult { 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; } }