Mercurial > libervia-backend
diff sat @ 0:c4bc297b82f0
sat:
- first public release, initial commit
author | goffi@necton2 |
---|---|
date | Sat, 29 Aug 2009 13:34:59 +0200 |
parents | |
children | a06a151fc31f |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sat Sat Aug 29 13:34:59 2009 +0200 @@ -0,0 +1,367 @@ +#!/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 <http://www.gnu.org/licenses/>. +""" + + +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")