Skip to content

Commit

Permalink
EventsListener API (#259)
Browse files Browse the repository at this point in the history
Node Events
***********

``node_event`` - events about File `Nodes`

Supported event sub-types:

* NodeCreatedEvent
* NodeTouchedEvent
* NodeWrittenEvent
* NodeDeletedEvent
* NodeRenamedEvent
* NodeCopiedEvent

Signed-off-by: Alexander Piskun <bigcat88@icloud.com>
  • Loading branch information
bigcat88 committed Apr 2, 2024
1 parent 326cbe7 commit 46a51fc
Show file tree
Hide file tree
Showing 17 changed files with 718 additions and 39 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [2.3.3 - 2024-04-xx]
## [2.4.0 - 2024-04-xx]

### Added

- API for listening to file system events. #

### Fixed

Expand Down
2 changes: 1 addition & 1 deletion appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ to join us in shaping a more versatile, stable, and secure app landscape.
*Your insights, suggestions, and contributions are invaluable to us.*
]]></description>
<version>2.3.2</version>
<version>2.4.0</version>
<licence>agpl</licence>
<author mail="andrey18106x@gmail.com" homepage="https://github.com/andrey18106">Andrey Borysenko</author>
<author mail="bigcat88@icloud.com" homepage="https://github.com/bigcat88">Alexander Piskun</author>
Expand Down
5 changes: 5 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@
// Notifications
['name' => 'Notifications#sendNotification', 'url' => '/api/v1/notification', 'verb' => 'POST'],

// Events
['name' => 'EventsListener#registerListener', 'url' => '/api/v1/events_listener', 'verb' => 'POST'],
['name' => 'EventsListener#unregisterListener', 'url' => '/api/v1/events_listener', 'verb' => 'DELETE'],
['name' => 'EventsListener#getListener', 'url' => '/api/v1/events_listener', 'verb' => 'GET'],

// Talk bots
['name' => 'TalkBot#registerExAppTalkBot', 'url' => '/api/v1/talk_bot', 'verb' => 'POST'],
['name' => 'TalkBot#unregisterExAppTalkBot', 'url' => '/api/v1/talk_bot', 'verb' => 'DELETE'],
Expand Down
1 change: 1 addition & 0 deletions docs/tech_details/ApiScopes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Supported API Groups include:
* ``50`` TALK
* ``60`` TALK_BOT
* ``61`` AI_PROVIDERS
* ``62`` EVENTS_LISTENER
* ``110`` ACTIVITIES
* ``120`` NOTES
* ``200`` TEXT_PROCESSING
Expand Down
83 changes: 83 additions & 0 deletions docs/tech_details/api/events_listener.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
.. _events_listener:

===============
Events Listener
===============

This API allows you to listen to `Nextcloud events <https://docs.nextcloud.com/server/latest/developer_manual/basics/events.html#events>`_

Currently only **limited** numbers of events are supported.

Please let us know if there are any specific event we should add support to.

.. note::

Unlike PHP events, all information from events comes to ExApp **asynchronously**, more like a notification system
to no slow down the server.

Register
^^^^^^^^

OCS endpoint: ``POST /apps/app_api/api/v1/events_listener``

Params
******

.. code-block:: json
{
"eventType": "node_event",
"actionHandler": "/action_handler_route"
"eventSubtypes": [],
}
.. note:: ``eventSubtypes`` is an optional parameter, when it is not specified all event subtypes will be propagated to ExApp.

Url in ``actionHandler`` is relative to the ExApp root, starting slash is not required.

Unregister
^^^^^^^^^^

OCS endpoint: ``DELETE /apps/app_api/api/v1/events_listener``

Params
******

To unregister EventsListener, you just need to provide an `eventType` of the registered EventsListener:

.. code-block:: json
{
"eventType": "node_event"
}
Event payload
^^^^^^^^^^^^^

.. code-block:: json
{
"event_type": "node_event",
"event_subtype": "NodeCreatedEvent",
"event_data": "associative array depending on `event_subtype`"
}
Events types
^^^^^^^^^^^^

Node Events
***********

``node_event`` - events about File `Nodes`

Supported event sub-types:
* ``NodeCreatedEvent``
* ``NodeTouchedEvent``
* ``NodeWrittenEvent``
* ``NodeDeletedEvent``
* ``NodeRenamedEvent``
* ``NodeCopiedEvent``

For all Node events ``event_data`` contain key **target** which has the same format like in :ref:`FileActionsMenu payload <node_info>`

For ``NodeCopiedEvent`` and ``NodeRenamedEvent`` there is also a ``source`` key in the same format.
2 changes: 2 additions & 0 deletions docs/tech_details/api/fileactionsmenu.rst
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ To unregister FileActionsMenu, you just need to provide name of registered FileA
"name": "unique_name_of_file_action_menu"
}
.. _node_info:

Action payload to ExApp
^^^^^^^^^^^^^^^^^^^^^^^

Expand Down
1 change: 1 addition & 0 deletions docs/tech_details/api/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ AppAPI Nextcloud APIs
topmenu
settings
notifications
events_listener
talkbots
speechtotext
textprocessing
Expand Down
13 changes: 13 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use OCA\AppAPI\Listener\DeclarativeSettings\GetValueListener;
use OCA\AppAPI\Listener\DeclarativeSettings\RegisterDeclarativeSettingsListener;
use OCA\AppAPI\Listener\DeclarativeSettings\SetValueListener;
use OCA\AppAPI\Listener\FileEventsListener;
use OCA\AppAPI\Listener\LoadFilesPluginListener;
use OCA\AppAPI\Listener\SabrePluginAuthInitListener;
use OCA\AppAPI\Listener\UserDeletedListener;
Expand All @@ -30,6 +31,12 @@
use OCP\AppFramework\Bootstrap\IBootstrap;
use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Events\Node\NodeCopiedEvent;
use OCP\Files\Events\Node\NodeCreatedEvent;
use OCP\Files\Events\Node\NodeDeletedEvent;
use OCP\Files\Events\Node\NodeRenamedEvent;
use OCP\Files\Events\Node\NodeTouchedEvent;
use OCP\Files\Events\Node\NodeWrittenEvent;
use OCP\IGroupManager;
use OCP\IL10N;
use OCP\INavigationManager;
Expand Down Expand Up @@ -89,6 +96,12 @@ public function register(IRegistrationContext $context): void {
$translationService->registerExAppTranslationProviders($context, $container->getServer());
} catch (NotFoundExceptionInterface|ContainerExceptionInterface) {
}
$context->registerEventListener(NodeCreatedEvent::class, FileEventsListener::class);
$context->registerEventListener(NodeTouchedEvent::class, FileEventsListener::class);
$context->registerEventListener(NodeWrittenEvent::class, FileEventsListener::class);
$context->registerEventListener(NodeDeletedEvent::class, FileEventsListener::class);
$context->registerEventListener(NodeRenamedEvent::class, FileEventsListener::class);
$context->registerEventListener(NodeCopiedEvent::class, FileEventsListener::class);
}

public function boot(IBootContext $context): void {
Expand Down
62 changes: 62 additions & 0 deletions lib/Controller/EventsListenerController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

declare(strict_types=1);

namespace OCA\AppAPI\Controller;

use OCA\AppAPI\AppInfo\Application;
use OCA\AppAPI\Attribute\AppAPIAuth;
use OCA\AppAPI\Service\ExAppEventsListenerService;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
use OCP\IRequest;

class EventsListenerController extends OCSController {
protected $request;

public function __construct(
IRequest $request,
private readonly ExAppEventsListenerService $service,
) {
parent::__construct(Application::APP_ID, $request);

$this->request = $request;
}

#[NoCSRFRequired]
#[PublicPage]
#[AppAPIAuth]
public function registerListener(string $eventType, string $actionHandler, array $eventSubtypes = []): DataResponse {
$listener = $this->service->registerEventsListener(
$this->request->getHeader('EX-APP-ID'), $eventType, $actionHandler, $eventSubtypes);
if ($listener === null) {
return new DataResponse([], Http::STATUS_BAD_REQUEST);
}
return new DataResponse();
}

#[NoCSRFRequired]
#[PublicPage]
#[AppAPIAuth]
public function unregisterListener(string $eventType): DataResponse {
$unregistered = $this->service->unregisterEventsListener($this->request->getHeader('EX-APP-ID'), $eventType);
if (!$unregistered) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}
return new DataResponse();
}

#[AppAPIAuth]
#[PublicPage]
#[NoCSRFRequired]
public function getListener(string $eventType): DataResponse {
$result = $this->service->getEventsListener($this->request->getHeader('EX-APP-ID'), $eventType);
if (!$result) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}
return new DataResponse($result, Http::STATUS_OK);
}
}
66 changes: 66 additions & 0 deletions lib/Db/ExAppEventsListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

declare(strict_types=1);

namespace OCA\AppAPI\Db;

use JsonSerializable;
use OCP\AppFramework\Db\Entity;

/**
* Class ExAppEventsListener
*
* @package OCA\AppAPI\Db
*
* @method string getAppid()
* @method string getEventType()
* @method array getEventSubtypes()
* @method string getActionHandler()
* @method void setAppid(string $appid)
* @method void setEventType(string $eventType)
* @method void setEventSubtypes(array $eventSubtypes)
* @method void setActionHandler(string $actionHandler)
*/
class ExAppEventsListener extends Entity implements JsonSerializable {
protected $appid;
protected $eventType;
protected $eventSubtypes;
protected $icon;
protected $actionHandler;

/**
* @param array $params
*/
public function __construct(array $params = []) {
$this->addType('appid', 'string');
$this->addType('eventType', 'string');
$this->addType('eventSubtypes', 'json');
$this->addType('actionHandler', 'string');

if (isset($params['id'])) {
$this->setId($params['id']);
}
if (isset($params['appid'])) {
$this->setAppid($params['appid']);
}
if (isset($params['event_type'])) {
$this->setEventType($params['event_type']);
}
if (isset($params['event_subtypes'])) {
$this->setEventSubtypes($params['event_subtypes']);
}
if (isset($params['action_handler'])) {
$this->setActionHandler($params['action_handler']);
}
}

public function jsonSerialize(): array {
return [
'id' => $this->getId(),
'appid' => $this->getAppid(),
'event_type' => $this->getEventType(),
'event_subtypes' => $this->getEventSubtypes(),
'action_handler' => $this->getActionHandler(),
];
}
}
85 changes: 85 additions & 0 deletions lib/Db/ExAppEventsListenerMapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

declare(strict_types=1);

namespace OCA\AppAPI\Db;

use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\AppFramework\Db\QBMapper;
use OCP\DB\Exception;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;

/**
* @template-extends QBMapper<ExAppEventsListener>
*/
class ExAppEventsListenerMapper extends QBMapper {
public function __construct(IDBConnection $db) {
parent::__construct($db, 'ex_event_handlers');
}

/**
* @throws Exception
*/
public function findAllEnabled(): array {
$qb = $this->db->getQueryBuilder();
$result = $qb->select('exs.*')
->from($this->tableName, 'exs')
->innerJoin('exs', 'ex_apps', 'exa', 'exa.appid = exs.appid')
->where(
$qb->expr()->eq('exa.enabled', $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT))
)
->executeQuery();
return $result->fetchAll();
}

public function removeByAppIdEventType(string $appId, string $eventType): bool {
$qb = $this->db->getQueryBuilder();
$qb->delete($this->tableName)
->where(
$qb->expr()->eq('appid', $qb->createNamedParameter($appId, IQueryBuilder::PARAM_STR)),
$qb->expr()->eq('event_type', $qb->createNamedParameter($eventType, IQueryBuilder::PARAM_STR))
);
try {
$result = $qb->executeStatement();
if ($result) {
return true;
}
} catch (Exception) {
}
return false;
}

/**
* @throws Exception
*/
public function removeAllByAppId(string $appId): int {
$qb = $this->db->getQueryBuilder();
$qb->delete($this->tableName)
->where(
$qb->expr()->eq('appid', $qb->createNamedParameter($appId, IQueryBuilder::PARAM_STR))
);
return $qb->executeStatement();
}

/**
* @param string $appId
* @param string $eventType
*
* @return ExAppEventsListener
* @throws Exception
* @throws MultipleObjectsReturnedException if more than one result
* @throws DoesNotExistException if not found
*/
public function findByAppIdEventType(string $appId, string $eventType): ExAppEventsListener {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->tableName)
->where(
$qb->expr()->eq('appid', $qb->createNamedParameter($appId, IQueryBuilder::PARAM_STR)),
$qb->expr()->eq('event_type', $qb->createNamedParameter($eventType, IQueryBuilder::PARAM_STR))
);
return $this->findEntity($qb);
}
}
Loading

0 comments on commit 46a51fc

Please sign in to comment.