comparison src/plugins/plugin_xep_0054.py @ 1367:f71a0fc26886

merged branch frontends_multi_profiles
author Goffi <goffi@goffi.org>
date Wed, 18 Mar 2015 10:52:28 +0100
parents 6dbeb2ef966c
children 069ad98b360d
comparison
equal deleted inserted replaced
1295:1e3b1f9ad6e2 1367:f71a0fc26886
34 from wokkel import disco, iwokkel 34 from wokkel import disco, iwokkel
35 35
36 from base64 import b64decode, b64encode 36 from base64 import b64decode, b64encode
37 from hashlib import sha1 37 from hashlib import sha1
38 from sat.core import exceptions 38 from sat.core import exceptions
39 from sat.memory.persistent import PersistentDict 39 from sat.memory import persistent
40 from PIL import Image 40 from PIL import Image
41 from cStringIO import StringIO 41 from cStringIO import StringIO
42 42
43 try: 43 try:
44 from twisted.words.protocols.xmlstream import XMPPHandler 44 from twisted.words.protocols.xmlstream import XMPPHandler
53 VCARD_REQUEST = IQ_GET + '/vCard[@xmlns="' + NS_VCARD + '"]' # TODO: manage requests 53 VCARD_REQUEST = IQ_GET + '/vCard[@xmlns="' + NS_VCARD + '"]' # TODO: manage requests
54 54
55 PRESENCE = '/presence' 55 PRESENCE = '/presence'
56 NS_VCARD_UPDATE = 'vcard-temp:x:update' 56 NS_VCARD_UPDATE = 'vcard-temp:x:update'
57 VCARD_UPDATE = PRESENCE + '/x[@xmlns="' + NS_VCARD_UPDATE + '"]' 57 VCARD_UPDATE = PRESENCE + '/x[@xmlns="' + NS_VCARD_UPDATE + '"]'
58
59 CACHED_DATA = {'avatar', 'nick'}
58 60
59 PLUGIN_INFO = { 61 PLUGIN_INFO = {
60 "name": "XEP 0054 Plugin", 62 "name": "XEP 0054 Plugin",
61 "import_name": "XEP-0054", 63 "import_name": "XEP-0054",
62 "type": "XEP", 64 "type": "XEP",
77 log.info(_("Plugin XEP_0054 initialization")) 79 log.info(_("Plugin XEP_0054 initialization"))
78 self.host = host 80 self.host = host
79 self.avatar_path = os.path.join(self.host.memory.getConfig('', 'local_dir'), AVATAR_PATH) 81 self.avatar_path = os.path.join(self.host.memory.getConfig('', 'local_dir'), AVATAR_PATH)
80 if not os.path.exists(self.avatar_path): 82 if not os.path.exists(self.avatar_path):
81 os.makedirs(self.avatar_path) 83 os.makedirs(self.avatar_path)
82 self.avatars_cache = PersistentDict(NS_VCARD) 84 self.cache = {}
83 self.avatars_cache.load() # FIXME: resulting deferred must be correctly managed 85 host.bridge.addMethod("getCard", ".plugin", in_sign='ss', out_sign='s', method=self._getCard)
84 host.bridge.addMethod("getCard", ".plugin", in_sign='ss', out_sign='s', method=self.getCard)
85 host.bridge.addMethod("getAvatarFile", ".plugin", in_sign='s', out_sign='s', method=self.getAvatarFile) 86 host.bridge.addMethod("getAvatarFile", ".plugin", in_sign='s', out_sign='s', method=self.getAvatarFile)
86 host.bridge.addMethod("setAvatar", ".plugin", in_sign='ss', out_sign='', method=self.setAvatar, async=True) 87 host.bridge.addMethod("setAvatar", ".plugin", in_sign='ss', out_sign='', method=self.setAvatar, async=True)
87 host.trigger.add("presence_available", self.presenceTrigger) 88 host.trigger.add("presence_available", self.presenceTrigger)
89 host.memory.setSignalOnUpdate("avatar")
90 host.memory.setSignalOnUpdate("nick")
88 91
89 def getHandler(self, profile): 92 def getHandler(self, profile):
90 return XEP_0054_handler(self) 93 return XEP_0054_handler(self)
91 94
92 def presenceTrigger(self, presence_elt, client): 95 def presenceTrigger(self, presence_elt, client):
93 if client.jid.userhost() in self.avatars_cache: 96 if client.jid.userhost() in self.cache[client.profile]:
94 x_elt = domish.Element((NS_VCARD_UPDATE, 'x')) 97 x_elt = domish.Element((NS_VCARD_UPDATE, 'x'))
95 x_elt.addElement('photo', content=self.avatars_cache[client.jid.userhost()]) 98 x_elt.addElement('photo', content=self.cache[client.profile][client.jid.userhost()]['avatar'])
96 presence_elt.addChild(x_elt) 99 presence_elt.addChild(x_elt)
97 100
98 return True 101 return True
99 102
100 def _fillCachedValues(self, result, client): 103 def _fillCachedValues(self, profile):
101 #FIXME: this is really suboptimal, need to be reworked 104 #FIXME: this is really suboptimal, need to be reworked
102 # the current naive approach keeps a map between all jids of all profiles 105 # the current naive approach keeps a map between all jids of all profiles
103 # in persistent cache, and check if cached jid are in roster, then put avatar 106 # in persistent cache, then put avatar
104 # hashs in memory. 107 # hashs in memory. Hashed should be shared between profiles
105 for _jid in client.roster.getBareJids() + [client.jid.userhost()]: 108 for jid_s, data in self.cache[profile].iteritems():
106 if _jid in self.avatars_cache: 109 jid_ = jid.JID(jid_s)
107 self.host.memory.updateEntityData(jid.JID(_jid), "avatar", self.avatars_cache[_jid], client.profile) 110 for name in CACHED_DATA:
108 111 try:
109 def profileConnected(self, profile): 112 self.host.memory.updateEntityData(jid_, name, data[name], silent=True, profile_key=profile)
110 client = self.host.getClient(profile) 113 except KeyError:
111 client.roster.got_roster.addCallback(self._fillCachedValues, client) 114 pass
112 115
113 def update_cache(self, jid, name, value, profile): 116 @defer.inlineCallbacks
117 def profileConnecting(self, profile):
118 self.cache[profile] = persistent.PersistentBinaryDict(NS_VCARD, profile)
119 yield self.cache[profile].load()
120 self._fillCachedValues(profile)
121
122 def profileDisconnected(self, profile):
123 log.debug(u"Deleting profile cache for avatars")
124 del self.cache[profile]
125
126 def updateCache(self, jid_, name, value, profile):
114 """update cache value 127 """update cache value
115 - save value in memory in case of change 128
116 @param jid: jid of the owner of the vcard 129 save value in memory in case of change
117 @param name: name of the item which changed 130 @param jid_(jid.JID): jid of the owner of the vcard
118 @param value: new value of the item 131 @param name(str): name of the item which changed
119 @param profile: profile which received the update 132 @param value(unicode): new value of the item
120 """ 133 @param profile(unicode): profile which received the update
121 try: 134 """
122 cached = self.host.memory.getEntityData(jid, [name], profile) 135 assert not jid_.resource # VCard are retrieved with bare jid
123 except exceptions.UnknownEntityError: 136 self.host.memory.updateEntityData(jid_, name, value, profile_key=profile)
124 cached = {} 137 if name in CACHED_DATA:
125 if not name in cached or cached[name] != value: 138 jid_s = jid_.userhost()
126 self.host.memory.updateEntityData(jid, name, value, profile) 139 self.cache[profile].setdefault(jid_s, {})[name] = value
127 if name == "avatar": 140 self.cache[profile].force(jid_s)
128 self.avatars_cache[jid.userhost()] = value 141
129 142 def getCache(self, entity_jid, name, profile):
130 def get_cache(self, entity_jid, name, profile):
131 """return cached value for jid 143 """return cached value for jid
144
132 @param entity_jid: target contact 145 @param entity_jid: target contact
133 @param name: name of the value ('nick' or 'avatar') 146 @param name: name of the value ('nick' or 'avatar')
134 @param profile: %(doc_profile)s 147 @param profile: %(doc_profile)s
135 @return: wanted value or None""" 148 @return: wanted value or None"""
149 assert not entity_jid.resource # VCard are retrieved with bare jid
136 try: 150 try:
137 data = self.host.memory.getEntityData(entity_jid, [name], profile) 151 data = self.host.memory.getEntityData(entity_jid, [name], profile)
138 except exceptions.UnknownEntityError: 152 except exceptions.UnknownEntityError:
139 return None 153 return None
140 return data.get(name) 154 return data.get(name)
141 155
142 def save_photo(self, photo_xml): 156 def _getFilename(self, hash_):
157 """Get filename from hash
158
159 @param hash_: hash of the avatar
160 @return (str): absolute filename of the avatar
161 """
162 return os.path.join(self.avatar_path, hash_)
163
164 def saveAvatarFile(self, data, hash_):
165 """Save the avatar picture if it doesn't already exists
166
167 @param data(str): binary image of the avatar
168 @param hash_(str): hash of the binary data (will be used for the filename)
169 """
170 filename = self._getFilename(hash_)
171 if not os.path.exists(filename):
172 with open(filename, 'wb') as file_:
173 file_.write(data)
174 log.debug(_("file saved to %s") % hash_)
175 else:
176 log.debug(_("file [%s] already in cache") % hash_)
177
178 def savePhoto(self, photo_xml):
143 """Parse a <PHOTO> elem and save the picture""" 179 """Parse a <PHOTO> elem and save the picture"""
144 for elem in photo_xml.elements(): 180 for elem in photo_xml.elements():
145 if elem.name == 'TYPE': 181 if elem.name == 'TYPE':
146 log.debug(_('Photo of type [%s] found') % str(elem)) 182 log.debug(_('Photo of type [%s] found') % str(elem))
147 if elem.name == 'BINVAL': 183 if elem.name == 'BINVAL':
148 log.debug(_('Decoding binary')) 184 log.debug(_('Decoding binary'))
149 decoded = b64decode(str(elem)) 185 decoded = b64decode(str(elem))
150 image_hash = sha1(decoded).hexdigest() 186 image_hash = sha1(decoded).hexdigest()
151 filename = self.avatar_path + '/' + image_hash 187 self.saveAvatarFile(decoded, image_hash)
152 if not os.path.exists(filename):
153 with open(filename, 'wb') as file_:
154 file_.write(decoded)
155 log.debug(_("file saved to %s") % image_hash)
156 else:
157 log.debug(_("file [%s] already in cache") % image_hash)
158 return image_hash 188 return image_hash
159 189
160 @defer.inlineCallbacks 190 @defer.inlineCallbacks
161 def vCard2Dict(self, vcard, target, profile): 191 def vCard2Dict(self, vcard, target, profile):
162 """Convert a VCard to a dict, and save binaries""" 192 """Convert a VCard to a dict, and save binaries"""
166 for elem in vcard.elements(): 196 for elem in vcard.elements():
167 if elem.name == 'FN': 197 if elem.name == 'FN':
168 dictionary['fullname'] = unicode(elem) 198 dictionary['fullname'] = unicode(elem)
169 elif elem.name == 'NICKNAME': 199 elif elem.name == 'NICKNAME':
170 dictionary['nick'] = unicode(elem) 200 dictionary['nick'] = unicode(elem)
171 self.update_cache(target, 'nick', dictionary['nick'], profile) 201 self.updateCache(target, 'nick', dictionary['nick'], profile)
172 elif elem.name == 'URL': 202 elif elem.name == 'URL':
173 dictionary['website'] = unicode(elem) 203 dictionary['website'] = unicode(elem)
174 elif elem.name == 'EMAIL': 204 elif elem.name == 'EMAIL':
175 dictionary['email'] = unicode(elem) 205 dictionary['email'] = unicode(elem)
176 elif elem.name == 'BDAY': 206 elif elem.name == 'BDAY':
177 dictionary['birthday'] = unicode(elem) 207 dictionary['birthday'] = unicode(elem)
178 elif elem.name == 'PHOTO': 208 elif elem.name == 'PHOTO':
179 dictionary["avatar"] = yield threads.deferToThread(self.save_photo, elem) 209 dictionary["avatar"] = yield threads.deferToThread(self.savePhoto, elem)
180 if not dictionary["avatar"]: # can happen in case of e.g. empty photo elem 210 if not dictionary["avatar"]: # can happen in case of e.g. empty photo elem
181 del dictionary['avatar'] 211 del dictionary['avatar']
182 else: 212 else:
183 self.update_cache(target, 'avatar', dictionary['avatar'], profile) 213 self.updateCache(target, 'avatar', dictionary['avatar'], profile)
184 else: 214 else:
185 log.info(_('FIXME: [%s] VCard tag is not managed yet') % elem.name) 215 log.info(_('FIXME: [%s] VCard tag is not managed yet') % elem.name)
186 216
217 # if a data in cache doesn't exist anymore, we need to reset it
218 # so we check CACHED_DATA no gotten (i.e. not in dictionary keys)
219 # and we reset them
220 for datum in CACHED_DATA.difference(dictionary.keys()):
221 log.debug(u"reseting vcard datum [{datum}] for {entity}".format(datum=datum, entity=target.full()))
222 self.updateCache(target, datum, '', profile)
223
187 defer.returnValue(dictionary) 224 defer.returnValue(dictionary)
188 225
189 def vcard_ok(self, answer, profile): 226 def _VCardCb(self, answer, profile):
190 """Called after the first get IQ""" 227 """Called after the first get IQ"""
191 log.debug(_("VCard found")) 228 log.debug(_("VCard found"))
192 229
193 if answer.firstChildElement().name == "vCard": 230 if answer.firstChildElement().name == "vCard":
194 _jid, steam = self.host.getJidNStream(profile) 231 _jid, steam = self.host.getJidNStream(profile)
200 d.addCallback(lambda data: self.host.bridge.actionResult("RESULT", answer['id'], data, profile)) 237 d.addCallback(lambda data: self.host.bridge.actionResult("RESULT", answer['id'], data, profile))
201 else: 238 else:
202 log.error(_("FIXME: vCard not found as first child element")) 239 log.error(_("FIXME: vCard not found as first child element"))
203 self.host.bridge.actionResult("SUPPRESS", answer['id'], {}, profile) # FIXME: maybe an error message would be better 240 self.host.bridge.actionResult("SUPPRESS", answer['id'], {}, profile) # FIXME: maybe an error message would be better
204 241
205 def vcard_err(self, failure, profile): 242 def _VCardEb(self, failure, profile):
206 """Called when something is wrong with registration""" 243 """Called when something is wrong with registration"""
207 try: 244 try:
208 log.error(_("Can't find VCard of %s") % failure.value.stanza['from']) 245 log.warning(_("Can't find VCard of %s") % failure.value.stanza['from'])
209 self.host.bridge.actionResult("SUPPRESS", failure.value.stanza['id'], {}, profile) # FIXME: maybe an error message would be better 246 self.host.bridge.actionResult("SUPPRESS", failure.value.stanza['id'], {}, profile) # FIXME: maybe an error message would be better
247 self.updateCache(jid.JID(failure.value.stanza['from']), "avatar", '', profile)
210 except AttributeError: # 'ConnectionLost' object has no attribute 'stanza' 248 except AttributeError: # 'ConnectionLost' object has no attribute 'stanza'
211 log.error(_("Can't find VCard: %s") % failure.getErrorMessage()) 249 log.warning(_("Can't find VCard: %s") % failure.getErrorMessage())
212 250
213 def getCard(self, target_s, profile_key=C.PROF_KEY_NONE): 251 def _getCard(self, target_s, profile_key=C.PROF_KEY_NONE):
252 return self.getCard(jid.JID(target_s), profile_key)
253
254 def getCard(self, target, profile_key=C.PROF_KEY_NONE):
214 """Ask server for VCard 255 """Ask server for VCard
215 @param target_s: jid from which we want the VCard 256
216 @result: id to retrieve the profile""" 257 @param target(jid.JID): jid from which we want the VCard
258 @result: id to retrieve the profile
259 """
217 current_jid, xmlstream = self.host.getJidNStream(profile_key) 260 current_jid, xmlstream = self.host.getJidNStream(profile_key)
218 if not xmlstream: 261 if not xmlstream:
219 log.error(_('Asking vcard for a non-existant or not connected profile')) 262 raise exceptions.ProfileUnknownError('Asking vcard for a non-existant or not connected profile ({})'.format(profile_key))
220 return ""
221 profile = self.host.memory.getProfileName(profile_key) 263 profile = self.host.memory.getProfileName(profile_key)
222 to_jid = jid.JID(target_s) 264 to_jid = target.userhostJID()
223 log.debug(_("Asking for %s's VCard") % to_jid.userhost()) 265 log.debug(_("Asking for %s's VCard") % to_jid.userhost())
224 reg_request = IQ(xmlstream, 'get') 266 reg_request = IQ(xmlstream, 'get')
225 reg_request["from"] = current_jid.full() 267 reg_request["from"] = current_jid.full()
226 reg_request["to"] = to_jid.userhost() 268 reg_request["to"] = to_jid.userhost()
227 reg_request.addElement('vCard', NS_VCARD) 269 reg_request.addElement('vCard', NS_VCARD)
228 reg_request.send(to_jid.userhost()).addCallbacks(self.vcard_ok, self.vcard_err, callbackArgs=[profile], errbackArgs=[profile]) 270 reg_request.send(to_jid.userhost()).addCallbacks(self._VCardCb, self._VCardEb, callbackArgs=[profile], errbackArgs=[profile])
229 return reg_request["id"] 271 return reg_request["id"]
230 272
231 def getAvatarFile(self, avatar_hash): 273 def getAvatarFile(self, avatar_hash):
232 """Give the full path of avatar from hash 274 """Give the full path of avatar from hash
233 @param hash: SHA1 hash 275 @param hash: SHA1 hash
264 vcard_elt = vcard_set.addElement('vCard', NS_VCARD) 306 vcard_elt = vcard_set.addElement('vCard', NS_VCARD)
265 photo_elt = vcard_elt.addElement('PHOTO') 307 photo_elt = vcard_elt.addElement('PHOTO')
266 photo_elt.addElement('TYPE', content='image/png') 308 photo_elt.addElement('TYPE', content='image/png')
267 photo_elt.addElement('BINVAL', content=b64encode(img_buf.getvalue())) 309 photo_elt.addElement('BINVAL', content=b64encode(img_buf.getvalue()))
268 img_hash = sha1(img_buf.getvalue()).hexdigest() 310 img_hash = sha1(img_buf.getvalue()).hexdigest()
311 self.saveAvatarFile(img_buf.getvalue(), img_hash)
269 return (vcard_set, img_hash) 312 return (vcard_set, img_hash)
270 313
271 def setAvatar(self, filepath, profile_key=C.PROF_KEY_NONE): 314 def setAvatar(self, filepath, profile_key=C.PROF_KEY_NONE):
272 """Set avatar of the profile 315 """Set avatar of the profile
273 @param filepath: path of the image of the avatar""" 316 @param filepath: path of the image of the avatar"""
279 d = threads.deferToThread(self._buildSetAvatar, vcard_set, filepath) 322 d = threads.deferToThread(self._buildSetAvatar, vcard_set, filepath)
280 323
281 def elementBuilt(result): 324 def elementBuilt(result):
282 """Called once the image is at the right size/format, and the vcard set element is build""" 325 """Called once the image is at the right size/format, and the vcard set element is build"""
283 set_avatar_elt, img_hash = result 326 set_avatar_elt, img_hash = result
284 self.avatars_cache[client.jid.userhost()] = img_hash # we need to update the hash, so we can send a new presence 327 self.updateCache(client.jid.userhostJID(), 'avatar', img_hash, client.profile)
285 # element with the right hash 328 return set_avatar_elt.send().addCallback(lambda ignore: client.presence.available()) # FIXME: should send the current presence, not always "available" !
286 return set_avatar_elt.send().addCallback(lambda ignore: client.presence.available())
287 329
288 d.addCallback(elementBuilt) 330 d.addCallback(elementBuilt)
289 331
290 return d 332 return d
291 333
305 347
306 def getDiscoItems(self, requestor, target, nodeIdentifier=''): 348 def getDiscoItems(self, requestor, target, nodeIdentifier=''):
307 return [] 349 return []
308 350
309 def update(self, presence): 351 def update(self, presence):
310 """Request for VCard's nickname 352 """Called on <presence/> stanza with vcard data
311 return the cached nickname if exists, else get VCard 353
312 """ 354 Check for avatar information, and get VCard if needed
313 from_jid = jid.JID(presence['from']) 355 @param presend(domish.Element): <presence/> stanza
356 """
357 # FIXME: doesn't manage MUC correctly
358 from_jid = jid.JID(presence['from']).userhostJID()
314 #FIXME: wokkel's data_form should be used here 359 #FIXME: wokkel's data_form should be used here
315 x_elem = filter(lambda x: x.name == "x", presence.elements())[0] # We only want the "x" element 360 x_elem = filter(lambda x: x.name == "x", presence.elements())[0] # We only want the "x" element
316 for elem in x_elem.elements(): 361 for elem in x_elem.elements():
317 if elem.name == 'photo': 362 if elem.name == 'photo':
318 _hash = str(elem) 363 hash_ = str(elem)
319 old_avatar = self.plugin_parent.get_cache(from_jid, 'avatar', self.parent.profile) 364 old_avatar = self.plugin_parent.getCache(from_jid, 'avatar', self.parent.profile)
320 if not old_avatar or old_avatar != _hash: 365 filename = self.plugin_parent._getFilename(hash_)
321 log.debug(_('New avatar found, requesting vcard')) 366 if not old_avatar or old_avatar != hash_:
322 self.plugin_parent.getCard(from_jid.userhost(), self.parent.profile) 367 if os.path.exists(filename):
368 log.debug(u"New avatar found for [{}], it's already in cache, we use it".format(from_jid.full()))
369 self.plugin_parent.updateCache(from_jid, 'avatar', hash_, self.parent.profile)
370 else:
371 log.debug(u'New avatar found for [{}], requesting vcard'.format(from_jid.full()))
372 self.plugin_parent.getCard(from_jid, self.parent.profile)
373 else:
374 if os.path.exists(filename):
375 log.debug(u"avatar for {} already in cache".format(from_jid.full()))
376 else:
377 log.error(u"Avatar for [{}] should be in cache but it is not ! We get it".format(from_jid.full()))
378 self.plugin_parent.getCard(from_jid, self.parent.profile)
379