diff 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
line wrap: on
line diff
--- a/sat_frontends/quick_frontend/quick_contact_list.py	Tue Apr 14 20:36:24 2020 +0200
+++ b/sat_frontends/quick_frontend/quick_contact_list.py	Tue Apr 14 21:00:33 2020 +0200
@@ -105,17 +105,11 @@
             callback=self._showOfflineContacts,
         )
 
-        # FIXME: workaround for a pyjamas issue: calling hash on a class method always
-        #        return a different value if that method is defined directly within the
-        #        class (with the "def" keyword)
-        self.presenceListener = self.onPresenceUpdate
-        self.host.addListener("presence", self.presenceListener, [self.profile])
-        self.nickListener = self.onNickUpdate
-        self.host.addListener("nick", self.nickListener, [self.profile])
-        self.notifListener = self.onNotification
-        self.host.addListener("notification", self.notifListener, [self.profile])
-        # notifListener only update the entity, so we can re-use it
-        self.host.addListener("notificationsClear", self.notifListener, [self.profile])
+        self.host.addListener("presence", self.onPresenceUpdate, [self.profile])
+        self.host.addListener("nicknames", self.onNicknamesUpdate, [self.profile])
+        self.host.addListener("notification", self.onNotification, [self.profile])
+        # onNotification only updates the entity, so we can re-use it
+        self.host.addListener("notificationsClear", self.onNotification, [self.profile])
 
     @property
     def whoami(self):
@@ -164,7 +158,7 @@
             [
                 entity
                 for entity in self._roster
-                if self.getCache(entity, C.PRESENCE_SHOW) is not None
+                if self.getCache(entity, C.PRESENCE_SHOW, default=None) is not None
             ]
         )
 
@@ -255,7 +249,9 @@
     def fill(self):
         handler.fill(self.profile)
 
-    def getCache(self, entity, name=None, bare_default=True, create_if_not_found=False):
+    def getCache(
+        self, entity, name=None, bare_default=True, create_if_not_found=False,
+        default=Exception):
         """Return a cache value for a contact
 
         @param entity(jid.JID): entity of the contact from who we want data
@@ -272,8 +268,12 @@
             If None, bare_default will be set to False if entity is in a room, True else
         @param create_if_not_found(bool): if True, create contact if it's not found
             in cache
+        @param default(object): value to return when name is not found in cache
+            if Exception is used, a KeyError will be returned
+            otherwise, the given value will be used
         @return: full cache if no name is given, or value of "name", or None
         @raise NotFound: entity not found in cache
+        @raise KeyError: name not found in cache
         """
         # FIXME: resource handling need to be reworked
         # FIXME: bare_default work for requesting full jid to get bare jid,
@@ -291,6 +291,10 @@
                 raise exceptions.NotFound
 
         if name is None:
+            if default is not Exception:
+                raise exceptions.InternalError(
+                    "default value can only Exception when name is not specified"
+                )
             # full cache is requested
             return cache
 
@@ -313,22 +317,28 @@
         elif entity.resource:
             try:
                 return cache[C.CONTACT_RESOURCES][entity.resource][name]
-            except KeyError:
+            except KeyError as e:
                 if bare_default is None:
                     bare_default = not self.isRoom(entity.bare)
                 if not bare_default:
-                    return None
+                    if default is Exception:
+                        raise e
+                    else:
+                        return default
 
         try:
             return cache[name]
-        except KeyError:
-            return None
+        except KeyError as e:
+            if default is Exception:
+                raise e
+            else:
+                return default
 
     def setCache(self, entity, name, value):
         """Set or update value for one data in cache
 
         @param entity(JID): entity to update
-        @param name(unicode): value to set or update
+        @param name(str): value to set or update
         """
         self.setContact(entity, attributes={name: value})
 
@@ -391,7 +401,7 @@
         @param special_type: one of special type (e.g. C.CONTACT_SPECIAL_GROUP)
         @return (bool): True if entity is from this special type
         """
-        return self.getCache(entity, C.CONTACT_SPECIAL) == special_type
+        return self.getCache(entity, C.CONTACT_SPECIAL, default=None) == special_type
 
     def setSpecial(self, entity, special_type):
         """Set special flag on an entity
@@ -417,7 +427,7 @@
                 continue
             if (
                 special_type is not None
-                and self.getCache(entity, C.CONTACT_SPECIAL) != special_type
+                and self.getCache(entity, C.CONTACT_SPECIAL, default=None) != special_type
             ):
                 continue
             yield entity
@@ -443,7 +453,7 @@
         """Add a contact to the list if it doesn't exist, else update it.
 
         This method can be called with groups=None for the purpose of updating
-        the contact's attributes (e.g. nickname). In that case, the groups
+        the contact's attributes (e.g. nicknames). In that case, the groups
         attribute must not be set to the default group but ignored. If not,
         you may move your contact from its actual group(s) to the default one.
 
@@ -454,7 +464,6 @@
             if entity is a full jid, attributes will be cached in for the full jid only
         @param groups (list): list of groups or None to ignore the groups membership.
         @param attributes (dict): attibutes of the added jid or to update
-            if attribute value is None, it will be removed
         @param in_roster (bool): True if contact is from roster
         """
         if attributes is None:
@@ -506,8 +515,8 @@
             else:
                 self._specials.add(entity)
                 cache[C.CONTACT_MAIN_RESOURCE] = None
-                if 'nick' in cache:
-                    del cache['nick']
+                if 'nicknames' in cache:
+                    del cache['nicknames']
 
         # now the attributes we keep in cache
         # XXX: if entity is a full jid, we store the value for the resource only
@@ -517,23 +526,18 @@
             else cache
         )
         for attribute, value in attributes.items():
-            if value is None:
-                # XXX: pyjamas hack: we need to use pop instead of del
-                try:
-                    cache_attr[attribute].pop(value)
-                except KeyError:
-                    pass
-            else:
-                if attribute == "nick" and self.isSpecial(
-                    entity, C.CONTACT_SPECIAL_GROUP
-                ):
-                    # we don't want to keep nick for MUC rooms
-                    # FIXME: this is here as plugin XEP-0054 can link resource's nick
-                    #        with bare jid which in the case of MUC
-                    #        set the nick for the whole MUC
-                    #        resulting in bad name displayed in some frontends
-                    continue
-                cache_attr[attribute] = value
+            if attribute == "nicknames" and self.isSpecial(
+                entity, C.CONTACT_SPECIAL_GROUP
+            ):
+                # we don't want to keep nicknames for MUC rooms
+                # FIXME: this is here as plugin XEP-0054 can link resource's nick
+                #        with bare jid which in the case of MUC
+                #        set the nick for the whole MUC
+                #        resulting in bad name displayed in some frontends
+                # FIXME: with plugin XEP-0054 + plugin identity refactoring, this
+                #        may not be needed anymore…
+                continue
+            cache_attr[attribute] = value
 
         # we can update the display if needed
         if self.entityVisible(entity_bare):
@@ -554,7 +558,7 @@
         """
         try:
             show = self.getCache(entity, C.PRESENCE_SHOW)
-        except exceptions.NotFound:
+        except (exceptions.NotFound, KeyError):
             return False
 
         if check_resource:
@@ -682,15 +686,15 @@
         elif was_visible:
             self.update([entity], C.UPDATE_DELETE, self.profile)
 
-    def onNickUpdate(self, entity, new_nick, profile):
-        """Update entity's nick
+    def onNicknamesUpdate(self, entity, nicknames, profile):
+        """Update entity's nicknames
 
         @param entity(jid.JID): entity updated
-        @param new_nick(unicode): new nick of the entity
+        @param nicknames(list[unicode]): nicknames of the entity
         @param profile: %(doc_profile)s
         """
         assert profile == self.profile
-        self.setCache(entity, "nick", new_nick)
+        self.setCache(entity, "nicknames", nicknames)
 
     def onNotification(self, entity, notif, profile):
         """Update entity with notification
@@ -1093,6 +1097,8 @@
             - C.UPDATE_MODIFY: entity updated
             - C.UPDATE_ADD: entity added
             - C.UPDATE_SELECTION: selection modified
+            - C.UPDATE_STRUCTURE: organisation of items is modified (not items
+              themselves)
             or None for undefined update
             Note that events correspond to addition, modification and deletion
             of items on the whole contact list. If the contact is visible or not