Mercurial > libervia-backend
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 |