comparison sat_frontends/quick_frontend/quick_contact_list.py @ 3254:6cf4bd6972c2

core, frontends: avatar refactoring: /!\ huge commit Avatar logic has been reworked around the IDENTITY plugin: plugins able to handle avatar or other identity related metadata (like nicknames) register to IDENTITY plugin in the same way as for other features like download/upload. Once registered, IDENTITY plugin will call them when suitable in order of priority, and handle caching. Methods to manage those metadata from frontend now use serialised data. For now `avatar` and `nicknames` are handled: - `avatar` is now a dict with `path` + metadata like `media_type`, instead of just a string path - `nicknames` is now a list of nicknames in order of priority. This list is never empty, and `nicknames[0]` should be the preferred nickname to use by frontends in most cases. In addition to contact specified nicknames, user set nickname (the one set in roster) is used in priority when available. Among the side changes done with this commit, there are: - a new `contactGet` bridge method to get roster metadata for a single contact - SatPresenceProtocol.send returns a Deferred to check when it has actually been sent - memory's methods to handle entities data now use `client` as first argument - metadata filter can be specified with `getIdentity` - `getAvatar` and `setAvatar` are now part of the IDENTITY plugin instead of XEP-0054 (and there signature has changed) - `isRoom` and `getBareOrFull` are now part of XEP-0045 plugin - jp avatar/get command uses `xdg-open` first when available for `--show` flag - `--no-cache` has been added to jp avatar/get and identity/get - jp identity/set has been simplified, explicit options (`--nickname` only for now) are used instead of `--field`. `--field` may come back in the future if necessary for extra data. - QuickContactList `SetContact` now handle None as a value, and doesn't use it to delete the metadata anymore - improved cache handling for `metadata` and `nicknames` in quick frontend - new `default` argument in QuickContactList `getCache`
author Goffi <goffi@goffi.org>
date Tue, 14 Apr 2020 21:00:33 +0200
parents 142ecb7f6338
children be6d91572633
comparison
equal deleted inserted replaced
3253:1af840e84af7 3254:6cf4bd6972c2
103 "General", 103 "General",
104 profile_key=profile, 104 profile_key=profile,
105 callback=self._showOfflineContacts, 105 callback=self._showOfflineContacts,
106 ) 106 )
107 107
108 # FIXME: workaround for a pyjamas issue: calling hash on a class method always 108 self.host.addListener("presence", self.onPresenceUpdate, [self.profile])
109 # return a different value if that method is defined directly within the 109 self.host.addListener("nicknames", self.onNicknamesUpdate, [self.profile])
110 # class (with the "def" keyword) 110 self.host.addListener("notification", self.onNotification, [self.profile])
111 self.presenceListener = self.onPresenceUpdate 111 # onNotification only updates the entity, so we can re-use it
112 self.host.addListener("presence", self.presenceListener, [self.profile]) 112 self.host.addListener("notificationsClear", self.onNotification, [self.profile])
113 self.nickListener = self.onNickUpdate
114 self.host.addListener("nick", self.nickListener, [self.profile])
115 self.notifListener = self.onNotification
116 self.host.addListener("notification", self.notifListener, [self.profile])
117 # notifListener only update the entity, so we can re-use it
118 self.host.addListener("notificationsClear", self.notifListener, [self.profile])
119 113
120 @property 114 @property
121 def whoami(self): 115 def whoami(self):
122 return self.host.profiles[self.profile].whoami 116 return self.host.profiles[self.profile].whoami
123 117
162 """ 156 """
163 return set( 157 return set(
164 [ 158 [
165 entity 159 entity
166 for entity in self._roster 160 for entity in self._roster
167 if self.getCache(entity, C.PRESENCE_SHOW) is not None 161 if self.getCache(entity, C.PRESENCE_SHOW, default=None) is not None
168 ] 162 ]
169 ) 163 )
170 164
171 @property 165 @property
172 def roster_entities_by_group(self): 166 def roster_entities_by_group(self):
253 self.host.bridge.getContacts(self.profile, callback=self._gotContacts) 247 self.host.bridge.getContacts(self.profile, callback=self._gotContacts)
254 248
255 def fill(self): 249 def fill(self):
256 handler.fill(self.profile) 250 handler.fill(self.profile)
257 251
258 def getCache(self, entity, name=None, bare_default=True, create_if_not_found=False): 252 def getCache(
253 self, entity, name=None, bare_default=True, create_if_not_found=False,
254 default=Exception):
259 """Return a cache value for a contact 255 """Return a cache value for a contact
260 256
261 @param entity(jid.JID): entity of the contact from who we want data 257 @param entity(jid.JID): entity of the contact from who we want data
262 (resource is used if given) 258 (resource is used if given)
263 if a resource specific information is requested: 259 if a resource specific information is requested:
270 the requested resource. 266 the requested resource.
271 If False, None is returned if no value is found for the requested resource. 267 If False, None is returned if no value is found for the requested resource.
272 If None, bare_default will be set to False if entity is in a room, True else 268 If None, bare_default will be set to False if entity is in a room, True else
273 @param create_if_not_found(bool): if True, create contact if it's not found 269 @param create_if_not_found(bool): if True, create contact if it's not found
274 in cache 270 in cache
271 @param default(object): value to return when name is not found in cache
272 if Exception is used, a KeyError will be returned
273 otherwise, the given value will be used
275 @return: full cache if no name is given, or value of "name", or None 274 @return: full cache if no name is given, or value of "name", or None
276 @raise NotFound: entity not found in cache 275 @raise NotFound: entity not found in cache
276 @raise KeyError: name not found in cache
277 """ 277 """
278 # FIXME: resource handling need to be reworked 278 # FIXME: resource handling need to be reworked
279 # FIXME: bare_default work for requesting full jid to get bare jid, 279 # FIXME: bare_default work for requesting full jid to get bare jid,
280 # but not the other way 280 # but not the other way
281 # e.g.: if we have set an avatar for user@server.tld/resource 281 # e.g.: if we have set an avatar for user@server.tld/resource
289 cache = self._cache[entity.bare] 289 cache = self._cache[entity.bare]
290 else: 290 else:
291 raise exceptions.NotFound 291 raise exceptions.NotFound
292 292
293 if name is None: 293 if name is None:
294 if default is not Exception:
295 raise exceptions.InternalError(
296 "default value can only Exception when name is not specified"
297 )
294 # full cache is requested 298 # full cache is requested
295 return cache 299 return cache
296 300
297 if name in ("status", C.PRESENCE_STATUSES, C.PRESENCE_PRIORITY, C.PRESENCE_SHOW): 301 if name in ("status", C.PRESENCE_STATUSES, C.PRESENCE_PRIORITY, C.PRESENCE_SHOW):
298 # these data are related to the resource 302 # these data are related to the resource
311 return cache[C.PRESENCE_STATUSES].get(C.PRESENCE_STATUSES_DEFAULT, "") 315 return cache[C.PRESENCE_STATUSES].get(C.PRESENCE_STATUSES_DEFAULT, "")
312 316
313 elif entity.resource: 317 elif entity.resource:
314 try: 318 try:
315 return cache[C.CONTACT_RESOURCES][entity.resource][name] 319 return cache[C.CONTACT_RESOURCES][entity.resource][name]
316 except KeyError: 320 except KeyError as e:
317 if bare_default is None: 321 if bare_default is None:
318 bare_default = not self.isRoom(entity.bare) 322 bare_default = not self.isRoom(entity.bare)
319 if not bare_default: 323 if not bare_default:
320 return None 324 if default is Exception:
325 raise e
326 else:
327 return default
321 328
322 try: 329 try:
323 return cache[name] 330 return cache[name]
324 except KeyError: 331 except KeyError as e:
325 return None 332 if default is Exception:
333 raise e
334 else:
335 return default
326 336
327 def setCache(self, entity, name, value): 337 def setCache(self, entity, name, value):
328 """Set or update value for one data in cache 338 """Set or update value for one data in cache
329 339
330 @param entity(JID): entity to update 340 @param entity(JID): entity to update
331 @param name(unicode): value to set or update 341 @param name(str): value to set or update
332 """ 342 """
333 self.setContact(entity, attributes={name: value}) 343 self.setContact(entity, attributes={name: value})
334 344
335 def getFullJid(self, entity): 345 def getFullJid(self, entity):
336 """Get full jid from a bare jid 346 """Get full jid from a bare jid
389 @param entity(jid.JID): jid of the special entity 399 @param entity(jid.JID): jid of the special entity
390 if the jid is full, will be added to special extras 400 if the jid is full, will be added to special extras
391 @param special_type: one of special type (e.g. C.CONTACT_SPECIAL_GROUP) 401 @param special_type: one of special type (e.g. C.CONTACT_SPECIAL_GROUP)
392 @return (bool): True if entity is from this special type 402 @return (bool): True if entity is from this special type
393 """ 403 """
394 return self.getCache(entity, C.CONTACT_SPECIAL) == special_type 404 return self.getCache(entity, C.CONTACT_SPECIAL, default=None) == special_type
395 405
396 def setSpecial(self, entity, special_type): 406 def setSpecial(self, entity, special_type):
397 """Set special flag on an entity 407 """Set special flag on an entity
398 408
399 @param entity(jid.JID): jid of the special entity 409 @param entity(jid.JID): jid of the special entity
415 for entity in self._specials: 425 for entity in self._specials:
416 if bare and entity.resource: 426 if bare and entity.resource:
417 continue 427 continue
418 if ( 428 if (
419 special_type is not None 429 special_type is not None
420 and self.getCache(entity, C.CONTACT_SPECIAL) != special_type 430 and self.getCache(entity, C.CONTACT_SPECIAL, default=None) != special_type
421 ): 431 ):
422 continue 432 continue
423 yield entity 433 yield entity
424 434
425 def disconnect(self): 435 def disconnect(self):
441 451
442 def setContact(self, entity, groups=None, attributes=None, in_roster=False): 452 def setContact(self, entity, groups=None, attributes=None, in_roster=False):
443 """Add a contact to the list if it doesn't exist, else update it. 453 """Add a contact to the list if it doesn't exist, else update it.
444 454
445 This method can be called with groups=None for the purpose of updating 455 This method can be called with groups=None for the purpose of updating
446 the contact's attributes (e.g. nickname). In that case, the groups 456 the contact's attributes (e.g. nicknames). In that case, the groups
447 attribute must not be set to the default group but ignored. If not, 457 attribute must not be set to the default group but ignored. If not,
448 you may move your contact from its actual group(s) to the default one. 458 you may move your contact from its actual group(s) to the default one.
449 459
450 None value for 'groups' has a different meaning than [None] 460 None value for 'groups' has a different meaning than [None]
451 which is for the default group. 461 which is for the default group.
452 462
453 @param entity (jid.JID): entity to add or replace 463 @param entity (jid.JID): entity to add or replace
454 if entity is a full jid, attributes will be cached in for the full jid only 464 if entity is a full jid, attributes will be cached in for the full jid only
455 @param groups (list): list of groups or None to ignore the groups membership. 465 @param groups (list): list of groups or None to ignore the groups membership.
456 @param attributes (dict): attibutes of the added jid or to update 466 @param attributes (dict): attibutes of the added jid or to update
457 if attribute value is None, it will be removed
458 @param in_roster (bool): True if contact is from roster 467 @param in_roster (bool): True if contact is from roster
459 """ 468 """
460 if attributes is None: 469 if attributes is None:
461 attributes = {} 470 attributes = {}
462 471
504 del attributes[C.CONTACT_SPECIAL] 513 del attributes[C.CONTACT_SPECIAL]
505 self._specials.remove(entity) 514 self._specials.remove(entity)
506 else: 515 else:
507 self._specials.add(entity) 516 self._specials.add(entity)
508 cache[C.CONTACT_MAIN_RESOURCE] = None 517 cache[C.CONTACT_MAIN_RESOURCE] = None
509 if 'nick' in cache: 518 if 'nicknames' in cache:
510 del cache['nick'] 519 del cache['nicknames']
511 520
512 # now the attributes we keep in cache 521 # now the attributes we keep in cache
513 # XXX: if entity is a full jid, we store the value for the resource only 522 # XXX: if entity is a full jid, we store the value for the resource only
514 cache_attr = ( 523 cache_attr = (
515 cache[C.CONTACT_RESOURCES].setdefault(entity.resource, {}) 524 cache[C.CONTACT_RESOURCES].setdefault(entity.resource, {})
516 if entity.resource 525 if entity.resource
517 else cache 526 else cache
518 ) 527 )
519 for attribute, value in attributes.items(): 528 for attribute, value in attributes.items():
520 if value is None: 529 if attribute == "nicknames" and self.isSpecial(
521 # XXX: pyjamas hack: we need to use pop instead of del 530 entity, C.CONTACT_SPECIAL_GROUP
522 try: 531 ):
523 cache_attr[attribute].pop(value) 532 # we don't want to keep nicknames for MUC rooms
524 except KeyError: 533 # FIXME: this is here as plugin XEP-0054 can link resource's nick
525 pass 534 # with bare jid which in the case of MUC
526 else: 535 # set the nick for the whole MUC
527 if attribute == "nick" and self.isSpecial( 536 # resulting in bad name displayed in some frontends
528 entity, C.CONTACT_SPECIAL_GROUP 537 # FIXME: with plugin XEP-0054 + plugin identity refactoring, this
529 ): 538 # may not be needed anymore…
530 # we don't want to keep nick for MUC rooms 539 continue
531 # FIXME: this is here as plugin XEP-0054 can link resource's nick 540 cache_attr[attribute] = value
532 # with bare jid which in the case of MUC
533 # set the nick for the whole MUC
534 # resulting in bad name displayed in some frontends
535 continue
536 cache_attr[attribute] = value
537 541
538 # we can update the display if needed 542 # we can update the display if needed
539 if self.entityVisible(entity_bare): 543 if self.entityVisible(entity_bare):
540 # if the contact was not visible, we need to add a widget 544 # if the contact was not visible, we need to add a widget
541 # else we just update id 545 # else we just update id
552 @param check_resource (bool): True if resource must be significant 556 @param check_resource (bool): True if resource must be significant
553 @return (bool): True if that contact should be showed in the list 557 @return (bool): True if that contact should be showed in the list
554 """ 558 """
555 try: 559 try:
556 show = self.getCache(entity, C.PRESENCE_SHOW) 560 show = self.getCache(entity, C.PRESENCE_SHOW)
557 except exceptions.NotFound: 561 except (exceptions.NotFound, KeyError):
558 return False 562 return False
559 563
560 if check_resource: 564 if check_resource:
561 selected = self._selected 565 selected = self._selected
562 else: 566 else:
680 update_type = C.UPDATE_MODIFY if was_visible else C.UPDATE_ADD 684 update_type = C.UPDATE_MODIFY if was_visible else C.UPDATE_ADD
681 self.update([entity], update_type, self.profile) 685 self.update([entity], update_type, self.profile)
682 elif was_visible: 686 elif was_visible:
683 self.update([entity], C.UPDATE_DELETE, self.profile) 687 self.update([entity], C.UPDATE_DELETE, self.profile)
684 688
685 def onNickUpdate(self, entity, new_nick, profile): 689 def onNicknamesUpdate(self, entity, nicknames, profile):
686 """Update entity's nick 690 """Update entity's nicknames
687 691
688 @param entity(jid.JID): entity updated 692 @param entity(jid.JID): entity updated
689 @param new_nick(unicode): new nick of the entity 693 @param nicknames(list[unicode]): nicknames of the entity
690 @param profile: %(doc_profile)s 694 @param profile: %(doc_profile)s
691 """ 695 """
692 assert profile == self.profile 696 assert profile == self.profile
693 self.setCache(entity, "nick", new_nick) 697 self.setCache(entity, "nicknames", nicknames)
694 698
695 def onNotification(self, entity, notif, profile): 699 def onNotification(self, entity, notif, profile):
696 """Update entity with notification 700 """Update entity with notification
697 701
698 @param entity(jid.JID): entity updated 702 @param entity(jid.JID): entity updated
1091 @param type_(unicode, None): update type, may be: 1095 @param type_(unicode, None): update type, may be:
1092 - C.UPDATE_DELETE: entity deleted 1096 - C.UPDATE_DELETE: entity deleted
1093 - C.UPDATE_MODIFY: entity updated 1097 - C.UPDATE_MODIFY: entity updated
1094 - C.UPDATE_ADD: entity added 1098 - C.UPDATE_ADD: entity added
1095 - C.UPDATE_SELECTION: selection modified 1099 - C.UPDATE_SELECTION: selection modified
1100 - C.UPDATE_STRUCTURE: organisation of items is modified (not items
1101 themselves)
1096 or None for undefined update 1102 or None for undefined update
1097 Note that events correspond to addition, modification and deletion 1103 Note that events correspond to addition, modification and deletion
1098 of items on the whole contact list. If the contact is visible or not 1104 of items on the whole contact list. If the contact is visible or not
1099 has no influence on the type_. 1105 has no influence on the type_.
1100 @param profile(unicode, None): profile concerned with the update 1106 @param profile(unicode, None): profile concerned with the update