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))