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