comparison 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
comparison
equal deleted inserted replaced
4129:51744ad00a42 4130:02f0adc745c6
14 # GNU Affero General Public License for more details. 14 # GNU Affero General Public License for more details.
15 15
16 # You should have received a copy of the GNU Affero General Public License 16 # You should have received a copy of the GNU Affero General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>. 17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18 18
19 from collections import namedtuple
20 import copy
21 from dataclasses import dataclass
22 from functools import partial
23 import mimetypes
19 import os.path 24 import os.path
20 import copy 25 from pathlib import Path
26 import time
27 from typing import Dict, Optional, Tuple
28 from uuid import uuid4
29
21 import shortuuid 30 import shortuuid
22 import mimetypes 31 from twisted.internet import defer, error, reactor
23 import time
24 from functools import partial
25 from typing import Optional, Tuple, Dict
26 from pathlib import Path
27 from uuid import uuid4
28 from collections import namedtuple
29 from twisted.python import failure 32 from twisted.python import failure
30 from twisted.internet import defer, reactor, error
31 from twisted.words.protocols.jabber import jid 33 from twisted.words.protocols.jabber import jid
34
35 from libervia.backend.core import exceptions
36 from libervia.backend.core.constants import Const as C
37 from libervia.backend.core.core_types import SatXMPPEntity
32 from libervia.backend.core.i18n import _ 38 from libervia.backend.core.i18n import _
33 from libervia.backend.core.log import getLogger 39 from libervia.backend.core.log import getLogger
34 from libervia.backend.core import exceptions
35 from libervia.backend.core.constants import Const as C
36 from libervia.backend.memory.sqla import Storage
37 from libervia.backend.memory.persistent import PersistentDict
38 from libervia.backend.memory.params import Params
39 from libervia.backend.memory.disco import Discovery
40 from libervia.backend.memory.crypto import BlockCipher 40 from libervia.backend.memory.crypto import BlockCipher
41 from libervia.backend.memory.crypto import PasswordHasher 41 from libervia.backend.memory.crypto import PasswordHasher
42 from libervia.backend.memory.disco import Discovery
43 from libervia.backend.memory.params import Params
44 from libervia.backend.memory.persistent import PersistentDict
45 from libervia.backend.memory.sqla import (
46 Notification,
47 NotificationPriority,
48 NotificationStatus,
49 NotificationType,
50 Storage,
51 )
42 from libervia.backend.tools import config as tools_config 52 from libervia.backend.tools import config as tools_config
43 from libervia.backend.tools.common import data_format 53 from libervia.backend.tools.common import data_format
44 from libervia.backend.tools.common import regex 54 from libervia.backend.tools.common import regex
45 55
46 56
1846 self._cache_path, 1856 self._cache_path,
1847 regex.path_escape(namespace), 1857 regex.path_escape(namespace),
1848 *(regex.path_escape(a) for a in args) 1858 *(regex.path_escape(a) for a in args)
1849 ) 1859 )
1850 1860
1861 ## Notifications ##
1862
1863
1864 def _add_notification(
1865 self,
1866 type_: str,
1867 body_plain: str,
1868 body_rich: str,
1869 title: str,
1870 is_global: bool,
1871 requires_action: bool,
1872 priority: str,
1873 expire_at: float,
1874 extra_s: str,
1875 profile_key: str
1876 ) -> defer.Deferred:
1877 client = self.host.get_client(profile_key)
1878
1879 if not client.is_admin:
1880 raise exceptions.PermissionError("Only admins can add a notification")
1881
1882 try:
1883 notification_type = NotificationType[type_]
1884 notification_priority = NotificationPriority[priority]
1885 except KeyError as e:
1886 raise exceptions.DataError(
1887 f"invalid notification type or priority data: {e}"
1888 )
1889
1890 return defer.ensureDeferred(
1891 self.add_notification(
1892 client,
1893 notification_type,
1894 body_plain,
1895 body_rich or None,
1896 title or None,
1897 is_global,
1898 requires_action,
1899 notification_priority,
1900 expire_at or None,
1901 data_format.deserialise(extra_s)
1902 )
1903 )
1904
1905 async def add_notification(
1906 self,
1907 client: SatXMPPEntity,
1908 type_: NotificationType,
1909 body_plain: str,
1910 body_rich: Optional[str] = None,
1911 title: Optional[str] = None,
1912 is_global: bool = False,
1913 requires_action: bool = False,
1914 priority: NotificationPriority = NotificationPriority.MEDIUM,
1915 expire_at: Optional[float] = None,
1916 extra: Optional[dict] = None,
1917 ) -> None:
1918 """Create and broadcast a new notification.
1919
1920 @param client: client associated with the notification. If None, the notification
1921 will be global (i.e. for all profiles).
1922 @param type_: type of the notification.
1923 @param body_plain: plain text body.
1924 @param body_rich: rich text (XHTML) body.
1925 @param title: optional title.
1926 @param is_global: True if the notification is for all profiles.
1927 @param requires_action: True if the notification requires user action (e.g. a
1928 dialog need to be answered).
1929 @priority: how urgent the notification is
1930 @param expire_at: expiration timestamp for the notification.
1931 @param extra: additional data.
1932 """
1933 notification = await self.storage.add_notification(
1934 None if is_global else client, type_, body_plain, body_rich, title,
1935 requires_action, priority, expire_at, extra
1936 )
1937 self.host.bridge.notification_new(
1938 str(notification.id),
1939 notification.timestamp,
1940 type_.value,
1941 body_plain,
1942 body_rich or '',
1943 title or '',
1944 requires_action,
1945 priority.value,
1946 expire_at or 0,
1947 data_format.serialise(extra) if extra else '',
1948 C.PROF_KEY_ALL if is_global else client.profile
1949 )
1950
1951 def _get_notifications(self, filters_s: str, profile_key: str) -> defer.Deferred:
1952 """Fetch notifications for bridge with given filters and profile key.
1953
1954 @param filters_s: serialized filter conditions. Keys can be:
1955 :type_ (str):
1956 Filter by type of the notification.
1957 :status (str):
1958 Filter by status of the notification.
1959 :requires_action (bool):
1960 Filter by notifications that require user action.
1961 :min_priority (str):
1962 Filter by minimum priority value.
1963 @param profile_key: key of the profile to fetch notifications for.
1964 @return: Deferred which fires with a list of serialised notifications.
1965 """
1966 client = self.host.get_client(profile_key)
1967
1968 filters = data_format.deserialise(filters_s)
1969
1970 try:
1971 if 'type' in filters:
1972 filters['type_'] = NotificationType[filters.pop('type')]
1973 if 'status' in filters:
1974 filters['status'] = NotificationStatus[filters['status']]
1975 if 'min_priority' in filters:
1976 filters['min_priority'] = NotificationPriority[filters['min_priority']].value
1977 except KeyError as e:
1978 raise exceptions.DataError(f"invalid filter data: {e}")
1979
1980 d = defer.ensureDeferred(self.storage.get_notifications(client, **filters))
1981 d.addCallback(
1982 lambda notifications: data_format.serialise(
1983 [notification.serialise() for notification in notifications]
1984 )
1985 )
1986 return d
1987
1988 def _delete_notification(
1989 self,
1990 id_: str,
1991 is_global: bool,
1992 profile_key: str
1993 ) -> defer.Deferred:
1994 client = self.host.get_client(profile_key)
1995 if is_global and not client.is_admin:
1996 raise exceptions.PermissionError(
1997 "Only admins can delete global notifications"
1998 )
1999 return defer.ensureDeferred(self.delete_notification(client, id_, is_global))
2000
2001 async def delete_notification(
2002 self,
2003 client: SatXMPPEntity,
2004 id_: str,
2005 is_global: bool=False
2006 ) -> None:
2007 """Delete a notification
2008
2009 the notification must be from the requesting profile.
2010 @param id_: ID of the notification
2011 is_global: if True, a global notification will be removed.
2012 """
2013 await self.storage.delete_notification(None if is_global else client, id_)
2014 self.host.bridge.notification_deleted(
2015 id_,
2016 C.PROF_KEY_ALL if is_global else client.profile
2017 )
2018
2019 def _notifications_expired_clean(
2020 self, limit_timestamp: float, profile_key: str
2021 ) -> defer.Deferred:
2022 if profile_key == C.PROF_KEY_NONE:
2023 client = None
2024 else:
2025 client = self.host.get_client(profile_key)
2026
2027 return defer.ensureDeferred(
2028 self.storage.clean_expired_notifications(
2029 client,
2030 None if limit_timestamp == -1.0 else limit_timestamp
2031 )
2032 )
2033
2034
1851 ## Misc ## 2035 ## Misc ##
1852 2036
1853 def is_entity_available(self, client, entity_jid): 2037 def is_entity_available(self, client, entity_jid):
1854 """Tell from the presence information if the given entity is available. 2038 """Tell from the presence information if the given entity is available.
1855 2039