From 85db77bb7b160bd0649807c4969434229919efa3 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Thu, 18 Jan 2024 00:22:25 +0100 Subject: [PATCH] [#298] Merged `nextcloud` backend and plugin. Closes: #298 --- docs/source/backends.rst | 1 - docs/source/platypush/backend/nextcloud.rst | 5 - platypush/backend/nextcloud/__init__.py | 170 ----- platypush/backend/nextcloud/manifest.yaml | 10 - platypush/message/event/nextcloud.py | 98 ++- platypush/plugins/nextcloud/__init__.py | 747 +++++++++++++++----- platypush/plugins/nextcloud/manifest.yaml | 5 +- 7 files changed, 651 insertions(+), 385 deletions(-) delete mode 100644 docs/source/platypush/backend/nextcloud.rst delete mode 100644 platypush/backend/nextcloud/__init__.py delete mode 100644 platypush/backend/nextcloud/manifest.yaml diff --git a/docs/source/backends.rst b/docs/source/backends.rst index 26dcef1852..7de6dc1b35 100644 --- a/docs/source/backends.rst +++ b/docs/source/backends.rst @@ -20,7 +20,6 @@ Backends platypush/backend/music.mopidy.rst platypush/backend/music.mpd.rst platypush/backend/music.spotify.rst - platypush/backend/nextcloud.rst platypush/backend/nfc.rst platypush/backend/nodered.rst platypush/backend/redis.rst diff --git a/docs/source/platypush/backend/nextcloud.rst b/docs/source/platypush/backend/nextcloud.rst deleted file mode 100644 index 9a755b0cc4..0000000000 --- a/docs/source/platypush/backend/nextcloud.rst +++ /dev/null @@ -1,5 +0,0 @@ -``nextcloud`` -=============================== - -.. automodule:: platypush.backend.nextcloud - :members: diff --git a/platypush/backend/nextcloud/__init__.py b/platypush/backend/nextcloud/__init__.py deleted file mode 100644 index a873b05148..0000000000 --- a/platypush/backend/nextcloud/__init__.py +++ /dev/null @@ -1,170 +0,0 @@ -from typing import Optional - -from platypush.backend import Backend -from platypush.context import get_plugin -from platypush.message.event.nextcloud import NextCloudActivityEvent -from platypush.plugins.nextcloud import NextcloudPlugin -from platypush.plugins.variable import VariablePlugin - - -class NextcloudBackend(Backend): - """ - This backend triggers events when new activities occur on a NextCloud instance. - - The field ``activity_type`` in the triggered :class:`platypush.message.event.nextcloud.NextCloudActivityEvent` - events identifies the activity type (e.g. ``file_created``, ``file_deleted``, - ``file_changed``). Example in the case of the creation of new files: - - .. code-block:: json - - { - "activity_id": 387, - "app": "files", - "activity_type": "file_created", - "user": "your-user", - "subject": "You created InstantUpload/Camera/IMG_0100.jpg", - "subject_rich": [ - "You created {file3}, {file2} and {file1}", - { - "file1": { - "type": "file", - "id": "41994", - "name": "IMG_0100.jpg", - "path": "InstantUpload/Camera/IMG_0100.jpg", - "link": "https://your-domain/nextcloud/index.php/f/41994" - }, - "file2": { - "type": "file", - "id": "42005", - "name": "IMG_0101.jpg", - "path": "InstantUpload/Camera/IMG_0102.jpg", - "link": "https://your-domain/nextcloud/index.php/f/42005" - }, - "file3": { - "type": "file", - "id": "42014", - "name": "IMG_0102.jpg", - "path": "InstantUpload/Camera/IMG_0102.jpg", - "link": "https://your-domain/nextcloud/index.php/f/42014" - } - } - ], - "message": "", - "message_rich": [ - "", - [] - ], - "object_type": "files", - "object_id": 41994, - "object_name": "/InstantUpload/Camera/IMG_0102.jpg", - "objects": { - "42014": "/InstantUpload/Camera/IMG_0100.jpg", - "42005": "/InstantUpload/Camera/IMG_0101.jpg", - "41994": "/InstantUpload/Camera/IMG_0102.jpg" - }, - "link": "https://your-domain/nextcloud/index.php/apps/files/?dir=/InstantUpload/Camera", - "icon": "https://your-domain/nextcloud/apps/files/img/add-color.svg", - "datetime": "2020-09-07T17:04:29+00:00" - } - - """ - - _LAST_ACTIVITY_VARNAME = '_NEXTCLOUD_LAST_ACTIVITY_ID' - - def __init__( - self, - url: Optional[str] = None, - username: Optional[str] = None, - password: Optional[str] = None, - object_type: Optional[str] = None, - object_id: Optional[int] = None, - poll_seconds: Optional[float] = 60.0, - **kwargs - ): - """ - :param url: NextCloud instance URL (default: same as the :class:`platypush.plugins.nextcloud.NextCloudPlugin`). - :param username: NextCloud username (default: same as the :class:`platypush.plugins.nextcloud.NextCloudPlugin`). - :param password: NextCloud password (default: same as the :class:`platypush.plugins.nextcloud.NextCloudPlugin`). - :param object_type: If set, only filter events on this type of object. - :param object_id: If set, only filter events on this object ID. - :param poll_seconds: How often the backend should poll the instance (default: one minute). - """ - super().__init__(**kwargs) - self.url: Optional[str] = None - self.username: Optional[str] = None - self.password: Optional[str] = None - self.object_type = object_type - self.object_id = object_id - self.poll_seconds = poll_seconds - self._last_seen_id = None - - try: - plugin: Optional[NextcloudPlugin] = get_plugin('nextcloud') - if plugin: - self.url = plugin.conf.url - self.username = plugin.conf.username - self.password = plugin.conf.password - except Exception as e: - self.logger.info('NextCloud plugin not configured: {}'.format(str(e))) - - self.url = url if url else self.url - self.username = username if username else self.username - self.password = password if password else self.password - - assert ( - self.url and self.username and self.password - ), 'No configuration provided neither for the NextCloud plugin nor the backend' - - @property - def last_seen_id(self) -> Optional[int]: - if self._last_seen_id is None: - variables: VariablePlugin = get_plugin('variable') - last_seen_id = variables.get(self._LAST_ACTIVITY_VARNAME).output.get( - self._LAST_ACTIVITY_VARNAME - ) - self._last_seen_id = last_seen_id - - return self._last_seen_id - - @last_seen_id.setter - def last_seen_id(self, value: Optional[int]): - variables: VariablePlugin = get_plugin('variable') - variables.set(**{self._LAST_ACTIVITY_VARNAME: value}) - self._last_seen_id = value - - @staticmethod - def _activity_to_event(activity: dict) -> NextCloudActivityEvent: - return NextCloudActivityEvent(activity_type=activity.pop('type'), **activity) - - def loop(self): - last_seen_id = int(self.last_seen_id) - new_last_seen_id = int(last_seen_id) - plugin: NextcloudPlugin = get_plugin('nextcloud') - # noinspection PyUnresolvedReferences - activities = plugin.get_activities( - sort='desc', - url=self.url, - username=self.username, - password=self.password, - object_type=self.object_type, - object_id=self.object_id, - ).output - - events = [] - for activity in activities: - if last_seen_id and activity['activity_id'] <= last_seen_id: - break - - events.append(self._activity_to_event(activity)) - - if not new_last_seen_id or activity['activity_id'] > new_last_seen_id: - new_last_seen_id = int(activity['activity_id']) - - for evt in events[::-1]: - self.bus.post(evt) - - if new_last_seen_id and last_seen_id != new_last_seen_id: - self.last_seen_id = new_last_seen_id - - -# vim:sw=4:ts=4:et: diff --git a/platypush/backend/nextcloud/manifest.yaml b/platypush/backend/nextcloud/manifest.yaml deleted file mode 100644 index 432a7a5b0c..0000000000 --- a/platypush/backend/nextcloud/manifest.yaml +++ /dev/null @@ -1,10 +0,0 @@ -manifest: - events: - platypush.message.event.nextcloud.NextCloudActivityEvent: 'when new activity occurs - on the instance.The field ``activity_type`` identifies the activity type (e.g. - ``file_created``, ``file_deleted``,``file_changed``). Example in the case of - the creation of new files:' - install: - pip: [] - package: platypush.backend.nextcloud - type: backend diff --git a/platypush/message/event/nextcloud.py b/platypush/message/event/nextcloud.py index bf39a81a42..3267ef3b25 100644 --- a/platypush/message/event/nextcloud.py +++ b/platypush/message/event/nextcloud.py @@ -1,9 +1,103 @@ +from datetime import datetime as dt +from typing import Optional + from platypush.message.event import Event class NextCloudActivityEvent(Event): - def __init__(self, activity_id: int, activity_type: str, *args, **kwargs): - super().__init__(*args, activity_id=activity_id, activity_type=activity_type, **kwargs) + """ + Event triggered when a new activity is detected on a NextCloud instance. + """ + + def __init__( + self, + *args, + activity_id: int, + activity_type: str, + object_id: int, + object_type: str, + object_name: str, + app: str, + user: str, + subject: str, + message: str, + subject_rich: Optional[list] = None, + message_rich: Optional[list] = None, + objects: Optional[dict] = None, + link: Optional[str] = None, + icon: Optional[str] = None, + datetime: Optional[dt] = None, + **kwargs, + ): + """ + :param activity_id: Activity ID. + :param activity_type: Activity type - can be ``file_created``, + ``file_deleted``, ``file_changed``, ``file_restored``, + ``file_shared``, ``file_unshared``, ``file_downloaded``, etc. + :param object_id: Object ID. + :param object_type: Object type - can be files, comment, tag, share, + etc. + :param object_name: Object name. In the case of files, it's the file + path relative to the user's root directory. + :param app: Application that generated the activity. + :param user: User that generated the activity. + :param subject: Activity subject, in plain text. For example, *You + created hd/test1.txt and hd/test2.txt*. + :param message: Activity message, in plain text. + :param subject_rich: Activity subject, in rich/structured format. + Example: + + .. code-block:: json + + [ + "You created {file2} and {file1}", + { + "file1": { + "type": "file", + "id": "1234", + "name": "test1.txt", + "path": "hd/text1.txt", + "link": "https://cloud.example.com/index.php/f/1234" + }, + "file2": { + "type": "file", + "id": "1235", + "name": "test2.txt", + "path": "hd/text2.txt", + "link": "https://cloud.example.com/index.php/f/1235" + } + } + ] + + :param message_rich: Activity message, in rich/structured format. + :param objects: Additional objects associated to the activity, in the + format ``{object_id: object}``. For example, if the activity + involves files, the ``objects`` dictionary will contain the mapping + of the involved files in the format ``{file_id: path}``. + :param link: Link to the main object of this activity. Example: + ``https://cloud.example.com/index.php/files/apps/files/?dir=/hd&fileid=1234`` + :param icon: URL of the icon associated to the activity. + :param datetime: Activity timestamp. + """ + super().__init__( + *args, + activity_id=activity_id, + activity_type=activity_type, + object_id=object_id, + object_type=object_type, + object_name=object_name, + app=app, + user=user, + subject=subject, + subject_rich=subject_rich, + message=message, + message_rich=message_rich, + objects=objects or {}, + link=link, + icon=icon, + datetime=datetime, + **kwargs, + ) # vim:sw=4:ts=4:et: diff --git a/platypush/plugins/nextcloud/__init__.py b/platypush/plugins/nextcloud/__init__.py index abf63c6aa8..3b561a3a1f 100644 --- a/platypush/plugins/nextcloud/__init__.py +++ b/platypush/plugins/nextcloud/__init__.py @@ -4,11 +4,17 @@ from datetime import datetime from enum import IntEnum from typing import Optional, List, Union, Dict -from platypush.plugins import Plugin, action +from platypush.context import Variable +from platypush.message.event.nextcloud import NextCloudActivityEvent +from platypush.plugins import RunnablePlugin, action @dataclass class ClientConfig: + """ + Configuration for the NextCloud client. + """ + url: str username: str password: str @@ -22,6 +28,10 @@ class ClientConfig: class ShareType(IntEnum): + """ + Enumerates the types of shares that can be created. + """ + USER = 0 GROUP = 1 PUBLIC_LINK = 3 @@ -32,6 +42,11 @@ class ShareType(IntEnum): class Permission(IntEnum): + """ + Enumerates the permissions that can be granted to a user/group on a + resource. + """ + READ = 1 UPDATE = 2 CREATE = 4 @@ -40,26 +55,35 @@ class Permission(IntEnum): ALL = 31 -class NextcloudPlugin(Plugin): +_LAST_ACTIVITY_VARNAME = '_NEXTCLOUD_LAST_ACTIVITY_ID' + + +class NextcloudPlugin(RunnablePlugin): """ Plugin to interact with a NextCloud instance. """ def __init__( self, - url: Optional[str] = None, - username: Optional[str] = None, - password: Optional[str] = None, - **kwargs + url: str, + username: str, + password: str, + poll_interval: Optional[float] = 30.0, + **kwargs, ): """ :param url: URL to the index of your default NextCloud instance. :param username: Default NextCloud username. - :param password: Default NextCloud password. + :param password: Default NextCloud password. If the user has 2FA + enabled then you can generate an app password from the personal + settings page of your NextCloud instance. + :param poll_interval: How often the plugin should poll for new activity + events (default: 30 seconds). """ - super().__init__(**kwargs) + super().__init__(poll_interval=poll_interval, **kwargs) self.conf = ClientConfig(url=url, username=username, password=password) - self._client = self._get_client(**self.conf.to_dict()) + self._client = self._get_client(**self.conf.to_dict(), raise_on_empty=False) + self._last_seen_id = None def _get_client( self, @@ -84,16 +108,39 @@ class NextcloudPlugin(Plugin): return NextCloud(endpoint=url, user=username, password=password) + @property + def _last_seen_var(self) -> Variable: + return Variable(_LAST_ACTIVITY_VARNAME) + + @property + def last_seen_id(self) -> int: + if self._last_seen_id is None: + self._last_seen_id = self._last_seen_var.get() or 0 + + return int(self._last_seen_id) + + @last_seen_id.setter + def last_seen_id(self, value: int = 0): + self._last_seen_var.set(value) + self._last_seen_id = value + + @staticmethod + def _activity_to_event(activity: dict) -> NextCloudActivityEvent: + activity_type = activity.pop('type') + dt = datetime.fromisoformat(activity.pop('datetime')) + return NextCloudActivityEvent( + activity_type=activity_type, datetime=dt, **activity + ) + @staticmethod def _get_permissions(permissions: Optional[List[str]]) -> int: int_perm = 0 for perm in permissions or []: perm = perm.upper() - assert hasattr( - Permission, perm - ), 'Unknown permissions type: {}. Supported permissions: {}'.format( - perm, [p.name.lower() for p in Permission] + assert hasattr(Permission, perm), ( + f'Unknown permissions type: {perm}. Supported permissions: ' + f'{[p.name.lower() for p in Permission]}' ) if perm == 'ALL': @@ -107,55 +154,46 @@ class NextcloudPlugin(Plugin): @staticmethod def _get_share_type(share_type: str) -> int: share_type = share_type.upper() - assert hasattr( - ShareType, share_type - ), 'Unknown share type: {}. Supported share types: {}'.format( - share_type, [s.name.lower() for s in ShareType] + assert hasattr(ShareType, share_type), ( + f'Unknown share type: {share_type}. Supported share types: ' + f'{[s.name.lower() for s in ShareType]}' ) return getattr(ShareType, share_type).value def _execute(self, server_args: dict, method: str, *args, **kwargs): client = self._get_client(**server_args) - assert hasattr(client, method), 'No such NextCloud method: {}'.format(method) + func = getattr(client, method, None) + assert func, f'No such NextCloud method: {method}' - response = getattr(client, method)(*args, **kwargs) - if response is None: - return - - assert response.is_ok, 'Error on {method}({args}{sep}{kwargs}): {error}'.format( - method=method, - args=', '.join(args), - sep=', ' if args and kwargs else '', - kwargs=', '.join(['{}={}'.format(k, v) for k, v in kwargs.items()]), - error=response.meta.get('message', '[No message]') - if hasattr(response, 'meta') - else response.raw.reason, + response = func(*args, **kwargs) + assert response is not None, 'No response from NextCloud server' + assert response.is_ok, ( + 'Error on ' + + method + + '(' + + ', '.join(args) + + (', ' if args and kwargs else '') + + ', '.join([f'{k}={v}' for k, v in kwargs.items()]) + + '): status=' + + str(response.status_code) + + ', message=' + + getattr(response, 'meta', {}).get('message', '[No message]') + + ', data=' + + str(response.json_data) ) return response.json_data - @action - def get_activities( + def _get_activities( self, - since: Optional[id] = None, - limit: Optional[int] = None, + since: Optional[str] = None, + limit: Optional[int] = 25, object_type: Optional[str] = None, object_id: Optional[int] = None, sort: str = 'desc', - **server_args - ) -> List[str]: - """ - Get the list of recent activities on an instance. - - :param since: Only return the activities that have occurred since the specified ID. - :param limit: Maximum number of activities to be returned (default: ``None``). - :param object_type: Filter by object type. - :param object_id: Only get the activities related to a specific ``object_id``. - :param sort: Sort mode, ``asc`` for ascending, ``desc`` for descending (default: ``desc``). - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). - :return: The list of selected activities. - """ + **server_args, + ) -> List[dict]: return self._execute( server_args, 'get_activities', @@ -166,12 +204,47 @@ class NextcloudPlugin(Plugin): sort=sort, ) + @action + def get_activities( + self, + since: Optional[str] = None, + limit: Optional[int] = 25, + object_type: Optional[str] = None, + object_id: Optional[int] = None, + sort: str = 'desc', + **server_args, + ) -> List[dict]: + """ + Get the list of recent activities on an instance. + + :param since: Only return the activities that have occurred since the + specified ID. + :param limit: Maximum number of activities to be returned (default: 25). + :param object_type: Filter by object type. + :param object_id: Only get the activities related to a specific + ``object_id``. + :param sort: Sort mode, ``asc`` for ascending, ``desc`` for descending + (default: ``desc``). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). + :return: The list of selected activities. + """ + return self._get_activities( + since=since, + limit=limit, + object_type=object_type, + object_id=object_id, + sort=sort, + **server_args, + ) + @action def get_apps(self, **server_args) -> List[str]: """ Get the list of apps installed on a NextCloud instance. - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). :return: The list of installed apps as strings. """ return self._execute(server_args, 'get_apps').get('apps', []) @@ -182,7 +255,8 @@ class NextcloudPlugin(Plugin): Enable an app. :param app_id: App ID. - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). """ self._execute(server_args, 'enable_app', app_id) @@ -192,7 +266,8 @@ class NextcloudPlugin(Plugin): Disable an app. :param app_id: App ID. - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). """ self._execute(server_args, 'disable_app', app_id) @@ -202,7 +277,8 @@ class NextcloudPlugin(Plugin): Provides information about an application. :param app_id: App ID. - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). """ return self._execute(server_args, 'get_app', app_id) @@ -211,37 +287,124 @@ class NextcloudPlugin(Plugin): """ Returns the capabilities of the server. - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). + :return: Server capabilities. Example: + + .. code-block:: json + + { + "version": { + "major": 27, + "minor": 1, + "micro": 5, + "string": "27.1.5", + "edition": "", + "extendedSupport": false + }, + "capabilities": { + "core": { + "pollinterval": 60, + "webdav-root": "remote.php/webdav", + }, + "files": { + "bigfilechunking": true, + "blacklisted_files": [ + ".htaccess" + ], + "comments": true, + "undelete": true, + "versioning": true, + "version_labeling": true, + "version_deletion": true + }, + "activity": { + "apiv2": [ + "filters", + "filters-api", + "previews", + "rich-strings" + ] + }, + "notifications": { + "ocs-endpoints": [ + "list", + "get", + "delete", + "delete-all", + "icons", + "rich-strings", + "action-web", + "user-status", + "exists" + ], + "push": [ + "devices", + "object-data", + "delete" + ], + "admin-notifications": [ + "ocs", + "cli" + ] + }, + "theming": { + "name": "Nextcloud", + "url": "https://nextcloud.com", + "slogan": "a safe home for all your data", + "color": "#1F5C98", + "color-text": "#ffffff", + "color-element": "#1F5C98", + "color-element-bright": "#1F5C98", + "color-element-dark": "#1F5C98", + "logo": "https://cloud.example.com/core/img/logo/logo.svg?v=0", + "background": "#1F5C98", + "background-plain": true, + "background-default": true, + "logoheader": "https://cloud.example.com/core/img/logo/logo.svg?v=0", + "favicon": "https://cloud.example.com/core/img/logo/logo.svg?v=0" + }, + "user_status": { + "enabled": true, + "restore": true, + "supports_emoji": true + }, + } + } + """ return self._execute(server_args, 'get_capabilities') @action - def add_group(self, group_id: Union[str], **server_args): + def add_group(self, group_id: Union[str, int], **server_args): """ Create a new group. :param group_id: New group unique ID. - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). """ self._execute(server_args, 'add_group', group_id) @action - def delete_group(self, group_id: Union[str], **server_args): + def delete_group(self, group_id: Union[str, int], **server_args): """ Delete a group. :param group_id: Group ID. - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). """ self._execute(server_args, 'delete_group', group_id) @action - def get_group(self, group_id: Union[str], **server_args) -> dict: + def get_group(self, group_id: Union[str, int], **server_args) -> dict: """ Get the information of a group. :param group_id: Group ID. - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). """ return self._execute(server_args, 'get_group', group_id) @@ -251,7 +414,7 @@ class NextcloudPlugin(Plugin): search: Optional[str] = None, limit: Optional[int] = None, offset: Optional[int] = None, - **server_args + **server_args, ) -> List[str]: """ Search for groups. @@ -259,7 +422,8 @@ class NextcloudPlugin(Plugin): :param search: Search for groups matching the specified substring. :param limit: Maximum number of returned entries. :param offset: Start offset. - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). """ return self._execute( server_args, 'get_groups', search=search, limit=limit, offset=offset @@ -271,7 +435,8 @@ class NextcloudPlugin(Plugin): Create a new group folder. :param name: Name/path of the folder. - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). """ self._execute(server_args, 'create_group_folder', name) @@ -281,7 +446,8 @@ class NextcloudPlugin(Plugin): Delete a new group folder. :param folder_id: Folder ID. - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). """ self._execute(server_args, 'delete_group_folder', folder_id) @@ -291,16 +457,18 @@ class NextcloudPlugin(Plugin): Get a new group folder. :param folder_id: Folder ID. - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). """ return self._execute(server_args, 'get_group_folder', folder_id) @action - def get_group_folders(self, **server_args) -> list: + def get_group_folders(self, **server_args) -> List: """ Get the list new group folder. - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). """ return self._execute(server_args, 'get_group_folders') @@ -313,7 +481,8 @@ class NextcloudPlugin(Plugin): :param folder_id: Folder ID. :param new_name: New folder name. - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). """ self._execute(server_args, 'rename_group_folder', folder_id, new_name) @@ -326,7 +495,8 @@ class NextcloudPlugin(Plugin): :param folder_id: Folder ID. :param group_id: Group ID. - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). """ self._execute(server_args, 'grant_access_to_group_folder', folder_id, group_id) @@ -339,7 +509,8 @@ class NextcloudPlugin(Plugin): :param folder_id: Folder ID. :param group_id: Group ID. - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). """ self._execute(server_args, 'revoke_access_to_group_folder', folder_id, group_id) @@ -352,7 +523,8 @@ class NextcloudPlugin(Plugin): :param folder_id: Folder ID. :param quota: Quota in bytes - set None for unlimited. - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). """ self._execute( server_args, @@ -367,23 +539,25 @@ class NextcloudPlugin(Plugin): folder_id: Union[int, str], group_id: str, permissions: List[str], - **server_args + **server_args, ): """ Set the permissions on a folder for a group. :param folder_id: Folder ID. :param group_id: Group ID. - :param permissions: New permissions, as a list including any of the following: + :param permissions: New permissions, as a list including any of the + following: - - ``read`` - - ``update`` - - ``create`` - - ``delete`` - - ``share`` - - ``all`` + - ``read`` + - ``update`` + - ``create`` + - ``delete`` + - ``share`` + - ``all`` - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). """ self._execute( server_args, @@ -394,11 +568,43 @@ class NextcloudPlugin(Plugin): ) @action - def get_notifications(self, **server_args) -> list: + def get_notifications(self, **server_args) -> List[dict]: """ Get the list of notifications for the logged user. - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). + :return: A list of notifications. Example: + + .. code-block:: json + + [ + { + "notification_id": 1234, + "app": "updatenotification", + "user": "username", + "datetime": "2024-01-01T19:00:00+00:00", + "object_type": "side_menu", + "object_id": "1.2.3", + "subject": "Update for MyApp to version 1.2.3 is available.", + "message": "", + "link": "https://cloud.example.com/index.php/settings/apps/updates#myapp", + "subjectRich": "Update for {app} to version 1.2.3 is available.", + "subjectRichParameters": { + "app": { + "type": "app", + "id": "myapp", + "name": "MyApp" + } + }, + "messageRich": "", + "messageRichParameters": [], + "icon": "https://cloud.example.com/apps/updatenotification/img/notification.svg", + "shouldNotify": true, + "actions": [] + } + ] + """ return self._execute(server_args, 'get_notifications') @@ -407,17 +613,48 @@ class NextcloudPlugin(Plugin): """ Delete all notifications for the logged user. - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). """ self._execute(server_args, 'delete_all_notifications') @action - def get_notification(self, notification_id: int, **server_args) -> Union[dict, str]: + def get_notification(self, notification_id: int, **server_args) -> dict: """ Get the content of a notification. :param notification_id: Notification ID. - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). + :return: Notification details. Example: + + .. code-block:: json + + { + "notification_id": 1234, + "app": "updatenotification", + "user": "username", + "datetime": "2024-01-01T19:00:00+00:00", + "object_type": "side_menu", + "object_id": "1.2.3", + "subject": "Update for MyApp to version 1.2.3 is available.", + "message": "", + "link": "https://cloud.example.com/index.php/settings/apps/updates#myapp", + "subjectRich": "Update for {app} to version 1.2.3 is available.", + "subjectRichParameters": { + "app": { + "type": "app", + "id": "myapp", + "name": "MyApp" + } + }, + "messageRich": "", + "messageRichParameters": [], + "icon": "https://cloud.example.com/apps/updatenotification/img/notification.svg", + "shouldNotify": true, + "actions": [] + } + """ return self._execute(server_args, 'get_notification', notification_id) @@ -427,7 +664,8 @@ class NextcloudPlugin(Plugin): Delete a notification. :param notification_id: Notification ID. - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). """ self._execute(server_args, 'delete_notification', notification_id) @@ -440,7 +678,7 @@ class NextcloudPlugin(Plugin): public_upload: bool = False, password: Optional[str] = None, permissions: Optional[List[str]] = None, - **server_args + **server_args, ) -> dict: """ Share a file/folder with a user/group or a public link. @@ -456,19 +694,23 @@ class NextcloudPlugin(Plugin): - ``circle`` - ``talk_conversation`` - :param share_with: User/group ID, email or conversation ID the resource should be shared with. - :param public_upload: Whether public upload to the shared folder is allowed (default: False). + :param share_with: User/group ID, email or conversation ID the resource + should be shared with. + :param public_upload: Whether public upload to the shared folder is + allowed (default: False). :param password: Optional password to protect the share. - :param permissions: Share permissions, as a list including any of the following (default: ``read``): + :param permissions: Share permissions, as a list including any of the + following (default: ``read``): - - ``read`` - - ``update`` - - ``create`` - - ``delete`` - - ``share`` - - ``all`` + - ``read`` + - ``update`` + - ``create`` + - ``delete`` + - ``share`` + - ``all`` - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). :return: The details of the newly created share. Example: .. code-block:: json @@ -508,17 +750,17 @@ class NextcloudPlugin(Plugin): } """ - share_type = self._get_share_type(share_type) - permissions = self._get_permissions(permissions or ['read']) + share_type_id = self._get_share_type(share_type) + permissions_id = self._get_permissions(permissions or ['read']) return self._execute( server_args, 'create_share', path, - share_type=share_type, + share_type=share_type_id, share_with=share_with, public_upload=public_upload, password=password, - permissions=permissions, + permissions=permissions_id, ) @action @@ -526,7 +768,8 @@ class NextcloudPlugin(Plugin): """ Get the list of shares available on the server. - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). :return: List of available shares. Example: .. code-block:: json @@ -576,7 +819,8 @@ class NextcloudPlugin(Plugin): Remove the shared state of a resource. :param share_id: Share ID. - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). """ self._execute(server_args, 'delete_share', str(share_id)) @@ -586,7 +830,46 @@ class NextcloudPlugin(Plugin): Get the information of a shared resource. :param share_id: Share ID. - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). + :return: The details of the share. Example: + + .. code-block:: json + + { + "id": "4", + "share_type": 3, + "uid_owner": "your_uid", + "displayname_owner": "Your Name", + "permissions": 17, + "can_edit": true, + "can_delete": true, + "stime": 1599691325, + "parent": null, + "expiration": null, + "token": "AbCdEfG0123456789", + "uid_file_owner": "your_uid", + "note": "", + "label": "", + "displayname_file_owner": "Your Name", + "path": "/Shared Path", + "item_type": "folder", + "mimetype": "httpd/unix-directory", + "storage_id": "home::your-uid", + "storage": 2, + "item_source": 13960, + "file_source": 13960, + "file_parent": 6, + "file_target": "/Shared Path", + "share_with": null, + "share_with_displayname": "(Shared link)", + "password": null, + "send_password_by_talk": false, + "url": "https://your-domain/nextcloud/index.php/s/AbCdEfG0123456789", + "mail_send": 1, + "hide_download": 0 + } + """ return self._execute(server_args, 'get_share_info', str(share_id)) @@ -598,36 +881,37 @@ class NextcloudPlugin(Plugin): password: Optional[str] = None, permissions: Optional[List[str]] = None, expire_date: Optional[str] = None, - **server_args + **server_args, ): """ Update the permissions of a shared resource. :param share_id: Share ID. - :param public_upload: Whether public upload to the shared folder is allowed (default: False). + :param public_upload: Whether public upload to the shared folder is + allowed (default: False). :param password: Optional password to protect the share. - :param permissions: Share permissions, as a list including any of the following (default: ``read``): + :param permissions: Share permissions, as a list including any of the + following (default: ``read``): - - ``read`` - - ``update`` - - ``create`` - - ``delete`` - - ``share`` - - ``all`` + - ``read`` + - ``update`` + - ``create`` + - ``delete`` + - ``share`` + - ``all`` :param expire_date: Share expiration date, in the format ``YYYY-MM-DD``. - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). """ - if permissions: - permissions = self._get_permissions(permissions) - + perms = self._get_permissions(permissions) if permissions else None self._execute( server_args, 'update_share', share_id, public_upload=public_upload, password=password, - permissions=permissions, + permissions=perms, expire_date=expire_date, ) @@ -638,7 +922,8 @@ class NextcloudPlugin(Plugin): :param user_id: User ID/name. :param password: User password - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). """ self._execute(server_args, 'add_user', user_id, password) @@ -649,7 +934,8 @@ class NextcloudPlugin(Plugin): :param user_id: User ID/name. :param properties: Key-value pair of user attributes to be edited. - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). """ for k, v in properties.items(): self._execute(server_args, 'edit_user', user_id, k, v) @@ -660,7 +946,8 @@ class NextcloudPlugin(Plugin): Get the details of a user. :param user_id: User ID/name. - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). :return: User details. Example: .. code-block:: json @@ -705,15 +992,52 @@ class NextcloudPlugin(Plugin): search: Optional[str] = None, limit: Optional[int] = None, offset: Optional[int] = None, - **server_args + **server_args, ) -> List[str]: """ Get the list of users matching some search criteria. :param search: Return users matching the provided string. - :param limit: Maximum number of results to be returned (default: no limit). + :param limit: Maximum number of results to be returned (default: no + limit). :param offset: Search results offset (default: None). - :return: List of the matched user IDs. + :return: List of the matched user IDs. Example: + + .. code-block:: json + + [ + { + "enabled": true, + "storageLocation": "/mnt/hd/nextcloud/user", + "id": "user", + "lastLogin": 1599693750000, + "backend": "Database", + "subadmin": [], + "quota": { + "free": 6869434515456, + "used": 1836924441, + "total": 6871271439897, + "relative": 0.03, + "quota": -3 + }, + "email": "info@yourdomain.com", + "displayname": "Your Name", + "phone": "+1234567890", + "address": "", + "website": "https://yourdomain.com", + "twitter": "@You", + "groups": [ + "admin" + ], + "language": "en", + "locale": "", + "backendCapabilities": { + "setDisplayName": true, + "setPassword": true + } + } + ] + """ return self._execute( server_args, 'get_users', search=search, limit=limit, offset=offset @@ -725,7 +1049,8 @@ class NextcloudPlugin(Plugin): Delete a user. :param user_id: User ID/name. - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). """ self._execute(server_args, 'delete_user', user_id) @@ -735,7 +1060,8 @@ class NextcloudPlugin(Plugin): Enable a user. :param user_id: User ID/name. - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). """ self._execute(server_args, 'enable_user', user_id) @@ -745,7 +1071,8 @@ class NextcloudPlugin(Plugin): Disable a user. :param user_id: User ID/name. - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). """ self._execute(server_args, 'disable_user', user_id) @@ -756,7 +1083,8 @@ class NextcloudPlugin(Plugin): :param user_id: User ID/name. :param group_id: Group ID. - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). """ self._execute(server_args, 'add_to_group', user_id, group_id) @@ -767,7 +1095,8 @@ class NextcloudPlugin(Plugin): :param user_id: User ID/name. :param group_id: Group ID. - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). """ self._execute(server_args, 'remove_from_group', user_id, group_id) @@ -778,7 +1107,8 @@ class NextcloudPlugin(Plugin): :param user_id: User ID/name. :param group_id: Group ID. - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). """ self._execute(server_args, 'create_subadmin', user_id, group_id) @@ -789,7 +1119,8 @@ class NextcloudPlugin(Plugin): :param user_id: User ID/name. :param group_id: Group ID. - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). """ self._execute(server_args, 'remove_subadmin', user_id, group_id) @@ -799,34 +1130,44 @@ class NextcloudPlugin(Plugin): Get the groups where a given user is subadmin. :param user_id: User ID/name. - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). :return: List of matched groups as strings. """ return self._execute(server_args, 'get_subadmin_groups', user_id) @action - def create_folder(self, path: str, user_id: Optional[str] = None, **server_args): + def create_folder(self, path: str, **server_args): """ Create a folder. :param path: Path to the folder. - :param user_id: User ID associated to the folder (default: same as the configured user). - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). """ - user_id = user_id or server_args.get('username', self.conf.username) - self._execute(server_args, 'create_folder', user_id, path) + self._execute(server_args, 'create_folder', path) @action - def delete_path(self, path: str, user_id: Optional[str] = None, **server_args): + def mkdir(self, path: str, **server_args): + """ + Alias for :meth:`.create_folder`. + + :param path: Path to the folder. + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). + """ + self.create_folder(path, **server_args) + + @action + def delete(self, path: str, **server_args): """ Delete a file or folder. :param path: Path to the resource. - :param user_id: User ID associated to the resource (default: same as the configured user). - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). """ - user_id = user_id or server_args.get('username', self.conf.username) - self._execute(server_args, 'delete_path', user_id, path) + self._execute(server_args, 'delete_path', path) @action def upload_file( @@ -834,23 +1175,22 @@ class NextcloudPlugin(Plugin): remote_path: str, local_path: Optional[str] = None, content: Optional[str] = None, - user_id: Optional[str] = None, timestamp: Optional[Union[datetime, int, str]] = None, - **server_args + **server_args, ): """ Upload a file. :param remote_path: Path to the remote resource. - :param local_path: If set, identifies the path to the local file to be uploaded. - :param content: If set, create a new file with this content instead of uploading an existing file. - :param user_id: User ID associated to the resource (default: same as the configured user). - :param timestamp: File timestamp. If not specified it will be retrieved from the file info or set to ``now`` - if ``content`` is specified. - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param local_path: If set, identifies the path to the local file to be + uploaded. + :param content: If set, create a new file with this content instead of + uploading an existing file. + :param timestamp: File timestamp. If not specified it will be retrieved + from the file info or set to ``now`` if ``content`` is specified. + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). """ - user_id = user_id or server_args.get('username', self.conf.username) - if isinstance(timestamp, str): timestamp = datetime.fromisoformat(timestamp) if isinstance(timestamp, datetime): @@ -859,6 +1199,7 @@ class NextcloudPlugin(Plugin): assert (local_path or content) and not ( local_path and content ), 'Please specify either local_path or content' + if local_path: method = 'upload_file' local_path = os.path.abspath(os.path.expanduser(local_path)) @@ -868,7 +1209,6 @@ class NextcloudPlugin(Plugin): return self._execute( server_args, method, - user_id, local_path or content, remote_path, timestamp=timestamp, @@ -879,24 +1219,22 @@ class NextcloudPlugin(Plugin): self, remote_path: str, local_path: str, - user_id: Optional[str] = None, - **server_args + **server_args, ): """ Download a file. :param remote_path: Path to the remote resource. :param local_path: Path to the local folder. - :param user_id: User ID associated to the resource (default: same as the configured user). - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). """ - user_id = user_id or server_args.get('username', self.conf.username) local_path = os.path.abspath(os.path.expanduser(local_path)) cur_dir = os.getcwd() try: os.chdir(local_path) - return self._execute(server_args, 'download_file', user_id, remote_path) + return self._execute(server_args, 'download_file', remote_path) finally: os.chdir(cur_dir) @@ -904,103 +1242,120 @@ class NextcloudPlugin(Plugin): def list( self, path: str, - user_id: Optional[str] = None, depth: int = 1, all_properties: bool = False, - **server_args + **server_args, ) -> List[dict]: """ List the content of a folder on the NextCloud instance. :param path: Remote path. - :param user_id: User ID associated to the resource (default: same as the configured user). :param depth: Search depth (default: 1). - :param all_properties: Return all the file properties available (default: ``False``). - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param all_properties: Return all the file properties available + (default: ``False``). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). """ - user_id = user_id or server_args.get('username', self.conf.username) return self._execute( server_args, 'list_folders', - user_id, path, depth=depth, all_properties=all_properties, ) @action - def list_favorites( - self, path: Optional[str] = None, user_id: Optional[str] = None, **server_args - ) -> List[dict]: + def list_favorites(self, path: Optional[str] = None, **server_args) -> List[dict]: """ List the favorite items for a user. :param path: Return only the favorites under this path. - :param user_id: User ID associated to the resource (default: same as the configured user). - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). """ - user_id = user_id or server_args.get('username', self.conf.username) - return self._execute(server_args, 'list_folders', user_id, path) + return self._execute(server_args, 'list_folders', path) @action - def mark_favorite( - self, path: Optional[str] = None, user_id: Optional[str] = None, **server_args - ): + def mark_favorite(self, path: Optional[str] = None, **server_args): """ Add a path to a user's favorites. :param path: Resource path. - :param user_id: User ID associated to the resource (default: same as the configured user). - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). """ - user_id = user_id or server_args.get('username', self.conf.username) - self._execute(server_args, 'set_favorites', user_id, path) + self._execute(server_args, 'set_favorites', path) @action def copy( self, path: str, destination: str, - user_id: Optional[str] = None, overwrite: bool = False, - **server_args + **server_args, ): """ Copy a resource to another path. :param path: Resource path. :param destination: Destination path. - :param user_id: User ID associated to the resource (default: same as the configured user). - :param overwrite: Set to ``True`` if you want to overwrite any existing file (default: ``False``). - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param overwrite: Set to ``True`` if you want to overwrite any existing + file (default: ``False``). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). """ - user_id = user_id or server_args.get('username', self.conf.username) - self._execute( - server_args, 'copy_path', user_id, path, destination, overwrite=overwrite - ) + self._execute(server_args, 'copy_path', path, destination, overwrite=overwrite) @action def move( self, path: str, destination: str, - user_id: Optional[str] = None, overwrite: bool = False, - **server_args + **server_args, ): """ Move a resource to another path. :param path: Resource path. :param destination: Destination path. - :param user_id: User ID associated to the resource (default: same as the configured user). - :param overwrite: Set to ``True`` if you want to overwrite any existing file (default: ``False``). - :param server_args: Override the default server settings (see :meth:`._get_client` arguments). + :param overwrite: Set to ``True`` if you want to overwrite any existing + file (default: ``False``). + :param server_args: Override the default server settings (see + :meth:`._get_client` arguments). """ - user_id = user_id or server_args.get('username', self.conf.username) - self._execute( - server_args, 'move_path', user_id, path, destination, overwrite=overwrite - ) + self._execute(server_args, 'move_path', path, destination, overwrite=overwrite) + + def main(self): + while not self.should_stop(): + last_seen_id = self.last_seen_id + new_last_seen_id = last_seen_id + activities = self._get_activities( + sort='desc', + url=self.conf.url, + username=self.conf.username, + password=self.conf.password, + limit=100, + ) + + events = [] + for activity in activities: + activity_id = int(activity['activity_id']) + if last_seen_id and activity_id <= last_seen_id: + break + + events.append(self._activity_to_event(activity)) + + if not new_last_seen_id or activity_id > new_last_seen_id: + new_last_seen_id = activity_id + + for evt in events[::-1]: + self._bus.post(evt) + + if new_last_seen_id and last_seen_id != new_last_seen_id: + self.last_seen_id = new_last_seen_id + + self.wait_stop(self.poll_interval) # vim:sw=4:ts=4:et: diff --git a/platypush/plugins/nextcloud/manifest.yaml b/platypush/plugins/nextcloud/manifest.yaml index 47a10c6be5..95fa9586c2 100644 --- a/platypush/plugins/nextcloud/manifest.yaml +++ b/platypush/plugins/nextcloud/manifest.yaml @@ -1,5 +1,8 @@ manifest: - events: {} + events: + platypush.message.event.nextcloud.NextCloudActivityEvent: | + When new activity occurs on the instance - e.g. a file or bookmark is + created, a comment is added, a message is received etc. install: pip: - nextcloud-api-wrapper