Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@
namespace OCA\Bookmarks\AppInfo;

use OCA\Bookmarks\Activity\ActivityPublisher;
use OCA\Bookmarks\ContextChat\ContextChatProvider;
use OCA\Bookmarks\Dashboard\Frequent;
use OCA\Bookmarks\Dashboard\Recent;
use OCA\Bookmarks\Events\BeforeDeleteEvent;
use OCA\Bookmarks\Events\BeforeSoftDeleteEvent;
use OCA\Bookmarks\Events\BeforeSoftUndeleteEvent;
use OCA\Bookmarks\Events\CreateEvent;
use OCA\Bookmarks\Events\InsertEvent;
use OCA\Bookmarks\Events\ManipulateEvent;
use OCA\Bookmarks\Events\MoveEvent;
use OCA\Bookmarks\Events\UpdateEvent;
use OCA\Bookmarks\Flow\CreateBookmark;
Expand All @@ -24,6 +27,7 @@
use OCA\Bookmarks\Reference\BookmarkReferenceProvider;
use OCA\Bookmarks\Search\Provider;
use OCA\Bookmarks\Service\TreeCacheManager;
use OCA\ContextChat\Event\ContentProviderRegisterEvent;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap;
Expand Down Expand Up @@ -97,6 +101,8 @@ public function register(IRegistrationContext $context): void {
$context->registerEventListener(BeforeTemplateRenderedEvent::class, BeforeTemplateRenderedListener::class);

$context->registerMiddleware(ExceptionMiddleware::class);

$context->registerEventListener(ContentProviderRegisterEvent::class, ContextChatProvider::class);
}

/**
Expand All @@ -105,6 +111,15 @@ public function register(IRegistrationContext $context): void {
* @throws \Throwable
*/
public function boot(IBootContext $context): void {
// Register with ContextChat
if (class_exists(ContentProviderRegisterEvent::class)) {
$this->getContainer()->get(ContextChatProvider::class)->register();
$eventDispatcher = $this->getContainer()->get(IEventDispatcher::class);
$eventDispatcher->addServiceListener(InsertEvent::class, ContextChatProvider::class);
$eventDispatcher->addServiceListener(ManipulateEvent::class, ContextChatProvider::class);
$eventDispatcher->addServiceListener(BeforeDeleteEvent::class, ContextChatProvider::class);
}
// Register with Nextcloud Flow
$container = $context->getServerContainer();
CreateBookmark::register($container->get(IEventDispatcher::class));
}
Expand Down
70 changes: 70 additions & 0 deletions lib/BackgroundJobs/ContextChatIndexJob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

/*
* Copyright (c) 2020-2025. The Nextcloud Bookmarks contributors.
*
* This file is licensed under the Affero General Public License version 3 or later. See the COPYING file.
*/

namespace OCA\Bookmarks\BackgroundJobs;

use OCA\Bookmarks\AppInfo\Application;
use OCA\Bookmarks\ContextChat\ContextChatProvider;
use OCA\Bookmarks\Service\BookmarkService;
use OCA\Bookmarks\Service\UserSettingsService;
use OCA\ContextChat\Public\ContentItem;
use OCA\ContextChat\Public\ContentManager;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\QueuedJob;
use OCP\IUserManager;

class ContextChatIndexJob extends QueuedJob {

public function __construct(
ITimeFactory $timeFactory,
private BookmarkService $bookmarkService,
private ?ContentManager $contentManager,
private IUserManager $userManager,
private ContextChatProvider $provider,
private UserSettingsService $userSettings,
) {
parent::__construct($timeFactory);
}

protected function run($argument) {
if ($this->contentManager === null) {
return;
}
if (!isset($argument['user'])) {
return;
}
$user = $this->userManager->get($argument['user']);
if ($user === null) {
return;
}
$this->userSettings->setUserId($user->getUID());
if ($this->userSettings->get('contextchat.enabled') !== 'true') {
return;
}
$items = [];
foreach ($this->bookmarkService->getIterator($user->getUID()) as $bookmark) {
$items[] = new ContentItem(
(string)$bookmark->getId(),
$this->provider->getId(),
$bookmark->getTitle(),
$bookmark->getTextContent(),
'Website',
new \DateTime('@' . $bookmark->getLastmodified()),
[$user->getUID()]
);
if (count($items) < 25) {
continue;
}
$this->contentManager->submitContent(Application::APP_ID, $items);
$items = [];
}
if (count($items) > 0) {
$this->contentManager->submitContent(Application::APP_ID, $items);
}
}
}
129 changes: 129 additions & 0 deletions lib/ContextChat/ContextChatProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<?php

/*
* Copyright (c) 2025. The Nextcloud Bookmarks contributors.
*
* This file is licensed under the Affero General Public License version 3 or later. See the COPYING file.
*/

namespace OCA\Bookmarks\ContextChat;

use OCA\Bookmarks\AppInfo\Application;
use OCA\Bookmarks\BackgroundJobs\ContextChatIndexJob;
use OCA\Bookmarks\Db\TreeMapper;
use OCA\Bookmarks\Events\BeforeDeleteEvent;
use OCA\Bookmarks\Events\ChangeEvent;
use OCA\Bookmarks\Events\InsertEvent;
use OCA\Bookmarks\Events\ManipulateEvent;
use OCA\Bookmarks\Service\BookmarkService;
use OCA\Bookmarks\Service\UserSettingsService;
use OCA\ContextChat\Event\ContentProviderRegisterEvent;
use OCA\ContextChat\Public\ContentItem;
use OCA\ContextChat\Public\ContentManager;
use OCA\ContextChat\Public\IContentProvider;
use OCP\BackgroundJob\IJobList;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\IUser;
use OCP\IUserManager;

/**
* @implements IEventListener<Event>
*/
class ContextChatProvider implements IContentProvider, IEventListener {

public function __construct(
private BookmarkService $bookmarkService,
private IUserManager $userManager,
private ?ContentManager $contentManager,
private IJobList $jobList,
private UserSettingsService $userSettings,
) {
}

public function handle(Event $event): void {
if ($this->contentManager === null) {
return;
}
if ($event instanceof ContentProviderRegisterEvent) {
$this->register();
return;
}
if (!$event instanceof ChangeEvent) {
return;
}
if ($this->userSettings->get('contextchat.enabled') !== 'true') {
return;
}
if ($event instanceof InsertEvent || $event instanceof ManipulateEvent) {
if ($event->getType() !== TreeMapper::TYPE_BOOKMARK) {
return;
}
$bookmark = $this->bookmarkService->findById($event->getId());
if ($bookmark === null) {
return;
}
$item = new ContentItem(
(string)$event->getId(),
$this->getId(),
$bookmark->getTitle(),
$bookmark->getTextContent(),
'Website',
new \DateTime('@' . $bookmark->getLastmodified()),
[$bookmark->getUserId()]
);
$this->contentManager->submitContent($this->getAppId(), [$item]);
return;
}
if ($event instanceof BeforeDeleteEvent) {
if ($event->getType() !== TreeMapper::TYPE_BOOKMARK) {
return;
}
$this->contentManager->deleteContent($this->getAppId(), $this->getId(), [(string)$event->getId()]);
return;
}
}

public function register(): void {
$this->contentManager->registerContentProvider($this->getAppId(), $this->getId(), self::class);
}

/**
* The ID of the provider
*
* @return string
*/
public function getId(): string {
return 'bookmarks';
}

/**
* The ID of the app making the provider available
*
* @return string
*/
public function getAppId(): string {
return Application::APP_ID;
}

/**
* The absolute URL to the content item
*
* @param string $id
* @return string
*/
public function getItemUrl(string $id): string {
return $this->bookmarkService->findById(intval($id))?->getUrl() ?? '';
}

/**
* Starts the initial import of content items into content chat
*
* @return void
*/
public function triggerInitialImport(): void {
$this->userManager->callForAllUsers(function (IUser $user) {
$this->jobList->add(ContextChatIndexJob::class, ['user' => $user->getUID()]);
});
}
}
6 changes: 6 additions & 0 deletions lib/Controller/WebViewController.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use OCA\Bookmarks\Db\PublicFolderMapper;
use OCA\Bookmarks\Service\SettingsService;
use OCA\Bookmarks\Service\UserSettingsService;
use OCP\App\IAppManager;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
Expand All @@ -23,6 +24,7 @@
use OCP\AppFramework\Http\NotFoundResponse;
use OCP\AppFramework\Http\StreamResponse;
use OCP\AppFramework\Http\Template\PublicTemplateResponse;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IRequest;
use OCP\IURLGenerator;
Expand All @@ -49,6 +51,8 @@ public function __construct(
private \OCA\Bookmarks\Controller\InternalTagsController $tagsController,
private UserSettingsService $userSettingsService,
private SettingsService $settings,
private IAppManager $appManager,
private IConfig $config,
) {
parent::__construct($appName, $request);
$this->userId = $userId;
Expand Down Expand Up @@ -78,6 +82,8 @@ public function index(): AugmentedTemplateResponse {
$this->initialState->provideInitialState($this->appName, 'allClicksCount', $this->bookmarkController->countAllClicks()->getData()['item']);
$this->initialState->provideInitialState($this->appName, 'withClicksCount', $this->bookmarkController->countWithClicks()->getData()['item']);
$this->initialState->provideInitialState($this->appName, 'tags', $this->tagsController->fullTags(true)->getData());
$this->initialState->provideInitialState($this->appName, 'contextChatInstalled', $this->appManager->isEnabledForUser('context_chat'));
$this->initialState->provideInitialState($this->appName, 'appStoreEnabled', $this->config->getSystemValueBool('appstoreenabled', true));

$settings = $this->userSettingsService->toArray();
$settings['shareapi_allow_links'] = $this->settings->getLinkSharingAllowed();
Expand Down
59 changes: 58 additions & 1 deletion lib/Db/BookmarkMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
namespace OCA\Bookmarks\Db;

use OCA\Bookmarks\Events\BeforeDeleteEvent;
use OCA\Bookmarks\Events\InsertEvent;
use OCA\Bookmarks\Events\ManipulateEvent;
use OCA\Bookmarks\Exception\AlreadyExistsError;
use OCA\Bookmarks\Exception\UrlParseError;
use OCA\Bookmarks\Exception\UserLimitExceededError;
Expand Down Expand Up @@ -257,6 +259,21 @@ protected function findEntitiesWithRawQuery(string $query, array $params, array
return $entities;
}

/**
* @throws \OCP\DB\Exception
*/
protected function getIteratorWithRawQuery(string $query, array $params, array $types): \Generator {
$cursor = $this->db->executeQuery($query, $params, $types);

$entities = [];

while ($row = $cursor->fetch()) {
yield $this->mapRowToEntity($row);
}

$cursor->closeCursor();
}

/**
* Common table expression that lists all items in a given folder, recursively
* @param int $folderId
Expand Down Expand Up @@ -773,7 +790,9 @@ public function update(Entity $entity): Bookmark {
$entity->setUrl($this->urlNormalizer->normalize($entity->getUrl()));
}
$entity->setLastmodified(time());
return parent::update($entity);
parent::update($entity);
$this->eventDispatcher->dispatchTyped(new ManipulateEvent('bookmark', $entity->getId()));
return $entity;
}

/**
Expand Down Expand Up @@ -806,6 +825,7 @@ public function insert(Entity $entity): Bookmark {
$this->findByUrl($entity->getUserId(), $entity->getUrl());
} catch (DoesNotExistException $e) {
parent::insert($entity);
$this->eventDispatcher->dispatchTyped(new InsertEvent('bookmark', $entity->getId()));
return $entity;
} catch (MultipleObjectsReturnedException $e) {
// noop
Expand Down Expand Up @@ -904,4 +924,41 @@ private function _findSharersFor(string $userId) :array {
return $share->getOwner();
}, $this->shareMapper->findByUser($userId));
}

public function getIterator(string $userId, QueryParameters $queryParams): \Generator {
$rootFolder = $this->folderMapper->findRootFolder($userId);
// gives us all bookmarks in this folder, recursively
[$cte, $params, $paramTypes] = $this->_generateCTE($rootFolder->getId(), $queryParams->getSoftDeletedFolders());

$qb = $this->db->getQueryBuilder();
$bookmark_cols = array_map(static function ($c) {
return 'b.' . $c;
}, Bookmark::$columns);

$qb->select($bookmark_cols);
$qb->groupBy($bookmark_cols);

$qb->automaticTablePrefix(false);

$qb
->from('*PREFIX*bookmarks', 'b')
->innerJoin('b', 'folder_tree', 'tree', 'tree.item_id = b.id AND tree.type = ' . $qb->createPositionalParameter(TreeMapper::TYPE_BOOKMARK)
. ($queryParams->getSoftDeleted() ? ' AND tree.soft_deleted_at is NOT NULL' : ' AND tree.soft_deleted_at is NULL'));

$this->_filterUrl($qb, $queryParams);
$this->_filterArchived($qb, $queryParams);
$this->_filterUnavailable($qb, $queryParams);
$this->_filterDuplicated($qb, $queryParams);
$this->_filterFolder($qb, $queryParams);
$this->_filterTags($qb, $queryParams);
$this->_filterUntagged($qb, $queryParams);
$this->_filterSearch($qb, $queryParams);
$this->_sortAndPaginate($qb, $queryParams);

$finalQuery = $cte . ' ' . $qb->getSQL();

$params = array_merge($params, $qb->getParameters());
$paramTypes = array_merge($paramTypes, $qb->getParameterTypes());
return $this->getIteratorWithRawQuery($finalQuery, $params, $paramTypes);
}
}
16 changes: 16 additions & 0 deletions lib/Events/InsertEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

/*
* Copyright (c) 2020-2024. The Nextcloud Bookmarks contributors.
*
* This file is licensed under the Affero General Public License version 3 or later. See the COPYING file.
*/

namespace OCA\Bookmarks\Events;

/**
* Event emitted when a bookmarks entity is inserted into the DB
* Not exposed via the activity app
*/
class InsertEvent extends ChangeEvent {
}
Loading
Loading