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: