Mercurial > libervia-backend
comparison sat/plugins/plugin_misc_identity.py @ 3277:cf07641b764d
plugin identity: fixed infinite loop on nicknames update
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 18 May 2020 23:52:34 +0200 |
parents | aa71f1d40300 |
children | 27d4b71e264a |
comparison
equal
deleted
inserted
replaced
3276:81c8910db91f | 3277:cf07641b764d |
---|---|
13 # GNU Affero General Public License for more details. | 13 # GNU Affero General Public License for more details. |
14 | 14 |
15 # You should have received a copy of the GNU Affero General Public License | 15 # You should have received a copy of the GNU Affero General Public License |
16 # along with this program. If not, see <http://www.gnu.org/licenses/>. | 16 # along with this program. If not, see <http://www.gnu.org/licenses/>. |
17 | 17 |
18 from typing import Dict, Union, Coroutine, Any, Optional | |
18 from collections import namedtuple | 19 from collections import namedtuple |
19 from pathlib import Path | 20 from pathlib import Path |
20 from twisted.internet import defer | 21 from twisted.internet import defer |
21 from twisted.words.protocols.jabber import jid | 22 from twisted.words.protocols.jabber import jid |
23 from sat.core.xmpp import SatXMPPEntity | |
22 from sat.core.i18n import _ | 24 from sat.core.i18n import _ |
23 from sat.core.constants import Const as C | 25 from sat.core.constants import Const as C |
24 from sat.core import exceptions | 26 from sat.core import exceptions |
25 from sat.core.log import getLogger | 27 from sat.core.log import getLogger |
26 from sat.memory import persistent | 28 from sat.memory import persistent |
42 C.PI_MAIN: "Identity", | 44 C.PI_MAIN: "Identity", |
43 C.PI_HANDLER: "no", | 45 C.PI_HANDLER: "no", |
44 C.PI_DESCRIPTION: _("""Identity manager"""), | 46 C.PI_DESCRIPTION: _("""Identity manager"""), |
45 } | 47 } |
46 | 48 |
47 Callback = namedtuple("Callback", ("get", "set", "priority")) | 49 Callback = namedtuple("Callback", ("origin", "get", "set", "priority")) |
48 | 50 |
49 | 51 |
50 class Identity: | 52 class Identity: |
51 | 53 |
52 def __init__(self, host): | 54 def __init__(self, host): |
112 method=self._setAvatar, | 114 method=self._setAvatar, |
113 async_=True, | 115 async_=True, |
114 ) | 116 ) |
115 | 117 |
116 async def profileConnecting(self, client): | 118 async def profileConnecting(self, client): |
119 client._identity_update_lock = [] | |
117 # we restore known identities from database | 120 # we restore known identities from database |
118 client._identity_storage = persistent.LazyPersistentBinaryDict( | 121 client._identity_storage = persistent.LazyPersistentBinaryDict( |
119 "identity", client.profile) | 122 "identity", client.profile) |
120 | 123 |
121 stored_data = await client._identity_storage.all() | 124 stored_data = await client._identity_storage.all() |
174 roster_item.jid | 177 roster_item.jid |
175 ) | 178 ) |
176 ) | 179 ) |
177 return True | 180 return True |
178 | 181 |
179 def register(self, metadata_name, cb_get, cb_set, priority=0): | 182 def register( |
183 self, | |
184 origin: str, | |
185 metadata_name: str, | |
186 cb_get: Union[Coroutine, defer.Deferred], | |
187 cb_set: Union[Coroutine, defer.Deferred], | |
188 priority: int=0): | |
180 """Register callbacks to handle identity metadata | 189 """Register callbacks to handle identity metadata |
181 | 190 |
182 @param metadata_name(str): name of metadata can be: | 191 @param origin: namespace of the plugin managing this metadata |
192 @param metadata_name: name of metadata can be: | |
183 - avatar | 193 - avatar |
184 - nicknames | 194 - nicknames |
185 @param cb_get(coroutine, Deferred): method to retrieve a metadata | 195 @param cb_get: method to retrieve a metadata |
186 the method will get client and metadata names to retrieve as arguments. | 196 the method will get client and metadata names to retrieve as arguments. |
187 @param cb_set(coroutine, Deferred): method to set a metadata | 197 @param cb_set: method to set a metadata |
188 the method will get client, metadata name to set, and value as argument. | 198 the method will get client, metadata name to set, and value as argument. |
189 @param priority(int): priority of this method for the given metadata. | 199 @param priority: priority of this method for the given metadata. |
190 methods with bigger priorities will be called first | 200 methods with bigger priorities will be called first |
191 """ | 201 """ |
192 if not metadata_name in self.metadata.keys(): | 202 if not metadata_name in self.metadata.keys(): |
193 raise ValueError(f"Invalid metadata_name: {metadata_name!r}") | 203 raise ValueError(f"Invalid metadata_name: {metadata_name!r}") |
194 callback = Callback(get=cb_get, set=cb_set, priority=priority) | 204 callback = Callback(origin=origin, get=cb_get, set=cb_set, priority=priority) |
195 cb_list = self.metadata[metadata_name].setdefault('callbacks', []) | 205 cb_list = self.metadata[metadata_name].setdefault('callbacks', []) |
196 cb_list.append(callback) | 206 cb_list.append(callback) |
197 cb_list.sort(key=lambda c: c.priority, reverse=True) | 207 cb_list.sort(key=lambda c: c.priority, reverse=True) |
198 | 208 |
199 def getIdentityJid(self, client, peer_jid): | 209 def getIdentityJid(self, client, peer_jid): |
217 if not isinstance(value, value_type): | 227 if not isinstance(value, value_type): |
218 raise ValueError( | 228 raise ValueError( |
219 f"{value} has wrong type: it is {type(value)} while {value_type} was " | 229 f"{value} has wrong type: it is {type(value)} while {value_type} was " |
220 f"expected") | 230 f"expected") |
221 | 231 |
222 async def get(self, client, metadata_name, entity, use_cache=True): | 232 async def get( |
233 self, | |
234 client: SatXMPPEntity, | |
235 metadata_name: str, | |
236 entity: Optional[jid.JID], | |
237 use_cache: bool=True, | |
238 prefilled_values: Optional[Dict[str, Any]]=None | |
239 ): | |
223 """Retrieve identity metadata of an entity | 240 """Retrieve identity metadata of an entity |
224 | 241 |
225 if metadata is already in cache, it is returned. Otherwise, registered callbacks | 242 if metadata is already in cache, it is returned. Otherwise, registered callbacks |
226 will be tried in priority order (bigger to lower) | 243 will be tried in priority order (bigger to lower) |
227 @param metadata_name(str): name of the metadata | 244 @param metadata_name: name of the metadata |
228 must be one of self.metadata key | 245 must be one of self.metadata key |
229 the name will also be used as entity data name in host.memory | 246 the name will also be used as entity data name in host.memory |
230 @param entity(jid.JID, None): entity for which avatar is requested | 247 @param entity: entity for which avatar is requested |
231 None to use profile's jid | 248 None to use profile's jid |
232 @param use_cache(bool): if False, cache won't be checked | 249 @param use_cache: if False, cache won't be checked |
250 @param prefilled_values: map of origin => value to use when `get_all` is set | |
233 """ | 251 """ |
234 entity = self.getIdentityJid(client, entity) | 252 entity = self.getIdentityJid(client, entity) |
235 try: | 253 try: |
236 metadata = self.metadata[metadata_name] | 254 metadata = self.metadata[metadata_name] |
237 except KeyError: | 255 except KeyError: |
253 .format(metadata_name=metadata_name)) | 271 .format(metadata_name=metadata_name)) |
254 return [] if get_all else None | 272 return [] if get_all else None |
255 | 273 |
256 if get_all: | 274 if get_all: |
257 all_data = [] | 275 all_data = [] |
276 elif prefilled_values is not None: | |
277 raise exceptions.InternalError( | |
278 "prefilled_values can only be used when `get_all` is set") | |
258 | 279 |
259 for callback in callbacks: | 280 for callback in callbacks: |
260 try: | 281 try: |
261 data = await defer.ensureDeferred(callback.get(client, entity)) | 282 if prefilled_values is not None and callback.origin in prefilled_values: |
283 data = prefilled_values[callback.origin] | |
284 log.debug( | |
285 f"using prefilled values {data!r} for {metadata_name} with " | |
286 f"{callback.origin}") | |
287 else: | |
288 data = await defer.ensureDeferred(callback.get(client, entity)) | |
262 except exceptions.CancelError: | 289 except exceptions.CancelError: |
263 continue | 290 continue |
264 except Exception as e: | 291 except Exception as e: |
265 log.warning( | 292 log.warning( |
266 _("Error while trying to get {metadata_name} with {callback}: {e}") | 293 _("Error while trying to get {metadata_name} with {callback}: {e}") |
332 | 359 |
333 post_treatment = metadata.get("set_post_treatment") | 360 post_treatment = metadata.get("set_post_treatment") |
334 if post_treatment is not None: | 361 if post_treatment is not None: |
335 await utils.asDeferred(post_treatment, client, entity, data) | 362 await utils.asDeferred(post_treatment, client, entity, data) |
336 | 363 |
337 async def update(self, client, metadata_name, data, entity): | 364 async def update( |
365 self, | |
366 client: SatXMPPEntity, | |
367 origin: str, | |
368 metadata_name: str, | |
369 data: Any, | |
370 entity: Optional[jid.JID] | |
371 ): | |
338 """Update a metadata in cache | 372 """Update a metadata in cache |
339 | 373 |
340 This method may be called by plugins when an identity metadata is available. | 374 This method may be called by plugins when an identity metadata is available. |
375 @param origin: namespace of the plugin which is source of the metadata | |
341 """ | 376 """ |
342 entity = self.getIdentityJid(client, entity) | 377 entity = self.getIdentityJid(client, entity) |
378 if (entity, metadata_name) in client._identity_update_lock: | |
379 log.debug(f"update is locked for {entity}'s {metadata_name}") | |
380 return | |
343 metadata = self.metadata[metadata_name] | 381 metadata = self.metadata[metadata_name] |
344 | 382 |
345 try: | 383 try: |
346 cached_data = self.host.memory.getEntityDatum( | 384 cached_data = self.host.memory.getEntityDatum( |
347 client, entity, metadata_name) | 385 client, entity, metadata_name) |
376 if metadata.get('get_all', False): | 414 if metadata.get('get_all', False): |
377 # get_all is set, meaning that we have to check all plugins | 415 # get_all is set, meaning that we have to check all plugins |
378 # so we first delete current cache | 416 # so we first delete current cache |
379 try: | 417 try: |
380 self.host.memory.delEntityDatum(client, entity, metadata_name) | 418 self.host.memory.delEntityDatum(client, entity, metadata_name) |
381 except KeyError: | 419 except (KeyError, exceptions.UnknownEntityError): |
382 pass | 420 pass |
383 # then fill it again by calling get, which will retrieve all values | 421 # then fill it again by calling get, which will retrieve all values |
384 await self.get(client, metadata_name, entity) | 422 # we lock update to avoid infinite recursions (update can be called during |
423 # get callbacks) | |
424 client._identity_update_lock.append((entity, metadata_name)) | |
425 await self.get(client, metadata_name, entity, prefilled_values={origin: data}) | |
426 client._identity_update_lock.remove((entity, metadata_name)) | |
385 return | 427 return |
386 | 428 |
387 if data is not None: | 429 if data is not None: |
388 data_filter = metadata['update_data_filter'] | 430 data_filter = metadata['update_data_filter'] |
389 if data_filter is not None: | 431 if data_filter is not None: |