core, jp: component handling + client handling refactoring:

- SàT can now handle components
    - plugin have now a "modes" key in PLUGIN_INFO where they declare if they can be used with clients and or components. They default to be client only.
    - components are really similar to clients, but with some changes in behaviour:
        * component has "entry point", which is a special plugin with a componentStart method, which is called just after component is connected
        * trigger end with a different suffixes (e.g. profileConnected vs profileConnectedComponent), so a plugin which manage both clients and components can have different workflow
        * for clients, only triggers of plugins handling client mode are launched
        * for components, only triggers of plugins needed in dependencies are launched. They all must handle component mode.
        * component have a sendHistory attribute (False by default) which can be set to True to allow saving sent messages into history
        * for convenience, "client" is still used in method even if it can now be a component
        * a new "component" boolean attribute tells if we have a component or a client
        * components have to add themselve Message protocol
        * roster and presence protocols are not added for components
        * component default port is 5347 (which is Prosody's default port)
    - asyncCreateProfile has been renamed for profileCreate, both to follow new naming convention and to prepare the transition to fully asynchronous bridge
    - createProfile has a new "component" attribute. When used to create a component, it must be set to a component entry point
    - jp: added --component argument to profile/create
    - disconnect bridge method is now asynchronous, this way frontends can know when disconnection is finished
    - new PI_* constants for PLUGIN_INFO values (not used everywhere yet)
    - client/component connection workflow has been moved to their classes instead of being a host methods
    - host.messageSend is now client.sendMessage, and former client.sendMessage is now client.sendMessageData.
    - identities are now handled in client.identities list, so it can be updated dynamically by plugins (in the future, frontends should be able to update them too through bridge)
    - profileConnecting* profileConnected* profileDisconnected* and getHandler now all use client instead of profile
This commit is contained in:
Goffi 2017-02-12 17:55:43 +01:00
commit 97337202cb
55 changed files with 839 additions and 523 deletions

View file

@ -164,15 +164,6 @@ class Bridge(BridgeFrontend):
kwargs['error_handler'] = error_handler
return self.db_core_iface.addContact(entity_jid, profile_key, **kwargs)
def asyncCreateProfile(self, profile, password='', callback=None, errback=None):
if callback is None:
error_handler = None
else:
if errback is None:
errback = log.error
error_handler = lambda err:errback(dbus_to_bridge_exception(err))
return self.db_core_iface.asyncCreateProfile(profile, password, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)
def asyncDeleteProfile(self, profile, callback=None, errback=None):
if callback is None:
error_handler = None
@ -243,12 +234,7 @@ class Bridge(BridgeFrontend):
if errback is None:
errback = log.error
error_handler = lambda err:errback(dbus_to_bridge_exception(err))
kwargs={}
if callback is not None:
kwargs['timeout'] = const_TIMEOUT
kwargs['reply_handler'] = callback
kwargs['error_handler'] = error_handler
return self.db_core_iface.disconnect(profile_key, **kwargs)
return self.db_core_iface.disconnect(profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)
def getConfig(self, section, name, callback=None, errback=None):
if callback is None:
@ -560,6 +546,15 @@ class Bridge(BridgeFrontend):
kwargs['error_handler'] = error_handler
return self.db_core_iface.paramsRegisterApp(xml, security_limit, app, **kwargs)
def profileCreate(self, profile, password='', component='', callback=None, errback=None):
if callback is None:
error_handler = None
else:
if errback is None:
errback = log.error
error_handler = lambda err:errback(dbus_to_bridge_exception(err))
return self.db_core_iface.profileCreate(profile, password, component, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)
def profileIsSessionStarted(self, profile_key="@DEFAULT@", callback=None, errback=None):
if callback is None:
error_handler = None

View file

@ -125,6 +125,8 @@ class ProfileCreate(base.CommandBase):
self.parser.add_argument('-j', '--jid', type=str, help=_('the jid of the profile'))
self.parser.add_argument('-x', '--xmpp-password', type=str, help=_('the password of the XMPP account (use profile password if not specified)'),
metavar='PASSWORD')
self.parser.add_argument('-C', '--component', type=base.unicode_decoder, default='',
help=_(u'set to component import name (entry point) if this is a component'))
def _session_started(self, dummy):
if self.args.jid:
@ -142,7 +144,7 @@ class ProfileCreate(base.CommandBase):
if self.args.profile in self.host.bridge.getProfilesList():
log.error("Profile %s already exists." % self.args.profile)
self.host.quit(1)
self.host.bridge.asyncCreateProfile(self.args.profile, self.args.password, callback=self._profile_created, errback=None)
self.host.bridge.profileCreate(self.args.profile, self.args.password, self.args.component, callback=self._profile_created, errback=None)
class ProfileModify(base.CommandBase):

View file

@ -88,7 +88,7 @@ class ProfileManager(QuickProfileManager, urwid.WidgetWrap):
def newProfile(self, button, edit):
"""Create the profile"""
name = edit.get_edit_text()
self.host.bridge.asyncCreateProfile(name, callback=lambda: self.newProfileCreated(name), errback=self.profileCreationFailure)
self.host.bridge.createProfile(name, callback=lambda: self.newProfileCreated(name), errback=self.profileCreationFailure)
def newProfileCreated(self, profile):
# new profile will be selected, and a selected profile assume the session is started

View file

@ -134,7 +134,7 @@ class QuickProfileManager(object):
def _getErrorMessage(self, reason):
"""Return an error message corresponding to profile creation error
@param reason (str): reason as returned by asyncCreateProfile
@param reason (str): reason as returned by createProfile
@return (unicode): human readable error message
"""
if reason == "ConflictError":

View file

@ -236,20 +236,23 @@ doc_param_2=%(doc_profile)s
doc_return=dictionary with jids as keys and dictionary of asked key as values
if key doesn't exist for a jid, the resulting dictionary will not have it
[asyncCreateProfile]
[profileCreate]
async=
type=method
category=core
sig_in=ss
sig_in=sss
sig_out=
param_1_default=''
param_2_default=''
doc=Create a new profile
doc_param_0=%(doc_profile)s
doc_param_1=password: password of the profile
doc_param_2=component: set to component entry point if it is a component, else use empty string
doc_return=callback is called when profile actually exists in database and memory
errback is called with error constant as parameter:
- ConflictError: the profile name already exists
- CancelError: profile creation canceled
- NotFound: component entry point is not available
[asyncDeleteProfile]
async=
@ -308,6 +311,7 @@ doc=Tell if a profile session is loaded
doc_param_0=%(doc_profile_key)s
[disconnect]
async=
type=method
category=core
sig_in=s

View file

@ -203,12 +203,6 @@ class DbusObject(dbus.service.Object):
def addContact(self, entity_jid, profile_key="@DEFAULT@"):
return self._callback("addContact", unicode(entity_jid), unicode(profile_key))
@dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
in_signature='ss', out_signature='',
async_callbacks=('callback', 'errback'))
def asyncCreateProfile(self, profile, password='', callback=None, errback=None):
return self._callback("asyncCreateProfile", unicode(profile), unicode(password), callback=callback, errback=errback)
@dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
in_signature='s', out_signature='',
async_callbacks=('callback', 'errback'))
@ -253,9 +247,9 @@ class DbusObject(dbus.service.Object):
@dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
in_signature='s', out_signature='',
async_callbacks=None)
def disconnect(self, profile_key="@DEFAULT@"):
return self._callback("disconnect", unicode(profile_key))
async_callbacks=('callback', 'errback'))
def disconnect(self, profile_key="@DEFAULT@", callback=None, errback=None):
return self._callback("disconnect", unicode(profile_key), callback=callback, errback=errback)
@dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
in_signature='ss', out_signature='s',
@ -407,6 +401,12 @@ class DbusObject(dbus.service.Object):
def paramsRegisterApp(self, xml, security_limit=-1, app=''):
return self._callback("paramsRegisterApp", unicode(xml), security_limit, unicode(app))
@dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
in_signature='sss', out_signature='',
async_callbacks=('callback', 'errback'))
def profileCreate(self, profile, password='', component='', callback=None, errback=None):
return self._callback("profileCreate", unicode(profile), unicode(password), unicode(component), callback=callback, errback=errback)
@dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
in_signature='s', out_signature='b',
async_callbacks=None)

View file

@ -47,6 +47,8 @@ class Const(object):
XMPP_C2S_PORT = 5222
XMPP_KEEP_ALIFE = 180
XMPP_MAX_RETRIES = 2
# default port used on Prosody, may differ on other servers
XMPP_COMPONENT_PORT = 5347
## Parameters ##
@ -171,6 +173,13 @@ class Const(object):
## Plugins ##
# PLUGIN_INFO keys
# XXX: we use PI instead of PLUG_INFO which would normally be used
# to make the header more readable
PI_IMPORT_NAME = u'import_name'
PI_DEPENDENCIES = u'dependencies'
PI_RECOMMENDATIONS = u'recommendations'
# Types
PLUG_TYPE_XEP = "XEP"
PLUG_TYPE_MISC = "MISC"
@ -178,7 +187,12 @@ class Const(object):
PLUG_TYPE_SEC = "SEC"
PLUG_TYPE_SYNTAXE = "SYNTAXE"
PLUG_TYPE_BLOG = "BLOG"
PLUG_TYPE_ENTRY_POINT = "ENTRY_POINT"
# Modes
PLUG_MODE_CLIENT = "client"
PLUG_MODE_COMPONENT = "component"
PLUG_MODE_DEFAULT = (PLUG_MODE_CLIENT,)
# names of widely used plugins
TEXT_CMDS = 'TEXT-COMMANDS'

View file

@ -22,7 +22,6 @@ from sat.core.i18n import _, languageSwitch
from twisted.application import service
from twisted.internet import defer
from twisted.words.protocols.jabber import jid
from twisted.words.xish import domish
from twisted.internet import reactor
from wokkel.xmppim import RosterItem
from sat.core import xmpp
@ -41,7 +40,6 @@ from uuid import uuid4
import sys
import os.path
import uuid
import time
try:
from collections import OrderedDict # only available from python 2.7
@ -81,7 +79,7 @@ class SAT(service.Service):
self.bridge.register_method("getProfilesList", self.memory.getProfilesList)
self.bridge.register_method("getEntityData", lambda jid_, keys, profile: self.memory.getEntityData(jid.JID(jid_), keys, profile))
self.bridge.register_method("getEntitiesData", self.memory._getEntitiesData)
self.bridge.register_method("asyncCreateProfile", self.memory.asyncCreateProfile)
self.bridge.register_method("profileCreate", self.memory.createProfile)
self.bridge.register_method("asyncDeleteProfile", self.memory.asyncDeleteProfile)
self.bridge.register_method("profileStartSession", self.memory.startSession)
self.bridge.register_method("profileIsSessionStarted", self.memory._isSessionStarted)
@ -156,8 +154,20 @@ class SAT(service.Service):
self.initialised.callback(None)
log.info(_(u"Backend is ready"))
def _unimport_plugin(self, plugin_path):
"""remove a plugin from sys.modules if it is there"""
try:
del sys.modules[plugin_path]
except KeyError:
pass
def _import_plugins(self):
"""Import all plugins found in plugins directory"""
# FIXME: module imported but cancelled should be deleted
# TODO: make this more generic and reusable in tools.common
# FIXME: should use imp
# TODO: do not import all plugins if no needed: component plugins are not needed if we
# just use a client, and plugin blacklisting should be possible in sat.conf
plugins_path = os.path.dirname(sat.plugins.__file__)
plugin_glob = "plugin*." + C.PLUGIN_EXT
plug_lst = [os.path.splitext(plugin)[0] for plugin in map(os.path.basename, glob(os.path.join(plugins_path, plugin_glob)))]
@ -167,23 +177,34 @@ class SAT(service.Service):
try:
__import__(plugin_path)
except exceptions.MissingModule as e:
try:
del sys.modules[plugin_path]
except KeyError:
pass
self._unimport_plugin(plugin_path)
log.warning(u"Can't import plugin [{path}] because of an unavailale third party module:\n{msg}".format(
path=plugin_path, msg=e))
continue
except exceptions.CancelError as e:
log.info(u"Plugin [{path}] cancelled its own import: {msg}".format(path=plugin_path, msg=e))
self._unimport_plugin(plugin_path)
continue
except Exception as e:
import traceback
log.error(_(u"Can't import plugin [{path}]:\n{error}").format(path=plugin_path, error=traceback.format_exc()))
self._unimport_plugin(plugin_path)
continue
mod = sys.modules[plugin_path]
plugin_info = mod.PLUGIN_INFO
import_name = plugin_info['import_name']
plugin_modes = plugin_info[u'modes'] = set(plugin_info.setdefault(u"modes", C.PLUG_MODE_DEFAULT))
# if the plugin is an entry point, it must work in component mode
if plugin_info[u'type'] == C.PLUG_TYPE_ENTRY_POINT:
# if plugin is an entrypoint, we cache it
if C.PLUG_MODE_COMPONENT not in plugin_modes:
log.error(_(u"{type} type must be used with {mode} mode, ignoring plugin").format(
type = C.PLUG_TYPE_ENTRY_POINT, mode = C.PLUG_MODE_COMPONENT))
self._unimport_plugin(plugin_path)
continue
if import_name in plugins_to_import:
log.error(_(u"Name conflict for import name [{import_name}], can't import plugin [{name}]").format(**plugin_info))
continue
@ -243,6 +264,8 @@ class SAT(service.Service):
self.plugins[import_name].is_handler = True
else:
self.plugins[import_name].is_handler = False
# we keep metadata as a Class attribute
self.plugins[import_name]._info = plugin_info
#TODO: test xmppclient presence and register handler parent
def pluginsUnload(self):
@ -268,12 +291,14 @@ class SAT(service.Service):
return self.connect(profile, password, options)
def connect(self, profile, password='', options=None, max_retries=C.XMPP_MAX_RETRIES):
"""Retrieve the individual parameters, authenticate the profile
and initiate the connection to the associated XMPP server.
"""Connect a profile (i.e. connect client.component to XMPP server)
Retrieve the individual parameters, authenticate the profile
and initiate the connection to the associated XMPP server.
@param profile: %(doc_profile)s
@param password (string): the SàT profile password
@param options (dict): connection options
@param options (dict): connection options. Key can be:
-
@param max_retries (int): max number of connection retries
@return (D(bool)):
- True if the XMPP connection was already established
@ -282,100 +307,32 @@ class SAT(service.Service):
"""
if options is None:
options={}
def connectXMPPClient(dummy=None):
def connectProfile(dummy=None):
if self.isConnected(profile):
log.info(_("already connected !"))
return True
d = self._connectXMPPClient(profile, max_retries)
if self.memory.isComponent(profile):
d = xmpp.SatXMPPComponent.startConnection(self, profile, max_retries)
else:
d = xmpp.SatXMPPClient.startConnection(self, profile, max_retries)
return d.addCallback(lambda dummy: False)
d = self.memory.startSession(password, profile)
d.addCallback(connectXMPPClient)
d.addCallback(connectProfile)
return d
@defer.inlineCallbacks
def _connectXMPPClient(self, profile, max_retries):
"""This part is called from connect when we have loaded individual parameters from memory"""
try:
port = int(self.memory.getParamA(C.FORCE_PORT_PARAM, "Connection", profile_key=profile))
except ValueError:
log.debug(_("Can't parse port value, using default value"))
port = None # will use default value 5222 or be retrieved from a DNS SRV record
password = yield self.memory.asyncGetParamA("Password", "Connection", profile_key=profile)
current = self.profiles[profile] = xmpp.SatXMPPClient(self, profile,
jid.JID(self.memory.getParamA("JabberID", "Connection", profile_key=profile)),
password, self.memory.getParamA(C.FORCE_SERVER_PARAM, "Connection", profile_key=profile),
port, max_retries)
current.messageProt = xmpp.SatMessageProtocol(self)
current.messageProt.setHandlerParent(current)
current.roster = xmpp.SatRosterProtocol(self)
current.roster.setHandlerParent(current)
current.presence = xmpp.SatPresenceProtocol(self)
current.presence.setHandlerParent(current)
current.fallBack = xmpp.SatFallbackHandler(self)
current.fallBack.setHandlerParent(current)
current.versionHandler = xmpp.SatVersionHandler(C.APP_NAME_FULL,
self.full_version)
current.versionHandler.setHandlerParent(current)
current.identityHandler = xmpp.SatIdentityHandler()
current.identityHandler.setHandlerParent(current)
log.debug(_("setting plugins parents"))
plugin_conn_cb = []
for plugin in self.plugins.iteritems():
if plugin[1].is_handler:
plugin[1].getHandler(profile).setHandlerParent(current)
connected_cb = getattr(plugin[1], "profileConnected", None) # profile connected is called after client is ready and roster is got
if connected_cb:
plugin_conn_cb.append((plugin[0], connected_cb))
try:
yield plugin[1].profileConnecting(profile) # profile connecting is called before actually starting client
except AttributeError:
pass
current.startService()
yield current.getConnectionDeferred()
yield current.roster.got_roster # we want to be sure that we got the roster
# Call profileConnected callback for all plugins, and print error message if any of them fails
conn_cb_list = []
for dummy, callback in plugin_conn_cb:
conn_cb_list.append(defer.maybeDeferred(callback, profile))
list_d = defer.DeferredList(conn_cb_list)
def logPluginResults(results):
all_succeed = all([success for success, result in results])
if not all_succeed:
log.error(_(u"Plugins initialisation error"))
for idx, (success, result) in enumerate(results):
if not success:
log.error(u"error (plugin %(name)s): %(failure)s" %
{'name': plugin_conn_cb[idx][0], 'failure': result})
yield list_d.addCallback(logPluginResults) # FIXME: we should have a timeout here, and a way to know if a plugin freeze
# TODO: mesure launch time of each plugin
def disconnect(self, profile_key):
"""disconnect from jabber server"""
# FIXME: client should not be deleted if only disconnected
# it shoud be deleted only when session is finished
if not self.isConnected(profile_key):
log.info(_("not connected !"))
return
profile = self.memory.getProfileName(profile_key)
log.info(_("Disconnecting..."))
self.profiles[profile].stopService()
for plugin in self.plugins.iteritems():
disconnected_cb = getattr(plugin[1], "profileDisconnected", None)
if disconnected_cb:
disconnected_cb(profile)
# isConnected is checked here and not on client
# because client is deleted when session is ended
log.info(_(u"not connected !"))
return defer.succeed(None)
client = self.getClient(profile_key)
return client.entityDisconnect()
def getFeatures(self, profile_key=C.PROF_KEY_NONE):
"""Get available features
@ -439,14 +396,17 @@ class SAT(service.Service):
client = self.getClient(profile_key)
return [jid_.full() for jid_ in client.roster.getJidsFromGroup(group)]
def purgeClient(self, profile):
"""Remove reference to a profile client and purge cache
the garbage collector can then free the memory"""
def purgeEntity(self, profile):
"""Remove reference to a profile client/component and purge cache
the garbage collector can then free the memory
"""
try:
del self.profiles[profile]
except KeyError:
log.error(_("Trying to remove reference to a client not referenced"))
self.memory.purgeProfileSession(profile)
else:
self.memory.purgeProfileSession(profile)
def startService(self):
log.info(u"Salut à toi ô mon frère !")
@ -515,6 +475,14 @@ class SAT(service.Service):
"""
return unicode(self.memory.getConfig(section, name, ''))
def logErrback(self, failure_):
"""generic errback logging
can be used as last errback to show unexpected error
"""
log.error(_(u"Unexpected error: {}".format(failure_)))
return failure_
## Client management ##
def setParam(self, name, value, category, security_limit, profile_key):
@ -537,168 +505,11 @@ class SAT(service.Service):
## XMPP methods ##
def generateMessageXML(self, data):
"""Generate <message/> stanza from message data
@param data(dict): message data
domish element will be put in data['xml']
following keys are needed:
- from
- to
- uid: can be set to '' if uid attribute is not wanted
- message
- type
- subject
- extra
@return (dict) message data
"""
data['xml'] = message_elt = domish.Element((None, 'message'))
message_elt["to"] = data["to"].full()
message_elt["from"] = data['from'].full()
message_elt["type"] = data["type"]
if data['uid']: # key must be present but can be set to ''
# by a plugin to avoid id on purpose
message_elt['id'] = data['uid']
for lang, subject in data["subject"].iteritems():
subject_elt = message_elt.addElement("subject", content=subject)
if lang:
subject_elt[(C.NS_XML, 'lang')] = lang
for lang, message in data["message"].iteritems():
body_elt = message_elt.addElement("body", content=message)
if lang:
body_elt[(C.NS_XML, 'lang')] = lang
try:
thread = data['extra']['thread']
except KeyError:
if 'thread_parent' in data['extra']:
raise exceptions.InternalError(u"thread_parent found while there is not associated thread")
else:
thread_elt = message_elt.addElement("thread", content=thread)
try:
thread_elt["parent"] = data["extra"]["thread_parent"]
except KeyError:
pass
return data
def _messageSend(self, to_jid_s, message, subject=None, mess_type='auto', extra=None, profile_key=C.PROF_KEY_NONE):
client = self.getClient(profile_key)
to_jid = jid.JID(to_jid_s)
#XXX: we need to use the dictionary comprehension because D-Bus return its own types, and pickle can't manage them. TODO: Need to find a better way
return self.messageSend(client, to_jid, message, subject, mess_type, {unicode(key): unicode(value) for key, value in extra.items()})
def messageSend(self, client, to_jid, message, subject=None, mess_type='auto', extra=None, uid=None, no_trigger=False):
"""Send a message to an entity
@param to_jid(jid.JID): destinee of the message
@param message(dict): message body, key is the language (use '' when unknown)
@param subject(dict): message subject, key is the language (use '' when unknown)
@param mess_type(str): one of standard message type (cf RFC 6121 §5.2.2) or:
- auto: for automatic type detection
- info: for information ("info_type" can be specified in extra)
@param extra(dict, None): extra data. Key can be:
- info_type: information type, can be
TODO
@param uid(unicode, None): unique id:
should be unique at least in this XMPP session
if None, an uuid will be generated
@param no_trigger (bool): if True, messageSend trigger will no be used
useful when a message need to be sent without any modification
"""
profile = client.profile
if subject is None:
subject = {}
if extra is None:
extra = {}
data = { # dict is similar to the one used in client.onMessage
"from": client.jid,
"to": to_jid,
"uid": uid or unicode(uuid.uuid4()),
"message": message,
"subject": subject,
"type": mess_type,
"extra": extra,
"timestamp": time.time(),
}
pre_xml_treatments = defer.Deferred() # XXX: plugin can add their pre XML treatments to this deferred
post_xml_treatments = defer.Deferred() # XXX: plugin can add their post XML treatments to this deferred
if data["type"] == "auto":
# we try to guess the type
if data["subject"]:
data["type"] = 'normal'
elif not data["to"].resource: # if to JID has a resource, the type is not 'groupchat'
# we may have a groupchat message, we check if the we know this jid
try:
entity_type = self.memory.getEntityData(data["to"], ['type'], profile)["type"]
#FIXME: should entity_type manage resources ?
except (exceptions.UnknownEntityError, KeyError):
entity_type = "contact"
if entity_type == "chatroom":
data["type"] = 'groupchat'
else:
data["type"] = 'chat'
else:
data["type"] == 'chat'
data["type"] == "chat" if data["subject"] else "normal"
# FIXME: send_only is used by libervia's OTR plugin to avoid
# the triggers from frontend, and no_trigger do the same
# thing internally, this could be unified
send_only = data['extra'].get('send_only', False)
if not no_trigger and not send_only:
if not self.trigger.point("messageSend", client, data, pre_xml_treatments, post_xml_treatments):
return defer.succeed(None)
log.debug(_(u"Sending message (type {type}, to {to})").format(type=data["type"], to=to_jid.full()))
pre_xml_treatments.addCallback(lambda dummy: self.generateMessageXML(data))
pre_xml_treatments.chainDeferred(post_xml_treatments)
post_xml_treatments.addCallback(client.sendMessage)
if send_only:
log.debug(_("Triggers, storage and echo have been inhibited by the 'send_only' parameter"))
else:
post_xml_treatments.addCallback(self.messageAddToHistory, client)
post_xml_treatments.addCallback(self.messageSendToBridge, client)
post_xml_treatments.addErrback(self._cancelErrorTrap)
pre_xml_treatments.callback(data)
return pre_xml_treatments
def _cancelErrorTrap(self, failure):
"""A message sending can be cancelled by a plugin treatment"""
failure.trap(exceptions.CancelError)
def messageAddToHistory(self, data, client):
"""Store message into database (for local history)
@param data: message data dictionnary
@param client: profile's client
"""
if data[u"type"] != C.MESS_TYPE_GROUPCHAT:
# we don't add groupchat message to history, as we get them back
# and they will be added then
if data[u'message'] or data[u'subject']: # we need a message to store
self.memory.addToHistory(client, data)
else:
log.warning(u"No message found") # empty body should be managed by plugins before this point
return data
def messageSendToBridge(self, data, client):
"""Send message to bridge, so frontends can display it
@param data: message data dictionnary
@param client: profile's client
"""
if data[u"type"] != C.MESS_TYPE_GROUPCHAT:
# we don't send groupchat message to bridge, as we get them back
# and they will be added the
if data[u'message'] or data[u'subject']: # we need a message to send something
# We send back the message, so all frontends are aware of it
self.bridge.messageNew(data[u'uid'], data[u'timestamp'], data[u'from'].full(), data[u'to'].full(), data[u'message'], data[u'subject'], data[u'type'], data[u'extra'], profile=client.profile)
else:
log.warning(_(u"No message found"))
return data
return client.sendMessage(to_jid, message, subject, mess_type, {unicode(key): unicode(value) for key, value in extra.items()})
def _setPresence(self, to="", show="", statuses=None, profile_key=C.PROF_KEY_NONE):
return self.setPresence(jid.JID(to) if to else None, show, statuses, profile_key)

View file

@ -28,6 +28,7 @@ from twisted.words.protocols.jabber import jid
from twisted.words.xish import domish
from twisted.python import failure
from wokkel import client as wokkel_client, disco, xmppim, generic, iwokkel
from wokkel import component
from wokkel import delay
from sat.core.log import getLogger
log = getLogger(__name__)
@ -39,40 +40,229 @@ import uuid
import sys
class SatXMPPClient(wokkel_client.XMPPClient):
implements(iwokkel.IDisco)
class SatXMPPEntity(object):
"""Common code for Client and Component"""
def __init__(self, host_app, profile, max_retries):
def __init__(self, host_app, profile, user_jid, password, host=None, port=C.XMPP_C2S_PORT, max_retries=C.XMPP_MAX_RETRIES):
# XXX: DNS SRV records are checked when the host is not specified.
# If no SRV record is found, the host is directly extracted from the JID.
self.started = time.time()
if sys.platform == "android":
# FIXME: temporary hack as SRV is not working on android
# TODO: remove this hack and fix SRV
log.info(u"FIXME: Android hack, ignoring SRV")
host = user_jid.host
hosts_map = host_app.memory.getConfig("android", "hosts_dict", {})
if host in hosts_map:
log.info(u"using {host_to_use} for host {host_ori} as requested in config".format(
host_ori = host,
host_to_use = hosts_map[host]))
host = hosts_map[host]
wokkel_client.XMPPClient.__init__(self, user_jid, password, host or None, port or C.XMPP_C2S_PORT)
self.factory.clientConnectionLost = self.connectionLost
self.factory.maxRetries = max_retries
self.__connected = False
# when self._connected is None, we are not connected
# else, it's a deferred which fire on disconnection
self._connected = None
self.profile = profile
self.host_app = host_app
self.cache = cache.Cache(host_app, profile)
self._mess_id_uid = {} # map from message id to uid use in history. Key: (full_jid,message_id) Value: uid
self._mess_id_uid = {} # map from message id to uid used in history. Key: (full_jid,message_id) Value: uid
self.conn_deferred = defer.Deferred()
self._progress_cb = {} # callback called when a progress is requested (key = progress id)
self.actions = {} # used to keep track of actions for retrieval (key = action_id)
## initialisation ##
@defer.inlineCallbacks
def _callConnectionTriggers(self):
"""Call conneting trigger prepare connected trigger
@param plugins(iterable): plugins to use
@return (list[object, callable]): plugin to trigger tuples with:
- plugin instance
- profileConnected* triggers (to call after connection)
"""
plugin_conn_cb = []
for plugin in self._getPluginsList():
# we check if plugin handle client mode
if plugin.is_handler:
plugin.getHandler(self).setHandlerParent(self)
# profileConnecting/profileConnected methods handling
# profile connecting is called right now (before actually starting client)
connecting_cb = getattr(plugin, "profileConnecting" + self.trigger_suffix, None)
if connecting_cb is not None:
yield connecting_cb(self)
# profile connected is called after client is ready and roster is got
connected_cb = getattr(plugin, "profileConnected" + self.trigger_suffix, None)
if connected_cb is not None:
plugin_conn_cb.append((plugin, connected_cb))
defer.returnValue(plugin_conn_cb)
def _getPluginsList(self):
"""Return list of plugin to use
need to be implemented by subclasses
this list is used to call profileConnect* triggers
@return(iterable[object]): plugins to use
"""
raise NotImplementedError
def _createSubProtocols(self):
return
def entityConnected(self):
"""Called once connection is done
may return a Deferred, to perform initialisation tasks
"""
return
@classmethod
@defer.inlineCallbacks
def startConnection(cls, host, profile, max_retries):
"""instantiate the entity and start the connection"""
# FIXME: reconnection doesn't seems to be handled correclty (client is deleted then recreated from scrash
# most of methods called here should be called once on first connection (e.g. adding subprotocols)
# but client should not be deleted except if session is finished (independently of connection/deconnection
#
try:
port = int(host.memory.getParamA(C.FORCE_PORT_PARAM, "Connection", profile_key=profile))
except ValueError:
log.debug(_("Can't parse port value, using default value"))
port = None # will use default value 5222 or be retrieved from a DNS SRV record
password = yield host.memory.asyncGetParamA("Password", "Connection", profile_key=profile)
entity = host.profiles[profile] = cls(host, profile,
jid.JID(host.memory.getParamA("JabberID", "Connection", profile_key=profile)),
password, host.memory.getParamA(C.FORCE_SERVER_PARAM, "Connection", profile_key=profile) or None,
port, max_retries)
entity._createSubProtocols()
entity.fallBack = SatFallbackHandler(host)
entity.fallBack.setHandlerParent(entity)
entity.versionHandler = SatVersionHandler(C.APP_NAME_FULL,
host.full_version)
entity.versionHandler.setHandlerParent(entity)
entity.identityHandler = SatIdentityHandler()
entity.identityHandler.setHandlerParent(entity)
log.debug(_("setting plugins parents"))
plugin_conn_cb = yield entity._callConnectionTriggers()
entity.startService()
yield entity.getConnectionDeferred()
yield defer.maybeDeferred(entity.entityConnected)
# Call profileConnected callback for all plugins, and print error message if any of them fails
conn_cb_list = []
for dummy, callback in plugin_conn_cb:
conn_cb_list.append(defer.maybeDeferred(callback, entity))
list_d = defer.DeferredList(conn_cb_list)
def logPluginResults(results):
all_succeed = all([success for success, result in results])
if not all_succeed:
log.error(_(u"Plugins initialisation error"))
for idx, (success, result) in enumerate(results):
if not success:
log.error(u"error (plugin %(name)s): %(failure)s" %
{'name': plugin_conn_cb[idx][0]._info['import_name'], 'failure': result})
yield list_d.addCallback(logPluginResults) # FIXME: we should have a timeout here, and a way to know if a plugin freeze
# TODO: mesure launch time of each plugin
def getConnectionDeferred(self):
"""Return a deferred which fire when the client is connected"""
return self.conn_deferred
def _disconnectionCb(self, dummy):
self._connected = None
def _disconnectionEb(self, failure_):
log.error(_(u"Error while disconnecting: {}".format(failure_)))
def _authd(self, xmlstream):
if not self.host_app.trigger.point("XML Initialized", xmlstream, self.profile):
return
super(SatXMPPEntity, self)._authd(xmlstream)
# the following Deferred is used to know when we are connected
# so we need to be set it to None when connection is lost
self._connected = defer.Deferred()
self._connected.addCallback(self._cleanConnection)
self._connected.addCallback(self._disconnectionCb)
self._connected.addErrback(self._disconnectionEb)
log.info(_("********** [%s] CONNECTED **********") % self.profile)
self.streamInitialized()
self.host_app.bridge.connected(self.profile, unicode(self.jid)) # we send the signal to the clients
def _finish_connection(self, dummy):
self.conn_deferred.callback(None)
def streamInitialized(self):
"""Called after _authd"""
log.debug(_(u"XML stream is initialized"))
self.keep_alife = task.LoopingCall(self.xmlstream.send, " ") # Needed to avoid disconnection (specially with openfire)
self.keep_alife.start(C.XMPP_KEEP_ALIFE)
self.disco = SatDiscoProtocol(self)
self.disco.setHandlerParent(self)
self.discoHandler = disco.DiscoHandler()
self.discoHandler.setHandlerParent(self)
disco_d = defer.succeed(None)
if not self.host_app.trigger.point("Disco handled", disco_d, self.profile):
return
disco_d.addCallback(self._finish_connection)
def initializationFailed(self, reason):
log.error(_(u"ERROR: XMPP connection failed for profile '%(profile)s': %(reason)s" % {'profile': self.profile, 'reason': reason}))
self.conn_deferred.errback(reason.value)
try:
super(SatXMPPEntity, self).initializationFailed(reason)
except:
# we already chained an errback, no need to raise an exception
pass
## connection ##
def connectionLost(self, connector, reason):
try:
self.keep_alife.stop()
except AttributeError:
log.debug(_("No keep_alife"))
if self._connected is not None:
self.host_app.bridge.disconnected(self.profile) # we send the signal to the clients
self._connected.callback(None)
self.host_app.purgeEntity(self.profile) # and we remove references to this client
log.info(_("********** [%s] DISCONNECTED **********") % self.profile)
if not self.conn_deferred.called:
# FIXME: real error is not gotten here (e.g. if jid is not know by Prosody,
# we should have the real error)
self.conn_deferred.errback(error.StreamError(u"Server unexpectedly closed the connection"))
@defer.inlineCallbacks
def _cleanConnection(self, dummy):
"""method called on disconnection
used to call profileDisconnected* triggers
"""
trigger_name = "profileDisconnected" + self.trigger_suffix
for plugin in self._getPluginsList():
disconnected_cb = getattr(plugin, trigger_name, None)
if disconnected_cb is not None:
yield disconnected_cb(self)
def isConnected(self):
return self._connected is not None
def entityDisconnect(self):
log.info(_(u"Disconnecting..."))
self.stopService()
if self._connected is not None:
return self._connected
else:
return defer.succeed(None)
## sending ##
def IQ(self, type_=u'set', timeout=None):
"""shortcut to create an IQ element managing deferred
@ -94,6 +284,223 @@ class SatXMPPClient(wokkel_client.XMPPClient):
iq_error_elt = error.StanzaError(condition).toResponse(iq_elt)
self.xmlstream.send(iq_error_elt)
def generateMessageXML(self, data):
"""Generate <message/> stanza from message data
@param data(dict): message data
domish element will be put in data['xml']
following keys are needed:
- from
- to
- uid: can be set to '' if uid attribute is not wanted
- message
- type
- subject
- extra
@return (dict) message data
"""
data['xml'] = message_elt = domish.Element((None, 'message'))
message_elt["to"] = data["to"].full()
message_elt["from"] = data['from'].full()
message_elt["type"] = data["type"]
if data['uid']: # key must be present but can be set to ''
# by a plugin to avoid id on purpose
message_elt['id'] = data['uid']
for lang, subject in data["subject"].iteritems():
subject_elt = message_elt.addElement("subject", content=subject)
if lang:
subject_elt[(C.NS_XML, 'lang')] = lang
for lang, message in data["message"].iteritems():
body_elt = message_elt.addElement("body", content=message)
if lang:
body_elt[(C.NS_XML, 'lang')] = lang
try:
thread = data['extra']['thread']
except KeyError:
if 'thread_parent' in data['extra']:
raise exceptions.InternalError(u"thread_parent found while there is not associated thread")
else:
thread_elt = message_elt.addElement("thread", content=thread)
try:
thread_elt["parent"] = data["extra"]["thread_parent"]
except KeyError:
pass
return data
def addPostXmlCallbacks(self, post_xml_treatments):
"""Used to add class level callbacks at the end of the workflow
@param post_xml_treatments(D): the same Deferred as in sendMessage trigger
"""
raise NotImplementedError
def sendMessage(self, to_jid, message, subject=None, mess_type='auto', extra=None, uid=None, no_trigger=False):
"""Send a message to an entity
@param to_jid(jid.JID): destinee of the message
@param message(dict): message body, key is the language (use '' when unknown)
@param subject(dict): message subject, key is the language (use '' when unknown)
@param mess_type(str): one of standard message type (cf RFC 6121 §5.2.2) or:
- auto: for automatic type detection
- info: for information ("info_type" can be specified in extra)
@param extra(dict, None): extra data. Key can be:
- info_type: information type, can be
TODO
@param uid(unicode, None): unique id:
should be unique at least in this XMPP session
if None, an uuid will be generated
@param no_trigger (bool): if True, sendMessage[suffix] trigger will no be used
useful when a message need to be sent without any modification
"""
if subject is None:
subject = {}
if extra is None:
extra = {}
data = { # dict is similar to the one used in client.onMessage
"from": self.jid,
"to": to_jid,
"uid": uid or unicode(uuid.uuid4()),
"message": message,
"subject": subject,
"type": mess_type,
"extra": extra,
"timestamp": time.time(),
}
pre_xml_treatments = defer.Deferred() # XXX: plugin can add their pre XML treatments to this deferred
post_xml_treatments = defer.Deferred() # XXX: plugin can add their post XML treatments to this deferred
if data["type"] == "auto":
# we try to guess the type
if data["subject"]:
data["type"] = 'normal'
elif not data["to"].resource: # if to JID has a resource, the type is not 'groupchat'
# we may have a groupchat message, we check if the we know this jid
try:
entity_type = self.host_app.memory.getEntityData(data["to"], ['type'], self.profile)["type"]
#FIXME: should entity_type manage resources ?
except (exceptions.UnknownEntityError, KeyError):
entity_type = "contact"
if entity_type == "chatroom":
data["type"] = 'groupchat'
else:
data["type"] = 'chat'
else:
data["type"] == 'chat'
data["type"] == "chat" if data["subject"] else "normal"
# FIXME: send_only is used by libervia's OTR plugin to avoid
# the triggers from frontend, and no_trigger do the same
# thing internally, this could be unified
send_only = data['extra'].get('send_only', False)
if not no_trigger and not send_only:
if not self.host_app.trigger.point("sendMessage" + self.trigger_suffix, self, data, pre_xml_treatments, post_xml_treatments):
return defer.succeed(None)
log.debug(_(u"Sending message (type {type}, to {to})").format(type=data["type"], to=to_jid.full()))
pre_xml_treatments.addCallback(lambda dummy: self.generateMessageXML(data))
pre_xml_treatments.chainDeferred(post_xml_treatments)
post_xml_treatments.addCallback(self.sendMessageData)
if send_only:
log.debug(_("Triggers, storage and echo have been inhibited by the 'send_only' parameter"))
else:
self.addPostXmlCallbacks(post_xml_treatments)
post_xml_treatments.addErrback(self._cancelErrorTrap)
post_xml_treatments.addErrback(self.host_app.logErrback)
pre_xml_treatments.callback(data)
return pre_xml_treatments
def _cancelErrorTrap(self, failure):
"""A message sending can be cancelled by a plugin treatment"""
failure.trap(exceptions.CancelError)
def messageAddToHistory(self, data):
"""Store message into database (for local history)
@param data: message data dictionnary
@param client: profile's client
"""
if data[u"type"] != C.MESS_TYPE_GROUPCHAT:
# we don't add groupchat message to history, as we get them back
# and they will be added then
if data[u'message'] or data[u'subject']: # we need a message to store
self.host_app.memory.addToHistory(self, data)
else:
log.warning(u"No message found") # empty body should be managed by plugins before this point
return data
def messageSendToBridge(self, data):
"""Send message to bridge, so frontends can display it
@param data: message data dictionnary
@param client: profile's client
"""
if data[u"type"] != C.MESS_TYPE_GROUPCHAT:
# we don't send groupchat message to bridge, as we get them back
# and they will be added the
if data[u'message'] or data[u'subject']: # we need a message to send something
# We send back the message, so all frontends are aware of it
self.host_app.bridge.messageNew(data[u'uid'], data[u'timestamp'], data[u'from'].full(), data[u'to'].full(), data[u'message'], data[u'subject'], data[u'type'], data[u'extra'], profile=self.profile)
else:
log.warning(_(u"No message found"))
return data
class SatXMPPClient(SatXMPPEntity, wokkel_client.XMPPClient):
implements(iwokkel.IDisco)
trigger_suffix = ""
component = False
def __init__(self, host_app, profile, user_jid, password, host=None, port=C.XMPP_C2S_PORT, max_retries=C.XMPP_MAX_RETRIES):
# XXX: DNS SRV records are checked when the host is not specified.
# If no SRV record is found, the host is directly extracted from the JID.
self.started = time.time()
# Currently, we use "client/pc/Salut à Toi", but as
# SàT is multi-frontends and can be used on mobile devices, as a bot, with a web frontend,
# etc., we should implement a way to dynamically update identities through the bridge
self.identities = [disco.DiscoIdentity(u"client", u"pc", C.APP_NAME)]
if sys.platform == "android":
# FIXME: temporary hack as SRV is not working on android
# TODO: remove this hack and fix SRV
log.info(u"FIXME: Android hack, ignoring SRV")
host = user_jid.host
hosts_map = host_app.memory.getConfig("android", "hosts_dict", {})
if host in hosts_map:
log.info(u"using {host_to_use} for host {host_ori} as requested in config".format(
host_ori = host,
host_to_use = hosts_map[host]))
host = hosts_map[host]
wokkel_client.XMPPClient.__init__(self, user_jid, password, host or None, port or C.XMPP_C2S_PORT)
SatXMPPEntity.__init__(self, host_app, profile, max_retries)
self._progress_cb = {} # callback called when a progress is requested (key = progress id)
self.actions = {} # used to keep track of actions for retrieval (key = action_id)
def _getPluginsList(self):
for p in self.host_app.plugins.itervalues():
if C.PLUG_MODE_CLIENT in p._info[u'modes']:
yield p
def _createSubProtocols(self):
self.messageProt = SatMessageProtocol(self.host_app)
self.messageProt.setHandlerParent(self)
self.roster = SatRosterProtocol(self.host_app)
self.roster.setHandlerParent(self)
self.presence = SatPresenceProtocol(self.host_app)
self.presence.setHandlerParent(self)
def entityConnected(self):
# we want to be sure that we got the roster
return self.roster.got_roster
def addPostXmlCallbacks(self, post_xml_treatments):
post_xml_treatments.addCallback(self.messageAddToHistory)
post_xml_treatments.addCallback(self.messageSendToBridge)
def send(self, obj):
# original send method accept string
# but we restrict to domish.Element to make trigger treatments easier
@ -109,7 +516,7 @@ class SatXMPPClient(wokkel_client.XMPPClient):
#  return
super(SatXMPPClient, self).send(obj)
def sendMessage(self, mess_data):
def sendMessageData(self, mess_data):
"""Convenient method to send message data to stream
This method will send mess_data[u'xml'] to stream, but a trigger is there
@ -121,7 +528,7 @@ class SatXMPPClient(wokkel_client.XMPPClient):
# XXX: This is the last trigger before u"send" (last but one globally) for sending message.
# This is intented for e2e encryption which doesn't do full stanza encryption (e.g. OTR)
# This trigger point can't cancel the method
self.host_app.trigger.point("sendMessageFinish", self, mess_data)
self.host_app.trigger.point("sendMessageData", self, mess_data)
self.send(mess_data[u'xml'])
return mess_data
@ -130,7 +537,7 @@ class SatXMPPClient(wokkel_client.XMPPClient):
This message will be an info message, not recorded in history.
It can be used to give feedback of a command
@param to_jid(jid.Jid): destinee jid
@param to_jid(jid.JID): destinee jid
@param message(unicode): message to send to frontends
"""
self.host_app.bridge.messageNew(uid=unicode(uuid.uuid4()),
@ -143,59 +550,108 @@ class SatXMPPClient(wokkel_client.XMPPClient):
extra={},
profile=self.profile)
def _authd(self, xmlstream):
if not self.host_app.trigger.point("XML Initialized", xmlstream, self.profile):
return
wokkel_client.XMPPClient._authd(self, xmlstream)
self.__connected = True
log.info(_("********** [%s] CONNECTED **********") % self.profile)
self.streamInitialized()
self.host_app.bridge.connected(self.profile, unicode(self.jid)) # we send the signal to the clients
def _finish_connection(self, dummy):
self.roster.requestRoster()
self.presence.available()
super(SatXMPPClient, self)._finish_connection(dummy)
def streamInitialized(self):
"""Called after _authd"""
log.debug(_("XML stream is initialized"))
self.keep_alife = task.LoopingCall(self.xmlstream.send, " ") # Needed to avoid disconnection (specially with openfire)
self.keep_alife.start(C.XMPP_KEEP_ALIFE)
self.disco = SatDiscoProtocol(self)
self.disco.setHandlerParent(self)
self.discoHandler = disco.DiscoHandler()
self.discoHandler.setHandlerParent(self)
disco_d = defer.succeed(None)
class SatXMPPComponent(SatXMPPEntity, component.Component):
"""XMPP component
if not self.host_app.trigger.point("Disco handled", disco_d, self.profile):
return
This component are similar but not identical to clients.
An entry point plugin is launched after component is connected.
Component need to instantiate MessageProtocol itself
"""
implements(iwokkel.IDisco)
trigger_suffix = "Component" # used for to distinguish some trigger points set in SatXMPPEntity
component = True
sendHistory = False # XXX: set to True from entry plugin to keep messages in history for received messages
def finish_connection(dummy):
self.roster.requestRoster()
self.presence.available()
self.conn_deferred.callback(None)
def __init__(self, host_app, profile, component_jid, password, host=None, port=None, max_retries=C.XMPP_MAX_RETRIES):
self.started = time.time()
if port is None:
port = C.XMPP_COMPONENT_PORT
disco_d.addCallback(finish_connection)
def initializationFailed(self, reason):
log.error(_(u"ERROR: XMPP connection failed for profile '%(profile)s': %(reason)s" % {'profile': self.profile, 'reason': reason}))
self.conn_deferred.errback(reason.value)
## entry point ##
entry_point = host_app.memory.getEntryPoint(profile)
try:
wokkel_client.XMPPClient.initializationFailed(self, reason)
except:
# we already chained an errback, no need to raise an exception
pass
self.entry_plugin = host_app.plugins[entry_point]
except KeyError:
raise exceptions.NotFound(_(u"The requested entry point ({entry_point}) is not available").format(
entry_point = entry_point))
def isConnected(self):
return self.__connected
self.identities = [disco.DiscoIdentity(u"component", u"generic", C.APP_NAME)]
# jid is set automatically on bind by Twisted for Client, but not for Component
self.jid = component_jid
if host is None:
try:
host = component_jid.host.split(u'.', 1)[1]
except IndexError:
raise ValueError(u"Can't guess host from jid, please specify a host")
# XXX: component.Component expect unicode jid, while Client expect jid.JID.
# this is not consistent, so we use jid.JID for SatXMPP*
component.Component.__init__(self, host, port, component_jid.full(), password)
SatXMPPEntity.__init__(self, host_app, profile, max_retries)
def connectionLost(self, connector, unused_reason):
try:
self.keep_alife.stop()
except AttributeError:
log.debug(_("No keep_alife"))
if self.__connected:
log.info(_("********** [%s] DISCONNECTED **********") % self.profile)
self.host_app.bridge.disconnected(self.profile) # we send the signal to the clients
self.host_app.purgeClient(self.profile) # and we remove references to this client
self.__connected = False
def _buildDependencies(self, current, plugins, required=True):
"""build recursively dependencies needed for a plugin
this method build list of plugin needed for a component and raises
errors if they are not available or not allowed for components
@param current(object): parent plugin to check
use entry_point for first call
@param plugins(list): list of validated plugins, will be filled by the method
give an empty list for first call
@param required(bool): True if plugin is mandatory
for recursive calls only, should not be modified by inital caller
@raise InternalError: one of the plugin is not handling components
@raise KeyError: one plugin should be present in self.host_app.plugins but it is not
"""
if C.PLUG_MODE_COMPONENT not in current._info[u'modes']:
if not required:
return
else:
log.error(_(u"Plugin {current_name} if needed for {entry_name}, but it doesn't handle component mode").format(
current_name = current._info[u'import_name'],
entry_name = self.entry_plugin._info[u'import_name']
))
raise exceptions.InternalError(_(u"invalid plugin mode"))
for import_name in current._info.get(C.PI_DEPENDENCIES, []):
# plugins are already loaded as dependencies
# so we know they are in self.host_app.plugins
dep = self.host_app.plugins[import_name]
self._checkDependencies(dep, plugins)
for import_name in current._info.get(C.PI_RECOMMENDATIONS, []):
# here plugins are only recommendations,
# so they may not exist in self.host_app.plugins
try:
dep = self.host_app.plugins[import_name]
except KeyError:
continue
self._buildDependencies(dep, plugins, required = False)
if current not in plugins:
# current can be required for several plugins and so
# it can already be present in the list
plugins.append(current)
def _getPluginsList(self):
# XXX: for component we don't launch all plugins triggers
# but only the ones from which there is a dependency
plugins = []
self._buildDependencies(self.entry_plugin, plugins)
return plugins
def entityConnected(self):
# we can now launch entry point
return self.entry_plugin.componentStart(self)
def addPostXmlCallbacks(self, post_xml_treatments):
if self.sendHistory:
post_xml_treatments.addCallback(self.messageAddToHistory)
class SatMessageProtocol(xmppim.MessageProtocol):
@ -432,8 +888,6 @@ class SatRosterProtocol(xmppim.RosterClientProtocol):
def isPresenceAuthorised(self, entity_jid):
"""Return True if entity is authorised to see our presence"""
import pudb
pudb.set_trace()
try:
item = self._jids[entity_jid.userhostJID()]
except KeyError:
@ -687,15 +1141,16 @@ class SatVersionHandler(generic.VersionHandler):
# it reject our features (resulting in e.g. no notification on PEP)
return generic.VersionHandler.getDiscoInfo(self, requestor, target, None)
class SatIdentityHandler(XMPPHandler):
""" Manage disco Identity of SàT. Currently, we use "client/pc/Salut à Toi", but as
SàT is multi-frontends and can be used on mobile devices, as a bot, with a web frontend,
etc, we should implement a way to dynamically update identities through the bridge """
""" Manage disco Identity of SàT.
"""
#TODO: dynamic identity update (see docstring). Note that a XMPP entity can have several identities
implements(iwokkel.IDisco)
def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
return [disco.DiscoIdentity(u"client", u"pc", C.APP_NAME)]
return self.parent.identities
def getDiscoItems(self, requestor, target, nodeIdentifier=''):
return []

View file

@ -453,12 +453,15 @@ class Memory(object):
self.memory_data['Profile_default'] = profile
def asyncCreateProfile(self, name, password):
def createProfile(self, name, password, component=None):
"""Create a new profile
@param name (unicode): profile name
@param password (unicode): profile password
@param name(unicode): profile name
@param password(unicode): profile password
Can be empty to disable password
@param component(None, unicode): set to entry point if this is a component
@return: Deferred
@raise exceptions.NotFound: component is not a known plugin import name
"""
if not name:
raise ValueError(u"Empty profile name")
@ -470,7 +473,16 @@ class Memory(object):
if name in self._entities_cache:
raise exceptions.ConflictError(u"A session for this profile exists")
d = self.params.asyncCreateProfile(name)
if component:
if not component in self.host.plugins:
raise exceptions.NotFound(_(u"Can't find component {component} entry point".format(
component = component)))
# FIXME: PLUGIN_INFO is not currently accessible after import, but type shoul be tested here
# if self.host.plugins[component].PLUGIN_INFO[u"type"] != C.PLUG_TYPE_ENTRY_POINT:
#  raise ValueError(_(u"Plugin {component} is not an entry point !".format(
#  component = component)))
d = self.params.createProfile(name, component)
def initPersonalKey(dummy):
# be sure to call this after checking that the profile doesn't exist yet
@ -495,6 +507,7 @@ class Memory(object):
def asyncDeleteProfile(self, name, force=False):
"""Delete an existing profile
@param name: Name of the profile
@param force: force the deletion even if the profile is connected.
To be used for direct calls only (not through the bridge).
@ -510,6 +523,24 @@ class Memory(object):
d.addCallback(cleanMemory)
return d
def isComponent(self, profile_name):
"""Tell if a profile is a component
@param profile_name(unicode): name of the profile
@return (bool): True if profile is a component
@raise exceptions.NotFound: profile doesn't exist
"""
return self.storage.profileIsComponent(profile_name)
def getEntryPoint(self, profile_name):
"""Get a component entry point
@param profile_name(unicode): name of the profile
@return (bool): True if profile is a component
@raise exceptions.NotFound: profile doesn't exist
"""
return self.storage.getEntryPoint(profile_name)
## History ##
def addToHistory(self, client, data):

View file

@ -166,10 +166,11 @@ class Params(object):
self.params = {}
self.params_gen = {}
def asyncCreateProfile(self, profile):
def createProfile(self, profile, component):
"""Create a new profile
@param profile: name of the profile
@param profile(unicode): name of the profile
@param component(unicode): entry point if profile is a component
@param callback: called when the profile actually exists in database and memory
@return: a Deferred instance
"""
@ -178,7 +179,7 @@ class Params(object):
return defer.fail(Failure(exceptions.ConflictError))
if not self.host.trigger.point("ProfileCreation", profile):
return defer.fail(Failure(exceptions.CancelError))
return self.storage.createProfile(profile)
return self.storage.createProfile(profile, component or None)
def asyncDeleteProfile(self, profile, force=False):
"""Delete an existing profile

View file

@ -207,22 +207,37 @@ class SqliteStorage(object):
except KeyError:
raise exceptions.NotFound(u"the requested profile doesn't exists")
def createProfile(self, name):
def getEntryPoint(self, profile_name):
try:
return self.components[self.profiles[profile_name]]
except KeyError:
raise exceptions.NotFound(u"the requested profile doesn't exists or is not a component")
def createProfile(self, name, component=None):
"""Create a new profile
@param name: name of the profile
@param name(unicode): name of the profile
@param component(None, unicode): if not None, must point to a component entry point
@return: deferred triggered once profile is actually created
"""
def getProfileId(ignore):
return self.dbpool.runQuery("SELECT (id) FROM profiles WHERE name = ?", (name, ))
def setComponent(profile_id):
id_ = profile_id[0][0]
d_comp = self.dbpool.runQuery("INSERT INTO components(profile_id, entry_point) VALUES (?, ?)", (id_, component))
d_comp.addCallback(lambda dummy: profile_id)
return d_comp
def profile_created(profile_id):
_id = profile_id[0][0]
self.profiles[name] = _id # we synchronise the cache
id_= profile_id[0][0]
self.profiles[name] = id_ # we synchronise the cache
d = self.dbpool.runQuery("INSERT INTO profiles(name) VALUES (?)", (name, ))
d.addCallback(getProfileId)
if component is not None:
d.addCallback(setComponent)
d.addCallback(profile_created)
return d

View file

@ -41,11 +41,11 @@ PLUGIN_INFO = {
class ExportCommandProtocol(protocol.ProcessProtocol):
""" Try to register an account with prosody """
def __init__(self, parent, target, options, profile):
def __init__(self, parent, client, target, options):
self.parent = parent
self.target = target
self.options = options
self.profile = profile
self.client = client
def _clean(self, data):
if not data:
@ -58,10 +58,10 @@ class ExportCommandProtocol(protocol.ProcessProtocol):
log.info("connectionMade :)")
def outReceived(self, data):
self.parent.host.messageSend(self.target, {'': self._clean(data)}, no_trigger=True, profile_key=self.profile)
self.client.sendMessage(self.target, {'': self._clean(data)}, no_trigger=True)
def errReceived(self, data):
self.parent.host.messageSend(self.target, {'': self._clean(data)}, no_trigger=True, profile_key=self.profile)
self.client.sendMessage(self.target, {'': self._clean(data)}, no_trigger=True)
def processEnded(self, reason):
log.info (u"process finished: %d" % (reason.value.exitCode,))
@ -81,8 +81,9 @@ class ExportCommandProtocol(protocol.ProcessProtocol):
class CommandExport(object):
"""Command export plugin: export a command to an entity"""
#XXX: This plugin can be potentially dangerous if we don't trust entities linked
# this is specially true if we have other triggers.
# XXX: This plugin can be potentially dangerous if we don't trust entities linked
# this is specially true if we have other triggers.
# FIXME: spawned should be a client attribute, not a class one
def __init__(self, host):
log.info(_("Plugin command export initialization"))
@ -96,10 +97,10 @@ class CommandExport(object):
@param entity: jid.JID attached to the process
@param process: process to remove"""
try:
processes_set = self.spawned[(entity, process.profile)]
processes_set = self.spawned[(entity, process.client.profile)]
processes_set.discard(process)
if not processes_set:
del(self.spawned[(entity, process.profile)])
del(self.spawned[(entity, process.client.profile)])
except ValueError:
pass
@ -140,11 +141,7 @@ class CommandExport(object):
- pty: if set, launch in a pseudo terminal
- continue: continue normal MessageReceived handling
"""
profile = self.host.memory.getProfileName(profile_key)
if not profile:
log.warning(u"Unknown profile [%s]" % (profile,))
return
client = self.host.getClient(profile_key)
for target in targets:
try:
_jid = jid.JID(target)
@ -154,7 +151,7 @@ class CommandExport(object):
except (RuntimeError, jid.InvalidFormat, AttributeError):
log.info(u"invalid target ignored: %s" % (target,))
continue
process_prot = ExportCommandProtocol(self, _jid, options, profile)
self.spawned.setdefault((_jid, profile),set()).add(process_prot)
process_prot = ExportCommandProtocol(self, client, _jid, options)
self.spawned.setdefault((_jid, client.profile),set()).add(process_prot)
reactor.spawnProcess(process_prot, command, args, usePTY = process_prot.boolOption('pty'))

View file

@ -65,7 +65,7 @@ class LangDetect(object):
self.host = host
host.memory.updateParams(PARAMS)
host.trigger.add("MessageReceived", self.MessageReceivedTrigger)
host.trigger.add("messageSend", self.MessageSendTrigger)
host.trigger.add("sendMessage", self.MessageSendTrigger)
def addLanguage(self, mess_data):
message = mess_data['message']

View file

@ -43,7 +43,7 @@ class Exp_Parrot(object):
"""Parrot mode plugin: repeat messages from one entity or MUC room to another one"""
# XXX: This plugin can be potentially dangerous if we don't trust entities linked
# this is specially true if we have other triggers.
# messageSendTrigger avoid other triggers execution, it's deactivated to allow
# sendMessageTrigger avoid other triggers execution, it's deactivated to allow
# /unparrot command in text commands plugin.
# FIXME: potentially unsecure, specially with e2e encryption
@ -51,13 +51,13 @@ class Exp_Parrot(object):
log.info(_("Plugin Parrot initialization"))
self.host = host
host.trigger.add("MessageReceived", self.MessageReceivedTrigger, priority=100)
#host.trigger.add("messageSend", self.messageSendTrigger, priority=100)
#host.trigger.add("sendMessage", self.sendMessageTrigger, priority=100)
try:
self.host.plugins[C.TEXT_CMDS].registerTextCommands(self)
except KeyError:
log.info(_(u"Text commands not available"))
#def messageSendTrigger(self, client, mess_data, treatments):
#def sendMessageTrigger(self, client, mess_data, treatments):
# """ Deactivate other triggers if recipient is in parrot links """
# try:
# _links = client.parrot_links
@ -103,7 +103,7 @@ class Exp_Parrot(object):
linked = _links[from_jid.userhostJID()]
self.host.messageSend(jid.JID(unicode(linked)), message, None, "auto", no_trigger=True, profile_key=profile)
client.sendMessage(jid.JID(unicode(linked)), message, None, "auto", no_trigger=True)
return True

View file

@ -242,7 +242,7 @@ class MiscAccount(object):
if profile.lower() in self.getConfig('reserved_list'):
return defer.fail(Failure(exceptions.ConflictError))
d = self.host.memory.asyncCreateProfile(profile, password)
d = self.host.memory.createProfile(profile, password)
d.addCallback(lambda dummy: self.profileCreated(password, jid_s, profile))
return d

View file

@ -219,7 +219,7 @@ class FilePlugin(object):
has_feature = yield self.host.hasFeature(namespace, peer_jid, profile)
if has_feature:
log.info(u"{name} method will be used to send the file".format(name=method_name))
progress_id = yield defer.maybeDeferred(callback, peer_jid, filepath, filename, file_desc, profile)
progress_id = yield callback(peer_jid, filepath, filename, file_desc, profile)
defer.returnValue({'progress': progress_id})
msg = u"Can't find any method to send file to {jid}".format(jid=peer_jid.full())
log.warning(msg)

View file

@ -65,14 +65,13 @@ class GroupBlog(object):
## plugin management methods ##
def getHandler(self, profile):
def getHandler(self, client):
return GroupBlog_handler()
@defer.inlineCallbacks
def profileConnected(self, profile):
client = self.host.getClient(profile)
def profileConnected(self, client):
try:
yield self.host.checkFeatures((NS_PUBSUB_GROUPBLOG,), profile=profile)
yield self.host.checkFeatures((NS_PUBSUB_GROUPBLOG,), profile=client.profile)
except exceptions.FeatureNotFound:
client.server_groupblog_available = False
log.warning(_(u"Server is not able to manage item-access pubsub, we can't use group blog"))

View file

@ -103,7 +103,7 @@ class IPPlugin(object):
self._external_ip_cache = None
self._local_ip_cache = None
def getHandler(self, profile):
def getHandler(self, client):
return IPPlugin_handler()
def refreshIP(self):

View file

@ -45,7 +45,7 @@ PLUGIN_INFO = {
MAILDIR_PATH = "Maildir"
CATEGORY = D_("Mail Server")
NAME = D_('Block "normal" messages propagation')
# FIXME: (very) old and (very) experimental code, need a big cleaning/review
# FIXME: (very) old and (very) experimental code, need a big cleaning/review or to be deprecated
class MaildirError(Exception):
@ -82,8 +82,9 @@ class MaildirBox(object):
#the triggers
host.trigger.add("MessageReceived", self.messageReceivedTrigger)
def profileConnected(self, profile):
"""Called on profile connection, create profile data"""
def profileConnected(self, client):
"""Called on client connection, create profile data"""
profile = client.profile
self.data[profile] = PersistentBinaryDict("plugin_maildir", profile)
self.__mailboxes[profile] = {}
@ -93,8 +94,9 @@ class MaildirBox(object):
self.data[profile]["INBOX"] = {"cur_idx": 0}
self.data[profile].load().addCallback(dataLoaded)
def profileDisconnected(self, profile):
def profileDisconnected(self, client):
"""Called on profile disconnection, free profile's resources"""
profile = client.profile
del self.__mailboxes[profile]
del self.data[profile]

Some files were not shown because too many files have changed in this diff Show more