#!/usr/bin/python # -*- coding: utf-8 -*- """ SAT: a jabber client Copyright (C) 2009 Jérôme Poisson (goffi@goffi.org) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ from twisted.internet import glib2reactor, protocol glib2reactor.install() from twisted.words.protocols.jabber import client, jid, xmlstream, error from twisted.words.xish import domish from twisted.internet import reactor import pdb from sat_bridge.DBus import DBusBridge import logging from logging import debug, info, error import signal, sys import os.path from tools.memory import Memory from glob import glob ### logging configuration FIXME: put this elsewhere ### logging.basicConfig(level=logging.DEBUG, format='%(message)s') ### class SAT: def __init__(self): self.reactor=reactor self.memory=Memory() self.server_features=[] #XXX: temp dic, need to be transfered into self.memory in the future self.connected=False #FIXME: use twisted var instead self._iq_cb_map = {} #callback called when ns is found on IQ self._waiting_conf = {} #callback called when a confirmation is received self._progress_cb_map = {} #callback called when a progress is requested (key = progress id) self.plugins = {} self.bridge=DBusBridge() self.bridge.register("connect", self.connect) self.bridge.register("getContacts", self.memory.getContacts) self.bridge.register("getPresenceStatus", self.memory.getPresenceStatus) self.bridge.register("sendMessage", self.sendMessage) self.bridge.register("setParam", self.setParam) self.bridge.register("getParam", self.memory.getParam) self.bridge.register("getParams", self.memory.getParams) self.bridge.register("getParamsCategories", self.memory.getParamsCategories) self.bridge.register("getHistory", self.memory.getHistory) self.bridge.register("setPresence", self.setPresence) self.bridge.register("addContact", self.addContact) self.bridge.register("delContact", self.delContact) self.bridge.register("isConnected", self.isConnected) self.bridge.register("confirmationAnswer", self.confirmationAnswer) self.bridge.register("getProgress", self.getProgress) self._import_plugins() self.connect() def _import_plugins(self): """Import all plugins found in plugins directory""" #TODO: manage dependencies plug_lst = [os.path.splitext(plugin)[0] for plugin in map(os.path.basename,glob ("plugins/plugin*.py"))] for plug in plug_lst: plug_path = 'plugins.'+plug __import__(plug_path) mod = sys.modules[plug_path] plug_info = mod.PLUGIN_INFO info ("importing plugin: %s", plug_info['name']) self.plugins[plug_info['import_name']] = getattr(mod, plug_info['main'])(self) def connect(self): if (self.connected): info("already connected !") return info("Connecting...") self.me = jid.JID(self.memory.getParamV("JabberID", "Connection")) self.factory = client.XMPPClientFactory(self.me, self.memory.getParamV("Password", "Connection")) self.factory.addBootstrap(xmlstream.STREAM_AUTHD_EVENT,self.authd) self.factory.addBootstrap(xmlstream.INIT_FAILED_EVENT,self.failed) reactor.connectTCP(self.memory.getParamV("Server", "Connection"), 5222, self.factory) self.connectionStatus="online" #TODO: check if connection is OK self.connected=True #TODO: use startedConnecting and clientConnectionLost of XMPPClientFactory def run(self): debug("running app") reactor.run() def stop(self): debug("stopping app") reactor.stop() def authd(self,xmlstream): self.xmlstream=xmlstream roster=client.IQ(xmlstream,'get') roster.addElement(('jabber:iq:roster', 'query')) roster.addCallback(self.rosterCb) roster.send() debug("server = %s",self.memory.getParamV("Server", "Connection")) ###FIXME: tmp disco ### self.memory.registerFeature("http://jabber.org/protocol/disco#info") self.disco(self.memory.getParamV("Server", "Connection"), self.serverDisco) #we now send our presence status self.setPresence(status="Online") # add a callback for the messages xmlstream.addObserver('/message', self.gotMessage) xmlstream.addObserver('/presence', self.presenceCb) xmlstream.addObserver("/iq[@type='set' or @type='get']", self.iqCb) #reactor.callLater(2,self.sendFile,"goffi2@jabber.goffi.int/Psi", "/tmp/fakefile") def sendMessage(self,to,msg,type='chat'): #FIXME: check validity of recipient debug("Sending jabber message to %s...", to) message = domish.Element(('jabber:client','message')) message["to"] = jid.JID(to).full() message["from"] = self.me.full() message["type"] = type message.addElement("body", "jabber:client", msg) self.xmlstream.send(message) self.memory.addToHistory(self.me, self.me, jid.JID(to), message["type"], unicode(msg)) self.bridge.newMessage(message['from'], unicode(msg), to=message['to']) #We send back the message, so all clients are aware of it def setParam(self, name, value, namespace): """set wanted paramater and notice observers""" info ("setting param: %s=%s in namespace %s", name, value, namespace) self.memory.setParam(name, value, namespace) self.bridge.paramUpdate(name, value, namespace) def setRoster(self, to): """Add a contact to roster list""" to_jid=jid.JID(to) roster=client.IQ(self.xmlstream,'set') query=roster.addElement(('jabber:iq:roster', 'query')) item=query.addElement("item") item.attributes["jid"]=to_jid.userhost() roster.send() #TODO: check IQ result def delRoster(self, to): """Remove a contact from roster list""" to_jid=jid.JID(to) roster=client.IQ(self.xmlstream,'set') query=roster.addElement(('jabber:iq:roster', 'query')) item=query.addElement("item") item.attributes["jid"]=to_jid.userhost() item.attributes["subscription"]="remove" roster.send() #TODO: check IQ result def failed(self,xmlstream): debug("failed: %s", xmlstream.getErrorMessage()) debug("failed: %s", dir(xmlstream)) def isConnected(self): return self.connected ## jabber methods ## def disco (self, item, callback, node=None): """XEP-0030 Service discovery Feature.""" disco=client.IQ(self.xmlstream,'get') disco["from"]=self.me.full() disco["to"]=item disco.addElement(('http://jabber.org/protocol/disco#info', 'query')) disco.addCallback(callback) disco.send() def setPresence(self, to="", type="", show="", status="", priority=0): """Send our presence information""" presence = domish.Element(('jabber:client', 'presence')) if not type in ["", "unavailable", "subscribed", "subscribe", "unsubscribe", "unsubscribed", "prob", "error"]: error("Type error !") #TODO: throw an error return if to: presence.attributes["to"]=to if type: presence.attributes["type"]=type for element in ["show", "status", "priority"]: if locals()[element]: presence.addElement(element).addContent(unicode(locals()[element])) self.xmlstream.send(presence) def addContact(self, to): """Add a contact in roster list""" to_jid=jid.JID(to) self.setRoster(to_jid.userhost()) self.setPresence(to_jid.userhost(), "subscribe") def delContact(self, to): """Remove contact from roster list""" to_jid=jid.JID(to) self.delRoster(to_jid.userhost()) self.bridge.contactDeleted(to) def gotMessage(self,message): debug (u"got_message from: %s", message["from"]) for e in message.elements(): if e.name == "body": self.bridge.newMessage(message["from"], e.children[0]) self.memory.addToHistory(self.me, jid.JID(message["from"]), self.me, "chat", e.children[0]) break ## callbacks ## def add_IQ_cb(self, ns, cb): """Add an IQ callback on namespace ns""" debug ("Registered callback for namespace %s", ns) self._iq_cb_map[ns]=cb def iqCb(self, stanza): info ("iqCb") debug ("="*20) debug ("DEBUG:\n") debug (stanza.toXml().encode('utf-8')) debug ("="*20) #FIXME: temporary ugly code uri = stanza.firstChildElement().uri if self._iq_cb_map.has_key(uri): self._iq_cb_map[uri](stanza) #TODO: manage errors stanza def presenceCb(self, elem): info ("presence update for [%s]", elem.getAttribute("from")) debug("\n\nXML=\n%s\n\n", elem.toXml()) presence={} presence["jid"]=elem.getAttribute("from") presence["type"]=elem.getAttribute("type") or "" presence["show"]="" presence["status"]="" presence["priority"]=0 for item in elem.elements(): if presence.has_key(item.name): presence[item.name]=item.children[0] ### we check if the status is not about subscription ### #TODO: check that from jid is one we wants to subscribe (ie: check a recent subscription asking) if jid.JID(presence["jid"]).userhost()!=self.me.userhost(): if presence["type"]=="subscribed": debug ("subscription answer") elif presence["type"]=="unsubscribed": debug ("unsubscription answer") elif presence["type"]=="subscribe": #FIXME: auto answer for subscribe request, must be checked ! debug ("subscription request") self.setPresence(to=presence["jid"], type="subscribed") else: #We keep presence information only if it is not for subscription self.memory.addPresenceStatus(presence["jid"], presence["type"], presence["show"], presence["status"], int(presence["priority"])) #now it's time to notify frontends self.bridge.presenceUpdate(presence["jid"], presence["type"], presence["show"], presence["status"], int(presence["priority"])) def rosterCb(self,roster): for contact in roster.firstChildElement().elements(): info ("new contact in roster list: %s", contact['jid']) #and now the groups groups=[] for group in contact.elements(): if group.name!="group": error("Unexpected element !") break groups.append(str(group)) self.memory.addContact(contact['jid'], contact.attributes, groups) self.bridge.newContact(contact['jid'], contact.attributes, groups) def serverDisco(self, disco): """xep-0030 Discovery Protocol.""" for element in disco.firstChildElement().elements(): if element.name=="feature": debug ("Feature dectetee: %s",element["var"]) self.server_features.append(element["var"]) elif element.name=="identity": debug ("categorie= %s",element["category"]) debug ("features= %s",self.server_features) ## Generic HMI ## def askConfirmation(self, id, type, data, cb): """Add a confirmation callback""" if self._waiting_conf.has_key(id): error ("Attempt to register two callbacks for the same confirmation") else: self._waiting_conf[id] = cb self.bridge.askConfirmation(type, id, data) def confirmationAnswer(self, id, accepted, data): """Called by frontends to answer confirmation requests""" debug ("Received confirmation answer for id [%s]: %s", id, "accepted" if accepted else "refused") if not self._waiting_conf.has_key(id): error ("Received an unknown confirmation") else: cb = self._waiting_conf[id] del self._waiting_conf[id] cb(id, accepted, data) def registerProgressCB(self, id, CB): """Register a callback called when progress is requested for id""" self._progress_cb_map[id] = CB def removeProgressCB(self, id): """Remove a progress callback""" if not self._progress_cb_map.has_key(id): error ("Trying to remove an unknow progress callback") else: del self._progress_cb_map[id] def getProgress(self, id): """Return a dict with progress information data['position'] : current possition data['size'] : end_position """ data = {} try: self._progress_cb_map[id](data) except KeyError: pass #debug("Requested progress for unknown id") return data app=SAT() #TODO: tmp, use twisted way instead app.run() app.memory.save() #FIXME: not the best place debug("Good Bye")