#!/usr/bin/python # -*- coding: utf-8 -*- """ SAT: a jabber client Copyright (C) 2009, 2010 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 . """ CONST = { 'client_name' : u'SàT (Salut à toi)', 'client_version' : u'0.0.2', #Please add 'D' at the end for dev versions 'local_dir' : '~/.sat' } from twisted.application import internet, service from twisted.internet import glib2reactor, protocol, task glib2reactor.install() from twisted.words.protocols.jabber import jid, xmlstream from twisted.words.protocols.jabber import error as jab_error from twisted.words.xish import domish from twisted.internet import reactor import pdb from wokkel import client, disco, xmppim, generic, compat 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 tools.xml_tools import XMLTools from glob import glob try: from twisted.words.protocols.xmlstream import XMPPHandler except ImportError: from wokkel.subprotocols import XMPPHandler ### logging configuration FIXME: put this elsewhere ### logging.basicConfig(level=logging.DEBUG, format='%(message)s') ### sat_id = 0 def sat_next_id(): global sat_id sat_id+=1 return "sat_id_"+str(sat_id) class SatXMPPClient(client.XMPPClient): def __init__(self, bridge, user_jid, password, host=None, port=5222): client.XMPPClient.__init__(self, user_jid, password, host, port) self.factory.clientConnectionLost = self.connectionLost self.__connected=False self.bridge = bridge def _authd(self, xmlstream): print "SatXMPPClient" client.XMPPClient._authd(self, xmlstream) self.__connected=True print "********** CONNECTED **********" self.streamInitialized() self.bridge.connected() #we send the signal to the clients def streamInitialized(self): """Called after _authd""" self.keep_alife = task.LoopingCall(self.xmlstream.send, " ") #Needed to avoid disconnection (specially with openfire) self.keep_alife.start(180) def isConnected(self): return self.__connected def connectionLost(self, connector, unused_reason): self.__connected=False print "********** DISCONNECTED **********" try: self.keep_alife.stop() except AttributeError: debug("No keep_alife") self.bridge.disconnected() #we send the signal to the clients class SatMessageProtocol(xmppim.MessageProtocol): def __init__(self, host): xmppim.MessageProtocol.__init__(self) self.host = host def onMessage(self, message): debug (u"got_message from: %s", message["from"]) for e in message.elements(): if e.name == "body": self.host.bridge.newMessage(message["from"], e.children[0]) self.host.memory.addToHistory(self.host.me, jid.JID(message["from"]), self.host.me, "chat", e.children[0]) break class SatRosterProtocol(xmppim.RosterClientProtocol): def __init__(self, host): xmppim.RosterClientProtocol.__init__(self) self.host = host def rosterCb(self, roster): for raw_jid, item in roster.iteritems(): self.onRosterSet(item) def requestRoster(self): """ ask the server for Roster list """ debug("requestRoster") self.getRoster().addCallback(self.rosterCb) def removeItem(self, to): """Remove a contact from roster list""" xmppim.RosterClientProtocol.removeItem(self, to) #TODO: check IQ result def addItem(self, to): """Add a contact to roster list""" xmppim.RosterClientProtocol.addItem(self, to) #TODO: check IQ result def onRosterSet(self, item): """Called when a new/update roster item is received""" #TODO: send a signal to frontends item_attr = {'to': str(item.subscriptionTo), 'from': str(item.subscriptionFrom), 'ask': str(item.ask) } if item.name: item_attr['name'] = item.name info ("new contact in roster list: %s", item.jid.full()) self.host.memory.addContact(item.jid, item_attr, item.groups) self.host.bridge.newContact(item.jid.full(), item_attr, item.groups) def onRosterRemove(self, entity): """Called when a roster removal event is received""" #TODO: send a signal to frontends print "removing %s from roster list" % entity.full() self.host.memory.delContact(entity) class SatPresenceProtocol(xmppim.PresenceClientProtocol): def __init__(self, host): xmppim.PresenceClientProtocol.__init__(self) self.host = host def availableReceived(self, entity, show=None, statuses=None, priority=0): info ("presence update for [%s]", entity) if statuses.has_key(None): #we only want string keys statuses["default"] = statuses[None] del statuses[None] self.host.memory.addPresenceStatus(entity, show or "", int(priority), statuses) #now it's time to notify frontends self.host.bridge.presenceUpdate(entity.full(), show or "", int(priority), statuses) def unavailableReceived(self, entity, statuses=None): if statuses and statuses.has_key(None): #we only want string keys statuses["default"] = statuses[None] del statuses[None] self.host.memory.addPresenceStatus(entity, "unavailable", 0, statuses) #now it's time to notify frontends self.host.bridge.presenceUpdate(entity.full(), "unavailable", 0, statuses) def available(self, entity=None, show=None, statuses=None, priority=0): if statuses and statuses.has_key('default'): statuses[None] = statuses['default'] del statuses['default'] xmppim.PresenceClientProtocol.available(self, entity, show, statuses, priority) def subscribedReceived(self, entity): debug ("subscription approved for [%s]" % entity.userhost()) self.host.memory.delWaitingSub(entity.userhost()) self.host.bridge.subscribe('subscribed', entity.userhost()) def unsubscribedReceived(self, entity): debug ("unsubscription confirmed for [%s]" % entity.userhost()) self.host.memory.delWaitingSub(entity.userhost()) self.host.bridge.subscribe('unsubscribed', entity.userhost()) def subscribeReceived(self, entity): debug ("subscription request for [%s]" % entity.userhost()) self.host.memory.addWaitingSub('subscribe', entity.userhost()) self.host.bridge.subscribe('subscribe', entity.userhost()) def unsubscribeReceived(self, entity): debug ("unsubscription asked for [%s]" % entity.userhost()) self.host.memory.addWaitingSub('unsubscribe', entity.userhost()) self.host.bridge.subscribe('unsubscribe', entity.userhost()) class SatDiscoProtocol(disco.DiscoClientProtocol): def __init__(self, host): disco.DiscoClientProtocol.__init__(self) class SatFallbackHandler(generic.FallbackHandler): def __init__(self, host): generic.FallbackHandler.__init__(self) def iqFallback(self, iq): #pdb.set_trace() print "iqFallback: xml = [%s], handled=%s" % (iq.toXml(), "True" if iq.handled else "False") generic.FallbackHandler.iqFallback(self, iq) class RegisteringAuthenticator(xmlstream.ConnectAuthenticator): def __init__(self, host, jabber_host, user_login, user_pass, answer_id): xmlstream.ConnectAuthenticator.__init__(self, jabber_host) self.host = host self.jabber_host = jabber_host self.user_login = user_login self.user_pass = user_pass self.answer_id = answer_id print "Registration asked for",user_login, user_pass, jabber_host def connectionMade(self): print "connectionMade" self.xmlstream.namespace = "jabber:client" self.xmlstream.sendHeader() iq = compat.IQ(self.xmlstream, 'set') iq["to"] = self.jabber_host query = iq.addElement(('jabber:iq:register', 'query')) _user = query.addElement('username') _user.addContent(self.user_login) _pass = query.addElement('password') _pass.addContent(self.user_pass) reg = iq.send(self.jabber_host).addCallbacks(self.registrationAnswer, self.registrationFailure) def registrationAnswer(self, answer): debug ("registration answer: %s" % answer.toXml()) answer_type = "SUCCESS" answer_data={"message":"Registration successfull"} self.host.bridge.actionResult(answer_type, self.answer_id, answer_data) self.xmlstream.sendFooter() def registrationFailure(self, failure): info ("Registration failure: %s" % str(failure.value)) answer_type = "ERROR" answer_data = {} if failure.value.condition == 'conflict': answer_data['reason'] = 'conflict' answer_data={"message":"Username already exists, please choose an other one"} else: answer_data['reason'] = 'unknown' answer_data={"message":"Registration failed (%s)" % str(failure.value.condition)} self.host.bridge.actionResult(answer_type, self.answer_id, answer_data) self.xmlstream.sendFooter() class SAT(service.Service): def get_next_id(self): return sat_next_id() def get_const(self, name): """Return a constant""" if not CONST.has_key(name): error('Trying to access an undefined constant') raise Exception return CONST[name] def set_const(self, name, value): """Save a constant""" if CONST.has_key(name): error('Trying to redefine a constant') raise Exception CONST[name] = value def __init__(self): #TODO: standardize callback system local_dir = os.path.expanduser(self.get_const('local_dir')) if not os.path.exists(local_dir): os.makedirs(local_dir) 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.__general_cb_map = {} #callback called for general reasons (key = name) self.__private_data = {} #used for internal callbacks (key = id) self.plugins = {} self.memory=Memory(self) self.server_features=[] #XXX: temp dic, need to be transfered into self.memory in the future self.bridge=DBusBridge() self.bridge.register("registerNewAccount", self.registerNewAccount) self.bridge.register("connect", self.connect) self.bridge.register("disconnect", self.disconnect) self.bridge.register("getContacts", self.memory.getContacts) self.bridge.register("getPresenceStatus", self.memory.getPresenceStatus) self.bridge.register("getWaitingSub", self.memory.getWaitingSub) self.bridge.register("sendMessage", self.sendMessage) self.bridge.register("setParam", self.setParam) self.bridge.register("getParamA", self.memory.getParamA) self.bridge.register("getParams", self.memory.getParams) self.bridge.register("getParamsForCategory", self.memory.getParamsForCategory) self.bridge.register("getParamsCategories", self.memory.getParamsCategories) self.bridge.register("getHistory", self.memory.getHistory) self.bridge.register("setPresence", self.setPresence) self.bridge.register("subscription", self.subscription) self.bridge.register("addContact", self.addContact) self.bridge.register("delContact", self.delContact) self.bridge.register("isConnected", self.isConnected) self.bridge.register("launchAction", self.launchAction) self.bridge.register("confirmationAnswer", self.confirmationAnswer) self.bridge.register("getProgress", self.getProgress) self._import_plugins() 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) #TODO: test xmppclient presence and register handler parent def connect(self): """Connect to jabber server""" if (self.isConnected()): info("already connected !") return print "connecting..." self.me = jid.JID(self.memory.getParamA("JabberID", "Connection")) self.xmppclient = SatXMPPClient(self.bridge, self.me, self.memory.getParamA("Password", "Connection"), self.memory.getParamA("Server", "Connection"), 5222) self.xmppclient.streamInitialized = self.streamInitialized self.messageProt = SatMessageProtocol(self) self.messageProt.setHandlerParent(self.xmppclient) self.roster = SatRosterProtocol(self) self.roster.setHandlerParent(self.xmppclient) self.presence = SatPresenceProtocol(self) self.presence.setHandlerParent(self.xmppclient) self.fallBack = SatFallbackHandler(self) self.fallBack.setHandlerParent(self.xmppclient) self.versionHandler = generic.VersionHandler(self.get_const('client_name'), self.get_const('client_version')) self.versionHandler.setHandlerParent(self.xmppclient) debug ("setting plugins parents") for plugin in self.plugins.iteritems(): if isinstance(plugin[1], XMPPHandler): plugin[1].setHandlerParent(self.xmppclient) self.xmppclient.startService() def disconnect(self): """disconnect from jabber server""" if (not self.isConnected()): info("not connected !") return info("Disconnecting...") self.xmppclient.stopService() def startService(self): info("Salut à toi ô mon frère !") self.connect() def stopService(self): self.memory.save() info("Salut aussi à Rantanplan") def run(self): debug("running app") reactor.run() def stop(self): debug("stopping app") reactor.stop() def streamInitialized(self): """Called when xmlstream is OK""" SatXMPPClient.streamInitialized(self.xmppclient) debug ("XML stream is initialized") self.xmlstream = self.xmppclient.xmlstream self.me = self.xmppclient.jid #in case of the ressource has changed self.disco = SatDiscoProtocol(self) self.disco.setHandlerParent(self.xmppclient) self.discoHandler = disco.DiscoHandler() self.discoHandler.setHandlerParent(self.xmppclient) self.roster.requestRoster() self.presence.available() self.disco.requestInfo(jid.JID(self.memory.getParamA("Server", "Connection"))).addCallback(self.serverDisco) ## Misc methods ## def registerNewAccount(self, login, password, server, port = 5222, id = None): """Connect to a server and create a new account using in-band registration""" next_id = id or sat_next_id() #the id is used to send server's answer serverRegistrer = xmlstream.XmlStreamFactory(RegisteringAuthenticator(self, server, login, password, next_id)) connector = reactor.connectTCP(server, port, serverRegistrer) serverRegistrer.clientConnectionLost = lambda conn, reason: connector.disconnect() return next_id def registerNewAccountCB(self, id, data): user = jid.parse(self.memory.getParamA("JabberID", "Connection"))[0] password = self.memory.getParamA("Password", "Connection") server = self.memory.getParamA("Server", "Connection") if not user or not password or not server: info ('No user or server given') #TODO: a proper error message must be sent to frontend self.actionResult(id, "ERROR", {'message':"No user, password or server given, can't register new account."}) return confirm_id = sat_next_id() self.__private_data[confirm_id]=id self.askConfirmation(confirm_id, "YES/NO", {"message":"Are you sure to register new account [%s] to server %s ?" % (user, server)}, self.regisConfirmCB) print ("===============+++++++++++ REGISTER NEW ACCOUNT++++++++++++++============") print "id=",id print "data=",data def regisConfirmCB(self, id, accepted, data): print "register Confirmation CB ! (%s)" % str(accepted) action_id = self.__private_data[id] del self.__private_data[id] user = jid.parse(self.memory.getParamA("JabberID", "Connection"))[0] password = self.memory.getParamA("Password", "Connection") server = self.memory.getParamA("Server", "Connection") if accepted: self.registerNewAccount(user, password, server, id=action_id) else: self.actionResult(action_id, "SUPPRESS", {}) def submitForm(self, action, target, fields): """submit a form @param target: target jid where we are submitting @param fields: list of tuples (name, value) @return: tuple: (id, deferred) """ to_jid = jid.JID(target) iq = compat.IQ(self.xmlstream, 'set') iq["to"] = target iq["from"] = self.me.full() query = iq.addElement(('jabber:iq:register', 'query')) if action=='SUBMIT': form = XMLTools.tupleList2dataForm(fields) query.addChild(form.toElement()) elif action=='CANCEL': query.addElement('remove') else: error ("FIXME FIXME FIXME: Unmanaged action (%s) in submitForm" % action) raise NotImplementedError deferred = iq.send(target) return (iq['id'], deferred) ## Client management ## def setParam(self, name, value, category): """set wanted paramater and notice observers""" info ("setting param: %s=%s in category %s", name, value, category) self.memory.setParam(name, value, category) def failed(self,xmlstream): debug("failed: %s", xmlstream.getErrorMessage()) debug("failed: %s", dir(xmlstream)) def isConnected(self): try: if self.xmppclient.isConnected(): return True except AttributeError: #xmppclient not available pass return False def launchAction(self, type, data): """Launch a specific action asked by client @param type: action type (button) @param data: needed data to launch the action @return: action id for result, or empty string in case or error """ if type=="button": try: cb_name = self.memory.getParamA(data["name"], data["category"], "callback") except KeyError: error ("Incomplete data") return "" id = sat_next_id() self.callGeneralCB(cb_name, id, data) return id else: error ("Unknown action type") return "" ## jabber methods ## 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 setPresence(self, to="", show="", priority = 0, statuses={}): """Send our presence information""" to_jid = jid.JID(to) if to else None self.presence.available(to_jid, show, statuses, priority) def subscription(self, type, raw_jid): """Called to manage subscription""" to_jid = jid.JID(raw_jid) debug ('subsciption request [%s] for %s', type, to_jid.full()) if type=="subscribe": self.presence.subscribe(to_jid) elif type=="subscribed": self.presence.subscribed(to_jid) contact = self.memory.getContact(to_jid) if not contact or not bool(contact['to']): #we automatically subscribe to 'to' presence debug('sending automatic "to" subscription request') self.subscription('subscribe', to_jid.userhost()) elif type=="unsubscribe": self.presence.unsubscribe(to_jid) elif type=="unsubscribed": self.presence.unsubscribed(to_jid) def addContact(self, to): """Add a contact in roster list""" to_jid=jid.JID(to) self.roster.addItem(to_jid) self.presence.subscribe(to_jid) def delContact(self, to): """Remove contact from roster list""" to_jid=jid.JID(to) self.roster.removeItem(to_jid) self.presence.unsubscribe(to_jid) self.bridge.contactDeleted(to) ## callbacks ## def serverDisco(self, disco): """xep-0030 Discovery Protocol.""" for feature in disco.features: debug ("Feature found: %s",feature) self.server_features.append(feature) for cat, type in disco.identities: debug ("Identity found: [%s/%s] %s" % (cat, type, disco.identities[(cat,type)])) ## Generic HMI ## def actionResult(self, id, type, data): """Send the result of an action @param id: same id used with action @param type: result type ("PARAM", "SUCCESS", "ERROR", "FORM") @param data: dictionary """ self.bridge.actionResult(type, id, data) def actionResultExt(self, id, type, data): """Send the result of an action, extended version @param id: same id used with action @param type: result type /!\ only "DICT_DICT" for this method @param data: dictionary of dictionaries """ if type != "DICT_DICT": error("type for actionResultExt must be DICT_DICT, fixing it") type = "DICT_DICT" self.bridge.actionResultExt(type, id, data) def askConfirmation(self, id, type, data, cb): """Add a confirmation callback @param id: id used to get answer @param type: confirmation type ("YES/NO", "FILE_TRANSFERT") @param data: data (depend of confirmation type) @param cb: callback called with the answer """ 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 def registerGeneralCB(self, name, CB): """Register a callback called for general reason""" self.__general_cb_map[name] = CB def removeGeneralCB(self, name): """Remove a general callback""" if not self.__general_cb_map.has_key(name): error ("Trying to remove an unknow general callback") else: del self.__general_cb_map[name] def callGeneralCB(self, name, *args, **kwargs): """Call general function back""" try: return self.__general_cb_map[name](*args, **kwargs) except KeyError: error("Trying to call unknown function") return None application = service.Application('SàT') service = SAT() service.setServiceParent(application)