Mercurial > libervia-backend
comparison src/plugins/plugin_xep_0054.py @ 1293:0541cb64217e frontends_multi_profiles
plugin XEP-0054: couple of fixes in VCard/avatar management:
- fixed a confusion between full jid and bare jid, resulting in always requesting VCard
- check of cache data loading before starting
- stored cache are restored for all jid, not only ones in roster
- client jid cache is correctly saved/restored
- minor other fixes
This plugin still need some work, but this patch fixes (hopefully) the major issues
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 26 Jan 2015 02:03:16 +0100 |
parents | faa1129559b8 |
children | be3a301540c0 |
comparison
equal
deleted
inserted
replaced
1292:b29a065a66f0 | 1293:0541cb64217e |
---|---|
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 = PersistentDict(NS_VCARD) |
83 self.avatars_cache.load() # FIXME: resulting deferred must be correctly managed | 83 self.initialised = self.avatars_cache.load() # FIXME: resulting deferred must be correctly managed |
84 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) | 85 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) | 86 host.bridge.addMethod("setAvatar", ".plugin", in_sign='ss', out_sign='', method=self.setAvatar, async=True) |
87 host.trigger.add("presence_available", self.presenceTrigger) | 87 host.trigger.add("presence_available", self.presenceTrigger) |
88 host.memory.setSignalOnUpdate("avatar") | 88 host.memory.setSignalOnUpdate("avatar") |
96 x_elt.addElement('photo', content=self.avatars_cache[client.jid.userhost()]) | 96 x_elt.addElement('photo', content=self.avatars_cache[client.jid.userhost()]) |
97 presence_elt.addChild(x_elt) | 97 presence_elt.addChild(x_elt) |
98 | 98 |
99 return True | 99 return True |
100 | 100 |
101 def _fillCachedValues(self, result, client): | 101 def _fillCachedValues(self, profile): |
102 #FIXME: this is really suboptimal, need to be reworked | 102 #FIXME: this is really suboptimal, need to be reworked |
103 # the current naive approach keeps a map between all jids of all profiles | 103 # the current naive approach keeps a map between all jids of all profiles |
104 # in persistent cache, and check if cached jid are in roster, then put avatar | 104 # in persistent cache, then put avatar |
105 # hashs in memory. | 105 # hashs in memory. Hashed should be shared between profiles |
106 for _jid in client.roster.getJids() + [client.jid]: | 106 for jid_s, avatar_hash in self.avatars_cache.iteritems(): |
107 if _jid.userhost() in self.avatars_cache: | 107 jid_ = jid.JID(jid_s) |
108 self.host.memory.updateEntityData(_jid, "avatar", self.avatars_cache[_jid.userhost()], client.profile) | 108 self.host.memory.updateEntityData(jid_, "avatar", avatar_hash, profile) |
109 | 109 |
110 @defer.inlineCallbacks | |
110 def profileConnected(self, profile): | 111 def profileConnected(self, profile): |
111 client = self.host.getClient(profile) | 112 yield self.initialised |
112 client.roster.got_roster.addCallback(self._fillCachedValues, client) | 113 self._fillCachedValues(profile) |
113 | 114 |
114 def update_cache(self, jid, name, value, profile): | 115 def updateCache(self, jid_, name, value, profile): |
115 """update cache value | 116 """update cache value |
116 - save value in memory in case of change | 117 |
117 @param jid: jid of the owner of the vcard | 118 save value in memory in case of change |
119 @param jid_: jid of the owner of the vcard | |
118 @param name: name of the item which changed | 120 @param name: name of the item which changed |
119 @param value: new value of the item | 121 @param value: new value of the item |
120 @param profile: profile which received the update | 122 @param profile: profile which received the update |
121 """ | 123 """ |
122 try: | 124 assert not jid_.resource # VCard are retrieved with bare jid |
123 cached = self.host.memory.getEntityData(jid, [name], profile) | 125 self.host.memory.updateEntityData(jid_, name, value, profile) |
124 except exceptions.UnknownEntityError: | 126 if name == "avatar": |
125 cached = {} | 127 self.avatars_cache[jid_.userhost()] = value |
126 if not name in cached or cached[name] != value: | 128 |
127 self.host.memory.updateEntityData(jid, name, value, profile) | 129 def getCache(self, entity_jid, name, profile): |
128 if name == "avatar": | |
129 self.avatars_cache[jid.userhost()] = value | |
130 | |
131 def get_cache(self, entity_jid, name, profile): | |
132 """return cached value for jid | 130 """return cached value for jid |
131 | |
133 @param entity_jid: target contact | 132 @param entity_jid: target contact |
134 @param name: name of the value ('nick' or 'avatar') | 133 @param name: name of the value ('nick' or 'avatar') |
135 @param profile: %(doc_profile)s | 134 @param profile: %(doc_profile)s |
136 @return: wanted value or None""" | 135 @return: wanted value or None""" |
136 assert not entity_jid.resource # VCard are retrieved with bare jid | |
137 try: | 137 try: |
138 data = self.host.memory.getEntityData(entity_jid, [name], profile) | 138 data = self.host.memory.getEntityData(entity_jid, [name], profile) |
139 except exceptions.UnknownEntityError: | 139 except exceptions.UnknownEntityError: |
140 return None | 140 return None |
141 return data.get(name) | 141 return data.get(name) |
142 | 142 |
143 def save_photo(self, photo_xml): | 143 def savePhoto(self, photo_xml): |
144 """Parse a <PHOTO> elem and save the picture""" | 144 """Parse a <PHOTO> elem and save the picture""" |
145 for elem in photo_xml.elements(): | 145 for elem in photo_xml.elements(): |
146 if elem.name == 'TYPE': | 146 if elem.name == 'TYPE': |
147 log.debug(_('Photo of type [%s] found') % str(elem)) | 147 log.debug(_('Photo of type [%s] found') % str(elem)) |
148 if elem.name == 'BINVAL': | 148 if elem.name == 'BINVAL': |
167 for elem in vcard.elements(): | 167 for elem in vcard.elements(): |
168 if elem.name == 'FN': | 168 if elem.name == 'FN': |
169 dictionary['fullname'] = unicode(elem) | 169 dictionary['fullname'] = unicode(elem) |
170 elif elem.name == 'NICKNAME': | 170 elif elem.name == 'NICKNAME': |
171 dictionary['nick'] = unicode(elem) | 171 dictionary['nick'] = unicode(elem) |
172 self.update_cache(target, 'nick', dictionary['nick'], profile) | 172 self.updateCache(target, 'nick', dictionary['nick'], profile) |
173 elif elem.name == 'URL': | 173 elif elem.name == 'URL': |
174 dictionary['website'] = unicode(elem) | 174 dictionary['website'] = unicode(elem) |
175 elif elem.name == 'EMAIL': | 175 elif elem.name == 'EMAIL': |
176 dictionary['email'] = unicode(elem) | 176 dictionary['email'] = unicode(elem) |
177 elif elem.name == 'BDAY': | 177 elif elem.name == 'BDAY': |
178 dictionary['birthday'] = unicode(elem) | 178 dictionary['birthday'] = unicode(elem) |
179 elif elem.name == 'PHOTO': | 179 elif elem.name == 'PHOTO': |
180 dictionary["avatar"] = yield threads.deferToThread(self.save_photo, elem) | 180 dictionary["avatar"] = yield threads.deferToThread(self.savePhoto, elem) |
181 if not dictionary["avatar"]: # can happen in case of e.g. empty photo elem | 181 if not dictionary["avatar"]: # can happen in case of e.g. empty photo elem |
182 del dictionary['avatar'] | 182 del dictionary['avatar'] |
183 else: | 183 else: |
184 self.update_cache(target, 'avatar', dictionary['avatar'], profile) | 184 self.updateCache(target, 'avatar', dictionary['avatar'], profile) |
185 else: | 185 else: |
186 log.info(_('FIXME: [%s] VCard tag is not managed yet') % elem.name) | 186 log.info(_('FIXME: [%s] VCard tag is not managed yet') % elem.name) |
187 | 187 |
188 defer.returnValue(dictionary) | 188 defer.returnValue(dictionary) |
189 | 189 |
190 def vcard_ok(self, answer, profile): | 190 def _VCardCb(self, answer, profile): |
191 """Called after the first get IQ""" | 191 """Called after the first get IQ""" |
192 log.debug(_("VCard found")) | 192 log.debug(_("VCard found")) |
193 | 193 |
194 if answer.firstChildElement().name == "vCard": | 194 if answer.firstChildElement().name == "vCard": |
195 _jid, steam = self.host.getJidNStream(profile) | 195 _jid, steam = self.host.getJidNStream(profile) |
201 d.addCallback(lambda data: self.host.bridge.actionResult("RESULT", answer['id'], data, profile)) | 201 d.addCallback(lambda data: self.host.bridge.actionResult("RESULT", answer['id'], data, profile)) |
202 else: | 202 else: |
203 log.error(_("FIXME: vCard not found as first child element")) | 203 log.error(_("FIXME: vCard not found as first child element")) |
204 self.host.bridge.actionResult("SUPPRESS", answer['id'], {}, profile) # FIXME: maybe an error message would be better | 204 self.host.bridge.actionResult("SUPPRESS", answer['id'], {}, profile) # FIXME: maybe an error message would be better |
205 | 205 |
206 def vcard_err(self, failure, profile): | 206 def _VCardEb(self, failure, profile): |
207 """Called when something is wrong with registration""" | 207 """Called when something is wrong with registration""" |
208 try: | 208 try: |
209 log.error(_("Can't find VCard of %s") % failure.value.stanza['from']) | 209 log.warning(_("Can't find VCard of %s") % failure.value.stanza['from']) |
210 self.host.bridge.actionResult("SUPPRESS", failure.value.stanza['id'], {}, profile) # FIXME: maybe an error message would be better | 210 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) | |
211 except AttributeError: # 'ConnectionLost' object has no attribute 'stanza' | 212 except AttributeError: # 'ConnectionLost' object has no attribute 'stanza' |
212 log.error(_("Can't find VCard: %s") % failure.getErrorMessage()) | 213 log.warning(_("Can't find VCard: %s") % failure.getErrorMessage()) |
213 | 214 |
214 def getCard(self, target_s, profile_key=C.PROF_KEY_NONE): | 215 def getCard(self, target_s, profile_key=C.PROF_KEY_NONE): |
215 """Ask server for VCard | 216 """Ask server for VCard |
216 @param target_s: jid from which we want the VCard | 217 @param target_s: jid from which we want the VCard |
217 @result: id to retrieve the profile""" | 218 @result: id to retrieve the profile""" |
218 current_jid, xmlstream = self.host.getJidNStream(profile_key) | 219 current_jid, xmlstream = self.host.getJidNStream(profile_key) |
219 if not xmlstream: | 220 if not xmlstream: |
220 log.error(_('Asking vcard for a non-existant or not connected profile')) | 221 raise exceptions.ProfileUnknownError('Asking vcard for a non-existant or not connected profile ({})'.format(profile_key)) |
221 return "" | |
222 profile = self.host.memory.getProfileName(profile_key) | 222 profile = self.host.memory.getProfileName(profile_key) |
223 to_jid = jid.JID(target_s) | 223 to_jid = jid.JID(target_s) |
224 log.debug(_("Asking for %s's VCard") % to_jid.userhost()) | 224 log.debug(_("Asking for %s's VCard") % to_jid.userhost()) |
225 reg_request = IQ(xmlstream, 'get') | 225 reg_request = IQ(xmlstream, 'get') |
226 reg_request["from"] = current_jid.full() | 226 reg_request["from"] = current_jid.full() |
227 reg_request["to"] = to_jid.userhost() | 227 reg_request["to"] = to_jid.userhost() |
228 reg_request.addElement('vCard', NS_VCARD) | 228 reg_request.addElement('vCard', NS_VCARD) |
229 reg_request.send(to_jid.userhost()).addCallbacks(self.vcard_ok, self.vcard_err, callbackArgs=[profile], errbackArgs=[profile]) | 229 reg_request.send(to_jid.userhost()).addCallbacks(self._VCardCb, self._VCardEb, callbackArgs=[profile], errbackArgs=[profile]) |
230 return reg_request["id"] | 230 return reg_request["id"] |
231 | 231 |
232 def getAvatarFile(self, avatar_hash): | 232 def getAvatarFile(self, avatar_hash): |
233 """Give the full path of avatar from hash | 233 """Give the full path of avatar from hash |
234 @param hash: SHA1 hash | 234 @param hash: SHA1 hash |
280 d = threads.deferToThread(self._buildSetAvatar, vcard_set, filepath) | 280 d = threads.deferToThread(self._buildSetAvatar, vcard_set, filepath) |
281 | 281 |
282 def elementBuilt(result): | 282 def elementBuilt(result): |
283 """Called once the image is at the right size/format, and the vcard set element is build""" | 283 """Called once the image is at the right size/format, and the vcard set element is build""" |
284 set_avatar_elt, img_hash = result | 284 set_avatar_elt, img_hash = result |
285 self.avatars_cache[client.jid.userhost()] = img_hash # we need to update the hash, so we can send a new presence | 285 self.updateCache(client.jid.userhostJID(), 'avatar', img_hash, client.profile) |
286 # element with the right hash | 286 return set_avatar_elt.send().addCallback(lambda ignore: client.presence.available()) # FIXME: should send the current presence, not always "available" ! |
287 return set_avatar_elt.send().addCallback(lambda ignore: client.presence.available()) | |
288 | 287 |
289 d.addCallback(elementBuilt) | 288 d.addCallback(elementBuilt) |
290 | 289 |
291 return d | 290 return d |
292 | 291 |
309 | 308 |
310 def update(self, presence): | 309 def update(self, presence): |
311 """Request for VCard's nickname | 310 """Request for VCard's nickname |
312 return the cached nickname if exists, else get VCard | 311 return the cached nickname if exists, else get VCard |
313 """ | 312 """ |
314 from_jid = jid.JID(presence['from']) | 313 # FIXME: doesn't manage MUC correctly |
314 from_jid = jid.JID(presence['from']).userhostJID() | |
315 #FIXME: wokkel's data_form should be used here | 315 #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 | 316 x_elem = filter(lambda x: x.name == "x", presence.elements())[0] # We only want the "x" element |
317 for elem in x_elem.elements(): | 317 for elem in x_elem.elements(): |
318 if elem.name == 'photo': | 318 if elem.name == 'photo': |
319 _hash = str(elem) | 319 _hash = str(elem) |
320 old_avatar = self.plugin_parent.get_cache(from_jid, 'avatar', self.parent.profile) | 320 old_avatar = self.plugin_parent.getCache(from_jid, 'avatar', self.parent.profile) |
321 if not old_avatar or old_avatar != _hash: | 321 if not old_avatar or old_avatar != _hash: |
322 log.debug(_('New avatar found, requesting vcard')) | 322 log.debug(_('New avatar found, requesting vcard')) |
323 self.plugin_parent.getCard(from_jid.userhost(), self.parent.profile) | 323 self.plugin_parent.getCard(from_jid.userhost(), self.parent.profile) |
324 else: | |
325 log.debug("avatar for {} already in cache".format(from_jid)) |