comparison src/plugins/plugin_xep_0054.py @ 2252:cffa50c9f26b

plugin XEP-0054: nick handling + don't remove data on avatar set - new getNick and setNick allow to manipulate nickname - setAvatar was removing any data previously in vCard, this is not the case anymore
author Goffi <goffi@goffi.org>
date Sun, 21 May 2017 20:01:24 +0200
parents 33c8c4973743
children 074c2f157dc9
comparison
equal deleted inserted replaced
2251:83bcd9ec4782 2252:cffa50c9f26b
21 from sat.core.i18n import _ 21 from sat.core.i18n import _
22 from sat.core.constants import Const as C 22 from sat.core.constants import Const as C
23 from sat.core.log import getLogger 23 from sat.core.log import getLogger
24 log = getLogger(__name__) 24 log = getLogger(__name__)
25 from twisted.internet import threads, defer 25 from twisted.internet import threads, defer
26 from twisted.words.protocols.jabber import jid 26 from twisted.words.protocols.jabber import jid, error
27 from twisted.words.xish import domish 27 from twisted.words.xish import domish
28 from twisted.python.failure import Failure 28 from twisted.python.failure import Failure
29 29
30 from zope.interface import implements 30 from zope.interface import implements
31 31
185 client._cache_0054.force(jid_s) 185 client._cache_0054.force(jid_s)
186 186
187 def getCache(self, client, entity_jid, name): 187 def getCache(self, client, entity_jid, name):
188 """return cached value for jid 188 """return cached value for jid
189 189
190 @param entity_jid: target contact 190 @param entity_jid(jid.JID): target contact
191 @param name: name of the value ('nick' or 'avatar') 191 @param name(unicode): name of the value ('nick' or 'avatar')
192 @return: wanted value or None""" 192 @return(unicode, None): wanted value or None"""
193 entity_jid = self.getBareOrFull(client, entity_jid) 193 entity_jid = self.getBareOrFull(client, entity_jid)
194 try: 194 try:
195 data = self.host.memory.getEntityData(entity_jid, [name], client.profile) 195 data = self.host.memory.getEntityData(entity_jid, [name], client.profile)
196 except exceptions.UnknownEntityError: 196 except exceptions.UnknownEntityError:
197 return None 197 return None
287 log.debug(u"reseting vcard datum [{datum}] for {entity}".format(datum=datum, entity=entity_jid.full())) 287 log.debug(u"reseting vcard datum [{datum}] for {entity}".format(datum=datum, entity=entity_jid.full()))
288 self.updateCache(client, entity_jid, datum, None) 288 self.updateCache(client, entity_jid, datum, None)
289 289
290 defer.returnValue(vcard_dict) 290 defer.returnValue(vcard_dict)
291 291
292 def _vCardCb(self, iq_elt, to_jid, client): 292 def _vCardCb(self, vcard_elt, to_jid, client):
293 """Called after the first get IQ""" 293 """Called after the first get IQ"""
294 log.debug(_("VCard found")) 294 log.debug(_("VCard found"))
295 295 iq_elt = vcard_elt.parent
296 try:
297 vcard_elt = iq_elt.elements(NS_VCARD, "vCard").next()
298 except StopIteration:
299 log.warning(u"Can't find vCard element in answer for jid {jid}", jid=to_jid.full())
300 return
301 try: 296 try:
302 from_jid = jid.JID(iq_elt["from"]) 297 from_jid = jid.JID(iq_elt["from"])
303 except KeyError: 298 except KeyError:
304 from_jid = client.jid.userhostJID() 299 from_jid = client.jid.userhostJID()
305 d = self.vCard2Dict(client, vcard_elt, from_jid) 300 d = self.vCard2Dict(client, vcard_elt, from_jid)
308 def _vCardEb(self, failure_, to_jid, client): 303 def _vCardEb(self, failure_, to_jid, client):
309 """Called when something is wrong with registration""" 304 """Called when something is wrong with registration"""
310 log.warning(u"Can't get vCard for {jid}: {failure}".format(jid=to_jid.full, failure=failure_)) 305 log.warning(u"Can't get vCard for {jid}: {failure}".format(jid=to_jid.full, failure=failure_))
311 self.updateCache(client, to_jid, "avatar", None) 306 self.updateCache(client, to_jid, "avatar", None)
312 307
313 def getCard(self, client, entity_jid): 308 def _getVcardElt(self, iq_elt):
314 """Ask server for VCard 309 return iq_elt.elements(NS_VCARD, "vCard").next()
315 310
316 @param entity_jid(jid.JID): jid from which we want the VCard 311 def getCardRaw(self, client, entity_jid):
317 @result: id to retrieve the profile 312 """get raw vCard XML
313
314 params are as in [getCard]
318 """ 315 """
319 entity_jid = self.getBareOrFull(client, entity_jid) 316 entity_jid = self.getBareOrFull(client, entity_jid)
320 log.debug(u"Asking for {}'s VCard".format(entity_jid.full())) 317 log.debug(u"Asking for {}'s VCard".format(entity_jid.full()))
321 reg_request = client.IQ('get') 318 reg_request = client.IQ('get')
322 reg_request["from"] = client.jid.full() 319 reg_request["from"] = client.jid.full()
323 reg_request["to"] = entity_jid.full() 320 reg_request["to"] = entity_jid.full()
324 reg_request.addElement('vCard', NS_VCARD) 321 reg_request.addElement('vCard', NS_VCARD)
325 d = reg_request.send(entity_jid.full()).addCallbacks(self._vCardCb, self._vCardEb, callbackArgs=[entity_jid, client], errbackArgs=[entity_jid, client]) 322 d = reg_request.send(entity_jid.full())
323 d.addCallback(self._getVcardElt)
324 return d
325
326 def getCard(self, client, entity_jid):
327 """Ask server for VCard
328
329 @param entity_jid(jid.JID): jid from which we want the VCard
330 @result: id to retrieve the profile
331 """
332 d = self.getCardRaw(client, entity_jid)
333 d.addCallbacks(self._vCardCb, self._vCardEb, callbackArgs=[entity_jid, client], errbackArgs=[entity_jid, client])
326 return d 334 return d
327 335
328 def _getCardCb(self, dummy, client, entity): 336 def _getCardCb(self, dummy, client, entity):
329 try: 337 try:
330 return client._cache_0054[entity.full()]['avatar'] 338 return client._cache_0054[entity.full()]['avatar']
380 d.addCallback(client.cache.getFilePath) 388 d.addCallback(client.cache.getFilePath)
381 else: 389 else:
382 d.addCallback(lambda dummy: full_path) 390 d.addCallback(lambda dummy: full_path)
383 return d 391 return d
384 392
385 def _buildSetAvatar(self, client, vcard_set, file_path): 393 @defer.inlineCallbacks
394 def getNick(self, client, entity):
395 """get nick from cache, or check vCard
396
397 @param entity(jid.JID): entity to get nick from
398 @return(unicode, None): nick or None if not found
399 """
400 nick = self.getCache(client, entity, u'nick')
401 if nick is not None:
402 defer.returnValue(nick)
403 yield self.getCard(client, entity)
404 defer.returnValue(self.getCache(client, entity, u'nick'))
405
406 @defer.inlineCallbacks
407 def setNick(self, client, nick):
408 """update our vCard and set a nickname
409
410 @param nick(unicode): new nickname to use
411 """
412 jid_ = client.jid.userhostJID()
413 try:
414 vcard_elt = yield self.getCardRaw(client, jid_)
415 except error.StanzaError as e:
416 if e.condition == 'item-not-found':
417 vcard_elt = domish.Element((NS_VCARD, 'vCard'))
418 else:
419 raise e
420 try:
421 nickname_elt = next(vcard_elt.elements(NS_VCARD, u'NICKNAME'))
422 except StopIteration:
423 pass
424 else:
425 vcard_elt.children.remove(nickname_elt)
426
427 nickname_elt = vcard_elt.addElement((NS_VCARD, u'NICKNAME'), content=nick)
428 iq_elt = client.IQ()
429 vcard_elt = iq_elt.addChild(vcard_elt)
430 yield iq_elt.send()
431 self.updateCache(client, jid_, u'nick', unicode(nick))
432
433 def _buildSetAvatar(self, client, vcard_elt, file_path):
386 # XXX: this method is executed in a separate thread 434 # XXX: this method is executed in a separate thread
387 try: 435 try:
388 img = Image.open(file_path) 436 img = Image.open(file_path)
389 except IOError: 437 except IOError:
390 return Failure(exceptions.DataError(u"Can't open image")) 438 return Failure(exceptions.DataError(u"Can't open image"))
403 right -= offset 451 right -= offset
404 img = img.crop((left, upper, right, lower)) 452 img = img.crop((left, upper, right, lower))
405 img_buf = StringIO() 453 img_buf = StringIO()
406 img.save(img_buf, 'PNG') 454 img.save(img_buf, 'PNG')
407 455
408 vcard_elt = vcard_set.addElement('vCard', NS_VCARD)
409 photo_elt = vcard_elt.addElement('PHOTO') 456 photo_elt = vcard_elt.addElement('PHOTO')
410 photo_elt.addElement('TYPE', content='image/png') 457 photo_elt.addElement('TYPE', content='image/png')
411 photo_elt.addElement('BINVAL', content=b64encode(img_buf.getvalue())) 458 photo_elt.addElement('BINVAL', content=b64encode(img_buf.getvalue()))
412 image_hash = sha1(img_buf.getvalue()).hexdigest() 459 image_hash = sha1(img_buf.getvalue()).hexdigest()
413 with client.cache.cacheData( 460 with client.cache.cacheData(
415 image_hash, 462 image_hash,
416 "image/png", 463 "image/png",
417 MAX_AGE 464 MAX_AGE
418 ) as f: 465 ) as f:
419 f.write(img_buf.getvalue()) 466 f.write(img_buf.getvalue())
420 return vcard_set, image_hash 467 return image_hash
421 468
422 def _setAvatar(self, file_path, profile_key=C.PROF_KEY_NONE): 469 def _setAvatar(self, file_path, profile_key=C.PROF_KEY_NONE):
423 client = self.host.getClient(profile_key) 470 client = self.host.getClient(profile_key)
424 return self.setAvatar(client, file_path) 471 return self.setAvatar(client, file_path)
425 472
473 @defer.inlineCallbacks
426 def setAvatar(self, client, file_path): 474 def setAvatar(self, client, file_path):
427 """Set avatar of the profile 475 """Set avatar of the profile
428 476
429 @param file_path: path of the image of the avatar 477 @param file_path: path of the image of the avatar
430 """ 478 """
431 #TODO: This is a temporary way of setting the avatar, as other VCard information is not managed. 479 try:
432 # A proper full VCard management should be done (and more generaly a public/private profile) 480 # we first check if a vcard already exists, to keep data
433 vcard_set = client.IQ() 481 vcard_elt = yield self.getCardRaw(client, client.jid.userhostJID())
434 d = threads.deferToThread(self._buildSetAvatar, client, vcard_set, file_path) 482 except error.StanzaError as e:
435 483 if e.condition == 'item-not-found':
436 def elementBuilt(result): 484 vcard_elt = domish.Element((NS_VCARD, 'vCard'))
437 """Called once the image is at the right size/format, and the vcard set element is build""" 485 else:
438 set_avatar_elt, image_hash = result 486 raise e
439 self.updateCache(client, client.jid.userhostJID(), 'avatar', image_hash) 487 else:
440 return set_avatar_elt.send().addCallback(lambda ignore: client.presence.available()) # FIXME: should send the current presence, not always "available" ! 488 # the vcard exists, we need to remove PHOTO element as we'll make a new one
441 489 try:
442 d.addCallback(elementBuilt) 490 photo_elt = next(vcard_elt.elements(NS_VCARD, u'PHOTO'))
443 491 except StopIteration:
444 return d 492 pass
493 else:
494 vcard_elt.children.remove(photo_elt)
495
496 iq_elt = client.IQ()
497 iq_elt.addChild(vcard_elt)
498 image_hash = yield threads.deferToThread(self._buildSetAvatar, client, vcard_elt, file_path)
499 # image is now at the right size/format
500
501 self.updateCache(client, client.jid.userhostJID(), 'avatar', image_hash)
502 yield iq_elt.send()
503 client.presence.available() # FIXME: should send the current presence, not always "available" !
445 504
446 505
447 class XEP_0054_handler(XMPPHandler): 506 class XEP_0054_handler(XMPPHandler):
448 implements(iwokkel.IDisco) 507 implements(iwokkel.IDisco)
449 508