Mercurial > libervia-backend
comparison src/plugins/plugin_xep_0054.py @ 223:86d249b6d9b7
Files reorganisation
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 29 Dec 2010 01:06:29 +0100 |
parents | plugins/plugin_xep_0054.py@f271fff3a713 |
children | b1794cbb88e5 |
comparison
equal
deleted
inserted
replaced
222:3198bfd66daa | 223:86d249b6d9b7 |
---|---|
1 #!/usr/bin/python | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 """ | |
5 SAT plugin for managing xep-0054 | |
6 Copyright (C) 2009, 2010 Jérôme Poisson (goffi@goffi.org) | |
7 | |
8 This program is free software: you can redistribute it and/or modify | |
9 it under the terms of the GNU General Public License as published by | |
10 the Free Software Foundation, either version 3 of the License, or | |
11 (at your option) any later version. | |
12 | |
13 This program is distributed in the hope that it will be useful, | |
14 but WITHOUT ANY WARRANTY; without even the implied warranty of | |
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
16 GNU General Public License for more details. | |
17 | |
18 You should have received a copy of the GNU General Public License | |
19 along with this program. If not, see <http://www.gnu.org/licenses/>. | |
20 """ | |
21 | |
22 from logging import debug, info, error | |
23 from twisted.words.xish import domish | |
24 from twisted.internet import protocol, defer, threads, reactor | |
25 from twisted.words.protocols.jabber import client, jid, xmlstream | |
26 from twisted.words.protocols.jabber import error as jab_error | |
27 from twisted.words.protocols.jabber.xmlstream import IQ | |
28 import os.path | |
29 import pdb | |
30 | |
31 from zope.interface import implements | |
32 | |
33 from wokkel import disco, iwokkel | |
34 | |
35 from base64 import b64decode | |
36 from hashlib import sha1 | |
37 from time import sleep | |
38 | |
39 try: | |
40 from twisted.words.protocols.xmlstream import XMPPHandler | |
41 except ImportError: | |
42 from wokkel.subprotocols import XMPPHandler | |
43 | |
44 AVATAR_PATH = "/avatars" | |
45 | |
46 IQ_GET = '/iq[@type="get"]' | |
47 NS_VCARD = 'vcard-temp' | |
48 VCARD_REQUEST = IQ_GET + '/vCard[@xmlns="' + NS_VCARD + '"]' #TODO: manage requests | |
49 | |
50 PRESENCE = '/presence' | |
51 NS_VCARD_UPDATE = 'vcard-temp:x:update' | |
52 VCARD_UPDATE = PRESENCE + '/x[@xmlns="' + NS_VCARD_UPDATE + '"]' | |
53 | |
54 PLUGIN_INFO = { | |
55 "name": "XEP 0054 Plugin", | |
56 "import_name": "XEP_0054", | |
57 "type": "XEP", | |
58 "protocols": ["XEP-0054", "XEP-0153"], | |
59 "dependencies": [], | |
60 "main": "XEP_0054", | |
61 "handler": "yes", | |
62 "description": _("""Implementation of vcard-temp""") | |
63 } | |
64 | |
65 class XEP_0054(): | |
66 | |
67 def __init__(self, host): | |
68 info(_("Plugin XEP_0054 initialization")) | |
69 self.host = host | |
70 self.avatar_path = os.path.expanduser(self.host.get_const('local_dir') + AVATAR_PATH) | |
71 self.vcard_cache = host.memory.getPrivate("vcard_cache") or {} #used to store nicknames and avatar, key = jid | |
72 if not os.path.exists(self.avatar_path): | |
73 os.makedirs(self.avatar_path) | |
74 host.bridge.addMethod("getCard", ".communication", in_sign='ss', out_sign='s', method=self.getCard) | |
75 host.bridge.addMethod("getAvatarFile", ".communication", in_sign='s', out_sign='s', method=self.getAvatarFile) | |
76 host.bridge.addMethod("getCardCache", ".communication", in_sign='s', out_sign='a{ss}', method=self.getCardCache) | |
77 | |
78 def getHandler(self, profile): | |
79 return XEP_0054_handler(self) | |
80 | |
81 def update_cache(self, jid, name, value): | |
82 """update cache value | |
83 - save value in memory in case of change | |
84 - send updatedValue signal if the value is new or updated | |
85 """ | |
86 if not self.vcard_cache.has_key(jid.userhost()): | |
87 self.vcard_cache[jid.userhost()] = {} | |
88 | |
89 cache = self.vcard_cache[jid.userhost()] | |
90 old_value = cache[name] if cache.has_key(name) else None | |
91 if not old_value or value != old_value: | |
92 cache[name] = value | |
93 self.host.memory.setPrivate("vcard_cache", self.vcard_cache) | |
94 self.host.bridge.updatedValue('card_'+name, {'jid':jid.userhost(), name:value}) | |
95 | |
96 def get_cache(self, jid, name): | |
97 """return cached value for jid | |
98 @param jid: target contact | |
99 @param name: name of the value ('nick' or 'avatar') | |
100 @return: wanted value or None""" | |
101 try: | |
102 return self.vcard_cache[jid.userhost()][name] | |
103 except KeyError: | |
104 return None | |
105 | |
106 | |
107 def save_photo(self, photo_xml): | |
108 """Parse a <PHOTO> elem and save the picture""" | |
109 for elem in photo_xml.elements(): | |
110 if elem.name == 'TYPE': | |
111 info(_('Photo of type [%s] found') % str(elem)) | |
112 if elem.name == 'BINVAL': | |
113 debug(_('Decoding binary')) | |
114 decoded = b64decode(str(elem)) | |
115 hash = sha1(decoded).hexdigest() | |
116 filename = self.avatar_path+'/'+hash | |
117 if not os.path.exists(filename): | |
118 with open(filename,'wb') as file: | |
119 file.write(decoded) | |
120 debug(_("file saved to %s") % hash) | |
121 else: | |
122 debug(_("file [%s] already in cache") % hash) | |
123 return hash | |
124 | |
125 @defer.deferredGenerator | |
126 def vCard2Dict(self, vcard, target): | |
127 """Convert a VCard to a dict, and save binaries""" | |
128 debug (_("parsing vcard")) | |
129 dictionary = {} | |
130 d = defer.Deferred() | |
131 | |
132 for elem in vcard.elements(): | |
133 if elem.name == 'FN': | |
134 dictionary['fullname'] = unicode(elem) | |
135 elif elem.name == 'NICKNAME': | |
136 dictionary['nick'] = unicode(elem) | |
137 self.update_cache(target, 'nick', dictionary['nick']) | |
138 elif elem.name == 'URL': | |
139 dictionary['website'] = unicode(elem) | |
140 elif elem.name == 'EMAIL': | |
141 dictionary['email'] = unicode(elem) | |
142 elif elem.name == 'BDAY': | |
143 dictionary['birthday'] = unicode(elem) | |
144 elif elem.name == 'PHOTO': | |
145 d2 = defer.waitForDeferred( | |
146 threads.deferToThread(self.save_photo, elem)) | |
147 yield d2 | |
148 dictionary["avatar"] = d2.getResult() | |
149 if not dictionary["avatar"]: #can happen in case of e.g. empty photo elem | |
150 del dictionary['avatar'] | |
151 else: | |
152 self.update_cache(target, 'avatar', dictionary['avatar']) | |
153 else: | |
154 info (_('FIXME: [%s] VCard tag is not managed yet') % elem.name) | |
155 | |
156 yield dictionary | |
157 | |
158 def vcard_ok(self, answer): | |
159 """Called after the first get IQ""" | |
160 debug (_("VCard found")) | |
161 | |
162 if answer.firstChildElement().name == "vCard": | |
163 d = self.vCard2Dict(answer.firstChildElement(), jid.JID(answer["from"])) | |
164 d.addCallback(lambda data: self.host.bridge.actionResult("RESULT", answer['id'], data)) | |
165 else: | |
166 error (_("FIXME: vCard not found as first child element")) | |
167 self.host.bridge.actionResult("SUPPRESS", answer['id'], {}) #FIXME: maybe an error message would be best | |
168 | |
169 def vcard_err(self, failure): | |
170 """Called when something is wrong with registration""" | |
171 error (_("Can't find VCard of %s") % failure.value.stanza['from']) | |
172 self.host.bridge.actionResult("SUPPRESS", failure.value.stanza['id'], {}) #FIXME: maybe an error message would be best | |
173 | |
174 def getCard(self, target, profile_key='@DEFAULT@'): | |
175 """Ask server for VCard | |
176 @param target: jid from which we want the VCard | |
177 @result: id to retrieve the profile""" | |
178 current_jid, xmlstream = self.host.getJidNStream(profile_key) | |
179 if not xmlstream: | |
180 error (_('Asking vcard for an non-existant or not connected profile')) | |
181 return "" | |
182 to_jid = jid.JID(target) | |
183 debug(_("Asking for %s's VCard") % to_jid.userhost()) | |
184 reg_request=IQ(xmlstream,'get') | |
185 reg_request["from"]=current_jid.full() | |
186 reg_request["to"] = to_jid.userhost() | |
187 query=reg_request.addElement('vCard', NS_VCARD) | |
188 reg_request.send(to_jid.userhost()).addCallbacks(self.vcard_ok, self.vcard_err) | |
189 return reg_request["id"] | |
190 | |
191 def getAvatarFile(self, hash): | |
192 """Give the full path of avatar from hash | |
193 @param hash: SHA1 hash | |
194 @return full_path | |
195 """ | |
196 filename = self.avatar_path+'/'+hash | |
197 if not os.path.exists(filename): | |
198 error (_("Asking for an uncached avatar [%s]") % hash) | |
199 return "" | |
200 return filename | |
201 | |
202 def getCardCache(self, target): | |
203 """Request for cached values of profile | |
204 return the cached nickname and avatar if exists, else get VCard | |
205 """ | |
206 to_jid = jid.JID(target) | |
207 result = {} | |
208 nick = self.get_cache(to_jid, 'nick') | |
209 if nick: | |
210 result['nick'] = nick | |
211 avatar = self.get_cache(to_jid, 'avatar') | |
212 if avatar: | |
213 result['avatar'] = avatar | |
214 return result | |
215 | |
216 | |
217 | |
218 class XEP_0054_handler(XMPPHandler): | |
219 implements(iwokkel.IDisco) | |
220 | |
221 def __init__(self, plugin_parent): | |
222 self.plugin_parent = plugin_parent | |
223 self.host = plugin_parent.host | |
224 | |
225 def connectionInitialized(self): | |
226 self.xmlstream.addObserver(VCARD_UPDATE, self.update) | |
227 | |
228 def getDiscoInfo(self, requestor, target, nodeIdentifier=''): | |
229 return [disco.DiscoFeature(NS_VCARD)] | |
230 | |
231 def getDiscoItems(self, requestor, target, nodeIdentifier=''): | |
232 return [] | |
233 | |
234 def update(self, presence): | |
235 """Request for VCard's nickname | |
236 return the cached nickname if exists, else get VCard | |
237 """ | |
238 to_jid = jid.JID(presence['from']) | |
239 x_elem = filter (lambda x:x.name == "x", presence.elements())[0] #We only want the "x" element | |
240 for elem in x_elem.elements(): | |
241 if elem.name == 'photo': | |
242 hash = str(elem) | |
243 old_avatar = self.plugin_parent.get_cache(to_jid, 'avatar') | |
244 if not old_avatar or old_avatar != hash: | |
245 debug(_('New avatar found, requesting vcard')) | |
246 self.plugin_parent.getCard(to_jid.userhost(), self.parent.profile) |