Mercurial > libervia-backend
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 |