634 lines
23 KiB
Python
634 lines
23 KiB
Python
"""
|
|
SleekXMPP: The Sleek XMPP Library
|
|
Copyright (C) 2010 Nathanael C. Fritz
|
|
This file is part of SleekXMPP.
|
|
|
|
See the file LICENSE for copying permission.
|
|
"""
|
|
|
|
from __future__ import with_statement, unicode_literals
|
|
|
|
import sys
|
|
import copy
|
|
import logging
|
|
|
|
import sleekxmpp
|
|
from sleekxmpp import plugins
|
|
|
|
import sleekxmpp.roster as roster
|
|
from sleekxmpp.stanza import Message, Presence, Iq, Error
|
|
from sleekxmpp.stanza.roster import Roster
|
|
from sleekxmpp.stanza.nick import Nick
|
|
from sleekxmpp.stanza.htmlim import HTMLIM
|
|
|
|
from sleekxmpp.xmlstream import XMLStream, JID, tostring
|
|
from sleekxmpp.xmlstream import ET, register_stanza_plugin
|
|
from sleekxmpp.xmlstream.matcher import *
|
|
from sleekxmpp.xmlstream.handler import *
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
# In order to make sure that Unicode is handled properly
|
|
# in Python 2.x, reset the default encoding.
|
|
if sys.version_info < (3, 0):
|
|
reload(sys)
|
|
sys.setdefaultencoding('utf8')
|
|
|
|
|
|
class BaseXMPP(XMLStream):
|
|
|
|
"""
|
|
The BaseXMPP class adapts the generic XMLStream class for use
|
|
with XMPP. It also provides a plugin mechanism to easily extend
|
|
and add support for new XMPP features.
|
|
|
|
Attributes:
|
|
auto_authorize -- Manage automatically accepting roster
|
|
subscriptions.
|
|
auto_subscribe -- Manage automatically requesting mutual
|
|
subscriptions.
|
|
is_component -- Indicates if this stream is for an XMPP component.
|
|
jid -- The XMPP JID for this stream.
|
|
plugin -- A dictionary of loaded plugins.
|
|
plugin_config -- A dictionary of plugin configurations.
|
|
plugin_whitelist -- A list of approved plugins.
|
|
sentpresence -- Indicates if an initial presence has been sent.
|
|
roster -- A dictionary containing subscribed JIDs and
|
|
their presence statuses.
|
|
|
|
Methods:
|
|
Iq -- Factory for creating an Iq stanzas.
|
|
Message -- Factory for creating Message stanzas.
|
|
Presence -- Factory for creating Presence stanzas.
|
|
get -- Return a plugin given its name.
|
|
make_iq -- Create and initialize an Iq stanza.
|
|
make_iq_error -- Create an Iq stanza of type 'error'.
|
|
make_iq_get -- Create an Iq stanza of type 'get'.
|
|
make_iq_query -- Create an Iq stanza with a given query.
|
|
make_iq_result -- Create an Iq stanza of type 'result'.
|
|
make_iq_set -- Create an Iq stanza of type 'set'.
|
|
make_message -- Create and initialize a Message stanza.
|
|
make_presence -- Create and initialize a Presence stanza.
|
|
make_query_roster -- Create a roster query.
|
|
process -- Overrides XMLStream.process.
|
|
register_plugin -- Load and configure a plugin.
|
|
register_plugins -- Load and configure multiple plugins.
|
|
send_message -- Create and send a Message stanza.
|
|
send_presence -- Create and send a Presence stanza.
|
|
send_presence_subscribe -- Send a subscription request.
|
|
"""
|
|
|
|
def __init__(self, jid='', default_ns='jabber:client'):
|
|
"""
|
|
Adapt an XML stream for use with XMPP.
|
|
|
|
Arguments:
|
|
default_ns -- Ensure that the correct default XML namespace
|
|
is used during initialization.
|
|
"""
|
|
XMLStream.__init__(self)
|
|
|
|
# To comply with PEP8, method names now use underscores.
|
|
# Deprecated method names are re-mapped for backwards compatibility.
|
|
self.registerPlugin = self.register_plugin
|
|
self.makeIq = self.make_iq
|
|
self.makeIqGet = self.make_iq_get
|
|
self.makeIqResult = self.make_iq_result
|
|
self.makeIqSet = self.make_iq_set
|
|
self.makeIqError = self.make_iq_error
|
|
self.makeIqQuery = self.make_iq_query
|
|
self.makeQueryRoster = self.make_query_roster
|
|
self.makeMessage = self.make_message
|
|
self.makePresence = self.make_presence
|
|
self.sendMessage = self.send_message
|
|
self.sendPresence = self.send_presence
|
|
self.sendPresenceSubscription = self.send_presence_subscription
|
|
|
|
self.default_ns = default_ns
|
|
self.stream_ns = 'http://etherx.jabber.org/streams'
|
|
|
|
self.boundjid = JID(jid)
|
|
|
|
self.plugin = {}
|
|
self.roster = roster.Roster(self)
|
|
self.roster.add(self.boundjid.bare)
|
|
|
|
self.is_component = False
|
|
self.auto_authorize = True
|
|
self.auto_subscribe = True
|
|
|
|
self.sentpresence = False
|
|
|
|
self.register_handler(
|
|
Callback('IM',
|
|
MatchXPath('{%s}message/{%s}body' % (self.default_ns,
|
|
self.default_ns)),
|
|
self._handle_message))
|
|
self.register_handler(
|
|
Callback('Presence',
|
|
MatchXPath("{%s}presence" % self.default_ns),
|
|
self._handle_presence))
|
|
|
|
self.add_event_handler('disconnected',
|
|
self._handle_disconnected)
|
|
self.add_event_handler('presence_available',
|
|
self._handle_available)
|
|
self.add_event_handler('presence_dnd',
|
|
self._handle_available)
|
|
self.add_event_handler('presence_xa',
|
|
self._handle_available)
|
|
self.add_event_handler('presence_chat',
|
|
self._handle_available)
|
|
self.add_event_handler('presence_away',
|
|
self._handle_available)
|
|
self.add_event_handler('presence_unavailable',
|
|
self._handle_unavailable)
|
|
self.add_event_handler('presence_subscribe',
|
|
self._handle_subscribe)
|
|
self.add_event_handler('presence_subscribed',
|
|
self._handle_subscribed)
|
|
self.add_event_handler('presence_unsubscribe',
|
|
self._handle_unsubscribe)
|
|
self.add_event_handler('presence_unsubscribed',
|
|
self._handle_unsubscribed)
|
|
self.add_event_handler('presence_probe',
|
|
self._handle_probe)
|
|
self.add_event_handler('roster_subscription_request',
|
|
self._handle_new_subscription)
|
|
|
|
# Set up the XML stream with XMPP's root stanzas.
|
|
self.register_stanza(Message)
|
|
self.register_stanza(Iq)
|
|
self.register_stanza(Presence)
|
|
|
|
# Initialize a few default stanza plugins.
|
|
register_stanza_plugin(Iq, Roster)
|
|
register_stanza_plugin(Message, Nick)
|
|
register_stanza_plugin(Message, HTMLIM)
|
|
|
|
def process(self, *args, **kwargs):
|
|
"""
|
|
Ensure that plugin inter-dependencies are handled before starting
|
|
event processing.
|
|
|
|
Overrides XMLStream.process.
|
|
"""
|
|
for name in self.plugin:
|
|
if not self.plugin[name].post_inited:
|
|
self.plugin[name].post_init()
|
|
return XMLStream.process(self, *args, **kwargs)
|
|
|
|
def register_plugin(self, plugin, pconfig={}, module=None):
|
|
"""
|
|
Register and configure a plugin for use in this stream.
|
|
|
|
Arguments:
|
|
plugin -- The name of the plugin class. Plugin names must
|
|
be unique.
|
|
pconfig -- A dictionary of configuration data for the plugin.
|
|
Defaults to an empty dictionary.
|
|
module -- Optional refence to the module containing the plugin
|
|
class if using custom plugins.
|
|
"""
|
|
try:
|
|
# Import the given module that contains the plugin.
|
|
if not module:
|
|
module = sleekxmpp.plugins
|
|
module = __import__("%s.%s" % (module.__name__, plugin),
|
|
globals(), locals(), [plugin])
|
|
if isinstance(module, str):
|
|
# We probably want to load a module from outside
|
|
# the sleekxmpp package, so leave out the globals().
|
|
module = __import__(module, fromlist=[plugin])
|
|
|
|
# Load the plugin class from the module.
|
|
self.plugin[plugin] = getattr(module, plugin)(self, pconfig)
|
|
|
|
# Let XEP implementing plugins have some extra logging info.
|
|
xep = ''
|
|
if hasattr(self.plugin[plugin], 'xep'):
|
|
xep = "(XEP-%s) " % self.plugin[plugin].xep
|
|
|
|
desc = (xep, self.plugin[plugin].description)
|
|
log.debug("Loaded Plugin %s%s" % desc)
|
|
except:
|
|
log.exception("Unable to load plugin: %s", plugin)
|
|
|
|
def register_plugins(self):
|
|
"""
|
|
Register and initialize all built-in plugins.
|
|
|
|
Optionally, the list of plugins loaded may be limited to those
|
|
contained in self.plugin_whitelist.
|
|
|
|
Plugin configurations stored in self.plugin_config will be used.
|
|
"""
|
|
if self.plugin_whitelist:
|
|
plugin_list = self.plugin_whitelist
|
|
else:
|
|
plugin_list = plugins.__all__
|
|
|
|
for plugin in plugin_list:
|
|
if plugin in plugins.__all__:
|
|
self.register_plugin(plugin,
|
|
self.plugin_config.get(plugin, {}))
|
|
else:
|
|
raise NameError("Plugin %s not in plugins.__all__." % plugin)
|
|
|
|
# Resolve plugin inter-dependencies.
|
|
for plugin in self.plugin:
|
|
self.plugin[plugin].post_init()
|
|
|
|
def __getitem__(self, key):
|
|
"""
|
|
Return a plugin given its name, if it has been registered.
|
|
"""
|
|
if key in self.plugin:
|
|
return self.plugin[key]
|
|
else:
|
|
log.warning("""Plugin "%s" is not loaded.""" % key)
|
|
return False
|
|
|
|
def get(self, key, default):
|
|
"""
|
|
Return a plugin given its name, if it has been registered.
|
|
"""
|
|
return self.plugin.get(key, default)
|
|
|
|
def Message(self, *args, **kwargs):
|
|
"""Create a Message stanza associated with this stream."""
|
|
return Message(self, *args, **kwargs)
|
|
|
|
def Iq(self, *args, **kwargs):
|
|
"""Create an Iq stanza associated with this stream."""
|
|
return Iq(self, *args, **kwargs)
|
|
|
|
def Presence(self, *args, **kwargs):
|
|
"""Create a Presence stanza associated with this stream."""
|
|
return Presence(self, *args, **kwargs)
|
|
|
|
def make_iq(self, id=0, ifrom=None):
|
|
"""
|
|
Create a new Iq stanza with a given Id and from JID.
|
|
|
|
Arguments:
|
|
id -- An ideally unique ID value for this stanza thread.
|
|
Defaults to 0.
|
|
ifrom -- The from JID to use for this stanza.
|
|
"""
|
|
return self.Iq()._set_stanza_values({'id': str(id),
|
|
'from': ifrom})
|
|
|
|
def make_iq_get(self, queryxmlns=None):
|
|
"""
|
|
Create an Iq stanza of type 'get'.
|
|
|
|
Optionally, a query element may be added.
|
|
|
|
Arguments:
|
|
queryxmlns -- The namespace of the query to use.
|
|
"""
|
|
return self.Iq()._set_stanza_values({'type': 'get',
|
|
'query': queryxmlns})
|
|
|
|
def make_iq_result(self, id):
|
|
"""
|
|
Create an Iq stanza of type 'result' with the given ID value.
|
|
|
|
Arguments:
|
|
id -- An ideally unique ID value. May use self.new_id().
|
|
"""
|
|
return self.Iq()._set_stanza_values({'id': id,
|
|
'type': 'result'})
|
|
|
|
def make_iq_set(self, sub=None):
|
|
"""
|
|
Create an Iq stanza of type 'set'.
|
|
|
|
Optionally, a substanza may be given to use as the
|
|
stanza's payload.
|
|
|
|
Arguments:
|
|
sub -- A stanza or XML object to use as the Iq's payload.
|
|
"""
|
|
iq = self.Iq()._set_stanza_values({'type': 'set'})
|
|
if sub != None:
|
|
iq.append(sub)
|
|
return iq
|
|
|
|
def make_iq_error(self, id, type='cancel',
|
|
condition='feature-not-implemented', text=None):
|
|
"""
|
|
Create an Iq stanza of type 'error'.
|
|
|
|
Arguments:
|
|
id -- An ideally unique ID value. May use self.new_id().
|
|
type -- The type of the error, such as 'cancel' or 'modify'.
|
|
Defaults to 'cancel'.
|
|
condition -- The error condition.
|
|
Defaults to 'feature-not-implemented'.
|
|
text -- A message describing the cause of the error.
|
|
"""
|
|
iq = self.Iq()._set_stanza_values({'id': id})
|
|
iq['error']._set_stanza_values({'type': type,
|
|
'condition': condition,
|
|
'text': text})
|
|
return iq
|
|
|
|
def make_iq_query(self, iq=None, xmlns=''):
|
|
"""
|
|
Create or modify an Iq stanza to use the given
|
|
query namespace.
|
|
|
|
Arguments:
|
|
iq -- Optional Iq stanza to modify. A new
|
|
stanza is created otherwise.
|
|
xmlns -- The query's namespace.
|
|
"""
|
|
if not iq:
|
|
iq = self.Iq()
|
|
iq['query'] = xmlns
|
|
return iq
|
|
|
|
def make_query_roster(self, iq=None):
|
|
"""
|
|
Create a roster query element.
|
|
|
|
Arguments:
|
|
iq -- Optional Iq stanza to modify. A new stanza
|
|
is created otherwise.
|
|
"""
|
|
if iq:
|
|
iq['query'] = 'jabber:iq:roster'
|
|
return ET.Element("{jabber:iq:roster}query")
|
|
|
|
def make_message(self, mto, mbody=None, msubject=None, mtype=None,
|
|
mhtml=None, mfrom=None, mnick=None):
|
|
"""
|
|
Create and initialize a new Message stanza.
|
|
|
|
Arguments:
|
|
mto -- The recipient of the message.
|
|
mbody -- The main contents of the message.
|
|
msubject -- Optional subject for the message.
|
|
mtype -- The message's type, such as 'chat' or 'groupchat'.
|
|
mhtml -- Optional HTML body content.
|
|
mfrom -- The sender of the message. If sending from a client,
|
|
be aware that some servers require that the full JID
|
|
of the sender be used.
|
|
mnick -- Optional nickname of the sender.
|
|
"""
|
|
message = self.Message(sto=mto, stype=mtype, sfrom=mfrom)
|
|
message['body'] = mbody
|
|
message['subject'] = msubject
|
|
if mnick is not None:
|
|
message['nick'] = mnick
|
|
if mhtml is not None:
|
|
message['html']['body'] = mhtml
|
|
return message
|
|
|
|
def make_presence(self, pshow=None, pstatus=None, ppriority=None,
|
|
pto=None, ptype=None, pfrom=None):
|
|
"""
|
|
Create and initialize a new Presence stanza.
|
|
|
|
Arguments:
|
|
pshow -- The presence's show value.
|
|
pstatus -- The presence's status message.
|
|
ppriority -- This connections' priority.
|
|
pto -- The recipient of a directed presence.
|
|
ptype -- The type of presence, such as 'subscribe'.
|
|
pfrom -- The sender of the presence.
|
|
"""
|
|
presence = self.Presence(stype=ptype, sfrom=pfrom, sto=pto)
|
|
if pshow is not None:
|
|
presence['type'] = pshow
|
|
if pfrom is None:
|
|
presence['from'] = self.boundjid.full
|
|
presence['priority'] = ppriority
|
|
presence['status'] = pstatus
|
|
return presence
|
|
|
|
def send_message(self, mto, mbody, msubject=None, mtype=None,
|
|
mhtml=None, mfrom=None, mnick=None):
|
|
"""
|
|
Create, initialize, and send a Message stanza.
|
|
|
|
|
|
"""
|
|
self.makeMessage(mto, mbody, msubject, mtype,
|
|
mhtml, mfrom, mnick).send()
|
|
|
|
def send_presence(self, pshow=None, pstatus=None, ppriority=None,
|
|
pto=None, pfrom=None, ptype=None):
|
|
"""
|
|
Create, initialize, and send a Presence stanza.
|
|
|
|
Arguments:
|
|
pshow -- The presence's show value.
|
|
pstatus -- The presence's status message.
|
|
ppriority -- This connections' priority.
|
|
pto -- The recipient of a directed presence.
|
|
ptype -- The type of presence, such as 'subscribe'.
|
|
pfrom -- The sender of the presence.
|
|
"""
|
|
self.makePresence(pshow, pstatus, ppriority, pto,
|
|
ptype=ptype, pfrom=pfrom).send()
|
|
# Unexpected errors may occur if
|
|
if not self.sentpresence:
|
|
self.event('sent_presence')
|
|
self.sentpresence = True
|
|
|
|
def send_presence_subscription(self, pto, pfrom=None,
|
|
ptype='subscribe', pnick=None):
|
|
"""
|
|
Create, initialize, and send a Presence stanza of type 'subscribe'.
|
|
|
|
Arguments:
|
|
pto -- The recipient of a directed presence.
|
|
pfrom -- The sender of the presence.
|
|
ptype -- The type of presence. Defaults to 'subscribe'.
|
|
pnick -- Nickname of the presence's sender.
|
|
"""
|
|
presence = self.makePresence(ptype=ptype,
|
|
pfrom=pfrom,
|
|
pto=self.getjidbare(pto))
|
|
if pnick:
|
|
nick = ET.Element('{http://jabber.org/protocol/nick}nick')
|
|
nick.text = pnick
|
|
presence.append(nick)
|
|
presence.send()
|
|
|
|
@property
|
|
def jid(self):
|
|
"""
|
|
Attribute accessor for bare jid
|
|
"""
|
|
log.warning("jid property deprecated. Use boundjid.bare")
|
|
return self.boundjid.bare
|
|
|
|
@jid.setter
|
|
def jid(self, value):
|
|
log.warning("jid property deprecated. Use boundjid.bare")
|
|
self.boundjid.bare = value
|
|
|
|
@property
|
|
def fulljid(self):
|
|
"""
|
|
Attribute accessor for full jid
|
|
"""
|
|
log.warning("fulljid property deprecated. Use boundjid.full")
|
|
return self.boundjid.full
|
|
|
|
@fulljid.setter
|
|
def fulljid(self, value):
|
|
log.warning("fulljid property deprecated. Use boundjid.full")
|
|
self.boundjid.full = value
|
|
|
|
@property
|
|
def resource(self):
|
|
"""
|
|
Attribute accessor for jid resource
|
|
"""
|
|
log.warning("resource property deprecated. Use boundjid.resource")
|
|
return self.boundjid.resource
|
|
|
|
@resource.setter
|
|
def resource(self, value):
|
|
log.warning("fulljid property deprecated. Use boundjid.full")
|
|
self.boundjid.resource = value
|
|
|
|
@property
|
|
def username(self):
|
|
"""
|
|
Attribute accessor for jid usernode
|
|
"""
|
|
log.warning("username property deprecated. Use boundjid.user")
|
|
return self.boundjid.user
|
|
|
|
@username.setter
|
|
def username(self, value):
|
|
log.warning("username property deprecated. Use boundjid.user")
|
|
self.boundjid.user = value
|
|
|
|
@property
|
|
def server(self):
|
|
"""
|
|
Attribute accessor for jid host
|
|
"""
|
|
log.warning("server property deprecated. Use boundjid.host")
|
|
return self.boundjid.server
|
|
|
|
@server.setter
|
|
def server(self, value):
|
|
log.warning("server property deprecated. Use boundjid.host")
|
|
self.boundjid.server = value
|
|
|
|
def set_jid(self, jid):
|
|
"""Rip a JID apart and claim it as our own."""
|
|
log.debug("setting jid to %s" % jid)
|
|
self.boundjid.full = jid
|
|
|
|
def getjidresource(self, fulljid):
|
|
if '/' in fulljid:
|
|
return fulljid.split('/', 1)[-1]
|
|
else:
|
|
return ''
|
|
|
|
def getjidbare(self, fulljid):
|
|
return fulljid.split('/', 1)[0]
|
|
|
|
def _handle_disconnected(self, event):
|
|
"""When disconnected, reset the roster"""
|
|
self.roster = {}
|
|
|
|
def _handle_message(self, msg):
|
|
"""Process incoming message stanzas."""
|
|
self.event('message', msg)
|
|
|
|
def _handle_available(self, presence):
|
|
pto = presence['to'].bare
|
|
pfrom = presence['from'].bare
|
|
self.roster[pto][pfrom].handle_available(presence)
|
|
|
|
def _handle_unavailable(self, presence):
|
|
pto = presence['to'].bare
|
|
pfrom = presence['from'].bare
|
|
self.roster[pto][pfrom].handle_unavailable(presence)
|
|
|
|
def _handle_new_subscription(self, stanza):
|
|
"""
|
|
Attempt to automatically handle subscription requests.
|
|
|
|
Subscriptions will be approved if the request is from
|
|
a whitelisted JID, of self.auto_authorize is True. They
|
|
will be rejected if self.auto_authorize is False. Setting
|
|
self.auto_authorize to None will disable automatic
|
|
subscription handling (except for whitelisted JIDs).
|
|
|
|
If a subscription is accepted, a request for a mutual
|
|
subscription will be sent if self.auto_subscribe is True.
|
|
"""
|
|
roster = self.roster[stanza['to'].bare]
|
|
item = self.roster[stanza['to'].bare][stanza['from'].bare]
|
|
if item['whitelisted']:
|
|
item.authorize()
|
|
elif roster.auto_authorize:
|
|
item.authorize()
|
|
if roster.auto_subscribe:
|
|
item.subscribe()
|
|
elif roster.auto_authorize == False:
|
|
item.unauthorize()
|
|
|
|
def _handle_removed_subscription(self, presence):
|
|
pto = presence['to'].bare
|
|
pfrom = presence['from'].bare
|
|
self.roster[pto][pfrom].unauthorize()
|
|
|
|
def _handle_subscribe(self, presence):
|
|
pto = presence['to'].bare
|
|
pfrom = presence['from'].bare
|
|
self.roster[pto][pfrom].handle_subscribe(presence)
|
|
|
|
def _handle_subscribed(self, presence):
|
|
pto = presence['to'].bare
|
|
pfrom = presence['from'].bare
|
|
self.roster[pto][pfrom].handle_subscribed(presence)
|
|
|
|
def _handle_unsubscribe(self, presence):
|
|
pto = presence['to'].bare
|
|
pfrom = presence['from'].bare
|
|
self.roster[pto][pfrom].handle_unsubscribe(presence)
|
|
|
|
def _handle_unsubscribed(self, presence):
|
|
pto = presence['to'].bare
|
|
pfrom = presence['from'].bare
|
|
self.roster[pto][pfrom].handle_unsubscribed(presence)
|
|
|
|
def _handle_probe(self, presence):
|
|
pto = presence['to'].bare
|
|
pfrom = presence['from'].bare
|
|
self.roster[pto][pfrom].handle_probe(presence)
|
|
|
|
def _handle_presence(self, presence):
|
|
"""
|
|
Process incoming presence stanzas.
|
|
|
|
Update the roster with presence information.
|
|
"""
|
|
logging.debug(presence['type'])
|
|
self.event("presence_%s" % presence['type'], presence)
|
|
|
|
# Check for changes in subscription state.
|
|
if presence['type'] in ('subscribe', 'subscribed',
|
|
'unsubscribe', 'unsubscribed'):
|
|
self.event('changed_subscription', presence)
|
|
return
|
|
elif not presence['type'] in ('available', 'unavailable') and \
|
|
not presence['type'] in presence.showtypes:
|
|
return
|
|
self.event("changed_status", presence)
|
|
|
|
# Restore the old, lowercased name for backwards compatibility.
|
|
basexmpp = BaseXMPP
|