From a40f503fddc9f4b0b7619388e47a7f6d7eaaaff3 Mon Sep 17 00:00:00 2001 From: Philipp Date: Mon, 18 Oct 2021 22:12:49 +0200 Subject: [PATCH] Move Introduction to new depository paradigm --- doc/Developer-Domain-Driven-Design.md | 9 +- .../Introduction/Collection/Introductions.php | 9 + .../Introduction/Depository/Introduction.php | 168 ++++++++++++++++++ .../Introduction/Entity/Introduction.php | 98 ++++++++++ .../IntroductionNotFoundException.php | 11 ++ .../IntroductionPersistenceException.php | 11 ++ .../Introduction/Factory/Introduction.php} | 29 ++- src/DI.php | 4 +- src/Factory/Api/Mastodon/FollowRequest.php | 4 +- src/Model/{ => Contact}/Introduction.php | 93 +++------- src/Module/Api/Mastodon/FollowRequests.php | 29 ++- src/Module/FollowConfirm.php | 6 +- src/Module/Notifications/Notification.php | 9 +- src/Repository/Introduction.php | 79 -------- 14 files changed, 380 insertions(+), 179 deletions(-) create mode 100644 src/Contact/Introduction/Collection/Introductions.php create mode 100644 src/Contact/Introduction/Depository/Introduction.php create mode 100644 src/Contact/Introduction/Entity/Introduction.php create mode 100644 src/Contact/Introduction/Exception/IntroductionNotFoundException.php create mode 100644 src/Contact/Introduction/Exception/IntroductionPersistenceException.php rename src/{Collection/Introductions.php => Contact/Introduction/Factory/Introduction.php} (53%) rename src/Model/{ => Contact}/Introduction.php (60%) delete mode 100644 src/Repository/Introduction.php diff --git a/doc/Developer-Domain-Driven-Design.md b/doc/Developer-Domain-Driven-Design.md index b8d886aa0d..1e77dd2f0b 100644 --- a/doc/Developer-Domain-Driven-Design.md +++ b/doc/Developer-Domain-Driven-Design.md @@ -36,17 +36,18 @@ doSomething($intros); ``` After: + ```php -function doSomething(\Friendica\Collection\Introductions $intros) +function doSomething(\Friendica\Contact\Introductions\Collection\Introductions $intros) { foreach ($intros as $intro) { - /** @var $intro \Friendica\Model\Introduction */ + /** @var $intro \Friendica\Contact\Introductions\Entity\Introduction */ $introId = $intro->id; } } -/** @var $intros \Friendica\Collection\Introductions */ -$intros = \Friendica\DI::intro()->select(['uid' => local_user()]); +/** @var $intros \Friendica\Contact\Introductions\Collection\Introductions */ +$intros = \Friendica\DI::intro()->selecForUser(local_user()); doSomething($intros); ``` diff --git a/src/Contact/Introduction/Collection/Introductions.php b/src/Contact/Introduction/Collection/Introductions.php new file mode 100644 index 0000000000..4df89cee43 --- /dev/null +++ b/src/Contact/Introduction/Collection/Introductions.php @@ -0,0 +1,9 @@ +. + * + */ + +namespace Friendica\Contact\Introduction\Depository; + +use Friendica\BaseDepository; +use Friendica\Contact\Introduction\Exception\IntroductionNotFoundException; +use Friendica\Contact\Introduction\Exception\IntroductionPersistenceException; +use Friendica\Contact\Introduction\Collection; +use Friendica\Contact\Introduction\Entity; +use Friendica\Contact\Introduction\Factory; +use Friendica\Database\Database; +use Friendica\Network\HTTPException\NotFoundException; +use Friendica\Util\DateTimeFormat; +use Psr\Log\LoggerInterface; + +class Introduction extends BaseDepository +{ + /** @var Factory\Introduction */ + protected $factory; + + protected static $table_name = 'intro'; + + public function __construct(Database $database, LoggerInterface $logger, Factory\Introduction $factory) + { + parent::__construct($database, $logger, $factory); + } + + /** + * @param array $condition + * @param array $params + * + * @return Entity\Introduction + * + * @throws NotFoundException the underlying exception if there's no Introduction with the given conditions + */ + private function selectOne(array $condition, array $params = []): Entity\Introduction + { + return parent::_selectOne($condition, $params); + } + + /** + * Converts a given Introduction into a DB compatible row array + * + * @param Entity\Introduction $introduction + * + * @return array + */ + protected function convertToTableRow(Entity\Introduction $introduction): array + { + return [ + 'uid' => $introduction->uid, + 'fid' => $introduction->fid, + 'contact-id' => $introduction->cid, + 'suggest-cid' => $introduction->sid, + 'knowyou' => $introduction->knowyou ? 1 : 0, + 'duplex' => $introduction->duplex ? 1 : 0, + 'note' => $introduction->note, + 'hash' => $introduction->hash, + 'blocked' => $introduction->blocked ? 1 : 0, + 'ignore' => $introduction->ignore ? 1 : 0 + ]; + } + + /** + * @param int $id + * @param int $uid + * + * @return Entity\Introduction + * + * @throws IntroductionNotFoundException in case there is no Introduction with this id + */ + public function selectOneById(int $id, int $uid): Entity\Introduction + { + try { + return $this->selectOne(['id' => $id, 'uid' => $uid]); + } catch (NotFoundException $exception) { + throw new IntroductionNotFoundException(sprintf('There is no Introduction with the ID %d for the user %d', $id, $uid), $exception); + } + } + + /** + * Selects introductions for a given user + * + * @param int $uid + * @param int|null $min_id + * @param int|null $max_id + * @param int $limit + * + * @return Collection\Introductions + */ + public function selectForUser(int $uid, int $min_id = null, int $max_id = null, int $limit = self::LIMIT): Collection\Introductions + { + try { + $BaseCollection = parent::_selectByBoundaries( + ['`uid = ?` AND NOT `ignore`',$uid], + ['order' => ['id' => 'DESC']], + $min_id, $max_id, $limit); + } catch (\Exception $e) { + throw new IntroductionPersistenceException(sprintf('Cannot select Introductions for used %d', $uid), $e); + } + + return new Collection\Introductions($BaseCollection->getArrayCopy(), $BaseCollection->getTotalCount()); + } + + /** + * @param Entity\Introduction $introduction + * + * @return bool + * + * @throws IntroductionPersistenceException in case the underlying storage cannot delete the Introduction + */ + public function delete(Entity\Introduction $introduction): bool + { + if (!$introduction->id) { + return false; + } + + try { + return $this->db->delete(self::$table_name, ['id' => $introduction->id]); + } catch (\Exception $e) { + throw new IntroductionPersistenceException(sprintf('Cannot delete Introduction with id %d', $introduction->id), $e); + } + } + + /** + * @param Entity\Introduction $introduction + * + * @return Entity\Introduction + * + * @throws IntroductionPersistenceException In case the underlying storage cannot save the Introduction + */ + public function save(Entity\Introduction $introduction): Entity\Introduction + { + try { + $fields = $this->convertToTableRow($introduction); + $fields['datetime'] = DateTimeFormat::utcNow(); + + if ($introduction->id) { + $this->db->update(self::$table_name, $fields, ['id' => $introduction->id]); + return $this->factory->createFromTableRow($fields); + } else { + $this->db->insert(self::$table_name, $fields); + return $this->selectOneById($this->db->lastInsertId(), $introduction->uid); + } + } catch (\Exception $exception) { + throw new IntroductionPersistenceException(sprintf('Cannot insert/update the Introduction %d for user %d', $introduction->id, $introduction->uid), $exception); + } + } +} diff --git a/src/Contact/Introduction/Entity/Introduction.php b/src/Contact/Introduction/Entity/Introduction.php new file mode 100644 index 0000000000..b9842a8429 --- /dev/null +++ b/src/Contact/Introduction/Entity/Introduction.php @@ -0,0 +1,98 @@ +. + * + */ + +namespace Friendica\Contact\Introduction\Entity; + +use Friendica\BaseEntity; + +/** + * @property-read int $uid + * @property-read int $fid + * @property-read int $cid + * @property-read int $sid + * @property-read bool $knowyou + * @property-read bool $duplex + * @property-read string $note + * @property-read string $hash + * @property-read \DateTime $datetime + * @property-read bool $blocked + * @property-read bool $ignore + * @property-read int|null $id + */ +class Introduction extends BaseEntity +{ + /** @var int */ + protected $uid; + /** @var int */ + protected $fid; + /** @var int */ + protected $cid; + /** @var int */ + protected $sid; + /** @var bool */ + protected $knowyou; + /** @var bool */ + protected $duplex; + /** @var string */ + protected $note; + /** @var string */ + protected $hash; + /** @var \DateTime */ + protected $datetime; + /** @var bool */ + protected $blocked; + /** @var bool */ + protected $ignore; + /** @var int|null */ + protected $id; + + /** + * @param int $uid + * @param int $fid + * @param int $cid + * @param bool $knowyou + * @param bool $duplex + * @param string $note + * @param string $hash + * @param bool $blocked + * @param bool $ignore + * @param int|null $id + */ + public function __construct(int $uid, int $fid, int $cid, int $sid, bool $knowyou, bool $duplex, string $note, string $hash, \DateTime $datetime, bool $blocked, bool $ignore, ?int $id) + { + $this->uid = $uid; + $this->fid = $fid; + $this->cid = $cid; + $this->sid = $sid; + $this->knowyou = $knowyou; + $this->duplex = $duplex; + $this->note = $note; + $this->hash = $hash; + $this->blocked = $blocked; + $this->ignore = $ignore; + $this->id = $id; + } + + public function setIgnore() + { + $this->ignore = true; + } +} diff --git a/src/Contact/Introduction/Exception/IntroductionNotFoundException.php b/src/Contact/Introduction/Exception/IntroductionNotFoundException.php new file mode 100644 index 0000000000..f48c9ea22c --- /dev/null +++ b/src/Contact/Introduction/Exception/IntroductionNotFoundException.php @@ -0,0 +1,11 @@ +create(Repository\Introduction::class); + return self::$dice->create(Contact\Introduction\Depository\Introduction::class); } public static function permissionSet(): Security\PermissionSet\Depository\PermissionSet diff --git a/src/Factory/Api/Mastodon/FollowRequest.php b/src/Factory/Api/Mastodon/FollowRequest.php index f2be0e589a..1d90c55f04 100644 --- a/src/Factory/Api/Mastodon/FollowRequest.php +++ b/src/Factory/Api/Mastodon/FollowRequest.php @@ -23,9 +23,9 @@ namespace Friendica\Factory\Api\Mastodon; use Friendica\App\BaseURL; use Friendica\BaseFactory; +use Friendica\Contact\Introduction\Entity\Introduction; use Friendica\Model\APContact; use Friendica\Model\Contact; -use Friendica\Model\Introduction; use Friendica\Network\HTTPException; use ImagickException; use Psr\Log\LoggerInterface; @@ -49,7 +49,7 @@ class FollowRequest extends BaseFactory */ public function createFromIntroduction(Introduction $introduction): \Friendica\Object\Api\Mastodon\FollowRequest { - $cdata = Contact::getPublicAndUserContactID($introduction->{'contact-id'}, $introduction->uid); + $cdata = Contact::getPublicAndUserContactID($introduction->cid, $introduction->uid); if (empty($cdata)) { $this->logger->warning('Wrong introduction data', ['Introduction' => $introduction]); diff --git a/src/Model/Introduction.php b/src/Model/Contact/Introduction.php similarity index 60% rename from src/Model/Introduction.php rename to src/Model/Contact/Introduction.php index aa71891214..692ba7befe 100644 --- a/src/Model/Introduction.php +++ b/src/Model/Contact/Introduction.php @@ -19,64 +19,41 @@ * */ -namespace Friendica\Model; +namespace Friendica\Model\Contact; -use Friendica\BaseModel; +use Friendica\Contact\Introduction\Entity; use Friendica\Core\Protocol; -use Friendica\Database\Database; +use Friendica\DI; use Friendica\Network\HTTPException; +use Friendica\Model\Contact; +use Friendica\Model\User; use Friendica\Protocol\ActivityPub; use Friendica\Protocol\Diaspora; -use Friendica\Repository; use Friendica\Util\DateTimeFormat; -use Psr\Log\LoggerInterface; -/** - * @property int uid - * @property int fid - * @property int contact-id - * @property bool knowyou - * @property bool duplex - * @property string note - * @property string hash - * @property string datetime - * @property bool blocked - * @property bool ignore - */ -class Introduction extends BaseModel +class Introduction { - /** @var Repository\Introduction */ - protected $intro; - - public function __construct(Database $dba, LoggerInterface $logger, Repository\Introduction $intro, array $data = []) - { - parent::__construct($dba, $logger, $data); - - $this->intro = $intro; - } - /** * Confirms a follow request and sends a notice to the remote contact. * - * @param bool $duplex Is it a follow back? - * @param bool|null $hidden Should this contact be hidden? null = no change - * @return bool + * @param Entity\Introduction $introduction + * * @throws HTTPException\InternalServerErrorException * @throws HTTPException\NotFoundException * @throws \ImagickException */ - public function confirm(bool $duplex = false, bool $hidden = null) + public static function confirm(Entity\Introduction $introduction): void { - $this->logger->info('Confirming follower', ['cid' => $this->{'contact-id'}]); + DI::logger()->info('Confirming follower', ['cid' => $introduction->cid]); - $contact = Contact::selectFirst([], ['id' => $this->{'contact-id'}, 'uid' => $this->uid]); + $contact = Contact::selectFirst([], ['id' => $introduction->cid, 'uid' => $introduction->uid]); if (!$contact) { throw new HTTPException\NotFoundException('Contact record not found.'); } $newRelation = $contact['rel']; - $writable = $contact['writable']; + $writable = $contact['writable']; if (!empty($contact['protocol'])) { $protocol = $contact['protocol']; @@ -89,7 +66,7 @@ class Introduction extends BaseModel } if (in_array($protocol, [Protocol::DIASPORA, Protocol::ACTIVITYPUB])) { - if ($duplex) { + if ($introduction->duplex) { $newRelation = Contact::FRIEND; } else { $newRelation = Contact::FOLLOWER; @@ -117,53 +94,41 @@ class Introduction extends BaseModel if ($newRelation == Contact::FRIEND) { if ($protocol == Protocol::DIASPORA) { $ret = Diaspora::sendShare(User::getById($contact['uid']), $contact); - $this->logger->info('share returns', ['return' => $ret]); + DI::logger()->info('share returns', ['return' => $ret]); } elseif ($protocol == Protocol::ACTIVITYPUB) { ActivityPub\Transmitter::sendActivity('Follow', $contact['url'], $contact['uid']); } } - - return $this->intro->delete($this); - } - - /** - * Silently ignores the introduction, hides it from notifications and prevents the remote contact from submitting - * additional follow requests. - * - * @return bool - * @throws \Exception - */ - public function ignore() - { - $this->ignore = true; - - return $this->intro->update($this); } /** * Discards the introduction and sends a rejection message to AP contacts. * - * @return bool + * @param Entity\Introduction $introduction + * * @throws HTTPException\InternalServerErrorException - * @throws HTTPException\NotFoundException * @throws \ImagickException */ - public function discard() + public static function discard(Entity\Introduction $introduction): void { // If it is a friend suggestion, the contact is not a new friend but an existing friend // that should not be deleted. - if (!$this->fid) { + if (!$introduction->fid) { // When the contact entry had been created just for that intro, we want to get rid of it now - $condition = ['id' => $this->{'contact-id'}, 'uid' => $this->uid, - 'self' => false, 'pending' => true, 'rel' => [0, Contact::FOLLOWER]]; - if ($this->dba->exists('contact', $condition)) { - Contact::remove($this->{'contact-id'}); + $condition = [ + 'id' => $introduction->cid, + 'uid' => $introduction->uid, + 'self' => false, + 'pending' => true, + 'rel' => [0, Contact::FOLLOWER]]; + if (DI::dba()->exists('contact', $condition)) { + Contact::remove($introduction->cid); } else { - Contact::update(['pending' => false], ['id' => $this->{'contact-id'}]); + Contact::update(['pending' => false], ['id' => $introduction->cid]); } } - $contact = Contact::selectFirst([], ['id' => $this->{'contact-id'}, 'uid' => $this->uid]); + $contact = Contact::selectFirst([], ['id' => $introduction->cid, 'uid' => $introduction->uid]); if (!empty($contact)) { if (!empty($contact['protocol'])) { $protocol = $contact['protocol']; @@ -175,7 +140,5 @@ class Introduction extends BaseModel ActivityPub\Transmitter::sendContactReject($contact['url'], $contact['hub-verify'], $contact['uid']); } } - - return $this->intro->delete($this); } } diff --git a/src/Module/Api/Mastodon/FollowRequests.php b/src/Module/Api/Mastodon/FollowRequests.php index cb7d5da220..dc6f336d6d 100644 --- a/src/Module/Api/Mastodon/FollowRequests.php +++ b/src/Module/Api/Mastodon/FollowRequests.php @@ -23,6 +23,7 @@ namespace Friendica\Module\Api\Mastodon; use Friendica\Core\System; use Friendica\DI; +use Friendica\Model\Contact; use Friendica\Module\BaseApi; use Friendica\Network\HTTPException; @@ -34,7 +35,6 @@ class FollowRequests extends BaseApi /** * @param array $parameters * @throws HTTPException\BadRequestException - * @throws HTTPException\ForbiddenException * @throws HTTPException\InternalServerErrorException * @throws HTTPException\NotFoundException * @throws HTTPException\UnauthorizedException @@ -48,25 +48,28 @@ class FollowRequests extends BaseApi self::checkAllowedScope(self::SCOPE_FOLLOW); $uid = self::getCurrentUserID(); - $introduction = DI::intro()->selectFirst(['id' => $parameters['id'], 'uid' => $uid]); + $introduction = DI::intro()->selectOneById($parameters['id'], $uid); - $contactId = $introduction->{'contact-id'}; + $contactId = $introduction->cid; switch ($parameters['action']) { case 'authorize': - $introduction->confirm(); - + Contact\Introduction::confirm($introduction); $relationship = DI::mstdnRelationship()->createFromContactId($contactId, $uid); + + DI::intro()->delete($introduction); break; case 'ignore': - $introduction->ignore(); - + $introduction->setIgnore(); $relationship = DI::mstdnRelationship()->createFromContactId($contactId, $uid); + + DI::intro()->save($introduction); break; case 'reject': - $introduction->discard(); - + Contact\Introduction::discard($introduction); $relationship = DI::mstdnRelationship()->createFromContactId($contactId, $uid); + + DI::intro()->delete($introduction); break; default: throw new HTTPException\BadRequestException('Unexpected action parameter, expecting "authorize", "ignore" or "reject"'); @@ -92,13 +95,7 @@ class FollowRequests extends BaseApi 'limit' => 40, // Maximum number of results to return. Defaults to 40. Paginate using the HTTP Link header. ]); - $introductions = DI::intro()->selectByBoundaries( - ['`uid` = ? AND NOT `ignore`', $uid], - ['order' => ['id' => 'DESC']], - $request['min_id'], - $request['max_id'], - $request['limit'] - ); + $introductions = DI::intro()->selectForUser($uid, $request['min_id'], $request['max_id'], $request['limit']); $return = []; diff --git a/src/Module/FollowConfirm.php b/src/Module/FollowConfirm.php index f4e4c5ebf9..f028a3d388 100644 --- a/src/Module/FollowConfirm.php +++ b/src/Module/FollowConfirm.php @@ -21,12 +21,10 @@ class FollowConfirm extends BaseModule $duplex = intval($_POST['duplex'] ?? 0); $hidden = intval($_POST['hidden'] ?? 0); - $intro = DI::intro()->selectFirst(['id' => $intro_id, 'uid' => local_user()]); - - $cid = $intro->{'contact-id'}; + $intro = DI::intro()->selectOneById($intro_id, local_user()); $intro->confirm($duplex, $hidden); - DI::baseUrl()->redirect('contact/' . intval($cid)); + DI::baseUrl()->redirect('contact/' . $intro->cid); } } diff --git a/src/Module/Notifications/Notification.php b/src/Module/Notifications/Notification.php index 19ee410d28..403b43781c 100644 --- a/src/Module/Notifications/Notification.php +++ b/src/Module/Notifications/Notification.php @@ -24,6 +24,7 @@ namespace Friendica\Module\Notifications; use Friendica\BaseModule; use Friendica\Core\System; use Friendica\DI; +use Friendica\Model\Contact; use Friendica\Module\Security\Login; use Friendica\Network\HTTPException; @@ -50,14 +51,16 @@ class Notification extends BaseModule $request_id = $parameters['id'] ?? false; if ($request_id) { - $intro = DI::intro()->selectFirst(['id' => $request_id, 'uid' => local_user()]); + $intro = DI::intro()->selectOneById($request_id, local_user()); switch ($_POST['submit']) { case DI::l10n()->t('Discard'): - $intro->discard(); + Contact\Introduction::discard($intro); + DI::intro()->delete($intro); break; case DI::l10n()->t('Ignore'): - $intro->ignore(); + $intro->setIgnore(); + DI::intro()->save($intro); break; } diff --git a/src/Repository/Introduction.php b/src/Repository/Introduction.php deleted file mode 100644 index de95229bd5..0000000000 --- a/src/Repository/Introduction.php +++ /dev/null @@ -1,79 +0,0 @@ -. - * - */ - -namespace Friendica\Repository; - -use Friendica\BaseRepository; -use Friendica\Collection; -use Friendica\Model; - -class Introduction extends BaseRepository -{ - protected static $table_name = 'intro'; - - protected static $model_class = Model\Introduction::class; - - protected static $collection_class = Collection\Introductions::class; - - /** - * @param array $data - * @return Model\Introduction - */ - protected function create(array $data) - { - return new Model\Introduction($this->dba, $this->logger, $this, $data); - } - - /** - * @param array $condition - * @return Model\Introduction - * @throws \Friendica\Network\HTTPException\NotFoundException - */ - public function selectFirst(array $condition) - { - return parent::selectFirst($condition); - } - - /** - * @param array $condition - * @param array $params - * @return Collection\Introductions - * @throws \Exception - */ - public function select(array $condition = [], array $params = []) - { - return parent::select($condition, $params); - } - - /** - * @param array $condition - * @param array $params - * @param int|null $min_id - * @param int|null $max_id - * @param int $limit - * @return Collection\Introductions - * @throws \Exception - */ - public function selectByBoundaries(array $condition = [], array $params = [], int $min_id = null, int $max_id = null, int $limit = self::LIMIT) - { - return parent::selectByBoundaries($condition, $params, $min_id, $max_id, $limit); - } -}