Mercurial > libervia-web
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 |