diff --git a/database.sql b/database.sql index a1e5b00d92..b717da4460 100644 --- a/database.sql +++ b/database.sql @@ -1462,23 +1462,6 @@ CREATE TABLE IF NOT EXISTS `user-contact` ( FOREIGN KEY (`uid`) REFERENCES `user` (`uid`) ON UPDATE RESTRICT ON DELETE CASCADE ) DEFAULT COLLATE utf8mb4_general_ci COMMENT='User specific public contact data'; --- --- TABLE user-item --- -CREATE TABLE IF NOT EXISTS `user-item` ( - `iid` int unsigned NOT NULL DEFAULT 0 COMMENT 'Item id', - `uid` mediumint unsigned NOT NULL DEFAULT 0 COMMENT 'User id', - `hidden` boolean NOT NULL DEFAULT '0' COMMENT 'Marker to hide an item from the user', - `ignored` boolean COMMENT 'Ignore this thread if set', - `pinned` boolean COMMENT 'The item is pinned on the profile page', - `notification-type` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '', - PRIMARY KEY(`uid`,`iid`), - INDEX `uid_pinned` (`uid`,`pinned`), - INDEX `iid_uid` (`iid`,`uid`), - FOREIGN KEY (`iid`) REFERENCES `item` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, - FOREIGN KEY (`uid`) REFERENCES `user` (`uid`) ON UPDATE RESTRICT ON DELETE CASCADE -) DEFAULT COLLATE utf8mb4_general_ci COMMENT='User specific item data'; - -- -- TABLE worker-ipc -- diff --git a/include/api.php b/include/api.php index 789a13d866..4c90ab80dc 100644 --- a/include/api.php +++ b/include/api.php @@ -43,7 +43,6 @@ use Friendica\Model\Notification; use Friendica\Model\Photo; use Friendica\Model\Post; use Friendica\Model\User; -use Friendica\Model\UserItem; use Friendica\Model\Verb; use Friendica\Network\HTTPException; use Friendica\Network\HTTPException\BadRequestException; @@ -2175,9 +2174,9 @@ function api_statuses_mentions($type) AND (`uid` = 0 OR (`uid` = ? AND NOT `global`)) AND `id` > ?"; $condition = [GRAVITY_PARENT, GRAVITY_COMMENT, api_user(), - UserItem::NOTIF_EXPLICIT_TAGGED | UserItem::NOTIF_IMPLICIT_TAGGED | - UserItem::NOTIF_THREAD_COMMENT | UserItem::NOTIF_DIRECT_COMMENT | - UserItem::NOTIF_DIRECT_THREAD_COMMENT, + Post\UserNotification::NOTIF_EXPLICIT_TAGGED | Post\UserNotification::NOTIF_IMPLICIT_TAGGED | + Post\UserNotification::NOTIF_THREAD_COMMENT | Post\UserNotification::NOTIF_DIRECT_COMMENT | + Post\UserNotification::NOTIF_DIRECT_THREAD_COMMENT, api_user(), $since_id]; if ($max_id > 0) { diff --git a/include/enotify.php b/include/enotify.php index aa5342dcc3..8936cadbe4 100644 --- a/include/enotify.php +++ b/include/enotify.php @@ -32,7 +32,6 @@ use Friendica\Model\Item; use Friendica\Model\Notification; use Friendica\Model\Post; use Friendica\Model\User; -use Friendica\Model\UserItem; use Friendica\Protocol\Activity; /** @@ -592,32 +591,39 @@ function notification($params) /** * Checks for users who should be notified * - * @param int $itemid ID of the item for which the check should be done + * @param int $uri_id URI ID of the item for which the check should be done + * @param int $uid User ID of the item * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ -function check_user_notification($itemid) { - // fetch all users with notifications - $useritems = DBA::select('user-item', ['uid', 'notification-type'], ['iid' => $itemid]); - while ($useritem = DBA::fetch($useritems)) { - check_item_notification($itemid, $useritem['uid'], $useritem['notification-type']); +function check_user_notification(int $uri_id, int $uid) { + $condition = ['uri-id' => $uri_id]; + + // fetch all users with notifications on public posts + if ($uid != 0) { + $condition['uid'] = $uid; } - DBA::close($useritems); + + $usernotifications = DBA::select('post-user-notification', ['uri-id', 'uid', 'notification-type'], $condition); + while ($usernotification = DBA::fetch($usernotifications)) { + check_item_notification($usernotification['uri-id'], $usernotification['uid'], $usernotification['notification-type']); + } + DBA::close($usernotifications); } /** * Checks for item related notifications and sends them * - * @param int $itemid ID of the item for which the check should be done + * @param int $uri_id URI ID of the item for which the check should be done * @param int $uid User ID * @param int $notification_type Notification bits * @return bool * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ -function check_item_notification($itemid, $uid, $notification_type) { +function check_item_notification(int $uri_id, int $uid, int $notification_type) { $fields = ['id', 'uri-id', 'mention', 'parent', 'parent-uri-id', 'thr-parent-id', 'title', 'body', 'author-link', 'author-name', 'author-avatar', 'author-id', 'gravity', 'guid', 'parent-uri', 'uri', 'contact-id', 'network']; - $condition = ['id' => $itemid, 'deleted' => false]; + $condition = ['uri-id' => $uri_id, 'uid' => $uid, 'deleted' => false]; $item = Post::selectFirstForUser($uid, $fields, $condition); if (!DBA::isResult($item)) { return false; @@ -637,19 +643,19 @@ function check_item_notification($itemid, $uid, $notification_type) { $params['link'] = DI::baseUrl() . '/display/' . urlencode($item['guid']); // Set the activity flags - $params['activity']['explicit_tagged'] = ($notification_type & UserItem::NOTIF_EXPLICIT_TAGGED); - $params['activity']['implicit_tagged'] = ($notification_type & UserItem::NOTIF_IMPLICIT_TAGGED); - $params['activity']['origin_comment'] = ($notification_type & UserItem::NOTIF_DIRECT_COMMENT); - $params['activity']['origin_thread'] = ($notification_type & UserItem::NOTIF_THREAD_COMMENT); - $params['activity']['thread_comment'] = ($notification_type & UserItem::NOTIF_COMMENT_PARTICIPATION); - $params['activity']['thread_activity'] = ($notification_type & UserItem::NOTIF_ACTIVITY_PARTICIPATION); + $params['activity']['explicit_tagged'] = ($notification_type & Post\UserNotification::NOTIF_EXPLICIT_TAGGED); + $params['activity']['implicit_tagged'] = ($notification_type & Post\UserNotification::NOTIF_IMPLICIT_TAGGED); + $params['activity']['origin_comment'] = ($notification_type & Post\UserNotification::NOTIF_DIRECT_COMMENT); + $params['activity']['origin_thread'] = ($notification_type & Post\UserNotification::NOTIF_THREAD_COMMENT); + $params['activity']['thread_comment'] = ($notification_type & Post\UserNotification::NOTIF_COMMENT_PARTICIPATION); + $params['activity']['thread_activity'] = ($notification_type & Post\UserNotification::NOTIF_ACTIVITY_PARTICIPATION); // Tagging a user in a direct post (first comment level) means a direct comment - if ($params['activity']['explicit_tagged'] && ($notification_type & UserItem::NOTIF_DIRECT_THREAD_COMMENT)) { + if ($params['activity']['explicit_tagged'] && ($notification_type & Post\UserNotification::NOTIF_DIRECT_THREAD_COMMENT)) { $params['activity']['origin_comment'] = true; } - if ($notification_type & UserItem::NOTIF_SHARED) { + if ($notification_type & Post\UserNotification::NOTIF_SHARED) { $params['type'] = Notification\Type::SHARE; $params['verb'] = Activity::POST; @@ -666,22 +672,22 @@ function check_item_notification($itemid, $uid, $notification_type) { $params['item'] = $parent_item; } } - } elseif ($notification_type & UserItem::NOTIF_EXPLICIT_TAGGED) { + } elseif ($notification_type & Post\UserNotification::NOTIF_EXPLICIT_TAGGED) { $params['type'] = Notification\Type::TAG_SELF; $params['verb'] = Activity::TAG; - } elseif ($notification_type & UserItem::NOTIF_IMPLICIT_TAGGED) { + } elseif ($notification_type & Post\UserNotification::NOTIF_IMPLICIT_TAGGED) { $params['type'] = Notification\Type::COMMENT; $params['verb'] = Activity::POST; - } elseif ($notification_type & UserItem::NOTIF_THREAD_COMMENT) { + } elseif ($notification_type & Post\UserNotification::NOTIF_THREAD_COMMENT) { $params['type'] = Notification\Type::COMMENT; $params['verb'] = Activity::POST; - } elseif ($notification_type & UserItem::NOTIF_DIRECT_COMMENT) { + } elseif ($notification_type & Post\UserNotification::NOTIF_DIRECT_COMMENT) { $params['type'] = Notification\Type::COMMENT; $params['verb'] = Activity::POST; - } elseif ($notification_type & UserItem::NOTIF_COMMENT_PARTICIPATION) { + } elseif ($notification_type & Post\UserNotification::NOTIF_COMMENT_PARTICIPATION) { $params['type'] = Notification\Type::COMMENT; $params['verb'] = Activity::POST; - } elseif ($notification_type & UserItem::NOTIF_ACTIVITY_PARTICIPATION) { + } elseif ($notification_type & Post\UserNotification::NOTIF_ACTIVITY_PARTICIPATION) { $params['type'] = Notification\Type::COMMENT; $params['verb'] = Activity::POST; } else { diff --git a/src/Database/PostUpdate.php b/src/Database/PostUpdate.php index 51ce0d2b55..3a1333780f 100644 --- a/src/Database/PostUpdate.php +++ b/src/Database/PostUpdate.php @@ -31,7 +31,6 @@ use Friendica\Model\Photo; use Friendica\Model\Post; use Friendica\Model\Post\Category; use Friendica\Model\Tag; -use Friendica\Model\UserItem; use Friendica\Model\Verb; use Friendica\Util\Strings; @@ -168,7 +167,7 @@ class PostUpdate } /** - * update user-item data with notifications + * update user notification data * * @return bool "true" when the job is done * @throws \Friendica\Network\HTTPException\InternalServerErrorException @@ -188,7 +187,7 @@ class PostUpdate $rows = 0; $condition = ["`id` > ?", $id]; $params = ['order' => ['id'], 'limit' => 10000]; - $items = DBA::select('item', ['id'], $condition, $params); + $items = DBA::select('item', ['id', 'uri-id', 'uid'], $condition, $params); if (DBA::errorNo() != 0) { Logger::error('Database error', ['no' => DBA::errorNo(), 'message' => DBA::errorMessage()]); @@ -198,7 +197,7 @@ class PostUpdate while ($item = DBA::fetch($items)) { $id = $item['id']; - UserItem::setNotification($item['id']); + Post\UserNotification::setNotification($item['uri-id'], $item['uid']); ++$rows; } diff --git a/src/Model/Item.php b/src/Model/Item.php index ab6fe15ba3..5041b7ef5b 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -1104,9 +1104,10 @@ class Item self::updateContact($item); - UserItem::setNotification($current_post); + Post\UserNotification::setNotification($item['uri-id'], $item['uid']); - check_user_notification($current_post); + check_user_notification($item['uri-id'], $item['uid']); + //check_user_notification($current_post); // Distribute items to users who subscribed to their tags self::distributeByTags($item); diff --git a/src/Model/Post/UserNotification.php b/src/Model/Post/UserNotification.php index 5dca875aba..9173cfbe2b 100644 --- a/src/Model/Post/UserNotification.php +++ b/src/Model/Post/UserNotification.php @@ -22,12 +22,33 @@ namespace Friendica\Model\Post; use \BadMethodCallException; +use Friendica\Core\Logger; +use Friendica\Core\Hook; use Friendica\Database\Database; use Friendica\Database\DBA; use Friendica\Database\DBStructure; +use Friendica\DI; +use Friendica\Model\Contact; +use Friendica\Model\Post; +use Friendica\Util\Strings; +use Friendica\Model\Tag; +use Friendica\Protocol\Activity; + class UserNotification { + // Notification types + const NOTIF_NONE = 0; + const NOTIF_EXPLICIT_TAGGED = 1; + const NOTIF_IMPLICIT_TAGGED = 2; + const NOTIF_THREAD_COMMENT = 4; + const NOTIF_DIRECT_COMMENT = 8; + const NOTIF_COMMENT_PARTICIPATION = 16; + const NOTIF_ACTIVITY_PARTICIPATION = 32; + const NOTIF_DIRECT_THREAD_COMMENT = 64; + const NOTIF_SHARED = 128; + + /** * Insert a new user notification entry * @@ -96,4 +117,310 @@ class UserNotification { return DBA::delete('post-user-notification', $conditions, $options); } + + /** + * Checks an item for notifications and sets the "notification-type" field + * @ToDo: + * - Check for mentions in posts with "uid=0" where the user hadn't interacted before + * + * @param int $uri_id URI ID + * @param int $uid user ID + */ + 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']; + $item = Post::selectFirst($fields, ['uri-id' => $uri_id, 'uid' => $uid, 'origin' => false]); + if (!DBA::isResult($item)) { + return; + } + + // "Activity::FOLLOW" is an automated activity, so we ignore it here + if ($item['verb'] == Activity::FOLLOW) { + return; + } + + if ($item['uid'] == 0) { + $uids = []; + } else { + // Always include the item user + $uids = [$item['uid']]; + } + + // Add every user who participated so far in this thread + // This can only happen with participations on global items. (means: uid = 0) + $users = DBA::p("SELECT DISTINCT(`contact-uid`) AS `uid` FROM `post-view` + WHERE `contact-uid` != 0 AND `parent-uri-id` = ? AND `uid` = ?", $item['parent-uri-id'], $uid); + while ($user = DBA::fetch($users)) { + $uids[] = $user['uid']; + } + DBA::close($users); + + foreach (array_unique($uids) as $uid) { + self::setNotificationForUser($item, $uid); + } + } + + /** + * Checks an item for notifications for the given user and sets the "notification-type" field + * + * @param array $item Item array + * @param int $uid User ID + */ + private static function setNotificationForUser(array $item, int $uid) + { + if (Post\ThreadUser::getIgnored($item['parent-uri-id'], $uid)) { + return; + } + + $notification_type = self::NOTIF_NONE; + + if (self::checkShared($item, $uid)) { + $notification_type = $notification_type | self::NOTIF_SHARED; + } + + $profiles = self::getProfileForUser($uid); + + // Fetch all contacts for the given profiles + $contacts = []; + $ret = DBA::select('contact', ['id'], ['uid' => 0, 'nurl' => $profiles]); + while ($contact = DBA::fetch($ret)) { + $contacts[] = $contact['id']; + } + DBA::close($ret); + + // Don't create notifications for user's posts + if (in_array($item['author-id'], $contacts)) { + 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 (empty($notification_type)) { + return; + } + + Logger::info('Set notification', ['iid' => $item['id'], 'uri-id' => $item['uri-id'], 'uid' => $uid, 'notification-type' => $notification_type]); + + $fields = ['notification-type' => $notification_type]; + Post\User::update($item['uri-id'], $uid, $fields); + self::update($item['uri-id'], $uid, $fields, true); + } + + /** + * Fetch all profiles (contact URL) of a given user + * @param int $uid User ID + * + * @return array Profile links + */ + private static function getProfileForUser(int $uid) + { + $notification_data = ['uid' => $uid, 'profiles' => []]; + Hook::callAll('check_item_notification', $notification_data); + + $profiles = $notification_data['profiles']; + + $user = DBA::selectFirst('user', ['nickname'], ['uid' => $uid]); + if (!DBA::isResult($user)) { + return []; + } + + $owner = DBA::selectFirst('contact', ['url', 'alias'], ['self' => true, 'uid' => $uid]); + if (!DBA::isResult($owner)) { + return []; + } + + // This is our regular URL format + $profiles[] = $owner['url']; + + // Now the alias + $profiles[] = $owner['alias']; + + // Notifications from Diaspora are often with an URL in the Diaspora format + $profiles[] = DI::baseUrl() . '/u/' . $user['nickname']; + + // Validate and add profile links + foreach ($profiles AS $key => $profile) { + // Check for invalid profile urls (without scheme, host or path) and remove them + if (empty(parse_url($profile, PHP_URL_SCHEME)) || empty(parse_url($profile, PHP_URL_HOST)) || empty(parse_url($profile, PHP_URL_PATH))) { + unset($profiles[$key]); + continue; + } + + // Add the normalized form + $profile = Strings::normaliseLink($profile); + $profiles[] = $profile; + + // Add the SSL form + $profile = str_replace('http://', 'https://', $profile); + $profiles[] = $profile; + } + + return array_unique($profiles); + } + + /** + * Check for a "shared" notification for every new post of contacts from the given user + * @param array $item + * @param int $uid User ID + * @return bool A contact had shared something + */ + private static function checkShared(array $item, int $uid) + { + // Only check on original posts and reshare ("announce") activities, otherwise return + if (($item['gravity'] != GRAVITY_PARENT) && ($item['verb'] != Activity::ANNOUNCE)) { + return false; + } + + // Check if the contact posted or shared something directly + if (DBA::exists('contact', ['id' => $item['contact-id'], 'notify_new_posts' => true])) { + return true; + } + + // The following check doesn't make sense on activities, so quit here + if ($item['verb'] == Activity::ANNOUNCE) { + return false; + } + + // Check if the contact is a mentioned forum + $tags = DBA::select('tag-view', ['url'], ['uri-id' => $item['uri-id'], 'type' => [Tag::MENTION, Tag::EXCLUSIVE_MENTION]]); + while ($tag = DBA::fetch($tags)) { + $condition = ['nurl' => Strings::normaliseLink($tag['url']), 'uid' => $uid, 'notify_new_posts' => true, 'contact-type' => Contact::TYPE_COMMUNITY]; + if (DBA::exists('contact', $condition)) { + return true; + } + } + DBA::close($tags); + + return false; + } + + /** + * Check for an implicit mention (only tag, no body) of the given user + * @param array $item + * @param array $profiles Profile links + * @return bool The user is mentioned + */ + private static function checkImplicitMention(array $item, array $profiles) + { + $mentions = Tag::getByURIId($item['uri-id'], [Tag::IMPLICIT_MENTION]); + foreach ($mentions as $mention) { + foreach ($profiles as $profile) { + if (Strings::compareLink($profile, $mention['url'])) { + return true; + } + } + } + + return false; + } + + /** + * Check for an explicit mention (tag and body) of the given user + * @param array $item + * @param array $profiles Profile links + * @return bool The user is mentioned + */ + private static function checkExplicitMention(array $item, array $profiles) + { + $mentions = Tag::getByURIId($item['uri-id'], [Tag::MENTION, Tag::EXCLUSIVE_MENTION]); + foreach ($mentions as $mention) { + foreach ($profiles as $profile) { + if (Strings::compareLink($profile, $mention['url'])) { + return true; + } + } + } + + return false; + } + + /** + * Check if the given user had created this thread + * @param array $item + * @param array $contacts Array of contact IDs + * @return bool The user had created this thread + */ + private static function checkCommentedThread(array $item, array $contacts) + { + $condition = ['parent' => $item['parent'], 'author-id' => $contacts, 'deleted' => false, 'gravity' => GRAVITY_PARENT]; + return Post::exists($condition); + } + + /** + * Check for a direct comment to a post of the given user + * @param array $item + * @param array $contacts Array of contact IDs + * @return bool The item is a direct comment to a user comment + */ + private static function checkDirectComment(array $item, array $contacts) + { + $condition = ['uri' => $item['thr-parent'], 'uid' => $item['uid'], 'author-id' => $contacts, 'deleted' => false, 'gravity' => GRAVITY_COMMENT]; + return Post::exists($condition); + } + + /** + * Check for a direct comment to the starting post of the given user + * @param array $item + * @param array $contacts Array of contact IDs + * @return bool The user had created this thread + */ + private static function checkDirectCommentedThread(array $item, array $contacts) + { + $condition = ['uri' => $item['thr-parent'], 'uid' => $item['uid'], 'author-id' => $contacts, 'deleted' => false, 'gravity' => GRAVITY_PARENT]; + return Post::exists($condition); + } + + /** + * Check if the user had commented in this thread + * @param array $item + * @param array $contacts Array of contact IDs + * @return bool The user had commented in the thread + */ + private static function checkCommentedParticipation(array $item, array $contacts) + { + $condition = ['parent' => $item['parent'], 'author-id' => $contacts, 'deleted' => false, 'gravity' => GRAVITY_COMMENT]; + return Post::exists($condition); + } + + /** + * Check if the user had interacted in this thread (Like, Dislike, ...) + * @param array $item + * @param array $contacts Array of contact IDs + * @return bool The user had interacted in the thread + */ + private static function checkActivityParticipation(array $item, array $contacts) + { + $condition = ['parent' => $item['parent'], 'author-id' => $contacts, 'deleted' => false, 'gravity' => GRAVITY_ACTIVITY]; + return Post::exists($condition); + } } diff --git a/src/Model/UserItem.php b/src/Model/UserItem.php deleted file mode 100644 index 3405d9a6a5..0000000000 --- a/src/Model/UserItem.php +++ /dev/null @@ -1,350 +0,0 @@ -. - * - */ - -namespace Friendica\Model; - -use Friendica\Core\Logger; -use Friendica\Core\Hook; -use Friendica\Database\DBA; -use Friendica\DI; -use Friendica\Util\Strings; -use Friendica\Model\Tag; -use Friendica\Protocol\Activity; - -class UserItem -{ - // Notification types - const NOTIF_NONE = 0; - const NOTIF_EXPLICIT_TAGGED = 1; - const NOTIF_IMPLICIT_TAGGED = 2; - const NOTIF_THREAD_COMMENT = 4; - const NOTIF_DIRECT_COMMENT = 8; - const NOTIF_COMMENT_PARTICIPATION = 16; - const NOTIF_ACTIVITY_PARTICIPATION = 32; - const NOTIF_DIRECT_THREAD_COMMENT = 64; - const NOTIF_SHARED = 128; - - /** - * Checks an item for notifications and sets the "notification-type" field - * @ToDo: - * - Check for mentions in posts with "uid=0" where the user hadn't interacted before - * - * @param int $iid Item ID - */ - public static function setNotification(int $iid) - { - $fields = ['id', 'uri-id', 'parent-uri-id', 'uid', 'body', 'parent', 'gravity', - 'private', 'contact-id', 'thr-parent', 'parent-uri-id', 'parent-uri', 'author-id', 'verb']; - $item = Post::selectFirst($fields, ['id' => $iid, 'origin' => false]); - if (!DBA::isResult($item)) { - return; - } - - // "Activity::FOLLOW" is an automated activity, so we ignore it here - if ($item['verb'] == Activity::FOLLOW) { - return; - } - - if ($item['uid'] == 0) { - $uids = []; - } else { - // Always include the item user - $uids = [$item['uid']]; - } - - // Add every user who participated so far in this thread - // This can only happen with participations on global items. (means: uid = 0) - $users = DBA::p("SELECT DISTINCT(`contact-uid`) AS `uid` FROM `post-view` - WHERE `contact-uid` != 0 AND `parent` IN (SELECT `parent` FROM `post-view` WHERE `id` = ?)", $iid); - while ($user = DBA::fetch($users)) { - $uids[] = $user['uid']; - } - DBA::close($users); - - foreach (array_unique($uids) as $uid) { - self::setNotificationForUser($item, $uid); - } - } - - /** - * Checks an item for notifications for the given user and sets the "notification-type" field - * - * @param array $item Item array - * @param int $uid User ID - */ - private static function setNotificationForUser(array $item, int $uid) - { - if (Post\ThreadUser::getIgnored($item['parent-uri-id'], $uid)) { - return; - } - - $notification_type = self::NOTIF_NONE; - - if (self::checkShared($item, $uid)) { - $notification_type = $notification_type | self::NOTIF_SHARED; - } - - $profiles = self::getProfileForUser($uid); - - // Fetch all contacts for the given profiles - $contacts = []; - $ret = DBA::select('contact', ['id'], ['uid' => 0, 'nurl' => $profiles]); - while ($contact = DBA::fetch($ret)) { - $contacts[] = $contact['id']; - } - DBA::close($ret); - - // Don't create notifications for user's posts - if (in_array($item['author-id'], $contacts)) { - 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 (empty($notification_type)) { - return; - } - - Logger::info('Set notification', ['iid' => $item['id'], 'uri-id' => $item['uri-id'], 'uid' => $uid, 'notification-type' => $notification_type]); - - $fields = ['notification-type' => $notification_type]; - Post\User::update($item['uri-id'], $uid, $fields); - Post\UserNotification::update($item['uri-id'], $uid, $fields, true); - DBA::update('user-item', $fields, ['iid' => $item['id'], 'uid' => $uid], true); - } - - /** - * Fetch all profiles (contact URL) of a given user - * @param int $uid User ID - * - * @return array Profile links - */ - private static function getProfileForUser(int $uid) - { - $notification_data = ['uid' => $uid, 'profiles' => []]; - Hook::callAll('check_item_notification', $notification_data); - - $profiles = $notification_data['profiles']; - - $user = DBA::selectFirst('user', ['nickname'], ['uid' => $uid]); - if (!DBA::isResult($user)) { - return []; - } - - $owner = DBA::selectFirst('contact', ['url', 'alias'], ['self' => true, 'uid' => $uid]); - if (!DBA::isResult($owner)) { - return []; - } - - // This is our regular URL format - $profiles[] = $owner['url']; - - // Now the alias - $profiles[] = $owner['alias']; - - // Notifications from Diaspora are often with an URL in the Diaspora format - $profiles[] = DI::baseUrl() . '/u/' . $user['nickname']; - - // Validate and add profile links - foreach ($profiles AS $key => $profile) { - // Check for invalid profile urls (without scheme, host or path) and remove them - if (empty(parse_url($profile, PHP_URL_SCHEME)) || empty(parse_url($profile, PHP_URL_HOST)) || empty(parse_url($profile, PHP_URL_PATH))) { - unset($profiles[$key]); - continue; - } - - // Add the normalized form - $profile = Strings::normaliseLink($profile); - $profiles[] = $profile; - - // Add the SSL form - $profile = str_replace('http://', 'https://', $profile); - $profiles[] = $profile; - } - - return array_unique($profiles); - } - - /** - * Check for a "shared" notification for every new post of contacts from the given user - * @param array $item - * @param int $uid User ID - * @return bool A contact had shared something - */ - private static function checkShared(array $item, int $uid) - { - // Only check on original posts and reshare ("announce") activities, otherwise return - if (($item['gravity'] != GRAVITY_PARENT) && ($item['verb'] != Activity::ANNOUNCE)) { - return false; - } - - // Check if the contact posted or shared something directly - if (DBA::exists('contact', ['id' => $item['contact-id'], 'notify_new_posts' => true])) { - return true; - } - - // The following check doesn't make sense on activities, so quit here - if ($item['verb'] == Activity::ANNOUNCE) { - return false; - } - - // Check if the contact is a mentioned forum - $tags = DBA::select('tag-view', ['url'], ['uri-id' => $item['uri-id'], 'type' => [Tag::MENTION, Tag::EXCLUSIVE_MENTION]]); - while ($tag = DBA::fetch($tags)) { - $condition = ['nurl' => Strings::normaliseLink($tag['url']), 'uid' => $uid, 'notify_new_posts' => true, 'contact-type' => Contact::TYPE_COMMUNITY]; - if (DBA::exists('contact', $condition)) { - return true; - } - } - DBA::close($tags); - - return false; - } - - /** - * Check for an implicit mention (only tag, no body) of the given user - * @param array $item - * @param array $profiles Profile links - * @return bool The user is mentioned - */ - private static function checkImplicitMention(array $item, array $profiles) - { - $mentions = Tag::getByURIId($item['uri-id'], [Tag::IMPLICIT_MENTION]); - foreach ($mentions as $mention) { - foreach ($profiles as $profile) { - if (Strings::compareLink($profile, $mention['url'])) { - return true; - } - } - } - - return false; - } - - /** - * Check for an explicit mention (tag and body) of the given user - * @param array $item - * @param array $profiles Profile links - * @return bool The user is mentioned - */ - private static function checkExplicitMention(array $item, array $profiles) - { - $mentions = Tag::getByURIId($item['uri-id'], [Tag::MENTION, Tag::EXCLUSIVE_MENTION]); - foreach ($mentions as $mention) { - foreach ($profiles as $profile) { - if (Strings::compareLink($profile, $mention['url'])) { - return true; - } - } - } - - return false; - } - - /** - * Check if the given user had created this thread - * @param array $item - * @param array $contacts Array of contact IDs - * @return bool The user had created this thread - */ - private static function checkCommentedThread(array $item, array $contacts) - { - $condition = ['parent' => $item['parent'], 'author-id' => $contacts, 'deleted' => false, 'gravity' => GRAVITY_PARENT]; - return Post::exists($condition); - } - - /** - * Check for a direct comment to a post of the given user - * @param array $item - * @param array $contacts Array of contact IDs - * @return bool The item is a direct comment to a user comment - */ - private static function checkDirectComment(array $item, array $contacts) - { - $condition = ['uri' => $item['thr-parent'], 'uid' => $item['uid'], 'author-id' => $contacts, 'deleted' => false, 'gravity' => GRAVITY_COMMENT]; - return Post::exists($condition); - } - - /** - * Check for a direct comment to the starting post of the given user - * @param array $item - * @param array $contacts Array of contact IDs - * @return bool The user had created this thread - */ - private static function checkDirectCommentedThread(array $item, array $contacts) - { - $condition = ['uri' => $item['thr-parent'], 'uid' => $item['uid'], 'author-id' => $contacts, 'deleted' => false, 'gravity' => GRAVITY_PARENT]; - return Post::exists($condition); - } - - /** - * Check if the user had commented in this thread - * @param array $item - * @param array $contacts Array of contact IDs - * @return bool The user had commented in the thread - */ - private static function checkCommentedParticipation(array $item, array $contacts) - { - $condition = ['parent' => $item['parent'], 'author-id' => $contacts, 'deleted' => false, 'gravity' => GRAVITY_COMMENT]; - return Post::exists($condition); - } - - /** - * Check if the user had interacted in this thread (Like, Dislike, ...) - * @param array $item - * @param array $contacts Array of contact IDs - * @return bool The user had interacted in the thread - */ - private static function checkActivityParticipation(array $item, array $contacts) - { - $condition = ['parent' => $item['parent'], 'author-id' => $contacts, 'deleted' => false, 'gravity' => GRAVITY_ACTIVITY]; - return Post::exists($condition); - } -} diff --git a/static/dbstructure.config.php b/static/dbstructure.config.php index 776d24486f..d6e21512a6 100644 --- a/static/dbstructure.config.php +++ b/static/dbstructure.config.php @@ -1507,22 +1507,6 @@ return [ "cid" => ["cid"], ] ], - "user-item" => [ - "comment" => "User specific item data", - "fields" => [ - "iid" => ["type" => "int unsigned", "not null" => "1", "default" => "0", "primary" => "1", "foreign" => ["item" => "id"], "comment" => "Item id"], - "uid" => ["type" => "mediumint unsigned", "not null" => "1", "default" => "0", "primary" => "1", "foreign" => ["user" => "uid"], "comment" => "User id"], - "hidden" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "Marker to hide an item from the user"], - "ignored" => ["type" => "boolean", "comment" => "Ignore this thread if set"], - "pinned" => ["type" => "boolean", "comment" => "The item is pinned on the profile page"], - "notification-type" => ["type" => "tinyint unsigned", "not null" => "1", "default" => "0", "comment" => ""], - ], - "indexes" => [ - "PRIMARY" => ["uid", "iid"], - "uid_pinned" => ["uid", "pinned"], - "iid_uid" => ["iid", "uid"] - ] - ], "worker-ipc" => [ "comment" => "Inter process communication between the frontend and the worker", "fields" => [