comparison src/plugins/plugin_xep_0054.py @ 562:0bb2e0d1c878

core, plugin XEP-0054: avatar upload: - plugin XEP-0054: new setAvatar bridge method - new "presence_available" trigger - new DataError
author Goffi <goffi@goffi.org>
date Fri, 28 Dec 2012 01:00:31 +0100
parents 7ffae708b176
children bf1505df088c
comparison
equal deleted inserted replaced
561:97f6a445d6e8 562:0bb2e0d1c878
22 from logging import debug, info, error 22 from logging import debug, info, error
23 from twisted.internet import threads 23 from twisted.internet import threads
24 from twisted.internet.defer import inlineCallbacks, returnValue 24 from twisted.internet.defer import inlineCallbacks, returnValue
25 from twisted.words.protocols.jabber import jid 25 from twisted.words.protocols.jabber import jid
26 from twisted.words.protocols.jabber.xmlstream import IQ 26 from twisted.words.protocols.jabber.xmlstream import IQ
27 from twisted.words.xish import domish
27 import os.path 28 import os.path
28 29
29 from zope.interface import implements 30 from zope.interface import implements
30 31
31 from wokkel import disco, iwokkel 32 from wokkel import disco, iwokkel
32 33
33 from base64 import b64decode 34 from base64 import b64decode,b64encode
34 from hashlib import sha1 35 from hashlib import sha1
35 from sat.core import exceptions 36 from sat.core import exceptions
36 from sat.memory.persistent import PersistentDict 37 from sat.memory.persistent import PersistentDict
38 import Image
39 from cStringIO import StringIO
37 40
38 try: 41 try:
39 from twisted.words.protocols.xmlstream import XMPPHandler 42 from twisted.words.protocols.xmlstream import XMPPHandler
40 except ImportError: 43 except ImportError:
41 from wokkel.subprotocols import XMPPHandler 44 from wokkel.subprotocols import XMPPHandler
74 os.makedirs(self.avatar_path) 77 os.makedirs(self.avatar_path)
75 self.avatars_cache = PersistentDict(NS_VCARD) 78 self.avatars_cache = PersistentDict(NS_VCARD)
76 self.avatars_cache.load() #FIXME: resulting deferred must be correctly managed 79 self.avatars_cache.load() #FIXME: resulting deferred must be correctly managed
77 host.bridge.addMethod("getCard", ".plugin", in_sign='ss', out_sign='s', method=self.getCard) 80 host.bridge.addMethod("getCard", ".plugin", in_sign='ss', out_sign='s', method=self.getCard)
78 host.bridge.addMethod("getAvatarFile", ".plugin", in_sign='s', out_sign='s', method=self.getAvatarFile) 81 host.bridge.addMethod("getAvatarFile", ".plugin", in_sign='s', out_sign='s', method=self.getAvatarFile)
82 host.bridge.addMethod("setAvatar", ".plugin", in_sign='ss', out_sign='', method=self.setAvatar, async = True)
83 host.trigger.add("presence_available", self.presenceTrigger)
79 84
80 def getHandler(self, profile): 85 def getHandler(self, profile):
81 return XEP_0054_handler(self) 86 return XEP_0054_handler(self)
87
88 def presenceTrigger(self, presence_elt, client):
89 if client.jid.userhost() in self.avatars_cache:
90 x_elt = domish.Element((NS_VCARD_UPDATE, 'x'))
91 x_elt.addElement('photo', content=self.avatars_cache[client.jid.userhost()])
92 presence_elt.addChild(x_elt)
93
94 return True
82 95
83 def _fillCachedValues(self, result, client): 96 def _fillCachedValues(self, result, client):
84 #FIXME: this is really suboptimal, need to be reworked 97 #FIXME: this is really suboptimal, need to be reworked
85 # the current naive approach keeps a map between all jids of all profiles 98 # the current naive approach keeps a map between all jids of all profiles
86 # in persistent cache, and check if cached jid are in roster, then put avatar 99 # in persistent cache, and check if cached jid are in roster, then put avatar
128 if elem.name == 'TYPE': 141 if elem.name == 'TYPE':
129 info(_('Photo of type [%s] found') % str(elem)) 142 info(_('Photo of type [%s] found') % str(elem))
130 if elem.name == 'BINVAL': 143 if elem.name == 'BINVAL':
131 debug(_('Decoding binary')) 144 debug(_('Decoding binary'))
132 decoded = b64decode(str(elem)) 145 decoded = b64decode(str(elem))
133 hash = sha1(decoded).hexdigest() 146 image_hash = sha1(decoded).hexdigest()
134 filename = self.avatar_path+'/'+hash 147 filename = self.avatar_path+'/'+image_hash
135 if not os.path.exists(filename): 148 if not os.path.exists(filename):
136 with open(filename,'wb') as file: 149 with open(filename,'wb') as file:
137 file.write(decoded) 150 file.write(decoded)
138 debug(_("file saved to %s") % hash) 151 debug(_("file saved to %s") % image_hash)
139 else: 152 else:
140 debug(_("file [%s] already in cache") % hash) 153 debug(_("file [%s] already in cache") % image_hash)
141 return hash 154 return image_hash
142 155
143 @inlineCallbacks 156 @inlineCallbacks
144 def vCard2Dict(self, vcard, target, profile): 157 def vCard2Dict(self, vcard, target, profile):
145 """Convert a VCard to a dict, and save binaries""" 158 """Convert a VCard to a dict, and save binaries"""
146 debug (_("parsing vcard")) 159 debug (_("parsing vcard"))
172 def vcard_ok(self, answer, profile): 185 def vcard_ok(self, answer, profile):
173 """Called after the first get IQ""" 186 """Called after the first get IQ"""
174 debug (_("VCard found")) 187 debug (_("VCard found"))
175 188
176 if answer.firstChildElement().name == "vCard": 189 if answer.firstChildElement().name == "vCard":
177 d = self.vCard2Dict(answer.firstChildElement(), jid.JID(answer["from"]), profile) 190 _jid, steam = self.host.getJidNStream(profile)
191 try:
192 from_jid = jid.JID(answer["from"])
193 except KeyError:
194 from_jid = _jid.userhostJID()
195 d = self.vCard2Dict(answer.firstChildElement(), from_jid, profile)
178 d.addCallback(lambda data: self.host.bridge.actionResult("RESULT", answer['id'], data, profile)) 196 d.addCallback(lambda data: self.host.bridge.actionResult("RESULT", answer['id'], data, profile))
179 else: 197 else:
180 error (_("FIXME: vCard not found as first child element")) 198 error (_("FIXME: vCard not found as first child element"))
181 self.host.bridge.actionResult("SUPPRESS", answer['id'], {}, profile) #FIXME: maybe an error message would be better 199 self.host.bridge.actionResult("SUPPRESS", answer['id'], {}, profile) #FIXME: maybe an error message would be better
182 200
189 """Ask server for VCard 207 """Ask server for VCard
190 @param target_s: jid from which we want the VCard 208 @param target_s: jid from which we want the VCard
191 @result: id to retrieve the profile""" 209 @result: id to retrieve the profile"""
192 current_jid, xmlstream = self.host.getJidNStream(profile_key) 210 current_jid, xmlstream = self.host.getJidNStream(profile_key)
193 if not xmlstream: 211 if not xmlstream:
194 error (_('Asking vcard for an non-existant or not connected profile')) 212 error (_('Asking vcard for a non-existant or not connected profile'))
195 return "" 213 return ""
196 profile = self.host.memory.getProfileName(profile_key) 214 profile = self.host.memory.getProfileName(profile_key)
197 to_jid = jid.JID(target_s) 215 to_jid = jid.JID(target_s)
198 debug(_("Asking for %s's VCard") % to_jid.userhost()) 216 debug(_("Asking for %s's VCard") % to_jid.userhost())
199 reg_request=IQ(xmlstream,'get') 217 reg_request=IQ(xmlstream,'get')
201 reg_request["to"] = to_jid.userhost() 219 reg_request["to"] = to_jid.userhost()
202 reg_request.addElement('vCard', NS_VCARD) 220 reg_request.addElement('vCard', NS_VCARD)
203 reg_request.send(to_jid.userhost()).addCallbacks(self.vcard_ok, self.vcard_err, callbackArgs=[profile], errbackArgs=[profile]) 221 reg_request.send(to_jid.userhost()).addCallbacks(self.vcard_ok, self.vcard_err, callbackArgs=[profile], errbackArgs=[profile])
204 return reg_request["id"] 222 return reg_request["id"]
205 223
206 def getAvatarFile(self, hash): 224 def getAvatarFile(self, avatar_hash):
207 """Give the full path of avatar from hash 225 """Give the full path of avatar from hash
208 @param hash: SHA1 hash 226 @param hash: SHA1 hash
209 @return full_path 227 @return full_path
210 """ 228 """
211 filename = self.avatar_path+'/'+hash 229 filename = self.avatar_path+'/'+avatar_hash
212 if not os.path.exists(filename): 230 if not os.path.exists(filename):
213 error (_("Asking for an uncached avatar [%s]") % hash) 231 error (_("Asking for an uncached avatar [%s]") % avatar_hash)
214 return "" 232 return ""
215 return filename 233 return filename
234
235 def _buildSetAvatar(self, vcard_set, filepath):
236 try:
237 img = Image.open(filepath)
238 except IOError:
239 raise exceptions.DataError("Can't open image")
240
241 if img.size != (64, 64):
242 img.resize((64, 64))
243 img_buf = StringIO()
244 img.save(img_buf, 'PNG')
245
246 vcard_elt = vcard_set.addElement('vCard', NS_VCARD)
247 photo_elt = vcard_elt.addElement('PHOTO')
248 photo_elt.addElement('TYPE', content='image/png')
249 photo_elt.addElement('BINVAL', content=b64encode(img_buf.getvalue()))
250 img_hash = sha1(img_buf.getvalue()).hexdigest()
251 return (vcard_set, img_hash)
252
253 def setAvatar(self, filepath, profile_key='@DEFAULT@'):
254 """Set avatar of the profile
255 @param filepath: path of the image of the avatar"""
256 #TODO: This is a temporary way of setting avatar, as other VCard informations are not managed.
257 # A proper full VCard management should be done (and more generaly a public/private profile)
258 client = self.host.getClient(profile_key)
259 if not client:
260 raise exceptions.NotConnectedProfileError(_('Trying to set avatar for a non-existant or not connected profile'))
261
262 vcard_set = IQ(client.xmlstream,'set')
263 d = threads.deferToThread(self._buildSetAvatar, vcard_set, filepath)
264
265 def elementBuilt(result):
266 """Called once the image is at the right size/format, and the vcard set element is build"""
267 set_avatar_elt, img_hash = result
268 self.avatars_cache[client.jid.userhost()] = img_hash # we need to update the hash, so we can send a new presence
269 # element with the right hash
270 return set_avatar_elt.send().addCallback(lambda ignore: client.presence.available())
271
272 d.addCallback(elementBuilt)
273
274 return d
216 275
217 276
218 class XEP_0054_handler(XMPPHandler): 277 class XEP_0054_handler(XMPPHandler):
219 implements(iwokkel.IDisco) 278 implements(iwokkel.IDisco)
220 279