From 47acb6a2786ea5854228501160dd22199e6c2748 Mon Sep 17 00:00:00 2001 From: Hypolite Petovan Date: Sat, 18 Sep 2021 01:08:29 -0400 Subject: [PATCH] Move notify to the new paradigm - Remove unused frion notify template - Update API test --- include/api.php | 53 ++- include/enotify.php | 51 +-- mod/display.php | 2 +- mod/ping.php | 2 +- src/DI.php | 15 +- src/Factory/Notification/Notification.php | 392 ------------------ src/Model/Notification.php | 92 ---- src/Module/BaseNotifications.php | 32 +- src/Module/Delegation.php | 4 +- src/Module/Notifications/Notification.php | 32 +- src/Module/Notifications/Notifications.php | 50 +-- src/Object/Api/Friendica/Notification.php | 40 +- src/Object/Notification/Notification.php | 143 ------- src/Repository/Notification.php | 130 ------ tests/datasets/api.fixture.php | 2 +- tests/legacy/ApiTest.php | 7 +- view/templates/notifications/attend_item.tpl | 2 +- .../templates/notifications/comments_item.tpl | 2 +- .../templates/notifications/dislikes_item.tpl | 2 +- view/templates/notifications/friends_item.tpl | 2 +- view/templates/notifications/likes_item.tpl | 2 +- view/templates/notifications/network_item.tpl | 2 +- view/templates/notifications/notification.tpl | 2 +- view/templates/notifications/posts_item.tpl | 2 +- .../frio/templates/notifications/notify.tpl | 12 - 25 files changed, 150 insertions(+), 925 deletions(-) delete mode 100644 src/Factory/Notification/Notification.php delete mode 100644 src/Object/Notification/Notification.php delete mode 100644 src/Repository/Notification.php delete mode 100644 view/theme/frio/templates/notifications/notify.tpl diff --git a/include/api.php b/include/api.php index af9fe7736..52f04d954 100644 --- a/include/api.php +++ b/include/api.php @@ -24,15 +24,14 @@ */ use Friendica\App; +use Friendica\Collection\Api\Notifications as ApiNotifications; use Friendica\Content\ContactSelector; use Friendica\Content\Text\BBCode; use Friendica\Content\Text\HTML; use Friendica\Core\Hook; use Friendica\Core\Logger; use Friendica\Core\Protocol; -use Friendica\Core\Session; use Friendica\Core\System; -use Friendica\Core\Worker; use Friendica\Database\DBA; use Friendica\DI; use Friendica\Model\Contact; @@ -54,6 +53,7 @@ use Friendica\Network\HTTPException\MethodNotAllowedException; use Friendica\Network\HTTPException\NotFoundException; use Friendica\Network\HTTPException\TooManyRequestsException; use Friendica\Network\HTTPException\UnauthorizedException; +use Friendica\Object\Api\Friendica\Notification as ApiNotification; use Friendica\Object\Image; use Friendica\Protocol\Activity; use Friendica\Protocol\Diaspora; @@ -5580,23 +5580,24 @@ api_register_func('api/friendica/activity/unattendmaybe', 'api_friendica_activit */ function api_friendica_notification($type) { - $a = DI::app(); - if (api_user() === false) { throw new ForbiddenException(); } if (DI::args()->getArgc()!==3) { - throw new BadRequestException("Invalid argument count"); + throw new BadRequestException('Invalid argument count'); } - $notifications = DI::notification()->getApiList(local_user()); + $Notifies = DI::notify()->selectAllForUser(local_user(), ['order' => ['seen' => 'ASC', 'date' => 'DESC'], 'limit' => 50]); - if ($type == "xml") { - $xmlnotes = false; - if (!empty($notifications)) { - foreach ($notifications as $notification) { - $xmlnotes[] = ["@attributes" => $notification->toArray()]; - } + $notifications = new ApiNotifications(); + foreach ($Notifies as $Notify) { + $notifications[] = new ApiNotification($Notify); + } + + if ($type == 'xml') { + $xmlnotes = []; + foreach ($notifications as $notification) { + $xmlnotes[] = ['@attributes' => $notification->toArray()]; } $result = $xmlnotes; @@ -5606,7 +5607,7 @@ function api_friendica_notification($type) $result = false; } - return api_format_data("notes", $type, ['note' => $result]); + return api_format_data('notes', $type, ['note' => $result]); } /** @@ -5631,26 +5632,36 @@ function api_friendica_notification_seen($type) throw new ForbiddenException(); } if (DI::args()->getArgc() !== 4) { - throw new BadRequestException("Invalid argument count"); + throw new BadRequestException('Invalid argument count'); } - $id = (!empty($_REQUEST['id']) ? intval($_REQUEST['id']) : 0); + $id = intval($_REQUEST['id'] ?? 0); try { - $notify = DI::notify()->getByID($id, api_user()); - DI::notify()->setSeen(true, $notify); + $Notify = DI::notify()->selectOneById($id); + if ($Notify->uid !== api_user()) { + throw new NotFoundException(); + } - if ($notify->otype === Notification\ObjectType::ITEM) { - $item = Post::selectFirstForUser(api_user(), [], ['id' => $notify->iid, 'uid' => api_user()]); + if ($Notify->uriId) { + DI::dba()->update('notification', ['seen' => true], ['uid' => $Notify->uid, 'target-uri-id' => $Notify->uriId]); + } + + $Notify->setSeen(); + DI::notify()->save($Notify); + + if ($Notify->otype === Notification\ObjectType::ITEM) { + $item = Post::selectFirstForUser(api_user(), [], ['id' => $Notify->iid, 'uid' => api_user()]); if (DBA::isResult($item)) { // we found the item, return it to the user $ret = api_format_items([$item], $user_info, false, $type); $data = ['status' => $ret]; - return api_format_data("status", $type, $data); + return api_format_data('status', $type, $data); } // the item can't be found, but we set the notification as seen, so we count this as a success } - return api_format_data('result', $type, ['result' => "success"]); + + return api_format_data('result', $type, ['result' => 'success']); } catch (NotFoundException $e) { throw new BadRequestException('Invalid argument', $e); } catch (Exception $e) { diff --git a/include/enotify.php b/include/enotify.php index 84b46edc0..c3d5e4a40 100644 --- a/include/enotify.php +++ b/include/enotify.php @@ -19,11 +19,9 @@ * */ -use Friendica\Content\Text\BBCode; use Friendica\Content\Text\Plaintext; use Friendica\Core\Hook; use Friendica\Core\Logger; -use Friendica\Core\Renderer; use Friendica\Core\System; use Friendica\Database\DBA; use Friendica\DI; @@ -33,6 +31,7 @@ use Friendica\Model\Notification; use Friendica\Model\Post; use Friendica\Model\User; use Friendica\Model\Verb; +use Friendica\Navigation\Notifications; use Friendica\Protocol\Activity; /** @@ -397,42 +396,22 @@ function notification_store_and_send($params, $sitelink, $tsitelink, $hsitelink, $notify_id = 0; if ($show_in_notification_page) { - $fields = [ - 'name' => $params['source_name'] ?? '', - 'name_cache' => substr(strip_tags(BBCode::convertForUriId($uri_id, $params['source_name'])), 0, 255), - 'url' => $params['source_link'] ?? '', - 'photo' => $params['source_photo'] ?? '', - 'link' => $itemlink ?? '', - 'uid' => $params['uid'] ?? 0, - 'type' => $params['type'] ?? '', - 'verb' => $params['verb'] ?? '', - 'otype' => $params['otype'] ?? '', - ]; - if (!empty($item_id)) { - $fields['iid'] = $item_id; - } - if (!empty($uri_id)) { - $fields['uri-id'] = $uri_id; - } - if (!empty($parent_id)) { - $fields['parent'] = $parent_id; - } - if (!empty($parent_uri_id)) { - $fields['parent-uri-id'] = $parent_uri_id; - } - $notification = DI::notify()->insert($fields); + /** @var $factory Notifications\Factory\Notify */ + $factory = DI::getDice()->create(Notifications\Factory\Notify::class); - // Notification insertion can be intercepted by an addon registering the 'enotify_store' hook - if (!$notification) { + $Notify = $factory->createFromParams($params, $itemlink, $item_id, $uri_id, $parent_id, $parent_uri_id); + try { + $Notify = DI::notify()->save($Notify); + } catch (Notifications\Exception\NotificationCreationInterceptedException $e) { + // Notification insertion can be intercepted by an addon registering the 'enotify_store' hook return false; } - $notification->msg = Renderer::replaceMacros($epreamble, ['$itemlink' => $notification->link]); + $Notify->updateMsgFromPreamble($epreamble); + $Notify = DI::notify()->save($Notify); - DI::notify()->update($notification); - - $itemlink = DI::baseUrl() . '/notification/' . $notification->id; - $notify_id = $notification->id; + $itemlink = DI::baseUrl() . '/notification/' . $Notify->id; + $notify_id = $Notify->id; } // send email notification if notification preferences permit @@ -566,9 +545,9 @@ function notification_from_array(array $notification) // Check to see if there was already a tag notify or comment notify for this post. // If so don't create a second notification $condition = ['type' => [Notification\Type::TAG_SELF, Notification\Type::COMMENT, Notification\Type::SHARE], - 'link' => $params['link'], 'verb' => Activity::POST, 'uid' => $notification['uid']]; - if (DBA::exists('notify', $condition)) { - Logger::info('Duplicate found, quitting', $condition); + 'link' => $params['link'], 'verb' => Activity::POST]; + if (DI::notify()->existsForUser($notification['uid'], $condition)) { + Logger::info('Duplicate found, quitting', $condition + ['uid' => $notification['uid']]); return false; } diff --git a/mod/display.php b/mod/display.php index 87775126e..26f6eae36 100644 --- a/mod/display.php +++ b/mod/display.php @@ -223,7 +223,7 @@ function display_content(App $a, $update = false, $update_uid = 0) if (!DI::pConfig()->get(local_user(), 'system', 'detailed_notif')) { DBA::update('notification', ['seen' => true], ['parent-uri-id' => $item['parent-uri-id'], 'uid' => local_user()]); - DBA::update('notify', ['seen' => true], ['parent-uri-id' => $item['parent-uri-id'], 'uid' => local_user()]); + DI::notify()->setAllSeenForUser(local_user(), ['parent-uri-id' => $item['parent-uri-id']]); } // We are displaying an "alternate" link if that post was public. See issue 2864 diff --git a/mod/ping.php b/mod/ping.php index 69aef1bf3..2036b8ec4 100644 --- a/mod/ping.php +++ b/mod/ping.php @@ -445,7 +445,7 @@ function ping_get_notifications($uid) $notification["message"] = $notification["msg_cache"]; } else { $notification["name"] = strip_tags(BBCode::convert($notification["name"])); - $notification["message"] = Notification::formatMessage($notification["name"], strip_tags(BBCode::convert($notification["msg"]))); + $notification["message"] = \Friendica\Navigation\Notifications\Entity\Notify::formatMessage($notification["name"], BBCode::toPlaintext($notification["msg"])); q( "UPDATE `notify` SET `name_cache` = '%s', `msg_cache` = '%s' WHERE `id` = %d", diff --git a/src/DI.php b/src/DI.php index ed67efe00..a54f26c07 100644 --- a/src/DI.php +++ b/src/DI.php @@ -370,14 +370,6 @@ abstract class DI return self::$dice->create(Factory\Api\Twitter\User::class); } - /** - * @return Factory\Notification\Notification - */ - public static function notification() - { - return self::$dice->create(Factory\Notification\Notification::class); - } - /** * @return Factory\Notification\Introduction */ @@ -469,12 +461,9 @@ abstract class DI return self::$dice->create(Repository\ProfileField::class); } - /** - * @return Repository\Notification - */ - public static function notify() + public static function notify(): Navigation\Notifications\Depository\Notify { - return self::$dice->create(Repository\Notification::class); + return self::$dice->create(Navigation\Notifications\Depository\Notify::class); } // diff --git a/src/Factory/Notification/Notification.php b/src/Factory/Notification/Notification.php deleted file mode 100644 index 9f833130a..000000000 --- a/src/Factory/Notification/Notification.php +++ /dev/null @@ -1,392 +0,0 @@ -. - * - */ - -namespace Friendica\Factory\Notification; - -use Exception; -use Friendica\App; -use Friendica\App\BaseURL; -use Friendica\BaseFactory; -use Friendica\Collection\Api\Notifications as ApiNotifications; -use Friendica\Content\Text\BBCode; -use Friendica\Core\L10n; -use Friendica\Core\PConfig\IPConfig; -use Friendica\Core\Protocol; -use Friendica\Core\Session\ISession; -use Friendica\Database\Database; -use Friendica\Model\Contact; -use Friendica\Model\Post; -use Friendica\Module\BaseNotifications; -use Friendica\Network\HTTPException\InternalServerErrorException; -use Friendica\Object\Api\Friendica\Notification as ApiNotification; -use Friendica\Protocol\Activity; -use Friendica\Repository; -use Friendica\Util\DateTimeFormat; -use Friendica\Util\Proxy; -use Friendica\Util\Temporal; -use Friendica\Util\XML; -use Psr\Log\LoggerInterface; - -/** - * Factory for creating notification objects based on items - * Currently, there are the following types of item based notifications: - * - network - * - system - * - home - * - personal - */ -class Notification extends BaseFactory -{ - /** @var Database */ - private $dba; - /** @var Repository\Notification */ - private $notification; - /** @var BaseURL */ - private $baseUrl; - /** @var L10n */ - private $l10n; - - public function __construct(LoggerInterface $logger, Database $dba, Repository\Notification $notification, BaseURL $baseUrl, L10n $l10n, App $app, IPConfig $pConfig, ISession $session) - { - parent::__construct($logger); - - $this->dba = $dba; - $this->notification = $notification; - $this->baseUrl = $baseUrl; - $this->l10n = $l10n; - } - - /** - * Format the item query in an usable array - * - * @param array $item The item from the db query - * - * @return array The item, extended with the notification-specific information - * - * @throws InternalServerErrorException - * @throws Exception - */ - private function formatItem(array $item) - { - $item['seen'] = ($item['unseen'] > 0 ? false : true); - - // For feed items we use the user's contact, since the avatar is mostly self choosen. - if (!empty($item['network']) && $item['network'] == Protocol::FEED) { - $item['author-avatar'] = $item['contact-avatar']; - } - - $item['label'] = (($item['gravity'] == GRAVITY_PARENT) ? 'post' : 'comment'); - $item['link'] = $this->baseUrl->get(true) . '/display/' . $item['parent-guid']; - $item['image'] = $item['author-avatar']; - $item['url'] = $item['author-link']; - $item['text'] = (($item['gravity'] == GRAVITY_PARENT) - ? $this->l10n->t("%s created a new post", $item['author-name']) - : $this->l10n->t("%s commented on %s's post", $item['author-name'], $item['parent-author-name'])); - $item['when'] = DateTimeFormat::local($item['created'], 'r'); - $item['ago'] = Temporal::getRelativeDate($item['created']); - - return $item; - } - - /** - * @param array $item - * - * @return \Friendica\Object\Notification\Notification - * - * @throws InternalServerErrorException - */ - private function createFromItem(array $item) - { - $item = $this->formatItem($item); - - // Transform the different types of notification in an usable array - switch ($item['verb'] ?? '') { - case Activity::LIKE: - return new \Friendica\Object\Notification\Notification([ - 'label' => 'like', - 'link' => $this->baseUrl->get(true) . '/display/' . $item['parent-guid'], - 'image' => $item['author-avatar'], - 'url' => $item['author-link'], - 'text' => $this->l10n->t("%s liked %s's post", $item['author-name'], $item['parent-author-name']), - 'when' => $item['when'], - 'ago' => $item['ago'], - 'seen' => $item['seen']]); - - case Activity::DISLIKE: - return new \Friendica\Object\Notification\Notification([ - 'label' => 'dislike', - 'link' => $this->baseUrl->get(true) . '/display/' . $item['parent-guid'], - 'image' => $item['author-avatar'], - 'url' => $item['author-link'], - 'text' => $this->l10n->t("%s disliked %s's post", $item['author-name'], $item['parent-author-name']), - 'when' => $item['when'], - 'ago' => $item['ago'], - 'seen' => $item['seen']]); - - case Activity::ATTEND: - return new \Friendica\Object\Notification\Notification([ - 'label' => 'attend', - 'link' => $this->baseUrl->get(true) . '/display/' . $item['parent-guid'], - 'image' => $item['author-avatar'], - 'url' => $item['author-link'], - 'text' => $this->l10n->t("%s is attending %s's event", $item['author-name'], $item['parent-author-name']), - 'when' => $item['when'], - 'ago' => $item['ago'], - 'seen' => $item['seen']]); - - case Activity::ATTENDNO: - return new \Friendica\Object\Notification\Notification([ - 'label' => 'attendno', - 'link' => $this->baseUrl->get(true) . '/display/' . $item['parent-guid'], - 'image' => $item['author-avatar'], - 'url' => $item['author-link'], - 'text' => $this->l10n->t("%s is not attending %s's event", $item['author-name'], $item['parent-author-name']), - 'when' => $item['when'], - 'ago' => $item['ago'], - 'seen' => $item['seen']]); - - case Activity::ATTENDMAYBE: - return new \Friendica\Object\Notification\Notification([ - 'label' => 'attendmaybe', - 'link' => $this->baseUrl->get(true) . '/display/' . $item['parent-guid'], - 'image' => $item['author-avatar'], - 'url' => $item['author-link'], - 'text' => $this->l10n->t("%s may attending %s's event", $item['author-name'], $item['parent-author-name']), - 'when' => $item['when'], - 'ago' => $item['ago'], - 'seen' => $item['seen']]); - - case Activity::FRIEND: - if (!isset($item['object'])) { - return new \Friendica\Object\Notification\Notification([ - 'label' => 'friend', - 'link' => $item['link'], - 'image' => $item['image'], - 'url' => $item['url'], - 'text' => $item['text'], - 'when' => $item['when'], - 'ago' => $item['ago'], - 'seen' => $item['seen']]); - } - - $xmlHead = "<" . "?xml version='1.0' encoding='UTF-8' ?" . ">"; - $obj = XML::parseString($xmlHead . $item['object']); - $item['fname'] = $obj->title; - - return new \Friendica\Object\Notification\Notification([ - 'label' => 'friend', - 'link' => $this->baseUrl->get(true) . '/display/' . $item['parent-guid'], - 'image' => $item['author-avatar'], - 'url' => $item['author-link'], - 'text' => $this->l10n->t("%s is now friends with %s", $item['author-name'], $item['fname']), - 'when' => $item['when'], - 'ago' => $item['ago'], - 'seen' => $item['seen']]); - - default: - return new \Friendica\Object\Notification\Notification($item); - break; - } - } - - /** - * Get system notifications - * - * @param bool $seen False => only include notifications into the query - * which aren't marked as "seen" - * @param int $start Start the query at this point - * @param int $limit Maximum number of query results - * - * @return \Friendica\Module\Notifications\Notification[] - */ - public function getSystemList(bool $seen = false, int $start = 0, int $limit = BaseNotifications::DEFAULT_PAGE_LIMIT) - { - $conditions = ['uid' => local_user()]; - - if (!$seen) { - $conditions['seen'] = false; - } - - $params = []; - $params['order'] = ['date' => 'DESC']; - $params['limit'] = [$start, $limit]; - - $formattedNotifications = []; - try { - $notifications = $this->notification->select($conditions, $params); - - foreach ($notifications as $notification) { - $formattedNotifications[] = new \Friendica\Object\Notification\Notification([ - 'label' => 'notification', - 'link' => $this->baseUrl->get(true) . '/notification/' . $notification->id, - 'image' => Contact::getAvatarUrlForUrl($notification->url, $notification->uid, Proxy::SIZE_MICRO), - 'url' => $notification->url, - 'text' => strip_tags(BBCode::convert($notification->msg)), - 'when' => DateTimeFormat::local($notification->date, 'r'), - 'ago' => Temporal::getRelativeDate($notification->date), - 'seen' => $notification->seen]); - } - } catch (Exception $e) { - $this->logger->warning('Select failed.', ['conditions' => $conditions, 'exception' => $e]); - } - - return $formattedNotifications; - } - - /** - * Get network notifications - * - * @param bool $seen False => only include notifications into the query - * which aren't marked as "seen" - * @param int $start Start the query at this point - * @param int $limit Maximum number of query results - * - * @return \Friendica\Object\Notification\Notification[] - */ - public function getNetworkList(bool $seen = false, int $start = 0, int $limit = BaseNotifications::DEFAULT_PAGE_LIMIT) - { - $conditions = ['wall' => false, 'uid' => local_user()]; - - if (!$seen) { - $conditions['unseen'] = true; - } - - $fields = ['id', 'parent', 'verb', 'author-name', 'unseen', 'author-link', 'author-avatar', 'contact-avatar', - 'network', 'created', 'object', 'parent-author-name', 'parent-author-link', 'parent-guid', 'gravity']; - $params = ['order' => ['received' => true], 'limit' => [$start, $limit]]; - - $formattedNotifications = []; - - try { - $items = Post::selectForUser(local_user(), $fields, $conditions, $params); - - while ($item = $this->dba->fetch($items)) { - $formattedNotifications[] = $this->createFromItem($item); - } - } catch (Exception $e) { - $this->logger->warning('Select failed.', ['conditions' => $conditions, 'exception' => $e]); - } - - return $formattedNotifications; - } - - /** - * Get personal notifications - * - * @param bool $seen False => only include notifications into the query - * which aren't marked as "seen" - * @param int $start Start the query at this point - * @param int $limit Maximum number of query results - * - * @return \Friendica\Object\Notification\Notification[] - */ - public function getPersonalList(bool $seen = false, int $start = 0, int $limit = BaseNotifications::DEFAULT_PAGE_LIMIT) - { - $condition = ["NOT `wall` AND `uid` = ? AND `author-id` = ?", local_user(), public_contact()]; - - if (!$seen) { - $condition[0] .= " AND `unseen`"; - } - - $fields = ['id', 'parent', 'verb', 'author-name', 'unseen', 'author-link', 'author-avatar', 'contact-avatar', - 'network', 'created', 'object', 'parent-author-name', 'parent-author-link', 'parent-guid', 'gravity']; - $params = ['order' => ['received' => true], 'limit' => [$start, $limit]]; - - $formattedNotifications = []; - - try { - $items = Post::selectForUser(local_user(), $fields, $condition, $params); - - while ($item = $this->dba->fetch($items)) { - $formattedNotifications[] = $this->createFromItem($item); - } - } catch (Exception $e) { - $this->logger->warning('Select failed.', ['conditions' => $condition, 'exception' => $e]); - } - - return $formattedNotifications; - } - - /** - * Get home notifications - * - * @param bool $seen False => only include notifications into the query - * which aren't marked as "seen" - * @param int $start Start the query at this point - * @param int $limit Maximum number of query results - * - * @return \Friendica\Object\Notification\Notification[] - */ - public function getHomeList(bool $seen = false, int $start = 0, int $limit = BaseNotifications::DEFAULT_PAGE_LIMIT) - { - $condition = ['wall' => true, 'uid' => local_user()]; - - if (!$seen) { - $condition['unseen'] = true; - } - - $fields = ['id', 'parent', 'verb', 'author-name', 'unseen', 'author-link', 'author-avatar', 'contact-avatar', - 'network', 'created', 'object', 'parent-author-name', 'parent-author-link', 'parent-guid', 'gravity']; - $params = ['order' => ['received' => true], 'limit' => [$start, $limit]]; - - $formattedNotifications = []; - - try { - $items = Post::selectForUser(local_user(), $fields, $condition, $params); - - while ($item = $this->dba->fetch($items)) { - $item = $this->formatItem($item); - - // Overwrite specific fields, not default item format - $item['label'] = 'comment'; - $item['text'] = $this->l10n->t("%s commented on %s's post", $item['author-name'], $item['parent-author-name']); - - $formattedNotifications[] = $this->createFromItem($item); - } - } catch (Exception $e) { - $this->logger->warning('Select failed.', ['conditions' => $condition, 'exception' => $e]); - } - - return $formattedNotifications; - } - - /** - * @param int $uid The user id of the API call - * @param array $params Additional parameters - * - * @return ApiNotifications - * - * @throws Exception - */ - public function getApiList(int $uid, array $params = ['order' => ['seen' => 'ASC', 'date' => 'DESC'], 'limit' => 50]) - { - $notifies = $this->notification->select(['uid' => $uid], $params); - - /** @var ApiNotification[] $notifications */ - $notifications = []; - - foreach ($notifies as $notify) { - $notifications[] = new ApiNotification($notify); - } - - return new ApiNotifications($notifications); - } -} diff --git a/src/Model/Notification.php b/src/Model/Notification.php index 1df420845..e9f9ff41c 100644 --- a/src/Model/Notification.php +++ b/src/Model/Notification.php @@ -33,101 +33,9 @@ use Psr\Log\LoggerInterface; /** * Model for an entry in the notify table - * - * @property string hash - * @property integer type - * @property string name Full name of the contact subject - * @property string url Profile page URL of the contact subject - * @property string photo Profile photo URL of the contact subject - * @property string date YYYY-MM-DD hh:mm:ss local server time - * @property string msg - * @property integer uid Owner User Id - * @property string link Notification URL - * @property integer iid Item Id - * @property integer parent Parent Item Id - * @property boolean seen Whether the notification was read or not. - * @property string verb Verb URL (@see http://activitystrea.ms) - * @property string otype Subject type ('item', 'intro' or 'mail') - * - * @property-read string name_cache Full name of the contact subject - * @property-read string msg_cache Plaintext version of the notification text with a placeholder (`{0}`) for the subject contact's name. */ class Notification extends BaseModel { - /** @var \Friendica\Repository\Notification */ - private $repo; - - public function __construct(Database $dba, LoggerInterface $logger, \Friendica\Repository\Notification $repo, array $data = []) - { - parent::__construct($dba, $logger, $data); - - $this->repo = $repo; - - $this->setNameCache(); - $this->setMsgCache(); - } - - /** - * Sets the pre-formatted name (caching) - */ - private function setNameCache() - { - try { - $this->name_cache = strip_tags(BBCode::convert($this->source_name)); - } catch (InternalServerErrorException $e) { - } - } - - /** - * Sets the pre-formatted msg (caching) - */ - private function setMsgCache() - { - try { - $this->msg_cache = self::formatMessage($this->name_cache, strip_tags(BBCode::convert($this->msg))); - } catch (InternalServerErrorException $e) { - } - } - - public function __set($name, $value) - { - parent::__set($name, $value); - - if ($name == 'msg') { - $this->setMsgCache(); - } - - if ($name == 'source_name') { - $this->setNameCache(); - } - } - - /** - * Formats a notification message with the notification author - * - * Replace the name with {0} but ensure to make that only once. The {0} is used - * later and prints the name in bold. - * - * @param string $name - * @param string $message - * - * @return string Formatted message - */ - public static function formatMessage($name, $message) - { - if ($name != '') { - $pos = strpos($message, $name); - } else { - $pos = false; - } - - if ($pos !== false) { - $message = substr_replace($message, '{0}', $pos, strlen($name)); - } - - return $message; - } - /** * Fetch the notification type for the given notification * diff --git a/src/Module/BaseNotifications.php b/src/Module/BaseNotifications.php index a472c520a..e7f9bdabe 100644 --- a/src/Module/BaseNotifications.php +++ b/src/Module/BaseNotifications.php @@ -27,8 +27,8 @@ use Friendica\Content\Pager; use Friendica\Core\Renderer; use Friendica\Core\System; use Friendica\DI; +use Friendica\Navigation\Notifications\ValueObject\FormattedNotification; use Friendica\Network\HTTPException\ForbiddenException; -use Friendica\Object\Notification\Notification; /** * Base Module for each tab of the notification display @@ -39,29 +39,29 @@ abstract class BaseNotifications extends BaseModule { /** @var array Array of URL parameters */ const URL_TYPES = [ - Notification::NETWORK => 'network', - Notification::SYSTEM => 'system', - Notification::HOME => 'home', - Notification::PERSONAL => 'personal', - Notification::INTRO => 'intros', + FormattedNotification::NETWORK => 'network', + FormattedNotification::SYSTEM => 'system', + FormattedNotification::HOME => 'home', + FormattedNotification::PERSONAL => 'personal', + FormattedNotification::INTRO => 'intros', ]; /** @var array Array of the allowed notifications and their printable name */ const PRINT_TYPES = [ - Notification::NETWORK => 'Network', - Notification::SYSTEM => 'System', - Notification::HOME => 'Home', - Notification::PERSONAL => 'Personal', - Notification::INTRO => 'Introductions', + FormattedNotification::NETWORK => 'Network', + FormattedNotification::SYSTEM => 'System', + FormattedNotification::HOME => 'Home', + FormattedNotification::PERSONAL => 'Personal', + FormattedNotification::INTRO => 'Introductions', ]; /** @var array The array of access keys for notification pages */ const ACCESS_KEYS = [ - Notification::NETWORK => 'w', - Notification::SYSTEM => 'y', - Notification::HOME => 'h', - Notification::PERSONAL => 'r', - Notification::INTRO => 'i', + FormattedNotification::NETWORK => 'w', + FormattedNotification::SYSTEM => 'y', + FormattedNotification::HOME => 'h', + FormattedNotification::PERSONAL => 'r', + FormattedNotification::INTRO => 'i', ]; /** @var int The default count of items per page */ diff --git a/src/Module/Delegation.php b/src/Module/Delegation.php index 54f868881..279c694b1 100644 --- a/src/Module/Delegation.php +++ b/src/Module/Delegation.php @@ -126,9 +126,9 @@ class Delegation extends BaseModule $identities[$key]['selected'] = ($identity['nickname'] === DI::app()->getLoggedInUserNickname()); - $condition = ["`uid` = ? AND `msg` != '' AND NOT (`type` IN (?, ?)) AND NOT `seen`", $identity['uid'], Notification\Type::INTRO, Notification\Type::MAIL]; + $condition = ["`msg` != '' AND NOT (`type` IN (?, ?)) AND NOT `seen`", Notification\Type::INTRO, Notification\Type::MAIL]; $params = ['distinct' => true, 'expression' => 'parent']; - $notifications = DBA::count('notify', $condition, $params); + $notifications = DI::notify()->countForUser($identity['uid'], $condition, $params); $params = ['distinct' => true, 'expression' => 'convid']; $notifications += DBA::count('mail', ['uid' => $identity['uid'], 'seen' => false], $params); diff --git a/src/Module/Notifications/Notification.php b/src/Module/Notifications/Notification.php index 14e58c217..6f032772c 100644 --- a/src/Module/Notifications/Notification.php +++ b/src/Module/Notifications/Notification.php @@ -78,7 +78,8 @@ class Notification extends BaseModule if (DI::args()->get(1) === 'mark' && DI::args()->get(2) === 'all') { try { - $success = DI::notify()->setSeen(); + DI::dba()->update('notification', ['seen' => true], ['uid' => local_user()]); + $success = DI::notify()->setAllSeenForUser(local_user()); } catch (\Exception $e) { DI::logger()->warning('set all seen failed.', ['exception' => $e]); $success = false; @@ -97,7 +98,7 @@ class Notification extends BaseModule * @throws HTTPException\InternalServerErrorException * @throws \Exception */ - public static function content(array $parameters = []) + public static function content(array $parameters = []): string { if (!local_user()) { notice(DI::l10n()->t('You must be logged in to show this page.')); @@ -107,17 +108,24 @@ class Notification extends BaseModule $request_id = $parameters['id'] ?? false; if ($request_id) { - $notify = DI::notify()->getByID($request_id, local_user()); - - if (DI::pConfig()->get(local_user(), 'system', 'detailed_notif')) { - $notify->seen = true; - DI::notify()->update($notify); - } else { - DI::notify()->setSeen(true, $notify); + $Notify = DI::notify()->selectOneById($request_id); + if ($Notify->uid !== local_user()) { + throw new HTTPException\ForbiddenException(); } - if (!empty($notify->link)) { - System::externalRedirect($notify->link); + if (DI::pConfig()->get(local_user(), 'system', 'detailed_notif')) { + $Notify->setSeen(); + DI::notify()->save($Notify); + } else { + if ($Notify->uriId) { + DI::dba()->update('notification', ['seen' => true], ['uid' => $Notify->uid, 'target-uri-id' => $Notify->uriId]); + } + + DI::notify()->setAllSeenForRelatedNotify($Notify); + } + + if ($Notify->link) { + System::externalRedirect($Notify->link); } DI::baseUrl()->redirect(); @@ -125,6 +133,6 @@ class Notification extends BaseModule DI::baseUrl()->redirect('notifications/system'); - throw new HTTPException\InternalServerErrorException('Invalid situation.'); + return ''; } } diff --git a/src/Module/Notifications/Notifications.php b/src/Module/Notifications/Notifications.php index 06b5ca80d..79f1596f9 100644 --- a/src/Module/Notifications/Notifications.php +++ b/src/Module/Notifications/Notifications.php @@ -25,7 +25,9 @@ use Friendica\Content\Nav; use Friendica\Core\Renderer; use Friendica\DI; use Friendica\Module\BaseNotifications; -use Friendica\Object\Notification\Notification; +use Friendica\Navigation\Notifications\Collection\FormattedNotifications; +use Friendica\Navigation\Notifications\ValueObject\FormattedNotification; +use Friendica\Network\HTTPException\InternalServerErrorException; /** * Prints all notification types except introduction: @@ -42,41 +44,35 @@ class Notifications extends BaseNotifications public static function getNotifications() { $notificationHeader = ''; - /** @var Notification[] $notifications */ $notifications = []; - // Get the network notifications + /** @var \Friendica\Navigation\Notifications\Factory\FormattedNotification $factory */ + $factory = DI::getDice()->create(\Friendica\Navigation\Notifications\Factory\FormattedNotification::class); + if ((DI::args()->get(1) == 'network')) { $notificationHeader = DI::l10n()->t('Network Notifications'); $notifications = [ - 'ident' => Notification::NETWORK, - 'notifications' => DI::notification()->getNetworkList(self::$showAll, self::$firstItemNum, self::ITEMS_PER_PAGE), + 'ident' => FormattedNotification::NETWORK, + 'notifications' => $factory->getNetworkList(self::$showAll, self::$firstItemNum, self::ITEMS_PER_PAGE), ]; - - // Get the system notifications } elseif ((DI::args()->get(1) == 'system')) { $notificationHeader = DI::l10n()->t('System Notifications'); $notifications = [ - 'ident' => Notification::SYSTEM, - 'notifications' => DI::notification()->getSystemList(self::$showAll, self::$firstItemNum, self::ITEMS_PER_PAGE), + 'ident' => FormattedNotification::SYSTEM, + 'notifications' => $factory->getSystemList(self::$showAll, self::$firstItemNum, self::ITEMS_PER_PAGE), ]; - - // Get the personal notifications } elseif ((DI::args()->get(1) == 'personal')) { $notificationHeader = DI::l10n()->t('Personal Notifications'); $notifications = [ - 'ident' => Notification::PERSONAL, - 'notifications' => DI::notification()->getPersonalList(self::$showAll, self::$firstItemNum, self::ITEMS_PER_PAGE), + 'ident' => FormattedNotification::PERSONAL, + 'notifications' => $factory->getPersonalList(self::$showAll, self::$firstItemNum, self::ITEMS_PER_PAGE), ]; - - // Get the home notifications } elseif ((DI::args()->get(1) == 'home')) { $notificationHeader = DI::l10n()->t('Home Notifications'); $notifications = [ - 'ident' => Notification::HOME, - 'notifications' => DI::notification()->getHomeList(self::$showAll, self::$firstItemNum, self::ITEMS_PER_PAGE), + 'ident' => FormattedNotification::HOME, + 'notifications' => $factory->getHomeList(self::$showAll, self::$firstItemNum, self::ITEMS_PER_PAGE), ]; - // fallback - redirect to main page } else { DI::baseUrl()->redirect('notifications'); } @@ -98,11 +94,10 @@ class Notifications extends BaseNotifications $notifications = $notificationResult['notifications'] ?? []; $notificationHeader = $notificationResult['header'] ?? ''; - if (!empty($notifications['notifications'])) { // Loop trough ever notification This creates an array with the output html for each // notification and apply the correct template according to the notificationtype (label). - /** @var Notification $notification */ + /** @var FormattedNotification $notification */ foreach ($notifications['notifications'] as $notification) { $notification_templates = [ 'like' => 'notifications/likes_item.tpl', @@ -116,21 +111,16 @@ class Notifications extends BaseNotifications 'notification' => 'notifications/notification.tpl', ]; - $notificationTemplate = Renderer::getMarkupTemplate($notification_templates[$notification->getLabel()]); + $notificationArray = $notification->toArray(); + + $notificationTemplate = Renderer::getMarkupTemplate($notification_templates[$notificationArray['label']]); $notificationContent[] = Renderer::replaceMacros($notificationTemplate, [ - '$item_label' => $notification->getLabel(), - '$item_link' => $notification->getLink(), - '$item_image' => $notification->getImage(), - '$item_url' => $notification->getUrl(), - '$item_text' => $notification->getText(), - '$item_when' => $notification->getWhen(), - '$item_ago' => $notification->getAgo(), - '$item_seen' => $notification->isSeen(), + '$notification' => $notificationArray ]); } } else { - $notificationNoContent = DI::l10n()->t('No more %s notifications.', $notifications['ident']); + $notificationNoContent = DI::l10n()->t('No more %s notifications.', $notificationResult['ident']); } $notificationShowLink = [ diff --git a/src/Object/Api/Friendica/Notification.php b/src/Object/Api/Friendica/Notification.php index e2ff3f677..1bec7cfed 100644 --- a/src/Object/Api/Friendica/Notification.php +++ b/src/Object/Api/Friendica/Notification.php @@ -2,7 +2,7 @@ /** * @copyright Copyright (C) 2010-2021, the Friendica project * - * @license GNU AGPL version 3 or any later version + * @license GNU AGPL version 3 or any later version * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -24,7 +24,7 @@ namespace Friendica\Object\Api\Friendica; use Friendica\BaseDataTransferObject; use Friendica\Content\Text\BBCode; use Friendica\Content\Text\HTML; -use Friendica\Model\Notification as NotificationModel; +use Friendica\Navigation\Notifications\Entity\Notify; use Friendica\Util\DateTimeFormat; use Friendica\Util\Temporal; @@ -37,8 +37,6 @@ class Notification extends BaseDataTransferObject { /** @var integer */ protected $id; - /** @var string */ - protected $hash; /** @var integer */ protected $type; /** @var string Full name of the contact subject */ @@ -78,21 +76,37 @@ class Notification extends BaseDataTransferObject /** @var string Message (Plaintext) */ protected $msg_plain; - public function __construct(NotificationModel $notify) + public function __construct(Notify $Notify) { - // map each notify attribute to the entity - foreach ($notify->toArray() as $key => $value) { - $this->{$key} = $value; + $this->id = $Notify->id; + $this->type = $Notify->type; + $this->name = $Notify->name; + $this->url = $Notify->url->__toString(); + $this->photo = $Notify->photo->__toString(); + $this->date = DateTimeFormat::local($Notify->date->format(DateTimeFormat::MYSQL)); + $this->msg = $Notify->msg; + $this->uid = $Notify->uid; + $this->link = $Notify->link->__toString(); + $this->iid = $Notify->itemId; + $this->parent = $Notify->parent; + $this->seen = $Notify->seen; + $this->verb = $Notify->verb; + $this->otype = $Notify->otype; + $this->name_cache = $Notify->name_cache; + $this->msg_cache = $Notify->msg_cache; + $this->timestamp = $Notify->date->format('U'); + $this->date_rel = Temporal::getRelativeDate($this->date); + + try { + $this->msg_html = BBCode::convert($this->msg, false); + } catch (\Exception $e) { + $this->msg_html = ''; } - // add additional attributes for the API try { - $this->timestamp = strtotime(DateTimeFormat::local($this->date)); - $this->msg_html = BBCode::convert($this->msg, false); $this->msg_plain = explode("\n", trim(HTML::toPlaintext($this->msg_html, 0)))[0]; } catch (\Exception $e) { + $this->msg_plain = ''; } - - $this->date_rel = Temporal::getRelativeDate($this->date); } } diff --git a/src/Object/Notification/Notification.php b/src/Object/Notification/Notification.php deleted file mode 100644 index 164e7cc03..000000000 --- a/src/Object/Notification/Notification.php +++ /dev/null @@ -1,143 +0,0 @@ -. - * - */ - -namespace Friendica\Object\Notification; - -/** - * A view-only object for printing item notifications to the frontend - */ -class Notification implements \JsonSerializable -{ - const SYSTEM = 'system'; - const PERSONAL = 'personal'; - const NETWORK = 'network'; - const INTRO = 'intro'; - const HOME = 'home'; - - /** @var string */ - private $label = ''; - /** @var string */ - private $link = ''; - /** @var string */ - private $image = ''; - /** @var string */ - private $url = ''; - /** @var string */ - private $text = ''; - /** @var string */ - private $when = ''; - /** @var string */ - private $ago = ''; - /** @var boolean */ - private $seen = false; - - /** - * @return string - */ - public function getLabel() - { - return $this->label; - } - - /** - * @return string - */ - public function getLink() - { - return $this->link; - } - - /** - * @return string - */ - public function getImage() - { - return $this->image; - } - - /** - * @return string - */ - public function getUrl() - { - return $this->url; - } - - /** - * @return string - */ - public function getText() - { - return $this->text; - } - - /** - * @return string - */ - public function getWhen() - { - return $this->when; - } - - /** - * @return string - */ - public function getAgo() - { - return $this->ago; - } - - /** - * @return bool - */ - public function isSeen() - { - return $this->seen; - } - - public function __construct(array $data) - { - $this->label = $data['label'] ?? ''; - $this->link = $data['link'] ?? ''; - $this->image = $data['image'] ?? ''; - $this->url = $data['url'] ?? ''; - $this->text = $data['text'] ?? ''; - $this->when = $data['when'] ?? ''; - $this->ago = $data['ago'] ?? ''; - $this->seen = $data['seen'] ?? false; - } - - /** - * @inheritDoc - */ - public function jsonSerialize() - { - return get_object_vars($this); - } - - /** - * @return array - */ - public function toArray() - { - return get_object_vars($this); - } -} diff --git a/src/Repository/Notification.php b/src/Repository/Notification.php deleted file mode 100644 index 1748759b6..000000000 --- a/src/Repository/Notification.php +++ /dev/null @@ -1,130 +0,0 @@ -. - * - */ - -namespace Friendica\Repository; - -use Exception; -use Friendica\BaseRepository; -use Friendica\Collection; -use Friendica\Core\Hook; -use Friendica\Model; -use Friendica\Network\HTTPException\InternalServerErrorException; -use Friendica\Network\HTTPException\NotFoundException; -use Friendica\Util\DateTimeFormat; - -class Notification extends BaseRepository -{ - protected static $table_name = 'notify'; - - protected static $model_class = Model\Notification::class; - - protected static $collection_class = Collection\Notifications::class; - - /** - * {@inheritDoc} - * - * @return Model\Notification - */ - protected function create(array $data) - { - return new Model\Notification($this->dba, $this->logger, $this, $data); - } - - /** - * {@inheritDoc} - * - * @return Collection\Notifications - */ - public function select(array $condition = [], array $params = []) - { - $params['order'] = $params['order'] ?? ['date' => 'DESC']; - - return parent::select($condition, $params); - } - - /** - * Return one notify instance based on ID / UID - * - * @param int $id The ID of the notify instance - * @param int $uid The user ID, bound to this notify instance (= security check) - * - * @return Model\Notification - * @throws NotFoundException - */ - public function getByID(int $id, int $uid) - { - return $this->selectFirst(['id' => $id, 'uid' => $uid]); - } - - /** - * Set seen state of notifications of the local_user() - * - * @param bool $seen optional true or false. default true - * @param Model\Notification $notify optional a notify, which should be set seen (including his parents) - * - * @return bool true on success, false on error - * - * @throws Exception - */ - public function setSeen(bool $seen = true, Model\Notification $notify = null) - { - if (empty($notify)) { - $this->dba->update('notification', ['seen' => $seen], ['uid' => local_user()]); - $conditions = ['uid' => local_user()]; - } else { - if (!empty($notify->{'uri-id'})) { - $this->dba->update('notification', ['seen' => $seen], ['uid' => local_user(), 'target-uri-id' => $notify->{'uri-id'}]); - } - - $conditions = ['(`link` = ? OR (`parent` != 0 AND `parent` = ? AND `otype` = ?)) AND `uid` = ?', - $notify->link, - $notify->parent, - $notify->otype, - local_user()]; - } - - return $this->dba->update('notify', ['seen' => $seen], $conditions); - } - - /** - * @param array $fields - * - * @return Model\Notification|false - * - * @throws InternalServerErrorException - * @throws Exception - */ - public function insert(array $fields) - { - $fields['date'] = DateTimeFormat::utcNow(); - - Hook::callAll('enotify_store', $fields); - - if (empty($fields)) { - $this->logger->debug('Abort adding notification entry'); - return false; - } - - $this->logger->debug('adding notification entry', ['fields' => $fields]); - - return parent::insert($fields); - } -} diff --git a/tests/datasets/api.fixture.php b/tests/datasets/api.fixture.php index 2c6512f8c..86b2bd564 100644 --- a/tests/datasets/api.fixture.php +++ b/tests/datasets/api.fixture.php @@ -732,7 +732,7 @@ return [ 'link' => 'http://localhost/notification/1', 'iid' => 4, 'seen' => 0, - 'verb' => '', + 'verb' => \Friendica\Protocol\Activity::POST, 'otype' => Notification\ObjectType::ITEM, 'name_cache' => 'Reply to', 'msg_cache' => 'A test reply from an item', diff --git a/tests/legacy/ApiTest.php b/tests/legacy/ApiTest.php index 8e0edaeef..553eef2a0 100644 --- a/tests/legacy/ApiTest.php +++ b/tests/legacy/ApiTest.php @@ -12,6 +12,7 @@ use Friendica\Core\Protocol; use Friendica\DI; use Friendica\Network\HTTPException; use Friendica\Test\FixtureTest; +use Friendica\Util\DateTimeFormat; use Friendica\Util\Temporal; use Monolog\Handler\TestHandler; @@ -3714,11 +3715,13 @@ class ApiTest extends FixtureTest { DI::args()->setArgv(['api', 'friendica', 'notification']); $result = api_friendica_notification('xml'); - $dateRel = Temporal::getRelativeDate('2020-01-01 12:12:02'); + $date = DateTimeFormat::local('2020-01-01 12:12:02'); + $dateRel = Temporal::getRelativeDate('2020-01-01 07:12:02'); + $assertXml=<< - + XML; self::assertXmlStringEqualsXmlString($assertXml, $result); diff --git a/view/templates/notifications/attend_item.tpl b/view/templates/notifications/attend_item.tpl index a8de3c793..07c770fb5 100644 --- a/view/templates/notifications/attend_item.tpl +++ b/view/templates/notifications/attend_item.tpl @@ -1,4 +1,4 @@ \ No newline at end of file diff --git a/view/templates/notifications/comments_item.tpl b/view/templates/notifications/comments_item.tpl index a8de3c793..07c770fb5 100644 --- a/view/templates/notifications/comments_item.tpl +++ b/view/templates/notifications/comments_item.tpl @@ -1,4 +1,4 @@ \ No newline at end of file diff --git a/view/templates/notifications/dislikes_item.tpl b/view/templates/notifications/dislikes_item.tpl index a8de3c793..07c770fb5 100644 --- a/view/templates/notifications/dislikes_item.tpl +++ b/view/templates/notifications/dislikes_item.tpl @@ -1,4 +1,4 @@ \ No newline at end of file diff --git a/view/templates/notifications/friends_item.tpl b/view/templates/notifications/friends_item.tpl index 9df155465..8e0158a0e 100644 --- a/view/templates/notifications/friends_item.tpl +++ b/view/templates/notifications/friends_item.tpl @@ -1,4 +1,4 @@ \ No newline at end of file diff --git a/view/templates/notifications/likes_item.tpl b/view/templates/notifications/likes_item.tpl index a8de3c793..07c770fb5 100644 --- a/view/templates/notifications/likes_item.tpl +++ b/view/templates/notifications/likes_item.tpl @@ -1,4 +1,4 @@ \ No newline at end of file diff --git a/view/templates/notifications/network_item.tpl b/view/templates/notifications/network_item.tpl index e2c0e249d..36b7dd424 100644 --- a/view/templates/notifications/network_item.tpl +++ b/view/templates/notifications/network_item.tpl @@ -1,4 +1,4 @@ diff --git a/view/templates/notifications/notification.tpl b/view/templates/notifications/notification.tpl index 4ff7bc7a8..b5c684cd8 100644 --- a/view/templates/notifications/notification.tpl +++ b/view/templates/notifications/notification.tpl @@ -1,4 +1,4 @@ diff --git a/view/templates/notifications/posts_item.tpl b/view/templates/notifications/posts_item.tpl index a8de3c793..07c770fb5 100644 --- a/view/templates/notifications/posts_item.tpl +++ b/view/templates/notifications/posts_item.tpl @@ -1,4 +1,4 @@ \ No newline at end of file diff --git a/view/theme/frio/templates/notifications/notify.tpl b/view/theme/frio/templates/notifications/notify.tpl deleted file mode 100644 index 58f3b0da9..000000000 --- a/view/theme/frio/templates/notifications/notify.tpl +++ /dev/null @@ -1,12 +0,0 @@ - -