comparison sat/plugins/plugin_xep_0054.py @ 2624:56f94936df1e

code style reformatting using black
author Goffi <goffi@goffi.org>
date Wed, 27 Jun 2018 20:14:46 +0200
parents 395a3d1c2888
children 378188abe941
comparison
equal deleted inserted replaced
2623:49533de4540b 2624:56f94936df1e
19 # along with this program. If not, see <http://www.gnu.org/licenses/>. 19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 20
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
24 log = getLogger(__name__) 25 log = getLogger(__name__)
25 from twisted.internet import threads, defer 26 from twisted.internet import threads, defer
26 from twisted.words.protocols.jabber import jid, error 27 from twisted.words.protocols.jabber import jid, error
27 from twisted.words.xish import domish 28 from twisted.words.xish import domish
28 from twisted.python.failure import Failure 29 from twisted.python.failure import Failure
34 from base64 import b64decode, b64encode 35 from base64 import b64decode, b64encode
35 from hashlib import sha1 36 from hashlib import sha1
36 from sat.core import exceptions 37 from sat.core import exceptions
37 from sat.memory import persistent 38 from sat.memory import persistent
38 import mimetypes 39 import mimetypes
40
39 try: 41 try:
40 from PIL import Image 42 from PIL import Image
41 except: 43 except:
42 raise exceptions.MissingModule(u"Missing module pillow, please download/install it from https://python-pillow.github.io") 44 raise exceptions.MissingModule(
45 u"Missing module pillow, please download/install it from https://python-pillow.github.io"
46 )
43 from cStringIO import StringIO 47 from cStringIO import StringIO
44 48
45 try: 49 try:
46 from twisted.words.protocols.xmlstream import XMPPHandler 50 from twisted.words.protocols.xmlstream import XMPPHandler
47 except ImportError: 51 except ImportError:
48 from wokkel.subprotocols import XMPPHandler 52 from wokkel.subprotocols import XMPPHandler
49 53
50 AVATAR_PATH = "avatars" 54 AVATAR_PATH = "avatars"
51 AVATAR_DIM = (64, 64) # FIXME: dim are not adapted to modern resolutions ! 55 AVATAR_DIM = (64, 64) #  FIXME: dim are not adapted to modern resolutions !
52 56
53 IQ_GET = '/iq[@type="get"]' 57 IQ_GET = '/iq[@type="get"]'
54 NS_VCARD = 'vcard-temp' 58 NS_VCARD = "vcard-temp"
55 VCARD_REQUEST = IQ_GET + '/vCard[@xmlns="' + NS_VCARD + '"]' # TODO: manage requests 59 VCARD_REQUEST = IQ_GET + '/vCard[@xmlns="' + NS_VCARD + '"]' # TODO: manage requests
56 60
57 PRESENCE = '/presence' 61 PRESENCE = "/presence"
58 NS_VCARD_UPDATE = 'vcard-temp:x:update' 62 NS_VCARD_UPDATE = "vcard-temp:x:update"
59 VCARD_UPDATE = PRESENCE + '/x[@xmlns="' + NS_VCARD_UPDATE + '"]' 63 VCARD_UPDATE = PRESENCE + '/x[@xmlns="' + NS_VCARD_UPDATE + '"]'
60 64
61 CACHED_DATA = {'avatar', 'nick'} 65 CACHED_DATA = {"avatar", "nick"}
62 MAX_AGE = 60 * 60 * 24 * 365 66 MAX_AGE = 60 * 60 * 24 * 365
63 67
64 PLUGIN_INFO = { 68 PLUGIN_INFO = {
65 C.PI_NAME: "XEP 0054 Plugin", 69 C.PI_NAME: "XEP 0054 Plugin",
66 C.PI_IMPORT_NAME: "XEP-0054", 70 C.PI_IMPORT_NAME: "XEP-0054",
68 C.PI_PROTOCOLS: ["XEP-0054", "XEP-0153"], 72 C.PI_PROTOCOLS: ["XEP-0054", "XEP-0153"],
69 C.PI_DEPENDENCIES: [], 73 C.PI_DEPENDENCIES: [],
70 C.PI_RECOMMENDATIONS: ["XEP-0045"], 74 C.PI_RECOMMENDATIONS: ["XEP-0045"],
71 C.PI_MAIN: "XEP_0054", 75 C.PI_MAIN: "XEP_0054",
72 C.PI_HANDLER: "yes", 76 C.PI_HANDLER: "yes",
73 C.PI_DESCRIPTION: _("""Implementation of vcard-temp""") 77 C.PI_DESCRIPTION: _("""Implementation of vcard-temp"""),
74 } 78 }
75 79
76 80
77 class XEP_0054(object): 81 class XEP_0054(object):
78 #TODO: - check that nickname is ok 82 # TODO: - check that nickname is ok
79 # - refactor the code/better use of Wokkel 83 # - refactor the code/better use of Wokkel
80 # - get missing values 84 # - get missing values
81 85
82 def __init__(self, host): 86 def __init__(self, host):
83 log.info(_(u"Plugin XEP_0054 initialization")) 87 log.info(_(u"Plugin XEP_0054 initialization"))
84 self.host = host 88 self.host = host
85 host.bridge.addMethod(u"avatarGet", u".plugin", in_sign=u'sbbs', out_sign=u's', method=self._getAvatar, async=True) 89 host.bridge.addMethod(
86 host.bridge.addMethod(u"avatarSet", u".plugin", in_sign=u'ss', out_sign=u'', method=self._setAvatar, async=True) 90 u"avatarGet",
91 u".plugin",
92 in_sign=u"sbbs",
93 out_sign=u"s",
94 method=self._getAvatar,
95 async=True,
96 )
97 host.bridge.addMethod(
98 u"avatarSet",
99 u".plugin",
100 in_sign=u"ss",
101 out_sign=u"",
102 method=self._setAvatar,
103 async=True,
104 )
87 host.trigger.add(u"presence_available", self.presenceAvailableTrigger) 105 host.trigger.add(u"presence_available", self.presenceAvailableTrigger)
88 host.memory.setSignalOnUpdate(u"avatar") 106 host.memory.setSignalOnUpdate(u"avatar")
89 host.memory.setSignalOnUpdate(u"nick") 107 host.memory.setSignalOnUpdate(u"nick")
90 108
91 def getHandler(self, client): 109 def getHandler(self, client):
96 114
97 @param entity_jid(jid.JID): full or bare jid of the entity check 115 @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 116 @return (bool): True if the bare jid of the entity is a room jid
99 """ 117 """
100 try: 118 try:
101 muc_plg = self.host.plugins['XEP-0045'] 119 muc_plg = self.host.plugins["XEP-0045"]
102 except KeyError: 120 except KeyError:
103 return False 121 return False
104 122
105 try: 123 try:
106 muc_plg.checkRoomJoined(client, entity_jid.userhostJID()) 124 muc_plg.checkRoomJoined(client, entity_jid.userhostJID())
121 return jid_ 139 return jid_
122 140
123 def presenceAvailableTrigger(self, presence_elt, client): 141 def presenceAvailableTrigger(self, presence_elt, client):
124 if client.jid.userhost() in client._cache_0054: 142 if client.jid.userhost() in client._cache_0054:
125 try: 143 try:
126 avatar_hash = client._cache_0054[client.jid.userhost()]['avatar'] 144 avatar_hash = client._cache_0054[client.jid.userhost()]["avatar"]
127 except KeyError: 145 except KeyError:
128 log.info(u"No avatar in cache for {}".format(client.jid.userhost())) 146 log.info(u"No avatar in cache for {}".format(client.jid.userhost()))
129 return True 147 return True
130 x_elt = domish.Element((NS_VCARD_UPDATE, 'x')) 148 x_elt = domish.Element((NS_VCARD_UPDATE, "x"))
131 x_elt.addElement('photo', content=avatar_hash) 149 x_elt.addElement("photo", content=avatar_hash)
132 presence_elt.addChild(x_elt) 150 presence_elt.addChild(x_elt)
133 return True 151 return True
134 152
135 @defer.inlineCallbacks 153 @defer.inlineCallbacks
136 def profileConnecting(self, client): 154 def profileConnecting(self, client):
137 client._cache_0054 = persistent.PersistentBinaryDict(NS_VCARD, client.profile) 155 client._cache_0054 = persistent.PersistentBinaryDict(NS_VCARD, client.profile)
138 yield client._cache_0054.load() 156 yield client._cache_0054.load()
139 self._fillCachedValues(client.profile) 157 self._fillCachedValues(client.profile)
140 158
141 def _fillCachedValues(self, profile): 159 def _fillCachedValues(self, profile):
142 #FIXME: this may need to be reworked 160 # FIXME: this may need to be reworked
143 # the current naive approach keeps a map between all jids 161 # the current naive approach keeps a map between all jids
144 # in persistent cache, then put avatar hashs in memory. 162 # in persistent cache, then put avatar hashs in memory.
145 # Hashes should be shared between profiles (or not ? what 163 # Hashes should be shared between profiles (or not ? what
146 # if the avatar is different depending on who is requesting it 164 # 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). 165 # this is not possible with vcard-tmp, but it is with XEP-0084).
151 jid_ = jid.JID(jid_s) 169 jid_ = jid.JID(jid_s)
152 for name in CACHED_DATA: 170 for name in CACHED_DATA:
153 try: 171 try:
154 value = data[name] 172 value = data[name]
155 if value is None: 173 if value is None:
156 log.error(u"{name} value for {jid_} is None, ignoring".format(name=name, jid_=jid_)) 174 log.error(
175 u"{name} value for {jid_} is None, ignoring".format(
176 name=name, jid_=jid_
177 )
178 )
157 continue 179 continue
158 self.host.memory.updateEntityData(jid_, name, data[name], silent=True, profile_key=profile) 180 self.host.memory.updateEntityData(
181 jid_, name, data[name], silent=True, profile_key=profile
182 )
159 except KeyError: 183 except KeyError:
160 pass 184 pass
161 185
162 def updateCache(self, client, jid_, name, value): 186 def updateCache(self, client, jid_, name, value):
163 """update cache value 187 """update cache value
182 except KeyError: 206 except KeyError:
183 pass 207 pass
184 else: 208 else:
185 client._cache_0054.force(jid_s) 209 client._cache_0054.force(jid_s)
186 else: 210 else:
187 self.host.memory.updateEntityData(jid_, name, value, profile_key=client.profile) 211 self.host.memory.updateEntityData(
212 jid_, name, value, profile_key=client.profile
213 )
188 if name in CACHED_DATA: 214 if name in CACHED_DATA:
189 client._cache_0054.setdefault(jid_s, {})[name] = value 215 client._cache_0054.setdefault(jid_s, {})[name] = value
190 client._cache_0054.force(jid_s) 216 client._cache_0054.force(jid_s)
191 217
192 def getCache(self, client, entity_jid, name): 218 def getCache(self, client, entity_jid, name):
204 230
205 def savePhoto(self, client, photo_elt, entity_jid): 231 def savePhoto(self, client, photo_elt, entity_jid):
206 """Parse a <PHOTO> photo_elt and save the picture""" 232 """Parse a <PHOTO> photo_elt and save the picture"""
207 # XXX: this method is launched in a separate thread 233 # XXX: this method is launched in a separate thread
208 try: 234 try:
209 mime_type = unicode(photo_elt.elements(NS_VCARD, 'TYPE').next()) 235 mime_type = unicode(photo_elt.elements(NS_VCARD, "TYPE").next())
210 except StopIteration: 236 except StopIteration:
211 log.warning(u"no MIME type found, assuming image/png") 237 log.warning(u"no MIME type found, assuming image/png")
212 mime_type = u"image/png" 238 mime_type = u"image/png"
213 else: 239 else:
214 if not mime_type: 240 if not mime_type:
218 if mime_type == "image/x-png": 244 if mime_type == "image/x-png":
219 # XXX: this old MIME type is still used by some clients 245 # XXX: this old MIME type is still used by some clients
220 mime_type = "image/png" 246 mime_type = "image/png"
221 else: 247 else:
222 # TODO: handle other image formats (svg?) 248 # TODO: handle other image formats (svg?)
223 log.warning(u"following avatar image format is not handled: {type} [{jid}]".format( 249 log.warning(
224 type=mime_type, jid=entity_jid.full())) 250 u"following avatar image format is not handled: {type} [{jid}]".format(
251 type=mime_type, jid=entity_jid.full()
252 )
253 )
225 raise Failure(exceptions.DataError()) 254 raise Failure(exceptions.DataError())
226 255
227 ext = mimetypes.guess_extension(mime_type, strict=False) 256 ext = mimetypes.guess_extension(mime_type, strict=False)
228 assert ext is not None 257 assert ext is not None
229 if ext == u'.jpe': 258 if ext == u".jpe":
230 ext = u'.jpg' 259 ext = u".jpg"
231 log.debug(u'photo of type {type} with extension {ext} found [{jid}]'.format( 260 log.debug(
232 type=mime_type, ext=ext, jid=entity_jid.full())) 261 u"photo of type {type} with extension {ext} found [{jid}]".format(
233 try: 262 type=mime_type, ext=ext, jid=entity_jid.full()
234 buf = str(photo_elt.elements(NS_VCARD, 'BINVAL').next()) 263 )
264 )
265 try:
266 buf = str(photo_elt.elements(NS_VCARD, "BINVAL").next())
235 except StopIteration: 267 except StopIteration:
236 log.warning(u"BINVAL element not found") 268 log.warning(u"BINVAL element not found")
237 raise Failure(exceptions.NotFound()) 269 raise Failure(exceptions.NotFound())
238 if not buf: 270 if not buf:
239 log.warning(u"empty avatar for {jid}".format(jid=entity_jid.full())) 271 log.warning(u"empty avatar for {jid}".format(jid=entity_jid.full()))
240 raise Failure(exceptions.NotFound()) 272 raise Failure(exceptions.NotFound())
241 log.debug(_(u'Decoding binary')) 273 log.debug(_(u"Decoding binary"))
242 decoded = b64decode(buf) 274 decoded = b64decode(buf)
243 del buf 275 del buf
244 image_hash = sha1(decoded).hexdigest() 276 image_hash = sha1(decoded).hexdigest()
245 with client.cache.cacheData( 277 with client.cache.cacheData(
246 PLUGIN_INFO['import_name'], 278 PLUGIN_INFO["import_name"],
247 image_hash, 279 image_hash,
248 mime_type, 280 mime_type,
249 # we keep in cache for 1 year 281 # we keep in cache for 1 year
250 MAX_AGE 282 MAX_AGE,
251 ) as f: 283 ) as f:
252 f.write(decoded) 284 f.write(decoded)
253 return image_hash 285 return image_hash
254 286
255 @defer.inlineCallbacks 287 @defer.inlineCallbacks
256 def vCard2Dict(self, client, vcard, entity_jid): 288 def vCard2Dict(self, client, vcard, entity_jid):
257 """Convert a VCard to a dict, and save binaries""" 289 """Convert a VCard to a dict, and save binaries"""
258 log.debug((u"parsing vcard")) 290 log.debug((u"parsing vcard"))
259 vcard_dict = {} 291 vcard_dict = {}
260 292
261 for elem in vcard.elements(): 293 for elem in vcard.elements():
262 if elem.name == 'FN': 294 if elem.name == "FN":
263 vcard_dict['fullname'] = unicode(elem) 295 vcard_dict["fullname"] = unicode(elem)
264 elif elem.name == 'NICKNAME': 296 elif elem.name == "NICKNAME":
265 vcard_dict['nick'] = unicode(elem) 297 vcard_dict["nick"] = unicode(elem)
266 self.updateCache(client, entity_jid, 'nick', vcard_dict['nick']) 298 self.updateCache(client, entity_jid, "nick", vcard_dict["nick"])
267 elif elem.name == 'URL': 299 elif elem.name == "URL":
268 vcard_dict['website'] = unicode(elem) 300 vcard_dict["website"] = unicode(elem)
269 elif elem.name == 'EMAIL': 301 elif elem.name == "EMAIL":
270 vcard_dict['email'] = unicode(elem) 302 vcard_dict["email"] = unicode(elem)
271 elif elem.name == 'BDAY': 303 elif elem.name == "BDAY":
272 vcard_dict['birthday'] = unicode(elem) 304 vcard_dict["birthday"] = unicode(elem)
273 elif elem.name == 'PHOTO': 305 elif elem.name == "PHOTO":
274 # TODO: handle EXTVAL 306 # TODO: handle EXTVAL
275 try: 307 try:
276 avatar_hash = yield threads.deferToThread( 308 avatar_hash = yield threads.deferToThread(
277 self.savePhoto, client, elem, entity_jid) 309 self.savePhoto, client, elem, entity_jid
310 )
278 except (exceptions.DataError, exceptions.NotFound) as e: 311 except (exceptions.DataError, exceptions.NotFound) as e:
279 avatar_hash = '' 312 avatar_hash = ""
280 vcard_dict['avatar'] = avatar_hash 313 vcard_dict["avatar"] = avatar_hash
281 except Exception as e: 314 except Exception as e:
282 log.error(u"avatar saving error: {}".format(e)) 315 log.error(u"avatar saving error: {}".format(e))
283 avatar_hash = None 316 avatar_hash = None
284 else: 317 else:
285 vcard_dict['avatar'] = avatar_hash 318 vcard_dict["avatar"] = avatar_hash
286 self.updateCache(client, entity_jid, 'avatar', avatar_hash) 319 self.updateCache(client, entity_jid, "avatar", avatar_hash)
287 else: 320 else:
288 log.debug(u'FIXME: [{}] VCard tag is not managed yet'.format(elem.name)) 321 log.debug(u"FIXME: [{}] VCard tag is not managed yet".format(elem.name))
289 322
290 # if a data in cache doesn't exist anymore, we need to delete it 323 # 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) 324 # so we check CACHED_DATA no gotten (i.e. not in vcard_dict keys)
292 # and we reset them 325 # and we reset them
293 for datum in CACHED_DATA.difference(vcard_dict.keys()): 326 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())) 327 log.debug(
328 u"reseting vcard datum [{datum}] for {entity}".format(
329 datum=datum, entity=entity_jid.full()
330 )
331 )
295 self.updateCache(client, entity_jid, datum, None) 332 self.updateCache(client, entity_jid, datum, None)
296 333
297 defer.returnValue(vcard_dict) 334 defer.returnValue(vcard_dict)
298 335
299 def _vCardCb(self, vcard_elt, to_jid, client): 336 def _vCardCb(self, vcard_elt, to_jid, client):
307 d = self.vCard2Dict(client, vcard_elt, from_jid) 344 d = self.vCard2Dict(client, vcard_elt, from_jid)
308 return d 345 return d
309 346
310 def _vCardEb(self, failure_, to_jid, client): 347 def _vCardEb(self, failure_, to_jid, client):
311 """Called when something is wrong with registration""" 348 """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_)) 349 log.warning(
350 u"Can't get vCard for {jid}: {failure}".format(
351 jid=to_jid.full, failure=failure_
352 )
353 )
313 self.updateCache(client, to_jid, "avatar", None) 354 self.updateCache(client, to_jid, "avatar", None)
314 355
315 def _getVcardElt(self, iq_elt): 356 def _getVcardElt(self, iq_elt):
316 return iq_elt.elements(NS_VCARD, "vCard").next() 357 return iq_elt.elements(NS_VCARD, "vCard").next()
317 358
318 def getCardRaw(self, client, entity_jid): 359 def getCardRaw(self, client, entity_jid):
319 """get raw vCard XML 360 """get raw vCard XML
320 361
321 params are as in [getCard] 362 params are as in [getCard]
322 """ 363 """
323 entity_jid = self.getBareOrFull(client, entity_jid) 364 entity_jid = self.getBareOrFull(client, entity_jid)
324 log.debug(u"Asking for {}'s VCard".format(entity_jid.full())) 365 log.debug(u"Asking for {}'s VCard".format(entity_jid.full()))
325 reg_request = client.IQ('get') 366 reg_request = client.IQ("get")
326 reg_request["from"] = client.jid.full() 367 reg_request["from"] = client.jid.full()
327 reg_request["to"] = entity_jid.full() 368 reg_request["to"] = entity_jid.full()
328 reg_request.addElement('vCard', NS_VCARD) 369 reg_request.addElement("vCard", NS_VCARD)
329 d = reg_request.send(entity_jid.full()) 370 d = reg_request.send(entity_jid.full())
330 d.addCallback(self._getVcardElt) 371 d.addCallback(self._getVcardElt)
331 return d 372 return d
332 373
333 def getCard(self, client, entity_jid): 374 def getCard(self, client, entity_jid):
335 376
336 @param entity_jid(jid.JID): jid from which we want the VCard 377 @param entity_jid(jid.JID): jid from which we want the VCard
337 @result: id to retrieve the profile 378 @result: id to retrieve the profile
338 """ 379 """
339 d = self.getCardRaw(client, entity_jid) 380 d = self.getCardRaw(client, entity_jid)
340 d.addCallbacks(self._vCardCb, self._vCardEb, callbackArgs=[entity_jid, client], errbackArgs=[entity_jid, client]) 381 d.addCallbacks(
382 self._vCardCb,
383 self._vCardEb,
384 callbackArgs=[entity_jid, client],
385 errbackArgs=[entity_jid, client],
386 )
341 return d 387 return d
342 388
343 def _getCardCb(self, dummy, client, entity): 389 def _getCardCb(self, dummy, client, entity):
344 try: 390 try:
345 return client._cache_0054[entity.full()]['avatar'] 391 return client._cache_0054[entity.full()]["avatar"]
346 except KeyError: 392 except KeyError:
347 raise Failure(exceptions.NotFound()) 393 raise Failure(exceptions.NotFound())
348 394
349 def _getAvatar(self, entity, cache_only, hash_only, profile): 395 def _getAvatar(self, entity, cache_only, hash_only, profile):
350 client = self.host.getClient(profile) 396 client = self.host.getClient(profile)
351 d = self.getAvatar(client, jid.JID(entity), cache_only, hash_only) 397 d = self.getAvatar(client, jid.JID(entity), cache_only, hash_only)
352 d.addErrback(lambda dummy: '') 398 d.addErrback(lambda dummy: "")
353 399
354 return d 400 return d
355 401
356 def getAvatar(self, client, entity, cache_only=True, hash_only=False): 402 def getAvatar(self, client, entity, cache_only=True, hash_only=False):
357 """get avatar full path or hash 403 """get avatar full path or hash
368 entity = self.getBareOrFull(client, entity) 414 entity = self.getBareOrFull(client, entity)
369 full_path = None 415 full_path = None
370 416
371 try: 417 try:
372 # we first check if we have avatar in cache 418 # we first check if we have avatar in cache
373 avatar_hash = client._cache_0054[entity.full()]['avatar'] 419 avatar_hash = client._cache_0054[entity.full()]["avatar"]
374 if avatar_hash: 420 if avatar_hash:
375 # avatar is known and exists 421 # avatar is known and exists
376 full_path = client.cache.getFilePath(avatar_hash) 422 full_path = client.cache.getFilePath(avatar_hash)
377 if full_path is None: 423 if full_path is None:
378 # cache file is not available (probably expired) 424 # cache file is not available (probably expired)
379 raise KeyError 425 raise KeyError
380 else: 426 else:
381 # avatar has already been checked but it is not set 427 # avatar has already been checked but it is not set
382 full_path = u'' 428 full_path = u""
383 except KeyError: 429 except KeyError:
384 # avatar is not in cache 430 # avatar is not in cache
385 if cache_only: 431 if cache_only:
386 return defer.fail(Failure(exceptions.NotFound())) 432 return defer.fail(Failure(exceptions.NotFound()))
387 # we request vCard to get avatar 433 # we request vCard to get avatar
404 """get nick from cache, or check vCard 450 """get nick from cache, or check vCard
405 451
406 @param entity(jid.JID): entity to get nick from 452 @param entity(jid.JID): entity to get nick from
407 @return(unicode, None): nick or None if not found 453 @return(unicode, None): nick or None if not found
408 """ 454 """
409 nick = self.getCache(client, entity, u'nick') 455 nick = self.getCache(client, entity, u"nick")
410 if nick is not None: 456 if nick is not None:
411 defer.returnValue(nick) 457 defer.returnValue(nick)
412 yield self.getCard(client, entity) 458 yield self.getCard(client, entity)
413 defer.returnValue(self.getCache(client, entity, u'nick')) 459 defer.returnValue(self.getCache(client, entity, u"nick"))
414 460
415 @defer.inlineCallbacks 461 @defer.inlineCallbacks
416 def setNick(self, client, nick): 462 def setNick(self, client, nick):
417 """update our vCard and set a nickname 463 """update our vCard and set a nickname
418 464
420 """ 466 """
421 jid_ = client.jid.userhostJID() 467 jid_ = client.jid.userhostJID()
422 try: 468 try:
423 vcard_elt = yield self.getCardRaw(client, jid_) 469 vcard_elt = yield self.getCardRaw(client, jid_)
424 except error.StanzaError as e: 470 except error.StanzaError as e:
425 if e.condition == 'item-not-found': 471 if e.condition == "item-not-found":
426 vcard_elt = domish.Element((NS_VCARD, 'vCard')) 472 vcard_elt = domish.Element((NS_VCARD, "vCard"))
427 else: 473 else:
428 raise e 474 raise e
429 try: 475 try:
430 nickname_elt = next(vcard_elt.elements(NS_VCARD, u'NICKNAME')) 476 nickname_elt = next(vcard_elt.elements(NS_VCARD, u"NICKNAME"))
431 except StopIteration: 477 except StopIteration:
432 pass 478 pass
433 else: 479 else:
434 vcard_elt.children.remove(nickname_elt) 480 vcard_elt.children.remove(nickname_elt)
435 481
436 nickname_elt = vcard_elt.addElement((NS_VCARD, u'NICKNAME'), content=nick) 482 nickname_elt = vcard_elt.addElement((NS_VCARD, u"NICKNAME"), content=nick)
437 iq_elt = client.IQ() 483 iq_elt = client.IQ()
438 vcard_elt = iq_elt.addChild(vcard_elt) 484 vcard_elt = iq_elt.addChild(vcard_elt)
439 yield iq_elt.send() 485 yield iq_elt.send()
440 self.updateCache(client, jid_, u'nick', unicode(nick)) 486 self.updateCache(client, jid_, u"nick", unicode(nick))
441 487
442 def _buildSetAvatar(self, client, vcard_elt, file_path): 488 def _buildSetAvatar(self, client, vcard_elt, file_path):
443 # XXX: this method is executed in a separate thread 489 # XXX: this method is executed in a separate thread
444 try: 490 try:
445 img = Image.open(file_path) 491 img = Image.open(file_path)
458 else: 504 else:
459 left += offset 505 left += offset
460 right -= offset 506 right -= offset
461 img = img.crop((left, upper, right, lower)) 507 img = img.crop((left, upper, right, lower))
462 img_buf = StringIO() 508 img_buf = StringIO()
463 img.save(img_buf, 'PNG') 509 img.save(img_buf, "PNG")
464 510
465 photo_elt = vcard_elt.addElement('PHOTO') 511 photo_elt = vcard_elt.addElement("PHOTO")
466 photo_elt.addElement('TYPE', content='image/png') 512 photo_elt.addElement("TYPE", content="image/png")
467 photo_elt.addElement('BINVAL', content=b64encode(img_buf.getvalue())) 513 photo_elt.addElement("BINVAL", content=b64encode(img_buf.getvalue()))
468 image_hash = sha1(img_buf.getvalue()).hexdigest() 514 image_hash = sha1(img_buf.getvalue()).hexdigest()
469 with client.cache.cacheData( 515 with client.cache.cacheData(
470 PLUGIN_INFO['import_name'], 516 PLUGIN_INFO["import_name"], image_hash, "image/png", MAX_AGE
471 image_hash, 517 ) as f:
472 "image/png",
473 MAX_AGE
474 ) as f:
475 f.write(img_buf.getvalue()) 518 f.write(img_buf.getvalue())
476 return image_hash 519 return image_hash
477 520
478 def _setAvatar(self, file_path, profile_key=C.PROF_KEY_NONE): 521 def _setAvatar(self, file_path, profile_key=C.PROF_KEY_NONE):
479 client = self.host.getClient(profile_key) 522 client = self.host.getClient(profile_key)
487 """ 530 """
488 try: 531 try:
489 # we first check if a vcard already exists, to keep data 532 # we first check if a vcard already exists, to keep data
490 vcard_elt = yield self.getCardRaw(client, client.jid.userhostJID()) 533 vcard_elt = yield self.getCardRaw(client, client.jid.userhostJID())
491 except error.StanzaError as e: 534 except error.StanzaError as e:
492 if e.condition == 'item-not-found': 535 if e.condition == "item-not-found":
493 vcard_elt = domish.Element((NS_VCARD, 'vCard')) 536 vcard_elt = domish.Element((NS_VCARD, "vCard"))
494 else: 537 else:
495 raise e 538 raise e
496 else: 539 else:
497 # the vcard exists, we need to remove PHOTO element as we'll make a new one 540 # the vcard exists, we need to remove PHOTO element as we'll make a new one
498 try: 541 try:
499 photo_elt = next(vcard_elt.elements(NS_VCARD, u'PHOTO')) 542 photo_elt = next(vcard_elt.elements(NS_VCARD, u"PHOTO"))
500 except StopIteration: 543 except StopIteration:
501 pass 544 pass
502 else: 545 else:
503 vcard_elt.children.remove(photo_elt) 546 vcard_elt.children.remove(photo_elt)
504 547
505 iq_elt = client.IQ() 548 iq_elt = client.IQ()
506 iq_elt.addChild(vcard_elt) 549 iq_elt.addChild(vcard_elt)
507 image_hash = yield threads.deferToThread(self._buildSetAvatar, client, vcard_elt, file_path) 550 image_hash = yield threads.deferToThread(
551 self._buildSetAvatar, client, vcard_elt, file_path
552 )
508 # image is now at the right size/format 553 # image is now at the right size/format
509 554
510 self.updateCache(client, client.jid.userhostJID(), 'avatar', image_hash) 555 self.updateCache(client, client.jid.userhostJID(), "avatar", image_hash)
511 yield iq_elt.send() 556 yield iq_elt.send()
512 client.presence.available() # FIXME: should send the current presence, not always "available" ! 557 client.presence.available() # FIXME: should send the current presence, not always "available" !
513 558
514 559
515 class XEP_0054_handler(XMPPHandler): 560 class XEP_0054_handler(XMPPHandler):
516 implements(iwokkel.IDisco) 561 implements(iwokkel.IDisco)
517 562
520 self.host = plugin_parent.host 565 self.host = plugin_parent.host
521 566
522 def connectionInitialized(self): 567 def connectionInitialized(self):
523 self.xmlstream.addObserver(VCARD_UPDATE, self.update) 568 self.xmlstream.addObserver(VCARD_UPDATE, self.update)
524 569
525 def getDiscoInfo(self, requestor, target, nodeIdentifier=''): 570 def getDiscoInfo(self, requestor, target, nodeIdentifier=""):
526 return [disco.DiscoFeature(NS_VCARD)] 571 return [disco.DiscoFeature(NS_VCARD)]
527 572
528 def getDiscoItems(self, requestor, target, nodeIdentifier=''): 573 def getDiscoItems(self, requestor, target, nodeIdentifier=""):
529 return [] 574 return []
530 575
531 def _checkAvatarHash(self, dummy, client, entity, given_hash): 576 def _checkAvatarHash(self, dummy, client, entity, given_hash):
532 """check that hash in cash (i.e. computed hash) is the same as given one""" 577 """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 578 # XXX: if they differ, the avater will be requested on each connection
534 # TODO: try to avoid re-requesting avatar in this case 579 # TODO: try to avoid re-requesting avatar in this case
535 computed_hash = self.plugin_parent.getCache(client, entity, 'avatar') 580 computed_hash = self.plugin_parent.getCache(client, entity, "avatar")
536 if computed_hash != given_hash: 581 if computed_hash != given_hash:
537 log.warning(u"computed hash differs from given hash for {entity}:\n" 582 log.warning(
583 u"computed hash differs from given hash for {entity}:\n"
538 "computed: {computed}\ngiven: {given}".format( 584 "computed: {computed}\ngiven: {given}".format(
539 entity=entity, computed=computed_hash, given=given_hash)) 585 entity=entity, computed=computed_hash, given=given_hash
586 )
587 )
540 588
541 def update(self, presence): 589 def update(self, presence):
542 """Called on <presence/> stanza with vcard data 590 """Called on <presence/> stanza with vcard data
543 591
544 Check for avatar information, and get VCard if needed 592 Check for avatar information, and get VCard if needed
545 @param presend(domish.Element): <presence/> stanza 593 @param presend(domish.Element): <presence/> stanza
546 """ 594 """
547 client = self.parent 595 client = self.parent
548 entity_jid = self.plugin_parent.getBareOrFull(client, jid.JID(presence['from'])) 596 entity_jid = self.plugin_parent.getBareOrFull(client, jid.JID(presence["from"]))
549 #FIXME: wokkel's data_form should be used here 597 # FIXME: wokkel's data_form should be used here
550 try: 598 try:
551 x_elt = presence.elements(NS_VCARD_UPDATE, 'x').next() 599 x_elt = presence.elements(NS_VCARD_UPDATE, "x").next()
552 except StopIteration: 600 except StopIteration:
553 return 601 return
554 602
555 try: 603 try:
556 photo_elt = x_elt.elements(NS_VCARD_UPDATE, 'photo').next() 604 photo_elt = x_elt.elements(NS_VCARD_UPDATE, "photo").next()
557 except StopIteration: 605 except StopIteration:
558 return 606 return
559 607
560 hash_ = unicode(photo_elt).strip() 608 hash_ = unicode(photo_elt).strip()
561 if hash_ == C.HASH_SHA1_EMPTY: 609 if hash_ == C.HASH_SHA1_EMPTY:
562 hash_ = u'' 610 hash_ = u""
563 old_avatar = self.plugin_parent.getCache(client, entity_jid, 'avatar') 611 old_avatar = self.plugin_parent.getCache(client, entity_jid, "avatar")
564 612
565 if old_avatar == hash_: 613 if old_avatar == hash_:
566 # no change, we can return... 614 # no change, we can return...
567 if hash_: 615 if hash_:
568 # ...but we double check that avatar is in cache 616 # ...but we double check that avatar is in cache
569 file_path = client.cache.getFilePath(hash_) 617 file_path = client.cache.getFilePath(hash_)
570 if file_path is None: 618 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())) 619 log.error(
620 u"Avatar for [{}] should be in cache but it is not! We get it".format(
621 entity_jid.full()
622 )
623 )
572 self.plugin_parent.getCard(client, entity_jid) 624 self.plugin_parent.getCard(client, entity_jid)
573 else: 625 else:
574 log.debug(u"avatar for {} already in cache".format(entity_jid.full())) 626 log.debug(u"avatar for {} already in cache".format(entity_jid.full()))
575 return 627 return
576 628
577 if not hash_: 629 if not hash_:
578 # the avatar has been removed 630 # the avatar has been removed
579 # XXX: we use empty string instead of None to indicate that we took avatar 631 # XXX: we use empty string instead of None to indicate that we took avatar
580 # but it is empty on purpose 632 # but it is empty on purpose
581 self.plugin_parent.updateCache(client, entity_jid, 'avatar', '') 633 self.plugin_parent.updateCache(client, entity_jid, "avatar", "")
582 return 634 return
583 635
584 file_path = client.cache.getFilePath(hash_) 636 file_path = client.cache.getFilePath(hash_)
585 if file_path is not None: 637 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())) 638 log.debug(
587 self.plugin_parent.updateCache(client, entity_jid, 'avatar', hash_) 639 u"New avatar found for [{}], it's already in cache, we use it".format(
640 entity_jid.full()
641 )
642 )
643 self.plugin_parent.updateCache(client, entity_jid, "avatar", hash_)
588 else: 644 else:
589 log.debug(u'New avatar found for [{}], requesting vcard'.format(entity_jid.full())) 645 log.debug(
646 u"New avatar found for [{}], requesting vcard".format(entity_jid.full())
647 )
590 d = self.plugin_parent.getCard(client, entity_jid) 648 d = self.plugin_parent.getCard(client, entity_jid)
591 d.addCallback(self._checkAvatarHash, client, entity_jid, hash_) 649 d.addCallback(self._checkAvatarHash, client, entity_jid, hash_)