448 lines
16 KiB
Python
448 lines
16 KiB
Python
#!/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.application import internet, service
|
|
from twisted.internet import glib2reactor, protocol, task
|
|
glib2reactor.install()
|
|
|
|
from twisted.words.protocols.jabber import jid, xmlstream, error
|
|
from twisted.words.xish import domish
|
|
|
|
from twisted.internet import reactor
|
|
import pdb
|
|
|
|
from wokkel import client, disco, xmppim
|
|
|
|
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 SatXMPPClient(client.XMPPClient):
|
|
|
|
def __init__(self, jid, password, host=None, port=5222):
|
|
client.XMPPClient.__init__(self, jid, password, host, port)
|
|
self.factory.clientConnectionLost = self.connectionLost
|
|
self.__connected=False
|
|
|
|
def _authd(self, xmlstream):
|
|
print "SatXMPPClient"
|
|
client.XMPPClient._authd(self, xmlstream)
|
|
self.__connected=True
|
|
print "********** CONNECTED **********"
|
|
self.streamInitialized()
|
|
|
|
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):
|
|
print "********** DISCONNECTED **********"
|
|
try:
|
|
self.keep_alife.stop()
|
|
except AttributeError:
|
|
debug("No keep_alife")
|
|
|
|
|
|
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 jid, item in roster.iteritems():
|
|
info ("new contact in roster list: %s", jid)
|
|
#FIXME: fill attributes
|
|
self.host.memory.addContact(jid, {}, item.groups)
|
|
self.host.bridge.newContact(jid, {}, item.groups)
|
|
|
|
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"""
|
|
to_jid=jid.JID(to)
|
|
xmppim.RosterClientProtocol.removeItem(self, to_jid)
|
|
#TODO: check IQ result
|
|
|
|
def addItem(self, to):
|
|
"""Add a contact to roster list"""
|
|
to_jid=jid.JID(to)
|
|
xmppim.RosterClientProtocol.addItem(self, to_jid)
|
|
#TODO: check IQ result
|
|
|
|
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)
|
|
|
|
### we check if the status is not about subscription ###
|
|
#FIXME: type is not needed anymore
|
|
#TODO: management of differents statuses (differents languages)
|
|
status = statuses.values()[0] if len(statuses) else ""
|
|
self.host.memory.addPresenceStatus(entity.full(), "", show or "",
|
|
status or "", int(priority))
|
|
|
|
#now it's time to notify frontends
|
|
pdb.set_trace()
|
|
self.host.bridge.presenceUpdate(entity.full(), "", show or "",
|
|
status or "", int(priority))
|
|
|
|
def unavailableReceived(self, entity, statuses=None):
|
|
#TODO: management of differents statuses (differents languages)
|
|
status = statuses.values()[0] if len(statuses) else ""
|
|
self.host.memory.addPresenceStatus(entity.full(), "unavailable", "",
|
|
status or "", 0)
|
|
|
|
#now it's time to notify frontends
|
|
self.host.bridge.presenceUpdate(entity.full(), "unavailable", "",
|
|
status or "", 0)
|
|
|
|
|
|
def subscribedReceived(self, entity):
|
|
debug ("subscription approved for [%s]" % entity)
|
|
|
|
def unsubscribedReceived(self, entity):
|
|
debug ("unsubscription confirmed for [%s]" % entity)
|
|
|
|
def subscribeReceived(self, entity):
|
|
#FIXME: auto answer for subscribe request, must be checked !
|
|
debug ("subscription request for [%s]" % entity)
|
|
self.subscribed(entity)
|
|
|
|
def unsubscribeReceived(self, entity):
|
|
debug ("unsubscription asked for [%s]" % entity)
|
|
|
|
class SAT(service.Service):
|
|
|
|
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._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("disconnect", self.disconnect)
|
|
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.isConnected()):
|
|
info("already connected !")
|
|
return
|
|
print "connecting..."
|
|
self.me = jid.JID(self.memory.getParamV("JabberID", "Connection"))
|
|
self.xmppclient = SatXMPPClient(self.me, self.memory.getParamV("Password", "Connection"),
|
|
self.memory.getParamV("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.xmppclient.startService()
|
|
|
|
def disconnect(self):
|
|
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):
|
|
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.roster.requestRoster()
|
|
|
|
self.presence.available()
|
|
|
|
#FIXME:tmp
|
|
self.xmlstream.addObserver("/iq[@type='set' or @type='get']", self.iqCb)
|
|
"""
|
|
###FIXME: tmp disco ###
|
|
#self.discoHandler = disco.discoHandler()
|
|
self.memory.registerFeature("http://jabber.org/protocol/disco#info")
|
|
self.disco(self.memory.getParamV("Server", "Connection"), self.serverDisco)
|
|
#we now send our presence status
|
|
|
|
# add a callback for the messages
|
|
|
|
#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 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
|
|
|
|
## 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"""
|
|
if not type in ["", "unavailable", "subscribed", "subscribe",
|
|
"unsubscribe", "unsubscribed", "prob", "error"]:
|
|
error("Type error !")
|
|
#TODO: throw an error
|
|
return
|
|
to_jid=jid.JID(to)
|
|
#TODO: refactor subscription bridge API
|
|
if type=="":
|
|
self.presence.available(to_jid, show, status, priority)
|
|
elif type=="subscribe":
|
|
self.presence.subscribe(to_jid)
|
|
elif type=="subscribed":
|
|
self.presence.subscribed(to_jid)
|
|
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.userhost())
|
|
self.setPresence(to_jid.userhost(), "subscribe")
|
|
|
|
def delContact(self, to):
|
|
"""Remove contact from roster list"""
|
|
to_jid=jid.JID(to)
|
|
self.roster.removeItem(to_jid.userhost())
|
|
self.bridge.contactDeleted(to)
|
|
|
|
|
|
## 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 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
|
|
|
|
|
|
application = service.Application('SàT')
|
|
service = SAT()
|
|
service.setServiceParent(application)
|
|
|
|
|
|
#app.memory.save() #FIXME: not the best place
|
|
#debug("Good Bye")
|