comparison src/plugins/plugin_xep_0054.py @ 1317:bd69d341d969 frontends_multi_profiles

plugin xep-0054: various improvments on avatars management: - avatars_cache is now managing cache load per profile - use of new profileConnecting method to load persistent data, this prevent to have presence_available trigger called before cache data is loaded - better management of avatars already in cache (prevent decoding avatars when it is already in cache)
author Goffi <goffi@goffi.org>
date Mon, 09 Feb 2015 21:39:51 +0100
parents be3a301540c0
children 6dbeb2ef966c
comparison
equal deleted inserted replaced
1316:8adcdf2cdfe1 1317:bd69d341d969
77 log.info(_("Plugin XEP_0054 initialization")) 77 log.info(_("Plugin XEP_0054 initialization"))
78 self.host = host 78 self.host = host
79 self.avatar_path = os.path.join(self.host.memory.getConfig('', 'local_dir'), AVATAR_PATH) 79 self.avatar_path = os.path.join(self.host.memory.getConfig('', 'local_dir'), AVATAR_PATH)
80 if not os.path.exists(self.avatar_path): 80 if not os.path.exists(self.avatar_path):
81 os.makedirs(self.avatar_path) 81 os.makedirs(self.avatar_path)
82 self.avatars_cache = PersistentDict(NS_VCARD) 82 self.avatars_cache = {}
83 self.initialised = self.avatars_cache.load() # FIXME: resulting deferred must be correctly managed 83 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) 84 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) 85 host.bridge.addMethod("setAvatar", ".plugin", in_sign='ss', out_sign='', method=self.setAvatar, async=True)
87 host.trigger.add("presence_available", self.presenceTrigger) 86 host.trigger.add("presence_available", self.presenceTrigger)
88 host.memory.setSignalOnUpdate("avatar") 87 host.memory.setSignalOnUpdate("avatar")
89 88
90 def getHandler(self, profile): 89 def getHandler(self, profile):
91 return XEP_0054_handler(self) 90 return XEP_0054_handler(self)
92 91
93 def presenceTrigger(self, presence_elt, client): 92 def presenceTrigger(self, presence_elt, client):
94 if client.jid.userhost() in self.avatars_cache: 93 if client.jid.userhost() in self.avatars_cache[client.profile]:
95 x_elt = domish.Element((NS_VCARD_UPDATE, 'x')) 94 x_elt = domish.Element((NS_VCARD_UPDATE, 'x'))
96 x_elt.addElement('photo', content=self.avatars_cache[client.jid.userhost()]) 95 x_elt.addElement('photo', content=self.avatars_cache[client.profile][client.jid.userhost()])
97 presence_elt.addChild(x_elt) 96 presence_elt.addChild(x_elt)
98 97
99 return True 98 return True
100 99
101 def _fillCachedValues(self, profile): 100 def _fillCachedValues(self, profile):
102 #FIXME: this is really suboptimal, need to be reworked 101 #FIXME: this is really suboptimal, need to be reworked
103 # the current naive approach keeps a map between all jids of all profiles 102 # the current naive approach keeps a map between all jids of all profiles
104 # in persistent cache, then put avatar 103 # in persistent cache, then put avatar
105 # hashs in memory. Hashed should be shared between profiles 104 # hashs in memory. Hashed should be shared between profiles
106 for jid_s, avatar_hash in self.avatars_cache.iteritems(): 105 for jid_s, avatar_hash in self.avatars_cache[profile].iteritems():
107 jid_ = jid.JID(jid_s) 106 jid_ = jid.JID(jid_s)
108 self.host.memory.updateEntityData(jid_, "avatar", avatar_hash, silent=True, profile_key=profile) 107 self.host.memory.updateEntityData(jid_, "avatar", avatar_hash, silent=True, profile_key=profile)
109 108
110 @defer.inlineCallbacks 109 @defer.inlineCallbacks
111 def profileConnected(self, profile): 110 def profileConnecting(self, profile):
112 yield self.initialised 111 self.avatars_cache[profile] = PersistentDict(NS_VCARD, profile)
112 yield self.avatars_cache[profile].load()
113 self._fillCachedValues(profile) 113 self._fillCachedValues(profile)
114
115 def profileDisconnected(self, profile):
116 log.debug(u"Deleting profile cache for avatars")
117 del self.avatars_cache[profile]
114 118
115 def updateCache(self, jid_, name, value, profile): 119 def updateCache(self, jid_, name, value, profile):
116 """update cache value 120 """update cache value
117 121
118 save value in memory in case of change 122 save value in memory in case of change
119 @param jid_: jid of the owner of the vcard 123 @param jid_(jid.JID): jid of the owner of the vcard
120 @param name: name of the item which changed 124 @param name(str): name of the item which changed
121 @param value: new value of the item 125 @param value(unicode): new value of the item
122 @param profile: profile which received the update 126 @param profile(unicode): profile which received the update
123 """ 127 """
124 assert not jid_.resource # VCard are retrieved with bare jid 128 assert not jid_.resource # VCard are retrieved with bare jid
125 self.host.memory.updateEntityData(jid_, name, value, profile_key=profile) 129 self.host.memory.updateEntityData(jid_, name, value, profile_key=profile)
126 if name == "avatar": 130 if name == "avatar":
127 self.avatars_cache[jid_.userhost()] = value 131 self.avatars_cache[profile][jid_.userhost()] = value
128 132
129 def getCache(self, entity_jid, name, profile): 133 def getCache(self, entity_jid, name, profile):
130 """return cached value for jid 134 """return cached value for jid
131 135
132 @param entity_jid: target contact 136 @param entity_jid: target contact
138 data = self.host.memory.getEntityData(entity_jid, [name], profile) 142 data = self.host.memory.getEntityData(entity_jid, [name], profile)
139 except exceptions.UnknownEntityError: 143 except exceptions.UnknownEntityError:
140 return None 144 return None
141 return data.get(name) 145 return data.get(name)
142 146
147 def _getFilename(self, hash_):
148 """Get filename from hash
149
150 @param hash_: hash of the avatar
151 @return (str): absolute filename of the avatar
152 """
153 return os.path.join(self.avatar_path, hash_)
154
155 def saveAvatarFile(self, data, hash_):
156 """Save the avatar picture if it doesn't already exists
157
158 @param data(str): binary image of the avatar
159 @param hash_(str): hash of the binary data (will be used for the filename)
160 """
161 filename = self._getFilename(hash_)
162 if not os.path.exists(filename):
163 with open(filename, 'wb') as file_:
164 file_.write(data)
165 log.debug(_("file saved to %s") % hash_)
166 else:
167 log.debug(_("file [%s] already in cache") % hash_)
168
143 def savePhoto(self, photo_xml): 169 def savePhoto(self, photo_xml):
144 """Parse a <PHOTO> elem and save the picture""" 170 """Parse a <PHOTO> elem and save the picture"""
145 for elem in photo_xml.elements(): 171 for elem in photo_xml.elements():
146 if elem.name == 'TYPE': 172 if elem.name == 'TYPE':
147 log.debug(_('Photo of type [%s] found') % str(elem)) 173 log.debug(_('Photo of type [%s] found') % str(elem))
148 if elem.name == 'BINVAL': 174 if elem.name == 'BINVAL':
149 log.debug(_('Decoding binary')) 175 log.debug(_('Decoding binary'))
150 decoded = b64decode(str(elem)) 176 decoded = b64decode(str(elem))
151 image_hash = sha1(decoded).hexdigest() 177 image_hash = sha1(decoded).hexdigest()
152 filename = self.avatar_path + '/' + image_hash 178 self.saveAvatarFile(decoded, image_hash)
153 if not os.path.exists(filename):
154 with open(filename, 'wb') as file_:
155 file_.write(decoded)
156 log.debug(_("file saved to %s") % image_hash)
157 else:
158 log.debug(_("file [%s] already in cache") % image_hash)
159 return image_hash 179 return image_hash
160 180
161 @defer.inlineCallbacks 181 @defer.inlineCallbacks
162 def vCard2Dict(self, vcard, target, profile): 182 def vCard2Dict(self, vcard, target, profile):
163 """Convert a VCard to a dict, and save binaries""" 183 """Convert a VCard to a dict, and save binaries"""
210 self.host.bridge.actionResult("SUPPRESS", failure.value.stanza['id'], {}, profile) # FIXME: maybe an error message would be better 230 self.host.bridge.actionResult("SUPPRESS", failure.value.stanza['id'], {}, profile) # FIXME: maybe an error message would be better
211 self.updateCache(jid.JID(failure.value.stanza['from']), "avatar", '', profile) 231 self.updateCache(jid.JID(failure.value.stanza['from']), "avatar", '', profile)
212 except AttributeError: # 'ConnectionLost' object has no attribute 'stanza' 232 except AttributeError: # 'ConnectionLost' object has no attribute 'stanza'
213 log.warning(_("Can't find VCard: %s") % failure.getErrorMessage()) 233 log.warning(_("Can't find VCard: %s") % failure.getErrorMessage())
214 234
215 def getCard(self, target_s, profile_key=C.PROF_KEY_NONE): 235 def _getCard(self, target_s, profile_key=C.PROF_KEY_NONE):
236 return self.getCard(jid.JID(target_s), profile_key)
237
238 def getCard(self, target, profile_key=C.PROF_KEY_NONE):
216 """Ask server for VCard 239 """Ask server for VCard
217 @param target_s: jid from which we want the VCard 240
218 @result: id to retrieve the profile""" 241 @param target(jid.JID): jid from which we want the VCard
242 @result: id to retrieve the profile
243 """
219 current_jid, xmlstream = self.host.getJidNStream(profile_key) 244 current_jid, xmlstream = self.host.getJidNStream(profile_key)
220 if not xmlstream: 245 if not xmlstream:
221 raise exceptions.ProfileUnknownError('Asking vcard for a non-existant or not connected profile ({})'.format(profile_key)) 246 raise exceptions.ProfileUnknownError('Asking vcard for a non-existant or not connected profile ({})'.format(profile_key))
222 profile = self.host.memory.getProfileName(profile_key) 247 profile = self.host.memory.getProfileName(profile_key)
223 to_jid = jid.JID(target_s) 248 to_jid = target.userhostJID()
224 log.debug(_("Asking for %s's VCard") % to_jid.userhost()) 249 log.debug(_("Asking for %s's VCard") % to_jid.userhost())
225 reg_request = IQ(xmlstream, 'get') 250 reg_request = IQ(xmlstream, 'get')
226 reg_request["from"] = current_jid.full() 251 reg_request["from"] = current_jid.full()
227 reg_request["to"] = to_jid.userhost() 252 reg_request["to"] = to_jid.userhost()
228 reg_request.addElement('vCard', NS_VCARD) 253 reg_request.addElement('vCard', NS_VCARD)
265 vcard_elt = vcard_set.addElement('vCard', NS_VCARD) 290 vcard_elt = vcard_set.addElement('vCard', NS_VCARD)
266 photo_elt = vcard_elt.addElement('PHOTO') 291 photo_elt = vcard_elt.addElement('PHOTO')
267 photo_elt.addElement('TYPE', content='image/png') 292 photo_elt.addElement('TYPE', content='image/png')
268 photo_elt.addElement('BINVAL', content=b64encode(img_buf.getvalue())) 293 photo_elt.addElement('BINVAL', content=b64encode(img_buf.getvalue()))
269 img_hash = sha1(img_buf.getvalue()).hexdigest() 294 img_hash = sha1(img_buf.getvalue()).hexdigest()
295 self.saveAvatarFile(img_buf.getvalue(), img_hash)
270 return (vcard_set, img_hash) 296 return (vcard_set, img_hash)
271 297
272 def setAvatar(self, filepath, profile_key=C.PROF_KEY_NONE): 298 def setAvatar(self, filepath, profile_key=C.PROF_KEY_NONE):
273 """Set avatar of the profile 299 """Set avatar of the profile
274 @param filepath: path of the image of the avatar""" 300 @param filepath: path of the image of the avatar"""
305 331
306 def getDiscoItems(self, requestor, target, nodeIdentifier=''): 332 def getDiscoItems(self, requestor, target, nodeIdentifier=''):
307 return [] 333 return []
308 334
309 def update(self, presence): 335 def update(self, presence):
310 """Request for VCard's nickname 336 """Called on <presence/> stanza with vcard data
311 return the cached nickname if exists, else get VCard 337
338 Check for avatar information, and get VCard if needed
339 @param presend(domish.Element): <presence/> stanza
312 """ 340 """
313 # FIXME: doesn't manage MUC correctly 341 # FIXME: doesn't manage MUC correctly
314 from_jid = jid.JID(presence['from']).userhostJID() 342 from_jid = jid.JID(presence['from']).userhostJID()
315 #FIXME: wokkel's data_form should be used here 343 #FIXME: wokkel's data_form should be used here
316 x_elem = filter(lambda x: x.name == "x", presence.elements())[0] # We only want the "x" element 344 x_elem = filter(lambda x: x.name == "x", presence.elements())[0] # We only want the "x" element
317 for elem in x_elem.elements(): 345 for elem in x_elem.elements():
318 if elem.name == 'photo': 346 if elem.name == 'photo':
319 _hash = str(elem) 347 hash_ = str(elem)
320 old_avatar = self.plugin_parent.getCache(from_jid, 'avatar', self.parent.profile) 348 old_avatar = self.plugin_parent.getCache(from_jid, 'avatar', self.parent.profile)
321 if not old_avatar or old_avatar != _hash: 349 filename = self.plugin_parent._getFilename(hash_)
322 log.debug(_('New avatar found, requesting vcard')) 350 if not old_avatar or old_avatar != hash_:
323 self.plugin_parent.getCard(from_jid.userhost(), self.parent.profile) 351 if os.path.exists(filename):
352 log.debug(u"New avatar found for [{}], it's already in cache, we use it".format(from_jid.full()))
353 self.plugin_parent.updateCache(from_jid, 'avatar', hash_, self.parent.profile)
354 else:
355 log.debug(u'New avatar found for [{}], requesting vcard'.format(from_jid.full()))
356 self.plugin_parent.getCard(from_jid, self.parent.profile)
324 else: 357 else:
325 log.debug("avatar for {} already in cache".format(from_jid)) 358 if os.path.exists(filename):
359 log.debug(u"avatar for {} already in cache".format(from_jid.full()))
360 else:
361 log.error(u"Avatar for [{}] should be in cache but it is not ! We get it".format(from_jid.full()))
362 self.plugin_parent.getCard(from_jid, self.parent.profile)
363