libervia-backend/sat.tac
Goffi af75e43558 wokkel integration, part II
- disco client
- fallback handler (for unmanaged iq requests)
- software version (XEP-0092)
2009-10-30 20:05:25 +01:00

445 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/>.
"""
client_name = u'SàT (Salut à toi)'
client_version = '0.0.1'
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, generic
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
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 SatDiscoProtocol(disco.DiscoClientProtocol):
def __init__(self, host):
disco.DiscoClientProtocol.__init__(self)
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.fallBack = generic.FallbackHandler()
self.fallBack.setHandlerParent(self.xmppclient)
self.versionHandler = generic.VersionHandler(unicode(client_name), client_version)
self.versionHandler.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.disco = SatDiscoProtocol(self)
self.disco.setHandlerParent(self.xmppclient)
self.disco.requestInfo(jid.JID(self.memory.getParamV("Server", "Connection"))).addCallback(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 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 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 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")