1
0
mirror of https://github.com/chylex/Nextcloud-News.git synced 2025-04-09 19:15:42 +02:00

NewsItem: add share functions (mapper + service)

Signed-off-by: Marco Nassabain <marco.nassabain@hotmail.com>
This commit is contained in:
Marco Nassabain 2021-01-17 19:50:45 +01:00 committed by Sean Molenaar
parent 5b09e74f40
commit 527eef0727
2 changed files with 900 additions and 0 deletions

545
lib/Db/ItemMapper.php Normal file
View File

@ -0,0 +1,545 @@
<?php
/**
* Nextcloud - News
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Alessandro Cosentino <cosenal@gmail.com>
* @author Bernhard Posselt <dev@bernhard-posselt.com>
* @copyright 2012 Alessandro Cosentino
* @copyright 2012-2014 Bernhard Posselt
*/
namespace OCA\News\Db;
use Exception;
use OCA\News\Utility\Time;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\Entity;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
/**
* Class LegacyItemMapper
*
* @package OCA\News\Db
* @deprecated use ItemMapper
*/
class ItemMapper extends NewsMapper
{
const TABLE_NAME = 'news_items';
public function __construct(IDBConnection $db, Time $time)
{
parent::__construct($db, $time, Item::class);
}
private function makeSelectQuery(
$prependTo = '',
$oldestFirst = false,
$distinctFingerprint = false
) {
if ($oldestFirst) {
$ordering = 'ASC';
} else {
$ordering = 'DESC';
}
return 'SELECT `items`.* FROM `*PREFIX*news_items` `items` ' .
'JOIN `*PREFIX*news_feeds` `feeds` ' .
'ON `feeds`.`id` = `items`.`feed_id` ' .
'AND `feeds`.`deleted_at` = 0 ' .
'AND `feeds`.`user_id` = ? ' .
$prependTo .
'LEFT OUTER JOIN `*PREFIX*news_folders` `folders` ' .
'ON `folders`.`id` = `feeds`.`folder_id` ' .
'WHERE `feeds`.`folder_id` IS NULL ' .
'OR `folders`.`deleted_at` = 0 ' .
'ORDER BY `items`.`id` ' . $ordering;
}
/**
* check if type is feed or all items should be shown
*
* @param bool $showAll
* @param int|null $type
* @return string
*/
private function buildStatusQueryPart($showAll, $type = null)
{
$sql = '';
if (isset($type) && $type === FeedType::STARRED) {
$sql = 'AND `items`.`starred` = ';
$sql .= $this->db->quote(true, IQueryBuilder::PARAM_BOOL) . ' ';
} elseif (!$showAll || $type === FeedType::UNREAD) {
$sql .= 'AND `items`.`unread` = ';
$sql .= $this->db->quote(true, IQueryBuilder::PARAM_BOOL) . ' ';
}
return $sql;
}
private function buildSearchQueryPart(array $search = [])
{
return str_repeat('AND `items`.`search_index` LIKE ? ', count($search));
}
/**
* wrap and escape search parameters in a like statement
*
* @param string[] $search an array of strings that should be searched
* @return array with like parameters
*/
private function buildLikeParameters($search = [])
{
return array_map(
function ($param) {
$param = addcslashes($param, '\\_%');
return '%' . mb_strtolower($param, 'UTF-8') . '%';
},
$search
);
}
/**
* @param int $id
* @param string $userId
* @return \OCA\News\Db\Item
*/
public function find(string $userId, int $id)
{
$sql = $this->makeSelectQuery('AND `items`.`id` = ? ');
return $this->findEntity($sql, [$userId, $id]);
}
public function starredCount(string $userId)
{
$sql = 'SELECT COUNT(*) AS size FROM `*PREFIX*news_items` `items` ' .
'JOIN `*PREFIX*news_feeds` `feeds` ' .
'ON `feeds`.`id` = `items`.`feed_id` ' .
'AND `feeds`.`deleted_at` = 0 ' .
'AND `feeds`.`user_id` = ? ' .
'AND `items`.`starred` = ? ' .
'LEFT OUTER JOIN `*PREFIX*news_folders` `folders` ' .
'ON `folders`.`id` = `feeds`.`folder_id` ' .
'WHERE `feeds`.`folder_id` IS NULL ' .
'OR `folders`.`deleted_at` = 0';
$params = [$userId, true];
$result = $this->execute($sql, $params)->fetch();
return (int)$result['size'];
}
public function readAll(int $highestItemId, $time, string $userId)
{
$sql = 'UPDATE `*PREFIX*news_items` ' .
'SET unread = ? ' .
', `last_modified` = ? ' .
'WHERE `feed_id` IN (' .
'SELECT `id` FROM `*PREFIX*news_feeds` ' .
'WHERE `user_id` = ? ' .
') ' .
'AND `id` <= ?';
$params = [false, $time, $userId, $highestItemId];
$this->execute($sql, $params);
}
public function readFolder(?int $folderId, $highestItemId, $time, $userId)
{
$folderWhere = is_null($folderId) ? 'IS' : '=';
$sql = 'UPDATE `*PREFIX*news_items` ' .
'SET unread = ? ' .
', `last_modified` = ? ' .
'WHERE `feed_id` IN (' .
'SELECT `id` FROM `*PREFIX*news_feeds` ' .
"WHERE `folder_id` ${folderWhere} ? " .
'AND `user_id` = ? ' .
') ' .
'AND `id` <= ?';
$params = [false, $time, $folderId, $userId,
$highestItemId];
$this->execute($sql, $params);
}
public function readFeed($feedId, $highestItemId, $time, $userId)
{
$sql = 'UPDATE `*PREFIX*news_items` ' .
'SET unread = ? ' .
', `last_modified` = ? ' .
'WHERE `feed_id` = ? ' .
'AND `id` <= ? ' .
'AND EXISTS (' .
'SELECT * FROM `*PREFIX*news_feeds` ' .
'WHERE `user_id` = ? ' .
'AND `id` = ? ) ';
$params = [false, $time, $feedId, $highestItemId,
$userId, $feedId];
$this->execute($sql, $params);
}
private function getOperator($oldestFirst)
{
if ($oldestFirst) {
return '>';
} else {
return '<';
}
}
public function findAllNew($updatedSince, $type, $showAll, $userId)
{
$sql = $this->buildStatusQueryPart($showAll, $type);
$sql .= 'AND `items`.`last_modified` >= ? ';
$sql = $this->makeSelectQuery($sql);
$params = [$userId, $updatedSince];
return $this->findEntities($sql, $params);
}
public function findAllNewFolder(?int $id, $updatedSince, $showAll, $userId)
{
$sql = $this->buildStatusQueryPart($showAll);
$folderWhere = is_null($id) ? 'IS' : '=';
$sql .= "AND `feeds`.`folder_id` ${$folderWhere} ? " .
'AND `items`.`last_modified` >= ? ';
$sql = $this->makeSelectQuery($sql);
$params = [$userId, $id, $updatedSince];
return $this->findEntities($sql, $params);
}
public function findAllNewFeed($id, $updatedSince, $showAll, $userId)
{
$sql = $this->buildStatusQueryPart($showAll);
$sql .= 'AND `items`.`feed_id` = ? ' .
'AND `items`.`last_modified` >= ? ';
$sql = $this->makeSelectQuery($sql);
$params = [$userId, $id, $updatedSince];
return $this->findEntities($sql, $params);
}
private function findEntitiesIgnoringNegativeLimit($sql, $params, $limit): array
{
// ignore limit if negative to offer a way to return all feeds
if ($limit >= 0) {
return $this->findEntities($sql, $params, $limit);
} else {
return $this->findEntities($sql, $params);
}
}
public function findAllFeed(
$id,
$limit,
$offset,
$showAll,
$oldestFirst,
$userId,
$search = []
) {
$params = [$userId];
$params = array_merge($params, $this->buildLikeParameters($search));
$params[] = $id;
$sql = $this->buildStatusQueryPart($showAll);
$sql .= $this->buildSearchQueryPart($search);
$sql .= 'AND `items`.`feed_id` = ? ';
if ($offset !== 0) {
$sql .= 'AND `items`.`id` ' .
$this->getOperator($oldestFirst) . ' ? ';
$params[] = $offset;
}
$sql = $this->makeSelectQuery($sql, $oldestFirst, $search);
return $this->findEntitiesIgnoringNegativeLimit($sql, $params, $limit);
}
public function findAllFolder(
?int $id,
$limit,
$offset,
$showAll,
$oldestFirst,
$userId,
$search = []
) {
$params = [$userId];
$params = array_merge($params, $this->buildLikeParameters($search));
$params[] = $id;
$sql = $this->buildStatusQueryPart($showAll);
$sql .= $this->buildSearchQueryPart($search);
$folderWhere = is_null($id) ? 'IS' : '=';
$sql .= "AND `feeds`.`folder_id` ${folderWhere} ? ";
if ($offset !== 0) {
$sql .= 'AND `items`.`id` ' . $this->getOperator($oldestFirst) . ' ? ';
$params[] = $offset;
}
$sql = $this->makeSelectQuery($sql, $oldestFirst, $search);
return $this->findEntitiesIgnoringNegativeLimit($sql, $params, $limit);
}
public function findAllItems(
$limit,
$offset,
$type,
$showAll,
$oldestFirst,
$userId,
$search = []
): array {
$params = [$userId];
$params = array_merge($params, $this->buildLikeParameters($search));
$sql = $this->buildStatusQueryPart($showAll, $type);
$sql .= $this->buildSearchQueryPart($search);
if ($offset !== 0) {
$sql .= 'AND `items`.`id` ' .
$this->getOperator($oldestFirst) . ' ? ';
$params[] = $offset;
}
$sql = $this->makeSelectQuery($sql, $oldestFirst);
return $this->findEntitiesIgnoringNegativeLimit($sql, $params, $limit);
}
public function findAllUnreadOrStarred($userId)
{
$params = [$userId, true, true];
$sql = 'AND (`items`.`unread` = ? OR `items`.`starred` = ?) ';
$sql = $this->makeSelectQuery($sql);
return $this->findEntities($sql, $params);
}
public function findByGuidHash($guidHash, $feedId, $userId)
{
$sql = $this->makeSelectQuery(
'AND `items`.`guid_hash` = ? ' .
'AND `feeds`.`id` = ? '
);
return $this->findEntity($sql, [$userId, $guidHash, $feedId]);
}
/**
* Delete all items for feeds that have over $threshold unread and not
* starred items
*
* @param int $threshold the number of items that should be deleted
*/
public function deleteReadOlderThanThreshold($threshold)
{
$params = [false, false, $threshold];
$sql = 'SELECT (COUNT(*) - `feeds`.`articles_per_update`) AS `size`, ' .
'`feeds`.`id` AS `feed_id`, `feeds`.`articles_per_update` ' .
'FROM `*PREFIX*news_items` `items` ' .
'JOIN `*PREFIX*news_feeds` `feeds` ' .
'ON `feeds`.`id` = `items`.`feed_id` ' .
'AND `items`.`unread` = ? ' .
'AND `items`.`starred` = ? ' .
'GROUP BY `feeds`.`id`, `feeds`.`articles_per_update` ' .
'HAVING COUNT(*) > ?';
$result = $this->execute($sql, $params);
while ($row = $result->fetch()) {
$size = (int)$row['size'];
$limit = $size - $threshold;
$feed_id = $row['feed_id'];
if ($limit > 0) {
$params = [false, false, $feed_id, $limit];
$sql = 'SELECT `id` FROM `*PREFIX*news_items` ' .
'WHERE `unread` = ? ' .
'AND `starred` = ? ' .
'AND `feed_id` = ? ' .
'ORDER BY `id` ASC ' .
'LIMIT 1 ' .
'OFFSET ? ';
}
$limit_result = $this->execute($sql, $params);
if ($limit_row = $limit_result->fetch()) {
$limit_id = (int)$limit_row['id'];
$params = [false, false, $feed_id, $limit_id];
$sql = 'DELETE FROM `*PREFIX*news_items` ' .
'WHERE `unread` = ? ' .
'AND `starred` = ? ' .
'AND `feed_id` = ? ' .
'AND `id` < ? ';
$this->execute($sql, $params);
}
}
}
public function getNewestItemId($userId)
{
$sql = 'SELECT MAX(`items`.`id`) AS `max_id` ' .
'FROM `*PREFIX*news_items` `items` ' .
'JOIN `*PREFIX*news_feeds` `feeds` ' .
'ON `feeds`.`id` = `items`.`feed_id` ' .
'AND `feeds`.`user_id` = ?';
$params = [$userId];
$result = $this->findOneQuery($sql, $params);
return (int)$result['max_id'];
}
/**
* Deletes all items of a user
*
* @param string $userId the name of the user
*/
public function deleteUser($userId)
{
$sql = 'DELETE FROM `*PREFIX*news_items` ' .
'WHERE `feed_id` IN (' .
'SELECT `feeds`.`id` FROM `*PREFIX*news_feeds` `feeds` ' .
'WHERE `feeds`.`user_id` = ?' .
')';
$this->execute($sql, [$userId]);
}
/**
* Returns a list of ids and userid of all items
*/
public function findAllIds($limit = null, $offset = null)
{
$sql = 'SELECT `id` FROM `*PREFIX*news_items`';
return $this->execute($sql, [], $limit, $offset)->fetchAll();
}
/**
* Update search indices of all items
*/
public function updateSearchIndices()
{
// update indices in steps to prevent memory issues on larger systems
$step = 1000; // update 1000 items at a time
$itemCount = 1;
$offset = 0;
// stop condition if there are no previously fetched items
while ($itemCount > 0) {
$items = $this->findAllIds($step, $offset);
$itemCount = count($items);
$this->updateSearchIndex($items);
$offset += $step;
}
}
private function updateSearchIndex(array $items = [])
{
foreach ($items as $row) {
$sql = 'SELECT * FROM `*PREFIX*news_items` WHERE `id` = ?';
$params = [$row['id']];
$item = $this->findEntity($sql, $params);
$item->generateSearchIndex();
$this->update($item);
}
}
public function readItem($itemId, $isRead, $lastModified, $userId)
{
$item = $this->find($userId, $itemId);
// reading an item should set all of the same items as read, whereas
// marking an item as unread should only mark the selected instance
// as unread
if ($isRead) {
$sql = 'UPDATE `*PREFIX*news_items`
SET `unread` = ?,
`last_modified` = ?
WHERE `fingerprint` = ?
AND `feed_id` IN (
SELECT `f`.`id` FROM `*PREFIX*news_feeds` AS `f`
WHERE `f`.`user_id` = ?
)';
$params = [false, $lastModified, $item->getFingerprint(), $userId];
$this->execute($sql, $params);
} else {
$item->setLastModified($lastModified);
$item->setUnread(true);
$this->update($item);
}
}
/**
* NO-OP
*
* @param string $userId
*
* @return array
*/
public function findAllFromUser(string $userId): array
{
return [];
}
public function findFromUser(string $userId, int $id): Entity
{
return $this->find($id, $userId);
}
/**
* NO-OP
* @return array
*/
public function findAll(): array
{
return [];
}
public function shareItem($itemId, $shareWithId, $userId)
{
// find existing item and copy it
$item = $this->find($userId, $itemId);
// copy item
$newItem = Item::fromImport($item->jsonSerialize());
// copy/initialize fields
$newItem->setUnread(false);
$newItem->setStarred(false);
$newItem->setFeedId(null);
$newItem->setFingerprint($item->getFingerprint());
$newItem->setContentHash($item->getContentHash());
$newItem->setSearchIndex($item->getSearchIndex());
// set share data
$newItem->setSharedBy($userId);
$newItem->setSharedWith($shareWithId);
// persist new item
$this->insert($newItem);
}
}

355
lib/Service/ItemService.php Normal file
View File

@ -0,0 +1,355 @@
<?php
/**
* Nextcloud - News
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Alessandro Cosentino <cosenal@gmail.com>
* @author Bernhard Posselt <dev@bernhard-posselt.com>
* @copyright 2012 Alessandro Cosentino
* @copyright 2012-2014 Bernhard Posselt
*/
namespace OCA\News\Service;
use OCA\News\AppInfo\Application;
use OCA\News\Db\Item;
use OCA\News\Service\Exceptions\ServiceNotFoundException;
use OCP\AppFramework\Db\Entity;
use OCP\IConfig;
use OCP\AppFramework\Db\DoesNotExistException;
use OCA\News\Db\ItemMapper;
use OCA\News\Db\FeedType;
use OCA\News\Utility\Time;
use Psr\Log\LoggerInterface;
/**
* Class LegacyItemService
*
* @package OCA\News\Service
* @deprecated use ItemServiceV2
*/
class ItemService extends Service
{
private $config;
private $timeFactory;
private $itemMapper;
public function __construct(
ItemMapper $itemMapper,
Time $timeFactory,
IConfig $config,
LoggerInterface $logger
) {
parent::__construct($itemMapper, $logger);
$this->config = $config;
$this->timeFactory = $timeFactory;
$this->itemMapper = $itemMapper;
}
/**
* Returns all new items
*
* @param int|null $id the id of the feed, 0 for starred or all items
* @param int $type the type of the feed
* @param int $updatedSince a timestamp with the last modification date
* returns only items with a >= modified
* timestamp
* @param boolean $showAll if unread items should also be returned
* @param string $userId the name of the user
*
* @return array of items
*/
public function findAllNew(?int $id, $type, $updatedSince, $showAll, $userId)
{
switch ($type) {
case FeedType::FEED:
return $this->itemMapper->findAllNewFeed(
$id,
$updatedSince,
$showAll,
$userId
);
case FeedType::FOLDER:
return $this->itemMapper->findAllNewFolder(
$id,
$updatedSince,
$showAll,
$userId
);
default:
return $this->itemMapper->findAllNew(
$updatedSince,
$type,
$showAll,
$userId
);
}
}
/**
* Returns all items
*
* @param int|null $id the id of the feed, 0 for starred or all items
* @param int $type the type of the feed
* @param int $limit how many items should be returned
* @param int $offset the offset
* @param boolean $showAll if unread items should also be returned
* @param boolean $oldestFirst if it should be ordered by oldest first
* @param string $userId the name of the user
* @param string[] $search an array of keywords that the result should
* contain in either the author, title, link
* or body
*
* @return array of items
*/
public function findAllItems(
?int $id,
$type,
$limit,
$offset,
$showAll,
$oldestFirst,
$userId,
$search = []
) {
switch ($type) {
case FeedType::FEED:
return $this->itemMapper->findAllFeed(
$id,
$limit,
$offset,
$showAll,
$oldestFirst,
$userId,
$search
);
case FeedType::FOLDER:
return $this->itemMapper->findAllFolder(
$id,
$limit,
$offset,
$showAll,
$oldestFirst,
$userId,
$search
);
default:
return $this->itemMapper->findAllItems(
$limit,
$offset,
$type,
$showAll,
$oldestFirst,
$userId,
$search
);
}
}
public function findAllForUser(string $userId, array $params = []): array
{
return $this->itemMapper->findAllFromUser($userId);
}
/**
* Star or unstar an item
*
* @param int $feedId the id of the item's feed that should be starred
* @param string $guidHash the guidHash of the item that should be starred
* @param boolean $isStarred if true the item will be marked as starred,
* if false unstar
* @param string $userId the name of the user for security reasons
* @throws ServiceNotFoundException if the item does not exist
*/
public function star($feedId, $guidHash, $isStarred, $userId)
{
try {
/**
* @var Item $item
*/
$item = $this->itemMapper->findByGuidHash(
$guidHash,
$feedId,
$userId
);
$item->setStarred($isStarred);
$this->itemMapper->update($item);
} catch (DoesNotExistException $ex) {
throw new ServiceNotFoundException($ex->getMessage());
}
}
/**
* Read or unread an item
*
* @param int $itemId the id of the item that should be read
* @param boolean $isRead if true the item will be marked as read,
* if false unread
* @param string $userId the name of the user for security reasons
* @throws ServiceNotFoundException if the item does not exist
*/
public function read($itemId, $isRead, $userId)
{
try {
$lastModified = $this->timeFactory->getMicroTime();
$this->itemMapper->readItem($itemId, $isRead, $lastModified, $userId);
} catch (DoesNotExistException $ex) {
throw new ServiceNotFoundException($ex->getMessage());
}
}
/**
* Set all items read
*
* @param int $highestItemId all items below that are marked read. This is
* used to prevent marking items as read that
* the users hasn't seen yet
* @param string $userId the name of the user
*/
public function readAll($highestItemId, $userId)
{
$time = $this->timeFactory->getMicroTime();
$this->itemMapper->readAll($highestItemId, $time, $userId);
}
/**
* Set a folder read
*
* @param int|null $folderId the id of the folder that should be marked read
* @param int $highestItemId all items below that are marked read. This is
* used to prevent marking items as read that
* the users hasn't seen yet
* @param string $userId the name of the user
*/
public function readFolder(?int $folderId, $highestItemId, $userId)
{
$time = $this->timeFactory->getMicroTime();
$this->itemMapper->readFolder(
$folderId,
$highestItemId,
$time,
$userId
);
}
/**
* Set a feed read
*
* @param int $feedId the id of the feed that should be marked read
* @param int $highestItemId all items below that are marked read. This is
* used to prevent marking items as read that
* the users hasn't seen yet
* @param string $userId the name of the user
*/
public function readFeed($feedId, $highestItemId, $userId)
{
$time = $this->timeFactory->getMicroTime();
$this->itemMapper->readFeed($feedId, $highestItemId, $time, $userId);
}
/**
* This method deletes all unread feeds that are not starred and over the
* count of $this->autoPurgeCount starting by the oldest. This is to clean
* up the database so that old entries don't spam your db. As criteria for
* old, the id is taken
*/
public function autoPurgeOld()
{
$count = $this->config->getAppValue(
Application::NAME,
'autoPurgeCount',
Application::DEFAULT_SETTINGS['autoPurgeCount']
);
if ($count >= 0) {
$this->itemMapper->deleteReadOlderThanThreshold($count);
}
}
/**
* Returns the newest item id, use this for marking feeds read
*
* @param string $userId the name of the user
* @throws ServiceNotFoundException if there is no newest item
* @return int
*/
public function getNewestItemId($userId)
{
try {
return $this->itemMapper->getNewestItemId($userId);
} catch (DoesNotExistException $ex) {
throw new ServiceNotFoundException($ex->getMessage());
}
}
/**
* Returns the starred count
*
* @param string $userId the name of the user
* @return int the count
*/
public function starredCount($userId)
{
return $this->itemMapper->starredCount($userId);
}
/**
* @param string $userId from which user the items should be taken
* @return array of items which are starred or unread
*/
public function getUnreadOrStarred($userId)
{
return $this->itemMapper->findAllUnreadOrStarred($userId);
}
/**
* Deletes all items of a user
*
* @param string $userId the name of the user
*/
public function deleteUser($userId)
{
$this->itemMapper->deleteUser($userId);
}
/**
* Regenerates the search index for all items
*/
public function generateSearchIndices()
{
$this->itemMapper->updateSearchIndices();
}
public function findAll(): array
{
return $this->mapper->findAll();
}
public function shareItem($itemId, $shareWithId, $userId)
{
try {
$this->itemMapper->shareItem($itemId, $shareWithId, $userId);
} catch (DoesNotExistException $ex) {
throw new ServiceNotFoundException($ex->getMessage());
}
}
}