#!/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.2D', #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 import gettext gettext.install('sat', "i18n", unicode=True) 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, host_app, profile, 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.profile = profile self.host_app = host_app def _authd(self, xmlstream): print "SatXMPPClient" client.XMPPClient._authd(self, xmlstream) self.__connected=True info (_("********** [%s] CONNECTED **********") % self.profile) self.streamInitialized() self.host_app.bridge.connected(self.profile) #we send the signal to the clients def streamInitialized(self): """Called after _authd""" debug (_("XML stream is initialized")) self.keep_alife = task.LoopingCall(self.xmlstream.send, " ") #Needed to avoid disconnection (specially with openfire) self.keep_alife.start(180) self.disco = SatDiscoProtocol(self) self.disco.setHandlerParent(self) self.discoHandler = disco.DiscoHandler() self.discoHandler.setHandlerParent(self) self.roster.requestRoster() self.presence.available() self.disco.requestInfo(jid.JID(self.host_app.memory.getParamA("Server", "Connection", profile_key=self.profile))).addCallback(self.host_app.serverDisco) #FIXME: use these informations def isConnected(self): return self.__connected def connectionLost(self, connector, unused_reason): self.__connected=False info (_("********** [%s] DISCONNECTED **********") % self.profile) try: self.keep_alife.stop() except AttributeError: debug (_("No keep_alife")) self.host_app.bridge.disconnected(self.profile) #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": type = message['type'] if message.hasAttribute('type') else 'chat' #FIXME: check specs self.host.bridge.newMessage(message["from"], e.children[0], type, profile=self.parent.profile) self.host.memory.addToHistory(self.parent.jid, jid.JID(message["from"]), self.parent.jid, "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.parent.profile) self.host.bridge.newContact(item.jid.full(), item_attr, item.groups, self.parent.profile) 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, self.parent.profile) 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, self.parent.profile) #now it's time to notify frontends self.host.bridge.presenceUpdate(entity.full(), show or "", int(priority), statuses, self.parent.profile) 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, self.parent.profile) #now it's time to notify frontends self.host.bridge.presenceUpdate(entity.full(), "unavailable", 0, statuses, self.parent.profile) 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.parent.profile) self.host.bridge.subscribe('subscribed', entity.userhost(), self.parent.profile) def unsubscribedReceived(self, entity): debug (_("unsubscription confirmed for [%s]") % entity.userhost()) self.host.memory.delWaitingSub(entity.userhost(), self.parent.profile) self.host.bridge.subscribe('unsubscribed', entity.userhost(), self.parent.profile) def subscribeReceived(self, entity): debug (_("subscription request for [%s]") % entity.userhost()) self.host.memory.addWaitingSub('subscribe', entity.userhost(), self.parent.profile) self.host.bridge.subscribe('subscribe', entity.userhost(), self.parent.profile) def unsubscribeReceived(self, entity): debug (_("unsubscription asked for [%s]") % entity.userhost()) self.host.memory.addWaitingSub('unsubscribe', entity.userhost(), self.parent.profile) self.host.bridge.subscribe('unsubscribe', entity.userhost(), self.parent.profile) 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.profiles = {} 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("getProfileName", self.memory.getProfileName) self.bridge.register("getProfilesList", self.memory.getProfilesList) self.bridge.register("createProfile", self.memory.createProfile) self.bridge.register("deleteProfile", self.memory.deleteProfile) 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) if plug_info.has_key('handler') and plug_info['handler'] == 'yes': self.plugins[plug_info['import_name']].is_handler = True else: self.plugins[plug_info['import_name']].is_handler = False #TODO: test xmppclient presence and register handler parent def connect(self, profile_key = '@DEFAULT@'): """Connect to jabber server""" profile = self.memory.getProfileName(profile_key) if not profile_key: error (_('Trying to connect a non-exsitant profile')) return if (self.isConnected()): info(_("already connected !")) return print "connecting..." current = self.profiles[profile] = SatXMPPClient(self, profile, jid.JID(self.memory.getParamA("JabberID", "Connection", profile_key = profile_key), profile), self.memory.getParamA("Password", "Connection", profile_key = profile_key), self.memory.getParamA("Server", "Connection", profile_key = profile_key), 5222) current.messageProt = SatMessageProtocol(self) current.messageProt.setHandlerParent(current) current.roster = SatRosterProtocol(self) current.roster.setHandlerParent(current) current.presence = SatPresenceProtocol(self) current.presence.setHandlerParent(current) current.fallBack = SatFallbackHandler(self) current.fallBack.setHandlerParent(current) current.versionHandler = generic.VersionHandler(self.get_const('client_name'), self.get_const('client_version')) current.versionHandler.setHandlerParent(current) debug (_("setting plugins parents")) for plugin in self.plugins.iteritems(): if plugin[1].is_handler: plugin[1].getHandler().setHandlerParent(current) current.startService() def disconnect(self, profile_key='@DEFAULT@'): """disconnect from jabber server""" if (not self.isConnected(profile_key)): info(_("not connected !")) return profile = self.memory.getProfileName(profile_key) info(_("Disconnecting...")) self.profiles[profile].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() ## Misc methods ## def getJidNStream(self, profile_key): """Convenient method to get jid and stream from profile key @return: tuple (jid, xmlstream) from profile, can be None""" profile = self.memory.getProfileName(profile_key) if not profile or not self.profiles[profile].isConnected(): return (None, None) return (self.profiles[profile].jid, self.profiles[profile].xmlstream) def getClient(self, profile_key): """Convenient method to get client from profile key @return: client or None if it doesn't exist""" profile = self.memory.getProfileName(profile_key) if not profile: return None return self.profiles[profile] 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): #FIXME: gof: profile not managed here ! 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 [%(user)s] to server %(server)s ?") % {'user':user, 'server':server}}, self.regisConfirmCB) print ("===============+++++++++++ REGISTER NEW ACCOUNT++++++++++++++============") print "id=",id print "data=",data def regisConfirmCB(self, id, accepted, data): #FIXME: gof: profile not managed here ! 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, profile_key='@DEFAULT@'): """submit a form @param target: target jid where we are submitting @param fields: list of tuples (name, value) @return: tuple: (id, deferred) """ profile = self.memory.getProfileName(profile_key) assert(profile) to_jid = jid.JID(target) iq = compat.IQ(self.profiles[profile].xmlstream, 'set') iq["to"] = target iq["from"] = self.profiles[profile].jid.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, profile_key='@DEFAULT@'): """set wanted paramater and notice observers""" info (_("setting param: %(name)s=%(value)s in category %(category)s") % {'name':name, 'value':value, 'category':category}) self.memory.setParam(name, value, category, profile_key) def isConnected(self, profile_key='@DEFAULT@'): """Return connection status of profile @param profile_key: key_word or profile name to determine profile name @return True if connected """ profile = self.memory.getProfileName(profile_key) if not profile: error (_('asking connection status for a non-existant profile')) raise Exception #TODO: raise a proper exception if not self.profiles.has_key(profile): return False return self.profiles[profile].isConnected() 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', profile_key='@DEFAULT@'): print "sendtype=", type #gof #FIXME: check validity of recipient profile = self.memory.getProfileName(profile_key) assert(profile) current_jid = self.profiles[profile].jid debug(_("Sending jabber message to %s..."), to) message = domish.Element(('jabber:client','message')) message["to"] = jid.JID(to).full() message["from"] = current_jid.full() message["type"] = type message.addElement("body", "jabber:client", msg) self.profiles[profile].xmlstream.send(message) self.memory.addToHistory(current_jid, current_jid, jid.JID(to), message["type"], unicode(msg)) self.bridge.newMessage(message['from'], unicode(msg), to=message['to'], type=type, profile=profile) #We send back the message, so all clients are aware of it def setPresence(self, to="", show="", priority = 0, statuses={}, profile_key='@DEFAULT@'): """Send our presence information""" profile = self.memory.getProfileName(profile_key) assert(profile) to_jid = jid.JID(to) if to else None self.profiles[profile].presence.available(to_jid, show, statuses, priority) def subscription(self, type, raw_jid, profile_key='@DEFAULT@'): """Called to manage subscription""" profile = self.memory.getProfileName(profile_key) assert(profile) to_jid = jid.JID(raw_jid) debug (_('subsciption request [%(type)s] for %(jid)s') % {'type':type, 'jid':to_jid.full()}) if type=="subscribe": self.profiles[profile].presence.subscribe(to_jid) elif type=="subscribed": self.profiles[profile].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.profiles[profile].presence.unsubscribe(to_jid) elif type=="unsubscribed": self.profiles[profile].presence.unsubscribed(to_jid) def addContact(self, to, profile_key='@DEFAULT@'): """Add a contact in roster list""" profile = self.memory.getProfileName(profile_key) assert(profile) to_jid=jid.JID(to) self.profiles[profile].roster.addItem(to_jid) self.profiles[profile].presence.subscribe(to_jid) def delContact(self, to, profile_key='@DEFAULT@'): """Remove contact from roster list""" profile = self.memory.getProfileName(profile_key) assert(profile) to_jid=jid.JID(to) self.profiles[profile].roster.removeItem(to_jid) self.profiles[profile].presence.unsubscribe(to_jid) self.host.bridge.contactDeleted(to, profile) ## 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: [%(category)s/%(type)s] %(identity)s") % {'category':cat, 'type':type, 'identity':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 [%(id)s]: %(success)s") % {'id': id, 'success':u("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)