Skip to content

Commit 4a2a66d

Browse files
authored
Merge pull request #2271 from nextcloud/enh/context-chat
feat: Add a ContextChat provider
2 parents 79ef8d3 + 5b56b85 commit 4a2a66d

14 files changed

Lines changed: 453 additions & 90 deletions

lib/AppInfo/Application.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@
99
namespace OCA\Bookmarks\AppInfo;
1010

1111
use OCA\Bookmarks\Activity\ActivityPublisher;
12+
use OCA\Bookmarks\ContextChat\ContextChatProvider;
1213
use OCA\Bookmarks\Dashboard\Frequent;
1314
use OCA\Bookmarks\Dashboard\Recent;
1415
use OCA\Bookmarks\Events\BeforeDeleteEvent;
1516
use OCA\Bookmarks\Events\BeforeSoftDeleteEvent;
1617
use OCA\Bookmarks\Events\BeforeSoftUndeleteEvent;
1718
use OCA\Bookmarks\Events\CreateEvent;
19+
use OCA\Bookmarks\Events\InsertEvent;
20+
use OCA\Bookmarks\Events\ManipulateEvent;
1821
use OCA\Bookmarks\Events\MoveEvent;
1922
use OCA\Bookmarks\Events\UpdateEvent;
2023
use OCA\Bookmarks\Flow\CreateBookmark;
@@ -24,6 +27,7 @@
2427
use OCA\Bookmarks\Reference\BookmarkReferenceProvider;
2528
use OCA\Bookmarks\Search\Provider;
2629
use OCA\Bookmarks\Service\TreeCacheManager;
30+
use OCA\ContextChat\Event\ContentProviderRegisterEvent;
2731
use OCP\AppFramework\App;
2832
use OCP\AppFramework\Bootstrap\IBootContext;
2933
use OCP\AppFramework\Bootstrap\IBootstrap;
@@ -97,6 +101,8 @@ public function register(IRegistrationContext $context): void {
97101
$context->registerEventListener(BeforeTemplateRenderedEvent::class, BeforeTemplateRenderedListener::class);
98102

99103
$context->registerMiddleware(ExceptionMiddleware::class);
104+
105+
$context->registerEventListener(ContentProviderRegisterEvent::class, ContextChatProvider::class);
100106
}
101107

102108
/**
@@ -105,6 +111,15 @@ public function register(IRegistrationContext $context): void {
105111
* @throws \Throwable
106112
*/
107113
public function boot(IBootContext $context): void {
114+
// Register with ContextChat
115+
if (class_exists(ContentProviderRegisterEvent::class)) {
116+
$this->getContainer()->get(ContextChatProvider::class)->register();
117+
$eventDispatcher = $this->getContainer()->get(IEventDispatcher::class);
118+
$eventDispatcher->addServiceListener(InsertEvent::class, ContextChatProvider::class);
119+
$eventDispatcher->addServiceListener(ManipulateEvent::class, ContextChatProvider::class);
120+
$eventDispatcher->addServiceListener(BeforeDeleteEvent::class, ContextChatProvider::class);
121+
}
122+
// Register with Nextcloud Flow
108123
$container = $context->getServerContainer();
109124
CreateBookmark::register($container->get(IEventDispatcher::class));
110125
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
/*
4+
* Copyright (c) 2020-2025. The Nextcloud Bookmarks contributors.
5+
*
6+
* This file is licensed under the Affero General Public License version 3 or later. See the COPYING file.
7+
*/
8+
9+
namespace OCA\Bookmarks\BackgroundJobs;
10+
11+
use OCA\Bookmarks\AppInfo\Application;
12+
use OCA\Bookmarks\ContextChat\ContextChatProvider;
13+
use OCA\Bookmarks\Service\BookmarkService;
14+
use OCA\Bookmarks\Service\UserSettingsService;
15+
use OCA\ContextChat\Public\ContentItem;
16+
use OCA\ContextChat\Public\ContentManager;
17+
use OCP\AppFramework\Utility\ITimeFactory;
18+
use OCP\BackgroundJob\QueuedJob;
19+
use OCP\IUserManager;
20+
21+
class ContextChatIndexJob extends QueuedJob {
22+
23+
public function __construct(
24+
ITimeFactory $timeFactory,
25+
private BookmarkService $bookmarkService,
26+
private ?ContentManager $contentManager,
27+
private IUserManager $userManager,
28+
private ContextChatProvider $provider,
29+
private UserSettingsService $userSettings,
30+
) {
31+
parent::__construct($timeFactory);
32+
}
33+
34+
protected function run($argument) {
35+
if ($this->contentManager === null) {
36+
return;
37+
}
38+
if (!isset($argument['user'])) {
39+
return;
40+
}
41+
$user = $this->userManager->get($argument['user']);
42+
if ($user === null) {
43+
return;
44+
}
45+
$this->userSettings->setUserId($user->getUID());
46+
if ($this->userSettings->get('contextchat.enabled') !== 'true') {
47+
return;
48+
}
49+
$items = [];
50+
foreach ($this->bookmarkService->getIterator($user->getUID()) as $bookmark) {
51+
$items[] = new ContentItem(
52+
(string)$bookmark->getId(),
53+
$this->provider->getId(),
54+
$bookmark->getTitle(),
55+
$bookmark->getTextContent(),
56+
'Website',
57+
new \DateTime('@' . $bookmark->getLastmodified()),
58+
[$user->getUID()]
59+
);
60+
if (count($items) < 25) {
61+
continue;
62+
}
63+
$this->contentManager->submitContent(Application::APP_ID, $items);
64+
$items = [];
65+
}
66+
if (count($items) > 0) {
67+
$this->contentManager->submitContent(Application::APP_ID, $items);
68+
}
69+
}
70+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
<?php
2+
3+
/*
4+
* Copyright (c) 2025. The Nextcloud Bookmarks contributors.
5+
*
6+
* This file is licensed under the Affero General Public License version 3 or later. See the COPYING file.
7+
*/
8+
9+
namespace OCA\Bookmarks\ContextChat;
10+
11+
use OCA\Bookmarks\AppInfo\Application;
12+
use OCA\Bookmarks\BackgroundJobs\ContextChatIndexJob;
13+
use OCA\Bookmarks\Db\TreeMapper;
14+
use OCA\Bookmarks\Events\BeforeDeleteEvent;
15+
use OCA\Bookmarks\Events\ChangeEvent;
16+
use OCA\Bookmarks\Events\InsertEvent;
17+
use OCA\Bookmarks\Events\ManipulateEvent;
18+
use OCA\Bookmarks\Service\BookmarkService;
19+
use OCA\Bookmarks\Service\UserSettingsService;
20+
use OCA\ContextChat\Event\ContentProviderRegisterEvent;
21+
use OCA\ContextChat\Public\ContentItem;
22+
use OCA\ContextChat\Public\ContentManager;
23+
use OCA\ContextChat\Public\IContentProvider;
24+
use OCP\BackgroundJob\IJobList;
25+
use OCP\EventDispatcher\Event;
26+
use OCP\EventDispatcher\IEventListener;
27+
use OCP\IUser;
28+
use OCP\IUserManager;
29+
30+
/**
31+
* @implements IEventListener<Event>
32+
*/
33+
class ContextChatProvider implements IContentProvider, IEventListener {
34+
35+
public function __construct(
36+
private BookmarkService $bookmarkService,
37+
private IUserManager $userManager,
38+
private ?ContentManager $contentManager,
39+
private IJobList $jobList,
40+
private UserSettingsService $userSettings,
41+
) {
42+
}
43+
44+
public function handle(Event $event): void {
45+
if ($this->contentManager === null) {
46+
return;
47+
}
48+
if ($event instanceof ContentProviderRegisterEvent) {
49+
$this->register();
50+
return;
51+
}
52+
if (!$event instanceof ChangeEvent) {
53+
return;
54+
}
55+
if ($this->userSettings->get('contextchat.enabled') !== 'true') {
56+
return;
57+
}
58+
if ($event instanceof InsertEvent || $event instanceof ManipulateEvent) {
59+
if ($event->getType() !== TreeMapper::TYPE_BOOKMARK) {
60+
return;
61+
}
62+
$bookmark = $this->bookmarkService->findById($event->getId());
63+
if ($bookmark === null) {
64+
return;
65+
}
66+
$item = new ContentItem(
67+
(string)$event->getId(),
68+
$this->getId(),
69+
$bookmark->getTitle(),
70+
$bookmark->getTextContent(),
71+
'Website',
72+
new \DateTime('@' . $bookmark->getLastmodified()),
73+
[$bookmark->getUserId()]
74+
);
75+
$this->contentManager->submitContent($this->getAppId(), [$item]);
76+
return;
77+
}
78+
if ($event instanceof BeforeDeleteEvent) {
79+
if ($event->getType() !== TreeMapper::TYPE_BOOKMARK) {
80+
return;
81+
}
82+
$this->contentManager->deleteContent($this->getAppId(), $this->getId(), [(string)$event->getId()]);
83+
return;
84+
}
85+
}
86+
87+
public function register(): void {
88+
$this->contentManager->registerContentProvider($this->getAppId(), $this->getId(), self::class);
89+
}
90+
91+
/**
92+
* The ID of the provider
93+
*
94+
* @return string
95+
*/
96+
public function getId(): string {
97+
return 'bookmarks';
98+
}
99+
100+
/**
101+
* The ID of the app making the provider available
102+
*
103+
* @return string
104+
*/
105+
public function getAppId(): string {
106+
return Application::APP_ID;
107+
}
108+
109+
/**
110+
* The absolute URL to the content item
111+
*
112+
* @param string $id
113+
* @return string
114+
*/
115+
public function getItemUrl(string $id): string {
116+
return $this->bookmarkService->findById(intval($id))?->getUrl() ?? '';
117+
}
118+
119+
/**
120+
* Starts the initial import of content items into content chat
121+
*
122+
* @return void
123+
*/
124+
public function triggerInitialImport(): void {
125+
$this->userManager->callForAllUsers(function (IUser $user) {
126+
$this->jobList->add(ContextChatIndexJob::class, ['user' => $user->getUID()]);
127+
});
128+
}
129+
}

lib/Controller/WebViewController.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use OCA\Bookmarks\Db\PublicFolderMapper;
1616
use OCA\Bookmarks\Service\SettingsService;
1717
use OCA\Bookmarks\Service\UserSettingsService;
18+
use OCP\App\IAppManager;
1819
use OCP\AppFramework\Controller;
1920
use OCP\AppFramework\Db\DoesNotExistException;
2021
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
@@ -23,6 +24,7 @@
2324
use OCP\AppFramework\Http\NotFoundResponse;
2425
use OCP\AppFramework\Http\StreamResponse;
2526
use OCP\AppFramework\Http\Template\PublicTemplateResponse;
27+
use OCP\IConfig;
2628
use OCP\IL10N;
2729
use OCP\IRequest;
2830
use OCP\IURLGenerator;
@@ -49,6 +51,8 @@ public function __construct(
4951
private \OCA\Bookmarks\Controller\InternalTagsController $tagsController,
5052
private UserSettingsService $userSettingsService,
5153
private SettingsService $settings,
54+
private IAppManager $appManager,
55+
private IConfig $config,
5256
) {
5357
parent::__construct($appName, $request);
5458
$this->userId = $userId;
@@ -78,6 +82,8 @@ public function index(): AugmentedTemplateResponse {
7882
$this->initialState->provideInitialState($this->appName, 'allClicksCount', $this->bookmarkController->countAllClicks()->getData()['item']);
7983
$this->initialState->provideInitialState($this->appName, 'withClicksCount', $this->bookmarkController->countWithClicks()->getData()['item']);
8084
$this->initialState->provideInitialState($this->appName, 'tags', $this->tagsController->fullTags(true)->getData());
85+
$this->initialState->provideInitialState($this->appName, 'contextChatInstalled', $this->appManager->isEnabledForUser('context_chat'));
86+
$this->initialState->provideInitialState($this->appName, 'appStoreEnabled', $this->config->getSystemValueBool('appstoreenabled', true));
8187

8288
$settings = $this->userSettingsService->toArray();
8389
$settings['shareapi_allow_links'] = $this->settings->getLinkSharingAllowed();

lib/Db/BookmarkMapper.php

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
namespace OCA\Bookmarks\Db;
1010

1111
use OCA\Bookmarks\Events\BeforeDeleteEvent;
12+
use OCA\Bookmarks\Events\InsertEvent;
13+
use OCA\Bookmarks\Events\ManipulateEvent;
1214
use OCA\Bookmarks\Exception\AlreadyExistsError;
1315
use OCA\Bookmarks\Exception\UrlParseError;
1416
use OCA\Bookmarks\Exception\UserLimitExceededError;
@@ -257,6 +259,21 @@ protected function findEntitiesWithRawQuery(string $query, array $params, array
257259
return $entities;
258260
}
259261

262+
/**
263+
* @throws \OCP\DB\Exception
264+
*/
265+
protected function getIteratorWithRawQuery(string $query, array $params, array $types): \Generator {
266+
$cursor = $this->db->executeQuery($query, $params, $types);
267+
268+
$entities = [];
269+
270+
while ($row = $cursor->fetch()) {
271+
yield $this->mapRowToEntity($row);
272+
}
273+
274+
$cursor->closeCursor();
275+
}
276+
260277
/**
261278
* Common table expression that lists all items in a given folder, recursively
262279
* @param int $folderId
@@ -773,7 +790,9 @@ public function update(Entity $entity): Bookmark {
773790
$entity->setUrl($this->urlNormalizer->normalize($entity->getUrl()));
774791
}
775792
$entity->setLastmodified(time());
776-
return parent::update($entity);
793+
parent::update($entity);
794+
$this->eventDispatcher->dispatchTyped(new ManipulateEvent('bookmark', $entity->getId()));
795+
return $entity;
777796
}
778797

779798
/**
@@ -806,6 +825,7 @@ public function insert(Entity $entity): Bookmark {
806825
$this->findByUrl($entity->getUserId(), $entity->getUrl());
807826
} catch (DoesNotExistException $e) {
808827
parent::insert($entity);
828+
$this->eventDispatcher->dispatchTyped(new InsertEvent('bookmark', $entity->getId()));
809829
return $entity;
810830
} catch (MultipleObjectsReturnedException $e) {
811831
// noop
@@ -904,4 +924,41 @@ private function _findSharersFor(string $userId) :array {
904924
return $share->getOwner();
905925
}, $this->shareMapper->findByUser($userId));
906926
}
927+
928+
public function getIterator(string $userId, QueryParameters $queryParams): \Generator {
929+
$rootFolder = $this->folderMapper->findRootFolder($userId);
930+
// gives us all bookmarks in this folder, recursively
931+
[$cte, $params, $paramTypes] = $this->_generateCTE($rootFolder->getId(), $queryParams->getSoftDeletedFolders());
932+
933+
$qb = $this->db->getQueryBuilder();
934+
$bookmark_cols = array_map(static function ($c) {
935+
return 'b.' . $c;
936+
}, Bookmark::$columns);
937+
938+
$qb->select($bookmark_cols);
939+
$qb->groupBy($bookmark_cols);
940+
941+
$qb->automaticTablePrefix(false);
942+
943+
$qb
944+
->from('*PREFIX*bookmarks', 'b')
945+
->innerJoin('b', 'folder_tree', 'tree', 'tree.item_id = b.id AND tree.type = ' . $qb->createPositionalParameter(TreeMapper::TYPE_BOOKMARK)
946+
. ($queryParams->getSoftDeleted() ? ' AND tree.soft_deleted_at is NOT NULL' : ' AND tree.soft_deleted_at is NULL'));
947+
948+
$this->_filterUrl($qb, $queryParams);
949+
$this->_filterArchived($qb, $queryParams);
950+
$this->_filterUnavailable($qb, $queryParams);
951+
$this->_filterDuplicated($qb, $queryParams);
952+
$this->_filterFolder($qb, $queryParams);
953+
$this->_filterTags($qb, $queryParams);
954+
$this->_filterUntagged($qb, $queryParams);
955+
$this->_filterSearch($qb, $queryParams);
956+
$this->_sortAndPaginate($qb, $queryParams);
957+
958+
$finalQuery = $cte . ' ' . $qb->getSQL();
959+
960+
$params = array_merge($params, $qb->getParameters());
961+
$paramTypes = array_merge($paramTypes, $qb->getParameterTypes());
962+
return $this->getIteratorWithRawQuery($finalQuery, $params, $paramTypes);
963+
}
907964
}

lib/Events/InsertEvent.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
/*
4+
* Copyright (c) 2020-2024. The Nextcloud Bookmarks contributors.
5+
*
6+
* This file is licensed under the Affero General Public License version 3 or later. See the COPYING file.
7+
*/
8+
9+
namespace OCA\Bookmarks\Events;
10+
11+
/**
12+
* Event emitted when a bookmarks entity is inserted into the DB
13+
* Not exposed via the activity app
14+
*/
15+
class InsertEvent extends ChangeEvent {
16+
}

0 commit comments

Comments
 (0)