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)