diff --git a/src/Entity/Actor.php b/src/Entity/Actor.php index d2815f0445..3f4dc074e0 100644 --- a/src/Entity/Actor.php +++ b/src/Entity/Actor.php @@ -1,5 +1,7 @@ location_service; } + public function setPreferredLangId(?string $preferred_lang_id): self + { + $this->preferred_lang_id = $preferred_lang_id; + return $this; + } + + public function getPreferredLangId(): ?int + { + return $this->preferred_lang_id; + } + public function setCreated(DateTimeInterface $created): self { $this->created = $created; @@ -222,30 +236,26 @@ class Actor extends Entity public static function getById(int $id): ?self { - return Cache::get('actor-id-' . $id, function () use ($id) { - return 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, function () use ($id) { - return 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, function () use ($id) { - return self::getById($id)->getFullname(); - }); + return Cache::get('actor-fullname-id-' . $id, fn () => self::getById($id)->getFullname()); } 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 Cache::get( + 'selftags-' . $this->id, + fn () => DB::findBy('actor_tag', ['tagger' => $this->id, 'tagged' => $this->id]), + beta: $_test_force_recompute ? \INF : 1.0, + ); } public function setSelfTags(array $tags, array $existing): void @@ -253,7 +263,7 @@ class Actor extends Entity $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)); + $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); @@ -267,20 +277,28 @@ class Actor extends Entity public function getSubscribersCount() { - return Cache::get('followers-' . $this->id, - function () { - return DB::dql('select count(f) from App\Entity\Follow f where f.followed = :followed', - ['followed' => $this->id])[0][1] - 1; // Remove self follow - }); + return Cache::get( + 'followers-' . $this->id, + function () { + return DB::dql( + 'select count(f) from App\Entity\Follow f where f.followed = :followed', + ['followed' => $this->id], + )[0][1] - 1; // Remove self follow + }, + ); } public function getSubscriptionsCount() { - return Cache::get('followed-' . $this->id, - function () { - return DB::dql('select count(f) from App\Entity\Follow f where f.follower = :follower', - ['follower' => $this->id])[0][1] - 1; // Remove self follow - }); + return Cache::get( + 'followed-' . $this->id, + function () { + return DB::dql( + 'select count(f) from App\Entity\Follow f where f.follower = :follower', + ['follower' => $this->id], + )[0][1] - 1; // Remove self follow + }, + ); } public function isPerson(): bool @@ -302,14 +320,17 @@ class Actor extends Entity { // Will throw exception on invalid input. $nickname = Nickname::normalize($nickname, check_already_used: false); - return Cache::get('relative-nickname-' . $nickname . '-' . $this->getId(), - fn () => DB::dql('select a from actor a where ' . - 'a.id in (select followed from follow f join actor a on f.followed = a.id where and f.follower = :actor_id and a.nickname = :nickname) or' . - 'a.id in (select follower from follow f join actor a on f.follower = a.id where and f.followed = :actor_id and a.nickname = :nickname) or' . - 'a.nickname = :nickname' . - 'limit 1', - ['nickname' => $nickname, 'actor_id' => $this->getId()] - )); + return Cache::get( + 'relative-nickname-' . $nickname . '-' . $this->getId(), + fn () => DB::dql( + 'select a from actor a where ' + . 'a.id in (select followed from follow f join actor a on f.followed = a.id where and f.follower = :actor_id and a.nickname = :nickname) or' + . 'a.id in (select follower from follow f join actor a on f.follower = a.id where and f.followed = :actor_id and a.nickname = :nickname) or' + . 'a.nickname = :nickname' + . 'limit 1', + ['nickname' => $nickname, 'actor_id' => $this->getId()], + ), + ); } public function getUri(int $type = Router::ABSOLUTE_PATH): string @@ -337,25 +358,32 @@ class Actor extends Entity return $aliases; } + public function getPreferredLanguageChoice() + { + $lang_id = $this->getPreferredLangId(); + return Cache::get("language-{$lang_id}", fn () => (string) DB::findOneBy('language', ['id' => $lang_id])); + } + public static function schemaDef(): array { - $def = [ + return [ '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'], - 'location_service' => ['type' => 'int', 'description' => 'service used to obtain location id'], - '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'], + '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'], + 'preferred_lang_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Language.id', 'multiplicity' => 'one to many', 'description' => 'preferred language'], + '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' => [ @@ -365,7 +393,5 @@ class Actor extends Entity 'actor_fulltext_idx' => ['nickname', 'fullname', 'location', 'bio', 'homepage'], ], ]; - - return $def; } } diff --git a/src/Entity/Language.php b/src/Entity/Language.php index 483871534f..a4e228ecd6 100644 --- a/src/Entity/Language.php +++ b/src/Entity/Language.php @@ -24,6 +24,7 @@ declare(strict_types = 1); namespace App\Entity; use App\Core\Entity; +use DateTimeInterface; /** * Entity for languages @@ -41,7 +42,7 @@ class Language extends Entity // @codeCoverageIgnoreStart private int $id; private string $language; - private \DateTimeInterface $created; + private DateTimeInterface $created; public function setId(int $id): self { @@ -78,6 +79,11 @@ class Language extends Entity // @codeCoverageIgnoreEnd // }}} Autocode + public function __toString() + { + return $this->getLanguage(); + } + public static function schemaDef(): array { return [ @@ -89,7 +95,10 @@ class Language extends Entity 'created' => ['type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was created'], ], 'primary key' => ['id'], - 'indexes' => [ + 'unique keys' => [ + 'language_language_uniq' => ['language'], + ], + 'indexes' => [ 'language_idx' => ['language'], ], ]; diff --git a/src/Entity/LocalUser.php b/src/Entity/LocalUser.php index 3ff4eb89bb..3a4a5bc6ec 100644 --- a/src/Entity/LocalUser.php +++ b/src/Entity/LocalUser.php @@ -57,7 +57,6 @@ class LocalUser extends Entity implements UserInterface private ?string $outgoing_email; private ?string $incoming_email; private ?bool $is_email_verified; - private ?int $preferred_language; private ?string $timezone; private ?PhoneNumber $phone_number; private ?int $sms_carrier; @@ -135,17 +134,6 @@ class LocalUser extends Entity implements UserInterface return $this->is_email_verified; } - public function setPreferredLanguage(?string $preferred_language): self - { - $this->preferred_language = $preferred_language; - return $this; - } - - public function getPreferredLanguage(): ?int - { - return $this->preferred_language; - } - public function setTimezone(?string $timezone): self { $this->timezone = $timezone; @@ -259,25 +247,7 @@ class LocalUser extends Entity implements UserInterface // @codeCoverageIgnoreEnd // }}} Autocode - public function getActor() - { - return DB::find('actor', ['id' => $this->id]); - } - - /** - * Returns the roles granted to the user - */ - public function getRoles() - { - return UserRoles::toArray($this->getActor()->getRoles()); - } - - /** - * Returns the password used to authenticate the user. - * - * Implemented in the auto code - */ - + // {{{ Authentication /** * Returns the salt that was originally used to encode the password. * BCrypt and Argon2 generate their own salts @@ -287,14 +257,6 @@ class LocalUser extends Entity implements UserInterface return null; } - /** - * Returns the username used to authenticate the user. - */ - public function getUsername() - { - return $this->nickname; - } - /** * Removes sensitive data from the user. * @@ -305,19 +267,6 @@ class LocalUser extends Entity implements UserInterface { } - public static function getByNickname(string $nickname): ?self - { - return Cache::get("user-nickname-{$nickname}", fn () => DB::findOneBy('local_user', ['nickname' => $nickname])); - } - - /** - * @return self Returns self if email found - */ - public static function getByEmail(string $email): ?self - { - return Cache::get("user-email-{$email}", fn () => DB::findOneBy('local_user', ['or' => ['outgoing_email' => $email, 'incoming_email' => $email]])); - } - /** * When authenticating, check a user's password in a timing safe * way. Will update the password by rehashing if deemed necessary @@ -380,6 +329,41 @@ class LocalUser extends Entity implements UserInterface throw new Exception('Unsupported or unsafe hashing algorithm requested'); } } + // }}} Authentication + + public function getActor() + { + return DB::find('actor', ['id' => $this->id]); + } + + /** + * Returns the roles granted to the user + */ + public function getRoles() + { + return UserRoles::toArray($this->getActor()->getRoles()); + } + + /** + * Returns the username used to authenticate the user. Part of the Symfony UserInterface + */ + public function getUsername() + { + return $this->nickname; + } + + public static function getByNickname(string $nickname): ?self + { + return Cache::get("user-nickname-{$nickname}", fn () => DB::findOneBy('local_user', ['nickname' => $nickname])); + } + + /** + * @return self Returns self if email found + */ + public static function getByEmail(string $email): ?self + { + return Cache::get("user-email-{$email}", fn () => DB::findOneBy('local_user', ['or' => ['outgoing_email' => $email, 'incoming_email' => $email]])); + } public static function schemaDef(): array { @@ -387,23 +371,22 @@ class LocalUser extends Entity implements UserInterface 'name' => 'local_user', 'description' => 'local users, bots, etc', 'fields' => [ - 'id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'foreign key to actor table'], - 'nickname' => ['type' => 'varchar', 'not null' => true, 'length' => 64, 'description' => 'nickname or username, foreign key to actor'], - 'password' => ['type' => 'varchar', 'length' => 191, 'description' => 'salted password, can be null for users with federated authentication'], - 'outgoing_email' => ['type' => 'varchar', 'length' => 191, 'description' => 'email address for password recovery, notifications, etc.'], - 'incoming_email' => ['type' => 'varchar', 'length' => 191, 'description' => 'email address for post-by-email'], - 'is_email_verified' => ['type' => 'bool', 'default' => false, 'description' => 'Whether the user opened the comfirmation email'], - 'preferred_language' => ['type' => 'int', 'foreign key' => true, 'target' => 'Language.id', 'multiplicity' => 'one to many', 'description' => 'preferred language'], - 'timezone' => ['type' => 'varchar', 'length' => 50, 'description' => 'timezone'], - 'phone_number' => ['type' => 'phone_number', 'description' => 'phone number'], - 'sms_carrier' => ['type' => 'int', 'foreign key' => true, 'target' => 'SmsCarrier.id', 'multiplicity' => 'one to one', 'description' => 'foreign key to sms_carrier'], - 'sms_email' => ['type' => 'varchar', 'length' => 191, 'description' => 'built from sms and carrier (see sms_carrier)'], - 'uri' => ['type' => 'varchar', 'length' => 191, 'description' => 'universally unique identifier, usually a tag URI'], - 'auto_follow_back' => ['type' => 'bool', 'default' => false, 'description' => 'automatically follow users who follow us'], - 'follow_policy' => ['type' => 'int', 'size' => 'tiny', 'default' => 0, 'description' => '0 = anybody can follow; 1 = require approval'], - 'is_stream_private' => ['type' => 'bool', 'default' => false, 'description' => 'whether to limit all notices to followers only'], - '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'], + 'id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'foreign key to actor table'], + 'nickname' => ['type' => 'varchar', 'not null' => true, 'length' => 64, 'description' => 'nickname or username, foreign key to actor'], + 'password' => ['type' => 'varchar', 'length' => 191, 'description' => 'salted password, can be null for users with federated authentication'], + 'outgoing_email' => ['type' => 'varchar', 'length' => 191, 'description' => 'email address for password recovery, notifications, etc.'], + 'incoming_email' => ['type' => 'varchar', 'length' => 191, 'description' => 'email address for post-by-email'], + 'is_email_verified' => ['type' => 'bool', 'default' => false, 'description' => 'Whether the user opened the comfirmation email'], + 'timezone' => ['type' => 'varchar', 'length' => 50, 'description' => 'timezone'], + 'phone_number' => ['type' => 'phone_number', 'description' => 'phone number'], + 'sms_carrier' => ['type' => 'int', 'foreign key' => true, 'target' => 'SmsCarrier.id', 'multiplicity' => 'one to one', 'description' => 'foreign key to sms_carrier'], + 'sms_email' => ['type' => 'varchar', 'length' => 191, 'description' => 'built from sms and carrier (see sms_carrier)'], + 'uri' => ['type' => 'varchar', 'length' => 191, 'description' => 'universally unique identifier, usually a tag URI'], + 'auto_follow_back' => ['type' => 'bool', 'default' => false, 'description' => 'automatically follow users who follow us'], + 'follow_policy' => ['type' => 'int', 'size' => 'tiny', 'default' => 0, 'description' => '0 = anybody can follow; 1 = require approval'], + 'is_stream_private' => ['type' => 'bool', 'default' => false, 'description' => 'whether to limit all notices to followers only'], + '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'], 'unique keys' => [ diff --git a/src/Entity/Note.php b/src/Entity/Note.php index 569d280db0..6c5f3578a8 100644 --- a/src/Entity/Note.php +++ b/src/Entity/Note.php @@ -288,7 +288,7 @@ class Note extends Entity public function getReplies(): array { - return Cache::getList('note-replies-' . $this->id, fn () => DB::dql('select n from App\Entity\Note n where n.reply_to = :id', ['id' => $this->id])); + return Cache::getList('note-replies-' . $this->id, fn () => DB::dql('select n from note n where n.reply_to = :id', ['id' => $this->id])); } public function getReplyToNickname(): ?string @@ -343,7 +343,7 @@ class Note extends Entity 'id' => ['type' => 'serial', 'not null' => true], 'actor_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'who made the note'], 'content' => ['type' => 'text', 'description' => 'note content'], - 'content_type' => ['type' => 'varchar', 'default' => 'text/plain', 'length' => 129, 'description' => 'A note can be written in a multitude of formats such as text/plain, text/markdown, application/x-latex, and text/html'], + 'content_type' => ['type' => 'varchar', 'not null' => true, 'default' => 'text/plain', 'length' => 129, 'description' => 'A note can be written in a multitude of formats such as text/plain, text/markdown, application/x-latex, and text/html'], 'rendered' => ['type' => 'text', 'description' => 'rendered note content, so we can keep the microtags (if not local)'], 'reply_to' => ['type' => 'int', 'foreign key' => true, 'target' => 'Note.id', 'multiplicity' => 'one to one', 'description' => 'note replied to, null if root of a conversation'], 'is_local' => ['type' => 'bool', 'description' => 'was this note generated by a local actor'],