diff libervia/backend/memory/memory.py @ 4130:02f0adc745c6

core: notifications implementation, first draft: add a new table for notifications, and methods/bridge methods to manipulate them.
author Goffi <goffi@goffi.org>
date Mon, 16 Oct 2023 17:29:31 +0200
parents 4b842c1fb686
children 54b8cf8c8daf
line wrap: on
line diff
--- a/libervia/backend/memory/memory.py	Wed Oct 18 15:30:07 2023 +0200
+++ b/libervia/backend/memory/memory.py	Mon Oct 16 17:29:31 2023 +0200
@@ -16,29 +16,39 @@
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-import os.path
+from collections import namedtuple
 import copy
-import shortuuid
-import mimetypes
-import time
+from dataclasses import dataclass
 from functools import partial
-from typing import Optional, Tuple, Dict
+import mimetypes
+import os.path
 from pathlib import Path
+import time
+from typing import Dict, Optional, Tuple
 from uuid import uuid4
-from collections import namedtuple
+
+import shortuuid
+from twisted.internet import defer, error, reactor
 from twisted.python import failure
-from twisted.internet import defer, reactor, error
 from twisted.words.protocols.jabber import jid
+
+from libervia.backend.core import exceptions
+from libervia.backend.core.constants import Const as C
+from libervia.backend.core.core_types import SatXMPPEntity
 from libervia.backend.core.i18n import _
 from libervia.backend.core.log import getLogger
-from libervia.backend.core import exceptions
-from libervia.backend.core.constants import Const as C
-from libervia.backend.memory.sqla import Storage
-from libervia.backend.memory.persistent import PersistentDict
-from libervia.backend.memory.params import Params
-from libervia.backend.memory.disco import Discovery
 from libervia.backend.memory.crypto import BlockCipher
 from libervia.backend.memory.crypto import PasswordHasher
+from libervia.backend.memory.disco import Discovery
+from libervia.backend.memory.params import Params
+from libervia.backend.memory.persistent import PersistentDict
+from libervia.backend.memory.sqla import (
+    Notification,
+    NotificationPriority,
+    NotificationStatus,
+    NotificationType,
+    Storage,
+)
 from libervia.backend.tools import config as tools_config
 from libervia.backend.tools.common import data_format
 from libervia.backend.tools.common import regex
@@ -1848,6 +1858,180 @@
             *(regex.path_escape(a) for a in args)
         )
 
+    ## Notifications ##
+
+
+    def _add_notification(
+        self,
+        type_: str,
+        body_plain: str,
+        body_rich: str,
+        title: str,
+        is_global: bool,
+        requires_action: bool,
+        priority: str,
+        expire_at: float,
+        extra_s: str,
+        profile_key: str
+    ) -> defer.Deferred:
+        client = self.host.get_client(profile_key)
+
+        if not client.is_admin:
+            raise exceptions.PermissionError("Only admins can add a notification")
+
+        try:
+            notification_type = NotificationType[type_]
+            notification_priority = NotificationPriority[priority]
+        except KeyError as e:
+            raise exceptions.DataError(
+                f"invalid notification type or priority data: {e}"
+            )
+
+        return defer.ensureDeferred(
+            self.add_notification(
+                client,
+                notification_type,
+                body_plain,
+                body_rich or None,
+                title or None,
+                is_global,
+                requires_action,
+                notification_priority,
+                expire_at or None,
+                data_format.deserialise(extra_s)
+            )
+        )
+
+    async def add_notification(
+        self,
+        client: SatXMPPEntity,
+        type_: NotificationType,
+        body_plain: str,
+        body_rich: Optional[str] = None,
+        title: Optional[str] = None,
+        is_global: bool = False,
+        requires_action: bool = False,
+        priority: NotificationPriority = NotificationPriority.MEDIUM,
+        expire_at: Optional[float] = None,
+        extra: Optional[dict] = None,
+    ) -> None:
+        """Create and broadcast a new notification.
+
+        @param client: client associated with the notification. If None, the notification
+            will be global (i.e. for all profiles).
+        @param type_: type of the notification.
+        @param body_plain: plain text body.
+        @param body_rich: rich text (XHTML) body.
+        @param title: optional title.
+        @param is_global: True if the notification is for all profiles.
+        @param requires_action: True if the notification requires user action (e.g. a
+            dialog need to be answered).
+        @priority: how urgent the notification is
+        @param expire_at: expiration timestamp for the notification.
+        @param extra: additional data.
+        """
+        notification = await self.storage.add_notification(
+            None if is_global else client, type_, body_plain, body_rich, title,
+            requires_action, priority, expire_at, extra
+        )
+        self.host.bridge.notification_new(
+            str(notification.id),
+            notification.timestamp,
+            type_.value,
+            body_plain,
+            body_rich or '',
+            title or '',
+            requires_action,
+            priority.value,
+            expire_at or 0,
+            data_format.serialise(extra) if extra else '',
+            C.PROF_KEY_ALL if is_global else client.profile
+        )
+
+    def _get_notifications(self, filters_s: str, profile_key: str) -> defer.Deferred:
+        """Fetch notifications for bridge with given filters and profile key.
+
+        @param filters_s: serialized filter conditions. Keys can be:
+            :type_ (str):
+                Filter by type of the notification.
+            :status (str):
+                Filter by status of the notification.
+            :requires_action (bool):
+                Filter by notifications that require user action.
+            :min_priority (str):
+                Filter by minimum priority value.
+        @param profile_key: key of the profile to fetch notifications for.
+        @return: Deferred which fires with a list of serialised notifications.
+        """
+        client = self.host.get_client(profile_key)
+
+        filters = data_format.deserialise(filters_s)
+
+        try:
+            if 'type' in filters:
+                filters['type_'] = NotificationType[filters.pop('type')]
+            if 'status' in filters:
+                filters['status'] = NotificationStatus[filters['status']]
+            if 'min_priority' in filters:
+                filters['min_priority'] = NotificationPriority[filters['min_priority']].value
+        except KeyError as e:
+            raise exceptions.DataError(f"invalid filter data: {e}")
+
+        d = defer.ensureDeferred(self.storage.get_notifications(client, **filters))
+        d.addCallback(
+            lambda notifications: data_format.serialise(
+                [notification.serialise() for notification in notifications]
+            )
+        )
+        return d
+
+    def _delete_notification(
+        self,
+        id_: str,
+        is_global: bool,
+        profile_key: str
+    ) -> defer.Deferred:
+        client = self.host.get_client(profile_key)
+        if is_global and not client.is_admin:
+            raise exceptions.PermissionError(
+                "Only admins can delete global notifications"
+            )
+        return defer.ensureDeferred(self.delete_notification(client, id_, is_global))
+
+    async def delete_notification(
+        self,
+        client: SatXMPPEntity,
+        id_: str,
+        is_global: bool=False
+    ) -> None:
+        """Delete a notification
+
+        the notification must be from the requesting profile.
+        @param id_: ID of the notification
+        is_global: if True, a global notification will be removed.
+        """
+        await self.storage.delete_notification(None if is_global else client, id_)
+        self.host.bridge.notification_deleted(
+            id_,
+            C.PROF_KEY_ALL if is_global else client.profile
+        )
+
+    def _notifications_expired_clean(
+        self, limit_timestamp: float, profile_key: str
+    ) -> defer.Deferred:
+        if profile_key == C.PROF_KEY_NONE:
+            client = None
+        else:
+            client = self.host.get_client(profile_key)
+
+        return defer.ensureDeferred(
+            self.storage.clean_expired_notifications(
+                client,
+                None if limit_timestamp == -1.0 else limit_timestamp
+            )
+        )
+
+
     ## Misc ##
 
     def is_entity_available(self, client, entity_jid):