0
|
1 #!/usr/bin/python |
|
2 # -*- coding: utf-8 -*- |
|
3 |
|
4 """ |
|
5 SAT: a jabber client |
|
6 Copyright (C) 2009 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 |
|
23 from twisted.internet import glib2reactor, protocol |
|
24 glib2reactor.install() |
|
25 |
|
26 from twisted.words.protocols.jabber import client, jid, xmlstream, error |
|
27 from twisted.words.xish import domish |
|
28 |
|
29 from twisted.internet import reactor |
|
30 import pdb |
|
31 |
|
32 from sat_bridge.DBus import DBusBridge |
|
33 import logging |
|
34 from logging import debug, info, error |
|
35 |
|
36 import signal, sys |
|
37 import os.path |
|
38 |
|
39 from tools.memory import Memory |
|
40 from glob import glob |
|
41 |
|
42 |
|
43 ### logging configuration FIXME: put this elsewhere ### |
|
44 logging.basicConfig(level=logging.DEBUG, |
|
45 format='%(message)s') |
|
46 ### |
|
47 |
|
48 |
|
49 |
|
50 |
|
51 class SAT: |
|
52 |
|
53 def __init__(self): |
|
54 self.reactor=reactor |
|
55 self.memory=Memory() |
|
56 self.server_features=[] #XXX: temp dic, need to be transfered into self.memory in the future |
|
57 self.connected=False #FIXME: use twisted var instead |
|
58 |
|
59 self._iq_cb_map = {} #callback called when ns is found on IQ |
|
60 self._waiting_conf = {} #callback called when a confirmation is received |
|
61 self._progress_cb_map = {} #callback called when a progress is requested (key = progress id) |
|
62 self.plugins = {} |
|
63 |
|
64 self.bridge=DBusBridge() |
|
65 self.bridge.register("connect", self.connect) |
|
66 self.bridge.register("getContacts", self.memory.getContacts) |
|
67 self.bridge.register("getPresenceStatus", self.memory.getPresenceStatus) |
|
68 self.bridge.register("sendMessage", self.sendMessage) |
|
69 self.bridge.register("setParam", self.setParam) |
|
70 self.bridge.register("getParam", self.memory.getParam) |
|
71 self.bridge.register("getParams", self.memory.getParams) |
|
72 self.bridge.register("getParamsCategories", self.memory.getParamsCategories) |
|
73 self.bridge.register("getHistory", self.memory.getHistory) |
|
74 self.bridge.register("setPresence", self.setPresence) |
|
75 self.bridge.register("addContact", self.addContact) |
|
76 self.bridge.register("delContact", self.delContact) |
|
77 self.bridge.register("isConnected", self.isConnected) |
|
78 self.bridge.register("confirmationAnswer", self.confirmationAnswer) |
|
79 self.bridge.register("getProgress", self.getProgress) |
|
80 |
|
81 self._import_plugins() |
|
82 self.connect() |
|
83 |
|
84 |
|
85 def _import_plugins(self): |
|
86 """Import all plugins found in plugins directory""" |
|
87 #TODO: manage dependencies |
|
88 plug_lst = [os.path.splitext(plugin)[0] for plugin in map(os.path.basename,glob ("plugins/plugin*.py"))] |
|
89 |
|
90 for plug in plug_lst: |
|
91 plug_path = 'plugins.'+plug |
|
92 __import__(plug_path) |
|
93 mod = sys.modules[plug_path] |
|
94 plug_info = mod.PLUGIN_INFO |
|
95 info ("importing plugin: %s", plug_info['name']) |
|
96 self.plugins[plug_info['import_name']] = getattr(mod, plug_info['main'])(self) |
|
97 |
|
98 def connect(self): |
|
99 if (self.connected): |
|
100 info("already connected !") |
|
101 return |
|
102 info("Connecting...") |
|
103 self.me = jid.JID(self.memory.getParamV("JabberID", "Connection")) |
|
104 self.factory = client.XMPPClientFactory(self.me, self.memory.getParamV("Password", "Connection")) |
|
105 self.factory.addBootstrap(xmlstream.STREAM_AUTHD_EVENT,self.authd) |
|
106 self.factory.addBootstrap(xmlstream.INIT_FAILED_EVENT,self.failed) |
|
107 reactor.connectTCP(self.memory.getParamV("Server", "Connection"), 5222, self.factory) |
|
108 self.connectionStatus="online" #TODO: check if connection is OK |
|
109 self.connected=True #TODO: use startedConnecting and clientConnectionLost of XMPPClientFactory |
|
110 |
|
111 |
|
112 def run(self): |
|
113 debug("running app") |
|
114 reactor.run() |
|
115 |
|
116 def stop(self): |
|
117 debug("stopping app") |
|
118 reactor.stop() |
|
119 |
|
120 def authd(self,xmlstream): |
|
121 self.xmlstream=xmlstream |
|
122 roster=client.IQ(xmlstream,'get') |
|
123 roster.addElement(('jabber:iq:roster', 'query')) |
|
124 roster.addCallback(self.rosterCb) |
|
125 roster.send() |
|
126 debug("server = %s",self.memory.getParamV("Server", "Connection")) |
|
127 |
|
128 ###FIXME: tmp disco ### |
|
129 self.memory.registerFeature("http://jabber.org/protocol/disco#info") |
|
130 self.disco(self.memory.getParamV("Server", "Connection"), self.serverDisco) |
|
131 |
|
132 |
|
133 #we now send our presence status |
|
134 self.setPresence(status="Online") |
|
135 |
|
136 # add a callback for the messages |
|
137 xmlstream.addObserver('/message', self.gotMessage) |
|
138 xmlstream.addObserver('/presence', self.presenceCb) |
|
139 xmlstream.addObserver("/iq[@type='set' or @type='get']", self.iqCb) |
|
140 #reactor.callLater(2,self.sendFile,"goffi2@jabber.goffi.int/Psi", "/tmp/fakefile") |
|
141 |
|
142 def sendMessage(self,to,msg,type='chat'): |
|
143 #FIXME: check validity of recipient |
|
144 debug("Sending jabber message to %s...", to) |
|
145 message = domish.Element(('jabber:client','message')) |
|
146 message["to"] = jid.JID(to).full() |
|
147 message["from"] = self.me.full() |
|
148 message["type"] = type |
|
149 message.addElement("body", "jabber:client", msg) |
|
150 self.xmlstream.send(message) |
|
151 self.memory.addToHistory(self.me, self.me, jid.JID(to), message["type"], unicode(msg)) |
|
152 self.bridge.newMessage(message['from'], unicode(msg), to=message['to']) #We send back the message, so all clients are aware of it |
|
153 |
|
154 def setParam(self, name, value, namespace): |
|
155 """set wanted paramater and notice observers""" |
|
156 info ("setting param: %s=%s in namespace %s", name, value, namespace) |
|
157 self.memory.setParam(name, value, namespace) |
|
158 self.bridge.paramUpdate(name, value, namespace) |
|
159 |
|
160 def setRoster(self, to): |
|
161 """Add a contact to roster list""" |
|
162 to_jid=jid.JID(to) |
|
163 roster=client.IQ(self.xmlstream,'set') |
|
164 query=roster.addElement(('jabber:iq:roster', 'query')) |
|
165 item=query.addElement("item") |
|
166 item.attributes["jid"]=to_jid.userhost() |
|
167 roster.send() |
|
168 #TODO: check IQ result |
|
169 |
|
170 def delRoster(self, to): |
|
171 """Remove a contact from roster list""" |
|
172 to_jid=jid.JID(to) |
|
173 roster=client.IQ(self.xmlstream,'set') |
|
174 query=roster.addElement(('jabber:iq:roster', 'query')) |
|
175 item=query.addElement("item") |
|
176 item.attributes["jid"]=to_jid.userhost() |
|
177 item.attributes["subscription"]="remove" |
|
178 roster.send() |
|
179 #TODO: check IQ result |
|
180 |
|
181 |
|
182 def failed(self,xmlstream): |
|
183 debug("failed: %s", xmlstream.getErrorMessage()) |
|
184 debug("failed: %s", dir(xmlstream)) |
|
185 |
|
186 def isConnected(self): |
|
187 return self.connected |
|
188 |
|
189 ## jabber methods ## |
|
190 |
|
191 def disco (self, item, callback, node=None): |
|
192 """XEP-0030 Service discovery Feature.""" |
|
193 disco=client.IQ(self.xmlstream,'get') |
|
194 disco["from"]=self.me.full() |
|
195 disco["to"]=item |
|
196 disco.addElement(('http://jabber.org/protocol/disco#info', 'query')) |
|
197 disco.addCallback(callback) |
|
198 disco.send() |
|
199 |
|
200 |
|
201 def setPresence(self, to="", type="", show="", status="", priority=0): |
|
202 """Send our presence information""" |
|
203 presence = domish.Element(('jabber:client', 'presence')) |
|
204 if not type in ["", "unavailable", "subscribed", "subscribe", |
|
205 "unsubscribe", "unsubscribed", "prob", "error"]: |
|
206 error("Type error !") |
|
207 #TODO: throw an error |
|
208 return |
|
209 |
|
210 if to: |
|
211 presence.attributes["to"]=to |
|
212 if type: |
|
213 presence.attributes["type"]=type |
|
214 |
|
215 for element in ["show", "status", "priority"]: |
|
216 if locals()[element]: |
|
217 presence.addElement(element).addContent(unicode(locals()[element])) |
|
218 |
|
219 self.xmlstream.send(presence) |
|
220 |
|
221 def addContact(self, to): |
|
222 """Add a contact in roster list""" |
|
223 to_jid=jid.JID(to) |
|
224 self.setRoster(to_jid.userhost()) |
|
225 self.setPresence(to_jid.userhost(), "subscribe") |
|
226 |
|
227 def delContact(self, to): |
|
228 """Remove contact from roster list""" |
|
229 to_jid=jid.JID(to) |
|
230 self.delRoster(to_jid.userhost()) |
|
231 self.bridge.contactDeleted(to) |
|
232 |
|
233 def gotMessage(self,message): |
|
234 debug (u"got_message from: %s", message["from"]) |
|
235 for e in message.elements(): |
|
236 if e.name == "body": |
|
237 self.bridge.newMessage(message["from"], e.children[0]) |
|
238 self.memory.addToHistory(self.me, jid.JID(message["from"]), self.me, "chat", e.children[0]) |
|
239 break |
|
240 |
|
241 ## callbacks ## |
|
242 |
|
243 def add_IQ_cb(self, ns, cb): |
|
244 """Add an IQ callback on namespace ns""" |
|
245 debug ("Registered callback for namespace %s", ns) |
|
246 self._iq_cb_map[ns]=cb |
|
247 |
|
248 def iqCb(self, stanza): |
|
249 info ("iqCb") |
|
250 debug ("="*20) |
|
251 debug ("DEBUG:\n") |
|
252 debug (stanza.toXml().encode('utf-8')) |
|
253 debug ("="*20) |
|
254 #FIXME: temporary ugly code |
|
255 uri = stanza.firstChildElement().uri |
|
256 if self._iq_cb_map.has_key(uri): |
|
257 self._iq_cb_map[uri](stanza) |
|
258 #TODO: manage errors stanza |
|
259 |
|
260 def presenceCb(self, elem): |
|
261 info ("presence update for [%s]", elem.getAttribute("from")) |
|
262 debug("\n\nXML=\n%s\n\n", elem.toXml()) |
|
263 presence={} |
|
264 presence["jid"]=elem.getAttribute("from") |
|
265 presence["type"]=elem.getAttribute("type") or "" |
|
266 presence["show"]="" |
|
267 presence["status"]="" |
|
268 presence["priority"]=0 |
|
269 |
|
270 for item in elem.elements(): |
|
271 if presence.has_key(item.name): |
|
272 presence[item.name]=item.children[0] |
|
273 |
|
274 ### we check if the status is not about subscription ### |
|
275 #TODO: check that from jid is one we wants to subscribe (ie: check a recent subscription asking) |
|
276 if jid.JID(presence["jid"]).userhost()!=self.me.userhost(): |
|
277 if presence["type"]=="subscribed": |
|
278 debug ("subscription answer") |
|
279 elif presence["type"]=="unsubscribed": |
|
280 debug ("unsubscription answer") |
|
281 elif presence["type"]=="subscribe": |
|
282 #FIXME: auto answer for subscribe request, must be checked ! |
|
283 debug ("subscription request") |
|
284 self.setPresence(to=presence["jid"], type="subscribed") |
|
285 else: |
|
286 #We keep presence information only if it is not for subscription |
|
287 self.memory.addPresenceStatus(presence["jid"], presence["type"], presence["show"], |
|
288 presence["status"], int(presence["priority"])) |
|
289 |
|
290 #now it's time to notify frontends |
|
291 self.bridge.presenceUpdate(presence["jid"], presence["type"], presence["show"], |
|
292 presence["status"], int(presence["priority"])) |
|
293 |
|
294 def rosterCb(self,roster): |
|
295 for contact in roster.firstChildElement().elements(): |
|
296 info ("new contact in roster list: %s", contact['jid']) |
|
297 #and now the groups |
|
298 groups=[] |
|
299 for group in contact.elements(): |
|
300 if group.name!="group": |
|
301 error("Unexpected element !") |
|
302 break |
|
303 groups.append(str(group)) |
|
304 self.memory.addContact(contact['jid'], contact.attributes, groups) |
|
305 self.bridge.newContact(contact['jid'], contact.attributes, groups) |
|
306 |
|
307 def serverDisco(self, disco): |
|
308 """xep-0030 Discovery Protocol.""" |
|
309 for element in disco.firstChildElement().elements(): |
|
310 if element.name=="feature": |
|
311 debug ("Feature dectetee: %s",element["var"]) |
|
312 self.server_features.append(element["var"]) |
|
313 elif element.name=="identity": |
|
314 debug ("categorie= %s",element["category"]) |
|
315 debug ("features= %s",self.server_features) |
|
316 |
|
317 ## Generic HMI ## |
|
318 |
|
319 def askConfirmation(self, id, type, data, cb): |
|
320 """Add a confirmation callback""" |
|
321 if self._waiting_conf.has_key(id): |
|
322 error ("Attempt to register two callbacks for the same confirmation") |
|
323 else: |
|
324 self._waiting_conf[id] = cb |
|
325 self.bridge.askConfirmation(type, id, data) |
|
326 |
|
327 |
|
328 def confirmationAnswer(self, id, accepted, data): |
|
329 """Called by frontends to answer confirmation requests""" |
|
330 debug ("Received confirmation answer for id [%s]: %s", id, "accepted" if accepted else "refused") |
|
331 if not self._waiting_conf.has_key(id): |
|
332 error ("Received an unknown confirmation") |
|
333 else: |
|
334 cb = self._waiting_conf[id] |
|
335 del self._waiting_conf[id] |
|
336 cb(id, accepted, data) |
|
337 |
|
338 def registerProgressCB(self, id, CB): |
|
339 """Register a callback called when progress is requested for id""" |
|
340 self._progress_cb_map[id] = CB |
|
341 |
|
342 def removeProgressCB(self, id): |
|
343 """Remove a progress callback""" |
|
344 if not self._progress_cb_map.has_key(id): |
|
345 error ("Trying to remove an unknow progress callback") |
|
346 else: |
|
347 del self._progress_cb_map[id] |
|
348 |
|
349 def getProgress(self, id): |
|
350 """Return a dict with progress information |
|
351 data['position'] : current possition |
|
352 data['size'] : end_position |
|
353 """ |
|
354 data = {} |
|
355 try: |
|
356 self._progress_cb_map[id](data) |
|
357 except KeyError: |
|
358 pass |
|
359 #debug("Requested progress for unknown id") |
|
360 return data |
|
361 |
|
362 |
|
363 app=SAT() #TODO: tmp, use twisted way instead |
|
364 app.run() |
|
365 |
|
366 app.memory.save() #FIXME: not the best place |
|
367 debug("Good Bye") |