Mercurial > libervia-backend
comparison sat/plugins/plugin_xep_0054.py @ 2562:26edcf3a30eb
core, setup: huge cleaning:
- moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention
- move twisted directory to root
- removed all hacks from setup.py, and added missing dependencies, it is now clean
- use https URL for website in setup.py
- removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed
- renamed sat.sh to sat and fixed its installation
- added python_requires to specify Python version needed
- replaced glib2reactor which use deprecated code by gtk3reactor
sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 02 Apr 2018 19:44:50 +0200 |
parents | src/plugins/plugin_xep_0054.py@4001aa395a04 |
children | 395a3d1c2888 |
comparison
equal
deleted
inserted
replaced
2561:bd30dc3ffe5a | 2562:26edcf3a30eb |
---|---|
1 #!/usr/bin/env python2 | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # SAT plugin for managing xep-0054 | |
5 # Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) | |
6 # Copyright (C) 2014 Emmanuel Gil Peyrot (linkmauve@linkmauve.fr) | |
7 | |
8 # 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 | |
10 # the Free Software Foundation, either version 3 of the License, or | |
11 # (at your option) any later version. | |
12 | |
13 # This program is distributed in the hope that it will be useful, | |
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
16 # GNU Affero General Public License for more details. | |
17 | |
18 # 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/>. | |
20 | |
21 from sat.core.i18n import _ | |
22 from sat.core.constants import Const as C | |
23 from sat.core.log import getLogger | |
24 log = getLogger(__name__) | |
25 from twisted.internet import threads, defer | |
26 from twisted.words.protocols.jabber import jid, error | |
27 from twisted.words.xish import domish | |
28 from twisted.python.failure import Failure | |
29 | |
30 from zope.interface import implements | |
31 | |
32 from wokkel import disco, iwokkel | |
33 | |
34 from base64 import b64decode, b64encode | |
35 from hashlib import sha1 | |
36 from sat.core import exceptions | |
37 from sat.memory import persistent | |
38 import mimetypes | |
39 try: | |
40 from PIL import Image | |
41 except: | |
42 raise exceptions.MissingModule(u"Missing module pillow, please download/install it from https://python-pillow.github.io") | |
43 from cStringIO import StringIO | |
44 | |
45 try: | |
46 from twisted.words.protocols.xmlstream import XMPPHandler | |
47 except ImportError: | |
48 from wokkel.subprotocols import XMPPHandler | |
49 | |
50 AVATAR_PATH = "avatars" | |
51 AVATAR_DIM = (64, 64) # FIXME: dim are not adapted to modern resolutions ! | |
52 | |
53 IQ_GET = '/iq[@type="get"]' | |
54 NS_VCARD = 'vcard-temp' | |
55 VCARD_REQUEST = IQ_GET + '/vCard[@xmlns="' + NS_VCARD + '"]' # TODO: manage requests | |
56 | |
57 PRESENCE = '/presence' | |
58 NS_VCARD_UPDATE = 'vcard-temp:x:update' | |
59 VCARD_UPDATE = PRESENCE + '/x[@xmlns="' + NS_VCARD_UPDATE + '"]' | |
60 | |
61 CACHED_DATA = {'avatar', 'nick'} | |
62 MAX_AGE = 60 * 60 * 24 * 365 | |
63 | |
64 PLUGIN_INFO = { | |
65 C.PI_NAME: "XEP 0054 Plugin", | |
66 C.PI_IMPORT_NAME: "XEP-0054", | |
67 C.PI_TYPE: "XEP", | |
68 C.PI_PROTOCOLS: ["XEP-0054", "XEP-0153"], | |
69 C.PI_DEPENDENCIES: [], | |
70 C.PI_RECOMMENDATIONS: ["XEP-0045"], | |
71 C.PI_MAIN: "XEP_0054", | |
72 C.PI_HANDLER: "yes", | |
73 C.PI_DESCRIPTION: _("""Implementation of vcard-temp""") | |
74 } | |
75 | |
76 | |
77 class XEP_0054(object): | |
78 #TODO: - check that nickname is ok | |
79 # - refactor the code/better use of Wokkel | |
80 # - get missing values | |
81 | |
82 def __init__(self, host): | |
83 log.info(_(u"Plugin XEP_0054 initialization")) | |
84 self.host = host | |
85 host.bridge.addMethod(u"avatarGet", u".plugin", in_sign=u'sbbs', out_sign=u's', method=self._getAvatar, async=True) | |
86 host.bridge.addMethod(u"avatarSet", u".plugin", in_sign=u'ss', out_sign=u'', method=self._setAvatar, async=True) | |
87 host.trigger.add(u"presence_available", self.presenceAvailableTrigger) | |
88 host.memory.setSignalOnUpdate(u"avatar") | |
89 host.memory.setSignalOnUpdate(u"nick") | |
90 | |
91 def getHandler(self, client): | |
92 return XEP_0054_handler(self) | |
93 | |
94 def isRoom(self, client, entity_jid): | |
95 """Tell if a jid is a MUC one | |
96 | |
97 @param entity_jid(jid.JID): full or bare jid of the entity check | |
98 @return (bool): True if the bare jid of the entity is a room jid | |
99 """ | |
100 try: | |
101 muc_plg = self.host.plugins['XEP-0045'] | |
102 except KeyError: | |
103 return False | |
104 | |
105 try: | |
106 muc_plg.checkRoomJoined(client, entity_jid.userhostJID()) | |
107 except exceptions.NotFound: | |
108 return False | |
109 else: | |
110 return True | |
111 | |
112 def getBareOrFull(self, client, jid_): | |
113 """use full jid if jid_ is an occupant of a room, bare jid else | |
114 | |
115 @param jid_(jid.JID): entity to test | |
116 @return (jid.JID): bare or full jid | |
117 """ | |
118 if jid_.resource: | |
119 if not self.isRoom(client, jid_): | |
120 return jid_.userhostJID() | |
121 return jid_ | |
122 | |
123 def presenceAvailableTrigger(self, presence_elt, client): | |
124 if client.jid.userhost() in client._cache_0054: | |
125 try: | |
126 avatar_hash = client._cache_0054[client.jid.userhost()]['avatar'] | |
127 except KeyError: | |
128 log.info(u"No avatar in cache for {}".format(client.jid.userhost())) | |
129 return True | |
130 x_elt = domish.Element((NS_VCARD_UPDATE, 'x')) | |
131 x_elt.addElement('photo', content=avatar_hash) | |
132 presence_elt.addChild(x_elt) | |
133 return True | |
134 | |
135 @defer.inlineCallbacks | |
136 def profileConnecting(self, client): | |
137 client._cache_0054 = persistent.PersistentBinaryDict(NS_VCARD, client.profile) | |
138 yield client._cache_0054.load() | |
139 self._fillCachedValues(client.profile) | |
140 | |
141 def _fillCachedValues(self, profile): | |
142 #FIXME: this may need to be reworked | |
143 # the current naive approach keeps a map between all jids | |
144 # in persistent cache, then put avatar hashs in memory. | |
145 # Hashes should be shared between profiles (or not ? what | |
146 # if the avatar is different depending on who is requesting it | |
147 # this is not possible with vcard-tmp, but it is with XEP-0084). | |
148 # Loading avatar on demand per jid may be a option to investigate. | |
149 client = self.host.getClient(profile) | |
150 for jid_s, data in client._cache_0054.iteritems(): | |
151 jid_ = jid.JID(jid_s) | |
152 for name in CACHED_DATA: | |
153 try: | |
154 value = data[name] | |
155 if value is None: | |
156 log.error(u"{name} value for {jid_} is None, ignoring".format(name=name, jid_=jid_)) | |
157 continue | |
158 self.host.memory.updateEntityData(jid_, name, data[name], silent=True, profile_key=profile) | |
159 except KeyError: | |
160 pass | |
161 | |
162 def updateCache(self, client, jid_, name, value): | |
163 """update cache value | |
164 | |
165 save value in memory in case of change | |
166 @param jid_(jid.JID): jid of the owner of the vcard | |
167 @param name(str): name of the item which changed | |
168 @param value(unicode, None): new value of the item | |
169 None to delete | |
170 """ | |
171 jid_ = self.getBareOrFull(client, jid_) | |
172 jid_s = jid_.full() | |
173 | |
174 if value is None: | |
175 try: | |
176 self.host.memory.delEntityDatum(jid_, name, client.profile) | |
177 except (KeyError, exceptions.UnknownEntityError): | |
178 pass | |
179 if name in CACHED_DATA: | |
180 try: | |
181 del client._cache_0054[jid_s][name] | |
182 except KeyError: | |
183 pass | |
184 else: | |
185 client._cache_0054.force(jid_s) | |
186 else: | |
187 self.host.memory.updateEntityData(jid_, name, value, profile_key=client.profile) | |
188 if name in CACHED_DATA: | |
189 client._cache_0054.setdefault(jid_s, {})[name] = value | |
190 client._cache_0054.force(jid_s) | |
191 | |
192 def getCache(self, client, entity_jid, name): | |
193 """return cached value for jid | |
194 | |
195 @param entity_jid(jid.JID): target contact | |
196 @param name(unicode): name of the value ('nick' or 'avatar') | |
197 @return(unicode, None): wanted value or None""" | |
198 entity_jid = self.getBareOrFull(client, entity_jid) | |
199 try: | |
200 data = self.host.memory.getEntityData(entity_jid, [name], client.profile) | |
201 except exceptions.UnknownEntityError: | |
202 return None | |
203 return data.get(name) | |
204 | |
205 def savePhoto(self, client, photo_elt, entity_jid): | |
206 """Parse a <PHOTO> photo_elt and save the picture""" | |
207 # XXX: this method is launched in a separate thread | |
208 try: | |
209 mime_type = unicode(photo_elt.elements(NS_VCARD, 'TYPE').next()) | |
210 except StopIteration: | |
211 log.warning(u"no MIME type found, assuming image/png") | |
212 mime_type = u"image/png" | |
213 else: | |
214 if not mime_type: | |
215 log.warning(u"empty MIME type, assuming image/png") | |
216 mime_type = u"image/png" | |
217 elif mime_type not in ("image/gif", "image/jpeg", "image/png"): | |
218 if mime_type == "image/x-png": | |
219 # XXX: this old MIME type is still used by some clients | |
220 mime_type = "image/png" | |
221 else: | |
222 # TODO: handle other image formats (svg?) | |
223 log.warning(u"following avatar image format is not handled: {type} [{jid}]".format( | |
224 type=mime_type, jid=entity_jid.full())) | |
225 raise Failure(exceptions.DataError()) | |
226 | |
227 ext = mimetypes.guess_extension(mime_type, strict=False) | |
228 assert ext is not None | |
229 if ext == u'.jpe': | |
230 ext = u'.jpg' | |
231 log.debug(u'photo of type {type} with extension {ext} found [{jid}]'.format( | |
232 type=mime_type, ext=ext, jid=entity_jid.full())) | |
233 try: | |
234 buf = str(photo_elt.elements(NS_VCARD, 'BINVAL').next()) | |
235 except StopIteration: | |
236 log.warning(u"BINVAL element not found") | |
237 raise Failure(exceptions.NotFound()) | |
238 if not buf: | |
239 log.warning(u"empty avatar for {jid}".format(jid=entity_jid.full())) | |
240 raise Failure(exceptions.NotFound()) | |
241 log.debug(_(u'Decoding binary')) | |
242 decoded = b64decode(buf) | |
243 del buf | |
244 image_hash = sha1(decoded).hexdigest() | |
245 with client.cache.cacheData( | |
246 PLUGIN_INFO['import_name'], | |
247 image_hash, | |
248 mime_type, | |
249 # we keep in cache for 1 year | |
250 MAX_AGE | |
251 ) as f: | |
252 f.write(decoded) | |
253 return image_hash | |
254 | |
255 @defer.inlineCallbacks | |
256 def vCard2Dict(self, client, vcard, entity_jid): | |
257 """Convert a VCard to a dict, and save binaries""" | |
258 log.debug((u"parsing vcard")) | |
259 vcard_dict = {} | |
260 | |
261 for elem in vcard.elements(): | |
262 if elem.name == 'FN': | |
263 vcard_dict['fullname'] = unicode(elem) | |
264 elif elem.name == 'NICKNAME': | |
265 vcard_dict['nick'] = unicode(elem) | |
266 self.updateCache(client, entity_jid, 'nick', vcard_dict['nick']) | |
267 elif elem.name == 'URL': | |
268 vcard_dict['website'] = unicode(elem) | |
269 elif elem.name == 'EMAIL': | |
270 vcard_dict['email'] = unicode(elem) | |
271 elif elem.name == 'BDAY': | |
272 vcard_dict['birthday'] = unicode(elem) | |
273 elif elem.name == 'PHOTO': | |
274 # TODO: handle EXTVAL | |
275 try: | |
276 avatar_hash = yield threads.deferToThread( | |
277 self.savePhoto, client, elem, entity_jid) | |
278 except (exceptions.DataError, exceptions.NotFound) as e: | |
279 avatar_hash = '' | |
280 vcard_dict['avatar'] = avatar_hash | |
281 except Exception as e: | |
282 log.error(u"avatar saving error: {}".format(e)) | |
283 avatar_hash = None | |
284 else: | |
285 vcard_dict['avatar'] = avatar_hash | |
286 self.updateCache(client, entity_jid, 'avatar', avatar_hash) | |
287 else: | |
288 log.debug(u'FIXME: [{}] VCard tag is not managed yet'.format(elem.name)) | |
289 | |
290 # if a data in cache doesn't exist anymore, we need to delete it | |
291 # so we check CACHED_DATA no gotten (i.e. not in vcard_dict keys) | |
292 # and we reset them | |
293 for datum in CACHED_DATA.difference(vcard_dict.keys()): | |
294 log.debug(u"reseting vcard datum [{datum}] for {entity}".format(datum=datum, entity=entity_jid.full())) | |
295 self.updateCache(client, entity_jid, datum, None) | |
296 | |
297 defer.returnValue(vcard_dict) | |
298 | |
299 def _vCardCb(self, vcard_elt, to_jid, client): | |
300 """Called after the first get IQ""" | |
301 log.debug(_("VCard found")) | |
302 iq_elt = vcard_elt.parent | |
303 try: | |
304 from_jid = jid.JID(iq_elt["from"]) | |
305 except KeyError: | |
306 from_jid = client.jid.userhostJID() | |
307 d = self.vCard2Dict(client, vcard_elt, from_jid) | |
308 return d | |
309 | |
310 def _vCardEb(self, failure_, to_jid, client): | |
311 """Called when something is wrong with registration""" | |
312 log.warning(u"Can't get vCard for {jid}: {failure}".format(jid=to_jid.full, failure=failure_)) | |
313 self.updateCache(client, to_jid, "avatar", None) | |
314 | |
315 def _getVcardElt(self, iq_elt): | |
316 return iq_elt.elements(NS_VCARD, "vCard").next() | |
317 | |
318 def getCardRaw(self, client, entity_jid): | |
319 """get raw vCard XML | |
320 | |
321 params are as in [getCard] | |
322 """ | |
323 entity_jid = self.getBareOrFull(client, entity_jid) | |
324 log.debug(u"Asking for {}'s VCard".format(entity_jid.full())) | |
325 reg_request = client.IQ('get') | |
326 reg_request["from"] = client.jid.full() | |
327 reg_request["to"] = entity_jid.full() | |
328 reg_request.addElement('vCard', NS_VCARD) | |
329 d = reg_request.send(entity_jid.full()) | |
330 d.addCallback(self._getVcardElt) | |
331 return d | |
332 | |
333 def getCard(self, client, entity_jid): | |
334 """Ask server for VCard | |
335 | |
336 @param entity_jid(jid.JID): jid from which we want the VCard | |
337 @result: id to retrieve the profile | |
338 """ | |
339 d = self.getCardRaw(client, entity_jid) | |
340 d.addCallbacks(self._vCardCb, self._vCardEb, callbackArgs=[entity_jid, client], errbackArgs=[entity_jid, client]) | |
341 return d | |
342 | |
343 def _getCardCb(self, dummy, client, entity): | |
344 try: | |
345 return client._cache_0054[entity.full()]['avatar'] | |
346 except KeyError: | |
347 raise Failure(exceptions.NotFound()) | |
348 | |
349 def _getAvatar(self, entity, cache_only, hash_only, profile): | |
350 client = self.host.getClient(profile) | |
351 d = self.getAvatar(client, jid.JID(entity), cache_only, hash_only) | |
352 d.addErrback(lambda dummy: '') | |
353 | |
354 return d | |
355 | |
356 def getAvatar(self, client, entity, cache_only=True, hash_only=False): | |
357 """get avatar full path or hash | |
358 | |
359 if avatar is not in local cache, it will be requested to the server | |
360 @param entity(jid.JID): entity to get avatar from | |
361 @param cache_only(bool): if False, will request vCard if avatar is | |
362 not in cache | |
363 @param hash_only(bool): if True only return hash, not full path | |
364 @raise exceptions.NotFound: no avatar found | |
365 """ | |
366 if not entity.resource and self.isRoom(client, entity): | |
367 raise exceptions.NotFound | |
368 entity = self.getBareOrFull(client, entity) | |
369 full_path = None | |
370 | |
371 try: | |
372 # we first check if we have avatar in cache | |
373 avatar_hash = client._cache_0054[entity.full()]['avatar'] | |
374 if avatar_hash: | |
375 # avatar is known and exists | |
376 full_path = client.cache.getFilePath(avatar_hash) | |
377 if full_path is None: | |
378 # cache file is not available (probably expired) | |
379 raise KeyError | |
380 else: | |
381 # avatar has already been checked but it is not set | |
382 full_path = u'' | |
383 except KeyError: | |
384 # avatar is not in cache | |
385 if cache_only: | |
386 return defer.fail(Failure(exceptions.NotFound())) | |
387 # we request vCard to get avatar | |
388 d = self.getCard(client, entity) | |
389 d.addCallback(self._getCardCb, client, entity) | |
390 else: | |
391 # avatar is in cache, we can return hash | |
392 d = defer.succeed(avatar_hash) | |
393 | |
394 if not hash_only: | |
395 # full path is requested | |
396 if full_path is None: | |
397 d.addCallback(client.cache.getFilePath) | |
398 else: | |
399 d.addCallback(lambda dummy: full_path) | |
400 return d | |
401 | |
402 @defer.inlineCallbacks | |
403 def getNick(self, client, entity): | |
404 """get nick from cache, or check vCard | |
405 | |
406 @param entity(jid.JID): entity to get nick from | |
407 @return(unicode, None): nick or None if not found | |
408 """ | |
409 nick = self.getCache(client, entity, u'nick') | |
410 if nick is not None: | |
411 defer.returnValue(nick) | |
412 yield self.getCard(client, entity) | |
413 defer.returnValue(self.getCache(client, entity, u'nick')) | |
414 | |
415 @defer.inlineCallbacks | |
416 def setNick(self, client, nick): | |
417 """update our vCard and set a nickname | |
418 | |
419 @param nick(unicode): new nickname to use | |
420 """ | |
421 jid_ = client.jid.userhostJID() | |
422 try: | |
423 vcard_elt = yield self.getCardRaw(client, jid_) | |
424 except error.StanzaError as e: | |
425 if e.condition == 'item-not-found': | |
426 vcard_elt = domish.Element((NS_VCARD, 'vCard')) | |
427 else: | |
428 raise e | |
429 try: | |
430 nickname_elt = next(vcard_elt.elements(NS_VCARD, u'NICKNAME')) | |
431 except StopIteration: | |
432 pass | |
433 else: | |
434 vcard_elt.children.remove(nickname_elt) | |
435 | |
436 nickname_elt = vcard_elt.addElement((NS_VCARD, u'NICKNAME'), content=nick) | |
437 iq_elt = client.IQ() | |
438 vcard_elt = iq_elt.addChild(vcard_elt) | |
439 yield iq_elt.send() | |
440 self.updateCache(client, jid_, u'nick', unicode(nick)) | |
441 | |
442 def _buildSetAvatar(self, client, vcard_elt, file_path): | |
443 # XXX: this method is executed in a separate thread | |
444 try: | |
445 img = Image.open(file_path) | |
446 except IOError: | |
447 return Failure(exceptions.DataError(u"Can't open image")) | |
448 | |
449 if img.size != AVATAR_DIM: | |
450 img.thumbnail(AVATAR_DIM) | |
451 if img.size[0] != img.size[1]: # we need to crop first | |
452 left, upper = (0, 0) | |
453 right, lower = img.size | |
454 offset = abs(right - lower) / 2 | |
455 if right == min(img.size): | |
456 upper += offset | |
457 lower -= offset | |
458 else: | |
459 left += offset | |
460 right -= offset | |
461 img = img.crop((left, upper, right, lower)) | |
462 img_buf = StringIO() | |
463 img.save(img_buf, 'PNG') | |
464 | |
465 photo_elt = vcard_elt.addElement('PHOTO') | |
466 photo_elt.addElement('TYPE', content='image/png') | |
467 photo_elt.addElement('BINVAL', content=b64encode(img_buf.getvalue())) | |
468 image_hash = sha1(img_buf.getvalue()).hexdigest() | |
469 with client.cache.cacheData( | |
470 PLUGIN_INFO['import_name'], | |
471 image_hash, | |
472 "image/png", | |
473 MAX_AGE | |
474 ) as f: | |
475 f.write(img_buf.getvalue()) | |
476 return image_hash | |
477 | |
478 def _setAvatar(self, file_path, profile_key=C.PROF_KEY_NONE): | |
479 client = self.host.getClient(profile_key) | |
480 return self.setAvatar(client, file_path) | |
481 | |
482 @defer.inlineCallbacks | |
483 def setAvatar(self, client, file_path): | |
484 """Set avatar of the profile | |
485 | |
486 @param file_path: path of the image of the avatar | |
487 """ | |
488 try: | |
489 # we first check if a vcard already exists, to keep data | |
490 vcard_elt = yield self.getCardRaw(client, client.jid.userhostJID()) | |
491 except error.StanzaError as e: | |
492 if e.condition == 'item-not-found': | |
493 vcard_elt = domish.Element((NS_VCARD, 'vCard')) | |
494 else: | |
495 raise e | |
496 else: | |
497 # the vcard exists, we need to remove PHOTO element as we'll make a new one | |
498 try: | |
499 photo_elt = next(vcard_elt.elements(NS_VCARD, u'PHOTO')) | |
500 except StopIteration: | |
501 pass | |
502 else: | |
503 vcard_elt.children.remove(photo_elt) | |
504 | |
505 iq_elt = client.IQ() | |
506 iq_elt.addChild(vcard_elt) | |
507 image_hash = yield threads.deferToThread(self._buildSetAvatar, client, vcard_elt, file_path) | |
508 # image is now at the right size/format | |
509 | |
510 self.updateCache(client, client.jid.userhostJID(), 'avatar', image_hash) | |
511 yield iq_elt.send() | |
512 client.presence.available() # FIXME: should send the current presence, not always "available" ! | |
513 | |
514 | |
515 class XEP_0054_handler(XMPPHandler): | |
516 implements(iwokkel.IDisco) | |
517 | |
518 def __init__(self, plugin_parent): | |
519 self.plugin_parent = plugin_parent | |
520 self.host = plugin_parent.host | |
521 | |
522 def connectionInitialized(self): | |
523 self.xmlstream.addObserver(VCARD_UPDATE, self.update) | |
524 | |
525 def getDiscoInfo(self, requestor, target, nodeIdentifier=''): | |
526 return [disco.DiscoFeature(NS_VCARD)] | |
527 | |
528 def getDiscoItems(self, requestor, target, nodeIdentifier=''): | |
529 return [] | |
530 | |
531 def _checkAvatarHash(self, dummy, client, entity, given_hash): | |
532 """check that hash in cash (i.e. computed hash) is the same as given one""" | |
533 # XXX: if they differ, the avater will be requested on each connection | |
534 # TODO: try to avoid re-requesting avatar in this case | |
535 computed_hash = self.plugin_parent.getCache(client, entity, 'avatar') | |
536 if computed_hash != given_hash: | |
537 log.warning(u"computed hash differs from given hash for {entity}:\n" | |
538 "computed: {computed}\ngiven: {given}".format( | |
539 entity=entity, computed=computed_hash, given=given_hash)) | |
540 | |
541 def update(self, presence): | |
542 """Called on <presence/> stanza with vcard data | |
543 | |
544 Check for avatar information, and get VCard if needed | |
545 @param presend(domish.Element): <presence/> stanza | |
546 """ | |
547 client = self.parent | |
548 entity_jid = self.plugin_parent.getBareOrFull(client, jid.JID(presence['from'])) | |
549 #FIXME: wokkel's data_form should be used here | |
550 try: | |
551 x_elt = presence.elements(NS_VCARD_UPDATE, 'x').next() | |
552 except StopIteration: | |
553 return | |
554 | |
555 try: | |
556 photo_elt = x_elt.elements(NS_VCARD_UPDATE, 'photo').next() | |
557 except StopIteration: | |
558 return | |
559 | |
560 hash_ = unicode(photo_elt).strip() | |
561 if hash_ == C.HASH_SHA1_EMPTY: | |
562 hash_ = u'' | |
563 old_avatar = self.plugin_parent.getCache(client, entity_jid, 'avatar') | |
564 | |
565 if old_avatar == hash_: | |
566 # no change, we can return... | |
567 if hash_: | |
568 # ...but we double check that avatar is in cache | |
569 file_path = client.cache.getFilePath(hash_) | |
570 if file_path is None: | |
571 log.error(u"Avatar for [{}] should be in cache but it is not! We get it".format(entity_jid.full())) | |
572 self.plugin_parent.getCard(client, entity_jid) | |
573 else: | |
574 log.debug(u"avatar for {} already in cache".format(entity_jid.full())) | |
575 return | |
576 | |
577 if not hash_: | |
578 # the avatar has been removed | |
579 # XXX: we use empty string instead of None to indicate that we took avatar | |
580 # but it is empty on purpose | |
581 self.plugin_parent.updateCache(client, entity_jid, 'avatar', '') | |
582 return | |
583 | |
584 file_path = client.cache.getFilePath(hash_) | |
585 if file_path is not None: | |
586 log.debug(u"New avatar found for [{}], it's already in cache, we use it".format(entity_jid.full())) | |
587 self.plugin_parent.updateCache(client, entity_jid, 'avatar', hash_) | |
588 else: | |
589 log.debug(u'New avatar found for [{}], requesting vcard'.format(entity_jid.full())) | |
590 d = self.plugin_parent.getCard(client, entity_jid) | |
591 d.addCallback(self._checkAvatarHash, client, entity_jid, hash_) |