Mercurial > libervia-backend
comparison sat/plugins/plugin_misc_identity.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 | 559a625a236b |
children | 704dada41df0 |
comparison
equal
deleted
inserted
replaced
3253:1af840e84af7 | 3254:6cf4bd6972c2 |
---|---|
1 #!/usr/bin/env python3 | 1 #!/usr/bin/env python3 |
2 | 2 |
3 | |
4 # SAT plugin for managing xep-0054 | |
5 # Copyright (C) 2009-2020 Jérôme Poisson (goffi@goffi.org) | 3 # Copyright (C) 2009-2020 Jérôme Poisson (goffi@goffi.org) |
6 # Copyright (C) 2014 Emmanuel Gil Peyrot (linkmauve@linkmauve.fr) | |
7 | 4 |
8 # This program is free software: you can redistribute it and/or modify | 5 # This program is free software: you can redistribute it and/or modify |
9 # it under the terms of the GNU Affero General Public License as published by | 6 # it under the terms of the GNU Affero General Public License as published by |
10 # the Free Software Foundation, either version 3 of the License, or | 7 # the Free Software Foundation, either version 3 of the License, or |
11 # (at your option) any later version. | 8 # (at your option) any later version. |
16 # GNU Affero General Public License for more details. | 13 # GNU Affero General Public License for more details. |
17 | 14 |
18 # 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 |
19 # 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/>. |
20 | 17 |
18 from collections import namedtuple | |
19 from pathlib import Path | |
20 from twisted.internet import defer | |
21 from twisted.words.protocols.jabber import jid | |
21 from sat.core.i18n import _ | 22 from sat.core.i18n import _ |
22 from sat.core.constants import Const as C | 23 from sat.core.constants import Const as C |
23 from sat.core import exceptions | 24 from sat.core import exceptions |
24 from sat.core.log import getLogger | 25 from sat.core.log import getLogger |
26 from sat.memory import persistent | |
27 from sat.tools import image | |
28 from sat.tools import utils | |
29 from sat.tools.common import data_format | |
30 | |
25 | 31 |
26 log = getLogger(__name__) | 32 log = getLogger(__name__) |
27 from twisted.internet import defer | |
28 from twisted.words.protocols.jabber import jid | |
29 import os.path | |
30 | 33 |
31 | 34 |
32 PLUGIN_INFO = { | 35 PLUGIN_INFO = { |
33 C.PI_NAME: "Identity Plugin", | 36 C.PI_NAME: "Identity Plugin", |
34 C.PI_IMPORT_NAME: "IDENTITY", | 37 C.PI_IMPORT_NAME: "IDENTITY", |
35 C.PI_TYPE: C.PLUG_TYPE_MISC, | 38 C.PI_TYPE: C.PLUG_TYPE_MISC, |
36 C.PI_PROTOCOLS: [], | 39 C.PI_PROTOCOLS: [], |
37 C.PI_DEPENDENCIES: ["XEP-0054"], | 40 C.PI_DEPENDENCIES: [], |
38 C.PI_RECOMMENDATIONS: [], | 41 C.PI_RECOMMENDATIONS: ["XEP-0045"], |
39 C.PI_MAIN: "Identity", | 42 C.PI_MAIN: "Identity", |
40 C.PI_HANDLER: "no", | 43 C.PI_HANDLER: "no", |
41 C.PI_DESCRIPTION: _("""Identity manager"""), | 44 C.PI_DESCRIPTION: _("""Identity manager"""), |
42 } | 45 } |
43 | 46 |
44 | 47 Callback = namedtuple("Callback", ("get", "set", "priority")) |
45 class Identity(object): | 48 |
49 | |
50 class Identity: | |
51 | |
46 def __init__(self, host): | 52 def __init__(self, host): |
47 log.info(_("Plugin Identity initialization")) | 53 log.info(_("Plugin Identity initialization")) |
48 self.host = host | 54 self.host = host |
49 self._v = host.plugins["XEP-0054"] | 55 self._m = host.plugins.get("XEP-0045") |
56 self.metadata = { | |
57 "avatar": { | |
58 "type": dict, | |
59 # convert avatar path to avatar metadata (and check validity) | |
60 "set_data_filter": self.avatarSetDataFilter, | |
61 # update profile avatar, so all frontends are aware | |
62 "set_post_treatment": self.avatarSetPostTreatment, | |
63 "update_is_new_data": self.avatarUpdateIsNewData, | |
64 "update_data_filter": self.avatarUpdateDataFilter, | |
65 # we store the metadata in database, to restore it on next connection | |
66 # (it is stored only for roster entities) | |
67 "store": True, | |
68 }, | |
69 "nicknames": { | |
70 "type": list, | |
71 # accumulate all nicknames from all callbacks in a list instead | |
72 # of returning only the data from the first successful callback | |
73 "get_all": True, | |
74 # append nicknames from roster, resource, etc. | |
75 "get_post_treatment": self.nicknamesGetPostTreatment, | |
76 "update_is_new_data": self.nicknamesUpdateIsNewData, | |
77 "store": True, | |
78 }, | |
79 } | |
80 host.trigger.add("roster_update", self._rosterUpdateTrigger) | |
81 host.memory.setSignalOnUpdate("avatar") | |
82 host.memory.setSignalOnUpdate("nicknames") | |
50 host.bridge.addMethod( | 83 host.bridge.addMethod( |
51 "identityGet", | 84 "identityGet", |
52 ".plugin", | 85 ".plugin", |
53 in_sign="ss", | 86 in_sign="sasbs", |
54 out_sign="a{ss}", | 87 out_sign="s", |
55 method=self._getIdentity, | 88 method=self._getIdentity, |
56 async_=True, | 89 async_=True, |
57 ) | 90 ) |
58 host.bridge.addMethod( | 91 host.bridge.addMethod( |
59 "identitySet", | 92 "identitySet", |
60 ".plugin", | 93 ".plugin", |
61 in_sign="a{ss}s", | 94 in_sign="ss", |
62 out_sign="", | 95 out_sign="", |
63 method=self._setIdentity, | 96 method=self._setIdentity, |
64 async_=True, | 97 async_=True, |
65 ) | 98 ) |
66 | 99 host.bridge.addMethod( |
67 def _getIdentity(self, jid_str, profile): | 100 "avatarGet", |
68 jid_ = jid.JID(jid_str) | 101 ".plugin", |
69 client = self.host.getClient(profile) | 102 in_sign="sbs", |
70 return self.getIdentity(client, jid_) | 103 out_sign="s", |
71 | 104 method=self._getAvatar, |
72 @defer.inlineCallbacks | 105 async_=True, |
73 def getIdentity(self, client, jid_): | 106 ) |
74 """Retrieve identity of an entity | 107 host.bridge.addMethod( |
75 | 108 "avatarSet", |
76 @param jid_(jid.JID): entity to check | 109 ".plugin", |
77 @return (dict(unicode, unicode)): identity data where key can be: | 110 in_sign="sss", |
78 - nick: nickname of the entity | 111 out_sign="", |
79 nickname is checked from, in this order: | 112 method=self._setAvatar, |
80 roster, vCard, user part of jid | 113 async_=True, |
81 cache is used when possible | 114 ) |
82 """ | 115 |
83 id_data = {} | 116 async def profileConnecting(self, client): |
84 # we first check roster | 117 # we restore known identities from database |
85 roster_item = yield client.roster.getItem(jid_.userhostJID()) | 118 client._identity_storage = persistent.LazyPersistentBinaryDict( |
86 if roster_item is not None and roster_item.name: | 119 "identity", client.profile) |
87 id_data["nick"] = roster_item.name | 120 |
88 elif jid_.resource and self._v.isRoom(client, jid_): | 121 stored_data = await client._identity_storage.all() |
89 id_data["nick"] = jid_.resource | 122 |
123 self.host.memory.storage.getPrivates( | |
124 namespace="identity", binary=True, profile=client.profile) | |
125 | |
126 to_delete = [] | |
127 | |
128 for key, value in stored_data.items(): | |
129 entity_s, name = key.split('\n') | |
130 if name not in self.metadata.keys(): | |
131 log.debug(f"removing {key} from storage: not an allowed metadata name") | |
132 to_delete.append(key) | |
133 continue | |
134 entity = jid.JID(entity_s) | |
135 | |
136 if name == 'avatar': | |
137 if value is not None: | |
138 try: | |
139 cache_uid = value['cache_uid'] | |
140 if not cache_uid: | |
141 raise ValueError | |
142 except (ValueError, KeyError): | |
143 log.warning( | |
144 f"invalid data for {entity} avatar, it will be deleted: " | |
145 f"{value}") | |
146 to_delete.append(key) | |
147 continue | |
148 cache = self.host.common_cache.getMetadata(cache_uid) | |
149 if cache is None: | |
150 log.debug( | |
151 f"purging avatar for {entity}: it is not in cache anymore") | |
152 to_delete.append(key) | |
153 continue | |
154 | |
155 self.host.memory.updateEntityData( | |
156 client, entity, name, value, silent=True | |
157 ) | |
158 | |
159 for key in to_delete: | |
160 await client._identity_storage.adel(key) | |
161 | |
162 def _rosterUpdateTrigger(self, client, roster_item): | |
163 old_item = client.roster.getItem(roster_item.jid) | |
164 if old_item is None or old_item.name != roster_item.name: | |
165 log.debug( | |
166 f"roster nickname has been updated to {roster_item.name!r} for " | |
167 f"{roster_item.jid}" | |
168 ) | |
169 defer.ensureDeferred( | |
170 self.update( | |
171 client, | |
172 "nicknames", | |
173 [roster_item.name], | |
174 roster_item.jid | |
175 ) | |
176 ) | |
177 return True | |
178 | |
179 def register(self, metadata_name, cb_get, cb_set, priority=0): | |
180 """Register callbacks to handle identity metadata | |
181 | |
182 @param metadata_name(str): name of metadata can be: | |
183 - avatar | |
184 - nicknames | |
185 @param cb_get(coroutine, Deferred): method to retrieve a metadata | |
186 the method will get client and metadata names to retrieve as arguments. | |
187 @param cb_set(coroutine, Deferred): method to set a metadata | |
188 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. | |
190 methods with bigger priorities will be called first | |
191 """ | |
192 if not metadata_name in self.metadata.keys(): | |
193 raise ValueError(f"Invalid metadata_name: {metadata_name!r}") | |
194 callback = Callback(get=cb_get, set=cb_set, priority=priority) | |
195 cb_list = self.metadata[metadata_name].setdefault('callbacks', []) | |
196 cb_list.append(callback) | |
197 cb_list.sort(key=lambda c: c.priority, reverse=True) | |
198 | |
199 def getIdentityJid(self, client, peer_jid): | |
200 """Return jid to use to set identity metadata | |
201 | |
202 if it's a jid of a room occupant, full jid will be used | |
203 otherwise bare jid will be used | |
204 if None, bare jid of profile will be used | |
205 @return (jid.JID): jid to use for avatar | |
206 """ | |
207 if peer_jid is None: | |
208 return client.jid.userhostJID() | |
209 if self._m is None: | |
210 return peer_jid.userhostJID() | |
90 else: | 211 else: |
91 # and finally then vcard | 212 return self._m.getBareOrFull(client, peer_jid) |
92 nick = yield self._v.getNick(client, jid_) | 213 |
93 if nick: | 214 def checkType(self, metadata_name, value): |
94 id_data["nick"] = nick | 215 """Check that type used for a metadata is the one declared in self.metadata""" |
95 elif jid_.user: | 216 value_type = self.metadata[metadata_name]["type"] |
96 id_data["nick"] = jid_.user.capitalize() | 217 if not isinstance(value, value_type): |
218 raise ValueError( | |
219 f"{value} has wrong type: it is {type(value)} while {value_type} was " | |
220 f"expected") | |
221 | |
222 async def get(self, client, metadata_name, entity, use_cache=True): | |
223 """Retrieve identity metadata of an entity | |
224 | |
225 if metadata is already in cache, it is returned. Otherwise, registered callbacks | |
226 will be tried in priority order (bigger to lower) | |
227 @param metadata_name(str): name of the metadata | |
228 must be one of self.metadata key | |
229 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 | |
231 None to use profile's jid | |
232 @param use_cache(bool): if False, cache won't be checked | |
233 """ | |
234 entity = self.getIdentityJid(client, entity) | |
235 try: | |
236 metadata = self.metadata[metadata_name] | |
237 except KeyError: | |
238 raise ValueError(f"Invalid metadata name: {metadata_name!r}") | |
239 get_all = metadata.get('get_all', False) | |
240 if use_cache: | |
241 try: | |
242 data = self.host.memory.getEntityDatum( | |
243 client, entity, metadata_name) | |
244 except (KeyError, exceptions.UnknownEntityError): | |
245 pass | |
97 else: | 246 else: |
98 id_data["nick"] = jid_.userhost() | 247 return data |
99 | 248 |
100 try: | 249 try: |
101 avatar_path = id_data["avatar"] = yield self._v.getAvatar( | 250 callbacks = metadata['callbacks'] |
102 client, jid_, cache_only=False | 251 except KeyError: |
103 ) | 252 log.warning(_("No callback registered for {metadata_name}") |
104 except exceptions.NotFound: | 253 .format(metadata_name=metadata_name)) |
254 return [] if get_all else None | |
255 | |
256 if get_all: | |
257 all_data = [] | |
258 | |
259 for callback in callbacks: | |
260 try: | |
261 data = await defer.ensureDeferred(callback.get(client, entity)) | |
262 except exceptions.CancelError: | |
263 continue | |
264 except Exception as e: | |
265 log.warning( | |
266 _("Error while trying to get {metadata_name} with {callback}: {e}") | |
267 .format(callback=callback.get, metadata_name=metadata_name, e=e)) | |
268 else: | |
269 if data: | |
270 self.checkType(metadata_name, data) | |
271 if get_all: | |
272 all_data.extend(data) | |
273 else: | |
274 break | |
275 else: | |
276 data = None | |
277 | |
278 if get_all: | |
279 data = all_data | |
280 | |
281 post_treatment = metadata.get("get_post_treatment") | |
282 if post_treatment is not None: | |
283 data = await utils.asDeferred(post_treatment, client, entity, data) | |
284 | |
285 self.host.memory.updateEntityData( | |
286 client, entity, metadata_name, data) | |
287 | |
288 if metadata.get('store', False): | |
289 key = f"{entity}\n{metadata_name}" | |
290 await client._identity_storage.aset(key, data) | |
291 | |
292 return data | |
293 | |
294 async def set(self, client, metadata_name, data, entity=None): | |
295 """Set identity metadata for an entity | |
296 | |
297 Registered callbacks will be tried in priority order (bigger to lower) | |
298 @param metadata_name(str): name of the metadata | |
299 must be one of self.metadata key | |
300 the name will also be used to set entity data in host.memory | |
301 @param data(object): value to set | |
302 @param entity(jid.JID, None): entity for which avatar is requested | |
303 None to use profile's jid | |
304 """ | |
305 entity = self.getIdentityJid(client, entity) | |
306 metadata = self.metadata[metadata_name] | |
307 data_filter = metadata.get("set_data_filter") | |
308 if data_filter is not None: | |
309 data = await utils.asDeferred(data_filter, client, entity, data) | |
310 self.checkType(metadata_name, data) | |
311 | |
312 try: | |
313 callbacks = metadata['callbacks'] | |
314 except KeyError: | |
315 log.warning(_("No callback registered for {metadata_name}") | |
316 .format(metadata_name=metadata_name)) | |
317 return exceptions.FeatureNotFound(f"Can't set {metadata_name} for {entity}") | |
318 | |
319 for callback in callbacks: | |
320 try: | |
321 await defer.ensureDeferred(callback.set(client, data, entity)) | |
322 except exceptions.CancelError: | |
323 continue | |
324 except Exception as e: | |
325 log.warning( | |
326 _("Error while trying to set {metadata_name} with {callback}: {e}") | |
327 .format(callback=callback.set, metadata_name=metadata_name, e=e)) | |
328 else: | |
329 break | |
330 else: | |
331 raise exceptions.FeatureNotFound(f"Can't set {metadata_name} for {entity}") | |
332 | |
333 post_treatment = metadata.get("set_post_treatment") | |
334 if post_treatment is not None: | |
335 await utils.asDeferred(post_treatment, client, entity, data) | |
336 | |
337 async def update(self, client, metadata_name, data, entity): | |
338 """Update a metadata in cache | |
339 | |
340 This method may be called by plugins when an identity metadata is available. | |
341 """ | |
342 entity = self.getIdentityJid(client, entity) | |
343 metadata = self.metadata[metadata_name] | |
344 | |
345 try: | |
346 cached_data = self.host.memory.getEntityDatum( | |
347 client, entity, metadata_name) | |
348 except (KeyError, exceptions.UnknownEntityError): | |
349 # metadata is not cached, we do the update | |
105 pass | 350 pass |
106 else: | 351 else: |
107 if avatar_path: | 352 # metadata is cached, we check if the new value differs from the cached one |
108 id_data["avatar_basename"] = os.path.basename(avatar_path) | 353 try: |
354 update_is_new_data = metadata["update_is_new_data"] | |
355 except KeyError: | |
356 update_is_new_data = self.defaultUpdateIsNewData | |
357 | |
358 if not update_is_new_data(client, entity, cached_data, data): | |
359 if cached_data is None: | |
360 log.debug( | |
361 f"{metadata_name} for {entity} is already disabled, nothing to " | |
362 f"do") | |
363 else: | |
364 log.debug( | |
365 f"{metadata_name} for {entity} is already in cache, nothing to " | |
366 f"do") | |
367 return | |
368 | |
369 # we can't use the cache, so we do the update | |
370 | |
371 log.debug(f"updating {metadata_name} for {entity}") | |
372 | |
373 if metadata.get('get_all', False): | |
374 # get_all is set, meaning that we have to check all plugins | |
375 # so we first delete current cache | |
376 self.host.memory.delEntityDatum(client, entity, metadata_name) | |
377 # then fill it again by calling get, which will retrieve all values | |
378 await self.get(client, metadata_name, entity) | |
379 return | |
380 | |
381 if data is not None: | |
382 data_filter = metadata['update_data_filter'] | |
383 if data_filter is not None: | |
384 data = await utils.asDeferred(data_filter, client, entity, data) | |
385 self.checkType(metadata_name, data) | |
386 | |
387 self.host.memory.updateEntityData(client, entity, metadata_name, data) | |
388 | |
389 if metadata.get('store', False): | |
390 key = f"{entity}\n{metadata_name}" | |
391 await client._identity_storage.aset(key, data) | |
392 | |
393 def defaultUpdateIsNewData(self, client, entity, cached_data, new_data): | |
394 return new_data != cached_data | |
395 | |
396 def _getAvatar(self, entity, use_cache, profile): | |
397 client = self.host.getClient(profile) | |
398 entity = jid.JID(entity) if entity else None | |
399 d = defer.ensureDeferred(self.get(client, "avatar", entity, use_cache)) | |
400 d.addCallback(lambda data: data_format.serialise(data)) | |
401 return d | |
402 | |
403 def _setAvatar(self, file_path, entity, profile_key=C.PROF_KEY_NONE): | |
404 client = self.host.getClient(profile_key) | |
405 entity = jid.JID(entity) if entity else None | |
406 return defer.ensureDeferred( | |
407 self.set(client, "avatar", file_path, entity)) | |
408 | |
409 async def avatarSetDataFilter(self, client, entity, file_path): | |
410 """Convert avatar file path to dict data""" | |
411 file_path = Path(file_path) | |
412 if not file_path.is_file(): | |
413 raise ValueError(f"There is no file at {file_path} to use as avatar") | |
414 avatar_data = { | |
415 'path': file_path, | |
416 'media_type': image.guess_type(file_path), | |
417 } | |
418 media_type = avatar_data['media_type'] | |
419 if media_type is None: | |
420 raise ValueError(f"Can't identify type of image at {file_path}") | |
421 if not media_type.startswith('image/'): | |
422 raise ValueError(f"File at {file_path} doesn't appear to be an image") | |
423 return avatar_data | |
424 | |
425 async def avatarSetPostTreatment(self, client, entity, avatar_data): | |
426 """Update our own avatar""" | |
427 await self.update(client, "avatar", avatar_data, entity) | |
428 | |
429 def avatarBuildMetadata(self, path, media_type=None, cache_uid=None): | |
430 """Helper method to generate avatar metadata | |
431 | |
432 @param path(str, Path, None): path to avatar file | |
433 avatar file must be in cache | |
434 None if avatar is explicitely not set | |
435 @param media_type(str, None): type of the avatar file (MIME type) | |
436 @param cache_uid(str, None): UID of avatar in cache | |
437 @return (dict, None): avatar metadata | |
438 None if avatar is not set | |
439 """ | |
440 if path is None: | |
441 return None | |
442 else: | |
443 if cache_uid is None: | |
444 raise ValueError("cache_uid must be set if path is set") | |
445 path = Path(path) | |
446 if media_type is None: | |
447 media_type = image.guess_type(path) | |
448 | |
449 return { | |
450 "path": path, | |
451 "media_type": media_type, | |
452 "cache_uid": cache_uid, | |
453 } | |
454 | |
455 def avatarUpdateIsNewData(self, client, entity, cached_data, file_path): | |
456 if cached_data is None: | |
457 return file_path is not None | |
458 | |
459 if file_path is not None and file_path == cached_data['path']: | |
460 if file_path is None: | |
461 log.debug( | |
462 f"Avatar is already disabled for {entity}, nothing to do") | |
109 else: | 463 else: |
110 del id_data["avatar"] | 464 log.debug( |
111 | 465 f"Avatar at {file_path} is already used by {entity}, nothing " |
112 defer.returnValue(id_data) | 466 f"to do") |
113 | 467 return |
114 def _setIdentity(self, id_data, profile): | 468 |
469 async def avatarUpdateDataFilter(self, client, entity, data): | |
470 if not isinstance(data, dict): | |
471 raise ValueError(f"Invalid data type ({type(data)}), a dict is expected") | |
472 mandatory_keys = {'path', 'cache_uid'} | |
473 if not data.keys() >= mandatory_keys: | |
474 raise ValueError(f"missing avatar data keys: {mandatory_keys - data.keys()}") | |
475 return data | |
476 | |
477 async def nicknamesGetPostTreatment(self, client, entity, plugin_nicknames): | |
478 """Prepend nicknames from core locations + set default nickname | |
479 | |
480 nicknames are checked from many locations, there is always at least | |
481 one nickname. First nickname of the list can be used in priority. | |
482 Nicknames are appended in this order: | |
483 - roster, plugins set nicknames | |
484 - if no nickname is found, user part of jid is then used, or bare jid | |
485 if there is no user part. | |
486 For MUC, room nick is always put first | |
487 """ | |
488 # we first check roster | |
489 nicknames = [] | |
490 if entity.resource: | |
491 # getIdentityJid let the resource only if the entity is a MUC room | |
492 # occupant jid | |
493 nicknames.append(entity.resource) | |
494 | |
495 roster_item = client.roster.getItem(entity.userhostJID()) | |
496 if roster_item is not None and roster_item.name: | |
497 # user set name has priority over entity set name | |
498 nicknames.append(roster_item.name) | |
499 | |
500 nicknames.extend(plugin_nicknames) | |
501 | |
502 if not nicknames: | |
503 if entity.user: | |
504 nicknames.append(entity.user.capitalize()) | |
505 else: | |
506 nicknames.append(entity.userhost()) | |
507 | |
508 # we remove duplicates while preserving order with dict | |
509 return list(dict.fromkeys(nicknames)) | |
510 | |
511 def nicknamesUpdateIsNewData(self, client, entity, cached_data, new_nicknames): | |
512 return not set(new_nicknames).issubset(cached_data) | |
513 | |
514 def _getIdentity(self, entity_s, metadata_filter, use_cache, profile): | |
515 entity = jid.JID(entity_s) | |
115 client = self.host.getClient(profile) | 516 client = self.host.getClient(profile) |
116 return self.setIdentity(client, id_data) | 517 d = defer.ensureDeferred( |
117 | 518 self.getIdentity(client, entity, metadata_filter, use_cache)) |
118 def setIdentity(self, client, id_data): | 519 d.addCallback(lambda data: data_format.serialise(data)) |
520 return d | |
521 | |
522 async def getIdentity( | |
523 self, client, entity=None, metadata_filter=None, use_cache=True): | |
524 """Retrieve identity of an entity | |
525 | |
526 @param entity(jid.JID, None): entity to check | |
527 @param metadata_filter(list[str], None): if not None or empty, only return | |
528 metadata in this filter | |
529 @param use_cache(bool): if False, cache won't be checked | |
530 should be True most of time, to avoid useless network requests | |
531 @return (dict): identity data | |
532 """ | |
533 id_data = {} | |
534 | |
535 if not metadata_filter: | |
536 metadata_names = self.metadata.keys() | |
537 else: | |
538 metadata_names = metadata_filter | |
539 | |
540 for metadata_name in metadata_names: | |
541 id_data[metadata_name] = await self.get( | |
542 client, metadata_name, entity, use_cache) | |
543 | |
544 return id_data | |
545 | |
546 def _setIdentity(self, id_data_s, profile): | |
547 client = self.host.getClient(profile) | |
548 id_data = data_format.deserialise(id_data_s) | |
549 return defer.ensureDeferred(self.setIdentity(client, id_data)) | |
550 | |
551 async def setIdentity(self, client, id_data): | |
119 """Update profile's identity | 552 """Update profile's identity |
120 | 553 |
121 @param id_data(dict[unicode, unicode]): data to update, key can be: | 554 @param id_data(dict): data to update, key can be on of self.metadata keys |
122 - nick: nickname | 555 """ |
123 the vCard will be updated | 556 if not id_data.keys() <= self.metadata.keys(): |
124 """ | 557 raise ValueError( |
125 if list(id_data.keys()) != ["nick"]: | 558 f"Invalid metadata names: {id_data.keys() - self.metadata.keys()}") |
126 raise NotImplementedError("Only nick can be updated for now") | 559 for metadata_name, data in id_data.items(): |
127 if "nick" in id_data: | 560 try: |
128 return self._v.setNick(client, id_data["nick"]) | 561 await self.set(client, metadata_name, data) |
562 except Exception as e: | |
563 log.warning( | |
564 _("Can't set metadata {metadata_name!r}: {reason}") | |
565 .format(metadata_name=metadata_name, reason=e)) |