comparison sat @ 0:c4bc297b82f0

sat: - first public release, initial commit
author goffi@necton2
date Sat, 29 Aug 2009 13:34:59 +0200
parents
children a06a151fc31f
comparison
equal deleted inserted replaced
-1:000000000000 0:c4bc297b82f0
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")