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:
parent
9b2923f161
commit
97337202cb
55 changed files with 839 additions and 523 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
607
src/core/xmpp.py
607
src/core/xmpp.py
|
|
@ -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 []
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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'))
|
||||
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue