[CORE][ENTITY] Properly port ProfileTag, ProfileTagSubscription and ProfileList as ActorTag, ActorTagSubscription and ActorCircle

This commit is contained in:
Diogo Peralta Cordeiro 2021-11-27 04:11:35 +00:00
parent 11d2cfb9ed
commit 51994406da
Signed by: diogo
GPG Key ID: 18D2D35001FBFAB0
7 changed files with 206 additions and 84 deletions

View File

@ -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']]]);
}

View File

@ -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:

View File

@ -1,6 +1,6 @@
<?php
declare(strict_types = 1);
declare(strict_types=1);
// {{{ License
@ -35,6 +35,8 @@ use App\Util\Nickname;
use Component\Avatar\Avatar;
use DateTimeInterface;
use Functional as F;
use function in_array;
use function is_null;
/**
* Entity for actors
@ -57,7 +59,7 @@ class Actor extends Entity
private int $id;
private string $nickname;
private ?string $fullname = null;
private int $roles = 4;
private int $roles = 4;
private ?string $homepage;
private ?string $bio;
private ?string $location;
@ -91,7 +93,7 @@ class Actor extends Entity
return $this->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' => [

View File

@ -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 <mmn@hethane.se>
* @copyright 2009-2014 Free Software Foundation, Inc http://www.fsf.org
* @author Hugo Sales <hugo@hsal.es>
* @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();
}
}

View File

@ -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 <mmn@hethane.se>
* @copyright 2009-2014 Free Software Foundation, Inc http://www.fsf.org
* @author Hugo Sales <hugo@hsal.es>
* @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'],
],
];
}

View File

@ -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'],

View File

@ -29,7 +29,7 @@
<nav class="profile-info-tags">
{% if actor_tags %}
{% for tag in actor_tags %}
<a href='#'><em>#{{ tag }}</em></a>
<a href='#'><em>#{{ tag.getTag() }}</em></a>
{% endfor %}
{% else %}
{{ '(No tags)' | trans }}