libervia-backend/src/sat.tac

882 lines
37 KiB
Python

#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
SAT: a jabber client
Copyright (C) 2009, 2010, 2011 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/>.
"""
CONST = {
'client_name' : u'SàT (Salut à toi)',
'client_version' : u'0.1.1D', #Please add 'D' at the end for dev versions
'local_dir' : '~/.sat'
}
import gettext
gettext.install('sat', "i18n", unicode=True)
from twisted.application import internet, service
from twisted.internet import glib2reactor, protocol, task, defer
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 sat.tools.memory import Memory
from sat.tools.xml_tools import tupleList2dataForm
from sat.tools.misc import TriggerManager
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, 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
self.client_initialized = defer.Deferred()
def _authd(self, xmlstream):
if not self.host_app.trigger.point("XML Initialized", xmlstream, self.profile):
return
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)
if not self.host_app.trigger.point("Disco Handled", self.profile):
return
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, self.profile) #FIXME: use these informations
self.disco.requestItems(jid.JID(self.host_app.memory.getParamA("Server", "Connection", profile_key=self.profile))).addCallback(self.host_app.serverDiscoItems, self.disco, self.profile, self.client_initialized)
def initializationFailed(self, reason):
print ("initializationFailed: %s" % reason)
self.host_app.bridge.connectionError("AUTH_ERROR", self.profile)
try:
client.XMPPClient.initializationFailed(self, reason)
except:
#we already send an error signal, no need to raise an exception
pass
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"])
if not self.host.trigger.point("MessageReceived",message, profile=self.parent.profile):
return
for e in message.elements():
if e.name == "body":
mess_type = message['type'] if message.hasAttribute('type') else 'normal'
self.host.bridge.newMessage(message["from"], e.children[0], mess_type, message['to'], 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
self._groups=set()
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
#XXX: disabled (cf http://wokkel.ik.nu/ticket/56))
#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)
self._groups.update(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, self.parent.profile)
def getGroups(self):
"""Return a set of groups"""
return self._groups
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):
debug (_("presence update for [%(entity)s] (available, show=%(show)s statuses=%(statuses)s priority=%(priority)d)") % {'entity':entity, 'show':show, 'statuses':statuses, 'priority':priority})
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):
debug (_("presence update for [%(entity)s] (unavailable, statuses=%(statuses)s)") % {'entity':entity, 'statuses':statuses})
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):
if iq.handled == True:
return
debug (u"iqFallback: xml = [%s]" % (iq.toXml()))
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 SatVersionHandler(generic.VersionHandler):
def getDiscoInfo(self, requestor, target, node):
#XXX: We need to work around wokkel's behavious (namespace not added if there is a
# node) as it cause issues with XEP-0115 & PEP (XEP-0163): there is a node when server
# ask for disco info, and not when we generate the key, so the hash is used with different
# disco features, and when the server (seen on ejabberd) generate its own hash for security check
# it reject our features (resulting in e.g. no notification on PEP)
return generic.VersionHandler.getDiscoInfo(self, requestor, target, None)
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.trigger = TriggerManager() #trigger are user to change SàT behaviour
self.profiles = {}
self.plugins = {}
self.menus = {} #used to know which new menus are wanted by plugins
self.memory=Memory(self)
self.bridge=DBusBridge()
self.bridge.register("getVersion", lambda: self.get_const('client_version'))
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("getParamsUI", self.memory.getParamsUI)
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.bridge.register("getMenus", self.getMenus)
self.bridge.register("getMenuHelp", self.getMenuHelp)
self.bridge.register("callMenu", self.callMenu)
self._import_plugins()
def _import_plugins(self):
"""Import all plugins found in plugins directory"""
import sat.plugins
plugins_path = os.path.dirname(sat.plugins.__file__)
plug_lst = [os.path.splitext(plugin)[0] for plugin in map(os.path.basename,glob (os.path.join(plugins_path,"plugin*.py")))]
__plugins_to_import = {} #plugins will still have to import
for plug in plug_lst:
plugin_path = 'sat.plugins.'+plug
__import__(plugin_path)
mod = sys.modules[plugin_path]
plugin_info = mod.PLUGIN_INFO
__plugins_to_import[plugin_info['import_name']] = (plugin_path, mod, plugin_info)
while True:
self._import_plugins_from_dict(__plugins_to_import)
if not __plugins_to_import:
break
def _import_plugins_from_dict(self, plugins_to_import, import_name=None):
"""Recursively import and their dependencies in the right order
@param plugins_to_import: dict where key=import_name and values= (plugin_path, module, plugin_info)"""
if self.plugins.has_key(import_name):
debug('Plugin [%s] already imported, passing' % import_name)
return
if not import_name:
import_name,(plugin_path, mod, plugin_info) = plugins_to_import.popitem()
else:
if not import_name in plugins_to_import:
raise ImportError(_('Dependency plugin not found: [%s]') % import_name)
plugin_path, mod, plugin_info = plugins_to_import.pop(import_name)
dependencies = plugin_info.setdefault("dependencies",[])
for dependency in dependencies:
if not self.plugins.has_key(dependency):
debug('Recursively import dependency of [%s]: [%s]' % (import_name, dependency))
self._import_plugins_from_dict(plugins_to_import, dependency)
info (_("importing plugin: %s"), plugin_info['name'])
self.plugins[import_name] = getattr(mod, plugin_info['main'])(self)
if plugin_info.has_key('handler') and plugin_info['handler'] == 'yes':
self.plugins[import_name].is_handler = True
else:
self.plugins[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:
error (_('Trying to connect a non-exsitant profile'))
return
if (self.isConnected(profile)):
info(_("already connected !"))
return
current = self.profiles[profile] = SatXMPPClient(self, profile,
jid.JID(self.memory.getParamA("JabberID", "Connection", profile_key = profile), profile),
self.memory.getParamA("Password", "Connection", profile_key = profile),
self.memory.getParamA("Server", "Connection", profile_key = profile), 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 = SatVersionHandler(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(profile).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 !")
#TODO: manage autoconnect
#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, profile):
user = jid.parse(self.memory.getParamA("JabberID", "Connection", profile_key=profile))[0]
password = self.memory.getParamA("Password", "Connection", profile_key=profile)
server = self.memory.getParamA("Server", "Connection", profile_key=profile)
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,profile)
self.askConfirmation(confirm_id, "YES/NO",
{"message":_("Are you sure to register new account [%(user)s] to server %(server)s ?") % {'user':user, 'server':server, 'profile':profile}},
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,profile = self.__private_data[id]
del self.__private_data[id]
if accepted:
user = jid.parse(self.memory.getParamA("JabberID", "Connection", profile_key=profile))[0]
password = self.memory.getParamA("Password", "Connection", profile_key=profile)
server = self.memory.getParamA("Server", "Connection", profile_key=profile)
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 = 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'))
return
if not self.profiles.has_key(profile):
return False
return self.profiles[profile].isConnected()
def launchAction(self, type, data, profile_key='@DEFAULT@'):
"""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
"""
profile = self.memory.getProfileName(profile_key)
if not profile:
error (_('trying to launch action with a non-existant profile'))
raise Exception #TODO: raise a proper exception
if type=="button":
try:
cb_name = data['callback_id']
except KeyError:
error (_("Incomplete data"))
return ""
id = sat_next_id()
self.callGeneralCB(cb_name, id, data, profile = profile)
return id
else:
error (_("Unknown action type"))
return ""
## jabber methods ##
def sendMessage(self, to, msg, subject=None, type='chat', profile_key='@DEFAULT@'):
#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
if subject:
message.addElement("subject", "jabber:client", subject)
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))
if type!="groupchat":
self.bridge.newMessage(message['from'], unicode(msg), mess_type=type, to_jid=message['to'], 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)
#XXX: FIXME: temporary fix to work around openfire 3.7.0 bug (presence is not broadcasted to generating resource)
if statuses.has_key(''):
statuses['default'] = statuses['']
del statuses['']
self.bridge.presenceUpdate(self.profiles[profile].jid.full(), show,
int(priority), statuses, profile)
def subscription(self, subs_type, raw_jid, profile_key='@DEFAULT@'):
"""Called to manage subscription
@param subs_type: subsciption type (cf RFC 3921)
@param raw_jid: unicode entity's jid
@param profile_key: profile"""
profile = self.memory.getProfileName(profile_key)
assert(profile)
to_jid = jid.JID(raw_jid)
debug (_('subsciption request [%(subs_type)s] for %(jid)s') % {'subs_type':subs_type, 'jid':to_jid.full()})
if subs_type=="subscribe":
self.profiles[profile].presence.subscribe(to_jid)
elif subs_type=="subscribed":
self.profiles[profile].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 subs_type=="unsubscribe":
self.profiles[profile].presence.unsubscribe(to_jid)
elif subs_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) XXX: disabled (cf http://wokkel.ik.nu/ticket/56))
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.bridge.contactDeleted(to, profile)
## callbacks ##
def serverDisco(self, disco, profile):
"""xep-0030 Discovery Protocol."""
for feature in disco.features:
debug (_("Feature found: %s"),feature)
self.memory.addServerFeature(feature, profile)
for cat, type in disco.identities:
debug (_("Identity found: [%(category)s/%(type)s] %(identity)s") % {'category':cat, 'type':type, 'identity':disco.identities[(cat,type)]})
def serverDiscoItems(self, disco_result, disco_client, profile, initialized):
"""xep-0030 Discovery Protocol.
@param disco_result: result of the disco item querry
@param disco_client: SatDiscoProtocol instance
@param profile: profile of the user
@param initialized: deferred which must be chained when everything is done"""
def _check_entity_cb(result, entity, profile):
for category, type in result.identities:
debug (_('Identity added: (%(category)s,%(type)s) ==> %(entity)s [%(profile)s]') % {
'category':category, 'type':type, 'entity':entity, 'profile':profile})
self.memory.addServerIdentity(category, type, entity, profile)
defer_list = []
for item in disco_result._items:
defer_list.append(disco_client.requestInfo(item.entity).addCallback(_check_entity_cb, item.entity, profile))
defer.DeferredList(defer_list).chainDeferred(initialized)
## 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", "XMLUI")
@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':_("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 (%s)") % name)
return None
#Menus management
def importMenu(self, category, name, callback, help_string = "", type = "NORMAL"):
"""register a new menu for frontends
@param category: category of the menu
@param name: menu item entry
@param callback: method to be called when menuitem is selected"""
if self.menus.has_key((category,name)):
error ("Want to register a menu which already existe")
return
self.menus[(category,name,type)] = {'callback':callback, 'help_string':help_string, 'type':type}
def getMenus(self):
"""Return all menus registered"""
return self.menus.keys()
def getMenuHelp(self, category, name, type="NORMAL"):
"""return the help string of the menu"""
try:
return self.menus[(category,name,type)]['help_string']
except KeyError:
error (_("Trying to access an unknown menu"))
return ""
def callMenu(self, category, name, type="NORMAL", profile_key='@DEFAULT@'):
"""return the id of the action"""
profile = self.memory.getProfileName(profile_key)
if not profile_key:
error (_('Non-exsitant profile'))
return ""
if self.menus.has_key((category,name,type)):
id = self.get_next_id()
self.menus[(category,name,type)]['callback'](id, profile)
return id
else:
error (_("Trying to access an unknown menu (%(category)s/%(name)s/%(type)s)")%{'category':category, 'name':name,'type':type})
return ""
application = service.Application('SàT')
service = SAT()
service.setServiceParent(application)