From 15162b4027c7726a325cc1907aa269bc7b45aeea Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 31 Jan 2021 18:32:22 +0000 Subject: [PATCH 01/14] New table "post-thread-user" --- include/api.php | 10 ++-- src/Model/Item.php | 4 ++ src/Model/Post.php | 33 +++++++++--- src/Model/Post/ThreadUser.php | 99 +++++++++++++++++++++++++++++++++++ static/dbstructure.config.php | 18 +++++++ update.php | 23 ++++++-- 6 files changed, 170 insertions(+), 17 deletions(-) create mode 100644 src/Model/Post/ThreadUser.php diff --git a/include/api.php b/include/api.php index bcf14d6a8..8543eef9e 100644 --- a/include/api.php +++ b/include/api.php @@ -2170,10 +2170,10 @@ function api_statuses_mentions($type) $start = max(0, ($page - 1) * $count); - $query = "`gravity` IN (?, ?) AND `id` IN (SELECT `iid` FROM `user-item` + $query = "`gravity` IN (?, ?) AND `uri-id` IN (SELECT `uri-id` FROM `post-user` WHERE (`hidden` IS NULL OR NOT `hidden`) AND - `uid` = ? AND `notification-type` & ? != 0 - AND `iid` > ?"; + `uid` = ? AND `notification-type` & ? != 0) + AND `id` > ?"; $condition = [GRAVITY_PARENT, GRAVITY_COMMENT, api_user(), UserItem::NOTIF_EXPLICIT_TAGGED | UserItem::NOTIF_IMPLICIT_TAGGED | @@ -2182,12 +2182,10 @@ function api_statuses_mentions($type) $since_id]; if ($max_id > 0) { - $query .= " AND `iid` <= ?"; + $query .= " AND `id` <= ?"; $condition[] = $max_id; } - $query .= ")"; - array_unshift($condition, $query); $params = ['order' => ['id' => true], 'limit' => [$start, $count]]; diff --git a/src/Model/Item.php b/src/Model/Item.php index 6d882463f..bb371e532 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -1099,6 +1099,10 @@ class Item $id = Post\User::insert($item['uri-id'], $item['uid'], $item); if ($id) { + if ($item['gravity'] == GRAVITY_PARENT) { + Post\ThreadUser::insert($item['uri-id'], $item['uid'], $item); + } + // Remove all fields that aren't part of the item table foreach ($item as $field => $value) { if (!in_array($field, $structure['item'])) { diff --git a/src/Model/Post.php b/src/Model/Post.php index 59e2f419c..5bb41d16c 100644 --- a/src/Model/Post.php +++ b/src/Model/Post.php @@ -258,7 +258,7 @@ class Post AND (NOT `causer-blocked` OR `causer-id` = ?) AND NOT `contact-blocked` AND ((NOT `contact-readonly` AND NOT `contact-pending` AND (`contact-rel` IN (?, ?))) OR `self` OR `gravity` != ? OR `contact-uid` = ?) - AND NOT EXISTS (SELECT `iid` FROM `user-item` WHERE `hidden` AND `iid` = `id` AND `uid` = ?) + AND NOT EXISTS (SELECT `uri-id` FROM `post-user` WHERE `hidden` AND `uri-id` = `" . $view . "`.`uri-id` AND `uid` = ?) AND NOT EXISTS (SELECT `cid` FROM `user-contact` WHERE `uid` = ? AND `cid` = `author-id` AND `blocked`) AND NOT EXISTS (SELECT `cid` FROM `user-contact` WHERE `uid` = ? AND `cid` = `owner-id` AND `blocked`) AND NOT EXISTS (SELECT `cid` FROM `user-contact` WHERE `uid` = ? AND `cid` = `author-id` AND `ignored` AND `gravity` = ?) @@ -431,6 +431,8 @@ class Post unset($fields['parent-uri']); unset($fields['parent-uri-id']); + $thread_condition = DBA::mergeConditions($condition, ['gravity' => GRAVITY_PARENT]); + // To ensure the data integrity we do it in an transaction DBA::transaction(); @@ -472,13 +474,32 @@ class Post $affected = max($affected, DBA::affectedRows()); } + $update_fields = DBStructure::getFieldsForTable('post-thread-user', $fields); + if (!empty($update_fields)) { + $rows = DBA::selectToArray('post-view', ['post-user-id'], $thread_condition); + $thread_puids = array_column($rows, 'post-user-id'); + + $post_thread_condition = DBA::collapseCondition(['id' => $thread_puids]); + + $post_thread_condition[0] = "EXISTS(SELECT `id` FROM `post-user` WHERE " . + $post_thread_condition[0] . " AND `uri-id` = `post-thread-user`.`uri-id` AND `uid` = `post-thread-user`.`uid`)"; + Logger::info('Test2-start', ['condition' => $post_thread_condition]); + if (!DBA::update('post-thread-user', $update_fields, $post_thread_condition)) { + DBA::rollback(); + Logger::notice('Updating post-thread-user failed', ['fields' => $update_fields, 'condition' => $condition]); + return false; + } + Logger::info('Test2-end'); + $affected = max($affected, DBA::affectedRows()); + } + $update_fields = DBStructure::getFieldsForTable('thread', $fields); if (!empty($update_fields)) { - $rows = DBA::selectToArray('post-view', ['id'], $condition); + $rows = DBA::selectToArray('post-view', ['id'], $thread_condition); $ids = array_column($rows, 'id'); if (!DBA::update('thread', $update_fields, ['iid' => $ids])) { DBA::rollback(); - Logger::notice('Updating thread failed', ['fields' => $update_fields, 'condition' => $condition]); + Logger::notice('Updating thread failed', ['fields' => $update_fields, 'condition' => $thread_condition]); return false; } $affected = max($affected, DBA::affectedRows()); @@ -496,10 +517,8 @@ class Post } } if (!empty($update_fields)) { - if (empty($ids)) { - $rows = DBA::selectToArray('post-view', ['id'], $condition, []); - $ids = array_column($rows, 'id'); - } + $rows = DBA::selectToArray('post-view', ['id'], $condition, []); + $ids = array_column($rows, 'id'); if (!DBA::update('item', $update_fields, ['id' => $ids])) { DBA::rollback(); Logger::notice('Updating item failed', ['fields' => $update_fields, 'condition' => $condition]); diff --git a/src/Model/Post/ThreadUser.php b/src/Model/Post/ThreadUser.php new file mode 100644 index 000000000..c824b44c8 --- /dev/null +++ b/src/Model/Post/ThreadUser.php @@ -0,0 +1,99 @@ +. + * + */ + +namespace Friendica\Model\Post; + +use \BadMethodCallException; +use Friendica\Database\Database; +use Friendica\Database\DBA; +use Friendica\Database\DBStructure; + +class ThreadUser +{ + /** + * Insert a new URI user entry + * + * @param integer $uri_id + * @param integer $uid + * @param array $fields + * @return bool success + * @throws \Exception + */ + public static function insert(int $uri_id, int $uid, array $data = []) + { + if (empty($uri_id)) { + throw new BadMethodCallException('Empty URI_id'); + } + + $fields = DBStructure::getFieldsForTable('post-thread-user', $data); + + // Additionally assign the key fields + $fields['uri-id'] = $uri_id; + $fields['uid'] = $uid; + + return DBA::insert('post-thread-user', $fields, Database::INSERT_IGNORE); + } + + /** + * Update a URI user entry + * + * @param integer $uri_id + * @param integer $uid + * @param array $data + * @param bool $insert_if_missing + * @return bool + * @throws \Exception + */ + public static function update(int $uri_id, int $uid, array $data = [], bool $insert_if_missing = false) + { + if (empty($uri_id)) { + throw new BadMethodCallException('Empty URI_id'); + } + + $fields = DBStructure::getFieldsForTable('post-thread-user', $data); + + // Remove the key fields + unset($fields['uri-id']); + unset($fields['uid']); + + if (empty($fields)) { + return true; + } + + return DBA::update('post-thread-user', $fields, ['uri-id' => $uri_id, 'uid' => $uid], $insert_if_missing ? true : []); + } + + /** + * Delete a row from the post-thread-user table + * + * @param array $conditions Field condition(s) + * @param array $options + * - cascade: If true we delete records in other tables that depend on the one we're deleting through + * relations (default: true) + * + * @return boolean was the delete successful? + * @throws \Exception + */ + public static function delete(array $conditions, array $options = []) + { + return DBA::delete('post-thread-user', $conditions, $options); + } +} diff --git a/static/dbstructure.config.php b/static/dbstructure.config.php index d0c35e4f8..e38e23469 100644 --- a/static/dbstructure.config.php +++ b/static/dbstructure.config.php @@ -1193,6 +1193,24 @@ return [ "cid" => ["cid"] ] ], + "post-thread-user" => [ + "comment" => "Thread related data per user", + "fields" => [ + "uri-id" => ["type" => "int unsigned", "not null" => "1", "primary" => "1", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the item uri"], + "uid" => ["type" => "mediumint unsigned", "not null" => "1", "default" => "0", "primary" => "1", "foreign" => ["user" => "uid"], "comment" => "Owner id which owns this copy of the item"], + "pinned" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "The thread is pinned on the profile page"], + "starred" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => ""], + "ignored" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "Ignore updates for this thread"], + "wall" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "This item was posted to the wall of uid"], + "pubmail" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => ""], + "forum_mode" => ["type" => "tinyint unsigned", "not null" => "1", "default" => "0", "comment" => ""] + ], + "indexes" => [ + "PRIMARY" => ["uid", "uri-id"], + "uid_wall" => ["uid", "wall"], + "uri-id" => ["uri-id"], + ] + ], "post-user" => [ "comment" => "User specific post data", "fields" => [ diff --git a/update.php b/update.php index 3e9292f2e..f2dd66577 100644 --- a/update.php +++ b/update.php @@ -466,7 +466,7 @@ function pre_update_1364() return Update::FAILED; } - if (!DBA::e("DELETE FROM `user-item` WHERE NOT `uid` IN (SELECT `uid` FROM `user`)")) { + if (DBStructure::existsTable('user-item') && !DBA::e("DELETE FROM `user-item` WHERE NOT `uid` IN (SELECT `uid` FROM `user`)")) { return Update::FAILED; } @@ -518,7 +518,7 @@ function pre_update_1364() return Update::FAILED; } - if (!DBA::e("DELETE FROM `user-item` WHERE NOT `iid` IN (SELECT `id` FROM `item`)")) { + if (DBStructure::existsTable('user-item') && !DBA::e("DELETE FROM `user-item` WHERE NOT `iid` IN (SELECT `id` FROM `item`)")) { return Update::FAILED; } @@ -686,7 +686,7 @@ function update_1395() return Update::FAILED; } - if (!DBA::e("INSERT INTO `post-user`(`uri-id`, `uid`, `hidden`, `notification-type`) + if (DBStructure::existsTable('user-item') && !DBA::e("INSERT INTO `post-user`(`uri-id`, `uid`, `hidden`, `notification-type`) SELECT `uri-id`, `user-item`.`uid`, `hidden`,`notification-type` FROM `user-item` INNER JOIN `item` ON `item`.`id` = `user-item`.`iid` ON DUPLICATE KEY UPDATE `hidden` = `user-item`.`hidden`, `notification-type` = `user-item`.`notification-type`")) { @@ -713,4 +713,19 @@ function update_1396() return Update::FAILED; } return Update::SUCCESS; -} \ No newline at end of file +} + +function update_139x() +{ + if (!DBStructure::existsTable('thread') || DBStructure::existsTable('user-item')) { + return Update::SUCCESS; + } + + if (!DBA::e("INSERT IGNORE INTO `post-thread-user`(`uri-id`, `uid`, `pinned`, `starred`, `ignored`, `wall`, `pubmail`, `forum_mode`) + SELECT `thread`.`uri-id`, `thread`.`uid`, `user-item`.`pinned`, `thread`.`starred`, + `thread`.`ignored`, `thread`.`wall`, `thread`.`pubmail`, `thread`.`forum_mode` + FROM `thread` LEFT JOIN `user-item` ON `user-item`.`iid` = `thread`.`iid`")) { + return Update::FAILED; + } + return Update::SUCCESS; +} From c1d99d6c4cefe482c29ad0a8b75b6c8288dec2ce Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 31 Jan 2021 23:37:34 +0000 Subject: [PATCH 02/14] Most user-item traces removed --- database.sql | 32 ++++++++++--- include/enotify.php | 5 +-- src/Factory/Api/Mastodon/Status.php | 4 +- src/Model/Item.php | 69 +++-------------------------- src/Model/Post.php | 46 +++++-------------- src/Model/Post/ThreadUser.php | 54 ++++++++++++++++++++++ src/Model/UserItem.php | 5 +-- src/Module/Item/Ignore.php | 22 +++------ src/Module/Item/Pin.php | 16 +++++-- src/Object/Post.php | 20 ++++----- static/dbstructure.config.php | 7 +-- static/dbview.config.php | 8 ++-- update.php | 4 +- 13 files changed, 139 insertions(+), 153 deletions(-) diff --git a/database.sql b/database.sql index 9afb7cb0f..a74d4ea6a 100644 --- a/database.sql +++ b/database.sql @@ -1,6 +1,6 @@ -- ------------------------------------------ -- Friendica 2021.03-dev (Red Hot Poker) --- DB_UPDATE_VERSION 1396 +-- DB_UPDATE_VERSION 1397 -- ------------------------------------------ @@ -684,7 +684,6 @@ CREATE TABLE IF NOT EXISTS `item` ( `guid` varchar(255) NOT NULL DEFAULT '' COMMENT 'A unique identifier for this item', `uri` varchar(255) NOT NULL DEFAULT '' COMMENT '', `uri-id` int unsigned COMMENT 'Id of the item-uri table entry that contains the item uri', - `uri-hash` varchar(80) NOT NULL DEFAULT '' COMMENT 'RIPEMD-128 hash from uri', `parent` int unsigned COMMENT 'item.id of the parent to this item if it is a reply of some form; otherwise this must be set to the id of this item', `parent-uri` varchar(255) NOT NULL DEFAULT '' COMMENT 'uri of the top-level parent to this item', `parent-uri-id` int unsigned COMMENT 'Id of the item-uri table that contains the top-level parent uri', @@ -720,6 +719,7 @@ CREATE TABLE IF NOT EXISTS `item` ( `psid` int unsigned COMMENT 'ID of the permission set of this post', `resource-id` varchar(32) NOT NULL DEFAULT '' COMMENT 'Used to link other tables to items, it identifies the linked resource (e.g. photo) and if set must also set resource_type', `event-id` int unsigned COMMENT 'Used to link to the event.id', + `uri-hash` varchar(80) COMMENT 'Deprecated', `iaid` int unsigned COMMENT 'Deprecated', `icid` int unsigned COMMENT 'Deprecated', `attach` mediumtext COMMENT 'Deprecated', @@ -1141,6 +1141,26 @@ CREATE TABLE IF NOT EXISTS `post-tag` ( FOREIGN KEY (`cid`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT ) DEFAULT COLLATE utf8mb4_general_ci COMMENT='post relation to tags'; +-- +-- TABLE post-thread-user +-- +CREATE TABLE IF NOT EXISTS `post-thread-user` ( + `uri-id` int unsigned NOT NULL COMMENT 'Id of the item-uri table entry that contains the item uri', + `uid` mediumint unsigned NOT NULL DEFAULT 0 COMMENT 'Owner id which owns this copy of the item', + `pinned` boolean NOT NULL DEFAULT '0' COMMENT 'The thread is pinned on the profile page', + `starred` boolean NOT NULL DEFAULT '0' COMMENT '', + `ignored` boolean NOT NULL DEFAULT '0' COMMENT 'Ignore updates for this thread', + `wall` boolean NOT NULL DEFAULT '0' COMMENT 'This item was posted to the wall of uid', + `pubmail` boolean NOT NULL DEFAULT '0' COMMENT '', + `forum_mode` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '', + PRIMARY KEY(`uid`,`uri-id`), + INDEX `uid_wall` (`uid`,`wall`), + INDEX `uid_pinned` (`uid`,`pinned`), + INDEX `uri-id` (`uri-id`), + FOREIGN KEY (`uri-id`) REFERENCES `item-uri` (`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='Thread related data per user'; + -- -- TABLE post-user -- @@ -1848,13 +1868,13 @@ CREATE VIEW `network-item-view` AS SELECT FROM `item` INNER JOIN `thread` ON `thread`.`iid` = `item`.`parent` STRAIGHT_JOIN `contact` ON `contact`.`id` = `thread`.`contact-id` - LEFT JOIN `user-item` ON `user-item`.`iid` = `item`.`id` AND `user-item`.`uid` = `thread`.`uid` + LEFT JOIN `post-user` ON `post-user`.`uri-id` = `item`.`uri-id` AND `post-user`.`uid` = `thread`.`uid` LEFT JOIN `user-contact` AS `author` ON `author`.`uid` = `thread`.`uid` AND `author`.`cid` = `thread`.`author-id` LEFT JOIN `user-contact` AS `owner` ON `owner`.`uid` = `thread`.`uid` AND `owner`.`cid` = `thread`.`owner-id` LEFT JOIN `contact` AS `ownercontact` ON `ownercontact`.`id` = `thread`.`owner-id` WHERE `thread`.`visible` AND NOT `thread`.`deleted` AND NOT `thread`.`moderated` AND (NOT `contact`.`readonly` AND NOT `contact`.`blocked` AND NOT `contact`.`pending`) - AND (`user-item`.`hidden` IS NULL OR NOT `user-item`.`hidden`) + AND (`post-user`.`hidden` IS NULL OR NOT `post-user`.`hidden`) AND (`author`.`blocked` IS NULL OR NOT `author`.`blocked`) AND (`owner`.`blocked` IS NULL OR NOT `owner`.`blocked`); @@ -1879,13 +1899,13 @@ CREATE VIEW `network-thread-view` AS SELECT FROM `thread` STRAIGHT_JOIN `contact` ON `contact`.`id` = `thread`.`contact-id` STRAIGHT_JOIN `item` ON `item`.`id` = `thread`.`iid` - LEFT JOIN `user-item` ON `user-item`.`iid` = `item`.`id` AND `user-item`.`uid` = `thread`.`uid` + LEFT JOIN `post-user` ON `post-user`.`uri-id` = `item`.`uri-id` AND `post-user`.`uid` = `thread`.`uid` LEFT JOIN `user-contact` AS `author` ON `author`.`uid` = `thread`.`uid` AND `author`.`cid` = `thread`.`author-id` LEFT JOIN `user-contact` AS `owner` ON `owner`.`uid` = `thread`.`uid` AND `owner`.`cid` = `thread`.`owner-id` LEFT JOIN `contact` AS `ownercontact` ON `ownercontact`.`id` = `thread`.`owner-id` WHERE `thread`.`visible` AND NOT `thread`.`deleted` AND NOT `thread`.`moderated` AND (NOT `contact`.`readonly` AND NOT `contact`.`blocked` AND NOT `contact`.`pending`) - AND (`user-item`.`hidden` IS NULL OR NOT `user-item`.`hidden`) + AND (`post-user`.`hidden` IS NULL OR NOT `post-user`.`hidden`) AND (`author`.`blocked` IS NULL OR NOT `author`.`blocked`) AND (`owner`.`blocked` IS NULL OR NOT `owner`.`blocked`); diff --git a/include/enotify.php b/include/enotify.php index eb5660327..aa5342dcc 100644 --- a/include/enotify.php +++ b/include/enotify.php @@ -149,9 +149,8 @@ function notification($params) } if ($params['type'] == Notification\Type::COMMENT || $params['type'] == Notification\Type::TAG_SELF) { - $thread = Post::selectFirstThreadForUser($params['uid'], ['ignored'], ['iid' => $parent_id, 'deleted' => false]); - if (DBA::isResult($thread) && $thread['ignored']) { - Logger::log('Thread ' . $parent_id . ' will be ignored', Logger::DEBUG); + if (Post\ThreadUser::getIgnored($parent_uri_id, $params['uid'])) { + Logger::info('Thread is ignored', ['parent' => $parent_id, 'parent-uri-id' => $parent_uri_id]); return false; } diff --git a/src/Factory/Api/Mastodon/Status.php b/src/Factory/Api/Mastodon/Status.php index cd611e8ee..6a3212c26 100644 --- a/src/Factory/Api/Mastodon/Status.php +++ b/src/Factory/Api/Mastodon/Status.php @@ -77,9 +77,9 @@ class Status extends BaseFactory $userAttributes = new \Friendica\Object\Api\Mastodon\Status\UserAttributes( Post::exists(['thr-parent-id' => $uriId, 'uid' => $uid, 'origin' => true, 'gravity' => GRAVITY_ACTIVITY, 'vid' => Verb::getID(Activity::LIKE)]), Post::exists(['thr-parent-id' => $uriId, 'uid' => $uid, 'origin' => true, 'gravity' => GRAVITY_ACTIVITY, 'vid' => Verb::getID(Activity::ANNOUNCE)]), - DBA::exists('thread', ['iid' => $item['id'], 'uid' => $item['uid'], 'ignored' => true]), + Post\ThreadUser::getIgnored($uriId, $item['uid']), (bool)$item['starred'], - DBA::exists('user-item', ['iid' => $item['id'], 'uid' => $item['uid'], 'pinned' => true]) + Post\ThreadUser::getPinned($uriId, $item['uid']) ); $sensitive = DBA::exists('tag-view', ['uri-id' => $uriId, 'name' => 'nsfw']); diff --git a/src/Model/Item.php b/src/Model/Item.php index bb371e532..9edafaf5f 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -31,7 +31,6 @@ use Friendica\Core\Session; use Friendica\Core\System; use Friendica\Model\Tag; use Friendica\Core\Worker; -use Friendica\Database\Database; use Friendica\Database\DBA; use Friendica\Database\DBStructure; use Friendica\DI; @@ -133,49 +132,6 @@ class Item const PRIVATE = 1; const UNLISTED = 2; - const TABLES = ['item', 'user-item', 'post-content', 'post-delivery-data', 'diaspora-interaction']; - - private static function getItemFields() - { - $definition = DBStructure::definition('', false); - - $postfields = []; - foreach (self::TABLES as $table) { - $postfields[$table] = array_keys($definition[$table]['fields']); - } - - return $postfields; - } - - /** - * Set the pinned state of an item - * - * @param integer $iid Item ID - * @param integer $uid User ID - * @param boolean $pinned Pinned state - */ - public static function setPinned(int $iid, int $uid, bool $pinned) - { - DBA::update('user-item', ['pinned' => $pinned], ['iid' => $iid, 'uid' => $uid], true); - } - - /** - * Get the pinned state - * - * @param integer $iid Item ID - * @param integer $uid User ID - * - * @return boolean pinned state - */ - public static function getPinned(int $iid, int $uid) - { - $useritem = DBA::selectFirst('user-item', ['pinned'], ['iid' => $iid, 'uid' => $uid]); - if (!DBA::isResult($useritem)) { - return false; - } - return (bool)$useritem['pinned']; - } - /** * Update existing item entries * @@ -285,12 +241,9 @@ class Item Post\User::update($item['uri-id'], $uid, ['hidden' => true], true); } - // "Deleting" global items just means hiding them - if ($item['uid'] == 0) { - DBA::update('user-item', ['hidden' => true], ['iid' => $item['id'], 'uid' => $uid], true); - } elseif ($item['uid'] == $uid) { + if ($item['uid'] == $uid) { self::markForDeletionById($item['id'], PRIORITY_HIGH); - } else { + } elseif ($item['uid'] != 0) { Logger::log('Wrong ownership. Not deleting item ' . $item['id']); } } @@ -393,12 +346,6 @@ class Item } } elseif ($item['uid'] != 0) { Post\User::update($item['uri-id'], $item['uid'], ['hidden' => true]); - - // When we delete just our local user copy of an item, we have to set a marker to hide it - $global_item = Post::selectFirst(['id'], ['uri-id' => $item['uri-id'], 'uid' => 0, 'deleted' => false]); - if (DBA::isResult($global_item)) { - DBA::update('user-item', ['hidden' => true], ['iid' => $global_item['id'], 'uid' => $item['uid']], true); - } } Logger::info('Item has been marked for deletion.', ['id' => $item_id]); @@ -755,8 +702,6 @@ class Item public static function insert($item, $notify = false, $dontcache = false) { - $structure = self::getItemFields(); - $orig_item = $item; $priority = PRIORITY_HIGH; @@ -1103,19 +1048,15 @@ class Item Post\ThreadUser::insert($item['uri-id'], $item['uid'], $item); } - // Remove all fields that aren't part of the item table - foreach ($item as $field => $value) { - if (!in_array($field, $structure['item'])) { - unset($item[$field]); - } - } - $condition = ['uri-id' => $item['uri-id'], 'uid' => $item['uid'], 'network' => $item['network']]; if (Post::exists($condition)) { Logger::notice('Item is already inserted - aborting', $condition); return 0; } + // Remove all fields that aren't part of the item table + $item = DBStructure::getFieldsForTable('item', $item); + $result = DBA::insert('item', $item); // When the item was successfully stored we fetch the ID of the item. diff --git a/src/Model/Post.php b/src/Model/Post.php index 5bb41d16c..c5a4b8ba1 100644 --- a/src/Model/Post.php +++ b/src/Model/Post.php @@ -272,7 +272,7 @@ class Post unset($selected['pinned']); $selected = array_flip($selected); - $select_string = "(SELECT `pinned` FROM `user-item` WHERE `iid` = `" . $view . "`.`id` AND uid=`" . $view . "`.`uid`) AS `pinned`, "; + $select_string = "(SELECT `pinned` FROM `post-thread-user` WHERE `uri-id` = `" . $view . "`.`uri-id` AND uid=`" . $view . "`.`uid`) AS `pinned`, "; } $select_string .= implode(', ', array_map([DBA::class, 'quoteIdentifier'], $selected)); @@ -344,32 +344,6 @@ class Post } } - /** - * Retrieve a single record from the starting post in the item table and returns it in an associative array - * - * @param integer $uid User ID - * @param array $selected - * @param array $condition - * @param array $params - * @return bool|array - * @throws \Exception - * @see DBA::select - */ - public static function selectFirstThreadForUser($uid, array $selected = [], array $condition = [], $params = []) - { - $params['limit'] = 1; - - $result = self::selectThreadForUser($uid, $selected, $condition, $params); - - if (is_bool($result)) { - return $result; - } else { - $row = self::fetch($result); - DBA::close($result); - return $row; - } - } - /** * Select pinned rows from the item table for a given user * @@ -383,24 +357,24 @@ class Post */ public static function selectPinned(int $uid, array $selected = [], array $condition = [], $params = []) { - $useritems = DBA::select('user-item', ['iid'], ['uid' => $uid, 'pinned' => true]); - if (!DBA::isResult($useritems)) { - return $useritems; + $postthreaduser = DBA::select('post-thread-user', ['uri-id'], ['uid' => $uid, 'pinned' => true]); + if (!DBA::isResult($postthreaduser)) { + return $postthreaduser; } - + $pinned = []; - while ($useritem = DBA::fetch($useritems)) { - $pinned[] = $useritem['iid']; + while ($useritem = DBA::fetch($postthreaduser)) { + $pinned[] = $useritem['uri-id']; } - DBA::close($useritems); + DBA::close($postthreaduser); if (empty($pinned)) { return []; } - $condition = DBA::mergeConditions(['iid' => $pinned], $condition); + $condition = DBA::mergeConditions(['uri-id' => $pinned, 'uid' => $uid, 'gravity' => GRAVITY_PARENT], $condition); - return self::selectThreadForUser($uid, $selected, $condition, $params); + return self::selectForUser($uid, $selected, $condition, $params); } /** diff --git a/src/Model/Post/ThreadUser.php b/src/Model/Post/ThreadUser.php index c824b44c8..f0f2f17a0 100644 --- a/src/Model/Post/ThreadUser.php +++ b/src/Model/Post/ThreadUser.php @@ -96,4 +96,58 @@ class ThreadUser { return DBA::delete('post-thread-user', $conditions, $options); } + + /** + * @param int $uri_id + * @param int $uid + * @return bool + * @throws Exception + */ + public static function getIgnored(int $uri_id, int $uid) + { + $threaduser = DBA::selectFirst('post-thread-user', ['ignored'], ['uri-id' => $uri_id, 'uid' => $uid]); + if (empty($threaduser)) { + return false; + } + return (bool)$threaduser['ignored']; + } + + /** + * @param int $uri_id + * @param int $uid + * @param int $ignored + * @return void + * @throws Exception + */ + public static function setIgnored(int $uri_id, int $uid, int $ignored) + { + DBA::update('post-thread-user', ['ignored' => $ignored], ['uri-id' => $uri_id, 'uid' => $uid], true); + } + + /** + * @param int $uri_id + * @param int $uid + * @return bool + * @throws Exception + */ + public static function getPinned(int $uri_id, int $uid) + { + $threaduser = DBA::selectFirst('post-thread-user', ['pinned'], ['uri-id' => $uri_id, 'uid' => $uid]); + if (empty($threaduser)) { + return false; + } + return (bool)$threaduser['pinned']; + } + + /** + * @param int $uri_id + * @param int $uid + * @param int $pinned + * @return void + * @throws Exception + */ + public static function setPinned(int $uri_id, int $uid, int $pinned) + { + DBA::update('post-thread-user', ['pinned' => $pinned], ['uri-id' => $uri_id, 'uid' => $uid], true); + } } diff --git a/src/Model/UserItem.php b/src/Model/UserItem.php index b4346c221..e4badadb0 100644 --- a/src/Model/UserItem.php +++ b/src/Model/UserItem.php @@ -52,7 +52,7 @@ class UserItem public static function setNotification(int $iid) { $fields = ['id', 'uri-id', 'parent-uri-id', 'uid', 'body', 'parent', 'gravity', - 'private', 'contact-id', 'thr-parent', 'parent-uri', 'author-id', 'verb']; + '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; @@ -92,8 +92,7 @@ class UserItem */ private static function setNotificationForUser(array $item, int $uid) { - $thread = Post::selectFirstThreadForUser($uid, ['ignored'], ['iid' => $item['parent'], 'deleted' => false]); - if (!empty($thread['ignored'])) { + if (Post\ThreadUser::getIgnored($item['parent-uri-id'], $uid)) { return; } diff --git a/src/Module/Item/Ignore.php b/src/Module/Item/Ignore.php index 668e49310..22d676b22 100644 --- a/src/Module/Item/Ignore.php +++ b/src/Module/Item/Ignore.php @@ -49,27 +49,17 @@ class Ignore extends BaseModule $dba = DI::dba(); - $thread = Post::selectFirstThreadForUser(local_user(), ['uid', 'ignored'], ['iid' => $itemId]); + $thread = Post::selectFirst(['uri-id', 'uid'], ['id' => $itemId, 'gravity' => GRAVITY_PARENT]); if (!$dba->isResult($thread)) { throw new HTTPException\NotFoundException(); } - // Numeric values are needed for the json output further below - $ignored = !empty($thread['ignored']) ? 0 : 1; + $ignored = !Post\ThreadUser::getIgnored($thread['uri-id'], local_user()); - switch ($thread['uid'] ?? 0) { - // if the thread is from the current user - case local_user(): - $dba->update('thread', ['ignored' => $ignored], ['iid' => $itemId]); - break; - // 0 (null will get transformed to 0) => it's a public post - case 0: - $dba->update('user-item', ['ignored' => $ignored], ['iid' => $itemId, 'uid' => local_user()], true); - break; - // Throws a BadRequestException and not a ForbiddenException on purpose - // Avoids harvesting existing, but forbidden IIDs (security issue) - default: - throw new HTTPException\BadRequestException(); + if (in_array($thread['uid'], [0, local_user()])) { + Post\ThreadUser::setIgnored($thread['uri-id'], local_user(), $ignored); + } else { + throw new HTTPException\BadRequestException(); } // See if we've been passed a return path to redirect to diff --git a/src/Module/Item/Pin.php b/src/Module/Item/Pin.php index d99b1a345..08c751de2 100644 --- a/src/Module/Item/Pin.php +++ b/src/Module/Item/Pin.php @@ -24,8 +24,9 @@ namespace Friendica\Module\Item; use Friendica\BaseModule; use Friendica\Core\Session; use Friendica\Core\System; +use Friendica\Database\DBA; use Friendica\DI; -use Friendica\Model\Item; +use Friendica\Model\Post; use Friendica\Network\HTTPException; /** @@ -47,9 +48,18 @@ class Pin extends BaseModule $itemId = intval($parameters['id']); - $pinned = !Item::getPinned($itemId, local_user()); + $item = Post::selectFirst(['uri-id', 'uid'], ['id' => $itemId]); + if (!DBA::isResult($item)) { + throw new HTTPException\NotFoundException(); + } - Item::setPinned($itemId, local_user(), $pinned); + if (!in_array($item['uid'], [0, local_user()])) { + throw new HttpException\ForbiddenException($l10n->t('Access denied.')); + } + + $pinned = !Post\ThreadUser::getPinned($item['uri-id'], local_user()); + + Post\ThreadUser::setPinned($item['uri-id'], local_user(), $pinned); // See if we've been passed a return path to redirect to $return_path = $_REQUEST['return'] ?? ''; diff --git a/src/Object/Post.php b/src/Object/Post.php index 34f46f797..d2c0b31e1 100644 --- a/src/Object/Post.php +++ b/src/Object/Post.php @@ -307,17 +307,15 @@ class Post if ($this->isToplevel()) { if(local_user()) { - $thread = PostModel::selectFirstThreadForUser(local_user(), ['ignored'], ['iid' => $item['id']]); - if (DBA::isResult($thread)) { - $ignore = [ - 'do' => DI::l10n()->t("ignore thread"), - 'undo' => DI::l10n()->t("unignore thread"), - 'toggle' => DI::l10n()->t("toggle ignore status"), - 'classdo' => $thread['ignored'] ? "hidden" : "", - 'classundo' => $thread['ignored'] ? "" : "hidden", - 'ignored' => DI::l10n()->t('ignored'), - ]; - } + $ignored = PostModel\ThreadUser::getIgnored($item['uri-id'], local_user()); + $ignore = [ + 'do' => DI::l10n()->t("ignore thread"), + 'undo' => DI::l10n()->t("unignore thread"), + 'toggle' => DI::l10n()->t("toggle ignore status"), + 'classdo' => $ignored ? "hidden" : "", + 'classundo' => $ignored ? "" : "hidden", + 'ignored' => DI::l10n()->t('ignored'), + ]; if ($conv->getProfileOwner() == local_user() && ($item['uid'] != 0)) { if ($origin) { diff --git a/static/dbstructure.config.php b/static/dbstructure.config.php index e38e23469..f201a3a5c 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', 1396); + define('DB_UPDATE_VERSION', 1397); } return [ @@ -750,7 +750,6 @@ return [ "guid" => ["type" => "varchar(255)", "not null" => "1", "default" => "", "comment" => "A unique identifier for this item"], "uri" => ["type" => "varchar(255)", "not null" => "1", "default" => "", "comment" => ""], "uri-id" => ["type" => "int unsigned", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the item uri"], - "uri-hash" => ["type" => "varchar(80)", "not null" => "1", "default" => "", "comment" => "RIPEMD-128 hash from uri"], "parent" => ["type" => "int unsigned", "relation" => ["item" => "id"], "comment" => "item.id of the parent to this item if it is a reply of some form; otherwise this must be set to the id of this item"], "parent-uri" => ["type" => "varchar(255)", "not null" => "1", "default" => "", "comment" => "uri of the top-level parent to this item"], "parent-uri-id" => ["type" => "int unsigned", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table that contains the top-level parent uri"], @@ -774,7 +773,7 @@ return [ "visible" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => ""], "moderated" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => ""], "deleted" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "item has been deleted"], - // User specific fields. Eventually they will move to user-item + // User specific fields. Eventually they will move to post-user and post-thread-user "uid" => ["type" => "mediumint unsigned", "not null" => "1", "default" => "0", "foreign" => ["user" => "uid"], "comment" => "Owner id which owns this copy of the item"], "contact-id" => ["type" => "int unsigned", "not null" => "1", "default" => "0", "foreign" => ["contact" => "id"], "comment" => "contact.id"], "wall" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "This item was posted to the wall of uid"], @@ -789,6 +788,7 @@ return [ "resource-id" => ["type" => "varchar(32)", "not null" => "1", "default" => "", "comment" => "Used to link other tables to items, it identifies the linked resource (e.g. photo) and if set must also set resource_type"], "event-id" => ["type" => "int unsigned", "relation" => ["event" => "id"], "comment" => "Used to link to the event.id"], // Deprecated fields. Will be removed in upcoming versions + "uri-hash" => ["type" => "varchar(80)", "comment" => "Deprecated"], "iaid" => ["type" => "int unsigned", "comment" => "Deprecated"], "icid" => ["type" => "int unsigned", "comment" => "Deprecated"], "attach" => ["type" => "mediumtext", "comment" => "Deprecated"], @@ -1208,6 +1208,7 @@ return [ "indexes" => [ "PRIMARY" => ["uid", "uri-id"], "uid_wall" => ["uid", "wall"], + "uid_pinned" => ["uid", "pinned"], "uri-id" => ["uri-id"], ] ], diff --git a/static/dbview.config.php b/static/dbview.config.php index c7f52a5cf..088e426ba 100644 --- a/static/dbview.config.php +++ b/static/dbview.config.php @@ -395,13 +395,13 @@ "query" => "FROM `item` INNER JOIN `thread` ON `thread`.`iid` = `item`.`parent` STRAIGHT_JOIN `contact` ON `contact`.`id` = `thread`.`contact-id` - LEFT JOIN `user-item` ON `user-item`.`iid` = `item`.`id` AND `user-item`.`uid` = `thread`.`uid` + LEFT JOIN `post-user` ON `post-user`.`uri-id` = `item`.`uri-id` AND `post-user`.`uid` = `thread`.`uid` LEFT JOIN `user-contact` AS `author` ON `author`.`uid` = `thread`.`uid` AND `author`.`cid` = `thread`.`author-id` LEFT JOIN `user-contact` AS `owner` ON `owner`.`uid` = `thread`.`uid` AND `owner`.`cid` = `thread`.`owner-id` LEFT JOIN `contact` AS `ownercontact` ON `ownercontact`.`id` = `thread`.`owner-id` WHERE `thread`.`visible` AND NOT `thread`.`deleted` AND NOT `thread`.`moderated` AND (NOT `contact`.`readonly` AND NOT `contact`.`blocked` AND NOT `contact`.`pending`) - AND (`user-item`.`hidden` IS NULL OR NOT `user-item`.`hidden`) + AND (`post-user`.`hidden` IS NULL OR NOT `post-user`.`hidden`) AND (`author`.`blocked` IS NULL OR NOT `author`.`blocked`) AND (`owner`.`blocked` IS NULL OR NOT `owner`.`blocked`)" ], @@ -424,13 +424,13 @@ "query" => "FROM `thread` STRAIGHT_JOIN `contact` ON `contact`.`id` = `thread`.`contact-id` STRAIGHT_JOIN `item` ON `item`.`id` = `thread`.`iid` - LEFT JOIN `user-item` ON `user-item`.`iid` = `item`.`id` AND `user-item`.`uid` = `thread`.`uid` + LEFT JOIN `post-user` ON `post-user`.`uri-id` = `item`.`uri-id` AND `post-user`.`uid` = `thread`.`uid` LEFT JOIN `user-contact` AS `author` ON `author`.`uid` = `thread`.`uid` AND `author`.`cid` = `thread`.`author-id` LEFT JOIN `user-contact` AS `owner` ON `owner`.`uid` = `thread`.`uid` AND `owner`.`cid` = `thread`.`owner-id` LEFT JOIN `contact` AS `ownercontact` ON `ownercontact`.`id` = `thread`.`owner-id` WHERE `thread`.`visible` AND NOT `thread`.`deleted` AND NOT `thread`.`moderated` AND (NOT `contact`.`readonly` AND NOT `contact`.`blocked` AND NOT `contact`.`pending`) - AND (`user-item`.`hidden` IS NULL OR NOT `user-item`.`hidden`) + AND (`post-user`.`hidden` IS NULL OR NOT `post-user`.`hidden`) AND (`author`.`blocked` IS NULL OR NOT `author`.`blocked`) AND (`owner`.`blocked` IS NULL OR NOT `owner`.`blocked`)" ], diff --git a/update.php b/update.php index f2dd66577..05064d8e0 100644 --- a/update.php +++ b/update.php @@ -715,9 +715,9 @@ function update_1396() return Update::SUCCESS; } -function update_139x() +function update_1397() { - if (!DBStructure::existsTable('thread') || DBStructure::existsTable('user-item')) { + if (!DBStructure::existsTable('thread') || !DBStructure::existsTable('user-item')) { return Update::SUCCESS; } From 08317036e738d8955ba6442b45b58cee1a03d7ae Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 1 Feb 2021 07:06:01 +0000 Subject: [PATCH 03/14] List of deprecated fields is checked --- database.sql | 6 +- src/Model/Item.php | 126 +++++++++++++--------------------- src/Model/Post.php | 9 +-- static/dbstructure.config.php | 7 +- 4 files changed, 56 insertions(+), 92 deletions(-) diff --git a/database.sql b/database.sql index a74d4ea6a..9fab6668d 100644 --- a/database.sql +++ b/database.sql @@ -717,8 +717,10 @@ CREATE TABLE IF NOT EXISTS `item` ( `mention` boolean NOT NULL DEFAULT '0' COMMENT 'The owner of this item was mentioned in it', `forum_mode` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '', `psid` int unsigned COMMENT 'ID of the permission set of this post', - `resource-id` varchar(32) NOT NULL DEFAULT '' COMMENT 'Used to link other tables to items, it identifies the linked resource (e.g. photo) and if set must also set resource_type', `event-id` int unsigned COMMENT 'Used to link to the event.id', + `type` varchar(20) COMMENT '', + `bookmark` boolean COMMENT '', + `resource-id` varchar(32) COMMENT 'Deprecated', `uri-hash` varchar(80) COMMENT 'Deprecated', `iaid` int unsigned COMMENT 'Deprecated', `icid` int unsigned COMMENT 'Deprecated', @@ -729,8 +731,6 @@ CREATE TABLE IF NOT EXISTS `item` ( `deny_gid` mediumtext COMMENT 'Deprecated', `postopts` text COMMENT 'Deprecated', `inform` mediumtext COMMENT 'Deprecated', - `type` varchar(20) COMMENT 'Deprecated', - `bookmark` boolean COMMENT 'Deprecated', `file` mediumtext COMMENT 'Deprecated', `location` varchar(255) COMMENT 'Deprecated', `coord` varchar(255) COMMENT 'Deprecated', diff --git a/src/Model/Item.php b/src/Model/Item.php index 9edafaf5f..19f382b67 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -98,14 +98,6 @@ class Item 'author-id', 'author-link', 'owner-link', 'contact-uid', 'signed_text', 'network']; - // Field list for "post-content" table that is mixed with the item table - const MIXED_CONTENT_FIELDLIST = ['title', 'content-warning', 'body', 'location', - 'coord', 'app', 'rendered-hash', 'rendered-html', 'verb', - 'object-type', 'object', 'target-type', 'target', 'plink']; - - // Field list for "post-content" table that is not present in the "item" table - const CONTENT_FIELDLIST = ['language', 'raw-body']; - // All fields in the item table const ITEM_FIELDLIST = ['id', 'uid', 'parent', 'uri', 'parent-uri', 'thr-parent', 'guid', 'uri-id', 'parent-uri-id', 'thr-parent-id', 'vid', @@ -120,6 +112,22 @@ class Item 'author-id', 'author-link', 'author-name', 'author-avatar', 'author-network', 'owner-id', 'owner-link', 'owner-name', 'owner-avatar', 'causer-id']; + // Item fiels that still are in use + const USED_FIELDLIST = ['id', 'parent', 'guid', 'uri', 'uri-id', 'parent-uri', 'parent-uri-id', + 'thr-parent', 'thr-parent-id', 'created', 'edited', 'commented', 'received', 'changed', + 'gravity', 'network', 'owner-id', 'author-id', 'causer-id', 'vid', 'extid', 'post-type', + 'global', 'private', 'visible', 'moderated', 'deleted', 'uid', 'contact-id', + 'wall', 'origin', 'pubmail', 'starred', 'unseen', 'mention', 'forum_mode', 'psid', + 'event-id', 'type', 'bookmark']; + + // Legacy item fields that aren't stored any more in the item table + const LEGACY_FIELDLIST = ['uri-hash', 'iaid', 'icid', 'attach', + 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', 'postopts', + 'resource-id', 'inform', 'file', 'location', 'coord', 'tag', 'plink', + 'title', 'content-warning', 'body', 'app', 'verb', 'object-type', 'object', + 'target-type', 'target', 'author-name', 'author-link', 'author-avatar', + 'owner-name', 'owner-link', 'owner-avatar', 'rendered-hash', 'rendered-html']; + // List of all verbs that don't need additional content data. // Never reorder or remove entries from this list. Just add new ones at the end, if needed. const ACTIVITIES = [ @@ -897,26 +905,11 @@ class Item // Update the contact relations Contact\Relation::store($toplevel_parent['author-id'], $item['author-id'], $item['created']); - - unset($item['parent_origin']); } else { $parent_id = 0; $parent_origin = $item['origin']; } - // We don't store the causer link, only the id - unset($item['causer-link']); - - // We don't store these fields anymore in the item table - unset($item['author-link']); - unset($item['author-name']); - unset($item['author-avatar']); - unset($item['author-network']); - - unset($item['owner-link']); - unset($item['owner-name']); - unset($item['owner-avatar']); - $item['parent-uri-id'] = ItemURI::getIdByURI($item['parent-uri']); $item['thr-parent-id'] = ItemURI::getIdByURI($item['thr-parent']); @@ -939,14 +932,10 @@ class Item $item['edit'] = false; $item['parent'] = $parent_id; Hook::callAll('post_local', $item); - unset($item['edit']); } else { Hook::callAll('post_remote', $item); } - // Set after the insert because top-level posts are self-referencing - unset($item['parent']); - if (!empty($item['cancel'])) { Logger::log('post cancelled by addon.'); return 0; @@ -965,15 +954,6 @@ class Item $item['deny_gid'] ); - unset($item['allow_cid']); - unset($item['allow_gid']); - unset($item['deny_cid']); - unset($item['deny_gid']); - - // This array field is used to trigger some automatic reactions - // It is mainly used in the "post_local" hook. - unset($item['api_source']); - if ($item['verb'] == Activity::ANNOUNCE) { self::setOwnerforResharedItem($item); } @@ -985,10 +965,6 @@ class Item // Check for hashtags in the body and repair or add hashtag links $item['body'] = self::setHashtags($item['body']); - if (!empty($item['attach'])) { - Post\Media::insertFromAttachment($item['uri-id'], $item['attach']); - } - // Fill the cache field self::putInCache($item); @@ -998,40 +974,27 @@ class Item $notify_type = Delivery::POST; } + // Filling item related side tables + if (!empty($item['attach'])) { + Post\Media::insertFromAttachment($item['uri-id'], $item['attach']); + } + if (!in_array($item['verb'], self::ACTIVITIES)) { Post\Content::insert($item['uri-id'], $item); } - $body = $item['body']; - $verb = $item['verb']; - - // We just remove everything that is content - foreach (array_merge(self::CONTENT_FIELDLIST, self::MIXED_CONTENT_FIELDLIST) as $field) { - unset($item[$field]); - } - - unset($item['activity']); - - // Filling item related side tables - // Diaspora signature if (!empty($item['diaspora_signed_text'])) { DBA::replace('diaspora-interaction', ['uri-id' => $item['uri-id'], 'interaction' => $item['diaspora_signed_text']]); } - unset($item['diaspora_signed_text']); - // Attached file links if (!empty($item['file'])) { Post\Category::storeTextByURIId($item['uri-id'], $item['uid'], $item['file']); } - unset($item['file']); - // Delivery relevant data $delivery_data = Post\DeliveryData::extractFields($item); - unset($item['postopts']); - unset($item['inform']); if (!empty($item['origin']) || !empty($item['wall']) || !empty($delivery_data['postopts']) || !empty($delivery_data['inform'])) { Post\DeliveryData::insert($item['uri-id'], $delivery_data); @@ -1039,33 +1002,38 @@ class Item // Store tags from the body if this hadn't been handled previously in the protocol classes if (!Tag::existsForPost($item['uri-id'])) { - Tag::storeFromBody($item['uri-id'], $body); + Tag::storeFromBody($item['uri-id'], $item['body']); } $id = Post\User::insert($item['uri-id'], $item['uid'], $item); - if ($id) { - if ($item['gravity'] == GRAVITY_PARENT) { - Post\ThreadUser::insert($item['uri-id'], $item['uid'], $item); - } - - $condition = ['uri-id' => $item['uri-id'], 'uid' => $item['uid'], 'network' => $item['network']]; - if (Post::exists($condition)) { - Logger::notice('Item is already inserted - aborting', $condition); - return 0; - } - - // Remove all fields that aren't part of the item table - $item = DBStructure::getFieldsForTable('item', $item); - - $result = DBA::insert('item', $item); - - // When the item was successfully stored we fetch the ID of the item. - $current_post = DBA::lastInsertId(); - } else { + if (!$id) { Logger::notice('Post-User is already inserted - aborting', ['uid' => $item['uid'], 'uri-id' => $item['uri-id']]); return 0; } + if ($item['gravity'] == GRAVITY_PARENT) { + Post\ThreadUser::insert($item['uri-id'], $item['uid'], $item); + } + + $condition = ['uri-id' => $item['uri-id'], 'uid' => $item['uid'], 'network' => $item['network']]; + if (Post::exists($condition)) { + Logger::notice('Item is already inserted - aborting', $condition); + return 0; + } + + // Remove all fields that aren't part of the item table + $item = DBStructure::getFieldsForTable('item', $item); + + // We remove all legacy fields that now are stored in other tables + foreach (self::LEGACY_FIELDLIST as $field) { + unset($item[$field]); + } + + $result = DBA::insert('item', $item); + + // When the item was successfully stored we fetch the ID of the item. + $current_post = DBA::lastInsertId(); + if (empty($current_post) || !DBA::isResult($result)) { // On failure store the data into a spool file so that the "SpoolPost" worker can try again later. Logger::warning('Could not store item. it will be spooled', ['result' => $result, 'id' => $current_post]); @@ -1091,7 +1059,7 @@ class Item $update_commented = in_array($item['gravity'], [GRAVITY_PARENT, GRAVITY_COMMENT]); } else { // Update when it isn't a follow or tag verb - $update_commented = !in_array($verb, [Activity::FOLLOW, Activity::TAG]); + $update_commented = !in_array($item['verb'], [Activity::FOLLOW, Activity::TAG]); } if ($update_commented) { diff --git a/src/Model/Post.php b/src/Model/Post.php index c5a4b8ba1..5cafdf25a 100644 --- a/src/Model/Post.php +++ b/src/Model/Post.php @@ -192,7 +192,7 @@ class Post $selected = array_merge(['author-addr', 'author-nick', 'owner-addr', 'owner-nick', 'causer-addr', 'causer-nick', 'causer-network', 'photo', 'name-date', 'uri-date', 'avatar-date', 'thumb', 'dfrn-id', 'parent-guid', 'parent-network', 'parent-author-id', 'parent-author-link', 'parent-author-name', - 'parent-author-network', 'signed_text'], Item::DISPLAY_FIELDLIST, Item::ITEM_FIELDLIST, Item::CONTENT_FIELDLIST); + 'parent-author-network', 'signed_text', 'language', 'raw-body'], Item::DISPLAY_FIELDLIST, Item::ITEM_FIELDLIST); if ($view == 'post-thread-view') { $selected = array_merge($selected, ['ignored', 'iid']); @@ -479,13 +479,8 @@ class Post $affected = max($affected, DBA::affectedRows()); } - $item_fields = ['guid', 'type', 'wall', 'gravity', 'extid', 'created', 'edited', 'commented', 'received', 'changed', - 'resource-id', 'post-type', 'private', 'pubmail', 'moderated', 'visible', 'starred', 'bookmark', - 'unseen', 'deleted', 'origin', 'forum_mode', 'mention', 'global', 'network', 'vid', 'psid', - 'contact-id', 'author-id', 'owner-id', 'causer-id', 'event-id']; - $update_fields = []; - foreach ($item_fields as $field) { + foreach (Item::USED_FIELDLIST as $field) { if (array_key_exists($field, $fields)) { $update_fields[$field] = $fields[$field]; } diff --git a/static/dbstructure.config.php b/static/dbstructure.config.php index f201a3a5c..3b833d9e3 100644 --- a/static/dbstructure.config.php +++ b/static/dbstructure.config.php @@ -785,9 +785,12 @@ return [ "forum_mode" => ["type" => "tinyint unsigned", "not null" => "1", "default" => "0", "comment" => ""], "psid" => ["type" => "int unsigned", "foreign" => ["permissionset" => "id", "on delete" => "restrict"], "comment" => "ID of the permission set of this post"], // It has to be decided whether these fields belong to the user or the structure - "resource-id" => ["type" => "varchar(32)", "not null" => "1", "default" => "", "comment" => "Used to link other tables to items, it identifies the linked resource (e.g. photo) and if set must also set resource_type"], "event-id" => ["type" => "int unsigned", "relation" => ["event" => "id"], "comment" => "Used to link to the event.id"], + // Check deprecation status + "type" => ["type" => "varchar(20)", "comment" => ""], + "bookmark" => ["type" => "boolean", "comment" => ""], // Deprecated fields. Will be removed in upcoming versions + "resource-id" => ["type" => "varchar(32)", "comment" => "Deprecated"], "uri-hash" => ["type" => "varchar(80)", "comment" => "Deprecated"], "iaid" => ["type" => "int unsigned", "comment" => "Deprecated"], "icid" => ["type" => "int unsigned", "comment" => "Deprecated"], @@ -798,8 +801,6 @@ return [ "deny_gid" => ["type" => "mediumtext", "comment" => "Deprecated"], "postopts" => ["type" => "text", "comment" => "Deprecated"], "inform" => ["type" => "mediumtext", "comment" => "Deprecated"], - "type" => ["type" => "varchar(20)", "comment" => "Deprecated"], - "bookmark" => ["type" => "boolean", "comment" => "Deprecated"], "file" => ["type" => "mediumtext", "comment" => "Deprecated"], "location" => ["type" => "varchar(255)", "comment" => "Deprecated"], "coord" => ["type" => "varchar(255)", "comment" => "Deprecated"], From 8c9346fc12b57d8737c608625878a8b4adc5ef2d Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 1 Feb 2021 10:31:38 +0000 Subject: [PATCH 04/14] New table "post-user-notification" --- src/Model/Post/User.php | 4 +- src/Model/Post/UserNotifcation.php | 99 ++++++++++++++++++++++++++++++ src/Model/UserItem.php | 3 +- static/dbstructure.config.php | 27 +++++--- 4 files changed, 123 insertions(+), 10 deletions(-) create mode 100644 src/Model/Post/UserNotifcation.php diff --git a/src/Model/Post/User.php b/src/Model/Post/User.php index 6c2176656..dc53b726e 100644 --- a/src/Model/Post/User.php +++ b/src/Model/Post/User.php @@ -29,7 +29,7 @@ use Friendica\Database\DBStructure; class User { /** - * Insert a new URI user entry + * Insert a new post user entry * * @param integer $uri_id * @param integer $uid @@ -66,7 +66,7 @@ class User } /** - * Update a URI user entry + * Update a post user entry * * @param integer $uri_id * @param integer $uid diff --git a/src/Model/Post/UserNotifcation.php b/src/Model/Post/UserNotifcation.php new file mode 100644 index 000000000..5dca875ab --- /dev/null +++ b/src/Model/Post/UserNotifcation.php @@ -0,0 +1,99 @@ +. + * + */ + +namespace Friendica\Model\Post; + +use \BadMethodCallException; +use Friendica\Database\Database; +use Friendica\Database\DBA; +use Friendica\Database\DBStructure; + +class UserNotification +{ + /** + * Insert a new user notification entry + * + * @param integer $uri_id + * @param integer $uid + * @param array $fields + * @return bool success + * @throws \Exception + */ + public static function insert(int $uri_id, int $uid, array $data = []) + { + if (empty($uri_id)) { + throw new BadMethodCallException('Empty URI_id'); + } + + $fields = DBStructure::getFieldsForTable('post-user-notification', $data); + + // Additionally assign the key fields + $fields['uri-id'] = $uri_id; + $fields['uid'] = $uid; + + return DBA::insert('post-user-notification', $fields, Database::INSERT_IGNORE); + } + + /** + * Update a user notification entry + * + * @param integer $uri_id + * @param integer $uid + * @param array $data + * @param bool $insert_if_missing + * @return bool + * @throws \Exception + */ + public static function update(int $uri_id, int $uid, array $data = [], bool $insert_if_missing = false) + { + if (empty($uri_id)) { + throw new BadMethodCallException('Empty URI_id'); + } + + $fields = DBStructure::getFieldsForTable('post-user-notification', $data); + + // Remove the key fields + unset($fields['uri-id']); + unset($fields['uid']); + + if (empty($fields)) { + return true; + } + + return DBA::update('post-user-notification', $fields, ['uri-id' => $uri_id, 'uid' => $uid], $insert_if_missing ? true : []); + } + + /** + * Delete a row from the post-user-notification table + * + * @param array $conditions Field condition(s) + * @param array $options + * - cascade: If true we delete records in other tables that depend on the one we're deleting through + * relations (default: true) + * + * @return boolean was the delete successful? + * @throws \Exception + */ + public static function delete(array $conditions, array $options = []) + { + return DBA::delete('post-user-notification', $conditions, $options); + } +} diff --git a/src/Model/UserItem.php b/src/Model/UserItem.php index e4badadb0..3405d9a6a 100644 --- a/src/Model/UserItem.php +++ b/src/Model/UserItem.php @@ -152,10 +152,11 @@ class UserItem return; } - Logger::info('Set notification', ['iid' => $item['id'], 'uid' => $uid, 'notification-type' => $notification_type]); + 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); } diff --git a/static/dbstructure.config.php b/static/dbstructure.config.php index 3b833d9e3..362f299c1 100644 --- a/static/dbstructure.config.php +++ b/static/dbstructure.config.php @@ -773,22 +773,23 @@ return [ "visible" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => ""], "moderated" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => ""], "deleted" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "item has been deleted"], - // User specific fields. Eventually they will move to post-user and post-thread-user + // Part of "post-user". Will be deprecated in a later step "uid" => ["type" => "mediumint unsigned", "not null" => "1", "default" => "0", "foreign" => ["user" => "uid"], "comment" => "Owner id which owns this copy of the item"], "contact-id" => ["type" => "int unsigned", "not null" => "1", "default" => "0", "foreign" => ["contact" => "id"], "comment" => "contact.id"], - "wall" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "This item was posted to the wall of uid"], - "origin" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "item originated at this site"], - "pubmail" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => ""], - "starred" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "item has been favourited"], "unseen" => ["type" => "boolean", "not null" => "1", "default" => "1", "comment" => "item has not been seen"], - "mention" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "The owner of this item was mentioned in it"], - "forum_mode" => ["type" => "tinyint unsigned", "not null" => "1", "default" => "0", "comment" => ""], + "origin" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "item originated at this site"], "psid" => ["type" => "int unsigned", "foreign" => ["permissionset" => "id", "on delete" => "restrict"], "comment" => "ID of the permission set of this post"], + // Part of "post-thread-user". Will be deprecated in a later step + "starred" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "item has been favourited"], + "wall" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "This item was posted to the wall of uid"], + "pubmail" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => ""], + "forum_mode" => ["type" => "tinyint unsigned", "not null" => "1", "default" => "0", "comment" => ""], // It has to be decided whether these fields belong to the user or the structure "event-id" => ["type" => "int unsigned", "relation" => ["event" => "id"], "comment" => "Used to link to the event.id"], // Check deprecation status "type" => ["type" => "varchar(20)", "comment" => ""], "bookmark" => ["type" => "boolean", "comment" => ""], + "mention" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "The owner of this item was mentioned in it"], // Deprecated fields. Will be removed in upcoming versions "resource-id" => ["type" => "varchar(32)", "comment" => "Deprecated"], "uri-hash" => ["type" => "varchar(80)", "comment" => "Deprecated"], @@ -1235,6 +1236,18 @@ return [ "psid" => ["psid"], ], ], +// todo + "post-user-notification" => [ + "comment" => "User specific post data", + "fields" => [ + "uri-id" => ["type" => "int unsigned", "not null" => "1", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the item uri"], + "uid" => ["type" => "mediumint unsigned", "not null" => "1", "foreign" => ["user" => "uid"], "comment" => "Owner id which owns this copy of the item"], + "notification-type" => ["type" => "tinyint unsigned", "not null" => "1", "default" => "0", "comment" => ""], + ], + "indexes" => [ + "PRIMARY" => ["uid", "uri-id"], + ], + ], "process" => [ "comment" => "Currently running system processes", "fields" => [ From c62d93cfe87d13384d9660167f04abd33cad010f Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 1 Feb 2021 10:57:25 +0000 Subject: [PATCH 05/14] Avoid "Undefined index: verb" --- src/Model/Item.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/Item.php b/src/Model/Item.php index 19f382b67..f528ea8da 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -1059,7 +1059,7 @@ class Item $update_commented = in_array($item['gravity'], [GRAVITY_PARENT, GRAVITY_COMMENT]); } else { // Update when it isn't a follow or tag verb - $update_commented = !in_array($item['verb'], [Activity::FOLLOW, Activity::TAG]); + $update_commented = !in_array($item['verb'] ?? '', [Activity::FOLLOW, Activity::TAG]); } if ($update_commented) { From d5c33216b7a6dbfbe87347d102625a9967b9d2f4 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 1 Feb 2021 11:00:35 +0000 Subject: [PATCH 06/14] Don't remove data from the item array --- src/Model/Item.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Model/Item.php b/src/Model/Item.php index f528ea8da..ab6fe15ba 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -1022,14 +1022,14 @@ class Item } // Remove all fields that aren't part of the item table - $item = DBStructure::getFieldsForTable('item', $item); + $table_fields = DBStructure::getFieldsForTable('item', $item); // We remove all legacy fields that now are stored in other tables foreach (self::LEGACY_FIELDLIST as $field) { - unset($item[$field]); + unset($table_fields[$field]); } - $result = DBA::insert('item', $item); + $result = DBA::insert('item', $table_fields); // When the item was successfully stored we fetch the ID of the item. $current_post = DBA::lastInsertId(); @@ -1059,7 +1059,7 @@ class Item $update_commented = in_array($item['gravity'], [GRAVITY_PARENT, GRAVITY_COMMENT]); } else { // Update when it isn't a follow or tag verb - $update_commented = !in_array($item['verb'] ?? '', [Activity::FOLLOW, Activity::TAG]); + $update_commented = !in_array($item['verb'], [Activity::FOLLOW, Activity::TAG]); } if ($update_commented) { From 4c1812a1ebca00a310a68dae36261770b35a30d0 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 1 Feb 2021 11:09:23 +0000 Subject: [PATCH 07/14] Improved notification description --- static/dbstructure.config.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/static/dbstructure.config.php b/static/dbstructure.config.php index 362f299c1..1e4bd602c 100644 --- a/static/dbstructure.config.php +++ b/static/dbstructure.config.php @@ -1236,16 +1236,16 @@ return [ "psid" => ["psid"], ], ], -// todo "post-user-notification" => [ "comment" => "User specific post data", "fields" => [ - "uri-id" => ["type" => "int unsigned", "not null" => "1", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the item uri"], - "uid" => ["type" => "mediumint unsigned", "not null" => "1", "foreign" => ["user" => "uid"], "comment" => "Owner id which owns this copy of the item"], + "uri-id" => ["type" => "int unsigned", "not null" => "1", "primary" => "1", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the item uri"], + "uid" => ["type" => "mediumint unsigned", "not null" => "1", "primary" => "1", "foreign" => ["user" => "uid"], "comment" => "Owner id which owns this copy of the item"], "notification-type" => ["type" => "tinyint unsigned", "not null" => "1", "default" => "0", "comment" => ""], ], "indexes" => [ "PRIMARY" => ["uid", "uri-id"], + "uri-id" => ["uri-id"], ], ], "process" => [ From 32e6b28aabf5e77ff4bcad1c6f0704eac59211a4 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 1 Feb 2021 13:36:20 +0000 Subject: [PATCH 08/14] Fixed class name --- src/Model/Post/{UserNotifcation.php => UserNotification.php} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/Model/Post/{UserNotifcation.php => UserNotification.php} (100%) diff --git a/src/Model/Post/UserNotifcation.php b/src/Model/Post/UserNotification.php similarity index 100% rename from src/Model/Post/UserNotifcation.php rename to src/Model/Post/UserNotification.php From b26157e6e30742980f9585b64fd4b4eade080bf4 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 1 Feb 2021 20:11:25 +0000 Subject: [PATCH 09/14] Update function for "post-user-notification" --- database.sql | 25 +++++++++++++++++++------ include/api.php | 9 ++++----- static/dbstructure.config.php | 2 +- update.php | 14 ++++++++++++++ 4 files changed, 38 insertions(+), 12 deletions(-) diff --git a/database.sql b/database.sql index 9fab6668d..a1e5b00d9 100644 --- a/database.sql +++ b/database.sql @@ -709,17 +709,17 @@ CREATE TABLE IF NOT EXISTS `item` ( `deleted` boolean NOT NULL DEFAULT '0' COMMENT 'item has been deleted', `uid` mediumint unsigned NOT NULL DEFAULT 0 COMMENT 'Owner id which owns this copy of the item', `contact-id` int unsigned NOT NULL DEFAULT 0 COMMENT 'contact.id', - `wall` boolean NOT NULL DEFAULT '0' COMMENT 'This item was posted to the wall of uid', - `origin` boolean NOT NULL DEFAULT '0' COMMENT 'item originated at this site', - `pubmail` boolean NOT NULL DEFAULT '0' COMMENT '', - `starred` boolean NOT NULL DEFAULT '0' COMMENT 'item has been favourited', `unseen` boolean NOT NULL DEFAULT '1' COMMENT 'item has not been seen', - `mention` boolean NOT NULL DEFAULT '0' COMMENT 'The owner of this item was mentioned in it', - `forum_mode` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '', + `origin` boolean NOT NULL DEFAULT '0' COMMENT 'item originated at this site', `psid` int unsigned COMMENT 'ID of the permission set of this post', + `starred` boolean NOT NULL DEFAULT '0' COMMENT 'item has been favourited', + `wall` boolean NOT NULL DEFAULT '0' COMMENT 'This item was posted to the wall of uid', + `pubmail` boolean NOT NULL DEFAULT '0' COMMENT '', + `forum_mode` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '', `event-id` int unsigned COMMENT 'Used to link to the event.id', `type` varchar(20) COMMENT '', `bookmark` boolean COMMENT '', + `mention` boolean NOT NULL DEFAULT '0' COMMENT 'The owner of this item was mentioned in it', `resource-id` varchar(32) COMMENT 'Deprecated', `uri-hash` varchar(80) COMMENT 'Deprecated', `iaid` int unsigned COMMENT 'Deprecated', @@ -1186,6 +1186,19 @@ CREATE TABLE IF NOT EXISTS `post-user` ( FOREIGN KEY (`psid`) REFERENCES `permissionset` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT ) DEFAULT COLLATE utf8mb4_general_ci COMMENT='User specific post data'; +-- +-- TABLE post-user-notification +-- +CREATE TABLE IF NOT EXISTS `post-user-notification` ( + `uri-id` int unsigned NOT NULL COMMENT 'Id of the item-uri table entry that contains the item uri', + `uid` mediumint unsigned NOT NULL COMMENT 'Owner id which owns this copy of the item', + `notification-type` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '', + PRIMARY KEY(`uid`,`uri-id`), + INDEX `uri-id` (`uri-id`), + FOREIGN KEY (`uri-id`) REFERENCES `item-uri` (`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 post notifications'; + -- -- TABLE process -- diff --git a/include/api.php b/include/api.php index 8543eef9e..789a13d86 100644 --- a/include/api.php +++ b/include/api.php @@ -2170,16 +2170,15 @@ function api_statuses_mentions($type) $start = max(0, ($page - 1) * $count); - $query = "`gravity` IN (?, ?) AND `uri-id` IN (SELECT `uri-id` FROM `post-user` - WHERE (`hidden` IS NULL OR NOT `hidden`) AND - `uid` = ? AND `notification-type` & ? != 0) - AND `id` > ?"; + $query = "`gravity` IN (?, ?) AND `uri-id` IN + (SELECT `uri-id` FROM `post-user-notification` WHERE `uid` = ? AND `notification-type` & ? != 0 ORDER BY `uri-id`) + 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, - $since_id]; + api_user(), $since_id]; if ($max_id > 0) { $query .= " AND `id` <= ?"; diff --git a/static/dbstructure.config.php b/static/dbstructure.config.php index 1e4bd602c..776d24486 100644 --- a/static/dbstructure.config.php +++ b/static/dbstructure.config.php @@ -1237,7 +1237,7 @@ return [ ], ], "post-user-notification" => [ - "comment" => "User specific post data", + "comment" => "User post notifications", "fields" => [ "uri-id" => ["type" => "int unsigned", "not null" => "1", "primary" => "1", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the item uri"], "uid" => ["type" => "mediumint unsigned", "not null" => "1", "primary" => "1", "foreign" => ["user" => "uid"], "comment" => "Owner id which owns this copy of the item"], diff --git a/update.php b/update.php index 05064d8e0..698d043a3 100644 --- a/update.php +++ b/update.php @@ -727,5 +727,19 @@ function update_1397() FROM `thread` LEFT JOIN `user-item` ON `user-item`.`iid` = `thread`.`iid`")) { return Update::FAILED; } + + if (!DBA::e("INSERT INTO `post-user-notification`(`uri-id`, `uid`, `notification-type`) + SELECT `uri-id`, `user-item`.`uid`, `notification-type` FROM `user-item` + INNER JOIN `item` ON `item`.`id` = `user-item`.`iid` WHERE `notification-type` != 0 + ON DUPLICATE KEY UPDATE `notification-type` = `user-item`.`notification-type`")) { + return Update::FAILED; + } + + if (!DBA::e("INSERT IGNORE INTO `post-user-notification`(`uri-id`, `uid`, `notification-type`) + SELECT `uri-id`, `uid`, `notification-type` FROM `post-user` WHERE `notification-type` != 0 + ON DUPLICATE KEY UPDATE `uri-id` = `post-user`.`uri-id`, `uid` = `post-user`.`uid`, `notification-type` = `post-user`.`notification-type`")) { + return Update::FAILED; + } + return Update::SUCCESS; } From df24c0daebc01866eebf2efeeba97cda414cd71e Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 2 Feb 2021 05:45:57 +0000 Subject: [PATCH 10/14] User-Item table is replaced --- database.sql | 17 -- include/api.php | 7 +- include/enotify.php | 56 +++-- src/Database/PostUpdate.php | 7 +- src/Model/Item.php | 5 +- src/Model/Post/UserNotification.php | 327 ++++++++++++++++++++++++++ src/Model/UserItem.php | 350 ---------------------------- static/dbstructure.config.php | 16 -- 8 files changed, 367 insertions(+), 418 deletions(-) delete mode 100644 src/Model/UserItem.php diff --git a/database.sql b/database.sql index a1e5b00d9..b717da446 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 789a13d86..4c90ab80d 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 aa5342dcc..8936cadbe 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 51ce0d2b5..3a1333780 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 ab6fe15ba..5041b7ef5 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 5dca875ab..9173cfbe2 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 3405d9a6a..000000000 --- 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 776d24486..d6e21512a 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" => [ From 48ba88b11ce26fef353065bc32f6a282fb6f33dc Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 2 Feb 2021 05:53:55 +0000 Subject: [PATCH 11/14] remove test logging --- src/Model/Post.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Model/Post.php b/src/Model/Post.php index 5cafdf25a..73742b3b2 100644 --- a/src/Model/Post.php +++ b/src/Model/Post.php @@ -457,13 +457,11 @@ class Post $post_thread_condition[0] = "EXISTS(SELECT `id` FROM `post-user` WHERE " . $post_thread_condition[0] . " AND `uri-id` = `post-thread-user`.`uri-id` AND `uid` = `post-thread-user`.`uid`)"; - Logger::info('Test2-start', ['condition' => $post_thread_condition]); if (!DBA::update('post-thread-user', $update_fields, $post_thread_condition)) { DBA::rollback(); Logger::notice('Updating post-thread-user failed', ['fields' => $update_fields, 'condition' => $condition]); return false; } - Logger::info('Test2-end'); $affected = max($affected, DBA::affectedRows()); } From 9f37a4d9a09d93acd6ac5fa5b8efd49b7470384e Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 2 Feb 2021 06:05:50 +0000 Subject: [PATCH 12/14] Rearranged order of update calls --- update.php | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/update.php b/update.php index 698d043a3..d5bfc5fe3 100644 --- a/update.php +++ b/update.php @@ -717,7 +717,24 @@ function update_1396() function update_1397() { - if (!DBStructure::existsTable('thread') || !DBStructure::existsTable('user-item')) { + if (!DBA::e("INSERT INTO `post-user-notification`(`uri-id`, `uid`, `notification-type`) + SELECT `uri-id`, `uid`, `notification-type` FROM `post-user` WHERE `notification-type` != 0 + ON DUPLICATE KEY UPDATE `uri-id` = `post-user`.`uri-id`, `uid` = `post-user`.`uid`, `notification-type` = `post-user`.`notification-type`")) { + return Update::FAILED; + } + + if (!DBStructure::existsTable('user-item')) { + return Update::SUCCESS; + } + + if (!DBA::e("INSERT INTO `post-user-notification`(`uri-id`, `uid`, `notification-type`) + SELECT `uri-id`, `user-item`.`uid`, `notification-type` FROM `user-item` + INNER JOIN `item` ON `item`.`id` = `user-item`.`iid` WHERE `notification-type` != 0 + ON DUPLICATE KEY UPDATE `notification-type` = `user-item`.`notification-type`")) { + return Update::FAILED; + } + + if (!DBStructure::existsTable('thread')) { return Update::SUCCESS; } @@ -728,18 +745,5 @@ function update_1397() return Update::FAILED; } - if (!DBA::e("INSERT INTO `post-user-notification`(`uri-id`, `uid`, `notification-type`) - SELECT `uri-id`, `user-item`.`uid`, `notification-type` FROM `user-item` - INNER JOIN `item` ON `item`.`id` = `user-item`.`iid` WHERE `notification-type` != 0 - ON DUPLICATE KEY UPDATE `notification-type` = `user-item`.`notification-type`")) { - return Update::FAILED; - } - - if (!DBA::e("INSERT IGNORE INTO `post-user-notification`(`uri-id`, `uid`, `notification-type`) - SELECT `uri-id`, `uid`, `notification-type` FROM `post-user` WHERE `notification-type` != 0 - ON DUPLICATE KEY UPDATE `uri-id` = `post-user`.`uri-id`, `uid` = `post-user`.`uid`, `notification-type` = `post-user`.`notification-type`")) { - return Update::FAILED; - } - return Update::SUCCESS; } From 5a2c5da81d0e8b81a822dab0d4bf3fa705e30637 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 2 Feb 2021 06:10:10 +0000 Subject: [PATCH 13/14] Added "user-item" table to the list of removable tables --- src/Database/DBStructure.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Database/DBStructure.php b/src/Database/DBStructure.php index 8a3d6cc80..3e1ef7aa1 100644 --- a/src/Database/DBStructure.php +++ b/src/Database/DBStructure.php @@ -79,7 +79,8 @@ class DBStructure } $old_tables = ['fserver', 'gcign', 'gcontact', 'gcontact-relation', 'gfollower' ,'glink', 'item-delivery-data', - 'item-activity', 'item-content', 'item_id', 'poll', 'poll_result', 'queue', 'retriever_rule', 'sign', 'spam', 'term']; + 'item-activity', 'item-content', 'item_id', 'poll', 'poll_result', 'queue', 'retriever_rule', + 'sign', 'spam', 'term', 'user-item']; $tables = DBA::selectToArray(['INFORMATION_SCHEMA' => 'TABLES'], ['TABLE_NAME'], ['TABLE_SCHEMA' => DBA::databaseName(), 'TABLE_TYPE' => 'BASE TABLE']); From e561cad844951d7711e92c96019698da70a47e02 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 2 Feb 2021 06:28:51 +0000 Subject: [PATCH 14/14] "participation" is removed since it is unused --- database.sql | 16 ---------------- doc/database.md | 1 - doc/database/db_particiation.md | 10 ---------- src/Database/DBStructure.php | 2 +- static/dbstructure.config.php | 14 -------------- update.php | 12 ------------ 6 files changed, 1 insertion(+), 54 deletions(-) delete mode 100644 doc/database/db_particiation.md diff --git a/database.sql b/database.sql index b717da446..60dca5f5f 100644 --- a/database.sql +++ b/database.sql @@ -970,22 +970,6 @@ CREATE TABLE IF NOT EXISTS `parsed_url` ( INDEX `created` (`created`) ) DEFAULT COLLATE utf8mb4_general_ci COMMENT='cache for \'parse_url\' queries'; --- --- TABLE participation --- -CREATE TABLE IF NOT EXISTS `participation` ( - `iid` int unsigned NOT NULL COMMENT '', - `server` varchar(60) NOT NULL COMMENT '', - `cid` int unsigned NOT NULL COMMENT '', - `fid` int unsigned NOT NULL COMMENT '', - PRIMARY KEY(`iid`,`server`), - INDEX `cid` (`cid`), - INDEX `fid` (`fid`), - FOREIGN KEY (`iid`) REFERENCES `item` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, - FOREIGN KEY (`cid`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, - FOREIGN KEY (`fid`) REFERENCES `fcontact` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE -) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Storage for participation messages from Diaspora'; - -- -- TABLE pconfig -- diff --git a/doc/database.md b/doc/database.md index e135b19ae..447607f8d 100644 --- a/doc/database.md +++ b/doc/database.md @@ -32,7 +32,6 @@ Database Tables | [notify-threads](help/database/db_notify-threads) | | | [oembed](help/database/db_oembed) | cache for OEmbed queries | | [parsed_url](help/database/db_parsed_url) | cache for "parse_url" queries | -| [participation](help/database/db_participation) | Storage for participation messages from Diaspora | | [pconfig](help/database/db_pconfig) | personal (per user) configuration storage | | [photo](help/database/db_photo) | photo storage | | [poll](help/database/db_poll) | data for polls | diff --git a/doc/database/db_particiation.md b/doc/database/db_particiation.md deleted file mode 100644 index 27f063241..000000000 --- a/doc/database/db_particiation.md +++ /dev/null @@ -1,10 +0,0 @@ -Table participation -=================== - -| Field | Description | Type | Null | Key | Default | Extra | -|-------------|------------------|------------------|------|-----|---------------------|-------| -| iid | item id | int(10) unsigned | NO | PRI | | | -| server | Name of server | varchar(60) | NO | PRI | | | -| cid | contact id | int(10) unsigned | NO | | | | - -Return to [database documentation](help/database) diff --git a/src/Database/DBStructure.php b/src/Database/DBStructure.php index 3e1ef7aa1..b9e53be04 100644 --- a/src/Database/DBStructure.php +++ b/src/Database/DBStructure.php @@ -79,7 +79,7 @@ class DBStructure } $old_tables = ['fserver', 'gcign', 'gcontact', 'gcontact-relation', 'gfollower' ,'glink', 'item-delivery-data', - 'item-activity', 'item-content', 'item_id', 'poll', 'poll_result', 'queue', 'retriever_rule', + 'item-activity', 'item-content', 'item_id', 'participation', 'poll', 'poll_result', 'queue', 'retriever_rule', 'sign', 'spam', 'term', 'user-item']; $tables = DBA::selectToArray(['INFORMATION_SCHEMA' => 'TABLES'], ['TABLE_NAME'], diff --git a/static/dbstructure.config.php b/static/dbstructure.config.php index d6e21512a..6b8aba3ac 100644 --- a/static/dbstructure.config.php +++ b/static/dbstructure.config.php @@ -1031,20 +1031,6 @@ return [ "created" => ["created"], ] ], - "participation" => [ - "comment" => "Storage for participation messages from Diaspora", - "fields" => [ - "iid" => ["type" => "int unsigned", "not null" => "1", "primary" => "1", "foreign" => ["item" => "id"], "comment" => ""], - "server" => ["type" => "varchar(60)", "not null" => "1", "primary" => "1", "comment" => ""], - "cid" => ["type" => "int unsigned", "not null" => "1", "foreign" => ["contact" => "id"], "comment" => ""], - "fid" => ["type" => "int unsigned", "not null" => "1", "foreign" => ["fcontact" => "id"], "comment" => ""], - ], - "indexes" => [ - "PRIMARY" => ["iid", "server"], - "cid" => ["cid"], - "fid" => ["fid"] - ] - ], "pconfig" => [ "comment" => "personal (per user) configuration storage", "fields" => [ diff --git a/update.php b/update.php index d5bfc5fe3..94029065b 100644 --- a/update.php +++ b/update.php @@ -490,10 +490,6 @@ function pre_update_1364() return Update::FAILED; } - if (!DBA::e("DELETE FROM `participation` WHERE NOT `cid` IN (SELECT `id` FROM `contact`)")) { - return Update::FAILED; - } - if (!DBA::e("DELETE FROM `profile_check` WHERE NOT `cid` IN (SELECT `id` FROM `contact`)")) { return Update::FAILED; } @@ -502,10 +498,6 @@ function pre_update_1364() return Update::FAILED; } - if (!DBA::e("DELETE FROM `participation` WHERE NOT `fid` IN (SELECT `id` FROM `fcontact`)")) { - return Update::FAILED; - } - if (!DBA::e("DELETE FROM `group_member` WHERE NOT `gid` IN (SELECT `id` FROM `group`)")) { return Update::FAILED; } @@ -514,10 +506,6 @@ function pre_update_1364() return Update::FAILED; } - if (!DBA::e("DELETE FROM `participation` WHERE NOT `iid` IN (SELECT `id` FROM `item`)")) { - return Update::FAILED; - } - if (DBStructure::existsTable('user-item') && !DBA::e("DELETE FROM `user-item` WHERE NOT `iid` IN (SELECT `id` FROM `item`)")) { return Update::FAILED; }