Mercurial > libervia-backend
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):