diff --git a/lib/Capabilities.php b/lib/Capabilities.php index d832a10fd..58cadcba1 100644 --- a/lib/Capabilities.php +++ b/lib/Capabilities.php @@ -50,6 +50,7 @@ public function getCapabilities(): array { 'user-status', 'exists', 'test-push', + 'list-filter', ], 'push' => [ 'devices', diff --git a/lib/Controller/EndpointController.php b/lib/Controller/EndpointController.php index 6a2402930..057e11b7c 100644 --- a/lib/Controller/EndpointController.php +++ b/lib/Controller/EndpointController.php @@ -57,6 +57,9 @@ public function __construct( * Get all notifications * * @param string $apiVersion Version of the API to use + * @param string $app Limit notifications to a given app + * @param string $objectType Limit notifications to a given object type + * @param string $objectId Limit notifications to a given object (only considered when object type is also provided) * @return DataResponse, array{'X-Nextcloud-User-Status': string}>|DataResponse * * 200: Notifications returned @@ -64,7 +67,7 @@ public function __construct( */ #[NoAdminRequired] #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/notifications', requirements: ['apiVersion' => '(v1|v2)'])] - public function listNotifications(string $apiVersion): DataResponse { + public function listNotifications(string $apiVersion, string $app = '', string $objectType = '', string $objectId = ''): DataResponse { $userStatus = $this->userStatusManager->getUserStatuses([ $this->getCurrentUser(), ]); @@ -84,6 +87,14 @@ public function listNotifications(string $apiVersion): DataResponse { $user = $this->session->getUser(); $filter = $this->manager->createNotification(); $filter->setUser($this->getCurrentUser()); + + if ($app !== '') { + $filter->setApp($app); + } + if ($objectType !== '') { + $filter->setObject($objectType, $objectId === '' ? Handler::FILTER_OBJECT_TYPE_ONLY : $objectId); + } + $language = $this->l10nFactory->getUserLanguage($user); $notifications = $this->handler->get($filter); diff --git a/lib/Handler.php b/lib/Handler.php index 282122d23..2087330b8 100644 --- a/lib/Handler.php +++ b/lib/Handler.php @@ -18,6 +18,8 @@ use OCP\Notification\INotification; class Handler { + public const string FILTER_OBJECT_TYPE_ONLY = 'noone-uses-this-objectid-c72c4349-00d9-4376-95c2-b1fc90f668d9'; + public function __construct( protected IDBConnection $connection, protected IManager $manager, @@ -280,7 +282,7 @@ protected function sqlWhere(IQueryBuilder $sql, INotification $notification) { $sql->andWhere($sql->expr()->eq('object_type', $sql->createNamedParameter($notification->getObjectType()))); } - if ($notification->getObjectId() !== '') { + if ($notification->getObjectId() !== '' && $notification->getObjectId() !== self::FILTER_OBJECT_TYPE_ONLY) { $sql->andWhere($sql->expr()->eq('object_id', $sql->createNamedParameter($notification->getObjectId()))); } diff --git a/openapi-full.json b/openapi-full.json index 2017f7aec..5b988068f 100644 --- a/openapi-full.json +++ b/openapi-full.json @@ -1537,6 +1537,33 @@ "default": "v2" } }, + { + "name": "app", + "in": "query", + "description": "Limit notifications to a given app", + "schema": { + "type": "string", + "default": "" + } + }, + { + "name": "objectType", + "in": "query", + "description": "Limit notifications to a given object type", + "schema": { + "type": "string", + "default": "" + } + }, + { + "name": "objectId", + "in": "query", + "description": "Limit notifications to a given object (only considered when object type is also provided)", + "schema": { + "type": "string", + "default": "" + } + }, { "name": "OCS-APIRequest", "in": "header", diff --git a/openapi.json b/openapi.json index 451b7c1b9..9fc767f08 100644 --- a/openapi.json +++ b/openapi.json @@ -319,6 +319,33 @@ "default": "v2" } }, + { + "name": "app", + "in": "query", + "description": "Limit notifications to a given app", + "schema": { + "type": "string", + "default": "" + } + }, + { + "name": "objectType", + "in": "query", + "description": "Limit notifications to a given object type", + "schema": { + "type": "string", + "default": "" + } + }, + { + "name": "objectId", + "in": "query", + "description": "Limit notifications to a given object (only considered when object type is also provided)", + "schema": { + "type": "string", + "default": "" + } + }, { "name": "OCS-APIRequest", "in": "header", diff --git a/tests/Unit/CapabilitiesTest.php b/tests/Unit/CapabilitiesTest.php index 9bfb95a1b..11586c39b 100644 --- a/tests/Unit/CapabilitiesTest.php +++ b/tests/Unit/CapabilitiesTest.php @@ -67,6 +67,7 @@ public function testGetCapabilities(bool $webpush, bool $webpushBrowser, array $ 'user-status', 'exists', 'test-push', + 'list-filter', ], 'push' => $expected, 'admin-notifications' => [ diff --git a/tests/Unit/Controller/EndpointControllerTest.php b/tests/Unit/Controller/EndpointControllerTest.php index fb1497f61..923bc1638 100644 --- a/tests/Unit/Controller/EndpointControllerTest.php +++ b/tests/Unit/Controller/EndpointControllerTest.php @@ -266,6 +266,70 @@ public function testListNotificationsNoNotifiers(string $apiVersion): void { $this->assertSame(Http::STATUS_NO_CONTENT, $response->getStatus()); } + public static function dataListNotificationsFilter(): array { + return [ + 'no filter' => ['', '', '', null, null], + 'app only' => ['spreed', '', '', 'spreed', null], + 'object type only' => ['', 'reminder', '', null, ['reminder', Handler::FILTER_OBJECT_TYPE_ONLY]], + 'object type and id' => ['', 'reminder', 'token', null, ['reminder', 'token']], + 'object id without type' => ['', '', 'token', null, null], + 'app and object' => ['spreed', 'reminder', 'token', 'spreed', ['reminder', 'token']], + ]; + } + + #[DataProvider(methodName: 'dataListNotificationsFilter')] + public function testListNotificationsFilter(string $app, string $objectType, string $objectId, ?string $expectedApp, ?array $expectedObject): void { + $controller = $this->getController(); + + $filter = $this->createMock(INotification::class); + $filter->expects($this->once()) + ->method('setUser') + ->with('username') + ->willReturn($filter); + + if ($expectedApp === null) { + $filter->expects($this->never()) + ->method('setApp'); + } else { + $filter->expects($this->once()) + ->method('setApp') + ->with($expectedApp) + ->willReturn($filter); + } + + if ($expectedObject === null) { + $filter->expects($this->never()) + ->method('setObject'); + } else { + $filter->expects($this->once()) + ->method('setObject') + ->with($expectedObject[0], $expectedObject[1]) + ->willReturn($filter); + } + + $this->manager->expects($this->once()) + ->method('hasNotifiers') + ->willReturn(true); + $this->manager->expects($this->once()) + ->method('createNotification') + ->willReturn($filter); + + $this->l10nFactory + ->method('getUserLanguage') + ->with($this->user) + ->willReturn('en'); + + $this->handler->expects($this->once()) + ->method('get') + ->with($filter) + ->willReturn([]); + + $response = $controller->listNotifications('v2', $app, $objectType, $objectId); + $this->assertInstanceOf(DataResponse::class, $response); + $this->assertSame(Http::STATUS_OK, $response->getStatus()); + $this->assertSame([], $response->getData()); + } + public static function dataGetNotification(): array { return [ ['v1', 42, 'username1'], diff --git a/tests/Unit/HandlerTest.php b/tests/Unit/HandlerTest.php index a802a5a08..4657e5425 100644 --- a/tests/Unit/HandlerTest.php +++ b/tests/Unit/HandlerTest.php @@ -223,6 +223,57 @@ public function testDeleteById(): void { $this->assertSame(0, $this->handler->count($limitedNotification), 'Wrong notification count for user1 after deleting'); } + public function testFilterByObject(): void { + $user = 'test_user1'; + foreach ([['reminder', 'token1'], ['reminder', 'token2'], ['chat', 'token1']] as [$objectType, $objectId]) { + $this->handler->add($this->getNotification([ + 'getApp' => 'testing_notifications', + 'getUser' => $user, + 'getDateTime' => new \DateTime(), + 'getObjectType' => $objectType, + 'getObjectId' => $objectId, + 'getSubject' => 'subject', + 'getSubjectParameters' => [], + 'getMessage' => 'message', + 'getMessageParameters' => [], + 'getLink' => 'https://example.tld/notification', + 'getIcon' => 'https://example.tld/icon', + 'getActions' => [], + ])); + } + + $unfiltered = $this->getNotification([ + 'getApp' => 'testing_notifications', + 'getUser' => $user, + ]); + $this->assertCount(3, $this->handler->get($unfiltered), 'Wrong notification count without an object filter'); + + $byTypeOnly = $this->getNotification([ + 'getApp' => 'testing_notifications', + 'getUser' => $user, + 'getObjectType' => 'reminder', + 'getObjectId' => Handler::FILTER_OBJECT_TYPE_ONLY, + ]); + $this->assertCount(2, $this->handler->get($byTypeOnly), 'Wrong notification count when filtering by object type only'); + $this->assertSame(2, $this->handler->count($byTypeOnly), 'Wrong notification count when filtering by object type only'); + + $byTypeAndId = $this->getNotification([ + 'getApp' => 'testing_notifications', + 'getUser' => $user, + 'getObjectType' => 'reminder', + 'getObjectId' => 'token1', + ]); + $this->assertCount(1, $this->handler->get($byTypeAndId), 'Wrong notification count when filtering by object type and id'); + + $byOtherType = $this->getNotification([ + 'getApp' => 'testing_notifications', + 'getUser' => $user, + 'getObjectType' => 'chat', + 'getObjectId' => Handler::FILTER_OBJECT_TYPE_ONLY, + ]); + $this->assertCount(1, $this->handler->get($byOtherType), 'Wrong notification count when filtering by another object type'); + } + protected function getNotification(array $values = []): INotification&MockObject { $notification = $this->getMockBuilder(INotification::class) ->getMock();