Mercurial > libervia-web
diff libervia/web/server/session_iface.py @ 1518:eb00d593801d
refactoring: rename `libervia` to `libervia.web` + update imports following backend changes
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 02 Jun 2023 16:49:28 +0200 |
parents | libervia/server/session_iface.py@106bae41f5c8 |
children | 7941444c1671 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/web/server/session_iface.py Fri Jun 02 16:49:28 2023 +0200 @@ -0,0 +1,289 @@ +#!/usr/bin/env python3 + +# Libervia: a SàT frontend +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# 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/>. +from collections import OrderedDict, deque +import os.path +import time +from typing import List, Dict, Optional + +import shortuuid +from zope.interface import Attribute, Interface +from zope.interface import implementer + +from libervia.backend.core.log import getLogger + +from libervia.web.server.classes import Notification +from libervia.web.server.constants import Const as C + + +log = getLogger(__name__) + +FLAGS_KEY = "_flags" +NOTIFICATIONS_KEY = "_notifications" +MAX_CACHE_AFFILIATIONS = 100 # number of nodes to keep in cache + + +class IWebSession(Interface): + profile = Attribute("Sat profile") + jid = Attribute("JID associated with the profile") + uuid = Attribute("uuid associated with the profile session") + identities = Attribute("Identities of XMPP entities") + + +@implementer(IWebSession) +class WebSession: + profiles_map: Dict[Optional[str], List["WebSession"]] = {} + + def __init__(self, session): + self._profile = None + self.jid = None + self.started = time.time() + # time when the backend session was started + self.backend_started = None + self.uuid = str(shortuuid.uuid()) + self.identities = {} + self.csrf_token = str(shortuuid.uuid()) + self.ws_token = str(shortuuid.uuid()) + self.ws_socket = None + self.ws_buffer = deque(maxlen=200) + self.locale = None # i18n of the pages + self.theme = None + self.pages_data = {} # used to keep data accross reloads (key is page instance) + self.affiliations = OrderedDict() # cache for node affiliations + self.profiles_map.setdefault(C.SERVICE_PROFILE, []).append(self) + log.debug( + f"session started for {C.SERVICE_PROFILE} " + f"({len(self.get_profile_sessions(C.SERVICE_PROFILE))} session(s) active)" + ) + + @property + def profile(self) -> Optional[str]: + return self._profile + + @profile.setter + def profile(self, profile: Optional[str]) -> None: + old_profile = self._profile or C.SERVICE_PROFILE + new_profile = profile or C.SERVICE_PROFILE + try: + self.profiles_map[old_profile].remove(self) + except (ValueError, KeyError): + log.warning(f"session was not registered for profile {old_profile}") + else: + nb_session_old = len(self.get_profile_sessions(old_profile)) + log.debug(f"{old_profile} has now {nb_session_old} session(s) active") + + self._profile = profile + self.profiles_map.setdefault(new_profile, []).append(self) + nb_session_new = len(self.get_profile_sessions(new_profile)) + log.debug(f"{new_profile} has now {nb_session_new} session(s) active") + + @property + def cache_dir(self): + if self.profile is None: + return self.service_cache_url + "/" + return os.path.join("/", C.CACHE_DIR, self.uuid) + "/" + + @property + def connected(self): + return self.profile is not None + + @property + def guest(self): + """True if this is a guest session""" + if self.profile is None: + return False + else: + return self.profile.startswith("guest@@") + + @classmethod + def send(cls, profile: str, data_type: str, data: dict) -> None: + """Send a message to all session + + If the session doesn't have an active websocket, the message is buffered until a + socket is available + """ + for session in cls.profiles_map.get(profile, []): + if session.ws_socket is None or not session.ws_socket.init_ok: + session.ws_buffer.append({"data_type": data_type, "data": data}) + else: + session.ws_socket.send(data_type, data) + + def on_expire(self) -> None: + profile = self._profile or C.SERVICE_PROFILE + try: + self.profiles_map[profile].remove(self) + except (ValueError, KeyError): + log.warning(f"session was not registered for profile {profile}") + else: + nb_session = len(self.get_profile_sessions(profile)) + log.debug( + f"Session for profile {profile} expired. {profile} has now {nb_session} " + f"session(s) active." + ) + + @classmethod + def get_profile_sessions(cls, profile: str) -> List["WebSession"]: + return cls.profiles_map.get(profile, []) + + def get_page_data(self, page, key): + """get session data for a page + + @param page(LiberviaPage): instance of the page + @param key(object): data key + return (None, object): value of the key + None if not found or page_data doesn't exist + """ + return self.pages_data.get(page, {}).get(key) + + def pop_page_data(self, page, key, default=None): + """like get_page_data, but remove key once value is gotten + + @param page(LiberviaPage): instance of the page + @param key(object): data key + @param default(object): value to return if key is not found + @return (object): found value or default + """ + page_data = self.pages_data.get(page) + if page_data is None: + return default + value = page_data.pop(key, default) + if not page_data: + # no need to keep unused page_data + del self.pages_data[page] + return value + + def set_page_data(self, page, key, value): + """set data to persist on reload + + @param page(LiberviaPage): instance of the page + @param key(object): data key + @param value(object): value to set + @return (object): set value + """ + page_data = self.pages_data.setdefault(page, {}) + page_data[key] = value + return value + + def set_page_flag(self, page, flag): + """set a flag for this page + + @param page(LiberviaPage): instance of the page + @param flag(unicode): flag to set + """ + flags = self.get_page_data(page, FLAGS_KEY) + if flags is None: + flags = self.set_page_data(page, FLAGS_KEY, set()) + flags.add(flag) + + def pop_page_flag(self, page, flag): + """return True if flag is set + + flag is removed if it was set + @param page(LiberviaPage): instance of the page + @param flag(unicode): flag to set + @return (bool): True if flaag was set + """ + page_data = self.pages_data.get(page, {}) + flags = page_data.get(FLAGS_KEY) + if flags is None: + return False + if flag in flags: + flags.remove(flag) + # we remove data if they are not used anymore + if not flags: + del page_data[FLAGS_KEY] + if not page_data: + del self.pages_data[page] + return True + else: + return False + + def set_page_notification(self, page, message, level=C.LVL_INFO): + """set a flag for this page + + @param page(LiberviaPage): instance of the page + @param flag(unicode): flag to set + """ + notif = Notification(message, level) + notifs = self.get_page_data(page, NOTIFICATIONS_KEY) + if notifs is None: + notifs = self.set_page_data(page, NOTIFICATIONS_KEY, []) + notifs.append(notif) + + def pop_page_notifications(self, page): + """Return and remove last page notification + + @param page(LiberviaPage): instance of the page + @return (list[Notification]): notifications if any + """ + page_data = self.pages_data.get(page, {}) + notifs = page_data.get(NOTIFICATIONS_KEY) + if not notifs: + return [] + ret = notifs[:] + del notifs[:] + return ret + + def get_affiliation(self, service, node): + """retrieve affiliation for a pubsub node + + @param service(jid.JID): pubsub service + @param node(unicode): pubsub node + @return (unicode, None): affiliation, or None if it is not in cache + """ + if service.resource: + raise ValueError("Service must not have a resource") + if not node: + raise ValueError("node must be set") + try: + affiliation = self.affiliations.pop((service, node)) + except KeyError: + return None + else: + # we replace at the top to get the most recently used on top + # so less recently used will be removed if cache is full + self.affiliations[(service, node)] = affiliation + return affiliation + + def set_affiliation(self, service, node, affiliation): + """cache affiliation for a node + + will empty cache when it become too big + @param service(jid.JID): pubsub service + @param node(unicode): pubsub node + @param affiliation(unicode): affiliation to this node + """ + if service.resource: + raise ValueError("Service must not have a resource") + if not node: + raise ValueError("node must be set") + self.affiliations[(service, node)] = affiliation + while len(self.affiliations) > MAX_CACHE_AFFILIATIONS: + self.affiliations.popitem(last=False) + + +class IWebGuestSession(Interface): + id = Attribute("UUID of the guest") + data = Attribute("data associated with the guest") + + +@implementer(IWebGuestSession) +class WebGuestSession(object): + + def __init__(self, session): + self.id = None + self.data = None