From 3f0937dd428714c9bd3aa78c1f26b5295149e0b9 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 1 Jun 2021 05:51:03 +0000 Subject: [PATCH] API: Show activity notifications --- database.sql | 29 ++++- mod/display.php | 1 + src/Factory/Api/Mastodon/Notification.php | 54 ++++---- src/Model/Contact.php | 2 + src/Model/Post/UserNotification.php | 122 +++++++++++++----- src/Module/Api/Mastodon/Notifications.php | 42 ++++-- .../Api/Mastodon/Notifications/Clear.php | 2 +- .../Api/Mastodon/Notifications/Dismiss.php | 2 +- src/Repository/Notification.php | 5 + static/dbstructure.config.php | 25 +++- 10 files changed, 204 insertions(+), 80 deletions(-) diff --git a/database.sql b/database.sql index 202d536552..cc2e4b790a 100644 --- a/database.sql +++ b/database.sql @@ -1,6 +1,6 @@ -- ------------------------------------------ -- Friendica 2021.06-rc (Siberian Iris) --- DB_UPDATE_VERSION 1420 +-- DB_UPDATE_VERSION 1421 -- ------------------------------------------ @@ -819,6 +819,33 @@ CREATE TABLE IF NOT EXISTS `manage` ( FOREIGN KEY (`mid`) REFERENCES `user` (`uid`) ON UPDATE RESTRICT ON DELETE CASCADE ) DEFAULT COLLATE utf8mb4_general_ci COMMENT='table of accounts that can manage each other'; +-- +-- TABLE notification +-- +CREATE TABLE IF NOT EXISTS `notification` ( + `id` int unsigned NOT NULL auto_increment COMMENT 'sequential ID', + `uid` mediumint unsigned COMMENT 'Owner User id', + `vid` smallint unsigned COMMENT 'Id of the verb table entry that contains the activity verbs', + `type` tinyint unsigned COMMENT '', + `actor-id` int unsigned COMMENT 'Link to the contact table with uid=0 of the actor that caused the notification', + `target-uri-id` int unsigned COMMENT 'Item-uri id of the related post', + `parent-uri-id` int unsigned COMMENT 'Item-uri id of the parent of the related post', + `created` datetime COMMENT '', + `seen` boolean DEFAULT '0' COMMENT '', + PRIMARY KEY(`id`), + UNIQUE INDEX `uid_vid_type_actor-id_target-uri-id` (`uid`,`vid`,`type`,`actor-id`,`target-uri-id`), + INDEX `vid` (`vid`), + INDEX `actor-id` (`actor-id`), + INDEX `target-uri-id` (`target-uri-id`), + INDEX `parent-uri-id` (`parent-uri-id`), + INDEX `seen_uid` (`seen`,`uid`), + FOREIGN KEY (`uid`) REFERENCES `user` (`uid`) ON UPDATE RESTRICT ON DELETE CASCADE, + FOREIGN KEY (`vid`) REFERENCES `verb` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT, + FOREIGN KEY (`actor-id`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, + FOREIGN KEY (`target-uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, + FOREIGN KEY (`parent-uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE +) DEFAULT COLLATE utf8mb4_general_ci COMMENT='notifications'; + -- -- TABLE notify -- diff --git a/mod/display.php b/mod/display.php index 2750102d7b..c9d39b1db0 100644 --- a/mod/display.php +++ b/mod/display.php @@ -236,6 +236,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()]); } diff --git a/src/Factory/Api/Mastodon/Notification.php b/src/Factory/Api/Mastodon/Notification.php index db5e7d4898..58c1fee9de 100644 --- a/src/Factory/Api/Mastodon/Notification.php +++ b/src/Factory/Api/Mastodon/Notification.php @@ -24,23 +24,18 @@ namespace Friendica\Factory\Api\Mastodon; use Friendica\BaseFactory; use Friendica\Database\DBA; use Friendica\DI; -use Friendica\Model\Contact; -use Friendica\Model\Notification as ModelNotification; +use Friendica\Model\Post; +use Friendica\Model\Verb; +use Friendica\Protocol\Activity; class Notification extends BaseFactory { public function createFromNotifyId(int $id) { - $notification = DBA::selectFirst('notify', [], ['id' => $id]); + $notification = DBA::selectFirst('notification', [], ['id' => $id]); if (!DBA::isResult($notification)) { return null; } - - $cid = Contact::getIdForURL($notification['url'], 0, false); - if (empty($cid)) { - return null; - } - /* follow = Someone followed you follow_request = Someone requested to follow you @@ -51,32 +46,27 @@ class Notification extends BaseFactory status = Someone you enabled notifications for has posted a status */ - switch ($notification['type']) { - case ModelNotification\Type::INTRO: - $type = 'follow_request'; - break; - - case ModelNotification\Type::WALL: - case ModelNotification\Type::COMMENT: - case ModelNotification\Type::MAIL: - case ModelNotification\Type::TAG_SELF: - case ModelNotification\Type::POKE: - $type = 'mention'; - break; - - case ModelNotification\Type::SHARE: - $type = 'status'; - break; - - default: - return null; + if (($notification['vid'] == Verb::getID(Activity::ANNOUNCE)) && + in_array($notification['type'], [Post\UserNotification::NOTIF_DIRECT_COMMENT, Post\UserNotification::NOTIF_DIRECT_THREAD_COMMENT])) { + $type = 'reblog'; + } elseif (in_array($notification['vid'], [Verb::getID(Activity::LIKE), Verb::getID(Activity::DISLIKE)]) && + in_array($notification['type'], [Post\UserNotification::NOTIF_DIRECT_COMMENT, Post\UserNotification::NOTIF_DIRECT_THREAD_COMMENT])) { + $type = 'favourite'; + } elseif ($notification['type'] == Post\UserNotification::NOTIF_SHARED) { + $type = 'status'; + } elseif (in_array($notification['type'], [Post\UserNotification::NOTIF_EXPLICIT_TAGGED, + Post\UserNotification::NOTIF_IMPLICIT_TAGGED, Post\UserNotification::NOTIF_DIRECT_COMMENT, + Post\UserNotification::NOTIF_DIRECT_THREAD_COMMENT, Post\UserNotification::NOTIF_THREAD_COMMENT])) { + $type = 'mention'; + } else { + return null; } - $account = DI::mstdnAccount()->createFromContactId($cid); + $account = DI::mstdnAccount()->createFromContactId($notification['actor-id']); - if (!empty($notification['uri-id'])) { + if (!empty($notification['target-uri-id'])) { try { - $status = DI::mstdnStatus()->createFromUriId($notification['uri-id'], $notification['uid']); + $status = DI::mstdnStatus()->createFromUriId($notification['target-uri-id'], $notification['uid']); } catch (\Throwable $th) { $status = null; } @@ -84,6 +74,6 @@ class Notification extends BaseFactory $status = null; } - return new \Friendica\Object\Api\Mastodon\Notification($id, $type, $notification['date'], $account, $status); + return new \Friendica\Object\Api\Mastodon\Notification($id, $type, $notification['created'], $account, $status); } } diff --git a/src/Model/Contact.php b/src/Model/Contact.php index cd4e608eae..916efe8330 100644 --- a/src/Model/Contact.php +++ b/src/Model/Contact.php @@ -715,6 +715,8 @@ class Contact DBA::update('contact', $fields, ['id' => $self['id']]); // Update the public contact as well + $fields['prvkey'] = null; + $fields['self'] = false; DBA::update('contact', $fields, ['uid' => 0, 'nurl' => $self['nurl']]); // Update the profile diff --git a/src/Model/Post/UserNotification.php b/src/Model/Post/UserNotification.php index ce7ca464e1..306584bbd6 100644 --- a/src/Model/Post/UserNotification.php +++ b/src/Model/Post/UserNotification.php @@ -33,7 +33,7 @@ use Friendica\Model\Post; use Friendica\Util\Strings; use Friendica\Model\Tag; use Friendica\Protocol\Activity; - +use Friendica\Util\DateTimeFormat; class UserNotification { @@ -128,8 +128,8 @@ class UserNotification */ public static function setNotification(int $uri_id, int $uid) { - $fields = ['id', 'uri-id', 'parent-uri-id', 'uid', 'body', 'parent', 'gravity', - 'private', 'contact-id', 'thr-parent', 'parent-uri-id', 'parent-uri', 'author-id', 'verb']; + $fields = ['id', 'uri-id', 'parent-uri-id', 'uid', 'body', 'parent', 'gravity', 'vid', 'gravity', + 'private', 'contact-id', 'thr-parent', 'thr-parent-id', 'parent-uri-id', 'parent-uri', 'author-id', 'verb']; $item = Post::selectFirst($fields, ['uri-id' => $uri_id, 'uid' => $uid, 'origin' => false]); if (!DBA::isResult($item)) { return; @@ -148,7 +148,7 @@ class UserNotification } // Add every user who participated so far in this thread - // This can only happen with participations on global items. (means: uid = 0) + // This can only happen with participations on global items. (means: uid = 0) $users = DBA::p("SELECT DISTINCT(`contact-uid`) AS `uid` FROM `post-user-view` WHERE `contact-uid` != 0 AND `parent-uri-id` = ? AND `uid` = ?", $item['parent-uri-id'], $uid); while ($user = DBA::fetch($users)) { @@ -177,6 +177,10 @@ class UserNotification if (self::checkShared($item, $uid)) { $notification_type = $notification_type | self::NOTIF_SHARED; + self::insertNotication(self::NOTIF_SHARED, $uid, $item); + $notified = true; + } else { + $notified = false; } $profiles = self::getProfileForUser($uid); @@ -194,38 +198,64 @@ class UserNotification return; } - // Only create notifications for posts and comments, not for activities - if (in_array($item['gravity'], [GRAVITY_PARENT, GRAVITY_COMMENT])) { - if (self::checkImplicitMention($item, $profiles)) { - $notification_type = $notification_type | self::NOTIF_IMPLICIT_TAGGED; - } - - if (self::checkExplicitMention($item, $profiles)) { - $notification_type = $notification_type | self::NOTIF_EXPLICIT_TAGGED; - } - - if (self::checkCommentedThread($item, $contacts)) { - $notification_type = $notification_type | self::NOTIF_THREAD_COMMENT; - } - - if (self::checkDirectComment($item, $contacts)) { - $notification_type = $notification_type | self::NOTIF_DIRECT_COMMENT; - } - - if (self::checkDirectCommentedThread($item, $contacts)) { - $notification_type = $notification_type | self::NOTIF_DIRECT_THREAD_COMMENT; - } - - if (self::checkCommentedParticipation($item, $contacts)) { - $notification_type = $notification_type | self::NOTIF_COMMENT_PARTICIPATION; - } - - if (self::checkActivityParticipation($item, $contacts)) { - $notification_type = $notification_type | self::NOTIF_ACTIVITY_PARTICIPATION; + if (self::checkExplicitMention($item, $profiles)) { + $notification_type = $notification_type | self::NOTIF_EXPLICIT_TAGGED; + if (!$notified) { + self::insertNotication( self::NOTIF_EXPLICIT_TAGGED, $uid, $item); + $notified = true; } } - if (empty($notification_type)) { + if (self::checkImplicitMention($item, $profiles)) { + $notification_type = $notification_type | self::NOTIF_IMPLICIT_TAGGED; + if (!$notified) { + self::insertNotication(self::NOTIF_IMPLICIT_TAGGED, $uid, $item); + $notified = true; + } + } + + if (self::checkDirectComment($item, $contacts)) { + $notification_type = $notification_type | self::NOTIF_DIRECT_COMMENT; + if (!$notified) { + self::insertNotication(self::NOTIF_DIRECT_COMMENT, $uid, $item); + $notified = true; + } + } + + if (self::checkDirectCommentedThread($item, $contacts)) { + $notification_type = $notification_type | self::NOTIF_DIRECT_THREAD_COMMENT; + if (!$notified) { + self::insertNotication(self::NOTIF_DIRECT_THREAD_COMMENT, $uid, $item); + $notified = true; + } + } + + if (self::checkCommentedThread($item, $contacts)) { + $notification_type = $notification_type | self::NOTIF_THREAD_COMMENT; + if (!$notified) { + self::insertNotication(self::NOTIF_THREAD_COMMENT, $uid, $item); + $notified = true; + } + } + + if (self::checkCommentedParticipation($item, $contacts)) { + $notification_type = $notification_type | self::NOTIF_COMMENT_PARTICIPATION; + if (!$notified) { + self::insertNotication(self::NOTIF_COMMENT_PARTICIPATION, $uid, $item); + $notified = true; + } + } + + if (self::checkActivityParticipation($item, $contacts)) { + $notification_type = $notification_type | self::NOTIF_ACTIVITY_PARTICIPATION; + if (!$notified) { + self::insertNotication(self::NOTIF_ACTIVITY_PARTICIPATION, $uid, $item); + $notified = true; + } + } + + // Only create notifications for posts and comments, not for activities + if (empty($notification_type) || !in_array($item['gravity'], [GRAVITY_PARENT, GRAVITY_COMMENT])) { return; } @@ -236,6 +266,32 @@ class UserNotification self::update($item['uri-id'], $uid, $fields, true); } + private static function insertNotication(int $type, int $uid, array $item) + { + if (($item['gravity'] == GRAVITY_ACTIVITY) && + !in_array($type, [self::NOTIF_DIRECT_COMMENT, self::NOTIF_DIRECT_THREAD_COMMENT])) { + // Activities are only stored when performed on the user's post or comment + return; + } + + $fields = [ + 'uid' => $uid, + 'vid' => $item['vid'], + 'type' => $type, + 'actor-id' => $item['author-id'], + 'parent-uri-id' => $item['parent-uri-id'], + 'created' => DateTimeFormat::utcNow(), + ]; + + if ($item['gravity'] == GRAVITY_ACTIVITY) { + $fields['target-uri-id'] = $item['thr-parent-id']; + } else { + $fields['target-uri-id'] = $item['uri-id']; + } + + dba::insert('notification', $fields); + } + /** * Fetch all profiles (contact URL) of a given user * @param int $uid User ID diff --git a/src/Module/Api/Mastodon/Notifications.php b/src/Module/Api/Mastodon/Notifications.php index 9a606c2aa7..4a25224d88 100644 --- a/src/Module/Api/Mastodon/Notifications.php +++ b/src/Module/Api/Mastodon/Notifications.php @@ -25,8 +25,10 @@ use Friendica\Core\System; use Friendica\Database\DBA; use Friendica\DI; use Friendica\Model\Contact; -use Friendica\Model\Notification; +use Friendica\Model\Post; +use Friendica\Model\Verb; use Friendica\Module\BaseApi; +use Friendica\Protocol\Activity; /** * @see https://docs.joinmastodon.org/methods/notifications/ @@ -63,7 +65,7 @@ class Notifications extends BaseApi $params = ['order' => ['id' => true], 'limit' => $request['limit']]; - $condition = ['uid' => $uid, 'seen' => false, 'type' => []]; + $condition = ['uid' => $uid, 'seen' => false]; if (!empty($request['account_id'])) { $contact = Contact::getById($request['account_id'], ['url']); @@ -72,17 +74,32 @@ class Notifications extends BaseApi } } - if (!in_array('follow_request', $request['exclude_types'])) { - $condition['type'] = array_merge($condition['type'], [Notification\Type::INTRO]); + if (in_array('follow_request', $request['exclude_types'])) { + $condition = DBA::mergeConditions($condition, ["NOT `vid` IN (?)", Verb::getID(Activity::FOLLOW)]); } - if (!in_array('mention', $request['exclude_types'])) { - $condition['type'] = array_merge($condition['type'], - [Notification\Type::WALL, Notification\Type::COMMENT, Notification\Type::MAIL, Notification\Type::TAG_SELF, Notification\Type::POKE]); + if (in_array('favourite', $request['exclude_types'])) { + $condition = DBA::mergeConditions($condition, ["(NOT `vid` IN (?, ?) OR NOT `type` IN (?, ?))", + Verb::getID(Activity::LIKE), Verb::getID(Activity::DISLIKE), + Post\UserNotification::NOTIF_DIRECT_COMMENT, Post\UserNotification::NOTIF_THREAD_COMMENT]); } - if (!in_array('status', $request['exclude_types'])) { - $condition['type'] = array_merge($condition['type'], [Notification\Type::SHARE]); + if (in_array('reblog', $request['exclude_types'])) { + $condition = DBA::mergeConditions($condition, ["(NOT `vid` IN (?) OR NOT `type` IN (?, ?))", + Verb::getID(Activity::ANNOUNCE), + Post\UserNotification::NOTIF_DIRECT_COMMENT, Post\UserNotification::NOTIF_THREAD_COMMENT]); + } + + if (in_array('mention', $request['exclude_types'])) { + $condition = DBA::mergeConditions($condition, ["(NOT `vid` IN (?) OR NOT `type` IN (?, ?, ?, ?, ?))", + Verb::getID(Activity::POST), Post\UserNotification::NOTIF_EXPLICIT_TAGGED, + Post\UserNotification::NOTIF_IMPLICIT_TAGGED, Post\UserNotification::NOTIF_DIRECT_COMMENT, + Post\UserNotification::NOTIF_DIRECT_THREAD_COMMENT, Post\UserNotification::NOTIF_THREAD_COMMENT]); + } + + if (in_array('status', $request['exclude_types'])) { + $condition = DBA::mergeConditions($condition, ["(NOT `vid` IN (?) OR NOT `type` IN (?))", + Verb::getID(Activity::POST), Post\UserNotification::NOTIF_SHARED]); } if (!empty($request['max_id'])) { @@ -101,9 +118,12 @@ class Notifications extends BaseApi $notifications = []; - $notify = DBA::select('notify', ['id'], $condition, $params); + $notify = DBA::select('notification', ['id'], $condition, $params); while ($notification = DBA::fetch($notify)) { - $notifications[] = DI::mstdnNotification()->createFromNotifyId($notification['id']); + $entry = DI::mstdnNotification()->createFromNotifyId($notification['id']); + if (!empty($entry)) { + $notifications[] = $entry; + } } if (!empty($request['min_id'])) { diff --git a/src/Module/Api/Mastodon/Notifications/Clear.php b/src/Module/Api/Mastodon/Notifications/Clear.php index c809ad2af9..b6961eb532 100644 --- a/src/Module/Api/Mastodon/Notifications/Clear.php +++ b/src/Module/Api/Mastodon/Notifications/Clear.php @@ -35,7 +35,7 @@ class Clear extends BaseApi self::login(self::SCOPE_WRITE); $uid = self::getCurrentUserID(); - DBA::update('notify', ['seen' => true], ['uid' => $uid]); + DBA::update('notification', ['seen' => true], ['uid' => $uid]); System::jsonExit([]); } diff --git a/src/Module/Api/Mastodon/Notifications/Dismiss.php b/src/Module/Api/Mastodon/Notifications/Dismiss.php index a0f57a4058..4c8d3deb9a 100644 --- a/src/Module/Api/Mastodon/Notifications/Dismiss.php +++ b/src/Module/Api/Mastodon/Notifications/Dismiss.php @@ -40,7 +40,7 @@ class Dismiss extends BaseApi DI::mstdnError()->UnprocessableEntity(); } - DBA::update('notify', ['seen' => true], ['uid' => $uid, 'id' => $parameters['id']]); + DBA::update('notification', ['seen' => true], ['uid' => $uid, 'id' => $parameters['id']]); System::jsonExit([]); } diff --git a/src/Repository/Notification.php b/src/Repository/Notification.php index 39abdad199..1748759b60 100644 --- a/src/Repository/Notification.php +++ b/src/Repository/Notification.php @@ -87,8 +87,13 @@ class Notification extends BaseRepository 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, diff --git a/static/dbstructure.config.php b/static/dbstructure.config.php index e3b1bea30c..961321e0fa 100644 --- a/static/dbstructure.config.php +++ b/static/dbstructure.config.php @@ -55,7 +55,7 @@ use Friendica\Database\DBA; if (!defined('DB_UPDATE_VERSION')) { - define('DB_UPDATE_VERSION', 1420); + define('DB_UPDATE_VERSION', 1421); } return [ @@ -882,6 +882,29 @@ return [ "mid" => ["mid"], ] ], + "notification" => [ + "comment" => "notifications", + "fields" => [ + "id" => ["type" => "int unsigned", "not null" => "1", "extra" => "auto_increment", "primary" => "1", "comment" => "sequential ID"], + "uid" => ["type" => "mediumint unsigned", "foreign" => ["user" => "uid"], "comment" => "Owner User id"], + "vid" => ["type" => "smallint unsigned", "foreign" => ["verb" => "id", "on delete" => "restrict"], "comment" => "Id of the verb table entry that contains the activity verbs"], + "type" => ["type" => "tinyint unsigned", "comment" => ""], + "actor-id" => ["type" => "int unsigned", "foreign" => ["contact" => "id"], "comment" => "Link to the contact table with uid=0 of the actor that caused the notification"], + "target-uri-id" => ["type" => "int unsigned", "foreign" => ["item-uri" => "id"], "comment" => "Item-uri id of the related post"], + "parent-uri-id" => ["type" => "int unsigned", "foreign" => ["item-uri" => "id"], "comment" => "Item-uri id of the parent of the related post"], + "created" => ["type" => "datetime", "comment" => ""], + "seen" => ["type" => "boolean", "default" => "0", "comment" => ""], + ], + "indexes" => [ + "PRIMARY" => ["id"], + "uid_vid_type_actor-id_target-uri-id" => ["UNIQUE", "uid", "vid", "type", "actor-id", "target-uri-id"], + "vid" => ["vid"], + "actor-id" => ["actor-id"], + "target-uri-id" => ["target-uri-id"], + "parent-uri-id" => ["parent-uri-id"], + "seen_uid" => ["seen", "uid"], + ] + ], "notify" => [ "comment" => "notifications", "fields" => [