comparison sat_frontends/quick_frontend/quick_app.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 f3c99e96ac03
children be6d91572633
comparison
equal deleted inserted replaced
3253:1af840e84af7 3254:6cf4bd6972c2
42 42
43 # TODO: handle waiting XMLUI requests: getWaitingConf doesn't exist anymore 43 # TODO: handle waiting XMLUI requests: getWaitingConf doesn't exist anymore
44 # and a way to keep some XMLUI request between sessions is expected in backend 44 # and a way to keep some XMLUI request between sessions is expected in backend
45 host = None 45 host = None
46 bridge = None 46 bridge = None
47 # cache_keys_to_get = ['avatar'] 47 cache_keys_to_get = ['avatar', 'nicknames']
48 48
49 def __init__(self, profile): 49 def __init__(self, profile):
50 self.profile = profile 50 self.profile = profile
51 self.connected = False 51 self.connected = False
52 self.whoami = None 52 self.whoami = None
134 log.error("Couldn't get features: {}".format(failure)) 134 log.error("Couldn't get features: {}".format(failure))
135 self._plug_profile_getFeaturesCb({}) 135 self._plug_profile_getFeaturesCb({})
136 136
137 def _plug_profile_getFeaturesCb(self, features): 137 def _plug_profile_getFeaturesCb(self, features):
138 self.host.features = features 138 self.host.features = features
139 # FIXME: we don't use cached value at the moment, but keep the code for later use 139 self.host.bridge.getEntitiesData([], ProfileManager.cache_keys_to_get,
140 # it was previously used for avatars, but as we don't get full path here, 140 profile=self.profile,
141 # it's better to request later 141 callback=self._plug_profile_gotCachedValues,
142 # self.host.bridge.getEntitiesData([], ProfileManager.cache_keys_to_get, 142 errback=self._plug_profile_failedCachedValues)
143 # profile=self.profile,
144 # callback=self._plug_profile_gotCachedValues,
145 # errback=self._plug_profile_failedCachedValues)
146 self._plug_profile_gotCachedValues({})
147 143
148 def _plug_profile_failedCachedValues(self, failure): 144 def _plug_profile_failedCachedValues(self, failure):
149 log.error("Couldn't get cached values: {}".format(failure)) 145 log.error("Couldn't get cached values: {}".format(failure))
150 self._plug_profile_gotCachedValues({}) 146 self._plug_profile_gotCachedValues({})
151 147
184 self.host.bridge.getPresenceStatuses( 180 self.host.bridge.getPresenceStatuses(
185 self.profile, callback=self._plug_profile_gotPresences 181 self.profile, callback=self._plug_profile_gotPresences
186 ) 182 )
187 183
188 def _plug_profile_gotPresences(self, presences): 184 def _plug_profile_gotPresences(self, presences):
189 def gotEntityData(data, contact):
190 for key in ("avatar", "nick"):
191 if key in data:
192 self.host.entityDataUpdatedHandler(
193 contact, key, data[key], self.profile
194 )
195
196 for contact in presences: 185 for contact in presences:
197 for res in presences[contact]: 186 for res in presences[contact]:
198 jabber_id = ("%s/%s" % (jid.JID(contact).bare, res)) if res else contact 187 jabber_id = ("%s/%s" % (jid.JID(contact).bare, res)) if res else contact
199 show = presences[contact][res][0] 188 show = presences[contact][res][0]
200 priority = presences[contact][res][1] 189 priority = presences[contact][res][1]
201 statuses = presences[contact][res][2] 190 statuses = presences[contact][res][2]
202 self.host.presenceUpdateHandler( 191 self.host.presenceUpdateHandler(
203 jabber_id, show, priority, statuses, self.profile 192 jabber_id, show, priority, statuses, self.profile
204 ) 193 )
205 self.host.bridge.getEntityData(
206 contact,
207 ["avatar", "nick"],
208 self.profile,
209 callback=lambda data, contact=contact: gotEntityData(data, contact),
210 errback=lambda failure, contact=contact: log.debug(
211 "No cache data for {}".format(contact)
212 ),
213 )
214 194
215 # At this point, profile should be fully plugged 195 # At this point, profile should be fully plugged
216 # and we launch frontend specific method 196 # and we launch frontend specific method
217 self.host.profilePlugged(self.profile) 197 self.host.profilePlugged(self.profile)
218 198
551 widget) 531 widget)
552 @param type_: type of event, can be: 532 @param type_: type of event, can be:
553 - contactsFilled: called when contact have been fully filled for a profiles 533 - contactsFilled: called when contact have been fully filled for a profiles
554 kwargs: profile 534 kwargs: profile
555 - avatar: called when avatar data is updated 535 - avatar: called when avatar data is updated
556 args: (entity, avatar file, profile) 536 args: (entity, avatar_data, profile)
557 - nick: called when nick data is updated 537 - nicknames: called when nicknames data is updated
558 args: (entity, new_nick, profile) 538 args: (entity, nicknames, profile)
559 - presence: called when a presence is received 539 - presence: called when a presence is received
560 args: (entity, show, priority, statuses, profile) 540 args: (entity, show, priority, statuses, profile)
561 - selected: called when a widget is selected 541 - selected: called when a widget is selected
562 args: (selected_widget,) 542 args: (selected_widget,)
563 - notification: called when a new notification is emited 543 - notification: called when a new notification is emited
1259 1239
1260 def contactDeletedHandler(self, jid_s, profile): 1240 def contactDeletedHandler(self, jid_s, profile):
1261 target = jid.JID(jid_s) 1241 target = jid.JID(jid_s)
1262 self.contact_lists[profile].removeContact(target) 1242 self.contact_lists[profile].removeContact(target)
1263 1243
1264 def entityDataUpdatedHandler(self, entity_s, key, value, profile): 1244 def entityDataUpdatedHandler(self, entity_s, key, value_raw, profile):
1265 entity = jid.JID(entity_s) 1245 entity = jid.JID(entity_s)
1266 if key == "nick": # this is the roster nick, not the MUC nick 1246 value = data_format.deserialise(value_raw, type_check=None)
1247 if key == "nicknames":
1248 assert isinstance(value, list) or value is None
1267 if entity in self.contact_lists[profile]: 1249 if entity in self.contact_lists[profile]:
1268 self.contact_lists[profile].setCache(entity, "nick", value) 1250 self.contact_lists[profile].setCache(entity, "nicknames", value)
1269 self.callListeners("nick", entity, value, profile=profile) 1251 self.callListeners("nicknames", entity, value, profile=profile)
1270 elif key == "avatar" and self.AVATARS_HANDLER: 1252 elif key == "avatar" and self.AVATARS_HANDLER:
1271 if value and entity in self.contact_lists[profile]: 1253 assert isinstance(value, dict) or value is None
1272 self.getAvatar(entity, ignore_cache=True, profile=profile) 1254 self.contact_lists[profile].setCache(entity, "avatar", value)
1255 self.callListeners("avatar", entity, value, profile=profile)
1273 1256
1274 def actionManager(self, action_data, callback=None, ui_show_cb=None, user_action=True, 1257 def actionManager(self, action_data, callback=None, ui_show_cb=None, user_action=True,
1275 progress_cb=None, progress_eb=None, profile=C.PROF_KEY_NONE): 1258 progress_cb=None, progress_eb=None, profile=C.PROF_KEY_NONE):
1276 """Handle backend action 1259 """Handle backend action
1277 1260
1392 profile, 1375 profile,
1393 callback=action_cb, 1376 callback=action_cb,
1394 errback=self.dialogFailure, 1377 errback=self.dialogFailure,
1395 ) 1378 )
1396 1379
1397 def _avatarGetCb(self, avatar_path, entity, contact_list, profile):
1398 path = avatar_path or self.getDefaultAvatar(entity)
1399 contact_list.setCache(entity, "avatar", path)
1400 self.callListeners("avatar", entity, path, profile=profile)
1401
1402 def _avatarGetEb(self, failure_, entity, contact_list):
1403 # FIXME: bridge needs a proper error handling
1404 if "NotFound" in str(failure_):
1405 log.info("No avatar found for {entity}".format(entity=entity))
1406 else:
1407 log.warning("Can't get avatar: {}".format(failure_))
1408 contact_list.setCache(entity, "avatar", self.getDefaultAvatar(entity))
1409
1410 def getAvatar(
1411 self,
1412 entity,
1413 cache_only=True,
1414 hash_only=False,
1415 ignore_cache=False,
1416 profile=C.PROF_KEY_NONE,
1417 ):
1418 """return avatar path for an entity
1419
1420 @param entity(jid.JID): entity to get avatar from
1421 @param cache_only(bool): if False avatar will be requested if not in cache
1422 with current vCard based implementation, it's better to keep True
1423 except if we request avatars for roster items
1424 @param hash_only(bool): if True avatar hash is returned, else full path
1425 @param ignore_cache(bool): if False, won't check local cache and will request
1426 backend in every case
1427 @return (unicode, None): avatar full path (None if no avatar found)
1428 """
1429 contact_list = self.contact_lists[profile]
1430 if ignore_cache:
1431 avatar = None
1432 else:
1433 try:
1434 avatar = contact_list.getCache(entity, "avatar", bare_default=None)
1435 except exceptions.NotFound:
1436 avatar = None
1437 if avatar is None:
1438 self.bridge.avatarGet(
1439 str(entity),
1440 cache_only,
1441 hash_only,
1442 profile=profile,
1443 callback=lambda path: self._avatarGetCb(
1444 path, entity, contact_list, profile
1445 ),
1446 errback=lambda failure: self._avatarGetEb(failure, entity, contact_list),
1447 )
1448 # we set avatar to empty string to avoid requesting several time the same
1449 # avatar while we are waiting for avatarGet result
1450 contact_list.setCache(entity, "avatar", "")
1451 return avatar
1452
1453 def getDefaultAvatar(self, entity=None):
1454 """return default avatar to use with given entity
1455
1456 must be implemented by frontend
1457 @param entity(jid.JID): entity for which a default avatar is needed
1458 """
1459 raise NotImplementedError
1460
1461 def disconnect(self, profile): 1380 def disconnect(self, profile):
1462 log.info("disconnecting") 1381 log.info("disconnecting")
1463 self.callListeners("disconnect", profile=profile) 1382 self.callListeners("disconnect", profile=profile)
1464 self.bridge.disconnect(profile) 1383 self.bridge.disconnect(profile)
1465 1384