comparison sat.tac @ 2:c49345fd7737

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