Mercurial > libervia-backend
comparison src/plugins/plugin_xep_0054.py @ 2123:c42aab22c2c0
plugin XEP-0054, quick frontend(app): various improvments:
- memory.cache is now used
- getAvatar and setAvatar has been renamed to avatarGet and avatarSet to follow new convention
- getAvatar now return (optionally) full path, and may request vCard or only return avatar in cache
- getAvatarFile has been removed as it is not needed anymore (avatarGet can return full path)
- getCard has been removed from bridge as it was only used to request avatar
- new internal method getBareOfFull return jid to use depending of the jid being part of a MUC or not
- cache is now set per profile in client, instead of a general one for all profiles
- thanks to the use of memory.cache, correct extension is now used in saved file, according to MIME type
- fixed and better cache handling
- a warning message is shown if given avatar hash differs from computed one
- empty hash value is now in a constant, and ignored if received
- QuickApp has been updated to follow new behaviour
- Primitivus has been fixed (it was not declaring not using avatars correclty)
- jp has been updated to follow new methods name
author | Goffi <goffi@goffi.org> |
---|---|
date | Sun, 15 Jan 2017 17:51:37 +0100 |
parents | 11fb5f5e2f89 |
children | 1d3f73e065e1 |
comparison
equal
deleted
inserted
replaced
2122:3970ebcf8830 | 2123:c42aab22c2c0 |
---|---|
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 |
27 from twisted.words.protocols.jabber.xmlstream import IQ | |
28 from twisted.words.xish import domish | 27 from twisted.words.xish import domish |
29 from twisted.python.failure import Failure | 28 from twisted.python.failure import Failure |
30 import os.path | |
31 | 29 |
32 from zope.interface import implements | 30 from zope.interface import implements |
33 | 31 |
34 from wokkel import disco, iwokkel | 32 from wokkel import disco, iwokkel |
35 | 33 |
36 from base64 import b64decode, b64encode | 34 from base64 import b64decode, b64encode |
37 from hashlib import sha1 | 35 from hashlib import sha1 |
38 from sat.core import exceptions | 36 from sat.core import exceptions |
39 from sat.memory import persistent | 37 from sat.memory import persistent |
38 import mimetypes | |
40 try: | 39 try: |
41 from PIL import Image | 40 from PIL import Image |
42 except: | 41 except: |
43 raise exceptions.MissingModule(u"Missing module pillow, please download/install it from https://python-pillow.github.io") | 42 raise exceptions.MissingModule(u"Missing module pillow, please download/install it from https://python-pillow.github.io") |
44 from cStringIO import StringIO | 43 from cStringIO import StringIO |
47 from twisted.words.protocols.xmlstream import XMPPHandler | 46 from twisted.words.protocols.xmlstream import XMPPHandler |
48 except ImportError: | 47 except ImportError: |
49 from wokkel.subprotocols import XMPPHandler | 48 from wokkel.subprotocols import XMPPHandler |
50 | 49 |
51 AVATAR_PATH = "avatars" | 50 AVATAR_PATH = "avatars" |
52 AVATAR_DIM = (64, 64) | 51 AVATAR_DIM = (64, 64) #Â FIXME: dim are not adapted to modern resolutions ! |
53 | 52 |
54 IQ_GET = '/iq[@type="get"]' | 53 IQ_GET = '/iq[@type="get"]' |
55 NS_VCARD = 'vcard-temp' | 54 NS_VCARD = 'vcard-temp' |
56 VCARD_REQUEST = IQ_GET + '/vCard[@xmlns="' + NS_VCARD + '"]' # TODO: manage requests | 55 VCARD_REQUEST = IQ_GET + '/vCard[@xmlns="' + NS_VCARD + '"]' # TODO: manage requests |
57 | 56 |
58 PRESENCE = '/presence' | 57 PRESENCE = '/presence' |
59 NS_VCARD_UPDATE = 'vcard-temp:x:update' | 58 NS_VCARD_UPDATE = 'vcard-temp:x:update' |
60 VCARD_UPDATE = PRESENCE + '/x[@xmlns="' + NS_VCARD_UPDATE + '"]' | 59 VCARD_UPDATE = PRESENCE + '/x[@xmlns="' + NS_VCARD_UPDATE + '"]' |
61 | 60 |
62 CACHED_DATA = {'avatar', 'nick'} | 61 CACHED_DATA = {'avatar', 'nick'} |
62 MAX_AGE = 60 * 60 * 24 * 365 | |
63 | 63 |
64 PLUGIN_INFO = { | 64 PLUGIN_INFO = { |
65 "name": "XEP 0054 Plugin", | 65 "name": "XEP 0054 Plugin", |
66 "import_name": "XEP-0054", | 66 "import_name": "XEP-0054", |
67 "type": "XEP", | 67 "type": "XEP", |
78 #TODO: - check that nickname is ok | 78 #TODO: - check that nickname is ok |
79 # - refactor the code/better use of Wokkel | 79 # - refactor the code/better use of Wokkel |
80 # - get missing values | 80 # - get missing values |
81 | 81 |
82 def __init__(self, host): | 82 def __init__(self, host): |
83 log.info(_("Plugin XEP_0054 initialization")) | 83 log.info(_(u"Plugin XEP_0054 initialization")) |
84 self.host = host | 84 self.host = host |
85 self.avatar_path = os.path.join(self.host.memory.getConfig('', 'local_dir'), AVATAR_PATH) | 85 host.bridge.addMethod(u"avatarGet", u".plugin", in_sign=u'sbbs', out_sign=u's', method=self._getAvatar, async=True) |
86 if not os.path.exists(self.avatar_path): | 86 host.bridge.addMethod(u"avatarSet", u".plugin", in_sign=u'ss', out_sign=u'', method=self._setAvatar, async=True) |
87 os.makedirs(self.avatar_path) | 87 host.trigger.add(u"presence_available", self.presenceAvailableTrigger) |
88 self.cache = {} | 88 host.memory.setSignalOnUpdate(u"avatar") |
89 host.bridge.addMethod("getCard", ".plugin", in_sign='ss', out_sign='', method=self._getCard, async=True) | 89 host.memory.setSignalOnUpdate(u"nick") |
90 host.bridge.addMethod("getAvatarFile", ".plugin", in_sign='s', out_sign='s', method=self.getAvatarFile) | |
91 host.bridge.addMethod("getAvatar", ".plugin", in_sign='ss', out_sign='s', method=self._getAvatar, async=True) | |
92 host.bridge.addMethod("setAvatar", ".plugin", in_sign='ss', out_sign='', method=self.setAvatar, async=True) | |
93 host.trigger.add("presence_available", self.presenceAvailableTrigger) | |
94 host.memory.setSignalOnUpdate("avatar") | |
95 host.memory.setSignalOnUpdate("nick") | |
96 | 90 |
97 def getHandler(self, profile): | 91 def getHandler(self, profile): |
98 return XEP_0054_handler(self) | 92 return XEP_0054_handler(self) |
99 | 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 self.host.plugins['XEP-0045'].checkRoomJoined(client, entity_jid.userhostJID()) | |
102 except exceptions.NotFound: | |
103 return False | |
104 else: | |
105 return True | |
106 | |
107 def getBareOrFull(self, client, jid_): | |
108 """use full jid if jid_ is an occupant of a room, bare jid else | |
109 | |
110 @param jid_(jid.JID): entity to test | |
111 @return (jid.JID): bare or full jid | |
112 """ | |
113 if jid_.resource: | |
114 if not self.isRoom(client, jid_): | |
115 return jid_.userhostJID() | |
116 return jid_ | |
117 | |
100 def presenceAvailableTrigger(self, presence_elt, client): | 118 def presenceAvailableTrigger(self, presence_elt, client): |
101 if client.jid.userhost() in self.cache[client.profile]: | 119 if client.jid.userhost() in client._cache_0054: |
102 try: | 120 try: |
103 avatar_hash = self.cache[client.profile][client.jid.userhost()]['avatar'] | 121 avatar_hash = client._cache_0054[client.jid.userhost()]['avatar'] |
104 except KeyError: | 122 except KeyError: |
105 log.info(u"No avatar in cache for {}".format(client.jid.userhost())) | 123 log.info(u"No avatar in cache for {}".format(client.jid.userhost())) |
106 return True | 124 return True |
107 x_elt = domish.Element((NS_VCARD_UPDATE, 'x')) | 125 x_elt = domish.Element((NS_VCARD_UPDATE, 'x')) |
108 x_elt.addElement('photo', content=avatar_hash) | 126 x_elt.addElement('photo', content=avatar_hash) |
109 presence_elt.addChild(x_elt) | 127 presence_elt.addChild(x_elt) |
110 | |
111 return True | 128 return True |
112 | 129 |
113 def isRoom(self, client, entity_jid): | 130 @defer.inlineCallbacks |
114 """Tell if a jid is a MUC one | 131 def profileConnecting(self, profile): |
115 | 132 client = self.host.getClient(profile) |
116 @param entity_jid(jid.JID): full or bare jid of the entity check | 133 client._cache_0054 = persistent.PersistentBinaryDict(NS_VCARD, profile) |
117 @return (bool): True if the bare jid of the entity is a room jid | 134 yield client._cache_0054.load() |
118 """ | 135 self._fillCachedValues(profile) |
119 try: | |
120 self.host.plugins['XEP-0045'].checkRoomJoined(client, entity_jid.userhostJID()) | |
121 except exceptions.NotFound: | |
122 return False | |
123 else: | |
124 return True | |
125 | 136 |
126 def _fillCachedValues(self, profile): | 137 def _fillCachedValues(self, profile): |
127 #FIXME: this is really suboptimal, need to be reworked | 138 #FIXME: this may need to be reworked |
128 # the current naive approach keeps a map between all jids of all profiles | 139 # the current naive approach keeps a map between all jids |
129 # in persistent cache, then put avatar hashs in memory. | 140 # in persistent cache, then put avatar hashs in memory. |
130 # Hashes should be shared between profiles | 141 # Hashes should be shared between profiles (or not ? what |
131 for jid_s, data in self.cache[profile].iteritems(): | 142 # if the avatar is different depending on who is requesting it |
143 # this is not possible with vcard-tmp, but it is with XEP-0084). | |
144 # Loading avatar on demand per jid may be a option to investigate. | |
145 client = self.host.getClient(profile) | |
146 for jid_s, data in client._cache_0054.iteritems(): | |
132 jid_ = jid.JID(jid_s) | 147 jid_ = jid.JID(jid_s) |
133 for name in CACHED_DATA: | 148 for name in CACHED_DATA: |
134 try: | 149 try: |
150 value = data[name] | |
151 if value is None: | |
152 log.error(u"{name} value for {jid_} is None, ignoring".format(name=name, jid_=jid_)) | |
153 continue | |
135 self.host.memory.updateEntityData(jid_, name, data[name], silent=True, profile_key=profile) | 154 self.host.memory.updateEntityData(jid_, name, data[name], silent=True, profile_key=profile) |
136 except KeyError: | 155 except KeyError: |
137 pass | 156 pass |
138 | 157 |
139 @defer.inlineCallbacks | |
140 def profileConnecting(self, profile): | |
141 self.cache[profile] = persistent.PersistentBinaryDict(NS_VCARD, profile) | |
142 yield self.cache[profile].load() | |
143 self._fillCachedValues(profile) | |
144 | |
145 def profileDisconnected(self, profile): | |
146 log.debug(u"Deleting profile cache for avatars") | |
147 del self.cache[profile] | |
148 | |
149 def updateCache(self, client, jid_, name, value): | 158 def updateCache(self, client, jid_, name, value): |
150 """update cache value | 159 """update cache value |
151 | 160 |
152 save value in memory in case of change | 161 save value in memory in case of change |
153 @param jid_(jid.JID): jid of the owner of the vcard | 162 @param jid_(jid.JID): jid of the owner of the vcard |
154 @param name(str): name of the item which changed | 163 @param name(str): name of the item which changed |
155 @param value(unicode): new value of the item | 164 @param value(unicode, None): new value of the item |
156 """ | 165 None to delete |
157 if jid_.resource: | 166 """ |
158 if not self.isRoom(client, jid_): | 167 jid_ = self.getBareOrFull(client, jid_) |
159 # VCard are retrieved with bare jid | 168 jid_s = jid_.full() |
160 # but MUC room is a special case | 169 |
161 jid_ = jid.userhostJID() | 170 if value is None: |
162 | 171 try: |
163 self.host.memory.updateEntityData(jid_, name, value, profile_key=client.profile) | 172 self.host.memory.delEntityDatum(jid_, name, client.profile) |
164 if name in CACHED_DATA: | 173 except (KeyError, exceptions.UnknownEntityError): |
165 jid_s = jid_.userhost() | 174 pass |
166 self.cache[client.profile].setdefault(jid_s, {})[name] = value | 175 if name in CACHED_DATA: |
167 self.cache[client.profile].force(jid_s) | 176 try: |
177 del client._cache_0054[jid_s][name] | |
178 except KeyError: | |
179 pass | |
180 else: | |
181 client._cache_0054.force(jid_s) | |
182 else: | |
183 self.host.memory.updateEntityData(jid_, name, value, profile_key=client.profile) | |
184 if name in CACHED_DATA: | |
185 client._cache_0054.setdefault(jid_s, {})[name] = value | |
186 client._cache_0054.force(jid_s) | |
168 | 187 |
169 def getCache(self, client, entity_jid, name): | 188 def getCache(self, client, entity_jid, name): |
170 """return cached value for jid | 189 """return cached value for jid |
171 | 190 |
172 @param entity_jid: target contact | 191 @param entity_jid: target contact |
173 @param name: name of the value ('nick' or 'avatar') | 192 @param name: name of the value ('nick' or 'avatar') |
174 @return: wanted value or None""" | 193 @return: wanted value or None""" |
175 if entity_jid.resource: | 194 entity_jid = self.getBareOrFull(client, entity_jid) |
176 if not self.isRoom(client, entity_jid): | |
177 # VCard are retrieved with bare jid | |
178 # but MUC room is a special case | |
179 entity_jid = jid.userhostJID() | |
180 try: | 195 try: |
181 data = self.host.memory.getEntityData(entity_jid, [name], client.profile) | 196 data = self.host.memory.getEntityData(entity_jid, [name], client.profile) |
182 except exceptions.UnknownEntityError: | 197 except exceptions.UnknownEntityError: |
183 return None | 198 return None |
184 return data.get(name) | 199 return data.get(name) |
185 | 200 |
186 def _getFilename(self, hash_): | 201 def savePhoto(self, client, photo_elt, entity_jid): |
187 """Get filename from hash | 202 """Parse a <PHOTO> photo_elt and save the picture""" |
188 | 203 # XXX: this method is launched in a separate thread |
189 @param hash_: hash of the avatar | 204 try: |
190 @return (str): absolute filename of the avatar | 205 mime_type = unicode(photo_elt.elements(NS_VCARD, 'TYPE').next()) |
191 """ | 206 except StopIteration: |
192 return os.path.join(self.avatar_path, hash_) | 207 log.warning(u"no MIME type found, assuming image/png") |
193 | 208 mime_type = u"image/png" |
194 def saveAvatarFile(self, data, hash_): | |
195 """Save the avatar picture if it doesn't already exists | |
196 | |
197 @param data(str): binary image of the avatar | |
198 @param hash_(str): hash of the binary data (will be used for the filename) | |
199 """ | |
200 filename = self._getFilename(hash_) | |
201 if not os.path.exists(filename): | |
202 with open(filename, 'wb') as file_: | |
203 file_.write(data) | |
204 log.debug(_(u"file saved to %s") % hash_) | |
205 else: | 209 else: |
206 log.debug(_(u"file [%s] already in cache") % hash_) | 210 if not mime_type: |
207 | 211 log.warning(u"empty MIME type, assuming image/png") |
208 def savePhoto(self, photo_elt, target): | 212 mime_type = u"image/png" |
209 """Parse a <PHOTO> photo_elt and save the picture""" | 213 elif mime_type not in ("image/gif", "image/jpeg", "image/png"): |
210 try: | 214 if mime_type == "image/x-png": |
211 type_ = unicode(photo_elt.elements(NS_VCARD, 'TYPE').next()) | 215 # XXX: this old MIME type is still used by some clients |
216 mime_type = "image/png" | |
217 else: | |
218 # TODO: handle other image formats (svg?) | |
219 log.warning(u"following avatar image format is not handled: {type} [{jid}]".format( | |
220 type=mime_type, jid=entity_jid.full())) | |
221 raise Failure(exceptions.DataError()) | |
222 | |
223 ext = mimetypes.guess_extension(mime_type, strict=False) | |
224 assert ext is not None | |
225 log.debug(u'photo of type {type} with extension {ext} found [{jid}]'.format( | |
226 type=mime_type, ext=ext, jid=entity_jid.full())) | |
227 try: | |
228 buf = str(photo_elt.elements(NS_VCARD, 'BINVAL').next()) | |
212 except StopIteration: | 229 except StopIteration: |
213 type_ = None | 230 log.warning(u"BINVAL element not found") |
214 else: | 231 raise Failure(exceptions.NotFound()) |
215 if type_ and type_ not in ("image/gif", "image/jpeg", "image/png"): | |
216 # TODO: handle other image formats (svg?) | |
217 log.warning(u"following avatar image format is not handled: {type} [{jid}]".format( | |
218 type=type_, jid=target.full())) | |
219 return | |
220 log.debug(u'Photo of type {type} found [{jid}]'.format( | |
221 type=type_, jid=target.full())) | |
222 try: | |
223 bin_elt = photo_elt.elements(NS_VCARD, 'BINVAL').next() | |
224 except StopIteration: | |
225 return | |
226 buf = str(bin_elt) | |
227 if not buf: | 232 if not buf: |
228 log.warning(u"empty avatar for {jid}".format(jid=target.full())) | 233 log.warning(u"empty avatar for {jid}".format(jid=entity_jid.full())) |
229 return | 234 raise Failure(exceptions.NotFound()) |
230 log.debug(_(u'Decoding binary')) | 235 log.debug(_(u'Decoding binary')) |
231 decoded = b64decode(buf) | 236 decoded = b64decode(buf) |
237 del buf | |
232 image_hash = sha1(decoded).hexdigest() | 238 image_hash = sha1(decoded).hexdigest() |
233 self.saveAvatarFile(decoded, image_hash) | 239 with client.cache.cacheData( |
240 PLUGIN_INFO['import_name'], | |
241 image_hash, | |
242 mime_type, | |
243 # we keep in cache for 1 year | |
244 MAX_AGE | |
245 ) as f: | |
246 f.write(decoded) | |
234 return image_hash | 247 return image_hash |
235 | 248 |
236 @defer.inlineCallbacks | 249 @defer.inlineCallbacks |
237 def vCard2Dict(self, client, vcard, target): | 250 def vCard2Dict(self, client, vcard, entity_jid): |
238 """Convert a VCard to a dict, and save binaries""" | 251 """Convert a VCard to a dict, and save binaries""" |
239 log.debug(_("parsing vcard")) | 252 log.debug((u"parsing vcard")) |
240 dictionary = {} | 253 vcard_dict = {} |
241 | 254 |
242 for elem in vcard.elements(): | 255 for elem in vcard.elements(): |
243 if elem.name == 'FN': | 256 if elem.name == 'FN': |
244 dictionary['fullname'] = unicode(elem) | 257 vcard_dict['fullname'] = unicode(elem) |
245 elif elem.name == 'NICKNAME': | 258 elif elem.name == 'NICKNAME': |
246 dictionary['nick'] = unicode(elem) | 259 vcard_dict['nick'] = unicode(elem) |
247 self.updateCache(client, target, 'nick', dictionary['nick']) | 260 self.updateCache(client, entity_jid, 'nick', vcard_dict['nick']) |
248 elif elem.name == 'URL': | 261 elif elem.name == 'URL': |
249 dictionary['website'] = unicode(elem) | 262 vcard_dict['website'] = unicode(elem) |
250 elif elem.name == 'EMAIL': | 263 elif elem.name == 'EMAIL': |
251 dictionary['email'] = unicode(elem) | 264 vcard_dict['email'] = unicode(elem) |
252 elif elem.name == 'BDAY': | 265 elif elem.name == 'BDAY': |
253 dictionary['birthday'] = unicode(elem) | 266 vcard_dict['birthday'] = unicode(elem) |
254 elif elem.name == 'PHOTO': | 267 elif elem.name == 'PHOTO': |
255 # TODO: handle EXTVAL | 268 # TODO: handle EXTVAL |
256 avatar_hash = yield threads.deferToThread(self.savePhoto, elem, target) | 269 try: |
257 if avatar_hash is None: # can happen e.g. in case of empty photo elem | 270 avatar_hash = yield threads.deferToThread( |
258 continue | 271 self.savePhoto, client, elem, entity_jid) |
259 dictionary['avatar'] = avatar_hash | 272 except (exceptions.DataError, exceptions.NotFound) as e: |
260 self.updateCache(client, target, 'avatar', dictionary['avatar']) | 273 avatar_hash = '' |
274 vcard_dict['avatar'] = avatar_hash | |
275 except Exception as e: | |
276 log.error(u"avatar saving error: {}".format(e)) | |
277 avatar_hash = None | |
278 else: | |
279 vcard_dict['avatar'] = avatar_hash | |
280 self.updateCache(client, entity_jid, 'avatar', avatar_hash) | |
261 else: | 281 else: |
262 log.debug(_('FIXME: [%s] VCard tag is not managed yet') % elem.name) | 282 log.debug(u'FIXME: [{}] VCard tag is not managed yet'.format(elem.name)) |
263 | 283 |
264 # if a data in cache doesn't exist anymore, we need to reset it | 284 # if a data in cache doesn't exist anymore, we need to delete it |
265 # so we check CACHED_DATA no gotten (i.e. not in dictionary keys) | 285 # so we check CACHED_DATA no gotten (i.e. not in vcard_dict keys) |
266 # and we reset them | 286 # and we reset them |
267 for datum in CACHED_DATA.difference(dictionary.keys()): | 287 for datum in CACHED_DATA.difference(vcard_dict.keys()): |
268 log.debug(u"reseting vcard datum [{datum}] for {entity}".format(datum=datum, entity=target.full())) | 288 log.debug(u"reseting vcard datum [{datum}] for {entity}".format(datum=datum, entity=entity_jid.full())) |
269 self.updateCache(client, target, datum, '') | 289 self.updateCache(client, entity_jid, datum, None) |
270 | 290 |
271 defer.returnValue(dictionary) | 291 defer.returnValue(vcard_dict) |
272 | 292 |
273 def _getCardCb(self, iq_elt, to_jid, client): | 293 def _vCardCb(self, iq_elt, to_jid, client): |
274 """Called after the first get IQ""" | 294 """Called after the first get IQ""" |
275 log.debug(_("VCard found")) | 295 log.debug(_("VCard found")) |
276 | 296 |
277 try: | 297 try: |
278 vcard_elt = iq_elt.elements(NS_VCARD, "vCard").next() | 298 vcard_elt = iq_elt.elements(NS_VCARD, "vCard").next() |
284 except KeyError: | 304 except KeyError: |
285 from_jid = client.jid.userhostJID() | 305 from_jid = client.jid.userhostJID() |
286 d = self.vCard2Dict(client, vcard_elt, from_jid) | 306 d = self.vCard2Dict(client, vcard_elt, from_jid) |
287 return d | 307 return d |
288 | 308 |
289 def _getCardEb(self, failure_, to_jid, client): | 309 def _vCardEb(self, failure_, to_jid, client): |
290 """Called when something is wrong with registration""" | 310 """Called when something is wrong with registration""" |
291 log.warning(u"Can't get vCard for {jid}: {failure}".format(jid=to_jid.full, failure=failure_)) | 311 log.warning(u"Can't get vCard for {jid}: {failure}".format(jid=to_jid.full, failure=failure_)) |
292 self.updateCache(client, to_jid, "avatar", '') | 312 self.updateCache(client, to_jid, "avatar", None) |
293 | 313 |
294 def _getCard(self, target_s, profile_key=C.PROF_KEY_NONE): | 314 def getCard(self, client, entity_jid): |
295 client = self.host.getClient(profile_key) | |
296 return self.getCard(client, jid.JID(target_s)) | |
297 | |
298 def getCard(self, client, target): | |
299 """Ask server for VCard | 315 """Ask server for VCard |
300 | 316 |
301 @param target(jid.JID): jid from which we want the VCard | 317 @param entity_jid(jid.JID): jid from which we want the VCard |
302 @result: id to retrieve the profile | 318 @result: id to retrieve the profile |
303 """ | 319 """ |
304 target_bare = target.userhostJID() | 320 entity_jid = self.getBareOrFull(client, entity_jid) |
305 to_jid = target if self.isRoom(client, target_bare) else target_bare | 321 log.debug(u"Asking for {}'s VCard".format(entity_jid.full())) |
306 log.debug(_(u"Asking for %s's VCard") % to_jid.userhost()) | |
307 reg_request = client.IQ('get') | 322 reg_request = client.IQ('get') |
308 reg_request["from"] = client.jid.full() | 323 reg_request["from"] = client.jid.full() |
309 reg_request["to"] = to_jid.full() | 324 reg_request["to"] = entity_jid.full() |
310 reg_request.addElement('vCard', NS_VCARD) | 325 reg_request.addElement('vCard', NS_VCARD) |
311 d = reg_request.send(to_jid.full()).addCallbacks(self._getCardCb, self._getCardEb, callbackArgs=[to_jid, client], errbackArgs=[to_jid, client]) | 326 d = reg_request.send(entity_jid.full()).addCallbacks(self._vCardCb, self._vCardEb, callbackArgs=[entity_jid, client], errbackArgs=[entity_jid, client]) |
312 return d | 327 return d |
313 | 328 |
314 def getAvatarFile(self, avatar_hash): | 329 def _getCardCb(self, dummy, client, entity): |
315 """Give the full path of avatar from hash | 330 try: |
316 @param avatar_hash(unicode): SHA1 hash | 331 return client._cache_0054[entity.full()]['avatar'] |
317 @return(unicode): full_path or empty string if avatar_hash is empty | 332 except KeyError: |
318 or if avatar is not found in cache | 333 raise Failure(exceptions.NotFound()) |
319 """ | 334 |
320 if not avatar_hash: | 335 def _getAvatar(self, entity, cache_only, hash_only, profile): |
321 return "" | |
322 filename = self.avatar_path + '/' + avatar_hash | |
323 if not os.path.exists(filename): | |
324 log.error(_(u"Asking for an uncached avatar [%s]") % avatar_hash) | |
325 return "" | |
326 return filename | |
327 | |
328 def _getAvatar(self, entity, profile): | |
329 client = self.host.getClient(profile) | 336 client = self.host.getClient(profile) |
330 return self.getAvatar(client, jid.JID(entity)) | 337 d = self.getAvatar(client, jid.JID(entity)) |
331 | 338 d.addErrback(lambda dummy: '') |
332 def _getAvatarGotCard(self, dummy, client, entity): | 339 |
333 try: | 340 return d |
334 self.cache[client.profile][entity.full()]['avatar'] | 341 |
342 def getAvatar(self, client, entity, cache_only=True, hash_only=False): | |
343 """get avatar full path or hash | |
344 | |
345 if avatar is not in local cache, it will be requested to the server | |
346 @param entity(jid.JID): entity to get avatar from | |
347 @param cache_only(bool): if False, will request vCard if avatar is | |
348 not in cache | |
349 @param hash_only(bool): if True only return hash, not full path | |
350 @raise exceptions.NotFound: no avatar found | |
351 """ | |
352 entity = self.getBareOrFull(client, entity) | |
353 full_path = None | |
354 | |
355 try: | |
356 # we first check if we have avatar in cache | |
357 avatar_hash = client._cache_0054[entity.full()]['avatar'] | |
358 if avatar_hash: | |
359 # avatar is known and exists | |
360 full_path = client.cache.getFilePath(avatar_hash) | |
361 if full_path is None: | |
362 # cache file is not available (probably expired) | |
363 raise KeyError | |
364 else: | |
365 # avatar has already been checked but it is not set | |
366 full_path = u'' | |
335 except KeyError: | 367 except KeyError: |
336 return "" | 368 # avatar is not in cache |
337 | 369 if cache_only: |
338 def getAvatar(self, client, entity): | 370 return defer.fail(Failure(exceptions.NotFound())) |
339 try: | 371 # we request vCard to get avatar |
340 avatar_hash = self.cache[client.profile][entity.full()]['avatar'] | |
341 except KeyError: | |
342 d = self.getCard(client, entity) | 372 d = self.getCard(client, entity) |
343 d.addCallback(self._getAvatarGotCard, client, entity) | 373 d.addCallback(self._getCardCb, client, entity) |
344 else: | 374 else: |
375 # avatar is in cache, we can return hash | |
345 d = defer.succeed(avatar_hash) | 376 d = defer.succeed(avatar_hash) |
346 d.addCallback(self.getAvatarFile) | 377 |
378 if not hash_only: | |
379 # full path is requested | |
380 if full_path is None: | |
381 d.addCallback(client.cache.getFilePath) | |
382 else: | |
383 d.addCallback(lambda dummy: full_path) | |
347 return d | 384 return d |
348 | 385 |
349 def _buildSetAvatar(self, vcard_set, filepath): | 386 def _buildSetAvatar(self, client, vcard_set, file_path): |
350 try: | 387 # XXX: this method is executed in a separate thread |
351 img = Image.open(filepath) | 388 try: |
389 img = Image.open(file_path) | |
352 except IOError: | 390 except IOError: |
353 return Failure(exceptions.DataError("Can't open image")) | 391 return Failure(exceptions.DataError(u"Can't open image")) |
354 | 392 |
355 if img.size != AVATAR_DIM: | 393 if img.size != AVATAR_DIM: |
356 img.thumbnail(AVATAR_DIM, Image.ANTIALIAS) | 394 img.thumbnail(AVATAR_DIM, Image.ANTIALIAS) |
357 if img.size[0] != img.size[1]: # we need to crop first | 395 if img.size[0] != img.size[1]: # we need to crop first |
358 left, upper = (0, 0) | 396 left, upper = (0, 0) |
370 | 408 |
371 vcard_elt = vcard_set.addElement('vCard', NS_VCARD) | 409 vcard_elt = vcard_set.addElement('vCard', NS_VCARD) |
372 photo_elt = vcard_elt.addElement('PHOTO') | 410 photo_elt = vcard_elt.addElement('PHOTO') |
373 photo_elt.addElement('TYPE', content='image/png') | 411 photo_elt.addElement('TYPE', content='image/png') |
374 photo_elt.addElement('BINVAL', content=b64encode(img_buf.getvalue())) | 412 photo_elt.addElement('BINVAL', content=b64encode(img_buf.getvalue())) |
375 img_hash = sha1(img_buf.getvalue()).hexdigest() | 413 image_hash = sha1(img_buf.getvalue()).hexdigest() |
376 self.saveAvatarFile(img_buf.getvalue(), img_hash) | 414 with client.cache.cacheData( |
377 return (vcard_set, img_hash) | 415 PLUGIN_INFO['import_name'], |
378 | 416 image_hash, |
379 def setAvatar(self, filepath, profile_key=C.PROF_KEY_NONE): | 417 "image/png", |
418 MAX_AGE | |
419 ) as f: | |
420 f.write(img_buf.getvalue()) | |
421 return vcard_set, image_hash | |
422 | |
423 def _setAvatar(self, file_path, profile_key=C.PROF_KEY_NONE): | |
424 client = self.host.getClient(profile_key) | |
425 return self.setAvatar(client, file_path) | |
426 | |
427 def setAvatar(self, client, file_path): | |
380 """Set avatar of the profile | 428 """Set avatar of the profile |
381 @param filepath: path of the image of the avatar""" | 429 |
430 @param file_path: path of the image of the avatar | |
431 """ | |
382 #TODO: This is a temporary way of setting the avatar, as other VCard information is not managed. | 432 #TODO: This is a temporary way of setting the avatar, as other VCard information is not managed. |
383 # A proper full VCard management should be done (and more generaly a public/private profile) | 433 # A proper full VCard management should be done (and more generaly a public/private profile) |
384 client = self.host.getClient(profile_key) | 434 vcard_set = client.IQ() |
385 | 435 d = threads.deferToThread(self._buildSetAvatar, client, vcard_set, file_path) |
386 vcard_set = IQ(client.xmlstream, 'set') | |
387 d = threads.deferToThread(self._buildSetAvatar, vcard_set, filepath) | |
388 | 436 |
389 def elementBuilt(result): | 437 def elementBuilt(result): |
390 """Called once the image is at the right size/format, and the vcard set element is build""" | 438 """Called once the image is at the right size/format, and the vcard set element is build""" |
391 set_avatar_elt, img_hash = result | 439 set_avatar_elt, image_hash = result |
392 self.updateCache(client, client.jid.userhostJID(), 'avatar', img_hash) | 440 self.updateCache(client, client.jid.userhostJID(), 'avatar', image_hash) |
393 return set_avatar_elt.send().addCallback(lambda ignore: client.presence.available()) # FIXME: should send the current presence, not always "available" ! | 441 return set_avatar_elt.send().addCallback(lambda ignore: client.presence.available()) # FIXME: should send the current presence, not always "available" ! |
394 | 442 |
395 d.addCallback(elementBuilt) | 443 d.addCallback(elementBuilt) |
396 | 444 |
397 return d | 445 return d |
411 return [disco.DiscoFeature(NS_VCARD)] | 459 return [disco.DiscoFeature(NS_VCARD)] |
412 | 460 |
413 def getDiscoItems(self, requestor, target, nodeIdentifier=''): | 461 def getDiscoItems(self, requestor, target, nodeIdentifier=''): |
414 return [] | 462 return [] |
415 | 463 |
464 def _checkAvatarHash(self, dummy, client, entity, given_hash): | |
465 """check that hash in cash (i.e. computed hash) is the same as given one""" | |
466 # XXX: if they differ, the avater will be requested on each connection | |
467 # TODO: try to avoid re-requesting avatar in this case | |
468 computed_hash = self.plugin_parent.getCache(client, entity, 'avatar') | |
469 if computed_hash != given_hash: | |
470 log.warning(u"computed hash differs from given hash for {entity}:\n" | |
471 "computed: {computed}\ngiven: {given}".format( | |
472 entity=entity, computed=computed_hash, given=given_hash)) | |
473 | |
416 def update(self, presence): | 474 def update(self, presence): |
417 """Called on <presence/> stanza with vcard data | 475 """Called on <presence/> stanza with vcard data |
418 | 476 |
419 Check for avatar information, and get VCard if needed | 477 Check for avatar information, and get VCard if needed |
420 @param presend(domish.Element): <presence/> stanza | 478 @param presend(domish.Element): <presence/> stanza |
421 """ | 479 """ |
422 from_jid = jid.JID(presence['from']) | 480 client = self.parent |
423 if from_jid.resource and not self.plugin_parent.isRoom(self.parent, from_jid): | 481 entity_jid = self.plugin_parent.getBareOrFull(client, jid.JID(presence['from'])) |
424 from_jid = from_jid.userhostJID() | |
425 #FIXME: wokkel's data_form should be used here | 482 #FIXME: wokkel's data_form should be used here |
426 try: | 483 try: |
427 x_elt = presence.elements(NS_VCARD_UPDATE, 'x').next() | 484 x_elt = presence.elements(NS_VCARD_UPDATE, 'x').next() |
428 except StopIteration: | 485 except StopIteration: |
429 return | 486 return |
431 try: | 488 try: |
432 photo_elt = x_elt.elements(NS_VCARD_UPDATE, 'photo').next() | 489 photo_elt = x_elt.elements(NS_VCARD_UPDATE, 'photo').next() |
433 except StopIteration: | 490 except StopIteration: |
434 return | 491 return |
435 | 492 |
436 hash_ = str(photo_elt) | 493 hash_ = unicode(photo_elt).strip() |
494 if hash_ == C.HASH_SHA1_EMPTY: | |
495 hash_ = u'' | |
496 old_avatar = self.plugin_parent.getCache(client, entity_jid, 'avatar') | |
497 | |
498 if old_avatar == hash_: | |
499 # no change, we can return... | |
500 if hash_: | |
501 # ...but we double check that avatar is in cache | |
502 file_path = client.cache.getFilePath(hash_) | |
503 if file_path is None: | |
504 log.error(u"Avatar for [{}] should be in cache but it is not! We get it".format(entity_jid.full())) | |
505 self.plugin_parent.getCard(client, entity_jid) | |
506 else: | |
507 log.debug(u"avatar for {} already in cache".format(entity_jid.full())) | |
508 return | |
509 | |
437 if not hash_: | 510 if not hash_: |
511 # the avatar has been removed | |
512 # XXX: we use empty string instead of None to indicate that we took avatar | |
513 # but it is empty on purpose | |
514 self.plugin_parent.updateCache(client, entity_jid, 'avatar', '') | |
438 return | 515 return |
439 old_avatar = self.plugin_parent.getCache(self.parent, from_jid, 'avatar') | 516 |
440 filename = self.plugin_parent._getFilename(hash_) | 517 file_path = client.cache.getFilePath(hash_) |
441 if not old_avatar or old_avatar != hash_: | 518 if file_path is not None: |
442 if os.path.exists(filename): | 519 log.debug(u"New avatar found for [{}], it's already in cache, we use it".format(entity_jid.full())) |
443 log.debug(u"New avatar found for [{}], it's already in cache, we use it".format(from_jid.full())) | 520 self.plugin_parent.updateCache(client, entity_jid, 'avatar', hash_) |
444 self.plugin_parent.updateCache(self.parent, from_jid, 'avatar', hash_) | |
445 else: | |
446 log.debug(u'New avatar found for [{}], requesting vcard'.format(from_jid.full())) | |
447 self.plugin_parent.getCard(self.parent, from_jid) | |
448 else: | 521 else: |
449 if os.path.exists(filename): | 522 log.debug(u'New avatar found for [{}], requesting vcard'.format(entity_jid.full())) |
450 log.debug(u"avatar for {} already in cache".format(from_jid.full())) | 523 d = self.plugin_parent.getCard(client, entity_jid) |
451 else: | 524 d.addCallback(self._checkAvatarHash, client, entity_jid, hash_) |
452 log.error(u"Avatar for [{}] should be in cache but it is not ! We get it".format(from_jid.full())) | |
453 self.plugin_parent.getCard(self.parent, from_jid) | |
454 |