comparison 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
comparison
equal deleted inserted replaced
1517:b8ed9726525b 1518:eb00d593801d
1 #!/usr/bin/env python3
2
3 # Libervia: a SàT frontend
4 # Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org)
5
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU Affero General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU Affero General Public License for more details.
15
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/>.
18 from collections import OrderedDict, deque
19 import os.path
20 import time
21 from typing import List, Dict, Optional
22
23 import shortuuid
24 from zope.interface import Attribute, Interface
25 from zope.interface import implementer
26
27 from libervia.backend.core.log import getLogger
28
29 from libervia.web.server.classes import Notification
30 from libervia.web.server.constants import Const as C
31
32
33 log = getLogger(__name__)
34
35 FLAGS_KEY = "_flags"
36 NOTIFICATIONS_KEY = "_notifications"
37 MAX_CACHE_AFFILIATIONS = 100 # number of nodes to keep in cache
38
39
40 class IWebSession(Interface):
41 profile = Attribute("Sat profile")
42 jid = Attribute("JID associated with the profile")
43 uuid = Attribute("uuid associated with the profile session")
44 identities = Attribute("Identities of XMPP entities")
45
46
47 @implementer(IWebSession)
48 class WebSession:
49 profiles_map: Dict[Optional[str], List["WebSession"]] = {}
50
51 def __init__(self, session):
52 self._profile = None
53 self.jid = None
54 self.started = time.time()
55 # time when the backend session was started
56 self.backend_started = None
57 self.uuid = str(shortuuid.uuid())
58 self.identities = {}
59 self.csrf_token = str(shortuuid.uuid())
60 self.ws_token = str(shortuuid.uuid())
61 self.ws_socket = None
62 self.ws_buffer = deque(maxlen=200)
63 self.locale = None # i18n of the pages
64 self.theme = None
65 self.pages_data = {} # used to keep data accross reloads (key is page instance)
66 self.affiliations = OrderedDict() # cache for node affiliations
67 self.profiles_map.setdefault(C.SERVICE_PROFILE, []).append(self)
68 log.debug(
69 f"session started for {C.SERVICE_PROFILE} "
70 f"({len(self.get_profile_sessions(C.SERVICE_PROFILE))} session(s) active)"
71 )
72
73 @property
74 def profile(self) -> Optional[str]:
75 return self._profile
76
77 @profile.setter
78 def profile(self, profile: Optional[str]) -> None:
79 old_profile = self._profile or C.SERVICE_PROFILE
80 new_profile = profile or C.SERVICE_PROFILE
81 try:
82 self.profiles_map[old_profile].remove(self)
83 except (ValueError, KeyError):
84 log.warning(f"session was not registered for profile {old_profile}")
85 else:
86 nb_session_old = len(self.get_profile_sessions(old_profile))
87 log.debug(f"{old_profile} has now {nb_session_old} session(s) active")
88
89 self._profile = profile
90 self.profiles_map.setdefault(new_profile, []).append(self)
91 nb_session_new = len(self.get_profile_sessions(new_profile))
92 log.debug(f"{new_profile} has now {nb_session_new} session(s) active")
93
94 @property
95 def cache_dir(self):
96 if self.profile is None:
97 return self.service_cache_url + "/"
98 return os.path.join("/", C.CACHE_DIR, self.uuid) + "/"
99
100 @property
101 def connected(self):
102 return self.profile is not None
103
104 @property
105 def guest(self):
106 """True if this is a guest session"""
107 if self.profile is None:
108 return False
109 else:
110 return self.profile.startswith("guest@@")
111
112 @classmethod
113 def send(cls, profile: str, data_type: str, data: dict) -> None:
114 """Send a message to all session
115
116 If the session doesn't have an active websocket, the message is buffered until a
117 socket is available
118 """
119 for session in cls.profiles_map.get(profile, []):
120 if session.ws_socket is None or not session.ws_socket.init_ok:
121 session.ws_buffer.append({"data_type": data_type, "data": data})
122 else:
123 session.ws_socket.send(data_type, data)
124
125 def on_expire(self) -> None:
126 profile = self._profile or C.SERVICE_PROFILE
127 try:
128 self.profiles_map[profile].remove(self)
129 except (ValueError, KeyError):
130 log.warning(f"session was not registered for profile {profile}")
131 else:
132 nb_session = len(self.get_profile_sessions(profile))
133 log.debug(
134 f"Session for profile {profile} expired. {profile} has now {nb_session} "
135 f"session(s) active."
136 )
137
138 @classmethod
139 def get_profile_sessions(cls, profile: str) -> List["WebSession"]:
140 return cls.profiles_map.get(profile, [])
141
142 def get_page_data(self, page, key):
143 """get session data for a page
144
145 @param page(LiberviaPage): instance of the page
146 @param key(object): data key
147 return (None, object): value of the key
148 None if not found or page_data doesn't exist
149 """
150 return self.pages_data.get(page, {}).get(key)
151
152 def pop_page_data(self, page, key, default=None):
153 """like get_page_data, but remove key once value is gotten
154
155 @param page(LiberviaPage): instance of the page
156 @param key(object): data key
157 @param default(object): value to return if key is not found
158 @return (object): found value or default
159 """
160 page_data = self.pages_data.get(page)
161 if page_data is None:
162 return default
163 value = page_data.pop(key, default)
164 if not page_data:
165 # no need to keep unused page_data
166 del self.pages_data[page]
167 return value
168
169 def set_page_data(self, page, key, value):
170 """set data to persist on reload
171
172 @param page(LiberviaPage): instance of the page
173 @param key(object): data key
174 @param value(object): value to set
175 @return (object): set value
176 """
177 page_data = self.pages_data.setdefault(page, {})
178 page_data[key] = value
179 return value
180
181 def set_page_flag(self, page, flag):
182 """set a flag for this page
183
184 @param page(LiberviaPage): instance of the page
185 @param flag(unicode): flag to set
186 """
187 flags = self.get_page_data(page, FLAGS_KEY)
188 if flags is None:
189 flags = self.set_page_data(page, FLAGS_KEY, set())
190 flags.add(flag)
191
192 def pop_page_flag(self, page, flag):
193 """return True if flag is set
194
195 flag is removed if it was set
196 @param page(LiberviaPage): instance of the page
197 @param flag(unicode): flag to set
198 @return (bool): True if flaag was set
199 """
200 page_data = self.pages_data.get(page, {})
201 flags = page_data.get(FLAGS_KEY)
202 if flags is None:
203 return False
204 if flag in flags:
205 flags.remove(flag)
206 # we remove data if they are not used anymore
207 if not flags:
208 del page_data[FLAGS_KEY]
209 if not page_data:
210 del self.pages_data[page]
211 return True
212 else:
213 return False
214
215 def set_page_notification(self, page, message, level=C.LVL_INFO):
216 """set a flag for this page
217
218 @param page(LiberviaPage): instance of the page
219 @param flag(unicode): flag to set
220 """
221 notif = Notification(message, level)
222 notifs = self.get_page_data(page, NOTIFICATIONS_KEY)
223 if notifs is None:
224 notifs = self.set_page_data(page, NOTIFICATIONS_KEY, [])
225 notifs.append(notif)
226
227 def pop_page_notifications(self, page):
228 """Return and remove last page notification
229
230 @param page(LiberviaPage): instance of the page
231 @return (list[Notification]): notifications if any
232 """
233 page_data = self.pages_data.get(page, {})
234 notifs = page_data.get(NOTIFICATIONS_KEY)
235 if not notifs:
236 return []
237 ret = notifs[:]
238 del notifs[:]
239 return ret
240
241 def get_affiliation(self, service, node):
242 """retrieve affiliation for a pubsub node
243
244 @param service(jid.JID): pubsub service
245 @param node(unicode): pubsub node
246 @return (unicode, None): affiliation, or None if it is not in cache
247 """
248 if service.resource:
249 raise ValueError("Service must not have a resource")
250 if not node:
251 raise ValueError("node must be set")
252 try:
253 affiliation = self.affiliations.pop((service, node))
254 except KeyError:
255 return None
256 else:
257 # we replace at the top to get the most recently used on top
258 # so less recently used will be removed if cache is full
259 self.affiliations[(service, node)] = affiliation
260 return affiliation
261
262 def set_affiliation(self, service, node, affiliation):
263 """cache affiliation for a node
264
265 will empty cache when it become too big
266 @param service(jid.JID): pubsub service
267 @param node(unicode): pubsub node
268 @param affiliation(unicode): affiliation to this node
269 """
270 if service.resource:
271 raise ValueError("Service must not have a resource")
272 if not node:
273 raise ValueError("node must be set")
274 self.affiliations[(service, node)] = affiliation
275 while len(self.affiliations) > MAX_CACHE_AFFILIATIONS:
276 self.affiliations.popitem(last=False)
277
278
279 class IWebGuestSession(Interface):
280 id = Attribute("UUID of the guest")
281 data = Attribute("data associated with the guest")
282
283
284 @implementer(IWebGuestSession)
285 class WebGuestSession(object):
286
287 def __init__(self, session):
288 self.id = None
289 self.data = None