From 51994406dac549f9db1948e7e8e91b93d7a8d079 Mon Sep 17 00:00:00 2001 From: Diogo Peralta Cordeiro Date: Sat, 27 Nov 2021 04:11:35 +0000 Subject: [PATCH] [CORE][ENTITY] Properly port ProfileTag, ProfileTagSubscription and ProfileList as ActorTag, ActorTagSubscription and ActorCircle --- src/Controller/UserPanel.php | 8 +- src/Core/GSFile.php | 4 +- src/Entity/Actor.php | 192 +++++++++++++----- src/Entity/ActorCircle.php | 42 +++- ...iption.php => ActorCircleSubscription.php} | 38 ++-- src/Entity/ActorTag.php | 4 +- templates/cards/profile/view.html.twig | 2 +- 7 files changed, 206 insertions(+), 84 deletions(-) rename src/Entity/{ActorTagSubscription.php => ActorCircleSubscription.php} (59%) diff --git a/src/Controller/UserPanel.php b/src/Controller/UserPanel.php index 46fe457fde..a92bf207e4 100644 --- a/src/Controller/UserPanel.php +++ b/src/Controller/UserPanel.php @@ -42,11 +42,10 @@ use App\Core\Controller; use App\Core\DB\DB; use App\Core\Event; use App\Core\Form; -use function App\Core\I18n\_m; use App\Core\Log; +use App\Entity\ActorCircle; use App\Entity\ActorLanguage; use App\Entity\Language; -use App\Entity\UserNotificationPrefs; use App\Util\Common; use App\Util\Exception\AuthenticationException; use App\Util\Exception\RedirectException; @@ -55,6 +54,7 @@ use App\Util\Form\ActorArrayTransformer; use App\Util\Form\ArrayTransformer; use App\Util\Form\FormFields; use App\Util\Formatting; +use Component\Notification\Entity\UserNotificationPrefs; use Doctrine\DBAL\Types\Types; use Exception; use Functional as F; @@ -66,6 +66,7 @@ use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\SubmitButton; use Symfony\Component\HttpFoundation\Request; +use function App\Core\I18n\_m; // }}} Imports @@ -110,9 +111,6 @@ class UserPanel extends Controller ]; $extra_step = function ($data, $extra_args) use ($user, $actor) { $user->setNickname($data['nickname']); - if (!$data['full_name'] && !$actor->getFullname()) { - $actor->setFullname($data['nickname']); - } }; return Form::handle($form_definition, $request, $actor, $extra, $extra_step, [['self_tags' => $extra['self_tags']]]); } diff --git a/src/Core/GSFile.php b/src/Core/GSFile.php index 3828e4c1cc..f03da2953c 100644 --- a/src/Core/GSFile.php +++ b/src/Core/GSFile.php @@ -199,11 +199,11 @@ class GSFile * Throw a client exception if the cache key $id doesn't contain * exactly one entry */ - public static function error($except, $id, array $res) + public static function error($exception, $id, array $res) { switch (\count($res)) { case 0: - throw new $except(); + throw new $exception(); case 1: return $res[0]; default: diff --git a/src/Entity/Actor.php b/src/Entity/Actor.php index 35c52cf464..dd85b1d3b4 100644 --- a/src/Entity/Actor.php +++ b/src/Entity/Actor.php @@ -1,6 +1,6 @@ nickname; } - public function setFullname(string $fullname): self + public function setFullname(?string $fullname): self { $this->fullname = $fullname; return $this; @@ -99,7 +101,7 @@ class Actor extends Entity public function getFullname(): ?string { - if (\is_null($this->fullname)) { + if (is_null($this->fullname)) { return null; } return $this->fullname; @@ -245,43 +247,123 @@ class Actor extends Entity public static function getById(int $id): ?self { - return Cache::get('actor-id-' . $id, fn () => DB::find('actor', ['id' => $id])); + return Cache::get('actor-id-' . $id, fn() => DB::find('actor', ['id' => $id])); } public static function getNicknameById(int $id): string { - return Cache::get('actor-nickname-id-' . $id, fn () => self::getById($id)->getNickname()); + return Cache::get('actor-nickname-id-' . $id, fn() => self::getById($id)->getNickname()); } public static function getFullnameById(int $id): ?string { - return Cache::get('actor-fullname-id-' . $id, fn () => self::getById($id)->getFullname()); + return Cache::get('actor-fullname-id-' . $id, fn() => self::getById($id)->getFullname()); } + /** + * Tags attributed to self + * + * @return [ActorCircle] + */ public function getSelfTags(bool $_test_force_recompute = false): array { - return Cache::get( - 'selftags-' . $this->id, - fn () => DB::findBy('actor_tag', ['tagger' => $this->id, 'tagged' => $this->id]), - beta: $_test_force_recompute ? \INF : 1.0, - ); + return $this->getOtherTags(scoped: $this->getId(), _test_force_recompute: $_test_force_recompute); } - public function setSelfTags(array $tags, array $existing): void + /** + * Get tags that other people put on this actor, in reverse-chron order + * + * @param Actor|int|null $scoped Actor we are requesting as: + * - If null = All tags attributed to self by other actors (excludes self tags) + * - If self = Same as getSelfTags + * - otherwise = Tags that $scoped attributed to $this + * @param int|null $offset Offset from latest + * @param int|null $limit Max number to get + * @param bool $_test_force_recompute + * @return [ActorCircle] resulting lists + */ + public function getOtherTags(Actor|int|null $scoped = null, ?int $offset = null, ?int $limit = null, bool $_test_force_recompute = false): array { - $tag_existing = F\map($existing, fn ($pt) => $pt->getTag()); - $tag_to_add = array_diff($tags, $tag_existing); - $tag_to_remove = array_diff($tag_existing, $tags); - $pt_to_remove = F\filter($existing, fn ($pt) => \in_array($pt->getTag(), $tag_to_remove)); - foreach ($tag_to_add as $tag) { - $pt = ActorTag::create(['tagger' => $this->id, 'tagged' => $this->id, 'tag' => $tag]); - DB::persist($pt); + if (is_null($scoped)) { + return Cache::get( + "othertags-{$this->getId()}", + fn() => DB::dql( + <<< EOQ + SELECT circle + FROM App\Entity\ActorTag tag + JOIN App\Entity\ActorCircle circle + WITH + tag.tagger = circle.tagger + AND tag.tag = circle.tag + WHERE tag.tagged = :id + ORDER BY tag.modified DESC, tag.tagged DESC + EOQ, + ['id' => $this->getId()], + ['offset' => $offset, + 'limit' => $limit]) + ); + } else { + $scoped_id = is_int($scoped) ? $scoped : $scoped->getId(); + return Cache::get( + "othertags-{$this->getId()}-by-{$scoped_id}", + fn() => DB::dql( + <<< EOQ + SELECT circle + FROM App\Entity\ActorTag tag + JOIN App\Entity\ActorCircle circle + WITH + tag.tagger = circle.tagger + AND tag.tag = circle.tag + WHERE + tag.tagged = :id + AND ( circle.private != true + OR ( circle.tagger = :scoped + AND circle.private = true + ) + ) + ORDER BY tag.modified DESC, tag.tagged DESC + EOQ, + ['id' => $this->getId(), + 'scoped' => $scoped_id], + ['offset' => $offset, + 'limit' => $limit] + ) + ); } - foreach ($pt_to_remove as $pt) { - DB::persist($pt); - DB::remove($pt); + } + + /** + * @param array $tags array of strings to become self tags + * @param array|null $existing array of existing self tags (actor_circle[]) + * @return $this + * @throws NotFoundException + * @throws \App\Util\Exception\DuplicateFoundException + */ + public function setSelfTags(array $tags, ?array $existing = null): self + { + if (is_null($existing)) { + $existing = $this->getSelfTags(); } - Cache::delete('selftags-' . $this->id); + $existing_actor_circles = F\map($existing, fn($actor_circle) => $actor_circle->getTag()); + $tags_to_add = array_diff($tags, $existing_actor_circles); + $tags_to_remove = array_diff($existing_actor_circles, $tags); + $actor_circles_to_remove = F\filter($existing, fn($actor_circle) => in_array($actor_circle->getTag(), $tags_to_remove)); + foreach ($tags_to_add as $tag) { + $actor_circle = ActorCircle::create(['tagger' => $this->getId(), 'tag' => $tag, 'private' => false]); + $actor_tag = ActorTag::create(['tagger' => $this->id, 'tagged' => $this->id, 'tag' => $tag]); + DB::persist($actor_circle); + DB::persist($actor_tag); + } + foreach ($actor_circles_to_remove as $actor_circle) { + $actor_tag = DB::findOneBy('actor_tag', ['tagger' => $this->getId(), 'tagged' => $this->getId(), 'tag' => $actor_circle->getTag()]); + DB::persist($actor_tag); + DB::remove($actor_tag); + // TODO: use DB::removeBy when implemented + DB::remove(DB::getReference('actor_circle', ['id' => $actor_circle->getId()])); + } + Cache::delete("selftags-{$this->getId()}"); + Cache::delete("othertags-{$this->getId()}-by-{$this->getId()}"); + return $this; } public function getSubscribersCount() @@ -290,9 +372,9 @@ class Actor extends Entity 'subscribers-' . $this->id, function () { return DB::dql( - 'select count(f) from App\Entity\Subscription f where f.subscribed = :subscribed', - ['subscribed' => $this->id], - )[0][1] - 1; // Remove self subscription + 'select count(f) from App\Entity\Subscription f where f.subscribed = :subscribed', + ['subscribed' => $this->id], + )[0][1] - 1; // Remove self subscription }, ); } @@ -303,9 +385,9 @@ class Actor extends Entity 'subscribed-' . $this->id, function () { return DB::dql( - 'select count(f) from App\Entity\Subscription f where f.subscriber = :subscriber', - ['subscriber' => $this->id], - )[0][1] - 1; // Remove self subscription + 'select count(f) from App\Entity\Subscription f where f.subscriber = :subscriber', + ['subscriber' => $this->id], + )[0][1] - 1; // Remove self subscription }, ); } @@ -331,16 +413,16 @@ class Actor extends Entity $nickname = Nickname::normalize($nickname, check_already_used: false); return Cache::get( 'relative-nickname-' . $nickname . '-' . $this->getId(), - fn () => DB::dql( - <<<'EOF' + fn() => DB::dql( + <<<'EOF' select a from actor a where a.id in (select fa.subscribed from subscription fa join actor aa with fa.subscribed = aa.id where fa.subscriber = :actor_id and aa.nickname = :nickname) or a.id in (select fb.subscriber from subscription fb join actor ab with fb.subscriber = ab.id where fb.subscribed = :actor_id and ab.nickname = :nickname) or a.nickname = :nickname EOF, - ['nickname' => $nickname, 'actor_id' => $this->getId()], - ['limit' => 1], - )[0] ?? null, + ['nickname' => $nickname, 'actor_id' => $this->getId()], + ['limit' => 1], + )[0] ?? null, ); } @@ -377,43 +459,43 @@ class Actor extends Entity */ public function getPreferredLanguageChoices(?self $context = null): array { - $id = $context?->getId() ?? $this->getId(); - $key = ActorLanguage::collectionCacheKey($this, $context); + $id = $context?->getId() ?? $this->getId(); + $key = ActorLanguage::collectionCacheKey($this, $context); $langs = Cache::getList( $key, - fn () => DB::dql( + fn() => DB::dql( 'select l from actor_language al join language l with al.language_id = l.id where al.actor_id = :id order by al.ordering ASC', ['id' => $id], ), ) ?: [ Language::getFromLocale(Common::config('site', 'language')), ]; - return array_merge(...F\map($langs, fn ($l) => $l->toChoiceFormat())); + return array_merge(...F\map($langs, fn($l) => $l->toChoiceFormat())); } public static function schemaDef(): array { return [ - 'name' => 'actor', + 'name' => 'actor', 'description' => 'local and remote users, groups and bots are actors, for instance', - 'fields' => [ - 'id' => ['type' => 'serial', 'not null' => true, 'description' => 'unique identifier'], - 'nickname' => ['type' => 'varchar', 'length' => 64, 'not null' => true, 'description' => 'nickname or username'], - 'fullname' => ['type' => 'text', 'description' => 'display name'], - 'roles' => ['type' => 'int', 'not null' => true, 'default' => UserRoles::USER, 'description' => 'Bitmap of permissions this actor has'], - 'homepage' => ['type' => 'text', 'description' => 'identifying URL'], - 'bio' => ['type' => 'text', 'description' => 'descriptive biography'], - 'location' => ['type' => 'text', 'description' => 'physical location'], - 'lat' => ['type' => 'numeric', 'precision' => 10, 'scale' => 7, 'description' => 'latitude'], - 'lon' => ['type' => 'numeric', 'precision' => 10, 'scale' => 7, 'description' => 'longitude'], - 'location_id' => ['type' => 'int', 'description' => 'location id if possible'], + 'fields' => [ + 'id' => ['type' => 'serial', 'not null' => true, 'description' => 'unique identifier'], + 'nickname' => ['type' => 'varchar', 'length' => 64, 'not null' => true, 'description' => 'nickname or username'], + 'fullname' => ['type' => 'text', 'description' => 'display name'], + 'roles' => ['type' => 'int', 'not null' => true, 'default' => UserRoles::USER, 'description' => 'Bitmap of permissions this actor has'], + 'homepage' => ['type' => 'text', 'description' => 'identifying URL'], + 'bio' => ['type' => 'text', 'description' => 'descriptive biography'], + 'location' => ['type' => 'text', 'description' => 'physical location'], + 'lat' => ['type' => 'numeric', 'precision' => 10, 'scale' => 7, 'description' => 'latitude'], + 'lon' => ['type' => 'numeric', 'precision' => 10, 'scale' => 7, 'description' => 'longitude'], + 'location_id' => ['type' => 'int', 'description' => 'location id if possible'], 'location_service' => ['type' => 'int', 'description' => 'service used to obtain location id'], - 'is_local' => ['type' => 'bool', 'not null' => true, 'description' => 'Does this actor have a LocalUser associated'], - 'created' => ['type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was created'], - 'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'], + 'is_local' => ['type' => 'bool', 'not null' => true, 'description' => 'Does this actor have a LocalUser associated'], + 'created' => ['type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was created'], + 'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'], ], 'primary key' => ['id'], - 'indexes' => [ + 'indexes' => [ 'actor_nickname_idx' => ['nickname'], ], 'fulltext indexes' => [ diff --git a/src/Entity/ActorCircle.php b/src/Entity/ActorCircle.php index f8ecefac64..3258288be7 100644 --- a/src/Entity/ActorCircle.php +++ b/src/Entity/ActorCircle.php @@ -21,6 +21,8 @@ declare(strict_types = 1); namespace App\Entity; +use App\Core\Cache; +use App\Core\DB\DB; use App\Core\Entity; use DateTimeInterface; @@ -35,6 +37,7 @@ use DateTimeInterface; * @author Mikael Nordfeldth * @copyright 2009-2014 Free Software Foundation, Inc http://www.fsf.org * @author Hugo Sales + * @author Diogo Peralta Cordeiro <@diogo.site> * @copyright 2020-2021 Free Software Foundation, Inc http://www.fsf.org * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later */ @@ -42,6 +45,7 @@ class ActorCircle extends Entity { // {{{ Autocode // @codeCoverageIgnoreStart + private int $id; private int $tagger; private string $tag; private ?string $description; @@ -49,6 +53,17 @@ class ActorCircle extends Entity private DateTimeInterface $created; private DateTimeInterface $modified; + public function setId(int $id): ActorCircle + { + $this->id = $id; + return $this; + } + + public function getId(): int + { + return $this->id; + } + public function setTagger(int $tagger): self { $this->tagger = $tagger; @@ -118,12 +133,32 @@ class ActorCircle extends Entity // @codeCoverageIgnoreEnd // }}} Autocode + public function getSubscribedActors(?int $offset = null, ?int $limit = null): array + { + return Cache::get( + "circle-{$this->getId()}", + fn() => DB::dql( + <<< EOQ + SELECT actor + FROM App\Entity\Actor actor + JOIN App\Entity\ActorCircleSubscription subscription + WITH actor.id = subscription.actor_id + ORDER BY subscription.created DESC, actor.id DESC + EOQ, + options: + ['offset' => $offset, + 'limit' => $limit] + ) + ); + } + public static function schemaDef(): array { return [ 'name' => 'actor_circle', 'description' => 'a actor can have lists of actors, to separate their feed', 'fields' => [ + 'id' => ['type' => 'serial', 'not null' => true, 'description' => 'unique identifier'], 'tagger' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'many to one', 'name' => 'actor_list_tagger_fkey', 'not null' => true, 'description' => 'user making the tag'], 'tag' => ['type' => 'varchar', 'length' => 64, 'foreign key' => true, 'target' => 'ActorTag.tag', 'multiplicity' => 'many to one', 'not null' => true, 'description' => 'actor tag'], // Join with ActorTag // // so, Doctrine doesn't like that the target is not unique, even though the pair is 'description' => ['type' => 'text', 'description' => 'description of the people tag'], @@ -131,7 +166,7 @@ class ActorCircle extends Entity 'created' => ['type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was created'], 'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'], ], - 'primary key' => ['tagger', 'tag'], + 'primary key' => ['id'], 'indexes' => [ 'actor_list_modified_idx' => ['modified'], 'actor_list_tag_idx' => ['tag'], @@ -139,4 +174,9 @@ class ActorCircle extends Entity ], ]; } + + public function __toString() + { + return $this->getTag(); + } } diff --git a/src/Entity/ActorTagSubscription.php b/src/Entity/ActorCircleSubscription.php similarity index 59% rename from src/Entity/ActorTagSubscription.php rename to src/Entity/ActorCircleSubscription.php index 88f54a9106..daa94304fd 100644 --- a/src/Entity/ActorTagSubscription.php +++ b/src/Entity/ActorCircleSubscription.php @@ -23,7 +23,7 @@ use App\Core\Entity; use DateTimeInterface; /** - * Entity for actor Tag Subscription + * Entity for actor circle subscriptions * * @category DB * @package GNUsocial @@ -33,17 +33,18 @@ use DateTimeInterface; * @author Mikael Nordfeldth * @copyright 2009-2014 Free Software Foundation, Inc http://www.fsf.org * @author Hugo Sales + * @author Diogo Peralta Cordeiro <@diogo.site> * @copyright 2020-2021 Free Software Foundation, Inc http://www.fsf.org * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later */ -class ActorTagSubscription extends Entity +class ActorCircleSubscription extends Entity { // {{{ Autocode // @codeCoverageIgnoreStart private int $actor_id; - private int $actor_tag; - private \DateTimeInterface $created; - private \DateTimeInterface $modified; + private int $circle_id; + private DateTimeInterface $created; + private DateTimeInterface $modified; public function setActorId(int $actor_id): self { @@ -56,15 +57,15 @@ class ActorTagSubscription extends Entity return $this->actor_id; } - public function setActorTag(int $actor_tag): self + public function setCircleid(int $circle_id): self { - $this->actor_tag = $actor_tag; + $this->circle_id = $circle_id; return $this; } - public function getActorTag(): int + public function getCircleid(): int { - return $this->actor_tag; + return $this->circle_id; } public function setCreated(DateTimeInterface $created): self @@ -95,18 +96,17 @@ class ActorTagSubscription extends Entity public static function schemaDef(): array { return [ - 'name' => 'actor_tag_subscription', + 'name' => 'actor_tag_subscription', 'fields' => [ - 'actor_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'name' => 'actor_tag_subscription_actor_id_fkey', 'not null' => true, 'description' => 'foreign key to actor table'], - 'actor_tag' => ['type' => 'int', // 'foreign key' => true, 'target' => 'ActorTag.tag', 'multiplicity' => 'one to one', // tag can't unique, but doctrine doesn't understand this - 'name' => 'actor_tag_subscription_actor_tag_fkey', 'not null' => true, 'description' => 'foreign key to actor_tag', ], - 'created' => ['type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was created'], - 'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'], + 'actor_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'name' => 'actor_circle_subscription_actor_id_fkey', 'not null' => true, 'description' => 'foreign key to actor table'], + 'circle_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'ActorCircle.id', 'multiplicity' => 'one to one', 'name' => 'actor_circle_subscription_actor_circle_fkey', 'not null' => true, 'description' => 'foreign key to actor_circle'], + 'created' => ['type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was created'], + 'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'], ], - 'primary key' => ['actor_tag_id', 'actor_id'], - 'indexes' => [ - 'actor_tag_subscription_actor_id_idx' => ['actor_id'], - 'actor_tag_subscription_created_idx' => ['created'], + 'primary key' => ['circle_id', 'actor_id'], + 'indexes' => [ + 'actor_circle_subscription_actor_id_idx' => ['actor_id'], + 'actor_circle_subscription_created_idx' => ['created'], ], ]; } diff --git a/src/Entity/ActorTag.php b/src/Entity/ActorTag.php index ea76711d3a..d2c0a557e5 100644 --- a/src/Entity/ActorTag.php +++ b/src/Entity/ActorTag.php @@ -19,6 +19,8 @@ namespace App\Entity; +use App\Core\Cache; +use App\Core\DB\DB; use App\Core\Entity; use DateTimeInterface; @@ -97,7 +99,7 @@ class ActorTag extends Entity return [ 'name' => 'actor_tag', 'fields' => [ - 'tagger' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'name' => 'actor_tag_tagger_fkey', 'not null' => true, 'description' => 'user making the tag'], + 'tagger' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'name' => 'actor_tag_tagger_fkey', 'not null' => true, 'description' => 'actor making the tag'], 'tagged' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'name' => 'actor_tag_tagged_fkey', 'not null' => true, 'description' => 'actor tagged'], 'tag' => ['type' => 'varchar', 'length' => 64, 'not null' => true, 'description' => 'hash tag associated with this notice'], 'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'], diff --git a/templates/cards/profile/view.html.twig b/templates/cards/profile/view.html.twig index 2df024cae5..22572e4044 100644 --- a/templates/cards/profile/view.html.twig +++ b/templates/cards/profile/view.html.twig @@ -29,7 +29,7 @@